package store import ( "slices" "strings" "testing" "time" ) func change(at time.Time, name string) Change { return Change{ At: at, Verb: Changed, Group: "apps", Version: "v1", Resource: "deployments", Kind: "Deployment", Namespace: "default", Name: name, UID: "uid-" + name, Cells: []string{"1/1", "Running"}, } } func noteOne(t *testing.T, store *Store, cluster string, one Change) { t.Helper() err := store.Timeline(cluster).Note(t.Context(), []Change{one}) if err != nil { t.Fatalf("note: %v", err) } } func TestAChangeComesBackTheWayItWentIn(t *testing.T) { store := openHistory(t, dbPath(t)) noteOne(t, store, p1, change(noon, "web")) found, err := store.Changed(t.Context(), Query{}) if err != nil { t.Fatalf("changed: %v", err) } if len(found.Rows) != 1 { t.Fatalf("wanted one change, got %d", len(found.Rows)) } row := found.Rows[0] if row.Name != "web" || row.Kind != "Deployment" || row.UID != "uid-web" { t.Fatalf("the change came back as %+v", row) } if !row.At.Equal(noon) { t.Fatalf("wanted %s, got %s", noon, row.At) } if len(row.Cells) != 2 || row.Cells[1] != "Running" { t.Fatalf("the cells came back as %v", row.Cells) } } func TestAChangeIsStampedWithTheClusterItIsOn(t *testing.T) { store := openHistory(t, dbPath(t)) noteOne(t, store, p1, change(noon, "web")) noteOne(t, store, p2, change(noon, "api")) found, err := store.Changed(t.Context(), Query{Cluster: p2}) if err != nil { t.Fatalf("changed: %v", err) } if len(found.Rows) != 1 { t.Fatalf("wanted one change, got %d", len(found.Rows)) } if found.Rows[0].Name != "api" || found.Rows[0].Cluster != p2 { t.Fatalf("the change came back as %+v", found.Rows[0]) } } func TestTheNewestChangeIsFirst(t *testing.T) { store := openHistory(t, dbPath(t)) noteOne(t, store, p1, change(noon.Add(-time.Hour), "old")) noteOne(t, store, p1, change(noon, "new")) found, err := store.Changed(t.Context(), Query{}) if err != nil { t.Fatalf("changed: %v", err) } if found.Rows[0].Name != "new" || found.Rows[1].Name != "old" { t.Fatalf("the order came back as %s then %s", found.Rows[0].Name, found.Rows[1].Name) } } func TestAWholeBatchOfChangesLandsAtOnce(t *testing.T) { store := openHistory(t, dbPath(t)) err := store.Timeline(p1).Note(t.Context(), []Change{ change(noon, "one"), change(noon, "two"), change(noon, "three"), }) if err != nil { t.Fatalf("note: %v", err) } found, readErr := store.Changed(t.Context(), Query{}) if readErr != nil { t.Fatalf("changed: %v", readErr) } if len(found.Rows) != 3 { t.Fatalf("wanted three changes, got %d", len(found.Rows)) } } func TestNotingNothingIsNotAWrite(t *testing.T) { store := openHistory(t, dbPath(t)) err := store.Timeline(p1).Note(t.Context(), nil) if err != nil { t.Fatalf("note: %v", err) } found, readErr := store.Changed(t.Context(), Query{}) if readErr != nil { t.Fatalf("changed: %v", readErr) } if len(found.Rows) != 0 { t.Fatalf("wanted no changes, got %d", len(found.Rows)) } } func TestTheChangeQuerySaysWhenThereAreMore(t *testing.T) { store := openHistory(t, dbPath(t)) noteOne(t, store, p1, change(noon, "one")) noteOne(t, store, p1, change(noon, "two")) found, err := store.Changed(t.Context(), Query{Limit: 1}) if err != nil { t.Fatalf("changed: %v", err) } if len(found.Rows) != 1 { t.Fatalf("wanted one change, got %d", len(found.Rows)) } if !found.More { t.Fatal("wanted to be told there are more") } } func TestChangesOlderThanTheWindowArePruned(t *testing.T) { store := openHistory(t, dbPath(t)) noteOne(t, store, p1, change(noon.AddDate(0, 0, -30), "ancient")) noteOne(t, store, p1, change(noon, "today")) err := store.Prune(t.Context(), Retention{Days: 7}, noon) if err != nil { t.Fatalf("prune: %v", err) } found, readErr := store.Changed(t.Context(), Query{}) if readErr != nil { t.Fatalf("changed: %v", readErr) } if len(found.Rows) != 1 || found.Rows[0].Name != "today" { t.Fatalf("what survived the prune was %+v", found.Rows) } } func TestTheRowCapIsWhatHoldsOnAClusterNobodyMeasured(t *testing.T) { store := openHistory(t, dbPath(t)) for at := range 10 { noteOne(t, store, p1, change(noon, "web-"+string(rune('a'+at)))) } err := store.Prune(t.Context(), Retention{Rows: 4}, noon) if err != nil { t.Fatalf("prune: %v", err) } found, readErr := store.Changed(t.Context(), Query{}) if readErr != nil { t.Fatalf("changed: %v", readErr) } if len(found.Rows) != 4 { t.Fatalf("wanted four changes left, got %d", len(found.Rows)) } if found.Rows[0].Name != "web-j" { t.Fatalf("the newest kept was %s", found.Rows[0].Name) } } func TestPruningAnEmptyTimelineIsFine(t *testing.T) { store := openHistory(t, dbPath(t)) err := store.Prune(t.Context(), Retention{Days: 7, Rows: 10}, noon) if err != nil { t.Fatalf("prune: %v", err) } } func TestPruningWithNoRetentionAskedForKeepsEverything(t *testing.T) { store := openHistory(t, dbPath(t)) noteOne(t, store, p1, change(noon.AddDate(0, 0, -400), "ancient")) err := store.Prune(t.Context(), Retention{}, noon) if err != nil { t.Fatalf("prune: %v", err) } found, readErr := store.Changed(t.Context(), Query{}) if readErr != nil { t.Fatalf("changed: %v", readErr) } if len(found.Rows) != 1 { t.Fatalf("wanted the row kept, got %d", len(found.Rows)) } } func TestClearingAClustersHistoryTakesItsChangesToo(t *testing.T) { store := openHistory(t, dbPath(t)) noteOne(t, store, p1, change(noon, "web")) noteOne(t, store, p2, change(noon, "api")) err := store.Forget(t.Context(), p1) if err != nil { t.Fatalf("forget: %v", err) } found, readErr := store.Changed(t.Context(), Query{}) if readErr != nil { t.Fatalf("changed: %v", readErr) } if len(found.Rows) != 1 || found.Rows[0].Cluster != p2 { t.Fatalf("what survived was %+v", found.Rows) } } func TestAStoreThatCouldNotOpenSaysNothingRatherThanFailing(t *testing.T) { store := unavailable("nowhere to keep history") err := store.Timeline(p1).Note(t.Context(), []Change{change(noon, "web")}) if err != nil { t.Fatalf("note: %v", err) } found, readErr := store.Changed(t.Context(), Query{}) if readErr != nil { t.Fatalf("changed: %v", readErr) } if len(found.Rows) != 0 { t.Fatalf("wanted nothing back, got %d", len(found.Rows)) } pruneErr := store.Prune(t.Context(), Retention{Days: 1, Rows: 1}, noon) if pruneErr != nil { t.Fatalf("prune: %v", pruneErr) } } func TestCellsThatCannotBeReadComeBackEmpty(t *testing.T) { store := openHistory(t, dbPath(t)) noteOne(t, store, p1, Change{At: noon, Verb: Added, Name: "web", Cells: nil}) found, err := store.Changed(t.Context(), Query{}) if err != nil { t.Fatalf("changed: %v", err) } if len(found.Rows[0].Cells) != 0 { t.Fatalf("wanted no cells, got %v", found.Rows[0].Cells) } } func TestAChangeWithNoRoomForItIsReported(t *testing.T) { store := openHistory(t, dbPath(t)) if _, err := store.writes.ExecContext(t.Context(), "DROP TABLE changes"); err != nil { t.Fatalf("drop: %v", err) } noteErr := store.Timeline(p1).Note(t.Context(), []Change{change(noon, "web")}) if noteErr == nil { t.Fatal("noting into a missing table reported success") } if _, readErr := store.Changed(t.Context(), Query{}); readErr == nil { t.Fatal("reading a missing table reported success") } if pruneErr := store.Prune(t.Context(), Retention{Days: 1}, noon); pruneErr == nil { t.Fatal("trimming a missing table reported success") } } func TestTrimmingByRowsAgainstAMissingTableIsReported(t *testing.T) { store := openHistory(t, dbPath(t)) if _, err := store.writes.ExecContext(t.Context(), "DROP TABLE changes"); err != nil { t.Fatalf("drop: %v", err) } err := store.Prune(t.Context(), Retention{Rows: 10}, noon) if err == nil { t.Fatal("trimming a missing table reported success") } } func TestAChangeColumnThatChangedShapeIsReported(t *testing.T) { store := openHistory(t, dbPath(t)) noteOne(t, store, p1, change(noon, "web")) if _, err := store.writes.ExecContext(t.Context(), "UPDATE changes SET at = 'not a number'"); err != nil { t.Fatalf("corrupt: %v", err) } _, err := store.Changed(t.Context(), Query{}) if err == nil { t.Fatal("a change that could not be read came back as a silent zero") } if !strings.Contains(err.Error(), "store") { t.Fatalf("error = %q, want it to say what it was doing", err.Error()) } } func TestNotingIntoAClosedStoreIsReported(t *testing.T) { store := openHistory(t, dbPath(t)) if err := store.writes.Close(); err != nil { t.Fatalf("close: %v", err) } noteErr := store.Timeline(p1).Note(t.Context(), []Change{change(noon, "web")}) if noteErr == nil { t.Fatal("noting into a closed database reported success") } } func TestCellsThatAreNotAListComeBackEmpty(t *testing.T) { store := openHistory(t, dbPath(t)) noteOne(t, store, p1, change(noon, "web")) if _, err := store.writes.ExecContext(t.Context(), "UPDATE changes SET cells = 'not json'"); err != nil { t.Fatalf("corrupt: %v", err) } found, err := store.Changed(t.Context(), Query{}) if err != nil { t.Fatalf("changed: %v", err) } if len(found.Rows[0].Cells) != 0 { t.Fatalf("wanted no cells, got %v", found.Rows[0].Cells) } } func TestTheLimitAStoreWillHonourIsSayable(t *testing.T) { if Limit(0) != defaultLimit { t.Fatalf("Limit(0) = %d", Limit(0)) } if Limit(maxLimit+1) != maxLimit { t.Fatalf("Limit above the cap = %d", Limit(maxLimit+1)) } if Limit(5) != 5 { t.Fatalf("Limit(5) = %d", Limit(5)) } } func TestRecolouringATabThatIsNotThereIsQuiet(t *testing.T) { store := openHistory(t, dbPath(t)) err := store.Tabs().Recolor(t.Context(), p1, 3) if err != nil { t.Fatalf("recolor: %v", err) } closed := unavailable("nowhere to keep history") if quietErr := closed.Tabs().Recolor(t.Context(), p1, 3); quietErr != nil { t.Fatalf("recolor: %v", quietErr) } } func TestRecordingIntoAMissingTableIsReported(t *testing.T) { store := openHistory(t, dbPath(t)) if _, err := store.writes.ExecContext(t.Context(), "DROP TABLE clusters"); err != nil { t.Fatalf("drop: %v", err) } err := store.Tabs().Recording(t.Context(), p1, "workloads") if err == nil { t.Fatal("recording against a missing table reported success") } } func TestAChangeRemembersWhatItMovedFrom(t *testing.T) { store := openHistory(t, dbPath(t)) one := change(noon, "web") one.Was = []string{"2/2", "Running"} noteOne(t, store, p1, one) found, err := store.Changed(t.Context(), Query{}) if err != nil { t.Fatalf("changed: %v", err) } if len(found.Rows[0].Was) != 2 || found.Rows[0].Was[1] != "Running" { t.Fatalf("was = %v", found.Rows[0].Was) } } func TestAPageOfChangesContinuesBelowTheCursor(t *testing.T) { store := openHistory(t, dbPath(t)) noteOne(t, store, p1, change(noon.Add(-2*time.Minute), "oldest")) noteOne(t, store, p1, change(noon.Add(-time.Minute), "middle")) noteOne(t, store, p1, change(noon, "newest")) first, err := store.Changed(t.Context(), Query{Limit: 1}) if err != nil { t.Fatalf("changed: %v", err) } next, nextErr := store.Changed(t.Context(), Query{Limit: 1, After: first.Rows[0].ID}) if nextErr != nil { t.Fatalf("changed: %v", nextErr) } if next.Rows[0].Name != "middle" { t.Fatalf("the next page started at %s", next.Rows[0].Name) } } func TestChangePaginationFollowsTimestampOrder(t *testing.T) { store := openHistory(t, dbPath(t)) noteOne(t, store, p1, change(noon, "oldest")) noteOne(t, store, p1, change(noon.Add(2*time.Minute), "newest")) noteOne(t, store, p1, change(noon.Add(time.Minute), "middle")) first, err := store.Changed(t.Context(), Query{Limit: 2}) if err != nil { t.Fatalf("changed: %v", err) } second, nextErr := store.Changed(t.Context(), Query{Limit: 2, After: first.Rows[1].ID}) if nextErr != nil { t.Fatalf("changed: %v", nextErr) } firstNames := []string{first.Rows[0].Name, first.Rows[1].Name} if !slices.Equal(firstNames, []string{"newest", "middle"}) { t.Fatalf("first page = %v", firstNames) } secondNames := make([]string, 0, len(second.Rows)) for _, row := range second.Rows { secondNames = append(secondNames, row.Name) } if !slices.Equal(secondNames, []string{"oldest"}) { t.Fatalf("second page = %v", secondNames) } } func TestTheLastPageSaysThereIsNoMore(t *testing.T) { store := openHistory(t, dbPath(t)) noteOne(t, store, p1, change(noon, "only")) found, err := store.Changed(t.Context(), Query{Limit: 10}) if err != nil { t.Fatalf("changed: %v", err) } if found.More { t.Fatal("a page that held everything said there was more") } }