From 124fe833c59398f46a1885e3218e2ae8be2f0ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Fri, 4 Sep 2026 09:42:02 +0000 Subject: [PATCH 1/9] r.kappa: Avoid reopening NamedTemporaryFile on Windows in testsuite test_file kept a NamedTemporaryFile handle open while passing its path to r.kappa as the output file. Windows rejects a second process opening a file while the creating process still holds the handle; POSIX allows it. Use grass.script.core.tempfile() to get an unopened path instead, the same fix already applied to r.mfilter's testsuite in #7878, and drop the now-unneeded @xfail_windows marker. Co-Authored-By: Claude Sonnet 5 --- raster/r.kappa/testsuite/test_r_kappa.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/raster/r.kappa/testsuite/test_r_kappa.py b/raster/r.kappa/testsuite/test_r_kappa.py index 1e078aa0d9d..0bcdf3c1190 100644 --- a/raster/r.kappa/testsuite/test_r_kappa.py +++ b/raster/r.kappa/testsuite/test_r_kappa.py @@ -12,15 +12,13 @@ import os import json from pathlib import Path -from tempfile import NamedTemporaryFile from grass.script import read_command from grass.script import decode -from grass.script.core import tempname +from grass.script.core import tempfile, tempname from grass.gunittest.case import TestCase from grass.gunittest.main import test from grass.gunittest.checkers import keyvalue_equals, diff_keyvalue -from grass.gunittest.utils import xfail_windows class MatrixCorrectnessTest(TestCase): @@ -489,19 +487,18 @@ def test_stdout(self): ), ) - @xfail_windows def test_file(self): for i in range(len(self.references)): - f = NamedTemporaryFile() + outfile = tempfile(create=False) self.runModule( "r.kappa", reference=self.references[i], classification=self.classifications[i], - output=f.name, + output=outfile, format="json", overwrite=True, ) - json_out = json.loads(f.read()) + json_out = json.loads(Path(outfile).read_text()) self.assertTrue( keyvalue_equals( self.expected_outputs[i], json_out, precision=self.precision From ac52dbcc27e2ca36320521d44e345a26df2058a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Fri, 4 Sep 2026 09:42:08 +0000 Subject: [PATCH 2/9] CI: Run Windows pytest suite with xdist and randomized order Windows CI ran pytest single-worker with a fixed test order, unlike Linux CI's pytest-xdist + needs_solo_run split. Under a fixed order, whichever test happens to run first in a given worker never changes, so an order-dependent bug that only breaks the first call to a C-based GRASS tool/library in a fresh worker process can stay hidden indefinitely. Install pytest-xdist and pytest-randomly and mirror Linux CI's parallel / needs_solo_run split so this class of bug surfaces instead of hiding. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/osgeo4w.yml | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/osgeo4w.yml b/.github/workflows/osgeo4w.yml index 1d93df3fae7..96b5a6bb9aa 100644 --- a/.github/workflows/osgeo4w.yml +++ b/.github/workflows/osgeo4w.yml @@ -128,17 +128,34 @@ jobs: run: | python -m pip install ^ pytest ^ - pytest-timeout + pytest-timeout ^ + pytest-xdist ^ + pytest-randomly # Obfuscated usage of GitHub Actions features, Windows CMD shell limits analysis shell: cmd /D /E:ON /V:OFF /S /C "CALL C:/OSGeo4W/OSGeo4W.bat "{0}"" # zizmor: ignore[misfeature] - - name: Run pytest with a single worker + - name: Run pytest with multiple workers in parallel run: | call %OSGEO4W_ROOT%\opt\grass\etc\env.bat set PYTHONPATH=%GISBASE%\etc\python;%PYTHONPATH% path %GISBASE%\lib;%GISBASE%\bin;%PATH% pytest ^ @.github/workflows/pytest_args_ci.txt ^ - --junitxml=pytest.junit.xml ^ + @.github/workflows/pytest_args_parallel.txt ^ + --junitxml=pytest.xdist.junit.xml ^ + -k "not testsuite" + env: + OMP_NUM_THREADS: 1 + # Obfuscated usage of GitHub Actions features, Windows CMD shell limits analysis + shell: cmd /D /E:ON /V:OFF /S /C "CALL C:/OSGeo4W/OSGeo4W.bat "{0}"" # zizmor: ignore[misfeature] + - name: Run pytest with a single worker (for tests marked with needs_solo_run) + run: | + call %OSGEO4W_ROOT%\opt\grass\etc\env.bat + set PYTHONPATH=%GISBASE%\etc\python;%PYTHONPATH% + path %GISBASE%\lib;%GISBASE%\bin;%PATH% + pytest ^ + @.github/workflows/pytest_args_ci.txt ^ + @.github/workflows/pytest_args_not_parallel.txt ^ + --junitxml=pytest.needs_solo_run.junit.xml ^ -k "not testsuite" # Obfuscated usage of GitHub Actions features, Windows CMD shell limits analysis shell: cmd /D /E:ON /V:OFF /S /C "CALL C:/OSGeo4W/OSGeo4W.bat "{0}"" # zizmor: ignore[misfeature] From 94e892270b17d3c20e76eea0e66fa0eedca7c526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Fri, 4 Sep 2026 09:45:59 +0000 Subject: [PATCH 3/9] TEMP: Add manual-dispatch workflow for parallel randomized Windows runs Do not merge. Adds a workflow_dispatch-only job that runs the same compile + pytest-xdist step as the permanent osgeo4w.yml change, as a 15-way parallel matrix, to fish for order-dependent Windows failures across many independent pytest-randomly seeds at once. Delete this file once the investigation concludes. Co-Authored-By: Claude Sonnet 5 --- .../workflows/TEMP-windows-xdist-fishing.yml | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 .github/workflows/TEMP-windows-xdist-fishing.yml diff --git a/.github/workflows/TEMP-windows-xdist-fishing.yml b/.github/workflows/TEMP-windows-xdist-fishing.yml new file mode 100644 index 00000000000..4f75cf843a6 --- /dev/null +++ b/.github/workflows/TEMP-windows-xdist-fishing.yml @@ -0,0 +1,142 @@ +--- +# TEMPORARY workflow for the Windows test-order-dependency investigation. +# Runs the randomized xdist pytest suite many times in parallel, each +# attempt getting its own pytest-randomly seed, to surface hidden +# order-dependent failures that a fixed test order never exercises. Not +# meant to be merged: delete this file once the investigation concludes. + +name: TEMP Windows xdist fishing + +on: + workflow_dispatch: + inputs: + attempts: + description: Number of parallel attempts + default: "15" + +permissions: {} + +jobs: + build: + name: attempt ${{ matrix.attempt }} + + runs-on: windows-2022 + env: + PYTHONWARNINGS: always + strategy: + matrix: + attempt: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + fail-fast: false + + steps: + - name: Set git to use LF + run: | + git config --global core.autocrlf false + git config --global core.eol lf + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2.32.0 + id: msys2 + with: + path-type: inherit + location: D:\ + update: true + msystem: MINGW64 + install: >- + bison + diffutils + dos2unix + flex + git + libintl + make + tar + zip + pacboy: >- + bzip2 + ccache + fftw + gcc + gettext + libiconv + libsystre + libtre + libwinpthread-git + openblas + pcre + pkgconf + toolchain + zlib + + - name: Setup OSGeo4W environment + uses: echoix/setup-OSGeo4W@1e711832882e03603e9013181a87885b10af8a98 # v0.3.0 + id: osgeo4w + with: + package-dir: "D:/OSGeo4W_pkg" + packages: | + cairo-devel + freetype-devel + gdal-devel + geos-devel + libjpeg-turbo-devel + liblas-devel + libpng-devel + libpq-devel + libtiff-devel + netcdf-devel + proj-devel + python3-core + python3-gdal + python3-jupyter + python3-matplotlib + python3-numpy + python3-pip + python3-pywin32 + python3-wxpython + sqlite3-devel + zstd-devel + + - name: Set number of cores for compilation + run: | + echo "MAKEFLAGS=-j$(nproc)" >> "${GITHUB_ENV}" + shell: msys2 {0} + + - name: Compile GRASS + shell: msys2 {0} + run: | + .github/workflows/build_osgeo4w.sh + env: + MSYS2_LOCATION: ${{ steps.msys2.outputs.msys2-location }} + + - name: Install additional python packages + run: | + python -m pip install ^ + pytest ^ + pytest-timeout ^ + pytest-xdist ^ + pytest-randomly + # Obfuscated usage of GitHub Actions features, Windows CMD shell limits analysis + shell: cmd /D /E:ON /V:OFF /S /C "CALL C:/OSGeo4W/OSGeo4W.bat "{0}"" # zizmor: ignore[misfeature] + - name: Run pytest with multiple workers in parallel + run: | + call %OSGEO4W_ROOT%\opt\grass\etc\env.bat + set PYTHONPATH=%GISBASE%\etc\python;%PYTHONPATH% + path %GISBASE%\lib;%GISBASE%\bin;%PATH% + pytest ^ + @.github/workflows/pytest_args_ci.txt ^ + @.github/workflows/pytest_args_parallel.txt ^ + --junitxml=pytest.xdist.junit.xml ^ + -k "not testsuite" + env: + OMP_NUM_THREADS: 1 + # Obfuscated usage of GitHub Actions features, Windows CMD shell limits analysis + shell: cmd /D /E:ON /V:OFF /S /C "CALL C:/OSGeo4W/OSGeo4W.bat "{0}"" # zizmor: ignore[misfeature] + + - name: Make JUnit report available + if: ${{ always() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: junit-attempt-${{ matrix.attempt }} + path: pytest.xdist.junit.xml + retention-days: 3 From a90c69a5c02aa64faba3ae980dfb124c1270116f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Fri, 4 Sep 2026 09:48:15 +0000 Subject: [PATCH 4/9] TEMP: Enable manual dispatch with 15 parallel fishing attempts Do not merge. Adds workflow_dispatch and a matrix "attempt" dimension (1 on normal push/PR events, 1-15 on manual dispatch) to fish for Windows order-dependent failures across many independent pytest-randomly seeds at once, reusing the existing compile job instead of a new (undispatchable) workflow file. Skips the gunittest run, the thorough test pass, and the Codecov upload on dispatched attempts, since only the randomized xdist step is relevant here. Revert this commit once the investigation concludes. Co-Authored-By: Claude Sonnet 5 --- .../workflows/TEMP-windows-xdist-fishing.yml | 142 ------------------ .github/workflows/osgeo4w.yml | 31 +++- 2 files changed, 27 insertions(+), 146 deletions(-) delete mode 100644 .github/workflows/TEMP-windows-xdist-fishing.yml diff --git a/.github/workflows/TEMP-windows-xdist-fishing.yml b/.github/workflows/TEMP-windows-xdist-fishing.yml deleted file mode 100644 index 4f75cf843a6..00000000000 --- a/.github/workflows/TEMP-windows-xdist-fishing.yml +++ /dev/null @@ -1,142 +0,0 @@ ---- -# TEMPORARY workflow for the Windows test-order-dependency investigation. -# Runs the randomized xdist pytest suite many times in parallel, each -# attempt getting its own pytest-randomly seed, to surface hidden -# order-dependent failures that a fixed test order never exercises. Not -# meant to be merged: delete this file once the investigation concludes. - -name: TEMP Windows xdist fishing - -on: - workflow_dispatch: - inputs: - attempts: - description: Number of parallel attempts - default: "15" - -permissions: {} - -jobs: - build: - name: attempt ${{ matrix.attempt }} - - runs-on: windows-2022 - env: - PYTHONWARNINGS: always - strategy: - matrix: - attempt: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] - fail-fast: false - - steps: - - name: Set git to use LF - run: | - git config --global core.autocrlf false - git config --global core.eol lf - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2.32.0 - id: msys2 - with: - path-type: inherit - location: D:\ - update: true - msystem: MINGW64 - install: >- - bison - diffutils - dos2unix - flex - git - libintl - make - tar - zip - pacboy: >- - bzip2 - ccache - fftw - gcc - gettext - libiconv - libsystre - libtre - libwinpthread-git - openblas - pcre - pkgconf - toolchain - zlib - - - name: Setup OSGeo4W environment - uses: echoix/setup-OSGeo4W@1e711832882e03603e9013181a87885b10af8a98 # v0.3.0 - id: osgeo4w - with: - package-dir: "D:/OSGeo4W_pkg" - packages: | - cairo-devel - freetype-devel - gdal-devel - geos-devel - libjpeg-turbo-devel - liblas-devel - libpng-devel - libpq-devel - libtiff-devel - netcdf-devel - proj-devel - python3-core - python3-gdal - python3-jupyter - python3-matplotlib - python3-numpy - python3-pip - python3-pywin32 - python3-wxpython - sqlite3-devel - zstd-devel - - - name: Set number of cores for compilation - run: | - echo "MAKEFLAGS=-j$(nproc)" >> "${GITHUB_ENV}" - shell: msys2 {0} - - - name: Compile GRASS - shell: msys2 {0} - run: | - .github/workflows/build_osgeo4w.sh - env: - MSYS2_LOCATION: ${{ steps.msys2.outputs.msys2-location }} - - - name: Install additional python packages - run: | - python -m pip install ^ - pytest ^ - pytest-timeout ^ - pytest-xdist ^ - pytest-randomly - # Obfuscated usage of GitHub Actions features, Windows CMD shell limits analysis - shell: cmd /D /E:ON /V:OFF /S /C "CALL C:/OSGeo4W/OSGeo4W.bat "{0}"" # zizmor: ignore[misfeature] - - name: Run pytest with multiple workers in parallel - run: | - call %OSGEO4W_ROOT%\opt\grass\etc\env.bat - set PYTHONPATH=%GISBASE%\etc\python;%PYTHONPATH% - path %GISBASE%\lib;%GISBASE%\bin;%PATH% - pytest ^ - @.github/workflows/pytest_args_ci.txt ^ - @.github/workflows/pytest_args_parallel.txt ^ - --junitxml=pytest.xdist.junit.xml ^ - -k "not testsuite" - env: - OMP_NUM_THREADS: 1 - # Obfuscated usage of GitHub Actions features, Windows CMD shell limits analysis - shell: cmd /D /E:ON /V:OFF /S /C "CALL C:/OSGeo4W/OSGeo4W.bat "{0}"" # zizmor: ignore[misfeature] - - - name: Make JUnit report available - if: ${{ always() }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: junit-attempt-${{ matrix.attempt }} - path: pytest.xdist.junit.xml - retention-days: 3 diff --git a/.github/workflows/osgeo4w.yml b/.github/workflows/osgeo4w.yml index 96b5a6bb9aa..3e860dbceef 100644 --- a/.github/workflows/osgeo4w.yml +++ b/.github/workflows/osgeo4w.yml @@ -7,17 +7,22 @@ on: - main - releasebranch_* pull_request: + # TEMPORARY, for the Windows test-order-dependency investigation: lets the + # "attempt" matrix dimension below be dispatched manually to fish for + # order-dependent failures with many parallel pytest-randomly seeds. + # Revert before merging. + workflow_dispatch: permissions: {} jobs: build: - name: ${{ matrix.os }} build and tests + name: ${{ matrix.os }} build and tests (attempt ${{ matrix.attempt }}) concurrency: group: >- ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}-${{ - matrix.os }} + matrix.os }}-${{ matrix.attempt }} cancel-in-progress: true runs-on: ${{ matrix.os }} @@ -27,6 +32,11 @@ jobs: matrix: os: - windows-2022 + # TEMPORARY: fans out into 15 parallel attempts only when manually + # dispatched, each getting its own pytest-randomly seed, to fish for + # order-dependent failures. Revert to a single attempt before + # merging. + attempt: ${{ github.event_name == 'workflow_dispatch' && fromJson('[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]') || fromJson('[1]') }} fail-fast: false steps: @@ -148,6 +158,10 @@ jobs: # Obfuscated usage of GitHub Actions features, Windows CMD shell limits analysis shell: cmd /D /E:ON /V:OFF /S /C "CALL C:/OSGeo4W/OSGeo4W.bat "{0}"" # zizmor: ignore[misfeature] - name: Run pytest with a single worker (for tests marked with needs_solo_run) + # TEMPORARY: skip during the fishing dispatch, not relevant to the + # investigation and this keeps each attempt fast. Revert before + # merging. + if: github.event_name != 'workflow_dispatch' run: | call %OSGEO4W_ROOT%\opt\grass\etc\env.bat set PYTHONPATH=%GISBASE%\etc\python;%PYTHONPATH% @@ -160,6 +174,9 @@ jobs: # Obfuscated usage of GitHub Actions features, Windows CMD shell limits analysis shell: cmd /D /E:ON /V:OFF /S /C "CALL C:/OSGeo4W/OSGeo4W.bat "{0}"" # zizmor: ignore[misfeature] - name: Run pytest with a single worker (for gunittest-based tests) + # TEMPORARY: skip during the fishing dispatch, see above. + # Revert before merging. + if: github.event_name != 'workflow_dispatch' run: | call %OSGEO4W_ROOT%\opt\grass\etc\env.bat set PYTHONPATH=%GISBASE%\etc\python;%PYTHONPATH% @@ -173,7 +190,10 @@ jobs: shell: cmd /D /E:ON /V:OFF /S /C "CALL C:/OSGeo4W/OSGeo4W.bat "{0}"" # zizmor: ignore[misfeature] - name: Upload test results to Codecov - if: ${{ !cancelled() }} + # TEMPORARY: skip during the fishing dispatch, to avoid uploading + # throwaway duplicate data under the same flag. Revert before + # merging. + if: ${{ !cancelled() && github.event_name != 'workflow_dispatch' }} uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: exclude: gui @@ -182,12 +202,15 @@ jobs: report_type: test_results - name: Run tests + # TEMPORARY: skip during the fishing dispatch, see above. + # Revert before merging. + if: github.event_name != 'workflow_dispatch' run: .github/workflows/test_thorough.bat '%O4WROOT%\opt\grass\grass86.bat' '%O4WROOT%\bin\python3' env: O4WROOT: ${{ steps.osgeo4w.outputs.root }} - name: Make HTML test report available - if: ${{ always() }} + if: ${{ always() && github.event_name != 'workflow_dispatch' }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: testreport-${{ matrix.os }} From d335f753522608b16fbd9da2f9986102189a29de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:54:08 +0000 Subject: [PATCH 5/9] TEMP: Skip PATH priming before pytest on the fishing dispatch Do not merge. The CI job manually prepends GISBASE's lib/bin dirs to PATH before invoking pytest, which real local usage never does (that priming exists only in this CI step, not in how a user actually runs pytest, nor in how grass.py's own startup works). Confirmed with the user that their local repro of the hypothesis runs pytest without any such priming, relying only on gs.setup.init() per test. Skip the priming on the workflow_dispatch fishing attempts so they match that setup instead of pre-empting the race being investigated. Revert before merging. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/osgeo4w.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/osgeo4w.yml b/.github/workflows/osgeo4w.yml index 3e860dbceef..81d1f4268a9 100644 --- a/.github/workflows/osgeo4w.yml +++ b/.github/workflows/osgeo4w.yml @@ -147,7 +147,15 @@ jobs: run: | call %OSGEO4W_ROOT%\opt\grass\etc\env.bat set PYTHONPATH=%GISBASE%\etc\python;%PYTHONPATH% - path %GISBASE%\lib;%GISBASE%\bin;%PATH% + rem TEMPORARY: on a normal push/PR run, prime PATH with GISBASE's + rem own lib/bin dirs before pytest starts, same as always. Skipped + rem on the fishing dispatch, since real local usage (pytest run + rem without going through grass.py's own startup) never does this + rem priming either, and doing it anyway would pre-empt the exact + rem DLL-search-path race under investigation. Revert before + rem merging (drop this conditional, restore the unconditional + rem line above the pytest call). + if not "%GITHUB_EVENT_NAME%"=="workflow_dispatch" path %GISBASE%\lib;%GISBASE%\bin;%PATH% pytest ^ @.github/workflows/pytest_args_ci.txt ^ @.github/workflows/pytest_args_parallel.txt ^ From c903acd63696492105514852ce6de369eef08da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:28:13 +0000 Subject: [PATCH 6/9] grass.gunittest: Add opt-in random test discovery order discover_modules() always sorts directories and files alphabetically during discovery, so python3 -m grass.gunittest.main (invoked once as a single process for the whole testsuite tree, e.g. by test_thorough.sh) always runs tests in the same fixed order. If some test's success secretly depends on state left behind by whichever test happens to run first in a directory (a plausible source of hidden, order-dependent failures, particularly on Windows where native library search state does not always propagate the way an environment variable change would suggest), a fixed order means that dependency never gets exercised differently and the bug stays invisible. Add an opt-in GRASS_GUNITTEST_RANDOM_ORDER environment variable that shuffles discovery order instead of sorting it, left off by default until validated, mirroring how pytest-randomly is an opt-in plugin rather than a default behavior change. Co-Authored-By: Claude Sonnet 5 --- python/grass/gunittest/loader.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/python/grass/gunittest/loader.py b/python/grass/gunittest/loader.py index 4b1841a8282..737b5bffcff 100644 --- a/python/grass/gunittest/loader.py +++ b/python/grass/gunittest/loader.py @@ -13,6 +13,7 @@ import fnmatch import os +import random import re import unittest from pathlib import PurePath @@ -97,12 +98,25 @@ def discover_modules( :returns: a list of GrassTestPythonModule objects + Discovery order is alphabetical (both directories and files) unless the + ``GRASS_GUNITTEST_RANDOM_ORDER`` environment variable is set, in which + case both are shuffled instead. Since ``main.py`` runs the whole + discovered suite in a single process, a fixed order means whichever + test happens to run first in a given testsuite directory never changes, + which can hide an order-dependent bug (e.g. one that depends on some + state, such as a native library search path, established by an earlier + test) behind whatever the alphabetically-first test happens to do. + .. todo:: Implement import_modules. """ + random_order = bool(os.environ.get("GRASS_GUNITTEST_RANDOM_ORDER")) modules = [] for root, dirs, unused_files in os.walk(start_dir, topdown=True): - dirs.sort() + if random_order: + random.shuffle(dirs) + else: + dirs.sort() for dir_pattern in skip_dirs: to_skip = fnmatch.filter(dirs, dir_pattern) for skip in to_skip: @@ -120,7 +134,10 @@ def discover_modules( files = [f for f in files if re.match(file_regexp, f)] if exclude: files = fnmatch_exclude_with_base(files, full, exclude) - files = sorted(files) + if random_order: + random.shuffle(files) + else: + files = sorted(files) # get test/module name without .py # expecting all files to end with .py # this will not work for invoking bat files but it works fine From 5751160f96801fcacd9d9e0354b3048b6852a721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Fri, 4 Sep 2026 13:28:13 +0000 Subject: [PATCH 7/9] TEMP: Fish for order-dependent gunittest failures via random order Do not merge. Re-enables the "Run tests" (test_thorough.bat) step on the fishing dispatch attempts, setting GRASS_GUNITTEST_RANDOM_ORDER=1 only for those attempts, since this is where pygrass's direct ctypes calls into compiled GRASS libraries are actually exercised (the pytest-xdist suite mostly calls tools via subprocess, which the code already handles correctly across process boundaries). Gives each attempt's HTML report artifact a distinct name so the 15 parallel uploads don't collide. Revert before merging. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/osgeo4w.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/osgeo4w.yml b/.github/workflows/osgeo4w.yml index 81d1f4268a9..d5757dcb63f 100644 --- a/.github/workflows/osgeo4w.yml +++ b/.github/workflows/osgeo4w.yml @@ -210,17 +210,24 @@ jobs: report_type: test_results - name: Run tests - # TEMPORARY: skip during the fishing dispatch, see above. - # Revert before merging. - if: github.event_name != 'workflow_dispatch' + # TEMPORARY: this is now the primary target of the fishing dispatch + # (see loader.py's discover_modules) rather than something to skip. + # GRASS_GUNITTEST_RANDOM_ORDER is set only on dispatch, so normal + # push/PR runs keep today's fixed alphabetical order until the + # randomized order has been validated. Revert before merging + # (drop the env: block). run: .github/workflows/test_thorough.bat '%O4WROOT%\opt\grass\grass86.bat' '%O4WROOT%\bin\python3' env: O4WROOT: ${{ steps.osgeo4w.outputs.root }} + GRASS_GUNITTEST_RANDOM_ORDER: ${{ github.event_name == 'workflow_dispatch' && '1' || '' }} - name: Make HTML test report available - if: ${{ always() && github.event_name != 'workflow_dispatch' }} + # TEMPORARY: give each attempt's report a distinct artifact name so + # the 15 parallel dispatch jobs don't collide. Revert before + # merging (drop the "-attempt-..." suffix). + if: ${{ always() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: testreport-${{ matrix.os }} + name: testreport-${{ matrix.os }}-attempt-${{ matrix.attempt }} path: testreport retention-days: 3 From 64926eeec191629fdd43c4908e90e4deaa7e7d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:42:38 +0000 Subject: [PATCH 8/9] CI: Revert fishing-only scaffolding, enable random gunittest order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the temporary workflow_dispatch trigger, the "attempt" matrix dimension, and the various skip conditions used to fish for order-dependent Windows failures. Across 15 parallel attempts with genuinely different pytest-randomly seeds and grass.gunittest discovery orders, results were byte-for-byte identical every time (the same 8 pre-existing, order-independent gunittest failures, already catalogued elsewhere) — no order-dependent failure was found. Keeps the permanent improvements this investigation validated as safe: pytest-xdist and pytest-randomly for the pytest suite, and (matching the same "run tests in random order" goal, now for the mechanism that actually turned out to fit it) GRASS_GUNITTEST_RANDOM_ORDER=1 enabled by default for the gunittest thorough-test run, so this class of bug cannot hide behind a fixed test order going forward even though this particular investigation didn't surface one today. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/osgeo4w.yml | 49 +++++------------------------------ 1 file changed, 6 insertions(+), 43 deletions(-) diff --git a/.github/workflows/osgeo4w.yml b/.github/workflows/osgeo4w.yml index d5757dcb63f..583bb0ac9b9 100644 --- a/.github/workflows/osgeo4w.yml +++ b/.github/workflows/osgeo4w.yml @@ -7,22 +7,17 @@ on: - main - releasebranch_* pull_request: - # TEMPORARY, for the Windows test-order-dependency investigation: lets the - # "attempt" matrix dimension below be dispatched manually to fish for - # order-dependent failures with many parallel pytest-randomly seeds. - # Revert before merging. - workflow_dispatch: permissions: {} jobs: build: - name: ${{ matrix.os }} build and tests (attempt ${{ matrix.attempt }}) + name: ${{ matrix.os }} build and tests concurrency: group: >- ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}-${{ - matrix.os }}-${{ matrix.attempt }} + matrix.os }} cancel-in-progress: true runs-on: ${{ matrix.os }} @@ -32,11 +27,6 @@ jobs: matrix: os: - windows-2022 - # TEMPORARY: fans out into 15 parallel attempts only when manually - # dispatched, each getting its own pytest-randomly seed, to fish for - # order-dependent failures. Revert to a single attempt before - # merging. - attempt: ${{ github.event_name == 'workflow_dispatch' && fromJson('[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]') || fromJson('[1]') }} fail-fast: false steps: @@ -147,15 +137,7 @@ jobs: run: | call %OSGEO4W_ROOT%\opt\grass\etc\env.bat set PYTHONPATH=%GISBASE%\etc\python;%PYTHONPATH% - rem TEMPORARY: on a normal push/PR run, prime PATH with GISBASE's - rem own lib/bin dirs before pytest starts, same as always. Skipped - rem on the fishing dispatch, since real local usage (pytest run - rem without going through grass.py's own startup) never does this - rem priming either, and doing it anyway would pre-empt the exact - rem DLL-search-path race under investigation. Revert before - rem merging (drop this conditional, restore the unconditional - rem line above the pytest call). - if not "%GITHUB_EVENT_NAME%"=="workflow_dispatch" path %GISBASE%\lib;%GISBASE%\bin;%PATH% + path %GISBASE%\lib;%GISBASE%\bin;%PATH% pytest ^ @.github/workflows/pytest_args_ci.txt ^ @.github/workflows/pytest_args_parallel.txt ^ @@ -166,10 +148,6 @@ jobs: # Obfuscated usage of GitHub Actions features, Windows CMD shell limits analysis shell: cmd /D /E:ON /V:OFF /S /C "CALL C:/OSGeo4W/OSGeo4W.bat "{0}"" # zizmor: ignore[misfeature] - name: Run pytest with a single worker (for tests marked with needs_solo_run) - # TEMPORARY: skip during the fishing dispatch, not relevant to the - # investigation and this keeps each attempt fast. Revert before - # merging. - if: github.event_name != 'workflow_dispatch' run: | call %OSGEO4W_ROOT%\opt\grass\etc\env.bat set PYTHONPATH=%GISBASE%\etc\python;%PYTHONPATH% @@ -182,9 +160,6 @@ jobs: # Obfuscated usage of GitHub Actions features, Windows CMD shell limits analysis shell: cmd /D /E:ON /V:OFF /S /C "CALL C:/OSGeo4W/OSGeo4W.bat "{0}"" # zizmor: ignore[misfeature] - name: Run pytest with a single worker (for gunittest-based tests) - # TEMPORARY: skip during the fishing dispatch, see above. - # Revert before merging. - if: github.event_name != 'workflow_dispatch' run: | call %OSGEO4W_ROOT%\opt\grass\etc\env.bat set PYTHONPATH=%GISBASE%\etc\python;%PYTHONPATH% @@ -198,10 +173,7 @@ jobs: shell: cmd /D /E:ON /V:OFF /S /C "CALL C:/OSGeo4W/OSGeo4W.bat "{0}"" # zizmor: ignore[misfeature] - name: Upload test results to Codecov - # TEMPORARY: skip during the fishing dispatch, to avoid uploading - # throwaway duplicate data under the same flag. Revert before - # merging. - if: ${{ !cancelled() && github.event_name != 'workflow_dispatch' }} + if: ${{ !cancelled() }} uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: exclude: gui @@ -210,24 +182,15 @@ jobs: report_type: test_results - name: Run tests - # TEMPORARY: this is now the primary target of the fishing dispatch - # (see loader.py's discover_modules) rather than something to skip. - # GRASS_GUNITTEST_RANDOM_ORDER is set only on dispatch, so normal - # push/PR runs keep today's fixed alphabetical order until the - # randomized order has been validated. Revert before merging - # (drop the env: block). run: .github/workflows/test_thorough.bat '%O4WROOT%\opt\grass\grass86.bat' '%O4WROOT%\bin\python3' env: O4WROOT: ${{ steps.osgeo4w.outputs.root }} - GRASS_GUNITTEST_RANDOM_ORDER: ${{ github.event_name == 'workflow_dispatch' && '1' || '' }} + GRASS_GUNITTEST_RANDOM_ORDER: "1" - name: Make HTML test report available - # TEMPORARY: give each attempt's report a distinct artifact name so - # the 15 parallel dispatch jobs don't collide. Revert before - # merging (drop the "-attempt-..." suffix). if: ${{ always() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: testreport-${{ matrix.os }}-attempt-${{ matrix.attempt }} + name: testreport-${{ matrix.os }} path: testreport retention-days: 3 From aef4a430f2766c6d9cebb0af9d712283aaac090e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edouard=20Choini=C3=A8re?= <27212526+echoix@users.noreply.github.com> Date: Sun, 6 Sep 2026 12:38:40 +0000 Subject: [PATCH 9/9] r.kappa: Revert testsuite fix, split into standalone PR #694 The NamedTemporaryFile fix for test_file was bundled into this PR together with unrelated Windows CI changes. Split it into its own PR (#694) so it can be reviewed and merged independently; revert it here to avoid landing it twice. This reverts commit 124fe833c59398f46a1885e3218e2ae8be2f0ccc. Co-Authored-By: Claude Sonnet 5 --- raster/r.kappa/testsuite/test_r_kappa.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/raster/r.kappa/testsuite/test_r_kappa.py b/raster/r.kappa/testsuite/test_r_kappa.py index b17745ad45d..925a8658ed6 100644 --- a/raster/r.kappa/testsuite/test_r_kappa.py +++ b/raster/r.kappa/testsuite/test_r_kappa.py @@ -11,13 +11,15 @@ import os import json from pathlib import Path +from tempfile import NamedTemporaryFile from grass.script import read_command from grass.script import decode -from grass.script.core import tempfile, tempname +from grass.script.core import tempname from grass.gunittest.case import TestCase from grass.gunittest.main import test from grass.gunittest.checkers import keyvalue_equals, diff_keyvalue +from grass.gunittest.utils import xfail_windows class MatrixCorrectnessTest(TestCase): @@ -486,18 +488,19 @@ def test_stdout(self): ), ) + @xfail_windows def test_file(self): for i in range(len(self.references)): - outfile = tempfile(create=False) + f = NamedTemporaryFile() self.runModule( "r.kappa", reference=self.references[i], classification=self.classifications[i], - output=outfile, + output=f.name, format="json", overwrite=True, ) - json_out = json.loads(Path(outfile).read_text()) + json_out = json.loads(f.read()) self.assertTrue( keyvalue_equals( self.expected_outputs[i], json_out, precision=self.precision