The controller asks no build of anybody (ADR 0501 §5, ADR 0525 clause 6, ADR 0503 §3). Every module of a walk is taken from the builder's done key; a key not done yet waits, named in the plan, and never fails it. - C2b/C2c: internal/builder and cmd/mesh-controller's asks go (askABuild, onlyTheBuildersOwn, buildOneAsked, takeIn from an outcome, buildAgentsOwn, the builder's say and its fallback, the daemon hearing an outcome). The server follows the retired seats' built subjects only to let go of them, because the installer's carried user list names them. - C2d: replay, queue, cancel, clear, kill, pause, resume, delivery-check, check-here and merge-gate are refused by name. - C3: the check queues (CHECKS_READY, CHECKS_OTHER) and their workers, the checks-running and plans-building buckets and their grants, the build queue and its hold (plans_building.go, queue.go, link/holds.go, builds_nats.go, checks_nats.go). - C4: merge_gate*.go, the pull request's repo-check path, the Checked event and its layers; a pull request asks the controller nothing but to tell mesh-config what its head declares. - C5: the builds bucket is written only by key_takein. - C10 (help and refusals): the terminal's help lists only what is served. Integration tests of the plan's tiers, sends, stops and retries now run on a stand-in builder with a done key for every module (walk_keys_scaffold_integration_test.go); the tests of the asks, the queue, the merge gate and the replays are deleted.
130 lines
5.1 KiB
Go
130 lines
5.1 KiB
Go
package link
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
)
|
|
|
|
// What a store restarting under an adoption answers with (novox/hq issues 082, 083).
|
|
var restarting = &pgconn.PgError{Code: "57P03", Message: "the database system is starting up"}
|
|
|
|
type recordsWith struct{ err error }
|
|
|
|
func (r recordsWith) Built(context.Context, BuildResult) error { return r.err }
|
|
|
|
type upgradesWith struct{ err error }
|
|
|
|
func (u upgradesWith) Upgraded(context.Context, Upgraded) error { return u.err }
|
|
func (u upgradesWith) SourceMoved(context.Context, SourceMoved) error { return u.err }
|
|
|
|
type replaysWith struct{ err error }
|
|
|
|
func (r replaysWith) Announceable(context.Context) ([]Announcement, error) { return nil, r.err }
|
|
|
|
// An upgrade announcement arriving while the store restarts is asked again; any other failure is
|
|
// acknowledged, so it cannot stop every upgrade behind it.
|
|
func TestAnUpgradeWaitsOutARestartingStoreAndNothingElse(t *testing.T) {
|
|
moved := Upgraded{Module: "gitea", Commit: "abcdef0123"}
|
|
for _, c := range []struct {
|
|
what string
|
|
err error
|
|
want func(*settled) bool
|
|
}{
|
|
{"the store away, said by the upgrader", errors.Join(ErrTryAgain, restarting), func(s *settled) bool { return s.unsettled() }},
|
|
{"a push that timed out", context.DeadlineExceeded, func(s *settled) bool { return s.acked && !s.nacked }},
|
|
{"cannot act", errors.New("anchor cannot be resolved"), func(s *settled) bool { return s.acked && !s.nacked }},
|
|
{"acted", nil, func(s *settled) bool { return s.acked && !s.nacked }},
|
|
} {
|
|
s, in := serving()
|
|
s.upgrader = upgradesWith{err: c.err}
|
|
to := &settled{}
|
|
s.act(context.Background(), in.sends(t, to, KindModuleMoved, moved))
|
|
if !c.want(to) {
|
|
t.Errorf("%s: an upgrade was settled as %+v", c.what, *to)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A catalogue's request to catch up is acknowledged after the work, and asked again while the
|
|
// store cannot be read — not lost until the catalogue next restarts.
|
|
func TestACatchUpWaitsOutARestartingStore(t *testing.T) {
|
|
for _, c := range []struct {
|
|
what string
|
|
err error
|
|
want func(*settled) bool
|
|
}{
|
|
{"restarting", restarting, func(s *settled) bool { return s.unsettled() }},
|
|
{"unreadable", errors.New("a build row is malformed"), func(s *settled) bool { return s.acked && !s.nacked }},
|
|
{"nothing to replay", nil, func(s *settled) bool { return s.acked && !s.nacked }},
|
|
} {
|
|
s, in := serving()
|
|
s.replayer = replaysWith{err: c.err}
|
|
to := &settled{}
|
|
s.act(context.Background(), in.sends(t, to, KindCatchUp, map[string]string{}))
|
|
if !c.want(to) {
|
|
t.Errorf("%s: a catch-up request was settled as %+v", c.what, *to)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A merge the controller could not record for now (the plans held by another controller, the store away) is held
|
|
// and handed over again, never taken: the bus keeps it until it is recorded (issue 921). Any other failure is an
|
|
// answer and is taken, as before.
|
|
func TestAMergeThatCouldNotBeRecordedNowIsHeldNotDropped(t *testing.T) {
|
|
merged := SourceMoved{Owner: "novox", Repo: "one", Base: "main", Commit: "abcdef0123"}
|
|
for _, c := range []struct {
|
|
what string
|
|
err error
|
|
want func(*settled) bool
|
|
}{
|
|
{"the plans held by another controller", errors.Join(ErrTryAgain, errors.New("another controller is working the plans")),
|
|
func(s *settled) bool { return s.unsettled() }},
|
|
{"the store away", errors.Join(ErrTryAgain, restarting), func(s *settled) bool { return s.unsettled() }},
|
|
{"cannot act", errors.New("no such module"), func(s *settled) bool { return s.acked && !s.nacked }},
|
|
{"kept", nil, func(s *settled) bool { return s.acked && !s.nacked }},
|
|
} {
|
|
s, in := serving()
|
|
s.upgrader = upgradesWith{err: c.err}
|
|
to := &settled{}
|
|
held := in.sends(t, to, KindSourceMoved, merged).(*fakeControl)
|
|
s.act(context.Background(), held)
|
|
if !c.want(to) {
|
|
t.Errorf("%s: a merge was settled as %+v", c.what, *to)
|
|
}
|
|
if c.err != nil && errors.Is(c.err, ErrTryAgain) && len(in.held) != 1 {
|
|
t.Errorf("%s: the merge was not held to be handed over again", c.what)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A merge being held when the controller is replaced (its context ended) or when the bus will not hold it is left
|
|
// unsettled, never taken: the bus hands it to whatever consumes next (review of issue 921).
|
|
func TestAMergeIsLeftUnsettledWhenTheControllerEndsOrTheHoldFails(t *testing.T) {
|
|
merged := SourceMoved{Owner: "novox", Repo: "one", Base: "main", Commit: "abcdef0123"}
|
|
try := errors.Join(ErrTryAgain, errors.New("another controller is working the plans"))
|
|
|
|
// The plans' hold returns a bare context.Canceled when the controller is replaced, not ErrTryAgain.
|
|
s, in := serving()
|
|
s.upgrader = upgradesWith{err: context.Canceled}
|
|
to := &settled{}
|
|
ended, end := context.WithCancel(context.Background())
|
|
end()
|
|
s.act(ended, in.sends(t, to, KindSourceMoved, merged))
|
|
if !to.unsettled() {
|
|
t.Errorf("a merge was settled as %+v when the controller's context ended", *to)
|
|
}
|
|
|
|
s, in = serving()
|
|
s.upgrader = upgradesWith{err: try}
|
|
to = &settled{}
|
|
m := in.sends(t, to, KindSourceMoved, merged).(*fakeControl)
|
|
m.holdFails = errors.New("the bus will not hold it")
|
|
s.act(context.Background(), m)
|
|
if !to.unsettled() {
|
|
t.Errorf("a merge was settled as %+v when it could not be held", *to)
|
|
}
|
|
}
|