Files
mesh-host/internal/seatwait/seatwait_test.go
T

150 lines
5.0 KiB
Go

package seatwait
import (
"errors"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/seatlist"
"git.novox.be/novox/mesh-sdk/go/views"
)
type fakeReader struct{ st seatlist.Status }
func (f *fakeReader) Status() seatlist.Status { return f.st }
type fakeTimer struct {
c *fakeClock
due time.Time
f func()
stopped bool
}
func (t *fakeTimer) Stop() bool { was := !t.stopped; t.stopped = true; return was }
type fakeClock struct {
now time.Time
timers []*fakeTimer
}
func (c *fakeClock) Now() time.Time { return c.now }
func (c *fakeClock) AfterFunc(d time.Duration, f func()) Timer {
t := &fakeTimer{c: c, due: c.now.Add(d), f: f}
c.timers = append(c.timers, t)
return t
}
// advance moves the clock and fires the timers that fell due and were not stopped.
func (c *fakeClock) advance(d time.Duration) {
c.now = c.now.Add(d)
for _, t := range c.timers {
if !t.stopped && !t.due.After(c.now) {
t.stopped = true
t.f()
}
}
}
type fakeFindings struct {
put, deleted []views.Finding
err error
}
func (f *fakeFindings) Put(x views.Finding) error {
if f.err != nil {
return f.err
}
f.put = append(f.put, x)
return nil
}
func (f *fakeFindings) Delete(x views.Finding) error { f.deleted = append(f.deleted, x); return nil }
// ADR 0636 §1: a reader that waited five minutes makes the node write one finding, which the first full read clears.
func caseTheNodeWritesOneFindingAfterFiveMinutesAndClearsItAtTheFirstFullRead(t *testing.T) {
reader := &fakeReader{st: seatlist.Status{Heard: false, LastError: "no answer"}}
clock := &fakeClock{now: time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC)}
findings := &fakeFindings{}
var said []string
w := &Waiter{Node: "ace", Observer: "node-engine", Reader: reader, Findings: findings, Clock: clock, Say: func(s string) { said = append(said, s) }}
w.Changed()
clock.advance(Bound - time.Second)
if len(findings.put) != 0 {
t.Fatalf("a finding was written before the bound: %+v", findings.put)
}
w.Changed() // another change while waiting starts no second bound
clock.advance(time.Second)
if len(findings.put) != 1 || findings.put[0].Kind != Kind || findings.put[0].ID != "ace" {
t.Fatalf("after five minutes the node wrote %+v, want one %s finding for ace", findings.put, Kind)
}
clock.advance(time.Hour)
if len(findings.put) != 1 {
t.Fatalf("the finding was written again: %d", len(findings.put))
}
reader.st = seatlist.Status{Heard: true}
w.Changed()
if len(findings.deleted) != 1 || findings.deleted[0].ID != "ace" {
t.Fatalf("the first full read left the finding: deleted %+v", findings.deleted)
}
if on, _ := w.Waiting(); on {
t.Error("the waiter still waits after the full read")
}
}
// A read that ends the wait stops the bound; a timer that fires after the list was read writes nothing.
func caseAReadWithinTheBoundWritesNothing(t *testing.T) {
reader := &fakeReader{st: seatlist.Status{Heard: false}}
clock := &fakeClock{now: time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC)}
findings := &fakeFindings{}
w := &Waiter{Node: "ace", Observer: "node-engine", Reader: reader, Findings: findings, Clock: clock}
w.Changed()
clock.advance(time.Minute)
reader.st = seatlist.Status{Heard: true}
w.Changed()
clock.advance(time.Hour)
if len(findings.put) != 0 || len(findings.deleted) != 0 {
t.Fatalf("a list read in time wrote %+v and cleared %+v", findings.put, findings.deleted)
}
// The seat answered the list already held: no change was said, so the timer is the one to look again.
reader.st = seatlist.Status{Heard: false}
w.Changed()
reader.st = seatlist.Status{Heard: true}
clock.advance(Bound)
if len(findings.put) != 0 {
t.Fatalf("a timer that found the list read wrote %+v", findings.put)
}
}
// A finding that cannot be written is said and tried again at the next change, not lost.
func caseAFindingThatCannotBeWrittenIsSaidAndTriedAgain(t *testing.T) {
reader := &fakeReader{st: seatlist.Status{Heard: false}}
clock := &fakeClock{now: time.Date(2026, 11, 5, 12, 0, 0, 0, time.UTC)}
findings := &fakeFindings{err: errors.New("bus down")}
var said []string
w := &Waiter{Node: "ace", Observer: "node-engine", Reader: reader, Findings: findings, Clock: clock, Say: func(s string) { said = append(said, s) }}
w.Changed()
clock.advance(Bound)
if len(said) != 1 || len(findings.put) != 0 {
t.Fatalf("said %v, wrote %v", said, findings.put)
}
findings.err = nil
w.Changed()
clock.advance(Bound)
if len(findings.put) != 1 {
t.Fatalf("the finding was not written at the next bound: %v", findings.put)
}
}
// ADR 0636 §1, the node's finding of a seat list unread.
func TestTheSeatListWait(t *testing.T) {
for name, run := range map[string]func(*testing.T){
"five minutes write one finding and the first full read clears it": caseTheNodeWritesOneFindingAfterFiveMinutesAndClearsItAtTheFirstFullRead,
"a read within the bound writes nothing": caseAReadWithinTheBoundWritesNothing,
"a finding that cannot be written is said and tried again": caseAFindingThatCannotBeWrittenIsSaidAndTriedAgain,
} {
t.Run(name, run)
}
}