Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a2de4953 authored by Felipe Leme's avatar Felipe Leme Committed by Automerger Merge Worker
Browse files

Merge "Refactored ExtendedMockitoTestCase into ExtendedMockitoRule" into udc-dev am: d2485b0b

parents efc5e234 d2485b0b
Loading
Loading
Loading
Loading
+181 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.server;

import com.google.common.annotations.GwtIncompatible;
import com.google.common.base.Optional;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multiset;
import com.google.common.collect.Table;
import com.google.common.truth.BigDecimalSubject;
import com.google.common.truth.BooleanSubject;
import com.google.common.truth.ClassSubject;
import com.google.common.truth.ComparableSubject;
import com.google.common.truth.DoubleSubject;
import com.google.common.truth.Expect;
import com.google.common.truth.FloatSubject;
import com.google.common.truth.GuavaOptionalSubject;
import com.google.common.truth.IntegerSubject;
import com.google.common.truth.IterableSubject;
import com.google.common.truth.LongSubject;
import com.google.common.truth.MapSubject;
import com.google.common.truth.MultimapSubject;
import com.google.common.truth.MultisetSubject;
import com.google.common.truth.ObjectArraySubject;
import com.google.common.truth.PrimitiveBooleanArraySubject;
import com.google.common.truth.PrimitiveByteArraySubject;
import com.google.common.truth.PrimitiveCharArraySubject;
import com.google.common.truth.PrimitiveDoubleArraySubject;
import com.google.common.truth.PrimitiveFloatArraySubject;
import com.google.common.truth.PrimitiveIntArraySubject;
import com.google.common.truth.PrimitiveLongArraySubject;
import com.google.common.truth.PrimitiveShortArraySubject;
import com.google.common.truth.StandardSubjectBuilder;
import com.google.common.truth.StringSubject;
import com.google.common.truth.Subject;
import com.google.common.truth.TableSubject;
import com.google.common.truth.ThrowableSubject;

import org.junit.Rule;

import java.math.BigDecimal;
import java.util.Map;

// NOTE: it could be a more generic AbstractTruthTestCase that provide similar methods
// for assertThat() / assertWithMessage(), but then we'd need to remove all static import imports
// from classes that indirectly extend it.
/**
 * Base class to make it easier to use {@code Truth} {@link Expect} assertions.
 */
public abstract class ExpectableTestCase {

    @Rule
    public final Expect mExpect = Expect.create();

    protected final StandardSubjectBuilder expectWithMessage(String msg) {
        return mExpect.withMessage(msg);
    }

    protected final StandardSubjectBuilder expectWithMessage(String format, Object...args) {
        return mExpect.withMessage(format, args);
    }

    protected final <ComparableT extends Comparable<?>> ComparableSubject<ComparableT> expectThat(
            ComparableT actual) {
        return mExpect.that(actual);
    }

    protected final BigDecimalSubject expectThat(BigDecimal actual) {
        return mExpect.that(actual);
    }

    protected final Subject expectThat(Object actual) {
        return mExpect.that(actual);
    }

    @GwtIncompatible("ClassSubject.java")
    protected final ClassSubject expectThat(Class<?> actual) {
        return mExpect.that(actual);
    }

    protected final ThrowableSubject expectThat(Throwable actual) {
        return mExpect.that(actual);
    }

    protected final LongSubject expectThat(Long actual) {
        return mExpect.that(actual);
    }

    protected final DoubleSubject expectThat(Double actual) {
        return mExpect.that(actual);
    }

    protected final FloatSubject expectThat(Float actual) {
        return mExpect.that(actual);
    }

    protected final IntegerSubject expectThat(Integer actual) {
        return mExpect.that(actual);
    }

    protected final BooleanSubject expectThat(Boolean actual) {
        return mExpect.that(actual);
    }

    protected final StringSubject expectThat(String actual) {
        return mExpect.that(actual);
    }

