| 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. | 6 """Entry point for fully-annotated builds. |
| 7 | 7 |
| 8 This script is part of the effort to move all builds to annotator-based | 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() | 9 systems. Any builder configured to use the AnnotatorFactory.BaseFactory() |
| 10 found in scripts/master/factory/annotator_factory.py executes a single | 10 found in scripts/master/factory/annotator_factory.py executes a single |
| (...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 69 import sys | 69 import sys |
| 70 | 70 |
| 71 import cStringIO | 71 import cStringIO |
| 72 | 72 |
| 73 import common.python26_polyfill # pylint: disable=W0611 | 73 import common.python26_polyfill # pylint: disable=W0611 |
| 74 import collections # Import after polyfill to get OrderedDict on 2.6 | 74 import collections # Import after polyfill to get OrderedDict on 2.6 |
| 75 | 75 |
| 76 from common import annotator | 76 from common import annotator |
| 77 from common import chromium_utils | 77 from common import chromium_utils |
| 78 from slave import recipe_api | 78 from slave import recipe_api |
| 79 from slave import recipe_loader |
| 80 from slave import recipe_test_api |
| 81 from slave import recipe_util |
| 82 |
| 79 | 83 |
| 80 SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) | 84 SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) |
| 81 BUILD_ROOT = os.path.dirname(os.path.dirname(SCRIPT_PATH)) | |
| 82 ROOT_PATH = os.path.abspath(os.path.join( | |
| 83 SCRIPT_PATH, os.pardir, os.pardir, os.pardir)) | |
| 84 MODULE_DIRS = [os.path.join(x, 'recipe_modules') for x in [ | |
| 85 SCRIPT_PATH, | |
| 86 os.path.join(ROOT_PATH, 'build_internal', 'scripts', 'slave') | |
| 87 ]] | |
| 88 | 85 |
| 89 | 86 |
| 90 class StepPresentation(object): | 87 class StepPresentation(object): |
| 91 STATUSES = set(('SUCCESS', 'FAILURE', 'WARNING', 'EXCEPTION')) | 88 STATUSES = set(('SUCCESS', 'FAILURE', 'WARNING', 'EXCEPTION')) |
| 92 | 89 |
| 93 def __init__(self): | 90 def __init__(self): |
| 94 self._finalized = False | 91 self._finalized = False |
| 95 | 92 |
| 96 self._logs = collections.OrderedDict() | 93 self._logs = collections.OrderedDict() |
| 97 self._links = collections.OrderedDict() | 94 self._links = collections.OrderedDict() |
| (...skipping 126 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 224 for s in fixup_seed_steps(list(flattened(step_or_steps))): | 221 for s in fixup_seed_steps(list(flattened(step_or_steps))): |
| 225 yield s | 222 yield s |
| 226 elif inspect.isgenerator(step_or_steps): | 223 elif inspect.isgenerator(step_or_steps): |
| 227 for i in step_or_steps: | 224 for i in step_or_steps: |
| 228 for s in ensure_sequence_of_steps(i): | 225 for s in ensure_sequence_of_steps(i): |
| 229 yield s | 226 yield s |
| 230 else: | 227 else: |
| 231 assert False, 'Item is not a sequence or a step: %s' % (step_or_steps,) | 228 assert False, 'Item is not a sequence or a step: %s' % (step_or_steps,) |
| 232 | 229 |
| 233 | 230 |
| 234 def render_step(step, test_data): | 231 def render_step(step, step_test): |
| 235 """Renders a step so that it can be fed to annotator.py. | 232 """Renders a step so that it can be fed to annotator.py. |
| 236 | 233 |
| 237 Args: | 234 Args: |
| 238 test_data: The test data json dictionary for this step, if any. | 235 step_test: The test data json dictionary for this step, if any. |
| 239 Passed through unaltered to each placeholder. | 236 Passed through unaltered to each placeholder. |
| 240 | 237 |
| 241 Returns any placeholder instances that were found while rendering the step. | 238 Returns any placeholder instances that were found while rendering the step. |
| 242 """ | 239 """ |
| 243 placeholders = collections.defaultdict(list) | 240 placeholders = collections.defaultdict(lambda: collections.defaultdict(list)) |
| 244 new_cmd = [] | 241 new_cmd = [] |
| 245 for item in step['cmd']: | 242 for item in step['cmd']: |
| 246 if isinstance(item, recipe_api.Placeholder): | 243 if isinstance(item, recipe_util.Placeholder): |
| 247 # __module__ is in the form of ...recipe_modules.<api_name>.* | 244 module_name, placeholder_name = item.name_pieces |
| 248 api_name = item.__module__.split('.')[-2] | 245 tdata = step_test.pop_placeholder(item.name_pieces) |
| 249 tdata = None if test_data is None else test_data.get(api_name, {}) | |
| 250 new_cmd.extend(item.render(tdata)) | 246 new_cmd.extend(item.render(tdata)) |
| 251 placeholders[api_name].append(item) | 247 placeholders[module_name][placeholder_name].append((item, tdata)) |
| 252 else: | 248 else: |
| 253 new_cmd.append(item) | 249 new_cmd.append(item) |
| 254 step['cmd'] = new_cmd | 250 step['cmd'] = new_cmd |
| 255 return placeholders | 251 return placeholders |
| 256 | 252 |
| 257 | 253 |
| 258 def call_placeholders(step_result, placeholders, test_data): | 254 def get_placeholder_results(step_result, placeholders): |
| 259 class BlankObject(object): | 255 class BlankObject(object): |
| 260 pass | 256 pass |
| 261 additions = {} | 257 for module_name, pholders in placeholders.iteritems(): |
| 262 for api, items in placeholders.iteritems(): | 258 assert not hasattr(step_result, module_name) |
| 263 additions[api] = BlankObject() | 259 o = BlankObject() |
| 264 test_datum = None if test_data is None else test_data.get(api, {}) | 260 setattr(step_result, module_name, o) |
| 265 for placeholder in items: | 261 |
| 266 placeholder.step_finished(step_result.presentation, additions[api], | 262 for placeholder_name, items in pholders.iteritems(): |
| 267 test_datum) | 263 lst = [ph.result(step_result.presentation, td) for ph, td in items] |
| 268 for api, obj in additions.iteritems(): | 264 setattr(o, placeholder_name+"_all", lst) |
| 269 setattr(step_result, api, obj) | 265 setattr(o, placeholder_name, lst[0]) |
| 270 | 266 |
| 271 | 267 |
| 272 def step_callback(step, step_history, placeholders, test_data_item): | 268 def step_callback(step, step_history, placeholders, step_test): |
| 269 assert step['name'] not in step_history, ( |
| 270 'Step "%s" is already in step_history!' % step['name']) |
| 271 step_result = StepData(step, None) |
| 272 step_history[step['name']] = step_result |
| 273 |
| 273 followup_fn = step.pop('followup_fn', None) | 274 followup_fn = step.pop('followup_fn', None) |
| 274 | 275 |
| 275 def _inner(annotator_step, retcode): | 276 def _inner(annotator_step, retcode): |
| 276 step_result = StepData(step, retcode) | 277 step_result._retcode = retcode # pylint: disable=W0212 |
| 277 if retcode > 0: | 278 if retcode > 0: |
| 278 step_result.presentation.status = 'FAILURE' | 279 step_result.presentation.status = 'FAILURE' |
| 279 | 280 |
| 280 step_history[step['name']] = step_result | |
| 281 annotator_step.annotation_stream.step_cursor(step['name']) | 281 annotator_step.annotation_stream.step_cursor(step['name']) |
| 282 if step_result.retcode != 0 and test_data_item is None: | 282 if step_result.retcode != 0 and step_test.enabled: |
| 283 # To avoid cluttering the expectations, don't emit this in testmode. | 283 # To avoid cluttering the expectations, don't emit this in testmode. |
| 284 annotator_step.emit('step returned non-zero exit code: %d' % | 284 annotator_step.emit('step returned non-zero exit code: %d' % |
| 285 step_result.retcode) | 285 step_result.retcode) |
| 286 | 286 |
| 287 call_placeholders(step_result, placeholders, test_data_item) | 287 get_placeholder_results(step_result, placeholders) |
| 288 | 288 |
| 289 try: | 289 try: |
| 290 if followup_fn: | 290 if followup_fn: |
| 291 followup_fn(step_result) | 291 followup_fn(step_result) |
| 292 except recipe_api.RecipeAbort as e: | 292 except recipe_util.RecipeAbort as e: |
| 293 step_result.abort_reason = str(e) | 293 step_result.abort_reason = str(e) |
| 294 | 294 |
| 295 step_result.presentation.finalize(annotator_step) | 295 step_result.presentation.finalize(annotator_step) |
| 296 return step_result | 296 return step_result |
| 297 if followup_fn: | 297 if followup_fn: |
| 298 _inner.__name__ = followup_fn.__name__ | 298 _inner.__name__ = followup_fn.__name__ |
| 299 | 299 |
| 300 return _inner | 300 return _inner |
| 301 | 301 |
| 302 | 302 |
| (...skipping 18 matching lines...) Expand all Loading... |
| 321 def main(argv=None): | 321 def main(argv=None): |
| 322 opts, _ = get_args(argv) | 322 opts, _ = get_args(argv) |
| 323 | 323 |
| 324 stream = annotator.StructuredAnnotationStream(seed_steps=['setup_build']) | 324 stream = annotator.StructuredAnnotationStream(seed_steps=['setup_build']) |
| 325 | 325 |
| 326 ret = run_steps(stream, opts.build_properties, opts.factory_properties) | 326 ret = run_steps(stream, opts.build_properties, opts.factory_properties) |
| 327 return ret.status_code | 327 return ret.status_code |
| 328 | 328 |
| 329 | 329 |
| 330 def run_steps(stream, build_properties, factory_properties, | 330 def run_steps(stream, build_properties, factory_properties, |
| 331 api=recipe_api.CreateRecipeApi, test_data=None): | 331 api=recipe_loader.CreateRecipeApi, |
| 332 test=recipe_test_api.DisabledTestData()): |
| 332 """Returns a tuple of (status_code, steps_ran). | 333 """Returns a tuple of (status_code, steps_ran). |
| 333 | 334 |
| 334 Only one of these values will be set at a time. This is mainly to support the | 335 Only one of these values will be set at a time. This is mainly to support the |
| 335 testing interface used by unittests/recipes_test.py. | 336 testing interface used by unittests/recipes_test.py. |
| 336 | |
| 337 test_data should be a dictionary of step_name -> (retcode, json_data) | |
| 338 """ | 337 """ |
| 339 stream.honor_zero_return_code() | 338 stream.honor_zero_return_code() |
| 340 MakeStepsRetval = collections.namedtuple('MakeStepsRetval', | 339 MakeStepsRetval = collections.namedtuple('MakeStepsRetval', |
| 341 'status_code steps_ran') | 340 'status_code steps_ran') |
| 342 | 341 |
| 343 # TODO(iannucci): Stop this when blamelist becomes sane data. | 342 # TODO(iannucci): Stop this when blamelist becomes sane data. |
| 344 if ('blamelist_real' in build_properties and | 343 if ('blamelist_real' in build_properties and |
| 345 'blamelist' in build_properties): | 344 'blamelist' in build_properties): |
| 346 build_properties['blamelist'] = build_properties['blamelist_real'] | 345 build_properties['blamelist'] = build_properties['blamelist_real'] |
| 347 del build_properties['blamelist_real'] | 346 del build_properties['blamelist_real'] |
| 348 | 347 |
| 349 step_history = collections.OrderedDict() | 348 step_history = collections.OrderedDict() |
| 350 with stream.step('setup_build') as s: | 349 with stream.step('setup_build') as s: |
| 351 assert 'recipe' in factory_properties | 350 assert 'recipe' in factory_properties |
| 352 recipe = factory_properties['recipe'] | 351 recipe = factory_properties['recipe'] |
| 353 | |
| 354 # If the recipe is specified as "module:recipe", then it is an recipe | |
| 355 # contained in a recipe_module as an example. Look for it in the modules | |
| 356 # imported by load_recipe_modules instead of the normal search paths. | |
| 357 if ':' in recipe: | |
| 358 module_name, recipe = recipe.split(':') | |
| 359 assert recipe.endswith('example') | |
| 360 RECIPE_MODULES = recipe_api.load_recipe_modules(MODULE_DIRS) | |
| 361 try: | |
| 362 recipe_module = getattr(getattr(RECIPE_MODULES, module_name), recipe) | |
| 363 except AttributeError: | |
| 364 s.step_text('recipe not found') | |
| 365 s.step_failure() | |
| 366 return MakeStepsRetval(2, None) | |
| 367 | |
| 368 else: | |
| 369 recipe_dirs = (os.path.abspath(p) for p in ( | |
| 370 os.path.join(SCRIPT_PATH, '..', '..', '..', 'build_internal', | |
| 371 'scripts', 'slave-internal', 'recipes'), | |
| 372 os.path.join(SCRIPT_PATH, '..', '..', '..', 'build_internal', | |
| 373 'scripts', 'slave', 'recipes'), | |
| 374 os.path.join(SCRIPT_PATH, 'recipes'), | |
| 375 )) | |
| 376 | |
| 377 for recipe_path in (os.path.join(p, recipe) for p in recipe_dirs): | |
| 378 recipe_module = chromium_utils.IsolatedImportFromPath(recipe_path) | |
| 379 if recipe_module: | |
| 380 break | |
| 381 else: | |
| 382 s.step_text('recipe not found') | |
| 383 s.step_failure() | |
| 384 return MakeStepsRetval(2, None) | |
| 385 | |
| 386 properties = factory_properties.copy() | 352 properties = factory_properties.copy() |
| 387 properties.update(build_properties) | 353 properties.update(build_properties) |
| 388 stream.emit('Running recipe with %s' % (properties,)) | 354 try: |
| 389 steps = recipe_module.GenSteps(api(recipe_module.DEPS, | 355 recipe_module = recipe_loader.LoadRecipe(recipe) |
| 390 mod_dirs=MODULE_DIRS, | 356 stream.emit('Running recipe with %s' % (properties,)) |
| 391 properties=properties, | 357 steps = recipe_module.GenSteps(api(recipe_module.DEPS, |
| 392 step_history=step_history)) | 358 properties=properties, |
| 393 assert inspect.isgenerator(steps) | 359 step_history=step_history)) |
| 360 assert inspect.isgenerator(steps) |
| 361 except recipe_loader.NoSuchRecipe as e: |
| 362 s.step_text('recipe not found: %s' % e) |
| 363 s.step_failure() |
| 364 return MakeStepsRetval(2, None) |
| 365 |
| 394 | 366 |
| 395 # Execute annotator.py with steps if specified. | 367 # Execute annotator.py with steps if specified. |
| 396 # annotator.py handles the seeding, execution, and annotation of each step. | 368 # annotator.py handles the seeding, execution, and annotation of each step. |
| 397 failed = False | 369 failed = False |
| 398 | 370 |
| 399 test_mode = test_data is not None | |
| 400 | |
| 401 for step in ensure_sequence_of_steps(steps): | 371 for step in ensure_sequence_of_steps(steps): |
| 402 if failed and not step.get('always_run', False): | 372 if failed and not step.get('always_run', False): |
| 403 step_result = StepData(step, None) | 373 step_result = StepData(step, None) |
| 404 step_history[step['name']] = step_result | 374 step_history[step['name']] = step_result |
| 405 continue | 375 continue |
| 406 | 376 |
| 407 test_data_item = test_data.pop(step['name'], {}) if test_mode else None | 377 if test.enabled: |
| 408 placeholders = render_step(step, test_data_item) | 378 step_test = step.pop('default_step_data', recipe_api.StepTestData()) |
| 379 if step['name'] in test.step_data: |
| 380 step_test = test.step_data.pop(step['name']) |
| 381 else: |
| 382 step_test = recipe_api.DisabledTestData() |
| 383 step.pop('default_step_data', None) |
| 409 | 384 |
| 410 assert step['name'] not in step_history, ( | 385 placeholders = render_step(step, step_test) |
| 411 'Step "%s" is already in step_history!' % step['name']) | |
| 412 | 386 |
| 413 callback = step_callback(step, step_history, placeholders, test_data_item) | 387 callback = step_callback(step, step_history, placeholders, step_test) |
| 414 | 388 |
| 415 if not test_mode: | 389 if not test.enabled: |
| 416 step_result = annotator.run_step( | 390 step_result = annotator.run_step( |
| 417 stream, followup_fn=callback, **step) | 391 stream, followup_fn=callback, **step) |
| 418 else: | 392 else: |
| 419 with stream.step(step['name']) as s: | 393 with stream.step(step['name']) as s: |
| 420 s.stream = cStringIO.StringIO() | 394 s.stream = cStringIO.StringIO() |
| 421 step_result = callback(s, test_data_item.pop('$R', 0)) | 395 step_result = callback(s, step_test.retcode) |
| 422 lines = filter(None, s.stream.getvalue().splitlines()) | 396 lines = filter(None, s.stream.getvalue().splitlines()) |
| 423 if lines: | 397 if lines: |
| 424 # Note that '~' sorts after 'z' so that this will be last on each | 398 # Note that '~' sorts after 'z' so that this will be last on each |
| 425 # step. Also use _step to get access to the mutable step dictionary. | 399 # step. Also use _step to get access to the mutable step dictionary. |
| 426 # pylint: disable=W0212 | 400 # pylint: disable=W0212 |
| 427 step_result._step['~followup_annotations'] = lines | 401 step_result._step['~followup_annotations'] = lines |
| 428 | 402 |
| 429 if step_result.abort_reason: | 403 if step_result.abort_reason: |
| 430 stream.emit('Aborted: %s' % step_result.abort_reason) | 404 stream.emit('Aborted: %s' % step_result.abort_reason) |
| 431 test_data = {} # Dump the rest of the test data | 405 test.step_data.clear() # Dump the rest of the test data |
| 432 failed = True | 406 failed = True |
| 433 break | 407 break |
| 434 | 408 |
| 435 # TODO(iannucci): Pull this failure calculation into callback. | 409 # TODO(iannucci): Pull this failure calculation into callback. |
| 436 failed = annotator.update_build_failure(failed, step_result.retcode, **step) | 410 failed = annotator.update_build_failure(failed, step_result.retcode, **step) |
| 437 | 411 |
| 438 assert not test_mode or test_data == {}, ( | 412 assert not test.enabled or not test.step_data, ( |
| 439 "Unconsumed test data! %s" % (test_data,)) | 413 "Unconsumed test data! %s" % (test.step_data,)) |
| 440 | 414 |
| 441 return MakeStepsRetval(0 if not failed else 1, step_history) | 415 return MakeStepsRetval(0 if not failed else 1, step_history) |
| 442 | 416 |
| 443 | 417 |
| 444 def UpdateScripts(): | 418 def UpdateScripts(): |
| 445 if os.environ.get('RUN_SLAVE_UPDATED_SCRIPTS'): | 419 if os.environ.get('RUN_SLAVE_UPDATED_SCRIPTS'): |
| 446 os.environ.pop('RUN_SLAVE_UPDATED_SCRIPTS') | 420 os.environ.pop('RUN_SLAVE_UPDATED_SCRIPTS') |
| 447 return False | 421 return False |
| 448 stream = annotator.StructuredAnnotationStream(seed_steps=['update_scripts']) | 422 stream = annotator.StructuredAnnotationStream(seed_steps=['update_scripts']) |
| 449 with stream.step('update_scripts') as s: | 423 with stream.step('update_scripts') as s: |
| (...skipping 11 matching lines...) Expand all Loading... |
| 461 | 435 |
| 462 def shell_main(argv): | 436 def shell_main(argv): |
| 463 if UpdateScripts(): | 437 if UpdateScripts(): |
| 464 return subprocess.call([sys.executable] + argv) | 438 return subprocess.call([sys.executable] + argv) |
| 465 else: | 439 else: |
| 466 return main(argv) | 440 return main(argv) |
| 467 | 441 |
| 468 | 442 |
| 469 if __name__ == '__main__': | 443 if __name__ == '__main__': |
| 470 sys.exit(shell_main(sys.argv)) | 444 sys.exit(shell_main(sys.argv)) |
| OLD | NEW |