Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 #!/usr/bin/env python | |
| 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 | |
| 4 # found in the LICENSE file. | |
| 5 | |
| 6 import contextlib | |
| 7 import json | |
| 8 import os | |
| 9 import sys | |
| 10 import unittest | |
| 11 | |
| 12 from glob import glob | |
| 13 | |
| 14 import test_env # pylint: disable=W0611 | |
| 15 | |
| 16 import coverage | |
| 17 | |
| 18 from common import annotator | |
| 19 from slave import annotated_run | |
| 20 from slave import recipe_util | |
| 21 | |
| 22 SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__)) | |
| 23 ROOT_PATH = os.path.abspath(os.path.join(SCRIPT_PATH, os.pardir, os.pardir, | |
| 24 os.pardir)) | |
| 25 SLAVE_DIR = os.path.join(ROOT_PATH, 'slave', 'fake_slave', 'build') | |
| 26 BASE_DIRS = { | |
| 27 'Public': os.path.dirname(SCRIPT_PATH) | |
| 28 } | |
| 29 # TODO(iannucci): Check for duplicate recipe names when we have more than one | |
| 30 # base_dir | |
| 31 | |
| 32 COVERAGE = coverage.coverage( | |
| 33 include=[os.path.join(x, 'recipes', '*') for x in BASE_DIRS.values()]) | |
| 34 @contextlib.contextmanager | |
|
Vadim Shtayura
2013/05/11 00:33:32
Add newline above this line.
iannucci
2013/05/11 04:12:27
Done.
| |
| 35 def cover(): | |
| 36 COVERAGE.start() | |
| 37 try: | |
| 38 yield | |
| 39 finally: | |
| 40 COVERAGE.stop() | |
| 41 | |
| 42 | |
| 43 class TestAPI(object): | |
| 44 @staticmethod | |
| 45 def tryserver_build_properties(**kwargs): | |
| 46 ret = { | |
| 47 'issue': 12853011, | |
| 48 'patchset': 1, | |
| 49 'description': 'This is a test', | |
| 50 'blamelist': ['cool_dev1337@chromium.org'], | |
| 51 'rietveld': 'https://chromiumcodereview.appspot.com', | |
| 52 } | |
| 53 ret.update(kwargs) | |
| 54 return ret | |
| 55 | |
| 56 | |
| 57 class DevNull(object): | |
| 58 @staticmethod | |
| 59 def write(data): | |
| 60 pass | |
| 61 | |
| 62 @staticmethod | |
| 63 def flush(): | |
| 64 pass | |
| 65 | |
| 66 | |
| 67 def test_path_for_recipe(recipe_path): | |
| 68 root = os.path.dirname(os.path.dirname(recipe_path)) | |
| 69 return os.path.join(root, 'recipes_test', os.path.basename(recipe_path)) | |
| 70 | |
| 71 | |
| 72 def has_test(recipe_path): | |
| 73 return os.path.exists(test_path_for_recipe(recipe_path)) | |
| 74 | |
| 75 | |
| 76 def expected_for(recipe_path, test_name): | |
| 77 test_base = os.path.splitext(test_path_for_recipe(recipe_path))[0] | |
| 78 return "%s.%s.expected" % (test_base, test_name) | |
| 79 | |
| 80 | |
| 81 def exec_test_file(recipe_path): | |
| 82 test_path = test_path_for_recipe(recipe_path) | |
| 83 gvars = {} | |
| 84 execfile(test_path, gvars) | |
| 85 return dict(item for item in gvars.iteritems() if item[0].endswith('_test')) | |
| 86 | |
| 87 | |
| 88 def execute_test_case(test_fn, recipe_path): | |
| 89 test_data = test_fn(TestAPI()) | |
| 90 bp = test_data.get('build_properties', {}) | |
| 91 fp = test_data.get('factory_properties', {}) | |
| 92 fp['recipe'] = os.path.basename(os.path.splitext(recipe_path)[0]) | |
| 93 | |
| 94 with cover(): | |
| 95 with recipe_util.mock_paths(): | |
| 96 return annotated_run.make_steps( | |
| 97 annotator.StructuredAnnotationStream(stream=DevNull()), bp, fp, True) | |
|
Vadim Shtayura
2013/05/11 00:33:32
Open real /dev/null? :)
iannucci
2013/05/11 04:12:27
No bueno on windows :/
| |
| 98 | |
| 99 | |
| 100 def train_from_tests(_base_dir_type, recipe_path): | |
| 101 if not has_test(recipe_path): | |
| 102 print 'FATAL: Recipe %s has NO tests!' % recipe_path | |
| 103 return False | |
| 104 | |
| 105 for path in glob(expected_for(recipe_path, '*')): | |
| 106 os.unlink(path) | |
| 107 | |
| 108 for name, test_fn in exec_test_file(recipe_path).iteritems(): | |
| 109 steps = execute_test_case(test_fn, recipe_path) | |
| 110 expected_path = expected_for(recipe_path, name) | |
| 111 print 'Writing', expected_path | |
| 112 with open(expected_path, 'w') as f: | |
| 113 json.dump(steps, f, indent=2, sort_keys=True) | |
| 114 | |
| 115 return True | |
| 116 | |
| 117 | |
| 118 def NoTestsForRecipe(recipe_path): | |
| 119 def test_DoesntExist(self): | |
| 120 self.assert_(False, 'No tests exist for %s' % recipe_path) | |
| 121 return test_DoesntExist | |
| 122 | |
| 123 | |
| 124 def RecipeTest(name, expected_path, test_fn, recipe_path): | |
| 125 def test_(self): | |
| 126 steps = execute_test_case(test_fn, recipe_path) | |
| 127 # Roundtrip json to get same string encoding as load | |
| 128 steps = json.loads(json.dumps(steps)) | |
| 129 with open(expected_path, 'r') as f: | |
| 130 expected = json.load(f) | |
| 131 self.assertEqual(steps, expected) | |
| 132 | |
| 133 test_.__name__ += name | |
| 134 return test_ | |
| 135 | |
| 136 | |
| 137 def add_tests(_base_dir_type, recipe_path, loader, suite): | |
|
Vadim Shtayura
2013/05/11 00:33:32
For readability reasons this stuff probably can be
iannucci
2013/05/11 04:12:27
Hm... I'll take a whack at this, but I'm not sure
| |
| 138 tests = [] | |
| 139 if not has_test(recipe_path): | |
| 140 tests.append(NoTestsForRecipe(recipe_path)) | |
| 141 else: | |
| 142 for name, test_fn in exec_test_file(recipe_path).iteritems(): | |
| 143 tests.append(RecipeTest(name, expected_for(recipe_path, name), | |
| 144 test_fn, recipe_path)) | |
| 145 | |
| 146 suite.addTest(loader.loadTestsFromTestCase( | |
| 147 type('RecipeTest_for_%s' % os.path.basename(recipe_path), | |
| 148 (unittest.TestCase,), dict((x.__name__, x) for x in tests)))) | |
| 149 return True | |
| 150 | |
| 151 | |
| 152 def load_tests(loader, standard_tests, _pattern): | |
| 153 assert not standard_tests.countTestCases() | |
| 154 suite = unittest.TestSuite() | |
| 155 assert not loop_over_recipes(add_tests, loader, suite) | |
| 156 return suite | |
| 157 | |
| 158 def loop_over_recipes(func, *args, **kwargs): | |
|
Vadim Shtayura
2013/05/11 00:33:32
Maybe convert to a generator? 'had_errors' is used
iannucci
2013/05/11 04:12:27
Done.
| |
| 159 had_errors = False | |
| 160 for name, path in BASE_DIRS.iteritems(): | |
| 161 recipe_dir = os.path.join(path, 'recipes') | |
| 162 for root, _dirs, files in os.walk(recipe_dir): | |
| 163 for recipe in (f for f in files if f.endswith('.py') and f[0] != '_'): | |
| 164 recipe_path = os.path.join(root, recipe) | |
| 165 with cover(): | |
| 166 # Force this file into coverage, even if there's no test for it. | |
| 167 execfile(recipe_path, {}) | |
|
Vadim Shtayura
2013/05/11 00:33:32
Do we care about an error in a single recipe block
iannucci
2013/05/11 04:12:27
I think the answer is 'maybe'. I'm OK with having
| |
| 168 | |
| 169 if not func(name, recipe_path, *args, **kwargs): | |
| 170 had_errors = True | |
| 171 return had_errors | |
| 172 | |
| 173 | |
| 174 def main(argv): | |
| 175 if not os.path.exists(SLAVE_DIR): | |
| 176 os.makedirs(SLAVE_DIR) | |
| 177 | |
| 178 os.chdir(SLAVE_DIR) | |
| 179 | |
| 180 training = False | |
| 181 if '--help' in argv or '-h' in argv: | |
|
Vadim Shtayura
2013/05/11 00:33:32
Use ArgumentParser or OptionParser?
iannucci
2013/05/11 04:12:27
Unfortunately we can't because of the way that uni
| |
| 182 print 'Pass --train to enter training mode.' | |
| 183 print | |
| 184 elif '--train' in argv: | |
| 185 argv.remove('--train') | |
| 186 training = True | |
| 187 | |
| 188 had_errors = loop_over_recipes(train_from_tests) if training else False | |
| 189 | |
| 190 retcode = 1 if had_errors else 0 | |
| 191 | |
| 192 if not training: | |
| 193 try: | |
| 194 unittest.main() | |
| 195 except SystemExit as e: | |
| 196 retcode = e.code or retcode | |
| 197 | |
| 198 total_covered = COVERAGE.report() | |
| 199 if total_covered != 100.0: | |
| 200 print 'FATAL: Recipes are not at 100% coverage.' | |
| 201 retcode = retcode or 2 | |
| 202 | |
| 203 return retcode | |
| 204 | |
| 205 | |
| 206 if __name__ == '__main__': | |
| 207 sys.exit(main(sys.argv)) | |
| OLD | NEW |