[GRASS-SVN] r61221 - in sandbox/wenzeslaus/gunittest: . testsuite

svn_grass at osgeo.org svn_grass at osgeo.org
Wed Jul 9 13:54:01 PDT 2014


Author: wenzeslaus
Date: 2014-07-09 13:54:01 -0700 (Wed, 09 Jul 2014)
New Revision: 61221

Modified:
   sandbox/wenzeslaus/gunittest/__init__.py
   sandbox/wenzeslaus/gunittest/case.py
   sandbox/wenzeslaus/gunittest/checkers.py
   sandbox/wenzeslaus/gunittest/gmodules.py
   sandbox/wenzeslaus/gunittest/gutils.py
   sandbox/wenzeslaus/gunittest/invoker.py
   sandbox/wenzeslaus/gunittest/loader.py
   sandbox/wenzeslaus/gunittest/main.py
   sandbox/wenzeslaus/gunittest/reporters.py
   sandbox/wenzeslaus/gunittest/runner.py
   sandbox/wenzeslaus/gunittest/testsuite/test_assertions.py
   sandbox/wenzeslaus/gunittest/testsuite/test_checkers.py
   sandbox/wenzeslaus/gunittest/utils.py
Log:
gunittest: add missing test for assert raster difference and run only r.info -r if it is enough, rename compare to equals, add module docstrings with licences

Modified: sandbox/wenzeslaus/gunittest/__init__.py
===================================================================
--- sandbox/wenzeslaus/gunittest/__init__.py	2014-07-09 15:16:18 UTC (rev 61220)
+++ sandbox/wenzeslaus/gunittest/__init__.py	2014-07-09 20:54:01 UTC (rev 61221)
@@ -1,2 +1,19 @@
+# -*- coding: utf-8 -*-
+"""!@package grass.gunittest
+
+ at brief GRASS Python testing framework module for running from command line
+
+Copyright (C) 2014 by the GRASS Development Team
+This program is free software under the GNU General Public
+License (>=v2). Read the file COPYING that comes with GRASS GIS
+for details.
+
+ at author Vaclav Petras
+ at author Soeren Gebbert
+
+Initial version of `gunittest` was created during Google Summer of Code 2014
+by Vaclav Petras as a student and Soeren Gebbert as a mentor.
+"""
+
 from .case import TestCase
 from .main import test

Modified: sandbox/wenzeslaus/gunittest/case.py
===================================================================
--- sandbox/wenzeslaus/gunittest/case.py	2014-07-09 15:16:18 UTC (rev 61220)
+++ sandbox/wenzeslaus/gunittest/case.py	2014-07-09 20:54:01 UTC (rev 61221)
@@ -22,7 +22,7 @@
 
 from .gmodules import call_module
 from .checkers import (check_text_ellipsis,
-                       text_to_keyvalue, compare_keyvalue, diff_keyvalue,
+                       text_to_keyvalue, keyvalue_equals, diff_keyvalue,
                        file_md5, files_equal_md5)
 
 
@@ -106,7 +106,7 @@
     def del_temp_region(cls):
         """Remove the temporary region.
 
-        Unsets WIND_OVERRIDE and removes any region named by it.
+        Unsets ``WIND_OVERRIDE`` and removes any region named by it.
         """
         assert cls._temp_region
         name = os.environ.pop('WIND_OVERRIDE')
@@ -145,16 +145,34 @@
     # (note that we don't need precision for strings and usually for integers)
     # TODO: auto-determine precision based on the map type
     # TODO: we can have also more general function without the subset reference
-    # TODO: implement this also for PyGRASS Module
+    # TODO: change name to Module
     def assertCommandKeyValue(self, module, reference, sep,
-                              precision=None, msg=None, **parameters):
+                              precision, msg=None, **parameters):
+        """Test that output of a module is the same as provided subset.
+
+        ::
+
+            self.assertCommandKeyValue('r.info', map='elevation', flags='gr',
+                                       reference=dict(min=55.58, max=156.33),
+                                       precision=0.01, sep='=')
+
+        ::
+
+            module = SimpleModule('r.info', map='elevation', flags='gr')
+            self.assertCommandKeyValue(module,
+                                       reference=dict(min=55.58, max=156.33),
+                                       precision=0.01, sep='=')
+
+        The output of the module should be key-value pairs (shell script style)
+        which is typically obtained using ``-g`` flag.
+        """
         if isinstance(reference, basestring):
             reference = text_to_keyvalue(reference, sep=sep, skip_empty=True)
         module = _module_from_parameters(module, **parameters)
         self.runModule(module)
         raster_univar = text_to_keyvalue(module.outputs.stdout,
                                          sep=sep, skip_empty=True)
