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 |
Paweł Hajdan Jr.
2014/06/10 08:36:56
Could you make one design change here?
Instead of
Vadim Sh.
2014/06/12 01:00:08
Can you provide any valid use case for pre_run or
Paweł Hajdan Jr.
2014/06/12 11:16:10
Given the three-phase run it seems it would always
Vadim Sh.
2014/06/12 18:34:10
A test can't be synchronous and asynchronous at th
|
+ # 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): |