diff options
author | Stephen Warren <swarren@nvidia.com> | 2016-02-03 16:46:34 -0700 |
---|---|---|
committer | Simon Glass <sjg@chromium.org> | 2016-02-09 15:41:19 -0700 |
commit | 83357fd5c2ec11b45607dfe35ba5fe5e44d1eee0 (patch) | |
tree | 3bb9e9a2f0958ff2f8958a0aa420152222ae57c4 /test | |
parent | ac99831b7d0f4e00c8dd32b604c2448260dd2776 (diff) |
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>
Diffstat (limited to 'test')
-rw-r--r-- | test/py/conftest.py | 61 | ||||
-rw-r--r-- | test/py/multiplexed_log.css | 41 | ||||
-rw-r--r-- | test/py/multiplexed_log.py | 142 | ||||
-rw-r--r-- | test/py/u_boot_console_exec_attach.py | 12 |
4 files changed, 178 insertions, 78 deletions
diff --git a/test/py/conftest.py b/test/py/conftest.py index 09638e64a3..3012c8e495 100644 --- a/test/py/conftest.py +++ b/test/py/conftest.py @@ -129,10 +129,12 @@ def pytest_configure(config): ['make', o_opt, '-s', board_type + '_defconfig'], ['make', o_opt, '-s', '-j8'], ) - runner = log.get_runner('make', sys.stdout) - for cmd in cmds: - runner.run(cmd, cwd=source_dir) - runner.close() + with log.section('make'): + runner = log.get_runner('make', sys.stdout) + for cmd in cmds: + runner.run(cmd, cwd=source_dir) + runner.close() + log.status_pass('OK') class ArbitraryAttributeContainer(object): pass @@ -255,6 +257,7 @@ def u_boot_console(request): console.ensure_spawned() return console +anchors = {} tests_not_run = set() tests_failed = set() tests_xpassed = set() @@ -294,27 +297,33 @@ def cleanup(): if console: console.close() if log: - log.status_pass('%d passed' % len(tests_passed)) - if tests_skipped: - log.status_skipped('%d skipped' % len(tests_skipped)) - for test in tests_skipped: - log.status_skipped('... ' + test) - if tests_xpassed: - log.status_xpass('%d xpass' % len(tests_xpassed)) - for test in tests_xpassed: - log.status_xpass('... ' + test) - if tests_xfailed: - log.status_xfail('%d xfail' % len(tests_xfailed)) - for test in tests_xfailed: - log.status_xfail('... ' + test) - if tests_failed: - log.status_fail('%d failed' % len(tests_failed)) - for test in tests_failed: - log.status_fail('... ' + test) - if tests_not_run: - log.status_fail('%d not run' % len(tests_not_run)) - for test in tests_not_run: - log.status_fail('... ' + test) + with log.section('Status Report', 'status_report'): + log.status_pass('%d passed' % len(tests_passed)) + if tests_skipped: + log.status_skipped('%d skipped' % len(tests_skipped)) + for test in tests_skipped: + anchor = anchors.get(test, None) + log.status_skipped('... ' + test, anchor) + if tests_xpassed: + log.status_xpass('%d xpass' % len(tests_xpassed)) + for test in tests_xpassed: + anchor = anchors.get(test, None) + log.status_xpass('... ' + test, anchor) + if tests_xfailed: + log.status_xfail('%d xfail' % len(tests_xfailed)) + for test in tests_xfailed: + anchor = anchors.get(test, None) + log.status_xfail('... ' + test, anchor) + if tests_failed: + log.status_fail('%d failed' % len(tests_failed)) + for test in tests_failed: + anchor = anchors.get(test, None) + log.status_fail('... ' + test, anchor) + if tests_not_run: + log.status_fail('%d not run' % len(tests_not_run)) + for test in tests_not_run: + anchor = anchors.get(test, None) + log.status_fail('... ' + test, anchor) log.close() atexit.register(cleanup) @@ -380,7 +389,7 @@ def pytest_runtest_setup(item): Nothing. """ - log.start_section(item.name) + anchors[item.name] = log.start_section(item.name) setup_boardspec(item) setup_buildconfigspec(item) diff --git a/test/py/multiplexed_log.css b/test/py/multiplexed_log.css index f6240d52da..f135b10a24 100644 --- a/test/py/multiplexed_log.css +++ b/test/py/multiplexed_log.css @@ -25,37 +25,24 @@ pre { color: #808080; } -.section { +.block { border-style: solid; border-color: #303030; border-width: 0px 0px 0px 5px; padding-left: 5px } -.section-header { +.block-header { background-color: #303030; margin-left: -5px; margin-top: 5px; } -.section-trailer { - display: none; +.block-header:hover { + text-decoration: underline; } -.stream { - 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 { +.block-trailer { display: none; } @@ -94,3 +81,21 @@ pre { .status-fail { color: #ff0000 } + +.hidden { + display: none; +} + +a:link { + text-decoration: inherit; + color: inherit; +} + +a:visited { + text-decoration: inherit; + color: inherit; +} + +a:hover { + text-decoration: underline; +} diff --git a/test/py/multiplexed_log.py b/test/py/multiplexed_log.py index 69a577e577..68917eb0ea 100644 --- a/test/py/multiplexed_log.py +++ b/test/py/multiplexed_log.py @@ -168,12 +168,13 @@ class SectionCtxMgr(object): Objects of this type should be created by factory functions in the Logfile class rather than directly.""" - def __init__(self, log, marker): + def __init__(self, log, marker, anchor): """Initialize a new object. Args: log: The Logfile object to log to. marker: The name of the nested log section. + anchor: The anchor value to pass to start_section(). Returns: Nothing. @@ -181,9 +182,10 @@ class SectionCtxMgr(object): self.log = log self.marker = marker + self.anchor = anchor 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): self.log.end_section(self.marker) @@ -206,11 +208,70 @@ class Logfile(object): self.last_stream = None self.blocks = [] self.cur_evt = 1 + self.anchor = 0 + shutil.copy(mod_dir + '/multiplexed_log.css', os.path.dirname(fn)) self.f.write('''\ <html> <head> <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> <body> <tt> @@ -273,45 +334,60 @@ class Logfile(object): if not self.last_stream: return self.f.write('</pre>\n') - self.f.write('<div class="stream-trailer" id="' + - self.last_stream.name + '">End stream: ' + + self.f.write('<div class="stream-trailer block-trailer">End stream: ' + self.last_stream.name + '</div>\n') self.f.write('</div>\n') + self.f.write('</div>\n') 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. Args: note_type: The type of note. This must be a value supported by the accompanying multiplexed_log.css. msg: The note/message to log. + anchor: Optional internal link target. Returns: Nothing. """ 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('\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. Args: 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: - Nothing. + Name of the HTML anchor emitted before section. """ self._terminate_stream() self.blocks.append(marker) + if not anchor: + self.anchor += 1 + anchor = str(self.anchor) blk_path = '/'.join(self.blocks) - self.f.write('<div class="section" id="' + blk_path + '">\n') - self.f.write('<div class="section-header" id="' + blk_path + - '">Section: ' + blk_path + '</div>\n') + self.f.write('<div class="section block" id="' + anchor + '">\n') + self.f.write('<div class="section-header block-header">Section: ' + + blk_path + '</div>\n') + self.f.write('<div class="section-content block-content">\n') + + return anchor def end_section(self, marker): """Terminate the current nested section in the log file. @@ -331,12 +407,13 @@ class Logfile(object): (marker, '/'.join(self.blocks))) self._terminate_stream() blk_path = '/'.join(self.blocks) - self.f.write('<div class="section-trailer" id="section-trailer-' + - blk_path + '">End section: ' + blk_path + '</div>\n') + self.f.write('<div class="section-trailer block-trailer">' + + 'End section: ' + blk_path + '</div>\n') + self.f.write('</div>\n') self.f.write('</div>\n') self.blocks.pop() - def section(self, marker): + def section(self, marker, anchor=None): """Create a temporary section in the log file. This function creates a context manager for Python's "with" statement, @@ -349,12 +426,13 @@ class Logfile(object): Args: marker: The name of the nested section. + anchor: The anchor value to pass to start_section(). Returns: A context manager object. """ - return SectionCtxMgr(self, marker) + return SectionCtxMgr(self, marker, anchor) def error(self, msg): """Write an error note to the log file. @@ -404,65 +482,70 @@ class Logfile(object): 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. Args: msg: A message describing the passed test(s). + anchor: Optional internal link target. Returns: 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). Args: msg: A message describing the skipped test(s). + anchor: Optional internal link target. Returns: 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). Args: msg: A message describing the xfailed test(s). + anchor: Optional internal link target. Returns: 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). Args: msg: A message describing the xpassed test(s). + anchor: Optional internal link target. Returns: 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). Args: msg: A message describing the failed test(s). + anchor: Optional internal link target. Returns: Nothing. """ - self._note("status-fail", msg) + self._note("status-fail", msg, anchor) def get_stream(self, name, chained_file=None): """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: self._terminate_stream() - self.f.write('<div class="stream" id="%s">\n' % stream.name) - self.f.write('<div class="stream-header" id="' + stream.name + - '">Stream: ' + stream.name + '</div>\n') + self.f.write('<div class="stream block">\n') + self.f.write('<div class="stream-header block-header">Stream: ' + + stream.name + '</div>\n') + self.f.write('<div class="stream-content block-content">\n') self.f.write('<pre>') if implicit: self.f.write('<span class="implicit">') diff --git a/test/py/u_boot_console_exec_attach.py b/test/py/u_boot_console_exec_attach.py index 19520cb3b9..1be27c1930 100644 --- a/test/py/u_boot_console_exec_attach.py +++ b/test/py/u_boot_console_exec_attach.py @@ -35,11 +35,13 @@ class ConsoleExecAttach(ConsoleBase): # HW flow control would mean this could be infinite. super(ConsoleExecAttach, self).__init__(log, config, max_fifo_fill=16) - self.log.action('Flashing U-Boot') - cmd = ['u-boot-test-flash', config.board_type, config.board_identity] - runner = self.log.get_runner(cmd[0], sys.stdout) - runner.run(cmd) - runner.close() + with self.log.section('flash'): + self.log.action('Flashing U-Boot') + cmd = ['u-boot-test-flash', config.board_type, config.board_identity] + runner = self.log.get_runner(cmd[0], sys.stdout) + runner.run(cmd) + runner.close() + self.log.status_pass('OK') def get_spawn(self): """Connect to a fresh U-Boot instance. |