OLD | NEW |
---|---|
(Empty) | |
1 #!/usr/bin/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 | |
35 | |
36 @contextlib.contextmanager | |
37 def cover(): | |
38 COVERAGE.start() | |
39 try: | |
40 yield | |
41 finally: | |
42 COVERAGE.stop() | |
43 | |
44 | |
45 class TestAPI(object): | |
46 | |
47 @staticmethod | |
48 def tryserver_build_properties(**kwargs): | |
49 ret = { | |
50 'issue': 12853011, | |
51 'patchset': 1, | |
52 'blamelist': ['cool_dev1337@chromium.org'], | |
53 'rietveld': 'https://chromiumcodereview.appspot.com', | |
54 } | |
55 ret.update(kwargs) | |
56 return ret | |
57 | |
58 | |
59 def test_path_for_recipe(recipe_path): | |
60 root = os.path.dirname(os.path.dirname(recipe_path)) | |
61 return os.path.join(root, 'recipes_test', os.path.basename(recipe_path)) | |
agable
2013/05/14 20:47:23
This would fail if we ever put recipes in categori
iannucci
2013/05/14 21:32:08
Yeah I'm not sure what that would look like.
| |
62 | |
63 | |
64 def has_test(recipe_path): | |
65 return os.path.exists(test_path_for_recipe(recipe_path)) | |
66 | |
67 | |
68 def expected_for(recipe_path, test_name): | |
69 test_base = os.path.splitext(test_path_for_recipe(recipe_path))[0] | |
70 return '%s.%s.expected' % (test_base, test_name) | |
71 | |
72 | |
73 def exec_test_file(recipe_path): | |
74 test_path = test_path_for_recipe(recipe_path) | |
75 gvars = {} | |
76 execfile(test_path, gvars) | |
77 ret = {} | |
78 for name, value in gvars.iteritems(): | |
79 if name.endswith('_test'): | |
80 ret[name[:-len('_test')]] = value | |
81 return ret | |
82 | |
83 | |
84 def execute_test_case(test_fn, recipe_path): | |
85 test_data = test_fn(TestAPI()) | |
86 bp = test_data.get('build_properties', {}) | |
87 fp = test_data.get('factory_properties', {}) | |
88 fp['recipe'] = os.path.basename(os.path.splitext(recipe_path)[0]) | |
89 | |
90 stream = annotator.StructuredAnnotationStream(stream=open(os.devnull, 'w')) | |
91 with cover(): | |
92 with recipe_util.mock_paths(): | |
93 retval = annotated_run.make_steps(stream, bp, fp, True) | |
94 assert retval.status_code is None | |
95 return retval.script or retval.steps | |
96 | |
97 | |
98 def train_from_tests(recipe_path): | |
99 if not has_test(recipe_path): | |
100 print 'FATAL: Recipe %s has NO tests!' % recipe_path | |
101 return False | |
102 | |
103 for path in glob(expected_for(recipe_path, '*')): | |
104 os.unlink(path) | |
105 | |
106 for name, test_fn in exec_test_file(recipe_path).iteritems(): | |
107 steps = execute_test_case(test_fn, recipe_path) | |
108 expected_path = expected_for(recipe_path, name) | |
109 print 'Writing', expected_path | |
110 with open(expected_path, 'w') as f: | |
111 json.dump(steps, f, indent=2, sort_keys=True) | |
112 | |
113 return True | |
114 | |
115 | |
116 def load_tests(loader, _standard_tests, _pattern): | |
117 """This method is invoked by unittest.main's automatic testloader.""" | |
118 def create_test_class(recipe_path): | |
119 class RecipeTest(unittest.TestCase): | |
120 def testExists(self): | |
121 self.assertTrue(has_test(recipe_path)) | |
122 | |
123 @classmethod | |
124 def add_test_methods(cls): | |
125 for name, test_fn in exec_test_file(recipe_path).iteritems(): | |
126 expected_path = expected_for(recipe_path, name) | |
127 def test_(self): | |
128 steps = execute_test_case(test_fn, recipe_path) | |
129 # Roundtrip json to get same string encoding as load | |
130 steps = json.loads(json.dumps(steps)) | |
131 with open(expected_path, 'r') as f: | |
132 expected = json.load(f) | |
133 self.assertEqual(steps, expected) | |
134 test_.__name__ += name | |
135 setattr(cls, test_.__name__, test_) | |
136 | |
137 if has_test(recipe_path): | |
138 RecipeTest.add_test_methods() | |
139 | |
140 RecipeTest.__name__ += 'for_%s' % os.path.basename(recipe_path) | |
141 return RecipeTest | |
142 | |
143 suite = unittest.TestSuite() | |
144 for test_class in map(create_test_class, loop_over_recipes()): | |
145 suite.addTest(loader.loadTestsFromTestCase(test_class)) | |
146 return suite | |
147 | |
148 | |
149 def loop_over_recipes(): | |
150 for _name, path in BASE_DIRS.iteritems(): | |
151 recipe_dir = os.path.join(path, 'recipes') | |
152 for root, _dirs, files in os.walk(recipe_dir): | |
153 for recipe in (f for f in files if f.endswith('.py') and f[0] != '_'): | |
154 recipe_path = os.path.join(root, recipe) | |
155 with cover(): | |
156 # Force this file into coverage, even if there's no test for it. | |
157 execfile(recipe_path, {}) | |
158 yield recipe_path | |
159 | |
160 | |
161 def main(argv): | |
162 if not os.path.exists(SLAVE_DIR): | |
163 os.makedirs(SLAVE_DIR) | |
164 | |
165 os.chdir(SLAVE_DIR) | |
166 | |
167 training = False | |
168 is_help = False | |
169 if '--help' in argv or '-h' in argv: | |
170 print 'Pass --train to enter training mode.' | |
171 print | |
172 is_help = True | |
173 if '--train' in argv: | |
174 argv.remove('--train') | |
175 training = True | |
176 | |
177 had_errors = False | |
178 if training and not is_help: | |
179 for result in map(train_from_tests, loop_over_recipes()): | |
180 had_errors = had_errors or result | |
181 if had_errors: | |
182 break | |
183 | |
184 retcode = 1 if had_errors else 0 | |
185 | |
186 if not training: | |
187 try: | |
188 unittest.main() | |
189 except SystemExit as e: | |
190 retcode = e.code or retcode | |
191 | |
192 if not is_help: | |
193 total_covered = COVERAGE.report() | |
194 if total_covered != 100.0: | |
195 print 'FATAL: Recipes are not at 100% coverage.' | |
196 retcode = retcode or 2 | |
197 | |
198 return retcode | |
199 | |
200 | |
201 if __name__ == '__main__': | |
202 sys.exit(main(sys.argv)) | |
OLD | NEW |