Skip to content

Commit 44c08c1

Browse files
authored
timeline: includeManaged source parity + delta/deep-link contract tests (#1147)
1 parent 832c431 commit 44c08c1

11 files changed

Lines changed: 923 additions & 623 deletions

File tree

internal/timeline/sqlite_store_test.go

Lines changed: 0 additions & 331 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,215 +1148,6 @@ func TestSQLiteStore_Migration_AddsClusterContext(t *testing.T) {
11481148
}
11491149
}
11501150

1151-
// A relist re-emits the same informer id; INSERT ... ON CONFLICT must leave the
1152-
// original row untouched (no k8s_event mutation path) instead of duplicating it.
1153-
// Mirrors TestMemoryStore_DedupesIdenticalInformerID.
1154-
func TestSQLiteStore_DedupesIdenticalInformerID(t *testing.T) {
1155-
store, cleanup := createTestSQLiteStore(t)
1156-
defer cleanup()
1157-
ctx := context.Background()
1158-
1159-
rv := "100"
1160-
add := NewInformerEvent("Deployment", "apps/v1", "team-a", "web", "uid-1", rv, EventTypeAdd, HealthHealthy, nil, nil, nil, nil)
1161-
relistUpdate := NewInformerEvent("Deployment", "apps/v1", "team-a", "web", "uid-1", rv, EventTypeUpdate, HealthHealthy, nil, nil, nil, nil)
1162-
if add.ID != relistUpdate.ID {
1163-
t.Fatalf("relist add/update must share an id: %q vs %q", add.ID, relistUpdate.ID)
1164-
}
1165-
1166-
if err := store.Append(ctx, add); err != nil {
1167-
t.Fatalf("Append add: %v", err)
1168-
}
1169-
if err := store.Append(ctx, relistUpdate); err != nil {
1170-
t.Fatalf("Append relistUpdate: %v", err)
1171-
}
1172-
1173-
got, err := store.Query(ctx, QueryOptions{Limit: 10, IncludeManaged: true})
1174-
if err != nil {
1175-
t.Fatalf("Query: %v", err)
1176-
}
1177-
if len(got) != 1 {
1178-
t.Fatalf("expected 1 deduped row, got %d: %+v", len(got), got)
1179-
}
1180-
// The original add is kept — the relist did NOT overwrite it into an update.
1181-
if got[0].EventType != EventTypeAdd {
1182-
t.Fatalf("informer relist must not mutate the row, got event_type %q want %q", got[0].EventType, EventTypeAdd)
1183-
}
1184-
1185-
del := NewInformerEvent("Deployment", "apps/v1", "team-a", "web", "uid-1", rv, EventTypeDelete, HealthUnknown, nil, nil, nil, nil)
1186-
if del.ID == add.ID {
1187-
t.Fatalf("delete must get a distinct id, got %q for both", del.ID)
1188-
}
1189-
if err := store.Append(ctx, del); err != nil {
1190-
t.Fatalf("Append del: %v", err)
1191-
}
1192-
got, err = store.Query(ctx, QueryOptions{Limit: 10, IncludeManaged: true})
1193-
if err != nil {
1194-
t.Fatalf("Query: %v", err)
1195-
}
1196-
if len(got) != 2 {
1197-
t.Fatalf("expected add + delete = 2 rows, got %d", len(got))
1198-
}
1199-
}
1200-
1201-
// A K8s Event's count bump reuses the uid-based id; the store must upsert the
1202-
// mutable fields (count/message/timestamp) in place, not drop the bump or append
1203-
// a duplicate. Mirrors TestMemoryStore_K8sEventCountBumpUpserts.
1204-
func TestSQLiteStore_K8sEventCountBumpUpserts(t *testing.T) {
1205-
store, cleanup := createTestSQLiteStore(t)
1206-
defer cleanup()
1207-
ctx := context.Background()
1208-
1209-
base := time.Now()
1210-
mk := func(count int32, message string, ts time.Time) TimelineEvent {
1211-
return TimelineEvent{
1212-
ID: "k8s-uid-1", Timestamp: ts, Source: SourceK8sEvent,
1213-
Kind: "Pod", Namespace: "team-a", Name: "web-abc",
1214-
EventType: EventTypeWarning, Reason: "BackOff", Message: message, Count: count,
1215-
}
1216-
}
1217-
if err := store.Append(ctx, mk(1, "back-off 10s", base)); err != nil {
1218-
t.Fatalf("Append first: %v", err)
1219-
}
1220-
if err := store.Append(ctx, mk(5, "back-off 40s", base.Add(30*time.Second))); err != nil {
1221-
t.Fatalf("Append bump: %v", err)
1222-
}
1223-
1224-
got, err := store.Query(ctx, QueryOptions{Limit: 10, IncludeManaged: true, IncludeK8sEvents: true})
1225-
if err != nil {
1226-
t.Fatalf("Query: %v", err)
1227-
}
1228-
if len(got) != 1 {
1229-
t.Fatalf("expected 1 upserted row, got %d: %+v", len(got), got)
1230-
}
1231-
if got[0].Count != 5 {
1232-
t.Fatalf("expected refreshed count 5, got %d", got[0].Count)
1233-
}
1234-
if got[0].Message != "back-off 40s" {
1235-
t.Fatalf("expected refreshed message, got %q", got[0].Message)
1236-
}
1237-
if !got[0].Timestamp.Equal(base.Add(30 * time.Second)) {
1238-
t.Fatalf("expected refreshed timestamp %v, got %v", base.Add(30*time.Second), got[0].Timestamp)
1239-
}
1240-
}
1241-
1242-
// An out-of-order older revision of the same K8s Event uid must NOT clobber the
1243-
// newer row already stored. Mirrors the recency guard in
1244-
// TestMemoryStore_K8sEventBumpMovesToRecency.
1245-
func TestSQLiteStore_K8sEventStaleBumpIgnored(t *testing.T) {
1246-
store, cleanup := createTestSQLiteStore(t)
1247-
defer cleanup()
1248-
ctx := context.Background()
1249-
1250-
base := time.Now()
1251-
mk := func(count int32, ts time.Time) TimelineEvent {
1252-
return TimelineEvent{
1253-
ID: "k8s-uid-1", Timestamp: ts, Source: SourceK8sEvent,
1254-
Kind: "Pod", Namespace: "team-a", Name: "web-abc",
1255-
EventType: EventTypeWarning, Reason: "BackOff", Count: count,
1256-
}
1257-
}
1258-
// Newest revision arrives first, then a stale older relay of the same uid.
1259-
if err := store.Append(ctx, mk(5, base.Add(30*time.Second))); err != nil {
1260-
t.Fatalf("Append newer: %v", err)
1261-
}
1262-
if err := store.Append(ctx, mk(1, base)); err != nil {
1263-
t.Fatalf("Append stale: %v", err)
1264-
}
1265-
1266-
got, err := store.Query(ctx, QueryOptions{Limit: 10, IncludeManaged: true, IncludeK8sEvents: true})
1267-
if err != nil {
1268-
t.Fatalf("Query: %v", err)
1269-
}
1270-
if len(got) != 1 {
1271-
t.Fatalf("expected 1 row, got %d: %+v", len(got), got)
1272-
}
1273-
if got[0].Count != 5 {
1274-
t.Fatalf("stale relay must not clobber newer row, got count %d want 5", got[0].Count)
1275-
}
1276-
if !got[0].Timestamp.Equal(base.Add(30 * time.Second)) {
1277-
t.Fatalf("stale relay must not roll back timestamp, got %v", got[0].Timestamp)
1278-
}
1279-
}
1280-
1281-
// Seq is the delta cursor: SinceSeq must return exactly what arrived after the
1282-
// cursor, keyed on arrival order — a late event with an older timestamp still
1283-
// lands ahead of the cursor. Mirrors TestMemoryStore_SeqCursor.
1284-
func TestSQLiteStore_SeqCursor(t *testing.T) {
1285-
store, cleanup := createTestSQLiteStore(t)
1286-
defer cleanup()
1287-
ctx := context.Background()
1288-
1289-
base := time.Now()
1290-
mk := func(id string, ts time.Time) TimelineEvent {
1291-
return TimelineEvent{
1292-
ID: id, Timestamp: ts, Source: SourceInformer,
1293-
Kind: "Pod", Namespace: "default", Name: id, EventType: EventTypeUpdate,
1294-
}
1295-
}
1296-
if err := store.AppendBatch(ctx, []TimelineEvent{mk("a", base), mk("b", base.Add(time.Second))}); err != nil {
1297-
t.Fatalf("AppendBatch: %v", err)
1298-
}
1299-
all, err := store.Query(ctx, QueryOptions{Limit: 10, IncludeManaged: true})
1300-
if err != nil {
1301-
t.Fatalf("Query: %v", err)
1302-
}
1303-
if len(all) != 2 || all[0].Seq == 0 || all[1].Seq == 0 || all[0].Seq == all[1].Seq {
1304-
t.Fatalf("expected 2 events with distinct non-zero seqs, got %+v", all)
1305-
}
1306-
cursor := max(all[0].Seq, all[1].Seq)
1307-
1308-
if err := store.Append(ctx, mk("late", base.Add(-time.Hour))); err != nil {
1309-
t.Fatalf("Append late: %v", err)
1310-
}
1311-
delta, err := store.Query(ctx, QueryOptions{Limit: 10, IncludeManaged: true, SinceSeq: cursor})
1312-
if err != nil {
1313-
t.Fatalf("Query delta: %v", err)
1314-
}
1315-
if len(delta) != 1 || delta[0].ID != "late" {
1316-
t.Fatalf("expected exactly the late arrival past the cursor, got %+v", delta)
1317-
}
1318-
if delta[0].Seq <= cursor {
1319-
t.Fatalf("late arrival seq %d must exceed cursor %d", delta[0].Seq, cursor)
1320-
}
1321-
}
1322-
1323-
// A K8s Event count bump upserts in place but must take a fresh seq, or a
1324-
// delta reader holding a cursor past the original arrival never sees the bump.
1325-
// Mirrors TestMemoryStore_K8sEventBumpAdvancesSeq.
1326-
func TestSQLiteStore_K8sEventBumpAdvancesSeq(t *testing.T) {
1327-
store, cleanup := createTestSQLiteStore(t)
1328-
defer cleanup()
1329-
ctx := context.Background()
1330-
1331-
base := time.Now()
1332-
mk := func(count int32, ts time.Time) TimelineEvent {
1333-
return TimelineEvent{
1334-
ID: "k8s-uid-1", Timestamp: ts, Source: SourceK8sEvent,
1335-
Kind: "Pod", Namespace: "default", Name: "web-abc",
1336-
EventType: EventTypeWarning, Reason: "BackOff", Count: count,
1337-
}
1338-
}
1339-
if err := store.Append(ctx, mk(1, base)); err != nil {
1340-
t.Fatalf("Append first: %v", err)
1341-
}
1342-
first, err := store.Query(ctx, QueryOptions{Limit: 10, IncludeManaged: true, IncludeK8sEvents: true})
1343-
if err != nil || len(first) != 1 {
1344-
t.Fatalf("Query first: %v %+v", err, first)
1345-
}
1346-
cursor := first[0].Seq
1347-
1348-
if err := store.Append(ctx, mk(5, base.Add(30*time.Second))); err != nil {
1349-
t.Fatalf("Append bump: %v", err)
1350-
}
1351-
delta, err := store.Query(ctx, QueryOptions{Limit: 10, IncludeManaged: true, IncludeK8sEvents: true, SinceSeq: cursor})
1352-
if err != nil {
1353-
t.Fatalf("Query delta: %v", err)
1354-
}
1355-
if len(delta) != 1 || delta[0].Count != 5 {
1356-
t.Fatalf("expected the bumped row past the cursor, got %+v", delta)
1357-
}
1358-
}
1359-
13601151
// A database created before the seq column existed must come back with arrival
13611152
// numbers backfilled from rowid, so pre-upgrade rows are addressable by cursor.
13621153
func TestSQLiteStore_SeqMigrationBackfillsFromRowid(t *testing.T) {
@@ -1452,76 +1243,6 @@ func TestSQLiteStore_ResourceCreatedAtRoundtrip(t *testing.T) {
14521243
}
14531244
}
14541245

