cat / 012
clone

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

cat/module-path
012 / internal / ui / sources_test.go
sources_test.go 8.7K
1package ui23import (4 "os"5 "path/filepath"6 "strings"7 "testing"89 "github.com/parquet-go/parquet-go"1011 "src.rest/cat/012/internal/room"12 "src.rest/cat/012/internal/sheet"13)1415// srcRow is a row of the Parquet files the tests link.16type srcRow struct {17 ID int64 `parquet:"id"`18 Region string `parquet:"region"`19 Amount float64 `parquet:"amount"`20}2122var srcRegions = []string{"North", "South", "East", "West"}2324// writeSource writes n rows to name: ids from 0, regions in turn, and25// amounts of id*3 mod 1000.26func writeSource(t testing.TB, name string, n int) (sum float64) {27 t.Helper()28 f, err := os.Create(name)29 if err != nil {30 t.Fatal(err)31 }32 w := parquet.NewGenericWriter[srcRow](f, parquet.MaxRowsPerRowGroup(500))33 rows := make([]srcRow, n)34 for i := range rows {35 rows[i] = srcRow{ID: int64(i), Region: srcRegions[i%len(srcRegions)], Amount: float64(i * 3 % 1000)}36 sum += rows[i].Amount37 }38 if _, err := w.Write(rows); err != nil {39 t.Fatal(err)40 }41 if err := w.Close(); err != nil {42 t.Fatal(err)43 }44 if err := f.Close(); err != nil {45 t.Fatal(err)46 }47 return sum48}4950// pumpSources runs what the sources have queued until nothing is.51func pumpSources(t *testing.T, m *Model) {52 t.Helper()53 if !settleSources(m) {54 t.Fatal("the sources never settled")55 }56}5758// linkSales writes sales.parquet with n rows here and links it.59func linkSales(t *testing.T, m *Model, n int) float64 {60 t.Helper()61 sum := writeSource(t, "sales.parquet", n)62 m.runCommand("data.link_source")63 press(t, m, "sales.parquet", "<enter>")64 pumpSources(t, m)65 m.note = ""66 return sum67}6869func TestSourceTab(t *testing.T) {70 t.Chdir(t.TempDir())71 m := newModel()72 linkSales(t, m, 3000)73 if !m.sheet.IsSource() || m.sheet.Name() != "sales" {74 t.Fatalf("the tab shown is %s", m.sheet.Name())75 }76 if l := line(m, contextLine); !strings.Contains(l, "▦ sales.parquet 3,000 rows, 3 columns") {77 t.Errorf("context line %q", l)78 }79 if l := line(m, menuLine); !strings.Contains(l, "SOURCE") {80 t.Errorf("menu bar %q", l)81 }82 if l := line(m, headerLine+1); !strings.Contains(l, "id") || !strings.Contains(l, "region") {83 t.Errorf("header row %q", l)84 }85 if l := line(m, headerLine+3); !strings.HasPrefix(strings.TrimSpace(l), "2 ") || !strings.Contains(l, "North") {86 t.Errorf("first row %q", l)87 }88 if st := status(m); !strings.Contains(st, "▦ sales") {89 t.Errorf("status %q", st)90 }91 // The last row, numbered as the sheet's row it is.92 press(t, m, "<ctrl+down>")93 pumpSources(t, m)94 if l := screen(m); !strings.Contains(l, "3001 ") {95 t.Errorf("the last row isn't shown:\n%s", l)96 }97 if name := strings.Fields(line(m, formulaLine))[0]; name != "A3001" {98 t.Errorf("name box %q", name)99 }100 if bar(m) != "2999" {101 t.Errorf("formula bar %q", bar(m))102 }103 // Go to, a row or a cell as the tab numbers them.104 press(t, m, "<ctrl+g>", "b2500", "<enter>")105 pumpSources(t, m)106 if name := strings.Fields(line(m, formulaLine))[0]; name != "B2500" || bar(m) != "East" { // row 2500 is the source's 2498th, counting from 0107 t.Errorf("go to B2500: %q %q", name, bar(m))108 }109 // Typing doesn't edit.110 press(t, m, "7", "<enter>")111 if !m.sheet.IsSource() {112 t.Error("typing left the tab")113 }114}115116func TestSourceSortAndFilter(t *testing.T) {117 t.Chdir(t.TempDir())118 m := newModel()119 linkSales(t, m, 2000)120 press(t, m, "<right>", "<right>") // amount121 m.runCommand("data.sort_sheet_za")122 pumpSources(t, m)123 if l := line(m, contextLine); !strings.Contains(l, "sorted by amount Z to A") {124 t.Errorf("context line %q", l)125 }126 if bar(m) != "999" {127 t.Errorf("the top amount sorted Z to A is %q", bar(m))128 }129 info, _ := m.sheet.Source()130 o := copyOrder(info.Source.Order)131 o.Filter = []sheet.SourceFilter{{Col: 1, Cond: sheet.Condition{Op: sheet.CondExactly, Arg: "east"}}}132 m.setSourceOrder(info, o)133 pumpSources(t, m)134 if l := line(m, contextLine); !strings.Contains(l, "showing 500") {135 t.Errorf("context line %q", l)136 }137 press(t, m, "<left>")138 if bar(m) != "East" {139 t.Errorf("the filter lets %q through", bar(m))140 }141 m.runCommand("data.source_all")142 pumpSources(t, m)143 if bar(m) != "North" {144 t.Errorf("in the source's order the first region is %q", bar(m))145 }146 // Undo puts the order back.147 press(t, m, "<ctrl+z>")148 pumpSources(t, m)149 if bar(m) != "East" {150 t.Errorf("after undo, %q", bar(m))151 }152}153154func TestFormulasReadASourceTab(t *testing.T) {155 t.Chdir(t.TempDir())156 m := newModel()157 sum := linkSales(t, m, 5000)158 m.showSheet(m.book().Sheet(0))159 press(t, m, "=SUM(sales[amount])", "<enter>", `=COUNTIF(sales[region],"west")`, "<enter>", "=sales!C4", "<enter>")160 pumpSources(t, m)161 if v := m.sheet.Value(addr("A1")); v.Num != sum {162 t.Errorf("SUM %v, want %v", v, sum)163 }164 if v := m.sheet.Value(addr("A2")); v.Num != 1250 {165 t.Errorf("COUNTIF %v", v)166 }167 if v := m.sheet.Value(addr("A3")); v.Num != 6 {168 t.Errorf("sales!C4 %v", v)169 }170}171172func TestSourceSortRecorded(t *testing.T) {173 t.Chdir(t.TempDir())174 m := newModel()175 linkSales(t, m, 100)176 src := record(t, m, false, "Top", "", "<right>", "<right>", "<ctrl+k>", "sort sheet z to a", "<enter>")177 if !strings.Contains(src, `run("data.sort_sheet_za")`) {178 t.Errorf("the macro:\n%s", src)179 }180}181182func TestSourceCommandsOnTheTab(t *testing.T) {183 t.Chdir(t.TempDir())184 m := newModel()185 linkSales(t, m, 100)186 if commands["format.bold"].available(m) {187 t.Error("Bold is offered on a source's tab")188 }189 m.runCommand("data.frequency")190 pumpSources(t, m)191 if !strings.HasPrefix(m.sheet.Name(), "Frequency of") {192 t.Fatalf("frequency table on %s", m.sheet.Name())193 }194 if v := m.sheet.Value(addr("B2")); v.Num != 1 {195 t.Errorf("each id is there once: %v", v)196 }197}198199// The files' ticks come at once in tests, whose ticks are dropped.200func init() { sourceInterval = 0 }201202// A source linked in a room is the room's: the one who keeps the room203// reads it and answers what anyone's formulas ask of it, each204// participant scrolls a tab of their own, and whoever keeps the room205// next goes on reading it.206func TestSharedSources(t *testing.T) {207 r := newRooms(t, room.Edit)208 sum := writeSource(t, filepath.Join(r.dir, "sales.parquet"), 1000)209 ann := r.open("ann", "@src")210 bob := r.open("bob", "@src")211 bob.run(bob.m.runCommand("data.link_source"))212 bob.press("sales.parquet", "<enter>")213 for range 5 {214 r.sync()215 }216 if !strings.Contains(bob.screen(), "North") {217 t.Fatalf("bob's tab:\n%s", bob.screen())218 }219 bob.press("<ctrl+down>")220 r.sync()221 if !strings.Contains(bob.screen(), "1001 ") || strings.Contains(ann.screen(), "1001 ") {222 t.Errorf("bob's scroll isn't his own:\n%s\n%s", bob.screen(), ann.screen())223 }224 bob.press("<ctrl+pgup>", "=SUM(sales[amount])", "<enter>")225 for range 5 {226 r.sync()227 }228 var got float64229 ann.sh.turn(func() { got = ann.m.book().Sheet(0).Value(addr("A1")).Num })230 if got != sum {231 t.Fatalf("SUM %v, want %v", got, sum)232 }233 if ann.m.LeaveRoom() {234 t.Fatal("ann was the last")235 }236 r.all = r.all[1:]237 r.sync()238 bob.press(`=COUNTIF(sales[region],"east")`, "<enter>")239 for range 5 {240 r.sync()241 }242 bob.sh.turn(func() { got = bob.m.book().Sheet(0).Value(addr("A2")).Num })243 if got != 250 {244 t.Errorf("bob, keeping the room now, answered COUNTIF with %v", got)245 }246}247248// In a room of one writer, whoever follows may scroll a source's tab249// but not sort it.250func TestSharedSourceOneWriter(t *testing.T) {251 r := newRooms(t, room.View)252 writeSource(t, filepath.Join(r.dir, "sales.parquet"), 100)253 ann := r.open("ann", "@one")254 bob := r.open("bob", "@one")255 ann.run(ann.m.runCommand("data.link_source"))256 ann.press("sales.parquet", "<enter>")257 for range 3 {258 r.sync()259 }260 bob.press("<ctrl+pgdown>", "<down>")261 bob.run(bob.m.runCommand("data.sort_sheet_za"))262 var order *sheet.SourceOrder263 bob.sh.turn(func() {264 info, _ := bob.m.book().LookupSource("sales")265 order = info.Source.Order266 })267 if order != nil || !strings.Contains(bob.screen(), "writes here and you follow") {268 t.Errorf("a follower sorted the source: %+v\n%s", order, bob.screen())269 }270}271272func TestMonochromeSource(t *testing.T) {273 t.Chdir(t.TempDir())274 m := newModel()275 linkSales(t, m, 3000)276 m.note = ""277 row := monoLine(m, headerLine+3) // the first row, its first cell active278 if !every(row[6:12], false, isReverse) {279 t.Errorf("the active cell isn't in reverse video: %+v", row[6:12])280 }281 if last := row[len(row)-1]; last.text != "┃" {282 t.Errorf("the scrollbar's thumb at the top is %q", last.text)283 }284 if last := monoLine(m, headerLine+6); last[len(last)-1].text != "│" {285 t.Errorf("the scrollbar's track is %q", last[len(last)-1].text)286 }287 if l := cellText(monoLine(m, contextLine)); !strings.Contains(l, "▦ sales.parquet") {288 t.Errorf("context line: %q", l)289 }290 if err := os.Remove("sales.parquet"); err != nil {291 t.Fatal(err)292 }293 m.sources().host.Reload("sales")294 pumpSources(t, m)295 if l := cellText(monoLine(m, m.height-1)); !strings.Contains(l, "▦ sales ! ") {296 t.Errorf("status line of a source whose file is gone: %q", l)297 }298}