mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
275 lines
9.4 KiB
Go
275 lines
9.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// startRunFuncSource extracts the source text of initStartCmd's "Run:"
|
|
// closure body from commands.go. Driving the closure itself end-to-end for
|
|
// every early-return branch is not practical in a unit test: within a few
|
|
// lines of any check failing, it reaches into the real OS service manager.
|
|
// Some invariants about its shape are cheaper and more reliable to pin by
|
|
// reading the source than by executing it.
|
|
func startRunFuncSource(t *testing.T) string {
|
|
t.Helper()
|
|
file := packageSourcePath(t, "commands.go")
|
|
fset := token.NewFileSet()
|
|
node, err := parser.ParseFile(fset, file, nil, 0)
|
|
if err != nil {
|
|
t.Fatalf("could not parse %s: %v", file, err)
|
|
}
|
|
var runLit *ast.FuncLit
|
|
ast.Inspect(node, func(n ast.Node) bool {
|
|
fn, ok := n.(*ast.FuncDecl)
|
|
if !ok || fn.Name.Name != "initStartCmd" {
|
|
return true
|
|
}
|
|
ast.Inspect(fn.Body, func(n ast.Node) bool {
|
|
kv, ok := n.(*ast.KeyValueExpr)
|
|
if !ok {
|
|
return true
|
|
}
|
|
// initStartCmd defines a second, unrelated "Run:" field further down
|
|
// for a start command alias; take only the first match, which is the
|
|
// real start command's closure this test cares about.
|
|
if runLit == nil {
|
|
if ident, ok := kv.Key.(*ast.Ident); ok && ident.Name == "Run" {
|
|
if lit, ok := kv.Value.(*ast.FuncLit); ok {
|
|
runLit = lit
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
return false
|
|
})
|
|
if runLit == nil {
|
|
t.Fatalf("Run: func literal not found in initStartCmd in %s", file)
|
|
}
|
|
src, err := os.ReadFile(file)
|
|
if err != nil {
|
|
t.Fatalf("could not read %s: %v", file, err)
|
|
}
|
|
start := fset.Position(runLit.Body.Lbrace).Offset
|
|
end := fset.Position(runLit.Body.Rbrace).Offset
|
|
return string(src[start:end])
|
|
}
|
|
|
|
// TestStartCommandClearsProvisionResultBeforeAnyCheck pins the ordering fix:
|
|
// clearProvisionResult() must run before every check in the start command's
|
|
// Run closure that can fail or return early, not just before doTasksE.
|
|
// Without this, a check between the top of the closure and the old call
|
|
// sites could return early (whether by writing its own classified failure
|
|
// or, like the "service already running" and service-manager-init-error
|
|
// paths, by writing nothing at all) while a previous attempt's result file
|
|
// was still sitting there to mislead diag/postinstall on retry.
|
|
func TestStartCommandClearsProvisionResultBeforeAnyCheck(t *testing.T) {
|
|
body := startRunFuncSource(t)
|
|
|
|
clearIdx := strings.Index(body, "clearProvisionResult()")
|
|
if clearIdx == -1 {
|
|
t.Fatal("start command no longer calls clearProvisionResult()")
|
|
}
|
|
|
|
// Every check or step that can return out of the closure before reaching
|
|
// doTasksE. Each must appear after the entry clear.
|
|
earlyChecks := []string{
|
|
"checkStrFlagEmpty(",
|
|
"validateCdAndNextDNSFlags(",
|
|
"validateInterceptModeFlag(",
|
|
"doTasksE(",
|
|
}
|
|
for _, check := range earlyChecks {
|
|
idx := strings.Index(body, check)
|
|
if idx == -1 {
|
|
t.Fatalf("expected the start command to still call %s", check)
|
|
}
|
|
if idx < clearIdx {
|
|
t.Errorf("%s appears before clearProvisionResult(): a failure there could leave a stale result file behind", check)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestStartCommandClassifiesServiceInitFailure pins the fix for a bare
|
|
// return: when newService fails in the start command, the closure must fail
|
|
// through failProvisionUnclassified, so a result file and the identifier
|
|
// line exist, instead of a plain return that exits 0.
|
|
func TestStartCommandClassifiesServiceInitFailure(t *testing.T) {
|
|
body := startRunFuncSource(t)
|
|
initIdx := strings.Index(body, "newService(p, sc)")
|
|
if initIdx == -1 {
|
|
t.Fatal("start command no longer calls newService(p, sc)")
|
|
}
|
|
branchEnd := strings.Index(body[initIdx:], "p.preRun()")
|
|
if branchEnd == -1 {
|
|
t.Fatal("could not find the end of the service init branch")
|
|
}
|
|
if !strings.Contains(body[initIdx:initIdx+branchEnd], "failProvisionUnclassified(") {
|
|
t.Error("service init failure does not fail through failProvisionUnclassified")
|
|
}
|
|
}
|
|
|
|
// TestStartCommandReplacesStaleResultOnEarlyClassifiedFailure is a
|
|
// behavioral companion to the structural test above: it drives the real
|
|
// start command through its earliest classified failure (an invalid
|
|
// --intercept-mode) and checks the file left behind names the new attempt,
|
|
// not a stale one seeded beforehand.
|
|
func TestStartCommandReplacesStaleResultOnEarlyClassifiedFailure(t *testing.T) {
|
|
exitCode, _ := stubProvisionGlobals(t)
|
|
oldIntercept, oldCdUID, oldCdOrg, oldNextdns := interceptMode, cdUID, cdOrg, nextdns
|
|
t.Cleanup(func() {
|
|
interceptMode, cdUID, cdOrg, nextdns = oldIntercept, oldCdUID, oldCdOrg, oldNextdns
|
|
})
|
|
|
|
// initStartCmd binds these globals to flag defaults as it registers them
|
|
// (StringVarP writes the default straight into the pointer), so the
|
|
// command must exist before the test overrides the values it drives the
|
|
// closure with.
|
|
cmd := initStartCmd()
|
|
cdUID, cdOrg, nextdns = "", "", ""
|
|
interceptMode = "bogus" // fails validateInterceptModeFlag before any OS work
|
|
|
|
if err := writeProvisionResult(newProvisionResult(provisionCodeServiceStartFailed, "a previous failed attempt", nil)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cmd.Run(cmd, nil)
|
|
|
|
wantExit := provisionExitCodeForCode[provisionCodeInterceptModeInvalid]
|
|
if *exitCode != wantExit {
|
|
t.Fatalf("exit = %d, want %d (validateInterceptModeFlag should have run)", *exitCode, wantExit)
|
|
}
|
|
r, err := readProvisionResult()
|
|
if err != nil {
|
|
t.Fatalf("no provision result written: %v", err)
|
|
}
|
|
if r.Code == string(provisionCodeServiceStartFailed) {
|
|
t.Fatal("stale result from a previous attempt survived the new attempt")
|
|
}
|
|
if r.Code != string(provisionCodeInterceptModeInvalid) {
|
|
t.Errorf("code = %q, want %q", r.Code, provisionCodeInterceptModeInvalid)
|
|
}
|
|
}
|
|
|
|
func TestServiceStageFailureCode(t *testing.T) {
|
|
tests := []struct {
|
|
taskName string
|
|
wantCode provisionFailureCode
|
|
wantOK bool
|
|
}{
|
|
{"Install", provisionCodeServiceInstall, true},
|
|
{"Start", provisionCodeServiceStartFailed, true},
|
|
{"Checking config", "", false},
|
|
{"", "", false},
|
|
}
|
|
for _, tc := range tests {
|
|
code, ok := serviceStageFailureCode(tc.taskName)
|
|
if code != tc.wantCode || ok != tc.wantOK {
|
|
t.Errorf("serviceStageFailureCode(%q) = (%q, %v), want (%q, %v)", tc.taskName, code, ok, tc.wantCode, tc.wantOK)
|
|
}
|
|
}
|
|
}
|
|
|
|
func stubProvisionExit(t *testing.T) *int {
|
|
t.Helper()
|
|
exitCode := -1
|
|
old := provisionExit
|
|
provisionExit = func(code int) { exitCode = code }
|
|
t.Cleanup(func() { provisionExit = old })
|
|
return &exitCode
|
|
}
|
|
|
|
func TestReportStartFailureUsesFreshDaemonResult(t *testing.T) {
|
|
overrideProvisionResultPath(t)
|
|
exitCode := stubProvisionExit(t)
|
|
|
|
startedAt := time.Now()
|
|
daemonResult := newProvisionResult(provisionCodeAPIUnreachable, "daemon could not reach the API", nil)
|
|
if err := writeProvisionResult(daemonResult); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
reportStartFailure(startedAt, "generic self-check failure")
|
|
|
|
if *exitCode != provisionExitCodeForCode[provisionCodeAPIUnreachable] {
|
|
t.Errorf("exit code = %d, want the daemon's own exit code %d", *exitCode, provisionExitCodeForCode[provisionCodeAPIUnreachable])
|
|
}
|
|
out, err := readProvisionResult()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out.Code != string(provisionCodeAPIUnreachable) {
|
|
t.Errorf("persisted code = %q, want the daemon's own code untouched", out.Code)
|
|
}
|
|
}
|
|
|
|
func TestReportStartFailureFallsBackOnStaleDaemonResult(t *testing.T) {
|
|
overrideProvisionResultPath(t)
|
|
exitCode := stubProvisionExit(t)
|
|
|
|
stale := newProvisionResult(provisionCodeAPIUnreachable, "an old failure", nil)
|
|
stale.Timestamp = time.Now().Add(-1 * time.Hour).UTC().Format(time.RFC3339)
|
|
if err := writeProvisionResult(stale); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
startedAt := time.Now()
|
|
reportStartFailure(startedAt, "test query failed: timeout")
|
|
|
|
if *exitCode != provisionExitCodeForCode[provisionCodeServiceSelfCheck] {
|
|
t.Errorf("exit code = %d, want SERVICE_SELFCHECK_FAILED exit %d", *exitCode, provisionExitCodeForCode[provisionCodeServiceSelfCheck])
|
|
}
|
|
out, err := readProvisionResult()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out.Code != string(provisionCodeServiceSelfCheck) {
|
|
t.Errorf("persisted code = %q, want %q", out.Code, provisionCodeServiceSelfCheck)
|
|
}
|
|
if out.Message != "test query failed: timeout" {
|
|
t.Errorf("persisted message = %q, want the fallback message", out.Message)
|
|
}
|
|
}
|
|
|
|
func TestReportStartFailureRejectsUntrustedFile(t *testing.T) {
|
|
overrideProvisionResultPath(t)
|
|
exitCode := stubProvisionExit(t)
|
|
|
|
planted := newProvisionResult(provisionCodeAPIUnreachable, "planted", nil)
|
|
planted.Code = "FAKE_CODE"
|
|
planted.ExitCode = 99
|
|
if err := writeProvisionResult(planted); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
reportStartFailure(time.Now().Add(-time.Minute), "self-check failed")
|
|
|
|
if *exitCode != provisionExitCodeForCode[provisionCodeServiceSelfCheck] {
|
|
t.Errorf("exit = %d, want the fallback %d, never the planted 99", *exitCode, provisionExitCodeForCode[provisionCodeServiceSelfCheck])
|
|
}
|
|
}
|
|
|
|
func TestReportStartFailureFallsBackWhenResultFileMissing(t *testing.T) {
|
|
overrideProvisionResultPath(t)
|
|
exitCode := stubProvisionExit(t)
|
|
|
|
reportStartFailure(time.Now(), "firewall hint")
|
|
|
|
if *exitCode != provisionExitCodeForCode[provisionCodeServiceSelfCheck] {
|
|
t.Errorf("exit code = %d, want SERVICE_SELFCHECK_FAILED exit %d", *exitCode, provisionExitCodeForCode[provisionCodeServiceSelfCheck])
|
|
}
|
|
out, err := readProvisionResult()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out.Message != "firewall hint" {
|
|
t.Errorf("persisted message = %q, want the fallback message", out.Message)
|
|
}
|
|
}
|