Files
build_soong/android/androidmk_test.go
Paul Duffin 74f05598eb Differentiate between no dist tag and an empty dist tag
Change https://r.android.com/1335521 added tag property to the Dist
struct so that it could be used to select one of a number of different
output files to copy to the dist instead of the single file that the
module type made available for dist. The output files were selected
by passing the tag to OutputFiles(tag).

Module types that wanted to support this new approach had to explicitly
set AndroidMkEntries.DistFiles = GenerateTaggedDistFiles(module).
Unfortunately, doing that had a side effect of changing the behavior of
dist entries without a tag.

That was because the change treated a tag that was not specified, as
being the same as "". So, prior to the change no tag meant use the
default dist file but after it meant use the paths returned by
OutputFiles(""). That changed the behavior of the java.Library type
which affected the behavior of the android_app module type.

Prior to the change the java_library would make the
Library.outputFile available for dist when no tag was specified. After
that change it would make Library.outputFile plus
Library.extraOutputFiles. The latter is usually empty except for
android_app which adds some extra files into there which will now be
copied to the dist. That change may have been intentional but there
was no mention of it in the change or the bug. Even if it wasn't
intentional it may still be beneficial.

Any module type that wants to add support for tags in dist runs the
risk of introducing similar changes in behavior. This change
differentiates between the tag not being set and the tag being set to
"" to avoid that possibility and to make the default behavior
explicit for those module types that have switched.

It does so as follows:
* Adds a DefaultDistTag constant that is used when the tag is not set.
  It is a string that is unlikely to be used as an actual tag as it
  does not start with a . and uses some special characters.
* The DefaultDistTag is used in MakeDefaultDistFiles(paths) to indicate
  that the supplied paths are the default ones and and also in
  GenerateTaggedDistFiles() for Dist structures that have no tag
  property set.
* The DefaultDistTag is passed to OutputFiles(tag) just in case the
  module type has explicitly defined the paths to associate with that
  tag in there. If it has then it overrides the legacy behavior. If it
  has not then it is just ignored and falls back to using the previous
  behavior.
* The java.Library.OutputFiles(tag) method explicitly handles the
  DefaultDistTag and returns Library.outputFile for it which restores
  the behavior from before the change that added dist.tag support.
* Similar change was made to apexBundle.OutputFiles(tag) in order to
  preserve its previous behaviour.
* The customModule used by TestGetDistContributions has been modified
  to also preserve its previous behavior after this change.

Test: m nothing
      m dist sdk - before and after this change, compare result to
      make sure that there are no significant differences.
      Test the effect on the apex by following instructions in
      http://b/172951145
Bug: 174226317
Change-Id: Ib8f0d9307751cc2ed34e3d9a5538d3c144666f6d
2020-11-27 15:17:44 +00:00

733 lines
18 KiB
Go

