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.ResultSyncCallback.getImmutableList; 19 import static com.android.adservices.shared.util.Preconditions.checkState; 20 21 import android.os.IBinder; 22 23 import androidx.annotation.Nullable; 24 25 import com.android.internal.annotations.VisibleForTesting; 26 27 import java.util.List; 28 import java.util.Objects; 29 import java.util.stream.Collectors; 30 31 /** 32 * {@code SyncCallback} use to return an object (result) or a failure. 33 * 34 * @param <R> type of the object received on success. 35 * @param <F> type of the object received on failure. 36 */ 37 public class FailableResultSyncCallback<R, F> extends AbstractSyncCallback 38 implements IResultSyncCallback<R> { 39 40 @VisibleForTesting 41 public static final String INJECT_RESULT_OR_FAILURE = "injectResult() or injectFailure()"; 42 43 @VisibleForTesting 44 public static final String MSG_WRONG_ERROR_RECEIVED = 45 "expected error of type %s, but received %s"; 46 47 private final ResultSyncCallback<ResultOrFailure<R, F>> mCallback; 48 FailableResultSyncCallback()49 public FailableResultSyncCallback() { 50 this(SyncCallbackFactory.newSettingsBuilder().build()); 51 } 52 FailableResultSyncCallback(SyncCallbackSettings settings)53 public FailableResultSyncCallback(SyncCallbackSettings settings) { 54 super(settings); 55 56 mCallback = new ResultSyncCallback<>(this, settings); 57 } 58 59 /** 60 * Sets a failure as the outcome of the callback. 61 * 62 * @throws IllegalStateException if {@link #injectResult(R)} or {@link #injectError(F)} was 63 * already called. 64 */ injectFailure(F failure)65 public final void injectFailure(F failure) { 66 mCallback.injectResult( 67 new ResultOrFailure<>( 68 /* isResult= */ false, 69 /* result= */ null, 70 Objects.requireNonNull(failure))); 71 } 72 73 /** 74 * Asserts that {@link #injectFailure(Object)} was called, waiting up to {@link 75 * #getMaxTimeoutMs()} milliseconds before failing (if not called). 76 * 77 * <p>NOTE: it returns the result of the first call, which is sufficient for most use cases - if 78 * you're expecting multiple calls, you can get the further ones using {@link #getFailures()}. 79 * 80 * @return the first failure passed to {@link #injectFailure(Object)} or {@code null} if {@link 81 * #injectResult(Object)} was called first. 82 */ assertFailureReceived()83 public final @Nullable F assertFailureReceived() throws InterruptedException { 84 assertCalled(); 85 return getFailure(); 86 } 87 88 /** 89 * Asserts that {@link #injectFailure(Object)} was called with a class of type {@code S}, 90 * waiting up to {@link #getMaxTimeoutMs()} milliseconds before failing (if not called). 91 * 92 * @return the failure 93 */ assertFailureReceived(Class<S> expectedClass)94 public final <S extends F> S assertFailureReceived(Class<S> expectedClass) 95 throws InterruptedException { 96 Objects.requireNonNull(expectedClass); 97 F failure = assertFailureReceived(); 98 checkState( 99 expectedClass.isInstance(failure), 100 MSG_WRONG_ERROR_RECEIVED, 101 expectedClass, 102 failure); 103 return expectedClass.cast(failure); 104 } 105 106 /** 107 * Gets first failure returned by {@link #injectFailure(Object)} (or {@code null} if {@link 108 * #injectResult(Object)} was called first). 109 */ getFailure()110 public final @Nullable F getFailure() { 111 var resultOrFailure = mCallback.getResult(); 112 return resultOrFailure == null ? null : resultOrFailure.failure; 113 } 114 115 // NOTE: cannot use Guava's ImmutableList because it doesn't support null elements 116 /** 117 * Gets the result of all calls to {@link #injectFailure(Object)}, in order. 118 * 119 * @return immutable list with all failures 120 */ getFailures()121 public final List<F> getFailures() { 122 return getImmutableList( 123 mCallback.getResults().stream() 124 .filter(rof -> !rof.isResult) 125 .map(rof -> rof.failure) 126 .collect(Collectors.toList())); 127 } 128 129 @Override injectResult(R result)130 public final void injectResult(R result) { 131 mCallback.injectResult( 132 new ResultOrFailure<>(/* isResult= */ true, result, /* failure= */ null)); 133 } 134 135 @Override getResult()136 public final R getResult() { 137 var resultOrFailure = mCallback.getResult(); 138 return resultOrFailure == null ? null : resultOrFailure.result; 139 } 140 141 @Override getResults()142 public List<R> getResults() { 143 return getImmutableList( 144 mCallback.getResults().stream() 145 .filter(rof -> rof.isResult) 146 .map(rof -> rof.result) 147 .collect(Collectors.toList())); 148 } 149 150 @Override assertResultReceived()151 public final R assertResultReceived() throws InterruptedException { 152 assertCalled(); 153 return getResult(); 154 } 155 156 @Override getNumberActualCalls()157 public final int getNumberActualCalls() { 158 return mCallback.getNumberActualCalls(); 159 } 160 161 @Override assertCalled()162 public final void assertCalled() throws InterruptedException { 163 mCallback.internalAssertCalled(); 164 } 165 166 @Override asBinder()167 public IBinder asBinder() { 168 return null; 169 } 170 171 @Override customizeToString(StringBuilder string)172 protected void customizeToString(StringBuilder string) { 173 super.customizeToString(string); 174 if (!isCalled()) { 175 // "(no result yet)" is already added by mCallback 176 string.append(" (no failure yet)"); 177 } 178 // NOTE: ideally we should replace the result=... by failure=... (when there is a failure), 179 // but that would be hard to implement - and realistically, who cares? 180 mCallback.customizeToString(string); 181 } 182 183 private static final class ResultOrFailure<T, F> { 184 public final boolean isResult; 185 public final @Nullable T result; 186 public final @Nullable F failure; 187 ResultOrFailure(boolean isResult, @Nullable T result, @Nullable F failure)188 ResultOrFailure(boolean isResult, @Nullable T result, @Nullable F failure) { 189 this.isResult = isResult; 190 this.result = result; 191 this.failure = failure; 192 } 193 194 @Override toString()195 public String toString() { 196 return String.valueOf(isResult ? result : failure); 197 } 198 } 199 } 200