Skip to content

Commit 5ff0678

Browse files
committed
fix: two flaky required checks — the Visual Studio mask and e2e 845 (#806)
* test: steer e2e 845 by the status row it asserts, not by the clock `845_play_game_restores_the_terminal.sh` sent its arrow keys and its Ctrl-C at a fixed 1.5s after the fork, while the build reaches Phase::Building only after its build program has compiled and run (and after the toolchain was resolved). On the runner where run 38406939721's macos leg failed, that work was 1.24s of the 1.5s budget, so the signal landed in the Programs phase and G1's `Building .* · snake \d+` found no row to match -- while the rows that did reach the terminal carried the score, and only the phase word differed (phase_status() prints `Running` for Phase::Programs). The wait is now the row itself: the arrows and the Ctrl-C go out when the frame the assertion names first appears, which is also the frame that proves the prepare action (the 4s sleep) is running. A 60s cap keeps a build that never draws the row from hanging the leg. The score assertion is unchanged. * fix: retry a refused Visual Studio mask, and keep it in one action The "Mask Visual Studio" step renamed `...\Microsoft Visual Studio\*\*\VC` to `VC.masked` in one attempt and treated "Access to the path is denied" as final. That refusal is a state of the host, not of the image: on windows-2025-vs2026 (image 20260925.250) run 38406939721 was refused at 13:59 UTC where the same image committed the same rename at 09:57 and 14:32. Since the step's postcondition is what keeps an incomplete mask from reading as a green "bare Windows" row, the refusal turned into a red required check. The rename is now retried (5 attempts, 2s apart), and a directory that stays locked is answered one level deeper: a handle into VC does not block renaming VC\Tools, and VC\Tools\MSVC is where vswhere's resolution, the environment strategy and the well-known-path scan all end. The postcondition, the env variables it clears and the failure it prints are unchanged. The block had three copies — ci-windows.yml, ci-fresh-install.yml and measure-windows-tool-crt.yml — which is why they could drift; it now lives in .github/actions/mask-visual-studio and the three call sites name it. (cherry picked from commit b5630e2)
1 parent 608d307 commit 5ff0678

5 files changed

Lines changed: 129 additions & 97 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
name: Mask Visual Studio
2+
description: >
3+
Make every MSVC discovery strategy of msvc.cppm fail on a runner that ships
4+
Visual Studio, and assert that they do. The runner is disposable, so this is
5+
both safe and closer to "absent" than any env-only trick.
6+
7+
inputs:
8+
attempts:
9+
description: How many times one rename is retried before it counts as denied
10+
required: false
11+
default: "5"
12+
retry-delay-ms:
13+
description: Milliseconds between two attempts at the same rename
14+
required: false
15+
default: "2000"
16+
17+
runs:
18+
using: composite
19+
steps:
20+
- name: Mask Visual Studio
21+
shell: pwsh
22+
env:
23+
MASK_ATTEMPTS: ${{ inputs.attempts }}
24+
MASK_RETRY_DELAY_MS: ${{ inputs.retry-delay-ms }}
25+
run: |
26+
$ErrorActionPreference = 'Continue'
27+
28+
# All three of msvc.cppm's discovery strategies converge on
29+
# <vsRoot>\VC\Tools\MSVC — vswhere returns an installationPath that
30+
# find_latest_msvc_tools then resolves through it, the env strategy
31+
# checks it explicitly, and the well-known-path scan tests for it — so
32+
# masking that directory is what makes this runner answer "no Visual
33+
# Studio" to all three.
34+
#
35+
# The rename is refused outright on some hosts: "Access to the path
36+
# '...\18\Enterprise\VC' is denied", while the same script on the same
37+
# runner image commits the same rename minutes earlier or later (image
38+
# windows-2025-vs2026 20260925.250 renamed it in one run and was
39+
# refused in another). A refusal is therefore retried, and then
40+
# answered one level deeper: a handle into VC does not block renaming
41+
# VC\Tools, and the postcondition below only needs VC\Tools\MSVC gone.
42+
# What outlasts both is reported, never swallowed.
43+
$attempts = [int]$env:MASK_ATTEMPTS
44+
$delay = [int]$env:MASK_RETRY_DELAY_MS
45+
46+
function Mask-Path([string]$Path) {
47+
for ($i = 1; $i -le $attempts; $i++) {
48+
try {
49+
Rename-Item -LiteralPath $Path -NewName "$(Split-Path $Path -Leaf).masked" -ErrorAction Stop
50+
Write-Host "masked $Path"
51+
return $true
52+
} catch {
53+
if ($i -lt $attempts) {
54+
Start-Sleep -Milliseconds $delay
55+
} else {
56+
Write-Host " denied after $attempts attempts: $Path — $($_.Exception.Message)"
57+
}
58+
}
59+
}
60+
return $false
61+
}
62+
63+
$vswhere = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"
64+
if (Test-Path $vswhere) { [void](Mask-Path $vswhere) }
65+
66+
# -Path with a trailing wildcard segment lists the CONTENTS of the
67+
# matches, not the matches themselves, so `…\*\*\VC` would hand back
68+
# VC's children. Resolve-Path returns the directories themselves.
69+
$vsRoots = @(Resolve-Path "C:\Program Files*\Microsoft Visual Studio\*\*\VC" `
70+
-ErrorAction SilentlyContinue)
71+
foreach ($p in $vsRoots) { [void](Mask-Path $p.Path) }
72+
73+
# The fallback, and the reason it is enough: vswhere's resolution and
74+
# the well-known-path scan both end at VC\Tools\MSVC, so masking Tools
75+
# answers "no Visual Studio" as well as masking VC does.
76+
$tools = @(Resolve-Path "C:\Program Files*\Microsoft Visual Studio\*\*\VC\Tools" `
77+
-ErrorAction SilentlyContinue)
78+
foreach ($p in $tools) { [void](Mask-Path $p.Path) }
79+
80+
foreach ($v in @('VSINSTALLDIR','VCINSTALLDIR','VCToolsInstallDir',
81+
'VS170COMNTOOLS','VS160COMNTOOLS','VS150COMNTOOLS')) {
82+
"$v=" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
83+
}
84+
85+
# Check the mask's own postcondition here, where the cause is obvious,
86+
# instead of letting it surface three steps later as a confusing pass.
87+
$remaining = Get-ChildItem "C:\Program Files*\Microsoft Visual Studio\*\*\VC\Tools\MSVC" `
88+
-Directory -ErrorAction SilentlyContinue
89+
if ($remaining) {
90+
Write-Host "FAIL: VC tools still present after masking:"
91+
$remaining | ForEach-Object { Write-Host " $($_.FullName)" }
92+
exit 1
93+
}
94+
Write-Host "Visual Studio masked: no VC\Tools\MSVC remains."

‎.github/workflows/ci-fresh-install.yml‎

Lines changed: 6 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -751,40 +751,13 @@ jobs:
751751
steps:
752752
- uses: actions/checkout@v4
753753

754+
# The mask retries a refused rename — "denied" is a state of the host,
755+
# not of the image — and then masks VC\Tools, which is where all three
756+
# of msvc.cppm's discovery strategies end. Its postcondition, no
757+
# VC\Tools\MSVC left, is what keeps an incomplete mask from reading as a
758+
# green no-Visual-Studio install.
754759
- name: Mask Visual Studio
755-
shell: pwsh
756-
run: |
757-
$ErrorActionPreference = 'Continue'
758-
759-
# All three of msvc.cppm's discovery strategies converge on
760-
# <vsRoot>\VC\Tools\MSVC, so mask the VC directory rather than the
761-
# Visual Studio root: the root is held open on the runner and
762-
# renaming it is denied, while VC one level down renames fine.
763-
# The runner is disposable, so this is both safe and closer to
764-
# "absent" than any env-only trick would be.
765-
$vswhere = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"
766-
if (Test-Path $vswhere) { Rename-Item $vswhere "vswhere.exe.masked" }
767-
768-
Get-ChildItem "C:\Program Files*\Microsoft Visual Studio\*\*\VC" `
769-
-Directory -ErrorAction SilentlyContinue | ForEach-Object {
770-
Rename-Item $_.FullName "$($_.Name).masked" -ErrorAction SilentlyContinue
771-
}
772-
773-
foreach ($v in @('VSINSTALLDIR','VCINSTALLDIR','VCToolsInstallDir',
774-
'VS170COMNTOOLS','VS160COMNTOOLS','VS150COMNTOOLS')) {
775-
"$v=" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
776-
}
777-
778-
# Check the mask's own postcondition here, where the cause is
779-
# obvious, rather than letting it surface later as a confusing pass.
780-
$left = Get-ChildItem "C:\Program Files*\Microsoft Visual Studio\*\*\VC\Tools\MSVC" `
781-
-Directory -ErrorAction SilentlyContinue
782-
if ($left) {
783-
Write-Host "FAIL: VC tools still present after masking:"
784-
$left | ForEach-Object { Write-Host " $($_.FullName)" }
785-
exit 1
786-
}
787-
Write-Host "Visual Studio masked: no VC\Tools\MSVC remains."
760+
uses: ./.github/actions/mask-visual-studio
788761

789762
- name: Install xlings
790763
shell: pwsh

‎.github/workflows/ci-windows.yml‎

Lines changed: 5 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -143,52 +143,12 @@ jobs:
143143
echo "MCPP_SELF=$MCPP_SELF" >> "$GITHUB_ENV"
144144
145145
# Masked before anything else touches Visual Studio, so no process of
146-
# ours is holding a handle into it.
146+
# ours is holding a handle into it. The mask retries a refused rename
147+
# (a state of the host, not of the image) and then masks VC\Tools; its
148+
# postcondition — no VC\Tools\MSVC left — is what keeps an incomplete
149+
# mask from reading as a green "bare Windows" row.
147150
- name: Mask Visual Studio
148-
shell: pwsh
149-
run: |
150-
$ErrorActionPreference = 'Continue'
151-
152-
# All three of msvc.cppm's discovery strategies converge on
153-
# <vsRoot>\VC\Tools\MSVC — vswhere returns an installationPath that
154-
# find_latest_msvc_tools then resolves through it, the env strategy
155-
# checks it explicitly, and the well-known-path scan tests for it.
156-
# So mask the VC directory rather than the Visual Studio root: the
157-
# root is held open on the runner and renaming it is denied, while
158-
# VC one level down renames fine. The runner is disposable, so this
159-
# is both safe and closer to "absent" than any env-only trick.
160-
$vswhere = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"
161-
if (Test-Path $vswhere) { Rename-Item $vswhere "vswhere.exe.masked" }
162-
163-
# -Path with a trailing wildcard segment lists the CONTENTS of the
164-
# matches, not the matches themselves, so `…\*\*\VC` would hand back
165-
# VC's children. Resolve-Path returns the directories themselves.
166-
# Errors are reported, not swallowed: a silent failure here is how
167-
# the first attempt "masked" nothing and still looked fine.
168-
Resolve-Path "C:\Program Files*\Microsoft Visual Studio\*\*\VC" `
169-
-ErrorAction SilentlyContinue | ForEach-Object {
170-
$p = $_.Path
171-
Write-Host "masking $p"
172-
try { Rename-Item -LiteralPath $p -NewName "VC.masked" -ErrorAction Stop }
173-
catch { Write-Host " rename failed: $($_.Exception.Message)" }
174-
}
175-
176-
foreach ($v in @('VSINSTALLDIR','VCINSTALLDIR','VCToolsInstallDir',
177-
'VS170COMNTOOLS','VS160COMNTOOLS','VS150COMNTOOLS')) {
178-
"$v=" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
179-
}
180-
181-
# Check the mask's own postcondition here, where the cause is
182-
# obvious, instead of letting it surface three steps later as a
183-
# confusing pass.
184-
$left = Get-ChildItem "C:\Program Files*\Microsoft Visual Studio\*\*\VC\Tools\MSVC" `
185-
-Directory -ErrorAction SilentlyContinue
186-
if ($left) {
187-
Write-Host "FAIL: VC tools still present after masking:"
188-
$left | ForEach-Object { Write-Host " $($_.FullName)" }
189-
exit 1
190-
}
191-
Write-Host "Visual Studio masked: no VC\Tools\MSVC remains."
151+
uses: ./.github/actions/mask-visual-studio
192152

193153
# After masking, so nothing this action does can be holding Visual
194154
# Studio open. It installs xlings (which mcpp resolves the winlibs

‎.github/workflows/measure-windows-tool-crt.yml‎

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -84,24 +84,12 @@ jobs:
8484
name: measure-candidate
8585
path: candidate
8686

87-
# The same mask as ci-windows.yml's bare-Windows row, applied before
88-
# anything touches Visual Studio.
87+
# The same mask as ci-windows.yml's bare-Windows row — one action, so
88+
# the two rows cannot drift apart — applied before anything touches
89+
# Visual Studio.
8990
- name: Mask Visual Studio
9091
if: matrix.row == 'bare'
91-
shell: pwsh
92-
run: |
93-
$ErrorActionPreference = 'Continue'
94-
$vswhere = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"
95-
if (Test-Path $vswhere) { Rename-Item $vswhere "vswhere.exe.masked" }
96-
Resolve-Path "C:\Program Files*\Microsoft Visual Studio\*\*\VC" -ErrorAction SilentlyContinue | ForEach-Object {
97-
try { Rename-Item -LiteralPath $_.Path -NewName "VC.masked" -ErrorAction Stop }
98-
catch { Write-Host " rename failed: $($_.Exception.Message)" }
99-
}
100-
foreach ($v in @('VSINSTALLDIR','VCINSTALLDIR','VCToolsInstallDir','VS170COMNTOOLS','VS160COMNTOOLS','VS150COMNTOOLS')) {
101-
"$v=" | Out-File -Append -FilePath $env:GITHUB_ENV -Encoding utf8
102-
}
103-
$left = Get-ChildItem "C:\Program Files*\Microsoft Visual Studio\*\*\VC\Tools\MSVC" -Directory -ErrorAction SilentlyContinue
104-
if ($left) { Write-Host "FAIL: VC tools still present after masking"; exit 1 }
92+
uses: ./.github/actions/mask-visual-studio
10593

10694
- uses: ./.github/actions/bootstrap-mcpp
10795

‎tests/e2e/845_play_game_restores_the_terminal.sh‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
# terminal's mode, and the prepare action does not outlive mcpp;
1515
# G4 with standard input not a terminal the game is off, a line says why,
1616
# and the build succeeds.
17+
#
18+
# The arrows and the Ctrl-C are sent when the status row that carries the
19+
# score first appears: the frame the build itself drew is what the test waits
20+
# for, not a wall-clock delay. A delay sent them into the Programs phase
21+
# whenever the build program took that long, and G1 then failed on the phase
22+
# word while the score was on the screen beside it.
1723
set -e
1824

1925
TMP=$(mktemp -d)
@@ -69,7 +75,18 @@ if pid == 0:
6975
script = ('trap : INT; stty -g > before.txt; "$0" build --play-game=snake; '
7076
'code=$?; stty -g > after.txt; exit $code')
7177
os.execvpe("sh", ["sh", "-c", script, mcpp], env)
72-
raw, t0, during, sent, status = b"", time.time(), None, False, 0
78+
79+
def strip_ansi(blob):
80+
return re.sub(r"\x1b\[[0-9;?]*[A-Za-z]", "", blob.decode("utf-8", "replace"))
81+
82+
# The game rides every frame, and the row the assertion below names is the row
83+
# to steer on: nothing is gained by guessing when it is up. The build reaches
84+
# Phase::Building only after its build program has compiled and run (and after
85+
# the toolchain was resolved), which on a loaded runner is a second or more —
86+
# so the arrows and the Ctrl-C wait for the row rather than for the clock.
87+
STEER_ROW = re.compile(r"Building .* · snake \d+")
88+
raw, during, sent, status = b"", None, False, 0
89+
give_up = time.time() + 60 # a build that never draws the row is a failure, not a hang
7390
while True:
7491
r, _, _ = select.select([master], [], [], 0.1)
7592
if r:
@@ -78,7 +95,7 @@ while True:
7895
except OSError:
7996
chunk = b""
8097
raw += chunk
81-
if time.time() - t0 > 1.5 and not sent:
98+
if not sent and (STEER_ROW.search(strip_ansi(raw)) or time.time() > give_up):
8299
during = termios.tcgetattr(slave)
83100
for key in (b"\x1b[B", b"\x1b[C", b"\x1b[A"):
84101
os.write(master, key)
@@ -109,7 +126,7 @@ def mode_of(path):
109126
for f in g.split(":"))
110127
return g
111128
before, after = mode_of("before.txt"), mode_of("after.txt")
112-
plain = re.sub(r"\x1b\[[0-9;?]*[A-Za-z]", "", raw.decode("utf-8", "replace")).replace("\r\n", "\n")
129+
plain = strip_ansi(raw).replace("\r\n", "\n")
113130
print(plain)
114131
assert during is not None, "the build ended before the game could be observed"
115132
# G1

0 commit comments

Comments
 (0)