mesh/merge-gate pass: builds mesh-issues → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
An open's lost answer is judged by the issue's first entry; an entry that does not fold faults its issue on start; a value written beside the log is never folded over; one issue is kept in at most 252 KiB so its whole operation fits a log entry.
820 lines
27 KiB
Go
820 lines
27 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// fakeLog is the module's log kept in this process with the runtime's semantics (novox/hq ADR 0297, mesh-sdk
|
|
// go/log.go): an append at expect is refused with stdio.ErrChanged when the key's last sequence is another,
|
|
// 0 standing for a key with no entry; a read pages from a sequence, narrowed to one key or none.
|
|
type fakeLog struct {
|
|
mu sync.Mutex
|
|
entries []stdio.LogEntry
|
|
// fail, when set, is what every append answers, writing nothing: a full log, or a bus that is down.
|
|
fail error
|
|
// failNext is how many of the next appends fail, writing nothing, as a bus that does not answer.
|
|
failNext int
|
|
// lost, when set, is what the next append answers after it was written: an answer lost on the way.
|
|
lost error
|
|
// between, when set, runs once before the next append is judged: another writer coming in between.
|
|
between func()
|
|
clock time.Time
|
|
}
|
|
|
|
func newFakeLog() *fakeLog {
|
|
return &fakeLog{clock: time.Date(2026, 10, 12, 9, 0, 0, 0, time.UTC)}
|
|
}
|
|
|
|
func (l *fakeLog) lastOf(key string) uint64 {
|
|
for i := len(l.entries) - 1; i >= 0; i-- {
|
|
if l.entries[i].Key == key {
|
|
return l.entries[i].Sequence
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (l *fakeLog) AppendAt(key string, value any, expect uint64) (uint64, error) {
|
|
l.mu.Lock()
|
|
if b := l.between; b != nil {
|
|
l.between = nil
|
|
l.mu.Unlock()
|
|
b()
|
|
l.mu.Lock()
|
|
}
|
|
defer l.mu.Unlock()
|
|
if l.fail != nil {
|
|
return 0, l.fail
|
|
}
|
|
if l.failNext > 0 {
|
|
l.failNext--
|
|
return 0, errors.New("the bus did not answer")
|
|
}
|
|
raw, err := json.Marshal(value)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if len(raw) > 256<<10 {
|
|
return 0, fmt.Errorf("the value for %s is %d bytes; a log entry holds at most 256 KiB", key, len(raw))
|
|
}
|
|
if last := l.lastOf(key); last != expect {
|
|
return 0, fmt.Errorf("the last entry of %s is %d, not %d: %w", key, last, expect, stdio.ErrChanged)
|
|
}
|
|
seq := uint64(len(l.entries) + 1)
|
|
l.entries = append(l.entries, stdio.LogEntry{Sequence: seq, Key: key, Value: raw, At: l.clock})
|
|
if lost := l.lost; lost != nil {
|
|
l.lost = nil
|
|
return 0, lost
|
|
}
|
|
return seq, nil
|
|
}
|
|
|
|
func (l *fakeLog) Read(from uint64, keys string, limit int) (*stdio.LogPage, error) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
if limit <= 0 {
|
|
limit = 100
|
|
}
|
|
limit = min(limit, 1000)
|
|
p := &stdio.LogPage{Entries: []stdio.LogEntry{}, Last: uint64(len(l.entries))}
|
|
for _, e := range l.entries {
|
|
if e.Sequence < from || (keys != "" && e.Key != keys) {
|
|
continue
|
|
}
|
|
if len(p.Entries) == limit {
|
|
p.Next = e.Sequence
|
|
break
|
|
}
|
|
p.Entries = append(p.Entries, e)
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// ops is every operation the log holds of one issue, oldest first.
|
|
func (l *fakeLog) ops(t *testing.T, n uint64) []Operation {
|
|
t.Helper()
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
var out []Operation
|
|
for _, e := range l.entries {
|
|
if e.Key != keyOf(n) {
|
|
continue
|
|
}
|
|
var op Operation
|
|
if err := json.Unmarshal(e.Value, &op); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out = append(out, op)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// foldCheck holds every issue the bucket folded to what its whole log adds up to, byte for byte: run after every
|
|
// test of a harness on this log (the review of PR 228).
|
|
func (l *fakeLog) foldCheck(t *testing.T, m *memory) {
|
|
l.mu.Lock()
|
|
entries := append([]stdio.LogEntry(nil), l.entries...)
|
|
l.mu.Unlock()
|
|
for k, v := range m.snapshot(IssuesState) {
|
|
var is Issue
|
|
if json.Unmarshal([]byte(v), &is) != nil || is.Folded == 0 {
|
|
continue
|
|
}
|
|
var f *Issue
|
|
var err error
|
|
for _, e := range entries {
|
|
if e.Key != k || e.Sequence > is.Folded {
|
|
continue
|
|
}
|
|
if f, err = foldEntry(f, e); err != nil {
|
|
t.Errorf("issue %s does not fold from its log: %v", k, err)
|
|
break
|
|
}
|
|
}
|
|
if raw, _ := json.Marshal(f); err == nil && string(raw) != v {
|
|
t.Errorf("issue %s in the bucket is not the fold of its log:\nbucket %s\nfold %s", k, v, raw)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (l *fakeLog) size() int {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
return len(l.entries)
|
|
}
|
|
|
|
// another is a second tracker on the same bucket and log: another instance, or the same after a restart.
|
|
func (h *harness) another(t *testing.T) *harness {
|
|
t.Helper()
|
|
o := newHarness(t)
|
|
o.mem, o.log = h.mem, h.log
|
|
o.Tracker.Store, o.Tracker.Log = h.mem, h.log
|
|
o.settings = h.settings
|
|
return o
|
|
}
|
|
|
|
// §1: a write whose append is refused changes nothing: not the bucket, not the log, and says no event.
|
|
func TestAWriteWhoseAppendIsRefusedChangesNothing(t *testing.T) {
|
|
h := newHarness(t)
|
|
n := h.open(t, "the bucket fills", "mesh-catalog modules/mesh-issues")
|
|
bucket, entries, events := h.mem.snapshot(IssuesState), h.log.size(), len(h.said())
|
|
h.log.fail = fmt.Errorf("the log is full: %w", errors.New("maximum bytes exceeded"))
|
|
if _, err := h.Note(n, "a note the log refuses"); err == nil || !strings.Contains(err.Error(), "log did not take") {
|
|
t.Fatalf("a note whose append is refused answers %v", err)
|
|
}
|
|
if _, err := h.Move(MoveAsk{Number: n, Status: StatusDiagnosing, By: "fix@g14"}); err == nil {
|
|
t.Fatal("a move whose append is refused was answered as made")
|
|
}
|
|
if got := h.mem.snapshot(IssuesState); fmt.Sprint(got) != fmt.Sprint(bucket) {
|
|
t.Fatalf("the bucket changed:\n%v\n%v", bucket, got)
|
|
}
|
|
if h.log.size() != entries || len(h.said()) != events {
|
|
t.Fatalf("the log holds %d entries (was %d), %d events said (was %d)", h.log.size(), entries, len(h.said()), events)
|
|
}
|
|
}
|
|
|
|
// §1: an operation that came in between refuses the stale append; the change is read again and applied to
|
|
// what the log holds, so neither is lost.
|
|
func TestAnOperationInBetweenIsNotLost(t *testing.T) {
|
|
h := newHarness(t)
|
|
n := h.open(t, "two notes at once", "hq")
|
|
other := h.another(t)
|
|
h.log.between = func() {
|
|
if _, err := other.Note(n, "the other's note"); err != nil {
|
|
t.Errorf("the other note: %v", err)
|
|
}
|
|
}
|
|
if _, err := h.Note(n, "this note"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
is, err := h.Get(n)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(is.Notes) != 2 || is.Notes[0].Text != "the other's note" || is.Notes[1].Text != "this note" {
|
|
t.Fatalf("notes %+v", is.Notes)
|
|
}
|
|
ops := h.log.ops(t, n)
|
|
if len(ops) != 3 || ops[0].Op != OpOpened || ops[1].Op != EventNoted || ops[2].Op != EventNoted {
|
|
t.Fatalf("the log of %d: %+v", n, ops)
|
|
}
|
|
if is.Folded != uint64(h.log.size()) {
|
|
t.Fatalf("folded at %d, the log's last is %d", is.Folded, h.log.size())
|
|
}
|
|
}
|
|
|
|
// §2: an open refused at validation takes no number.
|
|
func TestAnOpenRefusedAtValidationTakesNoNumber(t *testing.T) {
|
|
h := newHarness(t)
|
|
if _, err := h.Open(OpenAsk{Title: "", Report: "no title"}); err == nil {
|
|
t.Fatal("an open without a title was taken")
|
|
}
|
|
if _, err := h.Open(OpenAsk{Title: "t", Report: "r", LocatedIn: []string{"nowhere"}}); err == nil {
|
|
t.Fatal("an open naming an unregistered owner was taken")
|
|
}
|
|
if c := h.mem.counter(t); c != 0 || h.log.size() != 0 {
|
|
t.Fatalf("counter %d, log %d entries", c, h.log.size())
|
|
}
|
|
if n := h.open(t, "the first"); n != 365 {
|
|
t.Fatalf("the first open got %d", n)
|
|
}
|
|
}
|
|
|
|
// §2: an append that fails after the number was taken answers the failure and spends that number.
|
|
func TestAnAppendFailingAfterTheNumberSpendsIt(t *testing.T) {
|
|
h := newHarness(t)
|
|
h.log.fail = errors.New("the log is full")
|
|
_, err := h.Open(OpenAsk{Title: "refused by a full log", Report: "r"})
|
|
if err == nil || !strings.Contains(err.Error(), "365 is spent") || !strings.Contains(err.Error(), "the log is full") {
|
|
t.Fatalf("the open answered %v", err)
|
|
}
|
|
if c := h.mem.counter(t); c != 365 {
|
|
t.Fatalf("the counter is at %d: the number was not taken", c)
|
|
}
|
|
if len(h.mem.snapshot(IssuesState)) != 0 || h.log.size() != 0 || len(h.said()) != 0 {
|
|
t.Fatal("an open the log refused left an issue, an entry or an event")
|
|
}
|
|
h.log.fail = nil
|
|
if n := h.open(t, "the next"); n != 366 {
|
|
t.Fatalf("the next open got %d, not 366", n)
|
|
}
|
|
}
|
|
|
|
// §2: an append whose answer was lost is known as the open's own by its opening id: one issue, one number.
|
|
func TestAnAppendWhoseAnswerWasLostIsTheOpensOwn(t *testing.T) {
|
|
h := newHarness(t)
|
|
h.log.lost = errors.New("timeout")
|
|
o, err := h.Open(OpenAsk{Title: "lost answer", Report: "r"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if o.Number != 365 || h.log.size() != 1 {
|
|
t.Fatalf("number %d, %d entries", o.Number, h.log.size())
|
|
}
|
|
if is, err := h.Get(365); err != nil || is.Folded != 1 {
|
|
t.Fatalf("issue %+v, %v", is, err)
|
|
}
|
|
}
|
|
|
|
// writeEverything makes issues with every kind of write: open, move, note, link, set, delivery, triage.
|
|
func writeEverything(t *testing.T, h *harness) []uint64 {
|
|
t.Helper()
|
|
a := h.open(t, "the issues bucket fills toward its cap", "mesh-catalog modules/mesh-issues")
|
|
b := h.open(t, "status changes are kept for seven days", "mesh-controller")
|
|
c := h.open(t, "an unrelated one")
|
|
if _, err := h.Note(a, "measured 10.9 MB of 64 MiB"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := h.Link(LinkAsk{Number: b, Kind: LinkRelatesTo, Target: keyOf(a)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := h.Set(SetAsk{Number: a, Severity: "major", Needs: "decision"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := h.Move(MoveAsk{Number: a, Status: StatusFixing, By: "fix@g14", FixedBy: []string{"novox/mesh-catalog#1"}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := h.Move(MoveAsk{Number: c, Status: StatusWontfix, Reason: "not ours"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := h.Link(LinkAsk{Number: b, Kind: LinkRelatesTo, Target: keyOf(a), Remove: true}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return []uint64{a, b, c}
|
|
}
|
|
|
|
// §3: the bucket emptied, the fold on start restores every issue exactly.
|
|
func TestTheFoldOnStartRestoresAnEmptiedBucketExactly(t *testing.T) {
|
|
h := newHarness(t)
|
|
writeEverything(t, h)
|
|
before := h.mem.snapshot(IssuesState)
|
|
if len(before) != 3 {
|
|
t.Fatalf("%d issues", len(before))
|
|
}
|
|
h.mem.empty(IssuesState)
|
|
r := h.another(t)
|
|
s, err := r.Start()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.Rebuilt != 3 || s.Issues != 3 || len(s.Faults) != 0 {
|
|
t.Fatalf("started %+v", s)
|
|
}
|
|
after := h.mem.snapshot(IssuesState)
|
|
for k, v := range before {
|
|
if after[k] != v {
|
|
t.Errorf("issue %s was\n%s\nand is folded as\n%s", k, v, after[k])
|
|
}
|
|
}
|
|
if len(after) != len(before) {
|
|
t.Fatalf("%d issues after, %d before", len(after), len(before))
|
|
}
|
|
}
|
|
|
|
// §3: a bucket behind its log (the write after the append failed) catches up on start, and at the next change.
|
|
func TestABucketBehindItsLogCatchesUp(t *testing.T) {
|
|
h := newHarness(t)
|
|
n := h.open(t, "crash between the two", "hq")
|
|
h.mem.fail[IssuesState] = errors.New("the bus went away")
|
|
if _, err := h.Note(n, "kept in the log only"); err != nil {
|
|
t.Fatalf("a note the log took is a note made: %v", err)
|
|
}
|
|
is, _ := h.Get(n)
|
|
if len(is.Notes) != 0 {
|
|
t.Fatal("the bucket write was meant to fail")
|
|
}
|
|
r := h.another(t)
|
|
s, err := r.Start()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.Refolded != 1 {
|
|
t.Fatalf("started %+v", s)
|
|
}
|
|
if is, _ = h.Get(n); len(is.Notes) != 1 || is.Folded != 2 {
|
|
t.Fatalf("after start: %+v", is)
|
|
}
|
|
|
|
// And at the next change without a start: the stale append is refused, the log folded, the change applied.
|
|
h.mem.fail[IssuesState] = errors.New("the bus went away")
|
|
if _, err := h.Note(n, "second"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := h.Note(n, "third"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if is, _ = h.Get(n); len(is.Notes) != 3 || is.Folded != 4 {
|
|
t.Fatalf("after the next change: notes %d, folded %d", len(is.Notes), is.Folded)
|
|
}
|
|
}
|
|
|
|
// §3: the counter is kept at or above the highest number in the log.
|
|
func TestStartRaisesTheCounterToTheLog(t *testing.T) {
|
|
h := newHarness(t)
|
|
h.open(t, "one")
|
|
h.open(t, "two")
|
|
h.mem.put(CountersState, IssueCounter, uint64(365))
|
|
s, err := h.another(t).Start()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.Counter != 366 || h.mem.counter(t) != 366 {
|
|
t.Fatalf("counter %d, kept %d", s.Counter, h.mem.counter(t))
|
|
}
|
|
if n := h.open(t, "three"); n != 367 {
|
|
t.Fatalf("the next open got %d", n)
|
|
}
|
|
}
|
|
|
|
// §4: each write still says its event, alongside its one operation in the log.
|
|
func TestEachWriteSaysItsEventAlongsideItsOperation(t *testing.T) {
|
|
h := newHarness(t)
|
|
type step struct {
|
|
event string
|
|
do func() error
|
|
}
|
|
var n uint64
|
|
steps := []step{
|
|
{EventOpened, func() error {
|
|
o, err := h.Open(OpenAsk{Title: "t", Report: "r", LocatedIn: []string{"hq"}})
|
|
n = o.Number
|
|
return err
|
|
}},
|
|
{EventNoted, func() error { _, err := h.Note(n, "a note"); return err }},
|
|
{EventLinked, func() error { _, err := h.Link(LinkAsk{Number: n, Kind: LinkDecision, Target: "0300"}); return err }},
|
|
{EventSet, func() error { _, err := h.Set(SetAsk{Number: n, Severity: "minor"}); return err }},
|
|
{EventMoved, func() error { _, err := h.Move(MoveAsk{Number: n, Status: StatusLocated}); return err }},
|
|
}
|
|
for i, s := range steps {
|
|
events, entries := len(h.said()), h.log.size()
|
|
if err := s.do(); err != nil {
|
|
t.Fatalf("%s: %v", s.event, err)
|
|
}
|
|
said := h.said()
|
|
if len(said) != events+1 || said[len(said)-1].Event != s.event {
|
|
t.Fatalf("step %d said %v", i, said[events:])
|
|
}
|
|
ops := h.log.ops(t, n)
|
|
if h.log.size() != entries+1 || ops[len(ops)-1].Op != s.event || ops[len(ops)-1].By != Unproven {
|
|
t.Fatalf("step %d: %d entries (was %d), last %+v", i, h.log.size(), entries, ops[len(ops)-1])
|
|
}
|
|
}
|
|
// A note is kept as itself, not as every note again.
|
|
if op := h.log.ops(t, n)[1]; op.Set != nil || len(op.Add) != 1 || op.Add["notes"] == nil {
|
|
t.Fatalf("the note's operation is %+v", op)
|
|
}
|
|
}
|
|
|
|
// §5: before pages through more than 500 issues, without a gap or a repeat.
|
|
func TestBeforePagesThroughMoreThanFiveHundredIssues(t *testing.T) {
|
|
h := newHarness(t)
|
|
const issues = 530
|
|
for i := 0; i < issues; i++ {
|
|
h.open(t, fmt.Sprintf("issue %d", i))
|
|
}
|
|
if _, err := h.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seen := map[uint64]bool{}
|
|
var last uint64 = 0
|
|
var before uint64
|
|
pages := 0
|
|
for {
|
|
l, err := h.List(ListAsk{Limit: 100, Before: before})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pages++
|
|
for _, r := range l.Rows {
|
|
if seen[r.Number] {
|
|
t.Fatalf("%d repeated", r.Number)
|
|
}
|
|
if last != 0 && r.Number != last-1 {
|
|
t.Fatalf("a gap from %d to %d", last, r.Number)
|
|
}
|
|
seen[r.Number], last = true, r.Number
|
|
}
|
|
if l.Next == 0 {
|
|
break
|
|
}
|
|
before = l.Next
|
|
}
|
|
if len(seen) != issues || pages != 6 || last != 365 {
|
|
t.Fatalf("%d issues seen in %d pages, ending at %d", len(seen), pages, last)
|
|
}
|
|
// Over the tool too, as JSON gives the cursor.
|
|
out, err := h.call(t, "list", map[string]any{"before": 400, "limit": 2})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if l := out.(*Listed); len(l.Rows) != 2 || l.Rows[0].Number != 399 || l.Next != 398 {
|
|
t.Fatalf("listed %+v", l)
|
|
}
|
|
}
|
|
|
|
// §5: a search is ranked by relevance; the index is rebuilt on start and kept current by the bucket's watch.
|
|
func TestASearchIsRankedAndTheIndexRebuiltOnStart(t *testing.T) {
|
|
h := newHarness(t)
|
|
full := h.open(t, "the issues bucket fills toward its cap")
|
|
h.open(t, "a bucket of the controller")
|
|
week := h.open(t, "status changes are kept for seven days")
|
|
h.open(t, "nothing alike")
|
|
if _, err := h.Note(week, "the events stream drops them after a week; the bucket is not involved"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := h.another(t)
|
|
if _, err := r.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !r.view().ready() {
|
|
t.Fatal("the index was not built on start")
|
|
}
|
|
l, err := r.List(ListAsk{Search: "issues bucket cap"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(l.Rows) != 3 || l.Rows[0].Number != full {
|
|
t.Fatalf("ranked %+v", l.Rows)
|
|
}
|
|
// A note's words are found: notes are indexed.
|
|
if l, _ = r.List(ListAsk{Search: "week"}); len(l.Rows) != 1 || l.Rows[0].Number != week {
|
|
t.Fatalf("a note's word: %+v", l.Rows)
|
|
}
|
|
// Another instance's write reaches this one's index through the watch of the bucket.
|
|
if _, err := h.Note(full, "seen again by the operator on shanks"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
e, _ := h.mem.Get(IssuesState, keyOf(full))
|
|
if err := r.Watched(stdio.StateChange{State: IssuesState, Key: keyOf(full), Op: "put", Value: e.Value}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if l, _ = r.List(ListAsk{Search: "operator"}); len(l.Rows) != 1 || l.Rows[0].Number != full {
|
|
t.Fatalf("after the watch: %+v", l.Rows)
|
|
}
|
|
if _, err := r.List(ListAsk{Search: "bucket", Before: 400}); err == nil {
|
|
t.Fatal("a search with before was taken")
|
|
}
|
|
}
|
|
|
|
// keptBefore puts an issue in the bucket as the tracker kept it before the switch: values, no log.
|
|
func keptBefore(t *testing.T, h *harness, n uint64, titles ...string) {
|
|
t.Helper()
|
|
for _, title := range titles {
|
|
h.mem.put(IssuesState, keyOf(n), Issue{Number: n, Title: title, Status: StatusOpen, Report: "r",
|
|
Opened: time.Date(2026, 10, 10, 0, 0, 0, 0, time.UTC), OpenedBy: "x", Opening: fmt.Sprint("o", n)})
|
|
}
|
|
}
|
|
|
|
// §6: the carry appends each value the bucket keeps, oldest first, as carried operations holding the whole
|
|
// issue; run again it carries nothing; a write after it is one entry in the log.
|
|
func TestTheCarryIsIdempotentAndCarriesEveryValueKept(t *testing.T) {
|
|
h := newHarness(t)
|
|
keptBefore(t, h, 300, "first title", "second title", "third title")
|
|
keptBefore(t, h, 301, "only")
|
|
c, err := h.Carry()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Issues != 2 || c.Values != 4 || len(c.Faults) != 0 {
|
|
t.Fatalf("carried %+v", c)
|
|
}
|
|
ops := h.log.ops(t, 300)
|
|
if len(ops) != 3 {
|
|
t.Fatalf("%d operations of 300", len(ops))
|
|
}
|
|
for i, title := range []string{"first title", "second title", "third title"} {
|
|
if ops[i].Op != OpCarried || ops[i].Issue == nil || ops[i].Issue.Title != title {
|
|
t.Fatalf("operation %d: %+v", i, ops[i])
|
|
}
|
|
}
|
|
is, _ := h.Get(300)
|
|
if is.Title != "third title" || is.Folded != 3 {
|
|
t.Fatalf("300 is %+v", is)
|
|
}
|
|
again, err := h.Carry()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if again.Issues != 0 || again.Values != 0 || again.Already != 2 || h.log.size() != 4 {
|
|
t.Fatalf("carried again %+v, %d entries", again, h.log.size())
|
|
}
|
|
if _, err := h.Move(MoveAsk{Number: 300, Status: StatusDiagnosing, By: "fix@g14"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if h.log.size() != 5 || h.log.ops(t, 300)[3].Op != EventMoved {
|
|
t.Fatalf("a move after the carry: %d entries", h.log.size())
|
|
}
|
|
// The start carries too, and finds nothing more.
|
|
s, err := h.another(t).Start()
|
|
if err != nil || s.Carried.Issues != 0 || h.log.size() != 5 {
|
|
t.Fatalf("start %+v, %v", s, err)
|
|
}
|
|
}
|
|
|
|
// §6: a deletion among the values the bucket kept is passed over, said, and the values after it carried.
|
|
func TestTheCarryPassesOverADeletionAmongTheKeptValues(t *testing.T) {
|
|
h := newHarness(t)
|
|
keptBefore(t, h, 300, "before the deletion")
|
|
h.mem.deleteKey(IssuesState, keyOf(300))
|
|
keptBefore(t, h, 300, "after the deletion", "now")
|
|
c, err := h.Carry()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Issues != 1 || c.Values != 3 || c.Deletes != 1 || !strings.Contains(c.Said, "1 deletions") {
|
|
t.Fatalf("carried %+v", c)
|
|
}
|
|
ops := h.log.ops(t, 300)
|
|
if len(ops) != 3 || ops[0].Issue.Title != "before the deletion" || ops[2].Issue.Title != "now" {
|
|
t.Fatalf("the log of 300: %+v", ops)
|
|
}
|
|
if is, _ := h.Get(300); is.Title != "now" || is.Folded != 3 {
|
|
t.Fatalf("300 is %+v", is)
|
|
}
|
|
}
|
|
|
|
// §6: a write to an issue not carried yet carries it first, so its first operation in the log is a carried one.
|
|
func TestAWriteBeforeTheCarryCarriesItsIssueFirst(t *testing.T) {
|
|
h := newHarness(t)
|
|
keptBefore(t, h, 300, "before", "now")
|
|
if _, err := h.Note(300, "after the switch"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ops := h.log.ops(t, 300)
|
|
if len(ops) != 3 || ops[0].Op != OpCarried || ops[1].Op != OpCarried || ops[2].Op != EventNoted {
|
|
t.Fatalf("the log of 300: %+v", ops)
|
|
}
|
|
h.mem.empty(IssuesState)
|
|
if _, err := h.another(t).Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if is, _ := h.Get(300); is.Title != "now" || len(is.Notes) != 1 {
|
|
t.Fatalf("300 folds to %+v", is)
|
|
}
|
|
}
|
|
|
|
// The counter behind an issue the bucket kept before the switch: the open refuses, overwrites nothing.
|
|
func TestAnOpenOverAnIssueKeptBeforeTheSwitchOverwritesNothing(t *testing.T) {
|
|
h := newHarness(t)
|
|
keptBefore(t, h, 365, "kept")
|
|
h.mem.put(CountersState, IssueCounter, uint64(364))
|
|
if _, err := h.Open(OpenAsk{Title: "new", Report: "r"}); err == nil || !strings.Contains(err.Error(), "fault") {
|
|
t.Fatalf("the open answered %v", err)
|
|
}
|
|
if is, _ := h.Get(365); is.Title != "kept" {
|
|
t.Fatalf("365 is %+v", is)
|
|
}
|
|
}
|
|
|
|
// titlesOf is each operation of an issue's log as its kind and, for one holding the issue whole, its title.
|
|
func titlesOf(t *testing.T, h *harness, n uint64) string {
|
|
t.Helper()
|
|
var out []string
|
|
for _, op := range h.log.ops(t, n) {
|
|
if op.Issue != nil {
|
|
out = append(out, op.Op+":"+op.Issue.Title)
|
|
} else {
|
|
out = append(out, op.Op)
|
|
}
|
|
}
|
|
return strings.Join(out, " ")
|
|
}
|
|
|
|
// §6, the review of PR 228: a carry that fails partway, then a second carry, leaves every kept value carried
|
|
// once, oldest first, and each carry says what it really carried.
|
|
func TestAnInterruptedCarryResumesAfterTheLastValueCarried(t *testing.T) {
|
|
h := newHarness(t)
|
|
keptBefore(t, h, 300, "v1", "v2", "v3", "v4")
|
|
// The bus goes away after the first value is appended.
|
|
h.log.between = func() { h.log.between = func() { h.log.fail = errors.New("the bus went away") } }
|
|
c, err := h.Carry()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Values != 1 || len(c.Faults) != 1 || !strings.Contains(c.Said, "could not be carried") ||
|
|
!strings.Contains(c.Faults[0], "the bus went away") {
|
|
t.Fatalf("the first carry said %+v", c)
|
|
}
|
|
if is, _ := h.Get(300); is.Folded != 0 || is.Title != "v4" {
|
|
t.Fatalf("after the failure the bucket holds %+v", is)
|
|
}
|
|
// A write meanwhile is refused, and changes nothing.
|
|
if _, err := h.Note(300, "while the bus is away"); err == nil || !strings.Contains(err.Error(), "not carried") {
|
|
t.Fatalf("a note on an issue half carried: %v", err)
|
|
}
|
|
h.log.fail = nil
|
|
c, err = h.Carry()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Values != 3 || c.Issues != 1 || len(c.Faults) != 0 {
|
|
t.Fatalf("the second carry said %+v", c)
|
|
}
|
|
if got := titlesOf(t, h, 300); got != "carried:v1 carried:v2 carried:v3 carried:v4" {
|
|
t.Fatalf("the log of 300: %s", got)
|
|
}
|
|
if is, _ := h.Get(300); is.Title != "v4" || is.Folded != 4 {
|
|
t.Fatalf("300 is %+v", is)
|
|
}
|
|
}
|
|
|
|
// §6, the review of PR 228: one issue that cannot be carried is a fault, named, and the start carries the others,
|
|
// folds, raises the counter and loads memory. Writes to that issue are refused with the reason.
|
|
func TestOneIssueThatCannotBeCarriedDoesNotStopTheStart(t *testing.T) {
|
|
h := newHarness(t)
|
|
keptBefore(t, h, 300, "small")
|
|
// Kept before the switch at the bucket's limit: whole, in a carried operation, past what one log entry holds.
|
|
h.mem.put(IssuesState, "301", Issue{Number: 301, Title: "huge", Status: StatusOpen, Report: strings.Repeat("x", 256<<10-200),
|
|
OpenedBy: "x", Opening: "o301"})
|
|
keptBefore(t, h, 302, "also small")
|
|
s, err := h.Start()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.Carried.Issues != 2 || len(s.Faults) != 1 || !strings.Contains(s.Faults[0], "issue 301") ||
|
|
!strings.Contains(s.Faults[0], "256 KiB") || !strings.Contains(s.Said, "issue 301") {
|
|
t.Fatalf("started %+v", s)
|
|
}
|
|
if !h.view().ready() {
|
|
t.Fatal("memory was not loaded")
|
|
}
|
|
l, err := h.List(ListAsk{})
|
|
if err != nil || l.Matched != 3 {
|
|
t.Fatalf("listed %+v, %v", l, err)
|
|
}
|
|
if _, err := h.Note(301, "a note"); err == nil || !strings.Contains(err.Error(), "256 KiB") {
|
|
t.Fatalf("a note on the issue not carried: %v", err)
|
|
}
|
|
if is, _ := h.Get(301); is.Title != "huge" || is.Folded != 0 || len(is.Notes) != 0 {
|
|
t.Fatal("the issue not carried was changed")
|
|
}
|
|
if is, _ := h.Get(302); is.Folded == 0 {
|
|
t.Fatal("302 was not carried")
|
|
}
|
|
}
|
|
|
|
// §6, the review of PR 228: two instances carrying the same issue at the same moment carry each value once.
|
|
func TestTwoInstancesCarryingOneIssueAtOnceCarryEachValueOnce(t *testing.T) {
|
|
h := newHarness(t)
|
|
keptBefore(t, h, 300, "v1", "v2", "v3")
|
|
other := h.another(t)
|
|
var otherSaid *Carried
|
|
h.log.between = func() {
|
|
c, err := other.Carry()
|
|
if err != nil {
|
|
t.Errorf("the other carry: %v", err)
|
|
}
|
|
otherSaid = c
|
|
}
|
|
c, err := h.Carry()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := titlesOf(t, h, 300); got != "carried:v1 carried:v2 carried:v3" {
|
|
t.Fatalf("the log of 300: %s", got)
|
|
}
|
|
if otherSaid.Values+c.Values != 3 || len(c.Faults)+len(otherSaid.Faults) != 0 {
|
|
t.Fatalf("this carry %+v, the other %+v", c, otherSaid)
|
|
}
|
|
}
|
|
|
|
// §6, the review of PR 228: a value written beside the log after the carry, by an instance from before the switch,
|
|
// is never overwritten by the fold on start; it is carried as one more operation.
|
|
func TestAValueWrittenBesideTheLogIsCarriedNotOverwritten(t *testing.T) {
|
|
h := newHarness(t)
|
|
n := h.open(t, "carried", "hq")
|
|
if _, err := h.Note(n, "after the switch"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// An instance from before the switch writes the bucket: no folded.
|
|
is, _ := h.Get(n)
|
|
is.Folded, is.Title = 0, "written beside the log"
|
|
h.mem.put(IssuesState, keyOf(n), is)
|
|
// Its carry fails at this start: the fold is not written over it.
|
|
h.log.failNext = 1
|
|
s, err := h.another(t).Start()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, _ := h.Get(n); got.Title != "written beside the log" || got.Folded != 0 || len(s.Faults) != 2 {
|
|
t.Fatalf("issue %d is %+v after a start whose carry failed (faults %v)", n, got, s.Faults)
|
|
}
|
|
// The next start carries it.
|
|
if _, err := h.another(t).Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, _ := h.Get(n)
|
|
if got.Title != "written beside the log" || got.Folded != 3 || len(got.Notes) != 1 {
|
|
t.Fatalf("issue %d is %+v", n, got)
|
|
}
|
|
if ops := titlesOf(t, h, n); ops != "opened:carried noted carried:written beside the log" {
|
|
t.Fatalf("the log: %s", ops)
|
|
}
|
|
}
|
|
|
|
// §3, the review of PR 228: an entry that cannot be folded faults its issue on start, and nothing folded past it
|
|
// is written.
|
|
func TestAnEntryThatCannotBeFoldedLeavesItsIssueAsItIs(t *testing.T) {
|
|
h := newHarness(t)
|
|
n := h.open(t, "fine")
|
|
m := h.open(t, "broken")
|
|
if _, err := h.Note(m, "kept"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
h.log.mu.Lock()
|
|
for i, e := range h.log.entries {
|
|
if e.Key == keyOf(m) && e.Sequence == 2 {
|
|
h.log.entries[i].Value = json.RawMessage(`{"op":"noted","add":{"notes":"not a list"}}`)
|
|
}
|
|
}
|
|
h.log.mu.Unlock()
|
|
before := h.mem.snapshot(IssuesState)[keyOf(m)]
|
|
h.mem.empty(IssuesState)
|
|
h.mem.put(IssuesState, keyOf(m), json.RawMessage(before))
|
|
s, err := h.another(t).Start()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.Rebuilt != 1 || len(s.Faults) != 1 || !strings.Contains(s.Faults[0], fmt.Sprintf("issue %d cannot be folded", m)) {
|
|
t.Fatalf("started %+v", s)
|
|
}
|
|
if h.mem.snapshot(IssuesState)[keyOf(m)] != before {
|
|
t.Fatal("the broken issue's bucket value was written over")
|
|
}
|
|
if is, _ := h.Get(n); is.Title != "fine" {
|
|
t.Fatalf("%d is %+v", n, is)
|
|
}
|
|
// The broken entry is the test's doing: the fold check would find it, so take the issue out of the bucket.
|
|
h.mem.empty(IssuesState)
|
|
}
|
|
|
|
// §2, the review of PR 228: an open whose answer was lost, its issue already changed by another operation, is
|
|
// still known as its own by its first entry.
|
|
func TestALostOpenIsKnownByItsFirstEntryNotItsLast(t *testing.T) {
|
|
h := newHarness(t)
|
|
h.log.lost = errors.New("no answer in 30 s")
|
|
// After the first operation lands and its answer is lost, another operation follows before the retry.
|
|
h.log.between = func() {
|
|
h.log.between = func() {
|
|
h.log.mu.Lock()
|
|
h.log.entries = append(h.log.entries, stdio.LogEntry{Sequence: uint64(len(h.log.entries) + 1), Key: "365",
|
|
Value: json.RawMessage(`{"op":"triaged","by":"triage","set":{"severity":"\"minor\""}}`)})
|
|
h.log.mu.Unlock()
|
|
}
|
|
}
|
|
o, err := h.Open(OpenAsk{Title: "lost and followed", Report: "r"})
|
|
if err != nil || o.Number != 365 {
|
|
t.Fatalf("%+v %v", o, err)
|
|
}
|
|
// The entry added by hand is not one the tracker folded: take the issue out before the fold check.
|
|
h.mem.empty(IssuesState)
|
|
}
|