From 4cbd15db0f7d8b7beb954b27c62d706753ed4582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Thu, 3 Sep 2026 21:20:29 +0000 Subject: [PATCH 1/2] grass.gunittest: Preserve exact bytes in MD5 test fixtures Python's default text-mode file writing translates "\n" to the platform's os.linesep. On Windows that turns "\n" into "\r\n", which silently corrupts fixture files that two testsuite tests hash in binary mode and compare against a hardcoded MD5 computed from Unix-style ("\n") content: test_checkers.py's test_text_file_binary and test_assertions.py's test_assertFileMd5. Both are already marked with @xfail_windows, consistent with this being the cause. Write these fixtures with newline="" so the given content is stored byte-for-byte, matching the precomputed MD5 sums on every platform. Apply the same treatment to the other two fixtures written alongside file_with_md5 in test_assertions.py, since test_assertFilesEqualMd5 hashes them against file_with_md5 in the same binary mode and would otherwise start disagreeing with it once file_with_md5 alone stops being translated. This is a hypothesis for Windows CI to confirm, not a proven fix: there is no Windows environment available to verify it here, and the existing @xfail_windows markers are left in place pending that confirmation. This reworks two Windows-CI-testsuite commits ("handle OS newlines in testsuite", authored by Stefan Blumentrath / @ninsbl) from the unrebased OSGeo/grass#2616, extracted and adapted against current main with Claude's assistance rather than cherry-picked, since a direct cherry-pick would also revert unrelated changes both files have accumulated since 2022 (e.g. test_assertions.py switched from open() to Path.write_text()). Co-Authored-By: Claude Sonnet 5 --- python/grass/gunittest/testsuite/test_assertions.py | 9 ++++++--- python/grass/gunittest/testsuite/test_checkers.py | 6 ++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/python/grass/gunittest/testsuite/test_assertions.py b/python/grass/gunittest/testsuite/test_assertions.py index 64bc790b3dd..738eff78a9f 100644 --- a/python/grass/gunittest/testsuite/test_assertions.py +++ b/python/grass/gunittest/testsuite/test_assertions.py @@ -351,18 +351,21 @@ def setUpClass(cls): open(cls.emtpy_file, "w").close() cls.file_with_md5 = cls.__name__ + "_this_is_a_file_with_known_md5" file_content = "Content of the file with known MD5.\n" - Path(cls.file_with_md5).write_text(file_content) + # newline="" keeps the \n as-is instead of letting the platform + # default translate it to \r\n, so the MD5 matches file_md5 below + # on every platform (assertFileMd5 hashes the file in binary mode) + Path(cls.file_with_md5).write_text(file_content, newline="") # MD5 sum created using: # echo 'Content of the file with known MD5.' > some_file.txt # md5sum some_file.txt cls.file_md5 = "807bba4ffac4bb351bc3f27853009949" cls.file_with_same_content = cls.__name__ + "_file_with_same_content" - Path(cls.file_with_same_content).write_text(file_content) + Path(cls.file_with_same_content).write_text(file_content, newline="") cls.file_with_different_content = cls.__name__ + "_file_with_different_content" Path(cls.file_with_different_content).write_text( - file_content + " Something else here." + file_content + " Something else here.", newline="" ) @classmethod diff --git a/python/grass/gunittest/testsuite/test_checkers.py b/python/grass/gunittest/testsuite/test_checkers.py index 477e0f32877..d84160f0d52 100644 --- a/python/grass/gunittest/testsuite/test_checkers.py +++ b/python/grass/gunittest/testsuite/test_checkers.py @@ -371,8 +371,10 @@ def setUpClass(cls): with open(cls.correct_file_name_platform_nl, "w") as f: # \n should be converted to platform newline f.writelines(line + "\n" for line in CORRECT_LINES) - with open(cls.correct_file_name_unix_nl, "w") as f: - # binary mode will write pure \n + with open(cls.correct_file_name_unix_nl, "w", newline="") as f: + # newline="" keeps the \n as-is instead of letting the platform + # default translate it to \r\n, so file_md5() (binary mode) + # below matches correct_md5sum on every platform f.writelines(line + "\n" for line in CORRECT_LINES) with open(cls.wrong_file_name, "w") as f: # \n should be converted to platform newline From 8c7b6d91cb133caa91320d216d0dca1bae5f0887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Thu, 3 Sep 2026 21:26:20 +0000 Subject: [PATCH 2/2] grass.gunittest: Remove @xfail_windows now made obsolete by newline fix unittest.TestResult.wasSuccessful() (which python/grass/gunittest/main.py relies on for its exit code) returns False whenever unexpectedSuccesses is non-empty, and @xfail_windows applies unittest.expectedFailure on Windows. Left in place, the two decorators removed here would flip a passing Windows gunittest run to a failing one exactly when the newline fix in the previous commit works, since the previously-failing tests would now succeed unexpectedly instead of failing as declared. Since the fix is only a hypothesis pending confirmation on real Windows CI, removing the decorators now is also the more honest choice either way: if the hypothesis holds, the run is green for the right reason; if it does not, the tests fail plainly instead of being silently tolerated. Thanks to the review on this PR for catching this. Co-Authored-By: Claude Sonnet 5 --- python/grass/gunittest/testsuite/test_assertions.py | 1 - python/grass/gunittest/testsuite/test_checkers.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/python/grass/gunittest/testsuite/test_assertions.py b/python/grass/gunittest/testsuite/test_assertions.py index 738eff78a9f..631e0e9d2f8 100644 --- a/python/grass/gunittest/testsuite/test_assertions.py +++ b/python/grass/gunittest/testsuite/test_assertions.py @@ -389,7 +389,6 @@ def test_assertFileExists_empty_file(self): self.failureException, self.assertFileExists, filename=self.emtpy_file ) - @xfail_windows def test_assertFileMd5(self): self.assertFileMd5(filename=self.file_with_md5, md5=self.file_md5) self.assertRaises( diff --git a/python/grass/gunittest/testsuite/test_checkers.py b/python/grass/gunittest/testsuite/test_checkers.py index d84160f0d52..b14a6ea5b32 100644 --- a/python/grass/gunittest/testsuite/test_checkers.py +++ b/python/grass/gunittest/testsuite/test_checkers.py @@ -24,7 +24,6 @@ file_md5, text_file_md5, ) -from grass.gunittest.utils import xfail_windows class TestValuesEqual(TestCase): @@ -386,7 +385,6 @@ def tearDownClass(cls): try_remove(cls.correct_file_name_unix_nl) try_remove(cls.wrong_file_name) - @xfail_windows def test_text_file_binary(self): r"""File with ``\n`` (LF) newlines as binary (MD5 has ``\n``).""" self.assertEqual(