mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-15 04:53:00 +08:00
419 lines
11 KiB
Go
419 lines
11 KiB
Go
package application
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/matthewlu070111/smart-srun/core/internal/domain"
|
|
"github.com/matthewlu070111/smart-srun/core/internal/policy/faketime"
|
|
)
|
|
|
|
// epoch is where the fake clock starts. Every duration in these tests is
|
|
// relative to it, and none of them is a real one: the coordinator's deadlines
|
|
// are measured against the injected clock precisely so a six-hundred-second
|
|
// budget can be reached in a microsecond.
|
|
var epoch = time.Date(2026, 3, 5, 12, 0, 0, 0, time.UTC)
|
|
|
|
// patience bounds the waits for a real goroutine handoff. It is a failure
|
|
// timeout, not a delay: nothing in these tests waits for it on the happy path.
|
|
const patience = 5 * time.Second
|
|
|
|
// step is one instruction to a worker: report a phase, or finish.
|
|
type step struct {
|
|
phase Phase
|
|
outcome Outcome
|
|
}
|
|
|
|
// harness runs a coordinator whose clock and worker the test drives by hand.
|
|
type harness struct {
|
|
t *testing.T
|
|
clock *faketime.Clock
|
|
*Coordinator
|
|
|
|
// entered receives every action a worker started on.
|
|
entered chan Action
|
|
// sawCancel receives the id of every action whose worker observed its
|
|
// context being cancelled.
|
|
sawCancel chan string
|
|
|
|
mu sync.Mutex
|
|
steps map[string]chan step
|
|
linger map[string]bool
|
|
// deaf accounts get a worker that ignores cancellation entirely, which is
|
|
// how the stop path's own escape hatch gets exercised. Keyed by account
|
|
// rather than action id because the worker consults it the instant it
|
|
// starts, which can be before the test has seen the receipt.
|
|
deaf map[string]bool
|
|
seen []Action
|
|
notify chan struct{}
|
|
// teardown releases deaf workers when the test is over.
|
|
teardown chan struct{}
|
|
|
|
stop context.CancelFunc
|
|
// stopped is closed when Run returns, so several waiters -- a test and the
|
|
// cleanup behind it -- can each see it.
|
|
stopped chan struct{}
|
|
runErr error
|
|
}
|
|
|
|
func newHarness(t *testing.T, configure func(*Options)) *harness {
|
|
t.Helper()
|
|
|
|
h := &harness{
|
|
t: t,
|
|
clock: faketime.New(epoch),
|
|
entered: make(chan Action, 256),
|
|
sawCancel: make(chan string, 256),
|
|
steps: map[string]chan step{},
|
|
linger: map[string]bool{},
|
|
deaf: map[string]bool{},
|
|
notify: make(chan struct{}, 1),
|
|
stopped: make(chan struct{}),
|
|
teardown: make(chan struct{}),
|
|
}
|
|
t.Cleanup(func() { close(h.teardown) })
|
|
|
|
options := Options{
|
|
Clock: h.clock,
|
|
Runner: h,
|
|
Lines: func(request Request) string { return request.AccountID },
|
|
Observer: h.record,
|
|
}
|
|
if configure != nil {
|
|
configure(&options)
|
|
}
|
|
h.Coordinator = New(options)
|
|
|
|
ctx, stop := context.WithCancel(context.Background())
|
|
h.stop = stop
|
|
go func() {
|
|
h.runErr = h.Coordinator.Run(ctx)
|
|
close(h.stopped)
|
|
}()
|
|
t.Cleanup(h.shutdown)
|
|
|
|
// One round-trip before handing the harness over, so every test can assume
|
|
// the loop is already running. Without it a test that calls Run itself
|
|
// races the goroutine above for the right to be the loop.
|
|
h.list()
|
|
return h
|
|
}
|
|
|
|
// record is the Observer. It must not block the coordinator's loop.
|
|
func (h *harness) record(action Action) {
|
|
h.mu.Lock()
|
|
h.seen = append(h.seen, action)
|
|
h.mu.Unlock()
|
|
select {
|
|
case h.notify <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
// Run is the Runner. A worker waits for the test to tell it what to do.
|
|
func (h *harness) Run(ctx context.Context, action Action, report func(Phase)) Outcome {
|
|
h.entered <- action
|
|
steps := h.stepsFor(action.ID)
|
|
|
|
if h.isDeaf(action.Request.AccountID) {
|
|
// A worker that never notices its context. Nothing but the end of the
|
|
// test releases it.
|
|
select {
|
|
case next := <-steps:
|
|
return next.outcome
|
|
case <-h.teardown:
|
|
return Outcome{State: StateFailed, Code: domain.CodeInternal,
|
|
Message: "the test ended"}
|
|
}
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case next := <-steps:
|
|
if next.phase != "" {
|
|
report(next.phase)
|
|
continue
|
|
}
|
|
return next.outcome
|
|
case <-ctx.Done():
|
|
h.sawCancel <- action.ID
|
|
if !h.lingersOn(action.ID) {
|
|
return Outcome{State: StateFailed, Code: domain.CodeCancelled,
|
|
Message: "worker stopped when it was cancelled"}
|
|
}
|
|
// Keep going, so a test can show that the line is still held while
|
|
// a cancelled worker is unwinding.
|
|
select {
|
|
case next := <-steps:
|
|
return next.outcome
|
|
case <-time.After(patience):
|
|
return Outcome{State: StateFailed, Code: domain.CodeInternal,
|
|
Message: "the test never released a lingering worker"}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *harness) stepsFor(actionID string) chan step {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
channel, known := h.steps[actionID]
|
|
if !known {
|
|
channel = make(chan step, 8)
|
|
h.steps[actionID] = channel
|
|
}
|
|
return channel
|
|
}
|
|
|
|
func (h *harness) lingersOn(actionID string) bool {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
return h.linger[actionID]
|
|
}
|
|
|
|
// lingerOn makes the worker for the next action on this account keep running
|
|
// after it is cancelled, until the test finishes it.
|
|
func (h *harness) lingerOn(actionID string) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.linger[actionID] = true
|
|
}
|
|
|
|
func (h *harness) isDeaf(accountID string) bool {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
return h.deaf[accountID]
|
|
}
|
|
|
|
// deafOn makes this account's workers ignore cancellation. Call it before
|
|
// submitting: the worker reads it as soon as it starts.
|
|
func (h *harness) deafOn(accountID string) {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
h.deaf[accountID] = true
|
|
}
|
|
|
|
func (h *harness) callContext() (context.Context, context.CancelFunc) {
|
|
return context.WithTimeout(context.Background(), patience)
|
|
}
|
|
|
|
func (h *harness) submit(kind Kind, account, key string) Receipt {
|
|
h.t.Helper()
|
|
ctx, cancel := h.callContext()
|
|
defer cancel()
|
|
receipt, err := h.Submit(ctx, Request{Kind: kind, AccountID: account,
|
|
IdempotencyKey: key})
|
|
if err != nil {
|
|
h.t.Fatalf("submit %s/%s: %v", kind, account, err)
|
|
}
|
|
return receipt
|
|
}
|
|
|
|
func (h *harness) submitExpectingError(request Request) error {
|
|
h.t.Helper()
|
|
ctx, cancel := h.callContext()
|
|
defer cancel()
|
|
_, err := h.Submit(ctx, request)
|
|
if err == nil {
|
|
h.t.Fatalf("%+v was accepted", request)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (h *harness) cancel(actionID string) {
|
|
h.t.Helper()
|
|
ctx, cancel := h.callContext()
|
|
defer cancel()
|
|
if err := h.Cancel(ctx, actionID); err != nil {
|
|
h.t.Fatalf("cancel %s: %v", actionID, err)
|
|
}
|
|
}
|
|
|
|
func (h *harness) state(actionID string) Action {
|
|
h.t.Helper()
|
|
ctx, cancel := h.callContext()
|
|
defer cancel()
|
|
action, err := h.Action(ctx, actionID)
|
|
if err != nil {
|
|
h.t.Fatalf("read %s: %v", actionID, err)
|
|
}
|
|
return action
|
|
}
|
|
|
|
func (h *harness) list() []Action {
|
|
h.t.Helper()
|
|
ctx, cancel := h.callContext()
|
|
defer cancel()
|
|
all, err := h.Actions(ctx)
|
|
if err != nil {
|
|
h.t.Fatalf("list: %v", err)
|
|
}
|
|
return all
|
|
}
|
|
|
|
// settle drives the loop through a full iteration, so a deadline the clock has
|
|
// just passed has certainly been applied.
|
|
//
|
|
// Two round-trips, not one. The first may be answered inside the iteration that
|
|
// was already in progress when the clock moved, and expiry runs at the top of
|
|
// the loop -- so only between two consecutive round-trips is there guaranteed
|
|
// to be an expiry pass at the current time. This is why none of these tests
|
|
// needs a sleep to "let things happen".
|
|
func (h *harness) settle() {
|
|
h.t.Helper()
|
|
for range 2 {
|
|
h.list()
|
|
}
|
|
}
|
|
|
|
// finish releases a worker with a result.
|
|
func (h *harness) finish(actionID string, outcome Outcome) {
|
|
h.t.Helper()
|
|
select {
|
|
case h.stepsFor(actionID) <- step{outcome: outcome}:
|
|
case <-time.After(patience):
|
|
h.t.Fatalf("no worker was waiting for %s", actionID)
|
|
}
|
|
}
|
|
|
|
func (h *harness) succeed(actionID string) {
|
|
h.t.Helper()
|
|
h.finish(actionID, Outcome{State: StateSucceeded, Message: "认证完成"})
|
|
}
|
|
|
|
// reportPhase makes a worker announce progress without finishing.
|
|
func (h *harness) reportPhase(actionID string, phase Phase) {
|
|
h.t.Helper()
|
|
select {
|
|
case h.stepsFor(actionID) <- step{phase: phase}:
|
|
case <-time.After(patience):
|
|
h.t.Fatalf("no worker was waiting for %s", actionID)
|
|
}
|
|
}
|
|
|
|
// awaitStart waits for a worker to begin.
|
|
func (h *harness) awaitStart() Action {
|
|
h.t.Helper()
|
|
select {
|
|
case action := <-h.entered:
|
|
return action
|
|
case <-time.After(patience):
|
|
h.t.Fatal("no worker started")
|
|
return Action{}
|
|
}
|
|
}
|
|
|
|
// expectNoStart asserts that nothing began. It settles first, because dispatch
|
|
// happens at the top of the loop and an assertion made before the loop has run
|
|
// would pass for the wrong reason.
|
|
func (h *harness) expectNoStart(why string) {
|
|
h.t.Helper()
|
|
h.settle()
|
|
select {
|
|
case action := <-h.entered:
|
|
h.t.Fatalf("%s: %s started anyway on line %q", why, action.ID, action.Line)
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (h *harness) awaitCancelSeen(actionID string) {
|
|
h.t.Helper()
|
|
for {
|
|
select {
|
|
case seen := <-h.sawCancel:
|
|
if seen == actionID {
|
|
return
|
|
}
|
|
case <-time.After(patience):
|
|
h.t.Fatalf("the worker for %s never saw its cancellation", actionID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// awaitState waits for an action to be published in a given state.
|
|
func (h *harness) awaitState(actionID string, want State) Action {
|
|
h.t.Helper()
|
|
deadline := time.After(patience)
|
|
for {
|
|
h.mu.Lock()
|
|
for _, event := range h.seen {
|
|
if event.ID == actionID && event.State == want {
|
|
h.mu.Unlock()
|
|
return event
|
|
}
|
|
}
|
|
h.mu.Unlock()
|
|
|
|
select {
|
|
case <-h.notify:
|
|
case <-deadline:
|
|
h.t.Fatalf("%s never reached %s; it is %s",
|
|
actionID, want, h.state(actionID).State)
|
|
return Action{}
|
|
}
|
|
}
|
|
}
|
|
|
|
// recorded returns every state this action was published in, in order. It is
|
|
// the only way to check an action after Run has returned, and it is also how a
|
|
// test asks "did it ever pass through X" rather than only "where is it now".
|
|
func (h *harness) recorded(actionID string) []State {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
var states []State
|
|
for _, event := range h.seen {
|
|
if event.ID != actionID {
|
|
continue
|
|
}
|
|
if len(states) == 0 || states[len(states)-1] != event.State {
|
|
states = append(states, event.State)
|
|
}
|
|
}
|
|
return states
|
|
}
|
|
|
|
// awaitPhase waits for a phase to be published for a running action.
|
|
func (h *harness) awaitPhase(actionID string, want Phase) {
|
|
h.t.Helper()
|
|
deadline := time.After(patience)
|
|
for {
|
|
h.mu.Lock()
|
|
for _, event := range h.seen {
|
|
if event.ID == actionID && event.Phase == want {
|
|
h.mu.Unlock()
|
|
return
|
|
}
|
|
}
|
|
h.mu.Unlock()
|
|
|
|
select {
|
|
case <-h.notify:
|
|
case <-deadline:
|
|
h.t.Fatalf("%s never reported phase %q", actionID, want)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// shutdown stops the coordinator and waits for the loop to return. It is safe
|
|
// to call after a test has already stopped it.
|
|
func (h *harness) shutdown() {
|
|
h.stop()
|
|
select {
|
|
case <-h.stopped:
|
|
case <-time.After(patience):
|
|
h.t.Error("Run did not return after its context was cancelled")
|
|
}
|
|
}
|
|
|
|
func codeOf(t *testing.T, err error) domain.ErrorCode {
|
|
t.Helper()
|
|
code, ok := domain.CodeOf(err)
|
|
if !ok {
|
|
t.Fatalf("error carries no code: %v", err)
|
|
}
|
|
return code
|
|
}
|