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