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 |