[testrunner] enable the progress indicator

Using test generators meant that we had to remove the progress indicator since
the total number of tests weren't known before-hand.

This CL implements a progress indicator using test number estimations.

cctest and unittests progress indicator is accurate, however estimating
means the progress will terminate over 100% in big test suites and sometimes
under 100%.

R=machenbach@chromium.org
CC=​sergiyb@chromium.org,yangguo@chromium.org

Bug: v8:8769
Change-Id: I40ca5b40f9b1223376d33707f0945900ea98cea3
Reviewed-on: https://chromium-review.googlesource.com/c/1460471
Commit-Queue: Tamer Tas <tmrts@chromium.org>
Reviewed-by: Michael Achenbach <machenbach@chromium.org>
Cr-Commit-Position: refs/heads/master@{#59538}
This commit is contained in:
Tamer Tas 2019-02-12 16:30:16 +01:00 committed by Commit Bot
parent 9d863741fd
commit baeb4e324d
9 changed files with 115 additions and 37 deletions

View File

@ -38,7 +38,10 @@ class VariantsGenerator(testsuite.VariantsGenerator):
class TestLoader(testsuite.TestLoader): class TestLoader(testsuite.TestLoader):
pass def _list_test_filenames(self):
for file in os.listdir(self.suite.root):
if file.endswith(".pyt"):
yield file[:-4]
# TODO(tmrts): refactor the python template parsing then use the TestLoader. # TODO(tmrts): refactor the python template parsing then use the TestLoader.
@ -68,11 +71,12 @@ class TestSuite(testsuite.TestSuite):
def ListTests(self): def ListTests(self):
result = [] result = []
# Find all .pyt files in this directory. filenames = self._test_loader._list_test_filenames()
filenames = [f[:-4] for f in os.listdir(self.root) if f.endswith(".pyt")] for f in sorted(filenames):
filenames.sort()
for f in filenames:
self._ParsePythonTestTemplates(result, f) self._ParsePythonTestTemplates(result, f)
# TODO: remove after converting to use a full TestLoader
self._test_loader.test_count_estimation = len(result)
return result return result
def _create_test(self, path, source, template_flags): def _create_test(self, path, source, template_flags):

View File

@ -610,7 +610,9 @@ class BaseTestRunner(object):
names = self._args_to_suite_names(args, options.test_root) names = self._args_to_suite_names(args, options.test_root)
test_config = self._create_test_config(options) test_config = self._create_test_config(options)
variables = self._get_statusfile_variables(options) variables = self._get_statusfile_variables(options)
slow_chain, fast_chain = [], []
# Head generator with no elements
test_chain = testsuite.TestGenerator(0, [], [])
for name in names: for name in names:
if options.verbose: if options.verbose:
print '>>> Loading test suite: %s' % name print '>>> Loading test suite: %s' % name
@ -618,17 +620,10 @@ class BaseTestRunner(object):
os.path.join(options.test_root, name), test_config) os.path.join(options.test_root, name), test_config)
if self._is_testsuite_supported(suite, options): if self._is_testsuite_supported(suite, options):
slow_tests, fast_tests = suite.load_tests_from_disk(variables) tests = suite.load_tests_from_disk(variables)
slow_chain.append(slow_tests) test_chain.merge(tests)
fast_chain.append(fast_tests)
for tests in slow_chain: return test_chain
for test in tests:
yield test
for tests in fast_chain:
for test in tests:
yield test
def _is_testsuite_supported(self, suite, options): def _is_testsuite_supported(self, suite, options):
"""A predicate that can be overridden to filter out unsupported TestSuite """A predicate that can be overridden to filter out unsupported TestSuite
@ -762,13 +757,20 @@ class BaseTestRunner(object):
return shard_run, shard_count return shard_run, shard_count
def _create_progress_indicators(self, options): def _create_progress_indicators(self, test_count, options):
procs = [PROGRESS_INDICATORS[options.progress]()] procs = [PROGRESS_INDICATORS[options.progress]()]
if options.json_test_results: if options.json_test_results:
procs.append(progress.JsonTestProgressIndicator( procs.append(progress.JsonTestProgressIndicator(
options.json_test_results, options.json_test_results,
self.build_config.arch, self.build_config.arch,
self.mode_options.execution_mode)) self.mode_options.execution_mode))
for proc in procs:
try:
proc.set_test_count(test_count)
except AttributeError:
pass
return procs return procs
def _create_result_tracker(self, options): def _create_result_tracker(self, options):

