提交 6bb1f730 编写于 作者: C Cleber Rosa

Merge remote-tracking branch 'lmr/html-reports_v2'

include README.rst include README.rst
include LICENSE include LICENSE
recursive-include avocado/plugins/resources
...@@ -32,9 +32,24 @@ these days a framework) to perform automated testing. ...@@ -32,9 +32,24 @@ these days a framework) to perform automated testing.
%dir /etc/avocado %dir /etc/avocado
%config(noreplace)/etc/avocado/settings.ini %config(noreplace)/etc/avocado/settings.ini
%{_bindir}/avocado %{_bindir}/avocado
%exclude %{python_sitelib}/avocado/plugins/htmlresult.py*
%exclude %{python_sitelib}/avocado/plugins/resources/htmlresult/*
%{python_sitelib}/avocado* %{python_sitelib}/avocado*
%{_mandir}/man1/avocado.1.gz %{_mandir}/man1/avocado.1.gz
%package plugins-output-html
Summary: Avocado HTML report plugin
Requires: avocado, pystache
%description plugins-output-html
Adds to avocado the ability to generate an HTML report at every job results
directory. It also gives the user the ability to write a report on an
arbitrary filesystem location.
%files plugins-output-html
%{python_sitelib}/avocado/plugins/htmlresult.py*
%{python_sitelib}/avocado/plugins/resources/htmlresult/*
%package examples %package examples
Summary: Avocado Test Framework Example Tests Summary: Avocado Test Framework Example Tests
Requires: avocado Requires: avocado
......
...@@ -43,6 +43,11 @@ from avocado import sysinfo ...@@ -43,6 +43,11 @@ from avocado import sysinfo
from avocado import runtime from avocado import runtime
from avocado.plugins import xunit from avocado.plugins import xunit
from avocado.plugins import jsonresult from avocado.plugins import jsonresult
try:
from avocado.plugins import htmlresult
HTML_REPORT_SUPPORT = True
except ImportError:
HTML_REPORT_SUPPORT = False
_NEW_ISSUE_LINK = 'https://github.com/avocado-framework/avocado/issues/new' _NEW_ISSUE_LINK = 'https://github.com/avocado-framework/avocado/issues/new'
...@@ -388,6 +393,19 @@ class Job(object): ...@@ -388,6 +393,19 @@ class Job(object):
json_plugin = jsonresult.JSONTestResult(self.view, args) json_plugin = jsonresult.JSONTestResult(self.view, args)
self.result_proxy.add_output_plugin(json_plugin) self.result_proxy.add_output_plugin(json_plugin)
# Setup the json plugin to output to the debug directory
if HTML_REPORT_SUPPORT:
html_file = os.path.join(self.logdir, 'html', 'results.html')
args = argparse.Namespace()
args.html_output = html_file
if self.args is not None:
args.open_browser = getattr(self.args, 'open_browser')
else:
args.open_browser = False
args.relative_links = True
html_plugin = htmlresult.HTMLTestResult(self.view, args)
self.result_proxy.add_output_plugin(html_plugin)
op_set_stdout = self.result_proxy.output_plugins_using_stdout() op_set_stdout = self.result_proxy.output_plugins_using_stdout()
if len(op_set_stdout) > 1: if len(op_set_stdout) > 1:
msg = ('Options %s are trying to use stdout simultaneously' % msg = ('Options %s are trying to use stdout simultaneously' %
......
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See LICENSE for more details.
#
# Copyright: Red Hat Inc. 2014
# Author: Lucas Meneghel Rodrigues <lmr@redhat.com>
"""
HTML output module.
"""
import os
import time
import shutil
import sys
import webbrowser
import pystache
from avocado.core import output
from avocado.core import error_codes
from avocado.plugins import plugin
from avocado.result import TestResult
class ReportModel(object):
"""
Prepares JSON that can be passed up to mustache for rendering.
"""
def __init__(self, json_input, html_output, relative_links):
"""
Base JSON that comes from test results.
"""
self.json = json_input
self.relative_links = relative_links
self.html_output = html_output
def job_id(self):
return self.json['job_id']
def execution_time(self):
return "%.2f" % self.json['time']
def _results_dir(self, relative_links=True):
debuglog_abspath = os.path.abspath(os.path.dirname(self.json['debuglog']))
html_output_abspath = os.path.abspath(os.path.dirname(self.html_output))
if relative_links:
return os.path.relpath(debuglog_abspath, html_output_abspath)
else:
return debuglog_abspath
def results_dir(self):
return self._results_dir(relative_links=self.relative_links)
def results_dir_basename(self):
return os.path.basename(self._results_dir(relative_links=False))
def total(self):
return self.json['total']
def passed(self):
return self.json['pass']
def pass_rate(self):
pr = 100 * (float(self.json['pass']) / float(self.json['total']))
return "%.2f" % pr
def _get_sysinfo(self, sysinfo_file):
sysinfo_path = os.path.join(self._results_dir(relative_links=False), 'sysinfo', 'pre', sysinfo_file)
try:
with open(sysinfo_path, 'r') as sysinfo_file:
sysinfo_contents = sysinfo_file.read()
except OSError, details:
sysinfo_contents = "Error reading %s: %s" % (sysinfo_path, details)
return sysinfo_contents
def hostname(self):
return self._get_sysinfo('hostname')
@property
def tests(self):
mapping = {"TEST_NA": "warning",
"ABORT": "danger",
"ERROR": "danger",
"NOT_FOUND": "warning",
"FAIL": "danger",
"WARN": "warning",
"PASS": "success",
"START": "info",
"ALERT": "danger",
"RUNNING": "info",
"NOSTATUS": "info",
"INTERRUPTED": "danger"}
test_info = self.json['tests']
for t in test_info:
t['link'] = os.path.join(self._results_dir(relative_links=self.relative_links), 'test-results', t['url'], 'debug.log')
t['link_basename'] = os.path.basename(t['link'])
t['dir_link'] = os.path.join(self._results_dir(relative_links=self.relative_links), 'test-results', t['url'])
t['time'] = "%.2f" % t['time']
t['time_start'] = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(t['time_start']))
t['row_class'] = mapping[t['status']]
exibition_limit = 40
if len(t['fail_reason']) > exibition_limit:
t['fail_reason'] = ('<a data-container="body" data-toggle="popover" '
'data-placement="top" title="Error Details" data-content="%s">%s...</a>' %
(t['fail_reason'], t['fail_reason'][:exibition_limit]))
return test_info
def sysinfo(self):
base_path = os.path.join(self._results_dir(relative_links=False), 'sysinfo', 'pre')
sysinfo_files = os.listdir(base_path)
sysinfo_files.sort()
sysinfo_list = []
s_id = 1
for s_f in sysinfo_files:
sysinfo_dict = {}
sysinfo_path = os.path.join(base_path, s_f)
try:
with open(sysinfo_path, 'r') as sysinfo_file:
sysinfo_dict['file'] = " ".join(s_f.split("_"))
sysinfo_dict['contents'] = sysinfo_file.read()
sysinfo_dict['element_id'] = 'heading_%s' % s_id
sysinfo_dict['collapse_id'] = 'collapse_%s' % s_id
except OSError:
sysinfo_dict[s_f] = 'Error reading sysinfo file %s' % sysinfo_path
sysinfo_list.append(sysinfo_dict)
s_id += 1
return sysinfo_list
class HTMLTestResult(TestResult):
"""
HTML Test Result class.
"""
command_line_arg_name = '--html'
def __init__(self, stream=None, args=None):
TestResult.__init__(self, stream, args)
self.output = getattr(self.args, 'html_output')
self.args = args
self.view = output.View(app_args=args)
self.json = None
def start_tests(self):
"""
Called once before any tests are executed.
"""
TestResult.start_tests(self)
self.json = {'debuglog': self.stream.logfile,
'tests': []}
def end_test(self, state):
"""
Called when the given test has been run.
:param state: result of :class:`avocado.test.Test.get_state`.
:type state: dict
"""
TestResult.end_test(self, state)
if 'job_id' not in self.json:
self.json['job_id'] = state['job_unique_id']
if state['fail_reason'] is None:
state['fail_reason'] = ''
else:
state['fail_reason'] = str(state['fail_reason'])
t = {'test': state['tagged_name'],
'url': state['name'],
'time_start': state['time_start'],
'time_end': state['time_end'],
'time': state['time_elapsed'],
'status': state['status'],
'fail_reason': state['fail_reason'],
'whiteboard': state['whiteboard'],
}
self.json['tests'].append(t)
def end_tests(self):
"""
Called once after all tests are executed.
"""
TestResult.end_tests(self)
self.json.update({
'total': len(self.json['tests']),
'pass': len(self.passed),
'errors': len(self.errors),
'not_found': len(self.not_found),
'failures': len(self.failed),
'skip': len(self.skipped),
'time': self.total_time
})
self._render_report()
def _render_report(self):
if self.args is not None:
relative_links = getattr(self.args, 'relative_links')
else:
relative_links = False
context = ReportModel(json_input=self.json, html_output=self.output, relative_links=relative_links)
renderer = pystache.Renderer()
html = HTML()
template = html.get_resource_path('templates', 'report.mustache')
report_contents = renderer.render(open(template, 'r').read(), context)
static_basedir = html.get_resource_path('static')
if self.output == '-':
self.view.notify(event='error', msg="HTML to stdout not supported "
"(not all HTML resources can be embedded to a single file)")
sys.exit(error_codes.numeric_status['AVOCADO_JOB_FAIL'])
else:
output_dir = os.path.dirname(os.path.abspath(self.output))
if not os.path.exists(output_dir):
os.makedirs(output_dir)
for resource_dir in os.listdir(static_basedir):
res_dir = os.path.join(static_basedir, resource_dir)
out_dir = os.path.join(output_dir, resource_dir)
if os.path.exists(out_dir):
shutil.rmtree(out_dir)
shutil.copytree(res_dir, out_dir)
with open(self.output, 'w') as report_file:
report_file.write(report_contents)
if self.args is not None:
if getattr(self.args, 'open_browser'):
webbrowser.open(self.output)
class HTML(plugin.Plugin):
"""
HTML job report.
"""
name = 'htmlresult'
enabled = True
parser = None
def configure(self, parser):
self.parser = parser
self.parser.runner.add_argument(
'--html', type=str,
dest='html_output',
help=('Enable HTML output to the file where the result should be written. '
'The option - is not supported since not all HTML resources can be '
'embedded into a single file (page resources will be copied to the '
'output file dir)'))
self.parser.runner.add_argument(
'--relative-links',
dest='relative_links',
action='store_true',
default=False,
help=('On the HTML report, generate anchor links with relative instead of absolute paths. Default: %s' %
False))
self.parser.runner.add_argument(
'--open-browser',
dest='open_browser',
action='store_true',
default=False,
help='Open the generated report on your preferred browser. '
'This works even if --html was not explicitly passed, since an HTML '
'report is always generated on the job results dir. Default: %s' % False)
self.configured = True
def activate(self, app_args):
try:
if app_args.html_output:
self.parser.application.set_defaults(html_result=HTMLTestResult)
except AttributeError:
pass
...@@ -12,6 +12,9 @@ ...@@ -12,6 +12,9 @@
# Copyright: Red Hat Inc. 2013-2014 # Copyright: Red Hat Inc. 2013-2014
# Author: Ruda Moura <rmoura@redhat.com> # Author: Ruda Moura <rmoura@redhat.com>
import os
import sys
"""Plugins basic structure.""" """Plugins basic structure."""
...@@ -46,6 +49,17 @@ class Plugin(object): ...@@ -46,6 +49,17 @@ class Plugin(object):
def __repr__(self): def __repr__(self):
return "%s(name='%s')" % (self.__class__.__name__, self.name) return "%s(name='%s')" % (self.__class__.__name__, self.name)
def get_resource_path(self, *args):
"""
Get the path of a plugin resource (static files, templates, etc).
:param args: Path components (plugin resources dir is the root).
:return: Full resource path.
"""
plugins_dir = os.path.dirname(sys.modules[__name__].__file__)
resources_dir = os.path.join(plugins_dir, 'resources', self.name)
return os.path.join(resources_dir, *args)
def configure(self, parser): def configure(self, parser):
"""Configuration and argument parsing. """Configuration and argument parsing.
......
div.dataTables_length label {
font-weight: normal;
text-align: left;
white-space: nowrap;
}
div.dataTables_length select {
width: 75px;
display: inline-block;
}
div.dataTables_filter {
text-align: right;
}
div.dataTables_filter label {
font-weight: normal;
white-space: nowrap;
text-align: left;
}
div.dataTables_filter input {
margin-left: 0.5em;
display: inline-block;
}
div.dataTables_info {
padding-top: 8px;
white-space: nowrap;
}
div.dataTables_paginate {
margin: 0;
white-space: nowrap;
text-align: right;
}
div.dataTables_paginate ul.pagination {
margin: 2px 0;
white-space: nowrap;
}
@media screen and (max-width: 767px) {
div.dataTables_length,
div.dataTables_filter,
div.dataTables_info,
div.dataTables_paginate {
text-align: center;
}
}
table.dataTable td,
table.dataTable th {
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
table.dataTable {
clear: both;
margin-top: 6px !important;
margin-bottom: 6px !important;
max-width: none !important;
}
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 {
cursor: pointer;
}
table.dataTable thead .sorting { background: url('../images/sort_both.png') no-repeat center right; }
table.dataTable thead .sorting_asc { background: url('../images/sort_asc.png') no-repeat center right; }
table.dataTable thead .sorting_desc { background: url('../images/sort_desc.png') no-repeat center right; }
table.dataTable thead .sorting_asc_disabled { background: url('../images/sort_asc_disabled.png') no-repeat center right; }
table.dataTable thead .sorting_desc_disabled { background: url('../images/sort_desc_disabled.png') no-repeat center right; }
table.dataTable thead > tr > th {
padding-left: 18px;
padding-right: 18px;
}
table.dataTable th:active {
outline: none;
}
/* Scrolling */
div.dataTables_scrollHead table {
margin-bottom: 0 !important;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
div.dataTables_scrollHead table thead tr:last-child th:first-child,
div.dataTables_scrollHead table thead tr:last-child td:first-child {
border-bottom-left-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
div.dataTables_scrollBody table {
border-top: none;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
div.dataTables_scrollBody tbody tr:first-child th,
div.dataTables_scrollBody tbody tr:first-child td {
border-top: none;
}
div.dataTables_scrollFoot table {
margin-top: 0 !important;
border-top: none;
}
/* Frustratingly the border-collapse:collapse used by Bootstrap makes the column
width calculations when using scrolling impossible to align columns. We have
to use separate
*/
table.table-bordered.dataTable {
border-collapse: separate !important;
}
table.table-bordered thead th,
table.table-bordered thead td {
border-left-width: 0;
border-top-width: 0;
}
table.table-bordered tbody th,
table.table-bordered tbody td {
border-left-width: 0;
border-bottom-width: 0;
}
table.table-bordered th:last-child,
table.table-bordered td:last-child {
border-right-width: 0;
}
div.dataTables_scrollHead table.table-bordered {
border-bottom-width: 0;
}
/*
* TableTools styles
*/
.table.dataTable tbody tr.active td,
.table.dataTable tbody tr.active th {
background-color: #08C;
color: white;
}
.table.dataTable tbody tr.active:hover td,
.table.dataTable tbody tr.active:hover th {
background-color: #0075b0 !important;
}
.table.dataTable tbody tr.active th > a,
.table.dataTable tbody tr.active td > a {
color: white;
}
.table-striped.dataTable tbody tr.active:nth-child(odd) td,
.table-striped.dataTable tbody tr.active:nth-child(odd) th {
background-color: #017ebc;
}
table.DTTT_selectable tbody tr {
cursor: pointer;
}
div.DTTT .btn {
color: #333 !important;
font-size: 12px;
}
div.DTTT .btn:hover {
text-decoration: none !important;
}
ul.DTTT_dropdown.dropdown-menu {
z-index: 2003;
}
ul.DTTT_dropdown.dropdown-menu a {
color: #333 !important; /* needed only when demo_page.css is included */
}
ul.DTTT_dropdown.dropdown-menu li {
position: relative;
}
ul.DTTT_dropdown.dropdown-menu li:hover a {
background-color: #0088cc;
color: white !important;
}
div.DTTT_collection_background {
z-index: 2002;
}
/* TableTools information display */
div.DTTT_print_info {
position: fixed;
top: 50%;
left: 50%;
width: 400px;
height: 150px;
margin-left: -200px;
margin-top: -75px;
text-align: center;
color: #333;
padding: 10px 30px;
opacity: 0.95;
background-color: white;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 6px;
-webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.5);
box-shadow: 0 3px 7px rgba(0, 0, 0, 0.5);
}
div.DTTT_print_info h6 {
font-weight: normal;
font-size: 28px;
line-height: 28px;
margin: 1em;
}
div.DTTT_print_info p {
font-size: 14px;
line-height: 20px;
}
div.dataTables_processing {
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 60px;
margin-left: -50%;
margin-top: -25px;
padding-top: 20px;
padding-bottom: 20px;
text-align: center;
font-size: 1.2em;
background-color: white;
background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255,255,255,0)), color-stop(25%, rgba(255,255,255,0.9)), color-stop(75%, rgba(255,255,255,0.9)), color-stop(100%, rgba(255,255,255,0)));
background: -webkit-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);
background: -moz-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);
background: -ms-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);
background: -o-linear-gradient(left, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);
background: linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.9) 25%, rgba(255,255,255,0.9) 75%, rgba(255,255,255,0) 100%);
}
/*
* FixedColumns styles
*/
div.DTFC_LeftHeadWrapper table,
div.DTFC_LeftFootWrapper table,
div.DTFC_RightHeadWrapper table,
div.DTFC_RightFootWrapper table,
table.DTFC_Cloned tr.even {
background-color: white;
margin-bottom: 0;
}
div.DTFC_RightHeadWrapper table ,
div.DTFC_LeftHeadWrapper table {
border-bottom: none !important;
margin-bottom: 0 !important;
border-top-right-radius: 0 !important;
border-bottom-left-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
div.DTFC_RightHeadWrapper table thead tr:last-child th:first-child,
div.DTFC_RightHeadWrapper table thead tr:last-child td:first-child,
div.DTFC_LeftHeadWrapper table thead tr:last-child th:first-child,
div.DTFC_LeftHeadWrapper table thead tr:last-child td:first-child {
border-bottom-left-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
div.DTFC_RightBodyWrapper table,
div.DTFC_LeftBodyWrapper table {
border-top: none;
margin: 0 !important;
}
div.DTFC_RightBodyWrapper tbody tr:first-child th,
div.DTFC_RightBodyWrapper tbody tr:first-child td,
div.DTFC_LeftBodyWrapper tbody tr:first-child th,
div.DTFC_LeftBodyWrapper tbody tr:first-child td {
border-top: none;
}
div.DTFC_RightFootWrapper table,
div.DTFC_LeftFootWrapper table {
border-top: none;
margin-top: 0 !important;
}
/*
* FixedHeader styles
*/
div.FixedHeader_Cloned table {
margin: 0 !important
}
/*! DataTables Bootstrap 3 integration
* ©2011-2014 SpryMedia Ltd - datatables.net/license
*/
/**
* DataTables integration for Bootstrap 3. This requires Bootstrap 3 and
* DataTables 1.10 or newer.
*
* This file sets the defaults and adds options to DataTables to style its
* controls using Bootstrap. See http://datatables.net/manual/styling/bootstrap
* for further information.
*/
(function(window, document, undefined){
var factory = function( $, DataTable ) {
"use strict";
/* Set the defaults for DataTables initialisation */
$.extend( true, DataTable.defaults, {
dom:
"<'row'<'col-sm-6'l><'col-sm-6'f>>" +
"<'row'<'col-sm-12'tr>>" +
"<'row'<'col-sm-6'i><'col-sm-6'p>>",
renderer: 'bootstrap'
} );
/* Default class modification */
$.extend( DataTable.ext.classes, {
sWrapper: "dataTables_wrapper form-inline dt-bootstrap",
sFilterInput: "form-control input-sm",
sLengthSelect: "form-control input-sm"
} );
/* Bootstrap paging button renderer */
DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, buttons, page, pages ) {
var api = new DataTable.Api( settings );
var classes = settings.oClasses;
var lang = settings.oLanguage.oPaginate;
var btnDisplay, btnClass;
var attach = function( container, buttons ) {
var i, ien, node, button;
var clickHandler = function ( e ) {
e.preventDefault();
if ( !$(e.currentTarget).hasClass('disabled') ) {
api.page( e.data.action ).draw( false );
}
};
for ( i=0, ien=buttons.length ; i<ien ; i++ ) {
button = buttons[i];
if ( $.isArray( button ) ) {
attach( container, button );
}
else {
btnDisplay = '';
btnClass = '';
switch ( button ) {
case 'ellipsis':
btnDisplay = '&hellip;';
btnClass = 'disabled';
break;
case 'first':
btnDisplay = lang.sFirst;
btnClass = button + (page > 0 ?
'' : ' disabled');
break;
case 'previous':
btnDisplay = lang.sPrevious;
btnClass = button + (page > 0 ?
'' : ' disabled');
break;
case 'next':
btnDisplay = lang.sNext;
btnClass = button + (page < pages-1 ?
'' : ' disabled');
break;
case 'last':
btnDisplay = lang.sLast;
btnClass = button + (page < pages-1 ?
'' : ' disabled');
break;
default:
btnDisplay = button + 1;
btnClass = page === button ?
'active' : '';
break;
}
if ( btnDisplay ) {
node = $('<li>', {
'class': classes.sPageButton+' '+btnClass,
'aria-controls': settings.sTableId,
'tabindex': settings.iTabIndex,
'id': idx === 0 && typeof button === 'string' ?
settings.sTableId +'_'+ button :
null
} )
.append( $('<a>', {
'href': '#'
} )
.html( btnDisplay )
)
.appendTo( container );
settings.oApi._fnBindAction(
node, {action: button}, clickHandler
);
}
}
}
};
attach(
$(host).empty().html('<ul class="pagination"/>').children('ul'),
buttons
);
};
/*
* TableTools Bootstrap compatibility
* Required TableTools 2.1+
*/
if ( DataTable.TableTools ) {
// Set the classes that TableTools uses to something suitable for Bootstrap
$.extend( true, DataTable.TableTools.classes, {
"container": "DTTT btn-group",
"buttons": {
"normal": "btn btn-default",
"disabled": "disabled"
},
"collection": {
"container": "DTTT_dropdown dropdown-menu",
"buttons": {
"normal": "",
"disabled": "disabled"
}
},
"print": {
"info": "DTTT_print_info"
},
"select": {
"row": "active"
}
} );
// Have the collection use a bootstrap compatible drop down
$.extend( true, DataTable.TableTools.DEFAULTS.oTags, {
"collection": {
"container": "ul",
"button": "li",
"liner": "a"
}
} );
}
}; // /factory
// Define as an AMD module if possible
if ( typeof define === 'function' && define.amd ) {
define( ['jquery', 'datatables'], factory );
}
else if ( typeof exports === 'object' ) {
// Node/CommonJS
factory( require('jquery'), require('datatables') );
}
else if ( jQuery ) {
// Otherwise simply initialise as normal, stopping multiple evaluation
factory( jQuery, jQuery.fn.dataTable );
}
})(window, document);
<!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">
<title>Avocado Job Report</title>
<link href="css/bootstrap.min.css" rel="stylesheet">
<link href="css/dataTables.bootstrap.css" rel="stylesheet">
<script src="js/jquery.min.js"></script>
<script src="js/bootstrap.min.js"></script>
<script src="js/jquery.dataTables.min.js"></script>
<script src="js/dataTables.bootstrap.js"></script>
<script type="text/javascript" charset="utf-8">
$(document).ready(function() {
$('#results').dataTable();
$(function () {$('[data-toggle="popover"]').popover()})
} );
</script>
</head>
<body>
<div class="container">
<div class="page-header">
<h3>Avocado Job Report</h3>
</div>
<div class="panel-group" role="tablist">
<div class="panel panel-default">
<!-- Default panel contents -->
<div class="panel-heading">
<h4 class="panel-title">Summary</h4>
</div>
<!-- Table -->
<table class="table table-bordered">
<tr>
<td>ID</td><td><tt>{{job_id}}</tt></td>
</tr>
<tr>
<td>Host</td><td><tt>{{hostname}}</tt></td>
</tr>
<tr>
<td>Results Dir</td><td><a href="{{results_dir}}"><tt>{{results_dir_basename}}</tt></a></td>
</tr>
<tr>
<td>Execution time</td><td>{{execution_time}} s</td>
</tr>
<tr>
<td>Stats</td><td>From {{total}} tests executed, {{passed}} passed (pass rate of {{pass_rate}}%)</td>
</tr>
</table>
</div>
</div>
<table id="results" class="display" cellspacing="0" width="100%"><thead>
<tr>
<th>Start Time</th>
<th>Test ID</th>
<th>Status</th>
<th>Time (sec)</th>
<th>Info</th>
<th>Debug Log</th>
</tr>
</thead>
{{#tests}}
<tr class="{{row_class}}">
<td>{{time_start}}</td>
<td><a href="{{dir_link}}">{{url}}</a></td>
<td>{{status}}</td>
<td>{{time}}</td>
<td>{{& fail_reason}}</td>
<td><a href="{{link}}">{{link_basename}}</a></td>
</tr>
{{/tests}}
</table>
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingOne">
<h4 class="panel-title"><a data-toggle="collapse" data-parent="#accordion" href="#collapseOne" aria-expanded="false" aria-controls="collapseOne"> Sysinfo (pre job, click to expand) </a></h4>
</div>
<div id="collapseOne" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingOne">
<div class="panel-body">
<div class="panel-group" id="accordion2" role="tablist" aria-multiselectable="true">
{{#sysinfo}}
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="{{element_id}}">
<h4 class="panel-title"><a data-toggle="collapse" data-parent="#accordion2" href="#{{collapse_id}}" aria-expanded="false" aria-controls="{{collapse_id}}"><tt>{{file}}</tt></a></h4>
</div>
<div id="{{collapse_id}}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="{{element_id}}">
<div class="panel-body">
<pre>{{contents}}</pre>
</div>
</div>
</div>
{{/sysinfo}}
</div>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
$('#results')
.removeClass( 'display' )
.addClass('table table-striped table-bordered');
</script>
</body>
</html>
...@@ -20,6 +20,8 @@ It also contains the most basic test result class, HumanTestResult, ...@@ -20,6 +20,8 @@ It also contains the most basic test result class, HumanTestResult,
used by the test runner. used by the test runner.
""" """
import os
class InvalidOutputPlugin(Exception): class InvalidOutputPlugin(Exception):
pass pass
...@@ -253,6 +255,11 @@ class HumanTestResult(TestResult): ...@@ -253,6 +255,11 @@ class HumanTestResult(TestResult):
TestResult.start_tests(self) TestResult.start_tests(self)
self.stream.notify(event="message", msg="JOB ID : %s" % self.stream.job_unique_id) self.stream.notify(event="message", msg="JOB ID : %s" % self.stream.job_unique_id)
self.stream.notify(event="message", msg="JOB LOG : %s" % self.stream.logfile) self.stream.notify(event="message", msg="JOB LOG : %s" % self.stream.logfile)
if self.args is not None:
if 'html_output' in self.args:
logdir = os.path.dirname(self.stream.logfile)
html_file = os.path.join(logdir, 'html', 'results.html')
self.stream.notify(event="message", msg="JOB HTML : %s" % html_file)
self.stream.notify(event="message", msg="TESTS : %s" % self.tests_total) self.stream.notify(event="message", msg="TESTS : %s" % self.tests_total)
self.stream.set_tests_info({'tests_total': self.tests_total}) self.stream.set_tests_info({'tests_total': self.tests_total})
......
...@@ -7,3 +7,4 @@ mysql-python==1.2.3 ...@@ -7,3 +7,4 @@ mysql-python==1.2.3
requests==1.2.3 requests==1.2.3
fabric==1.7.0 fabric==1.7.0
pyliblzma==0.5.3 pyliblzma==0.5.3
pystache>0.5.0
...@@ -19,6 +19,7 @@ import tempfile ...@@ -19,6 +19,7 @@ import tempfile
import unittest import unittest
import os import os
import sys import sys
import shutil
from xml.dom import minidom from xml.dom import minidom
# simple magic for using scripts within a source tree # simple magic for using scripts within a source tree
...@@ -85,6 +86,19 @@ class OutputPluginTest(unittest.TestCase): ...@@ -85,6 +86,19 @@ class OutputPluginTest(unittest.TestCase):
self.assertIn(error_excerpt, output, self.assertIn(error_excerpt, output,
"Missing excerpt error message from output:\n%s" % output) "Missing excerpt error message from output:\n%s" % output)
def test_output_incompatible_setup_3(self):
os.chdir(basedir)
cmd_line = './scripts/avocado run --html - sleeptest'
result = process.run(cmd_line, ignore_status=True)
expected_rc = 2
output = result.stdout + result.stderr
self.assertEqual(result.exit_status, expected_rc,
"Avocado did not return rc %d:\n%s" %
(expected_rc, result))
error_excerpt = "HTML to stdout not supported"
self.assertIn(error_excerpt, output,
"Missing excerpt error message from output:\n%s" % output)
def test_output_compatible_setup(self): def test_output_compatible_setup(self):
tmpfile = tempfile.mktemp() tmpfile = tempfile.mktemp()
os.chdir(basedir) os.chdir(basedir)
...@@ -131,11 +145,17 @@ class OutputPluginTest(unittest.TestCase): ...@@ -131,11 +145,17 @@ class OutputPluginTest(unittest.TestCase):
def test_output_compatible_setup_3(self): def test_output_compatible_setup_3(self):
tmpfile = tempfile.mktemp() tmpfile = tempfile.mktemp()
tmpfile2 = tempfile.mktemp() tmpfile2 = tempfile.mktemp()
tmpdir = tempfile.mkdtemp()
tmpfile3 = tempfile.mktemp(dir=tmpdir)
os.chdir(basedir) os.chdir(basedir)
cmd_line = './scripts/avocado run --xunit %s --json %s passtest' % (tmpfile, tmpfile2) cmd_line = ('./scripts/avocado run --xunit %s --json %s --html %s passtest' %
(tmpfile, tmpfile2, tmpfile3))
result = process.run(cmd_line, ignore_status=True) result = process.run(cmd_line, ignore_status=True)
output = result.stdout + result.stderr output = result.stdout + result.stderr
expected_rc = 0 expected_rc = 0
tmpdir_contents = os.listdir(tmpdir)
self.assertEqual(len(tmpdir_contents), 5,
'Not all resources dir were created: %s' % tmpdir_contents)
try: try:
self.assertEqual(result.exit_status, expected_rc, self.assertEqual(result.exit_status, expected_rc,
"Avocado did not return rc %d:\n%s" % "Avocado did not return rc %d:\n%s" %
...@@ -151,6 +171,7 @@ class OutputPluginTest(unittest.TestCase): ...@@ -151,6 +171,7 @@ class OutputPluginTest(unittest.TestCase):
try: try:
os.remove(tmpfile) os.remove(tmpfile)
os.remove(tmpfile2) os.remove(tmpfile2)
shutil.rmtree(tmpdir)
except OSError: except OSError:
pass pass
......
...@@ -59,6 +59,18 @@ def get_data_files(): ...@@ -59,6 +59,18 @@ def get_data_files():
return data_files return data_files
def _get_plugin_resource_files(path):
"""
Given a path, return all the files in there to package
"""
flist = []
for root, _, files in sorted(os.walk(path)):
for name in files:
fullname = os.path.join(root, name)
flist.append(fullname[len('avocado/plugins/'):])
return flist
setup(name='avocado', setup(name='avocado',
version=avocado.version.VERSION, version=avocado.version.VERSION,
description='Avocado Test Framework', description='Avocado Test Framework',
...@@ -72,5 +84,6 @@ setup(name='avocado', ...@@ -72,5 +84,6 @@ setup(name='avocado',
'avocado.linux', 'avocado.linux',
'avocado.utils', 'avocado.utils',
'avocado.plugins'], 'avocado.plugins'],
package_data={'avocado.plugins': _get_plugin_resource_files('avocado/plugins/resources')},
data_files=get_data_files(), data_files=get_data_files(),
scripts=['scripts/avocado']) scripts=['scripts/avocado'])
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册