cat / 012
clone

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

main
012 / internal / ui / vast_test.go
vast_test.go 3.1K
1package ui23import (4 "fmt"5 "maps"6 "os"7 "runtime"8 "slices"9 "strings"10 "testing"11 "time"1213 tea "charm.land/bubbletea/v2"1415 "src.rest/cat/012/internal/sheet"16)1718// The guardrail against work proportional to the grid: with the whole19// sheet (or a whole column, or a whole row) selected on a nearly empty20// sheet of a million rows by 16,384 columns, every command that can run21// does, and the frame after it draws, in a small bound of time and22// memory. A command that walked every address of its selection would23// visit 17 billion cells here, and one that walked a whole column a24// million: either fails.25func TestCommandsCostTheDataNotTheGrid(t *testing.T) {26 if testing.Short() {27 t.Skip("runs every command three times")28 }29 skip := map[string]bool{30 "quit": true, // the session, not the sheet31 "settings.config_edit": true, "settings.config_reload": true, "settings.jev_key": true,32 }33 const maxTime, maxAlloc = 250 * time.Millisecond, 8 << 20 // a whole column of 8-byte entries is 8 MB34 selections := []struct {35 name string36 keys []string37 }{38 {"sheet", []string{"<ctrl+a>", "<ctrl+a>"}},39 {"column", []string{"<ctrl+space>"}},40 {"row", []string{"<shift+space>"}},41 }42 var report strings.Builder43 for _, id := range slices.Sorted(maps.Keys(commands)) {44 if skip[id] || strings.HasPrefix(id, "file.") {45 continue46 }47 for _, sel := range selections {48 m := vastModel(t)49 press(t, m, sel.keys...)50 if !commands[id].available(m) {51 continue52 }53 if r := m.selection(); sel.name != "row" && !r.AllRows() || sel.name != "column" && !r.AllCols() {54 t.Fatalf("%s selected %v", sel.name, r)55 }56 // Then Enter, which accepts what a dialog the command opened57 // offers (a sort, a filter, a chart), and a frame.58 d, alloc := measure(func() {59 run(m, commands[id].run(m))60 m.View()61 press(t, m, "<enter>")62 m.View()63 })64 fmt.Fprintf(&report, "%-28s %-7s %8.2f ms %9d B\n", id, sel.name, float64(d.Microseconds())/1000, alloc)65 if d > maxTime || alloc > maxAlloc {66 t.Errorf("%s over the whole %s took %v and allocated %d bytes", id, sel.name, d, alloc)67 }68 }69 }70 if testing.Verbose() {71 t.Log("\n" + report.String())72 }73}7475// vastModel is a model on a sheet with the full grid and a few cells,76// clear of the top row and the first column (so filling down or right77// from them has nothing to copy), in a temporary directory.78func vastModel(t *testing.T) *Model {79 t.Helper()80 dir := t.TempDir()81 wd, _ := os.Getwd()82 os.Chdir(dir)83 t.Cleanup(func() { os.Chdir(wd) })84 s := sheet.New()85 for _, in := range []struct{ a, v string }{{"B2", "Name"}, {"C2", "Qty"}, {"B3", "pen"}, {"C3", "3"}, {"B4", "ink"}, {"C4", "=C3*2"}, {"D5", "=SUM(C:C)"}} {86 s.Set(addr(in.a), in.v)87 }88 s.ClearHistory()89 m := New(s, "")90 m.Update(tea.WindowSizeMsg{Width: 200, Height: 60})91 return m92}9394// measure runs fn and returns how long it took and how much it95// allocated.96func measure(fn func()) (time.Duration, uint64) {97 var before, after runtime.MemStats98 runtime.GC()99 runtime.ReadMemStats(&before)100 start := time.Now()101 fn()102 d := time.Since(start)103 runtime.ReadMemStats(&after)104 return d, after.TotalAlloc - before.TotalAlloc105}