package org.sakaiproject.util;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import junit.framework.TestCase;
import org.sakaiproject.component.impl.SpringCompMgr;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.sakaiproject.component.api.ComponentManager;
import org.sakaiproject.util.SakaiApplicationContext;
/**
* Verifies behaviors of {@link ComponentsLoader}.
*
* @author dmccallum@unicon.net
*
*/
public class ComponentsLoaderTest extends TestCase {
/** the primary SUT */
private ComponentsLoader loader;
/** a helper for generating component dir layouts (and compiled code!) */
private ComponentBuilder builder;
/** Current ComponentsLoader impl obligates us to pass a SpringCompMgr,
* and we have to specify the app context explicitly b/c that field is
* normally set by init() which does _far_ too much work for our purposes.
*/
private SpringCompMgr componentMgr;
@Override
protected void setUp() throws Exception {
loader = new ComponentsLoader();
builder = new ComponentBuilder();
componentMgr = new SpringCompMgr(null) {{
m_ac = new SakaiApplicationContext();
}};
super.setUp();
}
/**
* Verifies that a single, dynamically generated Sakai component can
* be properly digested and registered with a given <code>ComponentManager</code>
* such that a bean who's implementation is known only to that component
* can be subsequently retrieved.
*
* <p>In reality, "registering with a given <code>ComponentManager</code>" actually
* means "registering with a given <code>ComponentManager's</code> underlying
* <code>ApplicationContext</code>". This particular test happens to use
* a "real" <code>ApplicationContext</code> instance for this purpose. This
* was deemed less fragile than mocking that interface since the mock would
* require knowledge of <code>BeanDefinitionReader</code> and
* <code>ApplicationContext</code> interactions, which are certainly out-of-scope
* for this test case. See <a href="http://xunitpatterns.com/Fragile%20Test.html#Overspecified%20Software">Overspecified Software</a></p>
*
* <p>Part of component registration typically involves making the component's
* classes visible to whatever <code>ClassLoader</code> materializes the component's
* Spring beans. Theoretically, then, just completing the load operation should be
* verification that a component-specific <code>ClassLoader</code> was used properly.
* However, because we cannot know (at least not given the current
* <code>ComponentsLoader</code> impl) whether or not a bean <code>ClassLoader</code>
* is specified when initializing the bean def reader, we assert on our ability
* to actually retrieve a bean from the <code>ApplicationContext</code> we passed
* (indirectly) to <code>load()</code>. We rely on the <code>ComponentBuilder</code>
* to guarantee that the retrieved bean's implementation can only be known to the
* component <code>ClassLoader</code>.</p>
*
* <p>Note that this test does not include verification that the component's
* <code>ClassLoader</code>'s parent is the <code>ComponentLoader's</code>
* <code>ClassLoader</code>. This is tested directly in more fine-grained BDD tests.</p>
*
*/
public void testLoadRegistersComponentWithComponentManager() {
if ( !(builder.isUseable()) ) {
sayUnusableBuilder("testLoadRegistersComponentWithComponentManager()");
return;
}
Component component = builder.buildComponent();
loader.load(componentMgr.getApplicationContext(), builder.getComponentsRootDir().getAbsolutePath());
// we are not interested in testing SpringCompMgr, but we can assume the underlying
// Spring context is a fully tested, known quantity. Hence the getBean() call
// (also for reasons outlined in the javadoc)
assertNotNull(componentMgr.getApplicationContext().getBean(component.getBeanId()));
}
/**
* Same as {@link #testLoadRegistersComponentWithComponentManager()} but for
* several components. The intent here is to (hopefully) distinguish clearly
* between failures related to loading any given component and failures related
* to the algorithm for walking the entire root components dir.
*/
public void testLoadRegistersMultipleComponentsWithComponentManager() {
if ( !(builder.isUseable()) ) {
sayUnusableBuilder("testLoadRegistersMultipleComponentsWithComponentManager()");
return;
}
Component component1 = builder.buildComponent();
Component component2 = builder.buildComponent();
loader.load(componentMgr.getApplicationContext(), builder.getComponentsRootDir().getAbsolutePath());
assertNotNull(componentMgr.getApplicationContext().getBean(component1.getBeanId()));
assertNotNull(componentMgr.getApplicationContext().getBean(component2.getBeanId()));
}
/**
* Verifies that the current thread's context class loader
* ({@link Thread#getContextClassLoader()}) is only temporarily
* replaced by the class loader returned from
* {@link ComponentsLoader#newPackageClassLoader(java.io.File)} when
* processing a call to
* {@link ComponentsLoader#loadComponentPackage(java.io.File, org.springframework.context.ConfigurableApplicationContext)}.
*
* <p>Unfortunately, given the current implementation, we cannot actually
* test that the loader returned from {@link ComponentsLoader#newPackageClassLoader(File)}
* is in fact ever assigned as the current context loader, but we have to
* assume the entire implementation is working properly if all the other
* tests in this class are passing,
* {@link #testLoadRegistersMultipleComponentsWithComponentManager()}
* in particular. This test is still necessary, though, to verify that
* the current thread is still in the expected state after components
* have been loaded.</p>
*/
public void testSetsAndUnsetsPackageClassLoaderAsThreadContextClassLoader() {
if ( !(builder.isUseable()) ) {
sayUnusableBuilder("testSetsAndUnsetsPackageClassLoaderAsThreadContextClassLoader()");
return;
}
builder.buildComponent();
ClassLoader existingContextClassLoader =
Thread.currentThread().getContextClassLoader();
loader.load(componentMgr.getApplicationContext(), builder.getComponentsRootDir().getAbsolutePath());
assertSame("Should have preserved existing context class loader after components load completed",
existingContextClassLoader,
Thread.currentThread().getContextClassLoader());
}
/**
* Verifies that {@link ComponentsLoader#load(ComponentManager, String)}
* dispatches internally in the expected fashion. This enables more
* direct testing of special implementations of those delegated-to
* methods because it guarantees that the internal "protected"
* contract of that class is respected. For example, were this
* test to be deleted, one may override
* {@link ComponentsLoader#newPackageClassLoader(File)} only to
* be surprised when the override is never invoked, even if all
* other black box tests in this test case were to succeed.
*/
public void testLoadDispatch() {
if ( !(builder.isUseable()) ) {
sayUnusableBuilder("testLoadDispatch()");
return;
}
List<String> expectedJournal = new ArrayList<String>() {{
add("validComponentsPackage");
add("loadComponentPackage");
add("newPackageClassLoader");
}};
final Component component = builder.buildComponent();
final File expectedDir = new File(component.getDir());
final List<String> journal = new ArrayList<String>();
// a poor-man's mock, here
loader = new ComponentsLoader() {
protected boolean validComponentsPackage(File dir) {
assertEquals(expectedDir, dir);
journal.add("validComponentsPackage");
return super.validComponentsPackage(dir);
}
protected ClassLoader newPackageClassLoader(File dir) {
assertEquals(expectedDir, dir);
journal.add("newPackageClassLoader");
return super.newPackageClassLoader(dir);
}
protected void loadComponentPackage(File dir, ConfigurableApplicationContext ac) {
assertEquals(expectedDir, dir);
assertNotNull(ac);
journal.add("loadComponentPackage");
super.loadComponentPackage(dir, ac);
}
};
loader.load(componentMgr.getApplicationContext(), builder.getComponentsRootDir().getAbsolutePath());
assertEquals("Did not invoke delegate methods in the expected order",
expectedJournal, journal);
}
/**
* Similar to {@link #testLoadDispatch()} but verifies internal
* dispatch to protected methods from
* {@link ComponentsLoader#loadComponentPackage(File, ConfigurableApplicationContext)},
* which the former test is unable to validate directly.
*/
public void testLoadComponentPackageDispatch() {
if ( !(builder.isUseable()) ) {
sayUnusableBuilder("testLoadComponentPackageDispatch()");
return;
}
// overkill for our needs, but such is life with anon inner classes
List<String> expectedJournal = new ArrayList<String>() {{
add("newPackageClassLoader");
}};
final Component component = builder.buildComponent();
final File expectedDir = new File(component.getDir());
final List<String> journal = new ArrayList<String>();
// a poor-man's mock, here
loader = new ComponentsLoader() {
protected ClassLoader newPackageClassLoader(File dir) {
assertEquals(expectedDir, dir);
journal.add("newPackageClassLoader");
return super.newPackageClassLoader(dir);
}
};
loader.loadComponentPackage(new File(component.getDir()),
componentMgr.getApplicationContext());
assertEquals("Did not invoke newPackageClassLoader()",
expectedJournal, journal);
}
private void sayUnusableBuilder(String invokingMethod) {
System.out.println("Unable to execute " + invokingMethod +", probably b/c necessary code generation tools are not available. Please see http://maven.apache.org/general.html#tools-jar-dependency for information on making tools.jar visible in the Maven classpaths.");
}
@Override
protected void tearDown() throws Exception {
builder.tearDown();
componentMgr.close();
super.tearDown();
}
}