Skip to content

Give every pwsh call in the suite the private startup cache - #10900

Merged
danielhanchen merged 2 commits into
mainfrom
tests-pwsh-private-cache
Sep 14, 2026
Merged

Give every pwsh call in the suite the private startup cache#10900
danielhanchen merged 2 commits into
mainfrom
tests-pwsh-private-cache

Conversation

@danielhanchen

@danielhanchen danielhanchen commented Sep 14, 2026

Copy link
Copy Markdown
Member

What this is, after a correction

I opened this against d49efcfe7 with a measured flake: tests/studio/install/test_installed_release_backend_line.py called pwsh through subprocess.run rather than through tests/_shared/unsloth_pwsh_runner.py, so it never got the per-worker startup cache, and at -n 16 it failed 1 run in 3, 14 cases at a time, returncode=-6 / Stack overflow.

That file has since been fixed on main by an equivalent change. Merging main in produced a conflict where upstream's side already routes it through run_pwsh; I took upstream's side. So the numbers this PR opened with describe a defect main has already closed, and they are not evidence for what is left here.

Re-measured on current main, same command, three runs:

2 failed, 7595 passed, 128 skipped   (234.68s)
2 failed, 7595 passed, 128 skipped   (231.61s)
2 failed, 7595 passed, 128 skipped   (233.77s)

No pwsh deaths, no host-fault skips. The two failures are the pre-existing tests/python/test_torchcodec_torch_compat.py pair on this box. So there is no failing test left for this PR to fix, and it should be judged as prevention plus a residue, not as a bug fix.

What is left, and why it is still worth having

Five call sites in five files still start PowerShell outside the private cache. They are in the same race, at lower volume than the file that was fixed: two subprocess.Popen holders in tests/python/test_windows_installer_concurrency_guard.py plus one subprocess.run there, one in tests/studio/test_installer_av_shapes.py, one in tests/studio/test_install_phase_timing.py, one in tests/python/test_pwsh_runner_encoding.py.

Three of those are skipif(os.name != "nt"), so they cannot show up on Linux at all, and one of the Linux ones — test_installer_av_shapes.py — converts an interpreter crash into a skip through its _PWSH_HOST_FAULT banner, which names System.IO.FileLoadException outright. That is the honest reason there is nothing to measure: that file has been quietly skipping past this bug rather than reporting it.

pwsh_env(env = None) in the runner returns env (default os.environ) with XDG_CACHE_HOME pointed at the existing per-worker cache dir; run_pwsh now calls it instead of doing the same six lines inline. Pure extract-method, behaviour unchanged, its own test file 27/27. It exists because three of those sites cannot become run_pwsh callers without changing what they test: a Popen holder written to over stdin while a second shell races it for a named mutex, a call site whose policy needs the crashed CompletedProcess back rather than an exception, and a deliberate control that must invoke pwsh the pre-fix way to show the fix changes something. Rewriting those around run_pwsh would bend the tests; handing them the cache directory removes them from the race and leaves their control flow alone.

tests/studio/test_pwsh_calls_use_the_shared_runner.py is the part that lasts. It walks the AST of every *.py under tests/ — not just test_*.py, so a conftest or a _shared helper cannot hide — resolves subprocess aliases and from-imports across run / Popen / call / check_call / check_output, and resolves the interpreter as a literal, as an .exe path, or through a module constant bound to one. A call passes if it goes through run_pwsh or passes env = pwsh_env(...), matched on the AST, so the exemption is a checkable property of the call and not a comment.

_ALLOWED_DIRECT_PWSH_CALLS mirrors _EXPECTED_CI_SKIPS in test_ci_shell_suite_coverage.py. One file is allowlisted, tests/test_windows_amd_gpu_scan_fallback.py (3 sites), because it hands its child a hermetic {"PATH": ..., "HOME": tmp_path} so XDG_CACHE_HOME already resolves inside tmp_path — which is why the runner's own header names it as the one pwsh-heavy file with zero failures in the CI run that prompted that module.

