Removing django code, not needed

This commit is contained in:
David Lyle 2015-09-14 13:56:20 -06:00
parent e919380739
commit c458d84097
50 changed files with 0 additions and 4256 deletions

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -1,3 +0,0 @@
.highlight {
font-weight: bold;
}

View File

@ -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" );
}
}
]
});
}

View File

@ -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');
}
});

View File

@ -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);
});
})
}

View File

@ -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); };
};
}
}

View File

@ -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 ]);
}
});
}

View File

@ -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);
});
});
});

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 -->

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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."""

View File

@ -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

View File

@ -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'))
)

View File

@ -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'

View File

@ -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()),
)

View File

@ -1 +0,0 @@
__author__ = 'tim'

View File

@ -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")

View File

@ -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()))

View File

@ -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'

View File

@ -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'

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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'),
)

View File

@ -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)

View File

@ -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'

View File

@ -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'

View File

@ -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')
)

View File

@ -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()