mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-09-04 13:36:35 +02:00
154 lines
5.8 KiB
Go
154 lines
5.8 KiB
Go
package cli
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// startFunctionSource extracts the source text of ServiceCommand.Start's body
|
|
// from commands_service_start.go. Driving Start() itself end-to-end for every
|
|
// early-return branch is not practical in a unit test: within a few lines of
|
|
// any check failing, Start() 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 startFunctionSource(t *testing.T) string {
|
|
t.Helper()
|
|
file := packageSourcePath(t, "commands_service_start.go")
|
|
fset := token.NewFileSet()
|
|
node, err := parser.ParseFile(fset, file, nil, 0)
|
|
if err != nil {
|
|
t.Fatalf("could not parse %s: %v", file, err)
|
|
}
|
|
for _, decl := range node.Decls {
|
|
fn, ok := decl.(*ast.FuncDecl)
|
|
if !ok || fn.Name.Name != "Start" || fn.Recv == nil {
|
|
continue
|
|
}
|
|
src, err := os.ReadFile(file)
|
|
if err != nil {
|
|
t.Fatalf("could not read %s: %v", file, err)
|
|
}
|
|
start := fset.Position(fn.Body.Lbrace).Offset
|
|
end := fset.Position(fn.Body.Rbrace).Offset
|
|
return string(src[start:end])
|
|
}
|
|
t.Fatalf("ServiceCommand.Start not found in %s", file)
|
|
return ""
|
|
}
|
|
|
|
// TestServiceCommandStartClearsProvisionResultBeforeAnyCheck pins the
|
|
// ordering fix: clearProvisionResult() must run before every check in
|
|
// Start() that can fail or return early, not just before doTasksE. Without
|
|
// this, a check between the top of Start() 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 TestServiceCommandStartClearsProvisionResultBeforeAnyCheck(t *testing.T) {
|
|
body := startFunctionSource(t)
|
|
|
|
clearIdx := strings.Index(body, "clearProvisionResult()")
|
|
if clearIdx == -1 {
|
|
t.Fatal("Start() no longer calls clearProvisionResult()")
|
|
}
|
|
|
|
// Every check or step that can return out of Start() before reaching
|
|
// doTasksE. Each must appear after the entry clear.
|
|
earlyChecks := []string{
|
|
"checkStrFlagEmpty(",
|
|
"validateCdAndNextDNSFlags(",
|
|
"validateInterceptModeFlag(",
|
|
"validateFirewallModeFlag(",
|
|
"initializeServiceManagerWithServiceConfig(",
|
|
"doTasksE(",
|
|
}
|
|
for _, check := range earlyChecks {
|
|
idx := strings.Index(body, check)
|
|
if idx == -1 {
|
|
t.Fatalf("expected Start() 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestServiceCommandStartClassifiesServiceManagerInitFailure pins the fix for
|
|
// a bare error return: a service-manager init failure in Start() must fail
|
|
// through failProvisionUnclassified, so a result file and the identifier line
|
|
// exist, instead of returning the error for a plain exit 1.
|
|
func TestServiceCommandStartClassifiesServiceManagerInitFailure(t *testing.T) {
|
|
body := startFunctionSource(t)
|
|
initIdx := strings.Index(body, "initializeServiceManagerWithServiceConfig(")
|
|
if initIdx == -1 {
|
|
t.Fatal("Start() no longer calls initializeServiceManagerWithServiceConfig")
|
|
}
|
|
branchEnd := strings.Index(body[initIdx:], "p.cfg = &cfg")
|
|
if branchEnd == -1 {
|
|
t.Fatal("could not find the end of the service-manager init branch")
|
|
}
|
|
branch := body[initIdx : initIdx+branchEnd]
|
|
if !strings.Contains(branch, "failProvisionUnclassified(") {
|
|
t.Error("service-manager init failure does not fail through failProvisionUnclassified")
|
|
}
|
|
if strings.Contains(branch, "return err") {
|
|
t.Error("service-manager init failure still returns the bare error, which exits 1 with no result file")
|
|
}
|
|
}
|
|
|
|
// startTestCommand builds the minimal cobra.Command ServiceCommand.Start needs
|
|
// before it can reach its early --intercept-mode check: the --cd/--cd-org
|
|
// flags must exist (checkStrFlagEmpty looks them up unconditionally) but stay
|
|
// unchanged, so neither Fatals.
|
|
func startTestCommand() *cobra.Command {
|
|
cmd := &cobra.Command{}
|
|
cmd.Flags().String(cdUidFlagName, "", "")
|
|
cmd.Flags().String(cdOrgFlagName, "", "")
|
|
return cmd
|
|
}
|
|
|
|
// TestServiceCommandStartReplacesStaleResultOnEarlyClassifiedFailure is a
|
|
// behavioral companion to the structural test above: it drives the real
|
|
// Start() 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 TestServiceCommandStartReplacesStaleResultOnEarlyClassifiedFailure(t *testing.T) {
|
|
exitCode, _ := stubProvisionGlobals(t)
|
|
oldIntercept, oldNextdns, oldFirewallChanged := interceptMode, nextdns, firewallModeFlagChanged
|
|
t.Cleanup(func() {
|
|
interceptMode, nextdns, firewallModeFlagChanged = oldIntercept, oldNextdns, oldFirewallChanged
|
|
})
|
|
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)
|
|
}
|
|
|
|
sc := NewServiceCommand()
|
|
if err := sc.Start(startTestCommand(), nil); err != nil {
|
|
t.Fatalf("Start() error = %v", err)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|