feat: runtime manager
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user