├── libs ├── testrunner-1.1.jar └── testrunner-runtime-1.1.jar ├── res ├── drawable-hdpi │ └── icon.png ├── drawable-ldpi │ └── icon.png ├── drawable-mdpi │ └── icon.png ├── values │ └── strings.xml └── layout │ └── main.xml ├── README.md ├── AndroidManifest.xml ├── project.properties ├── .gitignore ├── proguard-project.txt ├── LICENSE ├── LICENCE.txt ├── lint.xml └── src └── pl └── polidea └── instrumentation └── PolideaInstrumentationTestRunner.java /libs/testrunner-1.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/podio/mixed-android-instrumentation-test-runner/master/libs/testrunner-1.1.jar -------------------------------------------------------------------------------- /res/drawable-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/podio/mixed-android-instrumentation-test-runner/master/res/drawable-hdpi/icon.png -------------------------------------------------------------------------------- /res/drawable-ldpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/podio/mixed-android-instrumentation-test-runner/master/res/drawable-ldpi/icon.png -------------------------------------------------------------------------------- /res/drawable-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/podio/mixed-android-instrumentation-test-runner/master/res/drawable-mdpi/icon.png -------------------------------------------------------------------------------- /libs/testrunner-runtime-1.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/podio/mixed-android-instrumentation-test-runner/master/libs/testrunner-runtime-1.1.jar -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mixed-android-instrumentation-test-runner 2 | ========================================= 3 | 4 | Combining benefits of GoogleInstrumentationTestRunner and PolideaInstrumentationTestRunner. 5 | -------------------------------------------------------------------------------- /res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello World! 4 | PolideaTestInstrumentation 5 | 6 | -------------------------------------------------------------------------------- /AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system use, 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | 10 | android.library=true 11 | # Project target. 12 | target=android-16 13 | -------------------------------------------------------------------------------- /res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # files for the dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # generated files 12 | bin/ 13 | gen/ 14 | 15 | # Local configuration file (sdk path, etc) 16 | local.properties 17 | 18 | # Eclipse project files 19 | .classpath 20 | .project 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Intellij project files 26 | *.iml 27 | *.ipr 28 | *.iws 29 | .idea/ 30 | 31 | # Gradle 32 | 33 | build/ -------------------------------------------------------------------------------- /proguard-project.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Vitalii Grygoruk 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | Licence: 2 | 3 | Copyright (c) 2011, Polidea 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | 8 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 9 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer 10 | in the documentation and/or other materials provided with the distribution. 11 | Neither the name of the Polidea nor the names of its contributors may be used to endorse or promote products derived 12 | from this software without specific prior written permission. 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, 14 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 15 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, 16 | OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 17 | OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 18 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 19 | 20 | 21 | External licences: 22 | 23 | * LIBRARY 24 | URL 25 | Type of licence -------------------------------------------------------------------------------- /lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /src/pl/polidea/instrumentation/PolideaInstrumentationTestRunner.java: -------------------------------------------------------------------------------- 1 | package pl.polidea.instrumentation; 2 | 3 | import java.io.File; 4 | import java.io.FilenameFilter; 5 | import java.io.IOException; 6 | import java.io.PrintWriter; 7 | import java.io.StringWriter; 8 | import java.lang.reflect.Field; 9 | import java.lang.reflect.Modifier; 10 | import java.text.SimpleDateFormat; 11 | import java.util.Arrays; 12 | import java.util.LinkedHashMap; 13 | import java.util.Locale; 14 | import java.util.Map; 15 | 16 | import android.content.res.Configuration; 17 | import android.os.Build; 18 | import android.util.DisplayMetrics; 19 | import junit.framework.AssertionFailedError; 20 | import junit.framework.Test; 21 | import junit.framework.TestCase; 22 | import junit.framework.TestListener; 23 | 24 | import org.xmlpull.v1.XmlSerializer; 25 | 26 | import com.google.android.apps.common.testing.testrunner.GoogleInstrumentationTestRunner; 27 | 28 | import android.os.Bundle; 29 | import android.test.AndroidTestRunner; 30 | import android.util.Log; 31 | import android.util.Xml; 32 | 33 | /** 34 | * Test runner that should produce JUnit-compatible test results. It can be used 35 | * to produce output that is parseable by any tool that understands JUnit XML 36 | * output format. It is extremely useful for example when using CI systems (all 37 | * of which understand JUnit output XML) such as Jenkins, Hudson, Bamboo, 38 | * CruiseControl and many more. 39 | * 40 | * The runner is flexible enough to produce separate file for each package, 41 | * class or for the whole test execution. By default it produces results split 42 | * by package. 43 | * 44 | * Therefore you can run the runner with some extra parameters, like: 45 | *

