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 / recovery_test.go
recovery_test.go 5.6K
1package ui23import (4 "os"5 "path/filepath"6 "strings"7 "testing"8 "time"910 "src.rest/cat/012/internal/confine"11)1213// servedModel is a model served from a fresh directory, which it14// returns.15func servedModel(t *testing.T) (*Model, string) {16 t.Helper()17 dir := t.TempDir()18 root, err := confine.New(dir)19 if err != nil {20 t.Fatal(err)21 }22 m := newModel()23 m.Serve(root, []string{"TERM=xterm-256color"})24 return m, root.Dir()25}2627func TestRecoveryKey(t *testing.T) {28 for name, want := range map[string]string{29 "": "(untitled)",30 "book.012": "book",31 "book": "book",32 "sub/a b.012": "sub%2Fa%20b",33 "./x/../y.012": "y",34 "(untitled).012": "%28untitled%29",35 } {36 if got := recoveryKey(name); got != want {37 t.Errorf("recoveryKey(%q) = %q, want %q", name, got, want)38 }39 }40}4142// Only changed books that hold something are kept, only when served (or43// given a recovery directory, recovery tests in crash_test.go),44// and at most recoveryKeep per name, newest kept.45func TestRecoverKeepsFew(t *testing.T) {46 local := newModel()47 press(t, local, "1", "<enter>")48 if _, err := local.Recover(time.Now()); err == nil {49 t.Error("a local model kept a recovery file")50 }5152 m, dir := servedModel(t)53 if m.Unsaved() {54 t.Error("a new sheet is unsaved")55 }56 press(t, m, "x", "<enter>", "<up>", "<delete>")57 if !m.changed || m.Unsaved() {58 t.Errorf("an emptied sheet: changed %v, unsaved %v", m.changed, m.Unsaved())59 }60 press(t, m, "42", "<enter>")61 m.filename = "book.012"62 at := time.Date(2026, 9, 27, 14, 2, 3, 0, time.Local)63 var names []string64 for i := range 5 {65 name, err := m.Recover(at.Add(time.Duration(i/2) * time.Second))66 if err != nil {67 t.Fatal(err)68 }69 names = append(names, name)70 }71 if names[0] != filepath.Join(RecoveryDir, "book-20260927-140203.012") || names[1] != filepath.Join(RecoveryDir, "book-20260927-140203-2.012") {72 t.Errorf("names %v", names)73 }74 // Another name's files, and one that only starts like this one's,75 // are left alone.76 os.WriteFile(filepath.Join(dir, RecoveryDir, "book-20260101-000000-20260101-000000.012"), nil, 0o600)77 os.WriteFile(filepath.Join(dir, RecoveryDir, "other-20200101-000000.012"), nil, 0o600)78 if _, err := m.Recover(at.Add(time.Hour)); err != nil {79 t.Fatal(err)80 }81 files := recoveries(filepath.Join(dir, RecoveryDir), "book")82 var got []string83 for _, f := range files {84 got = append(got, filepath.Base(f.path))85 }86 want := []string{"book-20260927-140204-2.012", "book-20260927-140205.012", "book-20260927-150203.012"}87 if strings.Join(got, " ") != strings.Join(want, " ") {88 t.Errorf("kept %v, want %v", got, want)89 }90 for _, other := range []string{"book-20260101-000000-20260101-000000.012", "other-20200101-000000.012"} {91 if _, err := os.Stat(filepath.Join(dir, RecoveryDir, other)); err != nil {92 t.Errorf("%s removed", other)93 }94 }95}9697// A served session starting on a new sheet offers an untitled book's98// kept changes: Esc keeps them for later, D deletes them.99func TestRecoveryOfferUntitled(t *testing.T) {100 m, dir := servedModel(t)101 press(t, m, "kept", "<enter>")102 file, err := m.Recover(time.Now())103 if err != nil {104 t.Fatal(err)105 }106 for _, key := range []string{"<esc>", "d"} {107 m, _ := servedModel(t)108 m.root, _ = confine.New(dir)109 m.OpenOnStart("")110 run(m, m.Init())111 if !strings.Contains(line(m, contextLine), "Unsaved changes to an untitled sheet were kept.") {112 t.Fatalf("no offer: %q", line(m, contextLine))113 }114 press(t, m, key)115 _, err := os.Stat(filepath.Join(dir, file))116 if key == "<esc>" && err != nil || key == "d" && err == nil {117 t.Errorf("%s: recovery file there: %v", key, err == nil)118 }119 if input(m, "A1") != "" {120 t.Errorf("%s restored it", key)121 }122 }123}124125// Opening a file with kept changes offers them; restoring makes them126// unsaved changes to that file, and saving them removes the recovery127// file. A local model without a recovery directory never offers.128func TestRecoveryRestore(t *testing.T) {129 m, dir := servedModel(t)130 writeSheet(t, filepath.Join(dir, "book.012"), "on disk")131 m.OpenOnStart("book")132 run(m, m.Init())133 press(t, m, "mine", "<enter>")134 file, err := m.Recover(time.Now())135 if err != nil {136 t.Fatal(err)137 }138139 m, _ = servedModel(t)140 m.root, _ = confine.New(dir)141 m.OpenOnStart("book.012")142 run(m, m.Init())143 if input(m, "A1") != "on disk" || !strings.Contains(line(m, contextLine), "Unsaved changes to book.012 were kept.") {144 t.Fatalf("A1 %q, context %q", input(m, "A1"), line(m, contextLine))145 }146 press(t, m, "<enter>")147 if input(m, "A1") != "mine" || !m.changed || m.filename != "book.012" || m.recovered == "" {148 t.Fatalf("restore: A1 %q, changed %v, file %q", input(m, "A1"), m.changed, m.filename)149 }150 if !strings.Contains(line(m, contextLine), "Restored the kept changes") {151 t.Errorf("context %q", line(m, contextLine))152 }153 // Undoing to the restored state still differs from the file.154 press(t, m, "x", "<enter>", "<ctrl+z>")155 if !m.changed {156 t.Error("undo marked the restored book saved")157 }158 press(t, m, "<ctrl+s>")159 if m.overlay != nil || m.changed {160 t.Fatalf("save: overlay %T, changed %v, %q", m.overlay, m.changed, line(m, contextLine))161 }162 if got, _ := readSheet(filepath.Join(dir, "book.012")); a1Of(got) != "mine" {163 t.Errorf("saved A1 %q", a1Of(got))164 }165 if _, err := os.Stat(filepath.Join(dir, file)); err == nil {166 t.Error("saving kept the recovery file")167 }168169 // Locally, nothing is offered.170 os.MkdirAll(filepath.Join(dir, RecoveryDir), 0o700)171 os.WriteFile(filepath.Join(dir, RecoveryDir, "book-20260101-000000.012"), nil, 0o600)172 t.Chdir(dir)173 local := newModel()174 press(t, local, "<ctrl+o>", "book", "<enter>")175 if local.overlay != nil || input(local, "A1") != "mine" {176 t.Errorf("local open: overlay %T, A1 %q", local.overlay, input(local, "A1"))177 }178}