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