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