chromium/base/test/android/javatests/src/org/chromium/base/test/transit/Condition.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.util.ArrayMap;

import androidx.annotation.CallSuper;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.google.errorprone.annotations.FormatMethod;

import org.chromium.base.supplier.Supplier;
import org.chromium.base.test.transit.ConditionStatus.Status;
import org.chromium.base.test.transit.Transition.TransitionOptions;
import org.chromium.base.test.transit.Transition.Trigger;

/**
 * A condition that needs to be fulfilled for a state transition to be considered done.
 *
 * <p>{@link ConditionWaiter} waits for multiple Conditions to be fulfilled. {@link
 * ConditionChecker} performs one-time checks for whether multiple Conditions are fulfilled.
 */
public abstract class Condition {
    private String mDescription;

    private final boolean mIsRunOnUiThread;
    private ArrayMap<String, Supplier<?>> mDependentSuppliers;

    @VisibleForTesting boolean mHasStartedMonitoringForTesting;
    @VisibleForTesting boolean mHasStoppedMonitoringForTesting;

    /**
     * @param isRunOnUiThread true if the Condition should be checked on the UI Thread, false if it
     *     should be checked on the Instrumentation Thread. Other hooks such as {@link
     *     #onStartMonitoring()} are always run on the Instrumentation Thread.
     */
    public Condition(boolean isRunOnUiThread) {
        mIsRunOnUiThread = isRunOnUiThread;
    }

    /**
     * Should check the condition, report its status (if useful) and return whether it is fulfilled.
     *
     * <p>Depending on #shouldRunOnUiThread(), called on the UI or the instrumentation thread.
     *
     * @return {@link ConditionStatus} stating whether the condition has been fulfilled and
     *     optionally more details about its state.
     */
    protected abstract ConditionStatus checkWithSuppliers() throws Exception;

    /**
     * @return a short description to be printed as part of a list of conditions. Use {@link
     *     #getDescription()} to get a description as it caches the description until {@link
     *     #rebuildDescription()} invalidates it.
     */
    public abstract String buildDescription();

    /**
     * Hook run right before the condition starts being checked. Used, for example, to get initial
     * callback counts and install observers.
     */
    @CallSuper
    public void onStartMonitoring() {
        assert !mHasStartedMonitoringForTesting
                : getDescription() + ": onStartMonitoring should only be called once";
        mHasStartedMonitoringForTesting = true;
    }

    /**
     * Hook run right after the condition stops being checked. Used, for example, to uninstall
     * observers.
     */
    @CallSuper
    public void onStopMonitoring() {
        assert mHasStartedMonitoringForTesting
                : getDescription()
                        + ": onStartMonitoring was not called before onStopMonitoring (did you"
                        + " forget to call super.onStartMonitoring()?)";
        assert !mHasStoppedMonitoringForTesting
                : getDescription() + ": onStopMonitoring should only be called once";
        mHasStoppedMonitoringForTesting = true;
    }

    /**
     * @return a short description to be printed as part of a list of conditions.
     */
    public String getDescription() {
        if (mDescription == null) {
            rebuildDescription();
        }
        return mDescription;
    }

    /**
     * Invalidates last description; the next time {@link #getDescription()}, it will get a new one
     * from {@link #buildDescription()}.
     */
    protected void rebuildDescription() {
        mDescription = buildDescription();
        assert mDescription != null
                : this.getClass().getCanonicalName() + "#buildDescription() should not return null";
    }

    /**
     * @return true if the check is intended to be run on the UI Thread, false if it should be run
     *     on the instrumentation thread.
     */
    public boolean isRunOnUiThread() {
        return mIsRunOnUiThread;
    }

    /**
     * Declare a Supplier this Condition's check() depends on.
     *
     * <p>Call this from the constructor to delay check() to be called until |supplier| supplies a
     * value.
     */
    protected <T> Supplier<T> dependOnSupplier(Supplier<T> supplier, String inputName) {
        if (mDependentSuppliers == null) {
            mDependentSuppliers = new ArrayMap<>();
        }
        mDependentSuppliers.put(inputName, supplier);
        return supplier;
    }

    /**
     * The method called to actually check the Condition, including checking dependencies of
     * check().
     */
    public final ConditionStatus check() throws Exception {
        // If any Supplier is missing a value, the Condition can't be checked yet.
        ConditionStatus status = checkDependentSuppliers();
        if (status != null) {
            return status;
        }

        // Call the subclass' checkWithSuppliers().
        return checkWithSuppliers();
    }

    private ConditionStatus checkDependentSuppliers() {
        if (mDependentSuppliers == null) {
            return null;
        }

        StringBuilder suppliersMissing = null;
        for (var kv : mDependentSuppliers.entrySet()) {
            Supplier<?> supplier = kv.getValue();
            if (!supplier.hasValue()) {
                if (suppliersMissing == null) {
                    suppliersMissing = new StringBuilder("waiting for suppliers of: ");
                } else {
                    suppliersMissing.append(", ");
                }
                String inputName = kv.getKey();
                suppliersMissing.append(inputName);
            }
        }

        if (suppliersMissing != null) {
            return awaiting(suppliersMissing.toString());
        }

        return null;
    }

