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 com.android.adservices.shared.testing.Nullable;
19 
20 import com.google.common.annotations.VisibleForTesting;
21 import com.google.errorprone.annotations.FormatMethod;
22 import com.google.errorprone.annotations.FormatString;
23 import com.google.errorprone.annotations.concurrent.GuardedBy;
24 
25 import java.util.Locale;
26 import java.util.Objects;
27 import java.util.concurrent.atomic.AtomicInteger;
28 
29 /** Base implementation for all {@code SyncCallback} classes. */
30 public abstract class AbstractSyncCallback implements SyncCallback, FreezableToString {
31 
32     private static final AtomicInteger sIdGenerator = new AtomicInteger();
33 
34     protected final SyncCallbackSettings mSettings;
35 
36     private final String mId = String.valueOf(sIdGenerator.incrementAndGet());
37 
38     private final Object mLock = new Object();
39 
40     @GuardedBy("mLock")
41     private int mNumberCalls;
42 
43     @GuardedBy("mLock")
44     @Nullable
45     private String mFrozenToString;
46 
47     // Used to fail assertCalled() if something bad happened before
48     @Nullable private RuntimeException mOnAssertCalledException;
49 
50     // The "real" callback - used in cases (mostly loggin) where a callback delegates its methods
51     // to another one.
52     private final AbstractSyncCallback mRealCallback;
53 
54     /** Default constructor. */
AbstractSyncCallback(SyncCallbackSettings settings)55     public AbstractSyncCallback(SyncCallbackSettings settings) {
56         this(/* realCallback= */ null, settings);
57     }
58 
59     @VisibleForTesting
AbstractSyncCallback( @ullable AbstractSyncCallback realCallback, SyncCallbackSettings settings)60     AbstractSyncCallback(
61             @Nullable AbstractSyncCallback realCallback, SyncCallbackSettings settings) {
62         mRealCallback = realCallback != null ? realCallback : this;
63         mSettings = Objects.requireNonNull(settings, "settings cannot be null");
64     }
65 
66     @Override
freezeToString()67     public final void freezeToString() {
68         synchronized (mLock) {
69             mFrozenToString = "FROZEN" + toStringLite();
70         }
71     }
72 
73     /**
74      * By default is a no-op, but subclasses could override to add additional info to {@code
75      * toString()}.
76      */
customizeToString(StringBuilder string)77     protected void customizeToString(StringBuilder string) {
78         string.append(", ")
79                 .append(mSettings)
80                 .append(", numberActualCalls=")
81                 .append(getNumberActualCalls());
82     }
83 
84     @Override
getId()85     public final String getId() {
86         return mId;
87     }
88 
89     @Override
getSettings()90     public final SyncCallbackSettings getSettings() {
91         return mSettings;
92     }
93 
94     // Note: making msgFmt final to avoid [FormatStringAnnotation] errorprone warning
95     /**
96      * Convenience method to log an error message, it includes the whole {@link #toString()} in the
97      * message.
98      */
99     @FormatMethod
logE(@ormatString final String msgFmt, @Nullable Object... msgArgs)100     protected final void logE(@FormatString final String msgFmt, @Nullable Object... msgArgs) {
101         String msg = String.format(Locale.ENGLISH, msgFmt, msgArgs);
102         mSettings.getLogger().e("%s: %s", mRealCallback, msg);
103     }
104 
105     // Note: making msgFmt final to avoid [FormatStringAnnotation] errorprone warning
106     /**
107      * Convenience method to log a debug message, it includes the summarized {@link #toStringLite()}
108      * in the message.
109      */
110     @FormatMethod
logD(@ormatString final String msgFmt, @Nullable Object... msgArgs)111     protected final void logD(@FormatString final String msgFmt, @Nullable Object... msgArgs) {
112         String msg = String.format(Locale.ENGLISH, msgFmt, msgArgs);
113         mSettings.getLogger().d("%s: %s", mRealCallback.toStringLite(), msg);
114     }
115 
116     // Note: making msgFmt final to avoid [FormatStringAnnotation] errorprone warning
117     /**
118      * Convenience method to log a verbose message, it includes the whole {@link #toString()} in the
119      * message.
120      */
121     @FormatMethod
logV(@ormatString final String msgFmt, @Nullable Object... msgArgs)122     protected final void logV(@FormatString final String msgFmt, @Nullable Object... msgArgs) {
123         String msg = String.format(Locale.ENGLISH, msgFmt, msgArgs);
124         mSettings.getLogger().v("%s: %s", mRealCallback, msg);
125     }
126 
setOnAssertCalledException(@ullable RuntimeException exception)127     protected void setOnAssertCalledException(@Nullable RuntimeException exception) {
128         mOnAssertCalledException = exception;
129     }
130 
131     // TODO(b/342448771): make it package protected once classes are moved
132     /**
133      * Real implementation of {@code setCalled()}, should be called by subclass to "unblock" the
134      * callback.
135      *
136      * @return {@code methodName}
137      */
internalSetCalled(String methodName)138     public final String internalSetCalled(String methodName) {
139         logD("%s called on %s", methodName, Thread.currentThread().getName());
140         if (mSettings.isFailIfCalledOnMainThread() && mSettings.isMainThread()) {
141             String errorMsg =
142                     methodName
143                             + " called on main thread ("
144                             + Thread.currentThread().getName()
145                             + ")";
146             setOnAssertCalledException(new CalledOnMainThreadException(errorMsg));
147         }
148 
149         synchronized (mLock) {
150             mNumberCalls++;
151         }
152         mSettings.countDown();
153         logV("%s returning", methodName);
154         return methodName;
155     }
156 
157     @Override
assertCalled()158     public void assertCalled() throws InterruptedException {
159         internalAssertCalled();
160     }
161 
162     /**
163      * Real implementation of {@link #assertCalled()} - subclasses overriding {@link
164      * #assertCalled()} should call it.
165      */
internalAssertCalled()166     protected final void internalAssertCalled() throws InterruptedException {
167         logD("assertCalled() called on %s", Thread.currentThread().getName());
168         try {
169             mSettings.assertCalled(() -> toString());
170         } catch (Exception e) {
171             logE("assertCalled() failed: %s", e);
172             throw e;
173         }
174         if (mOnAssertCalledException != null) {
175             logE("assertCalled() failed: %s", mOnAssertCalledException);
176             throw mOnAssertCalledException;
177         }
178         logV("assertCalled() returning");
179     }
180 
181     @Override
isCalled()182     public final boolean isCalled() {
183         return mSettings.isCalled();
184     }
185 
186     @Override
getNumberActualCalls()187     public int getNumberActualCalls() {
188         synchronized (mLock) {
189             return mNumberCalls;
190         }
191     }
192 
193     @Override
toString()194     public final String toString() {
195         synchronized (mLock) {
196             if (mFrozenToString != null) {
197                 return mFrozenToString;
198             }
199         }
200         StringBuilder string =
201                 new StringBuilder("[")
202                         .append(getClass().getSimpleName())
203                         .append(": id=")
204                         .append(mId)
205                         .append(", onAssertCalledException=")
206                         .append(mOnAssertCalledException);
207         customizeToString(string);
208         return string.append(']').toString();
209     }
210 
211     /** Gets a simpler representation of the callback. */
toStringLite()212     public final String toStringLite() {
213         return '[' + getClass().getSimpleName() + "#" + mId + ']';
214     }
215 }
216