package main import ( "bufio" "encoding/json" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "strings" "testing" "github.com/novox/mesh-controller/internal/link" ) // The roles a copy of this test binary plays in TestAVerbRunsWhileItsBuildIsMovedOrDeleted. const selfExecRole = "MESH_CONTROLLER_SELFEXEC_ROLE" // TestSelfExecServer is a controller standing in, when run as one: it waits until its build has been // moved, then runs a verb as the seat runs one, and prints what came of it as JSON. func TestSelfExecServer(t *testing.T) { if os.Getenv(selfExecRole) != "server" { t.Skip("run by TestAVerbRunsWhileItsBuildIsMovedOrDeleted") } line, _ := bufio.NewReader(os.Stdin).ReadString('\n') if strings.TrimSpace(line) != "go" { t.Fatalf("told %q", line) } if err := os.Setenv(selfExecRole, "verb"); err != nil { t.Fatal(err) } answer, err := runVerb(t.Context(), []string{"-test.run", "^TestSelfExecVerb$", "-test.v"}) said := map[string]any{"ok": answer.OK, "output": answer.Output} if err != nil { said["error"] = err.Error() } body, _ := json.Marshal(said) fmt.Println("ANSWER " + string(body)) } // TestSelfExecVerb is the verb, when run as one. func TestSelfExecVerb(t *testing.T) { if os.Getenv(selfExecRole) != "verb" { t.Skip("run by TestSelfExecServer") } fmt.Println("the verb ran") } // **A verb runs while the build it was started from is moved where its user may not go, or deleted** // (novox/hq issue 289). The node-engine's witness moves a running controller's build into a directory // only it may enter as it places the next, and deletes it once the next is proved; the controller // still serving in between ran its verbs from the path and answered "permission denied". // // A copy of this test binary is the controller: started from one place, moved into a directory closed // to everyone (or deleted), and only then asked to run a verb. func TestAVerbRunsWhileItsBuildIsMovedOrDeleted(t *testing.T) { if runtime.GOOS != "linux" { t.Skip("the image is named through /proc on Linux only") } self, err := os.Executable() if err != nil { t.Fatal(err) } for _, how := range []string{"moved into a closed directory", "deleted"} { t.Run(how, func(t *testing.T) { root := t.TempDir() placed := filepath.Join(root, "mesh-controller", "mesh-controller") if err := os.MkdirAll(filepath.Dir(placed), 0o755); err != nil { t.Fatal(err) } copyFile(t, self, placed) server := exec.Command(placed, "-test.run", "^TestSelfExecServer$", "-test.v") server.Env = append(os.Environ(), selfExecRole+"=server") stdin, err := server.StdinPipe() if err != nil { t.Fatal(err) } stdout, err := server.StdoutPipe() if err != nil { t.Fatal(err) } server.Stderr = os.Stderr if err := server.Start(); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = server.Process.Kill(); _ = server.Wait() }) // What the witness does to a running build, once the process runs. switch how { case "deleted": if err := os.RemoveAll(filepath.Dir(placed)); err != nil { t.Fatal(err) } default: kept := filepath.Join(root, ".witness", "mesh-controller", "previous") if err := os.MkdirAll(filepath.Dir(kept), 0o700); err != nil { t.Fatal(err) } if err := os.Rename(filepath.Dir(placed), kept); err != nil { t.Fatal(err) } if err := os.Chmod(filepath.Join(root, ".witness"), 0); err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.Chmod(filepath.Join(root, ".witness"), 0o700) }) } if _, err := io.WriteString(stdin, "go\n"); err != nil { t.Fatal(err) } out, _ := io.ReadAll(stdout) _ = server.Wait() var said struct { OK bool `json:"ok"` Output string `json:"output"` Error string `json:"error"` } found := false for _, line := range strings.Split(string(out), "\n") { if rest, ok := strings.CutPrefix(line, "ANSWER "); ok { found = json.Unmarshal([]byte(rest), &said) == nil } } if !found { t.Fatalf("the controller standing in answered nothing:\n%s", out) } if said.Error != "" || !said.OK || !strings.Contains(said.Output, "the verb ran") { t.Fatalf("the verb did not run from the controller's own image after its build was %s: %+v", how, said) } }) } } // A verb the controller cannot start from its own build is refused as a handover — marked so its // caller asks again — never a permission error; and so is one arriving once the controller is stopping. func TestAVerbThatCannotRunIsRefusedAsAHandover(t *testing.T) { closed := filepath.Join(t.TempDir(), "closed") if err := os.MkdirAll(closed, 0o700); err != nil { t.Fatal(err) } was := ownImage ownImage = func() string { return filepath.Join(closed, "gone", "mesh-controller") } t.Cleanup(func() { ownImage = was }) _, err := runVerb(t.Context(), []string{"status"}) if !errors.Is(err, link.ErrHandingOver) { t.Fatalf("a build that is not there was answered %v, not a handover", err) } ownImage = was handingOver.Store(true) t.Cleanup(func() { handingOver.Store(false) }) if _, err := runVerb(t.Context(), []string{"-test.run", "^$"}); !errors.Is(err, link.ErrHandingOver) { t.Fatalf("a controller stopping ran a verb: %v", err) } } func copyFile(t *testing.T, from, to string) { t.Helper() in, err := os.Open(from) if err != nil { t.Fatal(err) } defer in.Close() out, err := os.OpenFile(to, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o755) if err != nil { t.Fatal(err) } if _, err := io.Copy(out, in); err != nil { t.Fatal(err) } if err := out.Close(); err != nil { t.Fatal(err) } }