mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-07-16 13:17:19 +02:00
723c7827ba
The test:windows CI job intermittently failed to clean up .testbin with
"Access to the path '...cmd_cli.test.exe' is denied". This was previously
attributed to Windows Defender scanning the large unsigned test binaries,
and mitigated with Defender exclusions and cleanup retries. That was
treating a symptom.
Root cause: performUpgrade() self-upgrades by running
exec.Command(os.Executable(), "upgrade", "prod", "-vv") as a detached,
windowless child. In the real ctrld binary this re-execs ctrld and is
correct. Under `go test`, os.Executable() is the test binary itself, and
`go test` stops flag parsing at the first positional arg ("upgrade") and
ignores the rest -- so the child silently re-runs the entire test suite.
That child hits the upgrade tests again and spawns more detached children,
recursively: a fork bomb of hidden processes that pins the runner's
CPU/memory and keeps the test binary's image file locked. Windows refuses
to delete the image of a running process, hence the "Access is denied"
during after_script. Whether any children are still alive when cleanup
runs is a timing race, which is why the failure was flaky.
Two tests reached this path: Test_performUpgrade (directly) and
Test_selfUpgradeCheck (via selfUpgradeCheck -> performUpgrade on the
"upgrade allowed" case).
Fix:
- prog.go: extract the command construction into a package-level
newUpgradeCmd var. Production behavior is unchanged.
- main_test.go: stub newUpgradeCmd once in TestMain so the whole test
binary self-execs with `-test.run=^$` (matches no tests, exits
immediately) instead of re-running the suite. This covers every test
that reaches performUpgrade, present and future, while still exercising
the cmd.Start() success path.
34 lines
1.0 KiB
Go
34 lines
1.0 KiB
Go
package cli
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
var logOutput strings.Builder
|
|
|
|
func TestMain(m *testing.M) {
|
|
l := zerolog.New(&logOutput)
|
|
mainLog.Store(&l)
|
|
|
|
// Stub the self-upgrade command builder for the whole test binary. The real
|
|
// builder execs os.Executable() — which under `go test` IS this test binary
|
|
// — with positional args ("upgrade", ...). `go test` stops flag parsing at
|
|
// the first positional arg and ignores the rest, so the child just re-runs
|
|
// the entire suite, hits the upgrade tests again, and spawns more children:
|
|
// a fork bomb of detached processes that stalls the host and (on Windows)
|
|
// holds the test binary's image locked, breaking CI artifact cleanup.
|
|
// Point it at the test binary with a no-match -test.run so any test that
|
|
// reaches performUpgrade still exercises the cmd.Start() success path while
|
|
// the child exits immediately without recursing.
|
|
newUpgradeCmd = func(exe string) *exec.Cmd {
|
|
return exec.Command(exe, "-test.run=^$")
|
|
}
|
|
|
|
os.Exit(m.Run())
|
|
}
|