View File

@ -9,7 +9,18 @@ from testrunner.local import testsuite, statusfile
class TestLoader(testsuite.TestLoader): class TestLoader(testsuite.TestLoader):
pass def _list_test_filenames(self):
return ["fast", "slow"]
def list_tests(self):
self.test_count_estimation = 2
fast = self._create_test("fast", self.suite)
slow = self._create_test("slow", self.suite)
slow._statusfile_outcomes.append(statusfile.SLOW)
yield fast
yield slow
class TestSuite(testsuite.TestSuite): class TestSuite(testsuite.TestSuite):
def _test_loader_class(self): def _test_loader_class(self):
@ -18,12 +29,5 @@ class TestSuite(testsuite.TestSuite):
def _test_class(self): def _test_class(self):
return testsuite.TestCase return testsuite.TestCase
def ListTests(self):
fast = self._test_loader._create_test("fast", self)
slow = self._test_loader._create_test("slow", self)
slow._statusfile_outcomes.append(statusfile.SLOW)
yield fast
yield slow
def GetSuite(*args, **kwargs): def GetSuite(*args, **kwargs):
return TestSuite(*args, **kwargs) return TestSuite(*args, **kwargs)

View File

@ -28,6 +28,7 @@
import fnmatch import fnmatch
import imp import imp
import itertools
import os import os
from contextlib import contextmanager from contextlib import contextmanager
@ -88,6 +89,7 @@ class TestLoader(object):
self.test_class = test_class self.test_class = test_class
self.test_config = test_config self.test_config = test_config
self.test_root = test_root self.test_root = test_root
self.test_count_estimation = len(list(self._list_test_filenames()))
def _list_test_filenames(self): def _list_test_filenames(self):
"""Implemented by the subclassed TestLoaders to list filenames. """Implemented by the subclassed TestLoaders to list filenames.
@ -199,6 +201,34 @@ class JSTestLoader(GenericTestLoader):
return ".js" return ".js"
class TestGenerator(object):
def __init__(self, test_count_estimate, slow_tests, fast_tests):
self.test_count_estimate = test_count_estimate
self.slow_tests = slow_tests
self.fast_tests = fast_tests
self._rebuild_iterator()
def _rebuild_iterator(self):
self._iterator = itertools.chain(self.slow_tests, self.fast_tests)
def __iter__(self):
return self
def __next__(self):
return self.next()
def next(self):
return next(self._iterator)
def merge(self, test_generator):
self.test_count_estimate += test_generator.test_count_estimate
self.slow_tests = itertools.chain(
self.slow_tests, test_generator.slow_tests)
self.fast_tests = itertools.chain(
self.fast_tests, test_generator.fast_tests)
self._rebuild_iterator()
@contextmanager @contextmanager
def _load_testsuite_module(name, root): def _load_testsuite_module(name, root):
f = None f = None
@ -236,14 +266,22 @@ class TestSuite(object):
def ListTests(self): def ListTests(self):
return self._test_loader.list_tests() return self._test_loader.list_tests()
def __initialize_test_count_estimation(self):
# Retrieves a single test to initialize the test generator.
next(iter(self.ListTests()))
def __calculate_test_count(self):
self.__initialize_test_count_estimation()
return self._test_loader.test_count_estimation
def load_tests_from_disk(self, statusfile_variables): def load_tests_from_disk(self, statusfile_variables):
self.statusfile = statusfile.StatusFile( self.statusfile = statusfile.StatusFile(
self.status_file(), statusfile_variables) self.status_file(), statusfile_variables)
test_count = self.__calculate_test_count()
slow_tests = (test for test in self.ListTests() if test.is_slow) slow_tests = (test for test in self.ListTests() if test.is_slow)
fast_tests = (test for test in self.ListTests() if not test.is_slow) fast_tests = (test for test in self.ListTests() if not test.is_slow)
return TestGenerator(test_count, slow_tests, fast_tests)
return slow_tests, fast_tests
def get_variants_gen(self, variants): def get_variants_gen(self, variants):
return self._variants_gen_class()(variants) return self._variants_gen_class()(variants)

