/* * 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.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.Mockito.when; 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.os.Handler; import android.os.HandlerThread; import android.os.Trace; import android.os.UserManager; import android.provider.Settings; import android.util.Log; 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.MockitoSession; import org.mockito.invocation.InvocationOnMock; import org.mockito.quality.Strictness; import org.mockito.session.MockitoSessionBuilder; import org.mockito.stubbing.Answer; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; 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 {
private static final String TAG = AbstractExtendedMockitoTestCase.class.getSimpleName();
private static final boolean TRACE = false;
private static final boolean VERBOSE = false;
private final ListThis can prevent pending Handler tasks of one test from affecting another. This does not
* work if the message is posted with delay.
*/
protected void completeAllHandlerThreadTasks() {
beginTrace("completeAllHandlerThreadTasks");
Set Typically, it should be overridden when mocking static methods.
*/
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 #newSessionBuilder()} and
* called {@code spyStatic(ActivityManager.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 #newSessionBuilder()} and
* called {@code spyStatic(UserManager.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();
}
/**
* 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() {
doAnswer((invocation) -> {
return addWtf(invocation);
}).when(() -> Log.wtf(anyString(), anyString()));
doAnswer((invocation) -> {
return addWtf(invocation);
}).when(() -> Log.wtf(anyString(), anyString(), notNull()));
doAnswer((invocation) -> {
return addWtf(invocation);
}).when(() -> Slog.wtf(anyString(), anyString()));
doAnswer((invocation) -> {
return addWtf(invocation);
}).when(() -> Slog.wtf(anyString(), anyString(), notNull()));
}
private Object addWtf(InvocationOnMock invocation) {
String message = "Called " + invocation;
Log.d(TAG, message); // Log always, as some test expect it
mWtfs.add(new IllegalStateException(message));
return null;
}
private void verifyWtfLogged() {
Preconditions.checkState(!mWtfs.isEmpty(), "no wtf() called");
}
private void verifyWtfNeverLogged() {
int size = mWtfs.size();
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());
}
}
@NonNull
private MockitoSessionBuilder newSessionBuilder() {
// TODO (b/155523104): change from mock to spy
StaticMockitoSessionBuilder builder = mockitoSession()
.strictness(getSessionStrictness())
.mockStatic(Settings.Global.class)
.mockStatic(Settings.System.class)
.mockStatic(Settings.Secure.class);
CustomMockitoSessionBuilder customBuilder =
new CustomMockitoSessionBuilder(builder, mStaticSpiedClasses)
.spyStatic(Log.class)
.spyStatic(Slog.class);
onSessionBuilder(customBuilder);
if (VERBOSE) Log.v(TAG, "spied classes" + customBuilder.mStaticSpiedClasses);
return builder.initMocks(this);
}
/**
* Gets a prefix for {@link Log} calls
*/
protected String getLogPrefix() {
return getClass().getSimpleName() + ".";
}
/**
* Asserts the given class is being spied in the Mockito session.
*/
protected void assertSpied(Class> clazz) {
Preconditions.checkArgument(mStaticSpiedClasses.contains(clazz),
"did not call spyStatic() on %s", clazz.getName());
}
/**
* Custom {@code MockitoSessionBuilder} used to make sure some pre-defined mock stations
* (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