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