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