-        if not compare_keyvalue(dict_a=reference, dict_b=raster_univar,
+        if not keyvalue_equals(dict_a=reference, dict_b=raster_univar,
                                 a_is_subset=True, precision=precision):
             unused, missing, mismatch = diff_keyvalue(dict_a=reference,
                                                       dict_b=raster_univar,
@@ -166,17 +184,18 @@
                                  " provided in reference"
                                  ": %s\n" % (module, ", ".join(missing)))
             if mismatch:
-                standardMsg = "%s difference:\n" % module
-                standardMsg += "mismatch values (key, reference, actual): %s\n" % mismatch
-                standardMsg += 'command: %s %s' % (module, parameters)
+                stdMsg = "%s difference:\n" % module
+                stdMsg += "mismatch values"
+                stdMsg += "(key, reference, actual): %s\n" % mismatch
+                stdMsg += 'command: %s %s' % (module, parameters)
             else:
                 # we can probably remove this once we have more tests
-                # of compare_keyvalue and diff_keyvalue against each other
-                raise RuntimeError("compare_keyvalue() showed difference but"
+                # of keyvalue_equals and diff_keyvalue against each other
+                raise RuntimeError("keyvalue_equals() showed difference but"
                                    " diff_keyvalue() did not. This can be"
                                    " a bug in one of them or in the caller"
                                    " (assertCommandKeyValue())")
-            self.fail(self._formatMessage(msg, standardMsg))
+            self.fail(self._formatMessage(msg, stdMsg))
 
     def assertRasterFitsUnivar(self, raster, reference,
                                precision=None, msg=None):
@@ -192,7 +211,7 @@
 
         Use keyword arguments syntax for all function parameters.
 
-        Does not -e (extended statistics) flag, use `assertCommandKeyValue`
+        Does not -e (extended statistics) flag, use `assertCommandKeyValue()`
         for the full interface of arbitrary module.
         """
         self.assertCommandKeyValue(module='r.univar',
@@ -266,7 +285,7 @@
         Use keyword arguments syntax for all function parameters.
 
         To check that more statistics have certain values use
-        `assertRasterFitsUnivar` or `assertRasterFitsInfo`
+        `assertRasterFitsUnivar()` or `assertRasterFitsInfo()`
         """
         stdout = call_module('r.info', map=map, flags='r')
         actual = text_to_keyvalue(stdout, sep='=')
@@ -347,21 +366,50 @@
                                                                         reference)
             self.fail(self._formatMessage(msg, stdmsg))
 
-    # TODO: add tests for this method
+    def _compute_difference_raster(self, first, second, name_part):
+        """Compute difference of two rasters (first - second)
+
+        The name of the new raster is a long name designed to be as unique as
+        possible and contains names of two input rasters.
+
+        :param first: raster to subtract from
+        :param second: raster used as decrement
+        :param name_part: a unique string to be used in the difference name
+
+        :returns: name of a new raster
+        """
+        diff = ('tmp_' + self.id() + '_compute_difference_raster_'
+                + name_part + '_' + first + '_minus_' + second)
+        call_module('r.mapcalc',
+                    stdin='"{d}" = "{f}" - "{s}"'.format(d=diff,
+                                                         f=first,
+                                                         s=second))
+        return diff
+
     def assertRastersNoDifference(self, actual, reference,
                                    precision, statistics=None, msg=None):
         """Test that `actual` raster is not different from `reference` raster
-        
+
         Method behaves in the same way as `assertRasterFitsUnivar()`
         but works on difference ``referece - actual``.
         If statistics is not given ``dict(min=-precision, max=precision)``
         is used.
         """
-        if statistics is None:
-            statistics = dict(min=-precision, max=precision)
+        if statistics is None or sorted(statistics.keys()) == ['max', 'min']:
+            if statistics is None:
+                statistics = dict(min=-precision, max=precision)
+            diff = self._compute_difference_raster(reference, actual,
+                                                   'assertRastersNoDifference')
+            try:
+                self.assertCommandKeyValue('r.info', map=diff, flags='r',
+                                           sep='=', precision=precision,
+                                           reference=statistics, msg=msg)
+            finally:
+                call_module('g.remove', rast=diff)
+        # general case
         self.assertRastersDifference(actual=actual, reference=reference,
-                                     statistics=statistics, precision=precision,
-                                     msg=msg)
+                                     statistics=statistics,
+                                     precision=precision, msg=msg)
 
     def assertRastersDifference(self, actual, reference,
                                 statistics, precision, msg=None):
@@ -369,12 +417,11 @@
 
         For cases when you are interested in no or minimal difference,
         use `assertRastersNoDifference()` instead.
+
+        This method should not be used to test r.mapcalc or r.univar.
         """
-        diff = ('tmp__' + self.id() + '__assertRastersDifference_diff'
-                + actual + '__diff__' + reference)
-        call_module('r.mapcalc', stdin='"{d}" = "{r}" - "{a}"'.format(d=diff,
-                                                                    a=actual,
-                                                                    r=reference))
+        diff = self._compute_difference_raster(reference, actual,
+                                               'assertRastersDifference')
         try:
             self.assertRasterFitsUnivar(raster=diff, reference=statistics,
                                         precision=precision, msg=msg)
@@ -437,10 +484,11 @@
     # TODO: this should be the function used for valgrind or profiling or debug
     # TODO: it asserts the rc but it does much more, so testModule?
     # TODO: do we need special function for testing module failures or just add parameter returncode=0?
-    # TODO: consider allwing to call this method more than once
+    # TODO: consider not allowing to call this method more than once
     # the original idea was to run this method just once for test method
     # but for "integration" tests  (script-like tests with more than one module)
     # it would be better to be able to use this multiple times
+    # TODO: enable merging streams?
     def assertModule(self, module, msg=None, **kwargs):
         """Run PyGRASS module in controled way and assert non-zero return code.
 
@@ -449,13 +497,11 @@
         the execution, error handling and storing of output.
 
         It will not print module stdout and stderr, instead it will always
-        store them for furthere examination.
-        ? If you don't specify stdout_=PIPE, it will merge stderr into
-        stdout. ?
+        store them for further examination. Streams are stored separately.
 
