chromium/base/test/android/junit/src/org/chromium/base/test/util/TestRunnerTestRule.java

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.base.test.util;

import static org.hamcrest.Matchers.isIn;
import static org.junit.Assert.fail;

import android.app.Instrumentation;
import android.content.Context;
import android.os.Bundle;

import androidx.test.InstrumentationRegistry;
import androidx.test.core.app.ApplicationProvider;

import org.junit.Assert;
import org.junit.rules.ExternalResource;
import org.junit.runner.Description;
import org.junit.runner.Runner;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.InitializationError;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;

/**
 * Helper rule to allow executing test runners in tests.
 *
 * Quis probat ipsas probas?
 */
class TestRunnerTestRule extends ExternalResource {
    final Class<? extends BlockJUnit4ClassRunner> mRunnerClass;

    /**
     * @param runnerClass The runner class to run the test
     */
    TestRunnerTestRule(Class<? extends BlockJUnit4ClassRunner> runnerClass) {
        mRunnerClass = runnerClass;
    }

    @Override
    protected void before() {
        // Register a fake Instrumentation so that class runners for instrumentation tests
        // can be run even in Robolectric tests.
        Instrumentation instrumentation =
                new Instrumentation() {
                    @Override
                    public Context getTargetContext() {
                        return ApplicationProvider.getApplicationContext();
                    }
                };
        InstrumentationRegistry.registerInstance(instrumentation, new Bundle());
    }

    @Override
    protected void after() {
        InstrumentationRegistry.registerInstance(null, new Bundle());
    }

    /** A struct-like class containing lists of run and skipped tests. */
    public static class TestLog {
        public final List<Description> runTests = new ArrayList<>();
        public final List<Description> skippedTests = new ArrayList<>();
    }

    /**
     * Creates a new test runner and executes the test in the given {@code testClass} on it,
     * returning lists of tests that were run and tests that were skipped.
     *
     * @param testClass The test class
     * @return A {@link TestLog} that contains lists of run and skipped tests.
     */
    public TestLog runTest(Class<?> testClass)
            throws InvocationTargetException,
                    NoSuchMethodException,
                    InstantiationException,
                    IllegalAccessException {
        TestLog testLog = new TestLog();

        // TODO(bauerb): Using Mockito mock() or spy() throws a ClassCastException.
        RunListener runListener =
                new RunListener() {
                    @Override
                    public void testStarted(Description description) {
                        testLog.runTests.add(description);
                    }

                    @Override
                    public void testFinished(Description description) {
                        Assert.assertThat(description, isIn(testLog.runTests));
                    }

                    @Override
                    public void testFailure(Failure failure) {
                        fail(failure.toString());
                    }

                    @Override
                    public void testAssumptionFailure(Failure failure) {
                        fail(failure.toString());
                    }

                    @Override
                    public void testIgnored(Description description) {
                        testLog.skippedTests.add(description);
                    }
                };
        RunNotifier runNotifier = new RunNotifier();
        runNotifier.addListener(runListener);
        Runner runner;
        try {
            runner = mRunnerClass.getConstructor(Class.class).newInstance(testClass);
        } catch (InvocationTargetException e) {
            // If constructing the runner caused initialization errors, unwrap them from the
            // InvocationTargetException.
            Throwable cause = e.getCause();
            if (!(cause instanceof InitializationError)) throw e;
            List<Throwable> causes = ((InitializationError) cause).getCauses();

            // If there was exactly one initialization error, rewrap that one.
            if (causes.size() == 1) {
                throw new InvocationTargetException(causes.get(0), "Initialization error");
            }

            // Otherwise, serialize all initialization errors to a string and throw that.
            throw new AssertionError(causes.toString());
        }
        runner.run(runNotifier);
        return testLog;
    }
}