chromium/base/test/android/javatests/src/org/chromium/base/test/transit/PublicTransitConfig.java

// Copyright 2023 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.transit;

import android.widget.Toast;

import androidx.test.platform.app.InstrumentationRegistry;

import org.chromium.base.Log;
import org.chromium.base.ResettersForTesting;
import org.chromium.base.ThreadUtils;

/** Configuration for PublicTransit tests. */
public class PublicTransitConfig {
    private static final String TAG = "Transit";
    private static long sTransitionPause;
    private static Runnable sOnExceptionCallback;
    private static boolean sFreezeOnException;
    private static boolean sOnExceptionCallbackIsRecurring;

    /**
     * Set a pause for all transitions for debugging.
     *
     * @param millis how long to pause for (1000 to 4000 ms is typical).
     */
    public static void setTransitionPauseForDebugging(long millis) {
        sTransitionPause = millis;
        ResettersForTesting.register(() -> sTransitionPause = 0);
    }

    /**
     * Set a callback to be run when a {@link TravelException} will be thrown.
     *
     * <p>Useful to print debug information for failures that can't be reproduced with a debugger.
     *
     * @param onExceptionCallback the callback to run on exception.
     * @param recurring if {@link #setFreezeOnException()} is also set, run the callback multiple
     *     times on an exponential backoff. Useful to check if asynchronous updates have happened
     *     after the failure, e.g. the View hierarchy has changed.
     */
    public static void setOnExceptionCallback(Runnable onExceptionCallback, boolean recurring) {
        sOnExceptionCallback = onExceptionCallback;
        sOnExceptionCallbackIsRecurring = recurring;
        ResettersForTesting.register(
                () -> {
                    sOnExceptionCallback = null;
                    sOnExceptionCallbackIsRecurring = false;
                });
    }

    /**
     * Set the test to freeze when a {@link TravelException} will be thrown.
     *
     * <p>Useful to watch the test behavior for some time after the Exception. In conjunction with
     * {@link #setOnExceptionCallback(Runnable)}, the callback will be run on an exponential backoff
     * schedule starting at 1 second and doubling from that.
     *
     * <p>Lasts until the test runner times out the test.
     */
    public static void setFreezeOnException() {
        sFreezeOnException = true;
        ResettersForTesting.register(() -> sFreezeOnException = false);
    }

    static void maybePauseAfterTransition(Transition transition) {
        long pauseMs = sTransitionPause;
        if (pauseMs > 0) {
            String toastText = buildToastText(transition);
            ThreadUtils.runOnUiThread(
                    () -> {
                        Toast.makeText(
                                        InstrumentationRegistry.getInstrumentation()
                                                .getTargetContext(),
                                        toastText,
                                        Toast.LENGTH_SHORT)
                                .show();
                    });
            try {
                Log.e(TAG, "Pause for %dms after %s", pauseMs, transition.toDebugString());
                Thread.sleep(pauseMs);
            } catch (InterruptedException e) {
                Log.e(TAG, "Interrupted pause", e);
            }
        }
    }

    private static String buildToastText(Transition transition) {
        StringBuilder textToDisplay = new StringBuilder();
        String currentTestCase = TrafficControl.getCurrentTestCase();
        if (currentTestCase != null) {
            textToDisplay.append("[");
            textToDisplay.append(currentTestCase);
            textToDisplay.append("]\n");
        }
        textToDisplay.append("Finished ").append(transition.toDebugString());
        return textToDisplay.toString();
    }

    static void onTravelException(TravelException travelException) {
        if (sFreezeOnException) {
            Log.e(TAG, "Frozen on TravelException:", travelException);
        }

        triggerOnExceptionCallback();

        if (sFreezeOnException) {
            int backoffTimer = 1000;
            int totalMsFrozen = 0;

            // Will freeze until the test runner times out.
            while (true) {
                try {
                    Thread.sleep(backoffTimer);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                totalMsFrozen += backoffTimer;
                backoffTimer = 2 * backoffTimer;
                Log.e(TAG, "Frozen for %sms on TravelException:", totalMsFrozen, travelException);
                if (sOnExceptionCallbackIsRecurring) {
                    triggerOnExceptionCallback();
                }
            }
        }
    }

    private static void triggerOnExceptionCallback() {
        if (sOnExceptionCallback != null) {
            sOnExceptionCallback.run();
        }
    }
}