TestLoader.java
/*
* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.test.internal.runner;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.test.internal.runner.junit3.AndroidJUnit3Builder;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.junit.runner.Description;
import org.junit.runner.Runner;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.RunnerBuilder;
/** A class for loading JUnit3 and JUnit4 test classes given a set of potential class names. */
class TestLoader {
private static final String LOG_TAG = "TestLoader";
private final ClassLoader classLoader;
private final RunnerBuilder runnerBuilder;
private final Map<String, Runner> runnersMap = new LinkedHashMap<>();
static TestLoader testLoader(
ClassLoader classLoader, RunnerBuilder runnerBuilder, boolean scanningPath) {
// If scanning then wrap the supplied RunnerBuilder with one that will ignore abstract
// classes.
if (scanningPath) {
runnerBuilder = new ScanningRunnerBuilder(runnerBuilder);
}
if (null == classLoader) {
classLoader = TestLoader.class.getClassLoader();
}
return new TestLoader(classLoader, runnerBuilder);
}
private TestLoader(ClassLoader classLoader, RunnerBuilder runnerBuilder) {
this.classLoader = classLoader;
this.runnerBuilder = runnerBuilder;
}
private void doCreateRunner(String className, boolean isScanningPath) {
if (runnersMap.containsKey(className)) {
// Class with the same name was already loaded, return
return;
}
Runner runner;
try {
Class<?> loadedClass = Class.forName(className, false, classLoader);
runner = runnerBuilder.safeRunnerForClass(loadedClass);
if (null == runner) {
logDebug(String.format("Skipping class %s: not a test", loadedClass.getName()));
} else if (runner == AndroidJUnit3Builder.NOT_A_VALID_TEST) {
logDebug(String.format("Skipping class %s: not a valid test", loadedClass.getName()));
runner = null;
}
// Can get NoClassDefFoundError on Android L when a class extends a non-existent class.
} catch (ClassNotFoundException | LinkageError e) {
String errMsg = String.format("Could not find class: %s", className);
Log.e(LOG_TAG, errMsg);
Description description = Description.createSuiteDescription(className);
Failure failure = new Failure(description, e);
runner = null;
if (!isScanningPath) {
// If we're not scanning all paths it means that a user provided an explicit class
// via the -e com.foo.ClassName runner argument. Therefore, treat it as a valid
// test case and report failure accordingly.
runner = new UnloadableClassRunner(description, failure);
}
}
if (runner != null) {
runnersMap.put(className, runner);
}
}
/**
* Get the {@link Collection) of {@link Runner runners}.
*/
List<Runner> getRunnersFor(Collection<String> classNames, boolean isScanningPath) {
for (String className : classNames) {
doCreateRunner(className, isScanningPath);
}
return new ArrayList<>(runnersMap.values());
}
/**
* Utility method for logging debug messages. Only actually logs a message if LOG_TAG is marked as
* loggable to limit log spam during normal use.
*/
private static void logDebug(String msg) {
if (Log.isLoggable(LOG_TAG, Log.DEBUG)) {
Log.d(LOG_TAG, msg);
}
}
/**
* Wrapper around a {@link RunnerBuilder} that will reject all abstract classes.
*
* <p>This is only used when loading classes found while scanning the class path.
*/
private static class ScanningRunnerBuilder extends RunnerBuilder {
private final RunnerBuilder runnerBuilder;
ScanningRunnerBuilder(RunnerBuilder runnerBuilder) {
this.runnerBuilder = runnerBuilder;
}
@Override
public Runner runnerForClass(Class<?> testClass) throws Throwable {
// Ignore abstract classes. This could theoretically ignore test classes that should be
// run, as some RunnerBuilders e.g. Suite do not strictly require the class to be
// instantiable. However, they have always been ignored during scanning and changing
// that would cause lots of problems.
if (Modifier.isAbstract(testClass.getModifiers())) {
logDebug(String.format("Skipping abstract class %s: not a test", testClass.getName()));
return null;
}
return runnerBuilder.runnerForClass(testClass);
}
}
@VisibleForTesting
static class UnloadableClassRunner extends Runner {
private final Description description;
private final Failure failure;
UnloadableClassRunner(Description description, Failure failure) {
this.description = description;
this.failure = failure;
}
@Override
public Description getDescription() {
return description;
}
@Override
public void run(RunNotifier notifier) {
notifier.fireTestStarted(description);
notifier.fireTestFailure(failure);
notifier.fireTestFinished(description);
}
}
}