Skip to content

grass.gunittest: Preserve exact bytes in MD5 test fixtures - #680

Closed
echoix wants to merge 4 commits into
mainfrom
claude/gunittest-md5-newline-fix
Closed

echoix wants to merge 4 commits into
mainfrom
claude/gunittest-md5-newline-fix

Conversation

@echoix

@echoix echoix commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Description

Two grass.gunittest testsuite fixture files are written with Python's
default text-mode newline handling, which translates \n to
os.linesep on write. On Windows that turns \n into \r\n, silently
corrupting fixtures that are later hashed in binary mode and
compared against a hardcoded MD5 computed from Unix-style (\n)
content:

  • python/grass/gunittest/testsuite/test_checkers.py:
    test_text_file_binary hashes correct_file_name_unix_nl with
    file_md5() (binary) and compares it to correct_md5sum, which was
    computed from a heredoc with \n line endings.
  • python/grass/gunittest/testsuite/test_assertions.py:
    test_assertFileMd5 hashes file_with_md5 (binary, via
    assertFileMd5) and compares it to a hardcoded MD5 computed the same
    way.

Both tests are already marked @xfail_windows, which is consistent
with this being the cause: os.linesep == "\n" on Linux/macOS, so the
bug is invisible there.

The fix opens/writes these two fixtures with newline="", which keeps
\n as-is instead of letting it get translated, per Python's
documented open() newline semantics (verified locally: with
newline="" the written bytes are \n-only regardless of platform,
whereas the default lets the platform newline through — on Linux this
is a no-op since os.linesep == "\n" there, so it can't be observed
without Windows).

I also extended the same treatment to the two other fixtures written
alongside file_with_md5 in test_assertions.py
(file_with_same_content, file_with_different_content), which the
original upstream commit did not touch. test_assertFilesEqualMd5
hashes file_with_same_content against file_with_md5 in the same
binary mode; leaving it on the default-translated path while
file_with_md5 switches to raw bytes would make them disagree on
Windows and turn a currently-passing test into a new failure.

Motivation and context

This extracts and reworks one small piece of the large, stale, never
rebased upstream PR
OSGeo/grass#2616 ("CI:
Address Windows specific issues in testsuite", by Stefan Blumentrath /
@ninsbl), so it can be reviewed and merged independently instead of as
part of a ~35-commit bundle. The underlying idea for these two specific
fixture writes comes from upstream's "handle OS newlines in testsuite"
commits (fb075bfa3, 45bb9d75c), authored by @ninsbl in 2022.

That branch was never rebased, so its diff was not cherry-picked
directly: both files have moved on since 2022 (e.g.
test_assertions.py switched from open() to Path.write_text()), and
a naive cherry-pick would have reverted those unrelated, independent
changes. Instead, I read the relevant upstream commits, compared the
pre-change baseline against current main for these two files, and
re-implemented just the newline handling against current main
(extending its scope slightly, per above).

This PR was extracted and reworked with the assistance of Claude
(Claude Code), per this repo's AGENTS.md guidance to disclose
substantial AI use. I reviewed the underlying Python open()/newline
behavior directly and confirmed the two hardcoded MD5 sums against the
intended \n-only content by hashing it myself; see caveats below.

How has this been tested?

  • No Windows machine or full GRASS build was available in this
    environment, so the fix has not been run against real Windows
    CI. This is a hypothesis for CI to confirm, not a proven fix — please
    treat it as such.
  • What I did verify:
    • Python's open(..., newline="") documented behavior, confirmed
      empirically on Linux (a no-op there since os.linesep == "\n",
      which is exactly why this bug would not show up outside Windows).
    • Both hardcoded MD5 sums (correct_md5sum in test_checkers.py,
      file_md5 in test_assertions.py) match hashlib.md5() of the
      intended \n-only content, confirming they were computed from
      Unix-style line endings and that the fix targets the right content.
    • ruff check --preview and ruff format --diff (ruff 0.15.17,
      matching .pre-commit-config.yaml) pass on both changed files.
  • I deliberately left the existing @xfail_windows decorators on
    test_text_file_binary and test_assertFileMd5 in place rather than
    removing them, since I cannot confirm on real Windows CI that no
    other issue remains in either test. If CI shows them passing, a
    maintainer (or a follow-up PR) can remove the decorators per the
    comment already in xfail_windows()'s docstring.

