• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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 com.android.server.art;
18 
19 import static com.android.server.art.ArtManagerLocal.DexoptDoneCallback;
20 import static com.android.server.art.model.DexoptResult.DexContainerFileDexoptResult;
21 import static com.android.server.art.model.DexoptResult.DexoptResultStatus;
22 import static com.android.server.art.model.DexoptResult.PackageDexoptResult;
23 
24 import static com.google.common.truth.Truth.assertThat;
25 
26 import static org.junit.Assert.assertThrows;
27 import static org.mockito.Mockito.any;
28 import static org.mockito.Mockito.eq;
29 import static org.mockito.Mockito.inOrder;
30 import static org.mockito.Mockito.lenient;
31 import static org.mockito.Mockito.mock;
32 import static org.mockito.Mockito.never;
33 import static org.mockito.Mockito.same;
34 import static org.mockito.Mockito.times;
35 import static org.mockito.Mockito.verify;
36 import static org.mockito.Mockito.when;
37 
38 import android.apphibernation.AppHibernationManager;
39 import android.os.CancellationSignal;
40 
41 import androidx.test.filters.SmallTest;
42 import androidx.test.runner.AndroidJUnit4;
43 
44 import com.android.modules.utils.pm.PackageStateModulesUtils;
45 import com.android.server.art.model.ArtFlags;
46 import com.android.server.art.model.Config;
47 import com.android.server.art.model.DexoptParams;
48 import com.android.server.art.model.DexoptResult;
49 import com.android.server.art.model.OperationProgress;
50 import com.android.server.art.testing.StaticMockitoRule;
51 import com.android.server.pm.PackageManagerLocal;
52 import com.android.server.pm.pkg.AndroidPackage;
53 import com.android.server.pm.pkg.AndroidPackageSplit;
54 import com.android.server.pm.pkg.PackageState;
55 import com.android.server.pm.pkg.SharedLibrary;
56 
57 import org.junit.After;
58 import org.junit.Before;
59 import org.junit.Rule;
60 import org.junit.Test;
61 import org.junit.runner.RunWith;
62 import org.mockito.InOrder;
63 import org.mockito.Mock;
64 
65 import java.util.ArrayList;
66 import java.util.List;
67 import java.util.concurrent.Executor;
68 import java.util.concurrent.ExecutorService;
69 import java.util.concurrent.Executors;
70 import java.util.concurrent.ForkJoinPool;
71 import java.util.concurrent.Future;
72 import java.util.concurrent.Semaphore;
73 import java.util.concurrent.TimeUnit;
74 import java.util.concurrent.atomic.AtomicBoolean;
75 import java.util.function.Consumer;
76 import java.util.stream.Stream;
77 
78 @SmallTest
79 @RunWith(AndroidJUnit4.class)
80 public class DexoptHelperTest {
81     private static final String PKG_NAME_FOO = "com.example.foo";
82     private static final String PKG_NAME_BAR = "com.example.bar";
83     private static final String PKG_NAME_LIB1 = "com.example.lib1";
84     private static final String PKG_NAME_LIB2 = "com.example.lib2";
85     private static final String PKG_NAME_LIB3 = "com.example.lib3";
86     private static final String PKG_NAME_LIB4 = "com.example.lib4";
87     private static final String PKG_NAME_LIBBAZ = "com.example.libbaz";
88     private static final String PKG_NAME_SDK = "com.example.sdk";
89 
90     @Rule
91     public StaticMockitoRule mockitoRule = new StaticMockitoRule(PackageStateModulesUtils.class);
92 
93     @Mock private DexoptHelper.Injector mInjector;
94     @Mock private PrimaryDexopter mPrimaryDexopter;
95     @Mock private SecondaryDexopter mSecondaryDexopter;
96     @Mock private AppHibernationManager mAhm;
97     @Mock private PackageManagerLocal.FilteredSnapshot mSnapshot;
98     private PackageState mPkgStateFoo;
99     private PackageState mPkgStateBar;
100     private PackageState mPkgStateLib1;
101     private PackageState mPkgStateLib2;
102     private PackageState mPkgStateLib4;
103     private PackageState mPkgStateLibbaz;
104     private AndroidPackage mPkgFoo;
105     private AndroidPackage mPkgBar;
106     private AndroidPackage mPkgLib1;
107     private AndroidPackage mPkgLib2;
108     private AndroidPackage mPkgLib4;
109     private AndroidPackage mPkgLibbaz;
110     private CancellationSignal mCancellationSignal;
111     private ExecutorService mExecutor;
112     private List<DexContainerFileDexoptResult> mPrimaryResults;
113     private List<DexContainerFileDexoptResult> mSecondaryResults;
114     private Config mConfig;
115     private DexoptParams mParams;
116     private List<String> mRequestedPackages;
117     private DexoptHelper mDexoptHelper;
118 
119     @Before
setUp()120     public void setUp() throws Exception {
121         lenient().when(mAhm.isHibernatingGlobally(any())).thenReturn(false);
122         lenient().when(mAhm.isOatArtifactDeletionEnabled()).thenReturn(true);
123 
124         mCancellationSignal = new CancellationSignal();
125         mExecutor = Executors.newSingleThreadExecutor();
126         mConfig = new Config();
127 
128         preparePackagesAndLibraries();
129 
130         mPrimaryResults = createResults("/somewhere/app/foo/base.apk",
131                 DexoptResult.DEXOPT_PERFORMED /* status1 */,
132                 DexoptResult.DEXOPT_PERFORMED /* status2 */);
133         mSecondaryResults = createResults("/data/user_de/0/foo/foo.apk",
134                 DexoptResult.DEXOPT_PERFORMED /* status1 */,
135                 DexoptResult.DEXOPT_PERFORMED /* status2 */);
136 
137         lenient()
138                 .when(mInjector.getPrimaryDexopter(any(), any(), any(), any()))
139                 .thenReturn(mPrimaryDexopter);
140         lenient().when(mPrimaryDexopter.dexopt()).thenReturn(mPrimaryResults);
141 
142         lenient()
143                 .when(mInjector.getSecondaryDexopter(any(), any(), any(), any()))
144                 .thenReturn(mSecondaryDexopter);
145         lenient().when(mSecondaryDexopter.dexopt()).thenReturn(mSecondaryResults);
146 
147         mParams = new DexoptParams.Builder("install")
148                           .setCompilerFilter("speed-profile")
149                           .setFlags(ArtFlags.FLAG_FOR_SECONDARY_DEX
150                                           | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES,
151                                   ArtFlags.FLAG_FOR_SECONDARY_DEX
152                                           | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES)
153                           .build();
154 
155         lenient().when(mInjector.getAppHibernationManager()).thenReturn(mAhm);
156         lenient().when(mInjector.getConfig()).thenReturn(mConfig);
157 
158         mDexoptHelper = new DexoptHelper(mInjector);
159     }
160 
161     @After
tearDown()162     public void tearDown() {
163         mExecutor.shutdown();
164     }
165 
166     @Test
testDexopt()167     public void testDexopt() throws Exception {
168         // Only package libbaz fails.
169         var failingPrimaryDexopter = mock(PrimaryDexopter.class);
170         List<DexContainerFileDexoptResult> partialFailureResults = createResults(
171                 "/somewhere/app/foo/base.apk", DexoptResult.DEXOPT_PERFORMED /* status1 */,
172                 DexoptResult.DEXOPT_FAILED /* status2 */);
173         lenient().when(failingPrimaryDexopter.dexopt()).thenReturn(partialFailureResults);
174         when(mInjector.getPrimaryDexopter(same(mPkgStateLibbaz), any(), any(), any()))
175                 .thenReturn(failingPrimaryDexopter);
176 
177         DexoptResult result = mDexoptHelper.dexopt(
178                 mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
179 
180         assertThat(result.getRequestedCompilerFilter()).isEqualTo("speed-profile");
181         assertThat(result.getReason()).isEqualTo("install");
182         assertThat(result.getFinalStatus()).isEqualTo(DexoptResult.DEXOPT_FAILED);
183 
184         // The requested packages must come first.
185         assertThat(result.getPackageDexoptResults()).hasSize(6);
186         checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, DexoptResult.DEXOPT_PERFORMED,
187                 List.of(mPrimaryResults, mSecondaryResults));
188         checkPackageResult(result, 1 /* index */, PKG_NAME_BAR, DexoptResult.DEXOPT_PERFORMED,
189                 List.of(mPrimaryResults, mSecondaryResults));
190         checkPackageResult(result, 2 /* index */, PKG_NAME_LIBBAZ, DexoptResult.DEXOPT_FAILED,
191                 List.of(partialFailureResults, mSecondaryResults));
192         checkPackageResult(result, 3 /* index */, PKG_NAME_LIB1, DexoptResult.DEXOPT_PERFORMED,
193                 List.of(mPrimaryResults, mSecondaryResults));
194         checkPackageResult(result, 4 /* index */, PKG_NAME_LIB2, DexoptResult.DEXOPT_PERFORMED,
195                 List.of(mPrimaryResults, mSecondaryResults));
196         checkPackageResult(result, 5 /* index */, PKG_NAME_LIB4, DexoptResult.DEXOPT_PERFORMED,
197                 List.of(mPrimaryResults, mSecondaryResults));
198 
199         // The order matters. When running in a single thread, it should dexopt primary dex files
200         // and the secondary dex files together for each package, and it should dexopt requested
201         // packages, in the given order, and then dexopt dependencies.
202         InOrder inOrder = inOrder(mInjector);
203         inOrder.verify(mInjector).getPrimaryDexopter(
204                 same(mPkgStateFoo), same(mPkgFoo), same(mParams), any());
205         inOrder.verify(mInjector).getSecondaryDexopter(
206                 same(mPkgStateFoo), same(mPkgFoo), same(mParams), any());
207         inOrder.verify(mInjector).getPrimaryDexopter(
208                 same(mPkgStateBar), same(mPkgBar), same(mParams), any());
209         inOrder.verify(mInjector).getSecondaryDexopter(
210                 same(mPkgStateBar), same(mPkgBar), same(mParams), any());
211         inOrder.verify(mInjector).getPrimaryDexopter(
212                 same(mPkgStateLibbaz), same(mPkgLibbaz), same(mParams), any());
213         inOrder.verify(mInjector).getSecondaryDexopter(
214                 same(mPkgStateLibbaz), same(mPkgLibbaz), same(mParams), any());
215         inOrder.verify(mInjector).getPrimaryDexopter(
216                 same(mPkgStateLib1), same(mPkgLib1), same(mParams), any());
217         inOrder.verify(mInjector).getSecondaryDexopter(
218                 same(mPkgStateLib1), same(mPkgLib1), same(mParams), any());
219         inOrder.verify(mInjector).getPrimaryDexopter(
220                 same(mPkgStateLib2), same(mPkgLib2), same(mParams), any());
221         inOrder.verify(mInjector).getSecondaryDexopter(
222                 same(mPkgStateLib2), same(mPkgLib2), same(mParams), any());
223         inOrder.verify(mInjector).getPrimaryDexopter(
224                 same(mPkgStateLib4), same(mPkgLib4), same(mParams), any());
225         inOrder.verify(mInjector).getSecondaryDexopter(
226                 same(mPkgStateLib4), same(mPkgLib4), same(mParams), any());
227 
228         verifyNoMoreDexopt(6 /* expectedPrimaryTimes */, 6 /* expectedSecondaryTimes */);
229     }
230 
231     @Test
testDexoptNoDependencies()232     public void testDexoptNoDependencies() throws Exception {
233         mParams = new DexoptParams.Builder("install")
234                           .setCompilerFilter("speed-profile")
235                           .setFlags(ArtFlags.FLAG_FOR_SECONDARY_DEX,
236                                   ArtFlags.FLAG_FOR_SECONDARY_DEX
237                                           | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES)
238                           .build();
239 
240         DexoptResult result = mDexoptHelper.dexopt(
241                 mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
242 
243         assertThat(result.getPackageDexoptResults()).hasSize(3);
244         checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, DexoptResult.DEXOPT_PERFORMED,
245                 List.of(mPrimaryResults, mSecondaryResults));
246         checkPackageResult(result, 1 /* index */, PKG_NAME_BAR, DexoptResult.DEXOPT_PERFORMED,
247                 List.of(mPrimaryResults, mSecondaryResults));
248         checkPackageResult(result, 2 /* index */, PKG_NAME_LIBBAZ, DexoptResult.DEXOPT_PERFORMED,
249                 List.of(mPrimaryResults, mSecondaryResults));
250 
251         verifyNoMoreDexopt(3 /* expectedPrimaryTimes */, 3 /* expectedSecondaryTimes */);
252     }
253 
254     @Test
testDexoptPrimaryOnly()255     public void testDexoptPrimaryOnly() throws Exception {
256         mParams = new DexoptParams.Builder("install")
257                           .setCompilerFilter("speed-profile")
258                           .setFlags(ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES,
259                                   ArtFlags.FLAG_FOR_SECONDARY_DEX
260                                           | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES)
261                           .build();
262 
263         DexoptResult result = mDexoptHelper.dexopt(
264                 mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
265 
266         assertThat(result.getPackageDexoptResults()).hasSize(6);
267         checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, DexoptResult.DEXOPT_PERFORMED,
268                 List.of(mPrimaryResults));
269         checkPackageResult(result, 1 /* index */, PKG_NAME_BAR, DexoptResult.DEXOPT_PERFORMED,
270                 List.of(mPrimaryResults));
271         checkPackageResult(result, 2 /* index */, PKG_NAME_LIBBAZ, DexoptResult.DEXOPT_PERFORMED,
272                 List.of(mPrimaryResults));
273         checkPackageResult(result, 3 /* index */, PKG_NAME_LIB1, DexoptResult.DEXOPT_PERFORMED,
274                 List.of(mPrimaryResults));
275         checkPackageResult(result, 4 /* index */, PKG_NAME_LIB2, DexoptResult.DEXOPT_PERFORMED,
276                 List.of(mPrimaryResults));
277         checkPackageResult(result, 5 /* index */, PKG_NAME_LIB4, DexoptResult.DEXOPT_PERFORMED,
278                 List.of(mPrimaryResults));
279 
280         verifyNoMoreDexopt(6 /* expectedPrimaryTimes */, 0 /* expectedSecondaryTimes */);
281     }
282 
283     @Test
testDexoptSdkPrimaryOnly()284     public void testDexoptSdkPrimaryOnly() throws Exception {
285         mParams = new DexoptParams.Builder("bg-dexopt")
286                           .setCompilerFilter("speed-profile")
287                           .setFlags(ArtFlags.FLAG_FOR_PRIMARY_DEX | ArtFlags.FLAG_FOR_SECONDARY_DEX)
288                           .build();
289 
290         PackageState sdkPackageState =
291                 createPackageState(PKG_NAME_SDK, -1 /* appId */, List.of(), false);
292         lenient().when(mSnapshot.getPackageState(PKG_NAME_SDK)).thenReturn(sdkPackageState);
293         mRequestedPackages = List.of(PKG_NAME_SDK);
294 
295         DexoptResult result = mDexoptHelper.dexopt(
296                 mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
297 
298         assertThat(result.getPackageDexoptResults()).hasSize(1);
299         checkPackageResult(result, 0 /* index */, PKG_NAME_SDK, DexoptResult.DEXOPT_PERFORMED,
300                 List.of(mPrimaryResults));
301 
302         verifyNoMoreDexopt(1 /* expectedPrimaryTimes */, 0 /* expectedSecondaryTimes */);
303     }
304 
305     @Test
testDexoptPrimaryOnlyNoDependencies()306     public void testDexoptPrimaryOnlyNoDependencies() throws Exception {
307         mParams = new DexoptParams.Builder("install")
308                           .setCompilerFilter("speed-profile")
309                           .setFlags(0,
310                                   ArtFlags.FLAG_FOR_SECONDARY_DEX
311                                           | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES)
312                           .build();
313 
314         DexoptResult result = mDexoptHelper.dexopt(
315                 mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
316 
317         assertThat(result.getPackageDexoptResults()).hasSize(3);
318         checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, DexoptResult.DEXOPT_PERFORMED,
319                 List.of(mPrimaryResults));
320         checkPackageResult(result, 1 /* index */, PKG_NAME_BAR, DexoptResult.DEXOPT_PERFORMED,
321                 List.of(mPrimaryResults));
322         checkPackageResult(result, 2 /* index */, PKG_NAME_LIBBAZ, DexoptResult.DEXOPT_PERFORMED,
323                 List.of(mPrimaryResults));
324 
325         verifyNoMoreDexopt(3 /* expectedPrimaryTimes */, 0 /* expectedSecondaryTimes */);
326     }
327 
328     @Test
testDexoptCancelledBetweenDex2oatInvocations()329     public void testDexoptCancelledBetweenDex2oatInvocations() throws Exception {
330         when(mPrimaryDexopter.dexopt()).thenAnswer(invocation -> {
331             mCancellationSignal.cancel();
332             return mPrimaryResults;
333         });
334 
335         DexoptResult result = mDexoptHelper.dexopt(
336                 mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
337 
338         assertThat(result.getFinalStatus()).isEqualTo(DexoptResult.DEXOPT_CANCELLED);
339 
340         assertThat(result.getPackageDexoptResults()).hasSize(6);
341         checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, DexoptResult.DEXOPT_CANCELLED,
342                 List.of(mPrimaryResults));
343         checkPackageResult(
344                 result, 1 /* index */, PKG_NAME_BAR, DexoptResult.DEXOPT_CANCELLED, List.of());
345         checkPackageResult(
346                 result, 2 /* index */, PKG_NAME_LIBBAZ, DexoptResult.DEXOPT_CANCELLED, List.of());
347         checkPackageResult(
348                 result, 3 /* index */, PKG_NAME_LIB1, DexoptResult.DEXOPT_CANCELLED, List.of());
349         checkPackageResult(
350                 result, 4 /* index */, PKG_NAME_LIB2, DexoptResult.DEXOPT_CANCELLED, List.of());
351         checkPackageResult(
352                 result, 5 /* index */, PKG_NAME_LIB4, DexoptResult.DEXOPT_CANCELLED, List.of());
353 
354         verify(mInjector).getPrimaryDexopter(
355                 same(mPkgStateFoo), same(mPkgFoo), same(mParams), any());
356 
357         verifyNoMoreDexopt(1 /* expectedPrimaryTimes */, 0 /* expectedSecondaryTimes */);
358     }
359 
360     // This test verifies that every child thread can register its own listener on the cancellation
361     // signal through `setOnCancelListener` (i.e., the listeners don't overwrite each other).
362     @Test
testDexoptCancelledDuringDex2oatInvocationsMultiThreaded()363     public void testDexoptCancelledDuringDex2oatInvocationsMultiThreaded() throws Exception {
364         final int NUM_PACKAGES = 6;
365         final long TIMEOUT_SEC = 10;
366         var dexoptStarted = new Semaphore(0);
367         var dexoptCancelled = new Semaphore(0);
368 
369         when(mInjector.getPrimaryDexopter(any(), any(), any(), any())).thenAnswer(invocation -> {
370             var cancellationSignal = invocation.<CancellationSignal>getArgument(3);
371             var dexopter = mock(PrimaryDexopter.class);
372             when(dexopter.dexopt()).thenAnswer(innerInvocation -> {
373                 // Simulate that the child thread registers its own listener.
374                 var isListenerCalled = new AtomicBoolean(false);
375                 cancellationSignal.setOnCancelListener(() -> isListenerCalled.set(true));
376 
377                 dexoptStarted.release();
378                 assertThat(dexoptCancelled.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue();
379 
380                 // Verify that the listener is called.
381                 assertThat(isListenerCalled.get()).isTrue();
382 
383                 return mPrimaryResults;
384             });
385             return dexopter;
386         });
387 
388         ExecutorService dexoptExecutor = Executors.newFixedThreadPool(NUM_PACKAGES);
389         Future<DexoptResult> future = ForkJoinPool.commonPool().submit(() -> {
390             return mDexoptHelper.dexopt(
391                     mSnapshot, mRequestedPackages, mParams, mCancellationSignal, dexoptExecutor);
392         });
393 
394         try {
395             // Wait for all dexopt operations to start.
396             for (int i = 0; i < NUM_PACKAGES; i++) {
397                 assertThat(dexoptStarted.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue();
398             }
399 
400             mCancellationSignal.cancel();
401 
402             for (int i = 0; i < NUM_PACKAGES; i++) {
403                 dexoptCancelled.release();
404             }
405         } finally {
406             dexoptExecutor.shutdown();
407             Utils.getFuture(future);
408         }
409     }
410 
411     // This test verifies that dexopt operation on the current thread can be cancelled.
412     @Test
testDexoptCancelledDuringDex2oatInvocationsOnCurrentThread()413     public void testDexoptCancelledDuringDex2oatInvocationsOnCurrentThread() throws Exception {
414         final long TIMEOUT_SEC = 10;
415         var dexoptStarted = new Semaphore(0);
416         var dexoptCancelled = new Semaphore(0);
417 
418         when(mInjector.getPrimaryDexopter(any(), any(), any(), any())).thenAnswer(invocation -> {
419             var cancellationSignal = invocation.<CancellationSignal>getArgument(3);
420             var dexopter = mock(PrimaryDexopter.class);
421             when(dexopter.dexopt()).thenAnswer(innerInvocation -> {
422                 if (cancellationSignal.isCanceled()) {
423                     return mPrimaryResults;
424                 }
425 
426                 var isListenerCalled = new AtomicBoolean(false);
427                 cancellationSignal.setOnCancelListener(() -> isListenerCalled.set(true));
428 
429                 dexoptStarted.release();
430                 assertThat(dexoptCancelled.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue();
431 
432                 // Verify that the listener is called.
433                 assertThat(isListenerCalled.get()).isTrue();
434 
435                 return mPrimaryResults;
436             });
437             return dexopter;
438         });
439 
440         // Use the current thread (the one in ForkJoinPool).
441         Executor dexoptExecutor = Runnable::run;
442         Future<DexoptResult> future = ForkJoinPool.commonPool().submit(() -> {
443             return mDexoptHelper.dexopt(
444                     mSnapshot, mRequestedPackages, mParams, mCancellationSignal, dexoptExecutor);
445         });
446 
447         try {
448             // Only one dexopt operation should start.
449             assertThat(dexoptStarted.tryAcquire(TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue();
450 
451             mCancellationSignal.cancel();
452 
453             dexoptCancelled.release();
454         } finally {
455             Utils.getFuture(future);
456         }
457     }
458 
459     @Test
testDexoptNotDexoptable()460     public void testDexoptNotDexoptable() throws Exception {
461         when(PackageStateModulesUtils.isDexoptable(mPkgStateFoo)).thenReturn(false);
462 
463         mRequestedPackages = List.of(PKG_NAME_FOO);
464         DexoptResult result = mDexoptHelper.dexopt(
465                 mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
466 
467         assertThat(result.getFinalStatus()).isEqualTo(DexoptResult.DEXOPT_SKIPPED);
468         assertThat(result.getPackageDexoptResults()).hasSize(1);
469         checkPackageResult(
470                 result, 0 /* index */, PKG_NAME_FOO, DexoptResult.DEXOPT_SKIPPED, List.of());
471 
472         verifyNoDexopt();
473     }
474 
475     @Test
testDexoptLibraryNotDexoptable()476     public void testDexoptLibraryNotDexoptable() throws Exception {
477         when(PackageStateModulesUtils.isDexoptable(mPkgStateLib1)).thenReturn(false);
478 
479         mRequestedPackages = List.of(PKG_NAME_FOO);
480         DexoptResult result = mDexoptHelper.dexopt(
481                 mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
482 
483         assertThat(result.getFinalStatus()).isEqualTo(DexoptResult.DEXOPT_PERFORMED);
484         assertThat(result.getPackageDexoptResults()).hasSize(1);
485         checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, DexoptResult.DEXOPT_PERFORMED,
486                 List.of(mPrimaryResults, mSecondaryResults));
487 
488         verifyNoMoreDexopt(1 /* expectedPrimaryTimes */, 1 /* expectedSecondaryTimes */);
489     }
490 
491     @Test
testDexoptIsHibernating()492     public void testDexoptIsHibernating() throws Exception {
493         lenient().when(mAhm.isHibernatingGlobally(PKG_NAME_FOO)).thenReturn(true);
494 
495         mRequestedPackages = List.of(PKG_NAME_FOO);
496         DexoptResult result = mDexoptHelper.dexopt(
497                 mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
498 
499         assertThat(result.getFinalStatus()).isEqualTo(DexoptResult.DEXOPT_SKIPPED);
500         checkPackageResult(
501                 result, 0 /* index */, PKG_NAME_FOO, DexoptResult.DEXOPT_SKIPPED, List.of());
502 
503         verifyNoDexopt();
504     }
505 
506     @Test
testDexoptIsHibernatingButOatArtifactDeletionDisabled()507     public void testDexoptIsHibernatingButOatArtifactDeletionDisabled() throws Exception {
508         lenient().when(mAhm.isHibernatingGlobally(PKG_NAME_FOO)).thenReturn(true);
509         lenient().when(mAhm.isOatArtifactDeletionEnabled()).thenReturn(false);
510 
511         DexoptResult result = mDexoptHelper.dexopt(
512                 mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
513 
514         assertThat(result.getPackageDexoptResults()).hasSize(6);
515         checkPackageResult(result, 0 /* index */, PKG_NAME_FOO, DexoptResult.DEXOPT_PERFORMED,
516                 List.of(mPrimaryResults, mSecondaryResults));
517         checkPackageResult(result, 1 /* index */, PKG_NAME_BAR, DexoptResult.DEXOPT_PERFORMED,
518                 List.of(mPrimaryResults, mSecondaryResults));
519         checkPackageResult(result, 2 /* index */, PKG_NAME_LIBBAZ, DexoptResult.DEXOPT_PERFORMED,
520                 List.of(mPrimaryResults, mSecondaryResults));
521         checkPackageResult(result, 3 /* index */, PKG_NAME_LIB1, DexoptResult.DEXOPT_PERFORMED,
522                 List.of(mPrimaryResults, mSecondaryResults));
523         checkPackageResult(result, 4 /* index */, PKG_NAME_LIB2, DexoptResult.DEXOPT_PERFORMED,
524                 List.of(mPrimaryResults, mSecondaryResults));
525         checkPackageResult(result, 5 /* index */, PKG_NAME_LIB4, DexoptResult.DEXOPT_PERFORMED,
526                 List.of(mPrimaryResults, mSecondaryResults));
527     }
528 
529     @Test(expected = IllegalArgumentException.class)
testDexoptPackageNotFound()530     public void testDexoptPackageNotFound() throws Exception {
531         when(mSnapshot.getPackageState(any())).thenReturn(null);
532 
533         mDexoptHelper.dexopt(
534                 mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
535 
536         verifyNoDexopt();
537     }
538 
539     @Test(expected = IllegalArgumentException.class)
testDexoptNoPackage()540     public void testDexoptNoPackage() throws Exception {
541         lenient().when(mPkgStateFoo.getAndroidPackage()).thenReturn(null);
542 
543         mDexoptHelper.dexopt(
544                 mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
545 
546         verifyNoDexopt();
547     }
548 
549     @Test
testDexoptSplit()550     public void testDexoptSplit() throws Exception {
551         mRequestedPackages = List.of(PKG_NAME_FOO);
552         mParams = new DexoptParams.Builder("install")
553                           .setCompilerFilter("speed-profile")
554                           .setFlags(ArtFlags.FLAG_FOR_PRIMARY_DEX | ArtFlags.FLAG_FOR_SINGLE_SPLIT)
555                           .setSplitName("split_0")
556                           .build();
557 
558         mDexoptHelper.dexopt(
559                 mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
560     }
561 
562     @Test
testDexoptSplitNotFound()563     public void testDexoptSplitNotFound() throws Exception {
564         mRequestedPackages = List.of(PKG_NAME_FOO);
565         mParams = new DexoptParams.Builder("install")
566                           .setCompilerFilter("speed-profile")
567                           .setFlags(ArtFlags.FLAG_FOR_PRIMARY_DEX | ArtFlags.FLAG_FOR_SINGLE_SPLIT)
568                           .setSplitName("split_bogus")
569                           .build();
570 
571         assertThrows(IllegalArgumentException.class, () -> {
572             mDexoptHelper.dexopt(
573                     mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
574         });
575     }
576 
577     @Test
testCallbacks()578     public void testCallbacks() throws Exception {
579         List<DexoptResult> list1 = new ArrayList<>();
580         mConfig.addDexoptDoneCallback(
581                 false /* onlyIncludeUpdates */, Runnable::run, result -> list1.add(result));
582 
583         List<DexoptResult> list2 = new ArrayList<>();
584         mConfig.addDexoptDoneCallback(
585                 false /* onlyIncludeUpdates */, Runnable::run, result -> list2.add(result));
586 
587         DexoptResult result = mDexoptHelper.dexopt(
588                 mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
589 
590         assertThat(list1).containsExactly(result);
591         assertThat(list2).containsExactly(result);
592     }
593 
594     @Test
testCallbackRemoved()595     public void testCallbackRemoved() throws Exception {
596         List<DexoptResult> list1 = new ArrayList<>();
597         DexoptDoneCallback callback1 = result -> list1.add(result);
598         mConfig.addDexoptDoneCallback(false /* onlyIncludeUpdates */, Runnable::run, callback1);
599 
600         List<DexoptResult> list2 = new ArrayList<>();
601         mConfig.addDexoptDoneCallback(
602                 false /* onlyIncludeUpdates */, Runnable::run, result -> list2.add(result));
603 
604         mConfig.removeDexoptDoneCallback(callback1);
605 
606         DexoptResult result = mDexoptHelper.dexopt(
607                 mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor);
608 
609         assertThat(list1).isEmpty();
610         assertThat(list2).containsExactly(result);
611     }
612 
613     @Test(expected = IllegalStateException.class)
testCallbackAlreadyAdded()614     public void testCallbackAlreadyAdded() throws Exception {
615         List<DexoptResult> list = new ArrayList<>();
616         DexoptDoneCallback callback = result -> list.add(result);
617         mConfig.addDexoptDoneCallback(false /* onlyIncludeUpdates */, Runnable::run, callback);
618         mConfig.addDexoptDoneCallback(false /* onlyIncludeUpdates */, Runnable::run, callback);
619     }
620 
621     // Tests `addDexoptDoneCallback` with `onlyIncludeUpdates` being true and false.
622     @Test
testCallbackWithFailureResults()623     public void testCallbackWithFailureResults() throws Exception {
624         mParams = new DexoptParams.Builder("install")
625                           .setCompilerFilter("speed-profile")
626                           .setFlags(0,
627                                   ArtFlags.FLAG_FOR_SECONDARY_DEX
628                                           | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES)
629                           .build();
630 
631         // This list should collect all results.
632         List<DexoptResult> listAll = new ArrayList<>();
633         mConfig.addDexoptDoneCallback(
634                 false /* onlyIncludeUpdates */, Runnable::run, result -> listAll.add(result));
635 
636         // This list should only collect results that have updates.
637         List<DexoptResult> listOnlyIncludeUpdates = new ArrayList<>();
638         mConfig.addDexoptDoneCallback(true /* onlyIncludeUpdates */, Runnable::run,
639                 result -> listOnlyIncludeUpdates.add(result));
640 
641         // Dexopt partially fails on package "foo".
642         List<DexContainerFileDexoptResult> partialFailureResults = createResults(
643                 "/somewhere/app/foo/base.apk", DexoptResult.DEXOPT_PERFORMED /* status1 */,
644                 DexoptResult.DEXOPT_FAILED /* status2 */);
645         var fooPrimaryDexopter = mock(PrimaryDexopter.class);
646         when(mInjector.getPrimaryDexopter(same(mPkgStateFoo), any(), any(), any()))
647                 .thenReturn(fooPrimaryDexopter);
648         when(fooPrimaryDexopter.dexopt()).thenReturn(partialFailureResults);
649 
650         // Dexopt totally fails on package "bar".
651         List<DexContainerFileDexoptResult> totalFailureResults = createResults(
652                 "/somewhere/app/bar/base.apk", DexoptResult.DEXOPT_FAILED /* status1 */,
653                 DexoptResult.DEXOPT_FAILED /* status2 */);
654         var barPrimaryDexopter = mock(PrimaryDexopter.class);
655         when(mInjector.getPrimaryDexopter(same(mPkgStateBar), any(), any(), any()))
656                 .thenReturn(barPrimaryDexopter);
657         when(barPrimaryDexopter.dexopt()).thenReturn(totalFailureResults);
658 
659         DexoptResult resultWithSomeUpdates = mDexoptHelper.dexopt(mSnapshot,
660                 List.of(PKG_NAME_FOO, PKG_NAME_BAR), mParams, mCancellationSignal, mExecutor);
661         DexoptResult resultWithNoUpdates = mDexoptHelper.dexopt(
662                 mSnapshot, List.of(PKG_NAME_BAR), mParams, mCancellationSignal, mExecutor);
663 
664         assertThat(listAll).containsExactly(resultWithSomeUpdates, resultWithNoUpdates);
665 
666         assertThat(listOnlyIncludeUpdates).hasSize(1);
667         assertThat(listOnlyIncludeUpdates.get(0)
668                            .getPackageDexoptResults()
669                            .stream()
670                            .map(PackageDexoptResult::getPackageName)
671                            .toList())
672                 .containsExactly(PKG_NAME_FOO);
673     }
674 
675     @Test
testProgressCallback()676     public void testProgressCallback() throws Exception {
677         mParams = new DexoptParams.Builder("install")
678                           .setCompilerFilter("speed-profile")
679                           .setFlags(ArtFlags.FLAG_FOR_SECONDARY_DEX,
680                                   ArtFlags.FLAG_FOR_SECONDARY_DEX
681                                           | ArtFlags.FLAG_SHOULD_INCLUDE_DEPENDENCIES)
682                           .build();
683 
684         // Delay the executor to verify that the commands passed to the executor are not bound to
685         // changing variables.
686         var progressCallbackExecutor = new DelayedExecutor();
687         Consumer<OperationProgress> progressCallback = mock(Consumer.class);
688 
689         mDexoptHelper.dexopt(mSnapshot, mRequestedPackages, mParams, mCancellationSignal, mExecutor,
690                 progressCallbackExecutor, progressCallback);
691 
692         progressCallbackExecutor.runAll();
693 
694         List<DexContainerFileDexoptResult> fileResults =
695                 Stream.concat(mPrimaryResults.stream(), mSecondaryResults.stream()).toList();
696 
697         InOrder inOrder = inOrder(progressCallback);
698         inOrder.verify(progressCallback)
699                 .accept(eq(OperationProgress.create(
700                         0 /* current */, 3 /* total */, null /* packageDexoptResult */)));
701         inOrder.verify(progressCallback)
702                 .accept(eq(OperationProgress.create(1 /* current */, 3 /* total */,
703                         PackageDexoptResult.create(
704                                 PKG_NAME_FOO, fileResults, null /* packageLevelStatus */))));
705         inOrder.verify(progressCallback)
706                 .accept(eq(OperationProgress.create(2 /* current */, 3 /* total */,
707                         PackageDexoptResult.create(
708                                 PKG_NAME_BAR, fileResults, null /* packageLevelStatus */))));
709         inOrder.verify(progressCallback)
710                 .accept(eq(OperationProgress.create(3 /* current */, 3 /* total */,
711                         PackageDexoptResult.create(
712                                 PKG_NAME_LIBBAZ, fileResults, null /* packageLevelStatus */))));
713     }
714 
createPackage(boolean multiSplit)715     private AndroidPackage createPackage(boolean multiSplit) {
716         AndroidPackage pkg = mock(AndroidPackage.class);
717 
718         var baseSplit = mock(AndroidPackageSplit.class);
719 
720         if (multiSplit) {
721             var split0 = mock(AndroidPackageSplit.class);
722             lenient().when(split0.getName()).thenReturn("split_0");
723 
724             lenient().when(pkg.getSplits()).thenReturn(List.of(baseSplit, split0));
725         } else {
726             lenient().when(pkg.getSplits()).thenReturn(List.of(baseSplit));
727         }
728 
729         return pkg;
730     }
731 
createPackageState( String packageName, List<SharedLibrary> deps, boolean multiSplit)732     private PackageState createPackageState(
733             String packageName, List<SharedLibrary> deps, boolean multiSplit) {
734         return createPackageState(packageName, 12345, deps, multiSplit);
735     }
736 
createPackageState( String packageName, int appId, List<SharedLibrary> deps, boolean multiSplit)737     private PackageState createPackageState(
738             String packageName, int appId, List<SharedLibrary> deps, boolean multiSplit) {
739         PackageState pkgState = mock(PackageState.class);
740         lenient().when(pkgState.getPackageName()).thenReturn(packageName);
741         lenient().when(pkgState.getAppId()).thenReturn(appId);
742         lenient().when(pkgState.getSharedLibraryDependencies()).thenReturn(deps);
743         AndroidPackage pkg = createPackage(multiSplit);
744         lenient().when(pkgState.getAndroidPackage()).thenReturn(pkg);
745         lenient().when(PackageStateModulesUtils.isDexoptable(pkgState)).thenReturn(true);
746         return pkgState;
747     }
748 
createLibrary( String libraryName, String packageName, List<SharedLibrary> deps)749     private SharedLibrary createLibrary(
750             String libraryName, String packageName, List<SharedLibrary> deps) {
751         SharedLibrary library = mock(SharedLibrary.class);
752         lenient().when(library.getName()).thenReturn(libraryName);
753         lenient().when(library.getPackageName()).thenReturn(packageName);
754         lenient().when(library.getDependencies()).thenReturn(deps);
755         lenient().when(library.isNative()).thenReturn(false);
756         return library;
757     }
758 
preparePackagesAndLibraries()759     private void preparePackagesAndLibraries() {
760         // Dependency graph:
761         //                foo                bar
762         //                 |                  |
763         //            lib1a (lib1)       lib1b (lib1)       lib1c (lib1)
764         //               /   \             /   \                  |
765         //              /     \           /     \                 |
766         //  libbaz (libbaz)    lib2 (lib2)    lib4 (lib4)    lib3 (lib3)
767         //
768         // "lib1a", "lib1b", and "lib1c" belong to the same package "lib1".
769 
770         mRequestedPackages = List.of(PKG_NAME_FOO, PKG_NAME_BAR, PKG_NAME_LIBBAZ);
771 
772         // The native library is not dexoptable.
773         SharedLibrary libNative = createLibrary("libnative", "com.example.libnative", List.of());
774         lenient().when(libNative.isNative()).thenReturn(true);
775 
776         SharedLibrary libbaz = createLibrary("libbaz", PKG_NAME_LIBBAZ, List.of());
777         SharedLibrary lib4 = createLibrary("lib4", PKG_NAME_LIB4, List.of());
778         SharedLibrary lib3 = createLibrary("lib3", PKG_NAME_LIB3, List.of());
779         SharedLibrary lib2 = createLibrary("lib2", PKG_NAME_LIB2, List.of());
780         SharedLibrary lib1a = createLibrary("lib1a", PKG_NAME_LIB1, List.of(libbaz, lib2));
781         SharedLibrary lib1b = createLibrary("lib1b", PKG_NAME_LIB1, List.of(lib2, libNative, lib4));
782         SharedLibrary lib1c = createLibrary("lib1c", PKG_NAME_LIB1, List.of(lib3));
783 
784         mPkgStateFoo =
785                 createPackageState(PKG_NAME_FOO, List.of(lib1a, libNative), true /* multiSplit */);
786         mPkgFoo = mPkgStateFoo.getAndroidPackage();
787         lenient().when(mSnapshot.getPackageState(PKG_NAME_FOO)).thenReturn(mPkgStateFoo);
788 
789         mPkgStateBar = createPackageState(PKG_NAME_BAR, List.of(lib1b), false /* multiSplit */);
790         mPkgBar = mPkgStateBar.getAndroidPackage();
791         lenient().when(mSnapshot.getPackageState(PKG_NAME_BAR)).thenReturn(mPkgStateBar);
792 
793         mPkgStateLib1 = createPackageState(
794                 PKG_NAME_LIB1, List.of(libbaz, lib2, lib3, lib4), false /* multiSplit */);
795         mPkgLib1 = mPkgStateLib1.getAndroidPackage();
796         lenient().when(mSnapshot.getPackageState(PKG_NAME_LIB1)).thenReturn(mPkgStateLib1);
797 
798         mPkgStateLib2 = createPackageState(PKG_NAME_LIB2, List.of(), false /* multiSplit */);
799         mPkgLib2 = mPkgStateLib2.getAndroidPackage();
800         lenient().when(mSnapshot.getPackageState(PKG_NAME_LIB2)).thenReturn(mPkgStateLib2);
801 
802         // This should not be considered as a transitive dependency of any requested package, even
803         // though it is a dependency of package "lib1".
804         PackageState pkgStateLib3 =
805                 createPackageState(PKG_NAME_LIB3, List.of(), false /* multiSplit */);
806         lenient().when(mSnapshot.getPackageState(PKG_NAME_LIB3)).thenReturn(pkgStateLib3);
807 
808         mPkgStateLib4 = createPackageState(PKG_NAME_LIB4, List.of(), false /* multiSplit */);
809         mPkgLib4 = mPkgStateLib4.getAndroidPackage();
810         lenient().when(mSnapshot.getPackageState(PKG_NAME_LIB4)).thenReturn(mPkgStateLib4);
811 
812         mPkgStateLibbaz = createPackageState(PKG_NAME_LIBBAZ, List.of(), false /* multiSplit */);
813         mPkgLibbaz = mPkgStateLibbaz.getAndroidPackage();
814         lenient().when(mSnapshot.getPackageState(PKG_NAME_LIBBAZ)).thenReturn(mPkgStateLibbaz);
815     }
816 
verifyNoDexopt()817     private void verifyNoDexopt() {
818         verify(mInjector, never()).getPrimaryDexopter(any(), any(), any(), any());
819         verify(mInjector, never()).getSecondaryDexopter(any(), any(), any(), any());
820     }
821 
verifyNoMoreDexopt(int expectedPrimaryTimes, int expectedSecondaryTimes)822     private void verifyNoMoreDexopt(int expectedPrimaryTimes, int expectedSecondaryTimes) {
823         verify(mInjector, times(expectedPrimaryTimes))
824                 .getPrimaryDexopter(any(), any(), any(), any());
825         verify(mInjector, times(expectedSecondaryTimes))
826                 .getSecondaryDexopter(any(), any(), any(), any());
827     }
828 
createResults( String dexPath, @DexoptResultStatus int status1, @DexoptResultStatus int status2)829     private List<DexContainerFileDexoptResult> createResults(
830             String dexPath, @DexoptResultStatus int status1, @DexoptResultStatus int status2) {
831         return List.of(DexContainerFileDexoptResult.create(
832                                dexPath, true /* isPrimaryAbi */, "arm64-v8a", "verify", status1),
833                 DexContainerFileDexoptResult.create(
834                         dexPath, false /* isPrimaryAbi */, "armeabi-v7a", "verify", status2));
835     }
836 
checkPackageResult(DexoptResult result, int index, String packageName, @DexoptResult.DexoptResultStatus int status, List<List<DexContainerFileDexoptResult>> dexContainerFileDexoptResults)837     private void checkPackageResult(DexoptResult result, int index, String packageName,
838             @DexoptResult.DexoptResultStatus int status,
839             List<List<DexContainerFileDexoptResult>> dexContainerFileDexoptResults) {
840         PackageDexoptResult packageResult = result.getPackageDexoptResults().get(index);
841         assertThat(packageResult.getPackageName()).isEqualTo(packageName);
842         assertThat(packageResult.getStatus()).isEqualTo(status);
843         assertThat(packageResult.getDexContainerFileDexoptResults())
844                 .containsExactlyElementsIn(
845                         dexContainerFileDexoptResults.stream().flatMap(r -> r.stream()).toList());
846     }
847 
848     /** An executor that delays execution until `runAll` is called. */
849     private static class DelayedExecutor implements Executor {
850         private List<Runnable> mCommands = new ArrayList<>();
851 
execute(Runnable command)852         public void execute(Runnable command) {
853             mCommands.add(command);
854         }
855 
runAll()856         public void runAll() {
857             for (Runnable command : mCommands) {
858                 command.run();
859             }
860             mCommands.clear();
861         }
862     }
863 }
864