/* * Copyright (C) 2020 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 android.car.test.mocks; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.car.test.AbstractExpectableTestCase; import android.os.Binder; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Trace; import android.os.UserManager; import android.util.ArraySet; import android.util.Log; import android.util.Log.TerribleFailure; import android.util.Log.TerribleFailureHandler; import android.util.Slog; import android.util.TimingsTraceLog; import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; import com.android.internal.util.Preconditions; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; import org.mockito.Mockito; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Set; /** * Base class for tests that must use {@link com.android.dx.mockito.inline.extended.ExtendedMockito} * to mock static classes and final methods. * *
Note: this class automatically spy on {@link Log} and {@link Slog} and fail tests that * all any of their {@code wtf()} methods. If a test is expect to call {@code wtf()}, it should be * annotated with {@link ExpectWtf}. * *
Note: when using this class, you must include the following * dependencies on {@code Android.bp} (or {@code Android.mk}: *
jni_libs: [
"libdexmakerjvmtiagent",
"libstaticjvmtiagent",
],
LOCAL_JNI_SHARED_LIBRARIES := \
libdexmakerjvmtiagent \
libstaticjvmtiagent \
*
*/
public abstract class AbstractExtendedMockitoTestCase extends AbstractExpectableTestCase {
static final String TAG = AbstractExtendedMockitoTestCase.class.getSimpleName();
private static final boolean TRACE = false;
private static final long SYNC_RUNNABLE_MAX_WAIT_TIME = 5_000L;
@SuppressWarnings("IsLoggableTagLength")
private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
/**
* Should be used on constructors for test case whose object under test doesn't make any logging
* call.
*/
protected static final String[] NO_LOG_TAGS = new String[] {
"I can't believe a test case is using this String as a log TAG! Well done!"
};
/**
* 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!
*
* 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 final List This can prevent pending Handler tasks of one test from affecting another. This does not
* work if the message is posted with delay.
*/
protected final void completeAllHandlerThreadTasks() {
beginTrace("completeAllHandlerThreadTasks");
Set Typically, it should be overridden when mocking static methods.
*
* NOTE: you don't need to call it to spy on {@link Log} or {@link Slog}, as those
* are already spied on.
*/
protected void onSessionBuilder(@NonNull CustomMockitoSessionBuilder session) {
if (VERBOSE) Log.v(TAG, getLogPrefix() + "onSessionBuilder()");
}
/**
* Changes the value of the session created by
* {@link #onSessionBuilder(CustomMockitoSessionBuilder)}.
*
* By default it's set to {@link Strictness.LENIENT}, but subclasses can overwrite this
* method to change the behavior.
*/
@NonNull
protected Strictness getSessionStrictness() {
return Strictness.LENIENT;
}
/**
* Mocks a call to {@link ActivityManager#getCurrentUser()}.
*
* @param userId result of such call
*
* @throws IllegalStateException if class didn't override
* {@link #onSessionBuilder(CustomMockitoSessionBuilder)} and called
* {@code spyStatic(Binder.class)} on the session passed to it.
*/
protected final void mockGetCurrentUser(@UserIdInt int userId) {
if (VERBOSE) Log.v(TAG, getLogPrefix() + "mockGetCurrentUser(" + userId + ")");
assertSpied(ActivityManager.class);
beginTrace("mockAmGetCurrentUser-" + userId);
AndroidMockitoHelper.mockAmGetCurrentUser(userId);
endTrace();
}
/**
* Mocks a call to {@link UserManager#isHeadlessSystemUserMode()}.
*
* @param mode result of such call
*
* @throws IllegalStateException if class didn't override
* {@link #onSessionBuilder(CustomMockitoSessionBuilder)} and called
* {@code spyStatic(Binder.class)} on the session passed to it.
*/
protected final void mockIsHeadlessSystemUserMode(boolean mode) {
if (VERBOSE) Log.v(TAG, getLogPrefix() + "mockIsHeadlessSystemUserMode(" + mode + ")");
assertSpied(UserManager.class);
beginTrace("mockUmIsHeadlessSystemUserMode");
AndroidMockitoHelper.mockUmIsHeadlessSystemUserMode(mode);
endTrace();
}
/**
* Mocks a call to {@link Binder#getCallingUserHandle()}.
*
* @throws IllegalStateException if class didn't override
* {@link #onSessionBuilder(CustomMockitoSessionBuilder)} and called
* {@code spyStatic(Binder.class)} on the session passed to it.
*/
protected final void mockGetCallingUserHandle(@UserIdInt int userId) {
if (VERBOSE) Log.v(TAG, getLogPrefix() + "mockBinderCallingUser(" + userId + ")");
assertSpied(Binder.class);
beginTrace("mockBinderCallingUser");
AndroidMockitoHelper.mockBinderGetCallingUserHandle(userId);
endTrace();
}
/**
* Starts a tracing message.
*
* MUST be followed by a {@link #endTrace()} calls.
*
* Ignored if {@value #VERBOSE} is {@code false}.
*/
protected final void beginTrace(@NonNull String message) {
if (mTracer == null) return;
Log.d(TAG, getLogPrefix() + message);
mTracer.traceBegin(message);
}
/**
* Ends a tracing call.
*
* MUST be called after {@link #beginTrace(String)}.
*
* Ignored if {@value #VERBOSE} is {@code false}.
*/
protected final void endTrace() {
if (mTracer == null) return;
mTracer.traceEnd();
}
private void interceptWtfCalls() {
mOldWtfHandler = Log.setWtfHandler((String tag, TerribleFailure what, boolean system) -> {
String message = "Called " + what;
Log.d(TAG, message); // Log always, as some test expect it
if (mLogTags != null && mLogTags.contains(tag)) {
mWtfs.add(new IllegalStateException(message));
} else if (VERBOSE) {
Log.v(TAG, "ignoring WTF invocation on tag " + tag + ". mLogTags=" + mLogTags);
}
});
}
private void resetWtfCalls() {
Log.setWtfHandler(mOldWtfHandler);
}
private void verifyWtfLogged() {
Preconditions.checkState(!mWtfs.isEmpty(), "no wtf() called");
}
private void verifyWtfNeverLogged() {
int size = mWtfs.size();
if (VERBOSE) {
Log.v(TAG, "verifyWtfNeverLogged(): mWtfs=" + mWtfs);
}
switch (size) {
case 0:
return;
case 1:
throw mWtfs.get(0);
default:
StringBuilder msg = new StringBuilder("wtf called ").append(size).append(" times")
.append(": ").append(mWtfs);
throw new AssertionError(msg.toString());
}
}
/**
* Gets a prefix for {@link Log} calls
*/
protected final String getLogPrefix() {
return getClass().getSimpleName() + ".";
}
/**
* Asserts the given class is being spied in the Mockito session.
*/
protected final void assertSpied(Class> clazz) {
Preconditions.checkArgument(mStaticSpiedClasses.contains(clazz),
"did not call spyStatic() on %s", clazz.getName());
}
/**
* Asserts the given class is being mocked in the Mockito session.
*/
protected final void assertMocked(Class> clazz) {
Preconditions.checkArgument(mStaticMockedClasses.contains(clazz),
"did not call mockStatic() on %s", clazz.getName());
}
/**
* Custom {@code MockitoSessionBuilder} used to make sure some pre-defined mock expectations
* (like {@link AbstractExtendedMockitoTestCase#mockGetCurrentUser(int)} fail if the test case
* didn't explicitly set it to spy / mock the required classes.
*
* NOTE: for now it only provides simple {@link #spyStatic(Class)}, but more methods
* (as provided by {@link StaticMockitoSessionBuilder}) could be provided as needed.
*/
public static final class CustomMockitoSessionBuilder {
private final StaticMockitoSessionBuilder mBuilder;
private final List