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_util | |
80 from slave import recipe_test_api | |
81 from slave import recipe_loader | |
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 125 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
223 for s in fixup_seed_steps(list(flattened(step_or_steps))): | 220 for s in fixup_seed_steps(list(flattened(step_or_steps))): |
224 yield s | 221 yield s |
225 elif inspect.isgenerator(step_or_steps): | 222 elif inspect.isgenerator(step_or_steps): |
226 for i in step_or_steps: | 223 for i in step_or_steps: |
227 for s in ensure_sequence_of_steps(i): | 224 for s in ensure_sequence_of_steps(i): |
228 yield s | 225 yield s |
229 else: | 226 else: |
230 assert False, 'Item is not a sequence or a step: %s' % (step_or_steps,) | 227 assert False, 'Item is not a sequence or a step: %s' % (step_or_steps,) |
231 | 228 |
232 | 229 |
233 def render_step(step, test_data): | 230 def render_step(step, step_test): |
234 """Renders a step so that it can be fed to annotator.py. | 231 """Renders a step so that it can be fed to annotator.py. |
235 | 232 |
236 Args: | 233 Args: |
237 test_data: The test data json dictionary for this step, if any. | 234 step_test: The test data json dictionary for this step, if any. |
238 Passed through unaltered to each placeholder. | 235 Passed through unaltered to each placeholder. |
239 | 236 |
240 Returns any placeholder instances that were found while rendering the step. | 237 Returns any placeholder instances that were found while rendering the step. |
241 """ | 238 """ |
242 placeholders = collections.defaultdict(list) | 239 placeholders = collections.defaultdict(lambda: collections.defaultdict(list)) |
243 new_cmd = [] | 240 new_cmd = [] |
244 for item in step['cmd']: | 241 for item in step['cmd']: |
245 if isinstance(item, recipe_api.Placeholder): | 242 if isinstance(item, recipe_util.Placeholder): |
246 # __module__ is in the form of ...recipe_modules.<api_name>.* | 243 module_name, placeholder_name = item.name_pieces |
247 api_name = item.__module__.split('.')[-2] | 244 tdata = step_test.pop_placeholder(item.name_pieces) |
248 tdata = None if test_data is None else test_data.get(api_name, {}) | |
249 new_cmd.extend(item.render(tdata)) | 245 new_cmd.extend(item.render(tdata)) |
250 placeholders[api_name].append(item) | 246 placeholders[module_name][placeholder_name].append((item, tdata)) |
251 else: | 247 else: |
252 new_cmd.append(item) | 248 new_cmd.append(item) |
253 step['cmd'] = new_cmd | 249 step['cmd'] = new_cmd |
254 return placeholders | 250 return placeholders |
255 | 251 |
256 | 252 |
257 def call_placeholders(step_result, placeholders, test_data): | 253 def get_placeholder_results(step_result, placeholders): |
258 class BlankObject(object): | 254 class BlankObject(object): |
259 pass | 255 pass |
260 additions = {} | 256 for module_name, pholders in placeholders.iteritems(): |
261 for api, items in placeholders.iteritems(): | 257 assert not hasattr(step_result, module_name) |
262 additions[api] = BlankObject() | 258 o = BlankObject() |
263 test_datum = None if test_data is None else test_data.get(api, {}) | 259 setattr(step_result, module_name, o) |
264 for placeholder in items: | 260 |
265 placeholder.step_finished(step_result.presentation, additions[api], | 261 for placeholder_name, items in pholders.iteritems(): |
266 test_datum) | 262 lst = [ph.result(step_result.presentation, td) for ph, td in items] |
267 for api, obj in additions.iteritems(): | 263 setattr(o, placeholder_name+"_all", lst) |
268 setattr(step_result, api, obj) | 264 setattr(o, placeholder_name, lst[0]) |
agable
2013/09/21 02:05:31
Why? Why not just put the whole list in placeholde
iannucci
2013/09/21 03:12:34
Because people /mostly always/ want the first entr
| |
269 | 265 |
270 | 266 |
271 def step_callback(step, step_history, placeholders, test_data_item): | 267 def step_callback(step, step_history, placeholders, step_test): |
268 assert step['name'] not in step_history, ( | |
269 'Step "%s" is already in step_history!' % step['name']) | |
270 step_result = StepData(step, None) | |
271 step_history[step['name']] = step_result | |
272 | |
272 followup_fn = step.pop('followup_fn', None) | 273 followup_fn = step.pop('followup_fn', None) |
273 | 274 |
274 def _inner(annotator_step, retcode): | 275 def _inner(annotator_step, retcode): |
275 step_result = StepData(step, retcode) | 276 step_result._retcode = retcode # pylint: disable=W0212 |
276 if retcode > 0: | 277 if retcode > 0: |
277 step_result.presentation.status = 'FAILURE' | 278 step_result.presentation.status = 'FAILURE' |
278 | 279 |
279 step_history[step['name']] = step_result | |
280 annotator_step.annotation_stream.step_cursor(step['name']) | 280 annotator_step.annotation_stream.step_cursor(step['name']) |
281 if step_result.retcode != 0 and test_data_item is None: | 281 if step_result.retcode != 0 and step_test.enabled: |
282 # To avoid cluttering the expectations, don't emit this in testmode. | 282 # To avoid cluttering the expectations, don't emit this in testmode. |
283 annotator_step.emit('step returned non-zero exit code: %d' % | 283 annotator_step.emit('step returned non-zero exit code: %d' % |
284 step_result.retcode) | 284 step_result.retcode) |
285 | 285 |
286 call_placeholders(step_result, placeholders, test_data_item) | 286 get_placeholder_results(step_result, placeholders) |
287 | 287 |
288 if followup_fn: | 288 if followup_fn: |
289 followup_fn(step_result) | 289 followup_fn(step_result) |
290 | 290 |
291 step_result.presentation.finalize(annotator_step) | 291 step_result.presentation.finalize(annotator_step) |
292 return step_result | 292 return step_result |
293 if followup_fn: | 293 if followup_fn: |
294 _inner.__name__ = followup_fn.__name__ | 294 _inner.__name__ = followup_fn.__name__ |
295 | 295 |
296 return _inner | 296 return _inner |
(...skipping 20 matching lines...) Expand all Loading... | |
317 def main(argv=None): | 317 def main(argv=None): |
318 opts, _ = get_args(argv) | 318 opts, _ = get_args(argv) |
319 | 319 |
320 stream = annotator.StructuredAnnotationStream(seed_steps=['setup_build']) | 320 stream = annotator.StructuredAnnotationStream(seed_steps=['setup_build']) |
321 | 321 |
322 ret = run_steps(stream, opts.build_properties, opts.factory_properties) | 322 ret = run_steps(stream, opts.build_properties, opts.factory_properties) |
323 return ret.status_code | 323 return ret.status_code |
324 | 324 |
325 | 325 |
326 def run_steps(stream, build_properties, factory_properties, | 326 def run_steps(stream, build_properties, factory_properties, |
327 api=recipe_api.CreateRecipeApi, test_data=None): | 327 api=recipe_loader.CreateRecipeApi, |
328 test=recipe_test_api.DisabledTestData()): | |
328 """Returns a tuple of (status_code, steps_ran). | 329 """Returns a tuple of (status_code, steps_ran). |
329 | 330 |
330 Only one of these values will be set at a time. This is mainly to support the | 331 Only one of these values will be set at a time. This is mainly to support the |
331 testing interface used by unittests/recipes_test.py. | 332 testing interface used by unittests/recipes_test.py. |
332 | |
333 test_data should be a dictionary of step_name -> (retcode, json_data) | |
334 """ | 333 """ |
335 stream.honor_zero_return_code() | 334 stream.honor_zero_return_code() |
336 MakeStepsRetval = collections.namedtuple('MakeStepsRetval', | 335 MakeStepsRetval = collections.namedtuple('MakeStepsRetval', |
337 'status_code steps_ran') | 336 'status_code steps_ran') |
338 | 337 |
339 # TODO(iannucci): Stop this when blamelist becomes sane data. | 338 # TODO(iannucci): Stop this when blamelist becomes sane data. |
340 if ('blamelist_real' in build_properties and | 339 if ('blamelist_real' in build_properties and |
341 'blamelist' in build_properties): | 340 'blamelist' in build_properties): |
342 build_properties['blamelist'] = build_properties['blamelist_real'] | 341 build_properties['blamelist'] = build_properties['blamelist_real'] |
343 del build_properties['blamelist_real'] | 342 del build_properties['blamelist_real'] |
344 | 343 |
345 step_history = collections.OrderedDict() | 344 step_history = collections.OrderedDict() |
346 with stream.step('setup_build') as s: | 345 with stream.step('setup_build') as s: |
347 assert 'recipe' in factory_properties | 346 assert 'recipe' in factory_properties |
348 recipe = factory_properties['recipe'] | 347 recipe = factory_properties['recipe'] |
349 | |
350 # If the recipe is specified as "module:recipe", then it is an recipe | |
351 # contained in a recipe_module as an example. Look for it in the modules | |
352 # imported by load_recipe_modules instead of the normal search paths. | |
353 if ':' in recipe: | |
354 module_name, recipe = recipe.split(':') | |
355 assert recipe.endswith('example') | |
356 RECIPE_MODULES = recipe_api.load_recipe_modules(MODULE_DIRS) | |
357 try: | |
358 recipe_module = getattr(getattr(RECIPE_MODULES, module_name), recipe) | |
359 except AttributeError: | |
360 s.step_text('recipe not found') | |
361 s.step_failure() | |
362 return MakeStepsRetval(2, None) | |
363 | |
364 else: | |
365 recipe_dirs = (os.path.abspath(p) for p in ( | |
366 os.path.join(SCRIPT_PATH, '..', '..', '..', 'build_internal', | |
367 'scripts', 'slave-internal', 'recipes'), | |
368 os.path.join(SCRIPT_PATH, '..', '..', '..', 'build_internal', | |
369 'scripts', 'slave', 'recipes'), | |
370 os.path.join(SCRIPT_PATH, 'recipes'), | |
371 )) | |
372 | |
373 for recipe_path in (os.path.join(p, recipe) for p in recipe_dirs): | |
374 recipe_module = chromium_utils.IsolatedImportFromPath(recipe_path) | |
375 if recipe_module: | |
376 break | |
377 else: | |
378 s.step_text('recipe not found') | |
379 s.step_failure() | |
380 return MakeStepsRetval(2, None) | |
381 | |
382 properties = factory_properties.copy() | 348 properties = factory_properties.copy() |
383 properties.update(build_properties) | 349 properties.update(build_properties) |
384 stream.emit('Running recipe with %s' % (properties,)) | 350 try: |
385 steps = recipe_module.GenSteps(api(recipe_module.DEPS, | 351 recipe_module = recipe_loader.LoadRecipe(recipe) |
386 mod_dirs=MODULE_DIRS, | 352 stream.emit('Running recipe with %s' % (properties,)) |
387 properties=properties, | 353 steps = recipe_module.GenSteps(api(recipe_module.DEPS, |
388 step_history=step_history)) | 354 properties=properties, |
389 assert inspect.isgenerator(steps) | 355 step_history=step_history)) |
356 assert inspect.isgenerator(steps) | |
357 except recipe_loader.NoSuchRecipe as e: | |
358 s.step_text('recipe not found: %s' % e) | |
359 s.step_failure() | |
360 return MakeStepsRetval(2, None) | |
361 | |
390 | 362 |
391 # Execute annotator.py with steps if specified. | 363 # Execute annotator.py with steps if specified. |
392 # annotator.py handles the seeding, execution, and annotation of each step. | 364 # annotator.py handles the seeding, execution, and annotation of each step. |
393 failed = False | 365 failed = False |
394 | 366 |
395 test_mode = test_data is not None | |
396 | |
397 for step in ensure_sequence_of_steps(steps): | 367 for step in ensure_sequence_of_steps(steps): |
398 if failed and not step.get('always_run', False): | 368 if failed and not step.get('always_run', False): |
399 step_result = StepData(step, None) | 369 step_result = StepData(step, None) |
400 step_history[step['name']] = step_result | 370 step_history[step['name']] = step_result |
401 continue | 371 continue |
402 | 372 |
403 test_data_item = test_data.pop(step['name'], {}) if test_mode else None | 373 if test.enabled: |
404 placeholders = render_step(step, test_data_item) | 374 step_test = step.pop('default_step_data', recipe_api.StepTestData()) |
375 step_test += test.step_data.pop(step['name'], recipe_api.StepTestData()) | |
376 else: | |
377 step_test = recipe_api.DisabledTestData() | |
378 step.pop('default_step_data', None) | |
405 | 379 |
406 assert step['name'] not in step_history, ( | 380 placeholders = render_step(step, step_test) |
407 'Step "%s" is already in step_history!' % step['name']) | |
408 | 381 |
409 callback = step_callback(step, step_history, placeholders, test_data_item) | 382 callback = step_callback(step, step_history, placeholders, step_test) |
410 | 383 |
411 if not test_mode: | 384 if not test.enabled: |
412 step_result = annotator.run_step( | 385 step_result = annotator.run_step( |
413 stream, followup_fn=callback, **step) | 386 stream, followup_fn=callback, **step) |
414 else: | 387 else: |
415 with stream.step(step['name']) as s: | 388 with stream.step(step['name']) as s: |
416 s.stream = cStringIO.StringIO() | 389 s.stream = cStringIO.StringIO() |
417 step_result = callback(s, test_data_item.pop('$R', 0)) | 390 step_result = callback(s, step_test.retcode) |
418 lines = filter(None, s.stream.getvalue().splitlines()) | 391 lines = filter(None, s.stream.getvalue().splitlines()) |
419 if lines: | 392 if lines: |
420 # Note that '~' sorts after 'z' so that this will be last on each | 393 # Note that '~' sorts after 'z' so that this will be last on each |
421 # step. Also use _step to get access to the mutable step dictionary. | 394 # step. Also use _step to get access to the mutable step dictionary. |
422 # pylint: disable=W0212 | 395 # pylint: disable=W0212 |
423 step_result._step['~followup_annotations'] = lines | 396 step_result._step['~followup_annotations'] = lines |
424 | 397 |
425 # TODO(iannucci): Pull this failure calculation into callback. | 398 # TODO(iannucci): Pull this failure calculation into callback. |
426 failed = annotator.update_build_failure(failed, step_result.retcode, **step) | 399 failed = annotator.update_build_failure(failed, step_result.retcode, **step) |
427 | 400 |
428 assert not test_mode or test_data == {}, ( | 401 assert not test.enabled or not test.step_data, ( |
429 "Unconsumed test data! %s" % (test_data,)) | 402 "Unconsumed test data! %s" % (test.step_data,)) |
430 | 403 |
431 return MakeStepsRetval(0 if not failed else 1, step_history) | 404 return MakeStepsRetval(0 if not failed else 1, step_history) |
432 | 405 |
433 | 406 |
434 def UpdateScripts(): | 407 def UpdateScripts(): |
435 if os.environ.get('RUN_SLAVE_UPDATED_SCRIPTS'): | 408 if os.environ.get('RUN_SLAVE_UPDATED_SCRIPTS'): |
436 os.environ.pop('RUN_SLAVE_UPDATED_SCRIPTS') | 409 os.environ.pop('RUN_SLAVE_UPDATED_SCRIPTS') |
437 return False | 410 return False |
438 stream = annotator.StructuredAnnotationStream(seed_steps=['update_scripts']) | 411 stream = annotator.StructuredAnnotationStream(seed_steps=['update_scripts']) |
439 with stream.step('update_scripts') as s: | 412 with stream.step('update_scripts') as s: |
(...skipping 11 matching lines...) Expand all Loading... | |
451 | 424 |
452 def shell_main(argv): | 425 def shell_main(argv): |
453 if UpdateScripts(): | 426 if UpdateScripts(): |
454 return subprocess.call([sys.executable] + argv) | 427 return subprocess.call([sys.executable] + argv) |
455 else: | 428 else: |
456 return main(argv) | 429 return main(argv) |
457 | 430 |
458 | 431 |
459 if __name__ == '__main__': | 432 if __name__ == '__main__': |
460 sys.exit(shell_main(sys.argv)) | 433 sys.exit(shell_main(sys.argv)) |
OLD | NEW |