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