// Copyright 2014 Google Inc. All rights reserved. // // 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 proptools import ( "bytes" "reflect" "testing" "github.com/google/blueprint/parser" ) var validUnpackTestCases = []struct { name string input string output []interface{} empty []interface{} errs []error }{ { name: "blank and unset", input: ` m { s: "abc", blank: "", } `, output: []interface{}{ &struct { S *string Blank *string Unset *string }{ S: StringPtr("abc"), Blank: StringPtr(""), Unset: nil, }, }, }, { name: "string", input: ` m { s: "abc", } `, output: []interface{}{ &struct { S string }{ S: "abc", }, }, }, { name: "bool", input: ` m { isGood: true, } `, output: []interface{}{ &struct { IsGood bool }{ IsGood: true, }, }, }, { name: "boolptr", input: ` m { isGood: true, isBad: false, } `, output: []interface{}{ &struct { IsGood *bool IsBad *bool IsUgly *bool }{ IsGood: BoolPtr(true), IsBad: BoolPtr(false), IsUgly: nil, }, }, }, { name: "slice", input: ` m { stuff: ["asdf", "jkl;", "qwert", "uiop", "bnm,"], empty: [] } `, output: []interface{}{ &struct { Stuff []string Empty []string Nil []string NonString []struct{ S string } `blueprint:"mutated"` }{ Stuff: []string{"asdf", "jkl;", "qwert", "uiop", "bnm,"}, Empty: []string{}, Nil: nil, NonString: nil, }, }, }, { name: "map", input: ` m { stuff: { "asdf": "jkl;", "qwert": "uiop"}, empty: {}, nested: { other_stuff: {}, }, } `, output: []interface{}{ &struct { Stuff map[string]string Empty map[string]string Nil map[string]string NonString map[string]struct{ S string } `blueprint:"mutated"` Nested struct { Other_stuff map[string]string } }{ Stuff: map[string]string{"asdf": "jkl;", "qwert": "uiop"}, Empty: map[string]string{}, Nil: nil, NonString: nil, Nested: struct{ Other_stuff map[string]string }{ Other_stuff: map[string]string{}, }, }, }, }, { name: "map with slice", input: ` m { stuff: { "asdf": ["jkl;"], "qwert": []}, empty: {}, } `, output: []interface{}{ &struct { Stuff map[string][]string Empty map[string][]string Nil map[string][]string NonString map[string]struct{ S string } `blueprint:"mutated"` }{ Stuff: map[string][]string{"asdf": []string{"jkl;"}, "qwert": []string{}}, Empty: map[string][]string{}, Nil: nil, NonString: nil, }, }, }, { name: "map with struct", input: ` m { stuff: { "asdf": {s:"a"}}, empty: {}, } `, output: []interface{}{ &struct { Stuff map[string]struct{ S string } Empty map[string]struct{ S string } Nil map[string]struct{ S string } }{ Stuff: map[string]struct{ S string }{"asdf": struct{ S string }{"a"}}, Empty: map[string]struct{ S string }{}, Nil: nil, }, }, }, { name: "double nested", input: ` m { nested: { nested: { s: "abc", }, }, } `, output: []interface{}{ &struct { Nested struct { Nested struct { S string } } }{ Nested: struct{ Nested struct{ S string } }{ Nested: struct{ S string }{ S: "abc", }, }, }, }, }, { name: "nested", input: ` m { nested: { s: "abc", } } `, output: []interface{}{ &struct { Nested struct { S string } }{ Nested: struct{ S string }{ S: "abc", }, }, }, }, { name: "nested interface", input: ` m { nested: { s: "def", } } `, output: []interface{}{ &struct { Nested interface{} }{ Nested: &struct{ S string }{ S: "def", }, }, }, }, { name: "mixed", input: ` m { nested: { foo: "abc", }, bar: false, baz: ["def", "ghi"], } `, output: []interface{}{ &struct { Nested struct { Foo string } Bar bool Baz []string }{ Nested: struct{ Foo string }{ Foo: "abc", }, Bar: false, Baz: []string{"def", "ghi"}, }, }, }, { name: "filter", input: ` m { nested: { foo: "abc", }, bar: false, baz: ["def", "ghi"], } `, output: []interface{}{ &struct { Nested struct { Foo string `allowNested:"true"` } `blueprint:"filter(allowNested:\"true\")"` Bar bool Baz []string }{ Nested: struct { Foo string `allowNested:"true"` }{ Foo: "abc", }, Bar: false, Baz: []string{"def", "ghi"}, }, }, }, // List of maps { name: "list of structs", input: ` m { mapslist: [ { foo: "abc", bar: true, }, { foo: "def", bar: false, } ], } `, output: []interface{}{ &struct { Mapslist []struct { Foo string Bar bool } }{ Mapslist: []struct { Foo string Bar bool }{ {Foo: "abc", Bar: true}, {Foo: "def", Bar: false}, }, }, }, }, // List of pointers to structs { name: "list of pointers to structs", input: ` m { mapslist: [ { foo: "abc", bar: true, }, { foo: "def", bar: false, } ], } `, output: []interface{}{ &struct { Mapslist []*struct { Foo string Bar bool } }{ Mapslist: []*struct { Foo string Bar bool }{ {Foo: "abc", Bar: true}, {Foo: "def", Bar: false}, }, }, }, }, // List of lists { name: "list of lists", input: ` m { listoflists: [ ["abc",], ["def",], ], } `, output: []interface{}{ &struct { Listoflists [][]string }{ Listoflists: [][]string{ []string{"abc"}, []string{"def"}, }, }, }, }, // Multilevel { name: "multilevel", input: ` m { name: "mymodule", flag: true, settings: ["foo1", "foo2", "foo3",], perarch: { arm: "32", arm64: "64", }, configvars: [ { var: "var1", values: ["1.1", "1.2", ], }, { var: "var2", values: ["2.1", ], }, ], } `, output: []interface{}{ &struct { Name string Flag bool Settings []string Perarch *struct { Arm string Arm64 string } Configvars []struct { Var string Values []string } }{ Name: "mymodule", Flag: true, Settings: []string{"foo1", "foo2", "foo3"}, Perarch: &struct { Arm string Arm64 string }{Arm: "32", Arm64: "64"}, Configvars: []struct { Var string Values []string }{ {Var: "var1", Values: []string{"1.1", "1.2"}}, {Var: "var2", Values: []string{"2.1"}}, }, }, }, }, // Anonymous struct { name: "embedded struct", input: ` m { s: "abc", nested: { s: "def", }, } `, output: []interface{}{ &struct { EmbeddedStruct Nested struct { EmbeddedStruct } }{ EmbeddedStruct: EmbeddedStruct{ S: "abc", }, Nested: struct { EmbeddedStruct }{ EmbeddedStruct: EmbeddedStruct{ S: "def", }, }, }, }, }, // Anonymous interface { name: "embedded interface", input: ` m { s: "abc", nested: { s: "def", }, } `, output: []interface{}{ &struct { EmbeddedInterface Nested struct { EmbeddedInterface } }{ EmbeddedInterface: &struct{ S string }{ S: "abc", }, Nested: struct { EmbeddedInterface }{ EmbeddedInterface: &struct{ S string }{ S: "def", }, }, }, }, }, // Anonymous struct with name collision { name: "embedded name collision", input: ` m { s: "abc", nested: { s: "def", }, } `, output: []interface{}{ &struct { S string EmbeddedStruct Nested struct { S string EmbeddedStruct } }{ S: "abc", EmbeddedStruct: EmbeddedStruct{ S: "abc", }, Nested: struct { S string EmbeddedStruct }{ S: "def", EmbeddedStruct: EmbeddedStruct{ S: "def", }, }, }, }, }, // Anonymous interface with name collision { name: "embeded interface name collision", input: ` m { s: "abc", nested: { s: "def", }, } `, output: []interface{}{ &struct { S string EmbeddedInterface Nested struct { S string EmbeddedInterface } }{ S: "abc", EmbeddedInterface: &struct{ S string }{ S: "abc", }, Nested: struct { S string EmbeddedInterface }{ S: "def", EmbeddedInterface: &struct{ S string }{ S: "def", }, }, }, }, }, // Variables { name: "variables", input: ` list = ["abc"] string = "def" list_with_variable = [string] struct_value = { name: "foo" } m { s: string, list: list, list2: list_with_variable, structattr: struct_value, } `, output: []interface{}{ &struct { S string List []string List2 []string Structattr struct { Name string } }{ S: "def", List: []string{"abc"}, List2: []string{"def"}, Structattr: struct { Name string }{ Name: "foo", }, }, }, }, // Multiple property structs { name: "multiple", input: ` m { nested: { s: "abc", } } `, output: []interface{}{ &struct { Nested struct { S string } }{ Nested: struct{ S string }{ S: "abc", }, }, &struct { Nested struct { S string } }{ Nested: struct{ S string }{ S: "abc", }, }, &struct { }{}, }, }, // Nil pointer to struct { name: "nil struct pointer", input: ` m { nested: { s: "abc", } } `, output: []interface{}{ &struct { Nested *struct { S string } }{ Nested: &struct{ S string }{ S: "abc", }, }, }, empty: []interface{}{ &struct { Nested *struct { S string } }{}, }, }, // Interface containing nil pointer to struct { name: "interface nil struct pointer", input: ` m { nested: { s: "abc", } } `, output: []interface{}{ &struct { Nested interface{} }{ Nested: &EmbeddedStruct{ S: "abc", }, }, }, empty: []interface{}{ &struct { Nested interface{} }{ Nested: (*EmbeddedStruct)(nil), }, }, }, // Factory set properties { name: "factory properties", input: ` m { string: "abc", string_ptr: "abc", bool: false, bool_ptr: false, list: ["a", "b", "c"], } `, output: []interface{}{ &struct { String string String_ptr *string Bool bool Bool_ptr *bool List []string }{ String: "012abc", String_ptr: StringPtr("abc"), Bool: true, Bool_ptr: BoolPtr(false), List: []string{"0", "1", "2", "a", "b", "c"}, }, }, empty: []interface{}{ &struct { String string String_ptr *string Bool bool Bool_ptr *bool List []string }{ String: "012", String_ptr: StringPtr("012"), Bool: true, Bool_ptr: BoolPtr(true), List: []string{"0", "1", "2"}, }, }, }, // Captitalized property { input: ` m { CAPITALIZED: "foo", } `, output: []interface{}{ &struct { CAPITALIZED string }{ CAPITALIZED: "foo", }, }, }, } func TestUnpackProperties(t *testing.T) { for _, testCase := range validUnpackTestCases { t.Run(testCase.name, func(t *testing.T) { r := bytes.NewBufferString(testCase.input) file, errs := parser.ParseAndEval("", r, parser.NewScope(nil)) if len(errs) != 0 { t.Errorf("test case: %s", testCase.input) t.Errorf("unexpected parse errors:") for _, err := range errs { t.Errorf(" %s", err) } t.FailNow() } for _, def := range file.Defs { module, ok := def.(*parser.Module) if !ok { continue } var output []interface{} if len(testCase.empty) > 0 { for _, p := range testCase.empty { output = append(output, CloneProperties(reflect.ValueOf(p)).Interface()) } } else { for _, p := range testCase.output { output = append(output, CloneEmptyProperties(reflect.ValueOf(p)).Interface()) } } _, errs = unpackProperties(module.Properties, []string{"stuff", "empty", "nil", "nested.other_stuff"}, output...) if len(errs) != 0 && len(testCase.errs) == 0 { t.Errorf("test case: %s", testCase.input) t.Errorf("unexpected unpack errors:") for _, err := range errs { t.Errorf(" %s", err) } t.FailNow() } else if !reflect.DeepEqual(errs, testCase.errs) { t.Errorf("test case: %s", testCase.input) t.Errorf("incorrect errors:") t.Errorf(" expected: %+v", testCase.errs) t.Errorf(" got: %+v", errs) } if len(output) != len(testCase.output) { t.Fatalf("incorrect number of property structs, expected %d got %d", len(testCase.output), len(output)) } for i := range output { got := reflect.ValueOf(output[i]).Interface() if !reflect.DeepEqual(got, testCase.output[i]) { t.Errorf("test case: %s", testCase.input) t.Errorf("incorrect output:") t.Errorf(" expected: %+v", testCase.output[i]) t.Errorf(" got: %+v", got) } } } }) } } func TestUnpackErrors(t *testing.T) { testCases := []struct { name string input string output []interface{} errors []string }{ { name: "missing", input: ` m { missing: true, } `, output: []interface{}{}, errors: []string{`:3:13: unrecognized property "missing"`}, }, { name: "missing nested", input: ` m { nested: { missing: true, }, } `, output: []interface{}{ &struct { Nested struct{} }{}, }, errors: []string{`:4:14: unrecognized property "nested.missing"`}, }, { name: "mutated", input: ` m { mutated: true, } `, output: []interface{}{ &struct { Mutated bool `blueprint:"mutated"` }{}, }, errors: []string{`:3:13: mutated field mutated cannot be set in a Blueprint file`}, }, { name: "nested mutated", input: ` m { nested: { mutated: true, }, } `, output: []interface{}{ &struct { Nested struct { Mutated bool `blueprint:"mutated"` } }{}, }, errors: []string{`:4:14: mutated field nested.mutated cannot be set in a Blueprint file`}, }, { name: "duplicate", input: ` m { exists: true, exists: true, } `, output: []interface{}{ &struct { Exists bool }{}, }, errors: []string{ `:4:12: property "exists" already defined`, `:3:12: <-- previous definition here`, }, }, { name: "nested duplicate", input: ` m { nested: { exists: true, exists: true, }, } `, output: []interface{}{ &struct { Nested struct { Exists bool } }{}, }, errors: []string{ `:5:13: property "nested.exists" already defined`, `:4:13: <-- previous definition here`, }, }, { name: "wrong type", input: ` m { int: "foo", } `, output: []interface{}{ &struct { Int *int64 }{}, }, errors: []string{ `:3:11: can't assign string value to int64 property "int"`, }, }, { name: "wrong type for map", input: ` m { map: "foo", } `, output: []interface{}{ &struct { Map struct { S string } }{}, }, errors: []string{ `:3:11: can't assign string value to map property "map"`, }, }, { name: "wrong type for list", input: ` m { list: "foo", } `, output: []interface{}{ &struct { List []string }{}, }, errors: []string{ `:3:12: can't assign string value to list property "list"`, }, }, { name: "wrong type for list of maps", input: ` m { map_list: "foo", } `, output: []interface{}{ &struct { Map_list []struct { S string } }{}, }, errors: []string{ `:3:16: can't assign string value to list property "map_list"`, }, }, { name: "invalid use of maps", input: ` m { map: {"foo": "bar"}, } `, output: []interface{}{ &struct { Map map[string]string }{}, }, errors: []string{ `: Uses of maps for properties must be allowlisted. "map" is an unsupported use case`, }, }, { name: "invalid use of maps, not used in bp file", input: ` m { } `, output: []interface{}{ &struct { Map map[string]string }{}, }, errors: []string{ `: Uses of maps for properties must be allowlisted. "map" is an unsupported use case`, }, }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { r := bytes.NewBufferString(testCase.input) file, errs := parser.ParseAndEval("", r, parser.NewScope(nil)) if len(errs) != 0 { t.Errorf("test case: %s", testCase.input) t.Errorf("unexpected parse errors:") for _, err := range errs { t.Errorf(" %s", err) } t.FailNow() } for _, def := range file.Defs { module, ok := def.(*parser.Module) if !ok { continue } var output []interface{} for _, p := range testCase.output { output = append(output, CloneEmptyProperties(reflect.ValueOf(p)).Interface()) } _, errs = UnpackProperties(module.Properties, output...) printErrors := false for _, expectedErr := range testCase.errors { foundError := false for _, err := range errs { if err.Error() == expectedErr { foundError = true } } if !foundError { t.Errorf("expected error %s", expectedErr) printErrors = true } } if printErrors { t.Errorf("got errors:") for _, err := range errs { t.Errorf(" %s", err.Error()) } } } }) } } func BenchmarkUnpackProperties(b *testing.B) { run := func(b *testing.B, props []interface{}, input string) { b.ReportAllocs() b.StopTimer() r := bytes.NewBufferString(input) file, errs := parser.ParseAndEval("", r, parser.NewScope(nil)) if len(errs) != 0 { b.Errorf("test case: %s", input) b.Errorf("unexpected parse errors:") for _, err := range errs { b.Errorf(" %s", err) } b.FailNow() } for i := 0; i < b.N; i++ { for _, def := range file.Defs { module, ok := def.(*parser.Module) if !ok { continue } var output []interface{} for _, p := range props { output = append(output, CloneProperties(reflect.ValueOf(p)).Interface()) } b.StartTimer() _, errs = UnpackProperties(module.Properties, output...) b.StopTimer() if len(errs) > 0 { b.Errorf("unexpected unpack errors:") for _, err := range errs { b.Errorf(" %s", err) } } } } } b.Run("basic", func(b *testing.B) { props := []interface{}{ &struct { Nested struct { S string } }{}, } bp := ` m { nested: { s: "abc", }, } ` run(b, props, bp) }) b.Run("interface", func(b *testing.B) { props := []interface{}{ &struct { Nested interface{} }{ Nested: (*struct { S string })(nil), }, } bp := ` m { nested: { s: "abc", }, } ` run(b, props, bp) }) b.Run("many", func(b *testing.B) { props := []interface{}{ &struct { A *string B *string C *string D *string E *string F *string G *string H *string I *string J *string }{}, } bp := ` m { a: "a", b: "b", c: "c", d: "d", e: "e", f: "f", g: "g", h: "h", i: "i", j: "j", } ` run(b, props, bp) }) b.Run("deep", func(b *testing.B) { props := []interface{}{ &struct { Nested struct { Nested struct { Nested struct { Nested struct { Nested struct { Nested struct { Nested struct { Nested struct { Nested struct { Nested struct { S string } } } } } } } } } } }{}, } bp := ` m { nested: { nested: { nested: { nested: { nested: { nested: { nested: { nested: { nested: { nested: { s: "abc", }, }, }, }, }, }, }, }, }, }, } ` run(b, props, bp) }) b.Run("mix", func(b *testing.B) { props := []interface{}{ &struct { Name string Flag bool Settings []string Perarch *struct { Arm string Arm64 string } Configvars []struct { Name string Values []string } }{}, } bp := ` m { name: "mymodule", flag: true, settings: ["foo1", "foo2", "foo3",], perarch: { arm: "32", arm64: "64", }, configvars: [ { name: "var1", values: ["var1:1", "var1:2", ], }, { name: "var2", values: ["var2:1", "var2:2", ], }, ], } ` run(b, props, bp) }) }