Handle property structs and BpPropertySets as values to AddProperty.

Both will create a nested property set, that may be merged with an
existing one.

Test: m nothing
Bug: 151303681
Change-Id: I30696ba3eb8960ca6fa54c9ee2cf6229ab9f5da9
diff --git a/android/sdk.go b/android/sdk.go
index 9ea7ff4..f2cdc88 100644
--- a/android/sdk.go
+++ b/android/sdk.go
@@ -237,9 +237,25 @@
 	// * string
 	// * array of the above
 	// * bool
+	// For these types it is an error if multiple properties with the same name
+	// are added.
+	//
+	// * pointer to a struct
 	// * BpPropertySet
 	//
-	// It is an error if multiple properties with the same name are added.
+	// A pointer to a Blueprint-style property struct is first converted into a
+	// BpPropertySet by traversing the fields and adding their values as
+	// properties in a BpPropertySet. A field with a struct value is itself
+	// converted into a BpPropertySet before adding.
+	//
+	// Adding a BpPropertySet is done as follows:
+	// * If no property with the name exists then the BpPropertySet is added
+	//   directly to this property. Care must be taken to ensure that it does not
+	//   introduce a cycle.
+	// * If a property exists with the name and the current value is a
+	//   BpPropertySet then every property of the new BpPropertySet is added to
+	//   the existing BpPropertySet.
+	// * Otherwise, if a property exists with the name then it is an error.
 	AddProperty(name string, value interface{})
 
 	// Add a property with an associated tag
diff --git a/sdk/bp.go b/sdk/bp.go
index 68fe7ab..11ec8c6 100644
--- a/sdk/bp.go
+++ b/sdk/bp.go
@@ -16,6 +16,8 @@
 
 import (
 	"fmt"
+	"reflect"
+	"strings"
 
 	"android/soong/android"
 )
@@ -33,7 +35,82 @@
 	s.tags = make(map[string]android.BpPropertyTag)
 }
 
+// Converts the given value, which is assumed to be a struct, to a
+// bpPropertySet.
+func convertToPropertySet(value reflect.Value) *bpPropertySet {
+	res := newPropertySet()
+	structType := value.Type()
+
+	for i := 0; i < structType.NumField(); i++ {
+		field := structType.Field(i)
+		fieldVal := value.Field(i)
+
+		switch fieldVal.Type().Kind() {
+		case reflect.Ptr:
+			if fieldVal.IsNil() {
+				continue // nil pointer means the property isn't set.
+			}
+			fieldVal = fieldVal.Elem()
+		case reflect.Slice:
+			if fieldVal.IsNil() {
+				continue // Ignore a nil slice (but not one with length zero).
+			}
+		}
+
+		if fieldVal.Type().Kind() == reflect.Struct {
+			fieldVal = fieldVal.Addr() // Avoid struct copy below.
+		}
+		res.AddProperty(strings.ToLower(field.Name), fieldVal.Interface())
+	}
+
+	return res
+}
+
+// Converts the given value to something that can be set in a property.
+func coercePropertyValue(value interface{}) interface{} {
+	val := reflect.ValueOf(value)
+	switch val.Kind() {
+	case reflect.Struct:
+		// convertToPropertySet requires an addressable struct, and this is probably
+		// a mistake.
+		panic(fmt.Sprintf("Value is a struct, not a pointer to one: %v", value))
+	case reflect.Ptr:
+		if _, ok := value.(*bpPropertySet); !ok {
+			derefValue := reflect.Indirect(val)
+			if derefValue.Kind() != reflect.Struct {
+				panic(fmt.Sprintf("A pointer must be to a struct, got: %v", value))
+			}
+			return convertToPropertySet(derefValue)
+		}
+	}
+	return value
+}
+
+// Merges the fields of the given property set into s.
+func (s *bpPropertySet) mergePropertySet(propSet *bpPropertySet) {
+	for _, name := range propSet.order {
+		if tag, ok := propSet.tags[name]; ok {
+			s.AddPropertyWithTag(name, propSet.properties[name], tag)
+		} else {
+			s.AddProperty(name, propSet.properties[name])
+		}
+	}
+}
+
 func (s *bpPropertySet) AddProperty(name string, value interface{}) {
+	value = coercePropertyValue(value)
+
+	if propSetValue, ok := value.(*bpPropertySet); ok {
+		if curValue, ok := s.properties[name]; ok {
+			if curSet, ok := curValue.(*bpPropertySet); ok {
+				curSet.mergePropertySet(propSetValue)
+				return
+			}
+			// If the current value isn't a property set we got conflicting types.
+			// Continue down to the check below to complain about it.
+		}
+	}
+
 	if s.properties[name] != nil {
 		panic(fmt.Sprintf("Property %q already exists in property set", name))
 	}
@@ -48,9 +125,8 @@
 }
 
 func (s *bpPropertySet) AddPropertySet(name string) android.BpPropertySet {
-	set := newPropertySet()
-	s.AddProperty(name, set)
-	return set
+	s.AddProperty(name, newPropertySet())
+	return s.properties[name].(android.BpPropertySet)
 }
 
 func (s *bpPropertySet) getValue(name string) interface{} {
diff --git a/sdk/bp_test.go b/sdk/bp_test.go
index c630c25..e1edc51 100644
--- a/sdk/bp_test.go
+++ b/sdk/bp_test.go
@@ -18,8 +18,142 @@
 	"testing"
 
 	"android/soong/android"
+
+	"github.com/google/blueprint/proptools"
 )
 
