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 } // A build result the store could not take right now is handed back; one it refused is rejected, // as before; one it kept is acknowledged. func TestABuildResultWaitsOutARestartingStore(t *testing.T) { built := BuildResult{On: "anchor", Repository: "/r", Commit: "abc"} for _, c := range []struct { what string err error want func(*settled) bool }{ {"restarting", restarting, func(s *settled) bool { return s.unsettled() }}, {"refused", errors.New("no such module"), func(s *settled) bool { return s.rejected && !s.nacked }}, {"kept", nil, func(s *settled) bool { return s.acked && !s.nacked }}, } { s, in := serving() s.recorder = recordsWith{err: c.err} to := &settled{} s.act(context.Background(), in.sends(t, to, KindBuilt, built)) if !c.want(to) { t.Errorf("%s: a build result was settled as %+v", c.what, *to) } } } // 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) } } } // Shutting down is not an answer about a message: one handled with a cancelled context is left // unsettled, for the bus to hand to whatever consumes next (issue 083, review). func TestAMessageHandledDuringShutdownIsLeftForTheBus(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() s, in := serving() s.recorder = recordsWith{err: context.Canceled} to := &settled{} s.act(ctx, in.sends(t, to, KindBuilt, BuildResult{On: "anchor", Repository: "/r", Commit: "abc"})) if !to.unsettled() { t.Fatalf("a build result handled during shutdown was settled, and so lost: %+v", *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) } }