Removing django code, not needed
This commit is contained in:
parent
e919380739
commit
c458d84097
@ -1,19 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import pbr.version
|
|
||||||
|
|
||||||
|
|
||||||
__version__ = pbr.version.VersionInfo(
|
|
||||||
'stackviz').version_string()
|
|
@ -1,177 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
import django
|
|
||||||
import gzip
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
from argparse import ArgumentParser
|
|
||||||
from django.http import Http404
|
|
||||||
|
|
||||||
from django.core.urlresolvers import resolve
|
|
||||||
from django.test import RequestFactory
|
|
||||||
|
|
||||||
from stackviz.parser import tempest_subunit
|
|
||||||
from stackviz import settings
|
|
||||||
|
|
||||||
|
|
||||||
EXPORT_PATHS = [
|
|
||||||
'/index.html',
|
|
||||||
'/tempest_aggregate.html'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
_base = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
|
|
||||||
|
|
||||||
def fake_render_view(path):
|
|
||||||
factory = RequestFactory()
|
|
||||||
request = factory.get(path)
|
|
||||||
|
|
||||||
match = resolve(path)
|
|
||||||
response = match.func(request, *match.args, **match.kwargs)
|
|
||||||
|
|
||||||
if hasattr(response, "render"):
|
|
||||||
response.render()
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
def export_single_page(path, dest_dir, use_gzip=False):
|
|
||||||
dest_file = path
|
|
||||||
if dest_file.startswith('/'):
|
|
||||||
dest_file = dest_file[1:]
|
|
||||||
|
|
||||||
open_func = open
|
|
||||||
if use_gzip:
|
|
||||||
open_func = gzip.open
|
|
||||||
dest_file += ".gz"
|
|
||||||
|
|
||||||
try:
|
|
||||||
content = fake_render_view(path).content
|
|
||||||
|
|
||||||
with open_func(os.path.join(dest_dir, dest_file), 'wb') as f:
|
|
||||||
f.write(content)
|
|
||||||
except Http404 as ex:
|
|
||||||
print("Warning: skipping %s due to error: %s" % (path, ex.message))
|
|
||||||
|
|
||||||
|
|
||||||
def init_django(args):
|
|
||||||
# remove leading / from static URL to give them correct filesystem paths
|
|
||||||
settings.STATIC_URL = settings.STATIC_URL[1:]
|
|
||||||
settings.USE_GZIP = args.gzip
|
|
||||||
settings.OFFLINE = True
|
|
||||||
|
|
||||||
if args.repository or args.stream_file or args.stdin:
|
|
||||||
settings.TEST_REPOSITORIES = []
|
|
||||||
settings.TEST_STREAMS = []
|
|
||||||
settings.TEST_STREAM_STDIN = False
|
|
||||||
|
|
||||||
if args.repository:
|
|
||||||
settings.TEST_REPOSITORIES = args.repository
|
|
||||||
|
|
||||||
if args.stream_file:
|
|
||||||
settings.TEST_STREAMS = args.stream_file
|
|
||||||
|
|
||||||
if args.stdin:
|
|
||||||
settings.TEST_STREAM_STDIN = True
|
|
||||||
|
|
||||||
if args.dstat:
|
|
||||||
settings.DSTAT_CSV = args.dstat
|
|
||||||
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "stackviz.settings")
|
|
||||||
django.setup()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = ArgumentParser(description="Generates a self-contained, static "
|
|
||||||
"StackViz site at the given path.")
|
|
||||||
parser.add_argument("path",
|
|
||||||
help="The output directory. Will be created if it "
|
|
||||||
"doesn't already exist.")
|
|
||||||
parser.add_argument("--ignore-bower",
|
|
||||||
help="Ignore missing Bower components.",
|
|
||||||
action="store_true")
|
|
||||||
parser.add_argument("-z", "--gzip",
|
|
||||||
help="Enable gzip compression for data files.",
|
|
||||||
action="store_true")
|
|
||||||
parser.add_argument("-f", "--stream-file",
|
|
||||||
action="append",
|
|
||||||
help="Include the given direct subunit stream.")
|
|
||||||
parser.add_argument("-r", "--repository",
|
|
||||||
action="append",
|
|
||||||
help="A directory containing a `.testrepository` to "
|
|
||||||
"include. If not provided, the `settings.py` "
|
|
||||||
"configured values will be used.")
|
|
||||||
parser.add_argument("-i", "--stdin",
|
|
||||||
help="Read a direct subunit stream from standard "
|
|
||||||
"input.",
|
|
||||||
action="store_true")
|
|
||||||
parser.add_argument("--dstat",
|
|
||||||
help="The path to the DStat log file (CSV-formatted) "
|
|
||||||
"to include. If not provided, the `settings.py` "
|
|
||||||
"configured value will be used.")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if not args.ignore_bower:
|
|
||||||
if not os.listdir(os.path.join(_base, 'static', 'components')):
|
|
||||||
print("Bower components have not been installed, please run "
|
|
||||||
"`bower install`")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
if os.path.exists(args.path):
|
|
||||||
if os.listdir(args.path):
|
|
||||||
print("Destination exists and is not empty, cannot continue")
|
|
||||||
return 1
|
|
||||||
else:
|
|
||||||
os.mkdir(args.path)
|
|
||||||
|
|
||||||
init_django(args)
|
|
||||||
|
|
||||||
print("Copying static files ...")
|
|
||||||
shutil.copytree(os.path.join(_base, 'static'),
|
|
||||||
os.path.join(args.path, 'static'))
|
|
||||||
|
|
||||||
for path in EXPORT_PATHS:
|
|
||||||
print("Rendering:", path)
|
|
||||||
export_single_page(path, args.path)
|
|
||||||
|
|
||||||
for provider in tempest_subunit.get_providers().values():
|
|
||||||
for i in range(provider.count):
|
|
||||||
param = (provider.name, i)
|
|
||||||
|
|
||||||
print("Rendering views for tempest run %s #%d" % param)
|
|
||||||
export_single_page('/tempest_timeline_%s_%d.html' % param,
|
|
||||||
args.path)
|
|
||||||
export_single_page('/tempest_results_%s_%d.html' % param,
|
|
||||||
args.path)
|
|
||||||
|
|
||||||
print("Exporting data for tempest run %s #%d" % param)
|
|
||||||
export_single_page('/tempest_api_tree_%s_%d.json' % param,
|
|
||||||
args.path, args.gzip)
|
|
||||||
export_single_page('/tempest_api_raw_%s_%d.json' % param,
|
|
||||||
args.path, args.gzip)
|
|
||||||
export_single_page('/tempest_api_details_%s_%d.json' % param,
|
|
||||||
args.path, args.gzip)
|
|
||||||
|
|
||||||
print("Exporting DStat log: dstat_log.csv")
|
|
||||||
export_single_page('/dstat_log.csv', args.path, args.gzip)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,35 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from stackviz.parser.tempest_subunit import get_providers
|
|
||||||
from stackviz.settings import OFFLINE
|
|
||||||
from stackviz.settings import USE_GZIP
|
|
||||||
|
|
||||||
|
|
||||||
def inject_extra_context(request):
|
|
||||||
ret = {
|
|
||||||
'use_gzip': USE_GZIP,
|
|
||||||
'offline': OFFLINE
|
|
||||||
}
|
|
||||||
|
|
||||||
providers = get_providers()
|
|
||||||
if providers:
|
|
||||||
default = providers.values()[0]
|
|
||||||
|
|
||||||
ret.update({
|
|
||||||
'tempest_providers': providers.values(),
|
|
||||||
'tempest_default_provider': default,
|
|
||||||
})
|
|
||||||
|
|
||||||
return ret
|
|
@ -1,255 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
#
|
|
||||||
# A parser for logs as generated by DevStack's `stack.sh` script.
|
|
||||||
# Logs can be generated by running:
|
|
||||||
# LOGFILE="output.log" ./stack.sh
|
|
||||||
#
|
|
||||||
# Two files will be generated:
|
|
||||||
# - test.log.(datestamp): referred to as "log"
|
|
||||||
# - test.log.(datestamp).summary.(datestamp): referred to as "summary"
|
|
||||||
#
|
|
||||||
# The full log can be parsed using `parse_log(path)` and the summary can be
|
|
||||||
# parsed using `parse_summary(path)`. Both functions will return a list of
|
|
||||||
# `LogNode` instances which can be combined and categorized using `merge()`.
|
|
||||||
#
|
|
||||||
# For testing purposes, it's recommended to use an IPython shell. Import this
|
|
||||||
# module, and call `bootstrap()` against the path to the primary logfile to get
|
|
||||||
# the fully merged list of `LogNode` instances. Nodes can be analyzed directly
|
|
||||||
# using the IPython `_repr_pretty_` functionality.
|
|
||||||
#
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from datetime import timedelta
|
|
||||||
from log_node import LogNode
|
|
||||||
|
|
||||||
#: The format of the timestamp prefixing each log entry
|
|
||||||
TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S.%f'
|
|
||||||
|
|
||||||
|
|
||||||
def extract_date(line):
|
|
||||||
"""Extracts a date from the given line
|
|
||||||
|
|
||||||
Returns the parsed date and remaining contents of the line.
|
|
||||||
|
|
||||||
:param line: the line to extract a date from
|
|
||||||
:return: a tuple of the parsed date and remaining line contents
|
|
||||||
"""
|
|
||||||
|
|
||||||
date_str, message = line.split(' | ', 1)
|
|
||||||
date = datetime.strptime(date_str, TIMESTAMP_FORMAT)
|
|
||||||
|
|
||||||
return date, message.strip()
|
|
||||||
|
|
||||||
|
|
||||||
def parse_summary(summary_path):
|
|
||||||
"""Parses a summary logfile
|
|
||||||
|
|
||||||
Summary entries are prefixed with identical datestamps to those in the
|
|
||||||
main log, but have only explicit log messages denoting the overall
|
|
||||||
execution progress.
|
|
||||||
|
|
||||||
While summary entries are also printed into the main log, the explicit
|
|
||||||
summary file is used to simplify parsing.
|
|
||||||
|
|
||||||
:param summary_path: the path to the summary file to parse
|
|
||||||
:return: a list of ordered `LogNode` instances
|
|
||||||
"""
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
|
|
||||||
last_node = None
|
|
||||||
|
|
||||||
with open(summary_path, 'r') as f:
|
|
||||||
for line in f:
|
|
||||||
date, message = extract_date(line)
|
|
||||||
|
|
||||||
node = LogNode(date, message)
|
|
||||||
if last_node:
|
|
||||||
last_node.next_sibling = node
|
|
||||||
|
|
||||||
ret.append(node)
|
|
||||||
last_node = node
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def parse_log(log_path):
|
|
||||||
"""Parses a general `stack.sh` logfile, forming a full log tree
|
|
||||||
|
|
||||||
The log tree is based on the hierarchy of nested commands as presented
|
|
||||||
in the log.
|
|
||||||
|
|
||||||
Note that command output (that is, lines not prefixed with one or more '+'
|
|
||||||
symbols) is ignored and will not be included it the returned list of log
|
|
||||||
nodes.
|
|
||||||
|
|
||||||
:param log_path: the path to the logfile to parse
|
|
||||||
:return: a list of parsed `LogNode` instances
|
|
||||||
"""
|
|
||||||
|
|
||||||
last_depth = 1
|
|
||||||
last_node = None
|
|
||||||
|
|
||||||
# fake node to act as root stack entry
|
|
||||||
ret = LogNode(None, None)
|
|
||||||
|
|
||||||
node_stack = [ret]
|
|
||||||
|
|
||||||
with open(log_path, 'r') as f:
|
|
||||||
for line in f:
|
|
||||||
date, message = extract_date(line)
|
|
||||||
|
|
||||||
if not message.startswith('+'):
|
|
||||||
# ignore command output - we only want actual commands
|
|
||||||
continue
|
|
||||||
|
|
||||||
depth = 0
|
|
||||||
for char in message:
|
|
||||||
if char == '+':
|
|
||||||
depth += 1
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
if depth - last_depth > 1:
|
|
||||||
# skip discontinuous lines (???)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# adjust the stack if the depth has changed since the last node
|
|
||||||
if depth < last_depth:
|
|
||||||
node_stack.pop()
|
|
||||||
elif depth > last_depth:
|
|
||||||
node_stack.append(last_node)
|
|
||||||
|
|
||||||
node = LogNode(date, message.lstrip('+ '))
|
|
||||||
|
|
||||||
parent = node_stack[-1]
|
|
||||||
parent.children.append(node)
|
|
||||||
|
|
||||||
if last_node:
|
|
||||||
last_node.next_sibling = node
|
|
||||||
|
|
||||||
last_depth = depth
|
|
||||||
last_node = node
|
|
||||||
|
|
||||||
return ret.children
|
|
||||||
|
|
||||||
|
|
||||||
def merge(summary, log):
|
|
||||||
"""Merges log entries into parent categories based on timestamp.
|
|
||||||
|
|
||||||
Merges general log entries into parent categories based on their timestamp
|
|
||||||
relative to the summary output timestamp.
|
|
||||||
|
|
||||||
Note that this function is destructive and will directly modify the nodes
|
|
||||||
within the `summary` list.
|
|
||||||
|
|
||||||
:type summary: list[LogNode]
|
|
||||||
:param summary: the list of summary nodes
|
|
||||||
:type log: list[LogNode]
|
|
||||||
:param log: the list of general log nodes
|
|
||||||
:return: the original summary nodes with children set to the log nodes
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not summary:
|
|
||||||
return []
|
|
||||||
|
|
||||||
current_node = summary[0]
|
|
||||||
next_node = current_node.next_sibling
|
|
||||||
|
|
||||||
for i, entry in enumerate(log):
|
|
||||||
if entry.date < current_node.date:
|
|
||||||
# skip forward until a possible summary node is reached
|
|
||||||
continue
|
|
||||||
|
|
||||||
if entry.date < next_node.date:
|
|
||||||
current_node.children.append(entry)
|
|
||||||
else:
|
|
||||||
next_node.children.append(entry)
|
|
||||||
|
|
||||||
current_node = next_node
|
|
||||||
next_node = current_node.next_sibling
|
|
||||||
|
|
||||||
return summary
|
|
||||||
|
|
||||||
|
|
||||||
def bootstrap(log_path, summary_path=None):
|
|
||||||
"""Loads, parses, and merges the given log and summary files.
|
|
||||||
|
|
||||||
The path to the summary file will be determined automatically based on the
|
|
||||||
path to the general logfile, but it must exist within the same directory.
|
|
||||||
|
|
||||||
If the log file names are changed from their default values, a summary path
|
|
||||||
can be explicitly provided using the optional `summary_path` parameter.
|
|
||||||
|
|
||||||
:param log_path: the path to the logfile to parse
|
|
||||||
:param summary_path: optional; bypasses autodetection of path names
|
|
||||||
:return: a list of merged `LogNode` instances, or `None` if no matching
|
|
||||||
summary file can be located automatically
|
|
||||||
"""
|
|
||||||
|
|
||||||
if summary_path:
|
|
||||||
return merge(parse_summary(summary_path), parse_log(log_path))
|
|
||||||
|
|
||||||
name = os.path.basename(log_path)
|
|
||||||
directory = os.path.dirname(os.path.abspath(log_path))
|
|
||||||
|
|
||||||
for candidate in os.listdir(directory):
|
|
||||||
if candidate.startswith('{0}.summary.'.format(name)):
|
|
||||||
return merge(parse_summary(os.path.join(directory, candidate)),
|
|
||||||
parse_log(os.path.join(directory, name)))
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_command_totals(node, totals=None):
|
|
||||||
if totals is None:
|
|
||||||
totals = {}
|
|
||||||
|
|
||||||
for entry in node.traverse():
|
|
||||||
# attempt to resolve a nice looking command, ignoring any obvious
|
|
||||||
# arguments to make subcommands float to left of array
|
|
||||||
tokens = filter(lambda tok: not tok.startswith('-'),
|
|
||||||
entry.message.split())
|
|
||||||
if not tokens:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# strip sudo and any variable declarations
|
|
||||||
if tokens[0] == 'sudo':
|
|
||||||
tokens.pop(0)
|
|
||||||
|
|
||||||
while '=' in tokens[0]:
|
|
||||||
tokens.pop(0)
|
|
||||||
|
|
||||||
if '/' in tokens[0]:
|
|
||||||
tokens[0] = tokens[0].split('/')[-1]
|
|
||||||
|
|
||||||
# select # of tokens to include based on the first token
|
|
||||||
token_count = {
|
|
||||||
'openstack': 2,
|
|
||||||
'read': 2,
|
|
||||||
'python': 2
|
|
||||||
}.get(tokens[0], 1)
|
|
||||||
|
|
||||||
# combine token_count tokens to generate the command to group by
|
|
||||||
combined = ' '.join(tokens[0:token_count])
|
|
||||||
if combined not in totals:
|
|
||||||
totals[combined] = timedelta()
|
|
||||||
|
|
||||||
totals[combined] += entry.duration_self
|
|
||||||
|
|
||||||
return totals
|
|
@ -1,205 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import six
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from inspect import getmembers
|
|
||||||
from numbers import Number
|
|
||||||
|
|
||||||
#: The default cutoff for log entries when pruning takes place, in seconds
|
|
||||||
DEFAULT_PRUNE_CUTOFF = 0.05
|
|
||||||
|
|
||||||
|
|
||||||
class LogNode(object):
|
|
||||||
"""Represents an entry in an ordered event log.
|
|
||||||
|
|
||||||
Represents an entry in an ordered event log. consisting of a date, message,
|
|
||||||
and an arbitrary set of child nodes.
|
|
||||||
|
|
||||||
Note that entries are assumed to be strictly sequential and linear, and all
|
|
||||||
nodes must have a correctly-set `next_sibling` pointing to the next
|
|
||||||
chronological log entry, regardless of child depth.
|
|
||||||
|
|
||||||
This class implements a custom IPython repr useful for interactive use.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, date, message):
|
|
||||||
self.date = date
|
|
||||||
self.message = message
|
|
||||||
self.next_sibling = None
|
|
||||||
|
|
||||||
self.children = []
|
|
||||||
|
|
||||||
@property
|
|
||||||
def duration(self):
|
|
||||||
"""Determines aggregate duration for this node
|
|
||||||
|
|
||||||
Determines the overall duration for this node, beginning at this parent
|
|
||||||
node's start time through the final child's ending time.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.children:
|
|
||||||
last_sibling = self.children[-1].next_sibling
|
|
||||||
if not last_sibling:
|
|
||||||
# if no last sibling exists, use the last child itself to
|
|
||||||
# keep as close as possible to the true value
|
|
||||||
last_sibling = self.children[-1]
|
|
||||||
|
|
||||||
diff = last_sibling.date - self.date
|
|
||||||
return diff + last_sibling.duration
|
|
||||||
else:
|
|
||||||
# attempt get self execution time
|
|
||||||
if self.next_sibling:
|
|
||||||
return self.next_sibling.date - self.date
|
|
||||||
else:
|
|
||||||
return timedelta()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def duration_self(self):
|
|
||||||
if not self.next_sibling:
|
|
||||||
return timedelta()
|
|
||||||
|
|
||||||
return self.next_sibling.date - self.date
|
|
||||||
|
|
||||||
def traverse(self):
|
|
||||||
"""A generator that traverses all nodes of this tree sequentially"""
|
|
||||||
|
|
||||||
for child in self.children:
|
|
||||||
yield child
|
|
||||||
|
|
||||||
for subchild in child.traverse():
|
|
||||||
yield subchild
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "%s(%s, '%s', duration=%d)" % (
|
|
||||||
self.__class__.__name__,
|
|
||||||
self.date.strftime('%H:%M:%S.%f'),
|
|
||||||
self.message,
|
|
||||||
self.duration.total_seconds()
|
|
||||||
)
|
|
||||||
|
|
||||||
def _repr_pretty_(self, p, cycle):
|
|
||||||
tc = __import__('IPython').utils.coloransi.TermColors()
|
|
||||||
|
|
||||||
c = self.__class__.__name__
|
|
||||||
if cycle:
|
|
||||||
p.text('%s(...)' % c)
|
|
||||||
return
|
|
||||||
|
|
||||||
with p.group(4, '%s%s%s(' % (tc.Green, c, tc.Normal), ')'):
|
|
||||||
i = 0
|
|
||||||
p.breakable()
|
|
||||||
for field_name, value in reversed(getmembers(self)):
|
|
||||||
if field_name.startswith('_'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if field_name in ('next_sibling', 'traverse'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if i:
|
|
||||||
p.text(',')
|
|
||||||
p.breakable()
|
|
||||||
|
|
||||||
p.text('%s%s %s=%s ' % (
|
|
||||||
tc.Brown, field_name,
|
|
||||||
tc.DarkGray, tc.Normal))
|
|
||||||
|
|
||||||
if isinstance(value, list):
|
|
||||||
lp = (tc.Cyan, tc.Normal)
|
|
||||||
with p.group(4, '%s[%s' % lp, '%s]%s' % lp):
|
|
||||||
l_first = True
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
for x in value:
|
|
||||||
if not l_first:
|
|
||||||
p.text(', ')
|
|
||||||
|
|
||||||
p.breakable()
|
|
||||||
p.pretty(x)
|
|
||||||
|
|
||||||
l_first = False
|
|
||||||
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
if count > 10:
|
|
||||||
p.breakable()
|
|
||||||
p.text('%s... %d more ...%s' % (
|
|
||||||
tc.LightGreen,
|
|
||||||
len(value) - count,
|
|
||||||
tc.Normal
|
|
||||||
))
|
|
||||||
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
if value is None:
|
|
||||||
p.text(tc.Blue)
|
|
||||||
elif isinstance(value, six.string_types):
|
|
||||||
p.text(tc.Red)
|
|
||||||
elif isinstance(value, Number):
|
|
||||||
p.text(tc.DarkGray)
|
|
||||||
|
|
||||||
p.pretty(value)
|
|
||||||
p.text(tc.Normal)
|
|
||||||
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
|
|
||||||
def prune(nodes, cutoff=DEFAULT_PRUNE_CUTOFF, fill=None):
|
|
||||||
"""Prunes given list of `LogNode` instances.
|
|
||||||
|
|
||||||
Prunes the given list of `LogNode` instances, removing nodes whose duration
|
|
||||||
is less than the given cutoff value. If a `fill` value is provided, removed
|
|
||||||
nodes will be replaced with a single filler value accounting for the lost
|
|
||||||
duration. This filler value will be inserted at the end of the list and
|
|
||||||
will not be properly linked to other values.
|
|
||||||
|
|
||||||
Note that returned values will not necessarily be a continuous list of
|
|
||||||
nodes. The original list will remain unchanged; sibling and child
|
|
||||||
references will not be modified to point to account any modified, removed,
|
|
||||||
or added nodes.
|
|
||||||
|
|
||||||
:param nodes: the list of log nodes to prune
|
|
||||||
:type nodes: list[LogNode]
|
|
||||||
:param fill: if set, replace removed nodes with a single larger node
|
|
||||||
:type fill: str | None
|
|
||||||
:param cutoff: the minimum duration for nodes that will be retained in the
|
|
||||||
tree
|
|
||||||
:type cutoff: float
|
|
||||||
:return: a (potentially) reduced list of nodes
|
|
||||||
"""
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
|
|
||||||
fill_amount = 0.0
|
|
||||||
|
|
||||||
for entry in nodes:
|
|
||||||
d = entry.duration.total_seconds()
|
|
||||||
if d >= cutoff:
|
|
||||||
ret.append(entry)
|
|
||||||
else:
|
|
||||||
fill_amount += d
|
|
||||||
|
|
||||||
if fill:
|
|
||||||
# create a dummy filler node with an arbitrary time to account for the
|
|
||||||
# change in total duration
|
|
||||||
time = datetime.now()
|
|
||||||
|
|
||||||
node = LogNode(time, fill)
|
|
||||||
node.next_sibling = LogNode(time + timedelta(seconds=fill_amount), '')
|
|
||||||
ret.append(node)
|
|
||||||
|
|
||||||
return ret
|
|
@ -1,359 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subunit
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from functools import partial
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
from testtools import CopyStreamResult
|
|
||||||
from testtools import StreamResult
|
|
||||||
from testtools import StreamSummary
|
|
||||||
from testtools import StreamToDict
|
|
||||||
|
|
||||||
from testrepository.repository.file import RepositoryFactory
|
|
||||||
from testrepository.repository.file import RepositoryNotFound
|
|
||||||
|
|
||||||
from stackviz import settings
|
|
||||||
|
|
||||||
|
|
||||||
NAME_SCENARIO_PATTERN = re.compile(r'^(.+) \((.+)\)$')
|
|
||||||
NAME_TAGS_PATTERN = re.compile(r'^(.+)\[(.+)\]$')
|
|
||||||
|
|
||||||
|
|
||||||
_provider_cache = None
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidSubunitProvider(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SubunitProvider(object):
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
"""Returns a unique name for this provider, such that a valid URL
|
|
||||||
fragment pointing to a particular stream from this provider is
|
|
||||||
`name_index`, applicable for paths to pages and data files making use
|
|
||||||
of the stream.
|
|
||||||
|
|
||||||
:return: a path fragment referring to the stream at `index` from this
|
|
||||||
provider
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def description(self):
|
|
||||||
"""Returns a user-facing description for this provider.
|
|
||||||
|
|
||||||
This description may be used in UI contexts, but will not be used
|
|
||||||
within paths or other content-sensitive contexts.
|
|
||||||
|
|
||||||
:return: a description for this provider
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def count(self):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def get_stream(self, index):
|
|
||||||
"""Returns a file-like object representing the subunit stream at the
|
|
||||||
given index.
|
|
||||||
|
|
||||||
:param index: the index of the stream; must be between `0` and
|
|
||||||
`count - 1` (inclusive)
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def indexes(self):
|
|
||||||
# for the benefit of django templates
|
|
||||||
return range(self.count)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def streams(self):
|
|
||||||
"""Creates a generator that iterates over each stream available in
|
|
||||||
this provider.
|
|
||||||
|
|
||||||
:return: each stream available from this generator
|
|
||||||
"""
|
|
||||||
for i in range(self.count):
|
|
||||||
yield self.get_stream(i)
|
|
||||||
|
|
||||||
|
|
||||||
class RepositoryProvider(SubunitProvider):
|
|
||||||
def __init__(self, repository_path):
|
|
||||||
self.repository_path = repository_path
|
|
||||||
self.repository = RepositoryFactory().open(repository_path)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return "repo_%s" % os.path.basename(self.repository_path)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def description(self):
|
|
||||||
return "Repository: %s" % os.path.basename(self.repository_path)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def count(self):
|
|
||||||
return self.repository.count()
|
|
||||||
|
|
||||||
def get_stream(self, index):
|
|
||||||
return self.repository.get_latest_run().get_subunit_stream()
|
|
||||||
|
|
||||||
|
|
||||||
class FileProvider(SubunitProvider):
|
|
||||||
def __init__(self, path):
|
|
||||||
if not os.path.exists(path):
|
|
||||||
raise InvalidSubunitProvider("Stream doesn't exist: %s" % path)
|
|
||||||
|
|
||||||
self.path = path
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return "file_%s" % os.path.basename(self.path)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def description(self):
|
|
||||||
return "Subunit File: %s" % os.path.basename(self.path)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def count(self):
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def get_stream(self, index):
|
|
||||||
if index != 0:
|
|
||||||
raise IndexError("Index out of bounds: %d" % index)
|
|
||||||
|
|
||||||
return open(self.path, "r")
|
|
||||||
|
|
||||||
|
|
||||||
class StandardInputProvider(SubunitProvider):
|
|
||||||
def __init__(self):
|
|
||||||
self.buffer = BytesIO()
|
|
||||||
shutil.copyfileobj(sys.stdin, self.buffer)
|
|
||||||
self.buffer.seek(0)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return "stdin"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def description(self):
|
|
||||||
return "Subunit Stream (stdin)"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def count(self):
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def get_stream(self, index):
|
|
||||||
if index != 0:
|
|
||||||
raise IndexError()
|
|
||||||
|
|
||||||
self.buffer.seek(0)
|
|
||||||
return self.buffer
|
|
||||||
|
|
||||||
|
|
||||||
def get_providers():
|
|
||||||
"""Loads all test providers from locations configured in settings.
|
|
||||||
|
|
||||||
:return: a dict of loaded provider names and their associated
|
|
||||||
:class:`SubunitProvider` instances
|
|
||||||
:rtype: dict[str, SubunitProvider]
|
|
||||||
"""
|
|
||||||
global _provider_cache
|
|
||||||
|
|
||||||
if _provider_cache is not None:
|
|
||||||
return _provider_cache
|
|
||||||
|
|
||||||
_provider_cache = {}
|
|
||||||
|
|
||||||
for path in settings.TEST_REPOSITORIES:
|
|
||||||
try:
|
|
||||||
p = RepositoryProvider(path)
|
|
||||||
_provider_cache[p.name] = p
|
|
||||||
except (ValueError, RepositoryNotFound):
|
|
||||||
continue
|
|
||||||
|
|
||||||
for path in settings.TEST_STREAMS:
|
|
||||||
try:
|
|
||||||
p = FileProvider(path)
|
|
||||||
_provider_cache[p.name] = p
|
|
||||||
except InvalidSubunitProvider:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if settings.TEST_STREAM_STDIN:
|
|
||||||
p = StandardInputProvider()
|
|
||||||
_provider_cache[p.name] = p
|
|
||||||
|
|
||||||
return _provider_cache
|
|
||||||
|
|
||||||
|
|
||||||
def _clean_name(name):
|
|
||||||
# TODO(Tim Buckley) currently throwing away other info - any worth keeping?
|
|
||||||
m = NAME_TAGS_PATTERN.match(name)
|
|
||||||
if m:
|
|
||||||
# tags = m.group(2).split(',')
|
|
||||||
return m.group(1)
|
|
||||||
|
|
||||||
m = NAME_SCENARIO_PATTERN.match(name)
|
|
||||||
if m:
|
|
||||||
return '{0}.{1}'.format(m.group(2), m.group(1))
|
|
||||||
|
|
||||||
return name
|
|
||||||
|
|
||||||
|
|
||||||
def _strip(text):
|
|
||||||
return re.sub(r'\W', '', text)
|
|
||||||
|
|
||||||
|
|
||||||
def _clean_details(details):
|
|
||||||
return {_strip(k): v.as_text() for k, v in details.iteritems()
|
|
||||||
if v.as_text()}
|
|
||||||
|
|
||||||
|
|
||||||
def _read_test(test, out, strip_details):
|
|
||||||
# clean up the result test info a bit
|
|
||||||
|
|
||||||
start, end = test['timestamps']
|
|
||||||
|
|
||||||
out.append({
|
|
||||||
'name': _clean_name(test['id']),
|
|
||||||
'status': test['status'],
|
|
||||||
'tags': list(test['tags']),
|
|
||||||
'timestamps': test['timestamps'],
|
|
||||||
'duration': (end - start).total_seconds(),
|
|
||||||
'details': {} if strip_details else _clean_details(test['details'])
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def convert_stream(stream_file, strip_details=False):
|
|
||||||
"""Converts a subunit stream into a raw list of test dicts.
|
|
||||||
|
|
||||||
:param stream_file: subunit stream to be converted
|
|
||||||
:param strip_details: if True, remove test details (e.g. stdout/stderr)
|
|
||||||
:return: a list of individual test results
|
|
||||||
"""
|
|
||||||
|
|
||||||
ret = []
|
|
||||||
|
|
||||||
result_stream = subunit.ByteStreamToStreamResult(stream_file)
|
|
||||||
starts = StreamResult()
|
|
||||||
summary = StreamSummary()
|
|
||||||
outcomes = StreamToDict(partial(_read_test,
|
|
||||||
out=ret,
|
|
||||||
strip_details=strip_details))
|
|
||||||
|
|
||||||
result = CopyStreamResult([starts, outcomes, summary])
|
|
||||||
|
|
||||||
result.startTestRun()
|
|
||||||
result_stream.run(result)
|
|
||||||
result.stopTestRun()
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def convert_run(test_run, strip_details=False):
|
|
||||||
"""Converts the given test run into a raw list of test dicts.
|
|
||||||
|
|
||||||
Uses the subunit stream as an intermediate format.(see: read_subunit.py
|
|
||||||
from subunit2sql)
|
|
||||||
|
|
||||||
:param test_run: the test run to convert
|
|
||||||
:type test_run: AbstractTestRun
|
|
||||||
:param strip_details: if True, remove test details (e.g. stdout/stderr)
|
|
||||||
:return: a list of individual test results
|
|
||||||
"""
|
|
||||||
|
|
||||||
return convert_stream(test_run.get_subunit_stream(), strip_details)
|
|
||||||
|
|
||||||
|
|
||||||
def _descend_recurse(parent, parts_remaining):
|
|
||||||
if not parts_remaining:
|
|
||||||
return parent
|
|
||||||
|
|
||||||
target = parts_remaining.pop()
|
|
||||||
|
|
||||||
# create elements on-the-fly
|
|
||||||
if 'children' not in parent:
|
|
||||||
parent['children'] = []
|
|
||||||
|
|
||||||
# attempt to find an existing matching child
|
|
||||||
child = None
|
|
||||||
for c in parent['children']:
|
|
||||||
if c['name'] == target:
|
|
||||||
child = c
|
|
||||||
break
|
|
||||||
|
|
||||||
# create manually if the target child doesn't already exist
|
|
||||||
if not child:
|
|
||||||
child = {'name': target}
|
|
||||||
parent['children'].append(child)
|
|
||||||
|
|
||||||
return _descend_recurse(child, parts_remaining)
|
|
||||||
|
|
||||||
|
|
||||||
def _descend(root, path):
|
|
||||||
"""Retrieves the node within the 'root' dict
|
|
||||||
|
|
||||||
Retrieves the node within the `root` dict denoted by the series of
|
|
||||||
'.'-separated children as specified in `path`. Children for each node must
|
|
||||||
be contained in a list `children`, and name comparison will be
|
|
||||||
performed on the field `name`.
|
|
||||||
|
|
||||||
If parts of the path (up to and including the last child itself) do not
|
|
||||||
exist, they will be created automatically under the root dict.
|
|
||||||
|
|
||||||
:param root: the root node
|
|
||||||
:param path: a '.'-separated path
|
|
||||||
:type path: str
|
|
||||||
:return: the dict node representing the last child
|
|
||||||
"""
|
|
||||||
|
|
||||||
path_parts = path.split('.')
|
|
||||||
path_parts.reverse()
|
|
||||||
|
|
||||||
root['name'] = path_parts.pop()
|
|
||||||
|
|
||||||
return _descend_recurse(root, path_parts)
|
|
||||||
|
|
||||||
|
|
||||||
def reorganize(converted_test_run):
|
|
||||||
"""Reorganizes test run, forming trees based on module paths
|
|
||||||
|
|
||||||
Reorganizes and categorizes the given test run, forming tree of tests
|
|
||||||
categorized by their module paths.
|
|
||||||
|
|
||||||
:param converted_test_run:
|
|
||||||
:return: a dict tree of test nodes, organized by module path
|
|
||||||
"""
|
|
||||||
|
|
||||||
ret = {}
|
|
||||||
|
|
||||||
for entry in converted_test_run:
|
|
||||||
entry['name_full'] = entry['name']
|
|
||||||
|
|
||||||
dest_node = _descend(ret, entry['name'])
|
|
||||||
|
|
||||||
# update the dest node with info from the current entry, but hold on to
|
|
||||||
# the already-parsed name
|
|
||||||
name = dest_node['name']
|
|
||||||
dest_node.update(entry)
|
|
||||||
dest_node['name'] = name
|
|
||||||
|
|
||||||
return ret
|
|
@ -1,126 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Django settings for stackviz project.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/1.7/topics/settings/
|
|
||||||
|
|
||||||
For the full list of settings and their values, see
|
|
||||||
https://docs.djangoproject.com/en/1.7/ref/settings/
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
|
||||||
import os
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
|
||||||
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
|
||||||
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
|
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
|
||||||
SECRET_KEY = '*to^*vlhq&05jo0^kad)=kboy$8@&x9s6i23ukh*^%w_$=5bmh'
|
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
|
||||||
DEBUG = True
|
|
||||||
|
|
||||||
TEMPLATE_DEBUG = True
|
|
||||||
|
|
||||||
ALLOWED_HOSTS = []
|
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
|
||||||
|
|
||||||
INSTALLED_APPS = (
|
|
||||||
'django.contrib.contenttypes',
|
|
||||||
'django.contrib.sessions',
|
|
||||||
'django.contrib.messages',
|
|
||||||
'django.contrib.staticfiles',
|
|
||||||
)
|
|
||||||
|
|
||||||
MIDDLEWARE_CLASSES = (
|
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
|
||||||
'django.middleware.common.CommonMiddleware',
|
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
|
||||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
||||||
)
|
|
||||||
|
|
||||||
ROOT_URLCONF = 'stackviz.urls'
|
|
||||||
|
|
||||||
WSGI_APPLICATION = 'stackviz.wsgi.application'
|
|
||||||
|
|
||||||
|
|
||||||
# Database
|
|
||||||
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
|
|
||||||
|
|
||||||
DATABASES = {}
|
|
||||||
|
|
||||||
# Internationalization
|
|
||||||
# https://docs.djangoproject.com/en/1.7/topics/i18n/
|
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
|
||||||
|
|
||||||
USE_I18N = True
|
|
||||||
|
|
||||||
USE_L10N = True
|
|
||||||
|
|
||||||
USE_TZ = True
|
|
||||||
|
|
||||||
TEMPLATE_CONTEXT_PROCESSORS = (
|
|
||||||
'stackviz.global_template_injector.inject_extra_context',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
|
||||||
# https://docs.djangoproject.com/en/1.7/howto/static-files/
|
|
||||||
|
|
||||||
STATIC_URL = '/static/'
|
|
||||||
|
|
||||||
STATICFILES_DIRS = [
|
|
||||||
os.path.join(BASE_DIR, 'stackviz', 'static')
|
|
||||||
]
|
|
||||||
|
|
||||||
TEMPLATE_DIRS = [
|
|
||||||
os.path.join(BASE_DIR, 'stackviz', 'templates')
|
|
||||||
]
|
|
||||||
|
|
||||||
# If True, read a stream from stdin (only valid for exported sites)
|
|
||||||
TEST_STREAM_STDIN = False
|
|
||||||
|
|
||||||
# A list of files containing directly-accessible subunit streams.
|
|
||||||
TEST_STREAMS = []
|
|
||||||
|
|
||||||
# A list of test repositories containing (potentially) multiple subunit
|
|
||||||
# streams.
|
|
||||||
TEST_REPOSITORIES = [
|
|
||||||
os.path.join(BASE_DIR, 'test_data')
|
|
||||||
]
|
|
||||||
|
|
||||||
# The input dstat file
|
|
||||||
DSTAT_CSV = 'dstat.log'
|
|
||||||
|
|
||||||
# If true, AJAX calls should attempt to load `*.json.gz` files rather than
|
|
||||||
# plain `*.json` files. This should only ever be toggled `True` for static site
|
|
||||||
# exports and is not currently supported on live servers.
|
|
||||||
USE_GZIP = False
|
|
||||||
|
|
||||||
# Toggles offline/online mode for static export. Will trigger menu to show
|
|
||||||
# either the full site or only links supported by static exporter.
|
|
||||||
OFFLINE = False
|
|
@ -1,354 +0,0 @@
|
|||||||
/*!
|
|
||||||
* Start Bootstrap - SB Admin 2 Bootstrap Admin Theme (http://startbootstrap.com)
|
|
||||||
* Code licensed under the Apache License v2.0.
|
|
||||||
* For details, see http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*/
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #f8f8f8;
|
|
||||||
}
|
|
||||||
|
|
||||||
#wrapper {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-wrapper {
|
|
||||||
padding: 0 15px;
|
|
||||||
min-height: 568px;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media(min-width:768px) {
|
|
||||||
#page-wrapper {
|
|
||||||
position: inherit;
|
|
||||||
margin: 0 0 0 250px;
|
|
||||||
padding: 0 30px;
|
|
||||||
border-left: 1px solid #e7e7e7;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top-links {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top-links li {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top-links li:last-child {
|
|
||||||
margin-right: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top-links li a {
|
|
||||||
padding: 15px;
|
|
||||||
min-height: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top-links .dropdown-menu li {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top-links .dropdown-menu li:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top-links .dropdown-menu li a {
|
|
||||||
padding: 3px 20px;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top-links .dropdown-menu li a div {
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top-links .dropdown-messages,
|
|
||||||
.navbar-top-links .dropdown-tasks,
|
|
||||||
.navbar-top-links .dropdown-alerts {
|
|
||||||
width: 310px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top-links .dropdown-messages {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top-links .dropdown-tasks {
|
|
||||||
margin-left: -59px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top-links .dropdown-alerts {
|
|
||||||
margin-left: -123px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top-links .dropdown-user {
|
|
||||||
right: 0;
|
|
||||||
left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .sidebar-nav.navbar-collapse {
|
|
||||||
padding-right: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .sidebar-search {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar ul li {
|
|
||||||
border-bottom: 1px solid #e7e7e7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar ul li a.active {
|
|
||||||
background-color: #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .arrow {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .fa.arrow:before {
|
|
||||||
content: "\f104";
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .active>a>.fa.arrow:before {
|
|
||||||
content: "\f107";
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav-second-level li,
|
|
||||||
.sidebar .nav-third-level li {
|
|
||||||
border-bottom: 0!important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav-second-level li a {
|
|
||||||
padding-left: 37px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar .nav-third-level li a {
|
|
||||||
padding-left: 52px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media(min-width:768px) {
|
|
||||||
.sidebar {
|
|
||||||
z-index: 1;
|
|
||||||
position: absolute;
|
|
||||||
width: 250px;
|
|
||||||
margin-top: 51px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-top-links .dropdown-messages,
|
|
||||||
.navbar-top-links .dropdown-tasks,
|
|
||||||
.navbar-top-links .dropdown-alerts {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline {
|
|
||||||
color: inherit;
|
|
||||||
background-color: transparent;
|
|
||||||
transition: all .5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary.btn-outline {
|
|
||||||
color: #428bca;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success.btn-outline {
|
|
||||||
color: #5cb85c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-info.btn-outline {
|
|
||||||
color: #5bc0de;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-warning.btn-outline {
|
|
||||||
color: #f0ad4e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger.btn-outline {
|
|
||||||
color: #d9534f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary.btn-outline:hover,
|
|
||||||
.btn-success.btn-outline:hover,
|
|
||||||
.btn-info.btn-outline:hover,
|
|
||||||
.btn-warning.btn-outline:hover,
|
|
||||||
.btn-danger.btn-outline:hover {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat li {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
border-bottom: 1px dotted #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat li.left .chat-body {
|
|
||||||
margin-left: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat li.right .chat-body {
|
|
||||||
margin-right: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat li .chat-body p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel .slidedown .glyphicon,
|
|
||||||
.chat .glyphicon {
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-panel .panel-body {
|
|
||||||
height: 350px;
|
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-panel {
|
|
||||||
margin-top: 25%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flot-chart {
|
|
||||||
display: block;
|
|
||||||
height: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flot-chart-content {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dataTables_wrapper {
|
|
||||||
position: relative;
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.dataTable thead .sorting,
|
|
||||||
table.dataTable thead .sorting_asc,
|
|
||||||
table.dataTable thead .sorting_desc,
|
|
||||||
table.dataTable thead .sorting_asc_disabled,
|
|
||||||
table.dataTable thead .sorting_desc_disabled {
|
|
||||||
background: 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.dataTable thead .sorting_asc:after {
|
|
||||||
content: "\f0de";
|
|
||||||
float: right;
|
|
||||||
font-family: fontawesome;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.dataTable thead .sorting_desc:after {
|
|
||||||
content: "\f0dd";
|
|
||||||
float: right;
|
|
||||||
font-family: fontawesome;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.dataTable thead .sorting:after {
|
|
||||||
content: "\f0dc";
|
|
||||||
float: right;
|
|
||||||
font-family: fontawesome;
|
|
||||||
color: rgba(50,50,50,.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-circle {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
padding: 6px 0;
|
|
||||||
border-radius: 15px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.428571429;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-circle.btn-lg {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
padding: 10px 16px;
|
|
||||||
border-radius: 25px;
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 1.33;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-circle.btn-xl {
|
|
||||||
width: 70px;
|
|
||||||
height: 70px;
|
|
||||||
padding: 10px 16px;
|
|
||||||
border-radius: 35px;
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 1.33;
|
|
||||||
}
|
|
||||||
|
|
||||||
.show-grid [class^=col-] {
|
|
||||||
padding-top: 10px;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
background-color: #eee!important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.show-grid {
|
|
||||||
margin: 15px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.huge {
|
|
||||||
font-size: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-green {
|
|
||||||
border-color: #5cb85c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-green .panel-heading {
|
|
||||||
border-color: #5cb85c;
|
|
||||||
color: #fff;
|
|
||||||
background-color: #5cb85c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-green a {
|
|
||||||
color: #5cb85c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-green a:hover {
|
|
||||||
color: #3d8b3d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-red {
|
|
||||||
border-color: #d9534f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-red .panel-heading {
|
|
||||||
border-color: #d9534f;
|
|
||||||
color: #fff;
|
|
||||||
background-color: #d9534f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-red a {
|
|
||||||
color: #d9534f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-red a:hover {
|
|
||||||
color: #b52b27;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-yellow {
|
|
||||||
border-color: #f0ad4e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-yellow .panel-heading {
|
|
||||||
border-color: #f0ad4e;
|
|
||||||
color: #fff;
|
|
||||||
background-color: #f0ad4e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-yellow a {
|
|
||||||
color: #f0ad4e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-yellow a:hover {
|
|
||||||
color: #df8a13;
|
|
||||||
}
|
|
@ -1,180 +0,0 @@
|
|||||||
.timeline {
|
|
||||||
position: relative;
|
|
||||||
padding: 20px 0 20px;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline:before {
|
|
||||||
content: " ";
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 50%;
|
|
||||||
width: 3px;
|
|
||||||
margin-left: -1.5px;
|
|
||||||
background-color: #eeeeee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline > li {
|
|
||||||
position: relative;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline > li:before,
|
|
||||||
.timeline > li:after {
|
|
||||||
content: " ";
|
|
||||||
display: table;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline > li:after {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline > li:before,
|
|
||||||
.timeline > li:after {
|
|
||||||
content: " ";
|
|
||||||
display: table;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline > li:after {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline > li > .timeline-panel {
|
|
||||||
float: left;
|
|
||||||
position: relative;
|
|
||||||
width: 46%;
|
|
||||||
padding: 20px;
|
|
||||||
border: 1px solid #d4d4d4;
|
|
||||||
border-radius: 2px;
|
|
||||||
-webkit-box-shadow: 0 1px 6px rgba(0,0,0,0.175);
|
|
||||||
box-shadow: 0 1px 6px rgba(0,0,0,0.175);
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline > li > .timeline-panel:before {
|
|
||||||
content: " ";
|
|
||||||
display: inline-block;
|
|
||||||
position: absolute;
|
|
||||||
top: 26px;
|
|
||||||
right: -15px;
|
|
||||||
border-top: 15px solid transparent;
|
|
||||||
border-right: 0 solid #ccc;
|
|
||||||
border-bottom: 15px solid transparent;
|
|
||||||
border-left: 15px solid #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline > li > .timeline-panel:after {
|
|
||||||
content: " ";
|
|
||||||
display: inline-block;
|
|
||||||
position: absolute;
|
|
||||||
top: 27px;
|
|
||||||
right: -14px;
|
|
||||||
border-top: 14px solid transparent;
|
|
||||||
border-right: 0 solid #fff;
|
|
||||||
border-bottom: 14px solid transparent;
|
|
||||||
border-left: 14px solid #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline > li > .timeline-badge {
|
|
||||||
z-index: 100;
|
|
||||||
position: absolute;
|
|
||||||
top: 16px;
|
|
||||||
left: 50%;
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
margin-left: -25px;
|
|
||||||
border-radius: 50% 50% 50% 50%;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.4em;
|
|
||||||
line-height: 50px;
|
|
||||||
color: #fff;
|
|
||||||
background-color: #999999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline > li.timeline-inverted > .timeline-panel {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline > li.timeline-inverted > .timeline-panel:before {
|
|
||||||
right: auto;
|
|
||||||
left: -15px;
|
|
||||||
border-right-width: 15px;
|
|
||||||
border-left-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline > li.timeline-inverted > .timeline-panel:after {
|
|
||||||
right: auto;
|
|
||||||
left: -14px;
|
|
||||||
border-right-width: 14px;
|
|
||||||
border-left-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-badge.primary {
|
|
||||||
background-color: #2e6da4 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-badge.success {
|
|
||||||
background-color: #3f903f !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-badge.warning {
|
|
||||||
background-color: #f0ad4e !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-badge.danger {
|
|
||||||
background-color: #d9534f !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-badge.info {
|
|
||||||
background-color: #5bc0de !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-title {
|
|
||||||
margin-top: 0;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-body > p,
|
|
||||||
.timeline-body > ul {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-body > p + p {
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media(max-width:767px) {
|
|
||||||
ul.timeline:before {
|
|
||||||
left: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.timeline > li > .timeline-panel {
|
|
||||||
width: calc(100% - 90px);
|
|
||||||
width: -moz-calc(100% - 90px);
|
|
||||||
width: -webkit-calc(100% - 90px);
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.timeline > li > .timeline-badge {
|
|
||||||
top: 16px;
|
|
||||||
left: 15px;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.timeline > li > .timeline-panel {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.timeline > li > .timeline-panel:before {
|
|
||||||
right: auto;
|
|
||||||
left: -15px;
|
|
||||||
border-right-width: 15px;
|
|
||||||
border-left-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul.timeline > li > .timeline-panel:after {
|
|
||||||
right: auto;
|
|
||||||
left: -14px;
|
|
||||||
border-right-width: 14px;
|
|
||||||
border-left-width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
.highlight {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var originalDetailsContent = null;
|
|
||||||
|
|
||||||
var detailsCache = null;
|
|
||||||
var detailsInProgress = false;
|
|
||||||
var detailsWaiting = [];
|
|
||||||
var runId = null;
|
|
||||||
|
|
||||||
var loadDetails = function(callback) {
|
|
||||||
if (detailsCache === null) {
|
|
||||||
detailsWaiting.push(callback);
|
|
||||||
|
|
||||||
if (!detailsInProgress) {
|
|
||||||
var url = "tempest_api_details_" + runId + ".json";
|
|
||||||
if ("{{use_gzip}}" === "True") {
|
|
||||||
url += ".gz";
|
|
||||||
}
|
|
||||||
|
|
||||||
detailsInProgress = true;
|
|
||||||
|
|
||||||
d3.json(url, function(error, data) {
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
detailsCache = data;
|
|
||||||
detailsWaiting.forEach(function(cb) {
|
|
||||||
cb(detailsCache);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
callback(detailsCache);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var showDetails = function(item) {
|
|
||||||
var parent = $("#details-dialog");
|
|
||||||
|
|
||||||
loadDetails(function(details) {
|
|
||||||
if (!details.hasOwnProperty(item.name_full)) {
|
|
||||||
console.log("Details not found for item:", item.name_full);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (originalDetailsContent === null) {
|
|
||||||
originalDetailsContent = parent.html();
|
|
||||||
}
|
|
||||||
|
|
||||||
parent.empty();
|
|
||||||
for (var prop in details[item.name_full]) {
|
|
||||||
$("<h3>").text(prop).appendTo(parent);
|
|
||||||
$("<pre>").text(details[item.name_full][prop]).appendTo(parent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function addDialogButton(parentID, run_id) {
|
|
||||||
//parentID: A string contiaining the parent div id to which the button will be appended
|
|
||||||
runId=run_id;
|
|
||||||
var button = $('<button/>',
|
|
||||||
{
|
|
||||||
text: 'View Log',
|
|
||||||
click: function() {$("#details-dialog").dialog("open");}
|
|
||||||
});
|
|
||||||
|
|
||||||
$(parentID).append(button);
|
|
||||||
|
|
||||||
$("#details-dialog").dialog({
|
|
||||||
dialogClass: 'ui-dialog',
|
|
||||||
autoOpen: false,
|
|
||||||
width: 800,
|
|
||||||
height: 500,
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
text: "OK",
|
|
||||||
click: function() {
|
|
||||||
$( this ).dialog( "close" );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
$(function() {
|
|
||||||
|
|
||||||
$('#side-menu').metisMenu();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
//Loads the correct sidebar on window load,
|
|
||||||
//collapses the sidebar on window resize.
|
|
||||||
// Sets the min-height of #page-wrapper to window size
|
|
||||||
$(function() {
|
|
||||||
$(window).bind("load resize", function() {
|
|
||||||
topOffset = 50;
|
|
||||||
width = (this.window.innerWidth > 0) ? this.window.innerWidth : this.screen.width;
|
|
||||||
if (width < 768) {
|
|
||||||
$('div.navbar-collapse').addClass('collapse');
|
|
||||||
topOffset = 100; // 2-row-menu
|
|
||||||
} else {
|
|
||||||
$('div.navbar-collapse').removeClass('collapse');
|
|
||||||
}
|
|
||||||
|
|
||||||
height = ((this.window.innerHeight > 0) ? this.window.innerHeight : this.screen.height) - 1;
|
|
||||||
height = height - topOffset;
|
|
||||||
if (height < 1) height = 1;
|
|
||||||
if (height > topOffset) {
|
|
||||||
$("#page-wrapper").css("min-height", (height) + "px");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var url = window.location;
|
|
||||||
var element = $('ul.nav a').filter(function() {
|
|
||||||
return this.href == url || url.href.indexOf(this.href) == 0;
|
|
||||||
}).addClass('active').parent().parent().addClass('in').parent();
|
|
||||||
if (element.is('li')) {
|
|
||||||
element.addClass('active');
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,118 +0,0 @@
|
|||||||
/*
|
|
||||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
/*
|
|
||||||
<div class="col-lg-12">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">Test Runs</div>
|
|
||||||
<div class="panel-body" id="run-summary-div">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
*/
|
|
||||||
|
|
||||||
//@param data: JSON data of the test run
|
|
||||||
function getData(data) {
|
|
||||||
|
|
||||||
var num_successes = 0;
|
|
||||||
var num_failures = 0;
|
|
||||||
var num_skipped = 0
|
|
||||||
var total_time = 0;
|
|
||||||
var longest_test={duration: 0};
|
|
||||||
|
|
||||||
function calculateChildrenTime(i) {
|
|
||||||
var dur = 0;
|
|
||||||
if (typeof i.duration !== "undefined") {
|
|
||||||
if (i.status=="success") num_successes++;
|
|
||||||
else if (i.status=="fail") num_failures++;
|
|
||||||
else if (i.status=="skip") num_skipped++;
|
|
||||||
|
|
||||||
if (longest_test.duration < i.duration)
|
|
||||||
longest_test = i;
|
|
||||||
|
|
||||||
dur = i.duration;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
for (var k in i.children) {
|
|
||||||
dur += calculateChildrenTime(i.children[k]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dur;
|
|
||||||
}
|
|
||||||
|
|
||||||
total_time=calculateChildrenTime(data);
|
|
||||||
|
|
||||||
var data_dict= { "Successes": num_successes,
|
|
||||||
"Failures": num_failures,
|
|
||||||
"Skipped": num_skipped,
|
|
||||||
"Total Time": total_time.toFixed(2),
|
|
||||||
"Longest Test": longest_test.name + " ("+longest_test.duration+")"};
|
|
||||||
|
|
||||||
return data_dict;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function createTable(data, entry) {
|
|
||||||
var container = $("<div>")
|
|
||||||
.addClass('col-lg-6' )
|
|
||||||
.appendTo($("#run-summary-div"));
|
|
||||||
|
|
||||||
var panel = $("<div>")
|
|
||||||
.addClass('panel panel-default')
|
|
||||||
.appendTo(container);
|
|
||||||
|
|
||||||
var head = $("<div>")
|
|
||||||
.addClass("panel-heading")
|
|
||||||
.appendTo(panel);
|
|
||||||
head.append($("<a>", {
|
|
||||||
href: 'tempest_timeline_' + entry.provider + '_' + entry.run + '.html',
|
|
||||||
text: entry.providerDescription + ", run #" + entry.run
|
|
||||||
}));
|
|
||||||
|
|
||||||
var body = $("<div>")
|
|
||||||
.addClass("panel-body")
|
|
||||||
.appendTo(panel);
|
|
||||||
|
|
||||||
var table = $("<table>")
|
|
||||||
.addClass("table table-bordered table-hover table-striped")
|
|
||||||
.appendTo(body);
|
|
||||||
|
|
||||||
var data_dict = getData(data);
|
|
||||||
for (var key in data_dict) {
|
|
||||||
$("<tr>")
|
|
||||||
.append($("<td>").text(key))
|
|
||||||
.append($("<td>").text(data_dict[key]))
|
|
||||||
.appendTo(table);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//@param run_id: The method is passed the latest run_id so it can populate the tables moving backwards
|
|
||||||
function createTables(entries) {
|
|
||||||
entries.forEach(function(entry) {
|
|
||||||
//TODO: Sort tables when inserting so they appear in correct order
|
|
||||||
d3.json(entry.url, function(error, data) {
|
|
||||||
if (error) throw error;
|
|
||||||
//create a table for the info
|
|
||||||
// TODO: entry now has provider description, etc which should be
|
|
||||||
// shown (categorized?)
|
|
||||||
createTable(data, entry);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,177 +0,0 @@
|
|||||||
/*
|
|
||||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var runId = null;
|
|
||||||
var providerName = null;
|
|
||||||
|
|
||||||
function populateTable(d, textColor) {
|
|
||||||
var oldtbl = document.getElementById("result-table-div");
|
|
||||||
oldtbl.innerHTML = "";
|
|
||||||
var tbl = document.createElement('table');
|
|
||||||
tbl.setAttribute("id","test-table");
|
|
||||||
tbl.setAttribute("class","table table-bordered table-hover table-striped");
|
|
||||||
if (typeof d.children == "undefined") {
|
|
||||||
for (var key in d) {
|
|
||||||
if (key=="status" || key=="name_full" || key=="name" || key=="duration" || key=="tags" || key=="timestamps") {
|
|
||||||
var row = tbl.insertRow();
|
|
||||||
var td1 = row.insertCell();
|
|
||||||
var td2 = row.insertCell();
|
|
||||||
td1.innerHTML = key;
|
|
||||||
td2.innerHTML = d[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
document.getElementById("result-table-div").appendChild(tbl);
|
|
||||||
document.getElementById("table-heading").innerHTML=d.name;
|
|
||||||
addDialogButton("#result-table-div",providerName + "_" + runId);
|
|
||||||
showDetails(d);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
for (var j in d.children) {
|
|
||||||
var row = tbl.insertRow();
|
|
||||||
var td1 = row.insertCell();
|
|
||||||
var td2 = row.insertCell();
|
|
||||||
td1.innerHTML = d.children[j].name;
|
|
||||||
td2.innerHTML = calculateChildrenTime(d.children[j]).toFixed(2);
|
|
||||||
td1.style.color = textColor(d.children[j]);
|
|
||||||
document.getElementById("result-table-div").appendChild(tbl);
|
|
||||||
document.getElementById("table-heading").innerHTML=d.name +
|
|
||||||
": " + calculateChildrenTime(d).toFixed(2) + " seconds"
|
|
||||||
$( "table-test" ).DataTable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateChildrenTime(i) {
|
|
||||||
var dur = 0;
|
|
||||||
if (typeof i.duration !== "undefined") {
|
|
||||||
dur = i.duration;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
for (var k in i.children) {
|
|
||||||
dur += calculateChildrenTime(i.children[k]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dur;
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayFailingTests(d) {
|
|
||||||
|
|
||||||
document.getElementById("failure-table-div").innerHTML="";
|
|
||||||
var tbl = document.createElement('table');
|
|
||||||
tbl.setAttribute("id","failure-table");
|
|
||||||
tbl.setAttribute("class","table table-bordered table-hover table-striped");
|
|
||||||
|
|
||||||
function findFailingTests(i,result) {
|
|
||||||
if (i.status == "fail") {
|
|
||||||
result.push(i);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
for (var k in i.children) {
|
|
||||||
findFailingTests(i.children[k],result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var failureList=[];
|
|
||||||
|
|
||||||
findFailingTests(d,failureList);
|
|
||||||
for (var row in failureList) {
|
|
||||||
var newRow = tbl.insertRow();
|
|
||||||
newRow.setAttribute("class","failure-row");
|
|
||||||
var td1 = newRow.insertCell();
|
|
||||||
var td2 = newRow.insertCell();
|
|
||||||
td1.innerHTML = failureList[row].name_full;
|
|
||||||
td2.innerHTML = parseFloat(failureList[row].duration).toFixed(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("failure-table-div").appendChild(tbl);
|
|
||||||
$( "#failure-table-div" ).hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function createSunburst(url, provider_name, run_id) {
|
|
||||||
runId = run_id;
|
|
||||||
providerName = provider_name;
|
|
||||||
|
|
||||||
var width = 700,
|
|
||||||
height = 500,
|
|
||||||
radius = Math.min(width, height) / 2;
|
|
||||||
|
|
||||||
var x = d3.scale.linear()
|
|
||||||
.range([0, 2 * Math.PI]);
|
|
||||||
|
|
||||||
var y = d3.scale.sqrt()
|
|
||||||
.range([0, radius]);
|
|
||||||
|
|
||||||
var color = d3.scale.category20c();
|
|
||||||
|
|
||||||
var svg = d3.select("#sunburst").append("svg")
|
|
||||||
.attr("width", width)
|
|
||||||
.attr("height", height)
|
|
||||||
.append("g")
|
|
||||||
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 10) + ")");
|
|
||||||
|
|
||||||
var partition = d3.layout.partition()
|
|
||||||
.value(function(d) { return d.duration; });
|
|
||||||
|
|
||||||
var arc = d3.svg.arc()
|
|
||||||
.startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
|
|
||||||
.endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
|
|
||||||
.innerRadius(function(d) { return Math.max(0, y(d.y)); })
|
|
||||||
.outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)); });
|
|
||||||
|
|
||||||
d3.json(url, function(error, root) {
|
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
displayFailingTests(root);
|
|
||||||
|
|
||||||
var path = svg.selectAll("path")
|
|
||||||
.data(partition.nodes(root))
|
|
||||||
.enter().append("path")
|
|
||||||
.attr("d", arc)
|
|
||||||
.style("fill", function(d) { return color(d.name); })
|
|
||||||
.on("click", click);
|
|
||||||
|
|
||||||
function click(d) {
|
|
||||||
path.transition()
|
|
||||||
.duration(750)
|
|
||||||
.attrTween("d", arcTween(d));
|
|
||||||
populateTable(d,mouse);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mouse(d) { return color(d.name); }
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
d3.select(self.frameElement).style("height", height + "px");
|
|
||||||
|
|
||||||
// Interpolate the scales!
|
|
||||||
function arcTween(d) {
|
|
||||||
var xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
|
|
||||||
yd = d3.interpolate(y.domain(), [d.y, 1]),
|
|
||||||
yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
|
|
||||||
return function(d, i) {
|
|
||||||
return i
|
|
||||||
? function(t) { return arc(d); }
|
|
||||||
: function(t) { x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); return arc(d); };
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,661 +0,0 @@
|
|||||||
/*
|
|
||||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*global d3:false*/
|
|
||||||
|
|
||||||
var statusColorMap = {
|
|
||||||
"success": "LightGreen",
|
|
||||||
"fail": "Crimson",
|
|
||||||
"skip": "DodgerBlue"
|
|
||||||
};
|
|
||||||
|
|
||||||
var binaryMinIndex = function(min, array, func) {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var left = 0;
|
|
||||||
var right = array.length - 1;
|
|
||||||
|
|
||||||
while (left < right) {
|
|
||||||
var mid = Math.floor((left + right) / 2);
|
|
||||||
|
|
||||||
if (min < func(array[mid])) {
|
|
||||||
right = mid - 1;
|
|
||||||
} else if (min > func(array[mid])) {
|
|
||||||
left = mid + 1;
|
|
||||||
} else {
|
|
||||||
right = mid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (left >= array.length) {
|
|
||||||
return array.length - 1;
|
|
||||||
} else if (func(array[left]) <= min) {
|
|
||||||
return left;
|
|
||||||
} else {
|
|
||||||
return left - 1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var binaryMaxIndex = function(max, array, func) {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var left = 0;
|
|
||||||
var right = array.length - 1;
|
|
||||||
|
|
||||||
while (left < right) {
|
|
||||||
var mid = Math.floor((left + right) / 2);
|
|
||||||
|
|
||||||
if (max < func(array[mid])) {
|
|
||||||
right = mid - 1;
|
|
||||||
} else if (max > func(array[mid])) {
|
|
||||||
left = mid + 1;
|
|
||||||
} else {
|
|
||||||
right = mid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (right < 0) {
|
|
||||||
return 0;
|
|
||||||
} else if (func(array[right]) <= max) {
|
|
||||||
return right + 1; // exclusive index
|
|
||||||
} else {
|
|
||||||
return right;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var parseWorker = function(tags) {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
for (var i = 0; i < tags.length; i++) {
|
|
||||||
if (!tags[i].startsWith("worker")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseInt(tags[i].split("-")[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
var getDstatLanes = function(data, mins, maxes) {
|
|
||||||
if (!data) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
var row = data[0];
|
|
||||||
var lanes = [];
|
|
||||||
|
|
||||||
if ('total_cpu_usage_usr' in row && 'total_cpu_usage_sys' in row) {
|
|
||||||
lanes.push([{
|
|
||||||
scale: d3.scale.linear().domain([0, 100]),
|
|
||||||
value: function(d) {
|
|
||||||
return d.total_cpu_usage_wai;
|
|
||||||
},
|
|
||||||
color: "rgba(224, 188, 188, 1)",
|
|
||||||
text: "CPU wait"
|
|
||||||
}, {
|
|
||||||
scale: d3.scale.linear().domain([0, 100]),
|
|
||||||
value: function(d) {
|
|
||||||
return d.total_cpu_usage_usr + d.total_cpu_usage_sys;
|
|
||||||
},
|
|
||||||
color: "rgba(102, 140, 178, 0.75)",
|
|
||||||
text: "CPU (user+sys)"
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('memory_usage_used' in row) {
|
|
||||||
lanes.push([{
|
|
||||||
scale: d3.scale.linear().domain([0, maxes.memory_usage_used]),
|
|
||||||
value: function(d) { return d.memory_usage_used; },
|
|
||||||
color: "rgba(102, 140, 178, 0.75)",
|
|
||||||
text: "Memory"
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('net_total_recv' in row && 'net_total_send' in row) {
|
|
||||||
lanes.push([{
|
|
||||||
scale: d3.scale.linear().domain([0, maxes.net_total_recv]),
|
|
||||||
value: function(d) { return d.net_total_recv; },
|
|
||||||
color: "rgba(224, 188, 188, 1)",
|
|
||||||
text: "Net Down"
|
|
||||||
}, {
|
|
||||||
scale: d3.scale.linear().domain([0, maxes.net_total_send]),
|
|
||||||
value: function(d) { return d.net_total_send; },
|
|
||||||
color: "rgba(102, 140, 178, 0.75)",
|
|
||||||
text: "Net Up",
|
|
||||||
type: "line"
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('dsk_total_read' in row && 'dsk_total_writ' in row) {
|
|
||||||
lanes.push([{
|
|
||||||
scale: d3.scale.linear().domain([0, maxes.dsk_total_read]),
|
|
||||||
value: function(d) { return d.dsk_total_read; },
|
|
||||||
color: "rgba(224, 188, 188, 1)",
|
|
||||||
text: "Disk Read",
|
|
||||||
type: "line"
|
|
||||||
}, {
|
|
||||||
scale: d3.scale.linear().domain([0, maxes.dsk_total_writ]),
|
|
||||||
value: function(d) { return d.dsk_total_writ; },
|
|
||||||
color: "rgba(102, 140, 178, 0.75)",
|
|
||||||
text: "Disk Write",
|
|
||||||
type: "line"
|
|
||||||
}]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return lanes;
|
|
||||||
};
|
|
||||||
|
|
||||||
var initTimeline = function(options, data, timeExtents) {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var container = $(options.container);
|
|
||||||
|
|
||||||
// http://bl.ocks.org/bunkat/2338034
|
|
||||||
var margin = { top: 20, right: 10, bottom: 10, left: 80 };
|
|
||||||
var width = container.width() - margin.left - margin.right;
|
|
||||||
var height = 550 - margin.top - margin.bottom;
|
|
||||||
|
|
||||||
// filter dstat data immediately. if no timestamps overlap, we want to throw
|
|
||||||
// it away quickly
|
|
||||||
options.dstatData = options.dstatData.slice(
|
|
||||||
binaryMinIndex(timeExtents[0], options.dstatData, function(d) { return d.system_time; }),
|
|
||||||
binaryMaxIndex(timeExtents[1], options.dstatData, function(d) { return d.system_time; })
|
|
||||||
);
|
|
||||||
|
|
||||||
var dstatLanes;
|
|
||||||
if (options.dstatData.length > 2) {
|
|
||||||
dstatLanes = getDstatLanes(
|
|
||||||
options.dstatData,
|
|
||||||
options.dstatMinimums,
|
|
||||||
options.dstatMaximums);
|
|
||||||
} else {
|
|
||||||
dstatLanes = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
var miniHeight = data.length * 12 + 30;
|
|
||||||
var dstatHeight = dstatLanes.length * 30 + 30;
|
|
||||||
var mainHeight = height - miniHeight - dstatHeight - 10;
|
|
||||||
|
|
||||||
var x = d3.time.scale()
|
|
||||||
.range([0, width])
|
|
||||||
.domain(timeExtents);
|
|
||||||
|
|
||||||
var x1 = d3.scale.linear().range([0, width]);
|
|
||||||
|
|
||||||
var y1 = d3.scale.linear()
|
|
||||||
.domain([0, data.length])
|
|
||||||
.range([0, mainHeight]);
|
|
||||||
var y2 = d3.scale.linear()
|
|
||||||
.domain([0, data.length])
|
|
||||||
.range([0, miniHeight]);
|
|
||||||
var y3 = d3.scale.linear()
|
|
||||||
.domain([0, dstatLanes.length])
|
|
||||||
.range([0, dstatHeight]);
|
|
||||||
|
|
||||||
var chart = d3.select(options.container)
|
|
||||||
.append("svg")
|
|
||||||
.attr("width", width + margin.left + margin.right)
|
|
||||||
.attr("height", height + margin.top + margin.bottom)
|
|
||||||
.attr("class", "chart");
|
|
||||||
|
|
||||||
var defs = chart.append("defs")
|
|
||||||
.append("clipPath")
|
|
||||||
.attr("id", "clip")
|
|
||||||
.append("rect")
|
|
||||||
.attr("width", width)
|
|
||||||
.attr("height", mainHeight);
|
|
||||||
|
|
||||||
var main = chart.append("g")
|
|
||||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
|
|
||||||
.attr("width", width)
|
|
||||||
.attr("height", mainHeight)
|
|
||||||
.attr("class", "main");
|
|
||||||
|
|
||||||
var laneLines = main.append("g");
|
|
||||||
var laneLabels = main.append("g");
|
|
||||||
|
|
||||||
var itemGroups = main.append("g");
|
|
||||||
|
|
||||||
var dstatOffset = margin.top + mainHeight;
|
|
||||||
var dstatGroup = chart.append("g")
|
|
||||||
.attr("transform", "translate(" + margin.left + "," + dstatOffset + ")")
|
|
||||||
.attr("width", width)
|
|
||||||
.attr("height", dstatHeight);
|
|
||||||
|
|
||||||
dstatLanes.forEach(function(lane, i) {
|
|
||||||
var laneGroup = dstatGroup.append("g");
|
|
||||||
|
|
||||||
var text = laneGroup.append("text")
|
|
||||||
.attr("y", function(d) { return y3(i + 0.5); })
|
|
||||||
.attr("dy", ".5ex")
|
|
||||||
.attr("text-anchor", "end")
|
|
||||||
.style("font", "10px sans-serif");
|
|
||||||
|
|
||||||
var dy = 0;
|
|
||||||
|
|
||||||
// precompute some known info for each lane's paths
|
|
||||||
lane.forEach(function(pathDef) {
|
|
||||||
var laneHeight = 0.8 * y3(1);
|
|
||||||
|
|
||||||
if ('text' in pathDef) {
|
|
||||||
text.append("tspan")
|
|
||||||
.attr("x", -margin.right)
|
|
||||||
.attr("dy", dy)
|
|
||||||
.text(pathDef.text)
|
|
||||||
.attr("fill", function(d) { return pathDef.color; });
|
|
||||||
|
|
||||||
dy += 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
pathDef.scale.range([laneHeight, 0]);
|
|
||||||
|
|
||||||
pathDef.path = laneGroup.append("path");
|
|
||||||
if (pathDef.type === "line") {
|
|
||||||
pathDef.area = d3.svg.line()
|
|
||||||
.x(function(d) { return x1(d.system_time); })
|
|
||||||
.y(function(d) { return y3(i) + pathDef.scale(pathDef.value(d)); });
|
|
||||||
|
|
||||||
pathDef.path
|
|
||||||
.style("stroke", pathDef.color)
|
|
||||||
.style("stroke-width", "1.5px")
|
|
||||||
.style("fill", "none");
|
|
||||||
//.style("shape-rendering", 'crispEdges');
|
|
||||||
} else {
|
|
||||||
pathDef.area = d3.svg.area()
|
|
||||||
.x(function(d) { return x1(d.system_time); })
|
|
||||||
.y0(function(d) { return y3(i) + laneHeight; })
|
|
||||||
.y1(function(d) {
|
|
||||||
return y3(i) + pathDef.scale(pathDef.value(d));
|
|
||||||
});
|
|
||||||
|
|
||||||
pathDef.path.style("fill", pathDef.color);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var cursorGroup = main.append("g")
|
|
||||||
.style("opacity", 0)
|
|
||||||
.style("pointer-events", "none");
|
|
||||||
|
|
||||||
var cursor = cursorGroup.append("line")
|
|
||||||
.attr("x1", 0)
|
|
||||||
.attr("x2", 0)
|
|
||||||
.attr("y1", y1(-0.1))
|
|
||||||
.attr("stroke", "blue");
|
|
||||||
|
|
||||||
var cursorText = cursorGroup.append("text")
|
|
||||||
.attr("x", 0)
|
|
||||||
.attr("y", -10)
|
|
||||||
.attr("dy", "-.5ex")
|
|
||||||
.text("")
|
|
||||||
.style("text-anchor", "middle")
|
|
||||||
.style("font", "9px sans-serif");
|
|
||||||
|
|
||||||
var miniOffset = margin.top + mainHeight + dstatHeight;
|
|
||||||
var mini = chart.append("g")
|
|
||||||
.attr("transform", "translate(" + margin.left + "," + miniOffset + ")")
|
|
||||||
.attr("width", width)
|
|
||||||
.attr("height", mainHeight)
|
|
||||||
.attr("class", "mini");
|
|
||||||
|
|
||||||
var miniGroups = mini.append("g");
|
|
||||||
|
|
||||||
// performance hack: performance in Firefox as of 39.0 is poor due to some
|
|
||||||
// d3 bugs
|
|
||||||
// Limit the initial selection to ~1/6th of the total to make things
|
|
||||||
// bearable (user can still increase as desired)
|
|
||||||
var start = timeExtents[0];
|
|
||||||
var end = timeExtents[1];
|
|
||||||
var reducedEnd = new Date(start.getTime() + ((end - start) / 8));
|
|
||||||
|
|
||||||
var brush = d3.svg.brush()
|
|
||||||
.x(x)
|
|
||||||
.extent([start, reducedEnd]);
|
|
||||||
|
|
||||||
chart.on("mouseout", function() {
|
|
||||||
cursorGroup.style("opacity", 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
chart.on("mousemove", function() {
|
|
||||||
var pos = d3.mouse(this);
|
|
||||||
var px = pos[0];
|
|
||||||
var py = pos[1];
|
|
||||||
|
|
||||||
if (px >= margin.left && px < (width + margin.left) &&
|
|
||||||
py > margin.top && py < (mainHeight + margin.top)) {
|
|
||||||
var relX = px - margin.left;
|
|
||||||
|
|
||||||
var currentTime = new Date(x1.invert(relX));
|
|
||||||
|
|
||||||
cursorGroup.style("opacity", "0.5");
|
|
||||||
cursorGroup.attr("transform", "translate(" + relX + ", 0)");
|
|
||||||
|
|
||||||
cursorText.text(d3.time.format("%X")(currentTime));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function updateLanes() {
|
|
||||||
var lines = laneLines.selectAll(".laneLine")
|
|
||||||
.data(data, function(d) { return d.key; });
|
|
||||||
|
|
||||||
lines.enter().append("line")
|
|
||||||
.attr("x1", 0)
|
|
||||||
.attr("x2", width)
|
|
||||||
.attr("stroke", "lightgray")
|
|
||||||
.attr("class", "laneLine");
|
|
||||||
|
|
||||||
lines.attr("y1", function(d, i) { return y1(i - 0.1); })
|
|
||||||
.attr("y2", function(d, i) { return y1(i - 0.1); });
|
|
||||||
|
|
||||||
lines.exit().remove();
|
|
||||||
|
|
||||||
var labels = laneLabels.selectAll(".laneLabel")
|
|
||||||
.data(data, function(d) { return d.key; });
|
|
||||||
|
|
||||||
labels.enter().append("text")
|
|
||||||
.text(function(d) { return "Worker #" + d.key; })
|
|
||||||
.attr("x", -margin.right)
|
|
||||||
.attr("dy", ".5ex")
|
|
||||||
.attr("text-anchor", "end")
|
|
||||||
.attr("class", "laneLabel");
|
|
||||||
|
|
||||||
labels.attr("y", function(d, i) { return y1(i + 0.5); });
|
|
||||||
|
|
||||||
labels.exit().remove();
|
|
||||||
|
|
||||||
cursor.attr("y2", y1(data.length - 0.1));
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateItems() {
|
|
||||||
var minExtent = brush.extent()[0];
|
|
||||||
var maxExtent = brush.extent()[1];
|
|
||||||
|
|
||||||
// filter visible items to include only those within the current extent
|
|
||||||
// additionally prune extremely small values to improve performance
|
|
||||||
var visibleItems = data.map(function(group) {
|
|
||||||
return {
|
|
||||||
key: group.key,
|
|
||||||
values: group.values.filter(function(e) {
|
|
||||||
if (x1(e.end_date) - x1(e.start_date) < 2) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.start_date > maxExtent || e.end_date < minExtent) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
var groups = itemGroups.selectAll("g")
|
|
||||||
.data(visibleItems, function(d) { return d.key; });
|
|
||||||
|
|
||||||
groups.enter().append("g");
|
|
||||||
|
|
||||||
var rects = groups.selectAll("rect")
|
|
||||||
.data(function(d) { return d.values; }, function(d) { return d.name; });
|
|
||||||
|
|
||||||
rects.enter().append("rect")
|
|
||||||
.attr("y", function(d) { return y1(parseWorker(d.tags)); })
|
|
||||||
.attr("height", 0.8 * y1(1))
|
|
||||||
.attr("stroke", 'rgba(100, 100, 100, 0.25)')
|
|
||||||
.attr("clip-path", "url(#clip)");
|
|
||||||
|
|
||||||
rects
|
|
||||||
.attr("x", function(d) {
|
|
||||||
return x1(d.start_date);
|
|
||||||
})
|
|
||||||
.attr("width", function(d) {
|
|
||||||
return x1(d.end_date) - x1(d.start_date);
|
|
||||||
})
|
|
||||||
.attr("fill", function(d) { return statusColorMap[d.status]; })
|
|
||||||
.on("mouseover", options.onMouseover)
|
|
||||||
.on("mouseout", options.onMouseout)
|
|
||||||
.on("click", options.onClick);
|
|
||||||
|
|
||||||
rects.exit().remove();
|
|
||||||
groups.exit().remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateDstat() {
|
|
||||||
if (dstatLanes.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var minExtent = brush.extent()[0];
|
|
||||||
var maxExtent = brush.extent()[1];
|
|
||||||
|
|
||||||
var dstat = options.dstatData;
|
|
||||||
var timeFunc = function(d) { return d.system_time; };
|
|
||||||
|
|
||||||
var visibleEntries = dstat.slice(
|
|
||||||
binaryMinIndex(minExtent, dstat, timeFunc),
|
|
||||||
binaryMaxIndex(maxExtent, dstat, timeFunc)
|
|
||||||
);
|
|
||||||
|
|
||||||
// apply the current dataset (visibleEntries) to each dstat path
|
|
||||||
//
|
|
||||||
dstatLanes.forEach(function(lane) {
|
|
||||||
lane.forEach(function(pathDef) {
|
|
||||||
pathDef.path
|
|
||||||
.datum(visibleEntries)
|
|
||||||
.attr("d", pathDef.area);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateMiniItems() {
|
|
||||||
var groups = miniGroups.selectAll("g")
|
|
||||||
.data(data, function(d) { return d.key; });
|
|
||||||
|
|
||||||
groups.enter().append("g");
|
|
||||||
|
|
||||||
var rects = groups.selectAll("rect").data(
|
|
||||||
function(d) { return d.values; },
|
|
||||||
function(d) { return d.name; });
|
|
||||||
|
|
||||||
rects.enter().append("rect")
|
|
||||||
.attr("y", function(d) { return y2(parseWorker(d.tags) + 0.5) - 5; })
|
|
||||||
.attr("height", 10);
|
|
||||||
|
|
||||||
rects.attr("x", function(d) { return x(d.start_date); })
|
|
||||||
.attr("width", function(d) { return x(d.end_date) - x(d.start_date); })
|
|
||||||
.attr("stroke", 'rgba(100, 100, 100, 0.25)')
|
|
||||||
.attr("fill", function(d) { return statusColorMap[d.status]; });
|
|
||||||
|
|
||||||
rects.exit().remove();
|
|
||||||
groups.exit().remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
x1.domain(brush.extent());
|
|
||||||
|
|
||||||
updateLanes();
|
|
||||||
updateItems();
|
|
||||||
updateDstat();
|
|
||||||
}
|
|
||||||
|
|
||||||
brush.on("brush", update);
|
|
||||||
|
|
||||||
mini.append("g")
|
|
||||||
.attr("class", "x brush")
|
|
||||||
.call(brush)
|
|
||||||
.selectAll("rect")
|
|
||||||
.attr("y", 1)
|
|
||||||
.attr("height", miniHeight - 1)
|
|
||||||
.attr("fill", "dodgerblue")
|
|
||||||
.attr("fill-opacity", 0.365);
|
|
||||||
|
|
||||||
updateMiniItems();
|
|
||||||
update();
|
|
||||||
|
|
||||||
$(window).resize(function() {
|
|
||||||
var brushExtent = brush.extent();
|
|
||||||
|
|
||||||
width = container.width() - margin.left - margin.right;
|
|
||||||
x.range([0, width]);
|
|
||||||
x1.range([0, width]);
|
|
||||||
|
|
||||||
chart.attr("width", container.width());
|
|
||||||
defs.attr("width", width);
|
|
||||||
main.attr("width", width);
|
|
||||||
mini.attr("width", width);
|
|
||||||
|
|
||||||
laneLines.selectAll(".laneLine").attr("x2", width);
|
|
||||||
|
|
||||||
brush.extent(brushExtent);
|
|
||||||
|
|
||||||
updateMiniItems();
|
|
||||||
update();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function fillArrayRight(array) {
|
|
||||||
// "fill" the array to the right, overwriting empty values with the next
|
|
||||||
// non-empty value to the left
|
|
||||||
// only false values will be overwritten (e.g. "", null, etc)
|
|
||||||
for (var i = 0; i < array.length - 1; i++) {
|
|
||||||
if (!array[i + 1]) {
|
|
||||||
array[i + 1] = array[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeNames(primary, secondary) {
|
|
||||||
// "zip" together strings in the same position in each array, and do some
|
|
||||||
// basic cleanup of results
|
|
||||||
var ret = [];
|
|
||||||
for (var i = 0; i < primary.length; i++) {
|
|
||||||
ret.push((primary[i] + '_' + secondary[i]).replace(/[ /]/g, '_'));
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function chainLoadDstat(path, yearOverride, callback) {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
d3.text(path, function(error, data) {
|
|
||||||
if (error) {
|
|
||||||
callback([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var primaryNames = null;
|
|
||||||
var secondaryNames = null;
|
|
||||||
var names = null;
|
|
||||||
|
|
||||||
var minimums = {};
|
|
||||||
var maximums = {};
|
|
||||||
|
|
||||||
// assume UTC - may not necessarily be the case?
|
|
||||||
// dstat doesn't include the year in its logs, so we'll need to copy it
|
|
||||||
// from the subunit logs
|
|
||||||
var dateFormat = d3.time.format.utc("%d-%m %H:%M:%S");
|
|
||||||
|
|
||||||
var parsed = d3.csv.parseRows(data, function(row, i) {
|
|
||||||
if (i <= 4) { // header rows - ignore
|
|
||||||
return null;
|
|
||||||
} else if (i == 5) { // primary
|
|
||||||
primaryNames = row;
|
|
||||||
fillArrayRight(primaryNames);
|
|
||||||
return null;
|
|
||||||
} else if (i == 6) { // secondary
|
|
||||||
secondaryNames = row;
|
|
||||||
|
|
||||||
names = mergeNames(primaryNames, secondaryNames);
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
var ret = {};
|
|
||||||
|
|
||||||
for (var col = 0; col < row.length; col++) {
|
|
||||||
var name = names[col];
|
|
||||||
var value = row[col];
|
|
||||||
|
|
||||||
if (name == "system_time") {
|
|
||||||
value = dateFormat.parse(value);
|
|
||||||
value.setFullYear(1900 + yearOverride);
|
|
||||||
} else {
|
|
||||||
value = parseFloat(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(name in minimums) || value < minimums[name]) {
|
|
||||||
minimums[name] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(name in maximums) || value > maximums[name]) {
|
|
||||||
maximums[name] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
ret[name] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
callback(parsed, minimums, maximums);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadTimeline(path, options) { // eslint-disable-line no-unused-vars
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
d3.json(path, function(error, data) {
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
var minStart = null;
|
|
||||||
var maxEnd = null;
|
|
||||||
data.forEach(function(d) {
|
|
||||||
/*eslint-disable camelcase*/
|
|
||||||
d.start_date = new Date(d.timestamps[0]);
|
|
||||||
if (minStart === null || d.start_date < minStart) {
|
|
||||||
minStart = d.start_date;
|
|
||||||
}
|
|
||||||
|
|
||||||
d.end_date = new Date(d.timestamps[1]);
|
|
||||||
if (maxEnd === null || d.end_date > maxEnd) {
|
|
||||||
maxEnd = d.end_date;
|
|
||||||
}
|
|
||||||
/*eslint-enable camelcase*/
|
|
||||||
});
|
|
||||||
|
|
||||||
data = data.filter(function (d) { return d.duration > 0; });
|
|
||||||
|
|
||||||
var nested = d3.nest()
|
|
||||||
.key(function(d) { return parseWorker(d.tags); })
|
|
||||||
.sortKeys(d3.ascending)
|
|
||||||
.entries(data);
|
|
||||||
|
|
||||||
// include dstat if available
|
|
||||||
if (options.dstatPath && !options.dstatData) {
|
|
||||||
var year = data[0].start_date.getYear();
|
|
||||||
chainLoadDstat(options.dstatPath, year, function(data, mins, maxes) {
|
|
||||||
options.dstatData = data;
|
|
||||||
options.dstatMinimums = mins;
|
|
||||||
options.dstatMaximums = maxes;
|
|
||||||
|
|
||||||
initTimeline(options, nested, [ minStart, maxEnd ]);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
initTimeline(options, nested, [ minStart, maxEnd ]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,104 +0,0 @@
|
|||||||
/*
|
|
||||||
* (c) Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
window.addEventListener('load', function() {
|
|
||||||
|
|
||||||
|
|
||||||
//default panel display
|
|
||||||
$("#runs-panel").hide();
|
|
||||||
$("#gerrit-panel").show();
|
|
||||||
$("#run-metadata-panel").hide();
|
|
||||||
|
|
||||||
//dict containing all run_metadata objects, keyed by run_id
|
|
||||||
var RUN_METADATA = {};
|
|
||||||
|
|
||||||
//sets the run metdata of the associated run in the proper div
|
|
||||||
function show_run_metadata(id) {
|
|
||||||
$("#run-metadata-table").html("<thead><th>Key</th><th>Value</th></thead>");
|
|
||||||
var meta = RUN_METADATA[id];
|
|
||||||
for (var i in meta) {
|
|
||||||
var obj = meta[i];
|
|
||||||
var row = $("<tr><td>" + obj['key'] + "</td>" +
|
|
||||||
"<td>" + obj['value'] + "</td></tr>");
|
|
||||||
$("#run-metadata-table").append(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//run_metadata will be queried from subunit2sql when given run_id
|
|
||||||
function get_run_metadata(request, run_id) {
|
|
||||||
$.getJSON((request),function(metadata) {
|
|
||||||
RUN_METADATA[run_id]=metadata;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//Takes a list of runs and creates a pretty div for each
|
|
||||||
function display_runs(data) {
|
|
||||||
$("#runs-panel").show();
|
|
||||||
$("#runs-panel").append("<ul id=\"runs-list\"></ul>");
|
|
||||||
|
|
||||||
for (var i in data) {
|
|
||||||
var run_obj = data[i];
|
|
||||||
//get run_metadata
|
|
||||||
var request = 'upstream_api_run_id_' + run_obj['id'] + '.json';
|
|
||||||
get_run_metadata(request, run_obj['id']);
|
|
||||||
|
|
||||||
var li = $("<li class =\"run-li\" id=\"run-li-" + i + "\" value=\"" + run_obj['id'] + "\"></li>");
|
|
||||||
//on mouseover, show the run_metadata for this run object (li)
|
|
||||||
$(li).hover(
|
|
||||||
function () {
|
|
||||||
$(this).addClass("highlight");
|
|
||||||
show_run_metadata($(this).attr("value"));
|
|
||||||
},
|
|
||||||
function () {
|
|
||||||
$(this).removeClass("highlight");
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
$(li.append("<a href=" + run_obj['artifacts'] + " target=\"_blank\">" + run_obj['artifacts'] + "\n</a>"));
|
|
||||||
$("#runs-list").append(li);
|
|
||||||
$("#runs-panel-heading").html("Displaying " + i + " Runs");
|
|
||||||
}
|
|
||||||
$("#run-metadata-panel").show();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$('#gerrit-id').keypress(function (e) {
|
|
||||||
if (e.which == 13) {
|
|
||||||
$( "#gerrit-id-button" ).click();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
//click triggers the api call that returns the run data
|
|
||||||
$('#gerrit-id-button').click(function() {
|
|
||||||
var request = 'upstream_api_changeid_'+$("#gerrit-id").val()+'.json';
|
|
||||||
$("#runs-panel").append("<a href=\"https://review.openstack.org/" + $("#gerrit-id").val() +
|
|
||||||
"/\" target=\"_blank\"><h2> Change ID: " + $("#gerrit-id").val() + "</h2></a>");
|
|
||||||
$("#gerrit-panel").html("Loading Test Runs...");
|
|
||||||
|
|
||||||
$.getJSON((request),function(data) {
|
|
||||||
$("#gerrit-panel").hide();
|
|
||||||
display_runs(data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
});
|
|
@ -1,14 +0,0 @@
|
|||||||
{% extends 'template.html' %}
|
|
||||||
|
|
||||||
{% block title %}Devstack Latest Results{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12">
|
|
||||||
<h1 class="page-header">Latest Results</h1>
|
|
||||||
</div>
|
|
||||||
<!-- /.col-lg-12 -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@ -1,53 +0,0 @@
|
|||||||
{% extends 'template.html' %}
|
|
||||||
|
|
||||||
{% load staticfiles %}
|
|
||||||
|
|
||||||
{% block title %}Index{% endblock %}
|
|
||||||
|
|
||||||
{% block head-extra %}
|
|
||||||
<!-- Script for summary page-->
|
|
||||||
<script src="{% static 'js/summary.js' %}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12">
|
|
||||||
<h1 class="page-header">Local Run Summary</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">Tempest Runs</div>
|
|
||||||
<div class="panel-body" id="run-summary-div">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
var urlSuffix = "";
|
|
||||||
if ("{{use_gzip}}" === "True") {
|
|
||||||
urlSuffix = ".gz";
|
|
||||||
}
|
|
||||||
|
|
||||||
var tempestRuns = [];
|
|
||||||
|
|
||||||
/* {% for provider in tempest_providers %} begin generated */
|
|
||||||
/* {% for index in provider.indexes %} */
|
|
||||||
tempestRuns.push({
|
|
||||||
provider: "{{provider.name}}",
|
|
||||||
providerDescription: "{{provider.description}}",
|
|
||||||
run: parseInt("{{index}}"),
|
|
||||||
url: "tempest_api_tree_{{provider.name}}_{{index}}.json" + urlSuffix
|
|
||||||
});
|
|
||||||
/* {% endfor %} */
|
|
||||||
/* {% endfor %} end generated */
|
|
||||||
|
|
||||||
window.addEventListener('load', createTables( tempestRuns ));
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@ -1,61 +0,0 @@
|
|||||||
<div class="sidebar-nav navbar-collapse">
|
|
||||||
<ul class="nav" id="side-menu">
|
|
||||||
<li>
|
|
||||||
<a href="index.html"><i class="fa fa-pie-chart fa-fw"></i> Overview</a>
|
|
||||||
</li>
|
|
||||||
<!--<li>
|
|
||||||
<a href="#"><i class="fa fa-bar-chart-o fa-fw"></i> DevStack<span class="fa arrow"></span></a>
|
|
||||||
<ul class="nav nav-second-level">
|
|
||||||
<li>
|
|
||||||
<a href="/devstack/results"><i class="fa fa-clock-o fa-fw"></i> Results</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/devstack/"><i class="fa fa-calendar fa-fw"></i> History</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="/devstack/"><i class="fa fa-database fa-fw"></i> Compare</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
--><!-- /.nav-second-level --><!--
|
|
||||||
</li>-->
|
|
||||||
<li>
|
|
||||||
<a href="#"><i class="fa fa-bar-chart-o fa-fw"></i> Tempest<span class="fa arrow"></span></a>
|
|
||||||
<ul class="nav nav-second-level">
|
|
||||||
<li>
|
|
||||||
<a href="tempest_results_{{ tempest_default_provider.name }}_{{ tempest_default_provider.count | add:'-1' }}.html">
|
|
||||||
<i class="fa fa-clock-o fa-fw"></i> Sunburst
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="tempest_timeline_{{ tempest_default_provider.name }}_{{ tempest_default_provider.count | add:'-1' }}.html">
|
|
||||||
<i class="fa fa-calendar fa-fw"></i> Timeline
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<!--<li>
|
|
||||||
<a href="/tempest/"><i class="fa fa-database fa-fw"></i> Compare</a>
|
|
||||||
</li>-->
|
|
||||||
{% if not offline %}
|
|
||||||
<li class="online">
|
|
||||||
<a href="tempest_aggregate.html"><i class="fa fa-bar-chart-o fa-fw"></i> Aggregate Results</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
<!-- /.nav-second-level -->
|
|
||||||
</li>
|
|
||||||
{% if not offline %}
|
|
||||||
<li class="online">
|
|
||||||
<a href="#"><i class="fa fa-bar-chart-o fa-fw"></i> Upstream<span class="fa arrow"></span></a>
|
|
||||||
<ul class="nav nav-second-level">
|
|
||||||
<li>
|
|
||||||
<a href="upstream_test.html"><i class="fa fa-bar-chart-o fa-fw"></i> Test Stats</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="upstream_run.html"><i class="fa fa-clock-o fa-fw"></i> Run Data</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<!-- /.nav-second-level -->
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<!-- /.sidebar-collapse -->
|
|
@ -1,14 +0,0 @@
|
|||||||
{% extends 'template.html' %}
|
|
||||||
|
|
||||||
{% block title %}Aggregate Results{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12">
|
|
||||||
<h1 class="page-header">Aggregate Tempest Results</h1>
|
|
||||||
</div>
|
|
||||||
<!-- /.col-lg-12 -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@ -1,99 +0,0 @@
|
|||||||
{% extends 'template.html' %}
|
|
||||||
|
|
||||||
{% load staticfiles %}
|
|
||||||
|
|
||||||
{% block title %}Tempest Results{% endblock %}
|
|
||||||
|
|
||||||
{% block head-extra %}
|
|
||||||
<!-- Scripts for visualization-->
|
|
||||||
<script src="{% static 'js/sunburst.js' %}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
|
|
||||||
<div id="details-dialog" title="Details Output">
|
|
||||||
<p>Details not found for test.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12">
|
|
||||||
<h1 class="page-header">Results from Run #{{run_id}}</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-8">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
Runtime Diagram
|
|
||||||
<div class="pull-right">
|
|
||||||
<div class="btn-group">
|
|
||||||
<button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
|
||||||
View Run...
|
|
||||||
<span class="caret"></span>
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu pull-right" role="menu">
|
|
||||||
{% for provider in tempest_providers %}
|
|
||||||
<li class="dropdown-header">{{ provider.description }}</li>
|
|
||||||
{% for index in provider.indexes %}
|
|
||||||
<li><a href="tempest_results_{{ provider.name }}_{{ index }}.html">Run #{{ index }}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<div id="sunburst"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-lg-4">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading" id="table-heading">Test Run Info</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<div class="table-responsive" id="result-table-div">
|
|
||||||
<table class="table table-bordered table-hover table-striped" id="test-table">
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading"><button type="button" id="show-hide-failures">Show/Hide Failures</button></div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<div class="table-responsive" id="failure-table-div">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
|
||||||
window.addEventListener('load', function() {
|
|
||||||
$("#details-dialog").hide();
|
|
||||||
var url = "tempest_api_tree_{{provider_name}}_{{run_id}}.json";
|
|
||||||
if ("{{use_gzip}}" === "True") {
|
|
||||||
url += ".gz";
|
|
||||||
}
|
|
||||||
|
|
||||||
createSunburst( url, "{{provider_name}}", {{run_id}} );
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
$(document).ready(function(){
|
|
||||||
$("#show-hide-failures").click(function() {
|
|
||||||
$("#failure-table-div").toggle();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@ -1,269 +0,0 @@
|
|||||||
{% extends 'template.html' %}
|
|
||||||
{% load staticfiles %}
|
|
||||||
|
|
||||||
{% block title %}Tempest: Execution Timeline (run #{{run_id}}){% endblock %}
|
|
||||||
|
|
||||||
{% block head-extra %}
|
|
||||||
<style>
|
|
||||||
#timeline-info table {
|
|
||||||
table-layout: fixed;
|
|
||||||
width: 100%;
|
|
||||||
word-wrap: break-word
|
|
||||||
}
|
|
||||||
|
|
||||||
#timeline-info table td:nth-child(1) {
|
|
||||||
width: 20%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#timeline-log pre {
|
|
||||||
overflow-x: scroll;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<div id="details-dialog" title="Details Output">
|
|
||||||
<p>Details not found for test.</p>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12">
|
|
||||||
<h1 class="page-header">Execution Timeline ({{provider_name}}, run #{{run_id}})</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-8">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<i class="fa fa-clock-o fa-fw"></i> Timeline
|
|
||||||
<div class="pull-right">
|
|
||||||
<div class="btn-group">
|
|
||||||
<button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
|
|
||||||
View Run...
|
|
||||||
<span class="caret"></span>
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu pull-right" role="menu">
|
|
||||||
{% for provider in tempest_providers %}
|
|
||||||
<li class="dropdown-header">{{ provider.description }}</li>
|
|
||||||
{% for index in provider.indexes %}
|
|
||||||
<li><a href="tempest_timeline_{{ provider.name }}_{{ index }}.html">Run #{{ index }}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="timeline-container" class="panel-body">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-4">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<i class="fa fa-info fa-fw"></i> Info
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="timeline-info" class="panel-body">
|
|
||||||
<em>Mouse over an item to view info.</em>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="{% static 'js/timeline.js' %}"></script>
|
|
||||||
<script>
|
|
||||||
var originalInfoContent = null;
|
|
||||||
var originalDetailsContent = null;
|
|
||||||
|
|
||||||
var detailsCache = null;
|
|
||||||
var detailsInProgress = false;
|
|
||||||
var detailsWaiting = [];
|
|
||||||
|
|
||||||
var showInfo = function(item) {
|
|
||||||
var parent = $("#timeline-info");
|
|
||||||
if (originalInfoContent === null) {
|
|
||||||
originalInfoContent = parent.html();
|
|
||||||
}
|
|
||||||
|
|
||||||
var e = $("<table>", {
|
|
||||||
"class": 'table table-bordered table-hover table-striped'
|
|
||||||
});
|
|
||||||
|
|
||||||
var nameParts = item.name.split(".");
|
|
||||||
var pkg = nameParts.slice(0, nameParts.length - 2).join('.');
|
|
||||||
|
|
||||||
e.append($("<tr>")
|
|
||||||
.append($("<td>", { text: 'Name' }))
|
|
||||||
.append($("<td>", { text: nameParts[nameParts.length - 1] })));
|
|
||||||
|
|
||||||
e.append($("<tr>")
|
|
||||||
.append($("<td>", { text: 'Class' }))
|
|
||||||
.append($("<td>", { text: nameParts[nameParts.length - 2] })));
|
|
||||||
|
|
||||||
e.append($("<tr>")
|
|
||||||
.append($("<td>", { text: 'Module' }))
|
|
||||||
.append($("<td>", { text: pkg })));
|
|
||||||
|
|
||||||
e.append($("<tr>")
|
|
||||||
.append($("<td>", { text: 'Status' }))
|
|
||||||
.append($("<td>", { text: item.status })));
|
|
||||||
|
|
||||||
e.append($("<tr>")
|
|
||||||
.append($("<td>", { text: 'Tags' }))
|
|
||||||
.append($("<td>", { text: item.tags.join(", ") })));
|
|
||||||
|
|
||||||
e.append($("<tr>")
|
|
||||||
.append($("<td>", { text: 'Duration' }))
|
|
||||||
.append($("<td>", { text: item.duration + " seconds" })));
|
|
||||||
|
|
||||||
parent.empty();
|
|
||||||
e.appendTo(parent);
|
|
||||||
|
|
||||||
addDialogButton(parent);
|
|
||||||
};
|
|
||||||
|
|
||||||
var hideInfo = function() {
|
|
||||||
$("#timeline-info").html(originalInfoContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
var loadDetails = function(callback) {
|
|
||||||
if (detailsCache === null) {
|
|
||||||
detailsWaiting.push(callback);
|
|
||||||
|
|
||||||
if (!detailsInProgress) {
|
|
||||||
var url = "tempest_api_details_{{provider_name}}_{{run_id}}.json";
|
|
||||||
if ("{{use_gzip}}" === "True") {
|
|
||||||
url += ".gz";
|
|
||||||
}
|
|
||||||
|
|
||||||
detailsInProgress = true;
|
|
||||||
|
|
||||||
d3.json(url, function(error, data) {
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
detailsCache = data;
|
|
||||||
detailsWaiting.forEach(function(cb) {
|
|
||||||
cb(detailsCache);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
callback(detailsCache);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var showDetails = function(item) {
|
|
||||||
var parent = $("#details-dialog");
|
|
||||||
|
|
||||||
showInfo(item);
|
|
||||||
|
|
||||||
loadDetails(function(details) {
|
|
||||||
if (!details.hasOwnProperty(item.name)) {
|
|
||||||
console.log("Details not found for item:", item.name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (originalDetailsContent === null) {
|
|
||||||
originalDetailsContent = parent.html();
|
|
||||||
}
|
|
||||||
|
|
||||||
parent.empty();
|
|
||||||
for (var prop in details[item.name]) {
|
|
||||||
$("<h3>").text(prop).appendTo(parent);
|
|
||||||
$("<pre>").text(details[item.name][prop]).appendTo(parent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var hideDetails = function() {
|
|
||||||
$("#timeline-details").html(originalDetailsContent);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('load', function() {
|
|
||||||
$("#details-dialog").hide();
|
|
||||||
var selectedItem = null;
|
|
||||||
var selectedValue = null;
|
|
||||||
|
|
||||||
var url = "tempest_api_raw_{{provider_name}}_{{run_id}}.json";
|
|
||||||
if ("{{use_gzip}}" === "True") {
|
|
||||||
url += ".gz";
|
|
||||||
}
|
|
||||||
|
|
||||||
var dstatUrl = "dstat_log.csv";
|
|
||||||
if ("{{use_gzip}}" === "True") {
|
|
||||||
dstatUrl += ".gz";
|
|
||||||
}
|
|
||||||
|
|
||||||
loadTimeline(url, {
|
|
||||||
dstatPath: dstatUrl,
|
|
||||||
container: $("#timeline-container")[0],
|
|
||||||
onClick: function(d) {
|
|
||||||
var self = d3.select(this);
|
|
||||||
|
|
||||||
// deselect old item, if any
|
|
||||||
if (selectedItem !== null) {
|
|
||||||
if (selectedItem.attr("data-old-fill")) {
|
|
||||||
selectedItem.attr("fill", selectedItem.attr("data-old-fill"));
|
|
||||||
selectedItem.attr("data-old-fill", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedValue.name === d.name) {
|
|
||||||
// remove selection on 2nd click - don't continue
|
|
||||||
selectedItem = null;
|
|
||||||
selectedValue = null;
|
|
||||||
hideDetails();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedItem = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// select new item
|
|
||||||
if (!self.attr("data-old-fill")) {
|
|
||||||
self.attr("data-old-fill", self.attr("fill"));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.attr("fill", "goldenrod");
|
|
||||||
selectedItem = self;
|
|
||||||
selectedValue = d;
|
|
||||||
|
|
||||||
showDetails(d);
|
|
||||||
},
|
|
||||||
|
|
||||||
onMouseover: function(d) {
|
|
||||||
if (selectedItem !== null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var self = d3.select(this);
|
|
||||||
if (!self.attr("data-old-fill")) {
|
|
||||||
self.attr("data-old-fill", self.attr("fill"));
|
|
||||||
}
|
|
||||||
|
|
||||||
d3.select(this).attr("fill", "darkturquoise");
|
|
||||||
|
|
||||||
showInfo(d);
|
|
||||||
},
|
|
||||||
|
|
||||||
onMouseout: function(d) {
|
|
||||||
if (selectedItem !== null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var self = d3.select(this);
|
|
||||||
if (self.attr("data-old-fill")) {
|
|
||||||
self.attr("fill", self.attr("data-old-fill"));
|
|
||||||
self.attr("data-old-fill", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
hideInfo();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
@ -1,114 +0,0 @@
|
|||||||
{% load staticfiles %}
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<meta name="description" content="">
|
|
||||||
<meta name="author" content="">
|
|
||||||
|
|
||||||
<title>{% block title %}{% endblock %}</title>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Bootstrap Core CSS -->
|
|
||||||
<link href="{% static 'components/bootstrap/dist/css/bootstrap.min.css' %}" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- MetisMenu CSS -->
|
|
||||||
<link href="{% static 'components/metisMenu/dist/metisMenu.min.css' %}" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- Timeline CSS -->
|
|
||||||
<link href="{% static 'css/timeline.css' %}" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- Custom CSS -->
|
|
||||||
<link href="{% static 'css/sb-admin-2.css' %}" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- Morris Charts CSS -->
|
|
||||||
<link href="{% static 'components/morrisjs/morris.css' %}" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- Custom Fonts -->
|
|
||||||
<link href="{% static 'components/font-awesome/css/font-awesome.min.css' %}" rel="stylesheet" type="text/css">
|
|
||||||
|
|
||||||
<!-- DataTables CSS -->
|
|
||||||
<link href="{% static 'components/datatables/media/css/jquery.dataTables.min.css' %}" rel="stylesheet" type="text/css">
|
|
||||||
|
|
||||||
<!-- jQueryUI CSS -->
|
|
||||||
<link href="{%static 'components/jquery-ui/themes/base/jquery-ui.css' %}" rel="stylesheet" type="text/css">
|
|
||||||
|
|
||||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
|
||||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
|
||||||
<!--[if lt IE 9]>
|
|
||||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
|
||||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
|
|
||||||
<![endif]-->
|
|
||||||
|
|
||||||
<!-- jQuery -->
|
|
||||||
<script src="{% static 'components/jquery/dist/jquery.min.js' %}"></script>
|
|
||||||
|
|
||||||
<!-- Bootstrap Core JavaScript -->
|
|
||||||
<script src="{% static 'components/bootstrap/dist/js/bootstrap.min.js' %}"></script>
|
|
||||||
|
|
||||||
<!-- Metis Menu Plugin JavaScript -->
|
|
||||||
<script src="{% static 'components/metisMenu/dist/metisMenu.min.js' %}"></script>
|
|
||||||
|
|
||||||
<!-- Custom Theme JavaScript -->
|
|
||||||
<script src="{% static 'js/sb-admin-2.js' %}"></script>
|
|
||||||
|
|
||||||
<!-- d3.js -->
|
|
||||||
<script src="{% static 'components/d3/d3.js' %}"></script>
|
|
||||||
|
|
||||||
<!-- DataTable for jQuery -->
|
|
||||||
<script src="{% static 'components/datatables/media/js/jquery.dataTables.min.js' %}"></script>
|
|
||||||
|
|
||||||
<!-- jQueryUI -->
|
|
||||||
<script src="{% static 'components/jquery-ui/jquery-ui.js' %}"></script>
|
|
||||||
<script src="{% static 'js/log-dialog.js' %}"></script>
|
|
||||||
|
|
||||||
{% block head-extra %}{% endblock %}
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div id="wrapper">
|
|
||||||
|
|
||||||
<!-- Navigation -->
|
|
||||||
<nav class="navbar navbar-default navbar-static-top" role="navigation" style="margin-bottom: 0">
|
|
||||||
<div class="navbar-header">
|
|
||||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
|
|
||||||
<span class="sr-only">Toggle navigation</span>
|
|
||||||
<span class="icon-bar"></span>
|
|
||||||
<span class="icon-bar"></span>
|
|
||||||
<span class="icon-bar"></span>
|
|
||||||
</button>
|
|
||||||
<a class="navbar-brand" href="index.html">StackViz</a>
|
|
||||||
</div>
|
|
||||||
<!-- /.navbar-header -->
|
|
||||||
|
|
||||||
<ul class="nav navbar-top-links navbar-right">
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
<!-- /.navbar-top-links -->
|
|
||||||
|
|
||||||
<div class="navbar-default sidebar" role="navigation">
|
|
||||||
{% include 'menu.html' %}
|
|
||||||
</div>
|
|
||||||
<!-- /.navbar-static-side -->
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div id="page-wrapper">
|
|
||||||
{% block body %}{% endblock %}
|
|
||||||
</div>
|
|
||||||
<!-- /#page-wrapper -->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<!-- /#wrapper -->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
@ -1,49 +0,0 @@
|
|||||||
{% extends 'template.html' %}
|
|
||||||
{% load staticfiles %}
|
|
||||||
|
|
||||||
{% block title %}Upstream Run Metadata{% endblock %}
|
|
||||||
|
|
||||||
{% block head-extra %}
|
|
||||||
<script src="{% static 'js/upstream_run.js' %}"></script>
|
|
||||||
<link href="{% static 'css/upstream_run.css' %}" rel="stylesheet" type="text/css">
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12">
|
|
||||||
<h1 class="page-header">Upstream Run Data</h1>
|
|
||||||
</div>
|
|
||||||
<!-- /.col-lg-12 -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel panel-heading" id="runs-panel-heading">Analyze Run</div>
|
|
||||||
<div class="panel panel-body">
|
|
||||||
<div id="gerrit-panel">
|
|
||||||
Enter a Gerrit Change ID (six-digit): <input type="text" id="gerrit-id">
|
|
||||||
<input id="gerrit-id-button" type="button" value="Submit">
|
|
||||||
</div>
|
|
||||||
<div id="runs-panel"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<div class="panel panel-default" id="run-metadata-panel">
|
|
||||||
<div class="panel panel-heading">Run Metadata</div>
|
|
||||||
<div class="panel panel-body">
|
|
||||||
<div class="table-responsive" id="run-metadata-table-div">
|
|
||||||
<table class="table table-bordered table-hover table-striped" id="run-metadata-table"></table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@ -1,30 +0,0 @@
|
|||||||
{% extends 'template.html' %}
|
|
||||||
|
|
||||||
{% block title %}Upstream Test Stats{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-12">
|
|
||||||
<h1 class="page-header">Upstream Test Stats</h1>
|
|
||||||
</div>
|
|
||||||
<!-- /.col-lg-12 -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-8">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel panel-heading" id="runs-panel-heading">Compare Tests</div>
|
|
||||||
<div class="panel panel-body">
|
|
||||||
<div class="col-lg-4">
|
|
||||||
<div id="test-1" class="span6">Test</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-4">
|
|
||||||
<div id="test-avg" class="span6">Avg</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@ -1,23 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright 2010-2011 OpenStack Foundation
|
|
||||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from oslotest import base
|
|
||||||
|
|
||||||
|
|
||||||
class TestCase(base.BaseTestCase):
|
|
||||||
|
|
||||||
"""Test case base class for all unit tests."""
|
|
@ -1,30 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
"""
|
|
||||||
test_stackviz
|
|
||||||
----------------------------------
|
|
||||||
|
|
||||||
Tests for `stackviz` module.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from stackviz.tests import base
|
|
||||||
|
|
||||||
|
|
||||||
class TestStackviz(base.TestCase):
|
|
||||||
|
|
||||||
def test_something(self):
|
|
||||||
pass
|
|
@ -1,29 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.conf.urls import include
|
|
||||||
from django.conf.urls import patterns
|
|
||||||
from django.conf.urls import url
|
|
||||||
|
|
||||||
from stackviz.views.index import IndexView
|
|
||||||
|
|
||||||
urlpatterns = patterns(
|
|
||||||
'',
|
|
||||||
url(r'^$', IndexView.as_view()),
|
|
||||||
url(r'^index.html$', IndexView.as_view(), name="index"),
|
|
||||||
url(r'^tempest_', include('stackviz.views.tempest.urls')),
|
|
||||||
url(r'^devstack_', include('stackviz.views.devstack.urls')),
|
|
||||||
url(r'^upstream_', include('stackviz.views.upstream.urls')),
|
|
||||||
url(r'^dstat_', include('stackviz.views.dstat.urls'))
|
|
||||||
)
|
|
@ -1,19 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.views.generic import TemplateView
|
|
||||||
|
|
||||||
|
|
||||||
class ResultsView(TemplateView):
|
|
||||||
template_name = 'devstack/results.html'
|
|
@ -1,23 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.conf.urls import patterns
|
|
||||||
from django.conf.urls import url
|
|
||||||
|
|
||||||
from stackviz.views.devstack.results import ResultsView
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
|
||||||
url(r'^results$', ResultsView.as_view()),
|
|
||||||
)
|
|
@ -1 +0,0 @@
|
|||||||
__author__ = 'tim'
|
|
@ -1,47 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.http import Http404
|
|
||||||
from django.http import HttpResponse
|
|
||||||
|
|
||||||
from django.views.generic import View
|
|
||||||
|
|
||||||
from stackviz import settings
|
|
||||||
|
|
||||||
_cached_csv = None
|
|
||||||
|
|
||||||
|
|
||||||
def _load_csv():
|
|
||||||
global _cached_csv
|
|
||||||
|
|
||||||
if _cached_csv:
|
|
||||||
return _cached_csv
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(settings.DSTAT_CSV, 'r') as f:
|
|
||||||
_cached_csv = f.readlines()
|
|
||||||
return _cached_csv
|
|
||||||
except IOError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class DStatCSVEndpoint(View):
|
|
||||||
def get(self, request):
|
|
||||||
csv = _load_csv()
|
|
||||||
|
|
||||||
if not csv:
|
|
||||||
raise Http404("DStat log could not be loaded at path %s"
|
|
||||||
% settings.DSTAT_CSV)
|
|
||||||
|
|
||||||
return HttpResponse(csv, content_type="text/csv")
|
|
@ -1,21 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.conf.urls import patterns
|
|
||||||
from django.conf.urls import url
|
|
||||||
|
|
||||||
from api import DStatCSVEndpoint
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns('', url(r'^log.csv$', DStatCSVEndpoint.as_view()))
|
|
@ -1,19 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.views.generic import TemplateView
|
|
||||||
|
|
||||||
|
|
||||||
class IndexView(TemplateView):
|
|
||||||
template_name = 'index.html'
|
|
@ -1,19 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.views.generic import TemplateView
|
|
||||||
|
|
||||||
|
|
||||||
class AggregateResultsView(TemplateView):
|
|
||||||
template_name = 'tempest/aggregate.html'
|
|
@ -1,136 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.http import Http404
|
|
||||||
from restless.views import Endpoint
|
|
||||||
|
|
||||||
from stackviz.parser.tempest_subunit import convert_stream
|
|
||||||
from stackviz.parser.tempest_subunit import get_providers
|
|
||||||
from stackviz.parser.tempest_subunit import reorganize
|
|
||||||
|
|
||||||
#: Cached results from loaded subunit logs indexed by their run number
|
|
||||||
_cached_run = {}
|
|
||||||
|
|
||||||
#: Cached results converted into tree form
|
|
||||||
_cached_tree = {}
|
|
||||||
|
|
||||||
#: Cached results for loaded subunit logs without details stripped out. Indexed
|
|
||||||
#: initially by log number, but contains nested dicts indexed by the test name.
|
|
||||||
_cached_details = {}
|
|
||||||
|
|
||||||
|
|
||||||
class NoRunDataException(Http404):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ProviderNotFoundException(Http404):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class RunNotFoundException(Http404):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TestNotFoundException(Http404):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _load_run(provider_name, run_id):
|
|
||||||
if (provider_name, run_id) in _cached_run:
|
|
||||||
return _cached_run[provider_name, run_id]
|
|
||||||
|
|
||||||
providers = get_providers()
|
|
||||||
if not providers:
|
|
||||||
raise NoRunDataException("No test providers could be loaded")
|
|
||||||
|
|
||||||
if provider_name not in providers:
|
|
||||||
raise ProviderNotFoundException("Requested subunit provider could not "
|
|
||||||
"be found")
|
|
||||||
|
|
||||||
p = providers[provider_name]
|
|
||||||
|
|
||||||
try:
|
|
||||||
# assume first repo for now
|
|
||||||
stream = p.get_stream(run_id)
|
|
||||||
|
|
||||||
# strip details for now
|
|
||||||
# TODO(provide method for getting details on demand)
|
|
||||||
# (preferably for individual tests to avoid bloat)
|
|
||||||
converted_run = convert_stream(stream, strip_details=True)
|
|
||||||
_cached_run[provider_name, run_id] = converted_run
|
|
||||||
|
|
||||||
return converted_run
|
|
||||||
except KeyError:
|
|
||||||
raise RunNotFoundException("Requested test run could not be found")
|
|
||||||
|
|
||||||
|
|
||||||
def _load_tree(provider, run_id):
|
|
||||||
if (provider, run_id) in _cached_tree:
|
|
||||||
return _cached_tree[provider, run_id]
|
|
||||||
|
|
||||||
run = _load_run(provider, run_id)
|
|
||||||
tree = reorganize(run)
|
|
||||||
|
|
||||||
_cached_tree[provider, run_id] = tree
|
|
||||||
return tree
|
|
||||||
|
|
||||||
|
|
||||||
def _load_details(provider_name, run_id, test_name):
|
|
||||||
if (provider_name, run_id) not in _cached_details:
|
|
||||||
providers = get_providers()
|
|
||||||
if not providers:
|
|
||||||
raise NoRunDataException("No test providers could be loaded")
|
|
||||||
|
|
||||||
if provider_name not in providers:
|
|
||||||
raise ProviderNotFoundException("Requested subunit provider could "
|
|
||||||
"not be found: " + provider_name)
|
|
||||||
|
|
||||||
provider = providers[provider_name]
|
|
||||||
try:
|
|
||||||
stream = provider.get_stream(run_id)
|
|
||||||
converted_run = convert_stream(stream, strip_details=False)
|
|
||||||
|
|
||||||
# remap dict to allow direct access to details via test name
|
|
||||||
dest = {}
|
|
||||||
for entry in converted_run:
|
|
||||||
dest[entry['name']] = entry['details']
|
|
||||||
|
|
||||||
_cached_details[provider_name, run_id] = dest
|
|
||||||
except (KeyError, IndexError):
|
|
||||||
raise RunNotFoundException("Requested test run could not be found")
|
|
||||||
|
|
||||||
details_map = _cached_details[provider_name, run_id]
|
|
||||||
if test_name is None:
|
|
||||||
return details_map
|
|
||||||
else:
|
|
||||||
if test_name in details_map:
|
|
||||||
return details_map[test_name]
|
|
||||||
else:
|
|
||||||
raise TestNotFoundException(
|
|
||||||
"Requested test could not be found in run")
|
|
||||||
|
|
||||||
|
|
||||||
class TempestRunRawEndpoint(Endpoint):
|
|
||||||
def get(self, request, provider_name, run_id):
|
|
||||||
return _load_run(provider_name, int(run_id))
|
|
||||||
|
|
||||||
|
|
||||||
class TempestRunTreeEndpoint(Endpoint):
|
|
||||||
def get(self, request, provider_name, run_id):
|
|
||||||
return _load_tree(provider_name, int(run_id))
|
|
||||||
|
|
||||||
|
|
||||||
class TempestRunDetailsEndpoint(Endpoint):
|
|
||||||
def get(self, request, run_id, provider_name, test_name=None):
|
|
||||||
return _load_details(provider_name, int(run_id), test_name)
|
|
@ -1,26 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.views.generic import TemplateView
|
|
||||||
|
|
||||||
|
|
||||||
class ResultsView(TemplateView):
|
|
||||||
template_name = 'tempest/results.html'
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(ResultsView, self).get_context_data(**kwargs)
|
|
||||||
context['provider_name'] = self.kwargs['provider_name']
|
|
||||||
context['run_id'] = self.kwargs['run_id']
|
|
||||||
|
|
||||||
return context
|
|
@ -1,26 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.views.generic import TemplateView
|
|
||||||
|
|
||||||
|
|
||||||
class TimelineView(TemplateView):
|
|
||||||
template_name = 'tempest/timeline.html'
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super(TimelineView, self).get_context_data(**kwargs)
|
|
||||||
context['provider_name'] = self.kwargs['provider_name']
|
|
||||||
context['run_id'] = self.kwargs['run_id']
|
|
||||||
|
|
||||||
return context
|
|
@ -1,52 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
|
|
||||||
from django.conf.urls import patterns
|
|
||||||
from django.conf.urls import url
|
|
||||||
|
|
||||||
from aggregate import AggregateResultsView
|
|
||||||
from results import ResultsView
|
|
||||||
from timeline import TimelineView
|
|
||||||
|
|
||||||
from api import TempestRunDetailsEndpoint
|
|
||||||
from api import TempestRunRawEndpoint
|
|
||||||
from api import TempestRunTreeEndpoint
|
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns(
|
|
||||||
'',
|
|
||||||
url(r'^results_(?P<provider_name>[\w_\.]+)_(?P<run_id>\d+).html$',
|
|
||||||
ResultsView.as_view(),
|
|
||||||
name='tempest_results'),
|
|
||||||
url(r'^timeline_(?P<provider_name>[\w_\.]+)_(?P<run_id>\d+).html$',
|
|
||||||
TimelineView.as_view(),
|
|
||||||
name='tempest_timeline'),
|
|
||||||
|
|
||||||
url(r'^api_tree_(?P<provider_name>[\w_\.]+)_(?P<run_id>\d+).json$',
|
|
||||||
TempestRunTreeEndpoint.as_view(),
|
|
||||||
name='tempest_api_tree'),
|
|
||||||
url(r'^api_raw_(?P<provider_name>[\w_\.]+)_(?P<run_id>\d+).json$',
|
|
||||||
TempestRunRawEndpoint.as_view(),
|
|
||||||
name='tempest_api_raw'),
|
|
||||||
url(r'^api_details_(?P<provider_name>[\w_\.]+)_(?P<run_id>\d+).json$',
|
|
||||||
TempestRunDetailsEndpoint.as_view()),
|
|
||||||
url(r'^api_details_(?P<provider_name>[\w_\.]+)_(?P<run_id>\d+)_'
|
|
||||||
r'(?P<test_name>[^/]+).json$',
|
|
||||||
TempestRunDetailsEndpoint.as_view()),
|
|
||||||
|
|
||||||
url(r'^aggregate.html$',
|
|
||||||
AggregateResultsView.as_view(),
|
|
||||||
name='tempest_aggregate_results'),
|
|
||||||
)
|
|
@ -1,76 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from restless.views import Endpoint
|
|
||||||
|
|
||||||
from subunit2sql.db import api
|
|
||||||
|
|
||||||
from sqlalchemy import create_engine
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
|
|
||||||
|
|
||||||
def _get_runs(change_id):
|
|
||||||
"""Returns the dict of run objects associated with a changeID
|
|
||||||
|
|
||||||
When given the change_id of a Gerrit change, a connection will be made to
|
|
||||||
the upstream subunit2sql db and query all run meta having that change_id
|
|
||||||
:param change_id: the Gerrit change_id to query
|
|
||||||
:return: a json dict of run_meta objects
|
|
||||||
"""
|
|
||||||
|
|
||||||
engine = create_engine('mysql://query:query@logstash.openstack.org' +
|
|
||||||
':3306/subunit2sql')
|
|
||||||
Session = sessionmaker(bind=engine)
|
|
||||||
session = Session()
|
|
||||||
|
|
||||||
list_of_runs = api.get_runs_by_key_value(key="build_change",
|
|
||||||
value=change_id,
|
|
||||||
session=session)
|
|
||||||
ret_list = []
|
|
||||||
|
|
||||||
for run in list_of_runs:
|
|
||||||
ret_list.append(run.to_dict())
|
|
||||||
|
|
||||||
return ret_list
|
|
||||||
|
|
||||||
def _get_metadata(run_id):
|
|
||||||
"""Returns a dict of run_metadata objects associated with a run_id
|
|
||||||
|
|
||||||
:param run_id:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
|
|
||||||
engine = create_engine('mysql://query:query@logstash.openstack.org' +
|
|
||||||
':3306/subunit2sql')
|
|
||||||
Session = sessionmaker(bind=engine)
|
|
||||||
session = Session()
|
|
||||||
|
|
||||||
metadata = api.get_run_metadata(run_id,session=session)
|
|
||||||
ret_list = []
|
|
||||||
|
|
||||||
for meta in metadata:
|
|
||||||
ret_list.append(meta.to_dict())
|
|
||||||
|
|
||||||
return ret_list
|
|
||||||
|
|
||||||
|
|
||||||
class GerritURLEndpoint(Endpoint):
|
|
||||||
|
|
||||||
def get(self, request, change_id):
|
|
||||||
return _get_runs(change_id)
|
|
||||||
|
|
||||||
class RunMetadataEndpoint(Endpoint):
|
|
||||||
|
|
||||||
def get(self, request, run_id):
|
|
||||||
return _get_metadata(run_id)
|
|
@ -1,19 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.views.generic import TemplateView
|
|
||||||
|
|
||||||
|
|
||||||
class RunView(TemplateView):
|
|
||||||
template_name = 'upstream/run.html'
|
|
@ -1,23 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.views.generic import TemplateView
|
|
||||||
|
|
||||||
# TODO(Planned functionality)
|
|
||||||
# Compare one specific test against its moving average
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
class TestView(TemplateView):
|
|
||||||
template_name = 'upstream/test.html'
|
|
@ -1,40 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from django.conf.urls import patterns
|
|
||||||
from django.conf.urls import url
|
|
||||||
|
|
||||||
from run import RunView
|
|
||||||
from test import TestView
|
|
||||||
|
|
||||||
from api import GerritURLEndpoint
|
|
||||||
from api import RunMetadataEndpoint
|
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
|
||||||
url(r'^run.html$',
|
|
||||||
RunView.as_view(),
|
|
||||||
name='run_metadata'),
|
|
||||||
|
|
||||||
url(r'^test.html$',
|
|
||||||
TestView.as_view(),
|
|
||||||
name='test_data'),
|
|
||||||
|
|
||||||
url(r'^api_changeid_(?P<change_id>\d+).json$',
|
|
||||||
GerritURLEndpoint.as_view(),
|
|
||||||
name='gerrit_url'),
|
|
||||||
|
|
||||||
url(r'^api_run_id_(?P<run_id>[a-zA-Z0-9!$* \t\r\n\-]+).json$',
|
|
||||||
RunMetadataEndpoint.as_view(),
|
|
||||||
name='run_metadata_url')
|
|
||||||
)
|
|
@ -1,28 +0,0 @@
|
|||||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
"""
|
|
||||||
WSGI config for stackviz project.
|
|
||||||
|
|
||||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
|
||||||
|
|
||||||
For more information on this file, see
|
|
||||||
https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "stackviz.settings")
|
|
||||||
|
|
||||||
from django.core.wsgi import get_wsgi_application
|
|
||||||
application = get_wsgi_application()
|
|
Loading…
Reference in New Issue
Block a user