Assisted-by: DeepSeek V4.1 Flash
This commit is contained in:
+200
@@ -8,6 +8,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"math"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -1372,3 +1373,202 @@ func TestEncodeErrorHeterogeneousArrayPath(t *testing.T) {
|
||||
t.Fatalf("Path = %q, want %q", ee.Path, "items[0]")
|
||||
}
|
||||
}
|
||||
|
||||
// --- encoding.TextMarshaler and time.Duration ------------------------------
|
||||
|
||||
// textTag is a value-receiver encoding.TextMarshaler, so the encoder finds the
|
||||
// method on the value itself.
|
||||
type textTag string
|
||||
|
||||
func (t textTag) MarshalText() ([]byte, error) { return []byte("tag:" + string(t)), nil }
|
||||
|
||||
// textPointer carries MarshalText on the pointer receiver only, so the encoder
|
||||
// has to look at the address of an addressable field.
|
||||
type textPointer struct{ V string }
|
||||
|
||||
func (t *textPointer) MarshalText() ([]byte, error) { return []byte(strings.ToUpper(t.V)), nil }
|
||||
|
||||
// textAndTOML implements both encoding interfaces; the TOML method wins.
|
||||
type textAndTOML struct{}
|
||||
|
||||
func (textAndTOML) MarshalTOML() (any, error) { return "toml", nil }
|
||||
func (textAndTOML) MarshalText() ([]byte, error) { return []byte("text"), nil }
|
||||
|
||||
// brokenText fails the marshal from MarshalText.
|
||||
type brokenText struct{}
|
||||
|
||||
func (brokenText) MarshalText() ([]byte, error) { return nil, errors.New("text boom") }
|
||||
|
||||
// notUTF8 renders bytes that no TOML string can carry.
|
||||
type notUTF8 struct{}
|
||||
|
||||
func (notUTF8) MarshalText() ([]byte, error) { return []byte{0xff, 0xfe}, nil }
|
||||
|
||||
// textTagBoth renders itself with a prefix and strips it again on decode, so
|
||||
// the round trip through a TOML string is lossless.
|
||||
type textTagBoth string
|
||||
|
||||
func (t textTagBoth) MarshalText() ([]byte, error) { return []byte("tag:" + string(t)), nil }
|
||||
|
||||
func (t *textTagBoth) UnmarshalText(text []byte) error {
|
||||
trimmed, ok := strings.CutPrefix(string(text), "tag:")
|
||||
if !ok {
|
||||
return errors.New("textTagBoth: missing the tag prefix")
|
||||
}
|
||||
*t = textTagBoth(trimmed)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestMarshalTextValues(t *testing.T) {
|
||||
// The pointer receiver is reachable only through an addressable field, so
|
||||
// the whole value is marshalled through a pointer here.
|
||||
type Cfg struct {
|
||||
IP net.IP `toml:"ip"`
|
||||
Duration time.Duration `toml:"duration"`
|
||||
Tag textTag `toml:"tag"`
|
||||
Pointer textPointer `toml:"pointer"`
|
||||
Both textAndTOML `toml:"both"`
|
||||
}
|
||||
out, err := Marshal(&Cfg{
|
||||
IP: net.IPv4(192, 0, 2, 1),
|
||||
Duration: 90 * time.Minute,
|
||||
Tag: "x",
|
||||
Pointer: textPointer{V: "abc"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
want := "ip = \"192.0.2.1\"\nduration = \"1h30m0s\"\ntag = \"tag:x\"\npointer = \"ABC\"\nboth = \"toml\"\n"
|
||||
if string(out) != want {
|
||||
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalTextValuesInContainers(t *testing.T) {
|
||||
// Slice elements are addressable, so a pointer-receiver MarshalText is used
|
||||
// there too, and an array of such values stays a value array: each element's
|
||||
// TOML form is a string, so the [[header]] form cannot carry it.
|
||||
type Cfg struct {
|
||||
Map map[string]net.IP `toml:"map"`
|
||||
Durs []time.Duration `toml:"durs"`
|
||||
Ptrs []textPointer `toml:"ptrs"`
|
||||
Empty []textPointer `toml:"empty"`
|
||||
}
|
||||
out, err := Marshal(Cfg{
|
||||
Map: map[string]net.IP{"a": net.IPv4(10, 0, 0, 1)},
|
||||
Durs: []time.Duration{0, 250 * time.Millisecond},
|
||||
Ptrs: []textPointer{{V: "a"}, {V: "b"}},
|
||||
Empty: []textPointer{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
want := "durs = [\"0s\", \"250ms\"]\nptrs = [\"A\", \"B\"]\nempty = []\n\n[map]\na = \"10.0.0.1\"\n"
|
||||
if string(out) != want {
|
||||
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalTextLeavesDateTimesAlone(t *testing.T) {
|
||||
// The four date-time types carry time.Time's text methods through an
|
||||
// embedded field; their TOML form is a bare atom, never a quoted string.
|
||||
stamp := time.Date(2026, 6, 26, 10, 0, 0, 0, time.UTC)
|
||||
type Cfg struct {
|
||||
Stamp time.Time `toml:"stamp"`
|
||||
Ptr *time.Time `toml:"ptr"`
|
||||
Day LocalDate `toml:"day"`
|
||||
At LocalDateTime `toml:"at"`
|
||||
Clock LocalTime `toml:"clock"`
|
||||
}
|
||||
out, err := Marshal(&Cfg{
|
||||
Stamp: stamp,
|
||||
Ptr: &stamp,
|
||||
Day: LocalDate{time.Date(1979, 5, 27, 0, 0, 0, 0, time.UTC)},
|
||||
At: LocalDateTime{time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC)},
|
||||
Clock: LocalTime{time.Date(0, 1, 1, 7, 32, 0, 0, time.UTC)},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
want := "stamp = 2026-06-26T10:00:00Z\nptr = 2026-06-26T10:00:00Z\nday = 1979-05-27\nat = 1979-05-27T07:32:00\nclock = 07:32:00\n"
|
||||
if string(out) != want {
|
||||
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalTextNilPointerOmitted(t *testing.T) {
|
||||
type Cfg struct {
|
||||
P *textPointer `toml:"p"`
|
||||
K string `toml:"k"`
|
||||
}
|
||||
out, err := Marshal(&Cfg{K: "x"})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
if want := "k = \"x\"\n"; string(out) != want {
|
||||
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalTextErrorCarriesPath(t *testing.T) {
|
||||
type Inner struct {
|
||||
F brokenText `toml:"f"`
|
||||
}
|
||||
type Cfg struct {
|
||||
Inner Inner `toml:"inner"`
|
||||
}
|
||||
_, err := Marshal(Cfg{})
|
||||
if err == nil {
|
||||
t.Fatal("expected an error from MarshalText")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "text boom") {
|
||||
t.Errorf("err = %v, want substring \"text boom\"", err)
|
||||
}
|
||||
ee, ok := errors.AsType[*EncodeError](err)
|
||||
if !ok {
|
||||
t.Fatalf("expected an *EncodeError, got %T: %v", err, err)
|
||||
}
|
||||
if ee.Path != "inner.f" {
|
||||
t.Fatalf("Path = %q, want %q", ee.Path, "inner.f")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalTextRejectsInvalidUTF8(t *testing.T) {
|
||||
// A TOML string holds UTF-8 only, so text that is not gets an error rather
|
||||
// than replacement characters.
|
||||
_, err := Marshal(struct {
|
||||
V notUTF8 `toml:"v"`
|
||||
}{})
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for text that is not valid UTF-8")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "UTF-8") {
|
||||
t.Errorf("err = %v, want a UTF-8 message", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalTextValuesRoundTrip(t *testing.T) {
|
||||
type Cfg struct {
|
||||
Duration time.Duration `toml:"duration"`
|
||||
IP net.IP `toml:"ip"`
|
||||
Tag textTagBoth `toml:"tag"`
|
||||
}
|
||||
in := Cfg{Duration: 90 * time.Minute, IP: net.IPv4(198, 51, 100, 7), Tag: "y"}
|
||||
out, err := Marshal(&in)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
var back Cfg
|
||||
if err := Unmarshal(out, &back); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if back.Duration != in.Duration {
|
||||
t.Errorf("Duration = %v, want %v", back.Duration, in.Duration)
|
||||
}
|
||||
if !back.IP.Equal(in.IP) {
|
||||
t.Errorf("IP = %v, want %v", back.IP, in.IP)
|
||||
}
|
||||
if back.Tag != in.Tag {
|
||||
t.Errorf("Tag = %q, want %q", back.Tag, in.Tag)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user