1 /* 2 * Copyright (C) 2023 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 com.android.ddmlib.MultiLineReceiver; 20 import com.android.tradefed.device.BackgroundDeviceAction; 21 import com.android.tradefed.device.ITestDevice; 22 import com.android.tradefed.result.ByteArrayInputStreamSource; 23 import com.android.tradefed.result.InputStreamSource; 24 import com.android.tradefed.result.LogDataType; 25 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner; 26 27 import java.util.ArrayList; 28 import java.util.Arrays; 29 import java.util.List; 30 import java.util.Objects; 31 import java.util.concurrent.CountDownLatch; 32 import java.util.concurrent.TimeUnit; 33 import java.util.function.Predicate; 34 import java.util.regex.Pattern; 35 36 /** Enables capturing device logs and exposing them to the host test. */ 37 public final class BackgroundLogReceiver extends MultiLineReceiver { 38 private volatile boolean mCancelled; 39 private final List<String> mLines = new ArrayList<>(); 40 private final CountDownLatch mCountDownLatch = new CountDownLatch(1); 41 private final String mName; 42 private final String mLogcatCmd; 43 private final ITestDevice mTestDevice; 44 private final Predicate<String[]> mEarlyStopCondition; 45 private BackgroundDeviceAction mBackgroundDeviceAction; 46 BackgroundLogReceiver( String name, String logcatCmd, ITestDevice device, Predicate<String[]> earlyStopCondition)47 private BackgroundLogReceiver( 48 String name, 49 String logcatCmd, 50 ITestDevice device, 51 Predicate<String[]> earlyStopCondition) { 52 mName = name; 53 mLogcatCmd = logcatCmd; 54 mEarlyStopCondition = earlyStopCondition; 55 mTestDevice = device; 56 } 57 58 @Override processNewLines(String[] lines)59 public void processNewLines(String[] lines) { 60 if (lines.length == 0) { 61 return; 62 } 63 64 Arrays.stream(lines).filter(s -> !s.trim().isEmpty()).forEach(mLines::add); 65 66 if (mEarlyStopCondition != null && mEarlyStopCondition.test(lines)) { 67 stopBackgroundCollection(); 68 mCountDownLatch.countDown(); 69 } 70 } 71 72 @Override isCancelled()73 public boolean isCancelled() { 74 return mCancelled; 75 } 76 77 /** 78 * Collects logs until timeout or stop condition is reached. This method needs to be only used 79 * once per instance. 80 * 81 * @param timeoutMilliseconds the maximum time after which log collection should stop, if the 82 * early stop condition was not encountered previously. 83 * @return true if log collection stopped because the early stop condition was encountered, 84 * false if log collection stopped due to timeout 85 * @throws InterruptedException if the current thread is interrupted 86 */ collectLogs(long timeoutMilliseconds)87 public boolean collectLogs(long timeoutMilliseconds) throws InterruptedException { 88 startBackgroundCollection(); 89 return waitForLogs(timeoutMilliseconds); 90 } 91 92 /** Begins log collection. This method needs to be only used once per instance. */ startBackgroundCollection()93 public void startBackgroundCollection() { 94 if (mBackgroundDeviceAction != null) { 95 throw new IllegalStateException("This method should only be called once per instance"); 96 } 97 98 mBackgroundDeviceAction = 99 new BackgroundDeviceAction(mLogcatCmd, mName, mTestDevice, this, 0); 100 mBackgroundDeviceAction.start(); 101 } 102 103 /** 104 * Wait until timeout or stop condition is reached. This method can only be used once per 105 * instance. 106 * 107 * @param timeoutMilliseconds the maximum time after which log collection should stop, if the 108 * early stop condition was not encountered previously. 109 * @return true if log collection stopped because the early stop condition was encountered, 110 * false if log collection stopped due to timeout 111 * @throws InterruptedException if the current thread is interrupted 112 */ waitForLogs(long timeoutMilliseconds)113 public boolean waitForLogs(long timeoutMilliseconds) throws InterruptedException { 114 if (mBackgroundDeviceAction == null) { 115 throw new IllegalStateException( 116 "Log collection not started. Call startBackgroundCollection first"); 117 } 118 119 boolean earlyStop = mCountDownLatch.await(timeoutMilliseconds, TimeUnit.MILLISECONDS); 120 stopBackgroundCollection(); 121 return earlyStop; 122 } 123 124 /** Stops log collection and adds all logs to input logger. */ stopAndAddTestLog(DeviceJUnit4ClassRunner.TestLogData logger)125 public void stopAndAddTestLog(DeviceJUnit4ClassRunner.TestLogData logger) { 126 if (mBackgroundDeviceAction != null) mBackgroundDeviceAction.cancel(); 127 if (isCancelled()) return; 128 mCancelled = true; 129 130 String joined = String.join("\n", mLines); 131 try (InputStreamSource data = new ByteArrayInputStreamSource(joined.getBytes())) { 132 logger.addTestLog(mName, LogDataType.TEXT, data); 133 } 134 } 135 136 /** Ends log collection. This method needs to be only used once per instance. */ stopBackgroundCollection()137 private void stopBackgroundCollection() { 138 if (mBackgroundDeviceAction != null) { 139 mBackgroundDeviceAction.cancel(); 140 } 141 if (isCancelled()) { 142 return; 143 } 144 mCancelled = true; 145 } 146 147 /** 148 * Checks if the collected logs match the specified regex pattern 149 * 150 * @param pattern the regex pattern to look for 151 * @return {@code true} if the logs are non-empty and match the pattern, {@code false} otherwise 152 */ patternMatches(Pattern pattern)153 public boolean patternMatches(Pattern pattern) { 154 String joined = String.join("\n", mLines); 155 return joined.length() > 0 && pattern.matcher(joined).find(); 156 } 157 158 /** 159 * Gets all the collected log lines. 160 * 161 * @return the log lines that have been collected. 162 */ getCollectedLogs()163 public List<String> getCollectedLogs() { 164 return mLines; 165 } 166 167 /** Builder class for the BackgroundLogReceiver. */ 168 public static final class Builder { 169 private String mName = "background-logcat-receiver"; 170 private ITestDevice mDevice; 171 private String mLogCatCommand; 172 private Predicate<String[]> mEarlyStopCondition; 173 174 /** Sets the name. */ setName(String name)175 public Builder setName(String name) { 176 mName = Objects.requireNonNull(name); 177 return this; 178 } 179 180 /** Sets the device. */ setDevice(ITestDevice device)181 public Builder setDevice(ITestDevice device) { 182 mDevice = Objects.requireNonNull(device); 183 return this; 184 } 185 186 /** Sets the logcat command. */ setLogCatCommand(String command)187 public Builder setLogCatCommand(String command) { 188 mLogCatCommand = Objects.requireNonNull(command); 189 return this; 190 } 191 192 /** 193 * Sets the condition that indicates whether to stop collecting logs before timeout happens 194 * 195 * @param earlyStopCondition the predicate to invoke with each batch of logs. If the 196 * predicate returns {@code true}, it will cause log collection to stop right away. 197 * @return the {@link Builder} instance 198 */ setEarlyStopCondition(Predicate<String[]> earlyStopCondition)199 public Builder setEarlyStopCondition(Predicate<String[]> earlyStopCondition) { 200 mEarlyStopCondition = earlyStopCondition; 201 return this; 202 } 203 204 /** Build Bob, build! */ build()205 public BackgroundLogReceiver build() { 206 Objects.requireNonNull(mDevice); 207 Objects.requireNonNull(mLogCatCommand); 208 209 return new BackgroundLogReceiver(mName, mLogCatCommand, mDevice, mEarlyStopCondition); 210 } 211 } 212 } 213