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