+func propertySetFixture() interface{} {
+	set := newPropertySet()
+	set.AddProperty("x", "taxi")
+	set.AddPropertyWithTag("y", 1729, "tag_y")
+	subset := set.AddPropertySet("sub")
+	subset.AddPropertyWithTag("x", "taxi", "tag_x")
+	subset.AddProperty("y", 1729)
+	return set
+}
+
+func intPtr(i int) *int { return &i }
+
+type propertyStruct struct {
+	X     *string
+	Y     *int
+	Unset *bool
+	Sub   struct {
+		X     *string
+		Y     *int
+		Unset *bool
+	}
+}
+
+func propertyStructFixture() interface{} {
+	str := &propertyStruct{}
+	str.X = proptools.StringPtr("taxi")
+	str.Y = intPtr(1729)
+	str.Sub.X = proptools.StringPtr("taxi")
+	str.Sub.Y = intPtr(1729)
+	return str
+}
+
+func checkPropertySetFixture(h *TestHelper, val interface{}, hasTags bool) {
+	set := val.(*bpPropertySet)
+	h.AssertDeepEquals("wrong x value", "taxi", set.getValue("x"))
+	h.AssertDeepEquals("wrong y value", 1729, set.getValue("y"))
+
+	subset := set.getValue("sub").(*bpPropertySet)
+	h.AssertDeepEquals("wrong sub.x value", "taxi", subset.getValue("x"))
+	h.AssertDeepEquals("wrong sub.y value", 1729, subset.getValue("y"))
+
+	if hasTags {
+		h.AssertDeepEquals("wrong y tag", "tag_y", set.getTag("y"))
+		h.AssertDeepEquals("wrong sub.x tag", "tag_x", subset.getTag("x"))
+	} else {
+		h.AssertDeepEquals("wrong y tag", nil, set.getTag("y"))
+		h.AssertDeepEquals("wrong sub.x tag", nil, subset.getTag("x"))
+	}
+}
+
+func TestAddPropertySimple(t *testing.T) {
+	h := &TestHelper{t}
+	set := newPropertySet()
+	for name, val := range map[string]interface{}{
+		"x":   "taxi",
+		"y":   1729,
+		"t":   true,
+		"f":   false,
+		"arr": []string{"a", "b", "c"},
+	} {
+		set.AddProperty(name, val)
+		h.AssertDeepEquals("wrong value", val, set.getValue(name))
+	}
+	h.AssertPanic("adding x again should panic",
+		func() { set.AddProperty("x", "taxi") })
+	h.AssertPanic("adding arr again should panic",
+		func() { set.AddProperty("arr", []string{"d"}) })
+}
+
+func TestAddPropertySubset(t *testing.T) {
+	h := &TestHelper{t}
+	getFixtureMap := map[string]func() interface{}{
+		"property set":    propertySetFixture,
+		"property struct": propertyStructFixture,
+	}
+
+	t.Run("add new subset", func(t *testing.T) {
+		for name, getFixture := range getFixtureMap {
+			t.Run(name, func(t *testing.T) {
+				set := propertySetFixture().(*bpPropertySet)
+				set.AddProperty("new", getFixture())
+				checkPropertySetFixture(h, set, true)
+				checkPropertySetFixture(h, set.getValue("new"), name == "property set")
+			})
+		}
+	})
+
+	t.Run("merge existing subset", func(t *testing.T) {
+		for name, getFixture := range getFixtureMap {
+			t.Run(name, func(t *testing.T) {
+				set := newPropertySet()
+				subset := set.AddPropertySet("sub")
+				subset.AddProperty("flag", false)
+				subset.AddPropertySet("sub")
+				set.AddProperty("sub", getFixture())
+				merged := set.getValue("sub").(*bpPropertySet)
+				h.AssertDeepEquals("wrong flag value", false, merged.getValue("flag"))
+				checkPropertySetFixture(h, merged, name == "property set")
+			})
+		}
+	})
+
+	t.Run("add conflicting subset", func(t *testing.T) {
+		set := propertySetFixture().(*bpPropertySet)
+		h.AssertPanic("adding x again should panic",
+			func() { set.AddProperty("x", propertySetFixture()) })
+	})
+
+	t.Run("add non-pointer struct", func(t *testing.T) {
+		set := propertySetFixture().(*bpPropertySet)
+		str := propertyStructFixture().(*propertyStruct)
+		h.AssertPanic("adding a non-pointer struct should panic",
+			func() { set.AddProperty("new", *str) })
+	})
+}
+
+func TestAddPropertySetNew(t *testing.T) {
+	h := &TestHelper{t}
+	set := newPropertySet()
+	subset := set.AddPropertySet("sub")
+	subset.AddProperty("new", "d^^b")
+	h.AssertDeepEquals("wrong sub.new value", "d^^b", set.getValue("sub").(*bpPropertySet).getValue("new"))
+}
+
+func TestAddPropertySetExisting(t *testing.T) {
+	h := &TestHelper{t}
+	set := propertySetFixture().(*bpPropertySet)
+	subset := set.AddPropertySet("sub")
+	subset.AddProperty("new", "d^^b")
+	h.AssertDeepEquals("wrong sub.new value", "d^^b", set.getValue("sub").(*bpPropertySet).getValue("new"))
+}
+
 type removeFredTransformation struct {
 	identityTransformation
 }
diff --git a/sdk/testing.go b/sdk/testing.go
index b53558d..68058ee 100644
--- a/sdk/testing.go
+++ b/sdk/testing.go
@@ -217,6 +217,22 @@
 	}
 }
 
+func (h *TestHelper) AssertPanic(message string, funcThatShouldPanic func()) {
+	h.t.Helper()
+	panicked := false
+	func() {
+		defer func() {
+			if x := recover(); x != nil {
+				panicked = true
+			}
+		}()
+		funcThatShouldPanic()
+	}()
+	if !panicked {
+		h.t.Error(message)
+	}
+}
+
 // Encapsulates result of processing an SDK definition. Provides support for
 // checking the state of the build structures.
 type testSdkResult struct {