package interpres import ( "math" "testing" "time" ) func TestParseScalars(t *testing.T) { tree, err := Parse([]byte(` title = "interpres" count = 42 ratio = 3.14 enabled = true disabled = false hexv = 0xFF octv = 0o755 binv = 0b1010 grouped = 1_000_000 neg = -17 expv = 1e3 `)) if err != nil { t.Fatalf("parse: %v", err) } cases := map[string]any{ "title": "interpres", "count": int64(42), "ratio": 3.14, "enabled": true, "disabled": false, "hexv": int64(255), "octv": int64(493), "binv": int64(10), "grouped": int64(1000000), "neg": int64(-17), "expv": 1000.0, } for k, want := range cases { if got := tree[k]; got != want { t.Errorf("%s = %#v (%T), want %#v (%T)", k, got, got, want, want) } } } func TestParseInfNan(t *testing.T) { tree, err := Parse([]byte("pos = inf\nneg = -inf\nbad = nan\n")) if err != nil { t.Fatalf("parse: %v", err) } if v := tree["pos"].(float64); !math.IsInf(v, 1) { t.Errorf("pos = %v, want +Inf", v) } if v := tree["neg"].(float64); !math.IsInf(v, -1) { t.Errorf("neg = %v, want -Inf", v) } if v := tree["bad"].(float64); !math.IsNaN(v) { t.Errorf("bad = %v, want NaN", v) } } func TestParseStrings(t *testing.T) { tree, err := Parse([]byte(` basic = "a\tb\nc" literal = 'C:\path\no\escape' quote = "say \"hi\"" unicode = "\u00e9" `)) if err != nil { t.Fatalf("parse: %v", err) } if tree["basic"] != "a\tb\nc" { t.Errorf("basic = %q", tree["basic"]) } if tree["literal"] != `C:\path\no\escape` { t.Errorf("literal = %q", tree["literal"]) } if tree["quote"] != `say "hi"` { t.Errorf("quote = %q", tree["quote"]) } if tree["unicode"] != "é" { t.Errorf("unicode = %q", tree["unicode"]) } } func TestParseMultilineString(t *testing.T) { tree, err := Parse([]byte("text = \"\"\"\nfirst\nsecond\"\"\"\n")) if err != nil { t.Fatalf("parse: %v", err) } if tree["text"] != "first\nsecond" { t.Errorf("text = %q, want %q", tree["text"], "first\nsecond") } } func TestParseMultilineLineEndingBackslash(t *testing.T) { tree, err := Parse([]byte("text = \"\"\"\\\n one \\\n two\"\"\"\n")) if err != nil { t.Fatalf("parse: %v", err) } if tree["text"] != "one two" { t.Errorf("text = %q, want %q", tree["text"], "one two") } } func TestParseTablesAndDottedKeys(t *testing.T) { tree, err := Parse([]byte(` owner.name = "Petr" [server] host = "localhost" port = 9090 [server.tls] enabled = true `)) if err != nil { t.Fatalf("parse: %v", err) } server := tree["server"].(map[string]any) if server["host"] != "localhost" || server["port"] != int64(9090) { t.Errorf("server = %#v", server) } tls := server["tls"].(map[string]any) if tls["enabled"] != true { t.Errorf("tls = %#v", tls) } owner := tree["owner"].(map[string]any) if owner["name"] != "Petr" { t.Errorf("owner = %#v", owner) } } func TestParseArrayOfTables(t *testing.T) { tree, err := Parse([]byte(` [[forms]] name = "contact" [[forms]] name = "feedback" `)) if err != nil { t.Fatalf("parse: %v", err) } forms := tree["forms"].([]map[string]any) if len(forms) != 2 { t.Fatalf("len(forms) = %d, want 2", len(forms)) } if forms[0]["name"] != "contact" || forms[1]["name"] != "feedback" { t.Errorf("forms = %#v", forms) } } func TestParseArraysAndInlineTables(t *testing.T) { tree, err := Parse([]byte(` ports = [80, 443] mixed = [ "a", "b", ] point = { x = 1, y = 2 } `)) if err != nil { t.Fatalf("parse: %v", err) } ports := tree["ports"].([]any) if len(ports) != 2 || ports[0] != int64(80) || ports[1] != int64(443) { t.Errorf("ports = %#v", ports) } mixed := tree["mixed"].([]any) if len(mixed) != 2 || mixed[0] != "a" || mixed[1] != "b" { t.Errorf("mixed = %#v", mixed) } point := tree["point"].(map[string]any) if point["x"] != int64(1) || point["y"] != int64(2) { t.Errorf("point = %#v", point) } } func TestParseDateTime(t *testing.T) { tree, err := Parse([]byte(` offset = 1979-05-27T07:32:00Z local = 1979-05-27T07:32:00 day = 1979-05-27 clock = 07:32:00 `)) if err != nil { t.Fatalf("parse: %v", err) } if off, ok := tree["offset"].(time.Time); !ok || off.Year() != 1979 || off.Hour() != 7 { t.Errorf("offset = %#v (%T)", tree["offset"], tree["offset"]) } if ldt, ok := tree["local"].(LocalDateTime); !ok || ldt.Year() != 1979 || ldt.Hour() != 7 { t.Errorf("local = %#v (%T)", tree["local"], tree["local"]) } if d, ok := tree["day"].(LocalDate); !ok || d.Month() != time.May || d.Day() != 27 { t.Errorf("day = %#v (%T)", tree["day"], tree["day"]) } if clk, ok := tree["clock"].(LocalTime); !ok || clk.Hour() != 7 || clk.Minute() != 32 { t.Errorf("clock = %#v (%T)", tree["clock"], tree["clock"]) } } func TestDateTimeFormats(t *testing.T) { tree, err := Parse([]byte("a = 1987-07-05 17:45:00Z\nb = 1987-07-05t17:45:00z\nc = 1977-12-21T10:32:00.555\n")) if err != nil { t.Fatalf("parse: %v", err) } if _, ok := tree["a"].(time.Time); !ok { t.Errorf("a is %T, want time.Time", tree["a"]) } if _, ok := tree["b"].(time.Time); !ok { t.Errorf("b is %T, want time.Time", tree["b"]) } if _, ok := tree["c"].(LocalDateTime); !ok { t.Errorf("c is %T, want LocalDateTime", tree["c"]) } } func TestUnmarshalDateTime(t *testing.T) { type Doc struct { Created time.Time `toml:"created"` Day LocalDate `toml:"day"` } var d Doc if err := Unmarshal([]byte("created = 2026-06-20T10:00:00Z\nday = 2026-06-20\n"), &d); err != nil { t.Fatalf("unmarshal: %v", err) } if d.Created.Year() != 2026 || d.Created.Hour() != 10 { t.Errorf("Created = %v", d.Created) } if d.Day.Year() != 2026 || d.Day.Month() != time.June || d.Day.Day() != 20 { t.Errorf("Day = %v", d.Day) } } func TestUnmarshalStruct(t *testing.T) { type SMTP struct { Host string `toml:"host"` Port int `toml:"port"` } type Form struct { Name string `toml:"name"` SMTP SMTP `toml:"smtp"` Origins []string `toml:"allowed_origins"` } type Config struct { Port int `toml:"port"` Forms []Form `toml:"forms"` } data := []byte(` port = 8080 [[forms]] name = "contact" allowed_origins = ["https://example.com"] [forms.smtp] host = "smtp.example.com" port = 587 `) var cfg Config if err := Unmarshal(data, &cfg); err != nil { t.Fatalf("unmarshal: %v", err) } if cfg.Port != 8080 { t.Errorf("Port = %d", cfg.Port) } if len(cfg.Forms) != 1 { t.Fatalf("len(Forms) = %d", len(cfg.Forms)) } f := cfg.Forms[0] if f.Name != "contact" || f.SMTP.Host != "smtp.example.com" || f.SMTP.Port != 587 { t.Errorf("form = %#v", f) } if len(f.Origins) != 1 || f.Origins[0] != "https://example.com" { t.Errorf("origins = %#v", f.Origins) } } func TestUnmarshalCaseInsensitiveAndUntagged(t *testing.T) { type Config struct { Title string Count int } var cfg Config if err := Unmarshal([]byte("title = \"x\"\ncount = 3\n"), &cfg); err != nil { t.Fatalf("unmarshal: %v", err) } if cfg.Title != "x" || cfg.Count != 3 { t.Errorf("cfg = %#v", cfg) } } func TestDisallowUnknownFields(t *testing.T) { type C struct { Known string `toml:"known"` } data := []byte("known = \"x\"\nbogus = 1\n") var lenient C if err := Unmarshal(data, &lenient); err != nil { t.Fatalf("lenient unmarshal: %v", err) } var strict C err := NewDecoder().DisallowUnknownFields().Decode(data, &strict) if err == nil { t.Fatal("expected error for unknown field, got nil") } } func TestSkippedFieldTag(t *testing.T) { type C struct { Keep string `toml:"keep"` Skip string `toml:"-"` } var c C if err := Unmarshal([]byte("keep = \"y\"\n"), &c); err != nil { t.Fatalf("unmarshal: %v", err) } if c.Keep != "y" || c.Skip != "" { t.Errorf("c = %#v", c) } } func TestSyntaxErrorReportsLine(t *testing.T) { _, err := Parse([]byte("a = 1\nb = \nc = 3\n")) if err == nil { t.Fatal("expected a syntax error") } se, ok := err.(*SyntaxError) if !ok { t.Fatalf("error is %T, want *SyntaxError", err) } if se.Line != 2 { t.Errorf("Line = %d, want 2", se.Line) } } func TestComments(t *testing.T) { tree, err := Parse([]byte(` # a leading comment key = "value" # trailing comment # another `)) if err != nil { t.Fatalf("parse: %v", err) } if tree["key"] != "value" { t.Errorf("key = %q", tree["key"]) } } func TestDuplicateKeyRejected(t *testing.T) { _, err := Parse([]byte("a = 1\na = 2\n")) if err == nil { t.Fatal("expected duplicate key error") } } func TestRejectsInvalidNumbers(t *testing.T) { for _, tok := range []string{ "01", "-01", "00", "1__0", "_1", "1_", "0x_1", "1_.0", "1.", ".5", "1.2.3", "1.e2", "0x", "0o", "0b", "0b2", "0o8", "0xG", "+0x1", } { if _, err := Parse([]byte("v = " + tok + "\n")); err == nil { t.Errorf("%q: expected an error, got none", tok) } } } func TestAcceptsNumberEdgeCases(t *testing.T) { cases := map[string]any{ "0": int64(0), "-0": int64(0), "+99": int64(99), "1_000": int64(1000), "0xDEAD_BEEF": int64(0xDEADBEEF), "0o755": int64(493), "0b1010": int64(10), "0.0": 0.0, "3.14": 3.14, "6.022e23": 6.022e23, "1e10": 1e10, "-2.5E-3": -2.5e-3, } for tok, want := range cases { tree, err := Parse([]byte("v = " + tok + "\n")) if err != nil { t.Errorf("%q: %v", tok, err) continue } if got := tree["v"]; got != want { t.Errorf("%q = %#v (%T), want %#v", tok, got, got, want) } } } func TestRejectsTableRedefinition(t *testing.T) { _, err := Parse([]byte("[a]\nx = 1\n\n[a]\ny = 2\n")) if err == nil { t.Fatal("expected a table-redefinition error") } } func TestAllowsImplicitThenExplicitTable(t *testing.T) { tree, err := Parse([]byte("[a.b]\nx = 1\n\n[a]\ny = 2\n")) if err != nil { t.Fatalf("parse: %v", err) } a := tree["a"].(map[string]any) if a["y"] != int64(2) { t.Errorf("a.y = %#v", a["y"]) } if b := a["b"].(map[string]any); b["x"] != int64(1) { t.Errorf("a.b.x = %#v", b["x"]) } } func TestRejectsControlCharInString(t *testing.T) { if _, err := Parse([]byte("v = \"a\x01b\"\n")); err == nil { t.Fatal("expected a control-character error") } } func TestAllowsEscapedControlChar(t *testing.T) { tree, err := Parse([]byte(`v = "\u0000"`)) if err != nil { t.Fatalf("parse: %v", err) } if tree["v"] != "\x00" { t.Errorf("v = %q", tree["v"]) } } func TestMultilineQuotesAtDelimiter(t *testing.T) { tree, err := Parse([]byte("a = '''''two quotes'''''\n")) if err != nil { t.Fatalf("parse: %v", err) } if tree["a"] != "''two quotes''" { t.Errorf("a = %q", tree["a"]) } } func TestRejectsInlineTableExtension(t *testing.T) { cases := map[string]string{ "by header": "a = { b = 1 }\n[a.c]\nx = 2\n", "by dotted key": "a = { b = 1 }\na.c = 2\n", "header over it": "a = { b = 1 }\n[a]\nx = 2\n", } for name, doc := range cases { if _, err := Parse([]byte(doc)); err == nil { t.Errorf("%s: expected an inline-table extension error", name) } } } func TestRejectsSpecInvalid(t *testing.T) { cases := map[string]string{ "single-digit hour": "a = 2023-10-01T1:32:00Z\n", "inline duplicate key": "a = { b = 1, b = 2 }\n", "inline dotted overwrite": "a = { b = 1, b.c = 2 }\n", "dotted over header": "[a.b]\nx = 1\n[a]\nb.y = 2\n", "table over array": "[[t]]\n[t]\n", } for name, doc := range cases { if _, err := Parse([]byte(doc)); err == nil { t.Errorf("%s: expected an error", name) } } } func TestArrayOfTablesPerElementSubtable(t *testing.T) { tree, err := Parse([]byte(` [[forms]] name = "a" [forms.smtp] host = "h1" [[forms]] name = "b" [forms.smtp] host = "h2" `)) if err != nil { t.Fatalf("parse: %v", err) } forms := tree["forms"].([]map[string]any) if len(forms) != 2 { t.Fatalf("len(forms) = %d", len(forms)) } if h := forms[0]["smtp"].(map[string]any)["host"]; h != "h1" { t.Errorf("forms[0].smtp.host = %v", h) } if h := forms[1]["smtp"].(map[string]any)["host"]; h != "h2" { t.Errorf("forms[1].smtp.host = %v", h) } }