//go:build linux package stopevidence import ( "context" "errors" "net/http" "net/http/httptest" "os" "path/filepath" "reflect" "sync/atomic" "testing" "time" "github.com/carlosframework/platform/internal/activator" "github.com/carlosframework/platform/internal/cgroup" "github.com/carlosframework/platform/internal/registry" ) type evidenceProc struct{ exited atomic.Bool } func (p *evidenceProc) Stop(context.Context) error { p.exited.Store(true); return nil } func (p *evidenceProc) Exited() bool { return p.exited.Load() } type evidenceRep struct{} func (evidenceRep) Err() error { return nil } func (evidenceRep) Close(context.Context) error { return nil } type evidenceLease struct{} func (evidenceLease) Release(context.Context) error { return nil } func (evidenceLease) Replicate(context.Context, string, time.Duration) (activator.Rep, error) { return evidenceRep{}, nil } type evidenceBackend struct{} func (evidenceBackend) Acquire(context.Context, registry.Route) (activator.Lease, error) { return evidenceLease{}, nil } func (evidenceBackend) Restore(context.Context, registry.Route) (bool, error) { return false, nil } func (evidenceBackend) Seed(context.Context, registry.Route) error { return nil } func (evidenceBackend) Start(context.Context, registry.Route) (activator.Proc, string, error) { return &evidenceProc{}, "test-build", nil } func (evidenceBackend) NextDue(context.Context, registry.Route) (int64, error) { return 0, nil } func (evidenceBackend) HasSidecar(registry.Route) bool { return false } func (evidenceBackend) StartSidecar(context.Context, registry.Route) (activator.Proc, error) { return nil, errors.New("unexpected sidecar") } func (evidenceBackend) StartWorker(context.Context, registry.Route, map[string]string) (activator.Proc, string, error) { return nil, "", errors.New("unexpected worker") } func fenceFixture(t *testing.T, host string, generation uint64) *activator.StopEvidence { t.Helper() dir := t.TempDir() reg, err := registry.Open(filepath.Join(dir, "registry.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { reg.Close() }) a := activator.New(activator.Config{Bucket: "test", Idle: time.Hour}, evidenceBackend{}, reg) t.Cleanup(func() { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() a.Shutdown(ctx) }) rt := registry.Route{Host: host, Kind: "instance", Hibernate: true, Socket: filepath.Join(dir, "app.sock"), DBPath: filepath.Join(dir, "data.db")} if err := reg.Upsert(rt); err != nil { t.Fatal(err) } ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second) defer cancel() called := false w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, "http://"+host+"/", nil).WithContext(ctx) if !a.ServeHTTP(w, r, rt, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { called = true; w.WriteHeader(http.StatusNoContent) })) || !called { t.Fatalf("wake failed: %d %s", w.Code, w.Body.String()) } proof, err := a.FenceAndStop(ctx, host, generation) if err != nil || !proof.Matches(host, generation) { t.Fatal("fence proof", err) } return proof } type evidenceGroup struct { populated bool members []int populationErr, membersErr error } func (g *evidenceGroup) Populated() (bool, error) { return g.populated, g.populationErr } func (g *evidenceGroup) Members() ([]int, error) { return g.members, g.membersErr } func TestCollectRequiresEveryCondition(t *testing.T) { host := "shop.example" proof := fenceFixture(t, host, 7) failure := errors.New("read failed") for _, tc := range []struct { name string change func(*testing.T, *Inputs, *evidenceGroup) want error }{ {"success", func(t *testing.T, in *Inputs, g *evidenceGroup) {}, nil}, {"no-fence", func(t *testing.T, in *Inputs, g *evidenceGroup) { in.Fence = nil }, ErrFence}, {"empty-fence", func(t *testing.T, in *Inputs, g *evidenceGroup) { in.Fence = &activator.StopEvidence{} }, ErrFence}, {"wrong-host", func(t *testing.T, in *Inputs, g *evidenceGroup) { in.Host = "other.example" }, ErrFence}, {"wrong-generation", func(t *testing.T, in *Inputs, g *evidenceGroup) { in.Generation = 8 }, ErrFence}, {"no-generation", func(t *testing.T, in *Inputs, g *evidenceGroup) { in.Generation = 0 }, ErrInvalidInput}, {"no-group", func(t *testing.T, in *Inputs, g *evidenceGroup) { in.Cgroup = nil }, ErrNoCgroupEvidence}, {"population", func(t *testing.T, in *Inputs, g *evidenceGroup) { g.populated = true }, ErrPopulated}, {"members", func(t *testing.T, in *Inputs, g *evidenceGroup) { g.members = []int{55} }, ErrMembers}, {"population-error", func(t *testing.T, in *Inputs, g *evidenceGroup) { g.populationErr = failure }, failure}, {"members-error", func(t *testing.T, in *Inputs, g *evidenceGroup) { g.membersErr = failure }, failure}, {"replicator-open", func(t *testing.T, in *Inputs, g *evidenceGroup) { in.ReplicatorsClosed = false }, ErrReplicatorsOpen}, {"no-paths", func(t *testing.T, in *Inputs, g *evidenceGroup) { in.DBPaths = nil }, ErrInvalidInput}, {"relative-path", func(t *testing.T, in *Inputs, g *evidenceGroup) { in.DBPaths = []string{"data.db"} }, ErrInvalidInput}, {"proc-read-error", func(t *testing.T, in *Inputs, g *evidenceGroup) { in.ProcRoot = filepath.Join(in.ProcRoot, "missing") }, ErrIncomplete}, {"db-holder", func(t *testing.T, in *Inputs, g *evidenceGroup) { addHolder(t, in, "") }, ErrFDHolders}, {"wal-holder", func(t *testing.T, in *Inputs, g *evidenceGroup) { addHolder(t, in, "-wal") }, ErrFDHolders}, {"shm-holder", func(t *testing.T, in *Inputs, g *evidenceGroup) { addHolder(t, in, "-shm") }, ErrFDHolders}, } { t.Run(tc.name, func(t *testing.T) { g := &evidenceGroup{} in := Inputs{Host: host, Generation: 7, Fence: proof, Cgroup: g, DBPaths: []string{filepath.Join(t.TempDir(), "data.db")}, ProcRoot: t.TempDir(), ReplicatorsClosed: true} tc.change(t, &in, g) e, err := Collect(t.Context(), in) if tc.want != nil { if !errors.Is(err, tc.want) || !reflect.DeepEqual(e, Evidence{}) { t.Fatalf("evidence on failure: %+v %v", e, err) } return } if err != nil || e.Host != host || e.Generation != 7 || !e.FenceClosed || !e.SpawnAttemptsDrained || !e.CgroupUnpopulated || !e.CgroupMembersEmpty || !e.ReplicatorsClosed || !e.FDScanComplete || e.CheckedAt.IsZero() || len(e.CheckedPaths) != 3 { t.Fatalf("incomplete evidence: %+v %v", e, err) } g.populated = true if _, err := Collect(t.Context(), in); !errors.Is(err, ErrPopulated) { t.Fatal("cached evidence", err) } }) } ctx, cancel := context.WithCancel(t.Context()) cancel() if e, err := Collect(ctx, Inputs{}); !errors.Is(err, context.Canceled) || !reflect.DeepEqual(e, Evidence{}) { t.Fatal(e, err) } if err := Cleanup(t.Context(), nil); !errors.Is(err, ErrInvalidInput) { t.Fatal(err) } if err := Cleanup(t.Context(), &cgroup.Cleanup{}); !errors.Is(err, cgroup.ErrStrictState) { t.Fatal(err) } } func addHolder(t *testing.T, in *Inputs, suffix string) { t.Helper() dir := filepath.Join(in.ProcRoot, "55", "task", "55", "fd") if err := os.MkdirAll(dir, 0700); err != nil { t.Fatal(err) } if err := os.Symlink(in.DBPaths[0]+suffix, filepath.Join(dir, "4")); err != nil { t.Fatal(err) } }