cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / nuon / nuon_test.go
nuon_test.go 11.3K
1package nuon23import (4 "errors"5 "io"6 "math"7 "strings"8 "testing"9 "time"10)1112func TestParseValues(t *testing.T) {13 utc := func(s string) Value {14 tm, err := time.Parse(time.RFC3339Nano, s)15 if err != nil {16 t.Fatal(err)17 }18 return DateValue(tm)19 }20 for in, want := range map[string]Value{21 `null`: NullValue(),22 `true`: BoolValue(true),23 `TRUE`: StringValue("TRUE"),24 `42`: IntValue(42),25 `-7`: IntValue(-7),26 `+3`: IntValue(3),27 `1_000`: IntValue(1000),28 `0x2a`: IntValue(42),29 `0o17`: IntValue(15),30 `0b101`: IntValue(5),31 `-0x10`: StringValue("-0x10"),32 `1.5`: FloatValue(1.5),33 `.5`: FloatValue(0.5),34 `5.`: FloatValue(5),35 `1e3`: FloatValue(1000),36 `-1.5e-3`: FloatValue(-0.0015),37 `inf`: FloatValue(math.Inf(1)),38 `-inf`: FloatValue(math.Inf(-1)),39 `1646b`: FilesizeValue(1646),40 `1.5kb`: FilesizeValue(1500),41 `1.5kB`: FilesizeValue(1500),42 `1KiB`: FilesizeValue(1024),43 `0.5b`: FilesizeValue(0),44 `-1.5kb`: FilesizeValue(-1500),45 `90sec`: DurationValue(90 * time.Second),46 `1.5sec`: DurationValue(1500 * time.Millisecond),47 `3us`: DurationValue(3 * time.Microsecond),48 `3ยตs`: DurationValue(3 * time.Microsecond),49 `2wk`: DurationValue(14 * 24 * time.Hour),50 `-3ms`: DurationValue(-3 * time.Millisecond),51 `1500000000ns`: DurationValue(1500 * time.Millisecond),52 `2026-09-27`: utc("2026-09-27T00:00:00Z"),53 `2026-09-27T11:27:31`: utc("2026-09-27T11:27:31Z"),54 `2026-09-27T11:27:31.351890163-06:00`: utc("2026-09-27T11:27:31.351890163-06:00"),55 `2026-09-27T11:27:31Z`: utc("2026-09-27T11:27:31Z"),56 `0x[DEAD]`: BinaryValue([]byte{0xde, 0xad}),57 `0x[de ad be]`: BinaryValue([]byte{0xde, 0xad, 0xbe}),58 `0b[1010 0101]`: BinaryValue([]byte{0xa5}),59 `abc`: StringValue("abc"),60 `-`: StringValue("-"),61 `1..3`: StringValue("1..3"),62 `"a\"b\n\u{41}B\t"`: StringValue("a\"b\nAB\t"),63 `"๐Ÿ˜€"`: StringValue("๐Ÿ˜€"),64 `'it\'`: StringValue(`it\`),65 "`b c`": StringValue("b c"),66 `r#'say 'hi''#`: StringValue("say 'hi'"),67 `r##'a'#b'##`: StringValue("a'#b"),68 `[1 2, 3,]`: ListValue(IntValue(1), IntValue(2), IntValue(3)),69 `[]`: ListValue(),70 `{}`: RecordValue(),71 `{a: 1, "b c": [x], 'd': {e: null} f:2}`: RecordValue(72 Field{"a", IntValue(1)}, Field{"b c", ListValue(StringValue("x"))},73 Field{"d", RecordValue(Field{"e", NullValue()})}, Field{"f", IntValue(2)}),74 `[[a, b]; [1, 2], [3, 4]]`: ListValue(75 RecordValue(Field{"a", IntValue(1)}, Field{"b", IntValue(2)}),76 RecordValue(Field{"a", IntValue(3)}, Field{"b", IntValue(4)})),77 `[[1, 2], [3]]`: ListValue(ListValue(IntValue(1), IntValue(2)), ListValue(IntValue(3))),78 "# a comment\n[1, # one\n 2]": ListValue(IntValue(1), IntValue(2)),79 } {80 got, err := Parse([]byte(in))81 if err != nil {82 t.Errorf("Parse(%s): %v", in, err)83 continue84 }85 if !Equal(got, want) {86 t.Errorf("Parse(%s) = %s (%s), want %s (%s)", in, got, got.Kind, want, want.Kind)87 }88 }89 if v, err := Parse([]byte("NaN")); err != nil || !math.IsNaN(v.Float) {90 t.Errorf("NaN: %v %v", v, err)91 }92}9394func TestParseErrors(t *testing.T) {95 for _, in := range []string{``, `[1, 2`, `{a 1}`, `{a: }`, `"abc`, `"\q"`, `[[a, b]; [1]]`, `[[a]; x]`,96 `1 2`, `(1 + 2)`, `$x`, `0x[zz]`, `[[{}]; [1]]`, strings.Repeat("[", 1000)} {97 if _, err := Parse([]byte(in)); err == nil {98 t.Errorf("Parse(%q) took it", in)99 }100 }101 _, err := Parse([]byte("[1,\n 2 }"))102 var se *SyntaxError103 if !errors.As(err, &se) || se.Line != 2 || se.Col != 5 {104 t.Errorf("error %v, want line 2 column 5", err)105 }106}107108func TestAppend(t *testing.T) {109 loc := time.FixedZone("", -6*3600)110 for want, v := range map[string]Value{111 `null`: NullValue(),112 `1.0`: FloatValue(1),113 `100000000000000000000.0`: FloatValue(1e20),114 `-0.5`: FloatValue(-0.5),115 `abc`: StringValue("abc"),116 `"a b"`: StringValue("a b"),117 `""`: StringValue(""),118 `"true"`: StringValue("true"),119 `"12"`: StringValue("12"),120 `"1kb"`: StringValue("1kb"),121 `"-"`: StringValue("-"),122 `"2026-01-01"`: StringValue("2026-01-01"),123 `"a\"b\n\u{1b}"`: StringValue("a\"b\n\x1b"),124 `1646b`: FilesizeValue(1646),125 `90000000000ns`: DurationValue(90 * time.Second),126 `2026-09-27T11:27:31-06:00`: DateValue(time.Date(2026, 9, 27, 11, 27, 31, 0, loc)),127 `2026-09-27T11:27:31.500+00:00`: DateValue(time.Date(2026, 9, 27, 11, 27, 31, 5e8, time.UTC)),128 `2026-09-27T11:27:31.000001+00:00`: DateValue(time.Date(2026, 9, 27, 11, 27, 31, 1e3, time.UTC)),129 `2026-09-27T11:27:31.351890163+00:00`: DateValue(time.Date(2026, 9, 27, 11, 27, 31, 351890163, time.UTC)),130 `0x[DEAD]`: BinaryValue([]byte{0xde, 0xad}),131 `{a: 1, "b c": [1, 2]}`: RecordValue(Field{"a", IntValue(1)}, Field{"b c", ListValue(IntValue(1), IntValue(2))}),132 `[[a, b]; [1, 2], [3, null]]`: ListValue(RecordValue(Field{"a", IntValue(1)}, Field{"b", IntValue(2)}), RecordValue(Field{"a", IntValue(3)}, Field{"b", NullValue()})),133 `[{a: 1}, {b: 2}]`: ListValue(RecordValue(Field{"a", IntValue(1)}), RecordValue(Field{"b", IntValue(2)})),134 } {135 if got := v.String(); got != want {136 t.Errorf("%v wrote %s, want %s", v, got, want)137 }138 back, err := Parse([]byte(want))139 if err != nil || !Equal(back, v) {140 t.Errorf("%s read back as %v, %v", want, back, err)141 }142 }143}144145func TestAppendJSON(t *testing.T) {146 v := RecordValue(Field{"a", FilesizeValue(1000)}, Field{"b", DurationValue(time.Second)},147 Field{"c", DateValue(time.Date(2026, 9, 27, 11, 27, 31, 5e8, time.FixedZone("", 7200)))},148 Field{"d", BinaryValue([]byte{1, 2})}, Field{"e", FloatValue(1)}, Field{"f", FloatValue(math.Inf(1))},149 Field{"g <", StringValue("x\"y")})150 want := `{"a":1000,"b":1000000000,"c":"2026-09-27T11:27:31.500+02:00","d":[1,2],"e":1.0,"f":null,"g <":"x\"y"}`151 if got := string(AppendJSON(nil, v)); got != want {152 t.Errorf("got %s\nwant %s", got, want)153 }154}155156// rows reads every row of in.157func rows(t *testing.T, in string) ([]Row, *Reader) {158 t.Helper()159 r := NewReader(strings.NewReader(in))160 var out []Row161 for {162 row, err := r.Next()163 if errors.Is(err, io.EOF) {164 return out, r165 }166 if err != nil {167 t.Fatalf("%s: %v", in, err)168 }169 out = append(out, row)170 }171}172173func TestReaderShapes(t *testing.T) {174 for in, want := range map[string]string{175 `[[a, b]; [1, 2], [3, 4]]`: `{a: 1, b: 2}|{a: 3, b: 4}`,176 `[{a: 1}, {b: 2, a: 3}]`: `{a: 1}|{b: 2, a: 3}`,177 `[1, x]`: `{value: 1}|{value: x}`,178 `[[1, 2], [3]]`: `{value: [1, 2]}|{value: [3]}`,179 `{a: 1}`: `{a: 1}`,180 `42`: `{value: 42}`,181 "{\"a\": 1}\n{\"a\": 2}\n": `{a: 1}|{a: 2}`,182 `[[a]; [1]] [[b]; [2]]`: `{a: 1}|{b: 2}`,183 `[]`: ``,184 ``: ``,185 " \n# nothing\n": ``,186 } {187 got, _ := rows(t, in)188 var parts []string189 for _, r := range got {190 parts = append(parts, RecordValue(r...).String())191 }192 if s := strings.Join(parts, "|"); s != want {193 t.Errorf("%s: rows %s, want %s", in, s, want)194 }195 }196}197198func TestReaderHeaderAndJSON(t *testing.T) {199 r := NewReader(strings.NewReader(`[[name, size]; [a, 1kb], [b, 2kb]]`))200 if _, err := r.Next(); err != nil {201 t.Fatal(err)202 }203 if h := r.Header(); strings.Join(h, ",") != "name,size" {204 t.Errorf("header %q", h)205 }206 if r.JSON() {207 t.Error("the table form is JSON")208 }209 for in, want := range map[string]bool{210 `[{"a": 1, "b": [true, null, "x"]}, {"a": -1.5e3}]`: true,211 `[{a: 1}]`: false,212 `[{"a": 1kb}]`: false,213 `[{"a": 'x'}]`: false,214 `{"a": "\u{41}"}`: false,215 "{\"a\": 1} # note": false,216 } {217 if _, r := rows(t, in); r.JSON() != want {218 t.Errorf("%s: JSON %v", in, r.JSON())219 }220 }221}222223// TestReaderStreams reads rows before the text is complete: each row is224// handed over once it has been read, so a pipe still being written225// yields what it has.226func TestReaderStreams(t *testing.T) {227 pr, pw := io.Pipe()228 r := NewReader(pr)229 got := make(chan Row)230 go func() {231 for {232 row, err := r.Next()233 if err != nil {234 close(got)235 return236 }237 got <- row238 }239 }()240 io.WriteString(pw, "[[a]; [1], ")241 if row := <-got; RecordValue(row...).String() != "{a: 1}" {242 t.Errorf("first row %v", row)243 }244 io.WriteString(pw, "[2]]")245 if row := <-got; RecordValue(row...).String() != "{a: 2}" {246 t.Errorf("second row %v", row)247 }248 pw.Close()249 if _, ok := <-got; ok {250 t.Error("a row after the end")251 }252}253254func TestTableWriter(t *testing.T) {255 var b strings.Builder256 w := NewTableWriter(&b, []string{"name", "size in"})257 w.Write([]Value{StringValue("a"), FilesizeValue(1)})258 w.Write([]Value{StringValue("b c"), NullValue()})259 if err := w.Close(); err != nil {260 t.Fatal(err)261 }262 want := "[[name, \"size in\"]; [a, 1b],\n[\"b c\", null]]\n"263 if b.String() != want {264 t.Errorf("wrote %q", b.String())265 }266 b.Reset()267 NewTableWriter(&b, []string{"a"}).Close()268 if b.String() != "[]\n" {269 t.Errorf("empty table %q", b.String())270 }271}272273// FuzzParse holds the parser to its writer: whatever parses writes NUON274// that parses back to the same value, and nothing panics.275func FuzzParse(f *testing.F) {276 for _, s := range []string{`[[a, b]; [1, 2kb]]`, `{a: [1 2], "b": 2026-09-27T11:27:31Z}`, `0x[de ad]`,277 `r#'x'#`, `"\u{41}"`, `[1.5sec, -3, 1e9, inf, NaN]`, `# c` + "\n" + `{}`, `[{"a": null}]`} {278 f.Add([]byte(s))279 }280 f.Fuzz(func(t *testing.T, data []byte) {281 v, err := Parse(data)282 if err != nil {283 return284 }285 text := v.String()286 back, err := Parse([]byte(text))287 if err != nil {288 t.Fatalf("%q wrote %s, which doesn't parse: %v", data, text, err)289 }290 if !Equal(v, back) {291 t.Fatalf("%q wrote %s, which reads as %s", data, text, back)292 }293 NewReader(strings.NewReader(string(data))).Next()294 })295}