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 |