46 | * 47 | * 48 | * adb shell am instrument -e junitSplitLevel class -w somepackage/pl.polidea.instrumentation.PolideaInstrumentationTestRunner 49 | * 50 | *

51 | * It supports the following parameters (none of the parameters is mandatory, 52 | * they assume reasonable default values): 53 | * 54 | * 76 | * 77 | * For more details about parameters, visit Android test runner command line documentation 80 | * 81 | * @author potiuk 82 | * 83 | */ 84 | public class PolideaInstrumentationTestRunner extends GoogleInstrumentationTestRunner { 85 | 86 | private static final String TESTSUITES = "testsuites"; 87 | private static final String TESTSUITE = "testsuite"; 88 | private static final String ERRORS = "errors"; 89 | private static final String FAILURES = "failures"; 90 | private static final String ERROR = "error"; 91 | private static final String FAILURE = "failure"; 92 | private static final String NAME = "name"; 93 | private static final String VALUE = "value"; 94 | private static final String PACKAGE = "package"; 95 | private static final String TESTS = "tests"; 96 | private static final String TESTCASE = "testcase"; 97 | private static final String CLASSNAME = "classname"; 98 | private static final String TIME = "time"; 99 | private static final String TIMESTAMP = "timestamp"; 100 | private static final String PROPERTIES = "properties"; 101 | private static final String PROPERTY = "property"; 102 | private static final String SYSTEM_OUT = "system-out"; 103 | private static final String SYSTEM_ERR = "system-err"; 104 | 105 | private static final String SPLIT_LEVEL_NONE = "none"; 106 | private static final String SPLIT_LEVEL_CLASS = "class"; 107 | private static final String SPLIT_LEVEL_PACKAGE = "package"; 108 | 109 | private static final String TAG = PolideaInstrumentationTestRunner.class.getSimpleName(); 110 | private static final String DEFAULT_JUNIT_FILE_POSTFIX = "-TEST.xml"; 111 | private static final String DEFAULT_NO_PACKAGE_PREFIX = "NO_PACKAGE"; 112 | private static final String DEFAULT_SINGLE_FILE_NAME = "ALL-TEST.xml"; 113 | private static final String DEFAULT_SPLIT_LEVEL = SPLIT_LEVEL_PACKAGE; 114 | private String junitOutputDirectory = null; 115 | private String junitOutputFilePostfix = null; 116 | private String junitNoPackagePrefix; 117 | private String junitSplitLevel; 118 | private String junitSingleFileName; 119 | 120 | private boolean junitOutputEnabled; 121 | private boolean justCount; 122 | private XmlSerializer currentXmlSerializer; 123 | private final LinkedHashMap caseMap = new LinkedHashMap(); 124 | private boolean outputEnabled; 125 | private AndroidTestRunner runner; 126 | private boolean logOnly; 127 | private PrintWriter currentFileWriter; 128 | 129 | /** 130 | * Stores information about single test run. 131 | * 132 | */ 133 | public static class TestInfo { 134 | public Package thePackage; 135 | public Class< ? extends TestCase> testCase; 136 | public String name; 137 | public Throwable error; 138 | public AssertionFailedError failure; 139 | public long time; 140 | 141 | @Override 142 | public String toString() { 143 | return name + "[" + testCase.getClass() + "] <" + thePackage + ">. Time: " + time + " ms. E<" + error 144 | + ">, F <" + failure + ">"; 145 | } 146 | } 147 | 148 | /** 149 | * Stores information about particular test case class - containing all 150 | * tests for that class. 151 | * 152 | */ 153 | public static class TestCaseInfo { 154 | public Package thePackage; 155 | public Class< ? extends TestCase> testCaseClass; 156 | public Map testMap = new LinkedHashMap(); 157 | } 158 | 159 | /** 160 | * Stores information about the whole package containing multiple test 161 | * cases. 162 | * 163 | */ 164 | public static class TestPackageInfo { 165 | public Package thePackage; 166 | public Map, TestCaseInfo> testCaseList = new LinkedHashMap, TestCaseInfo>(); 167 | } 168 | 169 | /** 170 | * Listener for executing test cases. It has the following purposes: 171 | * measures time of execution for each test, stores errors and failures that 172 | * occur during test as well as it optimizes garbage collection of the test 173 | * - after test is finished it cleans up all the static variables of the 174 | * test case. The last one is pretty useful if many tests are executed. 175 | * 176 | */ 177 | private class JunitTestListener implements TestListener { 178 | 179 | /** 180 | * The minimum time we expect a test to take. 181 | */ 182 | private static final int MINIMUM_TIME = 100; 183 | /** 184 | * Just in case it ever happens that the tests are run in parallell 185 | * (maybe future junit version?) we make sure that measured time is 186 | * separate per each thread running the tests. 187 | */ 188 | private final ThreadLocal startTime = new ThreadLocal(); 189 | 190 | @Override 191 | public void startTest(final Test test) { 192 | Log.d(TAG, "Starting test: " + test); 193 | if (test instanceof TestCase) { 194 | Thread.currentThread().setContextClassLoader(test.getClass().getClassLoader()); 195 | startTime.set(System.currentTimeMillis()); 196 | } 197 | } 198 | 199 | @Override 200 | public void endTest(final Test t) { 201 | if (t instanceof TestCase) { 202 | final TestCase testCase = (TestCase) t; 203 | cleanup(testCase); 204 | /* 205 | * Note! This is copied from InstrumentationCoreTestRunner in 206 | * android code 207 | * 208 | * Make sure all tests take at least MINIMUM_TIME to complete. 209 | * If they don't, we wait a bit. The Cupcake Binder can't handle 210 | * too many operations in a very short time, which causes 211 | * headache for the CTS. 212 | */ 213 | final long timeTaken = System.currentTimeMillis() - startTime.get(); 214 | getTestInfo(testCase).time = timeTaken; 215 | if (timeTaken < MINIMUM_TIME) { 216 | try { 217 | Thread.sleep(MINIMUM_TIME - timeTaken); 218 | } catch (final InterruptedException ignored) { 219 | // We don't care. 220 | } 221 | } 222 | } 223 | Log.d(TAG, "Finished test: " + t); 224 | } 225 | 226 | @Override 227 | public void addError(final Test test, final Throwable t) { 228 | if (test instanceof TestCase) { 229 | getTestInfo((TestCase) test).error = t; 230 | } 231 | } 232 | 233 | @Override 234 | public void addFailure(final Test test, final AssertionFailedError f) { 235 | if (test instanceof TestCase) { 236 | getTestInfo((TestCase) test).failure = f; 237 | } 238 | } 239 | 240 | /** 241 | * Nulls all non-static reference fields in the given test class. This 242 | * method helps us with those test classes that don't have an explicit 243 | * tearDown() method. Normally the garbage collector should take care of 244 | * everything, but since JUnit keeps references to all test cases, a 245 | * little help might be a good idea. 246 | * 247 | * Note! This is copied from InstrumentationCoreTestRunner in android 248 | * code 249 | */ 250 | private void cleanup(final TestCase test) { 251 | Class< ? > clazz = test.getClass(); 252 | 253 | while (clazz != TestCase.class) { 254 | final Field[] fields = clazz.getDeclaredFields(); 255 | for (final Field field : fields) { 256 | final Field f = field; 257 | if (!f.getType().isPrimitive() && !Modifier.isStatic(f.getModifiers())) { 258 | try { 259 | f.setAccessible(true); 260 | f.set(test, null); 261 | } catch (final Exception ignored) { 262 | // Nothing we can do about it. 263 | } 264 | } 265 | } 266 | 267 | clazz = clazz.getSuperclass(); 268 | } 269 | } 270 | } 271 | 272 | private synchronized TestInfo getTestInfo(final TestCase testCase) { 273 | final Class< ? extends TestCase> clazz = testCase.getClass(); 274 | final Package thePackage = clazz.getPackage(); 275 | final String name = testCase.getName(); 276 | StringBuilder sb = new StringBuilder(); 277 | sb.append(thePackage).append(".").append(clazz.getSimpleName()).append(".").append(name); 278 | final String mapKey = sb.toString(); 279 | TestCaseInfo caseInfo = caseMap.get(thePackage); 280 | if (caseInfo == null) { 281 | caseInfo = new TestCaseInfo(); 282 | caseInfo.testCaseClass = testCase.getClass(); 283 | caseInfo.thePackage = thePackage; 284 | caseMap.put(thePackage, caseInfo); 285 | } 286 | TestInfo ti = caseInfo.testMap.get(mapKey); 287 | if (ti == null) { 288 | ti = new TestInfo(); 289 | ti.name = name; 290 | ti.testCase = testCase.getClass(); 291 | ti.thePackage = thePackage; 292 | caseInfo.testMap.put(mapKey, ti); 293 | } 294 | return ti; 295 | } 296 | 297 | private void startFile(final File outputFile) throws IOException { 298 | Log.d(TAG, "Writing to file " + outputFile); 299 | currentXmlSerializer = Xml.newSerializer(); 300 | currentFileWriter = new PrintWriter(outputFile, "UTF-8"); 301 | currentXmlSerializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); 302 | currentXmlSerializer.setOutput(currentFileWriter); 303 | currentXmlSerializer.startDocument("UTF-8", null); 304 | currentXmlSerializer.startTag(null, TESTSUITES); 305 | } 306 | 307 | private void endFile() throws IOException { 308 | Log.d(TAG, "closing file"); 309 | currentXmlSerializer.endTag(null, TESTSUITES); 310 | currentXmlSerializer.endDocument(); 311 | currentFileWriter.flush(); 312 | currentFileWriter.close(); 313 | } 314 | 315 | private String getTimestamp() { 316 | final long time = System.currentTimeMillis(); 317 | final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); 318 | return sdf.format(time); 319 | } 320 | 321 | private void writeClassToFile(final TestCaseInfo tci) throws IllegalArgumentException, IllegalStateException, 322 | IOException { 323 | final Package thePackage = tci.thePackage; 324 | final Class< ? extends TestCase> clazz = tci.testCaseClass; 325 | final int tests = tci.testMap.size(); 326 | final String timestamp = getTimestamp(); 327 | int errors = 0; 328 | int failures = 0; 329 | int time = 0; 330 | for (final TestInfo testInfo : tci.testMap.values()) { 331 | if (testInfo.error != null) { 332 | errors++; 333 | } 334 | if (testInfo.failure != null) { 335 | failures++; 336 | } 337 | time += testInfo.time; 338 | } 339 | currentXmlSerializer.startTag(null, TESTSUITE); 340 | currentXmlSerializer.attribute(null, ERRORS, Integer.toString(errors)); 341 | currentXmlSerializer.attribute(null, FAILURES, Integer.toString(failures)); 342 | currentXmlSerializer.attribute(null, NAME, clazz.getName()); 343 | currentXmlSerializer.attribute(null, PACKAGE, thePackage == null ? "" : thePackage.getName()); 344 | currentXmlSerializer.attribute(null, TESTS, Integer.toString(tests)); 345 | currentXmlSerializer.attribute(null, TIME, Double.toString(time / 1000.0)); 346 | currentXmlSerializer.attribute(null, TIMESTAMP, timestamp); 347 | for (final TestInfo testInfo : tci.testMap.values()) { 348 | writeTestInfo(testInfo); 349 | } 350 | currentXmlSerializer.startTag(null, PROPERTIES); 351 | writeProperty("android.Build.BOARD", Build.BOARD); 352 | writeProperty("android.Build.BRAND", Build.BRAND); 353 | writeProperty("android.Build.CPU_ABI", Build.CPU_ABI); 354 | writeProperty("android.Build.DEVICE", Build.DEVICE); 355 | writeProperty("android.Build.DISPLAY", Build.DISPLAY); 356 | writeProperty("android.Build.FINGERPRINT", Build.FINGERPRINT); 357 | writeProperty("android.Build.HOST", Build.HOST); 358 | writeProperty("android.Build.ID", Build.ID); 359 | writeProperty("android.Build.MANUFACTURER", Build.MANUFACTURER); 360 | writeProperty("android.Build.MODEL", Build.MODEL); 361 | writeProperty("android.Build.PRODUCT", Build.PRODUCT); 362 | writeProperty("android.Build.TAGS", Build.TAGS); 363 | writeProperty("android.Build.TYPE", Build.TYPE); 364 | writeProperty("android.Build.USER", Build.USER); 365 | if (Build.VERSION.SDK_INT >= 8) { 366 | writeProperty("android.Build.BOOTLOADER", Build.BOOTLOADER); 367 | writeProperty("android.Build.CPU_ABI2", Build.CPU_ABI2); 368 | writeProperty("android.Build.HARDWARE", Build.HARDWARE); 369 | } 370 | if (Build.VERSION.SDK_INT >= 9) { 371 | writeProperty("android.Build.SERIAL", Build.SERIAL); 372 | } 373 | writeProperty("android.Build.VERSION.CODENAME", Build.VERSION.CODENAME); 374 | writeProperty("android.Build.VERSION.INCREMENTAL", Build.VERSION.INCREMENTAL); 375 | writeProperty("android.Build.VERSION.RELEASE", Build.VERSION.RELEASE); 376 | writeProperty("android.Build.VERSION.SDK_INT", Integer.toString(Build.VERSION.SDK_INT)); 377 | final Configuration configuration = getContext().getResources().getConfiguration(); 378 | writeProperty("android.Configuration.fontScale", Float.toString(configuration.fontScale)); 379 | writeProperty("android.Configuration.locale", String.valueOf(configuration.locale)); 380 | writeProperty("android.Configuration.orientation", translateOrientation(configuration.orientation)); 381 | writeProperty("android.Configuration.screenLayout.long", translateScreenLength(configuration.screenLayout)); 382 | writeProperty("android.Configuration.screenLayout.size", translateScreenSize(configuration.screenLayout)); 383 | if (Build.VERSION.SDK_INT >= 13) { 384 | writeProperty("android.Configuration.screenHeightDp", Integer.toString(configuration.screenHeightDp)); 385 | writeProperty("android.Configuration.screenWidthDp", Integer.toString(configuration.screenWidthDp)); 386 | writeProperty("android.Configuration.smallestScreenWidthDp", Integer.toString(configuration.smallestScreenWidthDp)); 387 | } 388 | final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); 389 | writeProperty("android.DisplayMetrics.density", Float.toString(metrics.density)); 390 | writeProperty("android.DisplayMetrics.densityDpi", translateDensityDpi(metrics.densityDpi)); 391 | writeProperty("android.DisplayMetrics.heightPixels", Integer.toString(metrics.heightPixels)); 392 | writeProperty("android.DisplayMetrics.scaledDensity", Float.toString(metrics.scaledDensity)); 393 | writeProperty("android.DisplayMetrics.widthPixels", Integer.toString(metrics.widthPixels)); 394 | writeProperty("android.DisplayMetrics.xdpi", Float.toString(metrics.xdpi)); 395 | writeProperty("android.DisplayMetrics.ydpi", Float.toString(metrics.ydpi)); 396 | writeProperty("java.util.Locale.default", String.valueOf(Locale.getDefault())); 397 | currentXmlSerializer.endTag(null, PROPERTIES); 398 | currentXmlSerializer.startTag(null, SYSTEM_OUT); 399 | currentXmlSerializer.endTag(null, SYSTEM_OUT); 400 | currentXmlSerializer.startTag(null, SYSTEM_ERR); 401 | currentXmlSerializer.endTag(null, SYSTEM_ERR); 402 | currentXmlSerializer.endTag(null, TESTSUITE); 403 | } 404 | 405 | private void writeTestInfo(final TestInfo testInfo) throws IllegalArgumentException, IllegalStateException, 406 | IOException { 407 | currentXmlSerializer.startTag(null, TESTCASE); 408 | currentXmlSerializer.attribute(null, CLASSNAME, testInfo.testCase.getName()); 409 | currentXmlSerializer.attribute(null, NAME, testInfo.name); 410 | currentXmlSerializer.attribute(null, TIME, Double.toString(testInfo.time / 1000.0)); 411 | if (testInfo.error != null) { 412 | currentXmlSerializer.startTag(null, ERROR); 413 | final StringWriter sw = new StringWriter(); 414 | final PrintWriter pw = new PrintWriter(sw, true); 415 | testInfo.error.printStackTrace(pw); 416 | currentXmlSerializer.text(sw.toString()); 417 | currentXmlSerializer.endTag(null, ERROR); 418 | } 419 | if (testInfo.failure != null) { 420 | currentXmlSerializer.startTag(null, FAILURE); 421 | final StringWriter sw = new StringWriter(); 422 | final PrintWriter pw = new PrintWriter(sw, true); 423 | testInfo.failure.printStackTrace(pw); 424 | currentXmlSerializer.text(sw.toString()); 425 | currentXmlSerializer.endTag(null, FAILURE); 426 | } 427 | currentXmlSerializer.endTag(null, TESTCASE); 428 | } 429 | 430 | private File getJunitOutputFile(final Package p) { 431 | return new File(junitOutputDirectory, (p == null ? junitNoPackagePrefix : p.getName()) + junitOutputFilePostfix); 432 | } 433 | 434 | private File getJunitOutputFile() { 435 | return new File(junitOutputDirectory, junitSingleFileName); 436 | } 437 | 438 | private File getJunitOutputFile(final Class< ? extends TestCase> clazz) { 439 | return new File(junitOutputDirectory, clazz.getName() + junitOutputFilePostfix); 440 | } 441 | 442 | private void setDefaultParameters() { 443 | if (junitOutputDirectory == null) { 444 | junitOutputDirectory = getTargetContext().getFilesDir().getAbsolutePath(); 445 | } 446 | if (junitOutputFilePostfix == null) { 447 | junitOutputFilePostfix = DEFAULT_JUNIT_FILE_POSTFIX; 448 | } 449 | if (junitNoPackagePrefix == null) { 450 | junitNoPackagePrefix = DEFAULT_NO_PACKAGE_PREFIX; 451 | } 452 | if (junitSplitLevel == null) { 453 | junitSplitLevel = DEFAULT_SPLIT_LEVEL; 454 | } 455 | if (junitSingleFileName == null) { 456 | junitSingleFileName = DEFAULT_SINGLE_FILE_NAME; 457 | } 458 | } 459 | 460 | private boolean getBooleanArgument(final Bundle arguments, final String tag, final boolean defaultValue) { 461 | final String tagString = arguments.getString(tag); 462 | if (tagString == null) { 463 | return defaultValue; 464 | } 465 | return Boolean.parseBoolean(tagString); 466 | } 467 | 468 | @Override 469 | public void onCreate(final Bundle arguments) { 470 | Log.d(TAG, "Creating the Test Runner with arguments: " + arguments.keySet()); 471 | if (arguments != null) { 472 | junitOutputEnabled = getBooleanArgument(arguments, "junitXmlOutput", true); 473 | junitOutputDirectory = arguments.getString("junitOutputDirectory"); 474 | junitOutputFilePostfix = arguments.getString("junitOutputFilePostfix"); 475 | junitNoPackagePrefix = arguments.getString("junitNoPackagePrefix"); 476 | junitSplitLevel = arguments.getString("junitSplitLevel"); 477 | junitSingleFileName = arguments.getString("junitSingleFileName"); 478 | justCount = getBooleanArgument(arguments, "count", false); 479 | logOnly = getBooleanArgument(arguments, "log", false); 480 | } 481 | setDefaultParameters(); 482 | logParameters(); 483 | createDirectoryIfNotExist(); 484 | deleteOldFiles(); 485 | super.onCreate(arguments); 486 | } 487 | 488 | private void logParameters() { 489 | Log.d(TAG, "Test runner is running with the following parameters:"); 490 | Log.d(TAG, "junitOutputDirectory: " + junitOutputDirectory); 491 | Log.d(TAG, "junitOutputFilePostfix: " + junitOutputFilePostfix); 492 | Log.d(TAG, "junitNoPackagePrefix: " + junitNoPackagePrefix); 493 | Log.d(TAG, "junitSplitLevel: " + junitSplitLevel); 494 | Log.d(TAG, "junitSingleFileName: " + junitSingleFileName); 495 | } 496 | 497 | private boolean createDirectoryIfNotExist(){ 498 | boolean created = false; 499 | Log.d(TAG, "Creating output directory if it does not exist"); 500 | File directory = new File(junitOutputDirectory); 501 | if (!directory.exists()){ 502 | created = directory.mkdirs(); 503 | } 504 | Log.d(TAG, "Created directory? " + created ); 505 | return created; 506 | } 507 | 508 | private void deleteOldFiles() { 509 | Log.d(TAG, "Deleting old files"); 510 | final File[] filesToDelete = new File(junitOutputDirectory).listFiles(new FilenameFilter() { 511 | @Override 512 | public boolean accept(final File dir, final String filename) { 513 | return filename.endsWith(junitOutputFilePostfix) || filename.equals(junitSingleFileName); 514 | } 515 | }); 516 | if (filesToDelete != null){ 517 | Log.d(TAG, "Deleting: " + Arrays.toString(filesToDelete)); 518 | for (final File f : filesToDelete) { 519 | f.delete(); 520 | } 521 | } 522 | } 523 | 524 | @Override 525 | public void finish(final int resultCode, final Bundle results) { 526 | if (outputEnabled) { 527 | Log.d(TAG, "Post processing"); 528 | if (SPLIT_LEVEL_PACKAGE.equals(junitSplitLevel)) { 529 | processPackageLevelSplit(); 530 | } else if (SPLIT_LEVEL_CLASS.equals(junitSplitLevel)) { 531 | processClassLevelSplit(); 532 | } else if (SPLIT_LEVEL_NONE.equals(junitSplitLevel)) { 533 | processNoSplit(); 534 | } else { 535 | Log.d(TAG, "Invalid split level " + junitSplitLevel + ", falling back to package level split."); 536 | processPackageLevelSplit(); 537 | } 538 | } 539 | super.finish(resultCode, results); 540 | } 541 | 542 | private void processNoSplit() { 543 | try { 544 | final File f = getJunitOutputFile(); 545 | startFile(f); 546 | try { 547 | for (final Package p : caseMap.keySet()) { 548 | try { 549 | final TestCaseInfo tc = caseMap.get(p); 550 | writeClassToFile(tc); 551 | } catch (final IOException e) { 552 | Log.e(TAG, "Error: " + e, e); 553 | } 554 | } 555 | } finally { 556 | endFile(); 557 | } 558 | } catch (final IOException e) { 559 | Log.e(TAG, "Error: " + e, e); 560 | } 561 | } 562 | 563 | private void processPackageLevelSplit() { 564 | Log.d(TAG, "Packages: " + caseMap.size()); 565 | for (final Package p : caseMap.keySet()) { 566 | Log.d(TAG, "Processing package " + p); 567 | try { 568 | final File f = getJunitOutputFile(p); 569 | startFile(f); 570 | try { 571 | final TestCaseInfo tc = caseMap.get(p); 572 | writeClassToFile(tc); 573 | } finally { 574 | endFile(); 575 | } 576 | } catch (final IOException e) { 577 | Log.e(TAG, "Error: " + e, e); 578 | } 579 | } 580 | } 581 | 582 | private void processClassLevelSplit() { 583 | for (final Package p : caseMap.keySet()) { 584 | try { 585 | final TestCaseInfo tc = caseMap.get(p); 586 | final File f = getJunitOutputFile(tc.testCaseClass); 587 | startFile(f); 588 | try { 589 | writeClassToFile(tc); 590 | } finally { 591 | endFile(); 592 | } 593 | } catch (final IOException e) { 594 | Log.e(TAG, "Error: " + e, e); 595 | } 596 | } 597 | } 598 | 599 | private void writeProperty(final String name, final String value) throws IOException { 600 | currentXmlSerializer.startTag(null, PROPERTY); 601 | currentXmlSerializer.attribute(null, NAME, name); 602 | currentXmlSerializer.attribute(null, VALUE, value); 603 | currentXmlSerializer.endTag(null, PROPERTY); 604 | } 605 | 606 | private static String translateDensityDpi(final int densityDpi) { 607 | switch (densityDpi) { 608 | case DisplayMetrics.DENSITY_XXHIGH: 609 | return "xxhdpi"; 610 | case DisplayMetrics.DENSITY_XHIGH: 611 | return "xhdpi"; 612 | case DisplayMetrics.DENSITY_HIGH: 613 | return "hdpi"; 614 | case DisplayMetrics.DENSITY_TV: 615 | return "tvdpi"; 616 | case DisplayMetrics.DENSITY_MEDIUM: 617 | return "mdpi"; 618 | case DisplayMetrics.DENSITY_LOW: 619 | return "ldpi"; 620 | } 621 | return Integer.toString(densityDpi); 622 | } 623 | 624 | private static String translateScreenLength(final int screenLayout) { 625 | switch (screenLayout & Configuration.SCREENLAYOUT_LONG_MASK) { 626 | case Configuration.SCREENLAYOUT_LONG_YES: 627 | return "long"; 628 | case Configuration.SCREENLAYOUT_LONG_NO: 629 | return "notlong"; 630 | default: 631 | return "undefined"; 632 | } 633 | } 634 | 635 | private static String translateScreenSize(final int screenLayout) { 636 | switch (screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) { 637 | case Configuration.SCREENLAYOUT_SIZE_XLARGE: 638 | return "xlarge"; 639 | case Configuration.SCREENLAYOUT_SIZE_LARGE: 640 | return "large"; 641 | case Configuration.SCREENLAYOUT_SIZE_NORMAL: 642 | return "normal"; 643 | case Configuration.SCREENLAYOUT_SIZE_SMALL: 644 | return "small"; 645 | default: 646 | return "undefined"; 647 | } 648 | } 649 | 650 | private static String translateOrientation(final int orientation) { 651 | switch (orientation) { 652 | case Configuration.ORIENTATION_PORTRAIT: 653 | return "portrait"; 654 | case Configuration.ORIENTATION_LANDSCAPE: 655 | return "landscape"; 656 | default: 657 | return "undefined"; 658 | } 659 | } 660 | 661 | @Override 662 | public void start() { 663 | Log.d(TAG, "Getting android test runner"); 664 | runner = super.getAndroidTestRunner(); 665 | if (junitOutputEnabled && !justCount && !logOnly) { 666 | Log.d(TAG, "JUnit test output enabled"); 667 | outputEnabled = true; 668 | runner.addTestListener(new JunitTestListener()); 669 | } else { 670 | outputEnabled = false; 671 | Log.d(TAG, "JUnit test output disabled: [ junitOutputEnabled : " + junitOutputEnabled + ", justCount : " 672 | + justCount + ", logOnly : " + logOnly + " ]"); 673 | } 674 | super.start(); 675 | } 676 | 677 | // @Override 678 | // public AndroidTestRunner getAndroidTestRunner() { 679 | // Log.d(TAG, "Getting android test runner"); 680 | // runner = super.getAndroidTestRunner(); 681 | // if (junitOutputEnabled && !justCount && !logOnly) { 682 | // Log.d(TAG, "JUnit test output enabled"); 683 | // outputEnabled = true; 684 | // runner.addTestListener(new JunitTestListener()); 685 | // } else { 686 | // outputEnabled = false; 687 | // Log.d(TAG, "JUnit test output disabled: [ junitOutputEnabled : " + junitOutputEnabled + ", justCount : " 688 | // + justCount + ", logOnly : " + logOnly + " ]"); 689 | // } 690 | // return runner; 691 | // } 692 | } 693 | --------------------------------------------------------------------------------