1package diff23import (4 "bytes"5 "strings"6 "testing"78 "src.rest/cat/012/internal/nuon"9 "src.rest/cat/012/internal/sheet"10)1112func addr(s string) sheet.Addr {13 a, _ := sheet.ParseAddr(s)14 return a15}1617// file writes a workbook of sheets, each a map of cells to inputs, after18// edit has its way with it.19func file(t *testing.T, sheets []string, cells []map[string]string, edit func(w *sheet.Workbook)) []byte {20 t.Helper()21 w := sheet.NewBook()22 for i, name := range sheets {23 s := w.Sheet(0)24 if i == 0 {25 if err := w.RenameSheet(s, name); err != nil {26 t.Fatal(err)27 }28 } else {29 var err error30 if s, err = w.AddSheet(name, i); err != nil {31 t.Fatal(err)32 }33 }34 for a, in := range cells[i] {35 if err := s.Set(addr(a), in); err != nil {36 t.Fatal(err)37 }38 }39 }40 if edit != nil {41 edit(w)42 }43 var b bytes.Buffer44 if err := w.Write(&b); err != nil {45 t.Fatal(err)46 }47 return b.Bytes()48}4950func read(t *testing.T, data []byte) *Book {51 t.Helper()52 b, err := Read(data)53 if err != nil {54 t.Fatal(err)55 }56 return b57}5859// lines is the text output of comparing a with b.60func lines(t *testing.T, a, b []byte) string {61 t.Helper()62 var out bytes.Buffer63 if err := WriteChanges(&out, Compare(read(t, a), read(t, b)), "text", false); err != nil {64 t.Fatal(err)65 }66 return out.String()67}6869func TestCompareCells(t *testing.T) {70 a := file(t, []string{"Sheet1"}, []map[string]string{{"A1": "Item", "B1": "5", "B2": "=B1*2", "C1": "gone", "D1": "=NOW()", "D2": "=D1+1"}}, nil)71 b := file(t, []string{"Sheet1"}, []map[string]string{{"A1": "Item", "B1": "6", "B2": "=B1*2", "C2": "new", "D1": "=NOW()", "D2": "=D1+1"}}, func(w *sheet.Workbook) {72 s := w.Sheet(0)73 s.SetStyle(sheet.NewRect(addr("A1"), addr("A1")), func(st *sheet.Style) { st.Bold = true })74 s.SetNote(addr("A1"), "the thing")75 s.SetColWidth(2, 14)76 })77 want := `Sheet1!A1 format + bold78Sheet1!A1 note + the thing79Sheet1!B1 input 5 → 680Sheet1!C1 input - gone81Sheet1!B2 value 10 → 1282Sheet1!C2 input + new83Sheet1 widths C + 1484`85 if got := lines(t, a, b); got != want {86 t.Errorf("got\n%s\nwant\n%s", got, want)87 }88 if got := lines(t, a, a); got != "" {89 t.Errorf("a workbook against itself:\n%s", got)90 }91}9293func TestCompareSheets(t *testing.T) {94 cells := map[string]string{"A1": "a", "A2": "b", "A3": "c", "A4": "d"}95 edited := map[string]string{"A1": "a", "A2": "b", "A3": "c", "A4": "D"}96 a := file(t, []string{"One", "Two", "Three", "Gone"}, []map[string]string{{"A1": "1"}, cells, {"A1": "3"}, {"A1": "x"}}, nil)97 b := file(t, []string{"Three", "One", "Renamed", "Added"}, []map[string]string{{"A1": "3"}, {"A1": "1"}, edited, {"A1": "y"}}, nil)98 got := lines(t, a, b)99 for _, want := range []string{100 "sheet Gone removed\n",101 "sheet Added added\n",102 "sheet Renamed renamed from Two\n",103 "sheet Three moved from 3 to 1\n",104 "Renamed!A4 input d → D\n",105 "Gone!A1 input - x\n",106 "Added!A1 input + y\n",107 } {108 if !strings.Contains(got, want) {109 t.Errorf("lacks %q:\n%s", want, got)110 }111 }112 if strings.Contains(got, "sheet One moved") {113 t.Errorf("more moves than needed:\n%s", got)114 }115}116117func TestCompareShapes(t *testing.T) {118 // One sheet written as a version 2 file and as a version 4 file is119 // the same workbook.120 v2 := []byte(`{"version": 2, "names": {"Sales": "A1:A2"}, "cells": {"A1": "1", "A2": "=A1+1"}}`)121 v4 := []byte(`{"version": 4, "names": {"Sales": "Sheet1!A1:A2"}, "sheets": [{"name": "Sheet1", "cells": {"A1": "1", "A2": "=A1 + 1"}}]}`)122 got := lines(t, v2, v4)123 if got != "Sheet1!A2 input =A1+1 → =A1 + 1\n" {124 t.Errorf("got\n%s", got)125 }126 empty := lines(t, nil, v2)127 if !strings.HasPrefix(empty, "sheet Sheet1 added\n") || !strings.Contains(empty, "name Sales added + Sheet1!A1:A2") {128 t.Errorf("from nothing:\n%s", empty)129 }130}131132func TestCompareRegionsAndBook(t *testing.T) {133 a := []byte(`{"version": 4, "locale": "en-US", "macros": [{"name": "m1", "api": 1, "source": "x"}], "sheets": [{"name": "S", "regions": [{"name": "app", "at": "A1", "path": "app.csv"}, {"name": "old", "at": "D1", "path": "old.csv"}], "cells": {}}]}`)134 b := []byte(`{"version": 4, "locale": "de-DE", "active": 0, "sheets": [{"name": "S", "regions": [{"name": "app", "at": "A1", "path": "app.log"}, {"name": "sales", "at": "H1", "output": true}], "cells": {}}]}`)135 want := `S region app path app.csv → app.log136S region old removed - old.csv137S region sales added + a notebook cell's output138macro m1 removed - x139workbook locale en-US → de-DE140`141 if got := lines(t, a, b); got != want {142 t.Errorf("got\n%s\nwant\n%s", got, want)143 }144}145146func TestCompareTables(t *testing.T) {147 cells := []map[string]string{{"A1": "Item", "B1": "Cost", "A2": "Rent", "B2": "5"}}148 a := file(t, []string{"S"}, cells, func(w *sheet.Workbook) {149 w.Sheet(0).CreateTable("Costs", sheet.NewRect(addr("A1"), addr("B2")))150 w.Sheet(0).CreateTable("Old", sheet.NewRect(addr("D1"), addr("D2")))151 })152 b := file(t, []string{"S"}, cells, func(w *sheet.Workbook) {153 w.Sheet(0).CreateTable("Costs", sheet.NewRect(addr("A1"), addr("B3")))154 })155 want := `S!D1 input - Column1156S table Costs range A1:B2 → A1:B3157S table Old removed - D1:D2158`159 if got := lines(t, a, b); got != want {160 t.Errorf("got\n%s\nwant\n%s", got, want)161 }162}163164func TestCompareNotebooks(t *testing.T) {165 nb := func(cells string) []byte {166 return []byte(`{"version": 4, "sheets": [{"name": "Sheet1", "cells": {}}, {"name": "Notebook", "tab": "notebook", "notebookCells": [` + cells + `], "cells": {}}]}`)167 }168 a := nb(`{"kind": "note", "source": "# Sales"}, {"source": "sales = open s.csv", "output": "[[n]; [1]]"}, {"source": "$sales | length", "output": "1"}, {"source": "gone"}`)169 b := nb(`{"source": "new = ls"}, {"kind": "note", "source": "# Sales"}, {"source": "sales = open s.csv", "output": "[[n]; [2]]"}, {"source": "$sales | length", "error": "boom"}`)170 want := `Notebook cell 1 added + new = ls171Notebook cell 3 output [[n]; [1]] → [[n]; [2]]172Notebook cell 4 error + boom173Notebook cell 4 output - 1174Notebook cell 4 removed - gone175`176 if got := lines(t, a, b); got != want {177 t.Errorf("got\n%s\nwant\n%s", got, want)178 }179 var out bytes.Buffer180 Dump(&out, read(t, b))181 if !strings.Contains(out.String(), "Notebook cell 3 sales = open s.csv = [[n]; [2]]\nNotebook cell 4 $sales | length error: boom\n") {182 t.Errorf("dump:\n%s", out.String())183 }184}185186func TestWriteChanges(t *testing.T) {187 a := file(t, []string{"Sheet1"}, []map[string]string{{"A1": "=1+1", "B1": "x"}}, nil)188 b := file(t, []string{"Sheet1"}, []map[string]string{{"A1": "=1+2"}}, nil)189 changes := Compare(read(t, a), read(t, b))190 var out bytes.Buffer191 WriteChanges(&out, changes, "nuon", false)192 v, err := nuon.Parse(out.Bytes())193 if err != nil || v.Kind != nuon.List || len(v.List) != 3 {194 t.Fatalf("NUON %v: %s", err, out.String())195 }196 if got := v.List[1].String(); got != `{kind: cell, sheet: Sheet1, item: A1, field: value, old: 2, new: 3}` {197 t.Errorf("a value's record: %s", got)198 }199 out.Reset()200 WriteChanges(&out, changes, "json", false)201 if !strings.Contains(out.String(), `{"kind":"cell","sheet":"Sheet1","item":"B1","field":"input","old":"x","new":null}`) {202 t.Errorf("JSON:\n%s", out.String())203 }204 out.Reset()205 WriteChanges(&out, changes[:1], "text", true)206 if out.String() != "\x1b[1mSheet1!A1\x1b[0m \x1b[36minput\x1b[0m \x1b[31m=1+1\x1b[0m → \x1b[32m=1+2\x1b[0m\n" {207 t.Errorf("colored: %q", out.String())208 }209 if err := WriteChanges(&out, changes, "xml", false); err == nil {210 t.Error("xml")211 }212}213214func TestDump(t *testing.T) {215 data := file(t, []string{"Sheet1"}, []map[string]string{{"A1": "Rent", "B1": "1450", "B2": "=B1*12", "C1": "=RAND()"}}, func(w *sheet.Workbook) {216 s := w.Sheet(0)217 s.SetNote(addr("A1"), "monthly")218 s.SetColWidth(0, 12)219 })220 var out bytes.Buffer221 if err := Dump(&out, read(t, data)); err != nil {222 t.Fatal(err)223 }224 want := `sheet Sheet1225Sheet1 widths {"A":12}226Sheet1!A1 Rent note: monthly227Sheet1!B1 1450228Sheet1!C1 =RAND()229Sheet1!B2 =B1*12 = 17400230`231 if out.String() != want {232 t.Errorf("got\n%s\nwant\n%s", out.String(), want)233 }234}