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

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

Issue 14988009: First cut of testing infrastructure for recipes. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Add comment 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
(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))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698