source: grass/trunk/lib/python/gunittest/case.py@ 61225

Last change on this file since 61225 was 61224, checked in by wenzeslaus, 10 years ago

gunittest: fix spelling

  • Property svn:eol-style set to native
  • Property svn:mime-type set to text/x-python
File size: 26.1 KB
Line 
1# -*- coding: utf-8 -*-
2
3"""!@package grass.gunittest.case
4
5@brief GRASS Python testing framework test case
6
7(C) 2014 by the GRASS Development Team
8This program is free software under the GNU General Public
9License (>=v2). Read the file COPYING that comes with GRASS
10for details.
11
12@author Vaclav Petras
13"""
14
15import os
16import subprocess
17import unittest
18from unittest.util import safe_repr
19
20from grass.pygrass.modules import Module
21from grass.exceptions import CalledModuleError
22
23from .gmodules import call_module
24from .checkers import (check_text_ellipsis,
25 text_to_keyvalue, keyvalue_equals, diff_keyvalue,
26 file_md5, files_equal_md5)
27
28
29class TestCase(unittest.TestCase):
30 # we dissable R0904 for all TestCase classes because their purpose is to
31 # provide a lot of assert methods
32 # pylint: disable=R0904
33 """
34
35 Always use keyword arguments for all parameters other than first two. For
36 the first two, it is recommended to use keyword arguments but not required.
37 """
38 longMessage = True # to get both standard and custom message
39 maxDiff = None # we can afford long diffs
40 _temp_region = None # to control the temporary region
41
42 def __init__(self, methodName):
43 super(TestCase, self).__init__(methodName)
44
45 def _formatMessage(self, msg, standardMsg):
46 """Honor the longMessage attribute when generating failure messages.
47
48 If longMessage is False this means:
49
50 * Use only an explicit message if it is provided
51 * Otherwise use the standard message for the assert
52
53 If longMessage is True:
54
55 * Use the standard message
56 * If an explicit message is provided, return string with both messages
57
58 Based on Python unittest _formatMessage, formatting changed.
59 """
60 if not self.longMessage:
61 return msg or standardMsg
62 if msg is None:
63 return standardMsg
64 try:
65 # don't switch to '{}' formatting in Python 2.X
66 # it changes the way unicode input is handled
67 return '%s \n%s' % (msg, standardMsg)
68 except UnicodeDecodeError:
69 return '%s \n%s' % (safe_repr(msg), safe_repr(standardMsg))
70
71 @classmethod
72 def use_temp_region(cls):
73 """Use temporary region instead of the standard one for this process.
74
75 If you use this method, you have to call it in `setUpClass()`
76 and call `del_temp_region()` in `tearDownClass()`. By this you
77 ensure that each test method will have its own region and will
78 not influence other classes.
79
80 ::
81
82 @classmethod
83 def setUpClass(self):
84 self.use_temp_region()
85
86 @classmethod
87 def tearDownClass(self):
88 self.del_temp_region()
89
90 You can also call the methods in `setUp()` and `tearDown()` if
91 you are using them.
92
93 Copies the current region to a temporary region with
94 ``g.region save=``, then sets ``WIND_OVERRIDE`` to refer
95 to that region.
96 """
97 # we use just the class name since we rely on the invokation system
98 # where each test file is separate process and nothing runs
99 # in parallel inside
100 name = "tmp.%s" % (cls.__name__)
101 call_module("g.region", save=name, overwrite=True)
102 os.environ['WIND_OVERRIDE'] = name
103 cls._temp_region = name
104
105 @classmethod
106 def del_temp_region(cls):
107 """Remove the temporary region.
108
109 Unsets ``WIND_OVERRIDE`` and removes any region named by it.
110 """
111 assert cls._temp_region
112 name = os.environ.pop('WIND_OVERRIDE')
113 if name != cls._temp_region:
114 # be strict about usage of region
115 raise RuntimeError("Inconsistent use of"
116 " TestCase.use_temp_region, WIND_OVERRIDE"
117 " or temporary region in general\n"
118 "Region to which should be now deleted ({n})"
119 " by TestCase class"
120 "does not corresond to currently set"
121 " WIND_OVERRIDE ({c})",
122 n=cls._temp_region, c=name)
123 call_module("g.remove", quiet=True, region=name)
124 # TODO: we don't know if user calls this
125 # so perhaps some decorator which would use with statemet
126 # but we have zero chance of infuencing another test class
127 # since we use class-specific name for temporary region
128
129 def assertLooksLike(self, actual, reference, msg=None):
130 """Test that ``actual`` text is the same as ``referece`` with ellipses.
131
132 See :func:`check_text_ellipsis` for details of behavior.
133 """
134 self.assertTrue(isinstance(actual, basestring), (
135 'actual argument is not a string'))
136 self.assertTrue(isinstance(reference, basestring), (
137 'reference argument is not a string'))
138 if not check_text_ellipsis(actual=actual, reference=reference):
139 # TODO: add support for multiline (first line general, others with details)
140 standardMsg = '"%s" does not correspond with "%s"' % (actual,
141 reference)
142 self.fail(self._formatMessage(msg, standardMsg))
143
144 # TODO: decide if precision is mandatory
145 # (note that we don't need precision for strings and usually for integers)
146 # TODO: auto-determine precision based on the map type
147 # TODO: we can have also more general function without the subset reference
148 # TODO: change name to Module
149 def assertCommandKeyValue(self, module, reference, sep,
150 precision, msg=None, **parameters):
151 """Test that output of a module is the same as provided subset.
152
153 ::
154
155 self.assertCommandKeyValue('r.info', map='elevation', flags='gr',
156 reference=dict(min=55.58, max=156.33),
157 precision=0.01, sep='=')
158
159 ::
160
161 module = SimpleModule('r.info', map='elevation', flags='gr')
162 self.assertCommandKeyValue(module,
163 reference=dict(min=55.58, max=156.33),
164 precision=0.01, sep='=')
165
166 The output of the module should be key-value pairs (shell script style)
167 which is typically obtained using ``-g`` flag.
168 """
169 if isinstance(reference, basestring):
170 reference = text_to_keyvalue(reference, sep=sep, skip_empty=True)
171 module = _module_from_parameters(module, **parameters)
172 self.runModule(module)
173 raster_univar = text_to_keyvalue(module.outputs.stdout,
174 sep=sep, skip_empty=True)
175 if not keyvalue_equals(dict_a=reference, dict_b=raster_univar,
176 a_is_subset=True, precision=precision):
177 unused, missing, mismatch = diff_keyvalue(dict_a=reference,
178 dict_b=raster_univar,
179 a_is_subset=True,
180 precision=precision)
181 if missing:
182 raise ValueError("%s output does not contain"
183 " the following keys"
184 " provided in reference"
185 ": %s\n" % (module, ", ".join(missing)))
186 if mismatch:
187 stdMsg = "%s difference:\n" % module
188 stdMsg += "mismatch values"
189 stdMsg += "(key, reference, actual): %s\n" % mismatch
190 stdMsg += 'command: %s %s' % (module, parameters)
191 else:
192 # we can probably remove this once we have more tests
193 # of keyvalue_equals and diff_keyvalue against each other
194 raise RuntimeError("keyvalue_equals() showed difference but"
195 " diff_keyvalue() did not. This can be"
196 " a bug in one of them or in the caller"
197 " (assertCommandKeyValue())")
198 self.fail(self._formatMessage(msg, stdMsg))
199
200 def assertRasterFitsUnivar(self, raster, reference,
201 precision=None, msg=None):
202 r"""Test that raster map has the values obtained by r.univar module.
203
204 The function does not require all values from r.univar.
205 Only the provided values are tested.
206 Typical example is checking minimum, maximum and number of NULL cells
207 in the map::
208
209 values = 'null_cells=0\nmin=55.5787925720215\nmax=156.329864501953'
210 self.assertRasterFitsUnivar(map='elevation', reference=values)
211
212 Use keyword arguments syntax for all function parameters.
213
214 Does not -e (extended statistics) flag, use `assertCommandKeyValue()`
215 for the full interface of arbitrary module.
216 """
217 self.assertCommandKeyValue(module='r.univar',
218 map=raster,
219 separator='=',
220 flags='g',
221 reference=reference, msg=msg, sep='=',
222 precision=precision)
223
224 def assertRasterFitsInfo(self, raster, reference,
225 precision=None, msg=None):
226 r"""Test that raster map has the values obtained by v.univar module.
227
228 The function does not require all values from v.univar.
229 Only the provided values are tested.
230 Typical example is checking minimum, maximum and type of the map::
231
232 minmax = 'min=0\nmax=1451\ndatatype=FCELL'
233 self.assertRasterFitsInfo(map='elevation', reference=values)
234
235 Use keyword arguments syntax for all function parameters.
236
237 This function supports values obtained -r (range) and
238 -e (extended metadata) flags.
239 """
240 self.assertCommandKeyValue(module='r.info',
241 map=raster, flags='gre',
242 reference=reference, msg=msg, sep='=',
243 precision=precision)
244
245 def assertVectorFitsUnivar(self, map, column, reference, msg=None,
246 layer=None, type=None, where=None,
247 precision=None):
248 r"""Test that vector map has the values obtained by v.univar module.
249
250 The function does not require all values from v.univar.
251 Only the provided values are tested.
252 Typical example is checking minimum and maximum of a column::
253
254 minmax = 'min=0\nmax=1451'
255 self.assertVectorFitsUnivar(map='bridges', column='WIDTH',
256 reference=minmax)
257
258 Use keyword arguments syntax for all function parameters.
259
260 Does not support -d (geometry distances) flag, -e (extended statistics)
261 flag and few other, use `assertCommandKeyValue` for the full interface
262 of arbitrary module.
263 """
264 parameters = dict(map=map, column=column, flags='g')
265 if layer:
266 parameters.update(layer=layer)
267 if type:
268 parameters.update(type=type)
269 if where:
270 parameters.update(where=where)
271 self.assertCommandKeyValue(module='v.univar',
272 reference=reference, msg=msg, sep='=',
273 precision=precision,
274 **parameters)
275
276 # TODO: use precision?
277 # TODO: write a test for this method with r.in.ascii
278 def assertRasterMinMax(self, map, refmin, refmax, msg=None):
279 """Test that raster map minimum and maximum are within limits.
280
281 Map minimum and maximum is tested against expression::
282
283 refmin <= actualmin and refmax >= actualmax
284
285 Use keyword arguments syntax for all function parameters.
286
287 To check that more statistics have certain values use
288 `assertRasterFitsUnivar()` or `assertRasterFitsInfo()`
289 """
290 stdout = call_module('r.info', map=map, flags='r')
291 actual = text_to_keyvalue(stdout, sep='=')
292 if refmin > actual['min']:
293 stdmsg = ('The actual minimum ({a}) is smaller than the reference'
294 ' one ({r}) for raster map {m}'
295 ' (with maximum {o})'.format(
296 a=actual['min'], r=refmin, m=map, o=actual['max']))
297 self.fail(self._formatMessage(msg, stdmsg))
298 if refmax < actual['max']:
299 stdmsg = ('The actual maximum ({a}) is greater than the reference'
300 ' one ({r}) for raster map {m}'
301 ' (with minimum {o})'.format(
302 a=actual['max'], r=refmax, m=map, o=actual['min']))
303 self.fail(self._formatMessage(msg, stdmsg))
304
305 def assertFileExists(self, filename, msg=None,
306 skip_size_check=False, skip_access_check=False):
307 """Test the existence of a file.
308
309 .. note:
310 By default this also checks if the file size is greater than 0
311 since we rarely want a file to be empty. And it also checks
312 if the file is access for reading.
313 """
314 if not os.path.isfile(filename):
315 stdmsg = 'File %s does not exist' % filename
316 self.fail(self._formatMessage(msg, stdmsg))
317 if not skip_size_check and not os.path.getsize(filename):
318 stdmsg = 'File %s is empty' % filename
319 self.fail(self._formatMessage(msg, stdmsg))
320 if not skip_access_check and not os.access(filename, os.R_OK):
321 stdmsg = 'File %s is not accessible for reading' % filename
322 self.fail(self._formatMessage(msg, stdmsg))
323
324 def assertFileMd5(self, filename, md5, msg=None):
325 """Test that file MD5 sum is equal to the provided sum.
326
327 The typical workflow is that you create a file in a way you
328 trust (that you obtain the right file). Then you compute MD5
329 sum of the file. And provide the sum in a test as a string::
330
331 self.assertFileMd5('result.txt', md5='807bba4ffa...')
332
333 Use `file_md5()` function from this package::
334
335 file_md5('original_result.txt')
336
337 Or in command line, use ``md5sum`` command if available:
338
339 .. code-block:: sh
340 md5sum some_file.txt
341
342 Finaly, you can use Python ``hashlib`` to obtain MD5::
343
344 import hashlib
345 hasher = hashlib.md5()
346 # expecting the file to fit into memory
347 hasher.update(open('original_result.txt', 'rb').read())
348 hasher.hexdigest()
349 """
350 self.assertFileExists(filename, msg=msg)
351 if not file_md5(filename) == md5:
352 standardMsg = 'File %s does not have the right MD5 sum' % filename
353 self.fail(self._formatMessage(msg, standardMsg))
354
355 def assertFilesEqualMd5(self, filename, reference, msg=None):
356 """Test that files are the same using MD5 sum.
357
358 This functions requires you to provide a file to test and
359 a reference file. For both, MD5 sum will be computed and compared with
360 each other.
361 """
362 self.assertFileExists(filename, msg=msg)
363 # nothing for ref, missing ref_filename is an error not a test failure
364 if not files_equal_md5(filename, reference):
365 stdmsg = 'Files %s and %s don\'t have the same MD5 sums' % (filename,
366 reference)
367 self.fail(self._formatMessage(msg, stdmsg))
368
369 def _compute_difference_raster(self, first, second, name_part):
370 """Compute difference of two rasters (first - second)
371
372 The name of the new raster is a long name designed to be as unique as
373 possible and contains names of two input rasters.
374
375 :param first: raster to subtract from
376 :param second: raster used as decrement
377 :param name_part: a unique string to be used in the difference name
378
379 :returns: name of a new raster
380 """
381 diff = ('tmp_' + self.id() + '_compute_difference_raster_'
382 + name_part + '_' + first + '_minus_' + second)
383 call_module('r.mapcalc',
384 stdin='"{d}" = "{f}" - "{s}"'.format(d=diff,
385 f=first,
386 s=second))
387 return diff
388
389 def assertRastersNoDifference(self, actual, reference,
390 precision, statistics=None, msg=None):
391 """Test that `actual` raster is not different from `reference` raster
392
393 Method behaves in the same way as `assertRasterFitsUnivar()`
394 but works on difference ``reference - actual``.
395 If statistics is not given ``dict(min=-precision, max=precision)``
396 is used.
397 """
398 if statistics is None or sorted(statistics.keys()) == ['max', 'min']:
399 if statistics is None:
400 statistics = dict(min=-precision, max=precision)
401 diff = self._compute_difference_raster(reference, actual,
402 'assertRastersNoDifference')
403 try:
404 self.assertCommandKeyValue('r.info', map=diff, flags='r',
405 sep='=', precision=precision,
406 reference=statistics, msg=msg)
407 finally:
408 call_module('g.remove', rast=diff)
409 # general case
410 self.assertRastersDifference(actual=actual, reference=reference,
411 statistics=statistics,
412 precision=precision, msg=msg)
413
414 def assertRastersDifference(self, actual, reference,
415 statistics, precision, msg=None):
416 """Test statistical values of difference of reference and actual rasters
417
418 For cases when you are interested in no or minimal difference,
419 use `assertRastersNoDifference()` instead.
420
421 This method should not be used to test r.mapcalc or r.univar.
422 """
423 diff = self._compute_difference_raster(reference, actual,
424 'assertRastersDifference')
425 try:
426 self.assertRasterFitsUnivar(raster=diff, reference=statistics,
427 precision=precision, msg=msg)
428 finally:
429 call_module('g.remove', rast=diff)
430
431 @classmethod
432 def runModule(cls, module, **kwargs):
433 """Run PyGRASS module.
434
435 Runs the module and raises an exception if the module ends with
436 non-zero return code. Usually, this is the same as testing the
437 return code and raising exception but by using this method,
438 you give testing framework more control over the execution,
439 error handling and storing of output.
440
441 In terms of testing framework, this function causes a common error,
442 not a test failure.
443
444 :raises CalledModuleError: if the module failed
445 """
446 module = _module_from_parameters(module, **kwargs)
447
448 if module.run_:
449 raise ValueError('Do not run the module manually, set run_=False')
450 if not module.finish_:
451 raise ValueError('This function will always finish module run,'
452 ' set finish_=None or finish_=True.')
453 # we expect most of the usages with stdout=PIPE
454 # TODO: in any case capture PIPE always?
455 if module.stdout_ is None:
456 module.stdout_ = subprocess.PIPE
457 elif module.stdout_ != subprocess.PIPE:
458 raise ValueError('stdout_ can be only PIPE or None')
459 if module.stderr_ is None:
460 module.stderr_ = subprocess.PIPE
461 elif module.stderr_ != subprocess.PIPE:
462 raise ValueError('stderr_ can be only PIPE or None')
463 # because we want to capture it
464 module.run()
465 if module.popen.returncode:
466 errors = module.outputs['stderr'].value
467 # provide diagnostic at least in English locale
468 # TODO: standardized error code would be handy here
469 import re
470 if re.search('Raster map.*not found', errors, flags=re.DOTALL):
471 errors += "\nSee available raster maps:\n"
472 errors += call_module('g.list', type='rast')
473 if re.search('Vector map.*not found', errors, flags=re.DOTALL):
474 errors += "\nSee available vector maps:\n"
475 errors += call_module('g.list', type='vect')
476 # TODO: message format, parameters
477 raise CalledModuleError(module.popen.returncode, module.name,
478 module.get_python(),
479 errors=errors)
480
481 # TODO: we can also comapre time to some expected but that's tricky
482 # maybe we should measure time but the real benchmarks with stdin/stdout
483 # should be done by some other function
484 # TODO: this should be the function used for valgrind or profiling or debug
485 # TODO: it asserts the rc but it does much more, so testModule?
486 # TODO: do we need special function for testing module failures or just add parameter returncode=0?
487 # TODO: consider not allowing to call this method more than once
488 # the original idea was to run this method just once for test method
489 # but for "integration" tests (script-like tests with more than one module)
490 # it would be better to be able to use this multiple times
491 # TODO: enable merging streams?
492 def assertModule(self, module, msg=None, **kwargs):
493 """Run PyGRASS module in controlled way and assert non-zero return code.
494
495 You should use this method to invoke module you are testing.
496 By using this method, you give testing framework more control over
497 the execution, error handling and storing of output.
498
499 It will not print module stdout and stderr, instead it will always
500 store them for further examination. Streams are stored separately.
501
502 This method is not suitable for testing error states of the module.
503 If you want to test behavior which involves non-zero return codes
504 and examine stderr in test, use `assertModuleFail()` method.
505
506 Runs the module and causes test failure if module ends with
507 non-zero return code.
508 """
509 module = _module_from_parameters(module, **kwargs)
510
511 # TODO: merge stderr to stdout? if caller gives PIPE, for sure not
512 if module.run_:
513 raise ValueError('Do not run the module manually, set run_=False')
514 if not module.finish_:
515 raise ValueError('This function will always finish module run,'
516 ' set finish_=None or finish_=True.')
517 if module.stdout_ is None:
518 module.stdout_ = subprocess.PIPE
519 elif module.stdout_ != subprocess.PIPE:
520 raise ValueError('stdout_ can be only PIPE or None')
521 # because we want to capture it
522 if module.stderr_ is None:
523 module.stderr_ = subprocess.PIPE
524 elif module.stderr_ != subprocess.PIPE:
525 raise ValueError('stderr_ can be only PIPE or None')
526 # because we want to capture it
527
528 module.run()
529 print module.outputs['stdout'].value
530 print module.outputs['stderr'].value
531 if module.popen.returncode:
532 # TODO: message format
533 # TODO: stderr?
534 stdmsg = ('Running <{m.name}> module ended'
535 ' with non-zero return code ({m.popen.returncode})\n'
536 'Called: {code}\n'
537 'See the folowing errors:\n'
538 '{errors}'.format(
539 m=module, code=module.get_python(),
540 errors=module.outputs["stderr"].value
541 ))
542 self.fail(self._formatMessage(msg, stdmsg))
543
544 # log these to final report
545 # TODO: always or only if the calling test method failed?
546 # in any case, this must be done before self.fail()
547 # module.outputs['stdout'].value
548 # module.outputs['stderr'].value
549
550 # TODO: should we merge stderr to stdout in this case?
551 def assertModuleFail(self, module, msg=None, **kwargs):
552 """Test that module fails with a non-zero return code.
553
554 Works like `assertModule()` but expects module to fail.
555 """
556 module = _module_from_parameters(module, **kwargs)
557
558 if module.run_:
559 raise ValueError('Do not run the module manually, set run_=False')
560 if not module.finish_:
561 raise ValueError('This function will always finish module run,'
562 ' set finish_=None or finish_=True.')
563 if module.stdout_ is None:
564 module.stdout_ = subprocess.PIPE
565 elif module.stdout_ != subprocess.PIPE:
566 raise ValueError('stdout_ can be only PIPE or None')
567 # because we want to capture it
568 if module.stderr_ is None:
569 module.stderr_ = subprocess.PIPE
570 elif module.stderr_ != subprocess.PIPE:
571 raise ValueError('stderr_ can be only PIPE or None')
572 # because we want to capture it
573
574 module.run()
575 print module.outputs['stdout'].value
576 print module.outputs['stderr'].value
577 if not module.popen.returncode:
578 stdmsg = ('Running <%s> ended with zero (successful) return code'
579 ' when expecting module to fail' % module.get_python())
580 self.fail(self._formatMessage(msg, stdmsg))
581
582
583# TODO: add tests and documentation to methods which are using this function
584# some test and documentation add to assertCommandKeyValue
585def _module_from_parameters(module, **kwargs):
586 if kwargs:
587 if not isinstance(module, basestring):
588 raise ValueError('module can be only string or PyGRASS Module')
589 if isinstance(module, Module):
590 raise ValueError('module can be only string if other'
591 ' parameters are given')
592 # allow to pass all parameters in one dictionary called parameters
593 if kwargs.keys() == ['parameters']:
594 kwargs = kwargs['parameters']
595 module = Module(module, run_=False, **kwargs)
596 return module
Note: See TracBrowser for help on using the repository browser.