浏览代码

python gunittest: Add a new assertion for raster equivalence (#1148)

Existing raster difference tests might miss corner cases when
rasters differ within specified statistics (e.g. min and max
match although rasters are not equal due to presence of NULL).
Māris Nartišs 4 年之前
父节点
当前提交
9a5b7879f7
共有 2 个文件被更改,包括 92 次插入5 次删除
  1. 76 2
      lib/python/gunittest/case.py
  2. 16 3
      lib/python/gunittest/testsuite/test_assertions.py

+ 76 - 2
lib/python/gunittest/case.py

@@ -282,9 +282,9 @@ class TestCase(unittest.TestCase):
 
     def assertRasterFitsInfo(self, raster, reference,
                              precision=None, msg=None):
-        r"""Test that raster map has the values obtained by r.univar module.
+        r"""Test that raster map has the values obtained by r.info module.
 
-        The function does not require all values from r.univar.
+        The function does not require all values from r.info.
         Only the provided values are tested.
         Typical example is checking minimum, maximum and type of the map::
 
@@ -453,6 +453,7 @@ class TestCase(unittest.TestCase):
     def assertRasterMinMax(self, map, refmin, refmax, msg=None):
         """Test that raster map minimum and maximum are within limits.
 
+        Minimum and maximum values are obtained from r.info.
         Map minimum and maximum is tested against expression::
 
             refmin <= actualmin and refmax >= actualmax
@@ -483,6 +484,7 @@ class TestCase(unittest.TestCase):
     def assertRaster3dMinMax(self, map, refmin, refmax, msg=None):
         """Test that 3D raster map minimum and maximum are within limits.
 
+        Minimum and maximum values are obtained from r3.info.
         Map minimum and maximum is tested against expression::
 
             refmin <= actualmin and refmax >= actualmax
@@ -707,6 +709,41 @@ class TestCase(unittest.TestCase):
                                                          s=second))
         return diff
 
+    def _map_different_raster_cells(self, first, second, name_part,
+                                    precision=0):
+        """Marks cells with different values with 1, equal with 0
+
+        For FCELL and DCELL maps precision (tolerance to difference)
+        should be set to a small positive value larger than 0
+        matching reference data precision.
+
+        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
+        :param precision: maximum difference between cell values
+
+        :returns: name of a new raster
+        """
+        diff = self._get_unique_name('map_different_raster_cells_' + name_part
+                                     + '_' + first + '_minus_' + second)
+        expression = (
+            '"{diff}" = ' +
+            'if( isnull("{first}") && isnull("{second}"), 0, ' +
+            'if( isnull("{first}") || isnull("{second}"), 1, ' +
+            'if( abs("{first}" - "{second}") > {precision}, 1, 0)))'
+        ).format(
+            diff=diff,
+            first=first,
+            second=second,
+            precision=precision
+        )
+
+        call_module('r.mapcalc', stdin=expression.encode("utf-8"))
+        return diff
+
     def _compute_vector_xor(self, ainput, alayer, binput, blayer, name_part):
         """Compute symmetric difference (xor) of two vectors
 
@@ -762,6 +799,11 @@ class TestCase(unittest.TestCase):
         but works on difference ``reference - actual``.
         If statistics is not given ``dict(min=-precision, max=precision)``
         is used.
+
+        Be ware – comparison is performed on overall statistics and thus
+        differences in individual cell values not changing overall
+        statistics might go unnoticed. Use `assertRastersEqual()`
+        for cell to cell equivalence testing.
         """
         if statistics is None or sorted(statistics.keys()) == ['max', 'min']:
             if statistics is None:
@@ -792,6 +834,11 @@ class TestCase(unittest.TestCase):
         use `assertRastersNoDifference()` instead.
 
         This method should not be used to test r.mapcalc or r.univar.
+
+        Be ware – comparison is performed on overall statistics and thus
+        differences in individual cell values not changing overall
+        statistics might go unnoticed. Use `assertRastersEqual()`
+        for cell to cell equivalence testing.
         """
         diff = self._compute_difference_raster(reference, actual,
                                                'assertRastersDifference')
@@ -848,6 +895,33 @@ class TestCase(unittest.TestCase):
         finally:
             call_module('g.remove', flags='f', type='raster_3d', name=diff)
 
+    def assertRastersEqual(self, actual, reference,
+                           precision=0, msg=None):
+        """Test that `actual` raster is equal to `reference` raster
+
+        Test compares if each cell value in `actual` raster is within
+        `precision` value to `reference` raster.
+        NULL values in both rasters are considered to be a match.
+
+        For CELL maps `precision` should be set to 0,
+        for FCELL and DCELL maps it should be set to match precision of
+        `reference` map (a positive number larger than 0).
+
+        Comparison is performed with r.mapcalc and r.info and thus is
+        affected by current computational region.
+        """
+
+        diff = self._map_different_raster_cells(reference, actual,
+                                                'assertRastersEqual',
+                                                precision)
+        try:
+            self.assertModuleKeyValue('r.info', map=diff, flags='r',
+                                      sep='=', precision=0,
+                                      reference={'min': 0, 'max': 0},
+                                      msg=msg)
+        finally:
+            call_module('g.remove', flags='f', type='raster', name=diff)
+
     # TODO: this works only in 2D
     # TODO: write tests
     def assertVectorIsVectorBuffered(self, actual, reference, precision, msg=None):

+ 16 - 3
lib/python/gunittest/testsuite/test_assertions.py

@@ -227,6 +227,19 @@ class TestRasterMapAssertions(TestCase):
                           statistics=dict(mean=0),
                           msg="The difference of different maps should have huge mean")
 
+    def test_assertRastersEqual(self):
+        """Test basic usage of assertRastersEqual"""
+        self.assertRastersEqual(actual='lakes',
+                                       reference='lakes',
+                                       precision=0,
+                                       msg="The same maps should have no difference")
+        self.assertRaises(self.failureException,
+                          self.assertRastersEqual,
+                          actual='elevation',
+                          reference='lakes',
+                          precision=1,
+                          msg="Different maps should have difference")
+
 
 class TestMapExistsAssertions(TestCase):
     # pylint: disable=R0904
@@ -281,9 +294,9 @@ class TestMapExistsAssertions(TestCase):
                           'does_not_exists')
 
     def test_rast_does_not_exist_in_current_mapset(self):
-		# expecting that there is elevation in PERMANENT
-		# TODO: use skip decorator
-		# TODO: add the same tests but for vect and rast3d
+        # expecting that there is elevation in PERMANENT
+        # TODO: use skip decorator
+        # TODO: add the same tests but for vect and rast3d
         self.assertRaises(self.failureException,
                           self.assertRasterExists,
                           'elevation',