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.mockito; 18 19 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; 20 21 import static org.mockito.ArgumentMatchers.any; 22 import static org.mockito.ArgumentMatchers.eq; 23 24 import android.util.ArrayMap; 25 import android.util.Log; 26 27 import com.android.adservices.shared.testing.LogEntry; 28 import com.android.adservices.shared.testing.Logger.LogLevel; 29 import com.android.internal.util.Preconditions; 30 31 import java.util.ArrayList; 32 import java.util.Collections; 33 import java.util.List; 34 import java.util.Map; 35 import java.util.Objects; 36 import java.util.stream.Collectors; 37 38 // TODO(b/306522832): move to shared project (device-side) 39 /** Class used to intercept static calls to {@link Log} so they can be verified. */ 40 public final class LogInterceptor { 41 42 private static final String TAG = LogInterceptor.class.getSimpleName(); 43 44 private Map<String, List<LogEntry>> mEntries = new ArrayMap<>(); 45 LogInterceptor()46 private LogInterceptor() {} 47 48 /** 49 * Creates an interceptor for {@link Log} calls using the given {@code tag} and {@code levels}. 50 */ forTagAndLevels(String tag, LogLevel... levels)51 public static LogInterceptor forTagAndLevels(String tag, LogLevel... levels) { 52 // Log.v is used inside the Answers below, so it cannot be spied upon 53 Preconditions.checkArgument( 54 !TAG.equals(Objects.requireNonNull(tag, "tag cannot be null")), 55 "cannot intercept tag %s", 56 TAG); 57 58 LogInterceptor interceptor = new LogInterceptor(); 59 for (LogLevel level : levels) { 60 switch (level) { 61 case DEBUG: 62 doAnswer( 63 invocation -> { 64 Log.d(TAG, invocation.toString()); 65 interceptor.log( 66 new LogEntry( 67 LogLevel.DEBUG, 68 invocation.getArgument(0), 69 invocation.getArgument(1))); 70 invocation.callRealMethod(); 71 return null; 72 }) 73 .when(() -> Log.d(eq(tag), any())); 74 75 doAnswer( 76 invocation -> { 77 Log.d(TAG, invocation.toString()); 78 interceptor.log( 79 new LogEntry( 80 LogLevel.DEBUG, 81 invocation.getArgument(0), 82 invocation.getArgument(1), 83 invocation.getArgument(2))); 84 invocation.callRealMethod(); 85 return null; 86 }) 87 .when(() -> Log.d(eq(tag), any(), any())); 88 break; 89 case VERBOSE: 90 doAnswer( 91 invocation -> { 92 Log.v(TAG, invocation.toString()); 93 interceptor.log( 94 new LogEntry( 95 LogLevel.VERBOSE, 96 invocation.getArgument(0), 97 invocation.getArgument(1))); 98 invocation.callRealMethod(); 99 return null; 100 }) 101 .when(() -> Log.v(eq(tag), any())); 102 103 doAnswer( 104 invocation -> { 105 Log.v(TAG, invocation.toString()); 106 interceptor.log( 107 new LogEntry( 108 LogLevel.VERBOSE, 109 invocation.getArgument(0), 110 invocation.getArgument(1), 111 invocation.getArgument(2))); 112 invocation.callRealMethod(); 113 return null; 114 }) 115 .when(() -> Log.v(eq(tag), any(), any())); 116 break; 117 case ERROR: 118 doAnswer( 119 invocation -> { 120 Log.v(TAG, invocation.toString()); 121 interceptor.log( 122 new LogEntry( 123 LogLevel.ERROR, 124 invocation.getArgument(0), 125 invocation.getArgument(1))); 126 invocation.callRealMethod(); 127 return null; 128 }) 129 .when(() -> Log.e(eq(tag), any())); 130 131 doAnswer( 132 invocation -> { 133 Log.v(TAG, invocation.toString()); 134 interceptor.log( 135 new LogEntry( 136 LogLevel.ERROR, 137 invocation.getArgument(0), 138 invocation.getArgument(1), 139 invocation.getArgument(2))); 140 invocation.callRealMethod(); 141 return null; 142 }) 143 .when(() -> Log.e(eq(tag), any(), any())); 144 break; 145 default: 146 // NOTE: current tests are only intercepting VERBOSE and ERROR; more levels 147 // will be added on demand. 148 throw new UnsupportedOperationException( 149 "Not intercepting level " + level + " yet"); 150 } 151 } 152 153 return interceptor; 154 } 155 156 /** Gets all calls that used that {@code tag}. */ getAllEntries(String tag)157 public List<LogEntry> getAllEntries(String tag) { 158 List<LogEntry> entries = mEntries.get(Objects.requireNonNull(tag, "tag cannot be null")); 159 return entries == null 160 ? Collections.emptyList() 161 : Collections.unmodifiableList(new ArrayList<>(entries)); 162 } 163 164 /** 165 * Gets the messages from all "plain" calls (i.e., whose argument was just the message, without 166 * a {@link Throwable}) to that {@code tag}. 167 */ getPlainMessages(String tag, LogLevel level)168 public List<String> getPlainMessages(String tag, LogLevel level) { 169 Objects.requireNonNull(tag, "tag cannot be null"); 170 Objects.requireNonNull(level, "level cannot be null"); 171 return getAllEntries(tag).stream() 172 .filter( 173 entry -> 174 entry.level.equals(level) 175 && entry.tag.equals(tag) 176 && entry.throwable == null) 177 .map(entry -> entry.message) 178 .collect(Collectors.toList()); 179 } 180 log(LogEntry logEntry)181 private void log(LogEntry logEntry) { 182 String tag = Objects.requireNonNull(logEntry, "logEntry cannot be null").tag; 183 184 List<LogEntry> entries = mEntries.get(tag); 185 if (entries == null) { 186 entries = new ArrayList<>(); 187 mEntries.put(tag, entries); 188 } 189 entries.add(logEntry); 190 } 191 } 192