RobolectricTestRunner.java revision 92bd76ae9b5439412a2bee3d3bad45319090015d
1package com.xtremelabs.robolectric;
2
3import android.app.Application;
4import android.net.Uri__FromAndroid;
5import com.xtremelabs.robolectric.bytecode.ClassHandler;
6import com.xtremelabs.robolectric.bytecode.RobolectricClassLoader;
7import com.xtremelabs.robolectric.bytecode.ShadowWrangler;
8import com.xtremelabs.robolectric.internal.RealObject;
9import com.xtremelabs.robolectric.internal.RobolectricTestRunnerInterface;
10import com.xtremelabs.robolectric.res.ResourceLoader;
11import com.xtremelabs.robolectric.shadows.ShadowApplication;
12import com.xtremelabs.robolectric.shadows.ShadowLog;
13import com.xtremelabs.robolectric.util.DatabaseConfig;
14import com.xtremelabs.robolectric.util.DatabaseConfig.DatabaseMap;
15import com.xtremelabs.robolectric.util.DatabaseConfig.UsingDatabaseMap;
16import com.xtremelabs.robolectric.util.SQLiteMap;
17import javassist.Loader;
18import org.junit.runners.BlockJUnit4ClassRunner;
19import org.junit.runners.model.FrameworkMethod;
20import org.junit.runners.model.InitializationError;
21import org.junit.runners.model.Statement;
22import org.w3c.dom.Document;
23import org.xml.sax.SAXException;
24
25import javax.xml.parsers.DocumentBuilder;
26import javax.xml.parsers.DocumentBuilderFactory;
27import javax.xml.parsers.ParserConfigurationException;
28import java.io.*;
29import java.lang.annotation.Annotation;
30import java.lang.reflect.Constructor;
31import java.lang.reflect.Field;
32import java.lang.reflect.Method;
33import java.lang.reflect.Modifier;
34import java.util.HashMap;
35import java.util.Map;
36
37/**
38 * Installs a {@link RobolectricClassLoader} and {@link com.xtremelabs.robolectric.res.ResourceLoader} in order to
39 * provide a simulation of the Android runtime environment.
40 */
41public class RobolectricTestRunner extends BlockJUnit4ClassRunner implements RobolectricTestRunnerInterface {
42
43    /** Instrument detector. We use it to check whether the current instance is instrumented. */
44  	private static InstrumentDetector instrumentDetector = InstrumentDetector.DEFAULT;
45
46    private static RobolectricClassLoader defaultLoader;
47    private static Map<RobolectricConfig, ResourceLoader> resourceLoaderForRootAndDirectory = new HashMap<RobolectricConfig, ResourceLoader>();
48
49    // fields in the RobolectricTestRunner in the original ClassLoader
50    private RobolectricClassLoader classLoader;
51    private ClassHandler classHandler;
52    private RobolectricTestRunnerInterface delegate;
53    private DatabaseMap databaseMap;
54
55	// fields in the RobolectricTestRunner in the instrumented ClassLoader
56    protected RobolectricConfig robolectricConfig;
57
58    private static RobolectricClassLoader getDefaultLoader() {
59        if (defaultLoader == null) {
60            defaultLoader = new RobolectricClassLoader(ShadowWrangler.getInstance());
61        }
62        return defaultLoader;
63    }
64
65    public static void setInstrumentDetector(final InstrumentDetector detector) {
66      instrumentDetector = detector;
67    }
68
69    public static void setDefaultLoader(Loader robolectricClassLoader) {
70    	//used by the RoboSpecs project to allow for mixed scala\java tests to be run with Maven Surefire (see the RoboSpecs project on github)
71        if (defaultLoader == null) {
72            defaultLoader = (RobolectricClassLoader)robolectricClassLoader;
73        } else throw new RuntimeException("You may not set the default robolectricClassLoader unless it is null!");
74    }
75
76    /**
77     * Call this if you would like Robolectric to rewrite additional classes and turn them
78     * into "do nothing" classes which proxy all method calls to shadow classes, just like it does
79     * with the android classes by default.
80     *
81     * @param classOrPackageToBeInstrumented fully-qualified class or package name
82     */
83    protected static void addClassOrPackageToInstrument(String classOrPackageToBeInstrumented) {
84        if (!isInstrumented()) {
85            defaultLoader.addCustomShadowClass(classOrPackageToBeInstrumented);
86        }
87    }
88
89    /**
90     * Creates a runner to run {@code testClass}. Looks in your working directory for your AndroidManifest.xml file
91     * and res directory.
92     *
93     * @param testClass the test class to be run
94     * @throws InitializationError if junit says so
95     */
96    public RobolectricTestRunner(final Class<?> testClass) throws InitializationError {
97        this(testClass, new RobolectricConfig(new File(".")));
98    }
99
100    /**
101     * Call this constructor in subclasses in order to specify non-default configuration (e.g. location of the
102     * AndroidManifest.xml file and resource directory).
103     *
104     * @param testClass         the test class to be run
105     * @param robolectricConfig the configuration data
106     * @throws InitializationError if junit says so
107     */
108    protected RobolectricTestRunner(final Class<?> testClass, final RobolectricConfig robolectricConfig)
109            throws InitializationError {
110        this(testClass,
111                isInstrumented() ? null : ShadowWrangler.getInstance(),
112                isInstrumented() ? null : getDefaultLoader(),
113                robolectricConfig, new SQLiteMap());
114    }
115
116    /**
117     * Call this constructor in subclasses in order to specify non-default configuration (e.g. location of the
118     * AndroidManifest.xml file, resource directory, and DatabaseMap).
119     *
120     * @param testClass         the test class to be run
121     * @param robolectricConfig the configuration data
122     * @param databaseMap		the database mapping
123     * @throws InitializationError if junit says so
124     */
125    protected RobolectricTestRunner(Class<?> testClass, RobolectricConfig robolectricConfig, DatabaseMap databaseMap)
126            throws InitializationError {
127        this(testClass,
128                isInstrumented() ? null : ShadowWrangler.getInstance(),
129                isInstrumented() ? null : getDefaultLoader(),
130                robolectricConfig, databaseMap);
131    }
132
133    /**
134     * Call this constructor in subclasses in order to specify the project root directory.
135     *
136     * @param testClass          the test class to be run
137     * @param androidProjectRoot the directory containing your AndroidManifest.xml file and res dir
138     * @throws InitializationError if the test class is malformed
139     */
140    public RobolectricTestRunner(final Class<?> testClass, final File androidProjectRoot) throws InitializationError {
141        this(testClass, new RobolectricConfig(androidProjectRoot));
142    }
143
144    /**
145     * Call this constructor in subclasses in order to specify the project root directory.
146     *
147     * @param testClass          the test class to be run
148     * @param androidProjectRoot the directory containing your AndroidManifest.xml file and res dir
149     * @throws InitializationError if junit says so
150     * @deprecated Use {@link #RobolectricTestRunner(Class, File)} instead.
151     */
152    @Deprecated
153    public RobolectricTestRunner(final Class<?> testClass, final String androidProjectRoot) throws InitializationError {
154        this(testClass, new RobolectricConfig(new File(androidProjectRoot)));
155    }
156
157    /**
158     * Call this constructor in subclasses in order to specify the location of the AndroidManifest.xml file and the
159     * resource directory. The #androidManifestPath is used to locate the AndroidManifest.xml file which, in turn,
160     * contains package name for the {@code R} class which contains the identifiers for all of the resources. The
161     * resource directory is where the resource loader will look for resources to load.
162     *
163     * @param testClass           the test class to be run
164     * @param androidManifestPath the AndroidManifest.xml file
165     * @param resourceDirectory   the directory containing the project's resources
166     * @throws InitializationError if junit says so
167     */
168    protected RobolectricTestRunner(final Class<?> testClass, final File androidManifestPath, final File resourceDirectory)
169            throws InitializationError {
170        this(testClass, new RobolectricConfig(androidManifestPath, resourceDirectory));
171    }
172
173    /**
174     * Call this constructor in subclasses in order to specify the location of the AndroidManifest.xml file and the
175     * resource directory. The #androidManifestPath is used to locate the AndroidManifest.xml file which, in turn,
176     * contains package name for the {@code R} class which contains the identifiers for all of the resources. The
177     * resource directory is where the resource loader will look for resources to load.
178     *
179     * @param testClass           the test class to be run
180     * @param androidManifestPath the relative path to the AndroidManifest.xml file
181     * @param resourceDirectory   the relative path to the directory containing the project's resources
182     * @throws InitializationError if junit says so
183     * @deprecated Use {@link #RobolectricTestRunner(Class, File, File)} instead.
184     */
185    @Deprecated
186    protected RobolectricTestRunner(final Class<?> testClass, final String androidManifestPath, final String resourceDirectory)
187            throws InitializationError {
188        this(testClass, new RobolectricConfig(new File(androidManifestPath), new File(resourceDirectory)));
189    }
190
191    protected RobolectricTestRunner(Class<?> testClass, ClassHandler classHandler, RobolectricClassLoader classLoader, RobolectricConfig robolectricConfig) throws InitializationError {
192        this(testClass, classHandler, classLoader, robolectricConfig, new SQLiteMap());
193    }
194
195
196    /**
197     * This is not the constructor you are looking for... probably. This constructor creates a bridge between the test
198     * runner called by JUnit and a second instance of the test runner that is loaded via the instrumenting class
199     * loader. This instrumented instance of the test runner, along with the instrumented instance of the actual test,
200     * provides access to Robolectric's features and the un-instrumented instance of the test runner delegates most of
201     * the interesting test runner behavior to it. Providing your own class handler and class loader here in order to
202     * get different functionality is a difficult and dangerous project. If you need to customize the project root and
203     * resource directory, use {@link #RobolectricTestRunner(Class, String, String)}. For other extensions, consider
204     * creating a subclass and overriding the documented methods of this class.
205     *
206     * @param testClass         the test class to be run
207     * @param classHandler      the {@link ClassHandler} to use to in shadow delegation
208     * @param classLoader       the {@link RobolectricClassLoader}
209     * @param robolectricConfig the configuration
210     * @throws InitializationError if junit says so
211     */
212    protected RobolectricTestRunner(final Class<?> testClass, final ClassHandler classHandler, final RobolectricClassLoader classLoader, final RobolectricConfig robolectricConfig, final DatabaseMap map) throws InitializationError {
213        super(isInstrumented() ? testClass : classLoader.bootstrap(testClass));
214
215        if (!isInstrumented()) {
216            this.classHandler = classHandler;
217            this.classLoader = classLoader;
218            this.robolectricConfig = robolectricConfig;
219            this.databaseMap = setupDatabaseMap(testClass, map);
220
221            Thread.currentThread().setContextClassLoader(classLoader);
222
223            delegateLoadingOf(Uri__FromAndroid.class.getName());
224            delegateLoadingOf(RobolectricTestRunnerInterface.class.getName());
225            delegateLoadingOf(RealObject.class.getName());
226            delegateLoadingOf(ShadowWrangler.class.getName());
227            delegateLoadingOf(RobolectricConfig.class.getName());
228            delegateLoadingOf(DatabaseMap.class.getName());
229            delegateLoadingOf(android.R.class.getName());
230
231            Class<?> delegateClass = classLoader.bootstrap(this.getClass());
232            try {
233                Constructor<?> constructorForDelegate = delegateClass.getConstructor(Class.class);
234                this.delegate = (RobolectricTestRunnerInterface) constructorForDelegate.newInstance(classLoader.bootstrap(testClass));
235                this.delegate.setRobolectricConfig(robolectricConfig);
236                this.delegate.setDatabaseMap(databaseMap);
237            } catch (Exception e) {
238                throw new RuntimeException(e);
239            }
240        }
241    }
242
243    protected static boolean isInstrumented() {
244        return instrumentDetector.isInstrumented();
245    }
246
247    /**
248     * Only used when creating the delegate instance within the instrumented ClassLoader.
249     * <p/>
250     * This is not the constructor you are looking for.
251     */
252    @SuppressWarnings({"UnusedDeclaration", "JavaDoc"})
253    protected RobolectricTestRunner(final Class<?> testClass, final ClassHandler classHandler, final RobolectricConfig robolectricConfig) throws InitializationError {
254        super(testClass);
255        this.classHandler = classHandler;
256        this.robolectricConfig = robolectricConfig;
257    }
258
259    public static void setStaticValue(Class<?> clazz, String fieldName, Object value) {
260        try {
261            Field field = clazz.getField(fieldName);
262            field.setAccessible(true);
263
264            Field modifiersField = Field.class.getDeclaredField("modifiers");
265            modifiersField.setAccessible(true);
266            modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
267
268            field.set(null, value);
269        } catch (Exception e) {
270            throw new RuntimeException(e);
271        }
272    }
273
274    protected void delegateLoadingOf(final String className) {
275        classLoader.delegateLoadingOf(className);
276    }
277
278    @Override protected Statement methodBlock(final FrameworkMethod method) {
279        setupI18nStrictState(method.getMethod(), robolectricConfig);
280
281    	if (classHandler != null) {
282            classHandler.configure(robolectricConfig);
283            classHandler.beforeTest();
284        }
285        delegate.internalBeforeTest(method.getMethod());
286
287        final Statement statement = super.methodBlock(method);
288        return new Statement() {
289            @Override public void evaluate() throws Throwable {
290                // todo: this try/finally probably isn't right -- should mimic RunAfters? [xw]
291                try {
292                    statement.evaluate();
293                } finally {
294                    delegate.internalAfterTest(method.getMethod());
295                    if (classHandler != null) {
296                        classHandler.afterTest();
297                    }
298                }
299            }
300        };
301    }
302
303    /*
304     * Called before each test method is run. Sets up the simulation of the Android runtime environment.
305     */
306    @Override public void internalBeforeTest(final Method method) {
307        setupApplicationState(robolectricConfig);
308
309        beforeTest(method);
310    }
311
312    @Override public void internalAfterTest(final Method method) {
313        afterTest(method);
314    }
315
316    @Override public void setRobolectricConfig(final RobolectricConfig robolectricConfig) {
317        this.robolectricConfig = robolectricConfig;
318    }
319
320    /**
321     * Called before each test method is run.
322     *
323     * @param method the test method about to be run
324     */
325    public void beforeTest(final Method method) {
326    }
327
328    /**
329     * Called after each test method is run.
330     *
331     * @param method the test method that just ran.
332     */
333    public void afterTest(final Method method) {
334    }
335
336    /**
337     * You probably don't want to override this method. Override #prepareTest(Object) instead.
338     *
339     * @see BlockJUnit4ClassRunner#createTest()
340     */
341    @Override
342    public Object createTest() throws Exception {
343        if (delegate != null) {
344            return delegate.createTest();
345        } else {
346            Object test = super.createTest();
347            prepareTest(test);
348            return test;
349        }
350    }
351
352    public void prepareTest(final Object test) {
353    }
354
355    public void setupApplicationState(final RobolectricConfig robolectricConfig) {
356        setupLogging();
357        ResourceLoader resourceLoader = createResourceLoader(robolectricConfig);
358
359        Robolectric.bindDefaultShadowClasses();
360        bindShadowClasses();
361
362        resourceLoader.setLayoutQualifierSearchPath();
363        Robolectric.resetStaticState();
364        resetStaticState();
365
366        DatabaseConfig.setDatabaseMap(this.databaseMap);//Set static DatabaseMap in DBConfig
367
368        Robolectric.application = ShadowApplication.bind(createApplication(), resourceLoader);
369    }
370
371
372
373    /**
374     * Override this method to bind your own shadow classes
375     */
376    protected void bindShadowClasses() {
377    }
378
379    /**
380     * Override this method to reset the state of static members before each test.
381     */
382    protected void resetStaticState() {
383    }
384
385    /**
386     * Sets Robolectric config to determine if Robolectric should blacklist API calls that are not
387     * I18N/L10N-safe.
388     * <p/>
389     * I18n-strict mode affects suitably annotated shadow methods. Robolectric will throw exceptions
390     * if these methods are invoked by application code. Additionally, Robolectric's ResourceLoader
391     * will throw exceptions if layout resources use bare string literals instead of string resource IDs.
392     * <p/>
393     * To enable or disable i18n-strict mode for specific test cases, annotate them with
394     * {@link com.xtremelabs.robolectric.annotation.EnableStrictI18n} or
395     * {@link com.xtremelabs.robolectric.annotation.DisableStrictI18n}.
396     * <p/>
397     *
398     * By default, I18n-strict mode is disabled.
399     *
400     * @param method
401     * @param robolectricConfig
402     */
403    private void setupI18nStrictState(Method method, RobolectricConfig robolectricConfig) {
404    	// Global
405    	boolean strictI18n = globalI18nStrictEnabled();
406
407    	// Test case class
408    	Annotation[] annos = method.getDeclaringClass().getAnnotations();
409    	strictI18n = lookForI18nAnnotations(strictI18n, annos);
410
411    	// Test case methods
412    	annos = method.getAnnotations();
413    	strictI18n = lookForI18nAnnotations(strictI18n, annos);
414
415		robolectricConfig.setStrictI18n(strictI18n);
416    }
417
418    /**
419     * Default implementation of global switch for i18n-strict mode.
420     * To enable i18n-strict mode globally, set the system property
421     * "robolectric.strictI18n" to true. This can be done via java
422     * system properties in either Ant or Maven.
423     * <p/>
424     * Subclasses can override this method and establish their own policy
425     * for enabling i18n-strict mode.
426     *
427     * @return
428     */
429    protected boolean globalI18nStrictEnabled() {
430    	return Boolean.valueOf(System.getProperty("robolectric.strictI18n"));
431    }
432
433    /**
434     * As test methods are loaded by the delegate's class loader, the normal
435 	 * method#isAnnotationPresent test fails. Look at string versions of the
436     * annotation names to test for their presence.
437     *
438     * @param strictI18n
439     * @param annos
440     * @return
441     */
442	private boolean lookForI18nAnnotations(boolean strictI18n, Annotation[] annos) {
443		for ( int i = 0; i < annos.length; i++ ) {
444    		String name = annos[i].annotationType().getName();
445    		if (name.equals("com.xtremelabs.robolectric.annotation.EnableStrictI18n")) {
446    			strictI18n = true;
447    			break;
448    		}
449    		if (name.equals("com.xtremelabs.robolectric.annotation.DisableStrictI18n")) {
450    			strictI18n = false;
451    			break;
452    		}
453    	}
454		return strictI18n;
455	}
456
457    private void setupLogging() {
458        String logging = System.getProperty("robolectric.logging");
459        if (logging != null && ShadowLog.stream == null) {
460            PrintStream stream = null;
461            if ("stdout".equalsIgnoreCase(logging)) {
462                stream = System.out;
463            } else if ("stderr".equalsIgnoreCase(logging)) {
464                stream = System.err;
465            } else {
466                try {
467                    final PrintStream file = new PrintStream(new FileOutputStream(logging));
468                    stream = file;
469                    Runtime.getRuntime().addShutdownHook(new Thread() {
470                        @Override public void run() {
471                            try { file.close(); } catch (Exception ignored) { }
472                        }
473                    });
474                } catch (IOException e) {
475                    e.printStackTrace();
476                }
477            }
478            ShadowLog.stream = stream;
479        }
480    }
481
482    /**
483     * Override this method if you want to provide your own implementation of Application.
484     * <p/>
485     * This method attempts to instantiate an application instance as specified by the AndroidManifest.xml.
486     *
487     * @return An instance of the Application class specified by the ApplicationManifest.xml or an instance of
488     *         Application if not specified.
489     */
490    protected Application createApplication() {
491        return new ApplicationResolver(robolectricConfig).resolveApplication();
492    }
493
494    private ResourceLoader createResourceLoader(final RobolectricConfig robolectricConfig) {
495        ResourceLoader resourceLoader = resourceLoaderForRootAndDirectory.get(robolectricConfig);
496        if (resourceLoader == null) {
497            try {
498                robolectricConfig.validate();
499
500                String rClassName = robolectricConfig.getRClassName();
501                Class rClass = Class.forName(rClassName);
502                resourceLoader = new ResourceLoader(robolectricConfig.getRealSdkVersion(), rClass, robolectricConfig.getResourceDirectory(), robolectricConfig.getAssetsDirectory());
503                resourceLoaderForRootAndDirectory.put(robolectricConfig, resourceLoader);
504            } catch (Exception e) {
505                throw new RuntimeException(e);
506            }
507        }
508
509        resourceLoader.setStrictI18n(robolectricConfig.getStrictI18n());
510        return resourceLoader;
511    }
512
513    private String findResourcePackageName(final File projectManifestFile) throws ParserConfigurationException, IOException, SAXException {
514        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
515        DocumentBuilder db = dbf.newDocumentBuilder();
516        Document doc = db.parse(projectManifestFile);
517
518        String projectPackage = doc.getElementsByTagName("manifest").item(0).getAttributes().getNamedItem("package").getTextContent();
519
520        return projectPackage + ".R";
521    }
522
523    /*
524     * Specifies what database to use for testing (ex: H2 or Sqlite),
525     * this will load H2 by default, the SQLite TestRunner version will override this.
526     */
527    protected DatabaseMap setupDatabaseMap(Class<?> testClass, DatabaseMap map) {
528    	DatabaseMap dbMap = map;
529
530    	if (testClass.isAnnotationPresent(UsingDatabaseMap.class)) {
531	    	UsingDatabaseMap usingMap = testClass.getAnnotation(UsingDatabaseMap.class);
532	    	if(usingMap.value()!=null){
533	    		dbMap = Robolectric.newInstanceOf(usingMap.value());
534	    	} else {
535	    		if (dbMap==null)
536		    		throw new RuntimeException("UsingDatabaseMap annotation value must provide a class implementing DatabaseMap");
537	    	}
538    	}
539    	return dbMap;
540    }
541
542    public DatabaseMap getDatabaseMap() {
543		return databaseMap;
544	}
545
546	public void setDatabaseMap(DatabaseMap databaseMap) {
547		this.databaseMap = databaseMap;
548	}
549
550	/**
551	 * Detects whether current instance is already instrumented.
552	 */
553	public interface InstrumentDetector {
554
555	    /** Default detector. */
556	    InstrumentDetector DEFAULT = new InstrumentDetector() {
557	        @Override
558	        public boolean isInstrumented() {
559	            return RobolectricTestRunner.class.getClassLoader().getClass().getName().contains(RobolectricClassLoader.class.getName());
560	        }
561	    };
562
563	    /**
564	     * @return true if current instance is already instrumented
565	     */
566	    boolean isInstrumented();
567
568	}
569
570}
571