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 / region_test.go
region_test.go 10.5K
1package sheet23import (4 "bytes"5 "strings"6 "testing"78 "src.rest/cat/012/internal/notebook"9)1011// sent adds a notebook cell named name to a new notebook tab and sends12// its output to s at a, returning the notebook.13func sent(t *testing.T, s *Sheet, name, a string) *Sheet {14 t.Helper()15 w := s.Book()16 nb := w.Notebook()17 if nb == nil {18 var err error19 if nb, err = w.AddNotebook("", s); err != nil {20 t.Fatal(err)21 }22 }23 nb.SetNotebookCells("add cell", append(nb.NotebookCells(), notebook.Cell{Source: name + " = ls"}))24 if err := s.AddRegion(Region{Name: name, At: at(a), Output: true}); err != nil {25 t.Fatal(err)26 }27 return nb28}2930func TestSentOutputFollowsRuns(t *testing.T) {31 s := New()32 w := s.Book()33 sent(t, s, "files", "B2")34 other, _ := w.AddSheet("Sheet2", 1)35 other.Set(at("A1"), "=SUM(nu.files)")36 wantShown(t, other, map[string]string{"A1": "#REF!"}) // not run yet37 apply(t, s, LiveOp{Region: "files", Reset: true, Header: liveRow("n", "size"), Rows: []LiveRow{liveRow("a", "1"), liveRow("b", "2")}})38 wantShown(t, s, map[string]string{"B2": "n", "C3": "1", "B4": "b", "B1": ""})39 wantShown(t, other, map[string]string{"A1": "3"})40 // A run that gives fewer rows clears the rest.41 apply(t, s, LiveOp{Region: "files", Reset: true, Header: liveRow("n", "size"), Rows: []LiveRow{liveRow("c", "10")}})42 wantShown(t, s, map[string]string{"B3": "c", "B4": ""})43 wantShown(t, other, map[string]string{"A1": "10"})44 if err := s.Set(at("B3"), "x"); err != ErrOutputEdit {45 t.Errorf("typing over an output: %v", err)46 }47 if w.UndoLabel() != "edit A1" {48 t.Errorf("rows arriving made an undo step: %q", w.UndoLabel())49 }50}5152// Undoing a deleted column puts back the cells a region moved over and53// was sent again at, rather than keep the region's values there.54func TestUndoPutsBackCellsARegionMovedOver(t *testing.T) {55 s := New()56 w := s.Book()57 s.Set(at("D19"), "=SEQUENCE(2)")58 sent(t, s, "r1", "E19")59 op := LiveOp{Region: "r1", Reset: true, Header: liveRow("n"), Rows: []LiveRow{liveRow("3")}}60 apply(t, s, op)61 s.DeleteCols(1, 1)62 apply(t, s, op) // sent again where it moved, as the UI does63 wantShown(t, s, map[string]string{"C19": "1", "D19": "n"})64 w.Undo()65 apply(t, s, op)66 wantShown(t, s, map[string]string{"D19": "1", "D20": "2", "E19": "n", "E20": "3"})67}6869// A formula naming a region whose table is blocked reads #REF!, even70// when the table it showed was only its header, where the region's71// first cell stays.72func TestFormulaNamingABlockedRegion(t *testing.T) {73 s := New()74 s.Set(at("D1"), "=SUM(nu.r1)")75 s.Set(at("F12"), "5")76 sent(t, s, "r1", "E12")77 apply(t, s, LiveOp{Region: "r1", Reset: true, Header: liveRow("n")})78 wantShown(t, s, map[string]string{"D1": "0"})79 apply(t, s, LiveOp{Region: "r1", Reset: true, Header: liveRow("n", "m"), Rows: []LiveRow{liveRow("1", "2")}})80 wantShown(t, s, map[string]string{"E12": "#REF!", "D1": "#REF!"})81}8283// Undoing a move puts back the cell a region's table grew over once the84// cell left, and the region, sent again, is blocked by it as before.85func TestUndoPutsBackCellsARegionGrewOver(t *testing.T) {86 s := New()87 w := s.Book()88 s.Set(at("F14"), "=1+1")89 sent(t, s, "r1", "E13")90 op := LiveOp{Region: "r1", Reset: true, Header: liveRow("n", "m"), Rows: []LiveRow{liveRow("1", "2")}}91 apply(t, s, op)92 wantShown(t, s, map[string]string{"E13": "#REF!", "F14": "2"})93 if _, err := s.Move(rect("F14"), at("A1")); err != nil {94 t.Fatal(err)95 }96 apply(t, s, op)97 wantShown(t, s, map[string]string{"E13": "n", "F14": "2", "A1": "2"})98 w.Undo()99 apply(t, s, op)100 wantShown(t, s, map[string]string{"E13": "#REF!", "F14": "2", "A1": ""})101 if s.Cell(at("F14")).Input != "=1+1" {102 t.Errorf("F14 = %q", s.Cell(at("F14")).Input)103 }104}105106// A region whose column is deleted is gone, and formulas naming it say107// so, as after reopening.108func TestFormulaNamingARegionDeleted(t *testing.T) {109 s := New()110 s.Set(at("D1"), "=SUM(nu.r1)")111 sent(t, s, "r1", "H3")112 apply(t, s, LiveOp{Region: "r1", Reset: true, Header: liveRow("n"), Rows: []LiveRow{liveRow("56")}})113 wantShown(t, s, map[string]string{"D1": "56"})114 s.DeleteCols(7, 2)115 wantShown(t, s, map[string]string{"D1": "#NAME?"})116}117118func TestSentOutputNames(t *testing.T) {119 s := New()120 w := s.Book()121 sent(t, s, "files", "A1")122 if err := s.AddRegion(Region{Name: "files", At: at("D1"), Output: true}); err == nil {123 t.Error("two regions share a name")124 }125 if _, err := s.AddLinked(at("F1"), LinkSource{Path: "files.csv"}); err != nil {126 t.Fatal(err)127 }128 if _, r, _ := w.Region("files_2"); !r.Linked() {129 t.Error("a linked file took a cell's name")130 }131 if got := w.FreeCellName("files"); got != "files_3" {132 t.Errorf("a free name like files: %q", got)133 }134 if err := w.DefineName("nu.big", s, rect("A9")); err != nil {135 t.Fatal(err)136 }137 if err := s.AddRegion(Region{Name: "big", At: at("H1"), Output: true}); err == nil {138 t.Error("a region took a named range's name")139 }140}141142// Undo puts a region back emptied and stale, for its source to send its143// rows again; moving it with inserted rows does the same.144func TestSentOutputUndoAndShift(t *testing.T) {145 s := New()146 w := s.Book()147 sent(t, s, "t", "A2")148 apply(t, s, LiveOp{Region: "t", Reset: true, Header: liveRow("n"), Rows: []LiveRow{liveRow("1")}})149 if err := s.DeleteRegion("t"); err != nil {150 t.Fatal(err)151 }152 wantShown(t, s, map[string]string{"A2": "", "A3": ""})153 w.Undo()154 if !s.regionMeta[nameKey("t")].stale {155 t.Error("the region undo put back isn't stale")156 }157 apply(t, s, LiveOp{Region: "t", Reset: true, Header: liveRow("n"), Rows: []LiveRow{liveRow("1")}})158 s.InsertRows(0, 2)159 if _, r, _ := w.Region("t"); r.At != at("A4") {160 t.Errorf("after inserting rows the region is at %v", r.At)161 }162 if !s.regionMeta[nameKey("t")].stale {163 t.Error("the region moved isn't stale")164 }165 if err := s.FreezeRegion("t"); err != nil {166 t.Fatal(err)167 }168}169170func TestNotebookCellsUndo(t *testing.T) {171 w := NewBook()172 nb, err := w.AddNotebook("", w.Sheet(0))173 if err != nil || nb.Name() != "Notebook" || !nb.IsNotebook() {174 t.Fatalf("%v %v", nb, err)175 }176 nb.SetNotebookCells("add cell", []notebook.Cell{{Source: "ls"}})177 cells := nb.NotebookCells()178 if len(cells) != 1 || cells[0].ID == 0 {179 t.Fatalf("cells %+v", cells)180 }181 nb.SetNotebookCells("edit cell 1", []notebook.Cell{{ID: cells[0].ID, Source: "files = ls"}})182 w.Undo()183 if got := nb.NotebookCells(); got[0].Source != "ls" {184 t.Errorf("after undo: %+v", got)185 }186 nb.SetReactive(true)187 w.Undo()188 if nb.Reactive() {189 t.Error("undo left the notebook reactive")190 }191 if w.Notebook() != nb {192 t.Error("the workbook's notebook")193 }194}195196func writeBook(t *testing.T, w *Workbook) string {197 t.Helper()198 var b bytes.Buffer199 if err := w.Write(&b); err != nil {200 t.Fatal(err)201 }202 return b.String()203}204205func TestNotebookRoundTrip(t *testing.T) {206 w := NewBook()207 nb, _ := w.AddNotebook("", w.Sheet(0))208 nb.SetNotebookCells("add cells", []notebook.Cell{209 {Kind: notebook.Note, Source: "# Files"},210 {Source: "files = ls"},211 {Source: "$files | where size > 1kb"},212 {Source: "nope"},213 })214 nb.SetReactive(true)215 cells := nb.NotebookCells()216 w.SetOutput(cells[1].ID, &notebook.Output{NUON: []byte(`[[name, size]; [a, 2kb]]`), Count: 1, Source: cells[1].Source})217 w.SetOutput(cells[2].ID, &notebook.Output{NUON: []byte(strings.Repeat("x", 100)), Count: 2})218 w.SetOutput(cells[3].ID, &notebook.Output{Err: "Command `nope` not found", Detail: "help: try ls"})219 w.SetOutputCaps(notebook.Caps{Cell: 50, Total: 1000})220 if got := w.UnsavedOutputs(); len(got) != 1 || got[0] != "cell 3" {221 t.Errorf("unsaved %v", got)222 }223 text := writeBook(t, w)224 for _, want := range []string{`"tab": "notebook"`, `"reactive": true`, `{"kind":"note","source":"# Files"}`,225 `"output":"[[name, size]; [a, 2kb]]"`, `"unsaved":true`, `"error":"Command`} {226 if !strings.Contains(text, want) {227 t.Errorf("the file lacks %s:\n%s", want, text)228 }229 }230 back, err := ReadBook(strings.NewReader(text))231 if err != nil {232 t.Fatal(err)233 }234 nb2 := back.Notebook()235 if nb2 == nil || !nb2.Reactive() || nb2.Name() != "Notebook" {236 t.Fatalf("read back %v", nb2)237 }238 got := nb2.NotebookCells()239 if len(got) != 4 || got[0].Kind != notebook.Note || got[1].Source != "files = ls" {240 t.Fatalf("cells %+v", got)241 }242 if o := back.Output(got[1].ID); o == nil || string(o.NUON) != `[[name, size]; [a, 2kb]]` || o.Count != 0 {243 t.Errorf("output of files: %+v", o)244 }245 if o := back.Output(got[2].ID); o == nil || !o.Unsaved || o.NUON != nil {246 t.Errorf("the output over the cap: %+v", o)247 }248 if o := back.Output(got[3].ID); o == nil || o.Err == "" || o.Detail != "help: try ls" {249 t.Errorf("the error: %+v", o)250 }251 if st := notebook.Stale(got, back.Output); len(st) != 0 {252 t.Errorf("stale on opening: %v", st)253 }254 if back.OutputsChanged() != 0 {255 t.Error("reading outputs counts as a change")256 }257 // Written again, it's the same file.258 if again := writeBook(t, back); again != text {259 t.Errorf("written again:\n%s\nwas\n%s", again, text)260 }261}262263// A notebook sheet of an earlier build opens with its commands as code264// cells of a notebook tab, their tables sent where they were.265func TestOldNotebookConverted(t *testing.T) {266 old := `{"version": 4, "sheets": [267 {"name": "Shell 1", "notebook": true, "cells": {}, "regions": [268 {"name": "r1", "command": "ls", "at": "A1", "rows": 3, "cols": 2},269 {"name": "big", "command": "$r1 | where size > 1kb", "at": "A6", "reads": ["r1"]},270 {"name": "sel", "command": "$in | math sum", "at": "A9", "input": "Data 1!A1:A4"},271 {"name": "two", "command": "let x = 1\n$x + 1", "at": "A12"}272 ]},273 {"name": "Data 1", "cells": {"B1": "=SUM(nu.big)"}}274]}`275 w, err := ReadBook(strings.NewReader(old))276 if err != nil {277 t.Fatal(err)278 }279 nb := w.Notebook()280 if nb == nil || w.Index(nb) != 1 || nb.Name() != "Notebook" {281 t.Fatalf("notebook %v at %d", nb, w.Index(nb))282 }283 cells := nb.NotebookCells()284 want := []string{"r1 = ls", "big = $r1 | where size > 1kb", "sel = $sheet.'Data 1'!A1:A4 | do {\n$in | math sum\n}", "two = do {\nlet x = 1\n$x + 1\n}"}285 for i, c := range cells {286 if c.Source != want[i] {287 t.Errorf("cell %d: %q, want %q", i+1, c.Source, want[i])288 }289 }290 shell := w.Lookup("Shell 1")291 if shell.IsNotebook() {292 t.Error("the old notebook sheet is a notebook tab")293 }294 if _, r, ok := w.Region("big"); !ok || !r.Output || r.At != at("A7") {295 t.Errorf("big's table: %+v %v", r, ok)296 }297 if notes := w.LoadNotes(); len(notes) != 1 || !strings.Contains(notes[0], "Shell 1") {298 t.Errorf("notes %v", notes)299 }300 apply(t, shell, LiveOp{Region: "big", Reset: true, Header: liveRow("n"), Rows: []LiveRow{liveRow("4")}})301 wantShown(t, w.Lookup("Data 1"), map[string]string{"B1": "4"})302 if text := writeBook(t, w); strings.Contains(text, `"command"`) || !strings.Contains(text, `"output":true`) {303 t.Errorf("saved again:\n%s", text)304 }305}