test/py: HTML awesome!

Implement three improvements to the HTML log file:
- Ability to expand/contract sections. All passing sections are contracted
  at file load time so the user can concentrate on issues requiring
  action.
- The overall status report is copied to the top of the log for easy
  access.
- Add links from the status report to the test logs, for easy navigation.

This all relies on Javascript and the jquery library. If the user doesn't
have Javascript enabled, or jquery can't be downloaded, the log should
look and behave identically to how it did before this patch.

A few notes on the diff:

- A few more 'with log.section("xxx")' were added, so that all stream
  blocks are kept within a section block for consistent HTML entity
  nesting structure. This changed indentation in a few places, making
  the diff look slightly larger.
- HTML entity IDs are cleaned up. We assign simple incrementing integer
  IDs now, rather than using mangled test names which were possibly
  invalid.
- Sections and streams now use common CSS class names (in addition to the
  current separate class names) to more easily share the new behaviour.
  This also reduces the CSS file size since rules don't need to be
  duplicated.
- An "OK" status is logged after some external command executions so that
  make and flash steps are auto-contracted at log file load time, assuming
  they passed.

Signed-off-by: Stephen Warren <swarren@nvidia.com>
This commit is contained in:
Stephen Warren 2016-02-03 16:46:34 -07:00 committed by Simon Glass
parent ac99831b7d
commit 83357fd5c2
4 changed files with 178 additions and 78 deletions

View file

@ -129,10 +129,12 @@ def pytest_configure(config):
['make', o_opt, '-s', board_type + '_defconfig'], ['make', o_opt, '-s', board_type + '_defconfig'],
['make', o_opt, '-s', '-j8'], ['make', o_opt, '-s', '-j8'],
) )
with log.section('make'):
runner = log.get_runner('make', sys.stdout) runner = log.get_runner('make', sys.stdout)
for cmd in cmds: for cmd in cmds:
runner.run(cmd, cwd=source_dir) runner.run(cmd, cwd=source_dir)
runner.close() runner.close()
log.status_pass('OK')
class ArbitraryAttributeContainer(object): class ArbitraryAttributeContainer(object):
pass pass
@ -255,6 +257,7 @@ def u_boot_console(request):
console.ensure_spawned() console.ensure_spawned()
return console return console
anchors = {}
tests_not_run = set() tests_not_run = set()
tests_failed = set() tests_failed = set()
tests_xpassed = set() tests_xpassed = set()
@ -294,27 +297,33 @@ def cleanup():
if console: if console:
console.close() console.close()
if log: if log:
with log.section('Status Report', 'status_report'):
log.status_pass('%d passed' % len(tests_passed)) log.status_pass('%d passed' % len(tests_passed))
if tests_skipped: if tests_skipped:
log.status_skipped('%d skipped' % len(tests_skipped)) log.status_skipped('%d skipped' % len(tests_skipped))
for test in tests_skipped: for test in tests_skipped:
log.status_skipped('... ' + test) anchor = anchors.get(test, None)
log.status_skipped('... ' + test, anchor)
if tests_xpassed: if tests_xpassed:
log.status_xpass('%d xpass' % len(tests_xpassed)) log.status_xpass('%d xpass' % len(tests_xpassed))
for test in tests_xpassed: for test in tests_xpassed:
log.status_xpass('... ' + test) anchor = anchors.get(test, None)
log.status_xpass('... ' + test, anchor)
if tests_xfailed: if tests_xfailed:
log.status_xfail('%d xfail' % len(tests_xfailed)) log.status_xfail('%d xfail' % len(tests_xfailed))
for test in tests_xfailed: for test in tests_xfailed:
log.status_xfail('... ' + test) anchor = anchors.get(test, None)
log.status_xfail('... ' + test, anchor)
if tests_failed: if tests_failed:
log.status_fail('%d failed' % len(tests_failed)) log.status_fail('%d failed' % len(tests_failed))
for test in tests_failed: for test in tests_failed:
log.status_fail('... ' + test) anchor = anchors.get(test, None)
log.status_fail('... ' + test, anchor)
if tests_not_run: if tests_not_run:
log.status_fail('%d not run' % len(tests_not_run)) log.status_fail('%d not run' % len(tests_not_run))
for test in tests_not_run: for test in tests_not_run:
log.status_fail('... ' + test) anchor = anchors.get(test, None)
log.status_fail('... ' + test, anchor)
log.close() log.close()
atexit.register(cleanup) atexit.register(cleanup)
@ -380,7 +389,7 @@ def pytest_runtest_setup(item):
Nothing. Nothing.
""" """
log.start_section(item.name) anchors[item.name] = log.start_section(item.name)
setup_boardspec(item) setup_boardspec(item)
setup_buildconfigspec(item) setup_buildconfigspec(item)

