| OLD | NEW |
| 1 #!/usr/bin/python | 1 #!/usr/bin/env python |
| 2 # Copyright 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 """Provides test coverage for common recipe configurations. | |
| 7 | |
| 8 recipe config expectations are located in ../recipe_configs_test/*.expected | |
| 9 | |
| 10 In training mode, this will loop over every config item in ../recipe_configs.py | |
| 11 crossed with every platform, and spit out the as_json() representation to | |
| 12 ../recipe_configs_test | |
| 13 | |
| 14 You must have 100% coverage of ../recipe_configs.py for this test to pass. | |
| 15 """ | |
| 16 | |
| 17 import argparse | |
| 18 import multiprocessing | |
| 19 import os | 6 import os |
| 20 import sys | 7 import sys |
| 21 import traceback | |
| 22 from itertools import product, imap | |
| 23 | 8 |
| 24 import test_env # "relative import" pylint: disable=W0403,W0611 | 9 import test_env # pylint: disable=W0403,W0611 |
| 25 | 10 |
| 26 from slave import recipe_loader | 11 from recipe_engine import configs_test |
| 27 from slave import recipe_util | 12 from slave import recipe_universe |
| 28 | |
| 29 import coverage | |
| 30 | |
| 31 SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__)) | |
| 32 SLAVE_DIR = os.path.abspath(os.path.join(SCRIPT_PATH, os.pardir)) | |
| 33 | |
| 34 COVERAGE = (lambda: coverage.coverage( | |
| 35 include=[os.path.join(x, '*', '*config.py') | |
| 36 for x in recipe_util.MODULE_DIRS()], | |
| 37 data_file='.recipe_configs_test_coverage', data_suffix=True))() | |
| 38 | |
| 39 def covered(fn, *args, **kwargs): | |
| 40 COVERAGE.start() | |
| 41 try: | |
| 42 return fn(*args, **kwargs) | |
| 43 finally: | |
| 44 COVERAGE.stop() | |
| 45 | |
| 46 UNIVERSE = recipe_loader.RecipeUniverse() | |
| 47 | |
| 48 def load_recipe_modules(): | |
| 49 modules = {} | |
| 50 for modpath in recipe_loader.loop_over_recipe_modules(): | |
| 51 # That's right, we're using the path as the local name! The local | |
| 52 # name really could be anything unique, we don't use it. | |
| 53 modules[modpath] = UNIVERSE.load(recipe_loader.PathDependency( | |
| 54 modpath, local_name=modpath, base_path=os.curdir)) | |
| 55 return modules | |
| 56 | |
| 57 | |
| 58 RECIPE_MODULES = None | |
| 59 def init_recipe_modules(): | |
| 60 global RECIPE_MODULES | |
| 61 RECIPE_MODULES = covered(load_recipe_modules) | |
| 62 | |
| 63 from slave import recipe_config # pylint: disable=F0401 | |
| 64 | |
| 65 | |
| 66 def evaluate_configurations(args): | |
| 67 mod_id, var_assignments = args | |
| 68 mod = RECIPE_MODULES[mod_id] | |
| 69 ctx = mod.CONFIG_CTX | |
| 70 | |
| 71 config_name = None | |
| 72 try: | |
| 73 make_item = lambda: covered(ctx.CONFIG_SCHEMA, **var_assignments) | |
| 74 | |
| 75 # Try ROOT_CONFIG_ITEM first. If it raises BadConf, then we can skip | |
| 76 # this config. | |
| 77 root_item = ctx.ROOT_CONFIG_ITEM | |
| 78 if root_item: | |
| 79 config_name = root_item.__name__ | |
| 80 try: | |
| 81 result = covered(root_item, make_item()) | |
| 82 if result.complete(): | |
| 83 covered(result.as_jsonish) | |
| 84 except recipe_config.BadConf, e: | |
| 85 pass # This is a possibly expected failure mode. | |
| 86 | |
| 87 for config_name, fn in ctx.CONFIG_ITEMS.iteritems(): | |
| 88 if fn.NO_TEST or fn.IS_ROOT: | |
| 89 continue | |
| 90 try: | |
| 91 result = covered(fn, make_item()) | |
| 92 if result.complete(): | |
| 93 covered(result.as_jsonish) | |
| 94 except recipe_config.BadConf: | |
| 95 pass # This is a possibly expected failure mode. | |
| 96 return True | |
| 97 except Exception as e: | |
| 98 print ('Caught unknown exception [%s] for config name [%s] for module ' | |
| 99 '[%s] with args %s') % (e, config_name, mod_id, var_assignments) | |
| 100 traceback.print_exc() | |
| 101 return False | |
| 102 | |
| 103 | |
| 104 def multiprocessing_init(): | |
| 105 # HACK: multiprocessing doesn't work with atexit, so shim the exit functions | |
| 106 # instead. This allows us to save exactly one coverage file per subprocess. | |
| 107 # pylint: disable=W0212 | |
| 108 real_os_exit = multiprocessing.forking.exit | |
| 109 def exitfn(code): | |
| 110 COVERAGE.save() | |
| 111 real_os_exit(code) | |
| 112 multiprocessing.forking.exit = exitfn | |
| 113 | |
| 114 # This check mirrors the logic in multiprocessing.forking.exit | |
| 115 if sys.platform != 'win32': | |
| 116 # Even though multiprocessing.forking.exit is defined, it's not used in the | |
| 117 # non-win32 version of multiprocessing.forking.Popen... *loss for words* | |
| 118 os._exit = exitfn | |
| 119 | |
| 120 | |
| 121 def coverage_parallel_map(fn): | |
| 122 combination_generator = ( | |
| 123 (mod_id, var_assignments) | |
| 124 for mod_id, mod in RECIPE_MODULES.iteritems() | |
| 125 if mod.CONFIG_CTX | |
| 126 for var_assignments in imap(dict, product(*[ | |
| 127 [(key_name, val) for val in vals] | |
| 128 for key_name, vals in mod.CONFIG_CTX.VAR_TEST_MAP.iteritems() | |
| 129 ])) | |
| 130 ) | |
| 131 | |
| 132 pool = multiprocessing.Pool(initializer=multiprocessing_init) | |
| 133 try: | |
| 134 return pool.map_async(fn, combination_generator).get(999999) | |
| 135 finally: | |
| 136 # necessary so that the subprocesses will write out their coverage due to | |
| 137 # the hack in multiprocessing_init() | |
| 138 pool.close() | |
| 139 pool.join() | |
| 140 | |
| 141 | |
| 142 def main(): | |
| 143 COVERAGE.erase() | |
| 144 init_recipe_modules() | |
| 145 | |
| 146 success = all(coverage_parallel_map(evaluate_configurations)) | |
| 147 | |
| 148 COVERAGE.combine() | |
| 149 total_covered = COVERAGE.report() | |
| 150 all_covered = total_covered == 100.0 | |
| 151 | |
| 152 if not success: | |
| 153 print 'FATAL: Some recipe configuration(s) failed' | |
| 154 if not all_covered: | |
| 155 print 'FATAL: Recipes configs are not at 100% coverage.' | |
| 156 | |
| 157 return 1 if (not success or not all_covered) else 0 | |
| 158 | |
| 159 | 13 |
| 160 if __name__ == '__main__': | 14 if __name__ == '__main__': |
| 161 sys.exit(main()) | 15 configs_test.main(recipe_universe.get_universe()) |
| OLD | NEW |