comparison tests/conftest.py @ 673:d6403c21bdea

tests: Improve failure reporting.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 06 Mar 2016 23:41:35 -0800
parents 81d9c3a3a0b5
children f987b29d6fab
comparison
equal deleted inserted replaced
672:783ff8bc4e03 673:d6403c21bdea
20 def pytest_addoption(parser): 20 def pytest_addoption(parser):
21 parser.addoption( 21 parser.addoption(
22 '--log-debug', 22 '--log-debug',
23 action='store_true', 23 action='store_true',
24 help="Sets the PieCrust logger to output debug info to stdout.") 24 help="Sets the PieCrust logger to output debug info to stdout.")
25 parser.addoption(
26 '--mock-debug',
27 action='store_true',
28 help="Prints contents of the mock file-system.")
25 29
26 30
27 def pytest_configure(config): 31 def pytest_configure(config):
28 if config.getoption('--log-debug'): 32 if config.getoption('--log-debug'):
29 hdl = logging.StreamHandler(stream=sys.stdout) 33 hdl = logging.StreamHandler(stream=sys.stdout)
42 return ChefTestFile(path, parent) 46 return ChefTestFile(path, parent)
43 elif category == 'servings': 47 elif category == 'servings':
44 return ServeTestFile(path, parent) 48 return ServeTestFile(path, parent)
45 49
46 50
51 def repr_nested_failure(excinfo):
52 # PyTest sadly doesn't show nested exceptions so we have to do it
53 # ourselves... it's not pretty, but at least it's more useful.
54 if excinfo.value.__cause__:
55 import traceback
56 ex = excinfo.value
57 return '\n'.join(
58 traceback.format_exception(
59 type(ex), ex, ex.__traceback__))
60 return ''
61
62
47 class YamlTestFileBase(pytest.File): 63 class YamlTestFileBase(pytest.File):
48 def collect(self): 64 def collect(self):
49 spec = yaml.load_all(self.fspath.open(encoding='utf8')) 65 spec = yaml.load_all(self.fspath.open(encoding='utf8'))
50 for i, item in enumerate(spec): 66 for i, item in enumerate(spec):
51 name = '%s_%d' % (self.fspath.basename, i) 67 name = '%s_%d' % (self.fspath.basename, i)
56 72
57 class YamlTestItemBase(pytest.Item): 73 class YamlTestItemBase(pytest.Item):
58 def __init__(self, name, parent, spec): 74 def __init__(self, name, parent, spec):
59 super(YamlTestItemBase, self).__init__(name, parent) 75 super(YamlTestItemBase, self).__init__(name, parent)
60 self.spec = spec 76 self.spec = spec
77
78 @property
79 def mock_debug(self):
80 return bool(self.config.getoption('--mock-debug'))
61 81
62 def _prepareMockFs(self): 82 def _prepareMockFs(self):
63 fs = mock_fs() 83 fs = mock_fs()
64 84
65 # Website config. 85 # Website config.
77 # Input file-system. 97 # Input file-system.
78 input_files = self.spec.get('in') 98 input_files = self.spec.get('in')
79 if input_files is not None: 99 if input_files is not None:
80 _add_mock_files(fs, '/kitchen', input_files) 100 _add_mock_files(fs, '/kitchen', input_files)
81 101
102 if self.mock_debug:
103 res = '\nMock File-System:\n'
104 res += 'At: %s\n' % fs.path('')
105 res += '\n'.join(print_fs_tree(fs.path('')))
106 res += '\n'
107 print(res)
108
82 return fs 109 return fs
110
111 def repr_failure(self, excinfo):
112 res = super(YamlTestItemBase, self).repr_failure(excinfo)
113 nested_res = repr_nested_failure(excinfo)
114 if nested_res:
115 res = str(res) + '\n' + nested_res
116 return res
83 117
84 118
85 def check_expected_outputs(spec, fs, error_type): 119 def check_expected_outputs(spec, fs, error_type):
86 cctx = CompareContext() 120 cctx = CompareContext()
87 expected_output_files = spec.get('out') 121 expected_output_files = spec.get('out')
149 argv = argv.split(' ') 183 argv = argv.split(' ')
150 184
151 expected_code = self.spec.get('code', 0) 185 expected_code = self.spec.get('code', 0)
152 expected_out = self.spec.get('out', None) 186 expected_out = self.spec.get('out', None)
153 187
154 with mock_fs_scope(fs): 188 with mock_fs_scope(fs, keep=self.mock_debug):
155 memstream = io.StringIO() 189 memstream = io.StringIO()
156 hdl = logging.StreamHandler(stream=memstream) 190 hdl = logging.StreamHandler(stream=memstream)
157 logging.getLogger().addHandler(hdl) 191 logging.getLogger().addHandler(hdl)
158 try: 192 try:
159 from piecrust.main import _pre_parse_chef_args, _run_chef 193 from piecrust.main import _pre_parse_chef_args, _run_chef
194 class BakeTestItem(YamlTestItemBase): 228 class BakeTestItem(YamlTestItemBase):
195 def runtest(self): 229 def runtest(self):
196 fs = self._prepareMockFs() 230 fs = self._prepareMockFs()
197 231
198 from piecrust.baking.baker import Baker 232 from piecrust.baking.baker import Baker
199 with mock_fs_scope(fs): 233 with mock_fs_scope(fs, keep=self.mock_debug):
200 out_dir = fs.path('kitchen/_counter') 234 out_dir = fs.path('kitchen/_counter')
201 app = fs.getApp() 235 app = fs.getApp()
202 236
203 variant = self.spec.get('config_variant') 237 variant = self.spec.get('config_variant')
204 values = self.spec.get('config_values') 238 values = self.spec.get('config_values')
227 return ('\n'.join( 261 return ('\n'.join(
228 ['Unexpected bake output. Left is expected output, ' 262 ['Unexpected bake output. Left is expected output, '
229 'right is actual output'] + 263 'right is actual output'] +
230 excinfo.value.args[0])) 264 excinfo.value.args[0]))
231 elif isinstance(excinfo.value, BakeError): 265 elif isinstance(excinfo.value, BakeError):
232 return ('\n'.join( 266 res = ('\n'.join(
233 ['Errors occured during bake:'] + 267 ['Errors occured during bake:'] +
234 excinfo.value.args[0])) 268 excinfo.value.args[0]))
269 res += repr_nested_failure(excinfo)
270 return res
235 return super(BakeTestItem, self).repr_failure(excinfo) 271 return super(BakeTestItem, self).repr_failure(excinfo)
236 272
237 273
238 class BakeError(Exception): 274 class BakeError(Exception):
239 pass 275 pass
250 class PipelineTestItem(YamlTestItemBase): 286 class PipelineTestItem(YamlTestItemBase):
251 def runtest(self): 287 def runtest(self):
252 fs = self._prepareMockFs() 288 fs = self._prepareMockFs()
253 289
254 from piecrust.processing.pipeline import ProcessorPipeline 290 from piecrust.processing.pipeline import ProcessorPipeline
255 with mock_fs_scope(fs): 291 with mock_fs_scope(fs, keep=self.mock_debug):
256 out_dir = fs.path('kitchen/_counter') 292 out_dir = fs.path('kitchen/_counter')
257 app = fs.getApp() 293 app = fs.getApp()
258 pipeline = ProcessorPipeline(app, out_dir) 294 pipeline = ProcessorPipeline(app, out_dir)
259 295
260 proc_names = self.spec.get('processors') 296 proc_names = self.spec.get('processors')
279 return ('\n'.join( 315 return ('\n'.join(
280 ['Unexpected pipeline output. Left is expected output, ' 316 ['Unexpected pipeline output. Left is expected output, '
281 'right is actual output'] + 317 'right is actual output'] +
282 excinfo.value.args[0])) 318 excinfo.value.args[0]))
283 elif isinstance(excinfo.value, PipelineError): 319 elif isinstance(excinfo.value, PipelineError):
284 return ('\n'.join( 320 res = ('\n'.join(
285 ['Errors occured during processing:'] + 321 ['Errors occured during processing:'] +
286 excinfo.value.args[0])) 322 excinfo.value.args[0]))
323 res += repr_nested_failure(excinfo)
324 return res
287 return super(PipelineTestItem, self).repr_failure(excinfo) 325 return super(PipelineTestItem, self).repr_failure(excinfo)
288 326
289 327
290 class PipelineError(Exception): 328 class PipelineError(Exception):
291 pass 329 pass
322 360
323 from werkzeug.test import Client 361 from werkzeug.test import Client
324 from werkzeug.wrappers import BaseResponse 362 from werkzeug.wrappers import BaseResponse
325 from piecrust.app import PieCrustFactory 363 from piecrust.app import PieCrustFactory
326 from piecrust.serving.server import Server 364 from piecrust.serving.server import Server
327 with mock_fs_scope(fs): 365 with mock_fs_scope(fs, keep=self.mock_debug):
328 appfactory = PieCrustFactory(fs.path('/kitchen')) 366 appfactory = PieCrustFactory(fs.path('/kitchen'))
329 server = Server(appfactory) 367 server = Server(appfactory)
330 test_app = self._TestApp(server) 368 test_app = self._TestApp(server)
331 client = Client(test_app, BaseResponse) 369 client = Client(test_app, BaseResponse)
332 resp = client.get(url) 370 resp = client.get(url)
347 return self.fspath, 0, "serve: %s" % self.name 385 return self.fspath, 0, "serve: %s" % self.name
348 386
349 def repr_failure(self, excinfo): 387 def repr_failure(self, excinfo):
350 from piecrust.serving.server import MultipleNotFound 388 from piecrust.serving.server import MultipleNotFound
351 if isinstance(excinfo.value, MultipleNotFound): 389 if isinstance(excinfo.value, MultipleNotFound):
352 return '\n'.join( 390 res = '\n'.join(
353 ["HTTP error 404 returned:", 391 ["HTTP error 404 returned:",
354 excinfo.value.description] + 392 excinfo.value.description] +
355 [e.description for e in excinfo.value._nfes]) 393 [e.description for e in excinfo.value._nfes])
394 res += repr_nested_failure(excinfo)
395 return res
356 elif isinstance(excinfo.value, HTTPException): 396 elif isinstance(excinfo.value, HTTPException):
357 return '\n'.join( 397 res = '\n'.join(
358 ["HTTP error %s returned:" % excinfo.value.code, 398 ["HTTP error %s returned:" % excinfo.value.code,
359 excinfo.value.description]) 399 excinfo.value.description])
400 res += repr_nested_failure(excinfo)
401 return res
360 return super(ServeTestItem, self).repr_failure(excinfo) 402 return super(ServeTestItem, self).repr_failure(excinfo)
361 403
362 404
363 class ServeTestFile(YamlTestFileBase): 405 class ServeTestFile(YamlTestFileBase):
364 __item_class__ = ServeTestItem 406 __item_class__ = ServeTestItem