cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / sheet / randedit_shared_test.go
randedit_shared_test.go 4.8K
1package sheet23import (4 "bytes"5 "fmt"6 "maps"7 "math/rand/v2"8 "testing"9)1011// Random edits by several participants: the edits of TestRandomEdits,12// each made by one of three authors drawn from the seed, interleaved in13// one workbook whose history is shared, as a room of 012 serve orders14// them, and among them the agent's (live mode): its edits are proposals15// (Propose), accepted whole, cell by cell or not at all, as drawn from16// the seed. Then the authors, the agent among them, undo, in turns drawn from the seed, each only17// their own steps, from under the others' where nothing later overlaps18// them. What must hold:19//20// - someone can always undo (the latest step's author, at least), so21// every step is undone in the end;22// - an undo that goes ahead, redone at once by its author, gives back23// exactly the file it undid;24// - once every step is undone, whatever the order, the workbook is25// back where it started: the file it saves and every value.26func TestRandomSharedEdits(t *testing.T) {27 seeds := max(*randSeeds/4, 1)28 if testing.Short() {29 seeds = min(seeds, 20)30 }31 for seed := range seeds {32 rng := rand.New(rand.NewPCG(uint64(seed), 34))33 data := make([]byte, 64+(*randSteps)*10)34 for i := range data {35 data[i] = byte(rng.Uint32())36 }37 t.Run(fmt.Sprint(seed), func(t *testing.T) {38 msg, log := randomSharedEdits(data, *randSteps, rng)39 if msg != "" {40 t.Fatalf("%s\nafter %d edits:\n %s", msg, len(log), joinLog(log))41 }42 })43 }44}4546// randAuthors is how many people edit; the agent is one more.47const (48 randAuthors = 349 randAgent = randAuthors + 150)5152// suggest has the agent propose an edit, made on a copy, and accepts it53// whole, some of its cells or none, as rng draws.54func (r *randBook) suggest(e *edits, rng *rand.Rand) string {55 from := *e56 var label string57 edit := func(w *Workbook) error {58 b := r59 if w != r.wb {60 b = &randBook{wb: w, nb: w.Lookup(r.nb.Name()), feeds: maps.Clone(r.feeds)}61 }62 d := from63 label = b.edit(&d)64 *e = d65 return nil66 }67 p, err := Propose(r.wb, "suggestion", edit)68 if err != nil {69 return fmt.Sprintf("%d suggests %q: %v", randAgent, label, err)70 }71 var pick func(int) bool72 what := "accepted"73 switch rng.IntN(3) {74 case 0:75 return fmt.Sprintf("%d suggests %q: rejected", randAgent, label)76 case 1:77 if !p.Whole() {78 pick, what = func(int) bool { return rng.IntN(2) == 0 }, "accepted in part"79 }80 }81 if err := p.Apply(r.wb, pick); err != nil {82 what = err.Error()83 }84 return fmt.Sprintf("%d suggests %q: %s", randAgent, label, what)85}8687func randomSharedEdits(data []byte, steps int, rng *rand.Rand) (string, []string) {88 e := &edits{data: data}89 r := newRandBook(e)90 r.wb.ShareHistory()91 start, startVals, startFeeds := r.save(), bookValues(r.wb), maps.Clone(r.feeds)92 var log []string93 for range steps {94 if e.done() {95 break96 }97 a := 1 + e.n(randAuthors+1)98 r.wb.SetAuthor(a)99 if a == randAgent {100 log = append(log, r.suggest(e, rng))101 } else {102 log = append(log, fmt.Sprintf("%d: %s", a, r.edit(e)))103 }104 feedStale(r.wb, r.feeds)105 }106 for undone := 0; ; undone++ {107 a, ok := r.nextUndo(rng)108 if !ok {109 break110 }111 r.wb.SetAuthor(a)112 label := r.wb.UndoLabel()113 before := r.save()114 if _, ok := r.wb.Undo(); !ok {115 return fmt.Sprintf("author %d's undo of %q refused though nothing blocks it", a, label), log116 }117 feedStale(r.wb, r.feeds)118 log = append(log, fmt.Sprintf("%d undoes %q", a, label))119 if rng.IntN(3) > 0 {120 continue121 }122 if _, ok := r.wb.Redo(); !ok {123 return fmt.Sprintf("author %d can't redo %q at once", a, label), log124 }125 feedStale(r.wb, r.feeds)126 if got := r.save(); !bytes.Equal(got, before) {127 return fmt.Sprintf("author %d's redo of %q saves differently:\n%s", a, label, lineDiff(string(before), string(got))), log128 }129 r.wb.Undo()130 feedStale(r.wb, r.feeds)131 }132 feedAll(r.wb, startFeeds)133 feedStale(r.wb, startFeeds)134 if got := r.save(); !bytes.Equal(got, start) {135 return "undoing every step saves differently:\n" + lineDiff(string(start), string(got)), log136 }137 if d := diffValues(startVals, bookValues(r.wb)); d != "" {138 return "undoing every step: " + d, log139 }140 return "", log141}142143// nextUndo picks an author who can undo a step now, in a turn drawn144// from rng, or reports false when nobody has a step left. It fails the145// run when steps are left but nobody can undo one.146func (r *randBook) nextUndo(rng *rand.Rand) (int, bool) {147 first := rng.IntN(randAgent)148 left := false149 for i := range randAgent {150 a := 1 + (first+i)%randAgent151 r.wb.SetAuthor(a)152 if !r.wb.CanUndo() {153 continue154 }155 left = true156 if _, blocked := r.wb.UndoBlocked(); !blocked {157 return a, true158 }159 }160 if left {161 panic("steps are left that nobody can undo")162 }163 return 0, false164}165166func joinLog(log []string) string {167 var b bytes.Buffer168 for i, l := range log {169 if i > 0 {170 b.WriteString("\n ")171 }172 b.WriteString(l)173 }174 return b.String()175}