-        This function is not suitable for testing error states of the module.
-        If you want to test behavior which involves non-zero return code
-        and examine stderr in test, use PyGRASS directly.
+        This method is not suitable for testing error states of the module.
+        If you want to test behavior which involves non-zero return codes
+        and examine stderr in test, use `assertModuleFail()` method.
 
         Runs the module and causes test failure if module ends with
         non-zero return code.
@@ -503,6 +549,10 @@
 
     # TODO: should we merge stderr to stdout in this case?
     def assertModuleFail(self, module, msg=None, **kwargs):
+        """Test that module fails with a non-zero return code.
+
+        Works like `assertModule()` but expects module to fail.
+        """
         module = _module_from_parameters(module, **kwargs)
 
         if module.run_:
@@ -531,6 +581,7 @@
 
 
 # TODO: add tests and documentation to methods which are using this function
+# some test and documentation add to assertCommandKeyValue
 def _module_from_parameters(module, **kwargs):
     if kwargs:
         if not isinstance(module, basestring):

Modified: sandbox/wenzeslaus/gunittest/checkers.py
===================================================================
--- sandbox/wenzeslaus/gunittest/checkers.py	2014-07-09 15:16:18 UTC (rev 61220)
+++ sandbox/wenzeslaus/gunittest/checkers.py	2014-07-09 20:54:01 UTC (rev 61221)
@@ -21,7 +21,6 @@
 # alternative term to check(er(s)) would be compare
 
 
-# TODO: these functions returns dictionary but they also modify it
 def unify_projection(dic):
     """Unifies names of projections.
 
@@ -29,21 +28,24 @@
     'Universal Transverse Mercator' and 'Universe Transverse Mercator'.
     This function replaces synonyms by a unified name.
 
-    >>> unify_projection({'name': ['Universe Transverse Mercator']})
-    {'name': ['Universal Transverse Mercator']}
+    Example of common typo in UTM replaced by correct spelling::
 
-    @param dic The dictionary containing information about projection
+        >>> unify_projection({'name': ['Universe Transverse Mercator']})
+        {'name': ['Universal Transverse Mercator']}
 
-    @return The dictionary with the new values if needed
+    :param dic: The dictionary containing information about projection
+
+    :return: The dictionary with the new values if needed or a copy of old one
     """
     # the lookup variable is a list of list, each list contains all the
     # possible name for a projection system
-    lookup = [['Universal Transverse Mercator', 'Universe Transverse Mercator']]
+    lookup = [['Universal Transverse Mercator',
+               'Universe Transverse Mercator']]
     dic = dict(dic)
-    for lo in lookup:
+    for l in lookup:
         for n in range(len(dic['name'])):
-            if dic['name'][n] in lo:
-                dic['name'][n] = lo[0]
+            if dic['name'][n] in l:
+                dic['name'][n] = l[0]
     return dic
 
 
@@ -53,12 +55,14 @@
     Some units have different spelling although they are the same units.
     This functions replaces different spelling options by unified one.
 
-    >>> unify_units({'units': ['metres'], 'unit': ['metre']})
-    {'units': ['meters'], 'unit': ['meter']}
+    Example of British English spelling replaced by US English spelling::
 
-    @param dic The dictionary containing information about units
+        >>> unify_units({'units': ['metres'], 'unit': ['metre']})
+        {'units': ['meters'], 'unit': ['meter']}
 
