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