From 906592651817cff60c4a5b12c347635aae592aa5 Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Thu, 11 Dec 2025 13:51:51 +0530 Subject: [PATCH 01/21] Add flags parameter to mapcalc and mapcalc_start --- python/grass/script/raster.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/python/grass/script/raster.py b/python/grass/script/raster.py index 85a3301ebea..6585d961246 100644 --- a/python/grass/script/raster.py +++ b/python/grass/script/raster.py @@ -120,6 +120,7 @@ def mapcalc( seed=None, nprocs=None, env=None, + flags="", **kwargs, ): """Interface to r.mapcalc. @@ -137,6 +138,7 @@ def mapcalc( :param seed: an integer used to seed the random-number generator for the rand() function, or 'auto' to generate a random seed :param nprocs: Number of threads for parallel computing + :param str flags: flags to be used (given as a string) :param dict env: dictionary of environment variables for child process :param kwargs: """ @@ -164,6 +166,7 @@ def mapcalc( superquiet=superquiet, verbose=verbose, overwrite=overwrite, + flags=flags, ) except CalledModuleError: fatal( @@ -181,6 +184,7 @@ def mapcalc_start( seed=None, nprocs=None, env=None, + flags="", **kwargs, ): """Interface to r.mapcalc, doesn't wait for it to finish, returns Popen object. @@ -212,6 +216,7 @@ def mapcalc_start( :param seed: an integer used to seed the random-number generator for the rand() function, or 'auto' to generate a random seed :param nprocs: Number of threads for parallel computing + :param str flags: flags to be used (given as a string) :param dict env: dictionary of environment variables for child process :param kwargs: @@ -239,6 +244,7 @@ def mapcalc_start( superquiet=superquiet, verbose=verbose, overwrite=overwrite, + flags=flags, ) p.stdin.write(e) p.stdin.close() From 5384e60afa9a1e26167ae8ca84334794a52544b1 Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Thu, 11 Dec 2025 13:53:53 +0530 Subject: [PATCH 02/21] tests for mapcalc() with seed and flags parameter --- .../tests/grass_script_raster_mapcalc_test.py | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 python/grass/script/tests/grass_script_raster_mapcalc_test.py diff --git a/python/grass/script/tests/grass_script_raster_mapcalc_test.py b/python/grass/script/tests/grass_script_raster_mapcalc_test.py new file mode 100644 index 00000000000..dff153ee98c --- /dev/null +++ b/python/grass/script/tests/grass_script_raster_mapcalc_test.py @@ -0,0 +1,147 @@ +"""Tests for grass.script.raster mapcalc functions""" + +import pytest + +import grass.script as gs + + +class TestMapcalcRandFunction: + """Tests for rand() function in mapcalc with flags parameter""" + + @pytest.fixture(autouse=True) + def setup_region(self, session_2x2): + """Setup region for tests""" + self.session = session_2x2 + gs.run_command("g.region", rows=10, cols=10, env=self.session.env) + + def teardown_method(self): + """Clean up maps after each test""" + gs.run_command( + "g.remove", + type="raster", + flags="f", + name="rand_map_auto_seed,rand_map_seed,rand_map_flag", + env=self.session.env, + quiet=True, + errors="ignore", + ) + + def test_mapcalc_rand_with_auto_seed(self): + """Test that rand() works with seed='auto'""" + gs.mapcalc( + "rand_map_auto_seed = rand(0.0, 1.0)", + seed="auto", + env=self.session.env, + ) + # Check that the map was created successfully + raster_info = gs.raster_info("rand_map_auto_seed", env=self.session.env) + assert raster_info is not None + assert "min" in raster_info + + def test_mapcalc_rand_with_explicit_seed(self): + """Test that rand() works with explicit seed value""" + gs.mapcalc( + "rand_map_seed = rand(0.0, 1.0)", + seed=12345, + env=self.session.env, + ) + # Check that the map was created successfully + raster_info = gs.raster_info("rand_map_seed", env=self.session.env) + assert raster_info is not None + assert "min" in raster_info + + def test_mapcalc_rand_with_flags_s(self): + """Test that rand() works with flags='s' to generate random seed""" + gs.mapcalc( + "rand_map_flag = rand(0.0, 1.0)", + flags="s", + env=self.session.env, + ) + # Check that the map was created successfully + raster_info = gs.raster_info("rand_map_flag", env=self.session.env) + assert raster_info is not None + assert "min" in raster_info + + def test_mapcalc_rand_flags_s_and_seed_are_mutually_exclusive(self): + """Test that flags='s' and seed= are mutually exclusive + + According to r.mapcalc CLI, -s flag and seed= option cannot be used together. + This test verifies that attempting to use both results in an error. + """ + from grass.script.core import CalledModuleError + + with pytest.raises(CalledModuleError): + gs.mapcalc( + "rand_map_error = rand(0.0, 1.0)", + flags="s", + seed=12345, + env=self.session.env, + ) + + +class TestMapcalcStartRandFunction: + """Tests for rand() function in mapcalc_start with flags parameter""" + + @pytest.fixture(autouse=True) + def setup_region(self, session_2x2): + """Setup region for tests""" + self.session = session_2x2 + gs.run_command("g.region", rows=10, cols=10, env=self.session.env) + + def teardown_method(self): + """Clean up maps after each test""" + gs.run_command( + "g.remove", + type="raster", + flags="f", + name="rand_map_start_auto,rand_map_start_seed,rand_map_start_flag", + env=self.session.env, + quiet=True, + errors="ignore", + ) + + def test_mapcalc_start_rand_with_auto_seed(self): + """Test that mapcalc_start with rand() works with seed='auto'""" + p = gs.mapcalc_start( + "rand_map_start_auto = rand(0.0, 1.0)", + seed="auto", + env=self.session.env, + ) + p.wait() + assert p.returncode == 0 + + def test_mapcalc_start_rand_with_explicit_seed(self): + """Test that mapcalc_start with rand() works with explicit seed value""" + p = gs.mapcalc_start( + "rand_map_start_seed = rand(0.0, 1.0)", + seed=12345, + env=self.session.env, + ) + returncode = p.wait() + assert returncode == 0 + + def test_mapcalc_start_rand_with_flags_s(self): + """Test that mapcalc_start with rand() works with flags='s'""" + p = gs.mapcalc_start( + "rand_map_start_flag = rand(0.0, 1.0)", + flags="s", + env=self.session.env, + ) + returncode = p.wait() + assert returncode == 0 + + def test_mapcalc_start_rand_flags_s_and_seed_are_mutually_exclusive(self): + """Test that flags='s' and seed= are mutually exclusive + + According to r.mapcalc CLI, -s flag and seed= option cannot be used together. + This test verifies that attempting to use both results in a non-zero return code. + """ + p = gs.mapcalc_start( + "rand_map_error = rand(0.0, 1.0)", + flags="s", + seed=12345, + env=self.session.env, + ) + returncode = p.wait() + # Should fail because -s and seed= are mutually exclusive + assert returncode != 0 From e5961c5ecdf28fb370091d55263ae8707ca05802 Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Fri, 12 Dec 2025 10:00:09 +0530 Subject: [PATCH 03/21] Auto-seed by default and remove deprecated seed='auto' parameter --- python/grass/script/raster.py | 19 ++--- .../tests/grass_script_raster_mapcalc_test.py | 73 +++++++++---------- 2 files changed, 40 insertions(+), 52 deletions(-) diff --git a/python/grass/script/raster.py b/python/grass/script/raster.py index 6585d961246..f3a297d4dbf 100644 --- a/python/grass/script/raster.py +++ b/python/grass/script/raster.py @@ -22,7 +22,6 @@ import os import string -import time from pathlib import Path from .core import ( @@ -120,7 +119,7 @@ def mapcalc( seed=None, nprocs=None, env=None, - flags="", + flags="s", **kwargs, ): """Interface to r.mapcalc. @@ -136,16 +135,13 @@ def mapcalc( :param bool verbose: True to run verbosely (``--v``) :param bool overwrite: True to enable overwriting the output (``--o``) :param seed: an integer used to seed the random-number generator for the - rand() function, or 'auto' to generate a random seed + rand() function :param nprocs: Number of threads for parallel computing - :param str flags: flags to be used (given as a string) + :param str flags: flags to be used, defaults to 's' for automatic seeding :param dict env: dictionary of environment variables for child process :param kwargs: """ - if seed == "auto": - seed = hash((os.getpid(), time.time())) % (2**32) - t = string.Template(exp) e = t.substitute(**kwargs) @@ -184,7 +180,7 @@ def mapcalc_start( seed=None, nprocs=None, env=None, - flags="", + flags="s", **kwargs, ): """Interface to r.mapcalc, doesn't wait for it to finish, returns Popen object. @@ -214,18 +210,15 @@ def mapcalc_start( :param bool verbose: True to run verbosely (``--v``) :param bool overwrite: True to enable overwriting the output (``--o``) :param seed: an integer used to seed the random-number generator for the - rand() function, or 'auto' to generate a random seed + rand() function :param nprocs: Number of threads for parallel computing - :param str flags: flags to be used (given as a string) + :param str flags: flags to be used, defaults to 's' for automatic seeding :param dict env: dictionary of environment variables for child process :param kwargs: :return: Popen object """ - if seed == "auto": - seed = hash((os.getpid(), time.time())) % (2**32) - t = string.Template(exp) e = t.substitute(**kwargs) diff --git a/python/grass/script/tests/grass_script_raster_mapcalc_test.py b/python/grass/script/tests/grass_script_raster_mapcalc_test.py index dff153ee98c..e6bb761dd47 100644 --- a/python/grass/script/tests/grass_script_raster_mapcalc_test.py +++ b/python/grass/script/tests/grass_script_raster_mapcalc_test.py @@ -6,7 +6,7 @@ class TestMapcalcRandFunction: - """Tests for rand() function in mapcalc with flags parameter""" + """Tests for rand() function in mapcalc with default auto-seeding""" @pytest.fixture(autouse=True) def setup_region(self, session_2x2): @@ -20,21 +20,20 @@ def teardown_method(self): "g.remove", type="raster", flags="f", - name="rand_map_auto_seed,rand_map_seed,rand_map_flag", + name="rand_map,rand_map_seed,rand_map_no_flags", env=self.session.env, quiet=True, errors="ignore", ) - def test_mapcalc_rand_with_auto_seed(self): - """Test that rand() works with seed='auto'""" + def test_mapcalc_rand_default_auto_seed(self): + """Test that rand() works with default auto-seeding (flags='s')""" gs.mapcalc( - "rand_map_auto_seed = rand(0.0, 1.0)", - seed="auto", + "rand_map = rand(0.0, 1.0)", env=self.session.env, ) # Check that the map was created successfully - raster_info = gs.raster_info("rand_map_auto_seed", env=self.session.env) + raster_info = gs.raster_info("rand_map", env=self.session.env) assert raster_info is not None assert "min" in raster_info @@ -50,26 +49,22 @@ def test_mapcalc_rand_with_explicit_seed(self): assert raster_info is not None assert "min" in raster_info - def test_mapcalc_rand_with_flags_s(self): - """Test that rand() works with flags='s' to generate random seed""" - gs.mapcalc( - "rand_map_flag = rand(0.0, 1.0)", - flags="s", - env=self.session.env, - ) - # Check that the map was created successfully - raster_info = gs.raster_info("rand_map_flag", env=self.session.env) - assert raster_info is not None - assert "min" in raster_info + def test_mapcalc_rand_disable_auto_seed(self): + """Test that rand() can disable auto-seeding with empty flags""" + from grass.script.core import CalledModuleError + + # Calling with flags='' should fail with unseeded PRNG error + with pytest.raises(CalledModuleError): + gs.mapcalc( + "rand_map_no_flags = rand(0.0, 1.0)", + flags="", + env=self.session.env, + ) def test_mapcalc_rand_flags_s_and_seed_are_mutually_exclusive(self): - """Test that flags='s' and seed= are mutually exclusive - - According to r.mapcalc CLI, -s flag and seed= option cannot be used together. - This test verifies that attempting to use both results in an error. - """ + """Test that flags='s' and seed= are mutually exclusive""" from grass.script.core import CalledModuleError - + with pytest.raises(CalledModuleError): gs.mapcalc( "rand_map_error = rand(0.0, 1.0)", @@ -80,7 +75,7 @@ def test_mapcalc_rand_flags_s_and_seed_are_mutually_exclusive(self): class TestMapcalcStartRandFunction: - """Tests for rand() function in mapcalc_start with flags parameter""" + """Tests for rand() function in mapcalc_start with default auto-seeding""" @pytest.fixture(autouse=True) def setup_region(self, session_2x2): @@ -94,21 +89,20 @@ def teardown_method(self): "g.remove", type="raster", flags="f", - name="rand_map_start_auto,rand_map_start_seed,rand_map_start_flag", + name="rand_map_start,rand_map_start_seed,rand_map_start_no_flags", env=self.session.env, quiet=True, errors="ignore", ) - def test_mapcalc_start_rand_with_auto_seed(self): - """Test that mapcalc_start with rand() works with seed='auto'""" + def test_mapcalc_start_rand_default_auto_seed(self): + """Test that mapcalc_start with rand() works with default auto-seeding""" p = gs.mapcalc_start( - "rand_map_start_auto = rand(0.0, 1.0)", - seed="auto", + "rand_map_start = rand(0.0, 1.0)", env=self.session.env, ) - p.wait() - assert p.returncode == 0 + returncode = p.wait() + assert returncode == 0 def test_mapcalc_start_rand_with_explicit_seed(self): """Test that mapcalc_start with rand() works with explicit seed value""" @@ -120,19 +114,20 @@ def test_mapcalc_start_rand_with_explicit_seed(self): returncode = p.wait() assert returncode == 0 - def test_mapcalc_start_rand_with_flags_s(self): - """Test that mapcalc_start with rand() works with flags='s'""" + def test_mapcalc_start_rand_disable_auto_seed(self): + """Test that mapcalc_start with rand() can disable auto-seeding with empty flags""" p = gs.mapcalc_start( - "rand_map_start_flag = rand(0.0, 1.0)", - flags="s", + "rand_map_start_no_flags = rand(0.0, 1.0)", + flags="", env=self.session.env, ) returncode = p.wait() - assert returncode == 0 + # Should fail because PRNG is not seeded + assert returncode != 0 def test_mapcalc_start_rand_flags_s_and_seed_are_mutually_exclusive(self): - """Test that flags='s' and seed= are mutually exclusive - + """Test that flags='s' and seed= are mutually exclusive in mapcalc_start + According to r.mapcalc CLI, -s flag and seed= option cannot be used together. This test verifies that attempting to use both results in a non-zero return code. """ From f150aa4a004cc238a0a85ad6e7455474e0eba4cd Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Sat, 13 Dec 2025 10:41:46 +0530 Subject: [PATCH 04/21] flags parameter support and handle deprecated seed=auto --- python/grass/script/raster.py | 20 +++-- .../tests/grass_script_raster_mapcalc_test.py | 74 +++++++++++++------ 2 files changed, 67 insertions(+), 27 deletions(-) diff --git a/python/grass/script/raster.py b/python/grass/script/raster.py index f3a297d4dbf..bcd1d1855a8 100644 --- a/python/grass/script/raster.py +++ b/python/grass/script/raster.py @@ -119,7 +119,7 @@ def mapcalc( seed=None, nprocs=None, env=None, - flags="s", + flags="", **kwargs, ): """Interface to r.mapcalc. @@ -135,9 +135,9 @@ def mapcalc( :param bool verbose: True to run verbosely (``--v``) :param bool overwrite: True to enable overwriting the output (``--o``) :param seed: an integer used to seed the random-number generator for the - rand() function + rand() function. If not provided, r.mapcalc uses automatic seeding. :param nprocs: Number of threads for parallel computing - :param str flags: flags to be used, defaults to 's' for automatic seeding + :param str flags: additional flags to pass to r.mapcalc :param dict env: dictionary of environment variables for child process :param kwargs: """ @@ -150,6 +150,10 @@ def mapcalc( if nprocs is None: nprocs = 1 + # Handle deprecated seed="auto" - ignore it, let r.mapcalc auto-seed + if seed == "auto": + seed = None + try: write_command( "r.mapcalc", @@ -180,7 +184,7 @@ def mapcalc_start( seed=None, nprocs=None, env=None, - flags="s", + flags="", **kwargs, ): """Interface to r.mapcalc, doesn't wait for it to finish, returns Popen object. @@ -210,9 +214,9 @@ def mapcalc_start( :param bool verbose: True to run verbosely (``--v``) :param bool overwrite: True to enable overwriting the output (``--o``) :param seed: an integer used to seed the random-number generator for the - rand() function + rand() function. If not provided, r.mapcalc uses automatic seeding. :param nprocs: Number of threads for parallel computing - :param str flags: flags to be used, defaults to 's' for automatic seeding + :param str flags: additional flags to pass to r.mapcalc :param dict env: dictionary of environment variables for child process :param kwargs: @@ -227,6 +231,10 @@ def mapcalc_start( if nprocs is None: nprocs = 1 + # Handle deprecated seed="auto" - ignore it, let r.mapcalc auto-seed + if seed == "auto": + seed = None + p = feed_command( "r.mapcalc", file="-", diff --git a/python/grass/script/tests/grass_script_raster_mapcalc_test.py b/python/grass/script/tests/grass_script_raster_mapcalc_test.py index e6bb761dd47..cd537c3e499 100644 --- a/python/grass/script/tests/grass_script_raster_mapcalc_test.py +++ b/python/grass/script/tests/grass_script_raster_mapcalc_test.py @@ -6,7 +6,10 @@ class TestMapcalcRandFunction: - """Tests for rand() function in mapcalc with default auto-seeding""" + """Tests for rand() function in mapcalc with automatic seeding + + r.mapcalc now auto-seeds by default when no explicit seed is provided. + """ @pytest.fixture(autouse=True) def setup_region(self, session_2x2): @@ -26,8 +29,8 @@ def teardown_method(self): errors="ignore", ) - def test_mapcalc_rand_default_auto_seed(self): - """Test that rand() works with default auto-seeding (flags='s')""" + def test_mapcalc_rand_autoseeded(self): + """Test that rand() works with automatic seeding (no parameters needed)""" gs.mapcalc( "rand_map = rand(0.0, 1.0)", env=self.session.env, @@ -49,17 +52,31 @@ def test_mapcalc_rand_with_explicit_seed(self): assert raster_info is not None assert "min" in raster_info - def test_mapcalc_rand_disable_auto_seed(self): - """Test that rand() can disable auto-seeding with empty flags""" - from grass.script.core import CalledModuleError + def test_mapcalc_rand_with_explicit_flags_s(self): + """Test that rand() works when explicitly using flags='s' (deprecated but still supported)""" + gs.mapcalc( + "rand_map_no_flags = rand(0.0, 1.0)", + flags="s", + env=self.session.env, + ) + # Check that the map was created successfully + raster_info = gs.raster_info("rand_map_no_flags", env=self.session.env) + assert raster_info is not None + assert "min" in raster_info - # Calling with flags='' should fail with unseeded PRNG error - with pytest.raises(CalledModuleError): - gs.mapcalc( - "rand_map_no_flags = rand(0.0, 1.0)", - flags="", - env=self.session.env, - ) + def test_mapcalc_rand_with_seed_auto_deprecated(self): + """Test that seed='auto' (deprecated) still works via graceful handling""" + # seed="auto" is deprecated but should still work + # Python converts it to None, r.mapcalc auto-seeds + gs.mapcalc( + "rand_map_seed = rand(0.0, 1.0)", + seed="auto", + env=self.session.env, + ) + # Check that the map was created successfully + raster_info = gs.raster_info("rand_map_seed", env=self.session.env) + assert raster_info is not None + assert "min" in raster_info def test_mapcalc_rand_flags_s_and_seed_are_mutually_exclusive(self): """Test that flags='s' and seed= are mutually exclusive""" @@ -75,7 +92,10 @@ def test_mapcalc_rand_flags_s_and_seed_are_mutually_exclusive(self): class TestMapcalcStartRandFunction: - """Tests for rand() function in mapcalc_start with default auto-seeding""" + """Tests for rand() function in mapcalc_start with automatic seeding + + r.mapcalc now auto-seeds by default when no explicit seed is provided. + """ @pytest.fixture(autouse=True) def setup_region(self, session_2x2): @@ -95,13 +115,14 @@ def teardown_method(self): errors="ignore", ) - def test_mapcalc_start_rand_default_auto_seed(self): - """Test that mapcalc_start with rand() works with default auto-seeding""" + def test_mapcalc_start_rand_autoseeded(self): + """Test that mapcalc_start with rand() works with automatic seeding (no parameters needed)""" p = gs.mapcalc_start( "rand_map_start = rand(0.0, 1.0)", env=self.session.env, ) returncode = p.wait() + # Should succeed - r.mapcalc now auto-seeds by default assert returncode == 0 def test_mapcalc_start_rand_with_explicit_seed(self): @@ -114,16 +135,27 @@ def test_mapcalc_start_rand_with_explicit_seed(self): returncode = p.wait() assert returncode == 0 - def test_mapcalc_start_rand_disable_auto_seed(self): - """Test that mapcalc_start with rand() can disable auto-seeding with empty flags""" + def test_mapcalc_start_rand_with_explicit_flags_s(self): + """Test that mapcalc_start with rand() works when explicitly using flags='s' (deprecated but still supported)""" p = gs.mapcalc_start( "rand_map_start_no_flags = rand(0.0, 1.0)", - flags="", + flags="s", env=self.session.env, ) returncode = p.wait() - # Should fail because PRNG is not seeded - assert returncode != 0 + assert returncode == 0 + + def test_mapcalc_start_rand_with_seed_auto_deprecated(self): + """Test that seed='auto' (deprecated) still works via graceful handling in mapcalc_start""" + # seed="auto" is deprecated but should still work + # Python converts it to None, r.mapcalc auto-seeds + p = gs.mapcalc_start( + "rand_map_start_seed = rand(0.0, 1.0)", + seed="auto", + env=self.session.env, + ) + returncode = p.wait() + assert returncode == 0 def test_mapcalc_start_rand_flags_s_and_seed_are_mutually_exclusive(self): """Test that flags='s' and seed= are mutually exclusive in mapcalc_start From bf3efb73ec59c73b4b24a3e5702aec8680c1909a Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Sun, 14 Dec 2025 21:43:09 +0530 Subject: [PATCH 05/21] rand() function when no explicit seed provided --- python/grass/script/raster.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/python/grass/script/raster.py b/python/grass/script/raster.py index bcd1d1855a8..b84528f11bc 100644 --- a/python/grass/script/raster.py +++ b/python/grass/script/raster.py @@ -154,6 +154,10 @@ def mapcalc( if seed == "auto": seed = None + # When no explicit seed is provided, add -s flag for automatic seeding + if seed is None and "s" not in flags: + flags += "s" + try: write_command( "r.mapcalc", @@ -235,6 +239,10 @@ def mapcalc_start( if seed == "auto": seed = None + # When no explicit seed is provided, add -s flag for automatic seeding + if seed is None and "s" not in flags: + flags += "s" + p = feed_command( "r.mapcalc", file="-", From 6c32923218daceceef1203eb11f7d3debbe3502e Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Mon, 15 Dec 2025 13:02:02 +0530 Subject: [PATCH 06/21] assigning priority to seed value when bith flag(s) and seed values are passed --- python/grass/script/raster.py | 8 +++ .../tests/grass_script_raster_mapcalc_test.py | 54 ++++++++++++------- 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/python/grass/script/raster.py b/python/grass/script/raster.py index b84528f11bc..d6b2ed502c2 100644 --- a/python/grass/script/raster.py +++ b/python/grass/script/raster.py @@ -155,8 +155,12 @@ def mapcalc( seed = None # When no explicit seed is provided, add -s flag for automatic seeding + # (until r.mapcalc C code implements native auto-seeding) if seed is None and "s" not in flags: flags += "s" + # If seed value is provided, remove -s flag to give precedence to seed + elif seed is not None and "s" in flags: + flags = flags.replace("s", "") try: write_command( @@ -240,8 +244,12 @@ def mapcalc_start( seed = None # When no explicit seed is provided, add -s flag for automatic seeding + # (until r.mapcalc C code implements native auto-seeding) if seed is None and "s" not in flags: flags += "s" + # If seed value is provided, remove -s flag to give precedence to seed + elif seed is not None and "s" in flags: + flags = flags.replace("s", "") p = feed_command( "r.mapcalc", diff --git a/python/grass/script/tests/grass_script_raster_mapcalc_test.py b/python/grass/script/tests/grass_script_raster_mapcalc_test.py index cd537c3e499..aea84af446c 100644 --- a/python/grass/script/tests/grass_script_raster_mapcalc_test.py +++ b/python/grass/script/tests/grass_script_raster_mapcalc_test.py @@ -23,7 +23,7 @@ def teardown_method(self): "g.remove", type="raster", flags="f", - name="rand_map,rand_map_seed,rand_map_no_flags", + name="rand_map,rand_map_seed,rand_map_no_flags,rand_map_seed_precedence", env=self.session.env, quiet=True, errors="ignore", @@ -78,17 +78,24 @@ def test_mapcalc_rand_with_seed_auto_deprecated(self): assert raster_info is not None assert "min" in raster_info - def test_mapcalc_rand_flags_s_and_seed_are_mutually_exclusive(self): - """Test that flags='s' and seed= are mutually exclusive""" - from grass.script.core import CalledModuleError + def test_mapcalc_rand_seed_precedence_over_flag(self): + """Test that seed value takes precedence when both seed and -s flag are provided. - with pytest.raises(CalledModuleError): - gs.mapcalc( - "rand_map_error = rand(0.0, 1.0)", - flags="s", - seed=12345, - env=self.session.env, - ) + The wrapper removes the -s flag when an explicit seed value is provided. + The seed value itself remains unchanged. + """ + # When both flags='s' and seed are provided, seed takes precedence + # The wrapper removes the -s flag automatically + gs.mapcalc( + "rand_map_seed_precedence = rand(0.0, 1.0)", + flags="s", + seed=12345, # Same seed value, -s flag is removed + env=self.session.env, + ) + # Verify the map was created successfully with the seed value + raster_info = gs.raster_info("rand_map_seed_precedence", env=self.session.env) + assert raster_info is not None + assert "min" in raster_info class TestMapcalcStartRandFunction: @@ -109,7 +116,7 @@ def teardown_method(self): "g.remove", type="raster", flags="f", - name="rand_map_start,rand_map_start_seed,rand_map_start_no_flags", + name="rand_map_start,rand_map_start_seed,rand_map_start_no_flags,rand_map_start_seed_precedence", env=self.session.env, quiet=True, errors="ignore", @@ -157,18 +164,25 @@ def test_mapcalc_start_rand_with_seed_auto_deprecated(self): returncode = p.wait() assert returncode == 0 - def test_mapcalc_start_rand_flags_s_and_seed_are_mutually_exclusive(self): - """Test that flags='s' and seed= are mutually exclusive in mapcalc_start + def test_mapcalc_start_rand_seed_precedence_over_flag(self): + """Test that seed value takes precedence when both seed and -s flag are provided in mapcalc_start. - According to r.mapcalc CLI, -s flag and seed= option cannot be used together. - This test verifies that attempting to use both results in a non-zero return code. + The wrapper removes the -s flag when an explicit seed value is provided. + The seed value itself remains unchanged. """ + # When both flags='s' and seed are provided, seed takes precedence + # The wrapper removes the -s flag automatically p = gs.mapcalc_start( - "rand_map_error = rand(0.0, 1.0)", + "rand_map_start_seed_precedence = rand(0.0, 1.0)", flags="s", - seed=12345, + seed=12345, # Same seed value, -s flag is removed env=self.session.env, ) returncode = p.wait() - # Should fail because -s and seed= are mutually exclusive - assert returncode != 0 + assert returncode == 0 + # Verify the map was created successfully with the seed value + raster_info = gs.raster_info( + "rand_map_start_seed_precedence", env=self.session.env + ) + assert raster_info is not None + assert "min" in raster_info From cef826cee8204b1f62d16a4a8f929c377853ccae Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Tue, 16 Dec 2025 09:09:42 +0530 Subject: [PATCH 07/21] native auto-seeding in C code --- python/grass/script/raster.py | 6 +----- raster/r.mapcalc/main.c | 7 +++++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/python/grass/script/raster.py b/python/grass/script/raster.py index d6b2ed502c2..e7658528440 100644 --- a/python/grass/script/raster.py +++ b/python/grass/script/raster.py @@ -154,12 +154,8 @@ def mapcalc( if seed == "auto": seed = None - # When no explicit seed is provided, add -s flag for automatic seeding - # (until r.mapcalc C code implements native auto-seeding) - if seed is None and "s" not in flags: - flags += "s" # If seed value is provided, remove -s flag to give precedence to seed - elif seed is not None and "s" in flags: + if seed is not None and "s" in flags: flags = flags.replace("s", "") try: diff --git a/raster/r.mapcalc/main.c b/raster/r.mapcalc/main.c index faa2fed12c1..0df9f6c430d 100644 --- a/raster/r.mapcalc/main.c +++ b/raster/r.mapcalc/main.c @@ -166,6 +166,13 @@ int main(int argc, char **argv) G_debug(3, "Generated random seed (-s): %ld", seed_value); } + /* Auto-seed if no explicit seed and no -s flag provided */ + if (!seed->answer && !random->answer) { + seed_value = G_srand48_auto(); + seeded = 1; + G_debug(3, "Generated random seed (auto): %ld", seed_value); + } + /* Set the global variable of the region setup approach */ region_approach = 1; From d784a3c294831dffee25ff38331b856432ad8479 Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Wed, 17 Dec 2025 12:03:56 +0530 Subject: [PATCH 08/21] stop forwarding flags parameter --- python/grass/script/raster.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/python/grass/script/raster.py b/python/grass/script/raster.py index e7658528440..020a57fedd1 100644 --- a/python/grass/script/raster.py +++ b/python/grass/script/raster.py @@ -154,9 +154,7 @@ def mapcalc( if seed == "auto": seed = None - # If seed value is provided, remove -s flag to give precedence to seed - if seed is not None and "s" in flags: - flags = flags.replace("s", "") + # We no longer forward `flags` to r.mapcalc; keep `seed` for compatibility try: write_command( @@ -170,7 +168,6 @@ def mapcalc( superquiet=superquiet, verbose=verbose, overwrite=overwrite, - flags=flags, ) except CalledModuleError: fatal( @@ -239,14 +236,7 @@ def mapcalc_start( if seed == "auto": seed = None - # When no explicit seed is provided, add -s flag for automatic seeding - # (until r.mapcalc C code implements native auto-seeding) - if seed is None and "s" not in flags: - flags += "s" - # If seed value is provided, remove -s flag to give precedence to seed - elif seed is not None and "s" in flags: - flags = flags.replace("s", "") - + # We no longer forward `flags` to r.mapcalc; keep `seed` for compatibility p = feed_command( "r.mapcalc", file="-", @@ -257,7 +247,6 @@ def mapcalc_start( superquiet=superquiet, verbose=verbose, overwrite=overwrite, - flags=flags, ) p.stdin.write(e) p.stdin.close() From 9b0876678c18d3738c9f463f808730051fc541bb Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Wed, 17 Dec 2025 12:05:12 +0530 Subject: [PATCH 09/21] update r.mapcalc Python API tests for wrapper refactoring --- .../tests/grass_script_raster_mapcalc_test.py | 98 ++++++------------- raster/r.mapcalc/main.c | 7 -- 2 files changed, 32 insertions(+), 73 deletions(-) diff --git a/python/grass/script/tests/grass_script_raster_mapcalc_test.py b/python/grass/script/tests/grass_script_raster_mapcalc_test.py index aea84af446c..f344007c746 100644 --- a/python/grass/script/tests/grass_script_raster_mapcalc_test.py +++ b/python/grass/script/tests/grass_script_raster_mapcalc_test.py @@ -6,9 +6,9 @@ class TestMapcalcRandFunction: - """Tests for rand() function in mapcalc with automatic seeding + """Tests for rand() function in mapcalc - r.mapcalc now auto-seeds by default when no explicit seed is provided. + r.mapcalc auto-seeds when no seed is provided. Explicit seed can be used for reproducibility. """ @pytest.fixture(autouse=True) @@ -23,14 +23,15 @@ def teardown_method(self): "g.remove", type="raster", flags="f", - name="rand_map,rand_map_seed,rand_map_no_flags,rand_map_seed_precedence", + name="rand_map,rand_map_seed,rand_map_auto_seed", env=self.session.env, quiet=True, errors="ignore", ) def test_mapcalc_rand_autoseeded(self): - """Test that rand() works with automatic seeding (no parameters needed)""" + """Test that rand() auto-seeds when no explicit seed is provided""" + # r.mapcalc auto-seeds when no seed is given gs.mapcalc( "rand_map = rand(0.0, 1.0)", env=self.session.env, @@ -52,56 +53,37 @@ def test_mapcalc_rand_with_explicit_seed(self): assert raster_info is not None assert "min" in raster_info - def test_mapcalc_rand_with_explicit_flags_s(self): - """Test that rand() works when explicitly using flags='s' (deprecated but still supported)""" - gs.mapcalc( - "rand_map_no_flags = rand(0.0, 1.0)", - flags="s", - env=self.session.env, - ) - # Check that the map was created successfully - raster_info = gs.raster_info("rand_map_no_flags", env=self.session.env) - assert raster_info is not None - assert "min" in raster_info - def test_mapcalc_rand_with_seed_auto_deprecated(self): - """Test that seed='auto' (deprecated) still works via graceful handling""" - # seed="auto" is deprecated but should still work - # Python converts it to None, r.mapcalc auto-seeds + """Test that seed='auto' is handled properly (converted to None, C auto-seeds)""" + # seed="auto" is deprecated; Python converts it to None + # r.mapcalc will auto-seed in this case gs.mapcalc( - "rand_map_seed = rand(0.0, 1.0)", + "rand_map_auto_seed = rand(0.0, 1.0)", seed="auto", env=self.session.env, ) - # Check that the map was created successfully - raster_info = gs.raster_info("rand_map_seed", env=self.session.env) + # Check that the map was created successfully with auto-seeding + raster_info = gs.raster_info("rand_map_auto_seed", env=self.session.env) assert raster_info is not None assert "min" in raster_info - def test_mapcalc_rand_seed_precedence_over_flag(self): - """Test that seed value takes precedence when both seed and -s flag are provided. - - The wrapper removes the -s flag when an explicit seed value is provided. - The seed value itself remains unchanged. - """ - # When both flags='s' and seed are provided, seed takes precedence - # The wrapper removes the -s flag automatically + def test_mapcalc_rand_with_explicit_seed_reproducible(self): + """Test that explicit seed produces reproducible results""" + # Create two maps with same seed gs.mapcalc( - "rand_map_seed_precedence = rand(0.0, 1.0)", - flags="s", - seed=12345, # Same seed value, -s flag is removed + "rand_map_seed = rand(0.0, 1.0)", + seed=12345, env=self.session.env, ) - # Verify the map was created successfully with the seed value - raster_info = gs.raster_info("rand_map_seed_precedence", env=self.session.env) + raster_info = gs.raster_info("rand_map_seed", env=self.session.env) assert raster_info is not None assert "min" in raster_info class TestMapcalcStartRandFunction: - """Tests for rand() function in mapcalc_start with automatic seeding + """Tests for rand() function in mapcalc_start - r.mapcalc now auto-seeds by default when no explicit seed is provided. + r.mapcalc auto-seeds when no seed is provided. Explicit seed can be used for reproducibility. """ @pytest.fixture(autouse=True) @@ -116,20 +98,20 @@ def teardown_method(self): "g.remove", type="raster", flags="f", - name="rand_map_start,rand_map_start_seed,rand_map_start_no_flags,rand_map_start_seed_precedence", + name="rand_map_start,rand_map_start_seed,rand_map_start_auto_seed", env=self.session.env, quiet=True, errors="ignore", ) def test_mapcalc_start_rand_autoseeded(self): - """Test that mapcalc_start with rand() works with automatic seeding (no parameters needed)""" + """Test that mapcalc_start with rand() auto-seeds when no explicit seed is provided""" + # r.mapcalc auto-seeds when no seed is given p = gs.mapcalc_start( "rand_map_start = rand(0.0, 1.0)", env=self.session.env, ) returncode = p.wait() - # Should succeed - r.mapcalc now auto-seeds by default assert returncode == 0 def test_mapcalc_start_rand_with_explicit_seed(self): @@ -142,47 +124,31 @@ def test_mapcalc_start_rand_with_explicit_seed(self): returncode = p.wait() assert returncode == 0 - def test_mapcalc_start_rand_with_explicit_flags_s(self): - """Test that mapcalc_start with rand() works when explicitly using flags='s' (deprecated but still supported)""" - p = gs.mapcalc_start( - "rand_map_start_no_flags = rand(0.0, 1.0)", - flags="s", - env=self.session.env, - ) - returncode = p.wait() - assert returncode == 0 - def test_mapcalc_start_rand_with_seed_auto_deprecated(self): - """Test that seed='auto' (deprecated) still works via graceful handling in mapcalc_start""" - # seed="auto" is deprecated but should still work - # Python converts it to None, r.mapcalc auto-seeds + """Test that seed='auto' is handled properly in mapcalc_start (converted to None, C auto-seeds)""" + # seed="auto" is deprecated; Python converts it to None + # r.mapcalc will auto-seed in this case p = gs.mapcalc_start( - "rand_map_start_seed = rand(0.0, 1.0)", + "rand_map_start_auto_seed = rand(0.0, 1.0)", seed="auto", env=self.session.env, ) returncode = p.wait() assert returncode == 0 - def test_mapcalc_start_rand_seed_precedence_over_flag(self): - """Test that seed value takes precedence when both seed and -s flag are provided in mapcalc_start. - - The wrapper removes the -s flag when an explicit seed value is provided. - The seed value itself remains unchanged. - """ - # When both flags='s' and seed are provided, seed takes precedence - # The wrapper removes the -s flag automatically + def test_mapcalc_start_rand_with_explicit_seed_reproducible(self): + """Test that explicit seed in mapcalc_start produces reproducible results""" + # Create map with explicit seed p = gs.mapcalc_start( - "rand_map_start_seed_precedence = rand(0.0, 1.0)", - flags="s", - seed=12345, # Same seed value, -s flag is removed + "rand_map_start_seed = rand(0.0, 1.0)", + seed=12345, env=self.session.env, ) returncode = p.wait() assert returncode == 0 # Verify the map was created successfully with the seed value raster_info = gs.raster_info( - "rand_map_start_seed_precedence", env=self.session.env + "rand_map_start_seed", env=self.session.env ) assert raster_info is not None assert "min" in raster_info diff --git a/raster/r.mapcalc/main.c b/raster/r.mapcalc/main.c index 0df9f6c430d..faa2fed12c1 100644 --- a/raster/r.mapcalc/main.c +++ b/raster/r.mapcalc/main.c @@ -166,13 +166,6 @@ int main(int argc, char **argv) G_debug(3, "Generated random seed (-s): %ld", seed_value); } - /* Auto-seed if no explicit seed and no -s flag provided */ - if (!seed->answer && !random->answer) { - seed_value = G_srand48_auto(); - seeded = 1; - G_debug(3, "Generated random seed (auto): %ld", seed_value); - } - /* Set the global variable of the region setup approach */ region_approach = 1; From 66346af976d238b7c7f6393d52548a3da867d667 Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Wed, 17 Dec 2025 12:10:23 +0530 Subject: [PATCH 10/21] format --- python/grass/script/tests/grass_script_raster_mapcalc_test.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python/grass/script/tests/grass_script_raster_mapcalc_test.py b/python/grass/script/tests/grass_script_raster_mapcalc_test.py index f344007c746..1a2f9ae9128 100644 --- a/python/grass/script/tests/grass_script_raster_mapcalc_test.py +++ b/python/grass/script/tests/grass_script_raster_mapcalc_test.py @@ -147,8 +147,6 @@ def test_mapcalc_start_rand_with_explicit_seed_reproducible(self): returncode = p.wait() assert returncode == 0 # Verify the map was created successfully with the seed value - raster_info = gs.raster_info( - "rand_map_start_seed", env=self.session.env - ) + raster_info = gs.raster_info("rand_map_start_seed", env=self.session.env) assert raster_info is not None assert "min" in raster_info From f9d2abecea876686ad5ba77e12943792aa188682 Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Thu, 18 Dec 2025 08:58:46 +0530 Subject: [PATCH 11/21] Remove flags parameter from mapcalc and mapcalc_start function --- python/grass/script/raster.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/python/grass/script/raster.py b/python/grass/script/raster.py index 020a57fedd1..75562181059 100644 --- a/python/grass/script/raster.py +++ b/python/grass/script/raster.py @@ -119,7 +119,6 @@ def mapcalc( seed=None, nprocs=None, env=None, - flags="", **kwargs, ): """Interface to r.mapcalc. @@ -137,7 +136,6 @@ def mapcalc( :param seed: an integer used to seed the random-number generator for the rand() function. If not provided, r.mapcalc uses automatic seeding. :param nprocs: Number of threads for parallel computing - :param str flags: additional flags to pass to r.mapcalc :param dict env: dictionary of environment variables for child process :param kwargs: """ @@ -150,12 +148,6 @@ def mapcalc( if nprocs is None: nprocs = 1 - # Handle deprecated seed="auto" - ignore it, let r.mapcalc auto-seed - if seed == "auto": - seed = None - - # We no longer forward `flags` to r.mapcalc; keep `seed` for compatibility - try: write_command( "r.mapcalc", @@ -185,7 +177,6 @@ def mapcalc_start( seed=None, nprocs=None, env=None, - flags="", **kwargs, ): """Interface to r.mapcalc, doesn't wait for it to finish, returns Popen object. @@ -217,7 +208,6 @@ def mapcalc_start( :param seed: an integer used to seed the random-number generator for the rand() function. If not provided, r.mapcalc uses automatic seeding. :param nprocs: Number of threads for parallel computing - :param str flags: additional flags to pass to r.mapcalc :param dict env: dictionary of environment variables for child process :param kwargs: @@ -232,11 +222,6 @@ def mapcalc_start( if nprocs is None: nprocs = 1 - # Handle deprecated seed="auto" - ignore it, let r.mapcalc auto-seed - if seed == "auto": - seed = None - - # We no longer forward `flags` to r.mapcalc; keep `seed` for compatibility p = feed_command( "r.mapcalc", file="-", From 062d2fd48a85991ff56822ad3e16722fd52ad1d7 Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Thu, 18 Dec 2025 21:15:26 +0530 Subject: [PATCH 12/21] Add backwards compatibility for seed='auto' in mapcalc functions --- python/grass/script/raster.py | 6 ++++++ .../tests/grass_script_raster_mapcalc_test.py | 18 +++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/python/grass/script/raster.py b/python/grass/script/raster.py index 75562181059..f91dfd84c22 100644 --- a/python/grass/script/raster.py +++ b/python/grass/script/raster.py @@ -139,6 +139,9 @@ def mapcalc( :param dict env: dictionary of environment variables for child process :param kwargs: """ + # Handle backwards compatibility: convert seed="auto" to None + if seed == "auto": + seed = None t = string.Template(exp) e = t.substitute(**kwargs) @@ -213,6 +216,9 @@ def mapcalc_start( :return: Popen object """ + # Handle backwards compatibility: convert seed="auto" to None + if seed == "auto": + seed = None t = string.Template(exp) e = t.substitute(**kwargs) diff --git a/python/grass/script/tests/grass_script_raster_mapcalc_test.py b/python/grass/script/tests/grass_script_raster_mapcalc_test.py index 1a2f9ae9128..859b8b2028d 100644 --- a/python/grass/script/tests/grass_script_raster_mapcalc_test.py +++ b/python/grass/script/tests/grass_script_raster_mapcalc_test.py @@ -53,10 +53,9 @@ def test_mapcalc_rand_with_explicit_seed(self): assert raster_info is not None assert "min" in raster_info - def test_mapcalc_rand_with_seed_auto_deprecated(self): - """Test that seed='auto' is handled properly (converted to None, C auto-seeds)""" - # seed="auto" is deprecated; Python converts it to None - # r.mapcalc will auto-seed in this case + def test_mapcalc_rand_with_seed_auto_backwards_compat(self): + """Test that seed='auto' is handled for backwards compatibility (converted to None)""" + # seed="auto" should be converted to None internally, enabling auto-seeding gs.mapcalc( "rand_map_auto_seed = rand(0.0, 1.0)", seed="auto", @@ -124,10 +123,9 @@ def test_mapcalc_start_rand_with_explicit_seed(self): returncode = p.wait() assert returncode == 0 - def test_mapcalc_start_rand_with_seed_auto_deprecated(self): - """Test that seed='auto' is handled properly in mapcalc_start (converted to None, C auto-seeds)""" - # seed="auto" is deprecated; Python converts it to None - # r.mapcalc will auto-seed in this case + def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self): + """Test that seed='auto' is handled for backwards compatibility (converted to None)""" + # seed="auto" should be converted to None internally, enabling auto-seeding p = gs.mapcalc_start( "rand_map_start_auto_seed = rand(0.0, 1.0)", seed="auto", @@ -147,6 +145,8 @@ def test_mapcalc_start_rand_with_explicit_seed_reproducible(self): returncode = p.wait() assert returncode == 0 # Verify the map was created successfully with the seed value - raster_info = gs.raster_info("rand_map_start_seed", env=self.session.env) + raster_info = gs.raster_info( + "rand_map_start_seed", env=self.session.env + ) assert raster_info is not None assert "min" in raster_info From 76ac729e018731043912c564089cb21b330b9802 Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Thu, 18 Dec 2025 21:21:02 +0530 Subject: [PATCH 13/21] line adjust --- python/grass/script/tests/grass_script_raster_mapcalc_test.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python/grass/script/tests/grass_script_raster_mapcalc_test.py b/python/grass/script/tests/grass_script_raster_mapcalc_test.py index 859b8b2028d..79522a52339 100644 --- a/python/grass/script/tests/grass_script_raster_mapcalc_test.py +++ b/python/grass/script/tests/grass_script_raster_mapcalc_test.py @@ -145,8 +145,6 @@ def test_mapcalc_start_rand_with_explicit_seed_reproducible(self): returncode = p.wait() assert returncode == 0 # Verify the map was created successfully with the seed value - raster_info = gs.raster_info( - "rand_map_start_seed", env=self.session.env - ) + raster_info = gs.raster_info("rand_map_start_seed", env=self.session.env) assert raster_info is not None assert "min" in raster_info From d575ccdc12c87a5263ba7d2e2e5a4c5133bcf7f3 Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Sat, 20 Dec 2025 12:26:26 +0530 Subject: [PATCH 14/21] comprehensive seed handling tests --- .../tests/grass_script_raster_mapcalc_test.py | 577 ++++++++++++++++-- 1 file changed, 524 insertions(+), 53 deletions(-) diff --git a/python/grass/script/tests/grass_script_raster_mapcalc_test.py b/python/grass/script/tests/grass_script_raster_mapcalc_test.py index 79522a52339..bf514bc7a15 100644 --- a/python/grass/script/tests/grass_script_raster_mapcalc_test.py +++ b/python/grass/script/tests/grass_script_raster_mapcalc_test.py @@ -1,8 +1,29 @@ """Tests for grass.script.raster mapcalc functions""" import pytest +import numpy as np +import re import grass.script as gs +from grass.pygrass.raster import raster2numpy + + +def extract_seed_from_comments(comments_str): + """Extract explicit numeric seed from raster comments/metadata. + + Parses the seed value from r.mapcalc metadata comments string. + Searches for patterns like 'seed=12345' or 'seed: 12345' (handles optional spacing). + + Args: + comments_str: The comments/metadata string from raster_info() + + Returns: + int: The seed value if found, None otherwise + """ + if not comments_str: + return None + match = re.search(r"\bseed\s*[=:]\s*(-?\d+)", comments_str) + return int(match.group(1)) if match else None class TestMapcalcRandFunction: @@ -18,71 +39,354 @@ def setup_region(self, session_2x2): gs.run_command("g.region", rows=10, cols=10, env=self.session.env) def teardown_method(self): - """Clean up maps after each test""" - gs.run_command( - "g.remove", - type="raster", - flags="f", - name="rand_map,rand_map_seed,rand_map_auto_seed", - env=self.session.env, - quiet=True, - errors="ignore", - ) + """Clean up maps after each test using pattern matching""" + try: + maps = gs.list_strings( + type="raster", pattern="rand_map*", env=self.session.env + ) + if maps: + gs.run_command( + "g.remove", + type="raster", + flags="f", + name=",".join(maps), + env=self.session.env, + quiet=True, + ) + except Exception: + # Silently ignore cleanup errors to avoid masking test failures + pass def test_mapcalc_rand_autoseeded(self): - """Test that rand() auto-seeds when no explicit seed is provided""" - # r.mapcalc auto-seeds when no seed is given + """Test that rand() auto-seeds when no explicit seed is provided. + + Verifies: + 1. Two consecutive runs without explicit seed produce different results (different auto-seeds) + 2. Metadata contains different auto-generated seed values + """ + # r.mapcalc auto-seeds when no seed is given — two runs should differ gs.mapcalc( - "rand_map = rand(0.0, 1.0)", + "rand_map_auto1 = rand(0.0, 1.0)", env=self.session.env, ) - # Check that the map was created successfully - raster_info = gs.raster_info("rand_map", env=self.session.env) - assert raster_info is not None - assert "min" in raster_info + gs.mapcalc( + "rand_map_auto2 = rand(0.0, 1.0)", + env=self.session.env, + ) + + # Verify arrays differ (outcome of different auto-seeds) + array1 = raster2numpy("rand_map_auto1") + array2 = raster2numpy("rand_map_auto2") + assert array1 is not None + assert array2 is not None + assert array1.size > 0 + assert array2.size > 0 + assert not np.array_equal(array1, array2), ( + "Auto-seeded runs should produce different maps" + ) + + # Verify mechanism: auto-generated seeds are recorded in metadata and differ + info1 = gs.raster_info("rand_map_auto1", env=self.session.env) + info2 = gs.raster_info("rand_map_auto2", env=self.session.env) + comments1 = info1.get("comments", "") + comments2 = info2.get("comments", "") + + seed1 = extract_seed_from_comments(comments1) + seed2 = extract_seed_from_comments(comments2) + + assert seed1 is not None, ( + "Auto-seed should be recorded in metadata for first run" + ) + assert seed2 is not None, ( + "Auto-seed should be recorded in metadata for second run" + ) + assert seed1 != seed2, "Auto-seeds should differ between runs" def test_mapcalc_rand_with_explicit_seed(self): - """Test that rand() works with explicit seed value""" + """Test that rand() works with explicit seed value. + + Verifies: + 1. Seed parameter is passed to r.mapcalc + 2. Metadata contains the explicit seed value + """ gs.mapcalc( "rand_map_seed = rand(0.0, 1.0)", seed=12345, env=self.session.env, ) - # Check that the map was created successfully + # Check that the map was created and seed was written to metadata raster_info = gs.raster_info("rand_map_seed", env=self.session.env) assert raster_info is not None - assert "min" in raster_info + + # Verify seed parameter was passed to r.mapcalc (check metadata) + comments = raster_info.get("comments", "") + seed_found = extract_seed_from_comments(comments) + assert seed_found == 12345, ( + f"Expected seed=12345 in metadata, got seed={seed_found}" + ) def test_mapcalc_rand_with_seed_auto_backwards_compat(self): - """Test that seed='auto' is handled for backwards compatibility (converted to None)""" - # seed="auto" should be converted to None internally, enabling auto-seeding + """Test that seed='auto' is handled for backwards compatibility. + + seed='auto' should be converted to None internally, enabling auto-seeding behavior. + This means it should behave identically to not providing a seed parameter. + + Verifies: + 1. seed='auto' does not pass literal 'auto' to r.mapcalc (it's converted before calling) + 2. Two runs with seed='auto' produce different results (auto-seeding enabled) + 3. Auto-generated seeds are recorded in metadata (consistent with no-seed behavior) + 4. Auto-generated seeds differ between runs + """ + # Create one map with seed='auto' gs.mapcalc( "rand_map_auto_seed = rand(0.0, 1.0)", seed="auto", env=self.session.env, ) - # Check that the map was created successfully with auto-seeding - raster_info = gs.raster_info("rand_map_auto_seed", env=self.session.env) - assert raster_info is not None - assert "min" in raster_info + raster_info_1 = gs.raster_info("rand_map_auto_seed", env=self.session.env) + assert raster_info_1 is not None + comments_1 = raster_info_1.get("comments", "") + seed_1 = extract_seed_from_comments(comments_1) + + # Since seed='auto' converts to None, it should enable auto-seeding + # Auto-seeding records generated seed in metadata (same as no-seed case) + assert seed_1 is not None, ( + "seed='auto' should convert to None and enable auto-seeding, recording an auto-generated seed in metadata" + ) + + # Create second map with seed='auto' — should differ from first (different auto-seeds) + gs.mapcalc( + "rand_map_auto_seed_2 = rand(0.0, 1.0)", + seed="auto", + env=self.session.env, + ) + raster_info_2 = gs.raster_info("rand_map_auto_seed_2", env=self.session.env) + assert raster_info_2 is not None + comments_2 = raster_info_2.get("comments", "") + seed_2 = extract_seed_from_comments(comments_2) + + # Verify both runs have auto-generated seeds and they differ + assert seed_2 is not None, ( + "seed='auto' should enable auto-seeding, recording an auto-generated seed" + ) + assert seed_1 != seed_2, ( + "seed='auto' should produce different auto-generated seeds on consecutive runs" + ) + + # Verify actual raster data differs + array_1 = raster2numpy("rand_map_auto_seed") + array_2 = raster2numpy("rand_map_auto_seed_2") + assert not np.array_equal(array_1, array_2, equal_nan=True), ( + "seed='auto' should enable auto-seeding, producing different results each time" + ) def test_mapcalc_rand_with_explicit_seed_reproducible(self): - """Test that explicit seed produces reproducible results""" - # Create two maps with same seed + """Test that explicit seed produces reproducible results. + + Verifies: + 1. Same seed produces identical raster arrays (byte-for-byte reproducibility) + 2. Different seeds produce different results (seed actually has effect) + 3. Seed metadata is correctly recorded for all variations + """ + # Create two maps with same seed=12345 gs.mapcalc( "rand_map_seed = rand(0.0, 1.0)", seed=12345, env=self.session.env, ) - raster_info = gs.raster_info("rand_map_seed", env=self.session.env) - assert raster_info is not None - assert "min" in raster_info + raster_info_1 = gs.raster_info("rand_map_seed", env=self.session.env) + assert raster_info_1 is not None + comments_1 = raster_info_1.get("comments", "") + seed_1 = extract_seed_from_comments(comments_1) + assert seed_1 == 12345, f"Expected seed=12345 in metadata, got seed={seed_1}" + + # Read actual raster data + array_1 = raster2numpy("rand_map_seed") + assert array_1 is not None + assert array_1.size > 0 + + # Create another map with the same seed + gs.mapcalc( + "rand_map_seed_2 = rand(0.0, 1.0)", + seed=12345, + env=self.session.env, + ) + raster_info_2 = gs.raster_info("rand_map_seed_2", env=self.session.env) + assert raster_info_2 is not None + comments_2 = raster_info_2.get("comments", "") + seed_2 = extract_seed_from_comments(comments_2) + assert seed_2 == 12345, f"Expected seed=12345 in metadata, got seed={seed_2}" + + # Read actual raster data + array_2 = raster2numpy("rand_map_seed_2") + assert array_2 is not None + assert array_2.size > 0 + + # Verify reproducibility: arrays must be identical (byte-for-byte) + assert np.array_equal(array_1, array_2, equal_nan=True), ( + "Maps with same seed should have identical cell values" + ) + + # Verify seed actually has an effect by comparing with different seed + gs.mapcalc( + "rand_map_seed_3 = rand(0.0, 1.0)", + seed=54321, + env=self.session.env, + ) + raster_info_3 = gs.raster_info("rand_map_seed_3", env=self.session.env) + comments_3 = raster_info_3.get("comments", "") + seed_3 = extract_seed_from_comments(comments_3) + assert seed_3 == 54321, f"Expected seed=54321 in metadata, got seed={seed_3}" + + array_3 = raster2numpy("rand_map_seed_3") + assert array_3 is not None + + # Different seed should produce different results + assert not np.array_equal(array_1, array_3, equal_nan=True), ( + "Maps with different seeds should have different cell values" + ) + + def test_mapcalc_rand_with_seed_zero(self): + """Test that seed=0 is handled correctly. + + Verifies: + 1. seed=0 is valid and produces reproducible results + 2. seed=0 differs from seed='auto' (explicit seed produces different results than auto-seeding) + """ + # Create map with seed=0 + gs.mapcalc( + "rand_map_seed_0 = rand(0.0, 1.0)", + seed=0, + env=self.session.env, + ) + raster_info_1 = gs.raster_info("rand_map_seed_0", env=self.session.env) + assert raster_info_1 is not None + comments_1 = raster_info_1.get("comments", "") + seed_1 = extract_seed_from_comments(comments_1) + assert seed_1 == 0, f"Expected seed=0 in metadata, got seed={seed_1}" + + array_1 = raster2numpy("rand_map_seed_0") + + # Create another map with seed=0 — should be reproducible + gs.mapcalc( + "rand_map_seed_0_2 = rand(0.0, 1.0)", + seed=0, + env=self.session.env, + ) + array_2 = raster2numpy("rand_map_seed_0_2") + assert np.array_equal(array_1, array_2, equal_nan=True), ( + "seed=0 should produce reproducible results" + ) + + # Verify seed=0 differs from auto-seeding + gs.mapcalc( + "rand_map_seed_0_auto = rand(0.0, 1.0)", + env=self.session.env, + ) + array_auto = raster2numpy("rand_map_seed_0_auto") + assert not np.array_equal(array_1, array_auto, equal_nan=True), ( + "seed=0 should produce different results than auto-seeding" + ) + + def test_mapcalc_rand_with_negative_seed(self): + """Test that negative seeds are handled correctly. + + Verifies: + 1. Negative seed values are valid and produce reproducible results + 2. Different negative seeds produce different results + """ + # Create map with negative seed + gs.mapcalc( + "rand_map_neg_seed = rand(0.0, 1.0)", + seed=-42, + env=self.session.env, + ) + raster_info_1 = gs.raster_info("rand_map_neg_seed", env=self.session.env) + assert raster_info_1 is not None + comments_1 = raster_info_1.get("comments", "") + seed_1 = extract_seed_from_comments(comments_1) + assert seed_1 == -42, f"Expected seed=-42 in metadata, got seed={seed_1}" + + array_1 = raster2numpy("rand_map_neg_seed") + + # Create another map with same negative seed — should be reproducible + gs.mapcalc( + "rand_map_neg_seed_2 = rand(0.0, 1.0)", + seed=-42, + env=self.session.env, + ) + array_2 = raster2numpy("rand_map_neg_seed_2") + assert np.array_equal(array_1, array_2, equal_nan=True), ( + "Negative seed should produce reproducible results" + ) + + # Different negative seed should produce different results + gs.mapcalc( + "rand_map_neg_seed_3 = rand(0.0, 1.0)", + seed=-99, + env=self.session.env, + ) + array_3 = raster2numpy("rand_map_neg_seed_3") + assert not np.array_equal(array_1, array_3, equal_nan=True), ( + "Different negative seeds should produce different results" + ) + + def test_mapcalc_rand_with_large_seed(self): + """Test that very large seed values are handled correctly. + + Verifies: + 1. Large seed values are valid and produce reproducible results + 2. Large seed differs from regular seeds (seed value actually has effect) + """ + large_seed = 2147483647 # Max 32-bit signed integer + + # Create map with large seed + gs.mapcalc( + "rand_map_large_seed = rand(0.0, 1.0)", + seed=large_seed, + env=self.session.env, + ) + raster_info_1 = gs.raster_info("rand_map_large_seed", env=self.session.env) + assert raster_info_1 is not None + comments_1 = raster_info_1.get("comments", "") + seed_1 = extract_seed_from_comments(comments_1) + assert seed_1 == large_seed, ( + f"Expected seed={large_seed} in metadata, got seed={seed_1}" + ) + + array_1 = raster2numpy("rand_map_large_seed") + + # Create another map with same large seed — should be reproducible + gs.mapcalc( + "rand_map_large_seed_2 = rand(0.0, 1.0)", + seed=large_seed, + env=self.session.env, + ) + array_2 = raster2numpy("rand_map_large_seed_2") + assert np.array_equal(array_1, array_2, equal_nan=True), ( + "Large seed should produce reproducible results" + ) + + # Verify large seed differs from regular seed (seed value has effect) + gs.mapcalc( + "rand_map_regular_seed = rand(0.0, 1.0)", + seed=12345, + env=self.session.env, + ) + array_regular = raster2numpy("rand_map_regular_seed") + assert not np.array_equal(array_1, array_regular, equal_nan=True), ( + "Large seed should produce different results than regular seed" + ) class TestMapcalcStartRandFunction: """Tests for rand() function in mapcalc_start r.mapcalc auto-seeds when no seed is provided. Explicit seed can be used for reproducibility. + + Note: Edge case tests (seed=0, negative seeds, large seeds) are only in TestMapcalcRandFunction + because mapcalc and mapcalc_start share the same underlying seed handling implementation. + Testing the mechanism once is sufficient to ensure both code paths work correctly. """ @pytest.fixture(autouse=True) @@ -92,29 +396,80 @@ def setup_region(self, session_2x2): gs.run_command("g.region", rows=10, cols=10, env=self.session.env) def teardown_method(self): - """Clean up maps after each test""" - gs.run_command( - "g.remove", - type="raster", - flags="f", - name="rand_map_start,rand_map_start_seed,rand_map_start_auto_seed", - env=self.session.env, - quiet=True, - errors="ignore", - ) + """Clean up maps after each test using pattern matching""" + try: + maps = gs.list_strings( + type="raster", pattern="rand_map_start*", env=self.session.env + ) + if maps: + gs.run_command( + "g.remove", + type="raster", + flags="f", + name=",".join(maps), + env=self.session.env, + quiet=True, + ) + except Exception: + # Silently ignore cleanup errors to avoid masking test failures + pass def test_mapcalc_start_rand_autoseeded(self): - """Test that mapcalc_start with rand() auto-seeds when no explicit seed is provided""" - # r.mapcalc auto-seeds when no seed is given + """Test that mapcalc_start with rand() auto-seeds when no explicit seed is provided. + + Verifies: + 1. Two consecutive runs without explicit seed produce different results (different auto-seeds) + 2. Metadata contains different auto-generated seed values + """ + # r.mapcalc auto-seeds when no seed is given — two runs should differ + p = gs.mapcalc_start( + "rand_map_start_auto1 = rand(0.0, 1.0)", + env=self.session.env, + ) + returncode = p.wait() + assert returncode == 0 p = gs.mapcalc_start( - "rand_map_start = rand(0.0, 1.0)", + "rand_map_start_auto2 = rand(0.0, 1.0)", env=self.session.env, ) returncode = p.wait() assert returncode == 0 + # Verify outcome: arrays differ (different auto-seeds) + array1 = raster2numpy("rand_map_start_auto1") + array2 = raster2numpy("rand_map_start_auto2") + assert array1 is not None + assert array2 is not None + assert array1.size > 0 + assert array2.size > 0 + assert not np.array_equal(array1, array2), ( + "Auto-seeded runs should produce different maps" + ) + + # Verify mechanism: auto-generated seeds are recorded in metadata and differ + info1 = gs.raster_info("rand_map_start_auto1", env=self.session.env) + info2 = gs.raster_info("rand_map_start_auto2", env=self.session.env) + comments1 = info1.get("comments", "") + comments2 = info2.get("comments", "") + + seed1 = extract_seed_from_comments(comments1) + seed2 = extract_seed_from_comments(comments2) + + assert seed1 is not None, ( + "Auto-seed should be recorded in metadata for first run" + ) + assert seed2 is not None, ( + "Auto-seed should be recorded in metadata for second run" + ) + assert seed1 != seed2, "Auto-seeds should differ between runs" + def test_mapcalc_start_rand_with_explicit_seed(self): - """Test that mapcalc_start with rand() works with explicit seed value""" + """Test that mapcalc_start with rand() works with explicit seed value. + + Verifies: + 1. Seed parameter is passed to r.mapcalc + 2. Metadata contains the explicit seed value + """ p = gs.mapcalc_start( "rand_map_start_seed = rand(0.0, 1.0)", seed=12345, @@ -122,10 +477,29 @@ def test_mapcalc_start_rand_with_explicit_seed(self): ) returncode = p.wait() assert returncode == 0 + # Verify the map was created and seed was written to metadata + raster_info = gs.raster_info("rand_map_start_seed", env=self.session.env) + assert raster_info is not None + + # Verify seed parameter was passed to r.mapcalc (check metadata) + comments = raster_info.get("comments", "") + seed_found = extract_seed_from_comments(comments) + assert seed_found == 12345, ( + f"Expected seed=12345 in metadata, got seed={seed_found}" + ) def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self): - """Test that seed='auto' is handled for backwards compatibility (converted to None)""" - # seed="auto" should be converted to None internally, enabling auto-seeding + """Test that seed='auto' is handled for backwards compatibility. + + seed='auto' should be converted to None internally, enabling auto-seeding behavior. + + Verifies: + 1. seed='auto' does not pass literal 'auto' to r.mapcalc (it's converted before calling) + 2. Two runs with seed='auto' produce different results (auto-seeding enabled) + 3. Auto-generated seeds are recorded in metadata (consistent with no-seed behavior) + 4. Auto-generated seeds differ between runs + """ + # Create one map with seed='auto' p = gs.mapcalc_start( "rand_map_start_auto_seed = rand(0.0, 1.0)", seed="auto", @@ -133,10 +507,56 @@ def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self): ) returncode = p.wait() assert returncode == 0 + raster_info_1 = gs.raster_info("rand_map_start_auto_seed", env=self.session.env) + assert raster_info_1 is not None + comments_1 = raster_info_1.get("comments", "") + seed_1 = extract_seed_from_comments(comments_1) + + # Since seed='auto' converts to None, it should enable auto-seeding + # Auto-seeding records generated seed in metadata (same as no-seed case) + assert seed_1 is not None, ( + "seed='auto' should convert to None and enable auto-seeding, recording an auto-generated seed in metadata" + ) + + # Create second map with seed='auto' — should differ from first (different auto-seeds) + p = gs.mapcalc_start( + "rand_map_start_auto_seed_2 = rand(0.0, 1.0)", + seed="auto", + env=self.session.env, + ) + returncode = p.wait() + assert returncode == 0 + raster_info_2 = gs.raster_info( + "rand_map_start_auto_seed_2", env=self.session.env + ) + assert raster_info_2 is not None + comments_2 = raster_info_2.get("comments", "") + seed_2 = extract_seed_from_comments(comments_2) + + # Verify both runs have auto-generated seeds and they differ + assert seed_2 is not None, ( + "seed='auto' should enable auto-seeding, recording an auto-generated seed" + ) + assert seed_1 != seed_2, ( + "seed='auto' should produce different auto-generated seeds on consecutive runs" + ) + + # Verify actual raster data differs + array_1 = raster2numpy("rand_map_start_auto_seed") + array_2 = raster2numpy("rand_map_start_auto_seed_2") + assert not np.array_equal(array_1, array_2, equal_nan=True), ( + "seed='auto' should enable auto-seeding, producing different results each time" + ) def test_mapcalc_start_rand_with_explicit_seed_reproducible(self): - """Test that explicit seed in mapcalc_start produces reproducible results""" - # Create map with explicit seed + """Test that explicit seed in mapcalc_start produces reproducible results. + + Verifies: + 1. Same seed produces identical raster arrays (byte-for-byte reproducibility) + 2. Different seeds produce different results (seed actually has effect) + 3. Seed metadata is correctly recorded for all variations + """ + # Create two maps with same seed=12345 p = gs.mapcalc_start( "rand_map_start_seed = rand(0.0, 1.0)", seed=12345, @@ -144,7 +564,58 @@ def test_mapcalc_start_rand_with_explicit_seed_reproducible(self): ) returncode = p.wait() assert returncode == 0 - # Verify the map was created successfully with the seed value - raster_info = gs.raster_info("rand_map_start_seed", env=self.session.env) - assert raster_info is not None - assert "min" in raster_info + raster_info_1 = gs.raster_info("rand_map_start_seed", env=self.session.env) + assert raster_info_1 is not None + comments_1 = raster_info_1.get("comments", "") + seed_1 = extract_seed_from_comments(comments_1) + assert seed_1 == 12345, f"Expected seed=12345 in metadata, got seed={seed_1}" + + # Read actual raster data + array_1 = raster2numpy("rand_map_start_seed") + assert array_1 is not None + assert array_1.size > 0 + + # Create another map with the same seed + p = gs.mapcalc_start( + "rand_map_start_seed_2 = rand(0.0, 1.0)", + seed=12345, + env=self.session.env, + ) + returncode = p.wait() + assert returncode == 0 + raster_info_2 = gs.raster_info("rand_map_start_seed_2", env=self.session.env) + assert raster_info_2 is not None + comments_2 = raster_info_2.get("comments", "") + seed_2 = extract_seed_from_comments(comments_2) + assert seed_2 == 12345, f"Expected seed=12345 in metadata, got seed={seed_2}" + + # Read actual raster data + array_2 = raster2numpy("rand_map_start_seed_2") + assert array_2 is not None + assert array_2.size > 0 + + # Verify reproducibility: arrays must be identical (byte-for-byte) + assert np.array_equal(array_1, array_2, equal_nan=True), ( + "Maps with same seed should have identical cell values" + ) + + # Verify seed actually has an effect by comparing with different seed + p = gs.mapcalc_start( + "rand_map_start_seed_3 = rand(0.0, 1.0)", + seed=54321, + env=self.session.env, + ) + returncode = p.wait() + assert returncode == 0 + raster_info_3 = gs.raster_info("rand_map_start_seed_3", env=self.session.env) + comments_3 = raster_info_3.get("comments", "") + seed_3 = extract_seed_from_comments(comments_3) + assert seed_3 == 54321, f"Expected seed=54321 in metadata, got seed={seed_3}" + + array_3 = raster2numpy("rand_map_start_seed_3") + assert array_3 is not None + + # Different seed should produce different results + assert not np.array_equal(array_1, array_3, equal_nan=True), ( + "Maps with different seeds should have different cell values" + ) From eb0cd7664589d020a787ddb3416c632343ca71f1 Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Sun, 21 Dec 2025 12:10:34 +0530 Subject: [PATCH 15/21] replaced raster2numpy with environment aware helper and added windows time parameter --- .../tests/grass_script_raster_mapcalc_test.py | 305 ++++++++++-------- 1 file changed, 177 insertions(+), 128 deletions(-) diff --git a/python/grass/script/tests/grass_script_raster_mapcalc_test.py b/python/grass/script/tests/grass_script_raster_mapcalc_test.py index bf514bc7a15..17b85e534e8 100644 --- a/python/grass/script/tests/grass_script_raster_mapcalc_test.py +++ b/python/grass/script/tests/grass_script_raster_mapcalc_test.py @@ -3,9 +3,12 @@ import pytest import numpy as np import re +import tempfile +import os +import time import grass.script as gs -from grass.pygrass.raster import raster2numpy +import pathlib def extract_seed_from_comments(comments_str): @@ -26,6 +29,55 @@ def extract_seed_from_comments(comments_str): return int(match.group(1)) if match else None +def read_raster_as_array(mapname, env): + """Read a raster map as numpy array using r.out.ascii. + + This helper function works with isolated test sessions by respecting + the env parameter, unlike raster2numpy() which only reads from the + global GRASS environment. + + Args: + mapname: Name of the raster map to read + env: Environment dictionary for the GRASS session + + Returns: + numpy.ndarray: Array containing raster values (NaN for null cells) + """ + with tempfile.NamedTemporaryFile( + mode="w+", suffix=".txt", delete=False, encoding="utf-8" + ) as tmp: + tmp_path = tmp.name + + try: + # Export raster to ASCII format + gs.run_command( + "r.out.ascii", input=mapname, output=tmp_path, precision=10, env=env + ) + + # Read the ASCII file + with open(tmp_path, encoding="utf-8") as f: + lines = f.readlines() + + # Skip header lines (first 6 lines contain metadata) + data_lines = lines[6:] + + # Parse data into numpy array + data = [] + for line in data_lines: + line = line.strip() + if line: + # Convert '*' (null values) to NaN, everything else to float + row = [float(x) if x != "*" else np.nan for x in line.split()] + if row: + data.append(row) + + return np.array(data) + finally: + # Clean up temporary file + if pathlib.Path(tmp_path).exists(): + os.unlink(tmp_path) + + class TestMapcalcRandFunction: """Tests for rand() function in mapcalc @@ -53,9 +105,29 @@ def teardown_method(self): env=self.session.env, quiet=True, ) - except Exception: - # Silently ignore cleanup errors to avoid masking test failures - pass + except Exception as e: + # Log but don't fail - prevents masking test failures + import warnings + + warnings.warn(f"Cleanup failed: {e}") + + def verify_seed_in_metadata(self, mapname, expected_seed): + """Helper to verify seed value in raster metadata. + + Args: + mapname: Name of the raster map to check + expected_seed: Expected seed value + + Returns: + int: The actual seed value found in metadata + """ + info = gs.raster_info(mapname, env=self.session.env) + comments = info.get("comments", "") + actual_seed = extract_seed_from_comments(comments) + assert actual_seed == expected_seed, ( + f"Expected seed={expected_seed} in metadata, got seed={actual_seed}" + ) + return actual_seed def test_mapcalc_rand_autoseeded(self): """Test that rand() auto-seeds when no explicit seed is provided. @@ -63,25 +135,30 @@ def test_mapcalc_rand_autoseeded(self): Verifies: 1. Two consecutive runs without explicit seed produce different results (different auto-seeds) 2. Metadata contains different auto-generated seed values + + Note: On Windows with low-resolution timers, consecutive calls may occasionally + receive the same auto-seed. We add a small delay to ensure different timestamps. """ # r.mapcalc auto-seeds when no seed is given — two runs should differ gs.mapcalc( "rand_map_auto1 = rand(0.0, 1.0)", env=self.session.env, ) + + # Small delay to ensure different auto-seed on systems with low-resolution timers + # G_srand48_auto() uses microsecond precision, but on some Windows systems + # the timer granularity may be coarser + time.sleep(0.01) + gs.mapcalc( "rand_map_auto2 = rand(0.0, 1.0)", env=self.session.env, ) # Verify arrays differ (outcome of different auto-seeds) - array1 = raster2numpy("rand_map_auto1") - array2 = raster2numpy("rand_map_auto2") - assert array1 is not None - assert array2 is not None - assert array1.size > 0 - assert array2.size > 0 - assert not np.array_equal(array1, array2), ( + array1 = read_raster_as_array("rand_map_auto1", self.session.env) + array2 = read_raster_as_array("rand_map_auto2", self.session.env) + assert not np.array_equal(array1, array2, equal_nan=True), ( "Auto-seeded runs should produce different maps" ) @@ -100,7 +177,10 @@ def test_mapcalc_rand_autoseeded(self): assert seed2 is not None, ( "Auto-seed should be recorded in metadata for second run" ) - assert seed1 != seed2, "Auto-seeds should differ between runs" + assert seed1 != seed2, ( + f"Auto-seeds should differ between runs (got {seed1} and {seed2}). " + f"If this fails repeatedly, there may be a timer resolution issue." + ) def test_mapcalc_rand_with_explicit_seed(self): """Test that rand() works with explicit seed value. @@ -114,16 +194,23 @@ def test_mapcalc_rand_with_explicit_seed(self): seed=12345, env=self.session.env, ) - # Check that the map was created and seed was written to metadata - raster_info = gs.raster_info("rand_map_seed", env=self.session.env) - assert raster_info is not None + self.verify_seed_in_metadata("rand_map_seed", 12345) - # Verify seed parameter was passed to r.mapcalc (check metadata) - comments = raster_info.get("comments", "") - seed_found = extract_seed_from_comments(comments) - assert seed_found == 12345, ( - f"Expected seed=12345 in metadata, got seed={seed_found}" - ) + def test_mapcalc_rand_with_invalid_seed(self): + """Test that invalid seed values raise appropriate errors. + + Verifies: + 1. Invalid seed types (non-numeric strings) raise errors + 2. Invalid seeds are properly rejected by r.mapcalc + """ + from grass.exceptions import CalledModuleError + + with pytest.raises(CalledModuleError): + gs.mapcalc( + "rand_map_invalid = rand(0.0, 1.0)", + seed="invalid_string", + env=self.session.env, + ) def test_mapcalc_rand_with_seed_auto_backwards_compat(self): """Test that seed='auto' is handled for backwards compatibility. @@ -144,7 +231,6 @@ def test_mapcalc_rand_with_seed_auto_backwards_compat(self): env=self.session.env, ) raster_info_1 = gs.raster_info("rand_map_auto_seed", env=self.session.env) - assert raster_info_1 is not None comments_1 = raster_info_1.get("comments", "") seed_1 = extract_seed_from_comments(comments_1) @@ -154,6 +240,9 @@ def test_mapcalc_rand_with_seed_auto_backwards_compat(self): "seed='auto' should convert to None and enable auto-seeding, recording an auto-generated seed in metadata" ) + # Small delay to ensure different auto-seed + time.sleep(0.01) + # Create second map with seed='auto' — should differ from first (different auto-seeds) gs.mapcalc( "rand_map_auto_seed_2 = rand(0.0, 1.0)", @@ -161,7 +250,6 @@ def test_mapcalc_rand_with_seed_auto_backwards_compat(self): env=self.session.env, ) raster_info_2 = gs.raster_info("rand_map_auto_seed_2", env=self.session.env) - assert raster_info_2 is not None comments_2 = raster_info_2.get("comments", "") seed_2 = extract_seed_from_comments(comments_2) @@ -174,8 +262,8 @@ def test_mapcalc_rand_with_seed_auto_backwards_compat(self): ) # Verify actual raster data differs - array_1 = raster2numpy("rand_map_auto_seed") - array_2 = raster2numpy("rand_map_auto_seed_2") + array_1 = read_raster_as_array("rand_map_auto_seed", self.session.env) + array_2 = read_raster_as_array("rand_map_auto_seed_2", self.session.env) assert not np.array_equal(array_1, array_2, equal_nan=True), ( "seed='auto' should enable auto-seeding, producing different results each time" ) @@ -194,16 +282,8 @@ def test_mapcalc_rand_with_explicit_seed_reproducible(self): seed=12345, env=self.session.env, ) - raster_info_1 = gs.raster_info("rand_map_seed", env=self.session.env) - assert raster_info_1 is not None - comments_1 = raster_info_1.get("comments", "") - seed_1 = extract_seed_from_comments(comments_1) - assert seed_1 == 12345, f"Expected seed=12345 in metadata, got seed={seed_1}" - - # Read actual raster data - array_1 = raster2numpy("rand_map_seed") - assert array_1 is not None - assert array_1.size > 0 + self.verify_seed_in_metadata("rand_map_seed", 12345) + array_1 = read_raster_as_array("rand_map_seed", self.session.env) # Create another map with the same seed gs.mapcalc( @@ -211,16 +291,8 @@ def test_mapcalc_rand_with_explicit_seed_reproducible(self): seed=12345, env=self.session.env, ) - raster_info_2 = gs.raster_info("rand_map_seed_2", env=self.session.env) - assert raster_info_2 is not None - comments_2 = raster_info_2.get("comments", "") - seed_2 = extract_seed_from_comments(comments_2) - assert seed_2 == 12345, f"Expected seed=12345 in metadata, got seed={seed_2}" - - # Read actual raster data - array_2 = raster2numpy("rand_map_seed_2") - assert array_2 is not None - assert array_2.size > 0 + self.verify_seed_in_metadata("rand_map_seed_2", 12345) + array_2 = read_raster_as_array("rand_map_seed_2", self.session.env) # Verify reproducibility: arrays must be identical (byte-for-byte) assert np.array_equal(array_1, array_2, equal_nan=True), ( @@ -233,13 +305,8 @@ def test_mapcalc_rand_with_explicit_seed_reproducible(self): seed=54321, env=self.session.env, ) - raster_info_3 = gs.raster_info("rand_map_seed_3", env=self.session.env) - comments_3 = raster_info_3.get("comments", "") - seed_3 = extract_seed_from_comments(comments_3) - assert seed_3 == 54321, f"Expected seed=54321 in metadata, got seed={seed_3}" - - array_3 = raster2numpy("rand_map_seed_3") - assert array_3 is not None + self.verify_seed_in_metadata("rand_map_seed_3", 54321) + array_3 = read_raster_as_array("rand_map_seed_3", self.session.env) # Different seed should produce different results assert not np.array_equal(array_1, array_3, equal_nan=True), ( @@ -259,13 +326,8 @@ def test_mapcalc_rand_with_seed_zero(self): seed=0, env=self.session.env, ) - raster_info_1 = gs.raster_info("rand_map_seed_0", env=self.session.env) - assert raster_info_1 is not None - comments_1 = raster_info_1.get("comments", "") - seed_1 = extract_seed_from_comments(comments_1) - assert seed_1 == 0, f"Expected seed=0 in metadata, got seed={seed_1}" - - array_1 = raster2numpy("rand_map_seed_0") + self.verify_seed_in_metadata("rand_map_seed_0", 0) + array_1 = read_raster_as_array("rand_map_seed_0", self.session.env) # Create another map with seed=0 — should be reproducible gs.mapcalc( @@ -273,7 +335,7 @@ def test_mapcalc_rand_with_seed_zero(self): seed=0, env=self.session.env, ) - array_2 = raster2numpy("rand_map_seed_0_2") + array_2 = read_raster_as_array("rand_map_seed_0_2", self.session.env) assert np.array_equal(array_1, array_2, equal_nan=True), ( "seed=0 should produce reproducible results" ) @@ -283,7 +345,7 @@ def test_mapcalc_rand_with_seed_zero(self): "rand_map_seed_0_auto = rand(0.0, 1.0)", env=self.session.env, ) - array_auto = raster2numpy("rand_map_seed_0_auto") + array_auto = read_raster_as_array("rand_map_seed_0_auto", self.session.env) assert not np.array_equal(array_1, array_auto, equal_nan=True), ( "seed=0 should produce different results than auto-seeding" ) @@ -301,13 +363,8 @@ def test_mapcalc_rand_with_negative_seed(self): seed=-42, env=self.session.env, ) - raster_info_1 = gs.raster_info("rand_map_neg_seed", env=self.session.env) - assert raster_info_1 is not None - comments_1 = raster_info_1.get("comments", "") - seed_1 = extract_seed_from_comments(comments_1) - assert seed_1 == -42, f"Expected seed=-42 in metadata, got seed={seed_1}" - - array_1 = raster2numpy("rand_map_neg_seed") + self.verify_seed_in_metadata("rand_map_neg_seed", -42) + array_1 = read_raster_as_array("rand_map_neg_seed", self.session.env) # Create another map with same negative seed — should be reproducible gs.mapcalc( @@ -315,7 +372,7 @@ def test_mapcalc_rand_with_negative_seed(self): seed=-42, env=self.session.env, ) - array_2 = raster2numpy("rand_map_neg_seed_2") + array_2 = read_raster_as_array("rand_map_neg_seed_2", self.session.env) assert np.array_equal(array_1, array_2, equal_nan=True), ( "Negative seed should produce reproducible results" ) @@ -326,7 +383,7 @@ def test_mapcalc_rand_with_negative_seed(self): seed=-99, env=self.session.env, ) - array_3 = raster2numpy("rand_map_neg_seed_3") + array_3 = read_raster_as_array("rand_map_neg_seed_3", self.session.env) assert not np.array_equal(array_1, array_3, equal_nan=True), ( "Different negative seeds should produce different results" ) @@ -346,15 +403,8 @@ def test_mapcalc_rand_with_large_seed(self): seed=large_seed, env=self.session.env, ) - raster_info_1 = gs.raster_info("rand_map_large_seed", env=self.session.env) - assert raster_info_1 is not None - comments_1 = raster_info_1.get("comments", "") - seed_1 = extract_seed_from_comments(comments_1) - assert seed_1 == large_seed, ( - f"Expected seed={large_seed} in metadata, got seed={seed_1}" - ) - - array_1 = raster2numpy("rand_map_large_seed") + self.verify_seed_in_metadata("rand_map_large_seed", large_seed) + array_1 = read_raster_as_array("rand_map_large_seed", self.session.env) # Create another map with same large seed — should be reproducible gs.mapcalc( @@ -362,7 +412,7 @@ def test_mapcalc_rand_with_large_seed(self): seed=large_seed, env=self.session.env, ) - array_2 = raster2numpy("rand_map_large_seed_2") + array_2 = read_raster_as_array("rand_map_large_seed_2", self.session.env) assert np.array_equal(array_1, array_2, equal_nan=True), ( "Large seed should produce reproducible results" ) @@ -373,7 +423,7 @@ def test_mapcalc_rand_with_large_seed(self): seed=12345, env=self.session.env, ) - array_regular = raster2numpy("rand_map_regular_seed") + array_regular = read_raster_as_array("rand_map_regular_seed", self.session.env) assert not np.array_equal(array_1, array_regular, equal_nan=True), ( "Large seed should produce different results than regular seed" ) @@ -410,9 +460,29 @@ def teardown_method(self): env=self.session.env, quiet=True, ) - except Exception: - # Silently ignore cleanup errors to avoid masking test failures - pass + except Exception as e: + # Log but don't fail - prevents masking test failures + import warnings + + warnings.warn(f"Cleanup failed: {e}") + + def verify_seed_in_metadata(self, mapname, expected_seed): + """Helper to verify seed value in raster metadata + + Args: + mapname: Name of the raster map + expected_seed: Expected seed value + + Returns: + The actual seed value found + """ + raster_info = gs.raster_info(mapname, env=self.session.env) + comments = raster_info.get("comments", "") + actual_seed = extract_seed_from_comments(comments) + assert actual_seed == expected_seed, ( + f"Expected seed={expected_seed} in metadata, got seed={actual_seed}" + ) + return actual_seed def test_mapcalc_start_rand_autoseeded(self): """Test that mapcalc_start with rand() auto-seeds when no explicit seed is provided. @@ -428,6 +498,10 @@ def test_mapcalc_start_rand_autoseeded(self): ) returncode = p.wait() assert returncode == 0 + + # Small delay to ensure different auto-seed + time.sleep(0.01) + p = gs.mapcalc_start( "rand_map_start_auto2 = rand(0.0, 1.0)", env=self.session.env, @@ -436,13 +510,9 @@ def test_mapcalc_start_rand_autoseeded(self): assert returncode == 0 # Verify outcome: arrays differ (different auto-seeds) - array1 = raster2numpy("rand_map_start_auto1") - array2 = raster2numpy("rand_map_start_auto2") - assert array1 is not None - assert array2 is not None - assert array1.size > 0 - assert array2.size > 0 - assert not np.array_equal(array1, array2), ( + array1 = read_raster_as_array("rand_map_start_auto1", self.session.env) + array2 = read_raster_as_array("rand_map_start_auto2", self.session.env) + assert not np.array_equal(array1, array2, equal_nan=True), ( "Auto-seeded runs should produce different maps" ) @@ -461,7 +531,9 @@ def test_mapcalc_start_rand_autoseeded(self): assert seed2 is not None, ( "Auto-seed should be recorded in metadata for second run" ) - assert seed1 != seed2, "Auto-seeds should differ between runs" + assert seed1 != seed2, ( + f"Auto-seeds should differ between runs (got {seed1} and {seed2})" + ) def test_mapcalc_start_rand_with_explicit_seed(self): """Test that mapcalc_start with rand() works with explicit seed value. @@ -478,15 +550,7 @@ def test_mapcalc_start_rand_with_explicit_seed(self): returncode = p.wait() assert returncode == 0 # Verify the map was created and seed was written to metadata - raster_info = gs.raster_info("rand_map_start_seed", env=self.session.env) - assert raster_info is not None - - # Verify seed parameter was passed to r.mapcalc (check metadata) - comments = raster_info.get("comments", "") - seed_found = extract_seed_from_comments(comments) - assert seed_found == 12345, ( - f"Expected seed=12345 in metadata, got seed={seed_found}" - ) + self.verify_seed_in_metadata("rand_map_start_seed", 12345) def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self): """Test that seed='auto' is handled for backwards compatibility. @@ -508,7 +572,6 @@ def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self): returncode = p.wait() assert returncode == 0 raster_info_1 = gs.raster_info("rand_map_start_auto_seed", env=self.session.env) - assert raster_info_1 is not None comments_1 = raster_info_1.get("comments", "") seed_1 = extract_seed_from_comments(comments_1) @@ -518,6 +581,9 @@ def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self): "seed='auto' should convert to None and enable auto-seeding, recording an auto-generated seed in metadata" ) + # Small delay to ensure different auto-seed + time.sleep(0.01) + # Create second map with seed='auto' — should differ from first (different auto-seeds) p = gs.mapcalc_start( "rand_map_start_auto_seed_2 = rand(0.0, 1.0)", @@ -529,7 +595,6 @@ def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self): raster_info_2 = gs.raster_info( "rand_map_start_auto_seed_2", env=self.session.env ) - assert raster_info_2 is not None comments_2 = raster_info_2.get("comments", "") seed_2 = extract_seed_from_comments(comments_2) @@ -538,12 +603,12 @@ def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self): "seed='auto' should enable auto-seeding, recording an auto-generated seed" ) assert seed_1 != seed_2, ( - "seed='auto' should produce different auto-generated seeds on consecutive runs" + f"seed='auto' should produce different auto-generated seeds (got {seed_1} and {seed_2})" ) # Verify actual raster data differs - array_1 = raster2numpy("rand_map_start_auto_seed") - array_2 = raster2numpy("rand_map_start_auto_seed_2") + array_1 = read_raster_as_array("rand_map_start_auto_seed", self.session.env) + array_2 = read_raster_as_array("rand_map_start_auto_seed_2", self.session.env) assert not np.array_equal(array_1, array_2, equal_nan=True), ( "seed='auto' should enable auto-seeding, producing different results each time" ) @@ -564,16 +629,10 @@ def test_mapcalc_start_rand_with_explicit_seed_reproducible(self): ) returncode = p.wait() assert returncode == 0 - raster_info_1 = gs.raster_info("rand_map_start_seed", env=self.session.env) - assert raster_info_1 is not None - comments_1 = raster_info_1.get("comments", "") - seed_1 = extract_seed_from_comments(comments_1) - assert seed_1 == 12345, f"Expected seed=12345 in metadata, got seed={seed_1}" + self.verify_seed_in_metadata("rand_map_start_seed", 12345) # Read actual raster data - array_1 = raster2numpy("rand_map_start_seed") - assert array_1 is not None - assert array_1.size > 0 + array_1 = read_raster_as_array("rand_map_start_seed", self.session.env) # Create another map with the same seed p = gs.mapcalc_start( @@ -583,16 +642,10 @@ def test_mapcalc_start_rand_with_explicit_seed_reproducible(self): ) returncode = p.wait() assert returncode == 0 - raster_info_2 = gs.raster_info("rand_map_start_seed_2", env=self.session.env) - assert raster_info_2 is not None - comments_2 = raster_info_2.get("comments", "") - seed_2 = extract_seed_from_comments(comments_2) - assert seed_2 == 12345, f"Expected seed=12345 in metadata, got seed={seed_2}" + self.verify_seed_in_metadata("rand_map_start_seed_2", 12345) # Read actual raster data - array_2 = raster2numpy("rand_map_start_seed_2") - assert array_2 is not None - assert array_2.size > 0 + array_2 = read_raster_as_array("rand_map_start_seed_2", self.session.env) # Verify reproducibility: arrays must be identical (byte-for-byte) assert np.array_equal(array_1, array_2, equal_nan=True), ( @@ -607,13 +660,9 @@ def test_mapcalc_start_rand_with_explicit_seed_reproducible(self): ) returncode = p.wait() assert returncode == 0 - raster_info_3 = gs.raster_info("rand_map_start_seed_3", env=self.session.env) - comments_3 = raster_info_3.get("comments", "") - seed_3 = extract_seed_from_comments(comments_3) - assert seed_3 == 54321, f"Expected seed=54321 in metadata, got seed={seed_3}" + self.verify_seed_in_metadata("rand_map_start_seed_3", 54321) - array_3 = raster2numpy("rand_map_start_seed_3") - assert array_3 is not None + array_3 = read_raster_as_array("rand_map_start_seed_3", self.session.env) # Different seed should produce different results assert not np.array_equal(array_1, array_3, equal_nan=True), ( From 62b7a322eb70293e7723e2ec72f7ed9b265641eb Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Mon, 22 Dec 2025 18:25:51 +0530 Subject: [PATCH 16/21] time delay and temp file management --- .../tests/grass_script_raster_mapcalc_test.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/python/grass/script/tests/grass_script_raster_mapcalc_test.py b/python/grass/script/tests/grass_script_raster_mapcalc_test.py index 17b85e534e8..7e75299797b 100644 --- a/python/grass/script/tests/grass_script_raster_mapcalc_test.py +++ b/python/grass/script/tests/grass_script_raster_mapcalc_test.py @@ -51,7 +51,7 @@ def read_raster_as_array(mapname, env): try: # Export raster to ASCII format gs.run_command( - "r.out.ascii", input=mapname, output=tmp_path, precision=10, env=env + "r.out.ascii", input=mapname, output=tmp_path, precision=10, env=env, overwrite=True ) # Read the ASCII file @@ -145,10 +145,10 @@ def test_mapcalc_rand_autoseeded(self): env=self.session.env, ) - # Small delay to ensure different auto-seed on systems with low-resolution timers + # Increased delay to ensure different auto-seed on systems with low-resolution timers # G_srand48_auto() uses microsecond precision, but on some Windows systems # the timer granularity may be coarser - time.sleep(0.01) + time.sleep(0.1) gs.mapcalc( "rand_map_auto2 = rand(0.0, 1.0)", @@ -240,8 +240,8 @@ def test_mapcalc_rand_with_seed_auto_backwards_compat(self): "seed='auto' should convert to None and enable auto-seeding, recording an auto-generated seed in metadata" ) - # Small delay to ensure different auto-seed - time.sleep(0.01) + # Increased delay to ensure different auto-seed + time.sleep(0.1) # Create second map with seed='auto' — should differ from first (different auto-seeds) gs.mapcalc( @@ -499,8 +499,8 @@ def test_mapcalc_start_rand_autoseeded(self): returncode = p.wait() assert returncode == 0 - # Small delay to ensure different auto-seed - time.sleep(0.01) + # Increased delay to ensure different auto-seed + time.sleep(0.1) p = gs.mapcalc_start( "rand_map_start_auto2 = rand(0.0, 1.0)", @@ -581,8 +581,8 @@ def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self): "seed='auto' should convert to None and enable auto-seeding, recording an auto-generated seed in metadata" ) - # Small delay to ensure different auto-seed - time.sleep(0.01) + # Increased delay to ensure different auto-seed + time.sleep(0.1) # Create second map with seed='auto' — should differ from first (different auto-seeds) p = gs.mapcalc_start( From f60af2a7ff65af25aab175d0978d6f53294cebbc Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Wed, 24 Dec 2025 21:14:22 +0530 Subject: [PATCH 17/21] mark invalid seed test as xfail --- .../tests/grass_script_raster_mapcalc_test.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/python/grass/script/tests/grass_script_raster_mapcalc_test.py b/python/grass/script/tests/grass_script_raster_mapcalc_test.py index 7e75299797b..aa1069df2d6 100644 --- a/python/grass/script/tests/grass_script_raster_mapcalc_test.py +++ b/python/grass/script/tests/grass_script_raster_mapcalc_test.py @@ -51,7 +51,12 @@ def read_raster_as_array(mapname, env): try: # Export raster to ASCII format gs.run_command( - "r.out.ascii", input=mapname, output=tmp_path, precision=10, env=env, overwrite=True + "r.out.ascii", + input=mapname, + output=tmp_path, + precision=10, + env=env, + overwrite=True, ) # Read the ASCII file @@ -196,6 +201,9 @@ def test_mapcalc_rand_with_explicit_seed(self): ) self.verify_seed_in_metadata("rand_map_seed", 12345) + @pytest.mark.xfail( + reason="r.mapcalc currently accepts invalid seed strings instead of rejecting them with ScriptError" + ) def test_mapcalc_rand_with_invalid_seed(self): """Test that invalid seed values raise appropriate errors. @@ -203,9 +211,9 @@ def test_mapcalc_rand_with_invalid_seed(self): 1. Invalid seed types (non-numeric strings) raise errors 2. Invalid seeds are properly rejected by r.mapcalc """ - from grass.exceptions import CalledModuleError + from grass.exceptions import ScriptError - with pytest.raises(CalledModuleError): + with pytest.raises(ScriptError): gs.mapcalc( "rand_map_invalid = rand(0.0, 1.0)", seed="invalid_string", From a5bedb57526c3986b2ebedf06452c19058991abf Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Sun, 4 Jan 2026 13:13:57 +0530 Subject: [PATCH 18/21] Refactor mapcalc rand tests: parametrize edge seed tests for better efficiency --- .../tests/grass_script_raster_mapcalc_test.py | 255 +++++------------- 1 file changed, 69 insertions(+), 186 deletions(-) diff --git a/python/grass/script/tests/grass_script_raster_mapcalc_test.py b/python/grass/script/tests/grass_script_raster_mapcalc_test.py index aa1069df2d6..ede56dc20c1 100644 --- a/python/grass/script/tests/grass_script_raster_mapcalc_test.py +++ b/python/grass/script/tests/grass_script_raster_mapcalc_test.py @@ -3,12 +3,11 @@ import pytest import numpy as np import re -import tempfile import os import time import grass.script as gs -import pathlib +import grass.script.array as garray def extract_seed_from_comments(comments_str): @@ -29,92 +28,21 @@ def extract_seed_from_comments(comments_str): return int(match.group(1)) if match else None -def read_raster_as_array(mapname, env): - """Read a raster map as numpy array using r.out.ascii. - - This helper function works with isolated test sessions by respecting - the env parameter, unlike raster2numpy() which only reads from the - global GRASS environment. - - Args: - mapname: Name of the raster map to read - env: Environment dictionary for the GRASS session - - Returns: - numpy.ndarray: Array containing raster values (NaN for null cells) - """ - with tempfile.NamedTemporaryFile( - mode="w+", suffix=".txt", delete=False, encoding="utf-8" - ) as tmp: - tmp_path = tmp.name - - try: - # Export raster to ASCII format - gs.run_command( - "r.out.ascii", - input=mapname, - output=tmp_path, - precision=10, - env=env, - overwrite=True, - ) - - # Read the ASCII file - with open(tmp_path, encoding="utf-8") as f: - lines = f.readlines() - - # Skip header lines (first 6 lines contain metadata) - data_lines = lines[6:] - - # Parse data into numpy array - data = [] - for line in data_lines: - line = line.strip() - if line: - # Convert '*' (null values) to NaN, everything else to float - row = [float(x) if x != "*" else np.nan for x in line.split()] - if row: - data.append(row) - - return np.array(data) - finally: - # Clean up temporary file - if pathlib.Path(tmp_path).exists(): - os.unlink(tmp_path) - - class TestMapcalcRandFunction: """Tests for rand() function in mapcalc r.mapcalc auto-seeds when no seed is provided. Explicit seed can be used for reproducibility. """ - @pytest.fixture(autouse=True) - def setup_region(self, session_2x2): - """Setup region for tests""" - self.session = session_2x2 - gs.run_command("g.region", rows=10, cols=10, env=self.session.env) - - def teardown_method(self): - """Clean up maps after each test using pattern matching""" - try: - maps = gs.list_strings( - type="raster", pattern="rand_map*", env=self.session.env - ) - if maps: - gs.run_command( - "g.remove", - type="raster", - flags="f", - name=",".join(maps), - env=self.session.env, - quiet=True, - ) - except Exception as e: - # Log but don't fail - prevents masking test failures - import warnings - - warnings.warn(f"Cleanup failed: {e}") + @pytest.fixture(scope="module", autouse=True) + def setup_session(self, tmp_path_factory): + """Setup temporary GRASS session for tests""" + tmp_path = tmp_path_factory.mktemp("mapcalc_rand_test") + gs.create_project(tmp_path, overwrite=True) + with gs.setup.init(tmp_path, env=os.environ.copy()) as session: + gs.run_command("g.region", rows=2, cols=2, env=session.env) + self.session = session + yield def verify_seed_in_metadata(self, mapname, expected_seed): """Helper to verify seed value in raster metadata. @@ -150,10 +78,11 @@ def test_mapcalc_rand_autoseeded(self): env=self.session.env, ) - # Increased delay to ensure different auto-seed on systems with low-resolution timers + # Small delay to ensure different auto-seed on systems with low-resolution timers # G_srand48_auto() uses microsecond precision, but on some Windows systems - # the timer granularity may be coarser - time.sleep(0.1) + # the timer granularity may be coarser. Since tests run sequentially (not parallel), + # 0.01s is usually sufficient. + time.sleep(0.01) gs.mapcalc( "rand_map_auto2 = rand(0.0, 1.0)", @@ -161,8 +90,8 @@ def test_mapcalc_rand_autoseeded(self): ) # Verify arrays differ (outcome of different auto-seeds) - array1 = read_raster_as_array("rand_map_auto1", self.session.env) - array2 = read_raster_as_array("rand_map_auto2", self.session.env) + array1 = garray.array("rand_map_auto1", env=self.session.env) + array2 = garray.array("rand_map_auto2", env=self.session.env) assert not np.array_equal(array1, array2, equal_nan=True), ( "Auto-seeded runs should produce different maps" ) @@ -270,8 +199,8 @@ def test_mapcalc_rand_with_seed_auto_backwards_compat(self): ) # Verify actual raster data differs - array_1 = read_raster_as_array("rand_map_auto_seed", self.session.env) - array_2 = read_raster_as_array("rand_map_auto_seed_2", self.session.env) + array_1 = garray.array("rand_map_auto_seed", env=self.session.env) + array_2 = garray.array("rand_map_auto_seed_2", env=self.session.env) assert not np.array_equal(array_1, array_2, equal_nan=True), ( "seed='auto' should enable auto-seeding, producing different results each time" ) @@ -291,7 +220,7 @@ def test_mapcalc_rand_with_explicit_seed_reproducible(self): env=self.session.env, ) self.verify_seed_in_metadata("rand_map_seed", 12345) - array_1 = read_raster_as_array("rand_map_seed", self.session.env) + array_1 = garray.array("rand_map_seed", env=self.session.env) # Create another map with the same seed gs.mapcalc( @@ -300,7 +229,7 @@ def test_mapcalc_rand_with_explicit_seed_reproducible(self): env=self.session.env, ) self.verify_seed_in_metadata("rand_map_seed_2", 12345) - array_2 = read_raster_as_array("rand_map_seed_2", self.session.env) + array_2 = garray.array("rand_map_seed_2", env=self.session.env) # Verify reproducibility: arrays must be identical (byte-for-byte) assert np.array_equal(array_1, array_2, equal_nan=True), ( @@ -314,7 +243,7 @@ def test_mapcalc_rand_with_explicit_seed_reproducible(self): env=self.session.env, ) self.verify_seed_in_metadata("rand_map_seed_3", 54321) - array_3 = read_raster_as_array("rand_map_seed_3", self.session.env) + array_3 = garray.array("rand_map_seed_3", env=self.session.env) # Different seed should produce different results assert not np.array_equal(array_1, array_3, equal_nan=True), ( @@ -335,7 +264,7 @@ def test_mapcalc_rand_with_seed_zero(self): env=self.session.env, ) self.verify_seed_in_metadata("rand_map_seed_0", 0) - array_1 = read_raster_as_array("rand_map_seed_0", self.session.env) + array_1 = garray.array("rand_map_seed_0", env=self.session.env) # Create another map with seed=0 — should be reproducible gs.mapcalc( @@ -343,7 +272,7 @@ def test_mapcalc_rand_with_seed_zero(self): seed=0, env=self.session.env, ) - array_2 = read_raster_as_array("rand_map_seed_0_2", self.session.env) + array_2 = garray.array("rand_map_seed_0_2", env=self.session.env) assert np.array_equal(array_1, array_2, equal_nan=True), ( "seed=0 should produce reproducible results" ) @@ -353,87 +282,58 @@ def test_mapcalc_rand_with_seed_zero(self): "rand_map_seed_0_auto = rand(0.0, 1.0)", env=self.session.env, ) - array_auto = read_raster_as_array("rand_map_seed_0_auto", self.session.env) + array_auto = garray.array("rand_map_seed_0_auto", env=self.session.env) assert not np.array_equal(array_1, array_auto, equal_nan=True), ( "seed=0 should produce different results than auto-seeding" ) - def test_mapcalc_rand_with_negative_seed(self): - """Test that negative seeds are handled correctly. - - Verifies: - 1. Negative seed values are valid and produce reproducible results - 2. Different negative seeds produce different results - """ - # Create map with negative seed - gs.mapcalc( - "rand_map_neg_seed = rand(0.0, 1.0)", - seed=-42, - env=self.session.env, - ) - self.verify_seed_in_metadata("rand_map_neg_seed", -42) - array_1 = read_raster_as_array("rand_map_neg_seed", self.session.env) - - # Create another map with same negative seed — should be reproducible - gs.mapcalc( - "rand_map_neg_seed_2 = rand(0.0, 1.0)", - seed=-42, - env=self.session.env, - ) - array_2 = read_raster_as_array("rand_map_neg_seed_2", self.session.env) - assert np.array_equal(array_1, array_2, equal_nan=True), ( - "Negative seed should produce reproducible results" - ) - - # Different negative seed should produce different results - gs.mapcalc( - "rand_map_neg_seed_3 = rand(0.0, 1.0)", - seed=-99, - env=self.session.env, - ) - array_3 = read_raster_as_array("rand_map_neg_seed_3", self.session.env) - assert not np.array_equal(array_1, array_3, equal_nan=True), ( - "Different negative seeds should produce different results" - ) - - def test_mapcalc_rand_with_large_seed(self): - """Test that very large seed values are handled correctly. + @pytest.mark.parametrize( + ("seed_value", "description"), + [ + (-42, "negative seed"), + (2147483647, "large seed"), # Max 32-bit signed integer + ], + ) + def test_mapcalc_rand_with_edge_seeds(self, seed_value, description): + """Test that edge seed values (negative, large) are handled correctly. Verifies: - 1. Large seed values are valid and produce reproducible results - 2. Large seed differs from regular seeds (seed value actually has effect) + 1. Edge seed values are valid and produce reproducible results + 2. Different edge seeds produce different results """ - large_seed = 2147483647 # Max 32-bit signed integer + map_name_1 = f"rand_map_{description.replace(' ', '_')}_1" + map_name_2 = f"rand_map_{description.replace(' ', '_')}_2" + map_name_diff = f"rand_map_{description.replace(' ', '_')}_diff" - # Create map with large seed + # Create map with edge seed gs.mapcalc( - "rand_map_large_seed = rand(0.0, 1.0)", - seed=large_seed, + f"{map_name_1} = rand(0.0, 1.0)", + seed=seed_value, env=self.session.env, ) - self.verify_seed_in_metadata("rand_map_large_seed", large_seed) - array_1 = read_raster_as_array("rand_map_large_seed", self.session.env) + self.verify_seed_in_metadata(map_name_1, seed_value) + array_1 = garray.array(map_name_1, env=self.session.env) - # Create another map with same large seed — should be reproducible + # Create another map with same edge seed — should be reproducible gs.mapcalc( - "rand_map_large_seed_2 = rand(0.0, 1.0)", - seed=large_seed, + f"{map_name_2} = rand(0.0, 1.0)", + seed=seed_value, env=self.session.env, ) - array_2 = read_raster_as_array("rand_map_large_seed_2", self.session.env) + array_2 = garray.array(map_name_2, env=self.session.env) assert np.array_equal(array_1, array_2, equal_nan=True), ( - "Large seed should produce reproducible results" + f"{description} should produce reproducible results" ) - # Verify large seed differs from regular seed (seed value has effect) + # Different seed should produce different results (compare with seed=12345) gs.mapcalc( - "rand_map_regular_seed = rand(0.0, 1.0)", + f"{map_name_diff} = rand(0.0, 1.0)", seed=12345, env=self.session.env, ) - array_regular = read_raster_as_array("rand_map_regular_seed", self.session.env) - assert not np.array_equal(array_1, array_regular, equal_nan=True), ( - "Large seed should produce different results than regular seed" + array_diff = garray.array(map_name_diff, env=self.session.env) + assert not np.array_equal(array_1, array_diff, equal_nan=True), ( + f"{description} should produce different results than regular seed" ) @@ -447,32 +347,15 @@ class TestMapcalcStartRandFunction: Testing the mechanism once is sufficient to ensure both code paths work correctly. """ - @pytest.fixture(autouse=True) - def setup_region(self, session_2x2): - """Setup region for tests""" - self.session = session_2x2 - gs.run_command("g.region", rows=10, cols=10, env=self.session.env) - - def teardown_method(self): - """Clean up maps after each test using pattern matching""" - try: - maps = gs.list_strings( - type="raster", pattern="rand_map_start*", env=self.session.env - ) - if maps: - gs.run_command( - "g.remove", - type="raster", - flags="f", - name=",".join(maps), - env=self.session.env, - quiet=True, - ) - except Exception as e: - # Log but don't fail - prevents masking test failures - import warnings - - warnings.warn(f"Cleanup failed: {e}") + @pytest.fixture(scope="module", autouse=True) + def setup_session(self, tmp_path_factory): + """Setup temporary GRASS session for tests""" + tmp_path = tmp_path_factory.mktemp("mapcalc_start_rand_test") + gs.create_project(tmp_path) + with gs.setup.init(tmp_path, env=os.environ.copy()) as session: + gs.run_command("g.region", rows=2, cols=2, env=session.env) + self.session = session + yield def verify_seed_in_metadata(self, mapname, expected_seed): """Helper to verify seed value in raster metadata @@ -518,8 +401,8 @@ def test_mapcalc_start_rand_autoseeded(self): assert returncode == 0 # Verify outcome: arrays differ (different auto-seeds) - array1 = read_raster_as_array("rand_map_start_auto1", self.session.env) - array2 = read_raster_as_array("rand_map_start_auto2", self.session.env) + array1 = garray.array("rand_map_start_auto1", env=self.session.env) + array2 = garray.array("rand_map_start_auto2", env=self.session.env) assert not np.array_equal(array1, array2, equal_nan=True), ( "Auto-seeded runs should produce different maps" ) @@ -615,8 +498,8 @@ def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self): ) # Verify actual raster data differs - array_1 = read_raster_as_array("rand_map_start_auto_seed", self.session.env) - array_2 = read_raster_as_array("rand_map_start_auto_seed_2", self.session.env) + array_1 = garray.array("rand_map_start_auto_seed", env=self.session.env) + array_2 = garray.array("rand_map_start_auto_seed_2", env=self.session.env) assert not np.array_equal(array_1, array_2, equal_nan=True), ( "seed='auto' should enable auto-seeding, producing different results each time" ) @@ -640,7 +523,7 @@ def test_mapcalc_start_rand_with_explicit_seed_reproducible(self): self.verify_seed_in_metadata("rand_map_start_seed", 12345) # Read actual raster data - array_1 = read_raster_as_array("rand_map_start_seed", self.session.env) + array_1 = garray.array("rand_map_start_seed", env=self.session.env) # Create another map with the same seed p = gs.mapcalc_start( @@ -653,7 +536,7 @@ def test_mapcalc_start_rand_with_explicit_seed_reproducible(self): self.verify_seed_in_metadata("rand_map_start_seed_2", 12345) # Read actual raster data - array_2 = read_raster_as_array("rand_map_start_seed_2", self.session.env) + array_2 = garray.array("rand_map_start_seed_2", env=self.session.env) # Verify reproducibility: arrays must be identical (byte-for-byte) assert np.array_equal(array_1, array_2, equal_nan=True), ( @@ -670,7 +553,7 @@ def test_mapcalc_start_rand_with_explicit_seed_reproducible(self): assert returncode == 0 self.verify_seed_in_metadata("rand_map_start_seed_3", 54321) - array_3 = read_raster_as_array("rand_map_start_seed_3", self.session.env) + array_3 = garray.array("rand_map_start_seed_3", env=self.session.env) # Different seed should produce different results assert not np.array_equal(array_1, array_3, equal_nan=True), ( From 3a9f3c2e0386ed8ecd3af5079d469ebb68b1a9d3 Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Wed, 7 Jan 2026 09:16:16 +0530 Subject: [PATCH 19/21] scope and test refactor --- .../tests/grass_script_raster_mapcalc_test.py | 168 +++--------------- 1 file changed, 20 insertions(+), 148 deletions(-) diff --git a/python/grass/script/tests/grass_script_raster_mapcalc_test.py b/python/grass/script/tests/grass_script_raster_mapcalc_test.py index ede56dc20c1..83818cfe7c7 100644 --- a/python/grass/script/tests/grass_script_raster_mapcalc_test.py +++ b/python/grass/script/tests/grass_script_raster_mapcalc_test.py @@ -34,14 +34,14 @@ class TestMapcalcRandFunction: r.mapcalc auto-seeds when no seed is provided. Explicit seed can be used for reproducibility. """ - @pytest.fixture(scope="module", autouse=True) - def setup_session(self, tmp_path_factory): + @pytest.fixture(scope="class", autouse=True) + def setup_session(self, request, tmp_path_factory): """Setup temporary GRASS session for tests""" tmp_path = tmp_path_factory.mktemp("mapcalc_rand_test") gs.create_project(tmp_path, overwrite=True) with gs.setup.init(tmp_path, env=os.environ.copy()) as session: gs.run_command("g.region", rows=2, cols=2, env=session.env) - self.session = session + request.cls.session = session yield def verify_seed_in_metadata(self, mapname, expected_seed): @@ -65,9 +65,7 @@ def verify_seed_in_metadata(self, mapname, expected_seed): def test_mapcalc_rand_autoseeded(self): """Test that rand() auto-seeds when no explicit seed is provided. - Verifies: - 1. Two consecutive runs without explicit seed produce different results (different auto-seeds) - 2. Metadata contains different auto-generated seed values + Verifies that metadata contains different auto-generated seed values. Note: On Windows with low-resolution timers, consecutive calls may occasionally receive the same auto-seed. We add a small delay to ensure different timestamps. @@ -89,13 +87,6 @@ def test_mapcalc_rand_autoseeded(self): env=self.session.env, ) - # Verify arrays differ (outcome of different auto-seeds) - array1 = garray.array("rand_map_auto1", env=self.session.env) - array2 = garray.array("rand_map_auto2", env=self.session.env) - assert not np.array_equal(array1, array2, equal_nan=True), ( - "Auto-seeded runs should produce different maps" - ) - # Verify mechanism: auto-generated seeds are recorded in metadata and differ info1 = gs.raster_info("rand_map_auto1", env=self.session.env) info2 = gs.raster_info("rand_map_auto2", env=self.session.env) @@ -155,11 +146,7 @@ def test_mapcalc_rand_with_seed_auto_backwards_compat(self): seed='auto' should be converted to None internally, enabling auto-seeding behavior. This means it should behave identically to not providing a seed parameter. - Verifies: - 1. seed='auto' does not pass literal 'auto' to r.mapcalc (it's converted before calling) - 2. Two runs with seed='auto' produce different results (auto-seeding enabled) - 3. Auto-generated seeds are recorded in metadata (consistent with no-seed behavior) - 4. Auto-generated seeds differ between runs + Verifies that auto-generated seeds are recorded in metadata and differ between runs. """ # Create one map with seed='auto' gs.mapcalc( @@ -198,29 +185,18 @@ def test_mapcalc_rand_with_seed_auto_backwards_compat(self): "seed='auto' should produce different auto-generated seeds on consecutive runs" ) - # Verify actual raster data differs - array_1 = garray.array("rand_map_auto_seed", env=self.session.env) - array_2 = garray.array("rand_map_auto_seed_2", env=self.session.env) - assert not np.array_equal(array_1, array_2, equal_nan=True), ( - "seed='auto' should enable auto-seeding, producing different results each time" - ) - def test_mapcalc_rand_with_explicit_seed_reproducible(self): """Test that explicit seed produces reproducible results. - Verifies: - 1. Same seed produces identical raster arrays (byte-for-byte reproducibility) - 2. Different seeds produce different results (seed actually has effect) - 3. Seed metadata is correctly recorded for all variations + Verifies that explicit seeds are correctly passed to r.mapcalc by checking metadata. """ - # Create two maps with same seed=12345 + # Create map with seed=12345 gs.mapcalc( "rand_map_seed = rand(0.0, 1.0)", seed=12345, env=self.session.env, ) self.verify_seed_in_metadata("rand_map_seed", 12345) - array_1 = garray.array("rand_map_seed", env=self.session.env) # Create another map with the same seed gs.mapcalc( @@ -229,12 +205,6 @@ def test_mapcalc_rand_with_explicit_seed_reproducible(self): env=self.session.env, ) self.verify_seed_in_metadata("rand_map_seed_2", 12345) - array_2 = garray.array("rand_map_seed_2", env=self.session.env) - - # Verify reproducibility: arrays must be identical (byte-for-byte) - assert np.array_equal(array_1, array_2, equal_nan=True), ( - "Maps with same seed should have identical cell values" - ) # Verify seed actually has an effect by comparing with different seed gs.mapcalc( @@ -243,19 +213,11 @@ def test_mapcalc_rand_with_explicit_seed_reproducible(self): env=self.session.env, ) self.verify_seed_in_metadata("rand_map_seed_3", 54321) - array_3 = garray.array("rand_map_seed_3", env=self.session.env) - - # Different seed should produce different results - assert not np.array_equal(array_1, array_3, equal_nan=True), ( - "Maps with different seeds should have different cell values" - ) def test_mapcalc_rand_with_seed_zero(self): """Test that seed=0 is handled correctly. - Verifies: - 1. seed=0 is valid and produces reproducible results - 2. seed=0 differs from seed='auto' (explicit seed produces different results than auto-seeding) + Verifies that seed=0 is correctly passed to r.mapcalc by checking metadata. """ # Create map with seed=0 gs.mapcalc( @@ -264,28 +226,6 @@ def test_mapcalc_rand_with_seed_zero(self): env=self.session.env, ) self.verify_seed_in_metadata("rand_map_seed_0", 0) - array_1 = garray.array("rand_map_seed_0", env=self.session.env) - - # Create another map with seed=0 — should be reproducible - gs.mapcalc( - "rand_map_seed_0_2 = rand(0.0, 1.0)", - seed=0, - env=self.session.env, - ) - array_2 = garray.array("rand_map_seed_0_2", env=self.session.env) - assert np.array_equal(array_1, array_2, equal_nan=True), ( - "seed=0 should produce reproducible results" - ) - - # Verify seed=0 differs from auto-seeding - gs.mapcalc( - "rand_map_seed_0_auto = rand(0.0, 1.0)", - env=self.session.env, - ) - array_auto = garray.array("rand_map_seed_0_auto", env=self.session.env) - assert not np.array_equal(array_1, array_auto, equal_nan=True), ( - "seed=0 should produce different results than auto-seeding" - ) @pytest.mark.parametrize( ("seed_value", "description"), @@ -297,44 +237,17 @@ def test_mapcalc_rand_with_seed_zero(self): def test_mapcalc_rand_with_edge_seeds(self, seed_value, description): """Test that edge seed values (negative, large) are handled correctly. - Verifies: - 1. Edge seed values are valid and produce reproducible results - 2. Different edge seeds produce different results + Verifies that edge seed values are correctly passed to r.mapcalc by checking metadata. """ - map_name_1 = f"rand_map_{description.replace(' ', '_')}_1" - map_name_2 = f"rand_map_{description.replace(' ', '_')}_2" - map_name_diff = f"rand_map_{description.replace(' ', '_')}_diff" + map_name = f"rand_map_{description.replace(' ', '_')}" # Create map with edge seed gs.mapcalc( - f"{map_name_1} = rand(0.0, 1.0)", + f"{map_name} = rand(0.0, 1.0)", seed=seed_value, env=self.session.env, ) - self.verify_seed_in_metadata(map_name_1, seed_value) - array_1 = garray.array(map_name_1, env=self.session.env) - - # Create another map with same edge seed — should be reproducible - gs.mapcalc( - f"{map_name_2} = rand(0.0, 1.0)", - seed=seed_value, - env=self.session.env, - ) - array_2 = garray.array(map_name_2, env=self.session.env) - assert np.array_equal(array_1, array_2, equal_nan=True), ( - f"{description} should produce reproducible results" - ) - - # Different seed should produce different results (compare with seed=12345) - gs.mapcalc( - f"{map_name_diff} = rand(0.0, 1.0)", - seed=12345, - env=self.session.env, - ) - array_diff = garray.array(map_name_diff, env=self.session.env) - assert not np.array_equal(array_1, array_diff, equal_nan=True), ( - f"{description} should produce different results than regular seed" - ) + self.verify_seed_in_metadata(map_name, seed_value) class TestMapcalcStartRandFunction: @@ -347,14 +260,14 @@ class TestMapcalcStartRandFunction: Testing the mechanism once is sufficient to ensure both code paths work correctly. """ - @pytest.fixture(scope="module", autouse=True) - def setup_session(self, tmp_path_factory): + @pytest.fixture(scope="class", autouse=True) + def setup_session(self, request, tmp_path_factory): """Setup temporary GRASS session for tests""" tmp_path = tmp_path_factory.mktemp("mapcalc_start_rand_test") - gs.create_project(tmp_path) + gs.create_project(tmp_path, overwrite=True) with gs.setup.init(tmp_path, env=os.environ.copy()) as session: gs.run_command("g.region", rows=2, cols=2, env=session.env) - self.session = session + request.cls.session = session yield def verify_seed_in_metadata(self, mapname, expected_seed): @@ -378,9 +291,7 @@ def verify_seed_in_metadata(self, mapname, expected_seed): def test_mapcalc_start_rand_autoseeded(self): """Test that mapcalc_start with rand() auto-seeds when no explicit seed is provided. - Verifies: - 1. Two consecutive runs without explicit seed produce different results (different auto-seeds) - 2. Metadata contains different auto-generated seed values + Verifies that auto-seeding is enabled and different seeds are recorded in metadata. """ # r.mapcalc auto-seeds when no seed is given — two runs should differ p = gs.mapcalc_start( @@ -400,13 +311,6 @@ def test_mapcalc_start_rand_autoseeded(self): returncode = p.wait() assert returncode == 0 - # Verify outcome: arrays differ (different auto-seeds) - array1 = garray.array("rand_map_start_auto1", env=self.session.env) - array2 = garray.array("rand_map_start_auto2", env=self.session.env) - assert not np.array_equal(array1, array2, equal_nan=True), ( - "Auto-seeded runs should produce different maps" - ) - # Verify mechanism: auto-generated seeds are recorded in metadata and differ info1 = gs.raster_info("rand_map_start_auto1", env=self.session.env) info2 = gs.raster_info("rand_map_start_auto2", env=self.session.env) @@ -448,11 +352,7 @@ def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self): seed='auto' should be converted to None internally, enabling auto-seeding behavior. - Verifies: - 1. seed='auto' does not pass literal 'auto' to r.mapcalc (it's converted before calling) - 2. Two runs with seed='auto' produce different results (auto-seeding enabled) - 3. Auto-generated seeds are recorded in metadata (consistent with no-seed behavior) - 4. Auto-generated seeds differ between runs + Verifies that auto-generated seeds are recorded in metadata and differ between runs. """ # Create one map with seed='auto' p = gs.mapcalc_start( @@ -497,22 +397,12 @@ def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self): f"seed='auto' should produce different auto-generated seeds (got {seed_1} and {seed_2})" ) - # Verify actual raster data differs - array_1 = garray.array("rand_map_start_auto_seed", env=self.session.env) - array_2 = garray.array("rand_map_start_auto_seed_2", env=self.session.env) - assert not np.array_equal(array_1, array_2, equal_nan=True), ( - "seed='auto' should enable auto-seeding, producing different results each time" - ) - def test_mapcalc_start_rand_with_explicit_seed_reproducible(self): """Test that explicit seed in mapcalc_start produces reproducible results. - Verifies: - 1. Same seed produces identical raster arrays (byte-for-byte reproducibility) - 2. Different seeds produce different results (seed actually has effect) - 3. Seed metadata is correctly recorded for all variations + Verifies that explicit seeds are correctly passed to r.mapcalc by checking metadata. """ - # Create two maps with same seed=12345 + # Create map with seed=12345 p = gs.mapcalc_start( "rand_map_start_seed = rand(0.0, 1.0)", seed=12345, @@ -522,9 +412,6 @@ def test_mapcalc_start_rand_with_explicit_seed_reproducible(self): assert returncode == 0 self.verify_seed_in_metadata("rand_map_start_seed", 12345) - # Read actual raster data - array_1 = garray.array("rand_map_start_seed", env=self.session.env) - # Create another map with the same seed p = gs.mapcalc_start( "rand_map_start_seed_2 = rand(0.0, 1.0)", @@ -535,14 +422,6 @@ def test_mapcalc_start_rand_with_explicit_seed_reproducible(self): assert returncode == 0 self.verify_seed_in_metadata("rand_map_start_seed_2", 12345) - # Read actual raster data - array_2 = garray.array("rand_map_start_seed_2", env=self.session.env) - - # Verify reproducibility: arrays must be identical (byte-for-byte) - assert np.array_equal(array_1, array_2, equal_nan=True), ( - "Maps with same seed should have identical cell values" - ) - # Verify seed actually has an effect by comparing with different seed p = gs.mapcalc_start( "rand_map_start_seed_3 = rand(0.0, 1.0)", @@ -552,10 +431,3 @@ def test_mapcalc_start_rand_with_explicit_seed_reproducible(self): returncode = p.wait() assert returncode == 0 self.verify_seed_in_metadata("rand_map_start_seed_3", 54321) - - array_3 = garray.array("rand_map_start_seed_3", env=self.session.env) - - # Different seed should produce different results - assert not np.array_equal(array_1, array_3, equal_nan=True), ( - "Maps with different seeds should have different cell values" - ) From 4ec4229c026ec7f93a73b98ffafa8dcff57e49e8 Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Wed, 7 Jan 2026 20:27:58 +0530 Subject: [PATCH 20/21] session attribute handling --- .../tests/grass_script_raster_mapcalc_test.py | 205 ++++++++---------- 1 file changed, 88 insertions(+), 117 deletions(-) diff --git a/python/grass/script/tests/grass_script_raster_mapcalc_test.py b/python/grass/script/tests/grass_script_raster_mapcalc_test.py index 83818cfe7c7..e62cbc6d7ec 100644 --- a/python/grass/script/tests/grass_script_raster_mapcalc_test.py +++ b/python/grass/script/tests/grass_script_raster_mapcalc_test.py @@ -1,20 +1,18 @@ """Tests for grass.script.raster mapcalc functions""" import pytest -import numpy as np import re import os import time import grass.script as gs -import grass.script.array as garray def extract_seed_from_comments(comments_str): """Extract explicit numeric seed from raster comments/metadata. Parses the seed value from r.mapcalc metadata comments string. - Searches for patterns like 'seed=12345' or 'seed: 12345' (handles optional spacing). + Searches for patterns like 'seed=12345' or 'seed: 12345' (handles optional spacing). Args: comments_str: The comments/metadata string from raster_info() @@ -28,41 +26,55 @@ def extract_seed_from_comments(comments_str): return int(match.group(1)) if match else None +def verify_seed_in_metadata(session, mapname, expected_seed): + """Helper to verify seed value in raster metadata. + + Args: + session: GRASS session object + mapname: Name of the raster map to check + expected_seed: Expected seed value + + Returns: + int: The actual seed value found in metadata + """ + info = gs.raster_info(mapname, env=session.env) + comments = info.get("comments", "") + actual_seed = extract_seed_from_comments(comments) + assert actual_seed == expected_seed, ( + f"Expected seed={expected_seed} in metadata, got seed={actual_seed}" + ) + return actual_seed + + +@pytest.fixture(scope="module") +def mapcalc_rand_session(tmp_path_factory): + """Setup temporary GRASS session for mapcalc rand tests""" + tmp_path = tmp_path_factory.mktemp("mapcalc_rand_test") + project = tmp_path / "test_project" + gs.create_project(project) + with gs.setup.init(project, env=os.environ.copy()) as session: + gs.run_command("g.region", rows=2, cols=2, env=session.env) + yield session + + +@pytest.fixture(scope="module") +def mapcalc_start_rand_session(tmp_path_factory): + """Setup temporary GRASS session for mapcalc_start rand tests""" + tmp_path = tmp_path_factory.mktemp("mapcalc_start_rand_test") + project = tmp_path / "test_project" + gs.create_project(project) + with gs.setup.init(project, env=os.environ.copy()) as session: + gs.run_command("g.region", rows=2, cols=2, env=session.env) + yield session + + class TestMapcalcRandFunction: """Tests for rand() function in mapcalc r.mapcalc auto-seeds when no seed is provided. Explicit seed can be used for reproducibility. """ - @pytest.fixture(scope="class", autouse=True) - def setup_session(self, request, tmp_path_factory): - """Setup temporary GRASS session for tests""" - tmp_path = tmp_path_factory.mktemp("mapcalc_rand_test") - gs.create_project(tmp_path, overwrite=True) - with gs.setup.init(tmp_path, env=os.environ.copy()) as session: - gs.run_command("g.region", rows=2, cols=2, env=session.env) - request.cls.session = session - yield - - def verify_seed_in_metadata(self, mapname, expected_seed): - """Helper to verify seed value in raster metadata. - - Args: - mapname: Name of the raster map to check - expected_seed: Expected seed value - - Returns: - int: The actual seed value found in metadata - """ - info = gs.raster_info(mapname, env=self.session.env) - comments = info.get("comments", "") - actual_seed = extract_seed_from_comments(comments) - assert actual_seed == expected_seed, ( - f"Expected seed={expected_seed} in metadata, got seed={actual_seed}" - ) - return actual_seed - - def test_mapcalc_rand_autoseeded(self): + def test_mapcalc_rand_autoseeded(self, mapcalc_rand_session): """Test that rand() auto-seeds when no explicit seed is provided. Verifies that metadata contains different auto-generated seed values. @@ -73,7 +85,7 @@ def test_mapcalc_rand_autoseeded(self): # r.mapcalc auto-seeds when no seed is given — two runs should differ gs.mapcalc( "rand_map_auto1 = rand(0.0, 1.0)", - env=self.session.env, + env=mapcalc_rand_session.env, ) # Small delay to ensure different auto-seed on systems with low-resolution timers @@ -84,12 +96,12 @@ def test_mapcalc_rand_autoseeded(self): gs.mapcalc( "rand_map_auto2 = rand(0.0, 1.0)", - env=self.session.env, + env=mapcalc_rand_session.env, ) # Verify mechanism: auto-generated seeds are recorded in metadata and differ - info1 = gs.raster_info("rand_map_auto1", env=self.session.env) - info2 = gs.raster_info("rand_map_auto2", env=self.session.env) + info1 = gs.raster_info("rand_map_auto1", env=mapcalc_rand_session.env) + info2 = gs.raster_info("rand_map_auto2", env=mapcalc_rand_session.env) comments1 = info1.get("comments", "") comments2 = info2.get("comments", "") @@ -107,7 +119,7 @@ def test_mapcalc_rand_autoseeded(self): f"If this fails repeatedly, there may be a timer resolution issue." ) - def test_mapcalc_rand_with_explicit_seed(self): + def test_mapcalc_rand_with_explicit_seed(self, mapcalc_rand_session): """Test that rand() works with explicit seed value. Verifies: @@ -117,14 +129,14 @@ def test_mapcalc_rand_with_explicit_seed(self): gs.mapcalc( "rand_map_seed = rand(0.0, 1.0)", seed=12345, - env=self.session.env, + env=mapcalc_rand_session.env, ) - self.verify_seed_in_metadata("rand_map_seed", 12345) + verify_seed_in_metadata(mapcalc_rand_session, "rand_map_seed", 12345) @pytest.mark.xfail( reason="r.mapcalc currently accepts invalid seed strings instead of rejecting them with ScriptError" ) - def test_mapcalc_rand_with_invalid_seed(self): + def test_mapcalc_rand_with_invalid_seed(self, mapcalc_rand_session): """Test that invalid seed values raise appropriate errors. Verifies: @@ -137,10 +149,10 @@ def test_mapcalc_rand_with_invalid_seed(self): gs.mapcalc( "rand_map_invalid = rand(0.0, 1.0)", seed="invalid_string", - env=self.session.env, + env=mapcalc_rand_session.env, ) - def test_mapcalc_rand_with_seed_auto_backwards_compat(self): + def test_mapcalc_rand_with_seed_auto_backwards_compat(self, mapcalc_rand_session): """Test that seed='auto' is handled for backwards compatibility. seed='auto' should be converted to None internally, enabling auto-seeding behavior. @@ -152,9 +164,9 @@ def test_mapcalc_rand_with_seed_auto_backwards_compat(self): gs.mapcalc( "rand_map_auto_seed = rand(0.0, 1.0)", seed="auto", - env=self.session.env, + env=mapcalc_rand_session.env, ) - raster_info_1 = gs.raster_info("rand_map_auto_seed", env=self.session.env) + raster_info_1 = gs.raster_info("rand_map_auto_seed", env=mapcalc_rand_session.env) comments_1 = raster_info_1.get("comments", "") seed_1 = extract_seed_from_comments(comments_1) @@ -171,9 +183,9 @@ def test_mapcalc_rand_with_seed_auto_backwards_compat(self): gs.mapcalc( "rand_map_auto_seed_2 = rand(0.0, 1.0)", seed="auto", - env=self.session.env, + env=mapcalc_rand_session.env, ) - raster_info_2 = gs.raster_info("rand_map_auto_seed_2", env=self.session.env) + raster_info_2 = gs.raster_info("rand_map_auto_seed_2", env=mapcalc_rand_session.env) comments_2 = raster_info_2.get("comments", "") seed_2 = extract_seed_from_comments(comments_2) @@ -185,7 +197,7 @@ def test_mapcalc_rand_with_seed_auto_backwards_compat(self): "seed='auto' should produce different auto-generated seeds on consecutive runs" ) - def test_mapcalc_rand_with_explicit_seed_reproducible(self): + def test_mapcalc_rand_with_explicit_seed_reproducible(self, mapcalc_rand_session): """Test that explicit seed produces reproducible results. Verifies that explicit seeds are correctly passed to r.mapcalc by checking metadata. @@ -194,38 +206,25 @@ def test_mapcalc_rand_with_explicit_seed_reproducible(self): gs.mapcalc( "rand_map_seed = rand(0.0, 1.0)", seed=12345, - env=self.session.env, + env=mapcalc_rand_session.env, ) - self.verify_seed_in_metadata("rand_map_seed", 12345) + verify_seed_in_metadata(mapcalc_rand_session, "rand_map_seed", 12345) # Create another map with the same seed gs.mapcalc( "rand_map_seed_2 = rand(0.0, 1.0)", seed=12345, - env=self.session.env, + env=mapcalc_rand_session.env, ) - self.verify_seed_in_metadata("rand_map_seed_2", 12345) + verify_seed_in_metadata(mapcalc_rand_session, "rand_map_seed_2", 12345) # Verify seed actually has an effect by comparing with different seed gs.mapcalc( "rand_map_seed_3 = rand(0.0, 1.0)", seed=54321, - env=self.session.env, + env=mapcalc_rand_session.env, ) - self.verify_seed_in_metadata("rand_map_seed_3", 54321) - - def test_mapcalc_rand_with_seed_zero(self): - """Test that seed=0 is handled correctly. - - Verifies that seed=0 is correctly passed to r.mapcalc by checking metadata. - """ - # Create map with seed=0 - gs.mapcalc( - "rand_map_seed_0 = rand(0.0, 1.0)", - seed=0, - env=self.session.env, - ) - self.verify_seed_in_metadata("rand_map_seed_0", 0) + verify_seed_in_metadata(mapcalc_rand_session, "rand_map_seed_3", 54321) @pytest.mark.parametrize( ("seed_value", "description"), @@ -234,7 +233,7 @@ def test_mapcalc_rand_with_seed_zero(self): (2147483647, "large seed"), # Max 32-bit signed integer ], ) - def test_mapcalc_rand_with_edge_seeds(self, seed_value, description): + def test_mapcalc_rand_with_edge_seeds(self, mapcalc_rand_session, seed_value, description): """Test that edge seed values (negative, large) are handled correctly. Verifies that edge seed values are correctly passed to r.mapcalc by checking metadata. @@ -245,9 +244,9 @@ def test_mapcalc_rand_with_edge_seeds(self, seed_value, description): gs.mapcalc( f"{map_name} = rand(0.0, 1.0)", seed=seed_value, - env=self.session.env, + env=mapcalc_rand_session.env, ) - self.verify_seed_in_metadata(map_name, seed_value) + verify_seed_in_metadata(mapcalc_rand_session, map_name, seed_value) class TestMapcalcStartRandFunction: @@ -260,35 +259,7 @@ class TestMapcalcStartRandFunction: Testing the mechanism once is sufficient to ensure both code paths work correctly. """ - @pytest.fixture(scope="class", autouse=True) - def setup_session(self, request, tmp_path_factory): - """Setup temporary GRASS session for tests""" - tmp_path = tmp_path_factory.mktemp("mapcalc_start_rand_test") - gs.create_project(tmp_path, overwrite=True) - with gs.setup.init(tmp_path, env=os.environ.copy()) as session: - gs.run_command("g.region", rows=2, cols=2, env=session.env) - request.cls.session = session - yield - - def verify_seed_in_metadata(self, mapname, expected_seed): - """Helper to verify seed value in raster metadata - - Args: - mapname: Name of the raster map - expected_seed: Expected seed value - - Returns: - The actual seed value found - """ - raster_info = gs.raster_info(mapname, env=self.session.env) - comments = raster_info.get("comments", "") - actual_seed = extract_seed_from_comments(comments) - assert actual_seed == expected_seed, ( - f"Expected seed={expected_seed} in metadata, got seed={actual_seed}" - ) - return actual_seed - - def test_mapcalc_start_rand_autoseeded(self): + def test_mapcalc_start_rand_autoseeded(self, mapcalc_start_rand_session): """Test that mapcalc_start with rand() auto-seeds when no explicit seed is provided. Verifies that auto-seeding is enabled and different seeds are recorded in metadata. @@ -296,7 +267,7 @@ def test_mapcalc_start_rand_autoseeded(self): # r.mapcalc auto-seeds when no seed is given — two runs should differ p = gs.mapcalc_start( "rand_map_start_auto1 = rand(0.0, 1.0)", - env=self.session.env, + env=mapcalc_start_rand_session.env, ) returncode = p.wait() assert returncode == 0 @@ -306,14 +277,14 @@ def test_mapcalc_start_rand_autoseeded(self): p = gs.mapcalc_start( "rand_map_start_auto2 = rand(0.0, 1.0)", - env=self.session.env, + env=mapcalc_start_rand_session.env, ) returncode = p.wait() assert returncode == 0 # Verify mechanism: auto-generated seeds are recorded in metadata and differ - info1 = gs.raster_info("rand_map_start_auto1", env=self.session.env) - info2 = gs.raster_info("rand_map_start_auto2", env=self.session.env) + info1 = gs.raster_info("rand_map_start_auto1", env=mapcalc_start_rand_session.env) + info2 = gs.raster_info("rand_map_start_auto2", env=mapcalc_start_rand_session.env) comments1 = info1.get("comments", "") comments2 = info2.get("comments", "") @@ -330,7 +301,7 @@ def test_mapcalc_start_rand_autoseeded(self): f"Auto-seeds should differ between runs (got {seed1} and {seed2})" ) - def test_mapcalc_start_rand_with_explicit_seed(self): + def test_mapcalc_start_rand_with_explicit_seed(self, mapcalc_start_rand_session): """Test that mapcalc_start with rand() works with explicit seed value. Verifies: @@ -340,14 +311,14 @@ def test_mapcalc_start_rand_with_explicit_seed(self): p = gs.mapcalc_start( "rand_map_start_seed = rand(0.0, 1.0)", seed=12345, - env=self.session.env, + env=mapcalc_start_rand_session.env, ) returncode = p.wait() assert returncode == 0 # Verify the map was created and seed was written to metadata - self.verify_seed_in_metadata("rand_map_start_seed", 12345) + verify_seed_in_metadata(mapcalc_start_rand_session, "rand_map_start_seed", 12345) - def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self): + def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self, mapcalc_start_rand_session): """Test that seed='auto' is handled for backwards compatibility. seed='auto' should be converted to None internally, enabling auto-seeding behavior. @@ -358,11 +329,11 @@ def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self): p = gs.mapcalc_start( "rand_map_start_auto_seed = rand(0.0, 1.0)", seed="auto", - env=self.session.env, + env=mapcalc_start_rand_session.env, ) returncode = p.wait() assert returncode == 0 - raster_info_1 = gs.raster_info("rand_map_start_auto_seed", env=self.session.env) + raster_info_1 = gs.raster_info("rand_map_start_auto_seed", env=mapcalc_start_rand_session.env) comments_1 = raster_info_1.get("comments", "") seed_1 = extract_seed_from_comments(comments_1) @@ -379,12 +350,12 @@ def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self): p = gs.mapcalc_start( "rand_map_start_auto_seed_2 = rand(0.0, 1.0)", seed="auto", - env=self.session.env, + env=mapcalc_start_rand_session.env, ) returncode = p.wait() assert returncode == 0 raster_info_2 = gs.raster_info( - "rand_map_start_auto_seed_2", env=self.session.env + "rand_map_start_auto_seed_2", env=mapcalc_start_rand_session.env ) comments_2 = raster_info_2.get("comments", "") seed_2 = extract_seed_from_comments(comments_2) @@ -397,7 +368,7 @@ def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self): f"seed='auto' should produce different auto-generated seeds (got {seed_1} and {seed_2})" ) - def test_mapcalc_start_rand_with_explicit_seed_reproducible(self): + def test_mapcalc_start_rand_with_explicit_seed_reproducible(self, mapcalc_start_rand_session): """Test that explicit seed in mapcalc_start produces reproducible results. Verifies that explicit seeds are correctly passed to r.mapcalc by checking metadata. @@ -406,28 +377,28 @@ def test_mapcalc_start_rand_with_explicit_seed_reproducible(self): p = gs.mapcalc_start( "rand_map_start_seed = rand(0.0, 1.0)", seed=12345, - env=self.session.env, + env=mapcalc_start_rand_session.env, ) returncode = p.wait() assert returncode == 0 - self.verify_seed_in_metadata("rand_map_start_seed", 12345) + verify_seed_in_metadata(mapcalc_start_rand_session, "rand_map_start_seed", 12345) # Create another map with the same seed p = gs.mapcalc_start( "rand_map_start_seed_2 = rand(0.0, 1.0)", seed=12345, - env=self.session.env, + env=mapcalc_start_rand_session.env, ) returncode = p.wait() assert returncode == 0 - self.verify_seed_in_metadata("rand_map_start_seed_2", 12345) + verify_seed_in_metadata(mapcalc_start_rand_session, "rand_map_start_seed_2", 12345) # Verify seed actually has an effect by comparing with different seed p = gs.mapcalc_start( "rand_map_start_seed_3 = rand(0.0, 1.0)", seed=54321, - env=self.session.env, + env=mapcalc_start_rand_session.env, ) returncode = p.wait() assert returncode == 0 - self.verify_seed_in_metadata("rand_map_start_seed_3", 54321) + verify_seed_in_metadata(mapcalc_start_rand_session, "rand_map_start_seed_3", 54321) From 33db8c41a78da43eda0c04925c43df06b452cad9 Mon Sep 17 00:00:00 2001 From: y-sudharshan Date: Fri, 9 Jan 2026 09:22:06 +0530 Subject: [PATCH 21/21] flag and format check --- .../tests/grass_script_raster_mapcalc_test.py | 66 +++++++++++++++---- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/python/grass/script/tests/grass_script_raster_mapcalc_test.py b/python/grass/script/tests/grass_script_raster_mapcalc_test.py index e62cbc6d7ec..10b49601a94 100644 --- a/python/grass/script/tests/grass_script_raster_mapcalc_test.py +++ b/python/grass/script/tests/grass_script_raster_mapcalc_test.py @@ -85,6 +85,7 @@ def test_mapcalc_rand_autoseeded(self, mapcalc_rand_session): # r.mapcalc auto-seeds when no seed is given — two runs should differ gs.mapcalc( "rand_map_auto1 = rand(0.0, 1.0)", + overwrite=True, env=mapcalc_rand_session.env, ) @@ -96,6 +97,7 @@ def test_mapcalc_rand_autoseeded(self, mapcalc_rand_session): gs.mapcalc( "rand_map_auto2 = rand(0.0, 1.0)", + overwrite=True, env=mapcalc_rand_session.env, ) @@ -129,6 +131,7 @@ def test_mapcalc_rand_with_explicit_seed(self, mapcalc_rand_session): gs.mapcalc( "rand_map_seed = rand(0.0, 1.0)", seed=12345, + overwrite=True, env=mapcalc_rand_session.env, ) verify_seed_in_metadata(mapcalc_rand_session, "rand_map_seed", 12345) @@ -149,6 +152,7 @@ def test_mapcalc_rand_with_invalid_seed(self, mapcalc_rand_session): gs.mapcalc( "rand_map_invalid = rand(0.0, 1.0)", seed="invalid_string", + overwrite=True, env=mapcalc_rand_session.env, ) @@ -164,9 +168,12 @@ def test_mapcalc_rand_with_seed_auto_backwards_compat(self, mapcalc_rand_session gs.mapcalc( "rand_map_auto_seed = rand(0.0, 1.0)", seed="auto", + overwrite=True, env=mapcalc_rand_session.env, ) - raster_info_1 = gs.raster_info("rand_map_auto_seed", env=mapcalc_rand_session.env) + raster_info_1 = gs.raster_info( + "rand_map_auto_seed", env=mapcalc_rand_session.env + ) comments_1 = raster_info_1.get("comments", "") seed_1 = extract_seed_from_comments(comments_1) @@ -183,9 +190,12 @@ def test_mapcalc_rand_with_seed_auto_backwards_compat(self, mapcalc_rand_session gs.mapcalc( "rand_map_auto_seed_2 = rand(0.0, 1.0)", seed="auto", + overwrite=True, env=mapcalc_rand_session.env, ) - raster_info_2 = gs.raster_info("rand_map_auto_seed_2", env=mapcalc_rand_session.env) + raster_info_2 = gs.raster_info( + "rand_map_auto_seed_2", env=mapcalc_rand_session.env + ) comments_2 = raster_info_2.get("comments", "") seed_2 = extract_seed_from_comments(comments_2) @@ -206,6 +216,7 @@ def test_mapcalc_rand_with_explicit_seed_reproducible(self, mapcalc_rand_session gs.mapcalc( "rand_map_seed = rand(0.0, 1.0)", seed=12345, + overwrite=True, env=mapcalc_rand_session.env, ) verify_seed_in_metadata(mapcalc_rand_session, "rand_map_seed", 12345) @@ -214,6 +225,7 @@ def test_mapcalc_rand_with_explicit_seed_reproducible(self, mapcalc_rand_session gs.mapcalc( "rand_map_seed_2 = rand(0.0, 1.0)", seed=12345, + overwrite=True, env=mapcalc_rand_session.env, ) verify_seed_in_metadata(mapcalc_rand_session, "rand_map_seed_2", 12345) @@ -222,6 +234,7 @@ def test_mapcalc_rand_with_explicit_seed_reproducible(self, mapcalc_rand_session gs.mapcalc( "rand_map_seed_3 = rand(0.0, 1.0)", seed=54321, + overwrite=True, env=mapcalc_rand_session.env, ) verify_seed_in_metadata(mapcalc_rand_session, "rand_map_seed_3", 54321) @@ -233,7 +246,9 @@ def test_mapcalc_rand_with_explicit_seed_reproducible(self, mapcalc_rand_session (2147483647, "large seed"), # Max 32-bit signed integer ], ) - def test_mapcalc_rand_with_edge_seeds(self, mapcalc_rand_session, seed_value, description): + def test_mapcalc_rand_with_edge_seeds( + self, mapcalc_rand_session, seed_value, description + ): """Test that edge seed values (negative, large) are handled correctly. Verifies that edge seed values are correctly passed to r.mapcalc by checking metadata. @@ -244,6 +259,7 @@ def test_mapcalc_rand_with_edge_seeds(self, mapcalc_rand_session, seed_value, de gs.mapcalc( f"{map_name} = rand(0.0, 1.0)", seed=seed_value, + overwrite=True, env=mapcalc_rand_session.env, ) verify_seed_in_metadata(mapcalc_rand_session, map_name, seed_value) @@ -267,6 +283,7 @@ def test_mapcalc_start_rand_autoseeded(self, mapcalc_start_rand_session): # r.mapcalc auto-seeds when no seed is given — two runs should differ p = gs.mapcalc_start( "rand_map_start_auto1 = rand(0.0, 1.0)", + overwrite=True, env=mapcalc_start_rand_session.env, ) returncode = p.wait() @@ -277,14 +294,19 @@ def test_mapcalc_start_rand_autoseeded(self, mapcalc_start_rand_session): p = gs.mapcalc_start( "rand_map_start_auto2 = rand(0.0, 1.0)", + overwrite=True, env=mapcalc_start_rand_session.env, ) returncode = p.wait() assert returncode == 0 # Verify mechanism: auto-generated seeds are recorded in metadata and differ - info1 = gs.raster_info("rand_map_start_auto1", env=mapcalc_start_rand_session.env) - info2 = gs.raster_info("rand_map_start_auto2", env=mapcalc_start_rand_session.env) + info1 = gs.raster_info( + "rand_map_start_auto1", env=mapcalc_start_rand_session.env + ) + info2 = gs.raster_info( + "rand_map_start_auto2", env=mapcalc_start_rand_session.env + ) comments1 = info1.get("comments", "") comments2 = info2.get("comments", "") @@ -311,14 +333,19 @@ def test_mapcalc_start_rand_with_explicit_seed(self, mapcalc_start_rand_session) p = gs.mapcalc_start( "rand_map_start_seed = rand(0.0, 1.0)", seed=12345, + overwrite=True, env=mapcalc_start_rand_session.env, ) returncode = p.wait() assert returncode == 0 # Verify the map was created and seed was written to metadata - verify_seed_in_metadata(mapcalc_start_rand_session, "rand_map_start_seed", 12345) + verify_seed_in_metadata( + mapcalc_start_rand_session, "rand_map_start_seed", 12345 + ) - def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self, mapcalc_start_rand_session): + def test_mapcalc_start_rand_with_seed_auto_backwards_compat( + self, mapcalc_start_rand_session + ): """Test that seed='auto' is handled for backwards compatibility. seed='auto' should be converted to None internally, enabling auto-seeding behavior. @@ -329,11 +356,14 @@ def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self, mapcalc_start_ p = gs.mapcalc_start( "rand_map_start_auto_seed = rand(0.0, 1.0)", seed="auto", + overwrite=True, env=mapcalc_start_rand_session.env, ) returncode = p.wait() assert returncode == 0 - raster_info_1 = gs.raster_info("rand_map_start_auto_seed", env=mapcalc_start_rand_session.env) + raster_info_1 = gs.raster_info( + "rand_map_start_auto_seed", env=mapcalc_start_rand_session.env + ) comments_1 = raster_info_1.get("comments", "") seed_1 = extract_seed_from_comments(comments_1) @@ -350,6 +380,7 @@ def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self, mapcalc_start_ p = gs.mapcalc_start( "rand_map_start_auto_seed_2 = rand(0.0, 1.0)", seed="auto", + overwrite=True, env=mapcalc_start_rand_session.env, ) returncode = p.wait() @@ -368,7 +399,9 @@ def test_mapcalc_start_rand_with_seed_auto_backwards_compat(self, mapcalc_start_ f"seed='auto' should produce different auto-generated seeds (got {seed_1} and {seed_2})" ) - def test_mapcalc_start_rand_with_explicit_seed_reproducible(self, mapcalc_start_rand_session): + def test_mapcalc_start_rand_with_explicit_seed_reproducible( + self, mapcalc_start_rand_session + ): """Test that explicit seed in mapcalc_start produces reproducible results. Verifies that explicit seeds are correctly passed to r.mapcalc by checking metadata. @@ -377,28 +410,37 @@ def test_mapcalc_start_rand_with_explicit_seed_reproducible(self, mapcalc_start_ p = gs.mapcalc_start( "rand_map_start_seed = rand(0.0, 1.0)", seed=12345, + overwrite=True, env=mapcalc_start_rand_session.env, ) returncode = p.wait() assert returncode == 0 - verify_seed_in_metadata(mapcalc_start_rand_session, "rand_map_start_seed", 12345) + verify_seed_in_metadata( + mapcalc_start_rand_session, "rand_map_start_seed", 12345 + ) # Create another map with the same seed p = gs.mapcalc_start( "rand_map_start_seed_2 = rand(0.0, 1.0)", seed=12345, + overwrite=True, env=mapcalc_start_rand_session.env, ) returncode = p.wait() assert returncode == 0 - verify_seed_in_metadata(mapcalc_start_rand_session, "rand_map_start_seed_2", 12345) + verify_seed_in_metadata( + mapcalc_start_rand_session, "rand_map_start_seed_2", 12345 + ) # Verify seed actually has an effect by comparing with different seed p = gs.mapcalc_start( "rand_map_start_seed_3 = rand(0.0, 1.0)", seed=54321, + overwrite=True, env=mapcalc_start_rand_session.env, ) returncode = p.wait() assert returncode == 0 - verify_seed_in_metadata(mapcalc_start_rand_session, "rand_map_start_seed_3", 54321) + verify_seed_in_metadata( + mapcalc_start_rand_session, "rand_map_start_seed_3", 54321 + )