6437d4e737
Select statements are a new blueprint feature inspired by bazel's select statements. They are essentially alternative syntax for soong config variables that require less boilerplate. In addition, they support making decisions based on a module's variant, which will eliminate the need for manual property struct manipulation, such as the arch mutator's arch: and target: properties. In order to support decisions based on the variant, select statements cannot be evaluated as soon as they're parsed. Instead, they must be stored in the property struct unevaluated. This means that individual properties need to change their type from say, string, to Configurable[string]. Currently, only configurable strings, bools, and string slices are supported, but more types can be added later. The module implementation must call my_property.Evaluate(ctx) in order to get the final, resolved value of the select statement. Bug: 323382414 Test: go tests Change-Id: I62f8721d7f0ac3d1df4a06d7eaa260a5aa7fcba3
1423 lines
24 KiB
Go
1423 lines
24 KiB
Go
// 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: "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",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "String configurable property that isn't configured",
|
|
input: `
|
|
m {
|
|
foo: "bar"
|
|
}
|
|
`,
|
|
output: []interface{}{
|
|
&struct {
|
|
Foo Configurable[string]
|
|
}{
|
|
Foo: Configurable[string]{
|
|
propertyName: "foo",
|
|
typ: parser.SelectTypeUnconfigured,
|
|
cases: map[string]string{
|
|
default_select_branch_name: "bar",
|
|
},
|
|
appendWrapper: &appendWrapper[string]{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "Bool configurable property that isn't configured",
|
|
input: `
|
|
m {
|
|
foo: true,
|
|
}
|
|
`,
|
|
output: []interface{}{
|
|
&struct {
|
|
Foo Configurable[bool]
|
|
}{
|
|
Foo: Configurable[bool]{
|
|
propertyName: "foo",
|
|
typ: parser.SelectTypeUnconfigured,
|
|
cases: map[string]bool{
|
|
default_select_branch_name: true,
|
|
},
|
|
appendWrapper: &appendWrapper[bool]{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "String list configurable property that isn't configured",
|
|
input: `
|
|
m {
|
|
foo: ["a", "b"],
|
|
}
|
|
`,
|
|
output: []interface{}{
|
|
&struct {
|
|
Foo Configurable[[]string]
|
|
}{
|
|
Foo: Configurable[[]string]{
|
|
propertyName: "foo",
|
|
typ: parser.SelectTypeUnconfigured,
|
|
cases: map[string][]string{
|
|
default_select_branch_name: {"a", "b"},
|
|
},
|
|
appendWrapper: &appendWrapper[[]string]{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "Configurable property",
|
|
input: `
|
|
m {
|
|
foo: select(soong_config_variable("my_namespace", "my_variable"), {
|
|
"a": "a2",
|
|
"b": "b2",
|
|
_: "c2",
|
|
})
|
|
}
|
|
`,
|
|
output: []interface{}{
|
|
&struct {
|
|
Foo Configurable[string]
|
|
}{
|
|
Foo: Configurable[string]{
|
|
propertyName: "foo",
|
|
typ: parser.SelectTypeSoongConfigVariable,
|
|
condition: "my_namespace:my_variable",
|
|
cases: map[string]string{
|
|
"a": "a2",
|
|
"b": "b2",
|
|
default_select_branch_name: "c2",
|
|
},
|
|
appendWrapper: &appendWrapper[string]{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "Configurable property appending",
|
|
input: `
|
|
m {
|
|
foo: select(soong_config_variable("my_namespace", "my_variable"), {
|
|
"a": "a2",
|
|
"b": "b2",
|
|
_: "c2",
|
|
}) + select(soong_config_variable("my_namespace", "my_2nd_variable"), {
|
|
"d": "d2",
|
|
"e": "e2",
|
|
_: "f2",
|
|
})
|
|
}
|
|
`,
|
|
output: []interface{}{
|
|
&struct {
|
|
Foo Configurable[string]
|
|
}{
|
|
Foo: Configurable[string]{
|
|
propertyName: "foo",
|
|
typ: parser.SelectTypeSoongConfigVariable,
|
|
condition: "my_namespace:my_variable",
|
|
cases: map[string]string{
|
|
"a": "a2",
|
|
"b": "b2",
|
|
default_select_branch_name: "c2",
|
|
},
|
|
appendWrapper: &appendWrapper[string]{
|
|
append: Configurable[string]{
|
|
propertyName: "foo",
|
|
typ: parser.SelectTypeSoongConfigVariable,
|
|
condition: "my_namespace:my_2nd_variable",
|
|
cases: map[string]string{
|
|
"d": "d2",
|
|
"e": "e2",
|
|
default_select_branch_name: "f2",
|
|
},
|
|
appendWrapper: &appendWrapper[string]{},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
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, 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{`<input>:3:13: unrecognized property "missing"`},
|
|
},
|
|
{
|
|
name: "missing nested",
|
|
input: `
|
|
m {
|
|
nested: {
|
|
missing: true,
|
|
},
|
|
}
|
|
`,
|
|
output: []interface{}{
|
|
&struct {
|
|
Nested struct{}
|
|
}{},
|
|
},
|
|
errors: []string{`<input>:4:14: unrecognized property "nested.missing"`},
|
|
},
|
|
{
|
|
name: "mutated",
|
|
input: `
|
|
m {
|
|
mutated: true,
|
|
}
|
|
`,
|
|
output: []interface{}{
|
|
&struct {
|
|
Mutated bool `blueprint:"mutated"`
|
|
}{},
|
|
},
|
|
errors: []string{`<input>: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{`<input>: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{
|
|
`<input>:4:12: property "exists" already defined`,
|
|
`<input>:3:12: <-- previous definition here`,
|
|
},
|
|
},
|
|
{
|
|
name: "nested duplicate",
|
|
input: `
|
|
m {
|
|
nested: {
|
|
exists: true,
|
|
exists: true,
|
|
},
|
|
}
|
|
`,
|
|
output: []interface{}{
|
|
&struct {
|
|
Nested struct {
|
|
Exists bool
|
|
}
|
|
}{},
|
|
},
|
|
errors: []string{
|
|
`<input>:5:13: property "nested.exists" already defined`,
|
|
`<input>:4:13: <-- previous definition here`,
|
|
},
|
|
},
|
|
{
|
|
name: "wrong type",
|
|
input: `
|
|
m {
|
|
int: "foo",
|
|
}
|
|
`,
|
|
output: []interface{}{
|
|
&struct {
|
|
Int *int64
|
|
}{},
|
|
},
|
|
errors: []string{
|
|
`<input>: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{
|
|
`<input>: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{
|
|
`<input>: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{
|
|
`<input>:3:16: can't assign string value to list property "map_list"`,
|
|
},
|
|
},
|
|
{
|
|
name: "non-existent property",
|
|
input: `
|
|
m {
|
|
foo: {
|
|
foo_prop1: true,
|
|
foo_prop2: false,
|
|
foo_prop3: true,
|
|
},
|
|
bar: {
|
|
bar_prop: false,
|
|
},
|
|
baz: true,
|
|
exist: false,
|
|
}
|
|
`,
|
|
output: []interface{}{
|
|
&struct {
|
|
Foo struct {
|
|
Foo_prop1 bool
|
|
}
|
|
Exist bool
|
|
}{},
|
|
},
|
|
errors: []string{
|
|
`<input>:5:16: unrecognized property "foo.foo_prop2"`,
|
|
`<input>:6:16: unrecognized property "foo.foo_prop3"`,
|
|
`<input>:9:15: unrecognized property "bar.bar_prop"`,
|
|
`<input>:11:9: unrecognized property "baz"`,
|
|
},
|
|
},
|
|
}
|
|
|
|
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)
|
|
})
|
|
}
|
|
|
|
func TestRemoveUnnecessaryUnusedNames(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
input []string
|
|
output []string
|
|
}{
|
|
{
|
|
name: "no unused names",
|
|
input: []string{},
|
|
output: []string{},
|
|
},
|
|
{
|
|
name: "only one unused name",
|
|
input: []string{"a.b.c"},
|
|
output: []string{"a.b.c"},
|
|
},
|
|
{
|
|
name: "unused names in a chain",
|
|
input: []string{"a", "a.b", "a.b.c"},
|
|
output: []string{"a.b.c"},
|
|
},
|
|
{
|
|
name: "unused names unrelated",
|
|
input: []string{"a.b.c", "s.t", "x.y"},
|
|
output: []string{"a.b.c", "s.t", "x.y"},
|
|
},
|
|
{
|
|
name: "unused names partially related one",
|
|
input: []string{"a.b", "a.b.c", "a.b.d"},
|
|
output: []string{"a.b.c", "a.b.d"},
|
|
},
|
|
{
|
|
name: "unused names partially related two",
|
|
input: []string{"a", "a.b.c", "a.c"},
|
|
output: []string{"a.b.c", "a.c"},
|
|
},
|
|
{
|
|
name: "unused names partially related three",
|
|
input: []string{"a.b.c", "b.c", "c"},
|
|
output: []string{"a.b.c", "b.c", "c"},
|
|
},
|
|
}
|
|
for _, testCase := range testCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
simplifiedNames := removeUnnecessaryUnusedNames(testCase.input)
|
|
if !reflect.DeepEqual(simplifiedNames, testCase.output) {
|
|
t.Errorf("test case: %s", testCase.name)
|
|
t.Errorf(" input: %s", testCase.input)
|
|
t.Errorf(" expect: %s", testCase.output)
|
|
t.Errorf(" got: %s", simplifiedNames)
|
|
}
|
|
})
|
|
}
|
|
}
|