1455-
// A bump that lost its enrichment (tombstone expired) must not erase the
1456-
// owner/labels/createdAt the row already knows; a bump that carries fresh
1457-
// enrichment wins. Mirrors TestMemoryStore_K8sEventBumpPreservesEnrichment.
1458-
func TestSQLiteStore_K8sEventBumpPreservesEnrichment(t *testing.T) {
1459-
store, cleanup := createTestSQLiteStore(t)
1460-
defer cleanup()
1461-
ctx := context.Background()
1462-
1463-
base := time.Now()
1464-
born := base.Add(-time.Hour).Truncate(time.Millisecond)
1465-
if err := store.Append(ctx, TimelineEvent{
1466-
ID: "k8s-uid-1", Timestamp: base, Source: SourceK8sEvent,
1467-
Kind: "Pod", Namespace: "default", Name: "web-abc",
1468-
EventType: EventTypeWarning, Reason: "BackOff", Count: 1,
1469-
CreatedAt: &born,
1470-
Owner: &OwnerInfo{Kind: "ReplicaSet", Name: "web"},
1471-
Labels: map[string]string{"app": "web"},
1472-
}); err != nil {
1473-
t.Fatalf("Append enriched: %v", err)
1474-
}
1475-
if err := store.Append(ctx, TimelineEvent{
1476-
ID: "k8s-uid-1", Timestamp: base.Add(30 * time.Second), Source: SourceK8sEvent,
1477-
Kind: "Pod", Namespace: "default", Name: "web-abc",
1478-
EventType: EventTypeWarning, Reason: "BackOff", Count: 5,
1479-
}); err != nil {
1480-
t.Fatalf("Append bare bump: %v", err)
1481-
}
1482-
1483-
got, err := store.Query(ctx, QueryOptions{Limit: 10, IncludeManaged: true, IncludeK8sEvents: true})
1484-
if err != nil || len(got) != 1 {
1485-
t.Fatalf("Query: %v %+v", err, got)
1486-
}
1487-
if got[0].Count != 5 {
1488-
t.Fatalf("expected bumped count 5, got %d", got[0].Count)
1489-
}
1490-
if got[0].CreatedAt == nil || !got[0].CreatedAt.Equal(born) {
1491-
t.Fatalf("bare bump erased CreatedAt: %+v", got[0].CreatedAt)
1492-
}
1493-
if got[0].Owner == nil || got[0].Owner.Name != "web" {
1494-
t.Fatalf("bare bump erased Owner: %+v", got[0].Owner)
1495-
}
1496-
if got[0].Labels["app"] != "web" {
1497-
t.Fatalf("bare bump erased Labels: %+v", got[0].Labels)
1498-
}
1499-
1500-
// The inverse: a bump that carries enrichment fills a row that lacked it.
1501-
if err := store.Append(ctx, TimelineEvent{
1502-
ID: "k8s-uid-2", Timestamp: base, Source: SourceK8sEvent,
1503-
Kind: "Pod", Namespace: "default", Name: "late-enriched",
1504-
EventType: EventTypeWarning, Reason: "BackOff", Count: 1,
1505-
}); err != nil {
1506-
t.Fatalf("Append bare first: %v", err)
1507-
}
1508-
if err := store.Append(ctx, TimelineEvent{
1509-
ID: "k8s-uid-2", Timestamp: base.Add(time.Minute), Source: SourceK8sEvent,
1510-
Kind: "Pod", Namespace: "default", Name: "late-enriched",
1511-
EventType: EventTypeWarning, Reason: "BackOff", Count: 2,
1512-
Owner: &OwnerInfo{Kind: "Job", Name: "batch"},
1513-
}); err != nil {
1514-
t.Fatalf("Append enriched bump: %v", err)
1515-
}
1516-
row, err := store.GetEvent(ctx, "k8s-uid-2")
1517-
if err != nil || row == nil {
1518-
t.Fatalf("GetEvent: %v %+v", err, row)
1519-
}
1520-
if row.Owner == nil || row.Owner.Name != "batch" {
1521-
t.Fatalf("enriched bump did not fill Owner: %+v", row.Owner)
1522-
}
1523-
}
1524-
15251246
// Timestamps are stored as TEXT and ordered lexically; informer events carry
15261247
// the host's local zone while K8s events are UTC. Without UTC normalization on
15271248
// write, a local-zone wall clock sorts ahead of an earlier-instant UTC string.
@@ -1559,58 +1280,6 @@ func TestSQLiteStore_UTCTimestampOrdering(t *testing.T) {
15591280
}
15601281
}
15611282

