| OLD | NEW |
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # Copyright 2013 The Chromium Authors. All rights reserved. | 2 # Copyright 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. | 6 """Provides test coverage for common recipe configurations. |
| 7 | 7 |
| 8 recipe config expectations are located in ../recipe_configs_test/*.expected | 8 recipe config expectations are located in ../recipe_configs_test/*.expected |
| 9 | 9 |
| 10 In training mode, this will loop over every config item in ../recipe_configs.py | 10 In training mode, this will loop over every config item in ../recipe_configs.py |
| (...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 81 return file_name, None, None | 81 return file_name, None, None |
| 82 | 82 |
| 83 for config_name, fn in ctx.CONFIG_ITEMS.iteritems(): | 83 for config_name, fn in ctx.CONFIG_ITEMS.iteritems(): |
| 84 if fn.NO_TEST or fn.IS_ROOT: | 84 if fn.NO_TEST or fn.IS_ROOT: |
| 85 continue | 85 continue |
| 86 try: | 86 try: |
| 87 result = covered(fn, make_item()) | 87 result = covered(fn, make_item()) |
| 88 if result.complete(): | 88 if result.complete(): |
| 89 ret[config_name] = result.as_jsonish() | 89 ret[config_name] = result.as_jsonish() |
| 90 except recipe_configs_util.BadConf, e: | 90 except recipe_configs_util.BadConf, e: |
| 91 ret[config_name] = e.message | 91 ret[config_name] = str(e) |
| 92 return file_name, (ctx.TEST_NAME_FORMAT % var_assignments), ret | 92 return file_name, (ctx.TEST_NAME_FORMAT % var_assignments), ret |
| 93 except Exception, e: | 93 except Exception, e: |
| 94 print 'Caught exception [%s] with args %s: %s' % (e, args, config_name) | 94 print 'Caught exception [%s] with args %s: %s' % (e, args, config_name) |
| 95 | 95 |
| 96 | 96 |
| 97 def train_from_tests(args): | 97 def train_from_tests(args): |
| 98 file_name, _, configuration_results = evaluate_configurations(args) | 98 file_name, _, configuration_results = evaluate_configurations(args) |
| 99 if configuration_results is not None: | 99 if configuration_results is not None: |
| 100 if configuration_results: | 100 if configuration_results: |
| 101 print 'Writing', file_name | 101 print 'Writing', file_name |
| 102 with open(file_name, 'w') as f: | 102 with open(file_name, 'wb') as f: |
| 103 json.dump(configuration_results, f, sort_keys=True, indent=2) | 103 json.dump(configuration_results, f, sort_keys=True, indent=2) |
| 104 else: | 104 else: |
| 105 print 'Empty', file_name | 105 print 'Empty', file_name |
| 106 | 106 |
| 107 if not configuration_results: # None or {} | 107 if not configuration_results: # None or {} |
| 108 if os.path.exists(file_name): | 108 if os.path.exists(file_name): |
| 109 os.unlink(file_name) | 109 os.unlink(file_name) |
| 110 return True | 110 return True |
| 111 | 111 |
| 112 | 112 |
| 113 def load_tests(loader, _standard_tests, _pattern): | 113 def load_tests(loader, _standard_tests, _pattern): |
| 114 """This method is invoked by unittest.main's automatic testloader.""" | 114 """This method is invoked by unittest.main's automatic testloader.""" |
| 115 def create_test_class(args): | 115 def create_test_class(args): |
| 116 file_name, test_name_suffix, configuration_results = args | 116 file_name, test_name_suffix, configuration_results = args |
| 117 if configuration_results is None: | 117 if configuration_results is None: |
| 118 return | 118 return |
| 119 | 119 |
| 120 json_expectation = {} | 120 json_expectation = {} |
| 121 if os.path.exists(file_name): | 121 if os.path.exists(file_name): |
| 122 with open(file_name, 'r') as f: | 122 with open(file_name, 'rb') as f: |
| 123 json_expectation = json.load(f) | 123 json_expectation = json.load(f) |
| 124 | 124 |
| 125 class RecipeConfigsTest(unittest.TestCase): | 125 class RecipeConfigsTest(unittest.TestCase): |
| 126 def testNoLeftovers(self): | 126 def testNoLeftovers(self): |
| 127 """add_test_methods() should completely drain json_expectation.""" | 127 """add_test_methods() should completely drain json_expectation.""" |
| 128 self.assertEqual(json_expectation, {}) | 128 self.assertEqual(json_expectation, {}) |
| 129 | 129 |
| 130 @classmethod | 130 @classmethod |
| 131 def add_test_methods(cls): | 131 def add_test_methods(cls): |
| 132 for name, result in configuration_results.iteritems(): | 132 for name, result in configuration_results.iteritems(): |
| (...skipping 13 matching lines...) Expand all Loading... |
| 146 | 146 |
| 147 suite = unittest.TestSuite() | 147 suite = unittest.TestSuite() |
| 148 for test_class in map(create_test_class, data): | 148 for test_class in map(create_test_class, data): |
| 149 if test_class is None: | 149 if test_class is None: |
| 150 continue | 150 continue |
| 151 suite.addTest(loader.loadTestsFromTestCase(test_class)) | 151 suite.addTest(loader.loadTestsFromTestCase(test_class)) |
| 152 return suite | 152 return suite |
| 153 | 153 |
| 154 | 154 |
| 155 def multiprocessing_init(): | 155 def multiprocessing_init(): |
| 156 # HACK: multiprocessing doesn't work with atexit, so shim os._exit instead. | 156 init_recipe_modules() |
| 157 # This allows us to save exactly one coverage file per subprocess | 157 |
| 158 # HACK: multiprocessing doesn't work with atexit, so shim the exit functions |
| 159 # instead. This allows us to save exactly one coverage file per subprocess. |
| 158 # pylint: disable=W0212 | 160 # pylint: disable=W0212 |
| 159 init_recipe_modules() | 161 real_os_exit = multiprocessing.forking.exit |
| 160 real_os_exit = os._exit | |
| 161 def exitfn(code): | 162 def exitfn(code): |
| 162 COVERAGE.save() | 163 COVERAGE.save() |
| 163 real_os_exit(code) | 164 real_os_exit(code) |
| 164 os._exit = exitfn | 165 multiprocessing.forking.exit = exitfn |
| 166 |
| 167 # This check mirrors the logic in multiprocessing.forking.exit |
| 168 if sys.platform != 'win32': |
| 169 # Even though multiprocessing.forking.exit is defined, it's not used in the |
| 170 # non-win32 version multiprocessing.forking.Popen... *loss for words* |
| 171 os._exit = exitfn |
| 165 | 172 |
| 166 | 173 |
| 167 def coverage_parallel_map(fn): | 174 def coverage_parallel_map(fn): |
| 168 combination_generator = ( | 175 combination_generator = ( |
| 169 (mod_id, var_assignments) | 176 (mod_id, var_assignments) |
| 170 for mod_id, mod in RECIPE_MODULES.__dict__.iteritems() | 177 for mod_id, mod in RECIPE_MODULES.__dict__.iteritems() |
| 171 if mod_id[0] != '_' and mod.CONFIG_CTX | 178 if mod_id[0] != '_' and mod.CONFIG_CTX |
| 172 for var_assignments in imap(dict, product(*[ | 179 for var_assignments in imap(dict, product(*[ |
| 173 [(key_name, val) for val in vals] | 180 [(key_name, val) for val in vals] |
| 174 for key_name, vals in mod.CONFIG_CTX.VAR_TEST_MAP.iteritems() | 181 for key_name, vals in mod.CONFIG_CTX.VAR_TEST_MAP.iteritems() |
| (...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 223 total_covered = COVERAGE.report() | 230 total_covered = COVERAGE.report() |
| 224 if total_covered != 100.0: | 231 if total_covered != 100.0: |
| 225 print 'FATAL: Recipes configs are not at 100% coverage.' | 232 print 'FATAL: Recipes configs are not at 100% coverage.' |
| 226 retcode = retcode or 2 | 233 retcode = retcode or 2 |
| 227 | 234 |
| 228 return retcode | 235 return retcode |
| 229 | 236 |
| 230 | 237 |
| 231 if __name__ == '__main__': | 238 if __name__ == '__main__': |
| 232 sys.exit(main(sys.argv)) | 239 sys.exit(main(sys.argv)) |
| OLD | NEW |