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

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

Issue 15270004: Add step generator protocol, remove annotated_checkout, remove script. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Checkout blobs do not need to be generators Created 7 years, 7 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
OLDNEW
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
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 ')
148 json.dump(step, f, sort_keys=True)
Mike Stip (use stip instead) 2013/05/18 00:37:22 might be possible to find a json prettifier...
iannucci 2013/05/18 04:00:36 Maybe. One thing I toyed with is emitting things i
149 first = False
150 f.write('\n]')
112 151
113 return True 152 return True
114 153
115 154
116 def load_tests(loader, _standard_tests, _pattern): 155 def load_tests(loader, _standard_tests, _pattern):
117 """This method is invoked by unittest.main's automatic testloader.""" 156 """This method is invoked by unittest.main's automatic testloader."""
118 def create_test_class(recipe_path): 157 def create_test_class(recipe_path):
119 class RecipeTest(unittest.TestCase): 158 class RecipeTest(unittest.TestCase):
120 def testExists(self): 159 def testExists(self):
121 self.assertTrue(has_test(recipe_path)) 160 self.assertTrue(has_test(recipe_path))
(...skipping 10 matching lines...) Expand all
132 with open(expected_path, 'r') as f: 171 with open(expected_path, 'r') as f:
133 expected = json.load(f) 172 expected = json.load(f)
134 self.assertEqual(steps, expected) 173 self.assertEqual(steps, expected)
135 test_.__name__ += name 174 test_.__name__ += name
136 setattr(cls, test_.__name__, test_) 175 setattr(cls, test_.__name__, test_)
137 add_test(test_fn, expected_path) 176 add_test(test_fn, expected_path)
138 177
139 if has_test(recipe_path): 178 if has_test(recipe_path):
140 RecipeTest.add_test_methods() 179 RecipeTest.add_test_methods()
141 180
142 RecipeTest.__name__ += 'for_%s' % os.path.basename(recipe_path) 181 RecipeTest.__name__ += '_for_%s' % (
182 os.path.splitext(os.path.basename(recipe_path))[0])
143 return RecipeTest 183 return RecipeTest
144 184
145 suite = unittest.TestSuite() 185 suite = unittest.TestSuite()
146 for test_class in map(create_test_class, loop_over_recipes()): 186 for test_class in map(create_test_class, loop_over_recipes()):
147 suite.addTest(loader.loadTestsFromTestCase(test_class)) 187 suite.addTest(loader.loadTestsFromTestCase(test_class))
148 return suite 188 return suite
149 189
150 190
151 def loop_over_recipes(): 191 def loop_over_recipes():
152 for _name, path in BASE_DIRS.iteritems(): 192 for _name, path in BASE_DIRS.iteritems():
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after
195 total_covered = COVERAGE.report() 235 total_covered = COVERAGE.report()
196 if total_covered != 100.0: 236 if total_covered != 100.0:
197 print 'FATAL: Recipes are not at 100% coverage.' 237 print 'FATAL: Recipes are not at 100% coverage.'
198 retcode = retcode or 2 238 retcode = retcode or 2
199 239
200 return retcode 240 return retcode
201 241
202 242
203 if __name__ == '__main__': 243 if __name__ == '__main__':
204 sys.exit(main(sys.argv)) 244 sys.exit(main(sys.argv))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698