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 / macro / macro_test.go
macro_test.go 8.1K
1package macro23import (4 "errors"5 "fmt"6 "strings"7 "testing"8 "time"9)1011// fakeHost records the calls it gets and answers from a few cells.12type fakeHost struct {13 calls []string14 cells map[string]any15}1617func (f *fakeHost) log(format string, args ...any) error {18 f.calls = append(f.calls, fmt.Sprintf(format, args...))19 return nil20}2122func (f *fakeHost) Values(ref string) ([][]any, error) {23 if ref == "bad" {24 return nil, errors.New("not a cell or range: bad")25 }26 if strings.Contains(ref, ":") {27 return [][]any{{f.cells["A1"], f.cells["B1"]}}, nil28 }29 return [][]any{{f.cells[ref]}}, nil30}31func (f *fakeHost) Formulas(ref string) ([][]string, error) { return [][]string{{"=1+1"}}, nil }32func (f *fakeHost) SetInput(ref, in string) error {33 f.cells[ref] = in34 return f.log("set %s %q", ref, in)35}36func (f *fakeHost) SetInputs(ref string, rows [][]string) error { return f.log("set %s %q", ref, rows) }37func (f *fakeHost) SetFormula(ref, fm string) error { return f.log("formula %s %s", ref, fm) }38func (f *fakeHost) Clear(ref string) error { return f.log("clear %q", ref) }39func (f *fakeHost) NumberFormat(string) (string, int, error) { return "currency", 2, nil }40func (f *fakeHost) SetNumberFormat(ref, kind string, d int, p string) error {41 return f.log("format %s %s %d %q", ref, kind, d, p)42}43func (f *fakeHost) Selection() (string, string) { return "B2:C3", "B2" }44func (f *fakeHost) Select(ref, active string) error { return f.log("select %s %q", ref, active) }45func (f *fakeHost) Move(c, r int) error { return f.log("move %d %d", c, r) }46func (f *fakeHost) Extend(c, r int, whole string) error { return f.log("extend %d %d %q", c, r, whole) }47func (f *fakeHost) Jump(to string, ext bool) error { return f.log("jump %s %v", to, ext) }48func (f *fakeHost) Enter(text string, fill bool, origin string) error {49 return f.log("enter %q %v %q", text, fill, origin)50}51func (f *fakeHost) PasteText(text string) error { return f.log("paste %q", text) }52func (f *fakeHost) Sheets() []string { return []string{"Sheet1", "Two"} }53func (f *fakeHost) ActiveSheet() string { return "Sheet1" }54func (f *fakeHost) ActivateSheet(name string) error { return f.log("activate %s", name) }55func (f *fakeHost) AddSheet(name string) (string, error) { return "Sheet3", f.log("add %q", name) }56func (f *fakeHost) MoveSheet(pos int) error { return f.log("move sheet %d", pos) }57func (f *fakeHost) Run(id string, answer *string) error {58 if answer == nil {59 return f.log("run %s", id)60 }61 return f.log("run %s %q", id, *answer)62}63func (f *fakeHost) SetWidth(cols string, w int) error { return f.log("width %s %d", cols, w) }64func (f *fakeHost) SetHeight(rows string, h int) error { return f.log("height %s %d", rows, h) }65func (f *fakeHost) Fill(to string, rows, cols int) error {66 return f.log("fill %q %d %d", to, rows, cols)67}6869func newFake() *fakeHost { return &fakeHost{cells: map[string]any{"A1": 2.0, "B1": "x", "C1": 2.5}} }7071func TestSourceRunsAsRecorded(t *testing.T) {72 actions := []Action{73 Call("select", "B2"),74 Call("enter", "=SUM(A1:A3)"),75 Call("move", 0, 1),76 Call("extend", 2, 0).With("whole", "columns"),77 Call("jump", "down").With("extend", true),78 Call("enter", "total").With("fill", true),79 Call("enter", "=A1").With("origin", "B2"),80 Call("paste_text", "a\tb\n\"c\""),81 Note("not recorded: sort A1:C9"),82 Call("run", "column.width").With("answer", "12"),83 Call("run", "format.bold"),84 Call("activate_sheet", "Q3 plan"),85 Call("set_width", "B:C", 14),86 Call("set_height", "3:4", 2),87 Call("fill").With("rows", 3),88 Call("move_sheet", 2),89 }90 src := Source("Recorded with absolute references.\n\nSecond line", actions)91 if !strings.HasPrefix(src, "# Recorded with absolute references.\n#\n# Second line\nselect(\"B2\")\n") {92 t.Errorf("source:\n%s", src)93 }94 f := newFake()95 if _, err := New("m", src, Env{Host: f}).Exec(); err != nil {96 t.Fatalf("%v\n%s", err, src)97 }98 want := []string{99 `select B2 ""`, `enter "=SUM(A1:A3)" false ""`, "move 0 1", `extend 2 0 "columns"`, "jump down true",100 `enter "total" true ""`, `enter "=A1" false "B2"`, `paste "a\tb\n\"c\""`, `run column.width "12"`, "run format.bold",101 "activate Q3 plan", "width B:C 14", "height 3:4 2", `fill "" 3 0`, "move sheet 2",102 }103 if strings.Join(f.calls, "\n") != strings.Join(want, "\n") {104 t.Errorf("calls:\n%s\nwant:\n%s", strings.Join(f.calls, "\n"), strings.Join(want, "\n"))105 }106}107108func TestScriptAPI(t *testing.T) {109 src := `110a = get("A1")111b = get("B1")112row = get("A1:B1")113set("D1", a * 3)114set("D2", get("C1") * 2)115set("D3", True)116set("D4", None)117set("E1:F2", [[1, "two"], [3.5, False]])118set_formula("G1:G9", "A1*2")119set("H1", "%s %s %s" % (type(a), b, row))120set("H2", get_formula("G1") + get_number_format("A1"))121set("H3", selection() + " " + active_cell() + " " + active_sheet() + " " + ",".join(sheets()))122set("H4", offset("A1", 2, 3) + " " + offset("'Q3 plan'!A1:B2", 1, 0))123number_format("A1:A3", "percent", decimals=1)124clear()125x = add_sheet()126print("done", x)127`128 f := newFake()129 var printed []string130 st, err := New("api", src, Env{Host: f, Print: func(s string) { printed = append(printed, s) }}).Exec()131 if err != nil {132 t.Fatal(err)133 }134 for ref, want := range map[string]string{135 "D1": "6", "D2": "5", "D3": "TRUE", "D4": "", "H1": `int x [[2, "x"]]`, "H2": "=1+1currency",136 "H3": "B2:C3 B2 Sheet1 Sheet1,Two", "H4": "C4 'Q3 plan'!B1:C2",137 } {138 if got := f.cells[ref]; got != want {139 t.Errorf("%s = %q, want %q", ref, got, want)140 }141 }142 for _, want := range []string{`set E1:F2 [["1" "two"] ["3.5" "FALSE"]]`, "formula G1:G9 =A1*2", `format A1:A3 percent 1 ""`, `clear ""`, `add ""`} {143 if !strings.Contains(strings.Join(f.calls, "\n"), want) {144 t.Errorf("no call %q in\n%s", want, strings.Join(f.calls, "\n"))145 }146 }147 if len(printed) != 1 || printed[0] != "done Sheet3" {148 t.Errorf("printed %q", printed)149 }150 if st.Calls < 15 || st.Steps == 0 {151 t.Errorf("stats %+v", st)152 }153}154155func TestErrorsHavePositions(t *testing.T) {156 for _, tc := range []struct {157 src, want string158 compiles bool159 }{160 {"move(0, 1)\nset(\"A1\"", "m:2:9: got end of file, want ')'", false},161 {"x = 1\nnope(2)", "m:2:1: undefined: nope", false},162 {"move(0, 1)\n get(\"bad\")", "m:2:3: got indent, want primary expression", false},163 {"move()\nget(\"bad\")", "m:2:4: get: not a cell or range: bad", true},164 {"def f():\n jump(\"sideways\")\nf()", "m:2:9: jump: to is one of up, down, left, right, home, start, end, not \"sideways\"", true},165 {"set(\"A1\", {})", "m:1:4: set: a cell takes a number, text, True, False or None, not dict", true},166 {"move(1, 2, 3)", "m:1:5: move: got 3 arguments, want at most 2", true},167 } {168 _, err := New("m", tc.src, Env{Host: newFake()}).Exec()169 if err == nil || err.Error() != tc.want {170 t.Errorf("%q: %v, want %q", tc.src, err, tc.want)171 }172 if err := Check("m", tc.src); (err == nil) != tc.compiles {173 t.Errorf("Check(%q) = %v", tc.src, err)174 }175 }176}177178func TestStepLimitStopsALoop(t *testing.T) {179 _, err := New("loop", "while True:\n pass\n", Env{Host: newFake(), MaxSteps: 10_000}).Exec()180 var me *Error181 if !errors.As(err, &me) || !me.Limit || !strings.HasPrefix(err.Error(), "loop:1:1: stopped after 1000") {182 t.Fatalf("err %v", err)183 }184}185186func TestCancelStopsARun(t *testing.T) {187 r := New("spin", "while True:\n move(0, 0)\n", Env{Host: newFake(), MaxSteps: 1 << 62})188 done := make(chan error)189 go func() { _, err := r.Exec(); done <- err }()190 time.Sleep(10 * time.Millisecond)191 r.Cancel()192 select {193 case err := <-done:194 var me *Error195 if !errors.As(err, &me) || !me.Cancel || !strings.Contains(err.Error(), "stopped with Esc") {196 t.Fatalf("err %v", err)197 }198 case <-time.After(5 * time.Second):199 t.Fatal("cancel didn't stop the run")200 }201}202203func TestNoWayOut(t *testing.T) {204 // Starlark has no file, network, time or random access of its own,205 // and without a loader nothing else can be brought in.206 for _, name := range []string{"open", "time", "random", "exec", "eval", "load_module", "__import__"} {207 if err := Check("m", name+"()"); err == nil {208 t.Errorf("%s is reachable", name)209 }210 }211 _, err := New("m", `load("time.star", "now")`, Env{Host: newFake()}).Exec()212 if err == nil {213 t.Error("load works")214 }215}