1 /*
2  * Copyright (C) 2024 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 package com.android.adservices.shared.testing.concurrency;
17 
18 import static com.android.adservices.shared.testing.concurrency.SyncCallback.LOG_TAG;
19 
20 import com.android.adservices.shared.testing.Identifiable;
21 import com.android.adservices.shared.testing.Logger;
22 import com.android.adservices.shared.testing.Logger.RealLogger;
23 
24 import com.google.common.annotations.VisibleForTesting;
25 
26 import java.util.Objects;
27 import java.util.concurrent.CountDownLatch;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.atomic.AtomicInteger;
30 import java.util.function.Supplier;
31 
32 /**
33  * Defines the settings and some other internal state of a {@code SyncCallback}.
34  *
35  * <p><b>Note: </b>the internal state includes the current invocations, so normally each callback
36  * should have its own settings (for example, it should not be used as a static variable with some
37  * default settings), although it could be shared when a test need to block after distinct callbacks
38  * are called.
39  */
40 public final class SyncCallbackSettings implements Identifiable {
41 
42     /** Timeout set by default constructor */
43     public static final long DEFAULT_TIMEOUT_MS = 5_000;
44 
45     private static final AtomicInteger sIdGenerator = new AtomicInteger();
46 
47     private final String mId = String.valueOf(sIdGenerator.incrementAndGet());
48     private final int mExpectedNumberCalls;
49     private final CountDownLatch mLatch;
50     private final long mMaxTimeoutMs;
51     private final boolean mFailIfCalledOnMainThread;
52     private final Supplier<Boolean> mIsMainThreadSupplier;
53     private final Logger mLogger;
54 
SyncCallbackSettings(Builder builder)55     private SyncCallbackSettings(Builder builder) {
56         mExpectedNumberCalls = builder.mExpectedNumberCalls;
57         mLatch = new CountDownLatch(mExpectedNumberCalls);
58         mMaxTimeoutMs = builder.mMaxTimeoutMs;
59         mFailIfCalledOnMainThread = builder.mFailIfCalledOnMainThread;
60         mIsMainThreadSupplier = builder.mIsMainThreadSupplier;
61         mLogger = new Logger(builder.mRealLogger, LOG_TAG);
62     }
63 
64     /** Gets the expected number of calls this callback should receive before it's done. */
getExpectedNumberCalls()65     public int getExpectedNumberCalls() {
66         return mExpectedNumberCalls;
67     }
68 
69     /** Gets the maximum time the callback could wait before failing. */
getMaxTimeoutMs()70     public long getMaxTimeoutMs() {
71         return mMaxTimeoutMs;
72     }
73 
74     /** Checks whether the callback should fail if called from the main thread. */
isFailIfCalledOnMainThread()75     public boolean isFailIfCalledOnMainThread() {
76         return mFailIfCalledOnMainThread;
77     }
78 
isMainThread()79     boolean isMainThread() {
80         return mIsMainThreadSupplier.get();
81     }
82 
83     @Override
getId()84     public String getId() {
85         return mId;
86     }
87 
88     @Override
toString()89     public String toString() {
90         return "settingsId="
91                 + mId
92                 + ", expectedNumberCalls="
93                 + mExpectedNumberCalls
94                 + ", maxTimeoutMs="
95                 + mMaxTimeoutMs
96                 + ", failIfCalledOnMainThread="
97                 + mFailIfCalledOnMainThread
98                 + ", missingCalls="
99                 + mLatch.getCount();
100     }
101 
getLogger()102     Logger getLogger() {
103         return mLogger;
104     }
105 
106     // NOTE: log of methods below are indirectly unit tested by the callback test - testing it again
107     // on SyncCallbackSettings would be an overkill (they're not public anyways)
108 
countDown()109     void countDown() {
110         mLatch.countDown();
111     }
112 
assertCalled(Supplier<String> caller)113     void assertCalled(Supplier<String> caller) throws InterruptedException {
114         long timeoutMs = getMaxTimeoutMs();
115         TimeUnit unit = TimeUnit.MILLISECONDS;
116         if (!mLatch.await(timeoutMs, unit)) {
117             throw new SyncCallbackTimeoutException(caller.get(), timeoutMs, unit);
118         }
119     }
120 
isCalled()121     boolean isCalled() {
122         return mLatch.getCount() == 0;
123     }
124 
125     /**
126      * Checks that the given settings is not configured to {@link
127      * SyncCallbackSettings#isFailIfCalledOnMainThread() fail if called in the main thread}.
128      *
129      * @return same settings
130      * @throws IllegalArgumentException if configured to {@link
131      *     SyncCallbackSettings#isFailIfCalledOnMainThread() fail if called in the main thread}.
132      */
checkCanFailOnMainThread(SyncCallbackSettings settings)133     public static SyncCallbackSettings checkCanFailOnMainThread(SyncCallbackSettings settings) {
134         if (settings.isFailIfCalledOnMainThread()) {
135             throw new IllegalArgumentException(
136                     "Cannot use a SyncCallbackSettings ("
137                             + settings
138                             + ") that fails if called on main thread");
139         }
140         return settings;
141     }
142 
143     /** Bob the Builder! */
144     public static final class Builder {
145 
146         private int mExpectedNumberCalls = 1;
147         private long mMaxTimeoutMs = DEFAULT_TIMEOUT_MS;
148         private boolean mFailIfCalledOnMainThread = true;
149         private final Supplier<Boolean> mIsMainThreadSupplier;
150         private final RealLogger mRealLogger;
151 
152         // Package protected so it's only called by SyncCallbackFactory and unit tests
Builder(RealLogger realLogger, Supplier<Boolean> isMainThreadSupplier)153         Builder(RealLogger realLogger, Supplier<Boolean> isMainThreadSupplier) {
154             mRealLogger = Objects.requireNonNull(realLogger, "realLogger cannot be null");
155             mIsMainThreadSupplier =
156                     Objects.requireNonNull(
157                             isMainThreadSupplier, "isMainThreadSupplier cannot be null");
158         }
159 
160         @VisibleForTesting
Builder(RealLogger realLogger)161         Builder(RealLogger realLogger) {
162             this(realLogger, () -> Boolean.FALSE);
163         }
164 
165         /** See {@link SyncCallbackSettings#getExpectedNumberCalls()}. */
setExpectedNumberCalls(int value)166         public Builder setExpectedNumberCalls(int value) {
167             assertIsPositive(value);
168             mExpectedNumberCalls = value;
169             return this;
170         }
171 
172         /** See {@link SyncCallbackSettings#getMaxTimeoutMs(long)}. */
setMaxTimeoutMs(long value)173         public Builder setMaxTimeoutMs(long value) {
174             assertIsPositive(value);
175             mMaxTimeoutMs = value;
176             return this;
177         }
178 
179         /** See {@link SyncCallbackSettings#isFailIfCalledOnMainThread()}. */
setFailIfCalledOnMainThread(boolean value)180         public Builder setFailIfCalledOnMainThread(boolean value) {
181             mFailIfCalledOnMainThread = value;
182             return this;
183         }
184 
185         /** Can we build it? Yes we can! */
build()186         public SyncCallbackSettings build() {
187             return new SyncCallbackSettings(this);
188         }
189     }
190 
assertIsPositive(long value)191     private static void assertIsPositive(long value) {
192         if (value <= 0) {
193             throw new IllegalArgumentException("must be a positive value");
194         }
195     }
196 }
197