View file

@ -25,37 +25,24 @@ pre {
color: #808080; color: #808080;
} }
.section { .block {
border-style: solid; border-style: solid;
border-color: #303030; border-color: #303030;
border-width: 0px 0px 0px 5px; border-width: 0px 0px 0px 5px;
padding-left: 5px padding-left: 5px
} }
.section-header { .block-header {
background-color: #303030; background-color: #303030;
margin-left: -5px; margin-left: -5px;
margin-top: 5px; margin-top: 5px;
} }
.section-trailer { .block-header:hover {
display: none; text-decoration: underline;
} }
.stream { .block-trailer {
border-style: solid;
border-color: #303030;
border-width: 0px 0px 0px 5px;
padding-left: 5px
}
.stream-header {
background-color: #303030;
margin-left: -5px;
margin-top: 5px;
}
.stream-trailer {
display: none; display: none;
} }
@ -94,3 +81,21 @@ pre {
.status-fail { .status-fail {
color: #ff0000 color: #ff0000
} }
.hidden {
display: none;
}
a:link {
text-decoration: inherit;
color: inherit;
}
a:visited {
text-decoration: inherit;
color: inherit;
}
a:hover {
text-decoration: underline;
}

View file

@ -168,12 +168,13 @@ class SectionCtxMgr(object):
Objects of this type should be created by factory functions in the Logfile Objects of this type should be created by factory functions in the Logfile
class rather than directly.""" class rather than directly."""
def __init__(self, log, marker): def __init__(self, log, marker, anchor):
"""Initialize a new object. """Initialize a new object.
Args: Args:
log: The Logfile object to log to. log: The Logfile object to log to.
marker: The name of the nested log section. marker: The name of the nested log section.
anchor: The anchor value to pass to start_section().
Returns: Returns:
Nothing. Nothing.
@ -181,9 +182,10 @@ class SectionCtxMgr(object):
self.log = log self.log = log
self.marker = marker self.marker = marker
self.anchor = anchor
def __enter__(self): def __enter__(self):
self.log.start_section(self.marker) self.anchor = self.log.start_section(self.marker, self.anchor)
def __exit__(self, extype, value, traceback): def __exit__(self, extype, value, traceback):
self.log.end_section(self.marker) self.log.end_section(self.marker)
@ -206,11 +208,70 @@ class Logfile(object):
self.last_stream = None self.last_stream = None
self.blocks = [] self.blocks = []
self.cur_evt = 1 self.cur_evt = 1
self.anchor = 0
shutil.copy(mod_dir + '/multiplexed_log.css', os.path.dirname(fn)) shutil.copy(mod_dir + '/multiplexed_log.css', os.path.dirname(fn))
self.f.write('''\ self.f.write('''\
<html> <html>
<head> <head>
<link rel="stylesheet" type="text/css" href="multiplexed_log.css"> <link rel="stylesheet" type="text/css" href="multiplexed_log.css">
<script src="http://code.jquery.com/jquery.min.js"></script>
<script>
$(document).ready(function () {
// Copy status report HTML to start of log for easy access
sts = $(".block#status_report")[0].outerHTML;
$("tt").prepend(sts);
// Add expand/contract buttons to all block headers
btns = "<span class=\\\"block-expand hidden\\\">[+] </span>" +
"<span class=\\\"block-contract\\\">[-] </span>";
$(".block-header").prepend(btns);
// Pre-contract all blocks which passed, leaving only problem cases
// expanded, to highlight issues the user should look at.
// Only top-level blocks (sections) should have any status
passed_bcs = $(".block-content:has(.status-pass)");
// Some blocks might have multiple status entries (e.g. the status
// report), so take care not to hide blocks with partial success.
passed_bcs = passed_bcs.not(":has(.status-fail)");
passed_bcs = passed_bcs.not(":has(.status-xfail)");
passed_bcs = passed_bcs.not(":has(.status-xpass)");
passed_bcs = passed_bcs.not(":has(.status-skipped)");
// Hide the passed blocks
passed_bcs.addClass("hidden");
// Flip the expand/contract button hiding for those blocks.
bhs = passed_bcs.parent().children(".block-header")
bhs.children(".block-expand").removeClass("hidden");
bhs.children(".block-contract").addClass("hidden");
// Add click handler to block headers.
// The handler expands/contracts the block.
$(".block-header").on("click", function (e) {
var header = $(this);
var content = header.next(".block-content");
var expanded = !content.hasClass("hidden");
if (expanded) {
content.addClass("hidden");
header.children(".block-expand").first().removeClass("hidden");
header.children(".block-contract").first().addClass("hidden");
} else {
header.children(".block-contract").first().removeClass("hidden");
header.children(".block-expand").first().addClass("hidden");
content.removeClass("hidden");
}
});
// When clicking on a link, expand the target block
$("a").on("click", function (e) {
var block = $($(this).attr("href"));
var header = block.children(".block-header");
var content = block.children(".block-content").first();
header.children(".block-contract").first().removeClass("hidden");
header.children(".block-expand").first().addClass("hidden");
content.removeClass("hidden");
});
});
</script>
</head> </head>
<body> <body>
<tt> <tt>
@ -273,45 +334,60 @@ class Logfile(object):
if not self.last_stream: if not self.last_stream:
return return
self.f.write('</pre>\n') self.f.write('</pre>\n')
self.f.write('<div class="stream-trailer" id="' + self.f.write('<div class="stream-trailer block-trailer">End stream: ' +
self.last_stream.name + '">End stream: ' +
self.last_stream.name + '</div>\n') self.last_stream.name + '</div>\n')
self.f.write('</div>\n') self.f.write('</div>\n')
self.f.write('</div>\n')
self.last_stream = None self.last_stream = None
def _note(self, note_type, msg): def _note(self, note_type, msg, anchor=None):
"""Write a note or one-off message to the log file. """Write a note or one-off message to the log file.
Args: Args:
note_type: The type of note. This must be a value supported by the note_type: The type of note. This must be a value supported by the
accompanying multiplexed_log.css. accompanying multiplexed_log.css.
msg: The note/message to log. msg: The note/message to log.
anchor: Optional internal link target.
Returns: Returns:
Nothing. Nothing.
""" """
self._terminate_stream() self._terminate_stream()
self.f.write('<div class="' + note_type + '">\n<pre>') self.f.write('<div class="' + note_type + '">\n')
if anchor:
self.f.write('<a href="#%s">\n' % anchor)
self.f.write('<pre>')
self.f.write(self._escape(msg)) self.f.write(self._escape(msg))
self.f.write('\n</pre></div>\n') self.f.write('\n</pre>\n')
if anchor:
self.f.write('</a>\n')
self.f.write('</div>\n')
def start_section(self, marker): def start_section(self, marker, anchor=None):
"""Begin a new nested section in the log file. """Begin a new nested section in the log file.
Args: Args:
marker: The name of the section that is starting. marker: The name of the section that is starting.
anchor: The value to use for the anchor. If None, a unique value
will be calculated and used
Returns: Returns:
Nothing. Name of the HTML anchor emitted before section.
""" """
self._terminate_stream() self._terminate_stream()
self.blocks.append(marker) self.blocks.append(marker)
if not anchor:
self.anchor += 1
anchor = str(self.anchor)
blk_path = '/'.join(self.blocks) blk_path = '/'.join(self.blocks)
self.f.write('<div class="section" id="' + blk_path + '">\n') self.f.write('<div class="section block" id="' + anchor + '">\n')
self.f.write('<div class="section-header" id="' + blk_path + self.f.write('<div class="section-header block-header">Section: ' +
'">Section: ' + blk_path + '</div>\n') blk_path + '</div>\n')
self.f.write('<div class="section-content block-content">\n')
return anchor
def end_section(self, marker): def end_section(self, marker):
"""Terminate the current nested section in the log file. """Terminate the current nested section in the log file.
@ -331,12 +407,13 @@ class Logfile(object):
(marker, '/'.join(self.blocks))) (marker, '/'.join(self.blocks)))
self._terminate_stream() self._terminate_stream()
blk_path = '/'.join(self.blocks) blk_path = '/'.join(self.blocks)
self.f.write('<div class="section-trailer" id="section-trailer-' + self.f.write('<div class="section-trailer block-trailer">' +
blk_path + '">End section: ' + blk_path + '</div>\n') 'End section: ' + blk_path + '</div>\n')
self.f.write('</div>\n')
self.f.write('</div>\n') self.f.write('</div>\n')
self.blocks.pop() self.blocks.pop()
def section(self, marker): def section(self, marker, anchor=None):
"""Create a temporary section in the log file. """Create a temporary section in the log file.
This function creates a context manager for Python's "with" statement, This function creates a context manager for Python's "with" statement,
@ -349,12 +426,13 @@ class Logfile(object):
Args: Args:
marker: The name of the nested section. marker: The name of the nested section.
anchor: The anchor value to pass to start_section().
Returns: Returns:
A context manager object. A context manager object.
""" """
return SectionCtxMgr(self, marker) return SectionCtxMgr(self, marker, anchor)
def error(self, msg): def error(self, msg):
"""Write an error note to the log file. """Write an error note to the log file.
@ -404,65 +482,70 @@ class Logfile(object):
self._note("action", msg) self._note("action", msg)
def status_pass(self, msg): def status_pass(self, msg, anchor=None):
"""Write a note to the log file describing test(s) which passed. """Write a note to the log file describing test(s) which passed.
Args: Args:
msg: A message describing the passed test(s). msg: A message describing the passed test(s).
anchor: Optional internal link target.
Returns: Returns:
Nothing. Nothing.
""" """
self._note("status-pass", msg) self._note("status-pass", msg, anchor)
def status_skipped(self, msg): def status_skipped(self, msg, anchor=None):
"""Write a note to the log file describing skipped test(s). """Write a note to the log file describing skipped test(s).
Args: Args:
msg: A message describing the skipped test(s). msg: A message describing the skipped test(s).
anchor: Optional internal link target.
Returns: Returns:
Nothing. Nothing.
""" """
self._note("status-skipped", msg) self._note("status-skipped", msg, anchor)
def status_xfail(self, msg): def status_xfail(self, msg, anchor=None):
"""Write a note to the log file describing xfailed test(s). """Write a note to the log file describing xfailed test(s).
Args: Args:
msg: A message describing the xfailed test(s). msg: A message describing the xfailed test(s).
anchor: Optional internal link target.
Returns: Returns:
Nothing. Nothing.
""" """
self._note("status-xfail", msg) self._note("status-xfail", msg, anchor)
def status_xpass(self, msg): def status_xpass(self, msg, anchor=None):
"""Write a note to the log file describing xpassed test(s). """Write a note to the log file describing xpassed test(s).
Args: Args:
msg: A message describing the xpassed test(s). msg: A message describing the xpassed test(s).
anchor: Optional internal link target.
Returns: Returns:
Nothing. Nothing.
""" """
self._note("status-xpass", msg) self._note("status-xpass", msg, anchor)
def status_fail(self, msg): def status_fail(self, msg, anchor=None):
"""Write a note to the log file describing failed test(s). """Write a note to the log file describing failed test(s).
Args: Args:
msg: A message describing the failed test(s). msg: A message describing the failed test(s).
anchor: Optional internal link target.
Returns: Returns:
Nothing. Nothing.
""" """
self._note("status-fail", msg) self._note("status-fail", msg, anchor)
def get_stream(self, name, chained_file=None): def get_stream(self, name, chained_file=None):
"""Create an object to log a single stream's data into the log file. """Create an object to log a single stream's data into the log file.
@ -519,9 +602,10 @@ class Logfile(object):
if stream != self.last_stream: if stream != self.last_stream:
self._terminate_stream() self._terminate_stream()
self.f.write('<div class="stream" id="%s">\n' % stream.name) self.f.write('<div class="stream block">\n')
self.f.write('<div class="stream-header" id="' + stream.name + self.f.write('<div class="stream-header block-header">Stream: ' +
'">Stream: ' + stream.name + '</div>\n') stream.name + '</div>\n')
self.f.write('<div class="stream-content block-content">\n')
self.f.write('<pre>') self.f.write('<pre>')
if implicit: if implicit:
self.f.write('<span class="implicit">') self.f.write('<span class="implicit">')

View file

@ -35,11 +35,13 @@ class ConsoleExecAttach(ConsoleBase):
# HW flow control would mean this could be infinite. # HW flow control would mean this could be infinite.
super(ConsoleExecAttach, self).__init__(log, config, max_fifo_fill=16) super(ConsoleExecAttach, self).__init__(log, config, max_fifo_fill=16)
with self.log.section('flash'):
self.log.action('Flashing U-Boot') self.log.action('Flashing U-Boot')
cmd = ['u-boot-test-flash', config.board_type, config.board_identity] cmd = ['u-boot-test-flash', config.board_type, config.board_identity]
runner = self.log.get_runner(cmd[0], sys.stdout) runner = self.log.get_runner(cmd[0], sys.stdout)
runner.run(cmd) runner.run(cmd)
runner.close() runner.close()
self.log.status_pass('OK')
def get_spawn(self): def get_spawn(self):
"""Connect to a fresh U-Boot instance. """Connect to a fresh U-Boot instance.