Extract module naming into an interface
in facilitate moving name resolution to Soong Bug: 65683273 Test: build/soong/scripts/diff_build_graphs.sh \ --products=aosp_arm \ 'build/blueprint:work^' 'build/blueprint:work' # and see that the only changes were: # 1. adding the name_interface.go file # 2. changing some line numbers Change-Id: Ifa7603cca59b3b3d592f2f146fdafe57012bd4b9
This commit is contained in:
parent
90d258833a
commit
d70bf75491
6 changed files with 254 additions and 90 deletions
|
@ -12,6 +12,7 @@ bootstrap_go_package {
|
|||
"live_tracker.go",
|
||||
"mangle.go",
|
||||
"module_ctx.go",
|
||||
"name_interface.go",
|
||||
"ninja_defs.go",
|
||||
"ninja_strings.go",
|
||||
"ninja_writer.go",
|
||||
|
|
152
context.go
152
context.go
|
@ -69,7 +69,7 @@ const MockModuleListFile = "bplist"
|
|||
type Context struct {
|
||||
// set at instantiation
|
||||
moduleFactories map[string]ModuleFactory
|
||||
moduleNames map[string]*moduleGroup
|
||||
nameInterface NameInterface
|
||||
moduleGroups []*moduleGroup
|
||||
moduleInfo map[Module]*moduleInfo
|
||||
modulesSorted []*moduleInfo
|
||||
|
@ -104,8 +104,8 @@ type Context struct {
|
|||
requiredNinjaMinor int // For the ninja_required_version variable
|
||||
requiredNinjaMicro int // For the ninja_required_version variable
|
||||
|
||||
// set lazily by sortedModuleNames
|
||||
cachedSortedModuleNames []string
|
||||
// set lazily by sortedModuleGroups
|
||||
cachedSortedModuleGroups []*moduleGroup
|
||||
|
||||
globs map[string]GlobPath
|
||||
globLock sync.Mutex
|
||||
|
@ -158,6 +158,8 @@ type moduleGroup struct {
|
|||
ninjaName string
|
||||
|
||||
modules []*moduleInfo
|
||||
|
||||
namespace Namespace
|
||||
}
|
||||
|
||||
type moduleInfo struct {
|
||||
|
@ -211,6 +213,10 @@ func (module *moduleInfo) String() string {
|
|||
return s
|
||||
}
|
||||
|
||||
func (module *moduleInfo) namespace() Namespace {
|
||||
return module.group.namespace
|
||||
}
|
||||
|
||||
// A Variation is a way that a variant of a module differs from other variants of the same module.
|
||||
// For example, two variants of the same module might have Variation{"arch","arm"} and
|
||||
// Variation{"arch","arm64"}
|
||||
|
@ -270,7 +276,7 @@ type mutatorInfo struct {
|
|||
func newContext() *Context {
|
||||
return &Context{
|
||||
moduleFactories: make(map[string]ModuleFactory),
|
||||
moduleNames: make(map[string]*moduleGroup),
|
||||
nameInterface: NewSimpleNameInterface(),
|
||||
moduleInfo: make(map[Module]*moduleInfo),
|
||||
moduleNinjaNames: make(map[string]*moduleGroup),
|
||||
globs: make(map[string]GlobPath),
|
||||
|
@ -416,6 +422,10 @@ func (c *Context) RegisterPreSingletonType(name string, factory SingletonFactory
|
|||
})
|
||||
}
|
||||
|
||||
func (c *Context) SetNameInterface(i NameInterface) {
|
||||
c.nameInterface = i
|
||||
}
|
||||
|
||||
func singletonPkgPath(singleton Singleton) string {
|
||||
typ := reflect.TypeOf(singleton)
|
||||
for typ.Kind() == reflect.Ptr {
|
||||
|
@ -1233,21 +1243,7 @@ func (c *Context) addModule(module *moduleInfo) []error {
|
|||
name := module.logicModule.Name()
|
||||
c.moduleInfo[module.logicModule] = module
|
||||
|
||||
if group, present := c.moduleNames[name]; present {
|
||||
return []error{
|
||||
&BlueprintError{
|
||||
Err: fmt.Errorf("module %q already defined", name),
|
||||
Pos: module.pos,
|
||||
},
|
||||
&BlueprintError{
|
||||
Err: fmt.Errorf("<-- previous definition here"),
|
||||
Pos: group.modules[0].pos,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
ninjaName := toNinjaName(name)
|
||||
|
||||
// The sanitizing in toNinjaName can result in collisions, uniquify the name if it
|
||||
// already exists
|
||||
for i := 0; c.moduleNinjaNames[ninjaName] != nil; i++ {
|
||||
|
@ -1260,7 +1256,18 @@ func (c *Context) addModule(module *moduleInfo) []error {
|
|||
modules: []*moduleInfo{module},
|
||||
}
|
||||
module.group = group
|
||||
c.moduleNames[name] = group
|
||||
namespace, errs := c.nameInterface.NewModule(
|
||||
&moduleCreationContextImpl{c.ModuleDir(module.logicModule)},
|
||||
ModuleGroup{moduleGroup: group},
|
||||
module.logicModule)
|
||||
if len(errs) > 0 {
|
||||
for i := range errs {
|
||||
errs[i] = &BlueprintError{Err: errs[i], Pos: module.pos}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
group.namespace = namespace
|
||||
|
||||
c.moduleNinjaNames[ninjaName] = group
|
||||
c.moduleGroups = append(c.moduleGroups, group)
|
||||
|
||||
|
@ -1347,17 +1354,9 @@ func (c *Context) addDependency(module *moduleInfo, tag DependencyTag, depName s
|
|||
}}
|
||||
}
|
||||
|
||||
possibleDeps := c.modulesFromName(depName)
|
||||
possibleDeps := c.modulesFromName(depName, module.namespace())
|
||||
if possibleDeps == nil {
|
||||
if c.allowMissingDependencies {
|
||||
module.missingDeps = append(module.missingDeps, depName)
|
||||
return nil
|
||||
}
|
||||
return []error{&BlueprintError{
|
||||
Err: fmt.Errorf("%q depends on undefined module %q",
|
||||
module.Name(), depName),
|
||||
Pos: module.pos,
|
||||
}}
|
||||
return c.discoveredMissingDependencies(module, depName)
|
||||
}
|
||||
|
||||
if m := c.findMatchingVariant(module, possibleDeps); m != nil {
|
||||
|
@ -1395,7 +1394,7 @@ func (c *Context) findReverseDependency(module *moduleInfo, destName string) (*m
|
|||
}}
|
||||
}
|
||||
|
||||
possibleDeps := c.modulesFromName(destName)
|
||||
possibleDeps := c.modulesFromName(destName, module.namespace())
|
||||
if possibleDeps == nil {
|
||||
return nil, []error{&BlueprintError{
|
||||
Err: fmt.Errorf("%q has a reverse dependency on undefined module %q",
|
||||
|
@ -1429,17 +1428,9 @@ func (c *Context) addVariationDependency(module *moduleInfo, variations []Variat
|
|||
panic("BaseDependencyTag is not allowed to be used directly!")
|
||||
}
|
||||
|
||||
possibleDeps := c.modulesFromName(depName)
|
||||
possibleDeps := c.modulesFromName(depName, module.namespace())
|
||||
if possibleDeps == nil {
|
||||
if c.allowMissingDependencies {
|
||||
module.missingDeps = append(module.missingDeps, depName)
|
||||
return nil
|
||||
}
|
||||
return []error{&BlueprintError{
|
||||
Err: fmt.Errorf("%q depends on undefined module %q",
|
||||
module.Name(), depName),
|
||||
Pos: module.pos,
|
||||
}}
|
||||
return c.discoveredMissingDependencies(module, depName)
|
||||
}
|
||||
|
||||
// We can't just append variant.Variant to module.dependencyVariants.variantName and
|
||||
|
@ -2190,11 +2181,7 @@ func (c *Context) generateModuleBuildActions(config interface{},
|
|||
if module.missingDeps != nil && !mctx.handledMissingDeps {
|
||||
var errs []error
|
||||
for _, depName := range module.missingDeps {
|
||||
errs = append(errs, &BlueprintError{
|
||||
Err: fmt.Errorf("%q depends on undefined module %q",
|
||||
module.Name(), depName),
|
||||
Pos: module.pos,
|
||||
})
|
||||
errs = append(errs, c.missingDependencyError(module, depName))
|
||||
}
|
||||
errsCh <- errs
|
||||
return true
|
||||
|
@ -2359,7 +2346,7 @@ type rename struct {
|
|||
}
|
||||
|
||||
func (c *Context) moduleMatchingVariant(module *moduleInfo, name string) *moduleInfo {
|
||||
targets := c.modulesFromName(name)
|
||||
targets := c.modulesFromName(name, module.namespace())
|
||||
|
||||
if targets == nil {
|
||||
return nil
|
||||
|
@ -2378,29 +2365,11 @@ func (c *Context) handleRenames(renames []rename) []error {
|
|||
var errs []error
|
||||
for _, rename := range renames {
|
||||
group, name := rename.group, rename.name
|
||||
if name == group.name {
|
||||
if name == group.name || len(group.modules) < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
existing := c.moduleNames[name]
|
||||
if existing != nil {
|
||||
errs = append(errs,
|
||||
&BlueprintError{
|
||||
Err: fmt.Errorf("renaming module %q to %q conflicts with existing module",
|
||||
group.name, name),
|
||||
Pos: group.modules[0].pos,
|
||||
},
|
||||
&BlueprintError{
|
||||
Err: fmt.Errorf("<-- existing module defined here"),
|
||||
Pos: existing.modules[0].pos,
|
||||
},
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
c.moduleNames[name] = group
|
||||
delete(c.moduleNames, group.name)
|
||||
group.name = name
|
||||
errs = append(errs, c.nameInterface.Rename(group.name, rename.name, group.namespace)...)
|
||||
}
|
||||
|
||||
return errs
|
||||
|
@ -2423,24 +2392,45 @@ func (c *Context) handleReplacements(replacements []replace) []error {
|
|||
return errs
|
||||
}
|
||||
|
||||
func (c *Context) modulesFromName(name string) []*moduleInfo {
|
||||
if group := c.moduleNames[name]; group != nil {
|
||||
func (c *Context) discoveredMissingDependencies(module *moduleInfo, depName string) (errs []error) {
|
||||
if c.allowMissingDependencies {
|
||||
module.missingDeps = append(module.missingDeps, depName)
|
||||
return nil
|
||||
}
|
||||
return []error{c.missingDependencyError(module, depName)}
|
||||
}
|
||||
|
||||
func (c *Context) missingDependencyError(module *moduleInfo, depName string) (errs error) {
|
||||
err := c.nameInterface.MissingDependencyError(module.Name(), module.namespace(), depName)
|
||||
|
||||
return &BlueprintError{
|
||||
Err: err,
|
||||
Pos: module.pos,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Context) modulesFromName(name string, namespace Namespace) []*moduleInfo {
|
||||
group, exists := c.nameInterface.ModuleFromName(name, namespace)
|
||||
if exists {
|
||||
return group.modules
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Context) sortedModuleNames() []string {
|
||||
if c.cachedSortedModuleNames == nil {
|
||||
c.cachedSortedModuleNames = make([]string, 0, len(c.moduleNames))
|
||||
for moduleName := range c.moduleNames {
|
||||
c.cachedSortedModuleNames = append(c.cachedSortedModuleNames,
|
||||
moduleName)
|
||||
func (c *Context) sortedModuleGroups() []*moduleGroup {
|
||||
if c.cachedSortedModuleGroups == nil {
|
||||
unwrap := func(wrappers []ModuleGroup) []*moduleGroup {
|
||||
result := make([]*moduleGroup, 0, len(wrappers))
|
||||
for _, group := range wrappers {
|
||||
result = append(result, group.moduleGroup)
|
||||
}
|
||||
return result
|
||||
}
|
||||
sort.Strings(c.cachedSortedModuleNames)
|
||||
|
||||
c.cachedSortedModuleGroups = unwrap(c.nameInterface.AllModules())
|
||||
}
|
||||
|
||||
return c.cachedSortedModuleNames
|
||||
return c.cachedSortedModuleGroups
|
||||
}
|
||||
|
||||
func (c *Context) visitAllModules(visit func(Module)) {
|
||||
|
@ -2453,9 +2443,8 @@ func (c *Context) visitAllModules(visit func(Module)) {
|
|||
}
|
||||
}()
|
||||
|
||||
for _, moduleName := range c.sortedModuleNames() {
|
||||
modules := c.modulesFromName(moduleName)
|
||||
for _, module = range modules {
|
||||
for _, moduleGroup := range c.sortedModuleGroups() {
|
||||
for _, module = range moduleGroup.modules {
|
||||
visit(module.logicModule)
|
||||
}
|
||||
}
|
||||
|
@ -2473,9 +2462,8 @@ func (c *Context) visitAllModulesIf(pred func(Module) bool,
|
|||
}
|
||||
}()
|
||||
|
||||
for _, moduleName := range c.sortedModuleNames() {
|
||||
modules := c.modulesFromName(moduleName)
|
||||
for _, module := range modules {
|
||||
for _, moduleGroup := range c.sortedModuleGroups() {
|
||||
for _, module := range moduleGroup.modules {
|
||||
if pred(module.logicModule) {
|
||||
visit(module.logicModule)
|
||||
}
|
||||
|
|
|
@ -183,7 +183,7 @@ func TestWalkDeps(t *testing.T) {
|
|||
|
||||
var outputDown string
|
||||
var outputUp string
|
||||
topModule := ctx.modulesFromName("A")[0]
|
||||
topModule := ctx.modulesFromName("A", nil)[0]
|
||||
ctx.walkDeps(topModule,
|
||||
func(dep depInfo, parent *moduleInfo) bool {
|
||||
if dep.module.logicModule.(Walker).Walk() {
|
||||
|
@ -239,10 +239,10 @@ func TestCreateModule(t *testing.T) {
|
|||
t.FailNow()
|
||||
}
|
||||
|
||||
a := ctx.modulesFromName("A")[0].logicModule.(*fooModule)
|
||||
b := ctx.modulesFromName("B")[0].logicModule.(*barModule)
|
||||
c := ctx.modulesFromName("C")[0].logicModule.(*barModule)
|
||||
d := ctx.modulesFromName("D")[0].logicModule.(*fooModule)
|
||||
a := ctx.modulesFromName("A", nil)[0].logicModule.(*fooModule)
|
||||
b := ctx.modulesFromName("B", nil)[0].logicModule.(*barModule)
|
||||
c := ctx.modulesFromName("C", nil)[0].logicModule.(*barModule)
|
||||
d := ctx.modulesFromName("D", nil)[0].logicModule.(*fooModule)
|
||||
|
||||
checkDeps := func(m Module, expected string) {
|
||||
var deps []string
|
||||
|
|
|
@ -142,6 +142,8 @@ type BaseModuleContext interface {
|
|||
|
||||
moduleInfo() *moduleInfo
|
||||
error(err error)
|
||||
|
||||
Namespace() Namespace
|
||||
}
|
||||
|
||||
type DynamicDependerModuleContext BottomUpMutatorContext
|
||||
|
@ -269,6 +271,10 @@ func (d *baseModuleContext) Fs() pathtools.FileSystem {
|
|||
return d.context.fs
|
||||
}
|
||||
|
||||
func (d *baseModuleContext) Namespace() Namespace {
|
||||
return d.context.nameInterface.GetNamespace(d)
|
||||
}
|
||||
|
||||
var _ ModuleContext = (*moduleContext)(nil)
|
||||
|
||||
type moduleContext struct {
|
||||
|
@ -650,7 +656,8 @@ func (mctx *mutatorContext) Module() Module {
|
|||
// correctly for all future mutator passes.
|
||||
func (mctx *mutatorContext) AddDependency(module Module, tag DependencyTag, deps ...string) {
|
||||
for _, dep := range deps {
|
||||
errs := mctx.context.addDependency(mctx.context.moduleInfo[module], tag, dep)
|
||||
modInfo := mctx.context.moduleInfo[module]
|
||||
errs := mctx.context.addDependency(modInfo, tag, dep)
|
||||
if len(errs) > 0 {
|
||||
mctx.errs = append(mctx.errs, errs...)
|
||||
}
|
||||
|
@ -731,7 +738,8 @@ func (mctx *mutatorContext) ReplaceDependencies(name string) {
|
|||
}
|
||||
|
||||
func (mctx *mutatorContext) OtherModuleExists(name string) bool {
|
||||
return mctx.context.moduleNames[name] != nil
|
||||
_, exists := mctx.context.nameInterface.ModuleFromName(name, mctx.module.namespace())
|
||||
return exists
|
||||
}
|
||||
|
||||
// Rename all variants of a module. The new name is not visible to calls to ModuleName,
|
||||
|
|
167
name_interface.go
Normal file
167
name_interface.go
Normal file
|
@ -0,0 +1,167 @@
|
|||
// Copyright 2017 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 blueprint
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// This file exposes the logic of locating a module via a query string, to enable
|
||||
// other projects to override it if desired.
|
||||
// The default name resolution implementation, SimpleNameInterface,
|
||||
// just treats the query string as a module name, and does a simple map lookup.
|
||||
|
||||
// A ModuleGroup just points to a moduleGroup to allow external packages to refer
|
||||
// to a moduleGroup but not use it
|
||||
type ModuleGroup struct {
|
||||
*moduleGroup
|
||||
}
|
||||
|
||||
func (h *ModuleGroup) String() string {
|
||||
return h.moduleGroup.name
|
||||
}
|
||||
|
||||
// The Namespace interface is just a marker interface for usage by the NameInterface,
|
||||
// to allow a NameInterface to specify that a certain parameter should be a Namespace.
|
||||
// In practice, a specific NameInterface will expect to only give and receive structs of
|
||||
// the same concrete type, but because Go doesn't support generics, we use a marker interface
|
||||
// for a little bit of clarity, and expect implementers to do typecasting instead.
|
||||
type Namespace interface {
|
||||
namespace(Namespace)
|
||||
}
|
||||
type NamespaceMarker struct {
|
||||
}
|
||||
|
||||
func (m *NamespaceMarker) namespace(Namespace) {
|
||||
}
|
||||
|
||||
// A NameInterface tells how to locate modules by name.
|
||||
// There should only be one name interface per Context, but potentially many namespaces
|
||||
type NameInterface interface {
|
||||
// Gets called when a new module is created
|
||||
NewModule(ctx NamespaceContext, group ModuleGroup, module Module) (namespace Namespace, err []error)
|
||||
|
||||
// Finds the module with the given name
|
||||
ModuleFromName(moduleName string, namespace Namespace) (group ModuleGroup, found bool)
|
||||
|
||||
// Returns an error indicating that the given module could not be found.
|
||||
// The error contains some diagnostic information about where the dependency can be found.
|
||||
MissingDependencyError(depender string, dependerNamespace Namespace, depName string) (err error)
|
||||
|
||||
// Rename
|
||||
Rename(oldName string, newName string, namespace Namespace) []error
|
||||
|
||||
// Returns all modules in a deterministic order.
|
||||
AllModules() []ModuleGroup
|
||||
|
||||
// gets the namespace for a given path
|
||||
GetNamespace(ctx NamespaceContext) (namespace Namespace)
|
||||
}
|
||||
|
||||
// A NamespaceContext stores the information given to a NameInterface to enable the NameInterface
|
||||
// to choose the namespace for any given module
|
||||
type NamespaceContext interface {
|
||||
ModuleDir() string
|
||||
}
|
||||
|
||||
type moduleCreationContextImpl struct {
|
||||
moduleDir string
|
||||
}
|
||||
|
||||
func (ctx *moduleCreationContextImpl) ModuleDir() string {
|
||||
return ctx.moduleDir
|
||||
}
|
||||
|
||||
// a SimpleNameInterface just stores all modules in a map based on name
|
||||
type SimpleNameInterface struct {
|
||||
modules map[string]ModuleGroup
|
||||
}
|
||||
|
||||
func NewSimpleNameInterface() *SimpleNameInterface {
|
||||
return &SimpleNameInterface{
|
||||
modules: make(map[string]ModuleGroup),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SimpleNameInterface) NewModule(ctx NamespaceContext, group ModuleGroup, module Module) (namespace Namespace, err []error) {
|
||||
name := group.name
|
||||
if group, present := s.modules[name]; present {
|
||||
return nil, []error{
|
||||
// seven characters at the start of the second line to align with the string "error: "
|
||||
fmt.Errorf("module %q already defined\n"+
|
||||
" %s <-- previous definition here", name, group.modules[0].pos),
|
||||
}
|
||||
}
|
||||
|
||||
s.modules[name] = group
|
||||
|
||||
return nil, []error{}
|
||||
}
|
||||
|
||||
func (s *SimpleNameInterface) ModuleFromName(moduleName string, namespace Namespace) (group ModuleGroup, found bool) {
|
||||
group, found = s.modules[moduleName]
|
||||
return group, found
|
||||
}
|
||||
|
||||
func (s *SimpleNameInterface) Rename(oldName string, newName string, namespace Namespace) (errs []error) {
|
||||
existingGroup, exists := s.modules[newName]
|
||||
if exists {
|
||||
errs = append(errs,
|
||||
// seven characters at the start of the second line to align with the string "error: "
|
||||
fmt.Errorf("renaming module %q to %q conflicts with existing module\n"+
|
||||
" %s <-- existing module defined here",
|
||||
oldName, newName, existingGroup.modules[0].pos),
|
||||
)
|
||||
return errs
|
||||
}
|
||||
|
||||
group := s.modules[oldName]
|
||||
s.modules[newName] = group
|
||||
delete(s.modules, group.name)
|
||||
group.name = newName
|
||||
return []error{}
|
||||
}
|
||||
|
||||
func (s *SimpleNameInterface) AllModules() []ModuleGroup {
|
||||
groups := make([]ModuleGroup, 0, len(s.modules))
|
||||
for _, group := range s.modules {
|
||||
groups = append(groups, group)
|
||||
}
|
||||
|
||||
duplicateName := ""
|
||||
less := func(i, j int) bool {
|
||||
if groups[i].name == groups[j].name {
|
||||
duplicateName = groups[i].name
|
||||
}
|
||||
return groups[i].name < groups[j].name
|
||||
}
|
||||
sort.Slice(groups, less)
|
||||
if duplicateName != "" {
|
||||
// It is permitted to have two moduleGroup's with the same name, but not within the same
|
||||
// Namespace. The SimpleNameInterface should catch this in NewModule, however, so this
|
||||
// should never happen.
|
||||
panic(fmt.Sprintf("Duplicate moduleGroup name %q", duplicateName))
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
func (s *SimpleNameInterface) MissingDependencyError(depender string, dependerNamespace Namespace, dependency string) (err error) {
|
||||
return fmt.Errorf("%q depends on undefined module %q", depender, dependency)
|
||||
}
|
||||
|
||||
func (s *SimpleNameInterface) GetNamespace(ctx NamespaceContext) Namespace {
|
||||
return nil
|
||||
}
|
|
@ -141,7 +141,7 @@ func setupVisitTest(t *testing.T) *Context {
|
|||
func TestVisit(t *testing.T) {
|
||||
ctx := setupVisitTest(t)
|
||||
|
||||
topModule := ctx.modulesFromName("A")[0].logicModule.(*visitModule)
|
||||
topModule := ctx.modulesFromName("A", nil)[0].logicModule.(*visitModule)
|
||||
assertString(t, topModule.properties.VisitDepsDepthFirst, "EDCB")
|
||||
assertString(t, topModule.properties.VisitDepsDepthFirstIf, "EDC")
|
||||
assertString(t, topModule.properties.VisitDirectDeps, "B")
|
||||
|
|
Loading…
Reference in a new issue