// 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" "runtime" "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) { m.base().licenseMetadataFile = PathForOutput(ctx, "meta_lic") // 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 } // buildContextAndCustomModuleFoo creates a config object, processes the supplied // bp module and then returns the config and the custom module called "foo". func buildContextAndCustomModuleFoo(t *testing.T, bp string) (*TestContext, *customModule) { t.Helper() result := GroupFixturePreparers( // Enable androidmk Singleton PrepareForTestWithAndroidMk, FixtureRegisterWithContext(func(ctx RegistrationContext) { ctx.RegisterModuleType("custom", customModuleFactory) }), FixtureModifyProductVariables(func(variables FixtureProductVariables) { variables.DeviceProduct = proptools.StringPtr("bar") }), FixtureWithRootAndroidBp(bp), ).RunTest(t) module := result.ModuleForTests("foo", "").Module().(*customModule) return result.TestContext, module } func TestAndroidMkSingleton_PassesUpdatedAndroidMkDataToCustomCallback(t *testing.T) { if runtime.GOOS == "darwin" { // Device modules are not exported on Mac, so this test doesn't work. t.SkipNow() } bp := ` custom { name: "foo", required: ["bar"], host_required: ["baz"], target_required: ["qux"], } ` _, m := buildContextAndCustomModuleFoo(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"), }, }, }, } dc.licenseMetadataFile = PathForTesting("meta_lic") makeOutput := generateDistContributionsForMake(dc) assertStringEquals(t, `.PHONY: my_goal $(if $(strip $(ALL_TARGETS.one.out.META_LIC)),,$(eval ALL_TARGETS.one.out.META_LIC := meta_lic)) $(call dist-for-goals,my_goal,one.out:one.out) $(if $(strip $(ALL_TARGETS.two.out.META_LIC)),,$(eval ALL_TARGETS.two.out.META_LIC := meta_lic)) $(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", "$(if $(strip $(ALL_TARGETS.two.out.META_LIC)),,$(eval ALL_TARGETS.two.out.META_LIC := meta_lic))\n", "$(call dist-for-goals,my_second_goal,two.out:two.out)\n", "$(if $(strip $(ALL_TARGETS.three/four.out.META_LIC)),,$(eval ALL_TARGETS.three/four.out.META_LIC := meta_lic))\n", "$(call dist-for-goals,my_second_goal,three/four.out:four.out)\n", ".PHONY: my_third_goal\n", "$(if $(strip $(ALL_TARGETS.one.out.META_LIC)),,$(eval ALL_TARGETS.one.out.META_LIC := meta_lic))\n", "$(call dist-for-goals,my_third_goal,one.out:test/dir/one.out)\n", ".PHONY: my_fourth_goal\n", "$(if $(strip $(ALL_TARGETS.one.out.META_LIC)),,$(eval ALL_TARGETS.one.out.META_LIC := meta_lic))\n", "$(call dist-for-goals,my_fourth_goal,one.out:one.suffix.out)\n", ".PHONY: my_fifth_goal\n", "$(if $(strip $(ALL_TARGETS.one.out.META_LIC)),,$(eval ALL_TARGETS.one.out.META_LIC := meta_lic))\n", "$(call dist-for-goals,my_fifth_goal,one.out:new-name)\n", ".PHONY: my_sixth_goal\n", "$(if $(strip $(ALL_TARGETS.one.out.META_LIC)),,$(eval ALL_TARGETS.one.out.META_LIC := meta_lic))\n", "$(call dist-for-goals,my_sixth_goal,one.out:some/dir/new-name.suffix)\n", ".PHONY: my_goal my_other_goal\n", "$(if $(strip $(ALL_TARGETS.two.out.META_LIC)),,$(eval ALL_TARGETS.two.out.META_LIC := meta_lic))\n", "$(call dist-for-goals,my_goal my_other_goal,two.out:two.out)\n", "$(if $(strip $(ALL_TARGETS.three/four.out.META_LIC)),,$(eval ALL_TARGETS.three/four.out.META_LIC := meta_lic))\n", "$(call dist-for-goals,my_goal my_other_goal,three/four.out:four.out)\n", } ctx, module := buildContextAndCustomModuleFoo(t, bp) entries := AndroidMkEntriesForTest(t, ctx, 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 := strings.ReplaceAll(expectedAndroidMkLines[idx], "meta_lic", module.base().licenseMetadataFile.String()) 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() ctx, module := buildContextAndCustomModuleFoo(t, bp) entries := AndroidMkEntriesForTest(t, ctx, 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, "append-artifact-with-product", ` custom { name: "foo", dist: { targets: ["my_goal"], append_artifact_with_product: true, } } `, &distContributions{ copiesForGoals: []*copiesForGoals{ { goals: "my_goal", copies: []distCopy{ distCopyForTest("one.out", "one_bar.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"], }, { targets: ["my_goal"], tag: ".multiple", }, ], } `, &distContributions{ copiesForGoals: []*copiesForGoals{ { goals: "my_goal", copies: []distCopy{ distCopyForTest("default-dist.out", "default-dist.out"), }, }, { goals: "my_goal", copies: []distCopy{ distCopyForTest("two.out", "two.out"), distCopyForTest("three/four.out", "four.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"], }, { targets: ["my_goal"], tag: ".multiple", }, ], } `, &distContributions{ copiesForGoals: []*copiesForGoals{ { goals: "my_goal", copies: []distCopy{ distCopyForTest("two.out", "two.out"), distCopyForTest("three/four.out", "four.out"), }, }, }, }) 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"], }, { targets: ["my_goal"], tag: ".multiple", }, ], } `, &distContributions{ copiesForGoals: []*copiesForGoals{ { goals: "my_goal", copies: []distCopy{ distCopyForTest("default-dist.out", "default-dist.out"), }, }, { goals: "my_goal", copies: []distCopy{ distCopyForTest("two.out", "two.out"), distCopyForTest("three/four.out", "four.out"), }, }, }, }) testHelper(t, "no-dist-files-default-output", ` custom { name: "foo", default_dist_files: "none", dist_output_file: true, dists: [ { targets: ["my_goal"], }, { targets: ["my_goal"], tag: ".multiple", }, ], } `, &distContributions{ copiesForGoals: []*copiesForGoals{ { goals: "my_goal", copies: []distCopy{ distCopyForTest("dist-output-file.out", "dist-output-file.out"), }, }, { goals: "my_goal", copies: []distCopy{ distCopyForTest("two.out", "two.out"), distCopyForTest("three/four.out", "four.out"), }, }, }, }) }