    /** {@link #checkWithSuppliers()} should return this when a Condition is fulfilled. */
    public static ConditionStatus fulfilled() {
        return fulfilled(/* message= */ null);
    }

    /** {@link #fulfilled()} with more details to be logged as a short message. */
    public static ConditionStatus fulfilled(@Nullable String message) {
        return new ConditionStatus(Status.FULFILLED, message);
    }

    /** {@link #fulfilled()} with more details to be logged as a short message. */
    @FormatMethod
    public static ConditionStatus fulfilled(String message, Object... args) {
        return new ConditionStatus(Status.FULFILLED, String.format(message, args));
    }

    /** {@link #checkWithSuppliers()} should return this when a Condition is not fulfilled. */
    public static ConditionStatus notFulfilled() {
        return notFulfilled(/* message= */ null);
    }

    /** {@link #notFulfilled()} with more details to be logged as a short message. */
    public static ConditionStatus notFulfilled(@Nullable String message) {
        return new ConditionStatus(Status.NOT_FULFILLED, message);
    }

    /** {@link #notFulfilled()} with more details to be logged as a short message. */
    @FormatMethod
    public static ConditionStatus notFulfilled(String message, Object... args) {
        return new ConditionStatus(Status.NOT_FULFILLED, String.format(message, args));
    }

    /**
     * {@link #checkWithSuppliers()} should return this when an error happens while checking a
     * Condition.
     *
     * <p>A short message is required.
     *
     * <p>Throwing an error in check() has the same effect.
     */
    public static ConditionStatus error(@Nullable String message) {
        return new ConditionStatus(Status.ERROR, message);
    }

    /** {@link #error(String)} with format parameters. */
    @FormatMethod
    public static ConditionStatus error(String message, Object... args) {
        return new ConditionStatus(Status.ERROR, String.format(message, args));
    }

    /** {@link #checkWithSuppliers()} should return this as a convenience method. */
    public static ConditionStatus whether(boolean isFulfilled) {
        return isFulfilled ? fulfilled() : notFulfilled();
    }

    /** {@link #whether(boolean)} with more details to be logged as a short message. */
    public static ConditionStatus whether(boolean isFulfilled, @Nullable String message) {
        return isFulfilled ? fulfilled(message) : notFulfilled(message);
    }

    /** {@link #whether(boolean)} with more details to be logged as a short message. */
    @FormatMethod
    public static ConditionStatus whether(boolean isFulfilled, String message, Object... args) {
        return whether(isFulfilled, String.format(message, args));
    }

    /**
     * {@link #checkWithSuppliers()} should return this when it does not have information to check
     * the Condition yet.
     *
     * <p>It is considered not fulfilled for most purposes. The exception is that if the Condition
     * is used as a gate Condition, the gated Condition will not be checked, considered FULFILLED,
     * or considered NOT_FULFILLED until the gate resolves to FULFILLED or NOT_FULFILLED.
     *
     * @param message A short message stating what is being awaited for
     */
    public static ConditionStatus awaiting(@Nullable String message) {
        return new ConditionStatus(Status.AWAITING, message);
    }

    /** {@link #awaiting(String)} with format parameters. */
    @FormatMethod
    public static ConditionStatus awaiting(String message, Object... args) {
        return new ConditionStatus(Status.AWAITING, String.format(message, args));
    }

    /** {@link #checkWithSuppliers()} can return this as a convenience method. */
    public static ConditionStatus fulfilledOrAwaiting(
            boolean isFulfilled, @Nullable String message) {
        return isFulfilled ? fulfilled(message) : awaiting(message);
    }

    /**
     * {@link #fulfilledOrAwaiting(boolean, String)} with more details to be logged as a short
     * message.
     */
    @FormatMethod
    public static ConditionStatus fulfilledOrAwaiting(
            boolean isFulfilled, String message, Object... args) {
        return fulfilledOrAwaiting(isFulfilled, String.format(message, args));
    }

    /** Runs |trigger| and waits for one or more Conditions using a Transition. */
    public static CarryOn runAndWaitFor(Trigger trigger, Condition... conditions) {
        return runAndWaitFor(TransitionOptions.DEFAULT, trigger, conditions);
    }

    /** Versions of {@link #runAndWaitFor(Trigger, Condition...)} with {@link TransitionOptions}. */
    public static CarryOn runAndWaitFor(
            TransitionOptions options, Trigger trigger, Condition... conditions) {
        return CarryOn.pickUp(CarryOn.fromConditions(conditions), options, trigger);
    }
}