    protected final IterableSubject expectThat(Iterable<?> actual) {
        return mExpect.that(actual);
    }

    protected final <T> ObjectArraySubject<T> expectThat(T[] actual) {
        return mExpect.that(actual);
    }

    protected final PrimitiveBooleanArraySubject expectThat(boolean[] actual) {
        return mExpect.that(actual);
    }

    protected final PrimitiveShortArraySubject expectThat(short[] actual) {
        return mExpect.that(actual);
    }

    protected final PrimitiveIntArraySubject expectThat(int[] actual) {
        return mExpect.that(actual);
    }

    protected final PrimitiveLongArraySubject expectThat(long[] actual) {
        return mExpect.that(actual);
    }

    protected final PrimitiveCharArraySubject expectThat(char[] actual) {
        return mExpect.that(actual);
    }

    protected final PrimitiveByteArraySubject expectThat(byte[] actual) {
        return mExpect.that(actual);
    }

    protected final PrimitiveFloatArraySubject expectThat(float[] actual) {
        return mExpect.that(actual);
    }

    protected final PrimitiveDoubleArraySubject expectThat(double[] actual) {
        return mExpect.that(actual);
    }

    protected final GuavaOptionalSubject expectThat(Optional<?> actual) {
        return mExpect.that(actual);
    }

    protected final MapSubject expectThat(Map<?, ?> actual) {
        return mExpect.that(actual);
    }

    protected final MultimapSubject expectThat(Multimap<?, ?> actual) {
        return mExpect.that(actual);
    }

    protected final MultisetSubject expectThat(Multiset<?> actual) {
        return mExpect.that(actual);
    }

    protected final TableSubject expectThat(Table<?, ?, ?> actual) {
        return mExpect.that(actual);
    }
}
+183 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server;

import android.annotation.Nullable;
import android.util.Log;

import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;
import com.android.internal.util.Preconditions;
import com.android.modules.utils.testing.StaticMockFixture;
import com.android.modules.utils.testing.StaticMockFixtureRule;

import org.mockito.Mockito;
import org.mockito.quality.Strictness;

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

/**
 * Rule to make it easier to use Extended Mockito.
 *
 * <p>It's derived from {@link StaticMockFixtureRule}, with the additional features:
 *
 * <ul>
 *   <li>Easier to define which classes must be statically mocked or spied
 *   <li>Automatically starts mocking (so tests don't need a mockito runner or rule)
 *   <li>Automatically clears the inlined mocks at the end (to avoid OOM)
 *   <li>Allows other customization like strictness
 * </ul>
 */
public final class ExtendedMockitoRule extends StaticMockFixtureRule {

    private static final String TAG = ExtendedMockitoRule.class.getSimpleName();

