From 0c3acbfd727a88c459fbe97f225f4a115dc48b83 Mon Sep 17 00:00:00 2001 From: Paul Duffin Date: Tue, 24 Aug 2021 19:01:25 +0100 Subject: [PATCH] Support pruning properties by build release Adds a general mechanism for pruning selected sdk member properties (i.e. setting their fields to their zero value) and uses that to prune any properties that do not support a specified target build release. Follow up changes will use that to allow building an sdk snapshot that is compatible with previous release S. Bug: 197842263 Test: m nothing Change-Id: Ib949a9cfe85fff30f86228eeb15d3a45c073b037 --- sdk/build_release.go | 164 ++++++++++++++++++++++++++++++++++++++ sdk/build_release_test.go | 85 ++++++++++++++++++++ 2 files changed, 249 insertions(+) diff --git a/sdk/build_release.go b/sdk/build_release.go index f2379ae36..a3f089973 100644 --- a/sdk/build_release.go +++ b/sdk/build_release.go @@ -16,6 +16,7 @@ package sdk import ( "fmt" + "reflect" "strings" ) @@ -158,3 +159,166 @@ func parseBuildReleaseSet(specification string) (*buildReleaseSet, error) { return set, nil } + +// Given a set of properties (struct value), set the value of a field within that struct (or one of +// its embedded structs) to its zero value. +type fieldPrunerFunc func(structValue reflect.Value) + +// A property that can be cleared by a propertyPruner. +type prunerProperty struct { + // The name of the field for this property. It is a "."-separated path for fields in non-anonymous + // sub-structs. + name string + + // Sets the associated field to its zero value. + prunerFunc fieldPrunerFunc +} + +// propertyPruner provides support for pruning (i.e. setting to their zero value) properties from +// a properties structure. +type propertyPruner struct { + // The properties that the pruner will clear. + properties []prunerProperty +} + +// gatherFields recursively processes the supplied structure and a nested structures, selecting the +// fields that require pruning and populates the propertyPruner.properties with the information +// needed to prune those fields. +// +// containingStructAccessor is a func that if given an object will return a field whose value is +// of the supplied structType. It is nil on initial entry to this method but when this method is +// called recursively on a field that is a nested structure containingStructAccessor is set to a +// func that provides access to the field's value. +// +// namePrefix is the prefix to the fields that are being visited. It is "" on initial entry to this +// method but when this method is called recursively on a field that is a nested structure +// namePrefix is the result of appending the field name (plus a ".") to the previous name prefix. +// Unless the field is anonymous in which case it is passed through unchanged. +// +// selector is a func that will select whether the supplied field requires pruning or not. If it +// returns true then the field will be added to those to be pruned, otherwise it will not. +func (p *propertyPruner) gatherFields(structType reflect.Type, containingStructAccessor fieldAccessorFunc, namePrefix string, selector fieldSelectorFunc) { + for f := 0; f < structType.NumField(); f++ { + field := structType.Field(f) + if field.PkgPath != "" { + // Ignore unexported fields. + continue + } + + // Save a copy of the field index for use in the function. + fieldIndex := f + + name := namePrefix + field.Name + + fieldGetter := func(container reflect.Value) reflect.Value { + if containingStructAccessor != nil { + // This is an embedded structure so first access the field for the embedded + // structure. + container = containingStructAccessor(container) + } + + // Skip through interface and pointer values to find the structure. + container = getStructValue(container) + + defer func() { + if r := recover(); r != nil { + panic(fmt.Errorf("%s for fieldIndex %d of field %s of container %#v", r, fieldIndex, name, container.Interface())) + } + }() + + // Return the field. + return container.Field(fieldIndex) + } + + zeroValue := reflect.Zero(field.Type) + fieldPruner := func(container reflect.Value) { + if containingStructAccessor != nil { + // This is an embedded structure so first access the field for the embedded + // structure. + container = containingStructAccessor(container) + } + + // Skip through interface and pointer values to find the structure. + container = getStructValue(container) + + defer func() { + if r := recover(); r != nil { + panic(fmt.Errorf("%s for fieldIndex %d of field %s of container %#v", r, fieldIndex, name, container.Interface())) + } + }() + + // Set the field. + container.Field(fieldIndex).Set(zeroValue) + } + + if selector(name, field) { + property := prunerProperty{ + name, + fieldPruner, + } + p.properties = append(p.properties, property) + } else if field.Type.Kind() == reflect.Struct { + // Gather fields from the nested or embedded structure. + var subNamePrefix string + if field.Anonymous { + subNamePrefix = namePrefix + } else { + subNamePrefix = name + "." + } + p.gatherFields(field.Type, fieldGetter, subNamePrefix, selector) + } + } +} + +// pruneProperties will prune (set to zero value) any properties in the supplied struct. +// +// The struct must be of the same type as was originally passed to newPropertyPruner to create this +// propertyPruner. +func (p *propertyPruner) pruneProperties(propertiesStruct interface{}) { + structValue := reflect.ValueOf(propertiesStruct) + for _, property := range p.properties { + property.prunerFunc(structValue) + } +} + +// fieldSelectorFunc is called to select whether a specific field should be pruned or not. +// name is the name of the field, including any prefixes from containing str +type fieldSelectorFunc func(name string, field reflect.StructField) bool + +// newPropertyPruner creates a new property pruner for the structure type for the supplied +// properties struct. +// +// The returned pruner can be used on any properties structure of the same type as the supplied set +// of properties. +func newPropertyPruner(propertiesStruct interface{}, selector fieldSelectorFunc) *propertyPruner { + structType := getStructValue(reflect.ValueOf(propertiesStruct)).Type() + pruner := &propertyPruner{} + pruner.gatherFields(structType, nil, "", selector) + return pruner +} + +// newPropertyPrunerByBuildRelease creates a property pruner that will clear any properties in the +// structure which are not supported by the specified target build release. +// +// A property is pruned if its field has a tag of the form: +// `supported_build_releases:""` +// and the resulting build release set does not contain the target build release. Properties that +// have no such tag are assumed to be supported by all releases. +func newPropertyPrunerByBuildRelease(propertiesStruct interface{}, targetBuildRelease *buildRelease) *propertyPruner { + return newPropertyPruner(propertiesStruct, func(name string, field reflect.StructField) bool { + if supportedBuildReleases, ok := field.Tag.Lookup("supported_build_releases"); ok { + set, err := parseBuildReleaseSet(supportedBuildReleases) + if err != nil { + panic(fmt.Errorf("invalid `supported_build_releases` tag on %s of %T: %s", name, propertiesStruct, err)) + } + + // If the field does not support tha target release then prune it. + return !set.contains(targetBuildRelease) + + } else { + // Any untagged fields are assumed to be supported by all build releases so should never be + // pruned. + return false + } + }) +} diff --git a/sdk/build_release_test.go b/sdk/build_release_test.go index fa6591cb8..dff276d0c 100644 --- a/sdk/build_release_test.go +++ b/sdk/build_release_test.go @@ -98,3 +98,88 @@ func TestBuildReleaseSetContains(t *testing.T) { android.AssertBoolEquals(t, "set does not contain T", false, set.contains(buildReleaseT)) }) } + +func TestPropertyPrunerInvalidTag(t *testing.T) { + type brokenStruct struct { + Broken string `supported_build_releases:"A"` + } + type containingStruct struct { + Nested brokenStruct + } + + t.Run("broken struct", func(t *testing.T) { + android.AssertPanicMessageContains(t, "error", "invalid `supported_build_releases` tag on Broken of *sdk.brokenStruct: unknown release \"A\"", func() { + newPropertyPrunerByBuildRelease(&brokenStruct{}, buildReleaseS) + }) + }) + + t.Run("nested broken struct", func(t *testing.T) { + android.AssertPanicMessageContains(t, "error", "invalid `supported_build_releases` tag on Nested.Broken of *sdk.containingStruct: unknown release \"A\"", func() { + newPropertyPrunerByBuildRelease(&containingStruct{}, buildReleaseS) + }) + }) +} + +func TestPropertyPrunerByBuildRelease(t *testing.T) { + type nested struct { + F1_only string `supported_build_releases:"F1"` + } + + type testBuildReleasePruner struct { + Default string + S_and_T_only string `supported_build_releases:"S-T"` + T_later string `supported_build_releases:"T+"` + Nested nested + } + + input := testBuildReleasePruner{ + Default: "Default", + S_and_T_only: "S_and_T_only", + T_later: "T_later", + Nested: nested{ + F1_only: "F1_only", + }, + } + + t.Run("target S", func(t *testing.T) { + testStruct := input + pruner := newPropertyPrunerByBuildRelease(&testStruct, buildReleaseS) + pruner.pruneProperties(&testStruct) + + expected := input + expected.T_later = "" + expected.Nested.F1_only = "" + android.AssertDeepEquals(t, "test struct", expected, testStruct) + }) + + t.Run("target T", func(t *testing.T) { + testStruct := input + pruner := newPropertyPrunerByBuildRelease(&testStruct, buildReleaseT) + pruner.pruneProperties(&testStruct) + + expected := input + expected.Nested.F1_only = "" + android.AssertDeepEquals(t, "test struct", expected, testStruct) + }) + + t.Run("target F1", func(t *testing.T) { + testStruct := input + pruner := newPropertyPrunerByBuildRelease(&testStruct, buildReleaseFuture1) + pruner.pruneProperties(&testStruct) + + expected := input + expected.S_and_T_only = "" + android.AssertDeepEquals(t, "test struct", expected, testStruct) + }) + + t.Run("target F2", func(t *testing.T) { + testStruct := input + pruner := newPropertyPrunerByBuildRelease(&testStruct, buildReleaseFuture2) + pruner.pruneProperties(&testStruct) + + expected := input + expected.S_and_T_only = "" + expected.Nested.F1_only = "" + android.AssertDeepEquals(t, "test struct", expected, testStruct) + }) +}