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 
17 package com.android.adservices.shared.testing;
18 
19 import android.util.Log;
20 
21 import org.junit.runner.Description;
22 
23 import java.util.Iterator;
24 import java.util.LinkedHashMap;
25 import java.util.LinkedHashSet;
26 import java.util.Map;
27 import java.util.Set;
28 import java.util.function.BiFunction;
29 import java.util.stream.Collectors;
30 
31 /**
32  * Abstract log verifier to hold common logic across all log verifiers.
33  *
34  * @param <T> expected {@link LogCall} type to be verified
35  */
36 public abstract class AbstractLogVerifier<T extends LogCall> implements LogVerifier {
37     protected final String mTag = getClass().getSimpleName();
38 
39     // Key is the LogCall object, value is the source of truth for times; we do not keep track of
40     // the times value in the LogCall object itself because we would like to group together all
41     // identical log call invocations, irrespective of number of times they have been invoked.
42     private final Map<T, Integer> mActualCalls = new LinkedHashMap<>();
43 
44     @Override
setup()45     public void setup() {
46         mockLogCalls();
47     }
48 
49     @Override
verify(Description description)50     public void verify(Description description) {
51         // Obtain expected log call entries from subclass.
52         Set<T> expectedCalls = getExpectedLogCalls(description);
53         // Extract actual log calls entries from map that was built through mocking log calls.
54         Set<T> actualCalls = getActualCalls();
55 
56         verifyCalls(expectedCalls, actualCalls);
57     }
58 
59     /**
60      * Mocks relevant calls in order to store metadata of log calls that were actually made.
61      * Subclasses are expected to call recordActualCall to store actual calls.
62      */
mockLogCalls()63     protected abstract void mockLogCalls();
64 
65     /**
66      * Return a set of {@link LogCall} to be verified.
67      *
68      * @param description test that was executed.
69      */
getExpectedLogCalls(Description description)70     protected abstract Set<T> getExpectedLogCalls(Description description);
71 
72     /**
73      * Returns relevant message providing information on how to use appropriate annotations when
74      * test fails due to mismatch of expected and actual log calls.
75      */
getResolutionMessage()76     protected abstract String getResolutionMessage();
77 
78     /**
79      * Subclasses are expected to use this method to record actual log call. Assumes only a single
80      * call is being recorded at once.
81      *
82      * @param actualCall Actual call to be recorded
83      */
recordActualCall(T actualCall)84     public void recordActualCall(T actualCall) {
85         mActualCalls.put(actualCall, mActualCalls.getOrDefault(actualCall, 0) + 1);
86     }
87 
88     /**
89      * Ensures log call parameter is > 0 for a given annotation type. Throws exception otherwise.
90      *
91      * @param times times to validate
92      * @param annotation name of annotation that holds the times value
93      */
validateTimes(int times, String annotation)94     public void validateTimes(int times, String annotation) {
95         if (times == 0) {
96             throw new IllegalArgumentException(
97                     "Detected @"
98                             + annotation
99                             + " with times = 0. Remove annotation as the "
100                             + "test will automatically fail if any log calls are detected.");
101         }
102 
103         if (times < 0) {
104             throw new IllegalArgumentException("Detected @" + annotation + " with times < 0!");
105         }
106     }
107 
108     /**
109      * Matching algorithm that detects any mismatches within expected and actual calls. This works
110      * for verifying wild card argument cases as well.
111      */
verifyCalls(Set<T> expectedCalls, Set<T> actualCalls)112     private void verifyCalls(Set<T> expectedCalls, Set<T> actualCalls) {
113         Log.v(mTag, "Total expected calls: " + expectedCalls.size());
114         Log.v(mTag, "Total actual calls: " + actualCalls.size());
115 
116         if (!containsAll(expectedCalls, actualCalls) || !containsAll(actualCalls, expectedCalls)) {
117             throw new IllegalStateException(constructErrorMessage(expectedCalls, actualCalls));
118         }
119 
120         Log.v(mTag, "All log calls successfully verified!");
121     }
122 
123     /**
124      * "Brute-force" algorithm to verify if all the LogCalls in first set are present in the second
125      * set.
126      *
127      * <p>To support wild card matching of parameters, we need to identify 1:1 matching of LogCalls
128      * in both sets. This is achieved by scanning for matching calls using {@link
129      * LogCall#equals(Object)} and then followed by {@link LogCall#isEquivalentInvocation(LogCall)}.
130      * This way, log calls are matched exactly based on their parameters first before wild card.
131      *
132      * <p>Note: In reality, the size of the sets are expected to be very small, so performance
133      * tuning isn't a major concern.
134      */
containsAll(Set<T> calls1, Set<T> calls2)135     private boolean containsAll(Set<T> calls1, Set<T> calls2) {
136         // Create wrapper objects so times can be altered safely.
137         Set<MutableLogCall> mutableCalls1 = createMutableLogCalls(calls1);
138         Set<MutableLogCall> mutableCalls2 = createMutableLogCalls(calls2);
139 
140         removeAll(mutableCalls1, mutableCalls2, MutableLogCall::isLogCallEqual);
141         if (!calls1.isEmpty()) {
142             removeAll(mutableCalls1, mutableCalls2, MutableLogCall::isLogCallEquivalentInvocation);
143         }
144 
145         return mutableCalls1.isEmpty();
146     }
147 
createMutableLogCalls(Set<T> calls)148     private Set<MutableLogCall> createMutableLogCalls(Set<T> calls) {
149         return calls.stream()
150                 .map(call -> new MutableLogCall(call, call.mTimes))
151                 .collect(Collectors.toCollection(LinkedHashSet::new));
152     }
153 
154     /**
155      * Algorithm iterates through each call in the second list and removes the corresponding match
156      * in the first list given an equality function definition. Also removes element in the second
157      * list if all corresponding matches are identified in the first list.
158      */
removeAll( Set<MutableLogCall> calls1, Set<MutableLogCall> calls2, BiFunction<MutableLogCall, MutableLogCall, Boolean> func)159     private void removeAll(
160             Set<MutableLogCall> calls1,
161             Set<MutableLogCall> calls2,
162             BiFunction<MutableLogCall, MutableLogCall, Boolean> func) {
163         Iterator<MutableLogCall> iterator2 = calls2.iterator();
164         while (iterator2.hasNext()) {
165             // LogCall to find a match for in the first list
166             MutableLogCall c2 = iterator2.next();
167             Iterator<MutableLogCall> iterator1 = calls1.iterator();
168             while (iterator1.hasNext()) {
169                 MutableLogCall c1 = iterator1.next();
170                 // Use custom equality definition to identify if two LogCalls are matching and
171                 // alter times based on their frequency.
172                 if (func.apply(c1, c2)) {
173                     if (c1.mTimes >= c2.mTimes) {
174                         c1.mTimes -= c2.mTimes;
175                         c2.mTimes = 0;
176                     } else {
177                         c2.mTimes -= c1.mTimes;
178                         c1.mTimes = 0;
179                     }
180                 }
181                 // LogCall in the first list has a corresponding match in the second list. Remove it
182                 // so it can no longer be used.
183                 if (c1.mTimes == 0) {
184                     iterator1.remove();
185                 }
186                 // Match for LogCall in the second list has been identified, remove it and move
187                 // on to the next element.
188                 if (c2.mTimes == 0) {
189                     iterator2.remove();
190                     break;
191                 }
192             }
193         }
194     }
195 
callsToStr(Set<T> calls)196     private String callsToStr(Set<T> calls) {
197         return calls.stream().map(call -> "\n\t" + call).reduce("", (a, b) -> a + b);
198     }
199 
getActualCalls()200     private Set<T> getActualCalls() {
201         // At this point, the map of actual calls will no longer be updated. Therefore, it's safe
202         // // to alter the times field in the actual LogCall objects and retrieve all keys.
203         mActualCalls
204                 .keySet()
205                 .forEach(actualLogCall -> actualLogCall.mTimes = mActualCalls.get(actualLogCall));
206 
207         // Create new set to rehash
208         return new LinkedHashSet<>(mActualCalls.keySet());
209     }
210 
constructErrorMessage(Set<T> expectedCalls, Set<T> actualCalls)211     private String constructErrorMessage(Set<T> expectedCalls, Set<T> actualCalls) {
212         StringBuilder message = new StringBuilder();
213         // Header
214         message.append("Detected mismatch in logging calls between expected and actual:\n");
215         // Print recorded expected calls
216         message.append("Expected Calls:\n[").append(callsToStr(expectedCalls)).append("\n]\n");
217         // Print recorded actual calls
218         message.append("Actual Calls:\n[").append(callsToStr(actualCalls)).append("\n]\n");
219         // Print hint to use annotations - just in case test author isn't aware.
220         message.append(getResolutionMessage()).append('\n');
221 
222         return message.toString();
223     }
224 
225     /**
226      * Internal wrapper class that encapsulates the log call and number of times so the times can be
227      * altered safely during the log verification process.
228      */
229     private final class MutableLogCall {
230         private final T mLogCall;
231         private int mTimes;
232 
MutableLogCall(T logCall, int times)233         private MutableLogCall(T logCall, int times) {
234             mLogCall = logCall;
235             mTimes = times;
236         }
237 
238         /*
239          * Util method to check if log calls encapsulated within two MutableLogCall objects are
240          * equal.
241          */
isLogCallEqual(MutableLogCall other)242         private boolean isLogCallEqual(MutableLogCall other) {
243             return mLogCall.equals(other.mLogCall);
244         }
245 
246         /*
247          * Util method to check if log calls encapsulated within two MutableLogCall objects have
248          * equivalent invocations.
249          */
isLogCallEquivalentInvocation(MutableLogCall other)250         private boolean isLogCallEquivalentInvocation(MutableLogCall other) {
251             return mLogCall.isEquivalentInvocation(other.mLogCall);
252         }
253     }
254 }
255