Mercurial > piecrust2
comparison tests/conftest.py @ 436:2aa879d63133
tests: Add pipeline processing tests.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Sat, 27 Jun 2015 21:48:26 -0700 |
parents | e7b865f8f335 |
children | c0700c6d9545 |
comparison
equal
deleted
inserted
replaced
435:5ceb86818dc5 | 436:2aa879d63133 |
---|---|
1 import io | 1 import io |
2 import sys | 2 import sys |
3 import time | |
3 import pprint | 4 import pprint |
4 import os.path | 5 import os.path |
5 import logging | 6 import logging |
6 import pytest | 7 import pytest |
7 import yaml | 8 import yaml |
31 def pytest_collect_file(parent, path): | 32 def pytest_collect_file(parent, path): |
32 if path.ext == '.yaml' and path.basename.startswith("test"): | 33 if path.ext == '.yaml' and path.basename.startswith("test"): |
33 category = os.path.basename(path.dirname) | 34 category = os.path.basename(path.dirname) |
34 if category == 'bakes': | 35 if category == 'bakes': |
35 return BakeTestFile(path, parent) | 36 return BakeTestFile(path, parent) |
37 elif category == 'procs': | |
38 return PipelineTestFile(path, parent) | |
36 elif category == 'cli': | 39 elif category == 'cli': |
37 return ChefTestFile(path, parent) | 40 return ChefTestFile(path, parent) |
38 elif category == 'servings': | 41 elif category == 'servings': |
39 return ServeTestFile(path, parent) | 42 return ServeTestFile(path, parent) |
40 | 43 |
75 _add_mock_files(fs, '/kitchen', input_files) | 78 _add_mock_files(fs, '/kitchen', input_files) |
76 | 79 |
77 return fs | 80 return fs |
78 | 81 |
79 | 82 |
83 def check_expected_outputs(spec, fs, error_type): | |
84 cctx = CompareContext() | |
85 expected_output_files = spec.get('out') | |
86 if expected_output_files: | |
87 actual = fs.getStructure('kitchen/_counter') | |
88 error = _compare_dicts(expected_output_files, actual, cctx) | |
89 if error: | |
90 raise error_type(error) | |
91 | |
92 expected_partial_files = spec.get('outfiles') | |
93 if expected_partial_files: | |
94 keys = list(sorted(expected_partial_files.keys())) | |
95 for key in keys: | |
96 try: | |
97 actual = fs.getFileEntry('kitchen/_counter/' + | |
98 key.lstrip('/')) | |
99 except Exception as e: | |
100 raise error_type([ | |
101 "Can't access output file %s: %s" % (key, e)]) | |
102 | |
103 expected = expected_partial_files[key] | |
104 # HACK because for some reason PyYAML adds a new line for | |
105 # those and I have no idea why. | |
106 actual = actual.rstrip('\n') | |
107 expected = expected.rstrip('\n') | |
108 cctx.path = key | |
109 cmpres = _compare_str(expected, actual, cctx) | |
110 if cmpres: | |
111 raise error_type(cmpres) | |
112 | |
113 | |
80 class ChefTestItem(YamlTestItemBase): | 114 class ChefTestItem(YamlTestItemBase): |
81 __initialized_logging__ = False | 115 __initialized_logging__ = False |
82 | 116 |
83 def runtest(self): | 117 def runtest(self): |
84 if not ChefTestItem.__initialized_logging__: | 118 if not ChefTestItem.__initialized_logging__: |
134 | 168 |
135 class BakeTestItem(YamlTestItemBase): | 169 class BakeTestItem(YamlTestItemBase): |
136 def runtest(self): | 170 def runtest(self): |
137 fs = self._prepareMockFs() | 171 fs = self._prepareMockFs() |
138 | 172 |
139 # Output file-system. | |
140 expected_output_files = self.spec.get('out') | |
141 expected_partial_files = self.spec.get('outfiles') | |
142 | |
143 # Bake! | |
144 from piecrust.baking.baker import Baker | 173 from piecrust.baking.baker import Baker |
145 with mock_fs_scope(fs): | 174 with mock_fs_scope(fs): |
146 out_dir = fs.path('kitchen/_counter') | 175 out_dir = fs.path('kitchen/_counter') |
147 app = fs.getApp() | 176 app = fs.getApp() |
148 baker = Baker(app, out_dir) | 177 baker = Baker(app, out_dir) |
152 errors = [] | 181 errors = [] |
153 for e in record.entries: | 182 for e in record.entries: |
154 errors += e.getAllErrors() | 183 errors += e.getAllErrors() |
155 raise BakeError(errors) | 184 raise BakeError(errors) |
156 | 185 |
157 if expected_output_files: | 186 check_expected_outputs(self.spec, fs, ExpectedBakeOutputError) |
158 actual = fs.getStructure('kitchen/_counter') | |
159 error = _compare_dicts(expected_output_files, actual) | |
160 if error: | |
161 raise ExpectedBakeOutputError(error) | |
162 | |
163 if expected_partial_files: | |
164 keys = list(sorted(expected_partial_files.keys())) | |
165 for key in keys: | |
166 try: | |
167 actual = fs.getFileEntry('kitchen/_counter/' + | |
168 key.lstrip('/')) | |
169 except Exception as e: | |
170 raise ExpectedBakeOutputError([ | |
171 "Can't access output file %s: %s" % (key, e)]) | |
172 | |
173 expected = expected_partial_files[key] | |
174 # HACK because for some reason PyYAML adds a new line for | |
175 # those and I have no idea why. | |
176 actual = actual.rstrip('\n') | |
177 expected = expected.rstrip('\n') | |
178 cmpres = _compare_str(expected, actual, key) | |
179 if cmpres: | |
180 raise ExpectedBakeOutputError(cmpres) | |
181 | 187 |
182 def reportinfo(self): | 188 def reportinfo(self): |
183 return self.fspath, 0, "bake: %s" % self.name | 189 return self.fspath, 0, "bake: %s" % self.name |
184 | 190 |
185 def repr_failure(self, excinfo): | 191 def repr_failure(self, excinfo): |
205 | 211 |
206 class BakeTestFile(YamlTestFileBase): | 212 class BakeTestFile(YamlTestFileBase): |
207 __item_class__ = BakeTestItem | 213 __item_class__ = BakeTestItem |
208 | 214 |
209 | 215 |
216 class PipelineTestItem(YamlTestItemBase): | |
217 def runtest(self): | |
218 fs = self._prepareMockFs() | |
219 | |
220 from piecrust.processing.pipeline import ProcessorPipeline | |
221 with mock_fs_scope(fs): | |
222 out_dir = fs.path('kitchen/_counter') | |
223 app = fs.getApp() | |
224 pipeline = ProcessorPipeline(app, out_dir) | |
225 | |
226 proc_names = self.spec.get('processors') | |
227 if proc_names: | |
228 pipeline.enabled_processors = proc_names | |
229 | |
230 record = pipeline.run() | |
231 | |
232 if not record.success: | |
233 errors = [] | |
234 for e in record.entries: | |
235 errors += e.errors | |
236 raise PipelineError(errors) | |
237 | |
238 check_expected_outputs(self.spec, fs, ExpectedPipelineOutputError) | |
239 | |
240 def reportinfo(self): | |
241 return self.fspath, 0, "pipeline: %s" % self.name | |
242 | |
243 def repr_failure(self, excinfo): | |
244 if isinstance(excinfo.value, ExpectedPipelineOutputError): | |
245 return ('\n'.join( | |
246 ['Unexpected pipeline output. Left is expected output, ' | |
247 'right is actual output'] + | |
248 excinfo.value.args[0])) | |
249 elif isinstance(excinfo.value, PipelineError): | |
250 return ('\n'.join( | |
251 ['Errors occured during processing:'] + | |
252 excinfo.value.args[0])) | |
253 return super(PipelineTestItem, self).repr_failure(excinfo) | |
254 | |
255 | |
256 class PipelineError(Exception): | |
257 pass | |
258 | |
259 | |
260 class ExpectedPipelineOutputError(Exception): | |
261 pass | |
262 | |
263 | |
264 class PipelineTestFile(YamlTestFileBase): | |
265 __item_class__ = PipelineTestItem | |
266 | |
267 | |
210 class ServeTestItem(YamlTestItemBase): | 268 class ServeTestItem(YamlTestItemBase): |
211 class _TestApp(object): | 269 class _TestApp(object): |
212 def __init__(self, server): | 270 def __init__(self, server): |
213 self.server = server | 271 self.server = server |
214 | 272 |
263 fs.withFile(path, subspec) | 321 fs.withFile(path, subspec) |
264 elif isinstance(subspec, dict): | 322 elif isinstance(subspec, dict): |
265 _add_mock_files(fs, path, subspec) | 323 _add_mock_files(fs, path, subspec) |
266 | 324 |
267 | 325 |
268 def _compare(left, right, path): | 326 class CompareContext(object): |
327 def __init__(self, path=None): | |
328 self.path = path or '' | |
329 self.time = time.time() | |
330 | |
331 def createChildContext(self, name): | |
332 ctx = CompareContext( | |
333 path='%s/%s' % (self.path, name), | |
334 time=self.time) | |
335 return ctx | |
336 | |
337 | |
338 def _compare(left, right, ctx): | |
269 if type(left) != type(right): | 339 if type(left) != type(right): |
270 return (["Different items: ", | 340 return (["Different items: ", |
271 "%s: %s" % (path, pprint.pformat(left)), | 341 "%s: %s" % (ctx.path, pprint.pformat(left)), |
272 "%s: %s" % (path, pprint.pformat(right))]) | 342 "%s: %s" % (ctx.path, pprint.pformat(right))]) |
273 if isinstance(left, str): | 343 if isinstance(left, str): |
274 return _compare_str(left, right, path) | 344 return _compare_str(left, right, ctx) |
275 elif isinstance(left, dict): | 345 elif isinstance(left, dict): |
276 return _compare_dicts(left, right, path) | 346 return _compare_dicts(left, right, ctx) |
277 elif isinstance(left, list): | 347 elif isinstance(left, list): |
278 return _compare_lists(left, right, path) | 348 return _compare_lists(left, right, ctx) |
279 elif left != right: | 349 elif left != right: |
280 return (["Different items: ", | 350 return (["Different items: ", |
281 "%s: %s" % (path, pprint.pformat(left)), | 351 "%s: %s" % (ctx.path, pprint.pformat(left)), |
282 "%s: %s" % (path, pprint.pformat(right))]) | 352 "%s: %s" % (ctx.path, pprint.pformat(right))]) |
283 | 353 |
284 | 354 |
285 def _compare_dicts(left, right, basepath=''): | 355 def _compare_dicts(left, right, ctx): |
286 key_diff = set(left.keys()) ^ set(right.keys()) | 356 key_diff = set(left.keys()) ^ set(right.keys()) |
287 if key_diff: | 357 if key_diff: |
288 extra_left = set(left.keys()) - set(right.keys()) | 358 extra_left = set(left.keys()) - set(right.keys()) |
289 if extra_left: | 359 if extra_left: |
290 return (["Left contains more items: "] + | 360 return (["Left contains more items: "] + |
291 ['- %s/%s' % (basepath, k) for k in extra_left]) | 361 ['- %s/%s' % (ctx.path, k) for k in extra_left]) |
292 extra_right = set(right.keys()) - set(left.keys()) | 362 extra_right = set(right.keys()) - set(left.keys()) |
293 if extra_right: | 363 if extra_right: |
294 return (["Right contains more items: "] + | 364 return (["Right contains more items: "] + |
295 ['- %s/%s' % (basepath, k) for k in extra_right]) | 365 ['- %s/%s' % (ctx.path, k) for k in extra_right]) |
296 return ["Unknown difference"] | 366 return ["Unknown difference"] |
297 | 367 |
298 for key in left.keys(): | 368 for key in left.keys(): |
299 lv = left[key] | 369 lv = left[key] |
300 rv = right[key] | 370 rv = right[key] |
301 childpath = basepath + '/' + key | 371 child_ctx = ctx.createChildContext(key) |
302 cmpres = _compare(lv, rv, childpath) | 372 cmpres = _compare(lv, rv, child_ctx) |
303 if cmpres: | 373 if cmpres: |
304 return cmpres | 374 return cmpres |
305 return None | 375 return None |
306 | 376 |
307 | 377 |
308 def _compare_lists(left, right, path): | 378 def _compare_lists(left, right, ctx): |
309 for i in range(min(len(left), len(right))): | 379 for i in range(min(len(left), len(right))): |
310 l = left[i] | 380 l = left[i] |
311 r = right[i] | 381 r = right[i] |
312 cmpres = _compare(l, r, path) | 382 cmpres = _compare(l, r, ctx) |
313 if cmpres: | 383 if cmpres: |
314 return cmpres | 384 return cmpres |
315 if len(left) > len(right): | 385 if len(left) > len(right): |
316 return (["Left '%s' contains more items. First extra item: " % path, | 386 return (["Left '%s' contains more items. First extra item: " % |
317 left[len(right)]]) | 387 ctx.path, left[len(right)]]) |
318 if len(right) > len(left): | 388 if len(right) > len(left): |
319 return (["Right '%s' contains more items. First extra item: " % path, | 389 return (["Right '%s' contains more items. First extra item: " % |
320 right[len(left)]]) | 390 ctx.path, right[len(left)]]) |
321 return None | 391 return None |
322 | 392 |
323 | 393 |
324 def _compare_str(left, right, path): | 394 def _compare_str(left, right, ctx): |
325 if left == right: | 395 if left == right: |
326 return None | 396 return None |
397 | |
398 test_time_iso8601 = time.strftime('%Y-%m-%dT%H:%M:%SZ', | |
399 time.gmtime(ctx.time)) | |
400 replacements = { | |
401 '%test_time_iso8601%': test_time_iso8601} | |
402 for token, repl in replacements.items(): | |
403 left = left.replace(token, repl) | |
404 right = right.replace(token, repl) | |
405 | |
327 for i in range(min(len(left), len(right))): | 406 for i in range(min(len(left), len(right))): |
328 if left[i] != right[i]: | 407 if left[i] != right[i]: |
329 start = max(0, i - 15) | 408 start = max(0, i - 15) |
330 l_end = min(len(left), i + 15) | 409 l_end = min(len(left), i + 15) |
331 r_end = min(len(right), i + 15) | 410 r_end = min(len(right), i + 15) |
344 c = repr(right[j]).strip("'") | 423 c = repr(right[j]).strip("'") |
345 r_str += c | 424 r_str += c |
346 if j < i: | 425 if j < i: |
347 r_offset += len(c) | 426 r_offset += len(c) |
348 | 427 |
349 return ["Items '%s' differ at index %d:" % (path, i), '', | 428 return ["Items '%s' differ at index %d:" % (ctx.path, i), '', |
350 "Left:", left, '', | 429 "Left:", left, '', |
351 "Right:", right, '', | 430 "Right:", right, '', |
352 "Difference:", | 431 "Difference:", |
353 l_str, (' ' * l_offset + '^'), | 432 l_str, (' ' * l_offset + '^'), |
354 r_str, (' ' * r_offset + '^')] | 433 r_str, (' ' * r_offset + '^')] |
355 if len(left) > len(right): | 434 if len(left) > len(right): |
356 return ["Left is longer.", | 435 return ["Left is longer.", |
357 "Left '%s': " % path, left, | 436 "Left '%s': " % ctx.path, left, |
358 "Right '%s': " % path, right, | 437 "Right '%s': " % ctx.path, right, |
359 "Extra items: %r" % left[len(right):]] | 438 "Extra items: %r" % left[len(right):]] |
360 if len(right) > len(left): | 439 if len(right) > len(left): |
361 return ["Right is longer.", | 440 return ["Right is longer.", |
362 "Left '%s': " % path, left, | 441 "Left '%s': " % ctx.path, left, |
363 "Right '%s': " % path, right, | 442 "Right '%s': " % ctx.path, right, |
364 "Extra items: %r" % right[len(left):]] | 443 "Extra items: %r" % right[len(left):]] |
365 | 444 |