test: expand coverage across readers, writers, cmd, ui, diff and merge

Implements tests/_plans and previously deferred packages; updates plan
README with new coverage numbers.
This commit is contained in:
2026-10-03 21:33:59 +02:00
parent a32647ee16
commit 495a21b67b
50 changed files with 8461 additions and 8 deletions
@@ -0,0 +1,207 @@
package sqltypes
import (
"database/sql/driver"
"encoding/json"
"encoding/xml"
"reflect"
"strings"
"testing"
"github.com/google/uuid"
"gopkg.in/yaml.v3"
)
// arrayPtr is the pointer-receiver surface shared by every nullable array type.
type arrayPtr[T any] interface {
*T
Scan(any) error
UnmarshalJSON([]byte) error
UnmarshalYAML(*yaml.Node) error
UnmarshalXML(*xml.Decoder, xml.StartElement) error
}
// arrayValue is the value-receiver surface shared by every nullable array type.
type arrayValue interface {
Value() (driver.Value, error)
MarshalJSON() ([]byte, error)
MarshalYAML() (any, error)
MarshalXML(*xml.Encoder, xml.StartElement) error
}
type wrapped[T any] struct {
XMLName xml.Name `yaml:"-" xml:"w"`
V T `yaml:"v" xml:"v"`
}
// arrayRoundTrip runs the full Scan/Value/JSON/YAML/XML contract for one array type.
// badScan is a literal the type's Scan must reject ("" skips the check).
func arrayRoundTrip[T any, P arrayPtr[T]](t *testing.T, sample T, null T, badScan string) {
t.Helper()
sv, ok := any(sample).(arrayValue)
if !ok {
t.Fatalf("%T does not implement the array value surface", sample)
}
nv := any(null).(arrayValue)
t.Run("scan-value", func(t *testing.T) {
val, err := sv.Value()
if err != nil || val == nil {
t.Fatalf("Value: %v %v", val, err)
}
for _, in := range []any{val, []byte(val.(string))} {
var got T
if err := P(&got).Scan(in); err != nil {
t.Fatalf("Scan(%T): %v", in, err)
}
if !reflect.DeepEqual(got, sample) {
t.Errorf("Scan(%T) = %+v, want %+v", in, got, sample)
}
}
if v, err := nv.Value(); v != nil || err != nil {
t.Errorf("null Value = %v, %v", v, err)
}
got := sample
if err := P(&got).Scan(nil); err != nil || !reflect.DeepEqual(got, null) {
t.Errorf("Scan(nil) = %+v, %v", got, err)
}
if err := P(&got).Scan(12345); err == nil {
t.Error("Scan(int) must fail")
}
if badScan != "" {
var bad T
if err := P(&bad).Scan(badScan); err == nil {
t.Errorf("Scan(%q) must fail", badScan)
}
}
})
t.Run("json", func(t *testing.T) {
b, err := sv.MarshalJSON()
if err != nil {
t.Fatal(err)
}
var got T
if err := P(&got).UnmarshalJSON(b); err != nil || !reflect.DeepEqual(got, sample) {
t.Errorf("round trip = %+v, %v", got, err)
}
nb, _ := nv.MarshalJSON()
if string(nb) != "null" {
t.Errorf("null marshals to %s", nb)
}
got = sample
if err := P(&got).UnmarshalJSON([]byte(" null ")); err != nil || !reflect.DeepEqual(got, null) {
t.Errorf("null unmarshal = %+v, %v", got, err)
}
if err := P(&got).UnmarshalJSON([]byte(`{}`)); err == nil {
t.Error("object must be rejected")
}
})
t.Run("yaml", func(t *testing.T) {
b, err := yaml.Marshal(wrapped[T]{V: sample})
if err != nil {
t.Fatal(err)
}
var got wrapped[T]
if err := yaml.Unmarshal(b, &got); err != nil || !reflect.DeepEqual(got.V, sample) {
t.Errorf("round trip = %+v, %v\n%s", got.V, err, b)
}
nb, err := yaml.Marshal(wrapped[T]{V: null})
if err != nil || !strings.Contains(string(nb), "null") {
t.Errorf("null marshal = %q, %v", nb, err)
}
// yaml.v3 skips UnmarshalYAML for null, so decode into a fresh value.
got = wrapped[T]{}
if err := yaml.Unmarshal(nb, &got); err != nil || !reflect.DeepEqual(got.V, null) {
t.Errorf("null unmarshal = %+v, %v", got.V, err)
}
var bad wrapped[T]
if err := yaml.Unmarshal([]byte("v: {a: b}\n"), &bad); err == nil {
t.Error("mapping must be rejected")
}
})
t.Run("xml", func(t *testing.T) {
b, err := xml.Marshal(wrapped[T]{V: sample})
if err != nil {
t.Fatal(err)
}
var got wrapped[T]
if err := xml.Unmarshal(b, &got); err != nil || !reflect.DeepEqual(got.V, sample) {
t.Errorf("round trip = %+v, %v\n%s", got.V, err, b)
}
if _, err := xml.Marshal(wrapped[T]{V: null}); err != nil {
t.Errorf("null marshal: %v", err)
}
var bad wrapped[T]
if err := xml.Unmarshal([]byte("<w><v><item>1</item>"), &bad); err == nil {
t.Error("truncated xml must fail")
}
})
}
func TestArrayTypes_FullContract(t *testing.T) {
u1, u2 := uuid.New(), uuid.New()
t.Run("string", func(t *testing.T) {
arrayRoundTrip(t, NewSqlStringArray([]string{"a", "b c", `q"uote`, "x,y"}), SqlStringArray{}, "")
})
t.Run("int16", func(t *testing.T) {
arrayRoundTrip(t, NewSqlInt16Array([]int16{1, -2, 300}), SqlInt16Array{}, "{99999}")
})
t.Run("int32", func(t *testing.T) {
arrayRoundTrip(t, NewSqlInt32Array([]int32{1, -2, 300000}), SqlInt32Array{}, "{x}")
})
t.Run("int64", func(t *testing.T) {
arrayRoundTrip(t, NewSqlInt64Array([]int64{1, -2, 1 << 40}), SqlInt64Array{}, "{x}")
})
t.Run("float32", func(t *testing.T) {
arrayRoundTrip(t, NewSqlFloat32Array([]float32{1.5, -2.25}), SqlFloat32Array{}, "{x}")
})
t.Run("float64", func(t *testing.T) {
arrayRoundTrip(t, NewSqlFloat64Array([]float64{1.5, -2.25, 1e10}), SqlFloat64Array{}, "{x}")
})
t.Run("bool", func(t *testing.T) {
arrayRoundTrip(t, NewSqlBoolArray([]bool{true, false, true}), SqlBoolArray{}, "not an array")
})
t.Run("uuid", func(t *testing.T) {
arrayRoundTrip(t, NewSqlUUIDArray([]uuid.UUID{u1, u2}), SqlUUIDArray{}, "{not-a-uuid}")
})
t.Run("vector", func(t *testing.T) {
arrayRoundTrip(t, NewSqlVector([]float32{1, 2.5, -3}), SqlVector{}, "1,2,3")
})
}
func TestArrayTypes_EmptyAndMalformedScan(t *testing.T) {
var s SqlStringArray
if err := s.Scan("{}"); err != nil || !s.Valid || len(s.Val) != 0 {
t.Errorf("empty array: %+v %v", s, err)
}
var i SqlInt32Array
if err := i.Scan("{}"); err != nil || !i.Valid || len(i.Val) != 0 {
t.Errorf("empty int array: %+v %v", i, err)
}
var v SqlVector
if err := v.Scan("[]"); err != nil || !v.Valid || len(v.Val) != 0 {
t.Errorf("empty vector: %+v %v", v, err)
}
if err := v.Scan("[1,x]"); err == nil {
t.Error("bad vector element must fail")
}
if err := v.Scan(42); err == nil {
t.Error("vector Scan(int) must fail")
}
for _, bad := range []string{"not an array", "{unterminated"} {
var a SqlInt32Array
if err := a.Scan(bad); err == nil {
t.Errorf("Scan(%q) must fail", bad)
}
}
}
func TestArrayJSONIsPlainSlice(t *testing.T) {
b, err := json.Marshal(NewSqlInt32Array([]int32{1, 2}))
if err != nil || string(b) != "[1,2]" {
t.Errorf("got %s, %v", b, err)
}
}