Skip to content

chore(ci): pass python -u to run_python_test_script.py callsites - #6071

Merged
Fedr merged 4 commits into
masterfrom
chore/run-python-test-stream-output
May 8, 2026
Merged

Fedr merged 4 commits into
masterfrom
chore/run-python-test-stream-output

Conversation

@Fedr

@Fedr Fedr commented May 7, 2026

Copy link
Copy Markdown
Contributor

Why

scripts/run_python_test_script.py invokes pytest via os.system(...). CI captures stdout via a pipe — block-buffered by default — so the script's own print() output and the subprocess's output go to the same fd but at different times: Python's buffer holds the script's prints until the process exits, while pytest's output writes through directly. The result is that print() calls flush after a child that ran before them, reordering the log.

Concretely visible in this recent macos-build-test failure (job 74880762447):

============================= test session starts ==============================
platform darwin -- Python 3.10.20, pytest-7.4.4, pluggy-1.2.0 -- /opt/...
cachedir: .pytest_cache
rootdir: /Users/admin/actions-runner/_work/MeshLib/MeshLib/test_python
plugins: check-2.3.1
collecting ... Namespace(cmd=None, multi_cmd=False, create_venv=False, ...)
python3.10 -m pytest -s -v --basetemp=../pytest_temp ...
ERROR: Some tests failed!

Namespace(...) is the script's own print(args) from line 40 — it should appear first in the output but flushed mid-line into pytest's collecting .... Worse, all the actual pytest test results / failure summaries between collecting ... and the final ERROR: line are missing from the log; CI only sees a generic "Some tests failed" with no diagnostic.

Changes

Pass -u (unbuffered stdout/stderr) to the Python interpreter at every callsite that runs scripts/run_python_test_script.py. The script itself is unchanged.

Workflow Job Before After
build-test-linux-vcpkg.yml Python Sanity Tests python3 … python3 -u …
build-test-macos.yml Python Sanity Tests python3 … python3 -u …
build-test-ubuntu-arm64.yml Python Sanity Tests python3 … python3 -u …
build-test-ubuntu-x64.yml Python Sanity Tests python3 … python3 -u …
build-test-windows.yml Python Sanity Tests py -3 … py -3 -u …
pip-build.yml manylinux Python Tests python3 … (inside uv run) python3 -u …
pip-build.yml windows Python Tests py … py -3 -u …
pip-build.yml macos Python Tests python3 … python3 -u …

8 lines added, 0 removed. No script change.

Test plan

  • On a green CI run, pytest output appears in chronological order with the script's prints — no collecting ... Namespace(...) interleaving. Verified on job 74984990805: Namespace(...) prints at line 10050 before test session starts at 10054 and collecting ... collected 86 items at 10059.
  • On a red CI run, pytest's actual test-failure summary will be visible in the log instead of being hidden behind a generic "ERROR: Some tests failed!" — same buffering fix surfaces both directions.

Fedr added 4 commits May 7, 2026 21:51
… os.system

run_python_test_script.py uses os.system() to invoke pytest. Python's own
stdout buffer (the script's print() calls) and the subprocess's output go
to the same fd in CI, but the buffer doesn't flush before os.system spawns
the shell, so the script's prints can flush *after* a subprocess that ran
*before* them. Visible in a recent macos-build-test log as:

  ============================= test session starts ==============================
  platform darwin -- Python 3.10.20, pytest-7.4.4, pluggy-1.2.0 -- /opt/...
  cachedir: .pytest_cache
  rootdir: /Users/admin/actions-runner/_work/MeshLib/MeshLib/test_python
  plugins: check-2.3.1
  collecting ... Namespace(cmd=None, multi_cmd=False, create_venv=False, ...)
  python3.10 -m pytest -s -v --basetemp=../pytest_temp ...
  ERROR: Some tests failed!

`Namespace(...)` is the script's own `print(args)` from line 40 — it
should appear FIRST in the output but ends up half-way through pytest's
"collecting ..." line because of the buffer mix-up. The actual pytest
test results / failure summaries between "collecting ..." and the
script's "ERROR" are missing entirely; CI only gets a generic
"Some tests failed" with no diagnostic.

Fix: introduce a small `run_shell()` helper that calls `sys.stdout.flush()`
+ `sys.stderr.flush()` before `subprocess.run(cmd, shell=True, check=False)`.
Replace every `os.system(...)` with `run_shell(...)`. Add `flush=True` to
all `print(...)` calls for belt-and-suspenders. Add a "^^^ pytest exited
with code N" line after a failed run so the actual exit code (5 = no
tests collected, 2 = collection error, 1 = test failures, …) is in the
log when the next person debugs a failure.

