• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 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 package android.car.hiddenapitest;
17 
18 import static com.google.common.truth.Truth.assertThat;
19 
20 import static org.junit.Assert.assertThrows;
21 
22 import android.Manifest;
23 import android.annotation.FloatRange;
24 import android.car.Car;
25 import android.car.CarBugreportManager;
26 import android.car.CarBugreportManager.CarBugreportManagerCallback;
27 import android.car.extendedapitest.testbase.CarApiTestBase;
28 import android.os.FileUtils;
29 import android.os.ParcelFileDescriptor;
30 
31 import androidx.test.filters.LargeTest;
32 import androidx.test.platform.app.InstrumentationRegistry;
33 import androidx.test.runner.AndroidJUnit4;
34 
35 import org.junit.After;
36 import org.junit.Before;
37 import org.junit.Test;
38 import org.junit.runner.RunWith;
39 
40 import java.io.ByteArrayOutputStream;
41 import java.io.Closeable;
42 import java.io.File;
43 import java.io.FileOutputStream;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.nio.charset.StandardCharsets;
47 import java.util.ArrayList;
48 import java.util.Collections;
49 import java.util.List;
50 import java.util.concurrent.CountDownLatch;
51 import java.util.concurrent.TimeUnit;
52 import java.util.zip.ZipEntry;
53 import java.util.zip.ZipFile;
54 
55 @RunWith(AndroidJUnit4.class)
56 @LargeTest
57 public final class CarBugreportManagerTest extends CarApiTestBase {
58     private static final String TAG = CarBugreportManagerTest.class.getSimpleName();
59 
60     // Note that most of the test environments have 600s time limit, and in some cases the time
61     // limit is shared between all the tests.
62     // Dumpstate with dry_run flag should finish within one minute, but it might work slower on
63     // busy devices.
64     private static final int BUGREPORT_TIMEOUT_MILLIS = 90_000;
65     private static final int NO_ERROR = -1;
66 
67     // These items will be closed during tearDown().
68     private final ArrayList<Closeable> mAllCloseables = new ArrayList<>();
69 
70     private CarBugreportManager mManager;
71     private FakeCarBugreportCallback mFakeCallback;
72     private ParcelFileDescriptor mOutput;
73     private ParcelFileDescriptor mExtraOutput;
74 
75     @Before
setUp()76     public void setUp() throws Exception {
77         mManager = (CarBugreportManager) getCar().getCarManager(Car.CAR_BUGREPORT_SERVICE);
78         mFakeCallback = new FakeCarBugreportCallback();
79         mOutput = openDevNullParcelFd();
80         mExtraOutput = openDevNullParcelFd();
81         mAllCloseables.addAll(List.of(mOutput, mExtraOutput));
82     }
83 
84     @After
tearDown()85     public void tearDown() throws Exception {
86         getPermissions();  // For cancelBugreport()
87         try {
88             mManager.cancelBugreport();
89         } finally {
90             dropPermissions();
91         }
92         for (Closeable closeable : mAllCloseables) {
93             try {
94                 closeable.close();
95             } catch (IOException e) {
96                 // No need to handle it
97             }
98         }
99     }
100 
101     @Test
test_requestBugreport_failsWhenNoPermission()102     public void test_requestBugreport_failsWhenNoPermission() {
103         dropPermissions();
104 
105         SecurityException expected =
106                 assertThrows(SecurityException.class,
107                         () -> mManager.requestBugreportForTesting(
108                             mOutput, mExtraOutput, mFakeCallback));
109         assertThat(expected).hasMessageThat().contains(
110                 "nor current process has android.permission.DUMP.");
111     }
112 
113     @Test
test_requestBugreport_works()114     public void test_requestBugreport_works() throws Exception {
115         getPermissions();
116         PipedTempFile output = PipedTempFile.create("bugreport-" + getTestName(), ".zip");
117         PipedTempFile extraOutput = PipedTempFile.create("screenshot-" + getTestName(), ".png");
118         mAllCloseables.addAll(List.of(output, extraOutput));
119 
120         mManager.requestBugreportForTesting(
121                 output.getWriteFd(), extraOutput.getWriteFd(), mFakeCallback);
122 
123         // The FDs must be duped and closed in requestBugreport() immediately.
124         assertFdIsClosed(output.getWriteFd());
125         assertFdIsClosed(extraOutput.getWriteFd());
126 
127         // Blocks the thread until bugreport is finished.
128         PipedTempFile.copyAllToPersistentFiles(output, extraOutput);
129 
130         mFakeCallback.waitTillDoneOrTimeout(BUGREPORT_TIMEOUT_MILLIS);
131         assertThat(mFakeCallback.isFinishedSuccessfully()).isEqualTo(true);
132         assertThat(mFakeCallback.getReceivedProgress()).isTrue();
133         assertContainsValidBugreport(output.getPersistentFile());
134     }
135 
136     @Test
test_requestBugreport_cannotRunMultipleBugreports()137     public void test_requestBugreport_cannotRunMultipleBugreports() throws Exception {
138         getPermissions();
139         FakeCarBugreportCallback callback2 = new FakeCarBugreportCallback();
140         ParcelFileDescriptor output2 = openDevNullParcelFd();
141         ParcelFileDescriptor extraOutput2 = openDevNullParcelFd();
142 
143         // 1st bugreport.
144         mManager.requestBugreportForTesting(mOutput, mExtraOutput, mFakeCallback);
145 
146         // 2nd bugreport.
147         mManager.requestBugreportForTesting(output2, extraOutput2, callback2);
148 
149         callback2.waitTillDoneOrTimeout(BUGREPORT_TIMEOUT_MILLIS);
150         assertThat(callback2.getErrorCode()).isEqualTo(
151                 CarBugreportManagerCallback.CAR_BUGREPORT_IN_PROGRESS);
152         assertThat(mFakeCallback.isFinished()).isFalse();
153     }
154 
155     @Test
test_cancelBugreport_works()156     public void test_cancelBugreport_works() throws Exception {
157         getPermissions();
158         FakeCarBugreportCallback callback2 = new FakeCarBugreportCallback();
159         ParcelFileDescriptor output2 = openDevNullParcelFd();
160         ParcelFileDescriptor extraOutput2 = openDevNullParcelFd();
161 
162         // 1st bugreport.
163         mManager.requestBugreportForTesting(mOutput, mExtraOutput, mFakeCallback);
164         mManager.cancelBugreport();
165 
166         // Allow the system to finish the bugreport cancellation, 0.5 seconds is enough.
167         Thread.sleep(500);
168 
169         // 2nd bugreport must work, because 1st bugreport was cancelled.
170         mManager.requestBugreportForTesting(output2, extraOutput2, callback2);
171 
172         callback2.waitTillProgressOrTimeout(BUGREPORT_TIMEOUT_MILLIS);
173         assertThat(callback2.getErrorCode()).isEqualTo(NO_ERROR);
174         assertThat(callback2.getReceivedProgress()).isEqualTo(true);
175     }
176 
getPermissions()177     private static void getPermissions() {
178         InstrumentationRegistry.getInstrumentation().getUiAutomation()
179                 .adoptShellPermissionIdentity(Manifest.permission.DUMP);
180     }
181 
dropPermissions()182     private static void dropPermissions() {
183         InstrumentationRegistry.getInstrumentation().getUiAutomation()
184                 .dropShellPermissionIdentity();
185     }
186 
assertFdIsClosed(ParcelFileDescriptor pfd)187     private static void assertFdIsClosed(ParcelFileDescriptor pfd) {
188         try {
189             int fd = pfd.getFd();
190             fail("Expected ParcelFileDescriptor argument to be closed, but got: " + fd);
191         } catch (IllegalStateException expected) {
192         }
193     }
194 
assertContainsValidBugreport(File file)195     private static void assertContainsValidBugreport(File file) throws IOException {
196         try (ZipFile zipFile = new ZipFile(file)) {
197             for (ZipEntry entry : Collections.list(zipFile.entries())) {
198                 if (entry.isDirectory()) {
199                     continue;
200                 }
201                 // Find "bugreport-TIMESTAMP.txt" file.
202                 if (!entry.getName().startsWith("bugreport-") || !entry.getName().endsWith(
203                         ".txt")) {
204                     continue;
205                 }
206                 try (InputStream entryStream = zipFile.getInputStream(entry)) {
207                     String data = streamToText(entryStream, /* maxSizeBytes= */  51200);
208                     assertThat(data).contains("== dumpstate: ");
209                     assertThat(data).contains("dry_run=1");
210                     assertThat(data).contains("Build fingerprint: ");
211                 }
212                 return;
213             }
214         }
215         fail("bugreport-TIMESTAMP.txt not found in the final zip file.");
216     }
217 
streamToText(InputStream in, int maxSizeBytes)218     private static String streamToText(InputStream in, int maxSizeBytes) throws IOException {
219         assertThat(maxSizeBytes).isGreaterThan(0);
220 
221         ByteArrayOutputStream result = new ByteArrayOutputStream();
222         byte[] data = new byte[maxSizeBytes];
223         int nRead;
224         int totalRead = 0;
225 
226         while ((nRead = in.read(data, 0, data.length)) != -1 && totalRead <= maxSizeBytes) {
227             result.write(data, 0, nRead);
228             totalRead += maxSizeBytes;
229         }
230 
231         return result.toString(StandardCharsets.UTF_8.name());
232     }
233 
openDevNullParcelFd()234     private static ParcelFileDescriptor openDevNullParcelFd() throws IOException {
235         return ParcelFileDescriptor.open(
236                 new File("/dev/null"),
237                 ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_APPEND);
238     }
239 
240     /**
241      * Creates a piped ParcelFileDescriptor that anyone can write. Clients must call
242      * {@link copyToPersistentFile}, otherwise writers will be blocked when writing to the pipe.
243      *
244      * <p>It was created because {@code CarService} is denied to write to a test cache file
245      * by SELinux.
246      */
247     private static class PipedTempFile implements Closeable {
248         private final File mPersistentFile;
249         private final ParcelFileDescriptor mReadFd;
250         private final ParcelFileDescriptor mWriteFd;
251 
create(String prefix, String extension)252         static PipedTempFile create(String prefix, String extension) throws IOException {
253             ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
254             File f = File.createTempFile(prefix, extension);
255             f.setReadable(/* readable= */ true, /* ownerOnly= */ true);
256             f.setWritable(/* readable= */ true, /* ownerOnly= */ true);
257             f.deleteOnExit();
258             return new PipedTempFile(pipe[0], pipe[1], f);
259         }
260 
copyAllToPersistentFiles(PipedTempFile... files)261         static void copyAllToPersistentFiles(PipedTempFile... files) throws IOException {
262             for (PipedTempFile f : files) {
263                 f.copyToPersistentFile();
264             }
265         }
266 
PipedTempFile( ParcelFileDescriptor readFd, ParcelFileDescriptor writeFd, File persistentFile)267         private PipedTempFile(
268                 ParcelFileDescriptor readFd, ParcelFileDescriptor writeFd, File persistentFile) {
269             mReadFd = readFd;
270             mWriteFd = writeFd;
271             mPersistentFile = persistentFile;
272         }
273 
getWriteFd()274         ParcelFileDescriptor getWriteFd() {
275             return mWriteFd;
276         }
277 
getPersistentFile()278         File getPersistentFile() {
279             return mPersistentFile;
280         }
281 
282         @Override
close()283         public void close() throws IOException {
284             try {
285                 mReadFd.close();
286             } finally {
287                 mWriteFd.close();
288             }
289         }
290 
291         /** Copies data from the pipe to the persistent file. Blocks the thread. */
copyToPersistentFile()292         void copyToPersistentFile() throws IOException {
293             try (InputStream in = new ParcelFileDescriptor.AutoCloseInputStream(mReadFd);
294                     FileOutputStream out = new FileOutputStream(mPersistentFile)) {
295                 FileUtils.copy(in, out);
296             }
297         }
298     }
299 
300     private static class FakeCarBugreportCallback extends CarBugreportManagerCallback {
301         private final Object mLock = new Object();
302         private final CountDownLatch mEndedLatch = new CountDownLatch(1);
303         private final CountDownLatch mProgressLatch = new CountDownLatch(1);
304         private boolean mReceivedProgress = false;
305         private int mErrorCode = NO_ERROR;
306 
307         @Override
onProgress(@loatRangefrom = 0f, to = 100f) float progress)308         public void onProgress(@FloatRange(from = 0f, to = 100f) float progress) {
309             synchronized (mLock) {
310                 mReceivedProgress = true;
311             }
312             mProgressLatch.countDown();
313         }
314 
315         @Override
onError( @arBugreportManagerCallback.CarBugreportErrorCode int errorCode)316         public void onError(
317                 @CarBugreportManagerCallback.CarBugreportErrorCode int errorCode) {
318             synchronized (mLock) {
319                 mErrorCode = errorCode;
320             }
321             mEndedLatch.countDown();
322             mProgressLatch.countDown();
323         }
324 
325         @Override
onFinished()326         public void onFinished() {
327             mEndedLatch.countDown();
328             mProgressLatch.countDown();
329         }
330 
getErrorCode()331         int getErrorCode() {
332             synchronized (mLock) {
333                 return mErrorCode;
334             }
335         }
336 
getReceivedProgress()337         boolean getReceivedProgress() {
338             synchronized (mLock) {
339                 return mReceivedProgress;
340             }
341         }
342 
isFinishedSuccessfully()343         boolean isFinishedSuccessfully() {
344             return mEndedLatch.getCount() == 0 && getErrorCode() == NO_ERROR;
345         }
346 
isFinished()347         boolean isFinished() {
348             return mEndedLatch.getCount() == 0;
349         }
350 
waitTillDoneOrTimeout(long timeoutMillis)351         void waitTillDoneOrTimeout(long timeoutMillis) throws InterruptedException {
352             mEndedLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
353             if (mEndedLatch.getCount() > 0) {
354                 fail("Time out. CarBugreportManager didn't finish.");
355             }
356         }
357 
waitTillProgressOrTimeout(long timeoutMillis)358         void waitTillProgressOrTimeout(long timeoutMillis) throws InterruptedException {
359             mProgressLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
360             if (mProgressLatch.getCount() > 0) {
361                 fail("Time out. CarBugreportManager didn't send progress or finish.");
362             }
363         }
364     }
365 }
366