Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions cli/azd/pkg/ioc/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ func NewNestedContainer(parent *NestedContainer) *NestedContainer {
current := container.New()

if parent != nil {
// Copy the bindings to the new container
// The bindings hold the concrete instance of singleton registrations
maps.Copy(current, parent.inner)
for registeredType, bindings := range parent.inner {
// clone bindings as well, otherwise changes in child scopes will
// actually modify the map for the parent!
current[registeredType] = maps.Clone(bindings)
}
}

instance := &NestedContainer{
Expand Down
181 changes: 164 additions & 17 deletions cli/azd/pkg/ioc/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ func Test_Container_Singleton_Instance_Register_Resolve(t *testing.T) {
require.Same(t, instance1, instance2)
})

t.Run("Nested Scope", func(t *testing.T) {
t.Run("Singleton, resolved", func(t *testing.T) {
rootContainer := NewNestedContainer(nil)

rootInstance := newSingletonService()
Expand All @@ -165,18 +165,136 @@ func Test_Container_Singleton_Instance_Register_Resolve(t *testing.T) {
err = scope1.Resolve(&scope1Instance1)
require.NoError(t, err)
require.NotNil(t, scope1Instance1)
require.Same(t, scope1Instance, scope1Instance1)

var scope2Instance1 *singletonService
err = scope2.Resolve(&scope2Instance1)
require.NoError(t, err)
require.NotNil(t, scope2Instance1)
require.Same(t, scope2Instance, scope2Instance1)
})

t.Run("Singleton, resolve at different levels", func(t *testing.T) {
t.Run("root first", func(t *testing.T) {
rootContainer := NewNestedContainer(nil)

// register, but not yet resolve, a singleton - we'll try to resolve it at different scopes
instance := newSingletonService()
RegisterInstance(rootContainer, instance)

childScope, err := rootContainer.NewScope()
require.NoError(t, err)

var rootResolved *singletonService
rootContainer.Resolve(&rootResolved)

var childResolved *singletonService
childScope.Resolve(&childResolved)
Comment on lines +188 to +192

require.Same(t, rootResolved, childResolved)
})

t.Run("child first", func(t *testing.T) {
rootContainer := NewNestedContainer(nil)

// Instance 1 & 2 are singletons but overriden in each child scope so they should be different
require.NotSame(t, rootInstance, rootInstanceResolved)
require.NotSame(t, scope1Instance, scope2Instance)
// register, but not yet resolve, a singleton - we'll try to resolve it at different scopes
instance := newSingletonService()
RegisterInstance(rootContainer, instance)

childScope, err := rootContainer.NewScope()
require.NoError(t, err)

var rootResolved *singletonService
childScope.Resolve(&rootResolved)

var childResolved *singletonService
childScope.Resolve(&childResolved)

require.Same(t, rootResolved, childResolved)
})
})

t.Run("Singleton, resolved at child first", func(t *testing.T) {
rootContainer := NewNestedContainer(nil)

rootInstance := newSingletonService()
RegisterInstance(rootContainer, rootInstance)

scope1, err := rootContainer.NewScope()
require.NoError(t, err)
scope1Instance := newSingletonService()
RegisterInstance(scope1, scope1Instance)

scope2, err := rootContainer.NewScope()
require.NoError(t, err)
scope2Instance := newSingletonService()
RegisterInstance(scope2, scope2Instance)

var rootInstanceResolved *singletonService
err = rootContainer.Resolve(&rootInstanceResolved)
require.NoError(t, err)
require.NotNil(t, rootInstanceResolved)

var scope1Instance1 *singletonService
err = scope1.Resolve(&scope1Instance1)
require.NoError(t, err)
require.NotNil(t, scope1Instance1)
require.Same(t, scope1Instance, scope1Instance1)

var scope2Instance1 *singletonService
err = scope2.Resolve(&scope2Instance1)
require.NoError(t, err)
require.NotNil(t, scope2Instance1)
require.Same(t, scope2Instance, scope2Instance1)
})
}

func Test_Container_LayerEnvironmentManagerOverridesAreIsolated(t *testing.T) {
// This is a case I ran into when I was trying to make it so we could inject a new
// environment manager instance for each infra provider. Prior to the fix for this
// we'd accidentally end up mutating the _root_ environment manager instead of just the
// lower level manager.

rootContainer := NewNestedContainer(nil)
rootEnvManager := &fakeEnvironmentManager{name: "root"}
RegisterInstance[environmentManager](rootContainer, rootEnvManager)

// now we're going to register an env manager, but each one is in a new
// scope, so the root should be unaffected (there was a bug where this was
// NOT the case)
layer1Scope, err := rootContainer.NewScope()
require.NoError(t, err)
layer1EnvManager := &fakeEnvironmentManager{name: "layer-1"}
RegisterInstance[environmentManager](layer1Scope, layer1EnvManager) // override with our own env manager

layer2Scope, err := rootContainer.NewScope()
require.NoError(t, err)
layer2EnvManager := &fakeEnvironmentManager{name: "layer-2"}
RegisterInstance[environmentManager](layer2Scope, layer2EnvManager) // override with our own env manager

// okay, at this point we've created this structure in our IoC, each independent from
// each other:
//
// root container
// |-- root env manager
// |-- layer 1 scope
// | |-- layer 1 env manager
// |-- layer 2 scope
// |-- layer 2 env manager

var resolvedRoot environmentManager
require.NoError(t, rootContainer.Resolve(&resolvedRoot))
require.Same(t, rootEnvManager, resolvedRoot)

var resolvedLayer1 environmentManager
require.NoError(t, layer1Scope.Resolve(&resolvedLayer1))
require.Same(t, layer1EnvManager, resolvedLayer1)

var resolvedLayer2 environmentManager
require.NoError(t, layer2Scope.Resolve(&resolvedLayer2))
require.Same(t, layer2EnvManager, resolvedLayer2)
}

type singletonService struct {
timestamp time.Time
}
Expand Down Expand Up @@ -209,6 +327,18 @@ func newTransientService() *transientService {

// ---------- helper types for tests ----------

type environmentManager interface {
Name() string
}

type fakeEnvironmentManager struct {
name string
}

func (m *fakeEnvironmentManager) Name() string {
return m.name
}

type greeter interface {
Greet() string
}
Expand Down Expand Up @@ -849,22 +979,39 @@ func Test_DependencyChain(t *testing.T) {
func Test_NewNestedContainer_InheritsParent(t *testing.T) {
t.Parallel()

parent := NewNestedContainer(nil)
parent.MustRegisterSingleton(func() *counterService {
return &counterService{calls: 99}
t.Run("ParentAccessesFirst", func(t *testing.T) {
parent := NewNestedContainer(nil)
parent.MustRegisterSingleton(func() *counterService {
return &counterService{calls: 99}
})
child := NewNestedContainer(parent)

var parentInst *counterService
err := parent.Resolve(&parentInst)
require.NoError(t, err)

var childInst *counterService
err = child.Resolve(&childInst)
require.NoError(t, err)
require.Same(t, parentInst, childInst)
})

// Resolve in parent first to cache the singleton
var parentInst *counterService
err := parent.Resolve(&parentInst)
require.NoError(t, err)
t.Run("ChildAccessesFirst", func(t *testing.T) {
parent := NewNestedContainer(nil)
parent.MustRegisterSingleton(func() *counterService {
return &counterService{calls: 99}
})
child := NewNestedContainer(parent)

child := NewNestedContainer(parent)
var childInst *counterService
err = child.Resolve(&childInst)
require.NoError(t, err)
// Child inherits parent's cached singleton
require.Same(t, parentInst, childInst)
var childInst *counterService
err := child.Resolve(&childInst)
require.NoError(t, err)

var parentInst *counterService
err = parent.Resolve(&parentInst)
require.NoError(t, err)
require.Same(t, parentInst, childInst)
})
}

// ---------- inspectResolveError ----------
Expand Down
Loading