From 45bbb5352deac75755a8256ac418a66a438b901a Mon Sep 17 00:00:00 2001 From: OSGeo Weblate <97247866+osgeoweblate@users.noreply.github.com> Date: Sat, 28 Mar 2026 07:25:45 -0400 Subject: [PATCH 1/5] temporal: add unit tests for AbstractSpaceTimeDataset shift() and snap() Introduces unittests_temporal_stds_management.py with dedicated coverage for core management methods that had no existing tests. - test_shift_absolute_time: timestamps advance by the given granularity - test_shift_invalid_granularity: regression test for #7228, ensures invalid input returns False rather than a formatting TypeError - test_snap_closes_gaps: end times are snapped to next map's start time test_shift_relative_time is commented out because relative time unit type resolution fails in GRASS 7.8.7 with 'unsupported unit type: None'. Needs verification against GRASS 8.x once #7231 is merged. Related: #7228 (fix string formatting in fatal error messages) #7231 (add explicit return True in shift()) --- .../unittests_temporal_stds_management.py | 230 ++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 python/grass/temporal/testsuite/unittests_temporal_stds_management.py diff --git a/python/grass/temporal/testsuite/unittests_temporal_stds_management.py b/python/grass/temporal/testsuite/unittests_temporal_stds_management.py new file mode 100644 index 00000000000..0f7b0a97933 --- /dev/null +++ b/python/grass/temporal/testsuite/unittests_temporal_stds_management.py @@ -0,0 +1,230 @@ +"""Unit tests for shift() and snap() methods +of AbstractSpaceTimeDataset + +(C) 2026 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 +for details. + +:authors: Selma Bentaiba +""" + +import datetime +import os +import uuid + +from grass.gunittest.case import TestCase +from grass.gunittest.main import test + +import grass.temporal as tgis + + +class TestShift(TestCase): + @classmethod + def setUpClass(cls) -> None: + os.putenv("GRASS_OVERWRITE", "1") + cls.runModule("g.gisenv", set="TGIS_USE_CURRENT_MAPSET=1") + tgis.init() + cls.use_temp_region() + cls.runModule( + "g.region", n=80.0, s=0.0, e=120.0, w=0.0, t=1.0, b=0.0, res=10.0 + ) + + @classmethod + def tearDownClass(cls) -> None: + cls.del_temp_region() + + def setUp(self) -> None: + uid = uuid.uuid4().hex[:6] + self.map1 = f"map_1_{uid}" + self.map2 = f"map_2_{uid}" + self.strds_name = f"shift_test_{uid}" + self.runModule( + "r.mapcalc", overwrite=True, quiet=True, + expression=f"{self.map1} = 1" + ) + self.runModule( + "r.mapcalc", overwrite=True, quiet=True, + expression=f"{self.map2} = 2" + ) + self.strds = tgis.open_new_stds( + name=self.strds_name, + type="strds", + temporaltype="absolute", + title="Test shift", + descr="Test shift", + semantic="field", + overwrite=True, + ) + tgis.register_maps_in_space_time_dataset( + type="raster", + name=self.strds.get_name(), + maps=f"{self.map1},{self.map2}", + start="2001-01-01", + increment="1 day", + interval=True, + ) + + def tearDown(self) -> None: + self.runModule( + "t.unregister", + type="raster", + maps=f"{self.map1},{self.map2}", + quiet=True, + ) + self.runModule( + "g.remove", flags="f", type="raster", + name=f"{self.map1},{self.map2}", quiet=True + ) + self.strds.delete() + + def test_shift_absolute_time(self) -> None: + """shift() moves all map timestamps forward by the given granularity""" + self.strds.shift(gran="1 days") + + maps = self.strds.get_registered_maps_as_objects( + where=None, order="start_time" + ) + start, end = maps[0].get_temporal_extent_as_tuple() + self.assertEqual(start, datetime.datetime(2001, 1, 2)) + self.assertEqual(end, datetime.datetime(2001, 1, 3)) + + start, end = maps[1].get_temporal_extent_as_tuple() + self.assertEqual(start, datetime.datetime(2001, 1, 3)) + self.assertEqual(end, datetime.datetime(2001, 1, 4)) + + def test_shift_invalid_granularity(self) -> None: + """shift() returns False for invalid granularity string""" + result = self.strds.shift(gran="invalid") + self.assertFalse(result) + +# TODO: test_shift_relative_time is disabled pending GRASS 8.x testing. + # The registration of relative time maps with unit="seconds" fails in + # GRASS 7.8.7 with "Unsupported relative time unit type: None". + # This should be re-enabled and verified once tested against GRASS 8.x. + # Related to #7231. + # + # def test_shift_relative_time(self) -> None: + # """shift() moves timestamps forward for relative time datasets""" + # uid = uuid.uuid4().hex[:6] + # map_rel_1 = f"map_rel_1_{uid}" + # map_rel_2 = f"map_rel_2_{uid}" + # strds_rel_name = f"shift_test_rel_{uid}" + # self.runModule( + # "r.mapcalc", overwrite=True, quiet=True, + # expression=f"{map_rel_1} = 1" + # ) + # self.runModule( + # "r.mapcalc", overwrite=True, quiet=True, + # expression=f"{map_rel_2} = 2" + # ) + # strds_rel = tgis.open_new_stds( + # name=strds_rel_name, + # type="strds", + # temporaltype="relative", + # title="Test shift relative", + # descr="Test shift relative", + # semantic="field", + # overwrite=True, + # ) + # tgis.register_maps_in_space_time_dataset( + # type="raster", + # name=strds_rel.get_name(), + # maps=f"{map_rel_1},{map_rel_2}", + # start=0, + # increment=1, + # unit="seconds", + # interval=True, + # ) + # strds_rel.shift(gran="5") + # maps = strds_rel.get_registered_maps_as_objects( + # where=None, order="start_time" + # ) + # start, end = maps[0].get_temporal_extent_as_tuple() + # self.assertEqual(start, 5) + # self.assertEqual(end, 6) + # self.runModule( + # "t.unregister", + # type="raster", + # maps=f"{map_rel_1},{map_rel_2}", + # quiet=True, + # ) + # strds_rel.delete() + # self.runModule( + # "g.remove", flags="f", type="raster", + # name=f"{map_rel_1},{map_rel_2}", quiet=True + # ) + +class TestSnap(TestCase): + @classmethod + def setUpClass(cls) -> None: + os.putenv("GRASS_OVERWRITE", "1") + cls.runModule("g.gisenv", set="TGIS_USE_CURRENT_MAPSET=1") + tgis.init() + cls.use_temp_region() + cls.runModule( + "g.region", n=80.0, s=0.0, e=120.0, w=0.0, t=1.0, b=0.0, res=10.0 + ) + + @classmethod + def tearDownClass(cls) -> None: + cls.del_temp_region() + + def setUp(self) -> None: + uid = uuid.uuid4().hex[:6] + self.map1 = f"map_1_{uid}" + self.map2 = f"map_2_{uid}" + self.strds_name = f"snap_test_{uid}" + self.runModule( + "r.mapcalc", overwrite=True, quiet=True, + expression=f"{self.map1} = 1" + ) + self.runModule( + "r.mapcalc", overwrite=True, quiet=True, + expression=f"{self.map2} = 2" + ) + self.strds = tgis.open_new_stds( + name=self.strds_name, + type="strds", + temporaltype="absolute", + title="Test snap", + descr="Test snap", + semantic="field", + overwrite=True, + ) + tgis.register_maps_in_space_time_dataset( + type="raster", + name=self.strds.get_name(), + maps=f"{self.map1},{self.map2}", + start="2001-01-01", + increment="2 days", + interval=False, + ) + + def tearDown(self) -> None: + self.runModule( + "t.unregister", + type="raster", + maps=f"{self.map1},{self.map2}", + quiet=True, + ) + self.runModule( + "g.remove", flags="f", type="raster", + name=f"{self.map1},{self.map2}", quiet=True + ) + self.strds.delete() + + def test_snap_closes_gaps(self) -> None: + """snap() sets end time of each map to start time of next neighbor""" + self.strds.snap() + + maps = self.strds.get_registered_maps_as_objects( + where=None, order="start_time" + ) + start, end = maps[0].get_temporal_extent_as_tuple() + self.assertEqual(start, datetime.datetime(2001, 1, 1)) + self.assertEqual(end, datetime.datetime(2001, 1, 3)) + + +if __name__ == "__main__": + test() From 9fc4c8c86a871e8022e7bb84d2a7645954ba0c08 Mon Sep 17 00:00:00 2001 From: selma-Bentaiba Date: Sat, 28 Mar 2026 22:59:44 +0100 Subject: [PATCH 2/5] Finalize tests: fix discovery, add SPDX, and remove dead code --- .../test_temporal_stds_management.py | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 python/grass/temporal/testsuite/test_temporal_stds_management.py diff --git a/python/grass/temporal/testsuite/test_temporal_stds_management.py b/python/grass/temporal/testsuite/test_temporal_stds_management.py new file mode 100644 index 00000000000..d77c81d549d --- /dev/null +++ b/python/grass/temporal/testsuite/test_temporal_stds_management.py @@ -0,0 +1,167 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +""" +Unit tests for shift() and snap() methods of AbstractSpaceTimeDataset +(C) 2026 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 +for details. + +:authors: Selma Bentaiba +""" + +import datetime +import os +import uuid + +from grass.gunittest.case import TestCase +from grass.gunittest.main import GrassTestProgram +import grass.temporal as tgis + + +class TestShift(TestCase): + @classmethod + def setUpClass(cls) -> None: + os.putenv("GRASS_OVERWRITE", "1") + cls.runModule("g.gisenv", set="TGIS_USE_CURRENT_MAPSET=1") + tgis.init() + cls.use_temp_region() + cls.runModule("g.region", n=80.0, s=0.0, e=120.0, w=0.0, t=1.0, b=0.0, res=10.0) + + @classmethod + def tearDownClass(cls) -> None: + cls.del_temp_region() + + def setUp(self) -> None: + uid = uuid.uuid4().hex[:6] + self.map1 = f"map_1_{uid}" + self.map2 = f"map_2_{uid}" + self.strds_name = f"shift_test_{uid}" + self.runModule( + "r.mapcalc", overwrite=True, quiet=True, expression=f"{self.map1} = 1" + ) + self.runModule( + "r.mapcalc", overwrite=True, quiet=True, expression=f"{self.map2} = 2" + ) + self.strds = tgis.open_new_stds( + name=self.strds_name, + type="strds", + temporaltype="absolute", + title="Test shift", + descr="Test shift", + semantic="field", + overwrite=True, + ) + tgis.register_maps_in_space_time_dataset( + type="raster", + name=self.strds.get_name(), + maps=f"{self.map1},{self.map2}", + start="2001-01-01", + increment="1 day", + interval=True, + ) + + def tearDown(self) -> None: + self.runModule( + "t.unregister", + type="raster", + maps=f"{self.map1},{self.map2}", + quiet=True, + ) + self.runModule( + "g.remove", + flags="f", + type="raster", + name=f"{self.map1},{self.map2}", + quiet=True, + ) + self.strds.delete() + + def test_shift_absolute_time(self) -> None: + """shift() moves all map timestamps forward by the given granularity""" + self.strds.shift(gran="1 days") + + maps = self.strds.get_registered_maps_as_objects(where=None, order="start_time") + start, end = maps[0].get_temporal_extent_as_tuple() + self.assertEqual(start, datetime.datetime(2001, 1, 2)) + self.assertEqual(end, datetime.datetime(2001, 1, 3)) + + start, end = maps[1].get_temporal_extent_as_tuple() + self.assertEqual(start, datetime.datetime(2001, 1, 3)) + self.assertEqual(end, datetime.datetime(2001, 1, 4)) + + def test_shift_invalid_granularity(self) -> None: + """shift() returns False for invalid granularity string""" + result = self.strds.shift(gran="invalid") + self.assertFalse(result) + + +class TestSnap(TestCase): + @classmethod + def setUpClass(cls) -> None: + os.putenv("GRASS_OVERWRITE", "1") + cls.runModule("g.gisenv", set="TGIS_USE_CURRENT_MAPSET=1") + tgis.init() + cls.use_temp_region() + cls.runModule("g.region", n=80.0, s=0.0, e=120.0, w=0.0, t=1.0, b=0.0, res=10.0) + + @classmethod + def tearDownClass(cls) -> None: + cls.del_temp_region() + + def setUp(self) -> None: + uid = uuid.uuid4().hex[:6] + self.map1 = f"map_1_{uid}" + self.map2 = f"map_2_{uid}" + self.strds_name = f"snap_test_{uid}" + self.runModule( + "r.mapcalc", overwrite=True, quiet=True, expression=f"{self.map1} = 1" + ) + self.runModule( + "r.mapcalc", overwrite=True, quiet=True, expression=f"{self.map2} = 2" + ) + self.strds = tgis.open_new_stds( + name=self.strds_name, + type="strds", + temporaltype="absolute", + title="Test snap", + descr="Test snap", + semantic="field", + overwrite=True, + ) + tgis.register_maps_in_space_time_dataset( + type="raster", + name=self.strds.get_name(), + maps=f"{self.map1},{self.map2}", + start="2001-01-01", + increment="2 days", + interval=False, + ) + + def tearDown(self) -> None: + self.runModule( + "t.unregister", + type="raster", + maps=f"{self.map1},{self.map2}", + quiet=True, + ) + self.runModule( + "g.remove", + flags="f", + type="raster", + name=f"{self.map1},{self.map2}", + quiet=True, + ) + self.strds.delete() + + def test_snap_closes_gaps(self) -> None: + """snap() sets end time of each map to start time of next neighbor""" + self.strds.snap() + + maps = self.strds.get_registered_maps_as_objects(where=None, order="start_time") + start, end = maps[0].get_temporal_extent_as_tuple() + self.assertEqual(start, datetime.datetime(2001, 1, 1)) + self.assertEqual(end, datetime.datetime(2001, 1, 3)) + + +if __name__ == "__main__": + GrassTestProgram(module="__main__", exit_at_end=False, grass_location="all") From 6244a6f183df48b4ad77a706754095b2b9d695fc Mon Sep 17 00:00:00 2001 From: selma-Bentaiba Date: Sat, 28 Mar 2026 23:27:14 +0100 Subject: [PATCH 3/5] Fix file naming: remove old unittests_ prefix and use test_ for discovery --- .../unittests_temporal_stds_management.py | 230 ------------------ 1 file changed, 230 deletions(-) delete mode 100644 python/grass/temporal/testsuite/unittests_temporal_stds_management.py diff --git a/python/grass/temporal/testsuite/unittests_temporal_stds_management.py b/python/grass/temporal/testsuite/unittests_temporal_stds_management.py deleted file mode 100644 index 0f7b0a97933..00000000000 --- a/python/grass/temporal/testsuite/unittests_temporal_stds_management.py +++ /dev/null @@ -1,230 +0,0 @@ -"""Unit tests for shift() and snap() methods -of AbstractSpaceTimeDataset - -(C) 2026 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 -for details. - -:authors: Selma Bentaiba -""" - -import datetime -import os -import uuid - -from grass.gunittest.case import TestCase -from grass.gunittest.main import test - -import grass.temporal as tgis - - -class TestShift(TestCase): - @classmethod - def setUpClass(cls) -> None: - os.putenv("GRASS_OVERWRITE", "1") - cls.runModule("g.gisenv", set="TGIS_USE_CURRENT_MAPSET=1") - tgis.init() - cls.use_temp_region() - cls.runModule( - "g.region", n=80.0, s=0.0, e=120.0, w=0.0, t=1.0, b=0.0, res=10.0 - ) - - @classmethod - def tearDownClass(cls) -> None: - cls.del_temp_region() - - def setUp(self) -> None: - uid = uuid.uuid4().hex[:6] - self.map1 = f"map_1_{uid}" - self.map2 = f"map_2_{uid}" - self.strds_name = f"shift_test_{uid}" - self.runModule( - "r.mapcalc", overwrite=True, quiet=True, - expression=f"{self.map1} = 1" - ) - self.runModule( - "r.mapcalc", overwrite=True, quiet=True, - expression=f"{self.map2} = 2" - ) - self.strds = tgis.open_new_stds( - name=self.strds_name, - type="strds", - temporaltype="absolute", - title="Test shift", - descr="Test shift", - semantic="field", - overwrite=True, - ) - tgis.register_maps_in_space_time_dataset( - type="raster", - name=self.strds.get_name(), - maps=f"{self.map1},{self.map2}", - start="2001-01-01", - increment="1 day", - interval=True, - ) - - def tearDown(self) -> None: - self.runModule( - "t.unregister", - type="raster", - maps=f"{self.map1},{self.map2}", - quiet=True, - ) - self.runModule( - "g.remove", flags="f", type="raster", - name=f"{self.map1},{self.map2}", quiet=True - ) - self.strds.delete() - - def test_shift_absolute_time(self) -> None: - """shift() moves all map timestamps forward by the given granularity""" - self.strds.shift(gran="1 days") - - maps = self.strds.get_registered_maps_as_objects( - where=None, order="start_time" - ) - start, end = maps[0].get_temporal_extent_as_tuple() - self.assertEqual(start, datetime.datetime(2001, 1, 2)) - self.assertEqual(end, datetime.datetime(2001, 1, 3)) - - start, end = maps[1].get_temporal_extent_as_tuple() - self.assertEqual(start, datetime.datetime(2001, 1, 3)) - self.assertEqual(end, datetime.datetime(2001, 1, 4)) - - def test_shift_invalid_granularity(self) -> None: - """shift() returns False for invalid granularity string""" - result = self.strds.shift(gran="invalid") - self.assertFalse(result) - -# TODO: test_shift_relative_time is disabled pending GRASS 8.x testing. - # The registration of relative time maps with unit="seconds" fails in - # GRASS 7.8.7 with "Unsupported relative time unit type: None". - # This should be re-enabled and verified once tested against GRASS 8.x. - # Related to #7231. - # - # def test_shift_relative_time(self) -> None: - # """shift() moves timestamps forward for relative time datasets""" - # uid = uuid.uuid4().hex[:6] - # map_rel_1 = f"map_rel_1_{uid}" - # map_rel_2 = f"map_rel_2_{uid}" - # strds_rel_name = f"shift_test_rel_{uid}" - # self.runModule( - # "r.mapcalc", overwrite=True, quiet=True, - # expression=f"{map_rel_1} = 1" - # ) - # self.runModule( - # "r.mapcalc", overwrite=True, quiet=True, - # expression=f"{map_rel_2} = 2" - # ) - # strds_rel = tgis.open_new_stds( - # name=strds_rel_name, - # type="strds", - # temporaltype="relative", - # title="Test shift relative", - # descr="Test shift relative", - # semantic="field", - # overwrite=True, - # ) - # tgis.register_maps_in_space_time_dataset( - # type="raster", - # name=strds_rel.get_name(), - # maps=f"{map_rel_1},{map_rel_2}", - # start=0, - # increment=1, - # unit="seconds", - # interval=True, - # ) - # strds_rel.shift(gran="5") - # maps = strds_rel.get_registered_maps_as_objects( - # where=None, order="start_time" - # ) - # start, end = maps[0].get_temporal_extent_as_tuple() - # self.assertEqual(start, 5) - # self.assertEqual(end, 6) - # self.runModule( - # "t.unregister", - # type="raster", - # maps=f"{map_rel_1},{map_rel_2}", - # quiet=True, - # ) - # strds_rel.delete() - # self.runModule( - # "g.remove", flags="f", type="raster", - # name=f"{map_rel_1},{map_rel_2}", quiet=True - # ) - -class TestSnap(TestCase): - @classmethod - def setUpClass(cls) -> None: - os.putenv("GRASS_OVERWRITE", "1") - cls.runModule("g.gisenv", set="TGIS_USE_CURRENT_MAPSET=1") - tgis.init() - cls.use_temp_region() - cls.runModule( - "g.region", n=80.0, s=0.0, e=120.0, w=0.0, t=1.0, b=0.0, res=10.0 - ) - - @classmethod - def tearDownClass(cls) -> None: - cls.del_temp_region() - - def setUp(self) -> None: - uid = uuid.uuid4().hex[:6] - self.map1 = f"map_1_{uid}" - self.map2 = f"map_2_{uid}" - self.strds_name = f"snap_test_{uid}" - self.runModule( - "r.mapcalc", overwrite=True, quiet=True, - expression=f"{self.map1} = 1" - ) - self.runModule( - "r.mapcalc", overwrite=True, quiet=True, - expression=f"{self.map2} = 2" - ) - self.strds = tgis.open_new_stds( - name=self.strds_name, - type="strds", - temporaltype="absolute", - title="Test snap", - descr="Test snap", - semantic="field", - overwrite=True, - ) - tgis.register_maps_in_space_time_dataset( - type="raster", - name=self.strds.get_name(), - maps=f"{self.map1},{self.map2}", - start="2001-01-01", - increment="2 days", - interval=False, - ) - - def tearDown(self) -> None: - self.runModule( - "t.unregister", - type="raster", - maps=f"{self.map1},{self.map2}", - quiet=True, - ) - self.runModule( - "g.remove", flags="f", type="raster", - name=f"{self.map1},{self.map2}", quiet=True - ) - self.strds.delete() - - def test_snap_closes_gaps(self) -> None: - """snap() sets end time of each map to start time of next neighbor""" - self.strds.snap() - - maps = self.strds.get_registered_maps_as_objects( - where=None, order="start_time" - ) - start, end = maps[0].get_temporal_extent_as_tuple() - self.assertEqual(start, datetime.datetime(2001, 1, 1)) - self.assertEqual(end, datetime.datetime(2001, 1, 3)) - - -if __name__ == "__main__": - test() From 0468ddc675ec51ea2cb4a9c17f28dacd2c16b661 Mon Sep 17 00:00:00 2001 From: Anna Petrasova Date: Fri, 22 May 2026 23:09:51 -0400 Subject: [PATCH 4/5] rewrite to pytest and drop duplicated test --- ..._abstract_space_time_dataset_shift_snap.py | 90 ++++++++++ .../test_temporal_stds_management.py | 167 ------------------ 2 files changed, 90 insertions(+), 167 deletions(-) create mode 100644 python/grass/temporal/tests/test_abstract_space_time_dataset_shift_snap.py delete mode 100644 python/grass/temporal/testsuite/test_temporal_stds_management.py diff --git a/python/grass/temporal/tests/test_abstract_space_time_dataset_shift_snap.py b/python/grass/temporal/tests/test_abstract_space_time_dataset_shift_snap.py new file mode 100644 index 00000000000..4a87a9a54c9 --- /dev/null +++ b/python/grass/temporal/tests/test_abstract_space_time_dataset_shift_snap.py @@ -0,0 +1,90 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +"""Unit tests for AbstractSpaceTimeDataset.shift() and .snap().""" + +import datetime + +import pytest + +import grass.script as gs +import grass.temporal as tgis +from grass.tools import Tools + + +@pytest.fixture(scope="module") +def grass_session(tmp_path_factory): + """Active GRASS session with tgis initialized.""" + project = tmp_path_factory.mktemp("project") / "test" + gs.create_project(project) + with gs.setup.init(project) as session: + tgis.init() + Tools(session=session).g_region(s=0, n=80, w=0, e=120, res=10) + yield session + + +def _make_strds(session, name, *, interval, increment): + """Create an STRDS with two raster maps registered starting 2001-01-01.""" + map_1, map_2 = f"{name}_a", f"{name}_b" + tools = Tools(session=session) + tools.r_mapcalc(expression=f"{map_1} = 1") + tools.r_mapcalc(expression=f"{map_2} = 2") + strds = tgis.open_new_stds( + name=name, + type="strds", + temporaltype="absolute", + title="Test", + descr="Test", + semantic="field", + overwrite=True, + ) + tgis.register_maps_in_space_time_dataset( + type="raster", + name=strds.get_name(), + maps=f"{map_1},{map_2}", + start="2001-01-01", + increment=increment, + interval=interval, + ) + # register_maps_in_space_time_dataset updates the database row but not the + # in-memory object, so the granularity stays None until we re-read it. + strds.select() + return strds + + +@pytest.fixture +def interval_strds(grass_session, request): + """STRDS with two consecutive 1-day intervals starting 2001-01-01.""" + return _make_strds( + grass_session, request.node.name, interval=True, increment="1 day" + ) + + +@pytest.fixture +def point_strds(grass_session, request): + """STRDS with two point-in-time maps two days apart starting 2001-01-01.""" + return _make_strds( + grass_session, request.node.name, interval=False, increment="2 days" + ) + + +def test_shift_invalid_granularity(interval_strds): + """shift() returns False for invalid granularity (regression for #7228). + + The happy-path of shift() on absolute time is covered by + temporal/t.shift/testsuite/test_shift.py. + """ + assert interval_strds.shift(gran="invalid") is False + + +def test_snap_closes_gaps(point_strds): + """snap() sets each map's end time to its successor's start time, + and extends the last map by the dataset granularity.""" + point_strds.snap() + maps = point_strds.get_registered_maps_as_objects(order="start_time") + assert maps[0].get_temporal_extent_as_tuple() == ( + datetime.datetime(2001, 1, 1), + datetime.datetime(2001, 1, 3), + ) + assert maps[1].get_temporal_extent_as_tuple() == ( + datetime.datetime(2001, 1, 3), + datetime.datetime(2001, 1, 5), + ) diff --git a/python/grass/temporal/testsuite/test_temporal_stds_management.py b/python/grass/temporal/testsuite/test_temporal_stds_management.py deleted file mode 100644 index d77c81d549d..00000000000 --- a/python/grass/temporal/testsuite/test_temporal_stds_management.py +++ /dev/null @@ -1,167 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-or-later -""" -Unit tests for shift() and snap() methods of AbstractSpaceTimeDataset -(C) 2026 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 -for details. - -:authors: Selma Bentaiba -""" - -import datetime -import os -import uuid - -from grass.gunittest.case import TestCase -from grass.gunittest.main import GrassTestProgram -import grass.temporal as tgis - - -class TestShift(TestCase): - @classmethod - def setUpClass(cls) -> None: - os.putenv("GRASS_OVERWRITE", "1") - cls.runModule("g.gisenv", set="TGIS_USE_CURRENT_MAPSET=1") - tgis.init() - cls.use_temp_region() - cls.runModule("g.region", n=80.0, s=0.0, e=120.0, w=0.0, t=1.0, b=0.0, res=10.0) - - @classmethod - def tearDownClass(cls) -> None: - cls.del_temp_region() - - def setUp(self) -> None: - uid = uuid.uuid4().hex[:6] - self.map1 = f"map_1_{uid}" - self.map2 = f"map_2_{uid}" - self.strds_name = f"shift_test_{uid}" - self.runModule( - "r.mapcalc", overwrite=True, quiet=True, expression=f"{self.map1} = 1" - ) - self.runModule( - "r.mapcalc", overwrite=True, quiet=True, expression=f"{self.map2} = 2" - ) - self.strds = tgis.open_new_stds( - name=self.strds_name, - type="strds", - temporaltype="absolute", - title="Test shift", - descr="Test shift", - semantic="field", - overwrite=True, - ) - tgis.register_maps_in_space_time_dataset( - type="raster", - name=self.strds.get_name(), - maps=f"{self.map1},{self.map2}", - start="2001-01-01", - increment="1 day", - interval=True, - ) - - def tearDown(self) -> None: - self.runModule( - "t.unregister", - type="raster", - maps=f"{self.map1},{self.map2}", - quiet=True, - ) - self.runModule( - "g.remove", - flags="f", - type="raster", - name=f"{self.map1},{self.map2}", - quiet=True, - ) - self.strds.delete() - - def test_shift_absolute_time(self) -> None: - """shift() moves all map timestamps forward by the given granularity""" - self.strds.shift(gran="1 days") - - maps = self.strds.get_registered_maps_as_objects(where=None, order="start_time") - start, end = maps[0].get_temporal_extent_as_tuple() - self.assertEqual(start, datetime.datetime(2001, 1, 2)) - self.assertEqual(end, datetime.datetime(2001, 1, 3)) - - start, end = maps[1].get_temporal_extent_as_tuple() - self.assertEqual(start, datetime.datetime(2001, 1, 3)) - self.assertEqual(end, datetime.datetime(2001, 1, 4)) - - def test_shift_invalid_granularity(self) -> None: - """shift() returns False for invalid granularity string""" - result = self.strds.shift(gran="invalid") - self.assertFalse(result) - - -class TestSnap(TestCase): - @classmethod - def setUpClass(cls) -> None: - os.putenv("GRASS_OVERWRITE", "1") - cls.runModule("g.gisenv", set="TGIS_USE_CURRENT_MAPSET=1") - tgis.init() - cls.use_temp_region() - cls.runModule("g.region", n=80.0, s=0.0, e=120.0, w=0.0, t=1.0, b=0.0, res=10.0) - - @classmethod - def tearDownClass(cls) -> None: - cls.del_temp_region() - - def setUp(self) -> None: - uid = uuid.uuid4().hex[:6] - self.map1 = f"map_1_{uid}" - self.map2 = f"map_2_{uid}" - self.strds_name = f"snap_test_{uid}" - self.runModule( - "r.mapcalc", overwrite=True, quiet=True, expression=f"{self.map1} = 1" - ) - self.runModule( - "r.mapcalc", overwrite=True, quiet=True, expression=f"{self.map2} = 2" - ) - self.strds = tgis.open_new_stds( - name=self.strds_name, - type="strds", - temporaltype="absolute", - title="Test snap", - descr="Test snap", - semantic="field", - overwrite=True, - ) - tgis.register_maps_in_space_time_dataset( - type="raster", - name=self.strds.get_name(), - maps=f"{self.map1},{self.map2}", - start="2001-01-01", - increment="2 days", - interval=False, - ) - - def tearDown(self) -> None: - self.runModule( - "t.unregister", - type="raster", - maps=f"{self.map1},{self.map2}", - quiet=True, - ) - self.runModule( - "g.remove", - flags="f", - type="raster", - name=f"{self.map1},{self.map2}", - quiet=True, - ) - self.strds.delete() - - def test_snap_closes_gaps(self) -> None: - """snap() sets end time of each map to start time of next neighbor""" - self.strds.snap() - - maps = self.strds.get_registered_maps_as_objects(where=None, order="start_time") - start, end = maps[0].get_temporal_extent_as_tuple() - self.assertEqual(start, datetime.datetime(2001, 1, 1)) - self.assertEqual(end, datetime.datetime(2001, 1, 3)) - - -if __name__ == "__main__": - GrassTestProgram(module="__main__", exit_at_end=False, grass_location="all") From 4124d3901a8773bdef1ba196629824c16af9fac4 Mon Sep 17 00:00:00 2001 From: Anna Petrasova Date: Tue, 26 May 2026 23:08:12 -0400 Subject: [PATCH 5/5] revert back to using unittest, temporal doesn't work well with pytest yet --- ..._abstract_space_time_dataset_shift_snap.py | 90 ------------------- temporal/t.shift/testsuite/test_shift.py | 8 +- temporal/t.snap/testsuite/test_snap.py | 54 +++++++++++ 3 files changed, 59 insertions(+), 93 deletions(-) delete mode 100644 python/grass/temporal/tests/test_abstract_space_time_dataset_shift_snap.py diff --git a/python/grass/temporal/tests/test_abstract_space_time_dataset_shift_snap.py b/python/grass/temporal/tests/test_abstract_space_time_dataset_shift_snap.py deleted file mode 100644 index 4a87a9a54c9..00000000000 --- a/python/grass/temporal/tests/test_abstract_space_time_dataset_shift_snap.py +++ /dev/null @@ -1,90 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-or-later -"""Unit tests for AbstractSpaceTimeDataset.shift() and .snap().""" - -import datetime - -import pytest - -import grass.script as gs -import grass.temporal as tgis -from grass.tools import Tools - - -@pytest.fixture(scope="module") -def grass_session(tmp_path_factory): - """Active GRASS session with tgis initialized.""" - project = tmp_path_factory.mktemp("project") / "test" - gs.create_project(project) - with gs.setup.init(project) as session: - tgis.init() - Tools(session=session).g_region(s=0, n=80, w=0, e=120, res=10) - yield session - - -def _make_strds(session, name, *, interval, increment): - """Create an STRDS with two raster maps registered starting 2001-01-01.""" - map_1, map_2 = f"{name}_a", f"{name}_b" - tools = Tools(session=session) - tools.r_mapcalc(expression=f"{map_1} = 1") - tools.r_mapcalc(expression=f"{map_2} = 2") - strds = tgis.open_new_stds( - name=name, - type="strds", - temporaltype="absolute", - title="Test", - descr="Test", - semantic="field", - overwrite=True, - ) - tgis.register_maps_in_space_time_dataset( - type="raster", - name=strds.get_name(), - maps=f"{map_1},{map_2}", - start="2001-01-01", - increment=increment, - interval=interval, - ) - # register_maps_in_space_time_dataset updates the database row but not the - # in-memory object, so the granularity stays None until we re-read it. - strds.select() - return strds - - -@pytest.fixture -def interval_strds(grass_session, request): - """STRDS with two consecutive 1-day intervals starting 2001-01-01.""" - return _make_strds( - grass_session, request.node.name, interval=True, increment="1 day" - ) - - -@pytest.fixture -def point_strds(grass_session, request): - """STRDS with two point-in-time maps two days apart starting 2001-01-01.""" - return _make_strds( - grass_session, request.node.name, interval=False, increment="2 days" - ) - - -def test_shift_invalid_granularity(interval_strds): - """shift() returns False for invalid granularity (regression for #7228). - - The happy-path of shift() on absolute time is covered by - temporal/t.shift/testsuite/test_shift.py. - """ - assert interval_strds.shift(gran="invalid") is False - - -def test_snap_closes_gaps(point_strds): - """snap() sets each map's end time to its successor's start time, - and extends the last map by the dataset granularity.""" - point_strds.snap() - maps = point_strds.get_registered_maps_as_objects(order="start_time") - assert maps[0].get_temporal_extent_as_tuple() == ( - datetime.datetime(2001, 1, 1), - datetime.datetime(2001, 1, 3), - ) - assert maps[1].get_temporal_extent_as_tuple() == ( - datetime.datetime(2001, 1, 3), - datetime.datetime(2001, 1, 5), - ) diff --git a/temporal/t.shift/testsuite/test_shift.py b/temporal/t.shift/testsuite/test_shift.py index 3d55b059b4c..4a850f8fe6c 100644 --- a/temporal/t.shift/testsuite/test_shift.py +++ b/temporal/t.shift/testsuite/test_shift.py @@ -526,9 +526,11 @@ def tearDownClass(cls): cls.del_temp_region() cls.runModule("t.remove", flags="df", type="strds", inputs="A") - def test_1(self): - pass - # self.assterModuleFail() + def test_invalid_granularity(self): + """shift() returns False for invalid granularity (regression for #7228).""" + A = tgis.open_old_stds("A", type="strds") + A.select() + self.assertIs(A.shift(gran="invalid"), False) if __name__ == "__main__": diff --git a/temporal/t.snap/testsuite/test_snap.py b/temporal/t.snap/testsuite/test_snap.py index e46e25c454d..7879bf863fd 100644 --- a/temporal/t.snap/testsuite/test_snap.py +++ b/temporal/t.snap/testsuite/test_snap.py @@ -8,6 +8,7 @@ :authors: Soeren Gebbert """ +import datetime import os import grass.temporal as tgis @@ -66,6 +67,59 @@ def test_1_metadata(self): self.assertEqual(A.get_map_time(), "interval") +class TestSnapAbsoluteSTRDSExtents(TestCase): + """Verify snap() sets exact datetime extents, not just the map_time flag.""" + + @classmethod + def setUpClass(cls): + """Initiate the temporal GIS and set the region""" + tgis.init() + cls.use_temp_region() + cls.runModule("g.region", s=0, n=80, w=0, e=120, b=0, t=50, res=10, res3=10) + cls.runModule("r.mapcalc", expression="b1 = 1", overwrite=True) + cls.runModule("r.mapcalc", expression="b2 = 2", overwrite=True) + cls.runModule( + "t.create", + type="strds", + temporaltype="absolute", + output="B", + title="Snap extents test", + description="Snap extents test", + overwrite=True, + ) + cls.runModule( + "t.register", + type="raster", + input="B", + maps="b1,b2", + start="2001-01-01", + increment="2 days", + overwrite=True, + ) + + @classmethod + def tearDownClass(cls): + """Remove the temporary region""" + cls.del_temp_region() + cls.runModule("t.remove", flags="df", type="strds", inputs="B") + + def test_snap_closes_gaps_and_extends_last(self): + """snap() sets each map's end time to its successor's start time, + and extends the last map by the dataset granularity.""" + B = tgis.open_old_stds("B", type="strds") + B.select() + B.snap() + maps = B.get_registered_maps_as_objects(order="start_time") + self.assertEqual( + maps[0].get_temporal_extent_as_tuple(), + (datetime.datetime(2001, 1, 1), datetime.datetime(2001, 1, 3)), + ) + self.assertEqual( + maps[1].get_temporal_extent_as_tuple(), + (datetime.datetime(2001, 1, 3), datetime.datetime(2001, 1, 5)), + ) + + class TestSnapRelativeSTRDS(TestCase): @classmethod def setUpClass(cls):