| Index: scripts/slave/recipe_modules/test_utils/api.py
|
| diff --git a/scripts/slave/recipe_modules/test_utils/api.py b/scripts/slave/recipe_modules/test_utils/api.py
|
| index a91b801feb820c1bbfeac33cb411ff90788e3e52..de876ff8bfcbedfd401b947bcd67e4bbf8cb5c71 100644
|
| --- a/scripts/slave/recipe_modules/test_utils/api.py
|
| +++ b/scripts/slave/recipe_modules/test_utils/api.py
|
| @@ -40,6 +40,13 @@ class TestUtilsApi(recipe_api.RecipeApi):
|
| Base class for tests that can be retried after deapplying a previously
|
| applied patch.
|
| """
|
| + # If True, the test supports asynchronous execution. In that case 'trigger'
|
| + # and 'collect' will be used instead of 'run'. 'trigger' produces a step
|
| + # that asynchronously launches the test (just starts the test and
|
| + # immediately returns control back to the recipe), and 'collect' produces
|
| + # a step that blocks until the test is finished. The recipe can execute any
|
| + # other steps in between. This mode is used by tests running on Swarming.
|
| + async = False
|
|
|
| @property
|
| def name(self): # pragma: no cover
|
| @@ -50,6 +57,14 @@ class TestUtilsApi(recipe_api.RecipeApi):
|
| """Run the test. suffix is 'with patch' or 'without patch'."""
|
| raise NotImplementedError()
|
|
|
| + def trigger(self, suffix): # pragma: no cover
|
| + """Launch the test asynchronously, used if self.async == True."""
|
| + raise NotImplementedError()
|
| +
|
| + def collect(self, suffix): # pragma: no cover
|
| + """Wait for triggered test to finish, used if self.async == True."""
|
| + raise NotImplementedError()
|
| +
|
| def has_valid_results(self, suffix): # pragma: no cover
|
| """
|
| Returns True if results (failures) are valid.
|
| @@ -79,6 +94,9 @@ class TestUtilsApi(recipe_api.RecipeApi):
|
| deapply_patch_fn - function that takes a list of failing tests
|
| and undoes any effect of the previously applied patch
|
| """
|
| + # Convert iterable to list, since it is enumerated multiple times.
|
| + tests = list(tests)
|
| +
|
| if self.m.step_history.failed:
|
| yield self.m.python.inline(
|
| 'Aborting due to failed build state.',
|
| @@ -86,7 +104,24 @@ class TestUtilsApi(recipe_api.RecipeApi):
|
| always_run=True, abort_on_failure=True)
|
| return # won't actually hit this, but be explicit
|
|
|
| - yield (t.run('with patch') for t in tests)
|
| + def run(prefix, tests):
|
| + """Runs synchronous and asynchronous tests (at the same time).
|
| +
|
| + Asynchronous tests are launched first (just launched, not being blocked
|
| + on). While they are running, the recipe blocks on synchronous tests
|
| + (sequentially, one by one). And then finally waits for all asynchronous
|
| + tests to finish. Effectively asynchronous tests are running in parallel
|
| + with each other and with synchronous tests.
|
| + """
|
| + # Trigger all async tests first, so that they are running in parallel
|
| + # with synchronous tests.
|
| + yield (t.trigger(prefix) for t in tests if t.async)
|
| + # Now block on all synchronous tests.
|
| + yield (t.run(prefix) for t in tests if not t.async)
|
| + # And finally wait for all pending asynchronous tests to complete.
|
| + yield (t.collect(prefix) for t in tests if t.async)
|
| +
|
| + yield run('with patch', tests)
|
|
|
| failing_tests = []
|
| for t in tests:
|
| @@ -106,7 +141,7 @@ class TestUtilsApi(recipe_api.RecipeApi):
|
|
|
| yield deapply_patch_fn(failing_tests)
|
|
|
| - yield (t.run('without patch') for t in failing_tests)
|
| + yield run('without patch', failing_tests)
|
| yield (self._summarize_retried_test(t) for t in failing_tests)
|
|
|
| def _summarize_retried_test(self, test):
|
|
|