-    @return The dictionary with the new values if needed
+    :param dic: The dictionary containing information about units
+
+    :return: The dictionary with the new values if needed or a copy of old one
     """
     # the lookup variable is a list of list, each list contains all the
     # possible name for a units
@@ -79,76 +83,93 @@
                 dic['unit'] = l[0]
         if not isinstance(dic['units'], types.StringTypes):
             for n in range(len(dic['units'])):
-                    if dic['units'][n] in l:
-                        dic['units'][n] = l[0]
+                if dic['units'][n] in l:
+                    dic['units'][n] = l[0]
         else:
             if dic['units'] in l:
                 dic['units'] = l[0]
     return dic
 
 
+def value_from_string(value):
+    """Create value of a most fitting type from a string.
+
+    Type conversions are applied in order ``int``, ``float``, ``string``
+    where string is no conversion.
+
+    >>> value_from_string('1')
+    1
+    >>> value_from_string('5.6')
+    5.6
+    >>> value_from_string('  5.6\t  ')
+    5.6
+    >>> value_from_string('hello')
+    'hello'
+    """
+    not_float = False
+    not_int = False
+    # Convert values into correct types
+    # We first try integer then float because
+    # int('1.0') is ValueError (although int(1.0) is not)
+    # while float('1') is not
+    try:
+        value_converted = int(value)
+    except ValueError:
+        not_int = True
+    if not_int:
+        try:
+            value_converted = float(value)
+        except ValueError:
+            not_float = True
+    # strip strings from whitespace (expecting spaces and tabs)
+    if not_int and not_float:
+        value_converted = value.strip()
+    return value_converted
+
+
+# TODO: what is the default separator?
 def text_to_keyvalue(text, sep=":", val_sep=",", functions=None,
-                     skip_invalid=False, skip_empty=False):
-    """
+                     skip_invalid=False, skip_empty=False,
+                     from_string=value_from_string):
+    """Convert test to key-value pairs (dictionary-like KeyValue object).
+
     Converts a key-value text file, where entries are separated
     by newlines and the key and value are separated by `sep`,
-    into a key-value dictionary and discover/use the correct
+    into a key-value dictionary and discovers/uses the correct
     data types (float, int or string) for values.
 
-    And empty string is a valid input because empty dictionary is a valid
-    dictionary.
+    Besides key-value pairs it also parses values itself. Value is created
+    with the best fitting type using `value_from_string()` function by default.
+    When val_sep is present in value part, the resulting value is
+    a list of values.
 
-    @param text string to convert
-    @param sep character that separates the keys and values, default is ":"
-    @param val_sep character that separates the values of a single key, default is ","
-    @param functions list of functions to apply on the resulting dictionary
-    @param skip_invalid skip all lines which does not contain separator
-    @param skip_empty skip empty lines
+    :param text: string to convert
+    :param sep: character that separates the keys and values
+    :param val_sep: character that separates the values of a single key
+    :param functions: list of functions to apply on the resulting dictionary
+    :param skip_invalid: skip all lines which does not contain separator
+    :param skip_empty: skip empty lines
+    :param from_string: a function used to convert strings to values,
+        use ``lambda x: x`` for no conversion
 
-    @return The dictionary
+    :return: a dictionary representation of text
+    :return type: grass.script.core.KeyValue
 
-    A text file with this content:
-    Will be represented as this dictionary:
+    And example of converting text with text, floats, integeres and list
+    to a dictionary::
 
-    >>> sorted(text_to_keyvalue('''a: Hello
-    ... b: 1.0
-    ... c: 1,2,3,4,5
-    ... d : hello,8,0.1''').items())  # sorted items from the dictionary
-    [('a', 'Hello'), ('b', 1.0), ('c', [1, 2, 3, 4, 5]), ('d', ['hello', 8, 0.1])]
+        >>> sorted(text_to_keyvalue('''a: Hello
+        ... b: 1.0
+        ... c: 1,2,3,4,5
+        ... d : hello,8,0.1''').items())  # sorted items from the dictionary
+        [('a', 'Hello'), ('b', 1.0), ('c', [1, 2, 3, 4, 5]), ('d', ['hello', 8, 0.1])]
+
+    .. warning::
+        And empty string is a valid input because empty dictionary is a valid
+        dictionary. You need to test this separately accorting
+        to the circumstances.
     """
-    def value_from_string(value):
-        """
-        >>> convert_from_string('1')
-        1
-        >>> convert_from_string('5.6')
-        5.6
-        >>> convert_from_string('  5.6\t  ')
-        5.6
-        >>> convert_from_string('hello')
-        'hello'
-        """
-        # note that above code is not invoked by doctest
-        not_float = False
-        not_int = False
-        # Convert values into correct types
-        # We first try integer then float because
-        # int('1.0') is ValueError (although int(1.0) is not)
-        # while float('1') is not
-        try:
-            value_converted = int(value)
-        except ValueError:
-            not_int = True
-        if not_int:
-            try:
-                value_converted = float(value)
-            except ValueError:
-                not_float = True
-        # strip strings from whitespace (expecting spaces and tabs)
-        if not_int and not_float:
-            value_converted = value.strip()
-        return value_converted
-
-    # split according to universal newlines approach
+    # splitting according to universal newlines approach
     # TODO: add also general split with vsep
     text = text.splitlines()
     kvdict = gcore.KeyValue()
@@ -189,19 +210,21 @@
             values = value.split(val_sep)
             value_list = []
             for value in values:
-                value_converted = value_from_string(value)
+                value_converted = from_string(value)
                 value_list.append(value_converted)
             kvdict[key] = value_list
         else:
             # single values
-            kvdict[key] = value_from_string(value)
+            kvdict[key] = from_string(value)
     for function in functions:
         kvdict = function(kvdict)
     return kvdict
 
 
 # TODO: decide if there should be some default for precision
+# TODO: define standard precisions for DCELL, FCELL, CELL, mm, ft, cm, ...
 # TODO: decide if None is valid, and use some default or no compare
