// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package interpres import ( "context" "errors" "fmt" "math" "strings" "testing" ) func TestSyntaxErrorMessage(t *testing.T) { err := &SyntaxError{Line: 7, Msg: "expected '=' after key"} want := "interpres: line 7: expected '=' after key" if got := err.Error(); got != want { t.Errorf("Error() = %q, want %q", got, want) } } func TestParseRejectsInvalidUTF8(t *testing.T) { _, err := Parse([]byte("v = \"\xff\"\n")) if err == nil { t.Fatal("expected a UTF-8 validation error") } se, ok := err.(*SyntaxError) if !ok { t.Fatalf("err is %T, want *SyntaxError", err) } if !strings.Contains(se.Msg, "UTF-8") { t.Errorf("Msg = %q, want it to mention UTF-8", se.Msg) } if se.Line != 1 { t.Errorf("Line = %d, want 1", se.Line) } } func TestUnmarshalIntoMap(t *testing.T) { var m map[string]any if err := Unmarshal([]byte(`name = "x" count = 3 `), &m); err != nil { t.Fatalf("unmarshal: %v", err) } if m["name"] != "x" { t.Errorf("name = %#v", m["name"]) } if m["count"] != int64(3) { t.Errorf("count = %#v (%T)", m["count"], m["count"]) } } func TestUnmarshalIntoMapNested(t *testing.T) { var m map[string]any if err := Unmarshal([]byte("[a]\nb = 2\n"), &m); err != nil { t.Fatalf("unmarshal: %v", err) } a, ok := m["a"].(map[string]any) if !ok { t.Fatalf("a = %T, want map[string]any", m["a"]) } if a["b"] != int64(2) { t.Errorf("a.b = %#v", a["b"]) } } func TestUnmarshalPrefilledMap(t *testing.T) { m := map[string]any{"keep": "yes"} if err := Unmarshal([]byte(`name = "x"`), &m); err != nil { t.Fatalf("unmarshal: %v", err) } if m["keep"] != "yes" { t.Errorf("keep = %#v", m["keep"]) } if m["name"] != "x" { t.Errorf("name = %#v", m["name"]) } } func TestUnmarshalTableToNonStructOrMap(t *testing.T) { var s string if err := Unmarshal([]byte("v = 1\n"), &s); err == nil { t.Fatal("expected an error for table-to-scalar") } } func TestUnmarshalIntoAny(t *testing.T) { // A non-nil any destination must accept the parsed tree. var x any if err := Unmarshal([]byte("[a]\nb = 2\n"), &x); err != nil { t.Fatalf("unmarshal: %v", err) } tree, ok := x.(map[string]any) if !ok { t.Fatalf("x is %T, want map[string]any", x) } a, ok := tree["a"].(map[string]any) if !ok { t.Fatalf("a is %T, want map[string]any", tree["a"]) } if a["b"] != int64(2) { t.Errorf("a.b = %#v", a["b"]) } } func TestUnmarshalIntoNilAny(t *testing.T) { // A nil any target must still receive the parsed tree without // panicking. var x any if err := Unmarshal([]byte("v = 1\n"), &x); err != nil { t.Fatalf("unmarshal: %v", err) } if x == nil { t.Fatal("x is still nil after Unmarshal") } tree, ok := x.(map[string]any) if !ok { t.Fatalf("x is %T, want map[string]any", x) } if tree["v"] != int64(1) { t.Errorf("v = %#v", tree["v"]) } } func TestParseContextHonoursCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() if _, err := ParseContext(ctx, []byte("a = 1\n")); !errors.Is(err, context.Canceled) { t.Fatalf("ParseContext returned %v, want context.Canceled", err) } } func TestUnmarshalContextHonoursCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() var cfg map[string]any if err := UnmarshalContext(ctx, []byte("a = 1\n"), &cfg); !errors.Is(err, context.Canceled) { t.Fatalf("UnmarshalContext returned %v, want context.Canceled", err) } } func TestDecoderDecodeContextHonoursCancellation(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() var cfg map[string]any err := NewDecoder().DecodeContext(ctx, []byte("a = 1\n"), &cfg) if !errors.Is(err, context.Canceled) { t.Fatalf("DecodeContext returned %v, want context.Canceled", err) } } func TestContextRoundTrip(t *testing.T) { // The *Context variants with a Background context must produce the same // result as the non-context variants for ordinary inputs. in := []byte(`title = "x" count = 3 `) if _, err := ParseContext(context.Background(), in); err != nil { t.Fatalf("ParseContext: %v", err) } var out struct { Title string `toml:"title"` Count int `toml:"count"` } if err := UnmarshalContext(context.Background(), in, &out); err != nil { t.Fatalf("UnmarshalContext: %v", err) } if out.Title != "x" || out.Count != 3 { t.Errorf("out = %#v", out) } if err := NewDecoder().DecodeContext(context.Background(), in, &map[string]any{}); err != nil { t.Fatalf("Decoder.DecodeContext: %v", err) } } func TestUnmarshalIntToUintOverflow(t *testing.T) { type C struct { X uint8 `toml:"x"` } var c C err := Unmarshal([]byte("x = 300\n"), &c) if err == nil { t.Fatal("expected overflow error") } if !strings.Contains(err.Error(), "overflow") { t.Errorf("err = %v, want substring 'overflow'", err) } } func TestUnmarshalIntToUint16Boundary(t *testing.T) { type C struct { X uint16 `toml:"x"` } // Exactly 65535 fits, 65536 does not. var ok C if err := Unmarshal([]byte("x = 65535\n"), &ok); err != nil { t.Fatalf("65535 should fit uint16, got %v", err) } if ok.X != 65535 { t.Errorf("X = %d, want 65535", ok.X) } var bad C if err := Unmarshal([]byte("x = 65536\n"), &bad); err == nil { t.Fatal("65536 must not fit uint16") } } func TestUnmarshalIntToUint64FitsMaxInt64(t *testing.T) { type C struct { X uint64 `toml:"x"` } var c C tok := "x = 9223372036854775807\n" // math.MaxInt64 if err := Unmarshal([]byte(tok), &c); err != nil { t.Fatalf("MaxInt64 should fit uint64, got %v", err) } if c.X != math.MaxInt64 { t.Errorf("X = %d, want %d", uint64(c.X), uint64(math.MaxInt64)) } } func TestUnmarshalNegativeIntToUint(t *testing.T) { type C struct { X uint8 `toml:"x"` } var c C err := Unmarshal([]byte("x = -1\n"), &c) if err == nil { t.Fatal("expected negative-to-uint error") } if !strings.Contains(err.Error(), "negative") { t.Errorf("err = %v, want substring 'negative'", err) } } func TestUnmarshalIntToFloat(t *testing.T) { type C struct { X float64 `toml:"x"` } var c C if err := Unmarshal([]byte("x = 5\n"), &c); err != nil { t.Fatalf("unmarshal: %v", err) } if c.X != 5.0 { t.Errorf("X = %v, want 5.0", c.X) } } func TestUnmarshalIntToStringFails(t *testing.T) { type C struct { X string `toml:"x"` } var c C if err := Unmarshal([]byte(`x = 5`), &c); err == nil { t.Fatal("expected type-mismatch error") } } func TestUnmarshalStringToIntFails(t *testing.T) { type C struct { X int `toml:"x"` } var c C if err := Unmarshal([]byte(`x = "hello"`), &c); err == nil { t.Fatal("expected type-mismatch error") } } func TestUnmarshalArrayToScalarFails(t *testing.T) { type C struct { X int `toml:"x"` } var c C if err := Unmarshal([]byte("x = [1, 2]\n"), &c); err == nil { t.Fatal("expected type-mismatch error") } } func TestUnmarshalArrayOfTablesToScalarFails(t *testing.T) { type Item struct { Name string `toml:"name"` } type C struct { X Item `toml:"x"` } var c C if err := Unmarshal([]byte("[[x]]\nname = \"a\"\n"), &c); err == nil { t.Fatal("expected type-mismatch error") } } func TestUnmarshalFloatToIntFails(t *testing.T) { type C struct { X int `toml:"x"` } var c C if err := Unmarshal([]byte("x = 1.5\n"), &c); err == nil { t.Fatal("expected type-mismatch error") } } func TestUnmarshalBoolToIntFails(t *testing.T) { type C struct { X int `toml:"x"` } var c C if err := Unmarshal([]byte("x = true\n"), &c); err == nil { t.Fatal("expected type-mismatch error") } } func TestUnmarshalNonPointerRejected(t *testing.T) { var v int if err := Unmarshal([]byte("x = 1\n"), v); err == nil { t.Fatal("expected error for non-pointer target") } } func TestUnmarshalAssignErrorWrapped(t *testing.T) { // assignStruct wraps inner assignment errors with the key name. type C struct { Inner struct { X int `toml:"x"` } `toml:"inner"` } var c C err := Unmarshal([]byte("[inner]\nx = \"oops\"\n"), &c) if err == nil { t.Fatal("expected an assignment error") } if !strings.Contains(err.Error(), "x") { t.Errorf("err = %v, want it to mention key x", err) } } func TestUnmarshalAssignMapErrorWrapped(t *testing.T) { // assignMap wraps inner errors with the key of the bad element. m := map[string]int{} err := Unmarshal([]byte("[s]\nx = 1\n"), &m) if err == nil { t.Fatal("expected an assignment error") } if !strings.Contains(err.Error(), "s") { t.Errorf("err = %v, want it to mention key 's'", err) } } func TestUnmarshalAssignSliceErrorWrapped(t *testing.T) { // assignSlice wraps inner errors with the bad element's index. type C struct { Items []int `toml:"items"` } var c C err := Unmarshal([]byte(`items = [1, "oops"]`), &c) if err == nil { t.Fatal("expected an assignment error") } if !strings.Contains(err.Error(), "[1]") { t.Errorf("err = %v, want it to mention index [1]", err) } } func TestUnmarshalAssignTimeToWrongTypeFails(t *testing.T) { // assign maps time.Time to time.Time only. type C struct { T string `toml:"t"` } var c C err := Unmarshal([]byte("t = 2026-01-01T00:00:00Z\n"), &c) if err == nil { t.Fatal("expected time-to-string assignment to fail") } } func TestMarshalerUsesCustomEncoding(t *testing.T) { // A type that implements Marshaler can be encoded through a wrapping struct. val := inlineMarshaler(func() (any, error) { return map[string]any{"k": "v"}, nil }) type wrap struct { Inner inlineMarshaler `toml:"inner"` } out, err := Marshal(wrap{Inner: val}) if err != nil { t.Fatalf("marshal: %v", err) } got := string(out) if !strings.Contains(got, "[inner]") || !strings.Contains(got, "k = \"v\"") { t.Errorf("out = %q, want a [inner] table with k = \"v\"", got) } } type inlineMarshaler func() (any, error) func (i inlineMarshaler) MarshalTOML() (any, error) { return i() } // --- Unmarshaler ----------------------------------------------------------- // receiverSetter implements *Unmarshaler by reshaping a parsed table. type receiverSetter struct { Field string Received any } func (r *receiverSetter) UnmarshalTOML(data any) error { m, ok := data.(map[string]any) if !ok { return fmt.Errorf("interpres: receiverSetter expects table, got %T", data) } if v, ok := m["field"].(string); ok { r.Field = v } r.Received = data return nil } func TestUnmarshalerByPointer(t *testing.T) { type Cfg struct { R receiverSetter `toml:"r"` } var cfg Cfg if err := Unmarshal([]byte(`[r] field = "x" `), &cfg); err != nil { t.Fatalf("unmarshal: %v", err) } if cfg.R.Field != "x" { t.Errorf("Field = %q, want \"x\"", cfg.R.Field) } if cfg.R.Received == nil { t.Error("Receiver did not see parsed data") } } type scalarUnmarshaler struct{ val string } func (s *scalarUnmarshaler) UnmarshalTOML(data any) error { str, ok := data.(string) if !ok { return fmt.Errorf("interpres: scalarUnmarshaler expects string, got %T", data) } s.val = str return nil } func TestUnmarshalerReceivesRawScalar(t *testing.T) { // Place the Unmarshaler-implementing field inside a wrapper struct so // the decoder dispatches the scalar value to its UnmarshalTOML. type Cfg struct { S scalarUnmarshaler `toml:"s"` } var cfg Cfg if err := Unmarshal([]byte(`s = "hello"`), &cfg); err != nil { t.Fatalf("unmarshal: %v", err) } if cfg.S.val != "hello" { t.Errorf("cfg.S.val = %q, want \"hello\"", cfg.S.val) } } type failingUnmarshaler struct{} func (f *failingUnmarshaler) UnmarshalTOML(_ any) error { return errors.New("boom") } func TestUnmarshalerErrorPropagates(t *testing.T) { type Cfg struct { F failingUnmarshaler `toml:"f"` } var cfg Cfg if err := Unmarshal([]byte("f = 1"), &cfg); err == nil { t.Fatal("expected error from UnmarshalTOML") } else if !strings.Contains(err.Error(), "boom") { t.Errorf("err = %v, want substring \"boom\"", err) } } func TestUnmarshalerTakesPrecedenceOverDefault(t *testing.T) { // Even when the field is a scalar type and the value is a table, // UnmarshalTOML wins, because the receiver decides. type Cfg struct { R receiverSetter `toml:"r"` } var cfg Cfg in := []byte(`[r] field = "y" `) if err := Unmarshal(in, &cfg); err != nil { t.Fatalf("unmarshal: %v", err) } if cfg.R.Field != "y" { t.Errorf("Field = %q, want \"y\"", cfg.R.Field) } } // --- embedded field symmetry ----------------------------------------------- type RoundTripBase struct { ID int `toml:"id"` Name string `toml:"name"` } type RoundTripDerived struct { RoundTripBase X string `toml:"x"` } func TestUnmarshalEmbeddedStructRoundTrip(t *testing.T) { orig := RoundTripDerived{ID: 1, Name: "b", X: "x"} out, err := Marshal(orig) if err != nil { t.Fatalf("marshal: %v", err) } var back RoundTripDerived if err := Unmarshal(out, &back); err != nil { t.Fatalf("unmarshal: %v", err) } if back != orig { t.Fatalf("round-trip mismatch:\nwas: %+v\nnow: %+v", orig, back) } } type RoundTripPtrCfg struct { *RoundTripBase X string `toml:"x"` } func TestUnmarshalEmbeddedPointerStruct(t *testing.T) { var cfg RoundTripPtrCfg if err := Unmarshal([]byte("id = 7\nname = \"n\"\nx = \"x\"\n"), &cfg); err != nil { t.Fatalf("unmarshal: %v", err) } if cfg.RoundTripBase == nil || cfg.ID != 7 || cfg.Name != "n" || cfg.X != "x" { t.Fatalf("decoded: %+v", cfg) } } type RoundTripExtra map[string]int type RoundTripMapCfg struct { RoundTripExtra X string `toml:"x"` } func TestUnmarshalEmbeddedMap(t *testing.T) { var cfg RoundTripMapCfg if err := Unmarshal([]byte("alpha = 1\nx = \"x\"\n"), &cfg); err != nil { t.Fatalf("unmarshal: %v", err) } if cfg.RoundTripExtra["alpha"] != 1 || cfg.X != "x" { t.Fatalf("decoded: %+v", cfg) } orig := RoundTripMapCfg{RoundTripExtra: RoundTripExtra{"a": 1}, X: "x"} out, err := Marshal(orig) if err != nil { t.Fatalf("marshal: %v", err) } var back RoundTripMapCfg if err := Unmarshal(out, &back); err != nil { t.Fatalf("unmarshal: %v", err) } if back.X != "x" || back.RoundTripExtra["a"] != 1 { t.Fatalf("round-trip mismatch: %+v", back) } } func TestUnmarshalEmbeddedNameClashShallowerWins(t *testing.T) { type Inner struct { Name string `toml:"name"` Deep string `toml:"deep"` } type Outer struct { Inner Name string `toml:"name"` } var v Outer if err := Unmarshal([]byte("name = \"outer\"\ndeep = \"d\"\n"), &v); err != nil { t.Fatalf("unmarshal: %v", err) } if v.Name != "outer" || v.Deep != "d" { t.Fatalf("decoded: %+v", v) } } func TestUnmarshalUnknownKeyWithoutEmbeddedMap(t *testing.T) { var cfg RoundTripDerived if err := Unmarshal([]byte("rogue = 1\n"), &cfg); err != nil { t.Fatalf("unmarshal: %v", err) } if cfg.ID != 0 || cfg.X != "" { t.Fatalf("decoded: %+v", cfg) } } func TestUnmarshalStrictEmbeddedMapStaysStrict(t *testing.T) { type Cfg struct { RoundTripExtra Name string `toml:"name"` } dec := NewDecoder().DisallowUnknownFields() err := dec.Decode([]byte("name = \"n\"\nrogue = 1\n"), &Cfg{}) if err == nil || !strings.Contains(err.Error(), "unknown field") { t.Fatalf("expected unknown field error, got: %v", err) } }