Initial commit of Sigil contribution. (FELIX-1142)


git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@793581 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/sigil/bld-junit/.classpath b/sigil/bld-junit/.classpath
new file mode 100644
index 0000000..d97a6bc
--- /dev/null
+++ b/sigil/bld-junit/.classpath
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.5"/>
+	<classpathentry kind="con" path="org.cauldron.sigil.core.classpathContainer"/>
+	<classpathentry kind="output" path="build/classes"/>
+</classpath>
diff --git a/sigil/bld-junit/.project b/sigil/bld-junit/.project
new file mode 100644
index 0000000..455b5c3
--- /dev/null
+++ b/sigil/bld-junit/.project
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>bld-junit</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+		<buildCommand>
+			<name>org.cauldron.sigil.core.newtonBuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+		<nature>org.cauldron.sigil.core.newtonnature</nature>
+	</natures>
+</projectDescription>
diff --git a/sigil/bld-junit/.settings/org.eclipse.jdt.core.prefs b/sigil/bld-junit/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..51d8662
--- /dev/null
+++ b/sigil/bld-junit/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,7 @@
+#Thu Feb 19 09:53:36 GMT 2009
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5
+org.eclipse.jdt.core.compiler.compliance=1.5
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.5
diff --git a/sigil/bld-junit/build.xml b/sigil/bld-junit/build.xml
new file mode 100644
index 0000000..46ce9d1
--- /dev/null
+++ b/sigil/bld-junit/build.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+
+<project name="bld-junit" default="bundle" basedir=".">
+  <import file="../bldcommon/common.xml"/>
+</project>
diff --git a/sigil/bld-junit/ivy.xml b/sigil/bld-junit/ivy.xml
new file mode 100644
index 0000000..9135f4a
--- /dev/null
+++ b/sigil/bld-junit/ivy.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<ivy-module version="1.0">
+    <info 
+        organisation="org.cauldron"
+        module="bld.junit"
+        status="integration"/>
+    <publications>
+      <artifact name="org.cauldron.sigil.junit" />
+    </publications>
+</ivy-module>
diff --git a/sigil/bld-junit/sigil.properties b/sigil/bld-junit/sigil.properties
new file mode 100644
index 0000000..c22f88c
--- /dev/null
+++ b/sigil/bld-junit/sigil.properties
@@ -0,0 +1,22 @@
+
+# sigil project file, saved by plugin.
+
+-activator: org.cauldron.sigil.junit.activator.Activator
+
+version: 0.8.0
+
+-bundles: \
+	org.cauldron.sigil.junit, \
+
+-sourcedirs: \
+	src, \
+
+-exports: \
+	org.cauldron.sigil.junit.server, \
+
+-imports: \
+	junit.framework;version=4.5.0, \
+	org.osgi.framework;version=1.4.0, \
+	org.osgi.util.tracker;version=1.3.3, \
+
+# end
diff --git a/sigil/bld-junit/src/org/cauldron/sigil/junit/AbstractSigilTestCase.java b/sigil/bld-junit/src/org/cauldron/sigil/junit/AbstractSigilTestCase.java
new file mode 100644
index 0000000..0c84937
--- /dev/null
+++ b/sigil/bld-junit/src/org/cauldron/sigil/junit/AbstractSigilTestCase.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.cauldron.sigil.junit;
+
+import java.lang.reflect.Method;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+import org.osgi.util.tracker.ServiceTrackerCustomizer;
+
+import junit.framework.TestCase;
+
+public abstract class AbstractSigilTestCase extends TestCase {
+
+	private final static List<ServiceTracker> trackers = new LinkedList<ServiceTracker>();
+	
+	private BundleContext ctx;
+	
+	public void setBundleContext(BundleContext ctx) {
+		this.ctx = ctx;
+	}
+	
+	protected BundleContext getBundleContext() {
+		return ctx;
+	}
+	
+	@Override
+	protected void setUp() {
+		for ( Class<?> c : getReferences() ) {
+			ServiceTracker t = createBindTracker(c);
+			t.open();
+			trackers.add( t );
+		}
+	}
+
+	@Override
+	protected void tearDown() {
+		for ( ServiceTracker t : trackers ) {
+			t.close();
+		}
+		trackers.clear();
+	}
+
+
+	private ServiceTracker createBindTracker(final Class<?> c) {
+		return new ServiceTracker(ctx, c.getName(), new ServiceTrackerCustomizer() {
+			public Object addingService(ServiceReference reference) {
+				Object o = ctx.getService(reference);
+				Method m = getBindMethod(c);
+				if ( m != null ) invoke( m, o );
+				return o;
+			}
+
+			public void modifiedService(ServiceReference reference,
+					Object service) {
+			}
+
+			public void removedService(ServiceReference reference,
+					Object service) {
+				Method m = getUnbindMethod(c);
+				if ( m != null ) invoke( m, service );
+				ctx.ungetService(reference);
+			}
+		});
+	}
+	
+	private void invoke(Method m, Object o) {
+		try {
+			m.invoke( this,  new Object[] { o } );
+		} catch (Exception e) {
+			throw new IllegalStateException( "Failed to invoke binding method " + m, e);
+		}
+	}
+
+	protected abstract Class<?>[] getReferences();
+	
+	protected abstract Method getBindMethod(Class<?> clazz);
+	
+	protected abstract  Method getUnbindMethod(Class<?> clazz);
+}
diff --git a/sigil/bld-junit/src/org/cauldron/sigil/junit/ReflectiveSigilTestCase.java b/sigil/bld-junit/src/org/cauldron/sigil/junit/ReflectiveSigilTestCase.java
new file mode 100644
index 0000000..2614113
--- /dev/null
+++ b/sigil/bld-junit/src/org/cauldron/sigil/junit/ReflectiveSigilTestCase.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.cauldron.sigil.junit;
+
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+public abstract class ReflectiveSigilTestCase extends AbstractSigilTestCase {
+
+	private Class<?>[] references;
+	private Map<Class<?>, Method> bindMethods;
+	private Map<Class<?>, Method> unbindMethods;
+	
+	@Override
+	protected Class<?>[] getReferences() {
+		introspect();
+		return references;
+	}
+
+	@Override
+	protected Method getBindMethod(Class<?> clazz) {
+		return bindMethods.get(clazz);
+	}
+
+	@Override
+	protected Method getUnbindMethod(Class<?> clazz) {
+		return unbindMethods.get(clazz);
+	}
+
+	private void introspect() {
+		if ( references == null ) {
+			bindMethods = findBindMethods(getClass(), "set", "add");
+			unbindMethods = findBindMethods(getClass(), "set", "remove");
+			
+			HashSet<Class<?>> refs = new HashSet<Class<?>>();
+			refs.addAll( bindMethods.keySet() );
+			refs.addAll( unbindMethods.keySet() );
+			references = refs.toArray( new Class<?>[refs.size()] );
+		}
+	}
+
+	private static Map<Class<?>, Method> findBindMethods(Class<?> clazz, String... prefix) {
+		HashMap<Class<?>, Method> found = new HashMap<Class<?>, Method>();
+		
+		checkDeclaredMethods(clazz, found, prefix);
+		
+		return found;
+	}
+	
+	private static void checkDeclaredMethods(Class<?> clazz, Map<Class<?>, Method> found, String...prefix) {
+		for ( Method m : clazz.getDeclaredMethods() ) {
+			if ( isMethodPrefixed(m, prefix) && isBindSignature(m) ) {
+				found.put( m.getParameterTypes()[0], m );
+			}
+		}
+		
+		Class<?> sup = clazz.getSuperclass();
+		if ( sup != null && sup != Object.class ) {
+			checkDeclaredMethods(sup, found, prefix);
+		}
+		
+		for ( Class<?> i : clazz.getInterfaces() ) {
+			checkDeclaredMethods(i, found, prefix);
+		}
+	}
+
+	private static boolean isMethodPrefixed(Method m, String...prefix) {
+		String n = m.getName();
+		for ( String p : prefix ) {
+			if ( n.startsWith( p ) && n.length() > p.length() ) {
+				return true;
+			}
+		}
+		return false;
+	}
+	private static boolean isBindSignature(Method m) {
+		return m.getReturnType() == Void.TYPE && m.getParameterTypes().length == 1;
+	}
+}
diff --git a/sigil/bld-junit/src/org/cauldron/sigil/junit/activator/Activator.java b/sigil/bld-junit/src/org/cauldron/sigil/junit/activator/Activator.java
new file mode 100644
index 0000000..b22920b
--- /dev/null
+++ b/sigil/bld-junit/src/org/cauldron/sigil/junit/activator/Activator.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.cauldron.sigil.junit.activator;
+
+import org.cauldron.sigil.junit.server.JUnitService;
+import org.cauldron.sigil.junit.server.impl.JUnitServiceFactory;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+
+/**
+ * @author dave
+ */
+public class Activator implements BundleActivator {
+	private ServiceRegistration reg;
+	private JUnitServiceFactory service;
+
+	public void start(final BundleContext ctx) {
+		service = new JUnitServiceFactory();
+		service.start(ctx);
+		reg = ctx.registerService(JUnitService.class.getName(), service, null);
+    }
+
+    public void stop(BundleContext ctx) {
+    	reg.unregister();
+    	reg = null;
+    	service.stop(ctx);
+    	service = null;
+    }
+}
diff --git a/sigil/bld-junit/src/org/cauldron/sigil/junit/server/JUnitService.java b/sigil/bld-junit/src/org/cauldron/sigil/junit/server/JUnitService.java
new file mode 100644
index 0000000..e111b26
--- /dev/null
+++ b/sigil/bld-junit/src/org/cauldron/sigil/junit/server/JUnitService.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.cauldron.sigil.junit.server;
+
+import java.util.Set;
+
+import org.osgi.framework.BundleContext;
+
+import junit.framework.TestSuite;
+
+public interface JUnitService {
+	Set<String> getTests();
+	
+	TestSuite createTest(String test);
+	
+	TestSuite createTest(String test, BundleContext ctx);
+}
\ No newline at end of file
diff --git a/sigil/bld-junit/src/org/cauldron/sigil/junit/server/impl/JUnitServiceFactory.java b/sigil/bld-junit/src/org/cauldron/sigil/junit/server/impl/JUnitServiceFactory.java
new file mode 100644
index 0000000..49fadf6
--- /dev/null
+++ b/sigil/bld-junit/src/org/cauldron/sigil/junit/server/impl/JUnitServiceFactory.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.cauldron.sigil.junit.server.impl;
+
+import java.util.HashMap;
+import java.util.Set;
+import java.util.TreeSet;
+
+import junit.framework.TestCase;
+import junit.framework.TestSuite;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceFactory;
+import org.osgi.framework.ServiceRegistration;
+
+public class JUnitServiceFactory implements ServiceFactory {
+
+	private HashMap<String, Class<? extends TestCase>> tests = new HashMap<String, Class<? extends TestCase>>();
+	private TestClassListener listener;
+	
+	public void start(BundleContext ctx) {
+		listener = new TestClassListener(this);
+		ctx.addBundleListener(listener);
+		//listener.index(ctx.getBundle());
+		for ( Bundle b : ctx.getBundles() ) {
+			if ( b.getState() == Bundle.RESOLVED ) {
+				listener.index(b);
+			}
+		}
+	}
+	
+	public void stop(BundleContext ctx) {
+		ctx.removeBundleListener(listener);
+		listener = null;
+	}
+	
+	public Object getService(Bundle bundle, ServiceRegistration reg) {
+		return new JUnitServiceImpl(this, bundle.getBundleContext());
+	}
+
+	public void ungetService(Bundle bundle, ServiceRegistration reg, Object service) {
+	}
+
+	public void registerTest(Class<? extends TestCase> clazz) {
+		tests.put( clazz.getName(), clazz );
+	}
+
+	public void unregister(Class<? extends TestCase> clazz) {
+		tests.remove(clazz.getName());
+	}
+
+	public Set<String> getTests() {
+		return new TreeSet<String>(tests.keySet());
+	}
+
+	public TestSuite getTest(String test) {
+		Class<? extends TestCase> tc = tests.get(test);
+		return tc == null ? null : new TestSuite(tc);
+	}
+
+}
diff --git a/sigil/bld-junit/src/org/cauldron/sigil/junit/server/impl/JUnitServiceImpl.java b/sigil/bld-junit/src/org/cauldron/sigil/junit/server/impl/JUnitServiceImpl.java
new file mode 100644
index 0000000..c549cea
--- /dev/null
+++ b/sigil/bld-junit/src/org/cauldron/sigil/junit/server/impl/JUnitServiceImpl.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.cauldron.sigil.junit.server.impl;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import junit.framework.Test;
+import junit.framework.TestResult;
+import junit.framework.TestSuite;
+
+import org.cauldron.sigil.junit.server.JUnitService;
+import org.osgi.framework.BundleContext;
+
+public class JUnitServiceImpl implements JUnitService {
+	
+	private static final Logger log = Logger.getLogger(JUnitServiceImpl.class.getName());
+
+	private static final Class<?>[] BUNDLE_CONTEXT_PARAMS = new Class[] { BundleContext.class };
+
+	private final JUnitServiceFactory junitServiceFactory;
+	private final BundleContext bundleContext;
+
+	public JUnitServiceImpl(JUnitServiceFactory junitServiceFactory, BundleContext bundleContext) {
+		this.junitServiceFactory = junitServiceFactory;
+		this.bundleContext = bundleContext;
+	}
+
+	public Set<String> getTests() {
+		return junitServiceFactory.getTests();
+	}
+	
+	public TestSuite createTest(String test) {
+		return createTest(test, null);
+	}
+	
+	public TestSuite createTest(String test, BundleContext ctx) {
+		try {
+			TestSuite ts = junitServiceFactory.getTest(test);
+			
+			if ( ts == null ) return null;
+			
+			TestSuite ret = new TestSuite(ts.getName());
+			
+			Enumeration<Test> e = ts.tests();
+			
+			while ( e.hasMoreElements() ) {
+				Test t = e.nextElement();
+				setContext(t, ctx);
+				ret.addTest(t);
+			}
+			
+			return ret;
+		}
+		catch (final NoClassDefFoundError e) {
+			TestSuite s = new TestSuite(test);
+			s.addTest( new Test() {
+				public int countTestCases() {
+					return 1;
+				}
+
+				public void run(TestResult result) {
+					result.addError(this, e);
+				}
+			});
+			return s;
+		}
+		catch (final RuntimeException e) {
+			TestSuite s = new TestSuite(test);
+			s.addTest( new Test() {
+				public int countTestCases() {
+					return 1;
+				}
+
+				public void run(TestResult result) {
+					result.addError(this, e);
+				}
+				
+			});
+			return s;
+		}
+	}
+
+	private void setContext(Test t, BundleContext ctx) {
+		try {
+			Method m = findMethod( t.getClass(), "setBundleContext", BUNDLE_CONTEXT_PARAMS );
+			if ( m != null )
+				m.invoke(t, ctx == null ? bundleContext : ctx );
+		} catch (SecurityException e) {
+			log.log( Level.WARNING, "Failed to set bundle context on " + t, e);
+		} catch (IllegalArgumentException e) {
+			log.log( Level.WARNING, "Failed to set bundle context on " + t, e);
+		} catch (IllegalAccessException e) {
+			log.log( Level.WARNING, "Failed to set bundle context on " + t, e);
+		} catch (InvocationTargetException e) {
+			log.log( Level.WARNING, "Failed to set bundle context on " + t, e);
+		}
+	}
+
+	private Method findMethod(Class<?> clazz, String name,
+			Class<?>[] params) {
+		Method found = null;
+		
+		for ( Method m : clazz.getDeclaredMethods() ) {
+			if ( m.getName().equals(name) && Arrays.deepEquals(m.getParameterTypes(), params) ) {
+				found = m;
+				break;
+			}
+		}
+		
+		if ( found == null ) {
+			Class<?> c = clazz.getSuperclass();
+			
+			if ( c != null && c != Object.class ) {
+				found = findMethod(c, name, params);
+			}
+		}
+		
+		if ( found == null ) {
+			for ( Class<?> c : clazz.getInterfaces() ) {
+				found = findMethod(c, name, params);
+				if ( found != null ) {
+					break;
+				}
+			}
+		}
+		
+		return found;
+	}
+}
diff --git a/sigil/bld-junit/src/org/cauldron/sigil/junit/server/impl/TestClassListener.java b/sigil/bld-junit/src/org/cauldron/sigil/junit/server/impl/TestClassListener.java
new file mode 100644
index 0000000..c3db3f9
--- /dev/null
+++ b/sigil/bld-junit/src/org/cauldron/sigil/junit/server/impl/TestClassListener.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.cauldron.sigil.junit.server.impl;
+
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import junit.framework.TestCase;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleEvent;
+import org.osgi.framework.SynchronousBundleListener;
+
+public class TestClassListener implements SynchronousBundleListener {
+	private static final Logger log = Logger.getLogger(TestClassListener.class.getName());
+	
+	private final JUnitServiceFactory service;
+	
+	private HashMap<Long, Class<TestCase>[]> registrations = new HashMap<Long, Class<TestCase>[]>(); 
+	
+	public TestClassListener(JUnitServiceFactory service) {
+		this.service = service;
+	}
+	
+	public void bundleChanged(BundleEvent event) {
+		switch( event.getType() ) {
+		case BundleEvent.RESOLVED:
+			index( event.getBundle() );
+			break;
+		case BundleEvent.UNRESOLVED:
+			unindex( event.getBundle() );
+			break;
+		}
+	}
+
+	void index(Bundle bundle) {
+		if ( isTestBundle( bundle ) ) {
+			List<String> tests = findTests( bundle );
+			
+			if ( !tests.isEmpty() ) {
+				LinkedList<Class<? extends TestCase>> regs = new LinkedList<Class<? extends TestCase>>();
+				
+				for ( String jc : tests ) {
+					try {
+						Class<?> clazz = bundle.loadClass(jc);
+						if ( isTestCase(clazz) ) {
+							Class<? extends TestCase> tc = clazz.asSubclass(TestCase.class);
+							regs.add( tc );
+							service.registerTest(tc);
+						}
+					} catch (ClassNotFoundException e) {
+						log.log( Level.WARNING, "Failed to load class " + jc, e );
+					} catch (NoClassDefFoundError e) {
+						log.log( Level.WARNING, "Failed to load class " + jc, e );
+					}
+				}
+				
+				registrations.put( bundle.getBundleId(), toArray(regs) );
+			}
+		}
+	}
+
+	private boolean isTestBundle(Bundle bundle) {
+		try {
+			bundle.loadClass(TestCase.class.getName());
+			return true;
+		} catch (ClassNotFoundException e) {
+			return false;
+		}
+	}
+
+	@SuppressWarnings("unchecked")
+	private Class<TestCase>[] toArray(LinkedList<Class<? extends TestCase>> regs) {
+		return regs.toArray( new Class[regs.size()] );
+	}
+
+	private boolean isTestCase(Class<?> clazz) {
+		return 
+			TestCase.class.isAssignableFrom(clazz) && 
+			!Modifier.isAbstract(clazz.getModifiers()) && 
+			!clazz.getPackage().getName().startsWith( "junit" );
+	}
+
+	void unindex(Bundle bundle) {
+		Class<TestCase>[] classes = registrations.remove(bundle.getBundleId());
+		if ( classes != null ) {
+			for ( Class<TestCase> tc : classes ) {
+				service.unregister(tc);
+			}
+		}
+	}
+	
+	private List<String> findTests(Bundle bundle) {
+		@SuppressWarnings("unchecked") Enumeration<URL> urls = bundle.findEntries("", "*.class", true);
+		
+		LinkedList<String> tests = new LinkedList<String>();
+		while( urls.hasMoreElements() ) { 
+			URL url = urls.nextElement();
+			tests.add( toClassName( url ) );
+		}
+		
+		return tests;
+	}
+
+	private String toClassName(URL url) {
+		String f = url.getFile();
+		String cn = f.substring(1, f.length() - 6 );
+		return cn.replace('/', '.');
+	}
+
+}