1562-
// Delta reads must page by ascending arrival order so a burst larger than the
1563-
// page limit isn't skipped as the server advances the cursor by each page's max
1564-
// seq. Successive polls must cover every unseen event with no gaps.
1565-
func TestSQLiteStore_DeltaPagesAscendingUnderLimit(t *testing.T) {
1566-
store, cleanup := createTestSQLiteStore(t)
1567-
defer cleanup()
1568-
ctx := context.Background()
1569-
1570-
base := time.Now()
1571-
for i := 0; i < 6; i++ {
1572-
ev := TimelineEvent{
1573-
ID: fmt.Sprintf("e%d", i), Timestamp: base.Add(time.Duration(i) * time.Second),
1574-
Source: SourceInformer, Kind: "Deployment", Namespace: "default",
1575-
Name: fmt.Sprintf("e%d", i), EventType: EventTypeUpdate,
1576-
}
1577-
if err := store.Append(ctx, ev); err != nil {
1578-
t.Fatalf("Append %d: %v", i, err)
1579-
}
1580-
}
1581-
1582-
cursor := int64(1)
1583-
seen := map[int64]bool{}
1584-
for polls := 0; polls < 10; polls++ {
1585-
page, err := store.Query(ctx, QueryOptions{Limit: 2, IncludeManaged: true, SinceSeq: cursor})
1586-
if err != nil {
1587-
t.Fatalf("delta query: %v", err)
1588-
}
1589-
if len(page) == 0 {
1590-
break
1591-
}
1592-
if len(page) == 2 && page[0].Seq >= page[1].Seq {
1593-
t.Fatalf("delta page not ascending by seq: %d,%d", page[0].Seq, page[1].Seq)
1594-
}
1595-
var maxSeq int64
1596-
for _, e := range page {
1597-
if seen[e.Seq] {
1598-
t.Fatalf("seq %d returned twice across polls", e.Seq)
1599-
}
1600-
seen[e.Seq] = true
1601-
if e.Seq > maxSeq {
1602-
maxSeq = e.Seq
1603-
}
1604-
}
1605-
cursor = maxSeq
1606-
}
1607-
for want := int64(2); want <= 6; want++ {
1608-
if !seen[want] {
1609-
t.Fatalf("delta paging skipped seq %d; seen=%v", want, seen)
1610-
}
1611-
}
1612-
}
1613-
16141283
// seq must not rewind when retention empties the table: a live client's cursor
16151284
// (seq>N) would otherwise go dead with no forced resync while the store lives.
16161285
func TestSQLiteStore_SeqSurvivesEmptyTable(t *testing.T) {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package timeline
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/skyhook-io/radar/pkg/timeline/storetest"
9+
)
10+
11+
// The shared EventStore contract lives in pkg/timeline/storetest and runs here
12+
// against SQLiteStore; MemoryStore runs the same suite from the pkg module.
13+
// Only SQLite-specific behavior (migrations, quarantine, retention, timestamp
14+
// encoding) is tested in this package's own files.
15+
func TestStoreConformance_SQLite(t *testing.T) {
16+
storetest.RunConformance(t, func(t *testing.T) EventStore {
17+
t.Helper()
18+
dir, err := os.MkdirTemp("", "timeline-conformance-*")
19+
if err != nil {
20+
t.Fatalf("MkdirTemp: %v", err)
21+
}
22+
t.Cleanup(func() { os.RemoveAll(dir) })
23+
store, err := NewSQLiteStore(filepath.Join(dir, "test.db"))
24+
if err != nil {
25+
t.Fatalf("NewSQLiteStore: %v", err)
26+
}
27+
t.Cleanup(func() { store.Close() })
28+
return store
29+
})
30+
}

pkg/timeline/conformance_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package timeline_test
2+
3+
import (
4+
"testing"
5+
6+
timeline "github.com/skyhook-io/radar/pkg/timeline"
7+
"github.com/skyhook-io/radar/pkg/timeline/storetest"
8+
)
9+
10+
func TestMemoryStore_Conformance(t *testing.T) {
11+
storetest.RunConformance(t, func(t *testing.T) timeline.EventStore {
12+
return timeline.NewMemoryStore(100)
13+
})
14+
}

0 commit comments

Comments
 (0)