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

// Copyright 2024 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 androidx.annotation.Nullable;

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

import java.util.concurrent.Callable;
import java.util.function.Function;

/**
 * Represents a logical expression that has to be true to consider the Station active and false to
 * consider the Station exited.
 *
 * <p>The logical expression is passed in as a |checkFunction|.
 *
 * <p>LogicalElements should be declared by calling {@link
 * Elements.Builder#declareLogicalElement(LogicalElement)} passing in an instance created by one of
 * the factory methods here such as {@link #uiThreadLogicalElement(String, Function, Supplier)}.
 *
 * <p>Generates ENTER and EXIT Conditions for the ConditionalState to ensure the LogicalElement is
 * in the right state.
 *
 * <p>LogicalElements that have no Exit condition should simply be enter Conditions, declared with
 * {@link Elements.Builder#declareEnterCondition(Condition)}.
 *
 * @param <ParamT> type of parameter the |checkFunction| requires.
 */
public class LogicalElement<ParamT> extends Element<Void> {

    private static final ConditionWithResult<Void> CONDITION_WITH_NULL_RESULT =
            new ConditionWithResult<>(/* isRunOnUiThread= */ false) {
                @Override
                public String buildDescription() {
                    return "Supplier of null";
                }

                @Override
                public boolean hasValue() {
                    return true;
                }

                @Override
                protected ConditionStatusWithResult<Void> resolveWithSuppliers() {
                    return fulfilled().withResult(null);
                }
            };
    private final boolean mIsRunOnUiThread;
    private final String mDescription;
    private final Function<ParamT, ConditionStatus> mCheckFunction;
    private final Supplier<ParamT> mParamSupplier;

    /**
     * Create a LogicalElement that runs the check on the UI Thread.
     *
     * <p>LogicalElements wait for the function to be true as an ENTER Condition. They also wait for
     * the function to be false as an EXIT Condition when transitioning to a ConditionalState that
     * does not declare the same LogicalElement.
     */
    public static <T> LogicalElement<T> uiThreadLogicalElement(
            String description,
            Function<T, ConditionStatus> checkFunction,
            Supplier<T> paramSupplier,
            String id) {
        return new LogicalElement<>(
                /* isRunOnUiThread= */ true, description, checkFunction, paramSupplier, id);
    }

    /**
     * Version of {@link #uiThreadLogicalElement(String, Function, Supplier, String)} using the
     * |description| as |id|.
     */
    public static <T> LogicalElement<T> uiThreadLogicalElement(
            String description,
            Function<T, ConditionStatus> checkFunction,
            Supplier<T> paramSupplier) {
        return new LogicalElement<>(
                /* isRunOnUiThread= */ true,
                description,
                checkFunction,
                paramSupplier,
                /* id= */ null);
    }

    /**
     * Version of {@link #uiThreadLogicalElement(String, Function, Supplier)} when |checkFunction|
     * has no dependencies.
     */
    public static LogicalElement<Void> uiThreadLogicalElement(
            String description, Callable<ConditionStatus> checkCallable) {
        return new LogicalElement<>(
                /* isRunOnUiThread= */ true,
                description,
                new CallableAsFunction(checkCallable),
                CONDITION_WITH_NULL_RESULT,
                /* id= */ null);
    }

    /**
     * Create a LogicalElement that runs the check on the Instrumentation Thread.
     *
     * <p>LogicalElements wait for the function to be true as an ENTER Condition. They also wait for
     * the function to be false as an EXIT Condition when transitioning to a ConditionalState that
     * does not declare the same LogicalElement.
     */
    public static <T> LogicalElement<T> instrumentationThreadLogicalElement(
            String description,
            Function<T, ConditionStatus> checkFunction,
            Supplier<T> paramSupplier,
            String id) {
        return new LogicalElement<>(
                /* isRunOnUiThread= */ false, description, checkFunction, paramSupplier, id);
    }

    /**
     * Version of {@link #instrumentationThreadLogicalElement(String, Function, Supplier, String)}
     * using the |description| as |id|.
     */
    public static <T> LogicalElement<T> instrumentationThreadLogicalElement(
            String description,
            Function<T, ConditionStatus> checkFunction,
            Supplier<T> paramSupplier) {
        return new LogicalElement<>(
                /* isRunOnUiThread= */ false,
                description,
                checkFunction,
                paramSupplier,
                /* id= */ null);
    }

    /**
     * Version of {@link #instrumentationThreadLogicalElement(String, Function, Supplier)} when
     * |checkFunction| has no dependencies.
     */
    public static LogicalElement<Void> instrumentationThreadLogicalElement(
            String description, Callable<ConditionStatus> checkCallable) {
        return new LogicalElement<>(
                /* isRunOnUiThread= */ false,
                description,
                new CallableAsFunction(checkCallable),
                CONDITION_WITH_NULL_RESULT,
                /* id= */ null);
    }

    LogicalElement(
            boolean isRunOnUiThread,
            String description,
            Function<ParamT, ConditionStatus> checkFunction,
            Supplier<ParamT> paramSupplier,
            @Nullable String id) {
        super("LE/" + (id != null ? id : description));
        mIsRunOnUiThread = isRunOnUiThread;
        mDescription = description;
        mCheckFunction = checkFunction;
        mParamSupplier = paramSupplier;
    }

    @Override
    public ConditionWithResult<Void> createEnterCondition() {
        return new EnterCondition(mIsRunOnUiThread);
    }

    @Override
    public Condition createExitCondition() {
        return new ExitCondition(mIsRunOnUiThread);
    }

    private class EnterCondition extends ConditionWithResult<Void> {
        private EnterCondition(boolean isRunOnUiThread) {
            super(isRunOnUiThread);
            dependOnSupplier(mParamSupplier, "Param");
        }

        @Override
        protected ConditionStatusWithResult<Void> resolveWithSuppliers() {
            return mCheckFunction.apply(mParamSupplier.get()).withoutResult();
        }

        @Override
        public String buildDescription() {
            return "True: " + mDescription;
        }
    }

    private class ExitCondition extends Condition {
        private ExitCondition(boolean isRunOnUiThread) {
            super(isRunOnUiThread);
            dependOnSupplier(mParamSupplier, "Param");
        }

        @Override
        protected ConditionStatus checkWithSuppliers() {
            ConditionStatus functionResult = mCheckFunction.apply(mParamSupplier.get());
            return new ConditionStatus(
                    invertStatus(functionResult.getStatus()), functionResult.getMessage());
        }

        @Override
        public String buildDescription() {
            return "False: " + mDescription;
        }
    }

    private @Status int invertStatus(@Status int status) {
        return switch (status) {
            case Status.NOT_FULFILLED -> Status.FULFILLED;
            case Status.FULFILLED -> Status.NOT_FULFILLED;
            case Status.ERROR -> Status.ERROR;
            case Status.AWAITING -> Status.AWAITING;
            default -> throw new IllegalStateException("Unexpected value: " + status);
        };
    }

    private static class CallableAsFunction implements Function<Void, ConditionStatus> {

        private final Callable<ConditionStatus> mCheckCallable;

        private CallableAsFunction(Callable<ConditionStatus> checkCallable) {
            mCheckCallable = checkCallable;
        }

        @Override
        public ConditionStatus apply(Void voidParam) {
            try {
                return mCheckCallable.call();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
}