+# TODO: is None a valid value for precision?
 def values_equal(value_a, value_b, precision=0.000001):
     """
     >>> values_equal(1.022, 1.02, precision=0.01)
@@ -233,7 +256,7 @@
         precision = float(precision)
         # we will apply precision to int-float comparison
         # rather than converting both to integer
-        # (as in the original function from core)
+        # (as in the original function from grass.script.core)
         if abs(value_a - value_b) > precision:
             return False
 
@@ -256,43 +279,46 @@
     return True
 
 
-# TODO: rename to equals
-def compare_keyvalue(dict_a, dict_b, precision=0.000001,
-                     def_equal=values_equal, key_equal=None,
-                     a_is_subset=False):
-    """Compare two dictionaries
+def keyvalue_equals(dict_a, dict_b, precision,
+                    def_equal=values_equal, key_equal=None,
+                    a_is_subset=False):
+    """Compare two dictionaries.
 
-    This method will print a warning in case keys that are present in the first
-    file are not present in the second one.
-    The comparison method tries to convert the values into their native format
-    (float, int or string) to allow correct comparison.
+    .. note::
+        Always use keyword arguments for all parameters with defaults.
+        It is a good idea to use keyword arguments also for the first
+        two parameters.
 
-    Always use keyword arguments for all parameters with defaults. It is a good
-    idea to use keyword arguments also for the first two parameters.
+    An example of key-value texts comparison::
 
-    An example key-value text file may have this content:
+        >>> keyvalue_equals(text_to_keyvalue('''a: Hello
+        ... b: 1.0
+        ... c: 1,2,3,4,5
+        ... d: hello,8,0.1'''),
+        ... text_to_keyvalue('''a: Hello
+        ... b: 1.1
+        ... c: 1,22,3,4,5
+        ... d: hello,8,0.1'''), precision=0.1)
+        False
 
-    >>> compare_keyvalue(text_to_keyvalue('''a: Hello
-    ... b: 1.0
-    ... c: 1,2,3,4,5
-    ... d: hello,8,0.1'''),
-    ... text_to_keyvalue('''a: Hello
-    ... b: 1.1
-    ... c: 1,22,3,4,5
-    ... d: hello,8,0.1'''), precision=0.1)
-    False
-
-    @param filename_a name of the first key-value text file
-    @param filenmae_b name of the second key-value text file
-    @param sep character that separates the keys and values, default is ":"
-    @param val_sep character that separates the values of a single key, default is ","
-    @param precision precision with which the floating point values are compared
-    @param proj True if it has to check some information about projection system
-    @param units True if it has to check some information about units
+    :param dict_a: first dictionary
+    :param dict_b: second dictionary
+    :param precision: precision with which the floating point values
+        are compared (passed to equality functions)
     :param callable def_equal: function used for comparison by default
-    :param dict key_equal: dictionary of functions used for comparison of specific keys
+    :param dict key_equal: dictionary of functions used for comparison
+        of specific keys, `def_equal` is used for the rest,
+        keys in dictionary are keys in `dict_a` and `dict_b` dictionaries,
+        values are the fuctions used to comapare the given key
+    :param a_is_subset: `True` if `dict_a` is a subset of `dict_b`,
+        `False` otherwise
 
-    @return True if full or almost identical, False if different
+    :return: `True` if identical, `False` if different
+
+    Use `diff_keyvalue()` to get information about differeces.
+    You can use this function to find out if there is a difference and then
+    use `diff_keyvalue()` to determine all the differences between
+    dictionaries.
     """
     key_equal = {} if key_equal is None else key_equal
 
@@ -309,31 +335,41 @@
         equal_fun = key_equal.get(key, def_equal)
         if not equal_fun(dict_a[key], dict_b[key], precision):
             return False
-        # bahavior change comparing to the original proj fun
-        # elif isinstance(dict_a[key], float) or isinstance(dict_b[key], float):
-            #warning(_("Mixing value types. Will try to compare after "
-            #          "integer conversion"))
-            #return int(dict_a[key]) == int(dict_b[key])
-        #elif key == "+towgs84":
-        #    # We compare the sum of the entries
-        #    if abs(sum(dict_a[key]) - sum(dict_b[key])) > precision:
-        #        return False
     return True
 
 
