1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.adservices.mockito;
18 
19 import static com.android.adservices.shared.testing.concurrency.SyncCallbackSettings.DEFAULT_TIMEOUT_MS;
20 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doCallRealMethod;
21 
22 import static org.mockito.ArgumentMatchers.any;
23 import static org.mockito.ArgumentMatchers.anyBoolean;
24 import static org.mockito.ArgumentMatchers.anyInt;
25 import static org.mockito.ArgumentMatchers.anyLong;
26 import static org.mockito.Mockito.doAnswer;
27 import static org.mockito.Mockito.mock;
28 import static org.mockito.Mockito.never;
29 import static org.mockito.Mockito.spy;
30 import static org.mockito.Mockito.verify;
31 import static org.mockito.Mockito.when;
32 
33 import android.app.job.JobParameters;
34 import android.app.job.JobService;
35 import android.content.Context;
36 import android.util.Log;
37 
38 import com.android.adservices.service.Flags;
39 import com.android.adservices.service.stats.AdServicesLogger;
40 import com.android.adservices.service.stats.ApiCallStats;
41 import com.android.adservices.shared.errorlogging.AdServicesErrorLogger;
42 import com.android.adservices.shared.testing.JobServiceLoggingCallback;
43 import com.android.adservices.shared.testing.concurrency.ResultSyncCallback;
44 import com.android.adservices.shared.testing.concurrency.SyncCallbackFactory;
45 import com.android.adservices.shared.util.Clock;
46 import com.android.adservices.spe.AdServicesJobInfo;
47 import com.android.adservices.spe.AdServicesJobServiceLogger;
48 import com.android.adservices.spe.AdServicesStatsdJobServiceLogger;
49 
50 import java.util.concurrent.Executors;
51 
52 /** Provides Mockito expectation for common calls. */
53 public final class MockitoExpectations {
54 
55     private static final String TAG = MockitoExpectations.class.getSimpleName();
56 
57     /**
58      * Mocks a call to {@link AdServicesLogger#logApiCallStats(ApiCallStats)} and returns a callback
59      * object that blocks until that call is made.
60      */
mockLogApiCallStats( AdServicesLogger adServicesLogger)61     public static ResultSyncCallback<ApiCallStats> mockLogApiCallStats(
62             AdServicesLogger adServicesLogger) {
63         return mockLogApiCallStats(adServicesLogger, DEFAULT_TIMEOUT_MS);
64     }
65 
66     /**
67      * Mocks a call to {@link AdServicesLogger#logApiCallStats(ApiCallStats)} and returns a callback
68      * object that blocks until that call is made. This method allows to pass in a customized
69      * timeout.
70      */
mockLogApiCallStats( AdServicesLogger adServicesLogger, long timeoutMs)71     public static ResultSyncCallback<ApiCallStats> mockLogApiCallStats(
72             AdServicesLogger adServicesLogger, long timeoutMs) {
73         ResultSyncCallback<ApiCallStats> callback =
74                 new ResultSyncCallback<>(
75                         SyncCallbackFactory.newSettingsBuilder()
76                                 .setMaxTimeoutMs(timeoutMs)
77                                 .build());
78 
79         doAnswer(
80                         inv -> {
81                             Log.v(TAG, "mockLogApiCallStats(): inv=" + inv);
82                             ApiCallStats apiCallStats = inv.getArgument(0);
83                             callback.injectResult(apiCallStats);
84                             return null;
85                         })
86                 .when(adServicesLogger)
87                 .logApiCallStats(any());
88 
89         return callback;
90     }
91 
92     /** Verifies methods in {@link AdServicesJobServiceLogger} were never called. */
verifyLoggingNotHappened(AdServicesJobServiceLogger logger)93     public static void verifyLoggingNotHappened(AdServicesJobServiceLogger logger) {
94         // Mock logger to call actual public logging methods. Because when the feature flag of
95         // logging is on, these methods are actually called, but the internal logging methods will
96         // not be invoked.
97         callRealPublicMethodsForBackgroundJobLogging(logger);
98 
99         verify(logger, never()).persistJobExecutionData(anyInt(), anyLong());
100         verify(logger, never()).logExecutionStats(anyInt(), anyLong(), anyInt(), anyInt());
101     }
102 
103     /** Verifies {@link AdServicesJobServiceLogger#recordJobSkipped(int, int)} is called once. */
verifyBackgroundJobsSkipLogged( AdServicesJobServiceLogger logger, JobServiceLoggingCallback callback)104     public static void verifyBackgroundJobsSkipLogged(
105             AdServicesJobServiceLogger logger, JobServiceLoggingCallback callback)
106             throws InterruptedException {
107         callback.assertLoggingFinished();
108 
109         verify(logger).recordJobSkipped(anyInt(), anyInt());
110     }
111 
112     /** Verifies the logging flow of a successful {@link JobService}'s execution is called once. */
verifyJobFinishedLogged( AdServicesJobServiceLogger logger, JobServiceLoggingCallback onStartJobCallback, JobServiceLoggingCallback onJobDoneCallback)113     public static void verifyJobFinishedLogged(
114             AdServicesJobServiceLogger logger,
115             JobServiceLoggingCallback onStartJobCallback,
116             JobServiceLoggingCallback onJobDoneCallback)
117             throws InterruptedException {
118         verifyOnStartJobLogged(logger, onStartJobCallback);
119         verifyOnJobFinishedLogged(logger, onJobDoneCallback);
120     }
121 
122     /**
123      * Verifies {@link AdServicesJobServiceLogger#recordOnStopJob(JobParameters, int, boolean)} is
124      * called once.
125      */
verifyOnStopJobLogged( AdServicesJobServiceLogger logger, JobServiceLoggingCallback callback)126     public static void verifyOnStopJobLogged(
127             AdServicesJobServiceLogger logger, JobServiceLoggingCallback callback)
128             throws InterruptedException {
129         callback.assertLoggingFinished();
130 
131         verify(logger).recordOnStopJob(any(), anyInt(), anyBoolean());
132     }
133 
134     /**
135      * Mocks a call to {@link Flags#getBackgroundJobsLoggingKillSwitch()}, returning overrideValue.
136      */
mockBackgroundJobsLoggingKillSwitch(Flags flag, boolean overrideValue)137     public static void mockBackgroundJobsLoggingKillSwitch(Flags flag, boolean overrideValue) {
138         when(flag.getBackgroundJobsLoggingKillSwitch()).thenReturn(overrideValue);
139     }
140 
141     /** Mocks a call to {@link Flags#getCobaltLoggingEnabled()}, returning overrideValue. */
mockCobaltLoggingEnabled(Flags flags, boolean enabled)142     public static void mockCobaltLoggingEnabled(Flags flags, boolean enabled) {
143         when(flags.getCobaltLoggingEnabled()).thenReturn(enabled);
144     }
145 
146     /**
147      * Mocks a call to {@link Flags#getAppNameApiErrorCobaltLoggingEnabled()}, returning
148      * overrideValue.
149      */
mockAppNameApiErrorCobaltLoggingEnabled(Flags flags, boolean enabled)150     public static void mockAppNameApiErrorCobaltLoggingEnabled(Flags flags, boolean enabled) {
151         when(flags.getAppNameApiErrorCobaltLoggingEnabled()).thenReturn(enabled);
152     }
153 
154     /**
155      * Mocks a call to {@link Flags#getMsmtRegistrationCobaltLoggingEnabled()} ()}, returning
156      * overrideValue.
157      */
mockMsmtRegistrationCobaltLoggingEnabled(Flags flags, boolean enabled)158     public static void mockMsmtRegistrationCobaltLoggingEnabled(Flags flags, boolean enabled) {
159         when(flags.getMsmtRegistrationCobaltLoggingEnabled()).thenReturn(enabled);
160     }
161 
162     /**
163      * Mocks a call to {@link Flags#getAdservicesReleaseStageForCobalt()}, returning {@code DEBUG}
164      * as the testing release stage.
165      */
mockAdservicesReleaseStageForCobalt(Flags flags)166     public static void mockAdservicesReleaseStageForCobalt(Flags flags) {
167         when(flags.getAdservicesReleaseStageForCobalt()).thenReturn("DEBUG");
168     }
169 
170     /** Mocks calls to override Cobalt app name api error logging related flags. */
mockCobaltLoggingFlags(Flags flags, boolean override)171     public static void mockCobaltLoggingFlags(Flags flags, boolean override) {
172         mockCobaltLoggingEnabled(flags, override);
173         mockAppNameApiErrorCobaltLoggingEnabled(flags, override);
174         mockAdservicesReleaseStageForCobalt(flags);
175     }
176 
177     /**
178      * Mock {@link AdServicesJobServiceLogger#persistJobExecutionData(int, long)} to wait for it to
179      * complete.
180      */
syncPersistJobExecutionData( AdServicesJobServiceLogger logger)181     public static JobServiceLoggingCallback syncPersistJobExecutionData(
182             AdServicesJobServiceLogger logger) {
183         JobServiceLoggingCallback callback = new JobServiceLoggingCallback();
184         doAnswer(
185                         invocation -> {
186                             invocation.callRealMethod();
187                             callback.onLoggingMethodCalled();
188                             return null;
189                         })
190                 .when(logger)
191                 .recordOnStartJob(anyInt());
192 
193         return callback;
194     }
195 
196     /**
197      * Mock one of below 3 endpoints for a {@link JobService}'s execution to wait for it to
198      * complete.
199      *
200      * <ul>
201      *   <li>{@link AdServicesJobServiceLogger#recordOnStopJob(JobParameters, int, boolean)}
202      *   <li>{@link AdServicesJobServiceLogger#recordJobSkipped(int, int)}
203      *   <li>{@link AdServicesJobServiceLogger#recordJobFinished(int, boolean, boolean)}
204      * </ul>
205      *
206      * @throws IllegalStateException if there is more than one method is called.
207      */
syncLogExecutionStats( AdServicesJobServiceLogger logger)208     public static JobServiceLoggingCallback syncLogExecutionStats(
209             AdServicesJobServiceLogger logger) {
210         JobServiceLoggingCallback callback = new JobServiceLoggingCallback();
211 
212         doAnswer(
213                         invocation -> {
214                             callback.onLoggingMethodCalled();
215                             return null;
216                         })
217                 .when(logger)
218                 .recordOnStopJob(any(), anyInt(), anyBoolean());
219 
220         doAnswer(
221                         invocation -> {
222                             callback.onLoggingMethodCalled();
223                             return null;
224                         })
225                 .when(logger)
226                 .recordJobSkipped(anyInt(), anyInt());
227 
228         doAnswer(
229                         invocation -> {
230                             callback.onLoggingMethodCalled();
231                             return null;
232                         })
233                 .when(logger)
234                 .recordJobFinished(anyInt(), anyBoolean(), anyBoolean());
235 
236         return callback;
237     }
238 
239     /**
240      * Verify the logging methods in {@link JobService#onStartJob(JobParameters)} has been invoked.
241      */
verifyOnStartJobLogged( AdServicesJobServiceLogger logger, JobServiceLoggingCallback callback)242     public static void verifyOnStartJobLogged(
243             AdServicesJobServiceLogger logger, JobServiceLoggingCallback callback)
244             throws InterruptedException {
245         callback.assertLoggingFinished();
246 
247         verify(logger).recordOnStartJob(anyInt());
248     }
249 
250     /**
251      * Verify the logging methods in {@link JobService#jobFinished(JobParameters, boolean)} has been
252      * invoked.
253      */
verifyOnJobFinishedLogged( AdServicesJobServiceLogger logger, JobServiceLoggingCallback callback)254     public static void verifyOnJobFinishedLogged(
255             AdServicesJobServiceLogger logger, JobServiceLoggingCallback callback)
256             throws InterruptedException {
257         callback.assertLoggingFinished();
258 
259         verify(logger).recordJobFinished(anyInt(), anyBoolean(), anyBoolean());
260     }
261 
262     /** Get a spied instance of {@link AdServicesJobServiceLogger}. */
getSpiedAdServicesJobServiceLogger( Context context, Flags flags)263     public static AdServicesJobServiceLogger getSpiedAdServicesJobServiceLogger(
264             Context context, Flags flags) {
265         return spy(
266                 new AdServicesJobServiceLogger(
267                         context,
268                         Clock.getInstance(),
269                         mock(AdServicesStatsdJobServiceLogger.class),
270                         mock(AdServicesErrorLogger.class),
271                         Executors.newCachedThreadPool(),
272                         AdServicesJobInfo.getJobIdToJobNameMap(),
273                         flags));
274     }
275 
MockitoExpectations()276     private MockitoExpectations() {
277         throw new UnsupportedOperationException("Provides only static methods");
278     }
279 
callRealPublicMethodsForBackgroundJobLogging( AdServicesJobServiceLogger logger)280     private static void callRealPublicMethodsForBackgroundJobLogging(
281             AdServicesJobServiceLogger logger) {
282         doCallRealMethod().when(logger).recordOnStartJob(anyInt());
283         doCallRealMethod().when(logger).recordOnStopJob(any(), anyInt(), anyBoolean());
284         doCallRealMethod().when(logger).recordJobSkipped(anyInt(), anyInt());
285         doCallRealMethod().when(logger).recordJobFinished(anyInt(), anyBoolean(), anyBoolean());
286     }
287 }
288