diff --git a/android/Android.bp b/android/Android.bp index bf7804ae5..4c59592c8 100644 --- a/android/Android.bp +++ b/android/Android.bp @@ -89,6 +89,7 @@ bootstrap_go_package { "sandbox.go", "sdk.go", "sdk_version.go", + "shared_properties.go", "singleton.go", "singleton_module.go", "soong_config_modules.go", @@ -102,6 +103,7 @@ bootstrap_go_package { "visibility.go", ], testSrcs: [ + "all_teams_test.go", "android_test.go", "androidmk_test.go", "apex_test.go", diff --git a/android/all_teams.go b/android/all_teams.go index b177e20e7..0c433a6cb 100644 --- a/android/all_teams.go +++ b/android/all_teams.go @@ -23,9 +23,18 @@ func registerAllTeamBuildComponents(ctx RegistrationContext) { } // For each module, list the team or the bpFile the module is defined in. -type moduleTeamInfo struct { +type moduleTeamAndTestInfo struct { + // Name field from bp file for the team teamName string - bpFile string + // Blueprint file the module is located in. + bpFile string + // Is this module only used by tests. + testOnly bool + // Is this a directly testable target by running the module directly + // or via tradefed. + topLevelTestTarget bool + // String name indicating the module, like `java_library` for reporting. + kind string } type allTeamsSingleton struct { @@ -37,16 +46,16 @@ type allTeamsSingleton struct { // Map of all team modules we visit during GenerateBuildActions teams map[string]teamProperties // Keeps track of team information or bp file for each module we visit. - teams_for_mods map[string]moduleTeamInfo + teams_for_mods map[string]moduleTeamAndTestInfo } // See if there is a package module for the given bpFilePath with a team defined, if so return the team. // If not ascend up to the parent directory and do the same. -func (this *allTeamsSingleton) lookupDefaultTeam(bpFilePath string) (teamProperties, bool) { +func (t *allTeamsSingleton) lookupDefaultTeam(bpFilePath string) (teamProperties, bool) { // return the Default_team listed in the package if is there. - if p, ok := this.packages[bpFilePath]; ok { - if t := p.Default_team; t != nil { - return this.teams[*p.Default_team], true + if p, ok := t.packages[bpFilePath]; ok { + if defaultTeam := p.Default_team; defaultTeam != nil { + return t.teams[*defaultTeam], true } } // Strip a directory and go up. @@ -57,72 +66,79 @@ func (this *allTeamsSingleton) lookupDefaultTeam(bpFilePath string) (teamPropert if current == "." { return teamProperties{}, false } - return this.lookupDefaultTeam(filepath.Join(parent, base)) + return t.lookupDefaultTeam(filepath.Join(parent, base)) } -// Create a rule to run a tool to collect all the intermediate files -// which list the team per module into one proto file. -func (this *allTeamsSingleton) GenerateBuildActions(ctx SingletonContext) { - this.packages = make(map[string]packageProperties) - this.teams = make(map[string]teamProperties) - this.teams_for_mods = make(map[string]moduleTeamInfo) +// Visit all modules and collect all teams and use WriteFileRuleVerbatim +// to write it out. +func (t *allTeamsSingleton) GenerateBuildActions(ctx SingletonContext) { + t.packages = make(map[string]packageProperties) + t.teams = make(map[string]teamProperties) + t.teams_for_mods = make(map[string]moduleTeamAndTestInfo) ctx.VisitAllModules(func(module Module) { bpFile := ctx.BlueprintFile(module) + testModInfo := TestModuleInformation{} + if tmi, ok := SingletonModuleProvider(ctx, module, TestOnlyProviderKey); ok { + testModInfo = tmi + } + // Package Modules and Team Modules are stored in a map so we can look them up by name for // modules without a team. if pack, ok := module.(*packageModule); ok { - // Packages don't have names, use the blueprint file as the key. we can't get qualifiedModuleId in this context. + // Packages don't have names, use the blueprint file as the key. we can't get qualifiedModuleId in t context. pkgKey := bpFile - this.packages[pkgKey] = pack.properties + t.packages[pkgKey] = pack.properties return } if team, ok := module.(*teamModule); ok { - this.teams[team.Name()] = team.properties + t.teams[team.Name()] = team.properties return } - // If a team name is given for a module, store it. - // Otherwise store the bpFile so we can do a package walk later. - if module.base().Team() != "" { - this.teams_for_mods[module.Name()] = moduleTeamInfo{teamName: module.base().Team(), bpFile: bpFile} - } else { - this.teams_for_mods[module.Name()] = moduleTeamInfo{bpFile: bpFile} + entry := moduleTeamAndTestInfo{ + bpFile: bpFile, + testOnly: testModInfo.TestOnly, + topLevelTestTarget: testModInfo.TopLevelTarget, + kind: ctx.ModuleType(module), + teamName: module.base().Team(), } + t.teams_for_mods[module.Name()] = entry + }) // Visit all modules again and lookup the team name in the package or parent package if the team // isn't assignged at the module level. - allTeams := this.lookupTeamForAllModules() + allTeams := t.lookupTeamForAllModules() - this.outputPath = PathForOutput(ctx, ownershipDirectory, allTeamsFile) + t.outputPath = PathForOutput(ctx, ownershipDirectory, allTeamsFile) data, err := proto.Marshal(allTeams) if err != nil { ctx.Errorf("Unable to marshal team data. %s", err) } - WriteFileRuleVerbatim(ctx, this.outputPath, string(data)) - ctx.Phony("all_teams", this.outputPath) + WriteFileRuleVerbatim(ctx, t.outputPath, string(data)) + ctx.Phony("all_teams", t.outputPath) } -func (this *allTeamsSingleton) MakeVars(ctx MakeVarsContext) { - ctx.DistForGoal("all_teams", this.outputPath) +func (t *allTeamsSingleton) MakeVars(ctx MakeVarsContext) { + ctx.DistForGoal("all_teams", t.outputPath) } // Visit every (non-package, non-team) module and write out a proto containing // either the declared team data for that module or the package default team data for that module. -func (this *allTeamsSingleton) lookupTeamForAllModules() *team_proto.AllTeams { - teamsProto := make([]*team_proto.Team, len(this.teams_for_mods)) - for i, moduleName := range SortedKeys(this.teams_for_mods) { - m, _ := this.teams_for_mods[moduleName] +func (t *allTeamsSingleton) lookupTeamForAllModules() *team_proto.AllTeams { + teamsProto := make([]*team_proto.Team, len(t.teams_for_mods)) + for i, moduleName := range SortedKeys(t.teams_for_mods) { + m, _ := t.teams_for_mods[moduleName] teamName := m.teamName var teamProperties teamProperties found := false if teamName != "" { - teamProperties, found = this.teams[teamName] + teamProperties, found = t.teams[teamName] } else { - teamProperties, found = this.lookupDefaultTeam(m.bpFile) + teamProperties, found = t.lookupDefaultTeam(m.bpFile) } trendy_team_id := "" @@ -130,22 +146,18 @@ func (this *allTeamsSingleton) lookupTeamForAllModules() *team_proto.AllTeams { trendy_team_id = *teamProperties.Trendy_team_id } - var files []string teamData := new(team_proto.Team) + *teamData = team_proto.Team{ + TargetName: proto.String(moduleName), + Path: proto.String(m.bpFile), + TestOnly: proto.Bool(m.testOnly), + TopLevelTarget: proto.Bool(m.topLevelTestTarget), + Kind: proto.String(m.kind), + } if trendy_team_id != "" { - *teamData = team_proto.Team{ - TrendyTeamId: proto.String(trendy_team_id), - TargetName: proto.String(moduleName), - Path: proto.String(m.bpFile), - File: files, - } + teamData.TrendyTeamId = proto.String(trendy_team_id) } else { // Clients rely on the TrendyTeamId optional field not being set. - *teamData = team_proto.Team{ - TargetName: proto.String(moduleName), - Path: proto.String(m.bpFile), - File: files, - } } teamsProto[i] = teamData } diff --git a/android/all_teams_test.go b/android/all_teams_test.go index a02b86e06..9c2b38e42 100644 --- a/android/all_teams_test.go +++ b/android/all_teams_test.go @@ -24,9 +24,8 @@ import ( func TestAllTeams(t *testing.T) { t.Parallel() ctx := GroupFixturePreparers( - PrepareForTestWithTeamBuildComponents, + prepareForTestWithTeamAndFakes, FixtureRegisterWithContext(func(ctx RegistrationContext) { - ctx.RegisterModuleType("fake", fakeModuleFactory) ctx.RegisterParallelSingletonType("all_teams", AllTeamsFactory) }), ).RunTestWithBp(t, ` @@ -51,6 +50,12 @@ func TestAllTeams(t *testing.T) { fake { name: "noteam", + test_only: true, + } + fake { + name: "test-and-team-and-top", + test_only: true, + team: "team2", } `) @@ -59,16 +64,31 @@ func TestAllTeams(t *testing.T) { // map of module name -> trendy team name. actualTeams := make(map[string]*string) + actualTests := []string{} + actualTopLevelTests := []string{} + for _, teamProto := range teams.Teams { actualTeams[teamProto.GetTargetName()] = teamProto.TrendyTeamId + if teamProto.GetTestOnly() { + actualTests = append(actualTests, teamProto.GetTargetName()) + } + if teamProto.GetTopLevelTarget() { + actualTopLevelTests = append(actualTopLevelTests, teamProto.GetTargetName()) + } } expectedTeams := map[string]*string{ - "main_test": proto.String("cool_team"), - "tool": proto.String("22222"), - "noteam": nil, + "main_test": proto.String("cool_team"), + "tool": proto.String("22222"), + "test-and-team-and-top": proto.String("22222"), + "noteam": nil, } + expectedTests := []string{ + "noteam", + "test-and-team-and-top", + } AssertDeepEquals(t, "compare maps", expectedTeams, actualTeams) + AssertDeepEquals(t, "test matchup", expectedTests, actualTests) } func getTeamProtoOutput(t *testing.T, ctx *TestResult) *team_proto.AllTeams { @@ -171,10 +191,9 @@ func TestPackageLookup(t *testing.T) { } ` ctx := GroupFixturePreparers( - PrepareForTestWithTeamBuildComponents, + prepareForTestWithTeamAndFakes, PrepareForTestWithPackageModule, FixtureRegisterWithContext(func(ctx RegistrationContext) { - ctx.RegisterModuleType("fake", fakeModuleFactory) ctx.RegisterParallelSingletonType("all_teams", AllTeamsFactory) }), FixtureAddTextFile("Android.bp", rootBp), @@ -206,3 +225,33 @@ func TestPackageLookup(t *testing.T) { } AssertDeepEquals(t, "compare maps", expectedTeams, actualTeams) } + +type fakeForTests struct { + ModuleBase + + sourceProperties SourceProperties +} + +func fakeFactory() Module { + module := &fakeForTests{} + module.AddProperties(&module.sourceProperties) + InitAndroidModule(module) + + return module +} + +var prepareForTestWithTeamAndFakes = GroupFixturePreparers( + FixtureRegisterWithContext(RegisterTeamBuildComponents), + FixtureRegisterWithContext(func(ctx RegistrationContext) { + ctx.RegisterModuleType("fake", fakeFactory) + }), +) + +func (f *fakeForTests) GenerateAndroidBuildActions(ctx ModuleContext) { + if Bool(f.sourceProperties.Test_only) { + SetProvider(ctx, TestOnlyProviderKey, TestModuleInformation{ + TestOnly: Bool(f.sourceProperties.Test_only), + TopLevelTarget: false, + }) + } +} diff --git a/android/shared_properties.go b/android/shared_properties.go new file mode 100644 index 000000000..84d68fa14 --- /dev/null +++ b/android/shared_properties.go @@ -0,0 +1,26 @@ +// Copyright 2024 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 + +// For storing user-supplied properties about source code on a module to be queried later. +type SourceProperties struct { + // Indicates that the module and its source code are only used in tests, not + // production code. Used by coverage reports and potentially other tools. + Test_only *bool + // Used internally to write if this is a top level test target. + // i.e. something that can be run directly or through tradefed as a test. + // `java_library` would be false, `java_test` would be true. + Top_level_test_target bool `blueprint:"mutated"` +} diff --git a/android/team.go b/android/team.go index df61f4021..c273dc647 100644 --- a/android/team.go +++ b/android/team.go @@ -14,6 +14,8 @@ package android +import "github.com/google/blueprint" + func init() { RegisterTeamBuildComponents(InitRegistrationContext) } @@ -37,6 +39,13 @@ type teamModule struct { properties teamProperties } +type TestModuleInformation struct { + TestOnly bool + TopLevelTarget bool +} + +var TestOnlyProviderKey = blueprint.NewProvider[TestModuleInformation]() + // Real work is done for the module that depends on us. // If needed, the team can serialize the config to json/proto file as well. func (t *teamModule) GenerateAndroidBuildActions(ctx ModuleContext) {} diff --git a/android/team_proto/team.pb.go b/android/team_proto/team.pb.go index 61260cf78..8414d176a 100644 --- a/android/team_proto/team.pb.go +++ b/android/team_proto/team.pb.go @@ -46,6 +46,13 @@ type Team struct { TrendyTeamId *string `protobuf:"bytes,3,opt,name=trendy_team_id,json=trendyTeamId" json:"trendy_team_id,omitempty"` // OPTIONAL: Files directly owned by this module. File []string `protobuf:"bytes,4,rep,name=file" json:"file,omitempty"` + // OPTIONAL: Is this a test-only module. + TestOnly *bool `protobuf:"varint,5,opt,name=test_only,json=testOnly" json:"test_only,omitempty"` + // OPTIONAL: Is this intended to be run as a test target. + // This target can be run directly as a test or passed to tradefed. + TopLevelTarget *bool `protobuf:"varint,6,opt,name=top_level_target,json=topLevelTarget" json:"top_level_target,omitempty"` + // OPTIONAL: Name of module kind, i.e. java_library + Kind *string `protobuf:"bytes,7,opt,name=kind" json:"kind,omitempty"` } func (x *Team) Reset() { @@ -108,6 +115,27 @@ func (x *Team) GetFile() []string { return nil } +func (x *Team) GetTestOnly() bool { + if x != nil && x.TestOnly != nil { + return *x.TestOnly + } + return false +} + +func (x *Team) GetTopLevelTarget() bool { + if x != nil && x.TopLevelTarget != nil { + return *x.TopLevelTarget + } + return false +} + +func (x *Team) GetKind() string { + if x != nil && x.Kind != nil { + return *x.Kind + } + return "" +} + type AllTeams struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -159,20 +187,26 @@ var File_team_proto protoreflect.FileDescriptor var file_team_proto_rawDesc = []byte{ 0x0a, 0x0a, 0x74, 0x65, 0x61, 0x6d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x74, 0x65, - 0x61, 0x6d, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x75, 0x0a, 0x04, 0x54, 0x65, 0x61, 0x6d, - 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x72, 0x65, 0x6e, 0x64, 0x79, 0x5f, - 0x74, 0x65, 0x61, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, - 0x72, 0x65, 0x6e, 0x64, 0x79, 0x54, 0x65, 0x61, 0x6d, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x66, - 0x69, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x66, 0x69, 0x6c, 0x65, 0x22, - 0x32, 0x0a, 0x08, 0x41, 0x6c, 0x6c, 0x54, 0x65, 0x61, 0x6d, 0x73, 0x12, 0x26, 0x0a, 0x05, 0x74, - 0x65, 0x61, 0x6d, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x74, 0x65, 0x61, - 0x6d, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x65, 0x61, 0x6d, 0x52, 0x05, 0x74, 0x65, - 0x61, 0x6d, 0x73, 0x42, 0x22, 0x5a, 0x20, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2f, 0x73, - 0x6f, 0x6f, 0x6e, 0x67, 0x2f, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2f, 0x74, 0x65, 0x61, - 0x6d, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x61, 0x6d, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xd0, 0x01, 0x0a, 0x04, 0x54, 0x65, 0x61, + 0x6d, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x72, 0x65, 0x6e, 0x64, 0x79, + 0x5f, 0x74, 0x65, 0x61, 0x6d, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, + 0x74, 0x72, 0x65, 0x6e, 0x64, 0x79, 0x54, 0x65, 0x61, 0x6d, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, + 0x66, 0x69, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x66, 0x69, 0x6c, 0x65, + 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x08, 0x74, 0x65, 0x73, 0x74, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x28, 0x0a, + 0x10, 0x74, 0x6f, 0x70, 0x5f, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x5f, 0x74, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x74, 0x6f, 0x70, 0x4c, 0x65, 0x76, 0x65, + 0x6c, 0x54, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x22, 0x32, 0x0a, 0x08, 0x41, + 0x6c, 0x6c, 0x54, 0x65, 0x61, 0x6d, 0x73, 0x12, 0x26, 0x0a, 0x05, 0x74, 0x65, 0x61, 0x6d, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x74, 0x65, 0x61, 0x6d, 0x5f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x54, 0x65, 0x61, 0x6d, 0x52, 0x05, 0x74, 0x65, 0x61, 0x6d, 0x73, 0x42, + 0x22, 0x5a, 0x20, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2f, 0x73, 0x6f, 0x6f, 0x6e, 0x67, + 0x2f, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2f, 0x74, 0x65, 0x61, 0x6d, 0x5f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, } var ( diff --git a/android/team_proto/team.proto b/android/team_proto/team.proto index 401eccc6e..69ebf4332 100644 --- a/android/team_proto/team.proto +++ b/android/team_proto/team.proto @@ -27,6 +27,16 @@ message Team { // OPTIONAL: Files directly owned by this module. repeated string file = 4; + + // OPTIONAL: Is this a test-only module. + optional bool test_only = 5; + + // OPTIONAL: Is this intended to be run as a test target. + // This target can be run directly as a test or passed to tradefed. + optional bool top_level_target = 6; + + // OPTIONAL: Name of module kind, i.e. java_library + optional string kind = 7; } message AllTeams { diff --git a/android/team_test.go b/android/team_test.go index 75b3e9fbb..ccfcaaa66 100644 --- a/android/team_test.go +++ b/android/team_test.go @@ -27,16 +27,19 @@ func fakeModuleFactory() Module { return module } +var prepareForTestWithTeamAndFakeBuildComponents = GroupFixturePreparers( + FixtureRegisterWithContext(RegisterTeamBuildComponents), + FixtureRegisterWithContext(func(ctx RegistrationContext) { + ctx.RegisterModuleType("fake", fakeModuleFactory) + }), +) + func (*fakeModuleForTests) GenerateAndroidBuildActions(ModuleContext) {} func TestTeam(t *testing.T) { t.Parallel() - ctx := GroupFixturePreparers( - PrepareForTestWithTeamBuildComponents, - FixtureRegisterWithContext(func(ctx RegistrationContext) { - ctx.RegisterModuleType("fake", fakeModuleFactory) - }), - ).RunTestWithBp(t, ` + ctx := prepareForTestWithTeamAndFakeBuildComponents. + RunTestWithBp(t, ` fake { name: "main_test", team: "someteam", @@ -66,12 +69,7 @@ func TestTeam(t *testing.T) { func TestMissingTeamFails(t *testing.T) { t.Parallel() - GroupFixturePreparers( - PrepareForTestWithTeamBuildComponents, - FixtureRegisterWithContext(func(ctx RegistrationContext) { - ctx.RegisterModuleType("fake", fakeModuleFactory) - }), - ). + prepareForTestWithTeamAndFakeBuildComponents. ExtendWithErrorHandler(FixtureExpectsAtLeastOneErrorMatchingPattern("depends on undefined module \"ring-bearer")). RunTestWithBp(t, ` fake { @@ -86,9 +84,6 @@ func TestPackageBadTeamNameFails(t *testing.T) { GroupFixturePreparers( PrepareForTestWithTeamBuildComponents, PrepareForTestWithPackageModule, - FixtureRegisterWithContext(func(ctx RegistrationContext) { - ctx.RegisterModuleType("fake", fakeModuleFactory) - }), ). ExtendWithErrorHandler(FixtureExpectsAtLeastOneErrorMatchingPattern("depends on undefined module \"ring-bearer")). RunTestWithBp(t, `