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 / fileio / tail_test.go
tail_test.go 5.0K
1package fileio23import (4 "context"5 "os"6 "path/filepath"7 "strings"8 "testing"9 "time"1011 "src.rest/cat/012/internal/locale"12 "src.rest/cat/012/internal/sheet"13)1415// liveText writes rows as a test reads them: cells' values by commas, rows16// by bars.17func liveText(rows ...sheet.LiveRow) string {18 var out []string19 for _, r := range rows {20 var cells []string21 for _, c := range r {22 cells = append(cells, c.V.String())23 }24 out = append(out, strings.Join(cells, ","))25 }26 return strings.Join(out, "|")27}2829// feed feeds pieces to a tail, returning what each completed.30func feed(t *testing.T, tl *Tail, pieces ...string) []TailRows {31 t.Helper()32 var out []TailRows33 for _, p := range pieces {34 got, err := tl.Feed([]byte(p))35 if err != nil {36 t.Fatalf("feeding %q: %v", p, err)37 }38 out = append(out, got)39 }40 return out41}4243func TestTailCSV(t *testing.T) {44 head := "when,level,msg\n9/29/2026,3,\"two\nlines\"\n"45 tl, err := NewTail(CSV, []byte(head), nil)46 if err != nil {47 t.Fatal(err)48 }49 defer tl.Close()50 got := feed(t, tl, "when,level,m", "sg\n9/29/2026,3,\"two\n", "lines\"\n10/1/2026,4,partial", "\n")51 if liveText(got[0].Header) != "" || len(got[0].Rows) != 0 {52 t.Fatalf("a partial header was read: %+v", got[0])53 }54 if liveText(got[1].Header) != "when,level,msg" || len(got[1].Rows) != 0 {55 t.Fatalf("header: %+v", got[1])56 }57 if len(got[2].Rows) != 1 || got[2].Rows[0][2].V.Str != "two lines" && got[2].Rows[0][2].V.Str != "two\nlines" {58 t.Fatalf("a quoted field across pieces: %+v", got[2].Rows)59 }60 if got[2].Rows[0][0].F.IsZero() || got[2].Rows[0][1].V.Num != 3 {61 t.Fatalf("fields weren't typed: %+v", got[2].Rows[0])62 }63 if len(got[3].Rows) != 1 || got[3].Rows[0][2].V.Str != "partial" {64 t.Fatalf("the last line: %+v", got[3])65 }66}6768func TestTailEncodingsAndDelimiters(t *testing.T) {69 // A UTF-16 file split mid-character, with a byte order mark.70 text := "a\tb\n1\tç\n"71 var utf16 []byte72 utf16 = append(utf16, 0xFF, 0xFE)73 for _, r := range text {74 utf16 = append(utf16, byte(r), byte(r>>8))75 }76 tl, err := NewTail(TSV, utf16, nil)77 if err != nil {78 t.Fatal(err)79 }80 defer tl.Close()81 var rows []sheet.LiveRow82 var header sheet.LiveRow83 for i := 0; i < len(utf16); i += 3 {84 got, err := tl.Feed(utf16[i:min(i+3, len(utf16))])85 if err != nil {86 t.Fatal(err)87 }88 if got.Header != nil {89 header = got.Header90 }91 rows = append(rows, got.Rows...)92 }93 if liveText(header) != "a,b" || liveText(rows...) != "1,ç" {94 t.Fatalf("UTF-16: %q %q", liveText(header), liveText(rows...))95 }96 // Semicolons sniffed, in a locale with decimal commas.97 head := "x;y\n1,5;2\n"98 tl, _ = NewTail(CSV, []byte(head), deLocale(t))99 defer tl.Close()100 got := feed(t, tl, head)101 if len(got[0].Rows) != 1 || got[0].Rows[0][0].V.Num != 1.5 {102 t.Fatalf("semicolons: %+v", got[0])103 }104}105106func TestTailNDJSONAndNUON(t *testing.T) {107 tl, err := NewTail(JSON, nil, nil)108 if err != nil {109 t.Fatal(err)110 }111 defer tl.Close()112 got := feed(t, tl, `{"a": 1, "b": "x"}`+"\n"+`{"a": 2`, `, "c": true}`+"\n")113 if liveText(got[0].Header) != "a,b" || liveText(got[0].Rows...) != "1,x" {114 t.Fatalf("first piece: %q %q", liveText(got[0].Header), liveText(got[0].Rows...))115 }116 if liveText(got[1].Header) != "a,b,c" || liveText(got[1].Rows...) != "2,,TRUE" {117 t.Fatalf("a new column: %q %q", liveText(got[1].Header), liveText(got[1].Rows...))118 }119 // NUON's table form, streaming, with types.120 tl, _ = NewTail(NUON, nil, nil)121 defer tl.Close()122 got = feed(t, tl, "[[size, took]; [1kb, 2sec],\n", "[3b, 1min]]\n")123 if liveText(got[0].Header) != "size,took" || len(got[0].Rows) != 1 || got[0].Rows[0][0].V.Num != 1000 || got[0].Rows[0][0].F.IsZero() {124 t.Fatalf("table form: %+v", got[0])125 }126 if len(got[1].Rows) != 1 || got[1].Header != nil {127 t.Fatalf("second piece: %+v", got[1])128 }129 // A syntax error ends the tail.130 tl, _ = NewTail(NUON, nil, nil)131 defer tl.Close()132 if _, err := tl.Feed([]byte("{a: }}\n")); err == nil {133 t.Fatal("no error for bad NUON")134 }135 if _, err := tl.Feed([]byte("{a: 1}\n")); err == nil {136 t.Fatal("a failed tail read on")137 }138}139140func TestTailClose(t *testing.T) {141 tl, _ := NewTail(CSV, nil, nil)142 feed(t, tl, "a,b\n1,")143 tl.Close()144 select {145 case <-tl.p.done:146 case <-time.After(5 * time.Second):147 t.Fatal("the tail's goroutine didn't end")148 }149 if _, err := tl.Feed([]byte("2\n")); err == nil {150 t.Fatal("a closed tail read on")151 }152 if _, err := NewTail(XLSX, nil, nil); err == nil {153 t.Fatal("a tail of XLSX")154 }155}156157func deLocale(t *testing.T) *locale.Locale {158 t.Helper()159 de, ok := locale.Lookup("de-DE")160 if !ok {161 t.Fatal("no de-DE")162 }163 return de164}165166func TestRowsOfImport(t *testing.T) {167 name := filepath.Join(t.TempDir(), "t.csv")168 os.WriteFile(name, []byte("a,b\n1,$2\n,x\n"), 0o644)169 res, err := Import(context.Background(), name, Options{})170 if err != nil {171 t.Fatal(err)172 }173 got := Rows(res.Sheet)174 if liveText(got.Header) != "a,b" || liveText(got.Rows...) != "1,2|,x" || got.Rows[0][1].F.IsZero() {175 t.Fatalf("%q %q", liveText(got.Header), liveText(got.Rows...))176 }177 if got := Rows(sheet.New()); got.Header != nil || got.Rows != nil {178 t.Fatalf("an empty sheet: %+v", got)179 }180}