diff mbox

[U-Boot] test/py: HTML awesome!

Message ID 1454543194-27563-1-git-send-email-swarren@wwwdotorg.org
State Accepted
Commit 83357fd5c2ec11b45607dfe35ba5fe5e44d1eee0
Delegated to: Simon Glass
Headers show

Commit Message

Stephen Warren Feb. 3, 2016, 11:46 p.m. UTC
From: Stephen Warren <swarren@nvidia.com>

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>
---
 test/py/conftest.py                   |  61 ++++++++-------
 test/py/multiplexed_log.css           |  41 +++++-----
 test/py/multiplexed_log.py            | 142 +++++++++++++++++++++++++++-------
 test/py/u_boot_console_exec_attach.py |  12 +--
 4 files changed, 178 insertions(+), 78 deletions(-)

Comments

Simon Glass Feb. 6, 2016, 8:29 p.m. UTC | #1
On 3 February 2016 at 16:46, Stephen Warren <swarren@wwwdotorg.org> wrote:
> From: Stephen Warren <swarren@nvidia.com>
>
> 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>
> ---
>  test/py/conftest.py                   |  61 ++++++++-------
>  test/py/multiplexed_log.css           |  41 +++++-----
>  test/py/multiplexed_log.py            | 142 +++++++++++++++++++++++++++-------
>  test/py/u_boot_console_exec_attach.py |  12 +--
>  4 files changed, 178 insertions(+), 78 deletions(-)

Acked-by: Simon Glass <sjg@chromium.org>
Tested on sandbox:
Tested-by: Simon Glass <sjg@chromium.org>
Simon Glass Feb. 6, 2016, 8:44 p.m. UTC | #2
On 6 February 2016 at 13:29, Simon Glass <sjg@chromium.org> wrote:
> On 3 February 2016 at 16:46, Stephen Warren <swarren@wwwdotorg.org> wrote:
>> From: Stephen Warren <swarren@nvidia.com>
>>
>> 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>
>> ---
>>  test/py/conftest.py                   |  61 ++++++++-------
>>  test/py/multiplexed_log.css           |  41 +++++-----
>>  test/py/multiplexed_log.py            | 142 +++++++++++++++++++++++++++-------
>>  test/py/u_boot_console_exec_attach.py |  12 +--
>>  4 files changed, 178 insertions(+), 78 deletions(-)
>
> Acked-by: Simon Glass <sjg@chromium.org>
> Tested on sandbox:
> Tested-by: Simon Glass <sjg@chromium.org>

Applied to u-boot-dm, thanks!
diff mbox

Patch

diff --git a/test/py/conftest.py b/test/py/conftest.py
index 84f71d47db6a..f55bf68686ba 100644
--- a/test/py/conftest.py
+++ b/test/py/conftest.py
@@ -123,10 +123,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
@@ -302,6 +304,7 @@  def u_boot_console(request):
     console.ensure_spawned()
     return console
 
+anchors = {}
 tests_not_run = set()
 tests_failed = set()
 tests_xpassed = set()
@@ -341,27 +344,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)
 
@@ -427,7 +436,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 f6240d52da66..f135b10a24c9 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 69a577e57720..68917eb0ea96 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 19520cb3b907..1be27c193079 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.