    private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);

    private final Object mTestClassInstance;
    private final Strictness mStrictness;

    private ExtendedMockitoRule(Builder builder) {
        super(() -> new SimpleStatickMockFixture(builder.mMockedStaticClasses,
                builder.mSpiedStaticClasses, builder.mDynamicSessionBuilderConfigurator,
                builder.mAfterSessionFinishedCallback));
        mTestClassInstance = builder.mTestClassInstance;
        mStrictness = builder.mStrictness;
        if (VERBOSE) {
            Log.v(TAG, "strictness=" + mStrictness + ", testClassInstance" + mTestClassInstance
                    + ", mockedStaticClasses=" + builder.mMockedStaticClasses
                    + ", spiedStaticClasses=" + builder.mSpiedStaticClasses
                    + ", dynamicSessionBuilderConfigurator="
                    + builder.mDynamicSessionBuilderConfigurator
                    + ", afterSessionFinishedCallback=" + builder.mAfterSessionFinishedCallback);
        }
    }

    @Override
    public StaticMockitoSessionBuilder getSessionBuilder() {
        StaticMockitoSessionBuilder sessionBuilder = super.getSessionBuilder();
        if (mStrictness != null) {
            if (VERBOSE) {
                Log.v(TAG, "Setting strictness to " + mStrictness + " on " + sessionBuilder);
            }
            sessionBuilder.strictness(mStrictness);
        }
        return sessionBuilder.initMocks(mTestClassInstance);
    }

    public static final class Builder {
        private final Object mTestClassInstance;
        private @Nullable Strictness mStrictness;
        private final List<Class<?>> mMockedStaticClasses = new ArrayList<>();
        private final List<Class<?>> mSpiedStaticClasses = new ArrayList<>();
        private @Nullable Visitor<StaticMockitoSessionBuilder> mDynamicSessionBuilderConfigurator;
        private @Nullable Runnable mAfterSessionFinishedCallback;

        public Builder(Object testClassInstance) {
            mTestClassInstance = Objects.requireNonNull(testClassInstance);
        }

        public Builder setStrictness(Strictness strictness) {
            mStrictness = Objects.requireNonNull(strictness);
            return this;
        }

        public Builder mockStatic(Class<?> clazz) {
            Objects.requireNonNull(clazz);
            Preconditions.checkState(!mMockedStaticClasses.contains(clazz),
                    "class %s already mocked", clazz);
            mMockedStaticClasses.add(clazz);
            return this;
        }

        public Builder spyStatic(Class<?> clazz) {
            Objects.requireNonNull(clazz);
            Preconditions.checkState(!mSpiedStaticClasses.contains(clazz),
                    "class %s already spied", clazz);
            mSpiedStaticClasses.add(clazz);
            return this;
        }

        public Builder dynamiclyConfigureSessionBuilder(
                Visitor<StaticMockitoSessionBuilder> dynamicSessionBuilderConfigurator) {
            mDynamicSessionBuilderConfigurator = Objects
                    .requireNonNull(dynamicSessionBuilderConfigurator);
            return this;
        }

        public Builder afterSessionFinished(Runnable runnable) {
            mAfterSessionFinishedCallback = Objects.requireNonNull(runnable);
            return this;
        }

        public ExtendedMockitoRule build() {
            return new ExtendedMockitoRule(this);
        }
    }

    private static final class SimpleStatickMockFixture implements StaticMockFixture {

        private final List<Class<?>> mMockedStaticClasses;
        private final List<Class<?>> mSpiedStaticClasses;
        @Nullable
        private final Visitor<StaticMockitoSessionBuilder> mDynamicSessionBuilderConfigurator;
        @Nullable
        private final Runnable mAfterSessionFinishedCallback;

        private SimpleStatickMockFixture(List<Class<?>> mockedStaticClasses,
                List<Class<?>> spiedStaticClasses,
                @Nullable Visitor<StaticMockitoSessionBuilder> dynamicSessionBuilderConfigurator,
                @Nullable Runnable afterSessionFinishedCallback) {
            mMockedStaticClasses = mockedStaticClasses;
            mSpiedStaticClasses = spiedStaticClasses;
            mDynamicSessionBuilderConfigurator = dynamicSessionBuilderConfigurator;
            mAfterSessionFinishedCallback = afterSessionFinishedCallback;
        }

        @Override
        public StaticMockitoSessionBuilder setUpMockedClasses(
                StaticMockitoSessionBuilder sessionBuilder) {
            mMockedStaticClasses.forEach((c) -> sessionBuilder.mockStatic(c));
            mSpiedStaticClasses.forEach((c) -> sessionBuilder.spyStatic(c));
            if (mDynamicSessionBuilderConfigurator != null) {
                mDynamicSessionBuilderConfigurator.visit(sessionBuilder);
            }
            return sessionBuilder;
        }

        @Override
        public void setUpMockBehaviors() {
        }

        @Override
        public void tearDown() {
            try {
                if (mAfterSessionFinishedCallback != null) {
                    mAfterSessionFinishedCallback.run();
                }
            } finally {
                if (VERBOSE) {
                    Log.v(TAG, "calling Mockito.framework().clearInlineMocks()");
                }
                // When using inline mock maker, clean up inline mocks to prevent OutOfMemory
                // errors. See https://github.com/mockito/mockito/issues/1614 and b/259280359.
                Mockito.framework().clearInlineMocks();
            }
        }
    }
}
+0 −249
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.server;