Without the guard, the fix that just landed on main is one subprocess.run away from being undone, in a file nobody would think to check.

Checks

  • Guard: 5 tests, 5 passed, including self-tests for five detection shapes (literal, .exe path, from-import, Popen([PWSH, ...]), args= kwarg) and seven that must not flag.
  • Not vacuous: inserting subprocess.run(["pwsh", "-NoProfile", "-Command", "Write-Output hi"]) into another test file fails it with tests/studio/test_ci_shell_suite_coverage.py:31: subprocess.run(...), naming file and line. Reverted after.
  • The touched files re-run after formatting: 605 passed, 19 skipped.
  • ruff check --select E9,F63,F7,F82 clean on all of them; the repo's full ruff check passes; scripts/run_ruff_format.py is a fixed point by md5 across two passes.

Not verified here: the three Windows sites are skipif(os.name != "nt") and never execute on Linux. Their change is reasoned and lint-clean, and only Windows CI can exercise it.

tests/_shared/unsloth_pwsh_runner.py exists because a `pwsh -NonInteractive`
startup reads and rewrites $XDG_CACHE_HOME/powershell/StartupProfileData-
NonInteractive, and under xdist every worker shares one $HOME, so the whole job
races on one file. A startup that deserialises a half-written one dies before it
reaches our script.

That protection is opt-in, and tests/studio/install/test_installed_release_
backend_line.py was calling pwsh directly through subprocess.run, so all 498 of
its parametrised PowerShell cases opted out. Measured on this box at -n 16 over
`pytest tests/python tests/kaggle tests/studio/install`, three runs of
origin/main: 0, 14 and 0 failures in that file, every one of them
returncode -6 with `Stack overflow.` on stderr, reported by the assertions as
the printer emitting the wrong line. Five runs with it routed through run_pwsh:
0, 0, 0, 0, 0.

Five other call sites were spawning PowerShell directly too. One more could take
run_pwsh as-is; the rest could not, because run_pwsh is a subprocess.run wrapper
and they are a long-lived Popen holder written to over stdin, a deliberate
control that has to invoke pwsh the old way, and a caller with its own crash
policy that needs the crashed CompletedProcess back rather than an exception.
Rewriting those around run_pwsh would change what they test, so the private
cache directory is exposed on its own as pwsh_env() and they take that instead.
run_pwsh now calls pwsh_env for its own env, so there is one implementation.

Adds tests/studio/test_pwsh_calls_use_the_shared_runner.py, which walks the AST
of every Python file under tests/ and fails on a subprocess spawn of pwsh or
powershell that neither goes through run_pwsh nor passes pwsh_env. Grep cannot
do this: the interpreter name sits in an argv list, and every file that already
uses the runner mentions pwsh in prose. tests/test_windows_amd_gpu_scan_
fallback.py is allowlisted with its reason, as it hands the child a hermetic env
whose HOME is tmp_path and so was never in the race.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 14, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-14T04:18:40.218856Z f7c785e PR opened
🔒 Security Review Completed 2026-09-14T04:19:05.926147Z f7c785e PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

# Conflicts:
#	tests/studio/install/test_installed_release_backend_line.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f7c785edda

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +253 to +256
offenders = {
rel: calls
for rel, calls in scan_tests().items()
if rel not in _ALLOWED_DIRECT_PWSH_CALLS

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Scope direct-call exemptions to individual call sites

When a new direct PowerShell spawn is added to test_windows_amd_gpu_scan_fallback.py without the existing hermetic HOME, scan_tests() detects it but this comprehension discards every finding from the file because the path is allowlisted. The stale-entry test only verifies that at least one direct call remains, so CI would stay green while the new call reintroduces the shared-cache race; key exemptions by call location or validate the environment of each detected call instead.

Useful? React with 👍 / 👎.

@danielhanchen
danielhanchen merged commit b876d50 into main Sep 14, 2026
20 of 23 checks passed
@danielhanchen
danielhanchen deleted the tests-pwsh-private-cache branch September 14, 2026 05:40
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