View File

@ -3,6 +3,7 @@
# Use of this source code is governed by a BSD-style license that can be # Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file. # found in the LICENSE file.
import itertools
import os import os
import sys import sys
import tempfile import tempfile
@ -13,7 +14,7 @@ TOOLS_PATH = os.path.dirname(os.path.dirname(os.path.dirname(
os.path.abspath(__file__)))) os.path.abspath(__file__))))
sys.path.append(TOOLS_PATH) sys.path.append(TOOLS_PATH)
from testrunner.local.testsuite import TestSuite from testrunner.local.testsuite import TestSuite, TestGenerator
from testrunner.objects.testcase import TestCase from testrunner.objects.testcase import TestCase
from testrunner.test_config import TestConfig from testrunner.test_config import TestConfig
@ -47,21 +48,38 @@ class TestSuiteTest(unittest.TestCase):
self.assertIsNone(self.suite.statusfile) self.assertIsNone(self.suite.statusfile)
def testLoadingTestsFromDisk(self): def testLoadingTestsFromDisk(self):
slow_tests, fast_tests = self.suite.load_tests_from_disk( tests = self.suite.load_tests_from_disk(
statusfile_variables={}) statusfile_variables={})
def is_generator(iterator): def is_generator(iterator):
return iterator == iter(iterator) return iterator == iter(iterator)
self.assertTrue(is_generator(slow_tests)) self.assertTrue(is_generator(tests))
self.assertTrue(is_generator(fast_tests)) self.assertEquals(tests.test_count_estimate, 2)
slow_tests, fast_tests = list(slow_tests), list(fast_tests) slow_tests, fast_tests = list(tests.slow_tests), list(tests.fast_tests)
# Verify that the components of the TestSuite are loaded. # Verify that the components of the TestSuite are loaded.
self.assertTrue(len(slow_tests) == len(fast_tests) == 1) self.assertTrue(len(slow_tests) == len(fast_tests) == 1)
self.assertTrue(all(test.is_slow for test in slow_tests)) self.assertTrue(all(test.is_slow for test in slow_tests))
self.assertFalse(any(test.is_slow for test in fast_tests)) self.assertFalse(any(test.is_slow for test in fast_tests))
self.assertIsNotNone(self.suite.statusfile) self.assertIsNotNone(self.suite.statusfile)
def testMergingTestGenerators(self):
tests = self.suite.load_tests_from_disk(
statusfile_variables={})
more_tests = self.suite.load_tests_from_disk(
statusfile_variables={})
# Merge the test generators
tests.merge(more_tests)
self.assertEquals(tests.test_count_estimate, 4)
# Check the tests are sorted by speed
test_speeds = []
for test in tests:
test_speeds.append(test.is_slow)
self.assertEquals(test_speeds, [True, True, False, False])
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View File