import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;

import android.annotation.Nullable;
import android.util.Log;

import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder;

import com.google.common.truth.Expect;
import com.google.common.truth.StandardSubjectBuilder;

import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.RuleChain;
import org.mockito.Mockito;
import org.mockito.MockitoSession;
import org.mockito.quality.Strictness;

import java.lang.reflect.Constructor;

/**
 * Base class to make it easier to write tests that uses {@code ExtendedMockito}.
 *
 */
public abstract class ExtendedMockitoTestCase {

    private static final String TAG = ExtendedMockitoTestCase.class.getSimpleName();

    private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);

    /**
     * Number of invocations, used to force a failure on {@link #forceFailure(int, Class, String)}.
     */
    private static int sInvocationsCounter;

    /**
     * Sessions follow the "Highlander Rule": There can be only one!
     *
     * <p>So, we keep track of that and force-close it if needed.
     */
    @Nullable
    private static MockitoSession sHighlanderSession;

    /**
     * Points to where the current session was created.
     */
    private static Exception sSessionCreationLocation;

    private MockitoSession mSession;

    protected final Expect mExpect = Expect.create();
    protected final DumpableDumperRule mDumpableDumperRule = new DumpableDumperRule();

    @Rule
    public final RuleChain mTwoRingsOfPowerAndOneChainToRuleThemAll = RuleChain
            .outerRule(mDumpableDumperRule)
            .around(mExpect);

    public ExtendedMockitoTestCase() {
        sInvocationsCounter++;
    }

    @Before
    public final void startSession() {
        if (VERBOSE) {
            Log.v(TAG, "startSession() for " + getTestName() + " on thread "
                    + Thread.currentThread() + "; sHighlanderSession=" + sHighlanderSession);
        }
        createSessionLocation();
        finishHighlanderSessionIfNeeded("startSession()");
        StaticMockitoSessionBuilder builder = mockitoSession()
                .initMocks(this)
                .strictness(getSessionStrictness());
        initializeSession(builder);
        sHighlanderSession = mSession = builder.startMocking();
    }

    private void createSessionLocation() {
        try {
            sSessionCreationLocation = new Exception(getTestName());
        } catch (Exception e) {
            // Better safe than sorry...
            Log.e(TAG, "Could not create sSessionCreationLocation with " + getTestName()
                    + " on thread " + Thread.currentThread(), e);
            sSessionCreationLocation = e;
        }
    }

    /**
     * Gets the session strictness.
     *
     * @return {@link Strictness.LENIENT} by default; subclasses can override.
     */
    protected Strictness getSessionStrictness() {
        return Strictness.LENIENT;
    }

    /**
     * Initializes the mockito session.
     *
     * <p>Typically used to define which classes should have static methods mocked or spied.
     */
    protected void initializeSession(StaticMockitoSessionBuilder builder) {
        if (VERBOSE) {
            Log.v(TAG, "initializeSession()");
        }
    }

    @After
    public final void finishSession() throws Exception {
        if (false) { // For obvious reasons, should NEVER be merged as true
            forceFailure(1, RuntimeException.class, "to simulate an unfinished session");
        }

        // mSession.finishMocking() must ALWAYS be called (hence the over-protective try/finally
        // statements), otherwise it would cause failures on future tests as mockito
        // cannot start a session when a previous one is not finished
        try {
            if (VERBOSE) {
                Log.v(TAG, "finishSession() for " + getTestName() + " on thread "
                        + Thread.currentThread() + "; sHighlanderSession=" + sHighlanderSession);

            }
        } finally {
            sHighlanderSession = null;
            finishSessionMocking();
            afterSessionFinished();
        }
    }

    /**
     * Called after the mockito session was finished
     *
     * <p>This method should be used by subclasses that MUST do their cleanup after the session is
     * finished (as methods marked with {@link After} in the subclasses would be called BEFORE
     * that).
     */
    protected void afterSessionFinished() {
        if (VERBOSE) {
            Log.v(TAG, "afterSessionFinished()");
        }
    }

    private void finishSessionMocking() {
        if (mSession == null) {
            Log.w(TAG, getClass().getSimpleName() + ".finishSession(): no session");
            return;
        }
        try {
            mSession.finishMocking();
        } finally {
            // Shouldn't need to set mSession to null as JUnit always instantiate a new object,
            // but it doesn't hurt....
            mSession = null;
            // When using inline mock maker, clean up inline mocks to prevent OutOfMemory
            // errors. See https://github.com/mockito/mockito/issues/1614 and b/259280359.
            Mockito.framework().clearInlineMocks();
        }
    }

    private void finishHighlanderSessionIfNeeded(String where) {
        if (sHighlanderSession == null) {
            if (VERBOSE) {
                Log.v(TAG, "finishHighlanderSessionIfNeeded(): sHighlanderSession already null");
            }
            return;
        }

        if (sSessionCreationLocation != null) {
            if (VERBOSE) {
                Log.e(TAG, where + ": There can be only one! Closing unfinished session, "
                        + "created at", sSessionCreationLocation);
            } else {
                Log.e(TAG, where + ": There can be only one! Closing unfinished session, "
                        + "created at " +  sSessionCreationLocation);
            }
        } else {
            Log.e(TAG, where + ": There can be only one! Closing unfinished session created at "
                    + "unknown location");
        }
        try {
            sHighlanderSession.finishMocking();
        } catch (Throwable t) {
            if (VERBOSE) {
                Log.e(TAG, "Failed to close unfinished session on " + getTestName(), t);
            } else {
                Log.e(TAG, "Failed to close unfinished session on " + getTestName() + ": " + t);
            }
        } finally {
            if (VERBOSE) {
                Log.v(TAG, "Resetting sHighlanderSession at finishHighlanderSessionIfNeeded()");
            }
            sHighlanderSession = null;
        }
    }

    /**
     * Forces a failure at the given invocation of a test method by throwing an exception.
     */
    protected final <T extends Throwable> void forceFailure(int invocationCount,
            Class<T> failureClass, String reason) throws T {
        if (sInvocationsCounter != invocationCount) {
            Log.d(TAG, "forceFailure(" + invocationCount + "): no-op on invocation #"
                    + sInvocationsCounter);
            return;
        }
        String message = "Throwing on invocation #" + sInvocationsCounter + ": " + reason;
        Log.e(TAG, message);
        T throwable;
        try {
            Constructor<T> constructor = failureClass.getConstructor(String.class);
            throwable = constructor.newInstance("Throwing on invocation #" + sInvocationsCounter
                    + ": " + reason);
        } catch (Exception e) {
            throw new IllegalArgumentException("Could not create exception of class " + failureClass
                    + " using msg='" + message + "' as constructor");
        }
        throw throwable;
    }

    protected final @Nullable String getTestName() {
        return mDumpableDumperRule.getTestName();
    }

    protected final StandardSubjectBuilder expectWithMessage(String msg) {
        return mExpect.withMessage(msg);
    }

    protected final StandardSubjectBuilder expectWithMessage(String format, Object...args) {
        return mExpect.withMessage(format, args);
    }
}
+23 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.server;

/**
 * Generic visitor.
 */
public interface Visitor<V> {
    void visit(V visitee);
}
+10 −18

File changed.

Preview size limit exceeded, changes collapsed.

Loading