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 / diff / merge_test.go
merge_test.go 8.3K
1package diff23import (4 "bytes"5 "strings"6 "testing"78 "src.rest/cat/012/internal/sheet"9)1011func merge(t *testing.T, base, ours, theirs []byte) (*sheet.Workbook, []string) {12 t.Helper()13 out, conflicts, err := Merge(base, ours, theirs)14 if err != nil {15 t.Fatal(err)16 }17 w, err := sheet.ReadBook(bytes.NewReader(out))18 if err != nil {19 t.Fatalf("the merge doesn't read: %v\n%s", err, out)20 }21 var said []string22 for _, c := range conflicts {23 said = append(said, c.String())24 }25 return w, said26}2728func input(w *sheet.Workbook, ref string) string {29 name, rest := sheet.SplitSheet(ref)30 s := w.Lookup(name)31 if s == nil {32 return "(no sheet " + name + ")"33 }34 c := s.Cell(addr(rest))35 if c == nil {36 return ""37 }38 return c.Input39}4041// Tables merge one by one: one side resizing a table and the other42// adding one keeps both.43func TestMergeTables(t *testing.T) {44 cells := []map[string]string{{"A1": "Item", "B1": "Cost", "A2": "Rent", "B2": "5", "D1": "Tag", "D2": "x"}}45 withTables := func(ts map[string]string) func(w *sheet.Workbook) {46 return func(w *sheet.Workbook) {47 for name, r := range ts {48 rr, _ := sheet.ParseRange(r)49 if err := w.Sheet(0).CreateTable(name, rr); err != nil {50 t.Fatal(err)51 }52 }53 }54 }55 base := file(t, []string{"S"}, cells, withTables(map[string]string{"Costs": "A1:B2"}))56 ours := file(t, []string{"S"}, cells, withTables(map[string]string{"Costs": "A1:B3"}))57 theirs := file(t, []string{"S"}, cells, withTables(map[string]string{"Costs": "A1:B2", "Tags": "D1:D2"}))58 w, said := merge(t, base, ours, theirs)59 if len(said) > 0 {60 t.Errorf("conflicts %v", said)61 }62 s := w.Sheet(0)63 costs, _ := s.TableAt(addr("A1"))64 tags, _ := s.TableAt(addr("D1"))65 if costs.Range.String() != "A1:B3" || tags.Name != "Tags" {66 t.Errorf("tables %+v %+v", costs, tags)67 }68}6970func TestMergeCells(t *testing.T) {71 base := file(t, []string{"Sheet1"}, []map[string]string{{"A1": "Item", "B1": "1", "B2": "2", "B3": "3", "B4": "=SUM(B1:B3)"}}, nil)72 ours := file(t, []string{"Sheet1"}, []map[string]string{{"A1": "Item", "B1": "10", "B2": "2", "B3": "30", "B4": "=SUM(B1:B3)", "C1": "ours"}}, nil)73 theirs := file(t, []string{"Sheet1"}, []map[string]string{{"A1": "Item", "B1": "1", "B2": "20", "B3": "31", "B4": "=SUM(B1:B3)", "D1": "theirs"}}, func(w *sheet.Workbook) {74 s := w.Sheet(0)75 s.SetStyle(sheet.NewRect(addr("B1"), addr("B1")), func(st *sheet.Style) { st.Bold = true })76 s.SetColWidth(3, 20)77 })78 w, said := merge(t, base, ours, theirs)79 for ref, want := range map[string]string{"Sheet1!B1": "10", "Sheet1!B2": "20", "Sheet1!B3": "30", "Sheet1!C1": "ours", "Sheet1!D1": "theirs"} {80 if got := input(w, ref); got != want {81 t.Errorf("%s = %q, want %q", ref, got, want)82 }83 }84 s := w.Sheet(0)85 if !s.CellStyle(addr("B1")).Bold || s.ColWidth(3) != 20 {86 t.Error("theirs' formatting and width were lost")87 }88 if v := s.Value(addr("B4")); v.Num != 60 {89 t.Errorf("B4 = %v", v)90 }91 if len(said) != 1 || said[0] != "Sheet1!B3 input: ours 30, theirs 31, base 3" {92 t.Errorf("conflicts: %v", said)93 }94 if note := s.Note(addr("B3")); note != "Merge conflict, kept ours; theirs had input 31" {95 t.Errorf("B3's note: %q", note)96 }97}9899func TestMergeSheets(t *testing.T) {100 cells := map[string]string{"A1": "a", "A2": "b", "A3": "c"}101 base := file(t, []string{"Plan", "Old", "Keep"}, []map[string]string{cells, {"A1": "old"}, {"A1": "k"}}, nil)102 // Ours renames Plan and adds a sheet; theirs edits Plan, removes Old103 // and adds a sheet of the same name as ours'.104 ours := file(t, []string{"Q3 plan", "Old", "Keep", "Notes"}, []map[string]string{cells, {"A1": "old"}, {"A1": "k"}, {"A1": "ours"}}, nil)105 edited := map[string]string{"A1": "a", "A2": "B", "A3": "c"}106 theirs := file(t, []string{"Plan", "Keep", "Notes"}, []map[string]string{edited, {"A1": "k"}, {"B1": "theirs"}}, nil)107 w, said := merge(t, base, ours, theirs)108 var names []string109 for _, s := range w.Sheets() {110 names = append(names, s.Name())111 }112 if strings.Join(names, ",") != "Q3 plan,Keep,Notes" {113 t.Errorf("sheets: %v", names)114 }115 if input(w, "'Q3 plan'!A2") != "B" || input(w, "Notes!A1") != "ours" || input(w, "Notes!B1") != "theirs" {116 t.Errorf("cells: %q %q %q", input(w, "'Q3 plan'!A2"), input(w, "Notes!A1"), input(w, "Notes!B1"))117 }118 if len(said) != 0 {119 t.Errorf("conflicts: %v", said)120 }121122 // A sheet removed on one side and changed on the other conflicts;123 // ours stays.124 oursEdit := file(t, []string{"Plan", "Old", "Keep"}, []map[string]string{cells, {"A1": "older"}, {"A1": "k"}}, nil)125 w, said = merge(t, base, oursEdit, theirs)126 if w.Lookup("Old") == nil || len(said) != 1 || said[0] != "sheet Old: changed in ours, removed in theirs" {127 t.Errorf("removed in theirs: %v %v", w.Lookup("Old"), said)128 }129 theirsEdit := file(t, []string{"Plan", "Old", "Keep"}, []map[string]string{cells, {"A1": "older"}, {"A1": "k"}}, nil)130 oursGone := file(t, []string{"Plan", "Keep"}, []map[string]string{cells, {"A1": "k"}}, nil)131 w, said = merge(t, base, oursGone, theirsEdit)132 if w.Lookup("Old") != nil || len(said) != 1 || said[0] != "sheet Old: removed in ours, changed in theirs" {133 t.Errorf("removed in ours: %v %v", w.Lookup("Old"), said)134 }135136 // Both renaming one sheet differently conflicts.137 theirsRenamed := file(t, []string{"Plan B", "Old", "Keep"}, []map[string]string{cells, {"A1": "old"}, {"A1": "k"}}, nil)138 _, said = merge(t, base, ours, theirsRenamed)139 if len(said) != 1 || said[0] != "sheet Q3 plan name: ours Q3 plan, theirs Plan B, base Plan" {140 t.Errorf("renamed twice: %v", said)141 }142}143144func TestMergeTrust(t *testing.T) {145 book := func(origin, command string) []byte {146 return []byte(`{"version": 4, "macroOrigin": "` + origin + `", "sheets": [{"name": "S", "cells": {}}, {"name": "N", "tab": "notebook", "notebookCells": [{"source": "` + command + `"}], "cells": {}}]}`)147 }148 base := book("here", "ls")149 cases := []struct {150 ours, theirs []byte151 want string152 }{153 {book("here", "ls -a"), book("here", "ls"), "here"}, // theirs brought no commands154 {book("here", "ls"), book("there", "ls"), "here"}, // only theirs' origin differs155 {book("here", "echo"), book("there", "echo"), "here"}, // both made the same change156 {book("here", "ls -a"), book("there", "ls -b"), "here"}, // a conflict keeps ours' command157 {book("here", "ls"), book("here", "ls -l"), "here"}, // theirs trusted on this computer too158 {book("there", "ls"), book("there", "ls -l"), "there"}, // both from elsewhere, as ours was159 {book("here", "ls"), book("there", "rm -rf ~"), ""}, // theirs' command from elsewhere160 {book("here", "ls"), book("", "ls -l"), ""}, // theirs' command, never trusted161 {book("here", "ls"), book("here2", "ls -l"), ""}, // another computer's id162 {book("", "ls"), book("here", "ls -l"), ""}, // ours stays untrusted163 }164 for i, c := range cases {165 w, _ := merge(t, base, c.ours, c.theirs)166 if got := w.MacroOrigin(); got != c.want {167 t.Errorf("case %d: origin %q, want %q", i, got, c.want)168 }169 }170}171172func TestMergeBroken(t *testing.T) {173 if _, _, err := Merge([]byte("{"), nil, nil); err == nil {174 t.Error("a broken base merged")175 }176 // Names that end up pointing at a sheet no longer there make a177 // workbook the engine refuses, which is an error, not a merge.178 base := []byte(`{"version": 4, "sheets": [{"name": "A", "cells": {}}, {"name": "B", "cells": {}}]}`)179 ours := []byte(`{"version": 4, "sheets": [{"name": "A", "cells": {}}]}`)180 theirs := []byte(`{"version": 4, "names": {"N": "B!A1"}, "sheets": [{"name": "A", "cells": {}}, {"name": "B", "cells": {}}]}`)181 if _, _, err := Merge(base, ours, theirs); err == nil {182 t.Error("a merge naming a sheet removed")183 }184}185186func TestMergeOneSheetFiles(t *testing.T) {187 base := []byte(`{"version": 3, "names": {"Sales": "A1:A2"}, "cells": {"A1": "1", "A2": "2"}}`)188 ours := []byte(`{"version": 3, "names": {"Sales": "A1:A3"}, "cells": {"A1": "1", "A2": "2", "A3": "3"}}`)189 theirs := []byte(`{"version": 3, "names": {"Sales": "A1:A2", "Cost": "B1"}, "widths": {"B": 14}, "cells": {"A1": "1", "A2": "2", "B1": "=SUM(Sales)"}}`)190 out, conflicts, err := Merge(base, ours, theirs)191 if err != nil || len(conflicts) > 0 {192 t.Fatal(err, conflicts)193 }194 want := `{195 "version": 3,196 "widths": {"B": 14},197 "names": {"Cost": "B1", "Sales": "A1:A3"},198 "cells": {199 "A1": "1",200 "B1": "=SUM(Sales)",201 "A2": "2",202 "A3": "3"203 }204}205`206 if string(out) != want {207 t.Errorf("got\n%s\nwant\n%s", out, want)208 }209}