1 /*
2  * Copyright (C) 2021 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 com.android.tradefed.device;
17 
18 import static org.junit.Assert.assertEquals;
19 import static org.junit.Assert.assertFalse;
20 import static org.junit.Assert.assertNotNull;
21 import static org.junit.Assert.assertNull;
22 import static org.junit.Assert.assertTrue;
23 
24 import com.android.ddmlib.IDevice;
25 import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
26 import com.android.tradefed.testtype.IDeviceTest;
27 import com.android.tradefed.util.CommandResult;
28 import com.android.tradefed.util.CommandStatus;
29 import com.android.tradefed.util.FileUtil;
30 import com.android.tradefed.util.StreamUtil;
31 
32 import org.junit.Before;
33 import org.junit.Test;
34 import org.junit.runner.RunWith;
35 
36 import java.io.ByteArrayInputStream;
37 import java.io.File;
38 import java.io.FileInputStream;
39 import java.io.IOException;
40 import java.io.InputStream;
41 import java.nio.file.Files;
42 import java.nio.file.Path;
43 import java.util.Arrays;
44 import java.util.List;
45 import java.util.Random;
46 import java.util.stream.Collectors;
47 import java.util.stream.Stream;
48 
49 /** Functional tests for the {@link ITestDevice} file management APIs */
50 @RunWith(DeviceJUnit4ClassRunner.class)
51 public class TestDeviceFileFuncTest implements IDeviceTest {
52     private String mInternalStorage;
53     private String mExternalStorage;
54     private TestDevice mTestDevice;
55 
56     @Before
setUp()57     public void setUp() throws Exception {
58         // Ensure at set-up that the device is available.
59         mTestDevice.waitForDeviceAvailable();
60         mExternalStorage = mTestDevice.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
61         mInternalStorage = "/data/local/tmp";
62     }
63 
64     /** Basic push-pull consistency test */
65     @Test
testPushPull_Basic()66     public void testPushPull_Basic() throws Exception {
67         String deviceFilePath = null;
68         File returnedFile = null;
69         try {
70             File testFile = WifiHelper.extractWifiUtilApk();
71             deviceFilePath = String.format("%s/%s", mInternalStorage, "testPushPull_Basic.txt");
72             mTestDevice.pushFile(testFile, deviceFilePath);
73             returnedFile = mTestDevice.pullFile(deviceFilePath);
74 
75             assertNotNull(returnedFile);
76 
77             assertEquals(computeChecksum(testFile), computeChecksum(returnedFile));
78         } finally {
79             if (deviceFilePath != null) {
80                 mTestDevice.deleteFile(deviceFilePath);
81             }
82             if (returnedFile != null) {
83                 returnedFile.delete();
84             }
85         }
86     }
87     /** Push-pull consistency test, but with external storage */
88     @Test
testPushPull_ExtStorage()89     public void testPushPull_ExtStorage() throws Exception {
90         String deviceFilePath = null;
91         File returnedFile = null;
92         try {
93             File testFile = WifiHelper.extractWifiUtilApk();
94             deviceFilePath =
95                     String.format("%s/%s", mExternalStorage, "tmp_testPushPull_ExtStorage.txt");
96             mTestDevice.pushFile(testFile, deviceFilePath);
97             returnedFile = mTestDevice.pullFile(deviceFilePath);
98 
99             assertNotNull(returnedFile);
100 
101             assertEquals(computeChecksum(testFile), computeChecksum(returnedFile));
102         } finally {
103             if (deviceFilePath != null) {
104                 mTestDevice.deleteFile(deviceFilePath);
105             }
106             if (returnedFile != null) {
107                 returnedFile.delete();
108             }
109         }
110     }
111     /** Ensures the string method is also push-pull consistent */
112     @Test
testPushPull_FromString()113     public void testPushPull_FromString() throws Exception {
114         String deviceFilePath = null;
115         File returnedFile = null;
116         try {
117             String testString = generateRandomString(128 * 1024); // 128 kilobyte-ish string;
118             deviceFilePath =
119                     String.format("%s/%s", mInternalStorage, "tmp_testPushPull_String.txt");
120             mTestDevice.pushString(testString, deviceFilePath);
121             returnedFile = mTestDevice.pullFile(deviceFilePath);
122 
123             assertNotNull(returnedFile);
124 
125             assertEquals(computeChecksum(testString), computeChecksum(returnedFile));
126         } finally {
127             if (deviceFilePath != null) {
128                 mTestDevice.deleteFile(deviceFilePath);
129             }
130             if (returnedFile != null) {
131                 returnedFile.delete();
132             }
133         }
134     }
135 
136     /** Ensures the pull contents method is also push-pull consistent */
137     @Test
testPushPull_PullContents()138     public void testPushPull_PullContents() throws Exception {
139         String deviceFilePath = null;
140         try {
141             String testString = generateRandomString(128 * 1024); // 128 kilobyte-ish string;
142             deviceFilePath =
143                     String.format("%s/%s", mInternalStorage, "tmp_testPushPull_String.txt");
144             mTestDevice.pushString(testString, deviceFilePath);
145             String returnedContents = mTestDevice.pullFileContents(deviceFilePath);
146 
147             assertNotNull(returnedContents);
148             assertEquals(computeChecksum(testString), computeChecksum(returnedContents));
149         } finally {
150             if (deviceFilePath != null) {
151                 mTestDevice.deleteFile(deviceFilePath);
152             }
153         }
154     }
155 
156     /** Tests when trying to pull a file that does not exist */
157     @Test
testPull_NoExist()158     public void testPull_NoExist() throws Exception {
159         String deviceFilePath = String.format("%s/%s", mInternalStorage, "thisfiledoesntexist");
160         assertFalse(
161                 String.format("%s exists", deviceFilePath),
162                 mTestDevice.doesFileExist(deviceFilePath));
163         assertNull(mTestDevice.pullFile(deviceFilePath));
164     }
165 
166     /** Tests when trying to pull a file that does not exist */
167     @Test
testPull_NoExistExtStorage()168     public void testPull_NoExistExtStorage() throws Exception {
169         String deviceFilePath = String.format("%s/%s", mExternalStorage, "thisfiledoesntexist");
170         assertFalse(
171                 String.format("%s exists", deviceFilePath),
172                 mTestDevice.doesFileExist(deviceFilePath));
173         assertNull(mTestDevice.pullFile(deviceFilePath));
174     }
175 
176     /** Tests when trying to pull a file that we lack permission to */
177     @Test
testPull_NoPermissions()178     public void testPull_NoPermissions() throws Exception {
179         String filePath = "/data/system/packages.xml";
180         mTestDevice.disableAdbRoot();
181         File returned = mTestDevice.pullFile(filePath);
182         assertNull(returned);
183     }
184 
185     /** Tests pushing a non-existent file */
186     @Test
testPush_NoExist()187     public void testPush_NoExist() throws Exception {
188         final String filename = "this_file_does_not_exist.txt";
189         String remotePath = null;
190         boolean needDelete = false;
191         try {
192             remotePath = String.format("%s/%s", mInternalStorage, filename);
193             boolean didPushFile = mTestDevice.pushFile(new File(filename), remotePath);
194             needDelete = didPushFile;
195             assertFalse(didPushFile);
196         } finally {
197             if (needDelete) {
198                 mTestDevice.deleteFile(remotePath);
199             }
200         }
201     }
202 
203     /** Tests push-pull consistency for directories */
204     @Test
testPushPullDir_Basic()205     public void testPushPullDir_Basic() throws Exception {
206         final int NUM_FILES = 10; // 10 files per level
207         final int FILE_SIZE = 16 * 1024; // 16 KB files
208         final String TEST_FILE_PREFIX = "test_file";
209         final String TEST_LABEL = "testPushPullDir_Basic";
210         File localParentDir = null;
211         String remotePath = null;
212         try {
213             // Set up directories for comparison
214             localParentDir = FileUtil.createTempDir(TEST_LABEL);
215             File localDir = FileUtil.createTempDir("local", localParentDir);
216             File pulledDir = FileUtil.createTempDir("pulled", localParentDir);
217 
218             // populate the local one with some test files
219             for (int i = 0; i < NUM_FILES; i++) {
220                 File tmp =
221                         FileUtil.createTempFile(
222                                 String.format("%s_%s", TEST_FILE_PREFIX, i), "", localDir);
223                 FileUtil.writeToFile(generateRandomString(FILE_SIZE), tmp);
224             }
225 
226             remotePath = String.format("%s/%s", mInternalStorage, TEST_LABEL);
227             CommandResult res =
228                     mTestDevice.executeShellV2Command(String.format("mkdir -p \"%s\"", remotePath));
229             assertEquals(CommandStatus.SUCCESS, res.getStatus());
230 
231             boolean didPushSucceed = mTestDevice.pushDir(localDir, remotePath);
232             assertTrue(didPushSucceed);
233 
234             boolean didPullSucceed = mTestDevice.pullDir(remotePath, pulledDir);
235             assertTrue(didPullSucceed);
236 
237             // contains a bunch of equality assertions in the helper method
238             compareFiles(localDir, pulledDir);
239         } finally {
240             if (localParentDir != null && localParentDir.exists()) {
241                 FileUtil.recursiveDelete(localParentDir);
242             }
243             if (remotePath != null) {
244                 mTestDevice.deleteFile(remotePath);
245             }
246         }
247     }
248 
249     /** Tests push-pull consistency for directories in external storage */
250     @Test
testPushPullDir_ExtStorage()251     public void testPushPullDir_ExtStorage() throws Exception {
252         final int NUM_FILES = 10; // 10 files per level
253         final int FILE_SIZE = 16 * 1024; // 16 KB files
254         final String TEST_FILE_PREFIX = "test_file";
255         final String TEST_LABEL = "testPushPullDir_Basic";
256         File localParentDir = null;
257         String remotePath = null;
258         try {
259             // Set up directories for comparison
260             localParentDir = FileUtil.createTempDir(TEST_LABEL);
261             File localDir = FileUtil.createTempDir("local", localParentDir);
262             File pulledDir = FileUtil.createTempDir("pulled", localParentDir);
263 
264             // populate the local one with some test files
265             for (int i = 0; i < NUM_FILES; i++) {
266                 File tmp =
267                         FileUtil.createTempFile(
268                                 String.format("%s_%s", TEST_FILE_PREFIX, i), "", localDir);
269                 FileUtil.writeToFile(generateRandomString(FILE_SIZE), tmp);
270             }
271 
272             remotePath = String.format("%s/%s", mExternalStorage, TEST_LABEL);
273             CommandResult res =
274                     mTestDevice.executeShellV2Command(String.format("mkdir -p \"%s\"", remotePath));
275             assertEquals(CommandStatus.SUCCESS, res.getStatus());
276 
277             boolean didPushSucceed = mTestDevice.pushDir(localDir, remotePath);
278             assertTrue(didPushSucceed);
279 
280             boolean didPullSucceed = mTestDevice.pullDir(remotePath, pulledDir);
281             assertTrue(didPullSucceed);
282 
283             // contains a bunch of equality assertions in the helper method
284             compareFiles(localDir, pulledDir);
285         } finally {
286             if (localParentDir != null && localParentDir.exists()) {
287                 FileUtil.recursiveDelete(localParentDir);
288             }
289             if (remotePath != null) {
290                 mTestDevice.deleteFile(remotePath);
291             }
292         }
293     }
294     /** Tests the doesFileExist method with by pushing a file */
295     @Test
testDoesFileExist_Basic()296     public void testDoesFileExist_Basic() throws Exception {
297         String remotePath = null;
298         final String filename = "testDoesFileExist";
299         try {
300             remotePath = String.format("%s/%s", mInternalStorage, filename);
301 
302             mTestDevice.pushString(generateRandomString(10 * 1024), remotePath);
303 
304             boolean doesFileExist = mTestDevice.doesFileExist(remotePath);
305 
306             assertTrue(doesFileExist);
307         } finally {
308             if (remotePath != null) {
309                 mTestDevice.deleteFile(remotePath);
310             }
311         }
312     }
313     /** Tests deleting a file, via push, delete, then doesFileExist */
314     @Test
testDeleteFile_Basic()315     public void testDeleteFile_Basic() throws Exception {
316         String remotePath = null;
317         final String testTag = "testDeleteFile_basic";
318         final String filename = String.format("%s%s", testTag, ".txt");
319         try {
320             remotePath = String.format("%s/%s", mInternalStorage, filename);
321 
322             mTestDevice.pushString(generateRandomString(10 * 1024), remotePath);
323 
324             assertTrue(mTestDevice.doesFileExist(remotePath));
325 
326             mTestDevice.deleteFile(remotePath);
327 
328             assertFalse(mTestDevice.doesFileExist(remotePath));
329 
330             remotePath = null;
331         } finally {
332             if (remotePath != null) {
333                 mTestDevice.deleteFile(remotePath);
334             }
335         }
336     }
337     /** Tests deleting a file on external storage by same way as the basic */
338     @Test
testDeleteFile_ExtStorage()339     public void testDeleteFile_ExtStorage() throws Exception {
340         String remotePath = null;
341         final String testTag = "testDeleteFile_extStorage";
342         final String filename = String.format("%s%s", testTag, ".txt");
343         try {
344             remotePath = String.format("%s/%s", mExternalStorage, filename);
345 
346             mTestDevice.pushString(generateRandomString(10 * 1024), remotePath);
347 
348             assertTrue(mTestDevice.doesFileExist(remotePath));
349 
350             mTestDevice.deleteFile(remotePath);
351 
352             assertFalse(mTestDevice.doesFileExist(remotePath));
353 
354             remotePath = null;
355         } finally {
356             if (remotePath != null) {
357                 mTestDevice.deleteFile(remotePath);
358             }
359         }
360     }
361     /** Tests isDirectory method by using a known existent directory */
362     @Test
testIsDirectory()363     public void testIsDirectory() throws Exception {
364         final int NUM_FILES = 10; // 10 files per level
365         final int FILE_SIZE = 16 * 1024; // 16 KB files
366         final String TEST_FILE_PREFIX = "test_file";
367         final String TEST_LABEL = "testIsDirectory";
368         File localParentDir = null;
369         String remotePath = null;
370         try {
371             // Set up directories for comparison
372             localParentDir = FileUtil.createTempDir(TEST_LABEL);
373             File localDir = FileUtil.createTempDir("local", localParentDir);
374 
375             // populate the local one with some test files
376             for (int i = 0; i < NUM_FILES; i++) {
377                 File tmp =
378                         FileUtil.createTempFile(
379                                 String.format("%s_%s", TEST_FILE_PREFIX, i), "", localDir);
380                 FileUtil.writeToFile(generateRandomString(FILE_SIZE), tmp);
381             }
382 
383             remotePath = String.format("%s/%s", mInternalStorage, TEST_LABEL);
384             CommandResult res =
385                     mTestDevice.executeShellV2Command(String.format("mkdir -p \"%s\"", remotePath));
386             assertEquals(CommandStatus.SUCCESS, res.getStatus());
387 
388             boolean didPushSucceed = mTestDevice.pushDir(localDir, remotePath);
389             assertTrue(didPushSucceed);
390 
391             assertTrue(mTestDevice.isDirectory(remotePath));
392         } finally {
393             if (localParentDir != null && localParentDir.exists()) {
394                 FileUtil.recursiveDelete(localParentDir);
395             }
396             if (remotePath != null) {
397                 mTestDevice.deleteFile(remotePath);
398             }
399         }
400     }
401     /** Tests getChildren method by pushing a directory then listing its children */
402     @Test
testGetChildren_Basic()403     public void testGetChildren_Basic() throws Exception {
404         final int NUM_FILES = 10; // 10 files per level
405         final int FILE_SIZE = 16 * 1024; // 16 KB files
406         final String TEST_FILE_PREFIX = "test_file";
407         final String TEST_LABEL = "testGetChildren_Basic";
408         File localParentDir = null;
409         String remotePath = null;
410         try {
411             // Set up directories for comparison
412             localParentDir = FileUtil.createTempDir(TEST_LABEL);
413             File localDir = FileUtil.createTempDir("local", localParentDir);
414 
415             // populate the local one with some test files
416             for (int i = 0; i < NUM_FILES; i++) {
417                 File tmp =
418                         FileUtil.createTempFile(
419                                 String.format("%s_%s", TEST_FILE_PREFIX, i), "", localDir);
420                 FileUtil.writeToFile(generateRandomString(FILE_SIZE), tmp);
421             }
422 
423             remotePath = String.format("%s/%s", mInternalStorage, TEST_LABEL);
424             CommandResult res =
425                     mTestDevice.executeShellV2Command(String.format("mkdir -p \"%s\"", remotePath));
426             assertEquals(CommandStatus.SUCCESS, res.getStatus());
427 
428             boolean didPushSucceed = mTestDevice.pushDir(localDir, remotePath);
429             assertTrue(didPushSucceed);
430 
431             assertTrue(mTestDevice.isDirectory(remotePath));
432 
433             String[] pulledChildren = mTestDevice.getChildren(remotePath);
434 
435             List<String> pulledChildrenList =
436                     Arrays.asList(pulledChildren).stream().sorted().collect(Collectors.toList());
437             List<String> localChildrenList =
438                     Arrays.asList(localDir.list()).stream().sorted().collect(Collectors.toList());
439 
440             assertEquals(localChildrenList.size(), pulledChildrenList.size());
441 
442             for (int i = 0; i < localChildrenList.size(); i++) {
443                 assertEquals(localChildrenList.get(i), pulledChildrenList.get(i));
444             }
445 
446         } finally {
447             if (localParentDir != null && localParentDir.exists()) {
448                 FileUtil.recursiveDelete(localParentDir);
449             }
450             if (remotePath != null) {
451                 mTestDevice.deleteFile(remotePath);
452             }
453         }
454     }
455     /** Tests getChildren on a file */
456     @Test
testGetChildren_NotDirectory()457     public void testGetChildren_NotDirectory() throws Exception {
458         final String TEST_LABEL = "testGetChildren_NotDirectory";
459         String remotePath = null;
460         try {
461             remotePath = String.format("%s/%s", mInternalStorage, TEST_LABEL);
462             boolean didPushSucceed =
463                     mTestDevice.pushString(generateRandomString(10 * 1024), remotePath);
464             assertTrue(didPushSucceed);
465 
466             assertFalse(mTestDevice.isDirectory(remotePath));
467 
468             String[] pulledChildren = mTestDevice.getChildren(remotePath);
469 
470             // This is really weird behavior, but is what the harness does right now.
471             // The contract wasn't specific on that method, so for now this test will at least
472             // enforce consistency
473             assertEquals(1, pulledChildren.length);
474             assertEquals(remotePath, pulledChildren[0]);
475         } finally {
476             if (remotePath != null) {
477                 mTestDevice.deleteFile(remotePath);
478             }
479         }
480     }
481     /** Tests getChildren on an empty directory */
482     @Test
testGetChildren_EmptyDirectory()483     public void testGetChildren_EmptyDirectory() throws Exception {
484         final String TEST_LABEL = "testGetChildren_EmptyDir";
485         String remotePath = null;
486         try {
487             remotePath = String.format("%s/%s", mInternalStorage, TEST_LABEL);
488             CommandResult res =
489                     mTestDevice.executeShellV2Command(String.format("mkdir -p \"%s\"", remotePath));
490             assertEquals(CommandStatus.SUCCESS, res.getStatus());
491 
492             assertTrue(mTestDevice.isDirectory(remotePath));
493 
494             String[] pulledChildren = mTestDevice.getChildren(remotePath);
495             assertEquals(0, pulledChildren.length);
496         } finally {
497             if (remotePath != null) {
498                 mTestDevice.deleteFile(remotePath);
499             }
500         }
501     }
502 
computeChecksum(File file)503     private static String computeChecksum(File file) throws IOException {
504         try (InputStream in = new FileInputStream(file)) {
505             return StreamUtil.calculateMd5(in);
506         }
507     }
508 
computeChecksum(String str)509     private static String computeChecksum(String str) throws IOException {
510         try (InputStream in = new ByteArrayInputStream(str.getBytes("UTF-8"))) {
511             return StreamUtil.calculateMd5(in);
512         }
513     }
514 
generateRandomString(int length)515     private static String generateRandomString(int length) {
516         int unicodeLower = 32;
517         int unicodeUpper = 126;
518         Random gen = new Random();
519 
520         return gen.ints(unicodeLower, unicodeUpper + 1)
521                 .limit(length)
522                 .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
523                 .toString();
524     }
525 
526     /**
527      * Performs a recursive pair-wise comparison of two file/directories
528      *
529      * <p>First this ensures the files have the same name (not path) and type (directory or file).
530      * Then this checks that the contents are the same. If they are directories then that means
531      * listing out their children, sorting them by name, ensuring the same number of elements, then
532      * recursing on them pairwise. If the two files are in fact regular files, to compare contents
533      * we just compute the checksum and compare those.
534      */
compareFiles(File firstFile, File secondFile)535     private static void compareFiles(File firstFile, File secondFile) throws Exception {
536         compareFiles(firstFile, secondFile, false);
537         return;
538     }
539 
compareFiles(File firstFile, File secondFile, boolean checkName)540     private static void compareFiles(File firstFile, File secondFile, boolean checkName)
541             throws Exception {
542         assertEquals(firstFile.isDirectory(), secondFile.isDirectory());
543         if (checkName) {
544             assertEquals(firstFile.getName(), secondFile.getName());
545         }
546 
547         if (firstFile.isDirectory()) {
548             List<Path> firstFileList;
549             try (Stream<Path> stream = Files.list(firstFile.toPath())) {
550                 firstFileList = stream.sorted().collect(Collectors.toList());
551             }
552             List<Path> secondFileList;
553             try (Stream<Path> stream = Files.list(secondFile.toPath())) {
554                 secondFileList = stream.sorted().collect(Collectors.toList());
555             }
556             assertEquals(firstFileList.size(), secondFileList.size());
557 
558             for (int i = 0; i < firstFileList.size(); i++) {
559                 compareFiles(firstFileList.get(i).toFile(), secondFileList.get(i).toFile(), true);
560             }
561         } else {
562             assertEquals(computeChecksum(firstFile), computeChecksum(secondFile));
563         }
564         return;
565     }
566 
567     @Override
setDevice(ITestDevice device)568     public void setDevice(ITestDevice device) {
569         mTestDevice = (TestDevice) device;
570     }
571 
572     @Override
getDevice()573     public ITestDevice getDevice() {
574         return mTestDevice;
575     }
576 }
577