// Copyright 2019 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 android
import (
"fmt"
"io"
"reflect"
"strings"
"testing"
"github.com/google/blueprint/proptools"
)
type customModule struct {
ModuleBase
properties struct {
Default_dist_files *string
Dist_output_file *bool
}
data AndroidMkData
distFiles TaggedDistFiles
outputFile OptionalPath
// The paths that will be used as the default dist paths if no tag is
// specified.
defaultDistPaths Paths
}
const (
defaultDistFiles_None = "none"
defaultDistFiles_Default = "default"
defaultDistFiles_Tagged = "tagged"
)
func (m *customModule) GenerateAndroidBuildActions(ctx ModuleContext) {
// If the dist_output_file: true then create an output file that is stored in
// the OutputFile property of the AndroidMkEntry.
if proptools.BoolDefault(m.properties.Dist_output_file, true) {
path := PathForTesting("dist-output-file.out")
m.outputFile = OptionalPathForPath(path)
// Previous code would prioritize the DistFiles property over the OutputFile
// property in AndroidMkEntry when determining the default dist paths.
// Setting this first allows it to be overridden based on the
// default_dist_files setting replicating that previous behavior.
m.defaultDistPaths = Paths{path}
}
// Based on the setting of the default_dist_files property possibly create a
// TaggedDistFiles structure that will be stored in the DistFiles property of
// the AndroidMkEntry.
defaultDistFiles := proptools.StringDefault(m.properties.Default_dist_files, defaultDistFiles_Tagged)
switch defaultDistFiles {
case defaultDistFiles_None:
// Do nothing
case defaultDistFiles_Default:
path := PathForTesting("default-dist.out")
m.defaultDistPaths = Paths{path}
m.distFiles = MakeDefaultDistFiles(path)
case defaultDistFiles_Tagged:
// Module types that set AndroidMkEntry.DistFiles to the result of calling
// GenerateTaggedDistFiles(ctx) relied on no tag being treated as "" which
// meant that the default dist paths would be whatever was returned by
// OutputFiles(""). In order to preserve that behavior when treating no tag
// as being equal to DefaultDistTag this ensures that
// OutputFiles(DefaultDistTag) will return the same as OutputFiles("").
m.defaultDistPaths = PathsForTesting("one.out")
// This must be called after setting defaultDistPaths/outputFile as
// GenerateTaggedDistFiles calls into OutputFiles(tag) which may use those
// fields.
m.distFiles = m.GenerateTaggedDistFiles(ctx)
}
}
func (m *customModule) AndroidMk() AndroidMkData {
return AndroidMkData{
Custom: func(w io.Writer, name, prefix, moduleDir string, data AndroidMkData) {
m.data = data
},
}
}
func (m *customModule) OutputFiles(tag string) (Paths, error) {
switch tag {
case DefaultDistTag:
if m.defaultDistPaths != nil {
return m.defaultDistPaths, nil
} else {
return nil, fmt.Errorf("default dist tag is not available")
}
case "":
return PathsForTesting("one.out"), nil
case ".multiple":
return PathsForTesting("two.out", "three/four.out"), nil
case ".another-tag":
return PathsForTesting("another.out"), nil
default:
return nil, fmt.Errorf("unsupported module reference tag %q", tag)
}
}
func (m *customModule) AndroidMkEntries() []AndroidMkEntries {
return []AndroidMkEntries{
{
Class: "CUSTOM_MODULE",
DistFiles: m.distFiles,
OutputFile: m.outputFile,
},
}
}
func customModuleFactory() Module {
module := &customModule{}
module.AddProperties(&module.properties)
InitAndroidModule(module)
return module
}
// buildConfigAndCustomModuleFoo creates a config object, processes the supplied
// bp module and then returns the config and the custom module called "foo".
func buildConfigAndCustomModuleFoo(t *testing.T, bp string) (Config, *customModule) {
t.Helper()
config := TestConfig(buildDir, nil, bp, nil)
config.katiEnabled = true // Enable androidmk Singleton
ctx := NewTestContext(config)
ctx.RegisterSingletonType("androidmk", AndroidMkSingleton)
ctx.RegisterModuleType("custom", customModuleFactory)
ctx.Register()
_, errs := ctx.ParseFileList(".", []string{"Android.bp"})
FailIfErrored(t, errs)
_, errs = ctx.PrepareBuildActions(config)
FailIfErrored(t, errs)
module := ctx.ModuleForTests("foo", "").Module().(*customModule)
return config, module
}
func TestAndroidMkSingleton_PassesUpdatedAndroidMkDataToCustomCallback(t *testing.T) {
bp := `
custom {
name: "foo",
required: ["bar"],
host_required: ["baz"],
target_required: ["qux"],
}
`
_, m := buildConfigAndCustomModuleFoo(t, bp)
assertEqual := func(expected interface{}, actual interface{}) {
if !reflect.DeepEqual(expected, actual) {
t.Errorf("%q expected, but got %q", expected, actual)
}
}
assertEqual([]string{"bar"}, m.data.Required)
assertEqual([]string{"baz"}, m.data.Host_required)
assertEqual([]string{"qux"}, m.data.Target_required)
}
func TestGenerateDistContributionsForMake(t *testing.T) {
dc := &distContributions{
copiesForGoals: []*copiesForGoals{
{
goals: "my_goal",
copies: []distCopy{
distCopyForTest("one.out", "one.out"),
distCopyForTest("two.out", "other.out"),
},
},
},
}
makeOutput := generateDistContributionsForMake(dc)
assertStringEquals(t, `.PHONY: my_goal
$(call dist-for-goals,my_goal,one.out:one.out)
$(call dist-for-goals,my_goal,two.out:other.out)
`, strings.Join(makeOutput, ""))
}
func TestGetDistForGoals(t *testing.T) {
bp := `
custom {
name: "foo",
dist: {
targets: ["my_goal", "my_other_goal"],
tag: ".multiple",
},
dists: [
{
targets: ["my_second_goal"],
tag: ".multiple",
},
{
targets: ["my_third_goal"],
dir: "test/dir",
},
{
targets: ["my_fourth_goal"],
suffix: ".suffix",
},
{
targets: ["my_fifth_goal"],
dest: "new-name",
},
{
targets: ["my_sixth_goal"],
dest: "new-name",
dir: "some/dir",
suffix: ".suffix",
},
],
}
`
expectedAndroidMkLines := []string{
".PHONY: my_second_goal\n",
"$(call dist-for-goals,my_second_goal,two.out:two.out)\n",
"$(call dist-for-goals,my_second_goal,three/four.out:four.out)\n",
".PHONY: my_third_goal\n",
"$(call dist-for-goals,my_third_goal,one.out:test/dir/one.out)\n",
".PHONY: my_fourth_goal\n",
"$(call dist-for-goals,my_fourth_goal,one.out:one.suffix.out)\n",
".PHONY: my_fifth_goal\n",
"$(call dist-for-goals,my_fifth_goal,one.out:new-name)\n",
".PHONY: my_sixth_goal\n",
"$(call dist-for-goals,my_sixth_goal,one.out:some/dir/new-name.suffix)\n",
".PHONY: my_goal my_other_goal\n",
"$(call dist-for-goals,my_goal my_other_goal,two.out:two.out)\n",
"$(call dist-for-goals,my_goal my_other_goal,three/four.out:four.out)\n",
}
config, module := buildConfigAndCustomModuleFoo(t, bp)
entries := AndroidMkEntriesForTest(t, config, "", module)
if len(entries) != 1 {
t.Errorf("Expected a single AndroidMk entry, got %d", len(entries))
}
androidMkLines := entries[0].GetDistForGoals(module)
if len(androidMkLines) != len(expectedAndroidMkLines) {
t.Errorf(
"Expected %d AndroidMk lines, got %d:\n%v",
len(expectedAndroidMkLines),
len(androidMkLines),
androidMkLines,
)
}
for idx, line := range androidMkLines {
expectedLine := expectedAndroidMkLines[idx]
if line != expectedLine {
t.Errorf(
"Expected AndroidMk line to be '%s', got '%s'",
expectedLine,
line,
)
}
}
}
func distCopyForTest(from, to string) distCopy {
return distCopy{PathForTesting(from), to}
}
func TestGetDistContributions(t *testing.T) {
compareContributions := func(d1 *distContributions, d2 *distContributions) error {
if d1 == nil || d2 == nil {
if d1 != d2 {
return fmt.Errorf("pointer mismatch, expected both to be nil but they were %p and %p", d1, d2)
} else {
return nil
}
}
if expected, actual := len(d1.copiesForGoals), len(d2.copiesForGoals); expected != actual {
return fmt.Errorf("length mismatch, expected %d found %d", expected, actual)
}
for i, copies1 := range d1.copiesForGoals {
copies2 := d2.copiesForGoals[i]
if expected, actual := copies1.goals, copies2.goals; expected != actual {
return fmt.Errorf("goals mismatch at position %d: expected %q found %q", i, expected, actual)
}
if expected, actual := len(copies1.copies), len(copies2.copies); expected != actual {
return fmt.Errorf("length mismatch in copy instructions at position %d, expected %d found %d", i, expected, actual)
}
for j, c1 := range copies1.copies {
c2 := copies2.copies[j]
if expected, actual := NormalizePathForTesting(c1.from), NormalizePathForTesting(c2.from); expected != actual {
return fmt.Errorf("paths mismatch at position %d.%d: expected %q found %q", i, j, expected, actual)
}
if expected, actual := c1.dest, c2.dest; expected != actual {
return fmt.Errorf("dest mismatch at position %d.%d: expected %q found %q", i, j, expected, actual)
}
}
}
return nil
}
formatContributions := func(d *distContributions) string {
buf := &strings.Builder{}
if d == nil {
fmt.Fprint(buf, "nil")
} else {
for _, copiesForGoals := range d.copiesForGoals {
fmt.Fprintf(buf, " Goals: %q {\n", copiesForGoals.goals)
for _, c := range copiesForGoals.copies {
fmt.Fprintf(buf, " %s -> %s\n", NormalizePathForTesting(c.from), c.dest)
}
fmt.Fprint(buf, " }\n")
}
}
return buf.String()
}
testHelper := func(t *testing.T, name, bp string, expectedContributions *distContributions) {
t.Helper()
t.Run(name, func(t *testing.T) {
t.Helper()
config, module := buildConfigAndCustomModuleFoo(t, bp)
entries := AndroidMkEntriesForTest(t, config, "", module)
if len(entries) != 1 {
t.Errorf("Expected a single AndroidMk entry, got %d", len(entries))
}
distContributions := entries[0].getDistContributions(module)
if err := compareContributions(expectedContributions, distContributions); err != nil {
t.Errorf("%s\nExpected Contributions\n%sActualContributions\n%s",
err,
formatContributions(expectedContributions),
formatContributions(distContributions))
}
})
}
testHelper(t, "dist-without-tag", `
custom {
name: "foo",
dist: {
targets: ["my_goal"]
}
}
`,
&distContributions{
copiesForGoals: []*copiesForGoals{
{
goals: "my_goal",
copies: []distCopy{
distCopyForTest("one.out", "one.out"),
},
},
},
})
testHelper(t, "dist-with-tag", `
custom {
name: "foo",
dist: {
targets: ["my_goal"],
tag: ".another-tag",
}
}
`,
&distContributions{
copiesForGoals: []*copiesForGoals{
{
goals: "my_goal",
copies: []distCopy{
distCopyForTest("another.out", "another.out"),
},
},
},
})
testHelper(t, "dists-with-tag", `
custom {
name: "foo",
dists: [
{
targets: ["my_goal"],
tag: ".another-tag",
},
],
}
`,
&distContributions{
copiesForGoals: []*copiesForGoals{
{
goals: "my_goal",
copies: []distCopy{
distCopyForTest("another.out", "another.out"),
},
},
},
})
testHelper(t, "multiple-dists-with-and-without-tag", `
custom {
name: "foo",
dists: [
{
targets: ["my_goal"],
},
{
targets: ["my_second_goal", "my_third_goal"],
},
],
}
`,
&distContributions{
copiesForGoals: []*copiesForGoals{
{
goals: "my_goal",
copies: []distCopy{
distCopyForTest("one.out", "one.out"),
},
},
{
goals: "my_second_goal my_third_goal",
copies: []distCopy{
distCopyForTest("one.out", "one.out"),
},
},
},
})
testHelper(t, "dist-plus-dists-without-tags", `
custom {
name: "foo",
dist: {
targets: ["my_goal"],
},
dists: [
{
targets: ["my_second_goal", "my_third_goal"],
},
],
}
`,
&distContributions{
copiesForGoals: []*copiesForGoals{
{
goals: "my_second_goal my_third_goal",
copies: []distCopy{
distCopyForTest("one.out", "one.out"),
},
},
{
goals: "my_goal",
copies: []distCopy{
distCopyForTest("one.out", "one.out"),
},
},
},
})
testHelper(t, "dist-plus-dists-with-tags", `
custom {
name: "foo",
dist: {
targets: ["my_goal", "my_other_goal"],
tag: ".multiple",
},
dists: [
{
targets: ["my_second_goal"],
tag: ".multiple",
},
{
targets: ["my_third_goal"],
dir: "test/dir",
},
{
targets: ["my_fourth_goal"],
suffix: ".suffix",
},
{
targets: ["my_fifth_goal"],
dest: "new-name",
},
{
targets: ["my_sixth_goal"],
dest: "new-name",
dir: "some/dir",
suffix: ".suffix",
},
],
}
`,
&distContributions{
copiesForGoals: []*copiesForGoals{
{
goals: "my_second_goal",
copies: []distCopy{
distCopyForTest("two.out", "two.out"),
distCopyForTest("three/four.out", "four.out"),
},
},
{
goals: "my_third_goal",
copies: []distCopy{
distCopyForTest("one.out", "test/dir/one.out"),
},
},
{
goals: "my_fourth_goal",
copies: []distCopy{
distCopyForTest("one.out", "one.suffix.out"),
},
},
{
goals: "my_fifth_goal",
copies: []distCopy{
distCopyForTest("one.out", "new-name"),
},
},
{
goals: "my_sixth_goal",
copies: []distCopy{
distCopyForTest("one.out", "some/dir/new-name.suffix"),
},
},
{
goals: "my_goal my_other_goal",
copies: []distCopy{
distCopyForTest("two.out", "two.out"),
distCopyForTest("three/four.out", "four.out"),
},
},
},
})
// The above test the default values of default_dist_files and use_output_file.
// The following tests explicitly test the different combinations of those settings.
testHelper(t, "tagged-dist-files-no-output", `
custom {
name: "foo",
default_dist_files: "tagged",
dist_output_file: false,
dists: [
{
targets: ["my_goal"],
},
{
targets: ["my_goal"],
tag: ".multiple",
},
],
}
`, &distContributions{
copiesForGoals: []*copiesForGoals{
{
goals: "my_goal",
copies: []distCopy{
distCopyForTest("one.out", "one.out"),
},
},
{
goals: "my_goal",
copies: []distCopy{
distCopyForTest("two.out", "two.out"),
distCopyForTest("three/four.out", "four.out"),
},
},
},
})
testHelper(t, "default-dist-files-no-output", `
custom {
name: "foo",
default_dist_files: "default",
dist_output_file: false,
dists: [
{
targets: ["my_goal"],
},
// The following is silently ignored because the dist files do not
// contain the tagged files.
{
targets: ["my_goal"],
tag: ".multiple",
},
],
}
`, &distContributions{
copiesForGoals: []*copiesForGoals{
{
goals: "my_goal",
copies: []distCopy{
distCopyForTest("default-dist.out", "default-dist.out"),
},
},
},
})
testHelper(t, "no-dist-files-no-output", `
custom {
name: "foo",
default_dist_files: "none",
dist_output_file: false,
dists: [
// The following is silently ignored because there is not default file
// in either the dist files or the output file.
{
targets: ["my_goal"],
},
// The following is silently ignored because the dist files do not
// contain the tagged files.
{
targets: ["my_goal"],
tag: ".multiple",
},
],
}
`, nil)
testHelper(t, "tagged-dist-files-default-output", `
custom {
name: "foo",
default_dist_files: "tagged",
dist_output_file: true,
dists: [
{
targets: ["my_goal"],
},
{
targets: ["my_goal"],
tag: ".multiple",
},
],
}
`, &distContributions{
copiesForGoals: []*copiesForGoals{
{
goals: "my_goal",
copies: []distCopy{
distCopyForTest("one.out", "one.out"),
},
},
{
goals: "my_goal",
copies: []distCopy{
distCopyForTest("two.out", "two.out"),
distCopyForTest("three/four.out", "four.out"),
},
},
},
})
testHelper(t, "default-dist-files-default-output", `
custom {
name: "foo",
default_dist_files: "default",
dist_output_file: true,
dists: [
{
targets: ["my_goal"],
},
// The following is silently ignored because the dist files do not
// contain the tagged files.
{
targets: ["my_goal"],
tag: ".multiple",
},
],
}
`, &distContributions{
copiesForGoals: []*copiesForGoals{
{
goals: "my_goal",
copies: []distCopy{
distCopyForTest("default-dist.out", "default-dist.out"),
},
},
},
})
testHelper(t, "no-dist-files-default-output", `
custom {
name: "foo",
default_dist_files: "none",
dist_output_file: true,
dists: [
{
targets: ["my_goal"],
},
// The following is silently ignored because the dist files do not
// contain the tagged files.
{
targets: ["my_goal"],
tag: ".multiple",
},
],
}
`, &distContributions{
copiesForGoals: []*copiesForGoals{
{
goals: "my_goal",
copies: []distCopy{
distCopyForTest("dist-output-file.out", "dist-output-file.out"),
},
},
},
})
}