Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2013 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Entry point for fully-annotated builds. | |
| 7 | |
| 8 This script is part of the effort to move all builds to annotator-based | |
| 9 systems. Any builder configured to use the AnnotatorFactory.BaseFactory() | |
| 10 found in scripts/master/factory/annotator_factory.py executes a single | |
| 11 AddAnnotatedScript step. That step (found in annotator_commands.py) calls | |
| 12 this script with the build- and factory-properties passed on the command | |
| 13 line. | |
| 14 | |
| 15 The main mode of operation is for factory_properties to contain a single | |
| 16 property 'recipe' whose value is the basename (without extension) of a python | |
| 17 script in one of the following locations (looked up in this order): | |
| 18 * build_internal/scripts/slave-internal/recipes | |
| 19 * build_internal/scripts/slave/recipes | |
| 20 * build/scripts/slave/recipes | |
| 21 | |
| 22 For example, these factory_properties would run the 'run_presubmit' recipe | |
| 23 located in build/scripts/slave/recipes: | |
| 24 { 'recipe': 'run_presubmit' } | |
| 25 | |
| 26 TODO(vadimsh, iannucci): The following docs are very outdated. | |
| 27 | |
| 28 Annotated_run.py will then import the recipe and expect to call a function whose | |
| 29 signature is: | |
| 30 GenSteps(api, properties) -> iterable_of_things. | |
| 31 | |
| 32 properties is a merged view of factory_properties with build_properties. | |
| 33 | |
| 34 Items in iterable_of_things must be one of: | |
| 35 * A step dictionary (as accepted by annotator.py) | |
| 36 * A sequence of step dictionaries | |
| 37 * A step generator | |
| 38 Iterable_of_things is also permitted to be a raw step generator. | |
| 39 | |
| 40 A step generator is called with the following protocol: | |
| 41 * The generator is initialized with 'step_history' and 'failed'. | |
| 42 * Each iteration of the generator is passed the current value of 'failed'. | |
| 43 | |
| 44 On each iteration, a step generator may yield: | |
| 45 * A single step dictionary | |
| 46 * A sequence of step dictionaries | |
| 47 * If a sequence of dictionaries is yielded, and the first step dictionary | |
| 48 does not have a 'seed_steps' key, the first step will be augmented with | |
| 49 a 'seed_steps' key containing the names of all the steps in the sequence. | |
| 50 | |
| 51 For steps yielded by the generator, if annotated_run enters the failed state, | |
| 52 it will only continue to call the generator if the generator sets the | |
| 53 'keep_going' key on the steps which it has produced. Otherwise annotated_run | |
| 54 will cease calling the generator and move on to the next item in | |
| 55 iterable_of_things. | |
| 56 | |
| 57 'step_history' is an OrderedDict of {stepname -> StepData}, always representing | |
| 58 the current history of what steps have run, what they returned, and any | |
| 59 json data they emitted. Additionally, the OrderedDict has the following | |
| 60 convenience functions defined: | |
| 61 * last_step - Returns the last step that ran or None | |
| 62 * nth_step(n) - Returns the N'th step that ran or None | |
| 63 | |
| 64 'failed' is a boolean representing if the build is in a 'failed' state. | |
| 65 """ | |
| 66 | |
| 67 import copy | |
| 68 import functools | |
| 69 import json | |
| 70 import optparse | 6 import optparse |
| 71 import os | 7 import os |
| 72 import subprocess | 8 import subprocess |
| 73 import sys | 9 import sys |
| 74 import traceback | |
| 75 | 10 |
| 76 import cStringIO | 11 BUILD_ROOT = os.path.dirname(os.path.dirname(os.path.dirname( |
| 77 | 12 os.path.abspath(__file__)))) |
| 78 import common.python26_polyfill # pylint: disable=W0611 | 13 sys.path.append(os.path.join(BUILD_ROOT, 'scripts')) |
| 79 import collections # Import after polyfill to get OrderedDict on 2.6 | 14 sys.path.append(os.path.join(BUILD_ROOT, 'third_party')) |
| 80 | 15 |
| 81 from common import annotator | 16 from common import annotator |
| 82 from common import chromium_utils | 17 from common import chromium_utils |
| 18 from slave import recipe_universe | |
| 83 | 19 |
| 84 from slave import recipe_loader | 20 from recipe_engine import annotated_run |
| 85 from slave import recipe_test_api | |
| 86 from slave import recipe_util | |
| 87 from slave import recipe_api | |
| 88 | |
| 89 | |
| 90 SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) | |
| 91 | |
| 92 | |
| 93 class StepPresentation(object): | |
| 94 STATUSES = set(('SUCCESS', 'FAILURE', 'WARNING', 'EXCEPTION')) | |
| 95 | |
| 96 def __init__(self): | |
| 97 self._finalized = False | |
| 98 | |
| 99 self._logs = collections.OrderedDict() | |
| 100 self._links = collections.OrderedDict() | |
| 101 self._perf_logs = collections.OrderedDict() | |
| 102 self._status = None | |
| 103 self._step_summary_text = '' | |
| 104 self._step_text = '' | |
| 105 self._properties = {} | |
| 106 | |
| 107 # (E0202) pylint bug: http://www.logilab.org/ticket/89092 | |
| 108 @property | |
| 109 def status(self): # pylint: disable=E0202 | |
| 110 return self._status | |
| 111 | |
| 112 @status.setter | |
| 113 def status(self, val): # pylint: disable=E0202 | |
| 114 assert not self._finalized | |
| 115 assert val in self.STATUSES | |
| 116 self._status = val | |
| 117 | |
| 118 @property | |
| 119 def step_text(self): | |
| 120 return self._step_text | |
| 121 | |
| 122 @step_text.setter | |
| 123 def step_text(self, val): | |
| 124 assert not self._finalized | |
| 125 self._step_text = val | |
| 126 | |
| 127 @property | |
| 128 def step_summary_text(self): | |
| 129 return self._step_summary_text | |
| 130 | |
| 131 @step_summary_text.setter | |
| 132 def step_summary_text(self, val): | |
| 133 assert not self._finalized | |
| 134 self._step_summary_text = val | |
| 135 | |
| 136 @property | |
| 137 def logs(self): | |
| 138 if not self._finalized: | |
| 139 return self._logs | |
| 140 else: | |
| 141 return copy.deepcopy(self._logs) | |
| 142 | |
| 143 @property | |
| 144 def links(self): | |
| 145 if not self._finalized: | |
| 146 return self._links | |
| 147 else: | |
| 148 return copy.deepcopy(self._links) | |
| 149 | |
| 150 @property | |
| 151 def perf_logs(self): | |
| 152 if not self._finalized: | |
| 153 return self._perf_logs | |
| 154 else: | |
| 155 return copy.deepcopy(self._perf_logs) | |
| 156 | |
| 157 @property | |
| 158 def properties(self): # pylint: disable=E0202 | |
| 159 if not self._finalized: | |
| 160 return self._properties | |
| 161 else: | |
| 162 return copy.deepcopy(self._properties) | |
| 163 | |
| 164 @properties.setter | |
| 165 def properties(self, val): # pylint: disable=E0202 | |
| 166 assert not self._finalized | |
| 167 assert isinstance(val, dict) | |
| 168 self._properties = val | |
| 169 | |
| 170 def finalize(self, annotator_step): | |
| 171 self._finalized = True | |
| 172 if self.step_text: | |
| 173 annotator_step.step_text(self.step_text) | |
| 174 if self.step_summary_text: | |
| 175 annotator_step.step_summary_text(self.step_summary_text) | |
| 176 for name, lines in self.logs.iteritems(): | |
| 177 annotator_step.write_log_lines(name, lines) | |
| 178 for name, lines in self.perf_logs.iteritems(): | |
| 179 annotator_step.write_log_lines(name, lines, perf=True) | |
| 180 for label, url in self.links.iteritems(): | |
| 181 annotator_step.step_link(label, url) | |
| 182 status_mapping = { | |
| 183 'WARNING': annotator_step.step_warnings, | |
| 184 'FAILURE': annotator_step.step_failure, | |
| 185 'EXCEPTION': annotator_step.step_exception, | |
| 186 } | |
| 187 status_mapping.get(self.status, lambda: None)() | |
| 188 for key, value in self._properties.iteritems(): | |
| 189 annotator_step.set_build_property(key, json.dumps(value, sort_keys=True)) | |
| 190 | |
| 191 | |
| 192 class StepData(object): | |
| 193 def __init__(self, step, retcode): | |
| 194 self._retcode = retcode | |
| 195 self._step = step | |
| 196 | |
| 197 self._presentation = StepPresentation() | |
| 198 self.abort_reason = None | |
| 199 | |
| 200 @property | |
| 201 def step(self): | |
| 202 return copy.deepcopy(self._step) | |
| 203 | |
| 204 @property | |
| 205 def retcode(self): | |
| 206 return self._retcode | |
| 207 | |
| 208 @property | |
| 209 def presentation(self): | |
| 210 return self._presentation | |
| 211 | |
| 212 # TODO(martiniss) update comment | |
| 213 # Result of 'render_step', fed into 'step_callback'. | |
| 214 Placeholders = collections.namedtuple( | |
| 215 'Placeholders', ['cmd', 'stdout', 'stderr', 'stdin']) | |
| 216 | |
| 217 | |
| 218 def render_step(step, step_test): | |
| 219 """Renders a step so that it can be fed to annotator.py. | |
| 220 | |
| 221 Args: | |
| 222 step_test: The test data json dictionary for this step, if any. | |
| 223 Passed through unaltered to each placeholder. | |
| 224 | |
| 225 Returns any placeholder instances that were found while rendering the step. | |
| 226 """ | |
| 227 # Process 'cmd', rendering placeholders there. | |
| 228 placeholders = collections.defaultdict(lambda: collections.defaultdict(list)) | |
| 229 new_cmd = [] | |
| 230 for item in step.get('cmd', []): | |
| 231 if isinstance(item, recipe_util.Placeholder): | |
| 232 module_name, placeholder_name = item.name_pieces | |
| 233 tdata = step_test.pop_placeholder(item.name_pieces) | |
| 234 new_cmd.extend(item.render(tdata)) | |
| 235 placeholders[module_name][placeholder_name].append((item, tdata)) | |
| 236 else: | |
| 237 new_cmd.append(item) | |
| 238 step['cmd'] = new_cmd | |
| 239 | |
| 240 # Process 'stdout', 'stderr' and 'stdin' placeholders, if given. | |
| 241 stdio_placeholders = {} | |
| 242 for key in ('stdout', 'stderr', 'stdin'): | |
| 243 placeholder = step.get(key) | |
| 244 tdata = None | |
| 245 if placeholder: | |
| 246 assert isinstance(placeholder, recipe_util.Placeholder), key | |
| 247 tdata = getattr(step_test, key) | |
| 248 placeholder.render(tdata) | |
| 249 assert placeholder.backing_file | |
| 250 step[key] = placeholder.backing_file | |
| 251 stdio_placeholders[key] = (placeholder, tdata) | |
| 252 | |
| 253 return Placeholders(cmd=placeholders, **stdio_placeholders) | |
| 254 | |
| 255 | |
| 256 def get_placeholder_results(step_result, placeholders): | |
| 257 class BlankObject(object): | |
| 258 pass | |
| 259 | |
| 260 # Placeholders inside step |cmd|. | |
| 261 for module_name, pholders in placeholders.cmd.iteritems(): | |
| 262 assert not hasattr(step_result, module_name) | |
| 263 o = BlankObject() | |
| 264 setattr(step_result, module_name, o) | |
| 265 | |
| 266 for placeholder_name, items in pholders.iteritems(): | |
| 267 lst = [ph.result(step_result.presentation, td) for ph, td in items] | |
| 268 setattr(o, placeholder_name+"_all", lst) | |
| 269 setattr(o, placeholder_name, lst[0]) | |
| 270 | |
| 271 # Placeholders that are used with IO redirection. | |
| 272 for key in ('stdout', 'stderr', 'stdin'): | |
| 273 assert not hasattr(step_result, key) | |
| 274 ph, td = getattr(placeholders, key) | |
| 275 result = ph.result(step_result.presentation, td) if ph else None | |
| 276 setattr(step_result, key, result) | |
| 277 | |
| 278 | |
| 279 def get_callable_name(func): | |
| 280 """Returns __name__ of a callable, handling functools.partial types.""" | |
| 281 if isinstance(func, functools.partial): | |
| 282 return get_callable_name(func.func) | |
| 283 else: | |
| 284 return func.__name__ | |
| 285 | |
| 286 | |
| 287 def get_args(argv): | |
| 288 """Process command-line arguments.""" | |
| 289 | |
| 290 parser = optparse.OptionParser( | |
| 291 description='Entry point for annotated builds.') | |
| 292 parser.add_option('--build-properties', | |
| 293 action='callback', callback=chromium_utils.convert_json, | |
| 294 type='string', default={}, | |
| 295 help='build properties in JSON format') | |
| 296 parser.add_option('--factory-properties', | |
| 297 action='callback', callback=chromium_utils.convert_json, | |
| 298 type='string', default={}, | |
| 299 help='factory properties in JSON format') | |
| 300 parser.add_option('--build-properties-gz', | |
| 301 action='callback', callback=chromium_utils.convert_gz_json, | |
| 302 type='string', default={}, dest='build_properties', | |
| 303 help='build properties in b64 gz JSON format') | |
| 304 parser.add_option('--factory-properties-gz', | |
| 305 action='callback', callback=chromium_utils.convert_gz_json, | |
| 306 type='string', default={}, dest='factory_properties', | |
| 307 help='factory properties in b64 gz JSON format') | |
| 308 parser.add_option('--keep-stdin', action='store_true', default=False, | |
| 309 help='don\'t close stdin when running recipe steps') | |
| 310 return parser.parse_args(argv) | |
| 311 | |
| 312 | |
| 313 def main(argv=None): | |
| 314 opts, _ = get_args(argv) | |
| 315 | |
| 316 stream = annotator.StructuredAnnotationStream() | |
| 317 universe = recipe_loader.RecipeUniverse() | |
| 318 | |
| 319 ret = run_steps(stream, opts.build_properties, opts.factory_properties, | |
| 320 universe) | |
| 321 return ret.status_code | |
| 322 | |
| 323 | |
| 324 # Return value of run_steps and RecipeEngine.run. | |
| 325 RecipeExecutionResult = collections.namedtuple( | |
| 326 'RecipeExecutionResult', 'status_code steps_ran') | |
| 327 | 21 |
| 328 | 22 |
| 329 def get_recipe_properties(factory_properties, build_properties): | 23 def get_recipe_properties(factory_properties, build_properties): |
| 330 """Constructs the recipe's properties from buildbot's properties. | 24 """Constructs the recipe's properties from buildbot's properties. |
| 331 | 25 |
| 332 This merges factory_properties and build_properties. Furthermore, it | 26 This merges factory_properties and build_properties. Furthermore, it |
| 333 tries to reconstruct the 'recipe' property from builders.pyl if it isn't | 27 tries to reconstruct the 'recipe' property from builders.pyl if it isn't |
| 334 already there, and in that case merges in properties form builders.pyl. | 28 already there, and in that case merges in properties form builders.pyl. |
| 335 """ | 29 """ |
| 336 properties = factory_properties.copy() | 30 properties = factory_properties.copy() |
| (...skipping 15 matching lines...) Expand all Loading... | |
| 352 # Update properties with builders.pyl data. | 46 # Update properties with builders.pyl data. |
| 353 properties['recipe'] = builder['recipe'] | 47 properties['recipe'] = builder['recipe'] |
| 354 properties.update(builder.get('properties', {})) | 48 properties.update(builder.get('properties', {})) |
| 355 else: | 49 else: |
| 356 raise LookupError('Cannot find recipe for %s on %s' % | 50 raise LookupError('Cannot find recipe for %s on %s' % |
| 357 (build_properties['buildername'], | 51 (build_properties['buildername'], |
| 358 build_properties['mastername'])) | 52 build_properties['mastername'])) |
| 359 return properties | 53 return properties |
| 360 | 54 |
| 361 | 55 |
| 362 def run_steps(stream, build_properties, factory_properties, | 56 def get_args(argv): |
| 363 universe, test_data=recipe_test_api.DisabledTestData()): | 57 """Process command-line arguments.""" |
| 364 """Returns a tuple of (status_code, steps_ran). | |
| 365 | 58 |
| 366 Only one of these values will be set at a time. This is mainly to support the | 59 parser = optparse.OptionParser( |
| 367 testing interface used by unittests/recipes_test.py. | 60 description='Entry point for annotated builds.') |
| 368 """ | 61 parser.add_option('--build-properties', |
| 369 stream.honor_zero_return_code() | 62 action='callback', callback=chromium_utils.convert_json, |
| 370 | 63 type='string', default={}, |
| 371 # TODO(iannucci): Stop this when blamelist becomes sane data. | 64 help='build properties in JSON format') |
| 372 if ('blamelist_real' in build_properties and | 65 parser.add_option('--factory-properties', |
| 373 'blamelist' in build_properties): | 66 action='callback', callback=chromium_utils.convert_json, |
| 374 build_properties['blamelist'] = build_properties['blamelist_real'] | 67 type='string', default={}, |
| 375 del build_properties['blamelist_real'] | 68 help='factory properties in JSON format') |
| 376 | 69 parser.add_option('--build-properties-gz', |
| 377 # NOTE(iannucci): 'root' was a terribly bad idea and has been replaced by | 70 action='callback', callback=chromium_utils.convert_gz_json, |
| 378 # 'patch_project'. 'root' had Rietveld knowing about the implementation of | 71 type='string', default={}, dest='build_properties', |
| 379 # the builders. 'patch_project' lets the builder (recipe) decide its own | 72 help='build properties in b64 gz JSON format') |
| 380 # destiny. | 73 parser.add_option('--factory-properties-gz', |
| 381 build_properties.pop('root', None) | 74 action='callback', callback=chromium_utils.convert_gz_json, |
| 382 | 75 type='string', default={}, dest='factory_properties', |
| 383 properties = get_recipe_properties( | 76 help='factory properties in b64 gz JSON format') |
| 384 factory_properties=factory_properties, | 77 parser.add_option('--keep-stdin', action='store_true', default=False, |
| 385 build_properties=build_properties) | 78 help='don\'t close stdin when running recipe steps') |
| 386 | 79 return parser.parse_args(argv) |
| 387 # TODO(iannucci): A much better way to do this would be to dynamically | |
| 388 # detect if the mirrors are actually available during the execution of the | |
| 389 # recipe. | |
| 390 if ('use_mirror' not in properties and ( | |
| 391 'TESTING_MASTERNAME' in os.environ or | |
| 392 'TESTING_SLAVENAME' in os.environ)): | |
| 393 properties['use_mirror'] = False | |
| 394 | |
| 395 # It's an integration point with a new recipe engine that can run steps | |
| 396 # in parallel (that is not implemented yet). Use new engine only if explicitly | |
| 397 # asked by setting 'engine' property to 'ParallelRecipeEngine'. | |
| 398 engine = RecipeEngine.create(stream, properties, test_data) | |
| 399 | |
| 400 # Create all API modules and an instance of top level GenSteps generator. | |
| 401 # It doesn't launch any recipe code yet (generator needs to be iterated upon | |
| 402 # to start executing code). | |
| 403 api = None | |
| 404 with stream.step('setup_build') as s: | |
| 405 assert 'recipe' in properties # Should be ensured by get_recipe_properties. | |
| 406 recipe = properties['recipe'] | |
| 407 | |
| 408 properties_to_print = properties.copy() | |
| 409 if 'use_mirror' in properties: | |
| 410 del properties_to_print['use_mirror'] | |
| 411 | |
| 412 run_recipe_help_lines = [ | |
| 413 'To repro this locally, run the following line from a build checkout:', | |
| 414 '', | |
| 415 './scripts/tools/run_recipe.py %s --properties-file - <<EOF' % recipe, | |
| 416 repr(properties_to_print), | |
| 417 'EOF', | |
| 418 '', | |
| 419 'To run on Windows, you can put the JSON in a file and redirect the', | |
| 420 'contents of the file into run_recipe.py, with the < operator.', | |
| 421 ] | |
| 422 | |
| 423 for line in run_recipe_help_lines: | |
| 424 s.step_log_line('run_recipe', line) | |
| 425 s.step_log_end('run_recipe') | |
| 426 | |
| 427 try: | |
| 428 recipe_module = universe.load_recipe(recipe) | |
| 429 stream.emit('Running recipe with %s' % (properties,)) | |
| 430 api = recipe_loader.create_recipe_api(recipe_module.LOADED_DEPS, | |
| 431 engine, | |
| 432 test_data) | |
| 433 steps = recipe_module.GenSteps | |
| 434 s.step_text('<br/>running recipe: "%s"' % recipe) | |
| 435 except recipe_loader.NoSuchRecipe as e: | |
| 436 s.step_text('<br/>recipe not found: %s' % e) | |
| 437 s.step_failure() | |
| 438 return RecipeExecutionResult(2, None) | |
| 439 | |
| 440 # Run the steps emitted by a recipe via the engine, emitting annotations | |
| 441 # into |stream| along the way. | |
| 442 return engine.run(steps, api) | |
| 443 | |
| 444 | |
| 445 class RecipeEngine(object): | |
| 446 """Knows how to execute steps emitted by a recipe, holds global state such as | |
| 447 step history and build properties. Each recipe module API has a reference to | |
| 448 this object. | |
| 449 | |
| 450 Recipe modules that are aware of the engine: | |
| 451 * properties - uses engine.properties. | |
| 452 * step_history - uses engine.step_history. | |
| 453 * step - uses engine.create_step(...). | |
| 454 | |
| 455 This class acts mostly as a documentation of expected public engine interface. | |
| 456 """ | |
| 457 | |
| 458 @staticmethod | |
| 459 def create(stream, properties, test_data): | |
| 460 """Create a new instance of RecipeEngine based on 'engine' property.""" | |
| 461 engine_cls_name = properties.get('engine', 'SequentialRecipeEngine') | |
| 462 for cls in RecipeEngine.__subclasses__(): | |
| 463 if cls.__name__ == engine_cls_name: | |
| 464 return cls(stream, properties, test_data) | |
| 465 raise ValueError('Invalid engine class: %s' % (engine_cls_name,)) | |
| 466 | |
| 467 @property | |
| 468 def properties(self): | |
| 469 """Global properties, merged --build_properties and --factory_properties.""" | |
| 470 raise NotImplementedError | |
| 471 | |
| 472 # TODO(martiniss) update documentation for this class | |
| 473 def run(self, steps_function, api): | |
| 474 """Run a recipe represented by top level GenSteps generator. | |
| 475 | |
| 476 This function blocks until recipe finishes. | |
| 477 | |
| 478 Args: | |
| 479 generator: instance of GenSteps generator. | |
| 480 | |
| 481 Returns: | |
| 482 RecipeExecutionResult with status code and list of steps ran. | |
| 483 """ | |
| 484 raise NotImplementedError | |
| 485 | |
| 486 def create_step(self, step): | |
| 487 """Called by step module to instantiate a new step. Return value of this | |
| 488 function eventually surfaces as object yielded by GenSteps generator. | |
| 489 | |
| 490 Args: | |
| 491 step: ConfigGroup object with information about the step, see | |
| 492 recipe_modules/step/config.py. | |
| 493 | |
| 494 Returns: | |
| 495 Opaque engine specific object that is understood by 'run_steps' method. | |
| 496 """ | |
| 497 raise NotImplementedError | |
| 498 | |
| 499 | |
| 500 class SequentialRecipeEngine(RecipeEngine): | |
| 501 """Always runs step sequentially. Currently the engine used by default.""" | |
| 502 def __init__(self, stream, properties, test_data): | |
| 503 super(SequentialRecipeEngine, self).__init__() | |
| 504 self._stream = stream | |
| 505 self._properties = properties | |
| 506 self._test_data = test_data | |
| 507 self._step_history = collections.OrderedDict() | |
| 508 | |
| 509 self._previous_step_annotation = None | |
| 510 self._previous_step_result = None | |
| 511 self._api = None | |
| 512 | |
| 513 @property | |
| 514 def properties(self): | |
| 515 return self._properties | |
| 516 | |
| 517 @property | |
| 518 def previous_step_result(self): | |
| 519 """Allows api.step to get the active result from any context.""" | |
| 520 return self._previous_step_result | |
| 521 | |
| 522 def _emit_results(self): | |
| 523 annotation = self._previous_step_annotation | |
| 524 step_result = self._previous_step_result | |
| 525 | |
| 526 self._previous_step_annotation = None | |
| 527 self._previous_step_result = None | |
| 528 | |
| 529 if not annotation or not step_result: | |
| 530 return | |
| 531 | |
| 532 step_result.presentation.finalize(annotation) | |
| 533 if self._test_data.enabled: | |
| 534 val = annotation.stream.getvalue() | |
| 535 lines = filter(None, val.splitlines()) | |
| 536 if lines: | |
| 537 # note that '~' sorts after 'z' so that this will be last on each | |
| 538 # step. also use _step to get access to the mutable step | |
| 539 # dictionary. | |
| 540 # pylint: disable=w0212 | |
| 541 step_result._step['~followup_annotations'] = lines | |
| 542 annotation.step_ended() | |
| 543 | |
| 544 def run_step(self, step): | |
| 545 ok_ret = step.pop('ok_ret') | |
| 546 infra_step = step.pop('infra_step') | |
| 547 | |
| 548 test_data_fn = step.pop('step_test_data', recipe_test_api.StepTestData) | |
| 549 step_test = self._test_data.pop_step_test_data(step['name'], | |
| 550 test_data_fn) | |
| 551 placeholders = render_step(step, step_test) | |
| 552 | |
| 553 self._step_history[step['name']] = step | |
| 554 self._emit_results() | |
| 555 | |
| 556 step_result = None | |
| 557 | |
| 558 if not self._test_data.enabled: | |
| 559 self._previous_step_annotation, retcode = annotator.run_step( | |
| 560 self._stream, **step) | |
| 561 | |
| 562 step_result = StepData(step, retcode) | |
| 563 self._previous_step_annotation.annotation_stream.step_cursor(step['name']) | |
| 564 else: | |
| 565 self._previous_step_annotation = annotation = self._stream.step( | |
| 566 step['name']) | |
| 567 annotation.step_started() | |
| 568 try: | |
| 569 annotation.stream = cStringIO.StringIO() | |
| 570 | |
| 571 step_result = StepData(step, step_test.retcode) | |
| 572 except OSError: | |
| 573 exc_type, exc_value, exc_tb = sys.exc_info() | |
| 574 trace = traceback.format_exception(exc_type, exc_value, exc_tb) | |
| 575 trace_lines = ''.join(trace).split('\n') | |
| 576 annotation.write_log_lines('exception', filter(None, trace_lines)) | |
| 577 annotation.step_exception() | |
| 578 | |
| 579 get_placeholder_results(step_result, placeholders) | |
| 580 self._previous_step_result = step_result | |
| 581 | |
| 582 if step_result.retcode in ok_ret: | |
| 583 step_result.presentation.status = 'SUCCESS' | |
| 584 return step_result | |
| 585 else: | |
| 586 if not infra_step: | |
| 587 state = 'FAILURE' | |
| 588 exc = recipe_api.StepFailure | |
| 589 else: | |
| 590 state = 'EXCEPTION' | |
| 591 exc = recipe_api.InfraFailure | |
| 592 | |
| 593 step_result.presentation.status = state | |
| 594 if step_test.enabled: | |
| 595 # To avoid cluttering the expectations, don't emit this in testmode. | |
| 596 self._previous_step_annotation.emit( | |
| 597 'step returned non-zero exit code: %d' % step_result.retcode) | |
| 598 | |
| 599 raise exc(step['name'], step_result) | |
| 600 | |
| 601 | |
| 602 def run(self, steps_function, api): | |
| 603 self._api = api | |
| 604 retcode = None | |
| 605 final_result = None | |
| 606 | |
| 607 try: | |
| 608 try: | |
| 609 retcode = steps_function(api) | |
| 610 assert retcode is None, ( | |
| 611 "Non-None return from GenSteps is not supported yet") | |
| 612 | |
| 613 assert not self._test_data.enabled or not self._test_data.step_data, ( | |
| 614 "Unconsumed test data! %s" % (self._test_data.step_data,)) | |
| 615 finally: | |
| 616 self._emit_results() | |
| 617 except recipe_api.StepFailure as f: | |
| 618 retcode = f.retcode or 1 | |
| 619 final_result = { | |
| 620 "name": "$final_result", | |
| 621 "reason": f.reason, | |
| 622 "status_code": retcode | |
| 623 } | |
| 624 | |
| 625 except Exception as ex: | |
| 626 unexpected_exception = self._test_data.is_unexpected_exception(ex) | |
| 627 | |
| 628 retcode = -1 | |
| 629 final_result = { | |
| 630 "name": "$final_result", | |
| 631 "reason": "Uncaught Exception: %r" % ex, | |
| 632 "status_code": retcode | |
| 633 } | |
| 634 | |
| 635 with self._stream.step('Uncaught Exception') as s: | |
| 636 s.step_exception() | |
| 637 s.write_log_lines('exception', traceback.format_exc().splitlines()) | |
| 638 | |
| 639 if unexpected_exception: | |
| 640 raise | |
| 641 | |
| 642 if final_result is not None: | |
| 643 self._step_history[final_result['name']] = final_result | |
| 644 | |
| 645 return RecipeExecutionResult(retcode, self._step_history) | |
| 646 | |
| 647 def create_step(self, step): # pylint: disable=R0201 | |
| 648 # This version of engine doesn't do anything, just converts step to dict | |
| 649 # (that is consumed by annotator engine). | |
| 650 return step.as_jsonish() | |
| 651 | |
| 652 | |
| 653 class ParallelRecipeEngine(RecipeEngine): | |
| 654 """New engine that knows how to run steps in parallel. | |
| 655 | |
| 656 TODO(vadimsh): Implement it. | |
| 657 """ | |
| 658 | |
| 659 def __init__(self, stream, properties, test_data): | |
| 660 super(ParallelRecipeEngine, self).__init__() | |
| 661 self._stream = stream | |
| 662 self._properties = properties | |
| 663 self._test_data = test_data | |
| 664 | |
| 665 @property | |
| 666 def properties(self): | |
| 667 return self._properties | |
| 668 | |
| 669 def run(self, steps_function, api): | |
| 670 raise NotImplementedError | |
| 671 | |
| 672 def create_step(self, step): | |
| 673 raise NotImplementedError | |
| 674 | 80 |
| 675 | 81 |
| 676 def update_scripts(): | 82 def update_scripts(): |
| 677 if os.environ.get('RUN_SLAVE_UPDATED_SCRIPTS'): | 83 if os.environ.get('RUN_SLAVE_UPDATED_SCRIPTS'): |
| 678 os.environ.pop('RUN_SLAVE_UPDATED_SCRIPTS') | 84 os.environ.pop('RUN_SLAVE_UPDATED_SCRIPTS') |
| 679 return False | 85 return False |
| 680 | 86 |
| 681 stream = annotator.StructuredAnnotationStream() | 87 stream = annotator.StructuredAnnotationStream() |
| 682 | 88 |
| 683 with stream.step('update_scripts') as s: | 89 with stream.step('update_scripts') as s: |
| 684 build_root = os.path.join(SCRIPT_PATH, '..', '..') | |
| 685 gclient_name = 'gclient' | 90 gclient_name = 'gclient' |
| 686 if sys.platform.startswith('win'): | 91 if sys.platform.startswith('win'): |
| 687 gclient_name += '.bat' | 92 gclient_name += '.bat' |
| 688 gclient_path = os.path.join(build_root, '..', 'depot_tools', gclient_name) | 93 gclient_path = os.path.join(BUILD_ROOT, '..', 'depot_tools', gclient_name) |
| 689 gclient_cmd = [gclient_path, 'sync', '--force', '--verbose'] | 94 gclient_cmd = [gclient_path, 'sync', '--force', '--verbose'] |
| 690 cmd_dict = { | 95 cmd_dict = { |
| 691 'name': 'update_scripts', | 96 'name': 'update_scripts', |
| 692 'cmd': gclient_cmd, | 97 'cmd': gclient_cmd, |
| 693 'cwd': build_root, | 98 'cwd': BUILD_ROOT, |
| 694 } | 99 } |
| 695 annotator.print_step(cmd_dict, os.environ, stream) | 100 annotator.print_step(cmd_dict, os.environ, stream) |
| 696 if subprocess.call(gclient_cmd, cwd=build_root) != 0: | 101 if subprocess.call(gclient_cmd, cwd=BUILD_ROOT) != 0: |
| 697 s.step_text('gclient sync failed!') | 102 s.step_text('gclient sync failed!') |
| 698 s.step_warnings() | 103 s.step_warnings() |
|
iannucci
2015/05/27 02:03:27
wtf spacing?
luqui
2015/05/28 21:47:37
No trouble here. I think it's the new rietveld ui.
| |
| 699 os.environ['RUN_SLAVE_UPDATED_SCRIPTS'] = '1' | 104 os.environ['RUN_SLAVE_UPDATED_SCRIPTS'] = '1' |
| 700 | 105 |
| 701 # After running update scripts, set PYTHONIOENCODING=UTF-8 for the real | 106 # After running update scripts, set PYTHONIOENCODING=UTF-8 for the real |
| 702 # annotated_run. | 107 # annotated_run. |
| 703 os.environ['PYTHONIOENCODING'] = 'UTF-8' | 108 os.environ['PYTHONIOENCODING'] = 'UTF-8' |
| 704 | 109 |
| 705 return True | 110 return True |
| 706 | 111 |
| 707 | 112 |
| 113 def main(argv): | |
| 114 opts, _ = get_args(argv) | |
| 115 properties = get_recipe_properties( | |
| 116 opts.factory_properties, opts.build_properties) | |
| 117 stream = annotator.StructuredAnnotationStream() | |
| 118 annotated_run.run_steps(properties, stream, | |
| 119 universe=recipe_universe.get_universe()) | |
| 120 | |
| 121 | |
| 708 def shell_main(argv): | 122 def shell_main(argv): |
| 709 if update_scripts(): | 123 if update_scripts(): |
| 710 return subprocess.call([sys.executable] + argv) | 124 return subprocess.call([sys.executable] + argv) |
| 711 else: | 125 else: |
| 712 return main(argv) | 126 return main(argv) |
| 713 | 127 |
| 714 | |
| 715 if __name__ == '__main__': | 128 if __name__ == '__main__': |
| 716 sys.exit(shell_main(sys.argv)) | 129 sys.exit(shell_main(sys.argv)) |
| OLD | NEW |