Bug 776184 - Simplify the progressbar interface; r=sfink DONTBUILD
authorTerrence Cole <terrence@mozilla.com>
Fri, 20 Jul 2012 18:33:29 -0700
changeset 100484 56a19b273c2b94197c94755d853285c10b6e5056
parent 100483 141484fd85681a0c4d4b27b3706e90a998fe5f28
child 100485 f9861971bbf83d05cc37c71c19d9c988a1e6846e
push id23182
push useremorley@mozilla.com
push dateThu, 26 Jul 2012 12:04:56 +0000
treeherdermozilla-central@20db7c6d82cc [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssfink
bugs776184
milestone17.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 776184 - Simplify the progressbar interface; r=sfink DONTBUILD This also fixes --no-progress and unifies the definition of a tty with jittest so that we can add color support to the output.
js/src/tests/jstests.py
js/src/tests/lib/progressbar.py
js/src/tests/lib/results.py
js/src/tests/lib/tasks_unix.py
--- a/js/src/tests/jstests.py
+++ b/js/src/tests/jstests.py
@@ -6,16 +6,17 @@ See the adjacent README.txt for more det
 """
 
 import os, sys
 from subprocess import list2cmdline, call
 
 from lib.results import NullTestOutput
 from lib.tests import TestCase
 from lib.results import ResultsSink
+from lib.progressbar import ProgressBar
 
 if (sys.platform.startswith('linux') or
     sys.platform.startswith('darwin')
    ):
     from lib.tasks_unix import run_all_tests
 else:
     from lib.tasks_win import run_all_tests
 
@@ -142,18 +143,21 @@ def parse_args():
     options.output_fp = sys.stdout
     if options.output_file and (options.show_cmd or options.show_output):
         try:
             options.output_fp = open(options.output_file, 'w')
         except IOError, ex:
             raise SystemExit("Failed to open output file: " + str(ex))
 
     # Hide the progress bar if it will get in the way of other output.
-    options.hide_progress = ((options.show_cmd or options.show_output) and
-                             options.output_fp == sys.stdout or options.tinderbox)
+    options.hide_progress = (((options.show_cmd or options.show_output) and
+                              options.output_fp == sys.stdout) or
+                             options.tinderbox or
+                             ProgressBar.conservative_isatty() or
+                             options.hide_progress)
 
     return (options, requested_paths, excluded_paths)
 
 def load_tests(options, requested_paths, excluded_paths):
     """
     Returns a tuple: (skipped_tests, test_list)
         skip_list: [iterable<Test>] Tests found but skipped.
         test_list: [iterable<Test>] Tests found that should be run.
--- a/js/src/tests/lib/progressbar.py
+++ b/js/src/tests/lib/progressbar.py
@@ -1,36 +1,93 @@
 # Text progress bar library, like curl or scp.
 
-import sys, datetime
+import sys
+from datetime import datetime
+
+class NullProgressBar:
+    def update(self, current, data): pass
+    def poke(self): pass
+    def finish(self, complete=True): pass
+    def message(self, msg): sys.stdout.write(msg + '\n')
 
 class ProgressBar:
-    def __init__(self, label, limit, label_width=12):
-        self.label = label
-        self.limit = limit
-        self.label_width = label_width
-        self.cur = 0
-        self.t0 = datetime.datetime.now()
+    GREEN = '\x1b[32;1m'
+    RED   = '\x1b[31;1m'
+    BLUE  = '\x1b[34;1m'
+    GRAY  = '\x1b[37;2m'
+
+    RESET = '\x1b[0m'
+
+    CLEAR_RIGHT = '\x1b[K'
+
+    def __init__(self, limit, fmt):
+        assert not self.conservative_isatty()
+        assert limit < 9999
+
+        self.prior = None
+        self.counters_fmt = fmt # [{str:str}] Describtion of how to lay out each
+                                #             field in the counters map.
+        self.limit = limit # int: The value of 'current' equal to 100%.
+        self.t0 = datetime.now() # datetime: The start time.
+
+        # Compute the width of the counters and build the format string.
+        self.counters_width = 1 # [
+        for layout in self.counters_fmt:
+            self.counters_width += 4 # Less than 9999 tests total.
+            self.counters_width += 1 # | (or ']' for the last one)
 
-        self.barlen = 64 - self.label_width
-        self.fmt = '\r%-' + str(label_width) + 's %3d%% %-' + str(self.barlen) + 's| %6.1fs'
+        self.barlen = 64 - self.counters_width
+        self.fmt = ('\r%-' + str(self.counters_width) + 's %3d%% %-' +
+                    str(self.barlen) + 's| %6.1fs' + self.CLEAR_RIGHT)
+
+    def update(self, current, data):
+        # Record prior for poke.
+        self.prior = (current, data)
 
-    def update(self, value):
-        self.cur = value
-        pct = int(100.0 * self.cur / self.limit)
-        barlen = int(1.0 * self.barlen * self.cur / self.limit) - 1
+        # Build counters string.
+        counters = '['
+        for layout in self.counters_fmt:
+            counters += layout['color'] + ('%4d' % data[layout['value']]) + self.RESET
+            counters += '|'
+        counters = counters[:-1] + ']'
+
+        # Build the bar.
+        pct = int(100.0 * current / self.limit)
+        barlen = int(1.0 * self.barlen * current / self.limit) - 1
         bar = '='*barlen + '>'
-        dt = datetime.datetime.now() - self.t0
+
+        # Update the bar.
+        dt = datetime.now() - self.t0
         dt = dt.seconds + dt.microseconds * 1e-6
-        sys.stdout.write(self.fmt % (self.label[:self.label_width], pct, bar, dt))
+        sys.stdout.write(self.fmt % (counters, pct, bar, dt))
+
+        # Force redisplay, since we didn't write a \n.
         sys.stdout.flush()
 
+    def poke(self):
+        if not self.prior:
+            return
+        self.update(*self.prior)
+
     def finish(self, complete=True):
-        final_count = self.limit if complete else self.cur
-        self.update(final_count)
+        final_count = self.limit if complete else self.prior[0]
+        self.update(final_count, self.prior[1])
         sys.stdout.write('\n')
 
-if __name__ == '__main__':
-    pb = ProgressBar('test', 12)
-    for i in range(12):
-        pb.update(i)
-        time.sleep(0.5)
-    pb.finish()
+    def message(self, msg):
+        sys.stdout.write('\n')
+        sys.stdout.write(msg)
+        sys.stdout.write('\n')
+
+    @staticmethod
+    def conservative_isatty():
+        """
+        Prefer erring on the side of caution and not using terminal commands if
+        the current output stream may be a file.  We explicitly check for the
+        Android platform because terminal commands work poorly over ADB's
+        redirection.
+        """
+        try:
+            import android
+        except ImportError:
+            return False
+        return sys.stdout.isatty()
--- a/js/src/tests/lib/results.py
+++ b/js/src/tests/lib/results.py
@@ -1,11 +1,11 @@
 import re
 from subprocess import list2cmdline
-from progressbar import ProgressBar
+from progressbar import NullProgressBar, ProgressBar
 
 class TestOutput:
     """Output from a test run."""
     def __init__(self, test, cmd, out, err, rc, dt, timed_out):
         self.test = test   # Test
         self.cmd = cmd     # str:   command line of test
         self.out = out     # str:   stdout
         self.err = err     # str:   stderr
@@ -81,19 +81,26 @@ class ResultsSink:
     def __init__(self, options, testcount):
         self.options = options
         self.fp = options.output_fp
 
         self.groups = {}
         self.counts = {'PASS': 0, 'FAIL': 0, 'TIMEOUT': 0, 'SKIP': 0}
         self.n = 0
 
-        self.pb = None
-        if not options.hide_progress:
-            self.pb = ProgressBar('', testcount, 21)
+        if options.hide_progress:
+            self.pb = NullProgressBar()
+        else:
+            fmt = [
+                {'value': 'PASS',    'color': ProgressBar.GREEN},
+                {'value': 'FAIL',    'color': ProgressBar.RED},
+                {'value': 'TIMEOUT', 'color': ProgressBar.BLUE},
+                {'value': 'SKIP',    'color': ProgressBar.GRAY},
+            ]
+            self.pb = ProgressBar(testcount, fmt)
 
     def push(self, output):
         if output.timed_out:
             self.counts['TIMEOUT'] += 1
         if isinstance(output, NullTestOutput):
             if self.options.tinderbox:
                 self.print_tinderbox_result('TEST-KNOWN-FAIL', output.test.path, time=output.dt, skip=True)
             self.counts['SKIP'] += 1
@@ -131,32 +138,24 @@ class ResultsSink:
                             label = 'TEST-PASS (EXPECTED RANDOM)'
                         self.print_tinderbox_result(label, result.test.path, time=output.dt, message=msg)
                 self.print_tinderbox_result(self.LABELS[
                     (result.result, result.test.expect, result.test.random)][0],
                     result.test.path, time=output.dt)
                 return
 
             if dev_label:
-                if self.pb:
-                    self.fp.write("\n")
                 def singular(label):
                     return "FIXED" if label == "FIXES" else label[:-1]
-                print >> self.fp, "%s - %s" % (singular(dev_label), output.test.path)
+                self.pb.message("%s - %s" % (singular(dev_label), output.test.path))
 
-        if self.pb:
-            self.pb.label = '[%4d|%4d|%4d|%4d]'% (self.counts['PASS'],
-                                                  self.counts['FAIL'],
-                                                  self.counts['TIMEOUT'],
-                                                  self.counts['SKIP'])
-            self.pb.update(self.n)
+        self.pb.update(self.n, self.counts)
 
     def finish(self, completed):
-        if self.pb:
-            self.pb.finish(completed)
+        self.pb.finish(completed)
         if not self.options.tinderbox:
             self.list(completed)
 
     # Conceptually, this maps (test result x test expection) to text labels.
     #      key   is (result, expect, random)
     #      value is (tinderbox label, dev test category)
     LABELS = {
         (TestResult.CRASH, False, False): ('TEST-UNEXPECTED-FAIL',               'REGRESSIONS'),
--- a/js/src/tests/lib/tasks_unix.py
+++ b/js/src/tests/lib/tasks_unix.py
@@ -56,18 +56,17 @@ def get_max_wait(tasks, results, timeout
 
     # The test harness uses a timeout of 0 to indicate we should wait forever,
     # but for select(), a timeout of 0 indicates a zero-length wait.  Instead,
     # translate the timeout into None to tell select to wait forever.
     if wait == 0:
         return None
 
     # If we have a progress-meter, we need to wake up to update it frequently.
-    if results.pb is not None:
-        wait = min(wait, PROGRESS_BAR_GRANULARITY)
+    wait = min(wait, PROGRESS_BAR_GRANULARITY)
 
     return wait
 
 def flush_input(fd, frags):
     """
     Read any pages sitting in the file descriptor 'fd' into the list 'frags'.
     """
     rv = os.read(fd, 4096)
@@ -190,13 +189,12 @@ def run_all_tests(tests, results, option
             tasks.append(spawn_test(tests.pop()))
 
         timeout = get_max_wait(tasks, results, options.timeout)
         read_input(tasks, timeout)
 
         kill_undead(tasks, results, options.timeout)
         tasks = reap_zombies(tasks, results, options.timeout)
 
-        if results.pb:
-            results.pb.update(results.n)
+        results.pb.poke()
 
     return True