elvish/pkg/eval/vals/conversion_test.go
Qi Xiao 6e36058d0a pkg/tt: Improve API.
- Use reflection to derive function name.

- Take test cases as variadic arguments, instead of requiring them to be wrapped
  in a Table.

- Support naming test cases.

- Run test cases as subtests with t.Run.
2024-01-11 15:42:43 +00:00

228 lines
6.6 KiB
Go

package vals
import (
"math/big"
"reflect"
"testing"
"src.elv.sh/pkg/eval/errs"
"src.elv.sh/pkg/testutil"
"src.elv.sh/pkg/tt"
)
type someType struct {
Foo string
}
func TestScanToGo_ConcreteTypeDst(t *testing.T) {
// A wrapper around ScanToGo, to make it easier to test. Instead of
// supplying a pointer to the destination, an initial value to the
// destination is supplied and the result is returned.
scanToGo := func(src any, dstInit any) (any, error) {
ptr := reflect.New(TypeOf(dstInit))
err := ScanToGo(src, ptr.Interface())
return ptr.Elem().Interface(), err
}
tt.Test(t, tt.Fn(scanToGo).Named("scanToGo"),
// int
Args("12", 0).Rets(12),
Args("0x12", 0).Rets(0x12),
Args(12.0, 0).Rets(0, errMustBeInteger),
Args(0.5, 0).Rets(0, errMustBeInteger),
Args(someType{}, 0).Rets(tt.Any, errMustBeInteger),
Args("x", 0).Rets(tt.Any, cannotParseAs{"integer", "x"}),
// float64
Args(23, 0.0).Rets(23.0),
Args(big.NewRat(1, 2), 0.0).Rets(0.5),
Args(1.2, 0.0).Rets(1.2),
Args("23", 0.0).Rets(23.0),
Args("0x23", 0.0).Rets(float64(0x23)),
Args(someType{}, 0.0).Rets(tt.Any, errMustBeNumber),
Args("x", 0.0).Rets(tt.Any, cannotParseAs{"number", "x"}),
// rune
Args("x", ' ').Rets('x'),
Args(someType{}, ' ').Rets(tt.Any, errMustBeString),
Args("\xc3\x28", ' ').Rets(tt.Any, errMustBeValidUTF8), // Invalid UTF8
Args("ab", ' ').Rets(tt.Any, errMustHaveSingleRune),
// Other types don't undergo any conversion, as long as the types match
Args("foo", "").Rets("foo"),
Args(someType{"foo"}, someType{}).Rets(someType{"foo"}),
Args(nil, nil).Rets(nil),
Args("x", someType{}).Rets(tt.Any, WrongType{"!!vals.someType", "string"}),
)
}
func TestScanToGo_NumDst(t *testing.T) {
scanToGo := func(src any) (Num, error) {
var n Num
err := ScanToGo(src, &n)
return n, err
}
tt.Test(t, tt.Fn(scanToGo).Named("scanToGo"),
// Strings are automatically converted
Args("12").Rets(12),
Args(z).Rets(bigInt(z)),
Args("1/2").Rets(big.NewRat(1, 2)),
Args("12.0").Rets(12.0),
// Already numbers
Args(12).Rets(12),
Args(bigInt(z)).Rets(bigInt(z)),
Args(big.NewRat(1, 2)).Rets(big.NewRat(1, 2)),
Args(12.0).Rets(12.0),
Args("bad").Rets(tt.Any, cannotParseAs{"number", "bad"}),
Args(EmptyList).Rets(tt.Any, errMustBeNumber),
)
}
func TestScanToGo_InterfaceDst(t *testing.T) {
scanToGo := func(src any) (any, error) {
var l List
err := ScanToGo(src, &l)
return l, err
}
tt.Test(t, tt.Fn(scanToGo).Named("scanToGo"),
Args(EmptyList).Rets(EmptyList),
Args("foo").Rets(tt.Any, WrongType{"!!vector.Vector", "string"}),
)
}
func TestScanToGo_CallableDstAdmitsNil(t *testing.T) {
type mockCallable interface {
Call()
}
testutil.Set(t, &CallableType, reflect.TypeOf((*mockCallable)(nil)).Elem())
scanToGo := func(src any) (any, error) {
var c mockCallable
err := ScanToGo(src, &c)
return c, err
}
tt.Test(t, tt.Fn(scanToGo).Named("scanToGo"),
Args(nil).Rets(mockCallable(nil)),
)
}
func TestScanToGo_ErrorsWithNonPointerDst(t *testing.T) {
err := ScanToGo("", 1)
if err == nil {
t.Errorf("did not return error")
}
}
func TestScanListToGo(t *testing.T) {
// A wrapper around ScanListToGo, to make it easier to test.
scanListToGo := func(src List, dstInit any) (any, error) {
ptr := reflect.New(TypeOf(dstInit))
ptr.Elem().Set(reflect.ValueOf(dstInit))
err := ScanListToGo(src, ptr.Interface())
return ptr.Elem().Interface(), err
}
tt.Test(t, tt.Fn(scanListToGo).Named("scanListToGo"),
Args(MakeList("1", "2"), []int{}).Rets([]int{1, 2}),
Args(MakeList("1", "2"), []string{}).Rets([]string{"1", "2"}),
Args(MakeList("1", "a"), []int{}).Rets([]int{}, cannotParseAs{"integer", "a"}),
)
}
func TestScanListElementsToGo(t *testing.T) {
// A wrapper around ScanListElementsToGo, to make it easier to test.
scanListElementsToGo := func(src List, inits ...any) ([]any, error) {
ptrs := make([]any, len(inits))
for i, init := range inits {
if o, ok := init.(optional); ok {
// Wrapping the init value with Optional translates to wrapping
// the pointer with Optional.
ptrs[i] = Optional(reflect.New(TypeOf(o.ptr)).Interface())
} else {
ptrs[i] = reflect.New(TypeOf(init)).Interface()
}
}
err := ScanListElementsToGo(src, ptrs...)
vals := make([]any, len(ptrs))
for i, ptr := range ptrs {
if o, ok := ptr.(optional); ok {
vals[i] = reflect.ValueOf(o.ptr).Elem().Interface()
} else {
vals[i] = reflect.ValueOf(ptr).Elem().Interface()
}
}
return vals, err
}
tt.Test(t, tt.Fn(scanListElementsToGo).Named("scanListElementsToGo"),
Args(MakeList("1", "2"), 0, 0).Rets([]any{1, 2}),
Args(MakeList("1", "2"), "", "").Rets([]any{"1", "2"}),
Args(MakeList("1", "2"), 0, Optional(0)).Rets([]any{1, 2}),
Args(MakeList("1"), 0, Optional(0)).Rets([]any{1, 0}),
Args(MakeList("a"), 0).Rets([]any{0},
cannotParseAs{"integer", "a"}),
Args(MakeList("1"), 0, 0).Rets([]any{0, 0},
errs.ArityMismatch{What: "list elements",
ValidLow: 2, ValidHigh: 2, Actual: 1}),
Args(MakeList("1"), 0, 0, Optional(0)).Rets([]any{0, 0, 0},
errs.ArityMismatch{What: "list elements",
ValidLow: 2, ValidHigh: 3, Actual: 1}),
)
}
type aStruct struct {
Foo int
bar any
}
// Equal is required by cmp.Diff, since aStruct contains unexported fields.
func (a aStruct) Equal(b aStruct) bool { return a == b }
func TestScanMapToGo(t *testing.T) {
// A wrapper around ScanMapToGo, to make it easier to test.
scanMapToGo := func(src Map, dstInit any) (any, error) {
ptr := reflect.New(TypeOf(dstInit))
ptr.Elem().Set(reflect.ValueOf(dstInit))
err := ScanMapToGo(src, ptr.Interface())
return ptr.Elem().Interface(), err
}
tt.Test(t, tt.Fn(scanMapToGo).Named("scanMapToGo"),
Args(MakeMap("foo", "1"), aStruct{}).Rets(aStruct{Foo: 1}),
// More fields is OK
Args(MakeMap("foo", "1", "bar", "x"), aStruct{}).Rets(aStruct{Foo: 1}),
// Fewer fields is OK
Args(MakeMap(), aStruct{}).Rets(aStruct{}),
// Unexported fields are ignored
Args(MakeMap("bar", 20), aStruct{bar: 10}).Rets(aStruct{bar: 10}),
// Conversion error
Args(MakeMap("foo", "a"), aStruct{}).
Rets(aStruct{}, cannotParseAs{"integer", "a"}),
)
}
func TestFromGo(t *testing.T) {
tt.Test(t, FromGo,
// BigInt -> int, when in range
Args(bigInt(z)).Rets(bigInt(z)),
Args(big.NewInt(100)).Rets(100),
// BigRat -> BigInt or int, when denominator is 1
Args(bigRat(z1+"/"+z)).Rets(bigRat(z1+"/"+z)),
Args(bigRat(z+"/1")).Rets(bigInt(z)),
Args(bigRat("2/1")).Rets(2),
// rune -> string
Args('x').Rets("x"),
// Other types don't undergo any conversion
Args(nil).Rets(nil),
Args(someType{"foo"}).Rets(someType{"foo"}),
)
}