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('/', '.');
+ }
+
+}