| OLD | NEW |
| 1 #!/usr/bin/python | 1 #!/usr/bin/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 """Provides test coverage for individual recipes. |
| 7 |
| 8 Recipe tests are located in ../recipes_test/*.py. |
| 9 |
| 10 Each py file's splitext'd name is expected to match a recipe in ../recipes/*.py. |
| 11 |
| 12 Each test py file contains one or more test functions: |
| 13 * A test function's name ends with '_test' and takes an instance of TestAPI |
| 14 as its only parameter. |
| 15 * The test should return a dictionary with any of the following keys: |
| 16 * factory_properties |
| 17 * build_properties |
| 18 * test_data |
| 19 * test_data's value should be a dictionary in the form of |
| 20 {stepname -> (retcode, json_data)} |
| 21 * Since the test doesn't run any steps, test_data allows you to simulate |
| 22 return values for particular steps. |
| 23 |
| 24 Once your test methods are set up, run `recipes_test.py --train`. This will |
| 25 take your tests and simulate what steps would have run, given the test inputs, |
| 26 and will record them as JSON into files of the form: |
| 27 ../recipes_test/<recipe_name>.<test_name>.expected |
| 28 |
| 29 If those files look right, make sure they get checked in with your changes. |
| 30 |
| 31 When this file runs as a test (i.e. as `recipes_test.py`), it will re-evaluate |
| 32 the recipes using the test function input data and compare the result to the |
| 33 values recorded in the .expected files. |
| 34 |
| 35 Additionally, this test cannot pass unless every recipe in ../recipes has 100% |
| 36 code coverage when executed via the tests in ../recipes_test. |
| 37 """ |
| 38 |
| 6 import contextlib | 39 import contextlib |
| 7 import json | 40 import json |
| 8 import os | 41 import os |
| 9 import sys | 42 import sys |
| 10 import unittest | 43 import unittest |
| 11 | 44 |
| 12 from glob import glob | 45 from glob import glob |
| 13 | 46 |
| 14 import test_env # pylint: disable=W0611 | 47 import test_env # pylint: disable=W0611 |
| 15 | 48 |
| (...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 78 for name, value in gvars.iteritems(): | 111 for name, value in gvars.iteritems(): |
| 79 if name.endswith('_test'): | 112 if name.endswith('_test'): |
| 80 ret[name[:-len('_test')]] = value | 113 ret[name[:-len('_test')]] = value |
| 81 return ret | 114 return ret |
| 82 | 115 |
| 83 | 116 |
| 84 def execute_test_case(test_fn, recipe_path): | 117 def execute_test_case(test_fn, recipe_path): |
| 85 test_data = test_fn(TestAPI()) | 118 test_data = test_fn(TestAPI()) |
| 86 bp = test_data.get('build_properties', {}) | 119 bp = test_data.get('build_properties', {}) |
| 87 fp = test_data.get('factory_properties', {}) | 120 fp = test_data.get('factory_properties', {}) |
| 121 td = test_data.get('test_data', {}) |
| 88 fp['recipe'] = os.path.basename(os.path.splitext(recipe_path)[0]) | 122 fp['recipe'] = os.path.basename(os.path.splitext(recipe_path)[0]) |
| 89 | 123 |
| 90 stream = annotator.StructuredAnnotationStream(stream=open(os.devnull, 'w')) | 124 stream = annotator.StructuredAnnotationStream(stream=open(os.devnull, 'w')) |
| 91 with cover(): | 125 with cover(): |
| 92 with recipe_util.mock_paths(): | 126 with recipe_util.mock_paths(): |
| 93 retval = annotated_run.make_steps(stream, bp, fp, True) | 127 step_data = annotated_run.run_steps(stream, bp, fp, td).steps_ran.values() |
| 94 assert retval.status_code is None | 128 return [s.step for s in step_data] |
| 95 return retval.script or retval.steps | |
| 96 | 129 |
| 97 | 130 |
| 98 def train_from_tests(recipe_path): | 131 def train_from_tests(recipe_path): |
| 99 if not has_test(recipe_path): | 132 if not has_test(recipe_path): |
| 100 print 'FATAL: Recipe %s has NO tests!' % recipe_path | 133 print 'FATAL: Recipe %s has NO tests!' % recipe_path |
| 101 return False | 134 return False |
| 102 | 135 |
| 103 for path in glob(expected_for(recipe_path, '*')): | 136 for path in glob(expected_for(recipe_path, '*')): |
| 104 os.unlink(path) | 137 os.unlink(path) |
| 105 | 138 |
| 106 for name, test_fn in exec_test_file(recipe_path).iteritems(): | 139 for name, test_fn in exec_test_file(recipe_path).iteritems(): |
| 107 steps = execute_test_case(test_fn, recipe_path) | 140 steps = execute_test_case(test_fn, recipe_path) |
| 108 expected_path = expected_for(recipe_path, name) | 141 expected_path = expected_for(recipe_path, name) |
| 109 print 'Writing', expected_path | 142 print 'Writing', expected_path |
| 110 with open(expected_path, 'w') as f: | 143 with open(expected_path, 'w') as f: |
| 111 json.dump(steps, f, indent=2, sort_keys=True) | 144 f.write('[') |
| 145 first = True |
| 146 for step in steps: |
| 147 f.write(('' if first else '\n },')+'\n {') |
| 148 first_dict_item = True |
| 149 for key, value in sorted(step.items(), key=lambda x: x[0]): |
| 150 f.write(('' if first_dict_item else ',')+'\n ') |
| 151 f.write('"%s": ' % key) |
| 152 json.dump(value, f, sort_keys=True) |
| 153 first_dict_item = False |
| 154 first = False |
| 155 f.write('\n }\n]') |
| 112 | 156 |
| 113 return True | 157 return True |
| 114 | 158 |
| 115 | 159 |
| 116 def load_tests(loader, _standard_tests, _pattern): | 160 def load_tests(loader, _standard_tests, _pattern): |
| 117 """This method is invoked by unittest.main's automatic testloader.""" | 161 """This method is invoked by unittest.main's automatic testloader.""" |
| 118 def create_test_class(recipe_path): | 162 def create_test_class(recipe_path): |
| 119 class RecipeTest(unittest.TestCase): | 163 class RecipeTest(unittest.TestCase): |
| 120 def testExists(self): | 164 def testExists(self): |
| 121 self.assertTrue(has_test(recipe_path)) | 165 self.assertTrue(has_test(recipe_path)) |
| (...skipping 10 matching lines...) Expand all Loading... |
| 132 with open(expected_path, 'r') as f: | 176 with open(expected_path, 'r') as f: |
| 133 expected = json.load(f) | 177 expected = json.load(f) |
| 134 self.assertEqual(steps, expected) | 178 self.assertEqual(steps, expected) |
| 135 test_.__name__ += name | 179 test_.__name__ += name |
| 136 setattr(cls, test_.__name__, test_) | 180 setattr(cls, test_.__name__, test_) |
| 137 add_test(test_fn, expected_path) | 181 add_test(test_fn, expected_path) |
| 138 | 182 |
| 139 if has_test(recipe_path): | 183 if has_test(recipe_path): |
| 140 RecipeTest.add_test_methods() | 184 RecipeTest.add_test_methods() |
| 141 | 185 |
| 142 RecipeTest.__name__ += 'for_%s' % os.path.basename(recipe_path) | 186 RecipeTest.__name__ += '_for_%s' % ( |
| 187 os.path.splitext(os.path.basename(recipe_path))[0]) |
| 143 return RecipeTest | 188 return RecipeTest |
| 144 | 189 |
| 145 suite = unittest.TestSuite() | 190 suite = unittest.TestSuite() |
| 146 for test_class in map(create_test_class, loop_over_recipes()): | 191 for test_class in map(create_test_class, loop_over_recipes()): |
| 147 suite.addTest(loader.loadTestsFromTestCase(test_class)) | 192 suite.addTest(loader.loadTestsFromTestCase(test_class)) |
| 148 return suite | 193 return suite |
| 149 | 194 |
| 150 | 195 |
| 151 def loop_over_recipes(): | 196 def loop_over_recipes(): |
| 152 for _name, path in BASE_DIRS.iteritems(): | 197 for _name, path in BASE_DIRS.iteritems(): |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 195 total_covered = COVERAGE.report() | 240 total_covered = COVERAGE.report() |
| 196 if total_covered != 100.0: | 241 if total_covered != 100.0: |
| 197 print 'FATAL: Recipes are not at 100% coverage.' | 242 print 'FATAL: Recipes are not at 100% coverage.' |
| 198 retcode = retcode or 2 | 243 retcode = retcode or 2 |
| 199 | 244 |
| 200 return retcode | 245 return retcode |
| 201 | 246 |
| 202 | 247 |
| 203 if __name__ == '__main__': | 248 if __name__ == '__main__': |
| 204 sys.exit(main(sys.argv)) | 249 sys.exit(main(sys.argv)) |
| OLD | NEW |