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