-# TODO: should the retrun depend on the a_is_subset parameter?
-# must have the same interface and behavior as compare_keyvalue
-def diff_keyvalue(dict_a, dict_b, precision=0.000001,
+# TODO: should the return depend on the a_is_subset parameter?
+# this function must have the same interface and behavior as keyvalue_equals
+def diff_keyvalue(dict_a, dict_b, precision,
                   def_equal=values_equal, key_equal=None,
                   a_is_subset=False):
-    """
+    """Determine the difference of two dictionaries.
 
-    >>> a = {'c': 2, 'b': 3, 'a': 4}
-    >>> b = {'c': 1, 'b': 3, 'd': 5}
-    >>> diff_keyvalue(a, b)
-    (['d'], ['a'], [('c', 2, 1)])
-    >>> diff_keyvalue(a, b, a_is_subset=True)
-    ([], ['a'], [('c', 2, 1)])
+    The function returns missing keys and differnt values for common keys::
+
+        >>> a = {'c': 2, 'b': 3, 'a': 4}
+        >>> b = {'c': 1, 'b': 3, 'd': 5}
+        >>> diff_keyvalue(a, b, precision=0)
+        (['d'], ['a'], [('c', 2, 1)])
+
+    You can provide only a subset of values in dict_a, in this case
+    first item in tuple is an emptu list::
+
+        >>> diff_keyvalue(a, b, a_is_subset=True, precision=0)
+        ([], ['a'], [('c', 2, 1)])
+
+    This function behaves the same as `keyvalue_equals()`.
+
+    :returns: A tuple of lists, fist is list of missing keys in dict_a,
+        second missing keys in dict_b and third is a list of mismatched
+        values as tuples (key, value_from_a, value_from_b)
+    :rtype: (list, list, list)
+
+    Comparing to the Python ``difflib`` package this function does not create
+    any difference output. It just returns the dictionaries.
+    Comapring to the Python ``unittest`` ``assertDictEqual()``,
+    this function does not issues error or exception, it just determines
+    what it the difference.
     """
     key_equal = {} if key_equal is None else key_equal
 
@@ -363,11 +399,12 @@
 
 
 def proj_info_equals(text_a, text_b):
+    """Test if two PROJ_INFO texts are equal."""
     def compare_sums(list_a, list_b, precision):
-        # We compare the sum of the entries
+        """Compare difference of sums of two list using precision"""
+        # derived from the code in grass.script.core
         if abs(sum(list_a) - sum(list_b)) > precision:
             return False
-
     sep = ':'
     val_sep = ','
     key_equal = {'+towgs84': compare_sums}
@@ -375,16 +412,22 @@
                               functions=[unify_projection])
     dict_b = text_to_keyvalue(text_b, sep=sep, val_sep=val_sep,
                               functions=[unify_projection])
-    return compare_keyvalue(dict_a, dict_b,
+    return keyvalue_equals(dict_a, dict_b,
                             precision=0.000001,
                             def_equal=values_equal,
                             key_equal=key_equal)
 
 
 def proj_units_equals(text_a, text_b):
+    """Test if two PROJ_UNITS texts are equal."""
     def lowercase_equals(string_a, string_b, precision=None):
+        # we don't need a waring for unused precision
+        # pylint: disable=W0613
+        """Test equality of two strings ignoring their case using ``lower()``.
+
+        Precision is accepted as require by `keyvalue_equals()` but ignored.
+        """
         return string_a.lower() == string_b.lower()
-
     sep = ':'
     val_sep = ','
     key_equal = {'unit': lowercase_equals, 'units': lowercase_equals}
@@ -392,7 +435,7 @@
                               functions=[unify_units])
     dict_b = text_to_keyvalue(text_b, sep, val_sep,
                               functions=[unify_units])
-    return compare_keyvalue(dict_a, dict_b,
+    return keyvalue_equals(dict_a, dict_b,
                             precision=0.000001,
                             def_equal=values_equal,
                             key_equal=key_equal)
@@ -510,6 +553,7 @@
 
 # TODO: accept also open file object
 def file_md5(filename):
+    """Get MD5 (check) sum of a file."""
     hasher = hashlib.md5()
     with open(filename, 'rb') as f:
         buf = f.read(_BUFFER_SIZE)
@@ -521,14 +565,24 @@
 
 def text_file_md5(filename, exclude_lines=None,
                   prepend_lines=None, append_lines=None):
+    """Get a MD5 (check) sum of a text file.
+
+    Works in the same way as `file_md5()` function but allows to
+    exclude lines from the file as well as prepend or append them.
+
+    .. todo::
+        Implement this function.
+    """
     raise NotImplementedError("Implement, or use file_md5() function instead")
 
 
 def files_equal_md5(filename_a, filename_b):
+    """Check equality of two files according to their MD5 sums"""
     return file_md5(filename_a) == file_md5(filename_b)
 
 
 def main():  # pragma: no cover
+    """Run the doctest"""
     ret = doctest.testmod()
     return ret.failed
 

Modified: sandbox/wenzeslaus/gunittest/gmodules.py
===================================================================
--- sandbox/wenzeslaus/gunittest/gmodules.py	2014-07-09 15:16:18 UTC (rev 61220)
+++ sandbox/wenzeslaus/gunittest/gmodules.py	2014-07-09 20:54:01 UTC (rev 61221)
@@ -1,17 +1,16 @@
 # -*- coding: utf-8 -*-
-"""!@package grass.gunittest.case
+"""!@package grass.gunittest.gmodules
 
- at brief GRASS Python testing framework specialized GRASS module interfaces
+ at brief Specialized interfaces for invoking modules for testing framework
 
-(C) 2014 by the GRASS Development Team
+Copyright (C) 2014 by the GRASS Development Team
 This program is free software under the GNU General Public
-License (>=v2). Read the file COPYING that comes with GRASS
+License (>=v2). Read the file COPYING that comes with GRASS GIS
 for details.
 
 @author Vaclav Petras
 @author Soeren Gebbert
 """
-
 import subprocess
 from grass.script.core import start_command
 from grass.exceptions import CalledModuleError

Modified: sandbox/wenzeslaus/gunittest/gutils.py
===================================================================
--- sandbox/wenzeslaus/gunittest/gutils.py	2014-07-09 15:16:18 UTC (rev 61220)
+++ sandbox/wenzeslaus/gunittest/gutils.py	2014-07-09 20:54:01 UTC (rev 61221)
@@ -1,8 +1,15 @@
 # -*- coding: utf-8 -*-
+"""!@package grass.gunittest.gutils
+
+ at brief Utilities related to GRASS GIS for GRASS Python testing framework
+
+Copyright (C) 2014 by the GRASS Development Team
+This program is free software under the GNU General Public
+License (>=v2). Read the file COPYING that comes with GRASS GIS
+for details.
+
+ at author Vaclav Petras
 """
-Utilities related to GRASS GIS.
-"""
-
 from .gmodules import call_module
 
 

Modified: sandbox/wenzeslaus/gunittest/invoker.py
===================================================================
--- sandbox/wenzeslaus/gunittest/invoker.py	2014-07-09 15:16:18 UTC (rev 61220)
+++ sandbox/wenzeslaus/gunittest/invoker.py	2014-07-09 20:54:01 UTC (rev 61221)
@@ -1,5 +1,16 @@
 # -*- coding: utf-8 -*-
+"""!@package grass.gunittest.invoker
 
+ at brief GRASS Python testing framework test files invoker (runner)
+
+Copyright (C) 2014 by the GRASS Development Team
+This program is free software under the GNU General Public
+License (>=v2). Read the file COPYING that comes with GRASS GIS
+for details.
+
+ at author Vaclav Petras
+"""
+
 import os
 import sys
 import shutil

Modified: sandbox/wenzeslaus/gunittest/loader.py
===================================================================
--- sandbox/wenzeslaus/gunittest/loader.py	2014-07-09 15:16:18 UTC (rev 61220)
+++ sandbox/wenzeslaus/gunittest/loader.py	2014-07-09 20:54:01 UTC (rev 61221)
@@ -1,7 +1,16 @@
 # -*- coding: utf-8 -*-
+"""!@package grass.gunittest.loader
 
-"""Loading unittests."""
+ at brief GRASS Python testing framework test loading functionality
 
+Copyright (C) 2014 by the GRASS Development Team
+This program is free software under the GNU General Public
+License (>=v2). Read the file COPYING that comes with GRASS GIS
+for details.
+
+ at author Vaclav Petras
+"""
+
 import os
 import sys
 import fnmatch

Modified: sandbox/wenzeslaus/gunittest/main.py
===================================================================
--- sandbox/wenzeslaus/gunittest/main.py	2014-07-09 15:16:18 UTC (rev 61220)
+++ sandbox/wenzeslaus/gunittest/main.py	2014-07-09 20:54:01 UTC (rev 61221)
@@ -1,5 +1,16 @@
 # -*- coding: utf-8 -*-
+"""!@package grass.gunittest.main
 
+ at brief GRASS Python testing framework module for running from command line
+
+Copyright (C) 2014 by the GRASS Development Team
+This program is free software under the GNU General Public
+License (>=v2). Read the file COPYING that comes with GRASS GIS
+for details.
+
+ at author Vaclav Petras
+"""
+
 import os
 import sys
 

Modified: sandbox/wenzeslaus/gunittest/reporters.py
===================================================================
--- sandbox/wenzeslaus/gunittest/reporters.py	2014-07-09 15:16:18 UTC (rev 61220)
+++ sandbox/wenzeslaus/gunittest/reporters.py	2014-07-09 20:54:01 UTC (rev 61221)
@@ -1,5 +1,17 @@
 # -*- coding: utf-8 -*-
+"""!@package grass.gunittest.reporters
 
+ at brief GRASS Python testing framework module for report generation
+
+Copyright (C) 2014 by the GRASS Development Team
+This program is free software under the GNU General Public
+License (>=v2). Read the file COPYING that comes with GRASS GIS
+for details.
+
+ at author Vaclav Petras
+"""
+
+
 import os
 import sys
 import datetime

Modified: sandbox/wenzeslaus/gunittest/runner.py
===================================================================
--- sandbox/wenzeslaus/gunittest/runner.py	2014-07-09 15:16:18 UTC (rev 61220)
+++ sandbox/wenzeslaus/gunittest/runner.py	2014-07-09 20:54:01 UTC (rev 61221)
@@ -1,11 +1,20 @@
 # -*- coding: utf-8 -*-
+"""!@package grass.gunittest.runner
 
-"""Running tests
+ at brief Testing framework module for running tests in Python unittest fashion
 
+Copyright (C) 2014 by the GRASS Development Team
+This program is free software under the GNU General Public
+License (>=v2). Read the file COPYING that comes with GRASS GIS
+for details.
+
+ at author Vaclav Petras
+
 File content taken from Python's  ``unittest.runner``, it will be used as
 a template. It is not expected that something will left.
 """
 
+
 import sys
 import time
 

Modified: sandbox/wenzeslaus/gunittest/testsuite/test_assertions.py
===================================================================
--- sandbox/wenzeslaus/gunittest/testsuite/test_assertions.py	2014-07-09 15:16:18 UTC (rev 61220)
+++ sandbox/wenzeslaus/gunittest/testsuite/test_assertions.py	2014-07-09 20:54:01 UTC (rev 61221)
@@ -11,6 +11,7 @@
 from grass.pygrass.modules import Module
 
 import gunittest
+from gunittest.gmodules import SimpleModule
 
 
 class TestTextAssertions(gunittest.TestCase):
@@ -83,6 +84,47 @@
 """
 
 
+class TestAssertCommandKeyValue(gunittest.TestCase):
+    """Test usage of `.assertCommandKeyValue` method."""
+    # pylint: disable=R0904
+
+    @classmethod
+    def setUpClass(cls):
+        cls.use_temp_region()
+        cls.runModule(Module('g.region', rast='elevation', run_=False))
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.del_temp_region()
+
+    def test_pygrass_module(self):
+        """Test syntax with Module as module"""
+        module = Module('r.info', map='elevation', flags='gr', run_=False)
+        self.assertCommandKeyValue(module,
+                                   reference=dict(min=55.58, max=156.33),
+                                   precision=0.01, sep='=')
+
+    def test_pygrass_simple_module(self):
+        """Test syntax with SimpleModule as module"""
+        module = SimpleModule('r.info', map='elevation', flags='gr')
+        self.assertCommandKeyValue(module,
+                                   reference=dict(min=55.58, max=156.33),
+                                   precision=0.01, sep='=')
+
+    def test_direct_parameters(self):
+        """Test syntax with module and its parameters as fnction parameters"""
+        self.assertCommandKeyValue('r.info', map='elevation', flags='gr',
+                                   reference=dict(min=55.58, max=156.33),
+                                   precision=0.01, sep='=')
+
+    def test_parameters_parameter(self):
+        """Test syntax with module parameters in one parameters dictionary"""
+        self.assertCommandKeyValue(module='r.info',
+                                   parameters=dict(map='elevation', flags='gr'),
+                                   reference=dict(min=55.58, max=156.33),
+                                   precision=0.01, sep='=')
+
+
 class TestRasterMapAssertations(gunittest.TestCase):
     # pylint: disable=R0904
 
@@ -125,7 +167,35 @@
         # this also tests if we are using r.info -e flag
         self.assertRasterFitsInfo('elevation', ELEVATION_MAPSET_DICT)
 
+    def test_assertRastersNoDifference(self):
+        """Test basic usage of assertRastersNoDifference"""
+        self.assertRastersNoDifference(actual='elevation',
+                                       reference='elevation',
+                                       precision=0,  # this might need to be increased
+                                       msg="The same maps should have no difference")
+        self.assertRaises(self.failureException,
+                          self.assertRastersNoDifference,
+                          actual='elevation',
+                          reference='aspect',
+                          precision=1,
+                          msg="Different maps should have difference")
 
+    def test_assertRastersNoDifference_mean(self):
+        """Test usage of assertRastersNoDifference with mean"""
+        self.assertRastersNoDifference(actual='elevation',
+                                       reference='elevation',
+                                       precision=0,  # this might need to be increased
+                                       statistics=dict(mean=0),
+                                       msg="The difference of same maps should have small mean")
+        self.assertRaises(self.failureException,
+                          self.assertRastersNoDifference,
+                          actual='elevation',
+                          reference='aspect',
+                          precision=1,
+                          statistics=dict(mean=0),
+                          msg="The difference of different maps should have huge mean")
+
+
 class TestVectorMapAssertations(gunittest.TestCase):
     # pylint: disable=R0904
     def test_assertVectorFitsUnivar(self):

Modified: sandbox/wenzeslaus/gunittest/testsuite/test_checkers.py
===================================================================
--- sandbox/wenzeslaus/gunittest/testsuite/test_checkers.py	2014-07-09 15:16:18 UTC (rev 61220)
+++ sandbox/wenzeslaus/gunittest/testsuite/test_checkers.py	2014-07-09 20:54:01 UTC (rev 61221)
@@ -18,7 +18,7 @@
 
 import gunittest
 from gunittest.checkers import (values_equal, text_to_keyvalue,
-                                compare_keyvalue,
+                                keyvalue_equals,
                                 proj_info_equals, proj_units_equals)
 
 
@@ -279,29 +279,31 @@
 class TestRasterMapComparisons(gunittest.TestCase):
 
     def test_compare_univars(self):
-        self.assertTrue(compare_keyvalue(text_to_keyvalue(R_UNIVAR_ELEVATION,
+        self.assertTrue(keyvalue_equals(text_to_keyvalue(R_UNIVAR_ELEVATION,
                                                           sep='='),
                                          text_to_keyvalue(R_UNIVAR_ELEVATION,
-                                                          sep='=')))
-        self.assertFalse(compare_keyvalue(text_to_keyvalue(R_UNIVAR_ELEVATION,
+                                                          sep='='),
+                                         precision=0))
+        self.assertFalse(keyvalue_equals(text_to_keyvalue(R_UNIVAR_ELEVATION,
                                                            sep='='),
                                           text_to_keyvalue(R_UNIVAR_ELEVATION_SUBSET,
-                                                           sep='=')))
+                                                           sep='='),
+                                          precision=0))
 
     def test_compare_univars_subset(self):
-        self.assertTrue(compare_keyvalue(text_to_keyvalue(R_UNIVAR_ELEVATION_SUBSET,
+        self.assertTrue(keyvalue_equals(text_to_keyvalue(R_UNIVAR_ELEVATION_SUBSET,
                                                           sep='='),
                                          text_to_keyvalue(R_UNIVAR_ELEVATION,
                                                           sep='='),
-                                         a_is_subset=True))
-        self.assertFalse(compare_keyvalue(text_to_keyvalue(R_UNIVAR_ELEVATION,
+                                         a_is_subset=True, precision=0))
+        self.assertFalse(keyvalue_equals(text_to_keyvalue(R_UNIVAR_ELEVATION,
                                                            sep='='),
                                           text_to_keyvalue(R_UNIVAR_ELEVATION_SUBSET,
                                                            sep='='),
-                                          a_is_subset=True))
+                                          a_is_subset=True, precision=0))
 
     def test_compare_univars_rounded(self):
-        self.assertTrue(compare_keyvalue(text_to_keyvalue(R_UNIVAR_ELEVATION,
+        self.assertTrue(keyvalue_equals(text_to_keyvalue(R_UNIVAR_ELEVATION,
                                                           sep='='),
                                          text_to_keyvalue(R_UNIVAR_ELEVATION_ROUNDED,
                                                           sep='='),

Modified: sandbox/wenzeslaus/gunittest/utils.py
===================================================================
--- sandbox/wenzeslaus/gunittest/utils.py	2014-07-09 15:16:18 UTC (rev 61220)
+++ sandbox/wenzeslaus/gunittest/utils.py	2014-07-09 20:54:01 UTC (rev 61221)
@@ -1,3 +1,16 @@
+# -*- coding: utf-8 -*-
+"""!@package grass.gunittest.utils
+
+ at brief GRASS Python testing framework utilities (general and test-specific)
+
+Copyright (C) 2014 by the GRASS Development Team
+This program is free software under the GNU General Public
+License (>=v2). Read the file COPYING that comes with GRASS GIS
+for details.
+
+ at author Vaclav Petras
+"""
+
 import os
 import sys
 import shutil
@@ -4,7 +17,6 @@
 import errno
 
 
-# TODO: move these to utils or even somewhere more general
 def ensure_dir(directory):
     """Create all directories in the given path if needed."""
     if not os.path.exists(directory):



More information about the grass-commit mailing list