Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(173)

Side by Side Diff: scripts/slave/unittests/recipes_test.py

Issue 220353003: Replace recipes_test.py with a new parallel expectation-based test runner. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: fix coverage race Created 6 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « scripts/slave/unittests/recipe_simulation_test.py ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(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 """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
39 import contextlib
40 import json
41 import os
42 import sys
43
44 from glob import glob
45
46 import test_env # pylint: disable=F0401,W0403,W0611
47
48 import coverage
49
50 import common.python26_polyfill # pylint: disable=W0611
51 import unittest
52
53 from common import annotator
54 from slave import recipe_util
55 from slave import recipe_config_types
56 from slave import annotated_run
57 from slave import recipe_loader
58
59 SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__))
60 ROOT_PATH = os.path.abspath(os.path.join(SCRIPT_PATH, os.pardir, os.pardir,
61 os.pardir))
62 SLAVE_DIR = os.path.join(ROOT_PATH, 'slave', 'fake_slave', 'build')
63
64 BASE_DIRS = recipe_util.BASE_DIRS
65 COVERAGE = None
66
67
68 @contextlib.contextmanager
69 def cover():
70 COVERAGE.start()
71 try:
72 yield
73 finally:
74 COVERAGE.stop()
75
76 def expected_for(recipe_path, test_name):
77 root, name = os.path.split(recipe_path)
78 name = os.path.splitext(name)[0]
79 expect_path = os.path.join(root, '%s.expected' % name)
80 if not os.path.isdir(expect_path):
81 os.makedirs(expect_path)
82 return os.path.join(expect_path, test_name+'.json')
83
84
85 def exec_test_file(recipe_name):
86 with cover():
87 recipe = recipe_loader.load_recipe(recipe_name)
88 try:
89 test_api = recipe_loader.create_test_api(recipe.DEPS)
90 gen = recipe.GenTests(test_api)
91 except Exception, e:
92 print "Caught exception while processing %s: %s" % (recipe_name, e)
93 raise
94 try:
95 while True:
96 with cover():
97 test_data = next(gen)
98 yield test_data
99 except StopIteration:
100 pass
101 except:
102 print 'Exception while processing "%s"!' % recipe_name
103 raise
104
105
106 def execute_test_case(test_data, recipe_path, recipe_name):
107 try:
108 props = test_data.properties
109 props['recipe'] = recipe_name
110
111 stream = annotator.StructuredAnnotationStream(stream=open(os.devnull, 'w'))
112
113 with cover():
114 recipe_config_types.ResetTostringFns()
115 step_data = annotated_run.run_steps(
116 stream, props, props, test_data).steps_ran.values()
117 return [s.step for s in step_data]
118 except:
119 print 'Exception while processing test case: "%s"!' % test_data.name
120 raise
121
122
123 def train_from_tests((recipe_path, recipe_name)):
124 for path in glob(expected_for(recipe_path, '*')):
125 os.unlink(path)
126
127 for test_data in exec_test_file(recipe_name):
128 steps = execute_test_case(test_data, recipe_path, recipe_name)
129 expected_path = expected_for(recipe_path, test_data.name)
130 print 'Writing', expected_path
131 with open(expected_path, 'wb') as f:
132 json.dump(steps, f, sort_keys=True, indent=2, separators=(',', ': '))
133
134 return True
135
136
137 def load_tests(loader, _standard_tests, _pattern):
138 """This method is invoked by unittest.main's automatic testloader."""
139 def create_test_class((recipe_path, recipe_name)):
140 class RecipeTest(unittest.TestCase):
141 @classmethod
142 def add_test_methods(cls):
143 for test_data in exec_test_file(recipe_name):
144 expected_path = expected_for(recipe_path, test_data.name)
145 def add_test(test_data, expected_path, recipe_name):
146 def test_(self):
147 steps = execute_test_case(test_data, recipe_path, recipe_name)
148 # Roundtrip json to get same string encoding as load
149 steps = json.loads(json.dumps(steps))
150 with open(expected_path, 'rb') as f:
151 expected = json.load(f)
152 self.assertEqual(steps, expected)
153 test_.__name__ += test_data.name
154 setattr(cls, test_.__name__, test_)
155 add_test(test_data, expected_path, recipe_name)
156
157 RecipeTest.add_test_methods()
158
159 RecipeTest.__name__ += '_for_%s' % (
160 os.path.splitext(os.path.basename(recipe_path))[0])
161 return RecipeTest
162
163 suite = unittest.TestSuite()
164 for test_class in map(create_test_class, recipe_loader.loop_over_recipes()):
165 suite.addTest(loader.loadTestsFromTestCase(test_class))
166 return suite
167
168
169 def main(argv):
170 # Pop these out so that we always generate consistent expectations, even
171 # if we're running the tests under a testing slave configuration (or if
172 # someone just has these set in their shell)
173 os.environ.pop('TESTING_MASTERNAME', None)
174 os.environ.pop('TESTING_SLAVENAME', None)
175
176 if not os.path.exists(SLAVE_DIR):
177 os.makedirs(SLAVE_DIR)
178
179 os.chdir(SLAVE_DIR)
180
181 training = False
182 is_help = False
183 if '--help' in argv or '-h' in argv:
184 print 'Pass --train to enter training mode.'
185 print
186 is_help = True
187 if '--train' in argv:
188 argv.remove('--train')
189 training = True
190 if '--external' in argv:
191 argv.remove('--external')
192 BASE_DIRS[:] = [d for d in BASE_DIRS if 'internal' not in d]
193 global COVERAGE
194 COVERAGE = coverage.coverage(
195 include=(
196 [os.path.join(x, '*') for x in recipe_util.RECIPE_DIRS()] +
197 [os.path.join(x, '*', '*api.py') for x in recipe_util.MODULE_DIRS()]
198 )
199 )
200
201 had_errors = False
202 if training and not is_help:
203 for result in map(train_from_tests, recipe_loader.loop_over_recipes()):
204 had_errors = had_errors or result
205 if had_errors:
206 break
207
208 retcode = 1 if had_errors else 0
209
210 if not training:
211 try:
212 unittest.main()
213 except SystemExit as e:
214 retcode = e.code or retcode
215
216 if not is_help:
217 total_covered = COVERAGE.report()
218 if total_covered != 100.0:
219 print 'FATAL: Recipes are not at 100% coverage.'
220 retcode = retcode or 2
221
222 if training:
223 test_env.print_coverage_warning()
224
225 return retcode
226
227
228 if __name__ == '__main__':
229 sys.exit(main(sys.argv))
OLDNEW
« no previous file with comments | « scripts/slave/unittests/recipe_simulation_test.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698