blob: 6b6a57f7f2fcabb12f0bf4148a0913afac596215 [file] [log] [blame]
// Copyright 2019 CUE Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gocodec
import (
"fmt"
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/kr/pretty"
"cuelang.org/go/cue"
)
type Sum struct {
A int `cue:"C-B" json:",omitempty"`
B int `cue:"C-A" json:",omitempty"`
C int `cue:"A+B & >=5" json:",omitempty"`
}
func checkErr(t *testing.T, got error, want string) {
t.Helper()
if (got == nil) != (want == "") {
t.Errorf("error: got %v; want %v", got, want)
}
}
func TestValidate(t *testing.T) {
fail := "some error"
testCases := []struct {
name string
value interface{}
constraints string
err string
}{{
name: "*Sum: nil disallowed by constraint",
value: (*Sum)(nil),
constraints: "!=null",
err: fail,
}, {
name: "Sum",
value: Sum{A: 1, B: 4, C: 5},
}, {
name: "*Sum",
value: &Sum{A: 1, B: 4, C: 5},
}, {
name: "*Sum: incorrect sum",
value: &Sum{A: 1, B: 4, C: 6},
err: fail,
}, {
name: "*Sum: field C is too low",
value: &Sum{A: 1, B: 3, C: 4},
err: fail,
}, {
name: "*Sum: nil value",
value: (*Sum)(nil),
}, {
// Not a typical constraint, but it is possible.
name: "string list",
value: []string{"a", "b", "c"},
constraints: `[_, "b", ...]`,
}, {
// Not a typical constraint, but it is possible.
name: "string list incompatible lengths",
value: []string{"a", "b", "c"},
constraints: `4*[string]`,
err: fail,
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := &cue.Runtime{}
codec := New(r, nil)
v, err := codec.ExtractType(tc.value)
if err != nil {
t.Fatal(err)
}
if tc.constraints != "" {
inst, err := r.Compile(tc.name, tc.constraints)
if err != nil {
t.Fatal(err)
}
v = v.Unify(inst.Value())
}
err = codec.Validate(v, tc.value)
checkErr(t, err, tc.err)
})
}
}
func TestComplete(t *testing.T) {
type updated struct {
A []*int `cue:"[...int|*1]"` // arbitrary length slice with values 1
B []int `cue:"3*[int|*1]"` // slice of length 3, defaults to [1,1,1]
// TODO: better errors if the user forgets to quote.
M map[string]int `cue:",opt"`
}
type sump struct {
A *int `cue:"C-B"`
B *int `cue:"C-A"`
C *int `cue:"A+B"`
}
one := 1
two := 2
fail := "some error"
_ = fail
_ = one
testCases := []struct {
name string
value interface{}
result interface{}
constraints string
err string
}{{
name: "*Sum",
value: &Sum{A: 1, B: 4, C: 5},
result: &Sum{A: 1, B: 4, C: 5},
}, {
name: "*Sum",
value: &Sum{A: 1, B: 4},
result: &Sum{A: 1, B: 4, C: 5},
}, {
name: "*sump",
value: &sump{A: &one, B: &one},
result: &sump{A: &one, B: &one, C: &two},
}, {
name: "*Sum: backwards",
value: &Sum{B: 4, C: 8},
result: &Sum{A: 4, B: 4, C: 8},
}, {
name: "*Sum: sum too low",
value: &Sum{A: 1, B: 3},
result: &Sum{A: 1, B: 3}, // Value should not be updated
err: fail,
}, {
name: "*Sum: sum underspecified",
value: &Sum{A: 1},
result: &Sum{A: 1}, // Value should not be updated
err: fail,
}, {
name: "Sum: cannot modify",
value: Sum{A: 3, B: 4, C: 7},
result: Sum{A: 3, B: 4, C: 7},
err: fail,
}, {
name: "*Sum: cannot update nil value",
value: (*Sum)(nil),
result: (*Sum)(nil),
err: fail,
}, {
name: "cannot modify slice",
value: []string{"a", "b", "c"},
result: []string{"a", "b", "c"},
err: fail,
}, {
name: "composite values update",
// allocate a slice with uninitialized values and let Update fill
// out default values.
value: &updated{A: make([]*int, 3)},
result: &updated{
A: []*int{&one, &one, &one},
B: []int{1, 1, 1},
M: map[string]int(nil),
},
}, {
name: "composite values update with unsatisfied map constraints",
value: &updated{},
result: &updated{},
constraints: ` { M: {foo: bar, bar: foo} } `,
err: fail, // incomplete values
}, {
name: "composite values update with map constraints",
value: &updated{M: map[string]int{"foo": 1}},
constraints: ` { M: {foo: bar, bar: foo} } `,
result: &updated{
// TODO: would be better if this is nil, but will not matter for
// JSON output: if omitempty is false, an empty list will be
// printed regardless, and if it is true, it will be omitted
// regardless.
A: []*int{},
B: []int{1, 1, 1},
M: map[string]int{"bar": 1, "foo": 1},
},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := &cue.Runtime{}
codec := New(r, nil)
v, err := codec.ExtractType(tc.value)
if err != nil {
t.Fatal(err)
}
if tc.constraints != "" {
inst, err := r.Compile(tc.name, tc.constraints)
if err != nil {
t.Fatal(err)
}
v = v.Unify(inst.Value())
}
err = codec.Complete(v, tc.value)
checkErr(t, err, tc.err)
if !reflect.DeepEqual(tc.value, tc.result) {
t.Error(pretty.Diff(tc.value, tc.result))
}
})
}
}
func TestEncode(t *testing.T) {
testCases := []struct {
in string
dst interface{}
want interface{}
}{{
in: "4",
dst: new(int),
want: 4,
}}
r := &cue.Runtime{}
c := New(r, nil)
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
inst, err := r.Compile("test", tc.in)
if err != nil {
t.Fatal(err)
}
err = c.Encode(inst.Value(), tc.dst)
if err != nil {
t.Fatal(err)
}
got := reflect.ValueOf(tc.dst).Elem().Interface()
if !cmp.Equal(got, tc.want) {
t.Error(cmp.Diff(got, tc.want))
}
})
}
}
func TestDecode(t *testing.T) {
testCases := []struct {
in interface{}
want string
}{{
in: "str",
want: `"str"`,
}, {
in: func() interface{} {
type T struct {
B int
}
type S struct {
A string
T
}
return S{}
}(),
want: `{
A: ""
B: 0
}`,
}, {
in: func() interface{} {
type T struct {
B int
}
type S struct {
A string
T `json:"t"`
}
return S{}
}(),
want: `{
A: ""
t: {
B: 0
}
}`,
}}
c := New(&cue.Runtime{}, nil)
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
v, err := c.Decode(tc.in)
if err != nil {
t.Fatal(err)
}
got := fmt.Sprint(v)
if got != tc.want {
t.Errorf("got %v; want %v", got, tc.want)
}
})
}
}
// For debugging purposes, do not remove.
func TestX(t *testing.T) {
t.Skip()
fail := "some error"
// Not a typical constraint, but it is possible.
var (
name = "string list incompatible lengths"
value = []string{"a", "b", "c"}
constraints = `4*[string]`
wantErr = fail
)
r := &cue.Runtime{}
codec := New(r, nil)
v, err := codec.ExtractType(value)
if err != nil {
t.Fatal(err)
}
if constraints != "" {
inst, err := r.Compile(name, constraints)
if err != nil {
t.Fatal(err)
}
w := inst.Value()
v = v.Unify(w)
}
err = codec.Validate(v, value)
checkErr(t, err, wantErr)
}