diff options
| author | 2021-11-16 00:52:39 +0000 | |
|---|---|---|
| committer | 2021-11-16 00:52:39 +0000 | |
| commit | 7b9d5d9d29fdf19112809a1d82b2fdc17c89aa6b (patch) | |
| tree | 953c0bcaebb35aff6b9e7bb5e55535b2ebc1828d | |
| parent | bbe80a038217d1ead4eb7104028536ba0f9ea006 (diff) | |
| parent | da73d5538c603e77a70c05c4763aed137daf5a7b (diff) | |
Merge "Add unit test for BackgroundDexOptService"
3 files changed, 526 insertions, 44 deletions
diff --git a/services/core/java/com/android/server/pm/BackgroundDexOptService.java b/services/core/java/com/android/server/pm/BackgroundDexOptService.java index af9c4013ce1e..27db2f90ab25 100644 --- a/services/core/java/com/android/server/pm/BackgroundDexOptService.java +++ b/services/core/java/com/android/server/pm/BackgroundDexOptService.java @@ -121,6 +121,8 @@ public final class BackgroundDexOptService { private final Injector mInjector; + private final DexOptHelper mDexOptHelper; + private final Object mLock = new Object(); // Thread currently running dexopt. This will be null if dexopt is not running. @@ -168,13 +170,15 @@ public final class BackgroundDexOptService { void onPackagesUpdated(ArraySet<String> updatedPackages); } - public BackgroundDexOptService(Context context, DexManager dexManager) { - this(new Injector(context, dexManager)); + public BackgroundDexOptService(Context context, DexManager dexManager, + PackageManagerService pm) { + this(new Injector(context, dexManager, pm)); } @VisibleForTesting public BackgroundDexOptService(Injector injector) { mInjector = injector; + mDexOptHelper = mInjector.getDexOptHelper(); LocalServices.addService(BackgroundDexOptService.class, this); mDowngradeUnusedAppsThresholdInMillis = mInjector.getDowngradeUnusedAppsThresholdInMillis(); } @@ -251,15 +255,13 @@ public final class BackgroundDexOptService { resetStatesForNewDexOptRunLocked(Thread.currentThread()); } PackageManagerService pm = mInjector.getPackageManagerService(); - DexOptHelper dexOptHelper = new DexOptHelper(pm); ArraySet<String> packagesToOptimize; if (packageNames == null) { - packagesToOptimize = dexOptHelper.getOptimizablePackages(); + packagesToOptimize = mDexOptHelper.getOptimizablePackages(); } else { packagesToOptimize = new ArraySet<>(packageNames); } - return runIdleOptimization(pm, dexOptHelper, packagesToOptimize, - /* isPostBootUpdate= */ false); + return runIdleOptimization(pm, packagesToOptimize, /* isPostBootUpdate= */ false); } finally { Binder.restoreCallingIdentity(identity); markDexOptCompleted(); @@ -320,8 +322,7 @@ public final class BackgroundDexOptService { return false; } - DexOptHelper dexOptHelper = new DexOptHelper(pm); - ArraySet<String> pkgs = dexOptHelper.getOptimizablePackages(); + ArraySet<String> pkgs = mDexOptHelper.getOptimizablePackages(); if (pkgs.isEmpty()) { Slog.i(TAG, "No packages to optimize"); markPostBootUpdateCompleted(params); @@ -347,7 +348,7 @@ public final class BackgroundDexOptService { tr.traceBegin("jobExecution"); boolean completed = false; try { - completed = runIdleOptimization(pm, dexOptHelper, pkgs, + completed = runIdleOptimization(pm, pkgs, params.getJobId() == JOB_POST_BOOT_UPDATE); } finally { // Those cleanup should be done always. tr.traceEnd(); @@ -461,7 +462,7 @@ public final class BackgroundDexOptService { @GuardedBy("mLock") private void controlDexOptBlockingLocked(boolean block) { PackageManagerService pm = mInjector.getPackageManagerService(); - new DexOptHelper(pm).controlDexOptBlocking(block); + mDexOptHelper.controlDexOptBlocking(block); } private void scheduleAJob(int jobId) { @@ -511,10 +512,10 @@ public final class BackgroundDexOptService { } /** Returns true if completed */ - private boolean runIdleOptimization(PackageManagerService pm, DexOptHelper dexOptHelper, - ArraySet<String> pkgs, boolean isPostBootUpdate) { + private boolean runIdleOptimization(PackageManagerService pm, ArraySet<String> pkgs, + boolean isPostBootUpdate) { long lowStorageThreshold = getLowStorageThreshold(); - int status = idleOptimizePackages(pm, dexOptHelper, pkgs, lowStorageThreshold, + int status = idleOptimizePackages(pm, pkgs, lowStorageThreshold, isPostBootUpdate); logStatus(status); synchronized (mLock) { @@ -562,8 +563,8 @@ public final class BackgroundDexOptService { } @Status - private int idleOptimizePackages(PackageManagerService pm, DexOptHelper dexOptHelper, - ArraySet<String> pkgs, long lowStorageThreshold, boolean isPostBootUpdate) { + private int idleOptimizePackages(PackageManagerService pm, ArraySet<String> pkgs, + long lowStorageThreshold, boolean isPostBootUpdate) { ArraySet<String> updatedPackages = new ArraySet<>(); ArraySet<String> updatedPackagesDueToSecondaryDex = new ArraySet<>(); @@ -600,7 +601,7 @@ public final class BackgroundDexOptService { // Should be aborted by the scheduler. return abortCode; } - @DexOptResult int downgradeResult = downgradePackage(pm, dexOptHelper, pkg, + @DexOptResult int downgradeResult = downgradePackage(pm, pkg, /* isForPrimaryDex= */ true, isPostBootUpdate); if (downgradeResult == PackageDexOptimizer.DEX_OPT_PERFORMED) { updatedPackages.add(pkg); @@ -611,7 +612,7 @@ public final class BackgroundDexOptService { return status; } if (supportSecondaryDex) { - downgradeResult = downgradePackage(pm, dexOptHelper, pkg, + downgradeResult = downgradePackage(pm, pkg, /* isForPrimaryDex= */false, isPostBootUpdate); status = convertPackageDexOptimizerStatusToInternal(downgradeResult); if (status != STATUS_OK) { @@ -625,9 +626,8 @@ public final class BackgroundDexOptService { } } - @Status int primaryResult = optimizePackages(dexOptHelper, pkgs, - lowStorageThreshold, /*isForPrimaryDex=*/ true, updatedPackages, - isPostBootUpdate); + @Status int primaryResult = optimizePackages(pkgs, lowStorageThreshold, + /*isForPrimaryDex=*/ true, updatedPackages, isPostBootUpdate); if (primaryResult != STATUS_OK) { return primaryResult; } @@ -636,9 +636,9 @@ public final class BackgroundDexOptService { return STATUS_OK; } - @Status int secondaryResult = optimizePackages(dexOptHelper, pkgs, - lowStorageThreshold, /*isForPrimaryDex*/ false, - updatedPackagesDueToSecondaryDex, isPostBootUpdate); + @Status int secondaryResult = optimizePackages(pkgs, lowStorageThreshold, + /*isForPrimaryDex*/ false, updatedPackagesDueToSecondaryDex, + isPostBootUpdate); return secondaryResult; } finally { // Always let the pinner service know about changes. @@ -651,9 +651,8 @@ public final class BackgroundDexOptService { } @Status - private int optimizePackages(DexOptHelper dexOptHelper, - ArraySet<String> pkgs, long lowStorageThreshold, boolean isForPrimaryDex, - ArraySet<String> updatedPackages, boolean isPostBootUpdate) { + private int optimizePackages(ArraySet<String> pkgs, long lowStorageThreshold, + boolean isForPrimaryDex, ArraySet<String> updatedPackages, boolean isPostBootUpdate) { for (String pkg : pkgs) { int abortCode = abortIdleOptimizations(lowStorageThreshold); if (abortCode != STATUS_OK) { @@ -661,8 +660,7 @@ public final class BackgroundDexOptService { return abortCode; } - @DexOptResult int result = optimizePackage(dexOptHelper, pkg, isForPrimaryDex, - isPostBootUpdate); + @DexOptResult int result = optimizePackage(pkg, isForPrimaryDex, isPostBootUpdate); if (result == PackageDexOptimizer.DEX_OPT_PERFORMED) { updatedPackages.add(pkg); } else if (result != PackageDexOptimizer.DEX_OPT_SKIPPED) { @@ -681,8 +679,8 @@ public final class BackgroundDexOptService { * @return PackageDexOptimizer.DEX_* */ @DexOptResult - private int downgradePackage(PackageManagerService pm, DexOptHelper dexOptHelper, String pkg, - boolean isForPrimaryDex, boolean isPostBootUpdate) { + private int downgradePackage(PackageManagerService pm, String pkg, boolean isForPrimaryDex, + boolean isPostBootUpdate) { if (DEBUG) { Slog.d(TAG, "Downgrading " + pkg); } @@ -704,10 +702,10 @@ public final class BackgroundDexOptService { // remove their compiler artifacts from dalvik cache. pm.deleteOatArtifactsOfPackage(pkg); } else { - result = performDexOptPrimary(dexOptHelper, pkg, reason, dexoptFlags); + result = performDexOptPrimary(pkg, reason, dexoptFlags); } } else { - result = performDexOptSecondary(dexOptHelper, pkg, reason, dexoptFlags); + result = performDexOptSecondary(pkg, reason, dexoptFlags); } if (result == PackageDexOptimizer.DEX_OPT_PERFORMED) { @@ -733,15 +731,13 @@ public final class BackgroundDexOptService { * * Optimize package if needed. Note that there can be no race between * concurrent jobs because PackageDexOptimizer.performDexOpt is synchronized. - * @param dexOptHelper An instance of DexOptHelper * @param pkg The package to be downgraded. * @param isForPrimaryDex Apps can have several dex file, primary and secondary. * @param isPostBootUpdate is post boot update or not. * @return PackageDexOptimizer#DEX_OPT_* */ @DexOptResult - private int optimizePackage(DexOptHelper dexOptHelper, String pkg, - boolean isForPrimaryDex, boolean isPostBootUpdate) { + private int optimizePackage(String pkg, boolean isForPrimaryDex, boolean isPostBootUpdate) { int reason = isPostBootUpdate ? PackageManagerService.REASON_POST_BOOT : PackageManagerService.REASON_BACKGROUND_DEXOPT; int dexoptFlags = DexoptOptions.DEXOPT_BOOT_COMPLETE; @@ -753,27 +749,27 @@ public final class BackgroundDexOptService { // System server share the same code path as primary dex files. // PackageManagerService will select the right optimization path for it. if (isForPrimaryDex || PLATFORM_PACKAGE_NAME.equals(pkg)) { - return performDexOptPrimary(dexOptHelper, pkg, reason, dexoptFlags); + return performDexOptPrimary(pkg, reason, dexoptFlags); } else { - return performDexOptSecondary(dexOptHelper, pkg, reason, dexoptFlags); + return performDexOptSecondary(pkg, reason, dexoptFlags); } } @DexOptResult - private int performDexOptPrimary(DexOptHelper dexOptHelper, String pkg, int reason, + private int performDexOptPrimary(String pkg, int reason, int dexoptFlags) { return trackPerformDexOpt(pkg, /*isForPrimaryDex=*/ true, - () -> dexOptHelper.performDexOptWithStatus( + () -> mDexOptHelper.performDexOptWithStatus( new DexoptOptions(pkg, reason, dexoptFlags))); } @DexOptResult - private int performDexOptSecondary(DexOptHelper dexOptHelper, String pkg, int reason, + private int performDexOptSecondary(String pkg, int reason, int dexoptFlags) { DexoptOptions dexoptOptions = new DexoptOptions(pkg, reason, dexoptFlags | DexoptOptions.DEXOPT_ONLY_SECONDARY_DEX); return trackPerformDexOpt(pkg, /*isForPrimaryDex=*/ false, - () -> dexOptHelper.performDexOpt(dexoptOptions) + () -> mDexOptHelper.performDexOpt(dexoptOptions) ? PackageDexOptimizer.DEX_OPT_PERFORMED : PackageDexOptimizer.DEX_OPT_FAILED ); } @@ -911,11 +907,13 @@ public final class BackgroundDexOptService { static final class Injector { private final Context mContext; private final DexManager mDexManager; + private final PackageManagerService mPackageManagerService; private final File mDataDir = Environment.getDataDirectory(); - Injector(Context context, DexManager dexManager) { + Injector(Context context, DexManager dexManager, PackageManagerService pm) { mContext = context; mDexManager = dexManager; + mPackageManagerService = pm; } Context getContext() { @@ -923,7 +921,11 @@ public final class BackgroundDexOptService { } PackageManagerService getPackageManagerService() { - return (PackageManagerService) ServiceManager.getService("package"); + return mPackageManagerService; + } + + DexOptHelper getDexOptHelper() { + return new DexOptHelper(getPackageManagerService()); } JobScheduler getJobScheduler() { diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index accb7a00bb30..5f56d4e10036 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -1486,7 +1486,7 @@ public class PackageManagerService extends IPackageManager.Stub new DefaultSystemWrapper(), LocalServices::getService, context::getSystemService, - (i, pm) -> new BackgroundDexOptService(i.getContext(), i.getDexManager())); + (i, pm) -> new BackgroundDexOptService(i.getContext(), i.getDexManager(), pm)); if (Build.VERSION.SDK_INT <= 0) { Slog.w(TAG, "**** ro.build.version.sdk not set!"); diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java new file mode 100644 index 000000000000..8223b8c86c5b --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundDexOptServiceUnitTest.java @@ -0,0 +1,480 @@ +/* + * 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.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.never; +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.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.util.ArraySet; + +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.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.Arrays; +import java.util.HashMap; +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 ArraySet<String> DEFAULT_PACKAGE_LIST = new ArraySet<>( + Arrays.asList("aaa", "bbb")); + private static final ArraySet<String> EMPTY_PACKAGE_LIST = new ArraySet<>(); + + @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.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(mDexOptHelper.getOptimizablePackages()).thenReturn(DEFAULT_PACKAGE_LIST); + when(mDexOptHelper.performDexOptWithStatus(any())).thenReturn( + PackageDexOptimizer.DEX_OPT_PERFORMED); + + mService = new BackgroundDexOptService(mInjector); + } + + @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()).thenReturn(EMPTY_PACKAGE_LIST); + + assertThat(mService.onStartJob(mJobServiceForPostBoot, + mJobParametersForPostBoot)).isFalse(); + verify(mDexOptHelper, never()).performDexOpt(any()); + } + + @Test + public void testPostBootUpdateFullRun() { + initUntilBootCompleted(); + + runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, false, 1); + } + + @Test + public void testIdleJobFullRun() { + initUntilBootCompleted(); + + runFullJob(mJobServiceForPostBoot, mJobParametersForPostBoot, false, 1); + runFullJob(mJobServiceForIdle, mJobParametersForIdle, true, 2); + } + + @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()); + } + + 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 totalJobRuns) { + when(mInjector.createAndStartThread(any(), any())).thenReturn(Thread.currentThread()); + 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).jobFinished(params, expectedReschedule); + // Never block + verify(mDexOptHelper, never()).controlDexOptBlocking(true); + verifyPerformDexOpt(DEFAULT_PACKAGE_LIST, totalJobRuns); + } + + private void verifyPerformDexOpt(ArraySet<String> pkgs, int expectedRuns) { + ArgumentCaptor<DexoptOptions> dexOptOptions = ArgumentCaptor.forClass(DexoptOptions.class); + verify(mDexOptHelper, atLeastOnce()).performDexOptWithStatus(dexOptOptions.capture()); + HashMap<String, Integer> primaryPkgs = new HashMap<>(); // K: pkg, V: dexopt runs left + for (String pkg : pkgs) { + primaryPkgs.put(pkg, expectedRuns); + } + + for (DexoptOptions opt : dexOptOptions.getAllValues()) { + assertThat(pkgs).contains(opt.getPackageName()); + assertThat(opt.isDexoptOnlySecondaryDex()).isFalse(); + Integer count = primaryPkgs.get(opt.getPackageName()); + assertThat(count).isNotNull(); + if (count == 1) { + primaryPkgs.remove(opt.getPackageName()); + } else { + primaryPkgs.put(opt.getPackageName(), count - 1); + } + } + assertThat(primaryPkgs).isEmpty(); + } + + 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; + } +} |