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

Side by Side Diff: unittests/test_test.py

Issue 2721613004: simulation_test_ng: initial CL (Closed)
Patch Set: tests Created 3 years, 9 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
« recipes.py ('K') | « unittests/stdlib_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/env python
2 # Copyright 2017 The LUCI Authors. All rights reserved.
3 # Use of this source code is governed under the Apache License, Version 2.0
4 # that can be found in the LICENSE file.
5
6 import json
7 import os
8 import shutil
9 import subprocess
10 import sys
11 import tempfile
12 import unittest
13
14
15 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
16 sys.path.insert(0, ROOT_DIR)
17 import recipe_engine.env
18
19
20 from recipe_engine import package
21 from recipe_engine import package_pb2
22
23
24 class RecipeWriter(object):
25 """Helper to write a recipe for tests."""
26
27 def __init__(self, root_dir, name):
28 self.root_dir = root_dir
29 self.name = name
30
31 # These are expected to be set appropriately by the caller.
32 self.DEPS = []
33 self.RunStepsLines = ['pass']
34 self.GenTestsLines = ['yield api.test("basic")']
35 self.expectations = {}
36
37 @property
38 def recipes_dir(self):
39 return os.path.join(self.root_dir, 'recipes')
40
41 @property
42 def expect_dir(self):
43 return os.path.join(self.recipes_dir, '%s.expected' % self.name)
44
45 def add_expectation(self, test_name, commands=None, recipe_result=None,
46 status_code=0):
47 """Adds expectation for a simulation test.
48
49 Arguments:
50 test_name(str): name of the test
51 commands(list): list of expected commands
iannucci 2017/03/10 01:20:09 I think this is a list of expectation dictionaries
Paweł Hajdan Jr. 2017/03/10 17:20:30 Done.
52 recipe_result(object): expected result of the recipe
53 status_code(int): expected exit code
54 """
55 self.expectations[test_name] = (commands or []) + [{
56 'name': '$result',
57 'recipe_result': recipe_result,
58 'status_code': status_code
59 }]
60
61 def write(self):
62 """Writes the recipe to disk."""
63 for d in (self.recipes_dir, self.expect_dir):
64 if not os.path.exists(d):
65 os.makedirs(d)
66 with open(os.path.join(self.recipes_dir, '%s.py' % self.name), 'w') as f:
67 f.write('\n'.join([
68 'from recipe_engine import post_process',
69 '',
70 'DEPS = %r' % self.DEPS,
71 '',
72 'def RunSteps(api):',
73 ] + [' %s' % l for l in self.RunStepsLines] + [
74 '',
75 'def GenTests(api):',
76 ] + [' %s' % l for l in self.GenTestsLines]))
77 for test_name, test_contents in self.expectations.iteritems():
78 with open(os.path.join(self.expect_dir, '%s.json' % test_name), 'w') as f:
79 json.dump(test_contents, f)
80
81
82 class RecipeModuleWriter(object):
83 """Helper to write a recipe module for tests."""
84
85 def __init__(self, root_dir, name):
86 self.root_dir = root_dir
87 self.name = name
88
89 # These are expected to be set appropriately by the caller.
90 self.DEPS = []
91 self.disable_strict_coverage = False
92 self.methods = {}
93
94 self.example = type('example_placeholder', (), {})()
95 self.example.enabled = False
96 self.example.DEPS = []
97 self.example.RunStepsLines = ['pass']
98 self.example.GenTestsLines = ['yield api.test("basic")']
99 self.example.expectations = {}
100
101 @property
102 def module_dir(self):
103 return os.path.join(self.root_dir, 'recipe_modules', self.name)
104
105 @property
106 def expect_dir(self):
107 return os.path.join(self.module_dir, 'example.expected')
108
109 def add_expectation(self, test_name, commands=None, recipe_result=None,
110 status_code=0):
111 """Adds expectation for a simulation test.
112
113 Arguments:
114 test_name(str): name of the test
115 commands(list): list of expected commands
116 recipe_result(object): expected result of the recipe
117 status_code(int): expected exit code
118 """
119 self.example.expectations[test_name] = (commands or []) + [{
120 'name': '$result',
121 'recipe_result': recipe_result,
122 'status_code': status_code
123 }]
124
125 def write(self):
126 """Writes the recipe module to disk."""
127
128 for d in (self.module_dir, self.expect_dir):
129 if not os.path.exists(d):
130 os.makedirs(d)
131
132 with open(os.path.join(self.module_dir, '__init__.py'), 'w') as f:
133 f.write('DEPS = %r\n' % self.DEPS)
134 if self.disable_strict_coverage:
135 f.write('\nDISABLE_STRICT_COVERAGE = True')
136
137 api_lines = [
138 'from recipe_engine import recipe_api',
139 '',
140 'class MyApi(recipe_api.RecipeApi):',
141 ]
142 if self.methods:
143 for m_name, m_lines in self.methods.iteritems():
144 api_lines.extend([
145 '',
146 ' def %s(self):' % m_name,
147 ] + [' %s' % l for l in m_lines] + [
148 '',
149 ])
150 else:
151 api_lines.append(' pass')
152 with open(os.path.join(self.module_dir, 'api.py'), 'w') as f:
153 f.write('\n'.join(api_lines))
154 if self.example.enabled:
155 with open(os.path.join(self.module_dir, 'example.py'), 'w') as f:
156 f.write('\n'.join([
157 'from recipe_engine import post_process',
158 '',
159 'DEPS = %r' % self.example.DEPS,
160 '',
161 'def RunSteps(api):',
162 ] + [' %s' % l for l in self.example.RunStepsLines] + [
163 '',
164 'def GenTests(api):',
iannucci 2017/03/10 01:20:09 this looks duplicated from RecipeWriter... could a
Paweł Hajdan Jr. 2017/03/10 17:20:30 I was considering that, but IMO it could make thin
iannucci 2017/03/10 18:14:33 How is it more complex to reuse a well-defined cla
165 ] + [' %s' % l for l in self.example.GenTestsLines]))
166 for test_name, test_contents in self.example.expectations.iteritems():
167 with open(os.path.join(
168 self.expect_dir, '%s.json' % test_name), 'w') as f:
169 json.dump(test_contents, f)
170
171
172 class TestTest(unittest.TestCase):
173 def setUp(self):
174 root_dir = tempfile.mkdtemp()
175 config_dir = os.path.join(root_dir, 'infra', 'config')
176 os.makedirs(config_dir)
177
178 self._root_dir = root_dir
179 self._recipes_cfg = os.path.join(config_dir, 'recipes.cfg')
180 self._recipe_tool = os.path.join(ROOT_DIR, 'recipes.py')
181
182 test_pkg = package_pb2.Package(
183 api_version=1,
184 project_id='test_pkg',
185 recipes_path='',
186 deps=[
187 package_pb2.DepSpec(
188 project_id='recipe_engine',
189 url='file://'+ROOT_DIR),
190 ],
191 )
192 package.ProtoFile(self._recipes_cfg).write(test_pkg)
193
194 def tearDown(self):
195 shutil.rmtree(self._root_dir)
196
197 def _run_recipes(self, *args):
198 return subprocess.check_output((
199 sys.executable,
200 self._recipe_tool,
201 '--use-bootstrap',
202 '--package', self._recipes_cfg,
203 ) + args, stderr=subprocess.STDOUT)
204
205 def test_list(self):
206 rw = RecipeWriter(self._root_dir, 'foo')
207 rw.RunStepsLines = ['pass']
208 rw.write()
209 self.assertEqual(
210 ['foo.basic'],
211 self._run_recipes('test', 'list').splitlines())
212
213 def test_test(self):
214 rw = RecipeWriter(self._root_dir, 'foo')
215 rw.RunStepsLines = ['pass']
216 rw.add_expectation('basic')
217 rw.write()
218 self._run_recipes('test', 'run')
219
220 def test_test_expectation_failure_empty(self):
221 rw = RecipeWriter(self._root_dir, 'foo')
222 rw.RunStepsLines = ['pass']
223 rw.write()
224 with self.assertRaises(subprocess.CalledProcessError) as cm:
225 self._run_recipes('test', 'run')
226 self.assertNotIn('FATAL: Insufficient coverage', cm.exception.output)
iannucci 2017/03/10 01:20:09 maybe TODO: use jsonproto output of 'test' instead
Paweł Hajdan Jr. 2017/03/10 17:20:30 Yes, I plan to add JSON output support in subseque
227 self.assertNotIn('CHECK(FAIL)', cm.exception.output)
228 self.assertIn(
229 'foo.basic failed',
230 cm.exception.output)
231
232 def test_test_expectation_failure_different(self):
233 rw = RecipeWriter(self._root_dir, 'foo')
234 rw.DEPS = ['recipe_engine/step']
235 rw.RunStepsLines = ['api.step("test", ["echo", "bar"])']
236 rw.add_expectation('basic')
237 rw.write()
238 with self.assertRaises(subprocess.CalledProcessError) as cm:
239 self._run_recipes('test', 'run')
240 self.assertNotIn('FATAL: Insufficient coverage', cm.exception.output)
241 self.assertNotIn('CHECK(FAIL)', cm.exception.output)
242 self.assertIn(
243 'foo.basic failed',
244 cm.exception.output)
245 self.assertIn(
246 '+[{\'cmd\': [\'echo\', \'bar\'], \'name\': \'test\'},\n',
247 cm.exception.output)
248
249 def test_test_expectation_pass(self):
250 rw = RecipeWriter(self._root_dir, 'foo')
251 rw.DEPS = ['recipe_engine/step']
252 rw.RunStepsLines = ['api.step("test", ["echo", "bar"])']
253 rw.add_expectation('basic', [{'cmd': ['echo', 'bar'], 'name': 'test'}])
254 rw.write()
255 self._run_recipes('test', 'run')
256
257 def test_test_recipe_not_covered(self):
258 rw = RecipeWriter(self._root_dir, 'foo')
259 rw.RunStepsLines = ['if False:', ' pass']
260 rw.add_expectation('basic')
261 rw.write()
262 with self.assertRaises(subprocess.CalledProcessError) as cm:
263 self._run_recipes('test', 'run')
264 self.assertIn('FATAL: Insufficient coverage', cm.exception.output)
265 self.assertNotIn('CHECK(FAIL)', cm.exception.output)
266 self.assertNotIn('foo.basic failed', cm.exception.output)
267
268 def test_test_check_failure(self):
269 rw = RecipeWriter(self._root_dir, 'foo')
270 rw.RunStepsLines = ['pass']
271 rw.GenTestsLines = [
272 'yield api.test("basic") + \\',
273 ' api.post_process(post_process.MustRun, "bar")'
274 ]
275 rw.add_expectation('basic')
276 rw.write()
277 with self.assertRaises(subprocess.CalledProcessError) as cm:
278 self._run_recipes('test', 'run')
279 self.assertNotIn('FATAL: Insufficient coverage', cm.exception.output)
280 self.assertIn('CHECK(FAIL)', cm.exception.output)
281 self.assertIn('foo.basic failed', cm.exception.output)
282
283 def test_test_check_success(self):
284 rw = RecipeWriter(self._root_dir, 'foo')
285 rw.RunStepsLines = ['pass']
286 rw.GenTestsLines = [
287 'yield api.test("basic") + \\',
288 ' api.post_process(post_process.DoesNotRun, "bar")'
289 ]
290 rw.add_expectation('basic')
291 rw.write()
292 self._run_recipes('test', 'run')
293
294 def test_test_recipe_syntax_error(self):
295 rw = RecipeWriter(self._root_dir, 'foo')
296 rw.RunStepsLines = ['baz']
297 rw.add_expectation('basic')
298 rw.write()
299 with self.assertRaises(subprocess.CalledProcessError) as cm:
300 self._run_recipes('test', 'run')
301 self.assertIn('NameError: global name \'baz\' is not defined',
302 cm.exception.output)
303
304 def test_test_recipe_module_uncovered(self):
305 mw = RecipeModuleWriter(self._root_dir, 'foo')
306 mw.write()
307 with self.assertRaises(subprocess.CalledProcessError) as cm:
308 self._run_recipes('test', 'run')
309 self.assertIn('The following modules lack test coverage: foo',
310 cm.exception.output)
311
312 def test_test_recipe_module_syntax_error(self):
313 mw = RecipeModuleWriter(self._root_dir, 'foo_module')
314 mw.methods['foo'] = ['baz']
315 mw.example.enabled = True
316 mw.example.DEPS = ['foo_module']
317 mw.example.RunStepsLines = ['api.foo_module.foo()']
318 mw.write()
319 with self.assertRaises(subprocess.CalledProcessError) as cm:
320 self._run_recipes('test', 'run')
321 self.assertIn('NameError: global name \'baz\' is not defined',
322 cm.exception.output)
323 self.assertIn('FATAL: Insufficient coverage', cm.exception.output)
324
325 def test_test_recipe_module_syntax_error_in_example(self):
326 mw = RecipeModuleWriter(self._root_dir, 'foo_module')
327 mw.methods['foo'] = ['pass']
328 mw.example.enabled = True
329 mw.example.DEPS = ['foo_module']
330 mw.example.RunStepsLines = ['baz']
331 mw.write()
332 with self.assertRaises(subprocess.CalledProcessError) as cm:
333 self._run_recipes('test', 'run')
334 self.assertIn('NameError: global name \'baz\' is not defined',
335 cm.exception.output)
336 self.assertIn('FATAL: Insufficient coverage', cm.exception.output)
337
338 def test_test_recipe_module_example_not_covered(self):
339 mw = RecipeModuleWriter(self._root_dir, 'foo_module')
340 mw.methods['foo'] = ['pass']
341 mw.example.enabled = True
342 mw.example.DEPS = ['foo_module']
343 mw.example.RunStepsLines = ['if False:', ' pass']
344 mw.write()
345 with self.assertRaises(subprocess.CalledProcessError) as cm:
346 self._run_recipes('test', 'run')
347 self.assertIn('FATAL: Insufficient coverage', cm.exception.output)
348
349 def test_test_recipe_module_uncovered_not_strict(self):
350 mw = RecipeModuleWriter(self._root_dir, 'foo')
351 mw.disable_strict_coverage = True
352 mw.write()
353 self._run_recipes('test', 'run')
354
355 def test_test_recipe_module_covered_by_recipe_not_strict(self):
356 mw = RecipeModuleWriter(self._root_dir, 'foo_module')
357 mw.methods['bar'] = ['pass']
358 mw.disable_strict_coverage = True
359 mw.write()
360 rw = RecipeWriter(self._root_dir, 'foo_recipe')
361 rw.DEPS = ['foo_module']
362 rw.RunStepsLines = ['api.foo_module.bar()']
363 rw.add_expectation('basic')
364 rw.write()
365 self._run_recipes('test', 'run')
366
367 def test_test_recipe_module_covered_by_recipe(self):
368 mw = RecipeModuleWriter(self._root_dir, 'foo_module')
369 mw.methods['bar'] = ['pass']
370 mw.write()
371 rw = RecipeWriter(self._root_dir, 'foo_recipe')
372 rw.DEPS = ['foo_module']
373 rw.RunStepsLines = ['api.foo_module.bar()']
374 rw.add_expectation('basic')
375 rw.write()
376 with self.assertRaises(subprocess.CalledProcessError) as cm:
377 self._run_recipes('test', 'run')
378 self.assertIn('The following modules lack test coverage: foo_module',
379 cm.exception.output)
380
381 def test_test_recipe_module_partially_covered_by_recipe_not_strict(self):
382 mw = RecipeModuleWriter(self._root_dir, 'foo_module')
383 mw.methods['bar'] = ['pass']
384 mw.methods['baz'] = ['pass']
385 mw.disable_strict_coverage = True
386 mw.example.enabled = True
387 mw.example.DEPS = ['foo_module']
388 mw.example.RunStepsLines = ['api.foo_module.baz()']
389 mw.add_expectation('basic')
390 mw.write()
391 rw = RecipeWriter(self._root_dir, 'foo_recipe')
392 rw.DEPS = ['foo_module']
393 rw.RunStepsLines = ['api.foo_module.bar()']
394 rw.add_expectation('basic')
395 rw.write()
396 self._run_recipes('test', 'run')
397
398 def test_test_recipe_module_partially_covered_by_recipe(self):
399 mw = RecipeModuleWriter(self._root_dir, 'foo_module')
400 mw.methods['bar'] = ['pass']
401 mw.methods['baz'] = ['pass']
402 mw.example.enabled = True
403 mw.example.DEPS = ['foo_module']
404 mw.example.RunStepsLines = ['api.foo_module.baz()']
405 mw.add_expectation('basic')
406 mw.write()
407 rw = RecipeWriter(self._root_dir, 'foo_recipe')
408 rw.DEPS = ['foo_module']
409 rw.RunStepsLines = ['api.foo_module.bar()']
410 rw.add_expectation('basic')
411 rw.write()
412 with self.assertRaises(subprocess.CalledProcessError) as cm:
413 self._run_recipes('test', 'run')
414 self.assertIn('FATAL: Insufficient coverage', cm.exception.output)
iannucci 2017/03/10 01:20:09 I don't see any tests for the post_process hooks?
Paweł Hajdan Jr. 2017/03/10 17:20:30 Did you see test_test_check_failure and test_test_
iannucci 2017/03/10 18:14:33 Yes, but they don't cover the case of a user-provi
415
416
417 if __name__ == '__main__':
418 sys.exit(unittest.main())
OLDNEW
« recipes.py ('K') | « unittests/stdlib_test.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698