• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 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 android.theme.cts;
18 
19 import android.platform.test.annotations.RequiresDevice;
20 
21 import com.android.ddmlib.Log;
22 import com.android.ddmlib.Log.LogLevel;
23 import com.android.tradefed.device.CollectingOutputReceiver;
24 import com.android.tradefed.device.DeviceNotAvailableException;
25 import com.android.tradefed.device.ITestDevice;
26 import com.android.tradefed.result.FileInputStreamSource;
27 import com.android.tradefed.result.InputStreamSource;
28 import com.android.tradefed.result.LogDataType;
29 import com.android.tradefed.testtype.DeviceTestCase;
30 import com.android.tradefed.util.Pair;
31 import com.android.tradefed.util.StreamUtil;
32 
33 import java.io.File;
34 import java.io.FileInputStream;
35 import java.io.FileOutputStream;
36 import java.io.IOException;
37 import java.io.InputStream;
38 import java.util.HashMap;
39 import java.util.Map;
40 import java.util.concurrent.ExecutorCompletionService;
41 import java.util.concurrent.ExecutorService;
42 import java.util.concurrent.Executors;
43 import java.util.concurrent.TimeUnit;
44 import java.util.regex.Matcher;
45 import java.util.regex.Pattern;
46 import java.util.zip.ZipEntry;
47 import java.util.zip.ZipInputStream;
48 
49 /**
50  * Test to check non-modifiable themes have not been changed.
51  */
52 public class ThemeHostTest extends DeviceTestCase {
53 
54     private static final String LOG_TAG = "ThemeHostTest";
55     private static final String APP_PACKAGE_NAME = "android.theme.app";
56 
57     private static final String GENERATED_ASSETS_ZIP = "/sdcard/cts-theme-assets.zip";
58 
59     /** The class name of the main activity in the APK. */
60     private static final String TEST_CLASS = "androidx.test.runner.AndroidJUnitRunner";
61 
62     /** The command to launch the main instrumentation test. */
63     private static final String START_CMD = String.format(
64             "am instrument -w --no-isolated-storage --no-window-animation %s/%s",
65             APP_PACKAGE_NAME, TEST_CLASS);
66 
67     private static final String CLEAR_GENERATED_CMD = "rm -rf %s/*.png";
68     private static final String STOP_CMD = String.format("am force-stop %s", APP_PACKAGE_NAME);
69 
70     /** Shell command used to obtain current device density. */
71     private static final String WM_DENSITY = "wm density";
72 
73     /** Overall test timeout is 30 minutes. Should only take about 5. */
74     private static final int TEST_RESULT_TIMEOUT = 30 * 60 * 1000;
75 
76     /** Map of reference image names and files. */
77     private Map<String, File> mReferences;
78 
79     /** A reference to the device under test. */
80     private ITestDevice mDevice;
81 
82     private ExecutorService mExecutionService;
83 
84     private ExecutorCompletionService<Pair<String, File>> mCompletionService;
85 
86     // Density to which the device should be restored, or -1 if unnecessary.
87     private int mRestoreDensity;
88 
89 
90     @Override
setUp()91     protected void setUp() throws Exception {
92         super.setUp();
93 
94         mDevice = getDevice();
95         mRestoreDensity = resetDensityIfNeeded(mDevice);
96         final String density = getDensityBucketForDevice(mDevice);
97         final String referenceZipAssetPath = String.format("/%s.zip", density);
98         mReferences = extractReferenceImages(referenceZipAssetPath);
99 
100         final int numCores = Runtime.getRuntime().availableProcessors();
101         mExecutionService = Executors.newFixedThreadPool(numCores * 2);
102         mCompletionService = new ExecutorCompletionService<>(mExecutionService);
103     }
104 
extractReferenceImages(String zipFile)105     private Map<String, File> extractReferenceImages(String zipFile) throws Exception {
106         final Map<String, File> references = new HashMap<>();
107         final InputStream zipStream = ThemeHostTest.class.getResourceAsStream(zipFile);
108         if (zipStream != null) {
109             try (ZipInputStream in = new ZipInputStream(zipStream)) {
110                 final byte[] buffer = new byte[1024];
111                 for (ZipEntry ze; (ze = in.getNextEntry()) != null; ) {
112                     final String name = ze.getName();
113                     final File tmp = File.createTempFile("ref_" + name, ".png");
114                     tmp.deleteOnExit();
115                     try (FileOutputStream out = new FileOutputStream(tmp)) {
116                         for (int count; (count = in.read(buffer)) != -1; ) {
117                             out.write(buffer, 0, count);
118                         }
119                     }
120 
121                     references.put(name, tmp);
122                 }
123             } catch (IOException e) {
124                 fail("Failed to unzip assets: " + zipFile);
125             }
126         } else {
127             if (checkHardwareTypeSkipTest()) {
128                 Log.logAndDisplay(LogLevel.WARN, LOG_TAG,
129                         "Could not obtain resources for skipped themes test: " + zipFile);
130             } else {
131                 fail("Failed to get resource: " + zipFile);
132             }
133         }
134 
135         return references;
136     }
137 
138     @Override
tearDown()139     protected void tearDown() throws Exception {
140         mExecutionService.shutdown();
141 
142         // Remove generated images.
143         mDevice.executeShellCommand(CLEAR_GENERATED_CMD);
144 
145         restoreDensityIfNeeded(mDevice, mRestoreDensity);
146 
147         super.tearDown();
148     }
149 
testThemes()150     public void testThemes() throws Exception {
151         if (checkHardwareTypeSkipTest()) {
152             Log.logAndDisplay(LogLevel.INFO, LOG_TAG, "Skipped themes test for watch / TV / automotive");
153             return;
154         }
155         if (mReferences.isEmpty()) {
156             Log.logAndDisplay(LogLevel.INFO, LOG_TAG,
157                     "Skipped themes test due to missing reference images");
158             return;
159         }
160 
161         assertTrue("Aborted image generation, see device log for details", generateDeviceImages());
162 
163         // Pull ZIP file from remote device.
164         final File localZip = File.createTempFile("generated", ".zip");
165         assertTrue("Failed to pull generated assets from device",
166                 mDevice.pullFile(GENERATED_ASSETS_ZIP, localZip));
167 
168         final int numTasks = extractGeneratedImages(localZip, mReferences);
169 
170         int failureCount = 0;
171         for (int i = numTasks; i > 0; i--) {
172             final Pair<String, File> comparison = mCompletionService.take().get();
173             if (comparison != null) {
174                 InputStreamSource inputStream = new FileInputStreamSource(comparison.second);
175                 try{
176                     // Log the diff file
177                     addTestLog(comparison.first, LogDataType.PNG, inputStream);
178                 } finally {
179                     StreamUtil.cancel(inputStream);
180                 }
181                 failureCount++;
182             }
183         }
184 
185         assertTrue(failureCount + " failures in theme test", failureCount == 0);
186     }
187 
extractGeneratedImages(File localZip, Map<String, File> references)188     private int extractGeneratedImages(File localZip, Map<String, File> references)
189             throws IOException {
190         int numTasks = 0;
191 
192         // Extract generated images to temporary files.
193         final byte[] data = new byte[8192];
194         try (ZipInputStream zipInput = new ZipInputStream(new FileInputStream(localZip))) {
195             for (ZipEntry entry; (entry = zipInput.getNextEntry()) != null; ) {
196                 final String name = entry.getName();
197                 final File expected = references.get(name);
198                 if (expected != null && expected.exists()) {
199                     final File actual = File.createTempFile("actual_" + name, ".png");
200                     actual.deleteOnExit();
201 
202                     try (FileOutputStream pngOutput = new FileOutputStream(actual)) {
203                         for (int count; (count = zipInput.read(data, 0, data.length)) != -1; ) {
204                             pngOutput.write(data, 0, count);
205                         }
206                     }
207 
208                     final String shortName = name.substring(0, name.indexOf('.'));
209                     mCompletionService.submit(new ComparisonTask(shortName, expected, actual));
210                     numTasks++;
211                 } else {
212                     Log.logAndDisplay(LogLevel.INFO, LOG_TAG,
213                             "Missing reference image for " + name);
214                 }
215 
216                 zipInput.closeEntry();
217             }
218         }
219 
220         return numTasks;
221     }
222 
generateDeviceImages()223     private boolean generateDeviceImages() throws Exception {
224         // Stop any existing instances.
225         mDevice.executeShellCommand(STOP_CMD);
226 
227         // Start instrumentation test.
228         final CollectingOutputReceiver receiver = new CollectingOutputReceiver();
229         mDevice.executeShellCommand(START_CMD, receiver, TEST_RESULT_TIMEOUT,
230                 TimeUnit.MILLISECONDS, 0);
231 
232         return receiver.getOutput().contains("OK ");
233     }
234 
getDensityBucketForDevice(ITestDevice device)235     private static String getDensityBucketForDevice(ITestDevice device) {
236         final int density;
237         try {
238             density = getDensityForDevice(device);
239         } catch (DeviceNotAvailableException e) {
240             throw new RuntimeException("Failed to detect device density", e);
241         }
242         final String bucket;
243         switch (density) {
244             case 120:
245                 bucket = "ldpi";
246                 break;
247             case 160:
248                 bucket = "mdpi";
249                 break;
250             case 213:
251                 bucket = "tvdpi";
252                 break;
253             case 240:
254                 bucket = "hdpi";
255                 break;
256             case 320:
257                 bucket = "xhdpi";
258                 break;
259             case 480:
260                 bucket = "xxhdpi";
261                 break;
262             case 640:
263                 bucket = "xxxhdpi";
264                 break;
265             default:
266                 bucket = density + "dpi";
267                 break;
268         }
269 
270         Log.logAndDisplay(LogLevel.INFO, LOG_TAG,
271                 "Device density detected as " + density + " (" + bucket + ")");
272         return bucket;
273     }
274 
resetDensityIfNeeded(ITestDevice device)275     private static int resetDensityIfNeeded(ITestDevice device) throws DeviceNotAvailableException {
276         final String output = device.executeShellCommand(WM_DENSITY);
277          final Pattern p = Pattern.compile("Override density: (\\d+)");
278          final Matcher m = p.matcher(output);
279          if (m.find()) {
280              device.executeShellCommand(WM_DENSITY + " reset");
281              int restoreDensity = Integer.parseInt(m.group(1));
282              return restoreDensity;
283          }
284          return -1;
285     }
286 
restoreDensityIfNeeded(ITestDevice device, int restoreDensity)287     private static void restoreDensityIfNeeded(ITestDevice device, int restoreDensity)
288             throws DeviceNotAvailableException {
289         if (restoreDensity > 0) {
290             device.executeShellCommand(WM_DENSITY + " " + restoreDensity);
291         }
292     }
293 
getDensityForDevice(ITestDevice device)294     private static int getDensityForDevice(ITestDevice device) throws DeviceNotAvailableException {
295         final String output = device.executeShellCommand(WM_DENSITY);
296         final Pattern p = Pattern.compile("Physical density: (\\d+)");
297         final Matcher m = p.matcher(output);
298         if (m.find()) {
299             return Integer.parseInt(m.group(1));
300         }
301         throw new RuntimeException("Failed to detect device density");
302     }
303 
checkHardwareTypeSkipTest()304     private boolean checkHardwareTypeSkipTest() {
305         try {
306          if( mDevice.hasFeature("feature:android.hardware.type.watch")
307                  || mDevice.hasFeature("feature:android.hardware.type.television")
308                  || mDevice.hasFeature("feature:android.hardware.type.automotive")) {
309              return true;
310          }
311         } catch (DeviceNotAvailableException ex) {
312              return false;
313         }
314         return false;
315     }
316 
isEmulator(ITestDevice device)317     private static boolean isEmulator(ITestDevice device) {
318         // Expecting something like "emulator-XXXX" or "EMULATORXXXX".
319         return device.getSerialNumber().toLowerCase().startsWith("emulator");
320     }
321 }
322