Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(1)

Side by Side Diff: scripts/slave/annotated_run.py

Issue 23889036: Refactor the way that TestApi works so that it is actually useful. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: License headers Created 7 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | scripts/slave/recipe_api.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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))
OLDNEW
« no previous file with comments | « no previous file | scripts/slave/recipe_api.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698