96 comments across the two repos named records that no longer exist. Each now points at the consolidated record that holds its reasoning -- ADR 0034 (a test defends a decision) is 0017, the eight host records are 0005, the four lab records are 0016. Worth noting for next time: these are references from outside HQ, so renumbering there is not free. It cost 38 files here.
196 lines
7.5 KiB
Go
196 lines
7.5 KiB
Go
package profile
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// The decision each test defends is named in the test, per novox/hq ADR 0017. These cover
|
|
// structure and logic; profile_system_test.go covers the same detectors against the real
|
|
// machine, because a test that fakes the system under detection asserts only that the fake
|
|
// behaves as expected.
|
|
|
|
func TestIssue007_installedIsNotUsable(t *testing.T) {
|
|
// 04-ISSUES/007 — an installed package is not a capability. The client was present and the
|
|
// daemon was down, and the node took work it could not do. A detector whose command fails
|
|
// must report ABSENT, however installed the thing looks.
|
|
failing := func(context.Context, string, ...string) (string, error) {
|
|
return "", errors.New("docker exited 1: Cannot connect to the Docker daemon")
|
|
}
|
|
got := Detect(context.Background(), Default(failing), time.Second)
|
|
|
|
if got.Has(CapContainerRuntime) {
|
|
t.Fatal("a runtime whose daemon refuses the connection was reported present")
|
|
}
|
|
for _, v := range got.Capabilities {
|
|
if v.Name != CapContainerRuntime {
|
|
continue
|
|
}
|
|
if !strings.Contains(v.Detail, "Cannot connect") {
|
|
t.Fatalf("the reason was lost; detail was %q", v.Detail)
|
|
}
|
|
if v.How == "" {
|
|
t.Fatal("a verdict with no stated method cannot be checked when it is wrong")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEveryVerdictSaysHowItKnows(t *testing.T) {
|
|
// A capability reported absent with no reason is the fault in a new place: something
|
|
// nobody can act on. Both outcomes must carry a method.
|
|
for _, runner := range []Runner{
|
|
func(context.Context, string, ...string) (string, error) { return "ok", nil },
|
|
func(context.Context, string, ...string) (string, error) { return "", errors.New("nope") },
|
|
} {
|
|
for _, v := range Detect(context.Background(), Default(runner), time.Second).Capabilities {
|
|
if strings.TrimSpace(v.How) == "" {
|
|
t.Errorf("%s reports present=%v with no stated method", v.Name, v.Present)
|
|
}
|
|
if strings.TrimSpace(v.Detail) == "" {
|
|
t.Errorf("%s reports present=%v with no detail", v.Name, v.Present)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDetectionSurvivesAFailingProbe(t *testing.T) {
|
|
// Deliberately NOT ADR 0010. That rule governs APPLYING state, where a failed step means
|
|
// the machine is not what was asked for. A failed probe is a finding, and aborting would
|
|
// replace one legible absence with total ignorance of the rest.
|
|
only := func(ctx context.Context, name string, args ...string) (string, error) {
|
|
if name == "pacman" {
|
|
return "pacman 7.0.0", nil
|
|
}
|
|
return "", errors.New("not here")
|
|
}
|
|
got := Detect(context.Background(), Default(only), time.Second)
|
|
|
|
if len(got.Capabilities) != len(Default(nil)) {
|
|
t.Fatalf("expected every detector to report, got %d of %d",
|
|
len(got.Capabilities), len(Default(nil)))
|
|
}
|
|
if !got.Has(CapPackageManager) {
|
|
t.Error("the one working capability was lost among the failures")
|
|
}
|
|
}
|
|
|
|
func TestAProbeCannotHangTheHost(t *testing.T) {
|
|
// A host that blocks forever on a wedged command reports nothing at all, which is worse
|
|
// than reporting the capability absent.
|
|
blocking := func(ctx context.Context, name string, args ...string) (string, error) {
|
|
<-ctx.Done()
|
|
return "", ctx.Err()
|
|
}
|
|
done := make(chan Profile, 1)
|
|
go func() { done <- Detect(context.Background(), Default(blocking), 50*time.Millisecond) }()
|
|
|
|
select {
|
|
case got := <-done:
|
|
if got.Has(CapFirewall) {
|
|
t.Error("a probe that never answered was reported present")
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("detection did not return — a wedged probe hung the host")
|
|
}
|
|
}
|
|
|
|
func TestUnknownCapabilityIsAbsentNotAnError(t *testing.T) {
|
|
// Asking about a capability nothing detects is a question with a true answer.
|
|
p := Detect(context.Background(), nil, time.Second)
|
|
if p.Has("something-nothing-detects") {
|
|
t.Error("an undetected capability reported present")
|
|
}
|
|
}
|
|
|
|
func TestMissingListsWhatCannotBeAskedOf(t *testing.T) {
|
|
// What a node CANNOT do is the half that decides whether work may be placed on it.
|
|
none := func(context.Context, string, ...string) (string, error) { return "", errors.New("no") }
|
|
missing := Detect(context.Background(), Default(none), time.Second).Missing()
|
|
|
|
if len(missing) == 0 {
|
|
t.Fatal("everything failed and nothing was reported missing")
|
|
}
|
|
for i := 1; i < len(missing); i++ {
|
|
if missing[i-1] > missing[i] {
|
|
t.Fatalf("Missing() is not ordered: %v", missing)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheProfileIsOrdered(t *testing.T) {
|
|
// Two runs on an unchanged machine produce the same profile. Without this, comparing what
|
|
// a node was against what it is would report differences that are only ordering.
|
|
ok := func(context.Context, string, ...string) (string, error) { return "fine", nil }
|
|
first := Detect(context.Background(), Default(ok), time.Second)
|
|
second := Detect(context.Background(), Default(ok), time.Second)
|
|
|
|
if len(first.Capabilities) != len(second.Capabilities) {
|
|
t.Fatal("two runs disagreed on how many capabilities exist")
|
|
}
|
|
for i := range first.Capabilities {
|
|
if first.Capabilities[i].Name != second.Capabilities[i].Name {
|
|
t.Fatalf("ordering is not stable at %d: %q then %q",
|
|
i, first.Capabilities[i].Name, second.Capabilities[i].Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestADegradedInitIsStillAnInit(t *testing.T) {
|
|
// Found by running against a real machine, not by reasoning. `systemctl is-system-running`
|
|
// exits non-zero for every state except `running` — including `degraded`, which means some
|
|
// units failed and the init is emphatically present. Reading the exit code declared no
|
|
// service manager on a machine whose init it was.
|
|
//
|
|
// This is 04-ISSUES/007 in the mirror: 007 is installed-but-broken reported present; this
|
|
// is working-but-imperfect reported absent. Both make the mesh place work wrongly.
|
|
degraded := func(ctx context.Context, name string, args ...string) (string, error) {
|
|
if name == "systemctl" {
|
|
return "degraded\n", errors.New("systemctl exited 1: ")
|
|
}
|
|
return "", errors.New("not here")
|
|
}
|
|
got := Detect(context.Background(), Default(degraded), time.Second)
|
|
|
|
if !got.Has(CapServiceManager) {
|
|
t.Fatal("a degraded init was reported absent — the machine would refuse work it can do")
|
|
}
|
|
for _, v := range got.Capabilities {
|
|
if v.Name == CapServiceManager && v.Detail != "degraded" {
|
|
t.Errorf("the state was lost; detail was %q", v.Detail)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAnInitThatIsNotManagingThisMachineIsAbsent(t *testing.T) {
|
|
// The other side of the same rule. `offline` means it is installed and not in charge —
|
|
// which must not be read as present just because a state came back.
|
|
for _, state := range []string{"offline", "unknown"} {
|
|
runner := func(ctx context.Context, name string, args ...string) (string, error) {
|
|
if name == "systemctl" {
|
|
return state + "\n", errors.New("systemctl exited 1: ")
|
|
}
|
|
return "", errors.New("not here")
|
|
}
|
|
if Detect(context.Background(), Default(runner), time.Second).Has(CapServiceManager) {
|
|
t.Errorf("state %q was reported as a working service manager", state)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAFailureWithNoMessageStillCarriesAReason(t *testing.T) {
|
|
// systemctl fails with empty stderr, which produced a verdict reading "exited 1:" — absent,
|
|
// with nothing anyone could act on.
|
|
silent := func(context.Context, string, ...string) (string, error) { return "", errors.New("") }
|
|
for _, v := range Detect(context.Background(), Default(silent), time.Second).Capabilities {
|
|
if v.Present {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(v.Detail) == "" || strings.HasSuffix(strings.TrimSpace(v.Detail), ":") {
|
|
t.Errorf("%s is absent for no stated reason: %q", v.Name, v.Detail)
|
|
}
|
|
}
|
|
}
|