/*
* 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.
*
*
It's derived from {@link StaticMockFixtureRule}, with the additional features:
*
*
* - Easier to define which classes must be statically mocked or spied
*
- Automatically starts mocking (so tests don't need a mockito runner or rule)
*
- Automatically clears the inlined mocks at the end (to avoid OOM)
*
- Allows other customization like strictness
*
*/
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> mMockedStaticClasses = new ArrayList<>();
private final List> mSpiedStaticClasses = new ArrayList<>();
private @Nullable Visitor 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 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> mMockedStaticClasses;
private final List> mSpiedStaticClasses;
@Nullable
private final Visitor mDynamicSessionBuilderConfigurator;
@Nullable
private final Runnable mAfterSessionFinishedCallback;
private SimpleStatickMockFixture(List> mockedStaticClasses,
List> spiedStaticClasses,
@Nullable Visitor 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();
}
}
}
}