inZone wrote time.Local while the bus test's nats server read it from a goroutine that outlives its test; the race detector the repository check now runs failed TestAReviewOwedOutlivesARestartAndItsLastTryIsSaid.
203 lines
8.5 KiB
Go
203 lines
8.5 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|