| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.pm; |
| |
| import static com.android.server.pm.BackgroundDexOptService.STATUS_DEX_OPT_FAILED; |
| import static com.android.server.pm.BackgroundDexOptService.STATUS_OK; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import static org.mockito.ArgumentMatchers.any; |
| import static org.mockito.ArgumentMatchers.argThat; |
| import static org.mockito.Mockito.atLeastOnce; |
| import static org.mockito.Mockito.inOrder; |
| import static org.mockito.Mockito.never; |
| import static org.mockito.Mockito.reset; |
| import static org.mockito.Mockito.timeout; |
| import static org.mockito.Mockito.times; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.when; |
| import static org.testng.Assert.assertThrows; |
| |
| import android.annotation.Nullable; |
| import android.app.job.JobInfo; |
| import android.app.job.JobParameters; |
| import android.app.job.JobScheduler; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.os.HandlerThread; |
| import android.os.PowerManager; |
| import android.os.Process; |
| import android.util.Log; |
| |
| import com.android.internal.util.IndentingPrintWriter; |
| import com.android.server.LocalServices; |
| import com.android.server.PinnerService; |
| import com.android.server.pm.dex.DexManager; |
| import com.android.server.pm.dex.DexoptOptions; |
| |
| import org.junit.After; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.ArgumentCaptor; |
| import org.mockito.InOrder; |
| import org.mockito.Mock; |
| import org.mockito.junit.MockitoJUnitRunner; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.stream.Collectors; |
| |
| @RunWith(MockitoJUnitRunner.class) |
| public final class BackgroundDexOptServiceUnitTest { |
| private static final String TAG = BackgroundDexOptServiceUnitTest.class.getSimpleName(); |
| |
| private static final long USABLE_SPACE_NORMAL = 1_000_000_000; |
| private static final long STORAGE_LOW_BYTES = 1_000_000; |
| |
| private static final long TEST_WAIT_TIMEOUT_MS = 10_000; |
| |
| private static final String PACKAGE_AAA = "aaa"; |
| private static final List<String> DEFAULT_PACKAGE_LIST = List.of(PACKAGE_AAA, "bbb"); |
| private int mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_PERFORMED; |
| |
| // Store expected dexopt sequence for verification. |
| private ArrayList<DexOptInfo> mDexInfoSequence = new ArrayList<>(); |
| |
| @Mock |
| private Context mContext; |
| @Mock |
| private PackageManagerService mPackageManager; |
| @Mock |
| private DexOptHelper mDexOptHelper; |
| @Mock |
| private DexManager mDexManager; |
| @Mock |
| private PinnerService mPinnerService; |
| @Mock |
| private JobScheduler mJobScheduler; |
| @Mock |
| private BackgroundDexOptService.Injector mInjector; |
| @Mock |
| private BackgroundDexOptJobService mJobServiceForPostBoot; |
| @Mock |
| private BackgroundDexOptJobService mJobServiceForIdle; |
| |
| private final JobParameters mJobParametersForPostBoot = new JobParameters(null, |
| BackgroundDexOptService.JOB_POST_BOOT_UPDATE, null, null, null, |
| 0, false, false, null, null, null); |
| private final JobParameters mJobParametersForIdle = new JobParameters(null, |
| BackgroundDexOptService.JOB_IDLE_OPTIMIZE, null, null, null, |
| 0, false, false, null, null, null); |
| |
| private BackgroundDexOptService mService; |
| |
| private StartAndWaitThread mDexOptThread; |
| private StartAndWaitThread mCancelThread; |
| |
| @Before |
| public void setUp() throws Exception { |
| when(mInjector.getCallingUid()).thenReturn(Process.FIRST_APPLICATION_UID); |
| when(mInjector.getContext()).thenReturn(mContext); |
| when(mInjector.getDexOptHelper()).thenReturn(mDexOptHelper); |
| when(mInjector.getDexManager()).thenReturn(mDexManager); |
| when(mInjector.getPinnerService()).thenReturn(mPinnerService); |
| when(mInjector.getJobScheduler()).thenReturn(mJobScheduler); |
| when(mInjector.getPackageManagerService()).thenReturn(mPackageManager); |
| |
| // These mocking can be overwritten in some tests but still keep it here as alternative |
| // takes too many repetitive codes. |
| when(mInjector.getDataDirUsableSpace()).thenReturn(USABLE_SPACE_NORMAL); |
| when(mInjector.getDataDirStorageLowBytes()).thenReturn(STORAGE_LOW_BYTES); |
| when(mInjector.getDexOptThermalCutoff()).thenReturn(PowerManager.THERMAL_STATUS_CRITICAL); |
| when(mInjector.getCurrentThermalStatus()).thenReturn(PowerManager.THERMAL_STATUS_NONE); |
| when(mInjector.supportSecondaryDex()).thenReturn(true); |
| setupDexOptHelper(); |
| |
| mService = new BackgroundDexOptService(mInjector); |
| } |
| |
| private void setupDexOptHelper() { |
| when(mDexOptHelper.getOptimizablePackages(any())).thenReturn(DEFAULT_PACKAGE_LIST); |
| when(mDexOptHelper.performDexOptWithStatus(any())).thenAnswer(inv -> { |
| DexoptOptions opt = inv.getArgument(0); |
| if (opt.getPackageName().equals(PACKAGE_AAA)) { |
| return mDexOptResultForPackageAAA; |
| } |
| return PackageDexOptimizer.DEX_OPT_PERFORMED; |
| }); |
| when(mDexOptHelper.performDexOpt(any())).thenReturn(true); |
| } |
| |
| @After |
| public void tearDown() throws Exception { |
| LocalServices.removeServiceForTest(BackgroundDexOptService.class); |
| } |
| |
| @Test |
| public void testGetService() { |
| assertThat(BackgroundDexOptService.getService()).isEqualTo(mService); |
| } |
| |
| @Test |
| public void testBootCompleted() { |
| initUntilBootCompleted(); |
| } |
| |
| @Test |
| public void testNoExecutionForIdleJobBeforePostBootUpdate() { |
| initUntilBootCompleted(); |
| |
| assertThat(mService.onStartJob(mJobServiceForIdle, mJobParametersForIdle)).isFalse(); |
| } |
| |
| @Test |
| public void testNoExecutionForLowStorage() { |
| initUntilBootCompleted(); |
| when(mPackageManager.isStorageLow()).thenReturn(true); |
| |
| assertThat(mService.onStartJob(mJobServiceForPostBoot, |
| mJobParametersForPostBoot)).isFalse(); |
| verify(mDexOptHelper, never()).performDexOpt(any()); |
| } |
| |
| @Test |
| public void testNoExecutionForNoOptimizablePackages() { |
| initUntilBootCompleted(); |
| when(mDexOptHelper.getOptimizablePackages(any())).thenReturn(Collections.emptyList()); |
| |
| assertThat(mService.onStartJob(mJobServiceForPostBoot, |
| mJobParametersForPostBoot)).isFalse(); |
| verify(mDexOptHelper, never()).performDexOpt(any()); |
| } |
| |
| @Test |
| public void testPostBootUpdateFullRun() { |
| initUntilBootCompleted(); |
| |
| runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, |
| /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK, |
| /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null); |
| } |
| |
| @Test |
| public void testPostBootUpdateFullRunWithPackageFailure() { |
| mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_FAILED; |
| |
| initUntilBootCompleted(); |
| |
| runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, |
| /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_DEX_OPT_FAILED, |
| /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA); |
| |
| assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA); |
| assertThat(getFailedPackageNamesSecondary()).isEmpty(); |
| } |
| |
| @Test |
| public void testIdleJobFullRun() { |
| initUntilBootCompleted(); |
| runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, |
| /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_OK, |
| /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null); |
| runFullJob(mJobServiceForIdle, mJobParametersForIdle, |
| /* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK, |
| /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ null); |
| } |
| |
| @Test |
| public void testIdleJobFullRunWithFailureOnceAndSuccessAfterUpdate() { |
| mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_FAILED; |
| |
| initUntilBootCompleted(); |
| |
| runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, |
| /* expectedReschedule= */ false, /* expectedStatus= */ STATUS_DEX_OPT_FAILED, |
| /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA); |
| |
| assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA); |
| assertThat(getFailedPackageNamesSecondary()).isEmpty(); |
| |
| runFullJob(mJobServiceForIdle, mJobParametersForIdle, |
| /* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK, |
| /* totalJobFinishedWithParams= */ 1, /* expectedSkippedPackage= */ PACKAGE_AAA); |
| |
| assertThat(getFailedPackageNamesPrimary()).containsExactly(PACKAGE_AAA); |
| assertThat(getFailedPackageNamesSecondary()).isEmpty(); |
| |
| mService.notifyPackageChanged(PACKAGE_AAA); |
| |
| assertThat(getFailedPackageNamesPrimary()).isEmpty(); |
| assertThat(getFailedPackageNamesSecondary()).isEmpty(); |
| |
| // Succeed this time. |
| mDexOptResultForPackageAAA = PackageDexOptimizer.DEX_OPT_PERFORMED; |
| |
| runFullJob(mJobServiceForIdle, mJobParametersForIdle, |
| /* expectedReschedule= */ true, /* expectedStatus= */ STATUS_OK, |
| /* totalJobFinishedWithParams= */ 2, /* expectedSkippedPackage= */ null); |
| |
| assertThat(getFailedPackageNamesPrimary()).isEmpty(); |
| assertThat(getFailedPackageNamesSecondary()).isEmpty(); |
| } |
| |
| @Test |
| public void testSystemReadyWhenDisabled() { |
| when(mInjector.isBackgroundDexOptDisabled()).thenReturn(true); |
| |
| mService.systemReady(); |
| |
| verify(mContext, never()).registerReceiver(any(), any()); |
| } |
| |
| @Test |
| public void testStopByCancelFlag() { |
| when(mInjector.createAndStartThread(any(), any())).thenReturn(Thread.currentThread()); |
| initUntilBootCompleted(); |
| |
| assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue(); |
| |
| ArgumentCaptor<Runnable> argDexOptThreadRunnable = ArgumentCaptor.forClass(Runnable.class); |
| verify(mInjector, atLeastOnce()).createAndStartThread(any(), |
| argDexOptThreadRunnable.capture()); |
| |
| // Stopping requires a separate thread |
| HandlerThread cancelThread = new HandlerThread("Stopping"); |
| cancelThread.start(); |
| when(mInjector.createAndStartThread(any(), any())).thenReturn(cancelThread); |
| |
| // Cancel |
| assertThat(mService.onStopJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue(); |
| |
| // Capture Runnable for cancel |
| ArgumentCaptor<Runnable> argCancelThreadRunnable = ArgumentCaptor.forClass(Runnable.class); |
| verify(mInjector, atLeastOnce()).createAndStartThread(any(), |
| argCancelThreadRunnable.capture()); |
| |
| // Execute cancelling part |
| cancelThread.getThreadHandler().post(argCancelThreadRunnable.getValue()); |
| |
| verify(mDexOptHelper, timeout(TEST_WAIT_TIMEOUT_MS)).controlDexOptBlocking(true); |
| |
| // Dexopt thread run and cancelled |
| argDexOptThreadRunnable.getValue().run(); |
| |
| // Wait until cancellation Runnable is completed. |
| assertThat(cancelThread.getThreadHandler().runWithScissors( |
| argCancelThreadRunnable.getValue(), TEST_WAIT_TIMEOUT_MS)).isTrue(); |
| |
| // Now cancel completed |
| verify(mJobServiceForPostBoot).jobFinished(mJobParametersForPostBoot, true); |
| verifyLastControlDexOptBlockingCall(false); |
| } |
| |
| @Test |
| public void testPostUpdateCancelFirst() throws Exception { |
| initUntilBootCompleted(); |
| when(mInjector.createAndStartThread(any(), any())).thenAnswer( |
| i -> createAndStartExecutionThread(i.getArgument(0), i.getArgument(1))); |
| |
| // Start |
| assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue(); |
| // Cancel |
| assertThat(mService.onStopJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue(); |
| |
| mCancelThread.runActualRunnable(); |
| |
| // Wait until cancel has set the flag. |
| verify(mDexOptHelper, timeout(TEST_WAIT_TIMEOUT_MS)).controlDexOptBlocking( |
| true); |
| |
| mDexOptThread.runActualRunnable(); |
| |
| // All threads should finish. |
| mDexOptThread.join(TEST_WAIT_TIMEOUT_MS); |
| mCancelThread.join(TEST_WAIT_TIMEOUT_MS); |
| |
| // Retry later if post boot job was cancelled |
| verify(mJobServiceForPostBoot).jobFinished(mJobParametersForPostBoot, true); |
| verifyLastControlDexOptBlockingCall(false); |
| } |
| |
| @Test |
| public void testPostUpdateCancelLater() throws Exception { |
| initUntilBootCompleted(); |
| when(mInjector.createAndStartThread(any(), any())).thenAnswer( |
| i -> createAndStartExecutionThread(i.getArgument(0), i.getArgument(1))); |
| |
| // Start |
| assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue(); |
| // Cancel |
| assertThat(mService.onStopJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue(); |
| |
| // Dexopt thread runs and finishes |
| mDexOptThread.runActualRunnable(); |
| mDexOptThread.join(TEST_WAIT_TIMEOUT_MS); |
| |
| mCancelThread.runActualRunnable(); |
| mCancelThread.join(TEST_WAIT_TIMEOUT_MS); |
| |
| // Already completed before cancel, so no rescheduling. |
| verify(mJobServiceForPostBoot).jobFinished(mJobParametersForPostBoot, false); |
| verify(mDexOptHelper, never()).controlDexOptBlocking(true); |
| } |
| |
| @Test |
| public void testPeriodicJobCancelFirst() throws Exception { |
| initUntilBootCompleted(); |
| when(mInjector.createAndStartThread(any(), any())).thenAnswer( |
| i -> createAndStartExecutionThread(i.getArgument(0), i.getArgument(1))); |
| |
| // Start and finish post boot job |
| assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue(); |
| mDexOptThread.runActualRunnable(); |
| mDexOptThread.join(TEST_WAIT_TIMEOUT_MS); |
| |
| // Start |
| assertThat(mService.onStartJob(mJobServiceForIdle, mJobParametersForIdle)).isTrue(); |
| // Cancel |
| assertThat(mService.onStopJob(mJobServiceForIdle, mJobParametersForIdle)).isTrue(); |
| |
| mCancelThread.runActualRunnable(); |
| |
| // Wait until cancel has set the flag. |
| verify(mDexOptHelper, timeout(TEST_WAIT_TIMEOUT_MS)).controlDexOptBlocking( |
| true); |
| |
| mDexOptThread.runActualRunnable(); |
| |
| // All threads should finish. |
| mDexOptThread.join(TEST_WAIT_TIMEOUT_MS); |
| mCancelThread.join(TEST_WAIT_TIMEOUT_MS); |
| |
| // Always reschedule for periodic job |
| verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, true); |
| verifyLastControlDexOptBlockingCall(false); |
| } |
| |
| @Test |
| public void testPeriodicJobCancelLater() throws Exception { |
| initUntilBootCompleted(); |
| when(mInjector.createAndStartThread(any(), any())).thenAnswer( |
| i -> createAndStartExecutionThread(i.getArgument(0), i.getArgument(1))); |
| |
| // Start and finish post boot job |
| assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue(); |
| mDexOptThread.runActualRunnable(); |
| mDexOptThread.join(TEST_WAIT_TIMEOUT_MS); |
| |
| // Start |
| assertThat(mService.onStartJob(mJobServiceForIdle, mJobParametersForIdle)).isTrue(); |
| // Cancel |
| assertThat(mService.onStopJob(mJobServiceForIdle, mJobParametersForIdle)).isTrue(); |
| |
| // Dexopt thread finishes first. |
| mDexOptThread.runActualRunnable(); |
| mDexOptThread.join(TEST_WAIT_TIMEOUT_MS); |
| |
| mCancelThread.runActualRunnable(); |
| mCancelThread.join(TEST_WAIT_TIMEOUT_MS); |
| |
| // Always reschedule for periodic job |
| verify(mJobServiceForIdle).jobFinished(mJobParametersForIdle, true); |
| verify(mDexOptHelper, never()).controlDexOptBlocking(true); |
| } |
| |
| @Test |
| public void testStopByThermal() { |
| when(mInjector.createAndStartThread(any(), any())).thenReturn(Thread.currentThread()); |
| initUntilBootCompleted(); |
| |
| assertThat(mService.onStartJob(mJobServiceForPostBoot, mJobParametersForPostBoot)).isTrue(); |
| |
| ArgumentCaptor<Runnable> argThreadRunnable = ArgumentCaptor.forClass(Runnable.class); |
| verify(mInjector, atLeastOnce()).createAndStartThread(any(), argThreadRunnable.capture()); |
| |
| // Thermal cancel level |
| when(mInjector.getCurrentThermalStatus()).thenReturn(PowerManager.THERMAL_STATUS_CRITICAL); |
| |
| argThreadRunnable.getValue().run(); |
| |
| verify(mJobServiceForPostBoot).jobFinished(mJobParametersForPostBoot, true); |
| verifyLastControlDexOptBlockingCall(false); |
| } |
| |
| @Test |
| public void testRunShellCommandWithInvalidUid() { |
| // Test uid cannot execute the command APIs |
| assertThrows(SecurityException.class, () -> mService.runBackgroundDexoptJob(null)); |
| } |
| |
| @Test |
| public void testCancelShellCommandWithInvalidUid() { |
| // Test uid cannot execute the command APIs |
| assertThrows(SecurityException.class, () -> mService.cancelBackgroundDexoptJob()); |
| } |
| |
| @Test |
| public void testDisableJobSchedulerJobs() { |
| when(mInjector.getCallingUid()).thenReturn(Process.SHELL_UID); |
| mService.setDisableJobSchedulerJobs(true); |
| assertThat(mService.onStartJob(mJobServiceForIdle, mJobParametersForIdle)).isFalse(); |
| verify(mDexOptHelper, never()).performDexOpt(any()); |
| verify(mDexOptHelper, never()).performDexOptWithStatus(any()); |
| } |
| |
| @Test |
| public void testSetDisableJobSchedulerJobsWithInvalidUid() { |
| // Test uid cannot execute the command APIs |
| assertThrows(SecurityException.class, () -> mService.setDisableJobSchedulerJobs(true)); |
| } |
| |
| private void initUntilBootCompleted() { |
| ArgumentCaptor<BroadcastReceiver> argReceiver = ArgumentCaptor.forClass( |
| BroadcastReceiver.class); |
| ArgumentCaptor<IntentFilter> argIntentFilter = ArgumentCaptor.forClass(IntentFilter.class); |
| |
| mService.systemReady(); |
| |
| verify(mContext).registerReceiver(argReceiver.capture(), argIntentFilter.capture()); |
| assertThat(argIntentFilter.getValue().getAction(0)).isEqualTo(Intent.ACTION_BOOT_COMPLETED); |
| |
| argReceiver.getValue().onReceive(mContext, null); |
| |
| verify(mContext).unregisterReceiver(argReceiver.getValue()); |
| ArgumentCaptor<JobInfo> argJobs = ArgumentCaptor.forClass(JobInfo.class); |
| verify(mJobScheduler, times(2)).schedule(argJobs.capture()); |
| |
| List<Integer> expectedJobIds = Arrays.asList(BackgroundDexOptService.JOB_IDLE_OPTIMIZE, |
| BackgroundDexOptService.JOB_POST_BOOT_UPDATE); |
| List<Integer> jobIds = argJobs.getAllValues().stream().map(job -> job.getId()).collect( |
| Collectors.toList()); |
| assertThat(jobIds).containsExactlyElementsIn(expectedJobIds); |
| } |
| |
| private void verifyLastControlDexOptBlockingCall(boolean expected) { |
| ArgumentCaptor<Boolean> argDexOptBlock = ArgumentCaptor.forClass(Boolean.class); |
| verify(mDexOptHelper, atLeastOnce()).controlDexOptBlocking(argDexOptBlock.capture()); |
| assertThat(argDexOptBlock.getValue()).isEqualTo(expected); |
| } |
| |
| private void runFullJob(BackgroundDexOptJobService jobService, JobParameters params, |
| boolean expectedReschedule, int expectedStatus, int totalJobFinishedWithParams, |
| @Nullable String expectedSkippedPackage) { |
| when(mInjector.createAndStartThread(any(), any())).thenReturn(Thread.currentThread()); |
| addFullRunSequence(expectedSkippedPackage); |
| assertThat(mService.onStartJob(jobService, params)).isTrue(); |
| |
| ArgumentCaptor<Runnable> argThreadRunnable = ArgumentCaptor.forClass(Runnable.class); |
| verify(mInjector, atLeastOnce()).createAndStartThread(any(), argThreadRunnable.capture()); |
| |
| argThreadRunnable.getValue().run(); |
| |
| verify(jobService, times(totalJobFinishedWithParams)).jobFinished(params, |
| expectedReschedule); |
| // Never block |
| verify(mDexOptHelper, never()).controlDexOptBlocking(true); |
| verifyPerformDexOpt(); |
| assertThat(getLastExecutionStatus()).isEqualTo(expectedStatus); |
| } |
| |
| private void verifyPerformDexOpt() { |
| InOrder inOrder = inOrder(mDexOptHelper); |
| inOrder.verify(mDexOptHelper).getOptimizablePackages(any()); |
| for (DexOptInfo info : mDexInfoSequence) { |
| if (info.isPrimary) { |
| verify(mDexOptHelper).performDexOptWithStatus( |
| argThat((option) -> option.getPackageName().equals(info.packageName) |
| && !option.isDexoptOnlySecondaryDex())); |
| } else { |
| inOrder.verify(mDexOptHelper).performDexOpt( |
| argThat((option) -> option.getPackageName().equals(info.packageName) |
| && option.isDexoptOnlySecondaryDex())); |
| } |
| } |
| |
| // Even InOrder cannot check the order if the same call is made multiple times. |
| // To check the order across multiple runs, we reset the mock so that order can be checked |
| // in each call. |
| mDexInfoSequence.clear(); |
| reset(mDexOptHelper); |
| setupDexOptHelper(); |
| } |
| |
| private String findDumpValueForKey(String key) { |
| ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| PrintWriter pw = new PrintWriter(out, true); |
| IndentingPrintWriter writer = new IndentingPrintWriter(pw, ""); |
| try { |
| mService.dump(writer); |
| writer.flush(); |
| Log.i(TAG, "dump output:" + out.toString()); |
| for (String line : out.toString().split(System.lineSeparator())) { |
| String[] vals = line.split(":"); |
| if (vals[0].equals(key)) { |
| if (vals.length == 2) { |
| return vals[1].strip(); |
| } else { |
| break; |
| } |
| } |
| } |
| return ""; |
| } finally { |
| writer.close(); |
| } |
| } |
| |
| List<String> findStringListFromDump(String key) { |
| String values = findDumpValueForKey(key); |
| if (values.isEmpty()) { |
| return Collections.emptyList(); |
| } |
| return Arrays.asList(values.split(",")); |
| } |
| |
| private List<String> getFailedPackageNamesPrimary() { |
| return findStringListFromDump("mFailedPackageNamesPrimary"); |
| } |
| |
| private List<String> getFailedPackageNamesSecondary() { |
| return findStringListFromDump("mFailedPackageNamesSecondary"); |
| } |
| |
| private int getLastExecutionStatus() { |
| return Integer.parseInt(findDumpValueForKey("mLastExecutionStatus")); |
| } |
| |
| private static class DexOptInfo { |
| public final String packageName; |
| public final boolean isPrimary; |
| |
| private DexOptInfo(String packageName, boolean isPrimary) { |
| this.packageName = packageName; |
| this.isPrimary = isPrimary; |
| } |
| } |
| |
| private void addFullRunSequence(@Nullable String expectedSkippedPackage) { |
| for (String packageName : DEFAULT_PACKAGE_LIST) { |
| if (packageName.equals(expectedSkippedPackage)) { |
| // only fails primary dexopt in mocking but add secodary |
| mDexInfoSequence.add(new DexOptInfo(packageName, /* isPrimary= */ false)); |
| } else { |
| mDexInfoSequence.add(new DexOptInfo(packageName, /* isPrimary= */ true)); |
| mDexInfoSequence.add(new DexOptInfo(packageName, /* isPrimary= */ false)); |
| } |
| } |
| } |
| |
| private static class StartAndWaitThread extends Thread { |
| private final Runnable mActualRunnable; |
| private final CountDownLatch mLatch = new CountDownLatch(1); |
| |
| private StartAndWaitThread(String name, Runnable runnable) { |
| super(name); |
| mActualRunnable = runnable; |
| } |
| |
| private void runActualRunnable() { |
| mLatch.countDown(); |
| } |
| |
| @Override |
| public void run() { |
| // Thread is started but does not run actual code. This is for controlling the execution |
| // order while still meeting Thread.isAlive() check. |
| try { |
| mLatch.await(); |
| } catch (InterruptedException e) { |
| throw new RuntimeException(e); |
| } |
| mActualRunnable.run(); |
| } |
| } |
| |
| private Thread createAndStartExecutionThread(String name, Runnable runnable) { |
| final boolean isDexOptThread = !name.equals("DexOptCancel"); |
| StartAndWaitThread thread = new StartAndWaitThread(name, runnable); |
| if (isDexOptThread) { |
| mDexOptThread = thread; |
| } else { |
| mCancelThread = thread; |
| } |
| thread.start(); |
| return thread; |
| } |
| } |