// 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 cuego import ( "reflect" "testing" ) 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", 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), // }, { // // TODO: figure out whether this constraint should constrain it // // to a struct or not. // name: "*Sum: nil disallowed by constraint", // value: (*Sum)(nil), // constraints: "!=null", // err: fail, }, { // 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) { c := &Context{} if tc.constraints != "" { err := c.Constrain(tc.value, tc.constraints) if err != nil { t.Fatal(err) } } err := c.Validate(tc.value) checkErr(t, err, tc.err) }) } } func TestUpdate(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) { c := &Context{} if tc.constraints != "" { err := c.Constrain(tc.value, tc.constraints) if err != nil { t.Fatal(err) } } err := c.Complete(tc.value) checkErr(t, err, tc.err) if !reflect.DeepEqual(tc.value, tc.result) { t.Errorf("value:\n got: %#v;\nwant: %#v", tc.value, tc.result) } }) } }