package com.socialize; import android.os.Bundle; import android.test.AndroidTestRunner; import android.test.InstrumentationTestRunner; import android.util.Log; import android.util.Xml; import junit.framework.AssertionFailedError; import junit.framework.Test; import junit.framework.TestCase; import junit.framework.TestListener; import org.xmlpull.v1.XmlSerializer; import java.io.*; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; public class SocializeTestRunner extends InstrumentationTestRunner { private static final String TESTSUITES = "testsuites"; private static final String TESTSUITE = "testsuite"; private static final String ERRORS = "errors"; private static final String FAILURES = "failures"; private static final String ERROR = "error"; private static final String FAILURE = "failure"; private static final String NAME = "name"; private static final String PACKAGE = "package"; private static final String TESTS = "tests"; private static final String TESTCASE = "testcase"; private static final String CLASSNAME = "classname"; private static final String TIME = "time"; private static final String TIMESTAMP = "timestamp"; private static final String PROPERTIES = "properties"; private static final String SYSTEM_OUT = "system-out"; private static final String SYSTEM_ERR = "system-err"; private static final String SPLIT_LEVEL_NONE = "none"; private static final String SPLIT_LEVEL_CLASS = "class"; private static final String SPLIT_LEVEL_PACKAGE = "package"; private static final String TAG = SocializeTestRunner.class.getSimpleName(); private static final String DEFAULT_JUNIT_FILE_POSTFIX = "-TEST.xml"; private static final String DEFAULT_NO_PACKAGE_PREFIX = "NO_PACKAGE"; private static final String DEFAULT_SINGLE_FILE_NAME = "ALL-TEST.xml"; private static final String DEFAULT_SPLIT_LEVEL = SPLIT_LEVEL_NONE; private String junitOutputDirectory = null; private String junitOutputFilePostfix = null; private String junitNoPackagePrefix; private String junitSplitLevel; private String junitSingleFileName; private boolean junitOutputEnabled; private boolean justCount; private XmlSerializer currentXmlSerializer; private final LinkedHashMap<Package, TestCaseInfo> caseMap = new LinkedHashMap<Package, TestCaseInfo>(); private boolean outputEnabled; private boolean logOnly; private PrintWriter currentFileWriter; /** * Stores information about single test run. * */ public static class TestInfo { public Package thePackage; public Class< ? extends TestCase> testCase; public String name; public Throwable error; public AssertionFailedError failure; public long time; @Override public String toString() { return name + "[" + testCase.getClass() + "] <" + thePackage + ">. Time: " + time + " ms. E<" + error + ">, F <" + failure + ">"; } } /** * Stores information about particular test case class - containing all * tests for that class. * */ public static class TestCaseInfo { public Package thePackage; public Class< ? extends TestCase> testCaseClass; public Map<String, TestInfo> testMap = new LinkedHashMap<String, TestInfo>(); } /** * Listener for executing test cases. It has the following purposes: * measures time of execution for each test, stores errors and failures that * occur during test as well as it optimizes garbage collection of the test * - after test is finished it cleans up all the static variables of the * test case. The last one is pretty useful if many tests are executed. * */ private class JunitTestListener implements TestListener { /** * The minimum time we expect a test to take. */ private static final int MINIMUM_TIME = 100; /** * Just in case it ever happens that the tests are run in parallell * (maybe future junit version?) we make sure that measured time is * separate per each thread running the tests. */ private final ThreadLocal<Long> startTime = new ThreadLocal<Long>(); @Override public void startTest(final Test test) { Log.d(TAG, "Starting test: " + test); if (test instanceof TestCase) { Thread.currentThread().setContextClassLoader(test.getClass().getClassLoader()); startTime.set(System.currentTimeMillis()); } } @Override public void endTest(final Test t) { if (t instanceof TestCase) { final TestCase testCase = (TestCase) t; cleanup(testCase); /* * Note! This is copied from InstrumentationCoreTestRunner in * android code * * Make sure all tests take at least MINIMUM_TIME to complete. * If they don't, we wait a bit. The Cupcake Binder can't handle * too many operations in a very short time, which causes * headache for the CTS. */ final long timeTaken = System.currentTimeMillis() - startTime.get(); getTestInfo(testCase).time = timeTaken; if (timeTaken < MINIMUM_TIME) { try { Thread.sleep(MINIMUM_TIME - timeTaken); } catch (final InterruptedException ignored) { // We don't care. } } } Log.d(TAG, "Finished test: " + t); } @Override public void addError(final Test test, final Throwable t) { if (test instanceof TestCase) { getTestInfo((TestCase) test).error = t; } } @Override public void addFailure(final Test test, final AssertionFailedError f) { if (test instanceof TestCase) { getTestInfo((TestCase) test).failure = f; } } /** * Nulls all non-static reference fields in the given test class. This * method helps us with those test classes that don't have an explicit * tearDown() method. Normally the garbage collector should take care of * everything, but since JUnit keeps references to all test cases, a * little help might be a good idea. * * Note! This is copied from InstrumentationCoreTestRunner in android * code */ private void cleanup(final TestCase test) { Class< ? > clazz = test.getClass(); while (clazz != TestCase.class) { final Field[] fields = clazz.getDeclaredFields(); for (final Field field : fields) { if (!field.getType().isPrimitive() && !Modifier.isStatic(field.getModifiers())) { try { field.setAccessible(true); field.set(test, null); } catch (final Exception ignored) { // Nothing we can do about it. } } } clazz = clazz.getSuperclass(); } } } private synchronized TestInfo getTestInfo(final TestCase testCase) { final Class< ? extends TestCase> clazz = testCase.getClass(); final Package thePackage = clazz.getPackage(); final String name = testCase.getName(); StringBuilder sb = new StringBuilder(); sb.append(thePackage).append(".").append(clazz.getSimpleName()).append(".").append(name); final String mapKey = sb.toString(); TestCaseInfo caseInfo = caseMap.get(thePackage); if (caseInfo == null) { caseInfo = new TestCaseInfo(); caseInfo.testCaseClass = testCase.getClass(); caseInfo.thePackage = thePackage; caseMap.put(thePackage, caseInfo); } TestInfo ti = caseInfo.testMap.get(mapKey); if (ti == null) { ti = new TestInfo(); ti.name = name; ti.testCase = testCase.getClass(); ti.thePackage = thePackage; caseInfo.testMap.put(mapKey, ti); } return ti; } private void startFile(final File outputFile) throws IOException { Log.d(TAG, "Writing to file " + outputFile); currentXmlSerializer = Xml.newSerializer(); currentFileWriter = new PrintWriter(outputFile, "UTF-8"); currentXmlSerializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); currentXmlSerializer.setOutput(currentFileWriter); currentXmlSerializer.startDocument("UTF-8", null); currentXmlSerializer.startTag(null, TESTSUITES); } private void endFile() throws IOException { Log.d(TAG, "closing file"); currentXmlSerializer.endTag(null, TESTSUITES); currentXmlSerializer.endDocument(); currentFileWriter.flush(); currentFileWriter.close(); } private String getTimestamp() { final long time = System.currentTimeMillis(); final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); return sdf.format(time); } private void writeClassToFile(final TestCaseInfo tci) throws IllegalArgumentException, IllegalStateException, IOException { final Package thePackage = tci.thePackage; final Class< ? extends TestCase> clazz = tci.testCaseClass; final int tests = tci.testMap.size(); final String timestamp = getTimestamp(); int errors = 0; int failures = 0; int time = 0; for (final TestInfo testInfo : tci.testMap.values()) { if (testInfo.error != null) { errors++; } if (testInfo.failure != null) { failures++; } time += testInfo.time; } currentXmlSerializer.startTag(null, TESTSUITE); currentXmlSerializer.attribute(null, ERRORS, Integer.toString(errors)); currentXmlSerializer.attribute(null, FAILURES, Integer.toString(failures)); currentXmlSerializer.attribute(null, NAME, clazz.getName()); currentXmlSerializer.attribute(null, PACKAGE, thePackage == null ? "" : thePackage.getName()); currentXmlSerializer.attribute(null, TESTS, Integer.toString(tests)); currentXmlSerializer.attribute(null, TIME, Double.toString(time / 1000.0)); currentXmlSerializer.attribute(null, TIMESTAMP, timestamp); for (final TestInfo testInfo : tci.testMap.values()) { writeTestInfo(testInfo); } currentXmlSerializer.startTag(null, PROPERTIES); currentXmlSerializer.endTag(null, PROPERTIES); currentXmlSerializer.startTag(null, SYSTEM_OUT); currentXmlSerializer.endTag(null, SYSTEM_OUT); currentXmlSerializer.startTag(null, SYSTEM_ERR); currentXmlSerializer.endTag(null, SYSTEM_ERR); currentXmlSerializer.endTag(null, TESTSUITE); } private void writeTestInfo(final TestInfo testInfo) throws IllegalArgumentException, IllegalStateException, IOException { currentXmlSerializer.startTag(null, TESTCASE); currentXmlSerializer.attribute(null, CLASSNAME, testInfo.testCase.getName()); currentXmlSerializer.attribute(null, NAME, testInfo.name); currentXmlSerializer.attribute(null, TIME, Double.toString(testInfo.time / 1000.0)); if (testInfo.error != null) { currentXmlSerializer.startTag(null, ERROR); final StringWriter sw = new StringWriter(); final PrintWriter pw = new PrintWriter(sw, true); testInfo.error.printStackTrace(pw); currentXmlSerializer.text(sw.toString()); currentXmlSerializer.endTag(null, ERROR); } if (testInfo.failure != null) { currentXmlSerializer.startTag(null, FAILURE); final StringWriter sw = new StringWriter(); final PrintWriter pw = new PrintWriter(sw, true); testInfo.failure.printStackTrace(pw); currentXmlSerializer.text(sw.toString()); currentXmlSerializer.endTag(null, FAILURE); } currentXmlSerializer.endTag(null, TESTCASE); } private File getJunitOutputFile(final Package p) { return new File(junitOutputDirectory, (p == null ? junitNoPackagePrefix : p.getName()) + junitOutputFilePostfix); } private File getJunitOutputFile() { return new File(junitOutputDirectory, junitSingleFileName); } private File getJunitOutputFile(final Class< ? extends TestCase> clazz) { return new File(junitOutputDirectory, clazz.getName() + junitOutputFilePostfix); } private void setDefaultParameters() { if (junitOutputDirectory == null) { junitOutputDirectory = "/data/data/" + getTargetContext().getPackageName(); } if (junitOutputFilePostfix == null) { junitOutputFilePostfix = DEFAULT_JUNIT_FILE_POSTFIX; } if (junitNoPackagePrefix == null) { junitNoPackagePrefix = DEFAULT_NO_PACKAGE_PREFIX; } if (junitSplitLevel == null) { junitSplitLevel = DEFAULT_SPLIT_LEVEL; } if (junitSingleFileName == null) { junitSingleFileName = DEFAULT_SINGLE_FILE_NAME; } } private boolean getBooleanArgument(final Bundle arguments, final String tag, final boolean defaultValue) { final String tagString = arguments.getString(tag); if (tagString == null) { return defaultValue; } return Boolean.parseBoolean(tagString); } @Override public void onCreate(final Bundle arguments) { if (arguments != null) { Log.d(TAG, "Creating the Test Runner with arguments: " + arguments.keySet()); junitOutputEnabled = getBooleanArgument(arguments, "junitXmlOutput", true); junitOutputDirectory = arguments.getString("junitOutputDirectory"); junitOutputFilePostfix = arguments.getString("junitOutputFilePostfix"); junitNoPackagePrefix = arguments.getString("junitNoPackagePrefix"); junitSplitLevel = arguments.getString("junitSplitLevel"); junitSingleFileName = arguments.getString("junitSingleFileName"); justCount = getBooleanArgument(arguments, "count", false); logOnly = getBooleanArgument(arguments, "log", false); } setDefaultParameters(); logParameters(); createDirectoryIfNotExist(); deleteOldFiles(); super.onCreate(arguments); } private void logParameters() { Log.d(TAG, "Test runner is running with the following parameters:"); Log.d(TAG, "junitOutputDirectory: " + junitOutputDirectory); Log.d(TAG, "junitOutputFilePostfix: " + junitOutputFilePostfix); Log.d(TAG, "junitNoPackagePrefix: " + junitNoPackagePrefix); Log.d(TAG, "junitSplitLevel: " + junitSplitLevel); Log.d(TAG, "junitSingleFileName: " + junitSingleFileName); } private boolean createDirectoryIfNotExist(){ boolean created = false; Log.d(TAG, "Creating output directory if it does not exist"); File directory = new File(junitOutputDirectory); if (!directory.exists()){ created = directory.mkdirs(); } Log.d(TAG, "Created directory? " + created ); return created; } private void deleteOldFiles() { Log.d(TAG, "Deleting old files"); final File[] filesToDelete = new File(junitOutputDirectory).listFiles(new FilenameFilter() { @Override public boolean accept(final File dir, final String filename) { return filename.endsWith(junitOutputFilePostfix) || filename.equals(junitSingleFileName); } }); if (filesToDelete != null){ Log.d(TAG, "Deleting: " + Arrays.toString(filesToDelete)); for (final File f : filesToDelete) { if(!f.delete()) { f.deleteOnExit(); } } } } @Override public void finish(final int resultCode, final Bundle results) { if (outputEnabled) { Log.d(TAG, "Post processing"); if (SPLIT_LEVEL_PACKAGE.equals(junitSplitLevel)) { processPackageLevelSplit(); } else if (SPLIT_LEVEL_CLASS.equals(junitSplitLevel)) { processClassLevelSplit(); } else if (SPLIT_LEVEL_NONE.equals(junitSplitLevel)) { processNoSplit(); } else { Log.d(TAG, "Invalid split level " + junitSplitLevel + ", falling back to package level split."); processPackageLevelSplit(); } } super.finish(resultCode, results); } private void processNoSplit() { try { final File f = getJunitOutputFile(); startFile(f); try { for (final Package p : caseMap.keySet()) { try { final TestCaseInfo tc = caseMap.get(p); writeClassToFile(tc); } catch (final IOException e) { Log.e(TAG, "Error: " + e, e); } } } finally { endFile(); } } catch (final IOException e) { Log.e(TAG, "Error: " + e, e); } } private void processPackageLevelSplit() { Log.d(TAG, "Packages: " + caseMap.size()); for (final Package p : caseMap.keySet()) { Log.d(TAG, "Processing package " + p); try { final File f = getJunitOutputFile(p); startFile(f); try { final TestCaseInfo tc = caseMap.get(p); writeClassToFile(tc); } finally { endFile(); } } catch (final IOException e) { Log.e(TAG, "Error: " + e, e); } } } private void processClassLevelSplit() { for (final Package p : caseMap.keySet()) { try { final TestCaseInfo tc = caseMap.get(p); final File f = getJunitOutputFile(tc.testCaseClass); startFile(f); try { writeClassToFile(tc); } finally { endFile(); } } catch (final IOException e) { Log.e(TAG, "Error: " + e, e); } } } @Override protected AndroidTestRunner getAndroidTestRunner() { Log.d(TAG, "Getting android test runner"); AndroidTestRunner runner = super.getAndroidTestRunner(); if (junitOutputEnabled && !justCount && !logOnly) { Log.d(TAG, "JUnit test output enabled"); outputEnabled = true; runner.addTestListener(new JunitTestListener()); } else { outputEnabled = false; Log.d(TAG, "JUnit test output disabled: [ junitOutputEnabled : " + junitOutputEnabled + ", justCount : " + justCount + ", logOnly : " + logOnly + " ]"); } return runner; } }