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.
276 lines
7.3 KiB
Go
276 lines
7.3 KiB
Go
package cli
|
|
|
|
import (
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Masterminds/semver/v3"
|
|
"github.com/rs/zerolog"
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/Control-D-Inc/ctrld"
|
|
)
|
|
|
|
func Test_prog_dnsWatchdogEnabled(t *testing.T) {
|
|
p := &prog{cfg: &ctrld.Config{}}
|
|
|
|
// Default value is true.
|
|
assert.True(t, p.dnsWatchdogEnabled())
|
|
|
|
tests := []struct {
|
|
name string
|
|
enabled bool
|
|
}{
|
|
{"enabled", true},
|
|
{"disabled", false},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
p.cfg.Service.DnsWatchdogEnabled = &tc.enabled
|
|
assert.Equal(t, tc.enabled, p.dnsWatchdogEnabled())
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_prog_dnsWatchdogInterval(t *testing.T) {
|
|
p := &prog{cfg: &ctrld.Config{}}
|
|
|
|
// Default value is 20s.
|
|
assert.Equal(t, dnsWatchdogDefaultInterval, p.dnsWatchdogDuration())
|
|
|
|
tests := []struct {
|
|
name string
|
|
duration time.Duration
|
|
expected time.Duration
|
|
}{
|
|
{"valid", time.Minute, time.Minute},
|
|
{"zero", 0, dnsWatchdogDefaultInterval},
|
|
{"nagative", time.Duration(-1 * time.Minute), dnsWatchdogDefaultInterval},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
p.cfg.Service.DnsWatchdogInvterval = &tc.duration
|
|
assert.Equal(t, tc.expected, p.dnsWatchdogDuration())
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_shouldUpgrade(t *testing.T) {
|
|
// Helper function to create a version
|
|
makeVersion := func(v string) *semver.Version {
|
|
ver, err := semver.NewVersion(v)
|
|
if err != nil {
|
|
t.Fatalf("failed to create version %s: %v", v, err)
|
|
}
|
|
return ver
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
versionTarget string
|
|
currentVersion *semver.Version
|
|
shouldUpgrade bool
|
|
description string
|
|
}{
|
|
{
|
|
name: "empty version target",
|
|
versionTarget: "",
|
|
currentVersion: makeVersion("v1.0.0"),
|
|
shouldUpgrade: false,
|
|
description: "should skip upgrade when version target is empty",
|
|
},
|
|
{
|
|
name: "invalid version target",
|
|
versionTarget: "invalid-version",
|
|
currentVersion: makeVersion("v1.0.0"),
|
|
shouldUpgrade: false,
|
|
description: "should skip upgrade when version target is invalid",
|
|
},
|
|
{
|
|
name: "same version",
|
|
versionTarget: "v1.0.0",
|
|
currentVersion: makeVersion("v1.0.0"),
|
|
shouldUpgrade: false,
|
|
description: "should skip upgrade when target version equals current version",
|
|
},
|
|
{
|
|
name: "older version",
|
|
versionTarget: "v1.0.0",
|
|
currentVersion: makeVersion("v1.1.0"),
|
|
shouldUpgrade: false,
|
|
description: "should skip upgrade when target version is older than current version",
|
|
},
|
|
{
|
|
name: "patch upgrade allowed",
|
|
versionTarget: "v1.0.1",
|
|
currentVersion: makeVersion("v1.0.0"),
|
|
shouldUpgrade: true,
|
|
description: "should allow patch version upgrade within same major version",
|
|
},
|
|
{
|
|
name: "minor upgrade allowed",
|
|
versionTarget: "v1.1.0",
|
|
currentVersion: makeVersion("v1.0.0"),
|
|
shouldUpgrade: true,
|
|
description: "should allow minor version upgrade within same major version",
|
|
},
|
|
{
|
|
name: "major upgrade blocked",
|
|
versionTarget: "v2.0.0",
|
|
currentVersion: makeVersion("v1.0.0"),
|
|
shouldUpgrade: false,
|
|
description: "should block major version upgrade",
|
|
},
|
|
{
|
|
name: "major downgrade blocked",
|
|
versionTarget: "v1.0.0",
|
|
currentVersion: makeVersion("v2.0.0"),
|
|
shouldUpgrade: false,
|
|
description: "should block major version downgrade",
|
|
},
|
|
{
|
|
name: "version without v prefix",
|
|
versionTarget: "1.0.1",
|
|
currentVersion: makeVersion("v1.0.0"),
|
|
shouldUpgrade: true,
|
|
description: "should handle version target without v prefix",
|
|
},
|
|
{
|
|
name: "complex version upgrade allowed",
|
|
versionTarget: "v1.5.3",
|
|
currentVersion: makeVersion("v1.4.2"),
|
|
shouldUpgrade: true,
|
|
description: "should allow complex version upgrade within same major version",
|
|
},
|
|
{
|
|
name: "complex major upgrade blocked",
|
|
versionTarget: "v3.1.0",
|
|
currentVersion: makeVersion("v2.5.3"),
|
|
shouldUpgrade: false,
|
|
description: "should block complex major version upgrade",
|
|
},
|
|
{
|
|
name: "pre-release version upgrade allowed",
|
|
versionTarget: "v1.0.1-beta.1",
|
|
currentVersion: makeVersion("v1.0.0"),
|
|
shouldUpgrade: true,
|
|
description: "should allow pre-release version upgrade within same major version",
|
|
},
|
|
{
|
|
name: "pre-release major upgrade blocked",
|
|
versionTarget: "v2.0.0-alpha.1",
|
|
currentVersion: makeVersion("v1.0.0"),
|
|
shouldUpgrade: false,
|
|
description: "should block pre-release major version upgrade",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Create test logger
|
|
testLogger := zerolog.New(zerolog.NewTestWriter(t)).With().Logger()
|
|
|
|
// Call the function and capture the result
|
|
result := shouldUpgrade(tc.versionTarget, tc.currentVersion, &testLogger)
|
|
|
|
// Assert the expected result
|
|
assert.Equal(t, tc.shouldUpgrade, result, tc.description)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_selfUpgradeCheck(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("skipped due to Windows file locking issue on Github Action runners")
|
|
}
|
|
|
|
// Helper function to create a version
|
|
makeVersion := func(v string) *semver.Version {
|
|
ver, err := semver.NewVersion(v)
|
|
if err != nil {
|
|
t.Fatalf("failed to create version %s: %v", v, err)
|
|
}
|
|
return ver
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
versionTarget string
|
|
currentVersion *semver.Version
|
|
shouldUpgrade bool
|
|
description string
|
|
}{
|
|
{
|
|
name: "upgrade allowed",
|
|
versionTarget: "v1.0.1",
|
|
currentVersion: makeVersion("v1.0.0"),
|
|
shouldUpgrade: true,
|
|
description: "should allow upgrade and attempt to perform it",
|
|
},
|
|
{
|
|
name: "upgrade blocked",
|
|
versionTarget: "v2.0.0",
|
|
currentVersion: makeVersion("v1.0.0"),
|
|
shouldUpgrade: false,
|
|
description: "should block upgrade and not attempt to perform it",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Create test logger
|
|
testLogger := zerolog.New(zerolog.NewTestWriter(t)).With().Logger()
|
|
|
|
// Call the function and capture the result
|
|
result := selfUpgradeCheck(tc.versionTarget, tc.currentVersion, &testLogger)
|
|
|
|
// Assert the expected result
|
|
assert.Equal(t, tc.shouldUpgrade, result, tc.description)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_performUpgrade(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("skipped due to Windows file locking issue on Github Action runners")
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
versionTarget string
|
|
expectedResult bool
|
|
description string
|
|
}{
|
|
{
|
|
name: "valid version target",
|
|
versionTarget: "v1.0.1",
|
|
expectedResult: true,
|
|
description: "should attempt to perform upgrade with valid version target",
|
|
},
|
|
{
|
|
name: "empty version target",
|
|
versionTarget: "",
|
|
expectedResult: true,
|
|
description: "should attempt to perform upgrade even with empty version target",
|
|
},
|
|
}
|
|
|
|
// newUpgradeCmd is stubbed in TestMain so performUpgrade does not re-exec
|
|
// (and fork-bomb) the test binary; see the comment there.
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Call the function and capture the result
|
|
result := performUpgrade(tc.versionTarget)
|
|
assert.Equal(t, tc.expectedResult, result, tc.description)
|
|
})
|
|
}
|
|
}
|