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.platform.test.rule; 17 18 import static com.google.common.truth.Truth.assertThat; 19 import static com.google.common.truth.Truth.assertWithMessage; 20 import static org.mockito.Mockito.any; 21 import static org.mockito.Mockito.atLeastOnce; 22 import static org.mockito.Mockito.doAnswer; 23 import static org.mockito.Mockito.doReturn; 24 import static org.mockito.Mockito.eq; 25 import static org.mockito.Mockito.never; 26 import static org.mockito.Mockito.spy; 27 import static org.mockito.Mockito.times; 28 import static org.mockito.Mockito.verify; 29 import static java.util.stream.Collectors.toList; 30 31 import android.os.Bundle; 32 import android.os.SystemClock; 33 import androidx.test.InstrumentationRegistry; 34 35 import org.junit.Before; 36 import org.junit.Test; 37 import org.junit.Rule; 38 import org.junit.rules.ExpectedException; 39 import org.junit.runner.Description; 40 import org.junit.runner.RunWith; 41 import org.junit.runners.JUnit4; 42 import org.junit.runners.model.Statement; 43 import org.mockito.ArgumentCaptor; 44 45 import java.io.File; 46 import java.nio.file.Files; 47 import java.text.SimpleDateFormat; 48 import java.util.Arrays; 49 import java.util.ArrayList; 50 import java.util.Collection; 51 import java.util.Collections; 52 import java.util.Date; 53 import java.util.HashSet; 54 import java.util.List; 55 import java.util.concurrent.atomic.AtomicBoolean; 56 import java.util.concurrent.TimeUnit; 57 import java.util.stream.Stream; 58 59 /** Unit tests for {@link Dex2oatPressureRule}. */ 60 @RunWith(JUnit4.class) 61 public class Dex2oatPressureRuleTest { 62 @Rule public ExpectedException expectedException = ExpectedException.none(); 63 64 private static final Description TEST_DESCRIPTION = 65 Description.createTestDescription("Class", "method"); 66 67 // The rule under test. 68 private Dex2oatPressureRule rule; 69 70 private AtomicBoolean dex2oatIsRunning = new AtomicBoolean(); 71 72 @Before setUp()73 public void setUp() { 74 rule = spy(new Dex2oatPressureRule()); 75 // A bit of a hack for testing -- the success and duration of a compilation will be coded 76 // into the package name. e.g., "success.3000" will mean a successful compilation after 3000 77 // ms, and "failure.1000" will mean that the compilation reports failure after 1000 ms. 78 // Arbiturary strings will be appended after the duration number to simulate different 79 // package names (e.g. "success.1000.some_package"). 80 doAnswer( 81 invocation -> { 82 dex2oatIsRunning.set(true); 83 String pkg = invocation.getArgument(0); 84 String status = pkg.split("\\.")[0]; 85 long duration = 86 TimeUnit.MILLISECONDS.toMillis( 87 Long.parseLong(pkg.split("\\.")[1])); 88 SystemClock.sleep(duration); 89 dex2oatIsRunning.set(false); 90 return status; 91 }) 92 .when(rule) 93 .runCompileCommand(any(String.class), any(String.class)); 94 } 95 96 @Test testInvokesCompilation()97 public void testInvokesCompilation() throws Throwable { 98 String packageName = "success.20"; 99 stubInstrumentationArgs(Dex2oatPressureRule.PACKAGES_OPTION, packageName, 100 Dex2oatPressureRule.ENABLE_OPTION, String.valueOf(true)); 101 stubInstalledPackages(Arrays.asList(packageName)); 102 fakeWhetherDex2oatIsRunningCheck(0); 103 rule.apply(createTestStatement(10L), TEST_DESCRIPTION).evaluate(); 104 verify(rule, times(1)).runCompileCommand(packageName, Dex2oatPressureRule.SPEED_FILTER); 105 } 106 107 @Test testInvokesCompilationCyclically()108 public void testInvokesCompilationCyclically() throws Throwable { 109 List<String> packages = 110 Arrays.asList("success.10.pkg1", "success.20.pkg2", "success.30.pkg3"); 111 stubInstrumentationArgs(Dex2oatPressureRule.PACKAGES_OPTION, String.join(",", packages), 112 Dex2oatPressureRule.ENABLE_OPTION, String.valueOf(true)); 113 stubInstalledPackages(packages); 114 fakeWhetherDex2oatIsRunningCheck(0); 115 rule.apply(createTestStatement(150L), TEST_DESCRIPTION).evaluate(); 116 ArgumentCaptor<String> compiledPackages = ArgumentCaptor.forClass(String.class); 117 verify(rule, atLeastOnce()) 118 .runCompileCommand(compiledPackages.capture(), any(String.class)); 119 for (int i = 0; i < compiledPackages.getAllValues().size(); i++) { 120 assertThat(compiledPackages.getAllValues().get(i)) 121 .isEqualTo(packages.get(i % packages.size())); 122 } 123 } 124 125 @Test testSchedulesCompilationUntilTestEnds()126 public void testSchedulesCompilationUntilTestEnds() throws Throwable { 127 List<String> packages = 128 Arrays.asList("success.100.pkg1", "success.200.pkg2", "success.300.pkg3"); 129 stubInstrumentationArgs(Dex2oatPressureRule.PACKAGES_OPTION, String.join(",", packages), 130 Dex2oatPressureRule.ENABLE_OPTION, String.valueOf(true)); 131 stubInstalledPackages(packages); 132 fakeWhetherDex2oatIsRunningCheck(0); 133 134 // Stub the stopDex2oat() method to check if there is a compile command running at the end 135 // of the test, before asking the rule to stop kicking off dex2oat. There is a tiny chance 136 // for a race condition (the check might fall between the kicking off of two compile 137 // commands) but given the ~100ms timing granularity this is unlikely to be an issue. 138 doAnswer( 139 invocation -> { 140 assertThat(dex2oatIsRunning.get()).isTrue(); 141 invocation.callRealMethod(); 142 return null; 143 }) 144 .when(rule) 145 .stopDex2oat(); 146 // Time the test statement to end around the middle of compiling the second package during 147 // the second round of compilation. 148 rule.apply(createTestStatement(800L), TEST_DESCRIPTION).evaluate(); 149 assertThat(dex2oatIsRunning.get()).isFalse(); 150 } 151 152 @Test testNoAdditionalCompilationIsTriggeredAfterTestFinishes()153 public void testNoAdditionalCompilationIsTriggeredAfterTestFinishes() throws Throwable { 154 List<String> packages = Arrays.asList("success.100.pkg1", "success.200.pkg2"); 155 stubInstrumentationArgs(Dex2oatPressureRule.PACKAGES_OPTION, String.join(",", packages), 156 Dex2oatPressureRule.ENABLE_OPTION, String.valueOf(true)); 157 stubInstalledPackages(packages); 158 fakeWhetherDex2oatIsRunningCheck(0); 159 160 // Stub the stopDex2oat() method to do a verification on the packages compiled. No other 161 // packages should be compiled after that. 162 ArgumentCaptor<String> compiledPackagesWhenTestEnded = 163 ArgumentCaptor.forClass(String.class); 164 doAnswer( 165 invocation -> { 166 invocation.callRealMethod(); 167 verify(rule, atLeastOnce()) 168 .runCompileCommand( 169 compiledPackagesWhenTestEnded.capture(), 170 any(String.class)); 171 return null; 172 }) 173 .when(rule) 174 .stopDex2oat(); 175 176 rule.apply(createTestStatement(600L), TEST_DESCRIPTION).evaluate(); 177 ArgumentCaptor<String> compiledPackagesWhenRuleReturns = 178 ArgumentCaptor.forClass(String.class); 179 verify(rule, atLeastOnce()) 180 .runCompileCommand(compiledPackagesWhenRuleReturns.capture(), any(String.class)); 181 assertThat(compiledPackagesWhenRuleReturns.getAllValues()) 182 .isEqualTo(compiledPackagesWhenTestEnded.getAllValues()); 183 } 184 185 @Test testFailedCompilationDoesNotBlockOtherCompilations()186 public void testFailedCompilationDoesNotBlockOtherCompilations() throws Throwable { 187 List<String> packages = Arrays.asList("failure.10", "success.10"); 188 stubInstrumentationArgs(Dex2oatPressureRule.PACKAGES_OPTION, String.join(",", packages), 189 Dex2oatPressureRule.ENABLE_OPTION, String.valueOf(true)); 190 stubInstalledPackages(packages); 191 fakeWhetherDex2oatIsRunningCheck(0); 192 rule.apply(createTestStatement(50L), TEST_DESCRIPTION).evaluate(); 193 for (String pkg : packages) { 194 verify(rule, atLeastOnce()).runCompileCommand(eq(pkg), any(String.class)); 195 } 196 } 197 198 @Test testWaitsUntilDex2oatIsRunningBeforeTest()199 public void testWaitsUntilDex2oatIsRunningBeforeTest() throws Throwable { 200 String packageName = "success.150"; 201 stubInstrumentationArgs(Dex2oatPressureRule.PACKAGES_OPTION, packageName, 202 Dex2oatPressureRule.ENABLE_OPTION, String.valueOf(true)); 203 stubInstalledPackages(Arrays.asList(packageName)); 204 int checksBeforeDex2oatIsRunning = 5; 205 fakeWhetherDex2oatIsRunningCheck(checksBeforeDex2oatIsRunning); 206 Statement testStatement = 207 new Statement() { 208 @Override 209 public void evaluate() { 210 // The check should have been performed unsuccessful count + 1 times. 211 verify(rule, times(checksBeforeDex2oatIsRunning + 1)).dex2oatIsRunning(); 212 SystemClock.sleep(100L); 213 } 214 }; 215 rule.apply(testStatement, TEST_DESCRIPTION).evaluate(); 216 } 217 218 @Test testCancelsAndThrowsIfDex2oatIsNotRunningAfterTimeout()219 public void testCancelsAndThrowsIfDex2oatIsNotRunningAfterTimeout() throws Throwable { 220 expectedException.expectMessage("dex2oat still isn't running"); 221 222 String packageName = "success.150"; 223 stubInstrumentationArgs(Dex2oatPressureRule.PACKAGES_OPTION, packageName, 224 Dex2oatPressureRule.ENABLE_OPTION, String.valueOf(true)); 225 stubInstalledPackages(Arrays.asList(packageName)); 226 fakeWhetherDex2oatIsRunningCheck(-1); 227 228 rule.apply(createTestStatement(10L), TEST_DESCRIPTION).evaluate(); 229 230 verify(rule, times(1)).stopDex2oat(); 231 } 232 233 @Test testValidCompilationFilters()234 public void testValidCompilationFilters() throws Throwable { 235 String packageName = "success.20"; 236 stubInstalledPackages(Arrays.asList(packageName)); 237 fakeWhetherDex2oatIsRunningCheck(0); 238 for (String filter : Dex2oatPressureRule.SUPPORTED_FILTERS_LIST) { 239 stubInstrumentationArgs( 240 Dex2oatPressureRule.PACKAGES_OPTION, 241 packageName, 242 Dex2oatPressureRule.COMPILATION_FILTER_OPTION, 243 filter, 244 Dex2oatPressureRule.ENABLE_OPTION, 245 String.valueOf(true)); 246 rule.apply(createTestStatement(10L), TEST_DESCRIPTION).evaluate(); 247 verify(rule, atLeastOnce()).runCompileCommand(any(String.class), eq(filter)); 248 } 249 } 250 251 @Test testInvalidCompilationFilterThrows()252 public void testInvalidCompilationFilterThrows() throws Throwable { 253 expectedException.expectMessage("Invalid compilation filter"); 254 255 String packageName = "success.20"; 256 stubInstrumentationArgs( 257 Dex2oatPressureRule.PACKAGES_OPTION, 258 packageName, 259 Dex2oatPressureRule.COMPILATION_FILTER_OPTION, 260 "not a filter", 261 Dex2oatPressureRule.ENABLE_OPTION, 262 String.valueOf(true)); 263 stubInstalledPackages(Arrays.asList(packageName)); 264 fakeWhetherDex2oatIsRunningCheck(0); 265 rule.apply(createTestStatement(10L), TEST_DESCRIPTION).evaluate(); 266 } 267 268 @Test testInvalidPackageThrows()269 public void testInvalidPackageThrows() throws Throwable { 270 List<String> packages = Arrays.asList("success.20.exists", "success.20.nonexistent"); 271 expectedException.expectMessage(packages.get(1)); 272 expectedException.expectMessage("not installed"); 273 274 stubInstrumentationArgs(Dex2oatPressureRule.PACKAGES_OPTION, String.join(",", packages), 275 Dex2oatPressureRule.ENABLE_OPTION, String.valueOf(true)); 276 stubInstalledPackages(Arrays.asList(packages.get(0))); 277 rule.apply(createTestStatement(10L), TEST_DESCRIPTION).evaluate(); 278 } 279 280 @Test testDisable()281 public void testDisable() throws Throwable { 282 String packageName = "success.10"; 283 stubInstrumentationArgs( 284 Dex2oatPressureRule.PACKAGES_OPTION, 285 packageName, 286 Dex2oatPressureRule.ENABLE_OPTION, 287 String.valueOf(false)); 288 stubInstalledPackages(Arrays.asList(packageName)); 289 rule.apply(createTestStatement(50L), TEST_DESCRIPTION).evaluate(); 290 verify(rule, never()).runCompileCommand(any(String.class), any(String.class)); 291 } 292 293 @Test testMultipleTestsParsePackageNamesIndependently()294 public void testMultipleTestsParsePackageNamesIndependently() throws Throwable { 295 List<String> test1PackageNames = Arrays.asList("success.10.test1", "success.20.test1"); 296 List<String> test2PackageNames = Arrays.asList("success.10.test2", "success.20.test2"); 297 stubInstalledPackages( 298 Stream.of(test1PackageNames, test2PackageNames) 299 .flatMap(l -> l.stream()) 300 .collect(toList())); 301 fakeWhetherDex2oatIsRunningCheck(0); 302 // Run the first test. 303 stubInstrumentationArgs( 304 Dex2oatPressureRule.PACKAGES_OPTION, String.join(",", test1PackageNames), 305 Dex2oatPressureRule.ENABLE_OPTION, String.valueOf(true)); 306 rule.apply(createTestStatement(10L), TEST_DESCRIPTION).evaluate(); 307 ArgumentCaptor<String> compiledInTest1 = ArgumentCaptor.forClass(String.class); 308 verify(rule, atLeastOnce()).runCompileCommand(compiledInTest1.capture(), any(String.class)); 309 // Run the second test. 310 stubInstrumentationArgs( 311 Dex2oatPressureRule.PACKAGES_OPTION, String.join(",", test2PackageNames), 312 Dex2oatPressureRule.ENABLE_OPTION, String.valueOf(true)); 313 rule.apply(createTestStatement(10L), TEST_DESCRIPTION).evaluate(); 314 ArgumentCaptor<String> compiledInBothTests = ArgumentCaptor.forClass(String.class); 315 verify(rule, atLeastOnce()) 316 .runCompileCommand(compiledInBothTests.capture(), any(String.class)); 317 // Check that compiledInTest1 only has packages for test 1. 318 assertThat(compiledInTest1.getAllValues()).isNotEmpty(); 319 assertThat(compiledInTest1.getAllValues().stream().allMatch(p -> p.endsWith("test1"))) 320 .isTrue(); 321 // Check that packages compiled in test2 only has packages for test 2. 322 List<String> compiledInTest2 = 323 compiledInBothTests 324 .getAllValues() 325 .subList( 326 compiledInTest1.getAllValues().size(), 327 compiledInBothTests.getAllValues().size()); 328 assertThat(compiledInTest2).isNotEmpty(); 329 assertThat(compiledInTest2.stream().allMatch(p -> p.endsWith("test2"))).isTrue(); 330 } 331 332 @Test testCompilationCommandCompiles()333 public void testCompilationCommandCompiles() throws Throwable { 334 boolean foundDex2oatPid = false; 335 Dex2oatPressureRule realRule = new Dex2oatPressureRule(); 336 SimpleDateFormat logcatTimestampFormatter = new SimpleDateFormat("MM-dd HH:mm:ss.SSS"); 337 String startLogcatAt = 338 logcatTimestampFormatter.format(new Date(System.currentTimeMillis())); 339 340 // Run the real compilation method on the package under test (which is guaranteed to always 341 // exist on the device), which does not interfere with the currently running test. 342 String packageName = InstrumentationRegistry.getContext().getPackageName(); 343 realRule.runCompileCommand(packageName, Dex2oatPressureRule.SPEED_FILTER); 344 345 // Dump the logcat since before the compile command, and check if there are logs about 346 // compiling the test package. 347 File logcatFile = File.createTempFile("logcat", ".txt"); 348 logcatFile.deleteOnExit(); 349 ProcessBuilder pb = new ProcessBuilder(Arrays.asList("logcat", "-t", startLogcatAt)); 350 pb.redirectOutput(logcatFile); 351 Process proc = pb.start(); 352 proc.waitFor(); 353 354 List<String> logcat = Files.readAllLines(logcatFile.toPath()); 355 assertWithMessage( 356 String.format( 357 "Expected a logcat line that mentioned dex2oat, the package to " 358 + "compile and the compilation filter, but got:\n%s", 359 String.join("\n", logcat))) 360 .that( 361 logcat.stream() 362 .map( 363 line -> 364 line.contains("dex2oat") 365 && line.contains(packageName) 366 && line.contains( 367 Dex2oatPressureRule.SPEED_FILTER)) 368 .collect(toList())) 369 .contains(true); 370 } 371 createTestStatement(long testDurationMs)372 private Statement createTestStatement(long testDurationMs) { 373 return new Statement() { 374 @Override 375 public void evaluate() { 376 SystemClock.sleep(TimeUnit.MILLISECONDS.toMillis(testDurationMs)); 377 } 378 }; 379 } 380 381 /** 382 * Stub instrumentation args for the rule under test. The arguments should be an alternating 383 * list of keys and values. 384 */ 385 private void stubInstrumentationArgs(String... keysAndValues) { 386 if (keysAndValues.length % 2 != 0) { 387 throw new IllegalArgumentException("Please supply keys and values in pairs."); 388 } 389 Bundle args = new Bundle(); 390 for (int i = 0; i < keysAndValues.length; i += 2) { 391 args.putString(keysAndValues[i], keysAndValues[i + 1]); 392 } 393 doReturn(args).when(rule).getArguments(); 394 } 395 396 /** 397 * Fake the check to whether dex2oat is running. Argument: the number of attempts before the 398 * check returns a positive result, or -1 for never being successful. 399 */ 400 private void fakeWhetherDex2oatIsRunningCheck(int failedAttempts) { 401 List<Boolean> returnValues = new ArrayList<>(); 402 if (failedAttempts == -1) { 403 returnValues.add(false); 404 } else { 405 returnValues.addAll(Collections.nCopies(failedAttempts, false)); 406 returnValues.add(true); 407 } 408 doReturn(returnValues.get(0), returnValues.subList(1, returnValues.size()).toArray()) 409 .when(rule) 410 .dex2oatIsRunning(); 411 } 412 413 private void stubInstalledPackages(Collection<String> packages) { 414 doReturn(new HashSet(packages)).when(rule).getInstalledPackages(); 415 } 416 } 417