import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringWriter; import java.io.Writer; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.net.Inet4Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.regex.Pattern; import junit.framework.Assert; import junit.framework.JUnit4TestAdapter; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestResult; import junit.framework.TestSuite; import junit.runner.Version; import libcore.io.Libcore; import libcore.util.BasicLruCache; import org.junit.internal.TextListener; import org.junit.runner.Description; import org.junit.runner.JUnitCore; import org.junit.runner.RunWith; import org.junit.runner.notification.Failure; import org.junit.runners.AllTests; import org.junit.runners.model.TestClass; import org.robovm.rt.VM; /** * Builds a JUnit {@link TestSuite} from the libcore tests. Tests listed in the * <code>/robovm-excluded-tests</code> file in the classpath will not be run. * The <code>ROBOVM_TESTS_INCLUDE_PATTERN</code> and <code>ROBOVM_TESTS_EXCLUDE_PATTERN</code> * environment variables can be used to specify regular expression pattern that are used to * include/exclude tests. These are matched against a test's fully qualified class name. */ @RunWith(AllTests.class) public class RoboVMAllTests { static List<String> excludedTests = null; static List<Class<?>> allClasses; static { // Read in the list of tests that should be excluded excludedTests = readLines("/robovm-excluded-tests"); // Read in the list of classes generated by the ANT build script. allClasses = new ArrayList<Class<?>>(3000); for (String line : readLines("/all-classes.txt")) { try { allClasses.add(Class.forName(line)); } catch (Throwable t) { } } // Many tests call InetAddress.getLocalHost() but it may fail if the // hostname doesn't resolve to an IP address. This will install a fake // InetAddress for the hostname if needed. try { InetAddress.getLocalHost(); } catch (UnknownHostException uhe) { try { installFakeLocalHostAddressCacheEntry(); } catch (Throwable e) { throw new Error(e); } } // user.dir is supposed to be the current working dir. On iOS user.dir // gets set to /. Some tests write files to this dir and when it's / // those tests fail with a "permission denied" error. if (System.getProperty("os.name").contains("iOS") && "/".equals(System.getProperty("user.dir"))) { System.setProperty("user.dir", VM.basePath() + "/../Library"); } } private static Field field(Class<?> cls, String name) throws Throwable { Field f = cls.getDeclaredField(name); f.setAccessible(true); return f; } private static InetAddress getFakeLocalHost() throws Throwable { Constructor<?> inet4AddressConstructor = Inet4Address.class.getDeclaredConstructor(byte[].class, String.class); inet4AddressConstructor.setAccessible(true); String[] candidates = new String[] {"eth0", "eth1", "wlan0", "wlan1", "en0", "en1", "lo", "lo0"}; String hostname = Libcore.os.uname().nodename; for (int i = 0; i < candidates.length; i++) { NetworkInterface ni = NetworkInterface.getByName(candidates[i]); if (ni != null && !ni.isLoopback()) { Enumeration<InetAddress> addrEn = ni.getInetAddresses(); while (addrEn.hasMoreElements()) { InetAddress addr = addrEn.nextElement(); if (addr instanceof Inet4Address) { return (InetAddress) inet4AddressConstructor.newInstance(addr.getAddress(), hostname); } } } } return (InetAddress) inet4AddressConstructor.newInstance(new byte[] {127, 0, 0, 1}, hostname); } private static void installFakeLocalHostAddressCacheEntry() throws Throwable { Class<?> addressCacheClass = Class.forName("java.net.AddressCache"); Class<?> addressCacheEntryClass = Class.forName("java.net.AddressCache$AddressCacheEntry"); Constructor<?> entryConstructor = addressCacheEntryClass.getDeclaredConstructor(Object.class); entryConstructor.setAccessible(true); InetAddress[] addresses = new InetAddress[] { getFakeLocalHost() }; Object entry = entryConstructor.newInstance(new Object[] {addresses}); field(addressCacheEntryClass, "expiryNanos").set(entry, Long.MAX_VALUE); BasicLruCache<Object, Object> cache = new BasicLruCache<Object, Object>(64); cache.put(Libcore.os.uname().nodename, entry); field(addressCacheClass, "cache").set(field(InetAddress.class, "addressCache").get(null), cache); } @SuppressWarnings("unchecked") private static void cleanupHttpConnectionPool() throws Throwable { // Class<?> poolClass = Class.forName("libcore.net.http.HttpConnectionPool"); // Map<Object, List<Object>> connections = // (Map<Object, List<Object>>) field(poolClass, "connectionPool").get(field(poolClass, "INSTANCE").get(null)); // Method closeSocketAndStreamsMethod = Class.forName("libcore.net.http.HttpConnection").getDeclaredMethod("closeSocketAndStreams"); // closeSocketAndStreamsMethod.setAccessible(true); // for (List<Object> l : connections.values()) { // for (Object connection : l) { // closeSocketAndStreamsMethod.invoke(connection); // } // } // connections.clear(); } /** * Wraps a {@link Test} and "forgets" it after it's been run in order to make it reclaimable * by the GC (unless it's reachable from somewhere other than JUnit). */ private static class ForgettingTestWrapper implements Test { private Test t; private int count; ForgettingTestWrapper(Test t) { this.t = t; this.count = t.countTestCases(); } @Override public int countTestCases() { return count; } @Override public void run(TestResult result) { if (t == null) { Assert.fail("Test has already been run"); } System.err.println(t); // Some tests set the default locale without resetting it which causes subsequent // tests to fail. Remember the default locale and restore it after each test. Locale defLoc = Locale.getDefault(); try { t.run(result); } finally { t = null; Locale.setDefault(defLoc); try { // Close any connections created by the test and now pooled by // libcore.net.http.HttpConnectionPool. If we don't do this // we'll end up with "Too many open files" errors on iOS when // running the tests. cleanupHttpConnectionPool(); } catch (Throwable e) { e.printStackTrace(); } } } } private static List<Test> findTests(Pattern include, Pattern exclude) throws IOException { List<Test> tests = new ArrayList<Test>(); for (Class<?> c : allClasses) { String name = c.getName(); if (!include.matcher(name).matches() || exclude.matcher(name).matches()) { continue; } try { if (!c.isInterface() && Modifier.isPublic(c.getModifiers()) && ! Modifier.isAbstract(c.getModifiers())) { Test test = createTest(c); if (hasTests(test)) { tests.add(test); } } } catch (Throwable t) { } } return tests; } private static Test createTest(Class<?> c) { if (TestCase.class.isAssignableFrom(c)) { return new TestSuite(c); } else if (!new TestClass(c).getAnnotatedMethods(org.junit.Test.class).isEmpty()) { return new JUnit4TestAdapter(c); } return null; } private static boolean hasTests(Test test) { if (test == null) { return false; } if (test.countTestCases() > 1) { return true; } if (!(test instanceof TestSuite)) { return true; } TestSuite suite = (TestSuite) test; if (!(suite.testAt(0) instanceof TestCase)) { return true; } TestCase singleTest = (TestCase) suite.testAt(0); return !singleTest.getName().equals("warning"); } private static void removeExcludedTests(TestSuite tests, TestSuite result) { Enumeration<Test> en = tests.tests(); while (en.hasMoreElements()) { Test test = en.nextElement(); if (test instanceof TestSuite) { removeExcludedTests((TestSuite) test, result); } else if (test instanceof TestCase) { String fullName = test.getClass().getName() + "#" + ((TestCase) test).getName(); boolean match = false; for (String excluded : excludedTests) { if (fullName.equals(excluded)) { match = true; break; } } if (!match) { result.addTest(test); } } else { // TODO: Filter Junit 4 tests? result.addTest(test); } } } private static TestSuite removeExcludedTests(TestSuite tests) { TestSuite result = new TestSuite(); removeExcludedTests(tests, result); return result; } private static TestSuite wrapTests(TestSuite tests) { TestSuite result = new TestSuite(); Enumeration<Test> en = tests.tests(); while (en.hasMoreElements()) { Test test = en.nextElement(); result.addTest(new ForgettingTestWrapper(test)); } return result; } private static TestSuite suite(Pattern include, Pattern exclude, boolean runExcluded) throws IOException { TestSuite suite = new TestSuite(); for (Test test : findTests(include, exclude)) { suite.addTest(test); } if (runExcluded) { return wrapTests(suite); } return wrapTests(removeExcludedTests(suite)); } public static TestSuite suite() throws IOException { String include = System.getenv("ROBOVM_TESTS_INCLUDE_PATTERN"); if (include == null) { include = ".*"; } String exclude = System.getenv("ROBOVM_TESTS_EXCLUDE_PATTERN"); if (exclude == null) { exclude = " "; } boolean runExcluded = Boolean.parseBoolean(System.getenv("ROBOVM_TESTS_RUN_EXCLUDED")); return suite(Pattern.compile(include), Pattern.compile(exclude), runExcluded); } private static long copy(Reader input, Writer output) throws IOException { char[] buffer = new char[4096]; long count = 0; int n = 0; while (-1 != (n = input.read(buffer))) { output.write(buffer, 0, n); count += n; } return count; } private static List<String> readLines(String path) { try { InputStreamReader in = new InputStreamReader(RoboVMAllTests.class.getResourceAsStream(path), "UTF8"); StringWriter out = new StringWriter(); copy(in, out); ArrayList<String> lines = new ArrayList<String>(); for (String s : out.toString().split("\\s+")) { s = s.trim(); if (!s.startsWith("#") && s.length() > 0) { lines.add(s); } } return lines; } catch (Throwable t) { throw new Error(t); } } public static void main(String[] args) throws Exception { System.out.println("JUnit version " + Version.id()); JUnitCore core = new JUnitCore(); core.addListener(new TextListener(System.out) { @Override public void testStarted(Description description) { System.out.print(description + " ... "); } @Override public void testFinished(Description description) { System.out.println(); } @Override public void testFailure(Failure failure) { System.out.print("FAILED!"); } @Override public void testIgnored(Description description) { System.out.println("IGNORED!"); } }); core.run( suite( Pattern.compile(args.length > 0 ? args[0] : ".*"), Pattern.compile(args.length > 1 ? args[1] : " "), false)); } }