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

Commit 7d97bad8 authored by Felipe Leme's avatar Felipe Leme
Browse files

Refactored ExtendedMockitoTestCase into ExtendedMockitoRule

ExtendedMockitoTestCase was created mostly for session management
(to make sure tests properly closed the session and to clear some
internal mockito state that could cause OOM), but to be really
effective, all classes would have to extend it.

Given that constraint - and the fact that there is already a rule
to facilitate session management - this CL gets rid of that super
class and provide another rule instead (which's built on top of the
existing one).

Also fixed CpuInfoReaderTest, which was using %d instead of %s on
assertWithMessage() and replace those calls by expectWithMessage()).

Bug:
Test: atest \
      FrameworksMockingServicesTests:AlarmManagerServiceTest \
      FrameworksMockingServicesTests:ActivityManagerServiceInjectorTest \
      FrameworksMockingServicesTests:BroadcastQueueModernImplTest \
      FrameworksMockingServicesTests:CachedAppOptimizerTest \
      FrameworksMockingServicesTests:CpuInfoReaderTest \
      FrameworksMockingServicesTests:CpuMonitorServiceTest \
      FrameworksMockingServicesTests:UserManagerServiceTest \
      FrameworksMockingServicesTests:UserVisibilityMediatorMUMDTest \
      FrameworksMockingServicesTests:UserVisibilityMediatorMUPANDTest \
      FrameworksMockingServicesTests:UserVisibilityMediatorSUSDTest
Bug: 268515318

Change-Id: I59f3ee693cbad01e9289cf069b7d039840f7c811
parent d8f90306
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