129 lines
4.3 KiB
Go
129 lines
4.3 KiB
Go
package harness
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"galaxy/rtmanager/internal/adapters/postgres/healthsnapshotstore"
|
|
"galaxy/rtmanager/internal/adapters/postgres/operationlogstore"
|
|
"galaxy/rtmanager/internal/adapters/postgres/runtimerecordstore"
|
|
"galaxy/rtmanager/internal/domain/health"
|
|
"galaxy/rtmanager/internal/domain/operation"
|
|
"galaxy/rtmanager/internal/domain/runtime"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// RuntimeRecord returns the persisted runtime record for gameID. The
|
|
// helper opens the store on every call (cheap; the harness `*sql.DB`
|
|
// is shared) so individual scenarios stay isolated even if a previous
|
|
// test mutated store state.
|
|
func RuntimeRecord(t testing.TB, env *Env, gameID string) (runtime.RuntimeRecord, error) {
|
|
t.Helper()
|
|
store, err := runtimerecordstore.New(runtimerecordstore.Config{
|
|
DB: env.Postgres.Pool(),
|
|
OperationTimeout: pgOperationTimeout,
|
|
})
|
|
require.NoError(t, err)
|
|
return store.Get(context.Background(), gameID)
|
|
}
|
|
|
|
// MustRuntimeRecord asserts that the record exists and returns it.
|
|
func MustRuntimeRecord(t testing.TB, env *Env, gameID string) runtime.RuntimeRecord {
|
|
t.Helper()
|
|
record, err := RuntimeRecord(t, env, gameID)
|
|
require.NoErrorf(t, err, "load runtime record for %s", gameID)
|
|
return record
|
|
}
|
|
|
|
// EventuallyRuntimeRecord polls until predicate matches the runtime
|
|
// record for gameID, or the deadline fires. Returns the matching
|
|
// record. Used by lifecycle assertions that depend on async state
|
|
// transitions (start consumer → record).
|
|
func EventuallyRuntimeRecord(t testing.TB, env *Env, gameID string, predicate func(runtime.RuntimeRecord) bool, timeout time.Duration) runtime.RuntimeRecord {
|
|
t.Helper()
|
|
if timeout <= 0 {
|
|
timeout = defaultStreamTimeout
|
|
}
|
|
deadline := time.Now().Add(timeout)
|
|
for {
|
|
record, err := RuntimeRecord(t, env, gameID)
|
|
if err == nil && predicate(record) {
|
|
return record
|
|
}
|
|
if err != nil && !errors.Is(err, runtime.ErrNotFound) {
|
|
t.Fatalf("rtmanager integration: load runtime record: %v", err)
|
|
}
|
|
if time.Now().After(deadline) {
|
|
if err != nil {
|
|
t.Fatalf("rtmanager integration: runtime record predicate not met within %s; last err=%v",
|
|
timeout, err)
|
|
}
|
|
t.Fatalf("rtmanager integration: runtime record predicate not met within %s; last record=%+v",
|
|
timeout, record)
|
|
}
|
|
time.Sleep(defaultStreamPoll)
|
|
}
|
|
}
|
|
|
|
// OperationEntries returns up to `limit` most-recent operation_log
|
|
// entries for gameID, ordered descending by started_at.
|
|
func OperationEntries(t testing.TB, env *Env, gameID string, limit int) []operation.OperationEntry {
|
|
t.Helper()
|
|
store, err := operationlogstore.New(operationlogstore.Config{
|
|
DB: env.Postgres.Pool(),
|
|
OperationTimeout: pgOperationTimeout,
|
|
})
|
|
require.NoError(t, err)
|
|
entries, err := store.ListByGame(context.Background(), gameID, limit)
|
|
require.NoErrorf(t, err, "list operation log entries for %s", gameID)
|
|
return entries
|
|
}
|
|
|
|
// EventuallyOperationKind polls operation_log until at least one entry
|
|
// for gameID has the requested kind, or the deadline fires. Returns
|
|
// the matching entry.
|
|
func EventuallyOperationKind(t testing.TB, env *Env, gameID string, kind operation.OpKind, timeout time.Duration) operation.OperationEntry {
|
|
t.Helper()
|
|
if timeout <= 0 {
|
|
timeout = defaultStreamTimeout
|
|
}
|
|
deadline := time.Now().Add(timeout)
|
|
for {
|
|
entries := OperationEntries(t, env, gameID, 50)
|
|
for _, entry := range entries {
|
|
if entry.OpKind == kind {
|
|
return entry
|
|
}
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("rtmanager integration: operation_log entry with op_kind=%s not seen within %s; observed=%v",
|
|
kind, timeout, opKindSummary(entries))
|
|
}
|
|
time.Sleep(defaultStreamPoll)
|
|
}
|
|
}
|
|
|
|
// HealthSnapshot returns the latest persisted health snapshot for
|
|
// gameID, or the underlying not-found sentinel when nothing has been
|
|
// recorded yet.
|
|
func HealthSnapshot(t testing.TB, env *Env, gameID string) (health.HealthSnapshot, error) {
|
|
t.Helper()
|
|
store, err := healthsnapshotstore.New(healthsnapshotstore.Config{
|
|
DB: env.Postgres.Pool(),
|
|
OperationTimeout: pgOperationTimeout,
|
|
})
|
|
require.NoError(t, err)
|
|
return store.Get(context.Background(), gameID)
|
|
}
|
|
|
|
func opKindSummary(entries []operation.OperationEntry) []string {
|
|
out := make([]string, 0, len(entries))
|
|
for _, entry := range entries {
|
|
out = append(out, string(entry.OpKind)+"/"+string(entry.Outcome))
|
|
}
|
|
return out
|
|
}
|