150 lines
5.0 KiB
Go
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)
|
|
}
|
|
}
|