Index: scripts/slave/annotated_run.py |
diff --git a/scripts/slave/annotated_run.py b/scripts/slave/annotated_run.py |
index 1c0f7bb69d9f84db336fdb70d86d4b07dc35b157..769977c66d12643f6e302eb61aa0862ee4d7cc3e 100755 |
--- a/scripts/slave/annotated_run.py |
+++ b/scripts/slave/annotated_run.py |
@@ -205,6 +205,11 @@ class StepData(object): |
def retcode(self): |
return self._retcode |
+ @retcode.setter |
+ def retcode(self, val): |
+ assert self._retcode is None, 'Can\'t override already-defined retcode' |
+ self._retcode = val |
+ |
@property |
def presentation(self): |
return self._presentation |
@@ -447,7 +452,6 @@ class RecipeEngine(object): |
Recipe modules that are aware of the engine: |
* properties - uses engine.properties. |
- * step_history - uses engine.step_history. |
* step - uses engine.create_step(...). |
This class acts mostly as a documentation of expected public engine interface. |
@@ -495,6 +499,13 @@ class RecipeEngine(object): |
raise NotImplementedError |
+def _merge(*dicts): |
+ result = {} |
+ for d in dicts: |
+ result.update(d) |
+ return result |
+ |
+ |
class SequentialRecipeEngine(RecipeEngine): |
"""Always runs step sequentially. Currently the engine used by default.""" |
def __init__(self, stream, properties, test_data): |
@@ -502,10 +513,11 @@ class SequentialRecipeEngine(RecipeEngine): |
self._stream = stream |
self._properties = properties |
self._test_data = test_data |
- self._step_history = collections.OrderedDict() |
+ self._step_results = collections.OrderedDict() |
+ self._step_disambiguation_index = {} |
- self._previous_step_annotation = None |
- self._previous_step_result = None |
+ self._annotation = None |
+ self._step_result = None |
self._api = None |
@property |
@@ -515,14 +527,14 @@ class SequentialRecipeEngine(RecipeEngine): |
@property |
def previous_step_result(self): |
"""Allows api.step to get the active result from any context.""" |
- return self._previous_step_result |
+ return self._step_result |
def _emit_results(self): |
- annotation = self._previous_step_annotation |
- step_result = self._previous_step_result |
+ annotation = self._annotation |
+ step_result = self._step_result |
- self._previous_step_annotation = None |
- self._previous_step_result = None |
+ self._annotation = None |
+ self._step_result = None |
if not annotation or not step_result: |
return |
@@ -539,47 +551,124 @@ class SequentialRecipeEngine(RecipeEngine): |
step_result._step['~followup_annotations'] = lines |
annotation.step_ended() |
- def run_step(self, step): |
- ok_ret = step.pop('ok_ret') |
- infra_step = step.pop('infra_step') |
+ def _disambiguate_name(self, step_name): |
+ if step_name in self._step_disambiguation_index: |
+ self._step_disambiguation_index[step_name] += 1 |
+ step_name += ' (%s)' % self._step_disambiguation_index[step_name] |
+ else: |
+ self._step_disambiguation_index[step_name] = 1 |
+ return step_name |
- test_data_fn = step.pop('step_test_data', recipe_test_api.StepTestData) |
- step_test = self._test_data.pop_step_test_data(step['name'], |
- test_data_fn) |
- placeholders = render_step(step, step_test) |
+ def _disambiguate_step(self, step): |
+ """Disambiguates step (destructively) by adding an index afterward. E.g. |
- self._step_history[step['name']] = step |
- self._emit_results() |
+ gclient sync |
+ gclient sync (2) |
+ ... |
+ """ |
+ step['name'] = self._disambiguate_name(step['name']) |
- step_result = None |
+ def _subannotator(self): |
+ class Subannotator(object): |
+ # We use ann as the self argument because we are closing over the |
+ # SequentialRecipeEngine self. |
+ # pylint: disable=e0213 |
+ def BUILD_STEP(ann, name): |
+ self._open_step({'name': self._disambiguate_name(name)}) |
- if not self._test_data.enabled: |
- self._previous_step_annotation, retcode = annotator.run_step( |
- self._stream, **step) |
+ def STEP_WARNINGS(ann): |
+ self._step_result.presentation.status = 'WARNING' |
+ def STEP_FAILURE(ann): |
+ self._step_result.presentation.status = 'FAILURE' |
+ def STEP_EXCEPTION(ann): |
+ self._step_result.presentation.status = 'EXCEPTION' |
+ |
+ def STEP_TEXT(ann, msg): |
+ self._step_result.presentation.step_text = msg |
+ |
+ def STEP_LINK(ann, link_label, link_url): |
+ self._step_result.presentation.links[link_label] = link_url |
+ |
+ def STEP_LOG_LINE(ann, log_label, log_line): |
+ self._step_result.presentation.logs[log_label] += log_line |
+ def STEP_LOG_END(ann, log_label): |
+ # We do step finalization all at once. |
+ pass |
+ |
+ def SET_BUILD_PROPERTY(ann, name, value): |
+ self._step_result.presentation.properties[name] = value |
+ |
+ def STEP_SUMMARY_TEXT(ann, msg): |
+ self._step_result.presentation.step_summary_text = msg |
- step_result = StepData(step, retcode) |
- self._previous_step_annotation.annotation_stream.step_cursor(step['name']) |
+ return Subannotator() |
+ |
+ def _open_step(self, step): |
+ self._emit_results() |
+ step_result = StepData(step, None) |
+ self._step_results[step['name']] = step_result |
+ self._step_result = step_result |
+ self._annotation = self._stream.step(step['name']) |
+ self._annotation.step_started() |
+ if self._test_data.enabled: |
+ self._annotation.stream = cStringIO.StringIO() |
+ |
+ def _step_kernel(self, step, step_test, subannotator=None): |
+ if not self._test_data.enabled: |
+ # Warning: run_step can change the current self._annotation and |
+ # self._step_result if it uses a subannotator. |
+ retcode = annotator.run_step( |
+ self._stream, |
+ step_annotation=self._annotation, |
+ subannotator=subannotator, |
+ **step) |
+ self._step_result.retcode = retcode |
+ # TODO(luqui): What is the purpose of this line? |
+ self._annotation.annotation_stream.step_cursor( |
+ self._step_result.step['name']) |
else: |
- self._previous_step_annotation = annotation = self._stream.step( |
- step['name']) |
- annotation.step_started() |
try: |
- annotation.stream = cStringIO.StringIO() |
- |
- step_result = StepData(step, step_test.retcode) |
+ self._step_result.retcode = step_test.retcode |
except OSError: |
exc_type, exc_value, exc_tb = sys.exc_info() |
trace = traceback.format_exception(exc_type, exc_value, exc_tb) |
trace_lines = ''.join(trace).split('\n') |
- annotation.write_log_lines('exception', filter(None, trace_lines)) |
- annotation.step_exception() |
+ self._annotation.write_log_lines( |
+ 'exception', filter(None, trace_lines)) |
+ self._annotation.step_exception() |
+ |
+ return self._step_result.retcode |
+ |
+ def run_step(self, step): |
+ self._disambiguate_step(step) |
+ ok_ret = step.pop('ok_ret') |
+ infra_step = step.pop('infra_step') |
+ allow_subannotations = step.get('allow_subannotations', False) |
+ |
+ test_data_fn = step.pop('step_test_data', recipe_test_api.StepTestData) |
+ step_test = self._test_data.pop_step_test_data(step['name'], test_data_fn) |
+ placeholders = render_step(step, step_test) |
+ |
+ if allow_subannotations: |
+ # TODO(luqui) Make this hierarchical. |
+ self._open_step(step) |
+ start_annotation = self._annotation |
+ retcode = self._step_kernel(step, step_test, |
+ subannotator=self._subannotator()) |
+ |
+ # Open a closing step for presentation modifications. |
+ if self._annotation != start_annotation: |
+ self._open_step({ 'name': step['name'] + ' (end)' }) |
+ self._step_result.retcode = retcode |
+ else: |
+ self._open_step(step) |
+ self._step_kernel(step, step_test) |
- get_placeholder_results(step_result, placeholders) |
- self._previous_step_result = step_result |
+ get_placeholder_results(self._step_result, placeholders) |
- if step_result.retcode in ok_ret: |
- step_result.presentation.status = 'SUCCESS' |
- return step_result |
+ if self._step_result.retcode in ok_ret: |
+ self._step_result.presentation.status = 'SUCCESS' |
+ return self._step_result |
else: |
if not infra_step: |
state = 'FAILURE' |
@@ -588,13 +677,13 @@ class SequentialRecipeEngine(RecipeEngine): |
state = 'EXCEPTION' |
exc = recipe_api.InfraFailure |
- step_result.presentation.status = state |
+ self._step_result.presentation.status = state |
if step_test.enabled: |
# To avoid cluttering the expectations, don't emit this in testmode. |
- self._previous_step_annotation.emit( |
- 'step returned non-zero exit code: %d' % step_result.retcode) |
+ self._annotation.emit( |
+ 'step returned non-zero exit code: %d' % self._step_result.retcode) |
- raise exc(step['name'], step_result) |
+ raise exc(step['name'], self._step_result) |
def run(self, steps_function, api): |
@@ -606,18 +695,18 @@ class SequentialRecipeEngine(RecipeEngine): |
try: |
retcode = steps_function(api) |
assert retcode is None, ( |
- "Non-None return from GenSteps is not supported yet") |
+ 'Non-None return from GenSteps is not supported yet') |
assert not self._test_data.enabled or not self._test_data.step_data, ( |
- "Unconsumed test data! %s" % (self._test_data.step_data,)) |
+ 'Unconsumed test data! %s' % (self._test_data.step_data,)) |
finally: |
self._emit_results() |
except recipe_api.StepFailure as f: |
retcode = f.retcode or 1 |
final_result = { |
- "name": "$final_result", |
- "reason": f.reason, |
- "status_code": retcode |
+ 'name': '$final_result', |
+ 'reason': f.reason, |
+ 'status_code': retcode |
} |
except Exception as ex: |
@@ -625,9 +714,9 @@ class SequentialRecipeEngine(RecipeEngine): |
retcode = -1 |
final_result = { |
- "name": "$final_result", |
- "reason": "Uncaught Exception: %r" % ex, |
- "status_code": retcode |
+ 'name': '$final_result', |
+ 'reason': 'Uncaught Exception: %r' % ex, |
+ 'status_code': retcode |
} |
with self._stream.step('Uncaught Exception') as s: |
@@ -638,9 +727,10 @@ class SequentialRecipeEngine(RecipeEngine): |
raise |
if final_result is not None: |
- self._step_history[final_result['name']] = final_result |
+ self._step_results[final_result['name']] = ( |
+ StepData(final_result, final_result['status_code'])) |
- return RecipeExecutionResult(retcode, self._step_history) |
+ return RecipeExecutionResult(retcode, self._step_results) |
def create_step(self, step): # pylint: disable=R0201 |
# This version of engine doesn't do anything, just converts step to dict |