package main import ( "context" "errors" "fmt" "strings" "testing" "time" ) // novox/hq issue 497: a review that could not be had is owed. What is owed is kept in the knowledge base's store, // with its try count and when its next try is due, so the background work takes it up after a restart of the tool // runner as it would have without one; the history says when it is asked again, so nobody takes a pause for a // loss; and after its last try the history says so and the operator is told once. It is never an approval. func TestAReviewOwedOutlivesARestartAndItsLastTryIsSaid(t *testing.T) { eachStore(t, func(t *testing.T, s Store) { inZone(t) w := imported(t, s) n := proposed(t, w, "author@g14", false) key := Key{KindDecision, n} lastWhy := func() string { vs, err := s.Versions(context.Background(), key) if err != nil || len(vs) == 0 { t.Fatalf("versions of %s: %v", key, err) } return vs[len(vs)-1].Why } var due time.Time for try := 1; try <= MostReviewTries; try++ { if try > 1 { // The tool runner restarts: a new knowledge base over the same store, nothing of the old one's memory. w = newWorld(t, s, fixture()) w.clock = due.Add(-time.Second) if said := w.reviewAll(t); len(said) != 0 { t.Fatalf("try %d asked before it was due: %v", try, said) } w.clock = due } w.judge.reviewFail = true if said := w.reviewAll(t); len(said) != 1 || !strings.Contains(said[0], "not reviewed") { t.Fatalf("try %d after a restart was not taken up: %v", try, said) } e := mustGet(t, s, key) if e.Current.Status != StatusProposed || e.Current.Judging == nil || e.Current.Judging.State != JudgingFailed || e.Current.Judging.Tries != try || len(e.Current.Reviews) != 0 { t.Fatalf("try %d is not counted in the store: %s, %+v", try, e.Current.Status, e.Current.Judging) } why := lastWhy() if !strings.Contains(why, fmt.Sprintf("try %d of %d", try, MostReviewTries)) { t.Fatalf("try %d: the history does not count it: %q", try, why) } if try < MostReviewTries { due = w.clock.Add(time.Duration(try) * 10 * time.Minute) when := "asked again at " + due.Local().Format("2006-01-02 15:04 MST") if utc := due.UTC().Format("15:04"); strings.Contains(when, utc) || !strings.Contains(when, "CEST") { t.Fatalf("the time is not said locally: %q (UTC %s)", when, utc) } if !strings.Contains(why, when) { t.Fatalf("try %d: the history does not say when it is asked again (%q): %q", try, when, why) } if g, _ := w.k.Get(GetAsk{Number: n}); !strings.Contains(g.Review, when) { t.Fatalf("try %d: get does not say when it is asked again (%q): %q", try, when, g.Review) } if len(w.told) != 0 { t.Fatalf("try %d: the operator was told before the last try: %v", try, w.told) } } } // After its last try: the history says it was the last, and the operator is told, once. if why := lastWhy(); !strings.Contains(why, "the last") || !strings.Contains(why, "the operator is told") { t.Fatalf("the last try is not said in the history: %q", why) } if len(w.told) != 1 || !strings.Contains(w.told[0], key.String()) || !strings.Contains(w.told[0], "never reviewed") { t.Fatalf("the operator is not told once of the last try: %v", w.told) } w.clock = w.clock.Add(24 * time.Hour) if said := w.reviewAll(t); len(said) != 0 || len(w.told) != 1 { t.Fatalf("after the last try: asked %v, told %v", said, w.told) } // Another restart neither asks it again nor tells the operator again. clock := w.clock w = newWorld(t, s, fixture()) w.clock = clock if said := w.reviewAll(t); len(said) != 0 || len(w.told) != 0 { t.Fatalf("after a restart past the last try: asked %v, told %v", said, w.told) } // Never an approval. if e := mustGet(t, s, key); e.Current.Status != StatusProposed || len(w.said.of(EventAccepted)) != 0 { t.Fatalf("a review never had approved it: %s", e.Current.Status) } }) } // novox/hq issue 497: the notice of a proposal never reviewed says it stays proposed, so it is cleared once that is // no longer so; and a later version's run of tries is a notice of its own, told once whatever else is written. func TestProposedAgainAfterTheLastTryClearsTheNotice(t *testing.T) { eachStore(t, func(t *testing.T, s Store) { w := imported(t, s) n := proposed(t, w, "author@g14", false) key := Key{KindDecision, n} runOut := func(from int) { t.Helper() w.judge.reviewFail = true for try := from; try <= MostReviewTries; try++ { if said := w.reviewAll(t); len(said) != 1 { t.Fatalf("try %d: %v", try, said) } w.clock = w.clock.Add(time.Duration(try) * ReviewPause) } } runOut(1) e := mustGet(t, s, key) first := fmt.Sprintf("knowledge.unreviewed.decision-%d.v%d", n, e.CurrentVersion) if len(w.toldKeys) != 1 || w.toldKeys[0] != first || len(w.cleared) != 0 || w.resolved[0] != key.String()+" no longer waits for its review" { t.Fatalf("after the last try: told %v, cleared %v", w.toldKeys, w.cleared) } // Another version written while it waits (as a link change on it writes one) tells nothing again. r := e.Current.Clone() if err := s.Save(context.Background(), Version{By: "test", At: w.clock, Why: "a link changed", Record: r}, true, false, e.Revision); err != nil { t.Fatal(err) } if w.reviewAll(t); len(w.toldKeys) != 1 || len(w.cleared) != 0 { t.Fatalf("a version written while it waits: told %v, cleared %v", w.toldKeys, w.cleared) } // Proposed again: the notice is cleared, saying so. if _, err := w.k.Propose("author@g14", valid(n)); err != nil { t.Fatal(err) } w.clock = w.clock.Add(time.Second) w.reviewAll(t) // its try 1, which fails, and the clearing w.clock = w.clock.Add(ReviewPause) if len(w.cleared) != 1 || w.cleared[0][0] != first || !strings.Contains(w.cleared[0][1], "proposed again") { t.Fatalf("proposed again: cleared %v", w.cleared) } // Its own run of tries out is a notice of its own, under its own version. runOut(2) if len(w.toldKeys) != 2 || w.toldKeys[1] == first || len(w.cleared) != 1 { t.Fatalf("a later version ran out: told %v, cleared %v", w.toldKeys, w.cleared) } // Proposed again and accepted at last: that notice is cleared too, and nothing is cleared twice, also after // a restart. w.judge.reviewFail = false w.accepted(t, "author@g14", valid(n)) if len(w.cleared) != 2 || w.cleared[1][0] != w.toldKeys[1] || !strings.Contains(w.cleared[1][1], "proposed again") { t.Fatalf("accepted: cleared %v", w.cleared) } clock := w.clock w = newWorld(t, s, fixture()) w.clock = clock if w.reviewAll(t); len(w.cleared) != 0 || len(w.told) != 0 { t.Fatalf("after a restart: told %v, cleared %v", w.told, w.cleared) } }) } // testZone is the local zone every test runs in, set once by TestMain: not UTC. var testZone = time.FixedZone("CEST", 2*60*60) // inZone says the test needs the local zone not to be UTC; TestMain set it before any goroutine started. Setting it // here, under a running bus server's goroutines, was a data race the race detector failed. func inZone(t *testing.T) { t.Helper() if time.Local != testZone { t.Fatal("the local zone is not the tests' own: TestMain sets it") } } // novox/hq issue 497: a clear the operator's channel refused keeps its notice as told, and is sent again at the next // step until it is taken. func TestAClearThatFailsIsSentAgain(t *testing.T) { eachStore(t, func(t *testing.T, s Store) { w := imported(t, s) n := proposed(t, w, "author@g14", false) w.judge.reviewFail = true for try := 1; try <= MostReviewTries; try++ { w.reviewAll(t) w.clock = w.clock.Add(time.Duration(try) * ReviewPause) } if len(w.toldKeys) != 1 { t.Fatalf("after the last try: told %v", w.toldKeys) } prefix := fmt.Sprintf("knowledge.unreviewed.decision-%d.", n) refused := 0 w.k.Clear = func(key, resolved string) error { refused++; return errors.New("the channel is down") } if _, err := w.k.Propose("author@g14", valid(n)); err != nil { t.Fatal(err) } w.reviewAll(t) if told, err := s.ToldOf(context.Background(), prefix); err != nil || len(told) != 1 || refused == 0 { t.Fatalf("a refused clear: told %v (%v), refused %d", told, err, refused) } w.k.Clear = func(key, resolved string) error { w.cleared = append(w.cleared, [2]string{key, resolved}); return nil } w.clock = w.clock.Add(time.Second) w.reviewAll(t) if told, err := s.ToldOf(context.Background(), prefix); err != nil || len(told) != 0 || len(w.cleared) != 1 || w.cleared[0][0] != w.toldKeys[0] { t.Fatalf("the next step: told %v (%v), cleared %v", told, err, w.cleared) } }) }