Behavior is otherwise unchanged — same args, same exit code on failure.
…utput

CI captures stdout via a pipe (block-buffered by default), which lets the
script's own print() output flush after a child's output and reorder the
log — visible in the macos-build-test failure where 'Namespace(...)'
(printed at line 40, before the loop) ended up *inside* pytest's
'collecting ...' line. Switch sys.stdout/sys.stderr to line-buffered;
keep the rest of the script (os.system + plain print) untouched.
…side

Move the line-buffering knob out of scripts/run_python_test_script.py
into each workflow callsite as the standard `-u` interpreter flag.
Same effect (Python writes stdout/stderr unbuffered, prints stay in
order with subprocess output in CI logs); no changes to the script
itself, so the fix is co-located with where the CI buffering matters.

Touched 8 callsites:

  build-test-{linux-vcpkg,macos,ubuntu-arm64,ubuntu-x64}.yml
  build-test-windows.yml
  pip-build.yml (manylinux + windows + macos pip-build jobs)

Each `python3 …` becomes `python3 -u …`; the Windows `py -3 …` and
`py …` invocations both become `py -3 -u …`.
@Fedr Fedr changed the title chore(ci): stream pytest output through subprocess.run instead of os.system chore(ci): pass python -u to run_python_test_script.py callsites May 8, 2026
@Fedr
Fedr merged commit 1b1ec84 into master May 8, 2026
39 checks passed
@Fedr
Fedr deleted the chore/run-python-test-stream-output branch May 8, 2026 09:54
Fedr added a commit that referenced this pull request May 8, 2026
Two follow-ups for the verify-meshlib-python-import step:

1) Add `timeout-minutes: 3` to each of the 8 callsites. The step is
   cheap (one or a few `import meshlib.mrmeshpy` calls), so 3 minutes
   is plenty even on slow runners; without it the step could in
   theory hang on a runner-side issue and inherit the surrounding
   job's much longer timeout (60–100 min).

2) Pass `-u` to python at the action invocation, same trick as #6071.
   Lets us drop the half-applied `flush=True` on individual prints
   in `scripts/ci/verify_meshlib_import.py` — `-u` flushes
   stdout/stderr unbuffered, so the script's own output stays in
   chronological order with the wrapping shell's `echo` lines (the
   per-shim `===== verify with pythonX.Y =====` headers, the final
   `Summary:` line, etc.) in the GitHub Actions log.
Fedr added a commit that referenced this pull request May 8, 2026
* ci: verify meshlib.mrmeshpy imports cleanly before Unit Tests

When the Python bindings are built but `mrmeshpy.pyd` / `.so` can't be
loaded — DLL load failure, missing PyInit_*, libpython ABI mismatch,
binding-generation regression that drops a referenced class — MRTest's
embedded-python smoke test surfaces the problem only as CPython's
opaque

  ImportError: initialization failed

…with no traceback and a single `<string>(N): <module>` location.
Diagnosing requires reproducing the import out-of-process to get a
real traceback.

Add a small Python script (`scripts/ci/verify_meshlib_import.py`) that
runs `import meshlib` and `import meshlib.mrmeshpy` with PYTHONPATH
pointing at the build's bin dir, exits non-zero with a real Python
traceback on failure, and a composite action
(`.github/actions/verify-meshlib-python-import`) that invokes it with
the right shell on Windows (pwsh + py -3) vs Unix (bash + python3).

Wire the action into all eight build pipelines that run
`MRTest`/`MRTest.exe`, immediately before the Unit Tests step:

  * build-test-windows.yml        (Windows main matrix)
  * build-test-linux-vcpkg.yml    (Linux vcpkg)
  * build-test-macos.yml          (macOS)
  * build-test-ubuntu-x64.yml     (Ubuntu x64)
  * build-test-ubuntu-arm64.yml   (Ubuntu arm64)
  * pip-build.yml                 (manylinux + windows + macos
                                   pip-wheel jobs)

Each invocation respects the same `if:` gate the workflow already uses
to decide whether bindings were built (`inputs.mrbind` for the build-test
matrices, the iterator-debug exclusion on Windows, unconditional for
the pip-wheel jobs which always build bindings).

* ci: verify import for every pybind11 non-limited-api shim, not just one

Previous version probed for one shim and used the matching Python.
That works for build-test-* (one shim per build), but pip-build's
wheel jobs (FOR_WHEEL=1) build a shim per Python in
scripts/mrbind-pybind11/python_versions.txt — currently 3.8 .. 3.14.
Verify against all of them.

For each pybind11nonlimitedapi_meshlib_<X.Y>.<ext> in
<build-bin-dir>/meshlib/:

