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
9 changes: 6 additions & 3 deletions system/ioc/Injector.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ component serializable="false" accessors="true" {
variables.childInjectors[ arguments.name ].shutdown( this );
return structDelete( variables.childInjectors, arguments.name );
}
return falase;
return false;
}

/**
Expand Down Expand Up @@ -440,8 +440,11 @@ component serializable="false" accessors="true" {
} );
}

// standalone cachebox? Yes, then shut it down baby!
if ( isCacheBoxLinked() ) {
// Shutdown CacheBox only if not in ColdBox Mode.
// In ColdBox Mode the application owns the CacheBox lifecycle and shuts it down
// itself via LoaderService.processShutdown(). A child injector shutting it down
// here would unregister every cache for the whole application.
if ( !isColdBoxLinked() && isCacheBoxLinked() ) {
variables.cacheBox.shutdown( this );
}

Expand Down
19 changes: 14 additions & 5 deletions system/web/services/ModuleService.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -542,16 +542,25 @@ component extends="coldbox.system.web.services.BaseService" accessors="true" {
thisChild & "/ModuleConfig.bx"
)
) {
// add to parent children
arrayAppend( mConfig.childModules, childname )
// A module with this name may already be known (e.g. a top-level copy)
var childWasKnown = structKeyExists( variables.moduleRegistry, childName )
// register child
registerModule(
var childRegistered = registerModule(
moduleName : childName,
invocationPath: mConfig.invocationPath & "." & thisInceptionPath,
parent : modName,
bundle : arguments.bundle,
parentInjector: moduleConfigAndInjector.injector
)
/*
* Track the child when it registered now, or when a module with its name was
* already known, so the activation cascade still reaches it as before.
* A child that refused registration (it is disabled) must not be tracked:
* activating it later throws IllegalModuleState.
*/
if ( childRegistered || childWasKnown ) {
arrayAppend( mConfig.childModules, childname )
}
} else if ( variables.logger.canInfo() ) {
variables.logger.info(
"> Inception Module #childName# does not have a valid ModuleConfig.cfc in its root, so skipping registration"
Expand Down Expand Up @@ -629,8 +638,8 @@ component extends="coldbox.system.web.services.BaseService" accessors="true" {
var sTime = getTickCount()
var modules = variables.registeredModules

// If module not registered, throw exception
if ( isNull( modules[ arguments.moduleName ] ) ) {
// If module not registered, throw exception. structKeyExists() because Adobe throws on isNull() of a missing key
if ( !structKeyExists( modules, arguments.moduleName ) ) {
throw(
message: "Cannot activate module (#arguments.moduleName#) as it has not been registered.",
detail : "Registered modules are #modules.keyList()#",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Fixture: a parent module with one nested (inception) child module that is disabled.
* Used to prove the activation cascade still works when a child never registers.
*/
component {

// Module Properties
this.title = "disabledChildParent";
this.version = "1.0.0";

function configure(){
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* A disabled nested module. Registration returns false, so the parent must not
* list it as a child module nor attempt to activate it.
*/
component {

// Module Properties
this.title = "disabledChild";
this.version = "1.0.0";
this.disabled = true;

function configure(){
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Fixture: a parent module that lists its own disabled nested child in this.dependencies.
* Used to prove a declared dependency that is disabled still stops activation.
*/
component {

// Module Properties
this.title = "disabledDependencyParent";
this.version = "1.0.0";
this.dependencies = [ "disabledDependency" ];

function configure(){
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* A disabled nested module that its parent declares as a dependency.
* Registration returns false, so activating the parent must fail.
*/
component {

// Module Properties
this.title = "disabledDependency";
this.version = "1.0.0";
this.disabled = true;

function configure(){
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Fixture: a parent module whose nested child has the same name as a module
* that is already known to the module registry (like a top-level copy).
*/
component {

// Module Properties
this.title = "sharedChildParent";
this.version = "1.0.0";

function configure(){
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* A nested module whose name is already known before its parent registers.
* The parent must still track it, so the parent's activation cascade activates it.
*/
component {

// Module Properties
this.title = "sharedChild";
this.version = "1.0.0";

function configure(){
}

}
32 changes: 32 additions & 0 deletions tests/specs/ioc/InjectorTest.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -261,4 +261,36 @@
expect( injector.removeChildInjector( "alexia" ) ).toBeTrue();
}

function testRemoveMissingChildInjectorReturnsFalse(){
expect( injector.hasChildInjector( "neverRegistered" ) ).toBeFalse();
// Must return false, not blow up on an undefined variable
expect( injector.removeChildInjector( "neverRegistered" ) ).toBeFalse();
}

function testShutdownLeavesCacheBoxAloneInColdBoxMode(){
// mocks
parent = createStub().$( "shutdown", injector );
cachebox = createStub().$( "shutdown" );
eventManager = createStub().$( "announce" );

injector.setParent( parent );
injector
.$property( "cachebox", "variables", cachebox )
.$property( "eventManager", "variables", eventManager )
.$( "isCacheBoxLinked", true )
.$( "isColdBoxLinked", true )
.$(
method = "removeFromScope",
returns = injector,
preserveReturnType = false
);
mockLogger.$( "canInfo", true ).$( "info" );

injector.shutdown();

// The application owns CacheBox in ColdBox mode, so the injector must not touch it
assertTrue( cacheBox.$never( "shutdown" ) );
assertTrue( injector.$once( "removeFromScope" ) );
}

}
103 changes: 103 additions & 0 deletions tests/specs/web/services/ModuleServiceInceptionTest.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* Covers how a parent module tracks its nested (inception) child modules when a
* child fails to register. A disabled child must not be tracked, otherwise the
* activation cascade throws IllegalModuleState. A child whose name was already
* known must still be tracked, and a disabled child the parent declares in
* this.dependencies must still stop activation.
*/
component extends="tests.resources.BaseIntegrationTest" {

function beforeAll(){
super.beforeAll();
variables.moduleService = getController().getModuleService();
variables.fixtureRoot = "tests.resources.inceptionfixtures";
}

function run(){
describe( "Nested (inception) module registration", function(){
beforeEach( function( currentSpec ){
// Setup as a new ColdBox request, VERY IMPORTANT. ELSE EVERYTHING LOOKS LIKE THE SAME REQUEST.
setup();
// Clean even if a previous spec failed mid-flight
cleanupFixtures();
} );

afterEach( function( currentSpec ){
cleanupFixtures();
} );

it( "does not track a disabled nested child and still activates the parent", function(){
variables.moduleService.registerAndActivateModule( "disabledChildParent", variables.fixtureRoot );

var modules = getController().getSetting( "modules" );

expect( modules[ "disabledChildParent" ].activated ).toBeTrue();
// The disabled child never registered, so the parent must not claim it
expect( modules[ "disabledChildParent" ].childModules ).notToInclude( "disabledChild" );
expect( modules ).notToHaveKey( "disabledChild" );
} );

it( "still fails when the parent lists its disabled nested child in this.dependencies", function(){
expect( function(){
variables.moduleService.registerAndActivateModule(
"disabledDependencyParent",
variables.fixtureRoot
);
} ).toThrow( "IllegalModuleState" );
} );

it( "tracks a nested child whose name is already known to the registry", function(){
var ms = variables.moduleService;
// Simulate a top-level copy: registerAllModules() fills the registry before registering anything
var knownInvocation = variables.fixtureRoot & ".sharedChildParent.modules";
var knownLocation = "/" & replace( knownInvocation, ".", "/", "all" );
ms.getModuleRegistry()[ "sharedChild" ] = {
locationPath : knownLocation,
physicalPath : expandPath( knownLocation ),
invocationPath : knownInvocation
};

ms.registerModule( "sharedChildParent", variables.fixtureRoot );
// The known copy registers on its own, as registerAllModules() would
ms.registerModule( "sharedChild" );
ms.activateModule( "sharedChildParent" );

var modules = getController().getSetting( "modules" );

expect( modules[ "sharedChildParent" ].childModules ).toInclude( "sharedChild" );
// Only the parent's activation cascade activates it
expect( modules[ "sharedChild" ].activated ).toBeTrue();
} );
} );
}

/*********************************** HELPERS ***********************************/

/**
* Removes every fixture module from the live test app so each spec starts clean.
* unload() does not touch the module registry, so we clean that explicitly.
*/
private function cleanupFixtures(){
var ms = variables.moduleService;
var registry = ms.getModuleRegistry();

// Children first, then parents
for (
var moduleName in [
"disabledChild",
"disabledDependency",
"sharedChild",
"disabledChildParent",
"disabledDependencyParent",
"sharedChildParent"
]
) {
if ( ms.isModuleRegistered( moduleName ) ) {
ms.unload( moduleName );
}
// The registry keeps entries after unload; remove so re-registration is possible
structDelete( registry, moduleName );
}
}

}
Loading