Screenshots (if appropriate)

N/A (test-only change).

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing
    functionality to not work as before)

Checklist

  • PR title provides summary of the changes and starts with one of the
    pre-defined prefixes
  • My code follows the code style
    of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes. (N/A — this only fixes
    existing test fixtures; no new behavior is introduced.)

🤖 Generated with Claude Code

https://claude.ai/code/session_013RUh1QAj3DCnVZEhe7HXuo


Generated by Claude Code

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#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 <noreply@anthropic.com>

@echoix echoix left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the change. What I verified as correct:

  • Path.write_text(..., newline="") is valid on this project's floor (pyproject.toml has requires-python = ">=3.11"; the newline kwarg landed in 3.10), and open(..., "w", newline="") is correct — both suppress the \n → os.linesep translation.
  • Both hardcoded sums match the intended \n-only content (reproduced locally: 807bba4ffac4bb351bc3f27853009949 and 9dd6c4bb9d2cf6051b12f4b5f9d70523).
  • Extending the change to file_with_same_content / file_with_different_content is necessary rather than scope creep: test_assertFilesEqualMd5 hashes them against file_with_md5 in binary mode, so leaving them translated would break a currently-passing test on Windows.
  • No collateral damage to the text-mode tests: text_file_md5() (python/grass/gunittest/checkers.py:633) reads with universal newlines, so test_text_file_unix is unaffected either way, and correct_file_name_platform_nl is deliberately left on the default path.

One thing worth flagging:

Leftover @xfail_windows decorators. test_checkers.py:387 (test_text_file_binary) and test_assertions.py:389 (test_assertFileMd5) still carry @xfail_windows. If the fix works on Windows, those tests pass, get recorded as unexpectedSuccesses, and unittest.TestResult.wasSuccessful() returns False — so main.py:124 exits non-zero exactly when the fix succeeds. Windows gunittest goes red on success.

The PR description calls this a deliberate choice pending CI confirmation, which is defensible, but the decorators should come off in this same PR once CI confirms (or immediately, accepting a red run if the hypothesis turns out wrong).


Generated by Claude Code

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 <noreply@anthropic.com>

echoix commented Sep 3, 2026

Copy link
Copy Markdown
Owner Author

Good catch, thanks. Confirmed: unittest.TestResult (which python/grass/gunittest/result.py's TestResult subclasses without overriding wasSuccessful()) returns False from wasSuccessful() whenever unexpectedSuccesses is non-empty, and main.py exits on that. So yes, the two @xfail_windows decorators would have turned a working fix into a red Windows run.

Pushed 8c7b6d9, removing @xfail_windows from test_text_file_binary and test_assertFileMd5 (and the now-unused xfail_windows import in test_checkers.py; test_assertions.py keeps it, it's still used by test_assertLooksLike_multiline_platform_dependent). Per your suggestion, I went with removing them now rather than waiting: if the newline fix holds, Windows CI goes green for the right reason; if it doesn't, the tests fail plainly instead of being masked.


Generated by Claude Code

echoix commented Sep 3, 2026

Copy link
Copy Markdown
Owner Author

Windows CI finished: windows-2022 build and tests passed. All 26 checks on 8c7b6d9 are green, so the newline hypothesis holds — test_text_file_binary and test_assertFileMd5 pass for real now that @xfail_windows is gone, rather than being masked. Nothing else outstanding on this PR from my side.


Generated by Claude Code

@echoix echoix closed this Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant