From d9902e3c24ff44317c07ec14984c2ddec4ad3698 Mon Sep 17 00:00:00 2001 From: Felipe Leme Date: Mon, 30 Nov 2020 20:03:14 -0800 Subject: [PATCH] Added unit tests for MasterClearReceiver. Test: atest FrameworksMockingServicesTests:MasterClearReceiverTest Bug: 171603586 Change-Id: Ib05af552d8a848be93fe4ba7198b90f4800f861b --- .../android/server/MasterClearReceiver.java | 9 +- .../mockingservicestests/AndroidManifest.xml | 3 + .../server/MasterClearReceiverTest.java | 210 ++++++++++++++++++ 3 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 services/tests/mockingservicestests/src/com/android/server/MasterClearReceiverTest.java diff --git a/services/core/java/com/android/server/MasterClearReceiver.java b/services/core/java/com/android/server/MasterClearReceiver.java index 793e342926239..e248b215ef4a7 100644 --- a/services/core/java/com/android/server/MasterClearReceiver.java +++ b/services/core/java/com/android/server/MasterClearReceiver.java @@ -25,7 +25,6 @@ import android.os.RecoverySystem; import android.os.UserHandle; import android.os.storage.StorageManager; import android.text.TextUtils; -import android.util.Log; import android.util.Slog; import android.view.WindowManager; @@ -59,6 +58,7 @@ public class MasterClearReceiver extends BroadcastReceiver { .getString(com.android.internal.R.string.config_factoryResetPackage); if (Intent.ACTION_FACTORY_RESET.equals(intent.getAction()) && !TextUtils.isEmpty(factoryResetPackage)) { + Slog.i(TAG, "Re-directing intent to " + factoryResetPackage); intent.setPackage(factoryResetPackage).setComponent(null); context.sendBroadcastAsUser(intent, UserHandle.SYSTEM); return; @@ -77,9 +77,12 @@ public class MasterClearReceiver extends BroadcastReceiver { @Override public void run() { try { + Slog.i(TAG, "Calling RecoverySystem.rebootWipeUserData(context, " + + "shutdown=" + shutdown + ", reason=" + reason + + ", forceWipe=" + forceWipe + ", wipeEsims=" + mWipeEsims + ")"); RecoverySystem .rebootWipeUserData(context, shutdown, reason, forceWipe, mWipeEsims); - Log.wtf(TAG, "Still running after master clear?!"); + Slog.wtf(TAG, "Still running after master clear?!"); } catch (IOException e) { Slog.e(TAG, "Can't perform master clear/factory reset", e); } catch (SecurityException e) { @@ -90,8 +93,10 @@ public class MasterClearReceiver extends BroadcastReceiver { if (mWipeExternalStorage) { // thr will be started at the end of this task. + Slog.i(TAG, "Wiping external storage on async task"); new WipeDataTask(context, thr).execute(); } else { + Slog.i(TAG, "NOT wiping external storage; starting thread " + thr.getName()); thr.start(); } } diff --git a/services/tests/mockingservicestests/AndroidManifest.xml b/services/tests/mockingservicestests/AndroidManifest.xml index 182fe9ae94767..fbde1d2cf1cf8 100644 --- a/services/tests/mockingservicestests/AndroidManifest.xml +++ b/services/tests/mockingservicestests/AndroidManifest.xml @@ -27,6 +27,9 @@ + + + diff --git a/services/tests/mockingservicestests/src/com/android/server/MasterClearReceiverTest.java b/services/tests/mockingservicestests/src/com/android/server/MasterClearReceiverTest.java new file mode 100644 index 0000000000000..f01120e1ed4c3 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/MasterClearReceiverTest.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2020 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; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.never; + +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.os.Looper; +import android.os.RecoverySystem; +import android.os.storage.StorageManager; +import android.platform.test.annotations.Presubmit; +import android.util.Log; +import android.view.WindowManager; + +import androidx.test.InstrumentationRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Run it as {@code FrameworksMockingServicesTests:MasterClearReceiverTest}. + */ +@Presubmit +public final class MasterClearReceiverTest { + + private static final String TAG = MasterClearReceiverTest.class.getSimpleName(); + + private MockitoSession mSession; + + // Cannot @Mock context because MasterClearReceiver shows an AlertDialog, which relies + // on resources - we'd need to mock them as well. + private final Context mContext = new ContextWrapper( + InstrumentationRegistry.getInstrumentation().getTargetContext()) { + + @Override + public Object getSystemService(String name) { + Log.v(TAG, "getSystemService(): " + name); + return name.equals(Context.STORAGE_SERVICE) ? mSm : super.getSystemService(name); + } + }; + + private final MasterClearReceiver mReceiver = new MasterClearReceiver(); + + // Used to make sure that wipeAdoptableDisks() is called before rebootWipeUserData() + private boolean mWipeExternalDataCalled; + + // Uset to block test until rebootWipeUserData() is called, as it might be asynchronous called + // in a different thread + private final CountDownLatch mRebootWipeUserDataLatch = new CountDownLatch(1); + + @Mock + private StorageManager mSm; + + @Mock + private WindowManager mWm; + + @Before + public void startSession() { + mSession = mockitoSession() + .initMocks(this) + .mockStatic(RecoverySystem.class) + .strictness(Strictness.LENIENT) + .startMocking(); + } + + @After + public void finishSession() { + if (mSession == null) { + Log.w(TAG, "finishSession(): no session"); + return; + } + mSession.finishMocking(); + } + + @Test + public void testNoExtras() throws Exception { + expectNoWipeExternalData(); + expectRebootWipeUserData(); + + Intent intent = new Intent(Intent.ACTION_FACTORY_RESET); + mReceiver.onReceive(mContext, intent); + + verifyRebootWipeUserData(); + verifyNoWipeExternalData(); + } + + @Test + public void testWipeExternalDirectory() throws Exception { + expectWipeExternalData(); + expectRebootWipeUserData(); + + Intent intent = new Intent(Intent.ACTION_FACTORY_RESET); + intent.putExtra(Intent.EXTRA_WIPE_EXTERNAL_STORAGE, true); + mReceiver.onReceive(mContext, intent); + + verifyRebootWipeUserData(); + verifyWipeExternalData(); + } + + @Test + public void testAllExtras() throws Exception { + expectWipeExternalData(); + expectRebootWipeUserData(); + + Intent intent = new Intent(Intent.ACTION_FACTORY_RESET); + intent.putExtra(Intent.EXTRA_WIPE_EXTERNAL_STORAGE, true); + intent.putExtra("shutdown", true); + intent.putExtra(Intent.EXTRA_REASON, "Self destruct"); + intent.putExtra(Intent.EXTRA_FORCE_FACTORY_RESET, true); + intent.putExtra(Intent.EXTRA_WIPE_ESIMS, true); + mReceiver.onReceive(mContext, intent); + + verifyRebootWipeUserData(/* shutdown= */ true, /* reason= */ "Self destruct", + /* force= */ true, /* wipeEuicc= */ true); + verifyWipeExternalData(); + } + + + private void expectNoWipeExternalData() { + // This is a trick to simplify how the order of methods are called: as wipeAdoptableDisks() + // should be called before rebootWipeUserData(), expectRebootWipeUserData() throws an + // exception if it's not called, so this method "emulates" a call when it's not neeeded. + // + // A more robust solution would be using internal counters for expected and actual mocked + // calls, so the expectXXX() methods would increment expected counter and the Answer + // implementations would increment the actual counter and check if they match, but that + // would be an overkill (and make the test logic more complicated). + mWipeExternalDataCalled = true; + } + + private void expectRebootWipeUserData() { + doAnswer((inv) -> { + Log.i(TAG, inv.toString()); + if (!mWipeExternalDataCalled) { + String error = "rebootWipeUserData() called before wipeAdoptableDisks()"; + Log.e(TAG, error); + throw new IllegalStateException(error); + } + mRebootWipeUserDataLatch.countDown(); + return null; + }).when(() -> RecoverySystem + .rebootWipeUserData(any(), anyBoolean(), any(), anyBoolean(), anyBoolean())); + } + + private void expectWipeExternalData() { + Looper.prepare(); // needed by Dialog + + doAnswer((inv) -> { + Log.i(TAG, inv.toString()); + mWipeExternalDataCalled = true; + return null; + }).when(mSm).wipeAdoptableDisks(); + } + + private void verifyRebootWipeUserData() throws Exception { + verifyRebootWipeUserData(/* shutdown= */ false, /* reason= */ null, /* force= */ false, + /* wipeEuicc= */ false); + + } + + private void verifyRebootWipeUserData(boolean shutdown, String reason, boolean force, + boolean wipeEuicc) throws Exception { + boolean called = mRebootWipeUserDataLatch.await(5, TimeUnit.SECONDS); + assertWithMessage("rebootWipeUserData not called in 5s").that(called).isTrue(); + + verify(()-> RecoverySystem.rebootWipeUserData(same(mContext), eq(shutdown), eq(reason), + eq(force), eq(wipeEuicc))); + } + + private void verifyWipeExternalData() { + verify(mSm).wipeAdoptableDisks(); + } + + private void verifyNoWipeExternalData() { + verify(mSm, never()).wipeAdoptableDisks(); + } +}