chromium/android_webview/test/shell/src/org/chromium/android_webview/test/AwJUnit4ClassRunner.java

// Copyright 2017 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.android_webview.test;

import androidx.annotation.CallSuper;
import androidx.test.InstrumentationRegistry;

import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;

import org.chromium.android_webview.common.AwSwitches;
import org.chromium.android_webview.test.OnlyRunIn.ProcessMode;
import org.chromium.base.CommandLine;
import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.components.policy.test.annotations.Policies;

import java.util.ArrayList;
import java.util.List;

/**
 * A custom runner for //android_webview instrumentation tests. Because WebView runs in
 * single-process mode on L-N and multi-process (AKA sandboxed renderer) mode on O+, this test
 * runner defaults to duplicating each test in both modes.
 *
 * <p>Tests can instead be run in single and/or multi process modes with {@link OnlyRunIn}:
 *
 * <ul>
 *   <li>SINGLE_PROCESS | MULTI_PROCESS: Run test only in single or multi process mode. This should
 *       only be used if the test case needs this for correctness.
 *   <li>EITHER_PROCESS: Run test in either single or multi process mode. This can be used to save
 *       infrastructure resources if the code being tested does not depend on the renderer process
 *       so the test will not benefit from duplication
 *   <li>SINGLE_AND_MULTI_PROCESS: Run test in both single and multi process modes.
 * </ul>
 */
public class AwJUnit4ClassRunner extends BaseJUnit4ClassRunner {
    // This should match the definition in Android test runner scripts: bit.ly/3ynoREM
    private static final String MULTIPROCESS_TEST_NAME_SUFFIX = "__multiprocess_mode";

    // Accepted choices are "single" or "multiple", indicating that only tests
    // runnable in the specified process mode should run once in the specified mode.
    // The argument works at listing test stage in `List(...)`.
    private static final String PROCESS_MODE_FLAG = "AwJUnit4ClassRunner.ProcessMode";

    private final TestHook mWebViewMultiProcessHook =
            (targetContext, testMethod) -> {
                if (testMethod instanceof WebViewMultiProcessFrameworkMethod) {
                    CommandLine.getInstance().appendSwitch(AwSwitches.WEBVIEW_SANDBOXED_RENDERER);
                }
            };

    /**
     * Create an AwJUnit4ClassRunner to run {@code klass} and initialize values
     *
     * @param klass Test class to run
     * @throws InitializationError if the test class is malformed
     */
    public AwJUnit4ClassRunner(Class<?> klass) throws InitializationError {
        super(klass);
    }

    @CallSuper
    @Override
    protected List<TestHook> getPreTestHooks() {
        return addToList(
                super.getPreTestHooks(), Policies.getRegistrationHook(), mWebViewMultiProcessHook);
    }

    private ProcessMode processModeForMethod(FrameworkMethod method) {
        // Prefer per-method annotations.
        //
        // Note: a per-class annotation might disagree with this. This can be confusing, but there's
        // no good option for us to forbid this (users won't see any exceptions we throw), and so we
        // instead enforce a priority.
        OnlyRunIn methodAnnotation = method.getAnnotation(OnlyRunIn.class);
        if (methodAnnotation != null) {
            return methodAnnotation.value();
        }
        // Per-class annotations have second priority.
        OnlyRunIn classAnnotation = method.getDeclaringClass().getAnnotation(OnlyRunIn.class);
        if (classAnnotation != null) {
            return classAnnotation.value();
        }
        // Default: run in both modes.
        return ProcessMode.SINGLE_AND_MULTI_PROCESS;
    }

    @Override
    protected List<FrameworkMethod> getChildren() {
        String processModeToExecute =
                InstrumentationRegistry.getArguments().getString(PROCESS_MODE_FLAG);
        boolean runSingleProcess =
                (processModeToExecute == null || "single".equals(processModeToExecute));
        boolean runMultiProcess =
                (processModeToExecute == null || "multiple".equals(processModeToExecute));
        if (!runSingleProcess && !runMultiProcess) {
            throw new IllegalArgumentException(
                    "'AwJUnit4ClassRunner.ProcessMode' value should be 'single' or 'multiple'.");
        }
        List<FrameworkMethod> singleProcessResults = new ArrayList<>();
        List<FrameworkMethod> multiProcessResults = new ArrayList<>();
        for (FrameworkMethod method : computeTestMethods()) {
            switch (processModeForMethod(method)) {
                case SINGLE_PROCESS:
                    singleProcessResults.add(method);
                    break;
                case MULTI_PROCESS:
                case EITHER_PROCESS:
                    multiProcessResults.add(new WebViewMultiProcessFrameworkMethod(method));
                    break;
                case SINGLE_AND_MULTI_PROCESS:
                default:
                    multiProcessResults.add(new WebViewMultiProcessFrameworkMethod(method));
                    singleProcessResults.add(method);
                    break;
            }
        }
        List<FrameworkMethod> result = new ArrayList<>();
        if (runSingleProcess) {
            result.addAll(singleProcessResults);
        }
        if (runMultiProcess) {
            result.addAll(multiProcessResults);
        }
        return result;
    }

    /**
     * Custom FrameworkMethod class indicate this test method will run in multiprocess mode.
     *
     * The class also adds MULTIPROCESS_TEST_NAME_SUFFIX postfix to the test name.
     */
    private static class WebViewMultiProcessFrameworkMethod extends FrameworkMethod {
        public WebViewMultiProcessFrameworkMethod(FrameworkMethod method) {
            super(method.getMethod());
        }

        @Override
        public String getName() {
            return super.getName() + MULTIPROCESS_TEST_NAME_SUFFIX;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof WebViewMultiProcessFrameworkMethod) {
                WebViewMultiProcessFrameworkMethod method =
                        (WebViewMultiProcessFrameworkMethod) obj;
                return super.equals(obj) && method.getName().equals(getName());
            }
            return false;
        }

        @Override
        public int hashCode() {
            int result = 17;
            result = 31 * result + super.hashCode();
            result = 31 * result + getName().hashCode();
            return result;
        }
    }
}