@ -134,7 +134,8 @@ class NumFuzzer(base_runner.BaseTestRunner):
results = self._create_result_tracker(options) results = self._create_result_tracker(options)
execproc = ExecutionProc(options.j) execproc = ExecutionProc(options.j)
sigproc = self._create_signal_proc() sigproc = self._create_signal_proc()
indicators = self._create_progress_indicators(options) indicators = self._create_progress_indicators(
tests.test_count_estimate, options)
procs = [ procs = [
loader, loader,
NameFilterProc(args) if args else None, NameFilterProc(args) if args else None,

View File

@ -283,7 +283,8 @@ class StandardTestRunner(base_runner.BaseTestRunner):
print '>>> Running with test processors' print '>>> Running with test processors'
loader = LoadProc(tests) loader = LoadProc(tests)
results = self._create_result_tracker(options) results = self._create_result_tracker(options)
indicators = self._create_progress_indicators(options) indicators = self._create_progress_indicators(
tests.test_count_estimate, options)
outproc_factory = None outproc_factory = None
if self.build_config.predictable: if self.build_config.predictable:

View File

@ -162,6 +162,9 @@ class CompactProgressIndicator(ProgressIndicator):
self._passed = 0 self._passed = 0
self._failed = 0 self._failed = 0
def set_test_count(self, test_count):
self._total = test_count
def _on_result_for(self, test, result): def _on_result_for(self, test, result):
# TODO(majeski): Support for dummy/grouped results # TODO(majeski): Support for dummy/grouped results
if result.has_unexpected_output: if result.has_unexpected_output:
@ -195,8 +198,13 @@ class CompactProgressIndicator(ProgressIndicator):
def _print_progress(self, name): def _print_progress(self, name):
self._clear_line(self._last_status_length) self._clear_line(self._last_status_length)
elapsed = time.time() - self._start_time elapsed = time.time() - self._start_time
if self._total:
progress = (self._passed + self._failed) * 100 // self._total
else:
progress = 0
status = self._templates['status_line'] % { status = self._templates['status_line'] % {
'passed': self._passed, 'passed': self._passed,
'progress': progress,
'failed': self._failed, 'failed': self._failed,
'test': name, 'test': name,
'mins': int(elapsed) / 60, 'mins': int(elapsed) / 60,
@ -221,6 +229,7 @@ class ColorProgressIndicator(CompactProgressIndicator):
def __init__(self): def __init__(self):
templates = { templates = {
'status_line': ("[%(mins)02i:%(secs)02i|" 'status_line': ("[%(mins)02i:%(secs)02i|"
"\033[34m%%%(progress) 4d\033[0m|"
"\033[32m+%(passed) 4d\033[0m|" "\033[32m+%(passed) 4d\033[0m|"
"\033[31m-%(failed) 4d\033[0m]: %(test)s"), "\033[31m-%(failed) 4d\033[0m]: %(test)s"),
'stdout': "\033[1m%s\033[0m", 'stdout': "\033[1m%s\033[0m",
@ -235,7 +244,7 @@ class ColorProgressIndicator(CompactProgressIndicator):
class MonochromeProgressIndicator(CompactProgressIndicator): class MonochromeProgressIndicator(CompactProgressIndicator):
def __init__(self): def __init__(self):
templates = { templates = {
'status_line': ("[%(mins)02i:%(secs)02i|" 'status_line': ("[%(mins)02i:%(secs)02i|%%%(progress) 4d|"
"+%(passed) 4d|-%(failed) 4d]: %(test)s"), "+%(passed) 4d|-%(failed) 4d]: %(test)s"),
'stdout': '%s', 'stdout': '%s',
'stderr': '%s', 'stderr': '%s',

View File

@ -622,10 +622,11 @@ class SystemTest(unittest.TestCase):
infra_staging=False, infra_staging=False,
) )
if name == 'color': if name == 'color':
expected = ('\033[32m+ 1\033[0m|' expected = ('\033[34m% 28\033[0m|'
'\033[32m+ 1\033[0m|'
'\033[31m- 1\033[0m]: Done') '\033[31m- 1\033[0m]: Done')
else: else:
expected = '+ 1|- 1]: Done' expected = '% 28|+ 1|- 1]: Done'
self.assertIn(expected, result.stdout) self.assertIn(expected, result.stdout)
self.assertIn('sweet/cherries', result.stdout) self.assertIn('sweet/cherries', result.stdout)
self.assertIn('sweet/bananas', result.stdout) self.assertIn('sweet/bananas', result.stdout)