Assisted-by: DeepSeek V4.1 Flash
This commit is contained in:
+204
@@ -8,9 +8,11 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSyntaxErrorMessage(t *testing.T) {
|
||||
@@ -747,3 +749,205 @@ func TestUnmarshalIntoDefinedScalarTypes(t *testing.T) {
|
||||
t.Error("F = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
// --- encoding.TextUnmarshaler and time.Duration ----------------------------
|
||||
|
||||
// textReceiver implements encoding.TextUnmarshaler on the pointer receiver.
|
||||
type textReceiver struct{ Text string }
|
||||
|
||||
func (t *textReceiver) UnmarshalText(text []byte) error {
|
||||
t.Text = "got:" + string(text)
|
||||
return nil
|
||||
}
|
||||
|
||||
// upperText is a defined string type whose UnmarshalText transforms the
|
||||
// content, so a plain string assignment would leave the wrong value behind.
|
||||
type upperText string
|
||||
|
||||
func (u *upperText) UnmarshalText(text []byte) error {
|
||||
*u = upperText(strings.ToUpper(string(text)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// failingTextUnmarshaler fails the decode from UnmarshalText.
|
||||
type failingTextUnmarshaler struct{}
|
||||
|
||||
func (f *failingTextUnmarshaler) UnmarshalText(_ []byte) error { return errors.New("text boom") }
|
||||
|
||||
// textAndTOMLReceiver implements both decode interfaces; the TOML method wins.
|
||||
type textAndTOMLReceiver struct{ From string }
|
||||
|
||||
func (t *textAndTOMLReceiver) UnmarshalTOML(any) error { t.From = "toml"; return nil }
|
||||
|
||||
func (t *textAndTOMLReceiver) UnmarshalText([]byte) error { t.From = "text"; return nil }
|
||||
|
||||
func TestTextUnmarshalerByPointer(t *testing.T) {
|
||||
type Cfg struct {
|
||||
R textReceiver `toml:"r"`
|
||||
}
|
||||
var cfg Cfg
|
||||
if err := Unmarshal([]byte(`r = "hello"`), &cfg); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if cfg.R.Text != "got:hello" {
|
||||
t.Errorf("Text = %q, want \"got:hello\"", cfg.R.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextUnmarshalerWinsOverKindAssignment(t *testing.T) {
|
||||
type Cfg struct {
|
||||
U upperText `toml:"u"`
|
||||
}
|
||||
var cfg Cfg
|
||||
if err := Unmarshal([]byte(`u = "abc"`), &cfg); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if cfg.U != "ABC" {
|
||||
t.Errorf("U = %q, want \"ABC\"", cfg.U)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextUnmarshalerForNetIP(t *testing.T) {
|
||||
type Cfg struct {
|
||||
V4 net.IP `toml:"v4"`
|
||||
V6 net.IP `toml:"v6"`
|
||||
IPs []net.IP `toml:"ips"`
|
||||
}
|
||||
in := "v4 = \"192.0.2.1\"\nv6 = \"2001:db8::68\"\nips = [\"198.51.100.7\", \"203.0.113.9\"]\n"
|
||||
var cfg Cfg
|
||||
if err := Unmarshal([]byte(in), &cfg); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if got := cfg.V4.String(); got != "192.0.2.1" {
|
||||
t.Errorf("V4 = %q, want \"192.0.2.1\"", got)
|
||||
}
|
||||
if got := cfg.V6.String(); got != "2001:db8::68" {
|
||||
t.Errorf("V6 = %q, want \"2001:db8::68\"", got)
|
||||
}
|
||||
if len(cfg.IPs) != 2 || cfg.IPs[0].String() != "198.51.100.7" || cfg.IPs[1].String() != "203.0.113.9" {
|
||||
t.Errorf("IPs = %v, want two addresses", cfg.IPs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextUnmarshalerSeesStringsOnly(t *testing.T) {
|
||||
// An integer keeps its own rule: the text method is not consulted, and the
|
||||
// value does not reach the receiver.
|
||||
type Cfg struct {
|
||||
R textReceiver `toml:"r"`
|
||||
}
|
||||
var cfg Cfg
|
||||
err := Unmarshal([]byte("r = 1\n"), &cfg)
|
||||
if err == nil {
|
||||
t.Fatal("expected an integer to be rejected for a text receiver")
|
||||
}
|
||||
if cfg.R.Text != "" {
|
||||
t.Errorf("Text = %q, want it untouched", cfg.R.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalTOMLWinsOverTextUnmarshaler(t *testing.T) {
|
||||
type Cfg struct {
|
||||
B textAndTOMLReceiver `toml:"b"`
|
||||
}
|
||||
var cfg Cfg
|
||||
if err := Unmarshal([]byte(`b = "x"`), &cfg); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if cfg.B.From != "toml" {
|
||||
t.Errorf("From = %q, want \"toml\"", cfg.B.From)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextUnmarshalerErrorCarriesPath(t *testing.T) {
|
||||
type Inner struct {
|
||||
F failingTextUnmarshaler `toml:"f"`
|
||||
}
|
||||
type Cfg struct {
|
||||
Inner Inner `toml:"inner"`
|
||||
}
|
||||
var cfg Cfg
|
||||
err := Unmarshal([]byte("[inner]\nf = \"x\"\n"), &cfg)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error from UnmarshalText")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unmarshal text: text boom") {
|
||||
t.Errorf("err = %v, want the text error wrapped", err)
|
||||
}
|
||||
de, ok := errors.AsType[*DecodeError](err)
|
||||
if !ok {
|
||||
t.Fatalf("expected a *DecodeError, got %T: %v", err, err)
|
||||
}
|
||||
if !slices.Equal(de.Path, []string{"inner", "f"}) {
|
||||
t.Fatalf("Path = %v, want [inner f]", de.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextUnmarshalerReportsBadText(t *testing.T) {
|
||||
var cfg struct {
|
||||
IP net.IP `toml:"ip"`
|
||||
}
|
||||
err := Unmarshal([]byte(`ip = "not-an-ip"`), &cfg)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for a malformed address")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unmarshal text:") {
|
||||
t.Errorf("err = %v, want it wrapped as a text error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalDurations(t *testing.T) {
|
||||
type Cfg struct {
|
||||
FromText time.Duration `toml:"from_text"`
|
||||
FromInt time.Duration `toml:"from_int"`
|
||||
Fraction time.Duration `toml:"fraction"`
|
||||
}
|
||||
in := "from_text = \"1h30m\"\nfrom_int = 5400000000000\nfraction = \"1.5s\"\n"
|
||||
var cfg Cfg
|
||||
if err := Unmarshal([]byte(in), &cfg); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if cfg.FromText != 90*time.Minute {
|
||||
t.Errorf("FromText = %v, want %v", cfg.FromText, 90*time.Minute)
|
||||
}
|
||||
if cfg.FromInt != 90*time.Minute {
|
||||
t.Errorf("FromInt = %v, want %v", cfg.FromInt, 90*time.Minute)
|
||||
}
|
||||
if cfg.Fraction != 1500*time.Millisecond {
|
||||
t.Errorf("Fraction = %v, want %v", cfg.Fraction, 1500*time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalDurationRejectsMalformedText(t *testing.T) {
|
||||
var cfg struct {
|
||||
D time.Duration `toml:"d"`
|
||||
}
|
||||
err := Unmarshal([]byte("d = \"90\"\n"), &cfg)
|
||||
if err == nil {
|
||||
t.Fatal("expected an error for a duration without a unit")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "invalid duration") {
|
||||
t.Errorf("err = %v, want an invalid-duration message", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuotedStringNeverBecomesDateTime(t *testing.T) {
|
||||
// The date-time types take a bare timestamp only, so the text path is
|
||||
// excluded for them and a quoted string stays a string.
|
||||
var stamp struct {
|
||||
S time.Time `toml:"s"`
|
||||
}
|
||||
err := Unmarshal([]byte("s = \"2026-06-26T10:00:00Z\"\n"), &stamp)
|
||||
if err == nil {
|
||||
t.Fatal("expected a quoted string to be rejected for time.Time")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "cannot assign string") {
|
||||
t.Errorf("err = %v, want a cannot-assign message", err)
|
||||
}
|
||||
|
||||
var day struct {
|
||||
D LocalDate `toml:"d"`
|
||||
}
|
||||
if err := Unmarshal([]byte("d = \"1979-05-27\"\n"), &day); err == nil {
|
||||
t.Fatal("expected a quoted string to be rejected for LocalDate")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user