* if `pythonX.Y` (Unix) or `py -X.Y` (Windows) is on the runner,
  invoke it on scripts/ci/verify_meshlib_import.py and tally
  pass/fail.
* if not on the runner, log "SKIP: pythonX.Y not on PATH" so the
  step's output is honest about coverage instead of silently
  hiding gaps.

Step exits non-zero if any *attempted* import failed, prints a
summary line of shims/attempted/failed/skipped at the end. The
single-shim build-test-* case still works — the loop runs once
with the one matching Python.

* ci: import every native submodule in meshlib/, not just mrmeshpy

The existing single-import check `import meshlib.mrmeshpy` is too narrow:
`test_python/helper/__init__.py` does both `import meshlib.mrmeshpy as
mrmesh` AND `import meshlib.mrmeshnumpy as mrmeshnumpy` at module
load. If pytest collects a test file that pulls in `helper`, both of
those imports run during collection. A crash in mrmeshnumpy (or any
other shipped submodule) takes pytest's collector down silently —
exactly what we're seeing on Daniil's macOS runner.

Iterate over every native-extension file in `<build>/meshlib/`
(`*.{so,dylib,pyd}`), strip ABI tags like `.cpython-310-darwin`, skip
`libpybind11nonlimitedapi*` shims, and import each with traceback on
failure. Print a per-module OK/FAIL line and a tally summary at the
end. Same exit-code semantics as before (non-zero on any failure).

* ci: mirror exact helper imports — drop mrviewerpy/mrcudapy auto-discovery

A run on the new code (Ubuntu Debug) imported all 4 native submodules
in meshlib/ and exposed an unrelated pre-existing bug: importing
`meshlib.mrviewerpy` and letting Python shut down trips a C++ debug
assertion in MRViewer/MRCommandLoop.cpp (CommandLoop destructor
expecting a drained queue), aborting the process despite all 4
imports having succeeded:

  OK:   meshlib.mrmeshpy ...
  OK:   meshlib.mrmeshnumpy ...
  OK:   meshlib.mrviewerpy ...
  Summary: 4/4 submodules imported OK
  python3.10: source/MRViewer/MRCommandLoop.cpp:13:
    MR::CommandLoop::~CommandLoop(): Assertion `commands_.empty()' failed.
  Aborted (core dumped)

That's a real but separate bug from what this script is gating
against (binding load failures pre-pytest), and it makes the verify
step false-positive on Debug builds.

Drop auto-discovery; hardcode the list to exactly what
test_python/helper/__init__.py imports — `mrmeshpy` and `mrmeshnumpy`
— since `helper` is what pytest collection pulls in transitively
through every test file. That's the meaningful set for catching
collection-time silent crashes (the original Daniil failure mode).
Comment links the list to the helper file so the next person knows
to keep it in sync.

* ci: drop mrmeshnumpy from verify step — needs numpy not installed in pip-build env

Last run on pip-build's manylinux x86_64 job hit:

  FAIL: import meshlib.mrmeshnumpy
  ModuleNotFoundError: No module named 'numpy'
  ImportError: initialization failed

That container's bare system Python has no numpy — the actual
Python Tests step uses `uv run --with-requirements requirements/python.txt`
to spawn each interpreter with numpy installed, but the verify step
runs against the bare system Python without that, so mrmeshnumpy's
module-init `import numpy` fails. False positive for the verify step.

Drop mrmeshnumpy from the import list. Keep mrmeshpy: it's the one
whose silent load failure was the original motivation, and it has no
external runtime deps. mrmeshnumpy load failures (in env where numpy
is actually present) still surface downstream in pytest collection.

* ci: add 3-minute timeout + run verify script with python -u

Two follow-ups for the verify-meshlib-python-import step:

1) Add `timeout-minutes: 3` to each of the 8 callsites. The step is
   cheap (one or a few `import meshlib.mrmeshpy` calls), so 3 minutes
   is plenty even on slow runners; without it the step could in
   theory hang on a runner-side issue and inherit the surrounding
   job's much longer timeout (60–100 min).

2) Pass `-u` to python at the action invocation, same trick as #6071.
   Lets us drop the half-applied `flush=True` on individual prints
   in `scripts/ci/verify_meshlib_import.py` — `-u` flushes
   stdout/stderr unbuffered, so the script's own output stays in
   chronological order with the wrapping shell's `echo` lines (the
   per-shim `===== verify with pythonX.Y =====` headers, the final
   `Summary:` line, etc.) in the GitHub Actions log.

* ci: shorten verify-step comments per review

Drop the long-winded background paragraphs from the script docstring and
the action comments; keep just enough to explain the per-shim loop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants