package aQute.junit;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.regex.Pattern;
import org.junit.runner.Description;
import org.junit.runner.manipulation.NoTestsRemainException;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.Constants;
import org.osgi.framework.SynchronousBundleListener;
import org.osgi.framework.Version;
import org.osgi.service.packageadmin.PackageAdmin;
import org.osgi.util.tracker.ServiceTracker;
import aQute.junit.constants.TesterConstants;
import junit.framework.JUnit4TestAdapter;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestResult;
import junit.framework.TestSuite;
public class Activator implements BundleActivator, TesterConstants, Runnable {
BundleContext context;
volatile boolean active;
int port = -1;
boolean continuous = false;
boolean trace = false;
PrintStream out = System.err;
JUnitEclipseReport jUnitEclipseReport;
volatile Thread thread;
ServiceTracker packageAdminTracker;
public Activator() {}
public void start(BundleContext context) throws Exception {
this.context = context;
this.packageAdminTracker = new ServiceTracker(context, PackageAdmin.class.getName(), null);
this.packageAdminTracker.open();
active = true;
if (!Boolean.valueOf(context.getProperty(TESTER_SEPARATETHREAD))
&& Boolean.valueOf(context.getProperty("launch.services"))) { // can't
// register
// services
// on
// mini
// framework
Hashtable<String,String> ht = new Hashtable<String,String>();
ht.put("main.thread", "true");
ht.put(Constants.SERVICE_DESCRIPTION, "JUnit tester");
context.registerService(Runnable.class.getName(), this, ht);
} else {
thread = new Thread(this, "bnd Runtime Test Bundle");
thread.start();
}
}
public void stop(BundleContext context) throws Exception {
this.packageAdminTracker.close();
active = false;
if (jUnitEclipseReport != null)
jUnitEclipseReport.close();
if (thread != null) {
thread.interrupt();
thread.join(10000);
}
}
public boolean active() {
return active;
}
public void run() {
continuous = Boolean.valueOf(context.getProperty(TESTER_CONTINUOUS));
trace = context.getProperty(TESTER_TRACE) != null;
if (thread == null)
trace("running in main thread");
// We can be started on our own thread or from the main code
thread = Thread.currentThread();
String testcases = context.getProperty(TESTER_NAMES);
trace("test cases %s", testcases);
if (context.getProperty(TESTER_PORT) != null) {
port = Integer.parseInt(context.getProperty(TESTER_PORT));
try {
trace("using port %s", port);
jUnitEclipseReport = new JUnitEclipseReport(port);
} catch (Exception e) {
System.err.println("Cannot create link Eclipse JUnit on port " + port);
System.exit(-2);
}
}
String testerDir = context.getProperty(TESTER_DIR);
if (testerDir == null)
testerDir = "testdir";
final File reportDir = new File(testerDir);
//
// Jenkins does not detect test failures unless reported
// by JUnit XML output. If we have an unresolved failure
// we timeout. The following will test if there are any
// unresolveds and report this as a JUnit failure. It can
// be disabled with -testunresolved=false
//
String unresolved = context.getProperty(TESTER_UNRESOLVED);
trace("run unresolved %s", unresolved);
if (unresolved == null || unresolved.equalsIgnoreCase("true")) {
//
// Check if there are any unresolved bundles.
// If yes, we run a test case to get a proper JUnit report
//
Bundle testBundle = null;
for (Bundle b : context.getBundles()) {
if (b.getState() == Bundle.INSTALLED) {
testBundle = b;
break;
}
}
if (testBundle != null) {
for (Bundle b : context.getBundles()) {
String testcasesheader = (String) b.getHeaders().get(aQute.bnd.osgi.Constants.TESTCASES);
if (testcasesheader != null) {
testBundle = b;
break;
}
}
int err = 0;
try {
err = test(context.getBundle(), "aQute.junit.UnresolvedTester",
getReportWriter(reportDir, testBundle));
} catch (IOException e) {
// ignore
}
if (err != 0) {
System.exit(err);
}
}
}
if (testcases == null) {
// if ( !continuous) {
// System.err.println("\nThe -testcontinuous property must be set if
// invoked without arguments\n");
// System.exit(-1);
// }
trace("automatic testing of all bundles with " + aQute.bnd.osgi.Constants.TESTCASES + " header");
try {
automatic(reportDir);
} catch (IOException e) {
// ignore
}
} else {
trace("receivednames of classes to test %s", testcases);
try {
int errors = test(null, testcases, null);
System.exit(errors);
} catch (Exception e) {
e.printStackTrace();
System.exit(-2);
}
}
}
void automatic(File reportDir) throws IOException {
final List<Bundle> queue = new Vector<Bundle>();
if (!reportDir.exists() && !reportDir.mkdirs()) {
throw new IOException("Could not create directory " + reportDir);
}
trace("using %s", reportDir);
trace("adding Bundle Listener for getting test bundle events");
context.addBundleListener(new SynchronousBundleListener() {
public void bundleChanged(BundleEvent event) {
if (event.getType() == BundleEvent.STARTED) {
checkBundle(queue, event.getBundle());
}
}
});
for (Bundle b : context.getBundles()) {
checkBundle(queue, b);
}
trace("starting queue");
int result = 0;
outer: while (active) {
Bundle bundle;
synchronized (queue) {
while (queue.isEmpty() && active) {
try {
queue.wait();
} catch (InterruptedException e) {
trace("tests bundle queue interrupted");
thread.interrupt();
break outer;
}
}
}
try {
bundle = queue.remove(0);
trace("received bundle to test: %s", bundle.getLocation());
try (Writer report = getReportWriter(reportDir, bundle)) {
trace("test will run");
result += test(bundle, (String) bundle.getHeaders().get(aQute.bnd.osgi.Constants.TESTCASES),
report);
trace("test ran");
if (queue.isEmpty() && !continuous) {
trace("queue " + queue);
System.exit(result);
}
}
} catch (Exception e) {
error("Not sure what happened anymore %s", e);
System.exit(-2);
}
}
}
void checkBundle(List<Bundle> queue, Bundle bundle) {
if (bundle.getState() == Bundle.ACTIVE) {
String testcases = (String) bundle.getHeaders().get(aQute.bnd.osgi.Constants.TESTCASES);
if (testcases != null) {
trace("found active bundle with test cases %s : %s", bundle, testcases);
synchronized (queue) {
queue.add(bundle);
queue.notifyAll();
}
}
}
}
private Writer getReportWriter(File reportDir, Bundle bundle) throws IOException {
if (reportDir.isDirectory()) {
Version v = bundle.getVersion();
File f = new File(reportDir, "TEST-" + bundle.getSymbolicName() + "-" + v.getMajor() + "." + v.getMinor()
+ "." + v.getMicro() + ".xml");
Writer writer = new OutputStreamWriter(Files.newOutputStream(f.toPath()), UTF_8);
writer.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
return writer;
}
return null;
}
/**
* The main test routine.
*
* @param bundle The bundle under test or null
* @param testnames The names to test
* @param report The report writer or null
* @return # of errors
*/
int test(Bundle bundle, String testnames, Writer report) {
trace("testing bundle %s with %s", bundle, testnames);
Bundle fw = context.getBundle(0);
try {
bundle = findHost(bundle);
List<String> names = new ArrayList<String>();
StringTokenizer st = new StringTokenizer(testnames, " ,");
//
// Collect the test names and remove any duplicates
//
while (st.hasMoreTokens()) {
String token = st.nextToken();
if (!names.contains(token))
names.add(token);
}
List<TestReporter> reporters = new ArrayList<TestReporter>();
final TestResult result = new TestResult();
Tee systemOut = new Tee(System.err);
Tee systemErr = new Tee(System.err);
systemOut.capture(isTrace()).echo(true);
systemErr.capture(isTrace()).echo(true);
final PrintStream originalOut = System.out;
final PrintStream originalErr = System.err;
System.setOut(systemOut.getStream());
System.setErr(systemErr.getStream());
trace("changed streams");
try {
BasicTestReport basic = new BasicTestReport(this, systemOut, systemErr, result);
add(reporters, result, basic);
if (port > 0) {
add(reporters, result, jUnitEclipseReport);
}
if (report != null) {
add(reporters, result, new JunitXmlReport(report, bundle, basic));
}
for (TestReporter tr : reporters) {
tr.setup(fw, bundle);
}
TestSuite suite = createSuite(bundle, names, result);
try {
trace("created suite %s #%s", suite.getName(), suite.countTestCases());
List<Test> flattened = new ArrayList<Test>();
int realcount = flatten(flattened, suite);
for (TestReporter tr : reporters) {
tr.begin(flattened, realcount);
}
trace("running suite %s", suite);
suite.run(result);
} catch (Throwable t) {
trace("%s", t);
result.addError(suite, t);
} finally {
for (TestReporter tr : reporters) {
tr.end();
}
}
} catch (Throwable t) {
System.err.println("exiting " + t);
t.printStackTrace(System.err);
} finally {
System.setOut(originalOut);
System.setErr(originalErr);
trace("unset streams");
}
System.err.println("Tests run : " + result.runCount());
System.err.println("Passed : " + (result.runCount() - result.errorCount() - result.failureCount()));
System.err.println("Errors : " + result.errorCount());
System.err.println("Failures : " + result.failureCount());
return result.errorCount() + result.failureCount();
} catch (Exception e) {
e.printStackTrace();
}
return -1;
}
/*
* Find the host for a fragment. We just iterate over all other bundles and
* ask for the fragments. We returns the first one found.
*/
private Bundle findHost(Bundle bundle) {
if (bundle == null)
return null;
PackageAdmin packageAdmin = (PackageAdmin) packageAdminTracker.getService();
if (packageAdmin == null) {
trace("Have a potential fragment but Package Admin not present to find the host");
return bundle;
}
if ((packageAdmin.getBundleType(bundle) & PackageAdmin.BUNDLE_TYPE_FRAGMENT) == 0)
return bundle;
Bundle found = null;
for (Bundle potentialFragmentHost : context.getBundles()) {
if (potentialFragmentHost == bundle || potentialFragmentHost.getBundleId() == 0)
continue;
Bundle[] fragments = packageAdmin.getFragments(potentialFragmentHost);
if (fragments == null)
continue;
for (Bundle fragment : fragments) {
if (fragment == bundle) {
if (found != null) {
trace("Have a test fragment but find multiple hosts. Fragment=%s, Previous=%s, Next=%s ",
bundle, found, fragment);
} else
found = potentialFragmentHost;
}
}
}
return found == null ? bundle : found;
}
private TestSuite createSuite(Bundle tfw, List<String> testNames, TestResult result) {
TestSuite suite = new TestSuite();
for (String fqn : testNames) {
addTest(tfw, suite, fqn, result);
}
return suite;
}
private void addTest(Bundle tfw, TestSuite suite, String fqn, TestResult testResult) {
try {
int n = fqn.indexOf(':');
if (n > -1) {
String method = fqn.substring(n + 1);
fqn = fqn.substring(0, n);
Class< ? > clazz = loadClass(tfw, fqn);
if (clazz != null)
addTest(suite, clazz, method);
else {
diagnoseNoClass(tfw, fqn);
testResult.addError(suite,
new Exception("Cannot load class " + fqn + ", was it included in the test bundle?"));
}
} else {
Class< ? > clazz = loadClass(tfw, fqn);
if (clazz != null)
addTest(suite, clazz, null);
else {
diagnoseNoClass(tfw, fqn);
testResult.addError(suite,
new Exception("Cannot load class " + fqn + ", was it included in the test bundle?"));
}
}
} catch (Throwable e) {
System.err.println("Can not create test case for: " + fqn + " : " + e);
testResult.addError(suite, e);
}
}
private void diagnoseNoClass(Bundle tfw, String fqn) {
if (tfw == null) {
error("No class found: %s, target bundle: %s", fqn, tfw);
trace("Installed bundles:");
for (Bundle bundle : context.getBundles()) {
Class< ? > c = loadClass(bundle, fqn);
String state;
switch (bundle.getState()) {
case Bundle.UNINSTALLED :
state = "UNINSTALLED";
break;
case Bundle.INSTALLED :
state = "INSTALLED";
break;
case Bundle.RESOLVED :
state = "RESOLVED";
break;
case Bundle.STARTING :
state = "STARTING";
break;
case Bundle.STOPPING :
state = "STOPPING";
break;
case Bundle.ACTIVE :
state = "ACTIVE";
break;
default :
state = "UNKNOWN";
}
trace("%s %s %s", bundle.getLocation(), state, c != null);
}
}
}
@SuppressWarnings("unchecked")
private void addTest(TestSuite suite, Class< ? > clazz, final String method) {
if (TestCase.class.isAssignableFrom(clazz)) {
if (hasJunit4Annotations(clazz)) {
error("The test class %s extends %s and it uses JUnit 4 annotations. This means that the annotations will be ignored.",
clazz.getName(), TestCase.class.getName());
}
trace("using JUnit 3");
if (method != null) {
suite.addTest(TestSuite.createTest(clazz, method));
return;
}
suite.addTestSuite((Class< ? extends TestCase>) clazz);
return;
}
trace("using JUnit 4");
JUnit4TestAdapter adapter = new JUnit4TestAdapter(clazz);
if (method != null) {
trace("method specified " + clazz + ":" + method);
final Pattern glob = Pattern.compile(method.replaceAll("\\*", ".*").replaceAll("\\?", ".?"));
try {
adapter.filter(new org.junit.runner.manipulation.Filter() {
@Override
public String describe() {
return "Method filter for " + method;
}
@Override
public boolean shouldRun(Description description) {
if (glob.matcher(description.getMethodName()).lookingAt()) {
trace("accepted " + description.getMethodName());
return true;
}
trace("rejected " + description.getMethodName());
return false;
}
});
} catch (NoTestsRemainException e) {
return;
}
}
suite.addTest(adapter);
}
private boolean hasJunit4Annotations(Class< ? > clazz) {
if (hasAnnotations("org.junit.", clazz.getAnnotations()))
return true;
for (Method m : clazz.getMethods()) {
if (hasAnnotations("org.junit.", m.getAnnotations()))
return true;
}
return false;
}
private boolean hasAnnotations(String prefix, Annotation[] annotations) {
if (annotations != null)
for (Annotation a : annotations)
if (a.getClass().getName().startsWith(prefix))
return true;
return false;
}
private Class< ? > loadClass(Bundle tfw, String fqn) {
try {
if (tfw != null) {
checkResolved(tfw);
try {
return tfw.loadClass(fqn);
} catch (ClassNotFoundException e1) {
return null;
}
}
trace("finding %s", fqn);
Bundle bundles[] = context.getBundles();
for (int i = bundles.length - 1; i >= 0; i--) {
try {
checkResolved(bundles[i]);
trace("found in %s", bundles[i]);
return bundles[i].loadClass(fqn);
} catch (ClassNotFoundException e1) {
trace("not in %s", bundles[i]);
// try next
}
}
} catch (Exception e) {
error("Exception during loading of class: %s. Exception %s and cause %s. This sometimes "
+ "happens when there is an error in the static initialization, the class has "
+ "no public constructor, it is an inner class, or it has no public access", fqn, e, e.getCause());
}
return null;
}
private void checkResolved(Bundle bundle) {
int state = bundle.getState();
if (state == Bundle.INSTALLED || state == Bundle.UNINSTALLED) {
trace("unresolved bundle %s", bundle.getLocation());
}
}
public int flatten(List<Test> list, TestSuite suite) {
int realCount = 0;
for (Enumeration< ? > e = suite.tests(); e.hasMoreElements();) {
Test test = (Test) e.nextElement();
if (test instanceof JUnit4TestAdapter) {
list.add(test);
for (Test t : ((JUnit4TestAdapter) test).getTests()) {
if (t instanceof TestSuite) {
realCount += flatten(list, (TestSuite) t);
} else {
list.add(t);
realCount++;
}
}
continue;
}
list.add(test);
if (test instanceof TestSuite) {
realCount += flatten(list, (TestSuite) test);
} else {
realCount++;
}
}
return realCount;
}
private void add(List<TestReporter> reporters, TestResult result, TestReporter rp) {
reporters.add(rp);
result.addListener(rp);
}
static public String replace(String source, String symbol, String replace) {
StringBuffer sb = new StringBuffer(source);
int n = sb.indexOf(symbol, 0);
while (n > 0) {
sb.replace(n, n + symbol.length(), replace);
n = n - symbol.length() + replace.length();
n = sb.indexOf(replace, n);
}
return sb.toString();
}
boolean isTrace() {
return trace;
}
public void trace(String msg, Object... objects) {
if (isTrace()) {
message("# ", msg, objects);
}
}
void message(String prefix, String string, Object... objects) {
Throwable e = null;
StringBuilder sb = new StringBuilder();
int n = 0;
sb.append(prefix);
for (int i = 0; i < string.length(); i++) {
char c = string.charAt(i);
if (c == '%') {
c = string.charAt(++i);
switch (c) {
case 's' :
if (n < objects.length) {
Object o = objects[n++];
if (o instanceof Throwable) {
Throwable t = e = (Throwable) o;
while (t instanceof InvocationTargetException) {
Throwable cause = t.getCause();
if (cause == null) {
break;
}
t = cause;
}
sb.append(t.getMessage());
} else {
sb.append(o);
}
} else
sb.append("<no more arguments>");
break;
default :
sb.append(c);
}
} else {
sb.append(c);
}
}
out.println(sb);
if (e != null)
e.printStackTrace(out);
}
public void error(String msg, Object... objects) {
message("! ", msg, objects);
}
/**
* Running a test from the command line
*
* @param args
*/
public static void main(String args[]) {
System.out.println("args " + Arrays.toString(args));
}
}