Repository: rasterio/rasterio

Issue:
Test failures with gdal-3.2.0 + proj-7.2.0
While working on packaging gdal-3.2.0 + proj-7.2.0 for Fedora, rebuilding python-rasterio against these results in the following test failures, which don't occur with gdal-3.1.4 + proj-6.3.2:

    =================================== FAILURES ===================================
    _________________________________ test_shapes __________________________________

    basic_image = array([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 1, 1, 0, 0, 0, 0, 0],
    ...0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=uint8)

        def test_shapes(basic_image):
            """Test creation of shapes from pixel values."""
            results = list(shapes(basic_image))
        
            assert len(results) == 2
        
            shape, value = results[0]
            assert shape == {
                'coordinates': [
                    [(2, 2), (2, 5), (5, 5), (5, 2), (2, 2)]
                ],
                'type': 'Polygon'
            }
            assert value == 1
        
            shape, value = results[1]
    >       assert shape == {
                'coordinates': [
                    [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)],
                    [(2, 2), (5, 2), (5, 5), (2, 5), (2, 2)]
                ],
                'type': 'Polygon'
            }
    E       AssertionError: assert {'coordinates...e': 'Polygon'} == {'coordinates...e': 'Polygon'}
    E         Omitting 1 identical items, use -vv to show
    E         Differing items:
    E         {'coordinates': [[(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0), (0.0, 0.0)], [(2.0, 2.0), (2.0, 5.0), (5.0, 5.0), (5.0, 2.0), (2.0, 2.0)]]} != {'coordinates': [[(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)], [(2, 2), (5, 2), (5, 5), (2, 5), (2, 2)]]}
    E         Full diff:
    E           {
    E         -  'coordinates': [[(0,
    E         +  'coordinates': [[(0.0,...
    E         
    E         ...Full output truncated (61 lines hidden), use '-vv' to show

    tests/test_features.py:824: AssertionError
    ------------------------------ Captured log call -------------------------------
    DEBUG    rasterio._env:env.py:310 GDAL data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 PROJ data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 Started GDALEnv <rasterio._env.GDALEnv object at 0x7f7b59788bb0>.
    DEBUG    rasterio._env:env.py:346 Stopped GDALEnv <rasterio._env.GDALEnv object at 0x7f7b59788bb0>.
    ______________________________ test_shapes_indent ______________________________

    runner = <click.testing.CliRunner object at 0x7f7b59186f10>
    pixelated_image_file = '/tmp/pytest-of-mockbuild/pytest-0/test_shapes_indent0/pixelated_image.tif'

        def test_shapes_indent(runner, pixelated_image_file):
            """
            --indent option should produce lots of newlines and contiguous spaces
            """
            with pytest.warns(None):
        
                result = runner.invoke(
                    main_group, ['shapes', '--collection', pixelated_image_file, '--indent', 2])
        
                assert result.exit_code == 0
                assert result.output.count('"FeatureCollection"') == 1
                assert result.output.count('"Feature"') == 4
    >           assert result.output.count('\n') == 231
    E           assert 227 == 231
    E             +227
    E             -231

    tests/test_rio_shapes.py:100: AssertionError
    ------------------------------ Captured log setup ------------------------------
    DEBUG    rasterio._env:env.py:310 GDAL data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 PROJ data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 Started GDALEnv <rasterio._env.GDALEnv object at 0x7f7b59302cd0>.
    DEBUG    rasterio._env:env.py:346 Stopped GDALEnv <rasterio._env.GDALEnv object at 0x7f7b59302cd0>.
    DEBUG    rasterio._env:env.py:310 GDAL data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 PROJ data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 Started GDALEnv <rasterio._env.GDALEnv object at 0x7f7b58ed49d0>.
    DEBUG    rasterio._env:env.py:346 Stopped GDALEnv <rasterio._env.GDALEnv object at 0x7f7b58ed49d0>.
    ------------------------------ Captured log call -------------------------------
    DEBUG    rasterio._env:env.py:310 GDAL data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 PROJ data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 Started GDALEnv <rasterio._env.GDALEnv object at 0x7f7b590c6460>.
    DEBUG    rasterio._env:env.py:346 Stopped GDALEnv <rasterio._env.GDALEnv object at 0x7f7b590c6460>.
    DEBUG    rasterio._env:env.py:310 GDAL data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 PROJ data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 Started GDALEnv <rasterio._env.GDALEnv object at 0x7f7b590c6310>.
    DEBUG    rasterio._env:env.py:346 Stopped GDALEnv <rasterio._env.GDALEnv object at 0x7f7b590c6310>.
    DEBUG    rasterio._env:env.py:310 GDAL data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 PROJ data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 Started GDALEnv <rasterio._env.GDALEnv object at 0x7f7b58ed43d0>.
    DEBUG    rasterio._env:env.py:346 Stopped GDALEnv <rasterio._env.GDALEnv object at 0x7f7b58ed43d0>.
    _______________________________ test_shapes_mask _______________________________

    runner = <click.testing.CliRunner object at 0x7f7b58964130>
    pixelated_image = array([[255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
        [255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
    ... 255, 255,   0,   0,   0,   0,   0, 255, 255],
        [255, 255, 255,   0,   0,   0,   0,   0, 255, 255]], dtype=uint8)
    pixelated_image_file = '/tmp/pytest-of-mockbuild/pytest-0/test_shapes_mask0/pixelated_image.tif'

        def test_shapes_mask(runner, pixelated_image, pixelated_image_file):
            """ --mask should extract the nodata area of the image """
        
            pixelated_image[0:5, 0:10] = 255
            pixelated_image[0:10, 0:3] = 255
            pixelated_image[8:10, 8:10] = 255
        
            with rasterio.open(pixelated_image_file, 'r+') as out:
                out.write(pixelated_image, indexes=1)
        
            with pytest.warns(None):
                result = runner.invoke(
                    main_group, ['shapes', '--collection', pixelated_image_file, '--mask'])
                assert result.exit_code == 0
                assert result.output.count('"FeatureCollection"') == 1
                assert result.output.count('"Feature"') == 1
    >           assert np.allclose(
                    json.loads(result.output)['features'][0]['geometry']['coordinates'],
                    [[[3, 5], [3, 10], [8, 10], [8, 8], [9, 8], [10, 8], [10, 5], [3, 5]]])

    tests/test_rio_shapes.py:155: 
    _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
    <__array_function__ internals>:5: in allclose
        ???
    /usr/lib64/python3.9/site-packages/numpy/core/numeric.py:2189: in allclose
        res = all(isclose(a, b, rtol=rtol, atol=atol, equal_nan=equal_nan))
    <__array_function__ internals>:5: in isclose
        ???
    /usr/lib64/python3.9/site-packages/numpy/core/numeric.py:2290: in isclose
        return within_tol(x, y, atol, rtol)
    _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

    x = array([[[ 3.,  5.],
            [ 3., 10.],
            [ 8., 10.],
            [ 8.,  8.],
            [10.,  8.],
            [10.,  5.],
            [ 3.,  5.]]])
    y = array([[[ 3.,  5.],
            [ 3., 10.],
            [ 8., 10.],
            [ 8.,  8.],
            [ 9.,  8.],
            [10.,  8.],
            [10.,  5.],
            [ 3.,  5.]]])
    atol = 1e-08, rtol = 1e-05

        def within_tol(x, y, atol, rtol):
            with errstate(invalid='ignore'):
    >           return less_equal(abs(x-y), atol + rtol * abs(y))
    E           ValueError: operands could not be broadcast together with shapes (1,7,2) (1,8,2)

    /usr/lib64/python3.9/site-packages/numpy/core/numeric.py:2276: ValueError
    ------------------------------ Captured log setup ------------------------------
    DEBUG    rasterio._env:env.py:310 GDAL data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 PROJ data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 Started GDALEnv <rasterio._env.GDALEnv object at 0x7f7b59302850>.
    DEBUG    rasterio._env:env.py:346 Stopped GDALEnv <rasterio._env.GDALEnv object at 0x7f7b59302850>.
    DEBUG    rasterio._env:env.py:310 GDAL data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 PROJ data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 Started GDALEnv <rasterio._env.GDALEnv object at 0x7f7b59302d30>.
    DEBUG    rasterio._env:env.py:346 Stopped GDALEnv <rasterio._env.GDALEnv object at 0x7f7b59302d30>.
    ------------------------------ Captured log call -------------------------------
    DEBUG    rasterio._env:env.py:310 GDAL data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 PROJ data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 Started GDALEnv <rasterio._env.GDALEnv object at 0x7f7b59302af0>.
    DEBUG    rasterio._env:env.py:346 Stopped GDALEnv <rasterio._env.GDALEnv object at 0x7f7b59302af0>.
    DEBUG    rasterio._env:env.py:310 GDAL data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 PROJ data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 Started GDALEnv <rasterio._env.GDALEnv object at 0x7f7b59302940>.
    DEBUG    rasterio._env:env.py:346 Stopped GDALEnv <rasterio._env.GDALEnv object at 0x7f7b59302940>.
    DEBUG    rasterio._env:env.py:310 GDAL data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 PROJ data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 Started GDALEnv <rasterio._env.GDALEnv object at 0x7f7b58e0b0d0>.
    DEBUG    rasterio._env:env.py:346 Stopped GDALEnv <rasterio._env.GDALEnv object at 0x7f7b58e0b0d0>.
    DEBUG    rasterio._env:env.py:310 GDAL data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 PROJ data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 Started GDALEnv <rasterio._env.GDALEnv object at 0x7f7b590c6070>.
    DEBUG    rasterio._env:env.py:346 Stopped GDALEnv <rasterio._env.GDALEnv object at 0x7f7b590c6070>.
    DEBUG    rasterio._env:env.py:310 GDAL data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 PROJ data files are available at built-in paths
    DEBUG    rasterio._env:env.py:310 Started GDALEnv <rasterio._env.GDALEnv object at 0x7f7b58955460>.
    DEBUG    rasterio._env:env.py:346 Stopped GDALEnv <rasterio._env.GDALEnv object at 0x7f7b58955460>.
    =============================== warnings summary ===============================
    tests/test__crs.py: 3 warnings
    tests/test_crs.py: 7 warnings
    tests/test_rio_warp.py: 1 warning
    tests/test_warp.py: 1 warning
    /usr/lib/python3.9/site-packages/_pytest/skipping.py:129: DeprecationWarning: NotImplemented should not be used in a boolean context
        result = bool(condition)

    tests/test_read.py::ReaderContextTest::test_read_window
    tests/test_read.py::ReaderContextTest::test_read_window
    tests/test_read.py::ReaderContextTest::test_read_window
    /builddir/build/BUILD/rasterio-1.1.8/tests/test_read.py:178: DeprecationWarning: tostring() is deprecated. Use tobytes() instead.
        self.assertEqual([md5(x.tostring()).hexdigest() for x in a],

    tests/test_read.py::ReaderContextTest::test_read_window
    tests/test_read.py::ReaderContextTest::test_read_window
    tests/test_read.py::ReaderContextTest::test_read_window
    /builddir/build/BUILD/rasterio-1.1.8/tests/test_read.py:187: DeprecationWarning: tostring() is deprecated. Use tobytes() instead.
        self.assertEqual([md5(x.tostring()).hexdigest() for x in a[:]],

    tests/test_rio_blocks.py::test_windows
    tests/test_rio_blocks.py::test_windows_sequence
    tests/test_rio_blocks.py::test_windows_precision
    tests/test_rio_blocks.py::test_windows_indent
    tests/test_rio_blocks.py::test_windows_compact
    tests/test_rio_blocks.py::test_windows_exception
    tests/test_rio_blocks.py::test_windows_projected
    /usr/lib/python3.9/site-packages/cligj/__init__.py:111: FutureWarning: Sequences of Features, not FeatureCollections, will be the default in version 1.0.0
        callback=lambda ctx, param, value: warn(

    -- Docs: https://docs.pytest.org/en/stable/warnings.html
    =========================== short test summary info ============================


    FAILED tests/test_features.py::test_shapes - AssertionError: assert {'coordin...
    FAILED tests/test_rio_shapes.py::test_shapes_indent - assert 227 == 231
    FAILED tests/test_rio_shapes.py::test_shapes_mask - ValueError: operands coul...
    = 3 failed, 1764 passed, 26 skipped, 21 deselected, 11 xfailed, 3 xpassed, 25 warnings in 55.44s =
    error: Bad exit status from /var/tmp/rpm-tmp.snIoQt (%check)
        Bad exit status from /var/tmp/rpm-tmp.snIoQt (%check)


Discussion:
@manisandro yes, GDAL changed its polygonizer algorithm and now polygon rings are wound in the reverse direction. We made rasterio's test assertions too brittle and now they've broken.
Thanks for the quick reply! Is it safe to ignore these test failures then?
I didn't see the change in polygonizer in the GDAL changelog, only [note about dummy geotransform](https://github.com/OSGeo/gdal/blob/v3.2.0/gdal/NEWS#L139)

The set of coordinates is not the same (last failure):
```
x = array([[[ 3.,  5.],
        [ 3., 10.],
        [ 8., 10.],
        [ 8.,  8.],
        [10.,  8.],
        [10.,  5.],
        [ 3.,  5.]]])
y = array([[[ 3.,  5.],
        [ 3., 10.],
        [ 8., 10.],
        [ 8.,  8.],
        [ 9.,  8.],
        [10.,  8.],
        [10.,  5.],
        [ 3.,  5.]]])
```

Note vertex `[9., 8.]` in `y`.  Provided I'm seeing this correctly, this concerns me more than coordinate order.



The vertex order does not seem consistently changed (first failure):
```
# expected
{'coordinates': [[(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)], [(2, 2), (5, 2), (5, 5), (2, 5), (2, 2)]]}

# got
{'coordinates': [[(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)], [(2, 2), (2, 5), (5, 5), (5, 2), (2, 2)]]}
```

Perhaps the first polygon was already ordered properly according to the new algorithm (increasing x coordinate)?

The fix for vertex order may require introducing a test helper function to normalize vertex order prior to comparison - so long as it is about order rather than also needing to rotate the polygons to a different starting vertex.

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -369,11 +369,12 @@ def run(self):
     "plot": ["matplotlib"],
     "s3": ["boto3>=1.2.4"],
     "test": [
-        "pytest>=2.8.2",
-        "pytest-cov>=2.2.0",
         "boto3>=1.2.4",
-        "packaging",
         "hypothesis",
+        "packaging",
+        "pytest-cov>=2.2.0",
+        "pytest>=2.8.2",
+        "shapely",
         'futures;python_version<"3.2"',
         'mock;python_version<"3.2"',
     ],


Test patch:
diff --git a/tests/test__env.py b/tests/test__env.py
--- a/tests/test__env.py
+++ b/tests/test__env.py
@@ -29,14 +29,11 @@ def mock_fhs(tmpdir):
 @pytest.fixture
 def mock_debian(tmpdir):
     """A fake Debian multi-install system"""
-    tmpdir.ensure("share/gdal/1.11/header.dxf")
-    tmpdir.ensure("share/gdal/2.0/header.dxf")
-    tmpdir.ensure("share/gdal/2.1/header.dxf")
-    tmpdir.ensure("share/gdal/2.2/header.dxf")
     tmpdir.ensure("share/gdal/2.3/header.dxf")
     tmpdir.ensure("share/gdal/2.4/header.dxf")
     tmpdir.ensure("share/gdal/3.0/header.dxf")
     tmpdir.ensure("share/gdal/3.1/header.dxf")
+    tmpdir.ensure("share/gdal/3.2/header.dxf")
     tmpdir.ensure("share/proj/epsg")
     return tmpdir
 
diff --git a/tests/test_features.py b/tests/test_features.py
--- a/tests/test_features.py
+++ b/tests/test_features.py
@@ -2,9 +2,10 @@
 import logging
 import sys
 
+from affine import Affine
 import numpy as np
 import pytest
-from affine import Affine
+import shapely.geometry
 
 import rasterio
 from rasterio.enums import MergeAlg
@@ -821,13 +822,7 @@ def test_shapes(basic_image):
     assert value == 1
 
     shape, value = results[1]
-    assert shape == {
-        'coordinates': [
-            [(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)],
-            [(2, 2), (5, 2), (5, 5), (2, 5), (2, 2)]
-        ],
-        'type': 'Polygon'
-    }
+    assert shapely.geometry.shape(shape).area == 91.0
     assert value == 0
 
 
diff --git a/tests/test_rio_shapes.py b/tests/test_rio_shapes.py
--- a/tests/test_rio_shapes.py
+++ b/tests/test_rio_shapes.py
@@ -6,6 +6,7 @@
 
 import numpy as np
 import pytest
+from shapely.geometry import shape
 
 import rasterio
 from rasterio.rio.main import main_group
@@ -97,8 +98,8 @@ def test_shapes_indent(runner, pixelated_image_file):
         assert result.exit_code == 0
         assert result.output.count('"FeatureCollection"') == 1
         assert result.output.count('"Feature"') == 4
-        assert result.output.count('\n') == 231
-        assert result.output.count('        ') == 180
+        assert result.output.count("\n") > 100
+        assert result.output.count("        ") > 100
 
 
 def test_shapes_compact(runner, pixelated_image_file):
@@ -152,9 +153,7 @@ def test_shapes_mask(runner, pixelated_image, pixelated_image_file):
         assert result.exit_code == 0
         assert result.output.count('"FeatureCollection"') == 1
         assert result.output.count('"Feature"') == 1
-        assert np.allclose(
-            json.loads(result.output)['features'][0]['geometry']['coordinates'],
-            [[[3, 5], [3, 10], [8, 10], [8, 8], [9, 8], [10, 8], [10, 5], [3, 5]]])
+        assert shape(json.loads(result.output)["features"][0]["geometry"]).area == 31.0
 
 
 def test_shapes_mask_sampling(runner, pixelated_image, pixelated_image_file):
@@ -177,10 +176,7 @@ def test_shapes_mask_sampling(runner, pixelated_image, pixelated_image_file):
         assert result.exit_code == 0
         assert result.output.count('"FeatureCollection"') == 1
         assert result.output.count('"Feature"') == 1
-
-        assert np.allclose(
-            json.loads(result.output)['features'][0]['geometry']['coordinates'],
-            [[[5, 5], [5, 10], [10, 10], [10, 5], [5, 5]]])
+        assert shape(json.loads(result.output)["features"][0]["geometry"]).area == 25.0
 
 
 def test_shapes_band1_as_mask(runner, pixelated_image, pixelated_image_file):
@@ -202,6 +198,4 @@ def test_shapes_band1_as_mask(runner, pixelated_image, pixelated_image_file):
         assert result.exit_code == 0
         assert result.output.count('"FeatureCollection"') == 1
         assert result.output.count('"Feature"') == 3
-        assert np.allclose(
-            json.loads(result.output)['features'][1]['geometry']['coordinates'],
-            [[[2, 2], [2, 5], [5, 5], [5, 2], [2, 2]]])
+        assert shape(json.loads(result.output)["features"][0]["geometry"]).area == 1.0