src.elv.sh@v0.21.0-dev.0.20240515223629-06979efb9a2a/pkg/eval/vals/conversion_test.go (about) 1 package vals 2 3 import ( 4 "math/big" 5 "reflect" 6 "testing" 7 8 "src.elv.sh/pkg/eval/errs" 9 "src.elv.sh/pkg/testutil" 10 "src.elv.sh/pkg/tt" 11 ) 12 13 type someType struct { 14 Foo string 15 } 16 17 func TestScanToGo_ConcreteTypeDst(t *testing.T) { 18 // A wrapper around ScanToGo, to make it easier to test. Instead of 19 // supplying a pointer to the destination, an initial value to the 20 // destination is supplied and the result is returned. 21 scanToGo := func(src any, dstInit any) (any, error) { 22 ptr := reflect.New(TypeOf(dstInit)) 23 err := ScanToGo(src, ptr.Interface()) 24 return ptr.Elem().Interface(), err 25 } 26 27 tt.Test(t, tt.Fn(scanToGo).Named("scanToGo"), 28 // int 29 Args("12", 0).Rets(12), 30 Args("0x12", 0).Rets(0x12), 31 Args(12.0, 0).Rets(0, errMustBeInteger), 32 Args(0.5, 0).Rets(0, errMustBeInteger), 33 Args(someType{}, 0).Rets(tt.Any, errMustBeInteger), 34 Args("x", 0).Rets(tt.Any, cannotParseAs{"integer", "x"}), 35 36 // float64 37 Args(23, 0.0).Rets(23.0), 38 Args(big.NewRat(1, 2), 0.0).Rets(0.5), 39 Args(1.2, 0.0).Rets(1.2), 40 Args("23", 0.0).Rets(23.0), 41 Args("0x23", 0.0).Rets(float64(0x23)), 42 Args(someType{}, 0.0).Rets(tt.Any, errMustBeNumber), 43 Args("x", 0.0).Rets(tt.Any, cannotParseAs{"number", "x"}), 44 45 // rune 46 Args("x", ' ').Rets('x'), 47 Args(someType{}, ' ').Rets(tt.Any, errMustBeString), 48 Args("\xc3\x28", ' ').Rets(tt.Any, errMustBeValidUTF8), // Invalid UTF8 49 Args("ab", ' ').Rets(tt.Any, errMustHaveSingleRune), 50 51 // Other types don't undergo any conversion, as long as the types match 52 Args("foo", "").Rets("foo"), 53 Args(someType{"foo"}, someType{}).Rets(someType{"foo"}), 54 Args(nil, nil).Rets(nil), 55 Args("x", someType{}).Rets(tt.Any, WrongType{"!!vals.someType", "string"}), 56 ) 57 } 58 59 func TestScanToGo_NumDst(t *testing.T) { 60 scanToGo := func(src any) (Num, error) { 61 var n Num 62 err := ScanToGo(src, &n) 63 return n, err 64 } 65 66 tt.Test(t, tt.Fn(scanToGo).Named("scanToGo"), 67 // Strings are automatically converted 68 Args("12").Rets(12), 69 Args(z).Rets(bigInt(z)), 70 Args("1/2").Rets(big.NewRat(1, 2)), 71 Args("12.0").Rets(12.0), 72 // Already numbers 73 Args(12).Rets(12), 74 Args(bigInt(z)).Rets(bigInt(z)), 75 Args(big.NewRat(1, 2)).Rets(big.NewRat(1, 2)), 76 Args(12.0).Rets(12.0), 77 78 Args("bad").Rets(tt.Any, cannotParseAs{"number", "bad"}), 79 Args(EmptyList).Rets(tt.Any, errMustBeNumber), 80 ) 81 } 82 83 func TestScanToGo_InterfaceDst(t *testing.T) { 84 scanToGo := func(src any) (any, error) { 85 var l List 86 err := ScanToGo(src, &l) 87 return l, err 88 } 89 90 tt.Test(t, tt.Fn(scanToGo).Named("scanToGo"), 91 Args(EmptyList).Rets(EmptyList), 92 93 Args("foo").Rets(tt.Any, WrongType{"!!vector.Vector", "string"}), 94 ) 95 } 96 97 func TestScanToGo_CallableDstAdmitsNil(t *testing.T) { 98 type mockCallable interface { 99 Call() 100 } 101 testutil.Set(t, &CallableType, reflect.TypeOf((*mockCallable)(nil)).Elem()) 102 scanToGo := func(src any) (any, error) { 103 var c mockCallable 104 err := ScanToGo(src, &c) 105 return c, err 106 } 107 108 tt.Test(t, tt.Fn(scanToGo).Named("scanToGo"), 109 Args(nil).Rets(mockCallable(nil)), 110 ) 111 } 112 113 func TestScanToGo_ErrorsWithNonPointerDst(t *testing.T) { 114 err := ScanToGo("", 1) 115 if err == nil { 116 t.Errorf("did not return error") 117 } 118 } 119 120 func TestScanListToGo(t *testing.T) { 121 // A wrapper around ScanListToGo, to make it easier to test. 122 scanListToGo := func(src List, dstInit any) (any, error) { 123 ptr := reflect.New(TypeOf(dstInit)) 124 ptr.Elem().Set(reflect.ValueOf(dstInit)) 125 err := ScanListToGo(src, ptr.Interface()) 126 return ptr.Elem().Interface(), err 127 } 128 129 tt.Test(t, tt.Fn(scanListToGo).Named("scanListToGo"), 130 Args(MakeList("1", "2"), []int{}).Rets([]int{1, 2}), 131 Args(MakeList("1", "2"), []string{}).Rets([]string{"1", "2"}), 132 133 Args(MakeList("1", "a"), []int{}).Rets([]int{}, cannotParseAs{"integer", "a"}), 134 ) 135 } 136 137 func TestScanListElementsToGo(t *testing.T) { 138 // A wrapper around ScanListElementsToGo, to make it easier to test. 139 scanListElementsToGo := func(src List, inits ...any) ([]any, error) { 140 ptrs := make([]any, len(inits)) 141 for i, init := range inits { 142 if o, ok := init.(optional); ok { 143 // Wrapping the init value with Optional translates to wrapping 144 // the pointer with Optional. 145 ptrs[i] = Optional(reflect.New(TypeOf(o.ptr)).Interface()) 146 } else { 147 ptrs[i] = reflect.New(TypeOf(init)).Interface() 148 } 149 } 150 err := ScanListElementsToGo(src, ptrs...) 151 vals := make([]any, len(ptrs)) 152 for i, ptr := range ptrs { 153 if o, ok := ptr.(optional); ok { 154 vals[i] = reflect.ValueOf(o.ptr).Elem().Interface() 155 } else { 156 vals[i] = reflect.ValueOf(ptr).Elem().Interface() 157 } 158 } 159 return vals, err 160 } 161 162 tt.Test(t, tt.Fn(scanListElementsToGo).Named("scanListElementsToGo"), 163 Args(MakeList("1", "2"), 0, 0).Rets([]any{1, 2}), 164 Args(MakeList("1", "2"), "", "").Rets([]any{"1", "2"}), 165 Args(MakeList("1", "2"), 0, Optional(0)).Rets([]any{1, 2}), 166 Args(MakeList("1"), 0, Optional(0)).Rets([]any{1, 0}), 167 168 Args(MakeList("a"), 0).Rets([]any{0}, 169 cannotParseAs{"integer", "a"}), 170 Args(MakeList("1"), 0, 0).Rets([]any{0, 0}, 171 errs.ArityMismatch{What: "list elements", 172 ValidLow: 2, ValidHigh: 2, Actual: 1}), 173 Args(MakeList("1"), 0, 0, Optional(0)).Rets([]any{0, 0, 0}, 174 errs.ArityMismatch{What: "list elements", 175 ValidLow: 2, ValidHigh: 3, Actual: 1}), 176 ) 177 } 178 179 type aStruct struct { 180 Foo int 181 bar any 182 } 183 184 // Equal is required by cmp.Diff, since aStruct contains unexported fields. 185 func (a aStruct) Equal(b aStruct) bool { return a == b } 186 187 func TestScanMapToGo(t *testing.T) { 188 // A wrapper around ScanMapToGo, to make it easier to test. 189 scanMapToGo := func(src Map, dstInit any) (any, error) { 190 ptr := reflect.New(TypeOf(dstInit)) 191 ptr.Elem().Set(reflect.ValueOf(dstInit)) 192 err := ScanMapToGo(src, ptr.Interface()) 193 return ptr.Elem().Interface(), err 194 } 195 196 tt.Test(t, tt.Fn(scanMapToGo).Named("scanMapToGo"), 197 Args(MakeMap("foo", "1"), aStruct{}).Rets(aStruct{Foo: 1}), 198 // More fields is OK 199 Args(MakeMap("foo", "1", "bar", "x"), aStruct{}).Rets(aStruct{Foo: 1}), 200 // Fewer fields is OK 201 Args(MakeMap(), aStruct{}).Rets(aStruct{}), 202 // Unexported fields are ignored 203 Args(MakeMap("bar", 20), aStruct{bar: 10}).Rets(aStruct{bar: 10}), 204 205 // Conversion error 206 Args(MakeMap("foo", "a"), aStruct{}). 207 Rets(aStruct{}, cannotParseAs{"integer", "a"}), 208 ) 209 } 210 211 func TestFromGo(t *testing.T) { 212 tt.Test(t, FromGo, 213 // BigInt -> int, when in range 214 Args(bigInt(z)).Rets(bigInt(z)), 215 Args(big.NewInt(100)).Rets(100), 216 // BigRat -> BigInt or int, when denominator is 1 217 Args(bigRat(z1+"/"+z)).Rets(bigRat(z1+"/"+z)), 218 Args(bigRat(z+"/1")).Rets(bigInt(z)), 219 Args(bigRat("2/1")).Rets(2), 220 // rune -> string 221 Args('x').Rets("x"), 222 223 // Other types don't undergo any conversion 224 Args(nil).Rets(nil), 225 Args(someType{"foo"}).Rets(someType{"foo"}), 226 ) 227 }