mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/233-a-failed-probe-fails-only-what-needs-the-tool-2 delivered: every member is delivered
210 lines
8.7 KiB
Go
210 lines
8.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
"github.com/novox/mesh-host/internal/link"
|
|
"github.com/novox/mesh-host/internal/system"
|
|
)
|
|
|
|
// Defends novox/hq issue 233: a package manager that does not answer is said even when nothing declared
|
|
// needs it — the apply is partial, never "applied" — and the report names it under one key, so the mesh
|
|
// counts it as one failure that goes on.
|
|
func TestAPackageManagerThatDoesNotAnswerIsSaidWithNoPackageDeclared(t *testing.T) {
|
|
unansweringPacman(t)
|
|
dir := t.TempDir()
|
|
opts := options{state: filepath.Join(dir, "state.json"), out: &strings.Builder{}}
|
|
conf := filepath.Join(dir, "pacman.conf")
|
|
raw := []byte(`{"declaration":1,"resources":[{"id":"pacman.conf","type":"file","path":"` + conf +
|
|
`","content":"[options]\n"}]}`)
|
|
d, err := declaration.ParseFileTrusted(raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = runApply(context.Background(), opts, d, raw, fromFile)
|
|
if err == nil || !strings.Contains(err.Error(), "applied only in part") || !strings.Contains(err.Error(), "could not be read") {
|
|
t.Fatalf("an apply whose package manager did not answer was not said to be partial: %v", err)
|
|
}
|
|
if _, statErr := os.Stat(conf); statErr != nil {
|
|
t.Fatalf("the file was not written: %v", statErr)
|
|
}
|
|
|
|
report := applyAndKeep(context.Background(), opts, raw, nil, nil, nil)
|
|
if report.Refused != "" {
|
|
t.Fatalf("refused whole: %s", report.Refused)
|
|
}
|
|
why, ok := report.Failed["the package manager (pacman)"]
|
|
if !ok || !strings.Contains(why, "could not be read") {
|
|
t.Errorf("the report does not name the package manager with the probe's words: %v", report.Failed)
|
|
}
|
|
}
|
|
|
|
// A system whose probe asks the system itself, not a tool a resource needs, still refuses whole: nothing
|
|
// the mesh declares can remove it, and nothing else stands between that host and the wrong machine.
|
|
func TestASystemWithNoPackageManagerStillRefusesWhole(t *testing.T) {
|
|
t.Setenv("PATH", t.TempDir())
|
|
sys, err := system.For("android")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
probe, err := probePackageManager(context.Background(), sys)
|
|
if err == nil || probe != nil {
|
|
t.Fatalf("the android host's failed probe did not refuse: %v, %+v", err, probe)
|
|
}
|
|
}
|
|
|
|
// pacmanAnsweringOnce is a pacman that does not answer until conf exists, as the workstation's did until its
|
|
// configuration file was written back, and installs into a directory of its own once it does.
|
|
func pacmanAnsweringOnce(t *testing.T, conf string) {
|
|
t.Helper()
|
|
bin, installed := t.TempDir(), t.TempDir()
|
|
script := "#!/bin/sh\n" +
|
|
"[ -f '" + conf + "' ] || { echo \"error: config file " + conf + " could not be read: No such file or directory\" >&2; exit 1; }\n" +
|
|
"for a; do last=$a; done\n" +
|
|
"case \"$1\" in\n" +
|
|
"-Q) [ \"$2\" = pacman ] && exit 0; [ -f '" + installed + "'/\"$2\" ] && exit 0; exit 1;;\n" +
|
|
"-S) touch '" + installed + "'/\"$last\"; exit 0;;\n" +
|
|
"esac\nexit 1\n"
|
|
if err := os.WriteFile(filepath.Join(bin, "pacman"), []byte(script), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
was := builtFor
|
|
builtFor = "arch"
|
|
t.Cleanup(func() { builtFor = was })
|
|
}
|
|
|
|
// Defends the review of novox/hq issue 233: **the apply that writes the package manager's configuration back
|
|
// installs its packages too**. The probe is asked again once the partial apply wrote something, and when it
|
|
// answers the declaration is applied again over what the first pass left — the packages in this apply, not five
|
|
// minutes later, and the report clean.
|
|
func TestThePackagesGoInWhenTheApplyThatRestoresThePackageManagerWroteIt(t *testing.T) {
|
|
dir := t.TempDir()
|
|
conf := filepath.Join(dir, "pacman.conf")
|
|
pacmanAnsweringOnce(t, conf)
|
|
opts := options{state: filepath.Join(dir, "state.json")}
|
|
// The package first: within one pass it is asked about before the file is written, which is the order that
|
|
// leaves it to a second pass.
|
|
raw := []byte(`{"declaration":1,"resources":[` +
|
|
`{"id":"pacman.base","type":"package","package":"base"},` +
|
|
`{"id":"pacman.conf","type":"file","path":"` + conf + `","content":"[options]\n"}]}`)
|
|
|
|
report := applyAndKeep(context.Background(), opts, raw, nil, nil, nil)
|
|
|
|
if report.Refused != "" || len(report.Failed) != 0 {
|
|
t.Fatalf("the apply that made the package manager answer did not install its packages: refused %q, failed %v",
|
|
report.Refused, report.Failed)
|
|
}
|
|
if !slices.Contains(report.Applied, "pacman.base") || !slices.Contains(report.Applied, "pacman.conf") {
|
|
t.Errorf("the report does not account for both: %v", report.Applied)
|
|
}
|
|
}
|
|
|
|
// Defends decision (1) of the review of novox/hq issue 233: **a package manager that is not there at all still
|
|
// refuses whole**. That is the host installed on the wrong machine, which the probe exists to catch.
|
|
func TestAPackageManagerThatIsNotThereRefusesWhole(t *testing.T) {
|
|
t.Setenv("PATH", t.TempDir())
|
|
was := builtFor
|
|
builtFor = "arch"
|
|
t.Cleanup(func() { builtFor = was })
|
|
dir := t.TempDir()
|
|
conf := filepath.Join(dir, "pacman.conf")
|
|
raw := []byte(`{"declaration":1,"resources":[{"id":"pacman.conf","type":"file","path":"` + conf +
|
|
`","content":"[options]\n"}]}`)
|
|
|
|
report := applyAndKeep(context.Background(), options{state: filepath.Join(dir, "state.json")}, raw, nil, nil, nil)
|
|
|
|
if !strings.Contains(report.Refused, "pacman does not answer") {
|
|
t.Fatalf("a machine with no pacman at all was not refused whole: %+v", report)
|
|
}
|
|
if _, err := os.Stat(conf); err == nil {
|
|
t.Error("the refused declaration touched the machine")
|
|
}
|
|
}
|
|
|
|
// Defends the review of novox/hq issue 233: **a re-apply whose result changed is said unasked**. A machine
|
|
// reconciles every five minutes and speaks only when its account is news; how the apply went was not part of
|
|
// it, so a package manager that stopped answering between deliveries was never said, and one that answered
|
|
// again never cleared. Now a failure is said once, the same failure again is not, and a clean re-apply after it
|
|
// is — which is what clears the mesh's condition.
|
|
func TestAReapplyWhoseResultChangedIsSaidUnasked(t *testing.T) {
|
|
dir := t.TempDir()
|
|
conf := filepath.Join(dir, "elsewhere", "pacman.conf") // not declared: the apply cannot make it answer
|
|
pacmanAnsweringOnce(t, conf)
|
|
opts := options{state: filepath.Join(dir, "state.json")}
|
|
raw := []byte(`{"declaration":1,"resources":[` +
|
|
`{"id":"motd","type":"file","path":"` + filepath.Join(dir, "motd") + `","content":"hello\n"},` +
|
|
`{"id":"pacman.base","type":"package","package":"base"}]}`)
|
|
w := &adoptionWatch{}
|
|
|
|
// Clean once, as it was before: base installed while pacman answered.
|
|
if err := os.MkdirAll(filepath.Dir(conf), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(conf, []byte("[options]\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
clean := applyAndKeep(context.Background(), opts, raw, nil, nil, nil)
|
|
if faulty(clean) {
|
|
t.Fatalf("the first apply was not clean: %+v", clean)
|
|
}
|
|
w.said(clean)
|
|
if w.news(applyAndKeep(context.Background(), opts, raw, nil, nil, nil)) {
|
|
t.Error("a clean re-apply after a clean one was said, so every machine speaks every five minutes")
|
|
}
|
|
|
|
// The configuration goes: the re-apply fails, and that is news once.
|
|
if err := os.Remove(conf); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
failed := applyAndKeep(context.Background(), opts, raw, nil, nil, nil)
|
|
if !faulty(failed) {
|
|
t.Fatalf("the re-apply did not fail: %+v", failed)
|
|
}
|
|
if !w.news(failed) {
|
|
t.Fatal("a re-apply that failed after a clean one was not said, so the mesh never hears the machine fails")
|
|
}
|
|
w.said(failed)
|
|
if w.news(applyAndKeep(context.Background(), opts, raw, nil, nil, nil)) {
|
|
t.Error("the same failure was said again on the next re-apply")
|
|
}
|
|
|
|
// It comes back: the clean re-apply is news, and is what clears the mesh's condition.
|
|
if err := os.WriteFile(conf, []byte("[options]\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
again := applyAndKeep(context.Background(), opts, raw, nil, nil, nil)
|
|
if faulty(again) {
|
|
t.Fatalf("the re-apply did not come back clean: %+v", again)
|
|
}
|
|
if !w.news(again) {
|
|
t.Fatal("a clean re-apply after a failed one was not said, so the mesh's condition never clears")
|
|
}
|
|
}
|
|
|
|
// A refusal is news too, once, and a reconcile with nothing to say after a clean one stays silent.
|
|
func TestARefusalIsSaidOnceAndNothingIsNotSaid(t *testing.T) {
|
|
w := &adoptionWatch{}
|
|
w.said(link.Report{})
|
|
if w.news(link.Report{}) {
|
|
t.Error("nothing was said again")
|
|
}
|
|
refused := link.Report{Refused: "pacman does not answer here"}
|
|
if !w.news(refused) {
|
|
t.Fatal("a refusal was not said")
|
|
}
|
|
w.said(refused)
|
|
if w.news(link.Report{Refused: "pacman does not answer here, at another time"}) {
|
|
t.Error("the same refusal in other words was said again")
|
|
}
|
|
if !w.news(link.Report{}) {
|
|
t.Error("a clean apply after a refusal was not said")
|
|
}
|
|
}
|