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:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user