//go:build linux package integration_test import ( "context" "errors" "os" "os/exec" "path/filepath" "testing" "yms-daemon/internal/processlock" "yms-daemon/internal/transaction" ) // 锁辅助进程通过环境变量传递模式与锁路径,用于在独立进程中验证 flock 的互斥语义。 const ( // lockHelperModeEnvironment 传递锁辅助进程运行模式的环境变量名。 lockHelperModeEnvironment = "YMS_DAEMON_INTEGRATION_LOCK_HELPER_MODE" // lockHelperPathEnvironment 传递锁文件路径的环境变量名。 lockHelperPathEnvironment = "YMS_DAEMON_INTEGRATION_LOCK_HELPER_PATH" // lockHelperExpectBlocked 表示期望锁辅助进程因锁被占用而加锁失败。 lockHelperExpectBlocked = "EXPECT_BLOCKED" // lockHelperExpectAcquired 表示期望锁辅助进程在锁释放后成功加锁。 lockHelperExpectAcquired = "EXPECT_ACQUIRED" ) // TestTransactionKernelIntegration 使用真实子进程、flock 和 SQLite 文件验证事务底座。 // 测试期间父进程始终持有 serve 锁,完整事务提交并重开数据库后才释放锁。 func TestTransactionKernelIntegration(t *testing.T) { ctx := context.Background() root := t.TempDir() lockPath := filepath.Join(root, "serve.lock") databasePath := filepath.Join(root, "transaction.db") serveLock, err := processlock.Acquire(lockPath) if err != nil { t.Fatalf("acquire serve lock: %v", err) } t.Cleanup(func() { if err := serveLock.Close(); err != nil { t.Errorf("release serve lock during cleanup: %v", err) } }) // 同进程重复加锁不足以证明客户服务器上的进程互斥,因此必须启动独立进程核对。 runLockHelper(t, lockHelperExpectBlocked, lockPath) store, err := transaction.OpenStore(ctx, databasePath) if err != nil { t.Fatalf("open transaction store: %v", err) } record, created, err := store.CreateTransaction(ctx, transaction.CreateRequest{ ID: "integration-transaction", IdempotencyKey: "integration-request", Source: "integration-test", Service: "all", }) if err != nil || !created { t.Fatalf("create transaction: record=%+v created=%v err=%v", record, created, err) } transitions := []transaction.State{ transaction.StateValidating, transaction.StatePrepared, transaction.StateStarting, transaction.StateSwitching, transaction.StateVerifying, transaction.StateDraining, transaction.StateCommitted, } for index, next := range transitions { record, err = store.Transition(ctx, record.ID, next, "integration transition") if err != nil { t.Fatalf("transition to %s: %v", next, err) } expectedVersion := int64(index + 2) if record.State != next || record.Version != expectedVersion { t.Fatalf("unexpected transition result: state=%s version=%d, want state=%s version=%d", record.State, record.Version, next, expectedVersion) } } if err := store.Close(); err != nil { t.Fatalf("close transaction store: %v", err) } // 重新打开同一文件,证明终态和事件不是内存中的临时结果。 reopened, err := transaction.OpenStore(ctx, databasePath) if err != nil { t.Fatalf("reopen transaction store: %v", err) } t.Cleanup(func() { if err := reopened.Close(); err != nil { t.Errorf("close reopened transaction store: %v", err) } }) persisted, err := reopened.Transaction(ctx, record.ID) if err != nil { t.Fatalf("read committed transaction: %v", err) } if persisted.State != transaction.StateCommitted || persisted.Version != 8 { t.Fatalf("unexpected committed transaction: %+v", persisted) } events, err := reopened.EventsAfter(ctx, record.ID, 0, 100) if err != nil { t.Fatalf("read transaction events: %v", err) } assertStateEvents(t, events, transitions) if err := serveLock.Close(); err != nil { t.Fatalf("release serve lock: %v", err) } runLockHelper(t, lockHelperExpectAcquired, lockPath) } // TestTransactionKernelLockHelper 只由 TestTransactionKernelIntegration 启动。 // 独立测试进程让 flock 的互斥语义得到端到端验证。 func TestTransactionKernelLockHelper(t *testing.T) { mode, enabled := os.LookupEnv(lockHelperModeEnvironment) if !enabled { t.Skip("lock helper process is not enabled") } lockPath, present := os.LookupEnv(lockHelperPathEnvironment) if !present || lockPath == "" { t.Fatal("lock helper path is missing") } lock, err := processlock.Acquire(lockPath) switch mode { case lockHelperExpectBlocked: if lock != nil { _ = lock.Close() } if !errors.Is(err, processlock.ErrAlreadyLocked) { t.Fatalf("expected kernel lock contention, lock=%v err=%v", lock, err) } case lockHelperExpectAcquired: if err != nil || lock == nil { t.Fatalf("expected released kernel lock to be acquired, lock=%v err=%v", lock, err) } if err := lock.Close(); err != nil { t.Fatalf("release helper lock: %v", err) } default: t.Fatalf("unknown lock helper mode: %q", mode) } } // runLockHelper 启动一个独立的锁辅助测试进程,通过环境变量传入模式与锁路径, // 并校验该进程按预期成功退出。 func runLockHelper(t *testing.T, mode, lockPath string) { t.Helper() command := exec.Command(os.Args[0], "-test.run=^TestTransactionKernelLockHelper$", "-test.v") command.Env = []string{ lockHelperModeEnvironment + "=" + mode, lockHelperPathEnvironment + "=" + lockPath, } output, err := command.CombinedOutput() if err != nil { t.Fatalf("run lock helper in mode %s: %v\n%s", mode, err, output) } t.Logf("lock helper mode %s passed", mode) } // assertStateEvents 校验事务事件流与给定的状态迁移序列一致:事件数量应为迁移数加一, // 首条为事务创建事件,其后每条状态变更事件的前后状态与序列号递增关系均需匹配,最终状态为 Committed。 func assertStateEvents(t *testing.T, events []transaction.Event, transitions []transaction.State) { t.Helper() if len(events) != len(transitions)+1 { t.Fatalf("unexpected event count: got %d, want %d", len(events), len(transitions)+1) } if events[0].Kind != "TRANSACTION_CREATED" || events[0].ToState != transaction.StateCreated { t.Fatalf("unexpected transaction creation event: %+v", events[0]) } previous := transaction.StateCreated for index, next := range transitions { event := events[index+1] if event.Kind != "TRANSACTION_STATE_CHANGED" || event.FromState != previous || event.ToState != next { t.Fatalf("unexpected state event at index %d: %+v", index+1, event) } if event.Sequence <= events[index].Sequence { t.Fatalf("event sequence is not increasing at index %d: previous=%d current=%d", index+1, events[index].Sequence, event.Sequence) } previous = next } if previous != transaction.StateCommitted { t.Fatalf("state flow ended at %s", previous) } }