From 889e37a22420e62bdf5cb34b79b71ab2929547ce Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Thu, 13 Aug 2026 04:34:40 +0000 Subject: [PATCH] fix(windows): Invoke-Checked rejected the empty argument list every no-arg test uses (#512) `windows-msvc-cpu` has failed on every pull request, ~21 minutes in, with zero compile diagnostics: Cannot bind argument to parameter 'Arguments' because it is an empty array. `Invoke-Checked` declared `Arguments` as `[Parameter(Mandatory)][string[]]`. PowerShell's `Mandatory` validation treats an empty collection as "not supplied", so every call that runs a test executable taking no arguments died at parameter binding before the process was ever started. There are six such production call sites, not four -- the two forced-CPU-tier invocations (`VT_CPU_MATMUL_TIER=portable` / `avx2`) are on the same path and would have failed next. The fix is `[AllowEmptyCollection()]` alongside `Mandatory`, not a `= @()` default. The existing intent is that a caller must state its argument list; `AllowEmptyCollection` keeps the omission an error while permitting an explicitly empty list, whereas a default would silently accept a call that forgot the parameter entirely. The contract step ran green all along because the suite never executed an empty-argument invocation, which is why a 21-minute build step caught what a seconds-long contract step should have. `Invoke-CheckedContractTests` closes that: it injects a recording runner -- mirroring the `DumpbinRunner` and unsupported-tier-probe seams already in this file -- and asserts the empty and non-empty argument lists are forwarded verbatim and that a nonzero status still throws on both. `Invoke-Checked` gains the matching optional `-Runner` seam. RED-first, run under pwsh 7.6.4: the new contract test fails with the exact CI message above before `[AllowEmptyCollection()]` is applied, and passes after. Five mutations of the claimed guarantees are each caught by their intended assertion (drop `AllowEmptyCollection`; swallow the argument list; never throw on nonzero; forward the wrong program; truncate the arguments), and the real `& $Program @Arguments` path -- not just the fake runner -- was exercised against `/bin/true`, `/bin/echo a b`, and `/bin/false`. The pre-existing unsupported-tier contract assertion is untouched and proven still non-vacuous: making the probe send an empty, a wrong, or a two-element argument list each still trips "did not receive one exact filter argument". That empty-args mutation also shows the fake-runner scriptblock parameters need no `AllowEmptyCollection` of their own -- being non-mandatory, they bind `@()` and fail on the assertion rather than on binding. This is a distinct defect from #514, the POSIX `setenv`/`unsetenv` C3861 error that fails `windows-msvc-vulkan`; that one is fixed on its own branch and `windows-msvc-vulkan` stays red here until it lands. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [Claude Code] --- scripts/build-windows-release.ps1 | 65 +++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/scripts/build-windows-release.ps1 b/scripts/build-windows-release.ps1 index b8725cce1..7c339dbcf 100644 --- a/scripts/build-windows-release.ps1 +++ b/scripts/build-windows-release.ps1 @@ -19,10 +19,66 @@ if ($ArtifactId -ne "windows-x86_64-msvc-$Backend") { } function Invoke-Checked { param([Parameter(Mandatory)][string]$Program, - [Parameter(Mandatory)][string[]]$Arguments) - & $Program @Arguments - if ($LASTEXITCODE -ne 0) { - throw "$Program exited with status $LASTEXITCODE" + [Parameter(Mandatory)][AllowEmptyCollection()][string[]]$Arguments, + [scriptblock]$Runner) + if ($null -eq $Runner) { + & $Program @Arguments + $exitCode = $LASTEXITCODE + } else { + $exitCode = [int](& $Runner $Program $Arguments) + } + if ($exitCode -ne 0) { + throw "$Program exited with status $exitCode" + } +} + +# Most of this script's checked invocations run a test executable that takes no +# arguments, so `Invoke-Checked` must bind an explicitly empty argument list and +# still forward it verbatim (#512). +function Invoke-CheckedContractTests { + $calls = [System.Collections.Generic.List[object]]::new() + $recorder = { + param([string]$Program, [string[]]$Arguments) + $calls.Add([pscustomobject]@{ + Program = $Program + Arguments = @($Arguments) + }) | Out-Null + return 0 + }.GetNewClosure() + + Invoke-Checked "fake-empty.exe" @() -Runner $recorder + Invoke-Checked "fake-args.exe" @("--help", "--verbose") -Runner $recorder + + if ($calls.Count -ne 2) { + throw "checked-invocation fake runner was not invoked exactly twice" + } + if ($calls[0].Program -ne "fake-empty.exe" -or $calls[1].Program -ne "fake-args.exe") { + throw "checked invocation did not forward its exact program" + } + if ($calls[0].Arguments.Count -ne 0) { + throw "checked invocation did not forward an explicitly empty argument list" + } + if ($calls[1].Arguments.Count -ne 2 -or + $calls[1].Arguments[0] -ne "--help" -or + $calls[1].Arguments[1] -ne "--verbose") { + throw "checked invocation did not forward its exact argument list" + } + + $failing = { param([string]$Program, [string[]]$Arguments) return 3 } + foreach ($rejectedName in @("empty", "non-empty")) { + $rejected = $false + try { + if ($rejectedName -eq "empty") { + Invoke-Checked "fake-fail.exe" @() -Runner $failing + } else { + Invoke-Checked "fake-fail.exe" @("--help") -Runner $failing + } + } catch { + $rejected = $true + } + if (-not $rejected) { + throw "nonzero $rejectedName-argument exit status was accepted" + } } } @@ -157,6 +213,7 @@ function Invoke-UnsupportedTierContractTests { } if ($ContractTest) { + Invoke-CheckedContractTests Invoke-CrtContractTests Invoke-UnsupportedTierContractTests Write-Host "Windows PowerShell/CRT contract tests OK"