1package room23import (4 "fmt"5 "sync"6 "sync/atomic"7 "testing"89 "src.rest/cat/012/internal/sheet"10)1112// who is a participant counting the times it was told the room changed.13type who struct {14 name string15 kicks atomic.Int6416}1718func (w *who) Name() string { return w.name }19func (w *who) Notify() { w.kicks.Add(1) }2021func addr(s string) sheet.Addr {22 a, _ := sheet.ParseAddr(s)23 return a24}2526func newBook() *sheet.Workbook { return sheet.New().Book() }2728// Sessions opening the same key share one workbook: each turn's steps29// are its author's, the others are told, and the room remembers who30// changed which cells, in order.31func TestRoomSharesOneWorkbook(t *testing.T) {32 g := NewRegistry(Edit)33 ann, bob := &who{name: "ann"}, &who{name: "bob"}34 a, created := g.Join("/srv/budget.012", ann, newBook)35 if !created {36 t.Fatal("the first join didn't open the room")37 }38 b, created := g.Join("/srv/budget.012", bob, func() *sheet.Workbook { t.Fatal("opened twice"); return nil })39 if created || a.Book() != b.Book() || a.Color() == b.Color() {40 t.Fatalf("created %v, same book %v, colors %d %d", created, a.Book() == b.Book(), a.Color(), b.Color())41 }42 before := bob.kicks.Load()43 a.Do(func(w *sheet.Workbook) { w.Sheets()[0].Set(addr("A1"), "ann's") })44 if bob.kicks.Load() == before {45 t.Error("bob wasn't told of ann's edit")46 }47 b.Do(func(w *sheet.Workbook) { w.Sheets()[0].Set(addr("B1"), "bob's") })48 var marks []Mark49 a.Do(func(*sheet.Workbook) { marks = a.Marks(0) })50 if len(marks) != 2 || marks[0].Name != "ann" || marks[1].Name != "bob" || marks[1].Rect.From != addr("B1") || marks[0].Seq >= marks[1].Seq {51 t.Fatalf("marks %+v", marks)52 }53 // Undo is each author's own.54 a.Do(func(w *sheet.Workbook) { w.Undo() })55 s := a.Book().Sheets()[0]56 if s.Filled(addr("A1")) || s.Value(addr("B1")).Str != "bob's" {57 t.Errorf("after ann's undo: A1 %v, B1 %v", s.Value(addr("A1")), s.Value(addr("B1")))58 }59}6061// Presence reaches the others; a turn that changes nothing tells no one.62func TestRoomPresence(t *testing.T) {63 g := NewRegistry(Edit)64 ann, bob := &who{name: "ann"}, &who{name: "bob"}65 a, _ := g.Join("@standup", ann, newBook)66 b, _ := g.Join("@standup", bob, nil)67 before := bob.kicks.Load()68 a.Do(func(*sheet.Workbook) {})69 if bob.kicks.Load() != before {70 t.Error("told of nothing")71 }72 a.Do(func(w *sheet.Workbook) { a.Move(Presence{Sheet: w.Sheets()[0], Cursor: addr("C3")}) })73 if bob.kicks.Load() == before {74 t.Error("not told ann moved")75 }76 var peers []Peer77 b.Do(func(*sheet.Workbook) { peers = b.Peers() })78 if len(peers) != 1 || peers[0].Name != "ann" || peers[0].Presence.Cursor != addr("C3") {79 t.Errorf("peers %+v", peers)80 }81}8283// In a View room one writes: the first in, until they hand it over or84// leave.85func TestRoomWriter(t *testing.T) {86 g := NewRegistry(View)87 a, _ := g.Join("k", &who{name: "ann"}, newBook)88 b, _ := g.Join("k", &who{name: "bob"}, nil)89 c, _ := g.Join("k", &who{name: "cy"}, nil)90 a.Do(func(*sheet.Workbook) {91 if !a.Writing() || b.Writing() || a.Writer() != "ann" {92 t.Errorf("writing: ann %v, bob %v", a.Writing(), b.Writing())93 }94 if b.HandTo(c.ID()) {95 t.Error("bob handed over what he doesn't hold")96 }97 if !a.HandTo(b.ID()) || !b.Writing() || a.Writing() {98 t.Error("ann didn't hand over to bob")99 }100 })101 b.Leave()102 a.Do(func(*sheet.Workbook) {103 if !a.Writing() {104 t.Error("the writer left, and ann, there longest, doesn't write")105 }106 })107}108109// What is posted goes to the keeper, the one there longest, and to the110// next one once the keeper leaves; the last to leave closes the room.111func TestRoomKeeperAndLeaving(t *testing.T) {112 g := NewRegistry(Edit)113 ann, bob := &who{name: "ann"}, &who{name: "bob"}114 a, _ := g.Join("k", ann, newBook)115 b, _ := g.Join("k", bob, nil)116 b.Post("done")117 var got []any118 b.Do(func(*sheet.Workbook) { got = b.Take() })119 if got != nil {120 t.Errorf("bob took %v", got)121 }122 if a.Leave() {123 t.Fatal("ann was last")124 }125 b.Do(func(*sheet.Workbook) { got = b.Take() })126 if len(got) != 1 || got[0] != "done" {127 t.Errorf("bob, keeper now, took %v", got)128 }129 if !g.Has("k") || !b.Leave() || g.Has("k") || g.Rooms() != 0 {130 t.Error("the last to leave didn't close the room")131 }132 // A save under another name makes the room that file's too.133 c, _ := g.Join("@r", &who{name: "cy"}, newBook)134 c.Do(func(*sheet.Workbook) { c.SetSaved(Saved{Name: "r.012", Key: "/srv/r.012"}) })135 if !g.Has("/srv/r.012") || g.Rooms() != 1 {136 t.Error("saving didn't alias the room")137 }138}139140// Many participants taking turns at once: every step lands, in one141// order, and the race detector finds nothing.142func TestRoomTurnsAtOnce(t *testing.T) {143 g := NewRegistry(Edit)144 var wg sync.WaitGroup145 const n, edits = 6, 50146 for i := range n {147 s, _ := g.Join("k", &who{name: fmt.Sprint(i)}, newBook)148 wg.Add(1)149 go func() {150 defer wg.Done()151 for j := range edits {152 s.Do(func(w *sheet.Workbook) {153 w.Sheets()[0].Set(sheet.Addr{Col: i, Row: j}, fmt.Sprint(j))154 s.Move(Presence{Cursor: sheet.Addr{Col: i, Row: j}})155 })156 }157 }()158 }159 wg.Wait()160 s, _ := g.Join("k", &who{name: "last"}, nil)161 s.Do(func(w *sheet.Workbook) {162 if got := w.Sheets()[0].Len(); got != n*edits {163 t.Errorf("%d cells", got)164 }165 if s.Last() != n*edits {166 t.Errorf("%d operations", s.Last())167 }168 })169}170171// bot is one of the agents participant.172type bot struct{ who }173174func (*bot) Agent() {}175176// The agent is marked as one to the others, keeps nothing the room runs177// even when it was there first, and a touch tells the others.178func TestRoomAgents(t *testing.T) {179 g := NewRegistry(Edit)180 claude := &bot{who{name: "claude"}}181 c, _ := g.Join("k", claude, newBook)182 ann := &who{name: "ann"}183 a, _ := g.Join("k", ann, nil)184 a.Do(func(*sheet.Workbook) {185 peers := a.Peers()186 if len(peers) != 1 || !peers[0].Agent || a.Agent() || !c.Agent() {187 t.Errorf("peers %+v", peers)188 }189 if !a.Keeper() || c.Keeper() {190 t.Error("the agent keeps the room")191 }192 if p, ok := a.Peer(c.ID()); !ok || p.Name != "claude" {193 t.Errorf("peer %+v", p)194 }195 })196 c.Post("ran")197 var got []any198 a.Do(func(*sheet.Workbook) { got = a.Take() })199 if len(got) != 1 {200 t.Errorf("ann took %v", got)201 }202 before := ann.kicks.Load()203 c.Do(func(*sheet.Workbook) { c.Touch() })204 if ann.kicks.Load() == before {205 t.Error("a touch didn't tell ann")206 }207 // The last person out closes the room, agents or not.208 if !a.Leave() || g.Has("k") {209 t.Error("ann, the last person, didn't close the room")210 }211 c.Do(func(*sheet.Workbook) {212 if !c.Closed() {213 t.Error("the agent's room isn't closed")214 }215 })216 if _, ok := g.JoinOpen("k", &bot{who{name: "late"}}); ok {217 t.Error("the agent joined a closed room")218 }219}