From 391f622c7543b2bd939c9af30203b43281e7996c Mon Sep 17 00:00:00 2001 From: naiba Date: Mon, 20 Jul 2026 04:37:59 +0000 Subject: [PATCH] test(agentcompat): cover SQLite hold arbitration Co-authored-by: naiba/CloudCode --- ...ite_hold_tracker_agentcompat_linux_test.go | 289 ++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 service/singleton/sqlite_hold_tracker_agentcompat_linux_test.go diff --git a/service/singleton/sqlite_hold_tracker_agentcompat_linux_test.go b/service/singleton/sqlite_hold_tracker_agentcompat_linux_test.go new file mode 100644 index 00000000..8050ed7e --- /dev/null +++ b/service/singleton/sqlite_hold_tracker_agentcompat_linux_test.go @@ -0,0 +1,289 @@ +//go:build agentcompat && linux + +package singleton + +import ( + "errors" + "testing" +) + +var sqliteHoldTestJournal = SQLiteJournalIdentity{ + MountID: 2, DeviceMajor: 8, DeviceMinor: 1, Inode: 13, BirthSeconds: 10, BirthNanoseconds: 20, +} + +func sqliteHoldTestTransaction(id SQLiteTransactionIdentity) SQLiteTransaction { + return SQLiteTransaction{Connection: SQLiteConnectionIdentity(3), Identity: id} +} + +func recordSQLiteHoldTestUpdate(t *testing.T, tracker *SQLiteHoldTracker, transaction SQLiteTransaction) { + t.Helper() + if err := tracker.RecordSQLiteExecution(transaction, SQLiteExecutionOrigin{ + Operation: SQLiteOperationUpdate, Table: "settings", StackHash: 21, FirstNezhaFrame: "singleton.test", + }); err != nil { + t.Fatal(err) + } + if err := tracker.RecordSQLiteUpdate(transaction, SQLiteUpdateObservation{ + Operation: SQLiteOperationUpdate, Table: "settings", Journal: sqliteHoldTestJournal, + }); err != nil { + t.Fatal(err) + } +} + +func TestSQLiteHoldTrackerSelectsFutureCandidateAfterZeroCandidateArm(t *testing.T) { + // Given + tracker := NewSQLiteHoldTracker() + session, err := tracker.ArmSQLiteHold(sqliteHoldTestJournal) + if err != nil { + t.Fatal(err) + } + transaction := sqliteHoldTestTransaction(1) + if err := tracker.BeginSQLiteTransaction(transaction); err != nil { + t.Fatal(err) + } + + // When + recordSQLiteHoldTestUpdate(t, tracker, transaction) + finalization, err := tracker.BeginSQLiteFinalization(transaction) + + // Then + if err != nil { + t.Fatal(err) + } + if err := tracker.ReleaseSQLiteHold(session); err != nil { + t.Fatal(err) + } + if err := finalization.Wait(); err != nil { + t.Fatal(err) + } +} + +func TestSQLiteHoldTrackerSelectsSingleActiveCandidate(t *testing.T) { + // Given + tracker := NewSQLiteHoldTracker() + transaction := sqliteHoldTestTransaction(2) + if err := tracker.BeginSQLiteTransaction(transaction); err != nil { + t.Fatal(err) + } + recordSQLiteHoldTestUpdate(t, tracker, transaction) + + // When + session, err := tracker.ArmSQLiteHold(sqliteHoldTestJournal) + finalization, finalizationErr := tracker.BeginSQLiteFinalization(transaction) + + // Then + if err != nil || finalizationErr != nil { + t.Fatalf("arm=%v finalization=%v", err, finalizationErr) + } + if err := tracker.ReleaseSQLiteHold(session); err != nil { + t.Fatal(err) + } + if err := finalization.Wait(); err != nil { + t.Fatal(err) + } +} + +func TestSQLiteHoldTrackerAbortsDuplicateCandidates(t *testing.T) { + // Given + tracker := NewSQLiteHoldTracker() + first := sqliteHoldTestTransaction(3) + second := sqliteHoldTestTransaction(4) + for _, transaction := range []SQLiteTransaction{first, second} { + if err := tracker.BeginSQLiteTransaction(transaction); err != nil { + t.Fatal(err) + } + recordSQLiteHoldTestUpdate(t, tracker, transaction) + } + + // When + _, err := tracker.ArmSQLiteHold(sqliteHoldTestJournal) + + // Then + if !errors.Is(err, ErrSQLiteHoldAmbiguousCandidate) { + t.Fatalf("expected ambiguous candidate error, got %v", err) + } + if _, err := tracker.BeginSQLiteFinalization(first); !errors.Is(err, ErrSQLiteHoldNotSelected) { + t.Fatalf("expected no selected transaction, got %v", err) + } +} + +func TestSQLiteHoldTrackerStaleReleaseCannotReleaseNewSession(t *testing.T) { + // Given + tracker := NewSQLiteHoldTracker() + first := sqliteHoldTestTransaction(5) + if err := tracker.BeginSQLiteTransaction(first); err != nil { + t.Fatal(err) + } + recordSQLiteHoldTestUpdate(t, tracker, first) + staleSession, err := tracker.ArmSQLiteHold(sqliteHoldTestJournal) + if err != nil { + t.Fatal(err) + } + if err := tracker.AbortSQLiteHold(staleSession); err != nil { + t.Fatal(err) + } + if err := tracker.FinishSQLiteTransaction(first); err != nil { + t.Fatal(err) + } + second := sqliteHoldTestTransaction(6) + if err := tracker.BeginSQLiteTransaction(second); err != nil { + t.Fatal(err) + } + recordSQLiteHoldTestUpdate(t, tracker, second) + session, err := tracker.ArmSQLiteHold(sqliteHoldTestJournal) + if err != nil { + t.Fatal(err) + } + finalization, err := tracker.BeginSQLiteFinalization(second) + if err != nil { + t.Fatal(err) + } + + // When + err = tracker.ReleaseSQLiteHold(staleSession) + + // Then + if !errors.Is(err, ErrSQLiteHoldStaleSession) { + t.Fatalf("expected stale session error, got %v", err) + } + if finalization.Released() { + t.Fatal("stale release released the new session") + } + if err := tracker.ReleaseSQLiteHold(session); err != nil { + t.Fatal(err) + } + if err := finalization.Wait(); err != nil { + t.Fatal(err) + } +} + +func TestSQLiteHoldTrackerLinearizesArmBeforeFinalization(t *testing.T) { + // Given + tracker := NewSQLiteHoldTracker() + transaction := sqliteHoldTestTransaction(7) + if err := tracker.BeginSQLiteTransaction(transaction); err != nil { + t.Fatal(err) + } + recordSQLiteHoldTestUpdate(t, tracker, transaction) + session, err := tracker.ArmSQLiteHold(sqliteHoldTestJournal) + if err != nil { + t.Fatal(err) + } + + // When + finalization, err := tracker.BeginSQLiteFinalization(transaction) + + // Then + if err != nil { + t.Fatal(err) + } + if finalization.Released() { + t.Fatal("finalization released before its session release") + } + if err := tracker.ReleaseSQLiteHold(session); err != nil { + t.Fatal(err) + } + if err := finalization.Wait(); err != nil { + t.Fatal(err) + } +} + +func TestSQLiteHoldTrackerRollbackNeverWaits(t *testing.T) { + // Given + tracker := NewSQLiteHoldTracker() + transaction := sqliteHoldTestTransaction(8) + if err := tracker.BeginSQLiteTransaction(transaction); err != nil { + t.Fatal(err) + } + recordSQLiteHoldTestUpdate(t, tracker, transaction) + session, err := tracker.ArmSQLiteHold(sqliteHoldTestJournal) + if err != nil { + t.Fatal(err) + } + finalization, err := tracker.BeginSQLiteFinalization(transaction) + if err != nil { + t.Fatal(err) + } + + // When + err = tracker.FinishSQLiteTransaction(transaction) + + // Then + if err != nil { + t.Fatal(err) + } + if err := finalization.Wait(); !errors.Is(err, ErrSQLiteHoldAborted) { + t.Fatalf("expected aborted finalization, got %v", err) + } + if err := tracker.ReleaseSQLiteHold(session); !errors.Is(err, ErrSQLiteHoldStaleSession) { + t.Fatalf("expected stale release, got %v", err) + } +} + +func TestSQLiteHoldTrackerAbortUnblocksSelectedFinalization(t *testing.T) { + // Given + tracker := NewSQLiteHoldTracker() + transaction := sqliteHoldTestTransaction(9) + if err := tracker.BeginSQLiteTransaction(transaction); err != nil { + t.Fatal(err) + } + recordSQLiteHoldTestUpdate(t, tracker, transaction) + session, err := tracker.ArmSQLiteHold(sqliteHoldTestJournal) + if err != nil { + t.Fatal(err) + } + finalization, err := tracker.BeginSQLiteFinalization(transaction) + if err != nil { + t.Fatal(err) + } + + // When + err = tracker.AbortSQLiteHold(session) + + // Then + if err != nil { + t.Fatal(err) + } + if err := finalization.Wait(); !errors.Is(err, ErrSQLiteHoldAborted) { + t.Fatalf("expected aborted finalization, got %v", err) + } +} + +func TestSQLiteHoldTrackerCleansUpAfterReleasedFinalization(t *testing.T) { + // Given + tracker := NewSQLiteHoldTracker() + first := sqliteHoldTestTransaction(10) + if err := tracker.BeginSQLiteTransaction(first); err != nil { + t.Fatal(err) + } + recordSQLiteHoldTestUpdate(t, tracker, first) + session, err := tracker.ArmSQLiteHold(sqliteHoldTestJournal) + if err != nil { + t.Fatal(err) + } + finalization, err := tracker.BeginSQLiteFinalization(first) + if err != nil { + t.Fatal(err) + } + if err := tracker.ReleaseSQLiteHold(session); err != nil { + t.Fatal(err) + } + if err := finalization.Wait(); err != nil { + t.Fatal(err) + } + if err := tracker.FinishSQLiteTransaction(first); err != nil { + t.Fatal(err) + } + second := sqliteHoldTestTransaction(11) + if err := tracker.BeginSQLiteTransaction(second); err != nil { + t.Fatal(err) + } + recordSQLiteHoldTestUpdate(t, tracker, second) + + // When + _, err = tracker.ArmSQLiteHold(sqliteHoldTestJournal) + + // Then + if err != nil { + t.Fatal(err) + } +}