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