diff options
author | Bookatz <bookatz@google.com> | 2019-04-11 09:24:54 -0700 |
---|---|---|
committer | Bookatz <bookatz@google.com> | 2019-04-16 16:31:31 -0700 |
commit | 312da055c2dc5e24f8a9841b4cd378238c8c9106 (patch) | |
tree | 056d8fabfc9a5adc5842d3a3adab8574e4487c22 /apct-tests/perftests/multiuser | |
parent | 5df3c64c9babbb1f7f832e79d01255c63e71536b (diff) |
Multi-user profile perf-tests
Tests that time how long it takes to
create/start/install-an-app/launch-an-app in a secondary profile.
Also, in the test, adds newly created users to the remove list, so that
even if the test crashes, the user will get removed by test cleanup.
Test: atest multiuser.UserLifecycleTests
Bug: 129857107
Change-Id: I13f7cb1628bf4a97771cb1b143ab524d7d0a2073
Diffstat (limited to 'apct-tests/perftests/multiuser')
7 files changed, 364 insertions, 58 deletions
diff --git a/apct-tests/perftests/multiuser/AndroidManifest.xml b/apct-tests/perftests/multiuser/AndroidManifest.xml index e96771cf1c17..b2a9524d29c4 100644 --- a/apct-tests/perftests/multiuser/AndroidManifest.xml +++ b/apct-tests/perftests/multiuser/AndroidManifest.xml @@ -17,7 +17,9 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.perftests.multiuser"> + <uses-permission android:name="android.permission.CONTROL_KEYGUARD" /> <uses-permission android:name="android.permission.MANAGE_USERS" /> + <uses-permission android:name="android.permission.INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" /> <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> diff --git a/apct-tests/perftests/multiuser/AndroidTest.xml b/apct-tests/perftests/multiuser/AndroidTest.xml index 512dbc9a8de7..d8e3f0102c35 100644 --- a/apct-tests/perftests/multiuser/AndroidTest.xml +++ b/apct-tests/perftests/multiuser/AndroidTest.xml @@ -19,6 +19,7 @@ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> <option name="cleanup-apks" value="true" /> <option name="test-file-name" value="MultiUserPerfTests.apk" /> + <option name="test-file-name" value="PerftestMultiuserDummyApp.apk" /> </target_preparer> <test class="com.android.tradefed.testtype.AndroidJUnitTest" > diff --git a/apct-tests/perftests/multiuser/apps/Android.mk b/apct-tests/perftests/multiuser/apps/Android.mk new file mode 100644 index 000000000000..e01225bfc4fa --- /dev/null +++ b/apct-tests/perftests/multiuser/apps/Android.mk @@ -0,0 +1,20 @@ +# Copyright (C) 2019 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. + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +# Build the test APKs using their own makefiles +include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/apct-tests/perftests/multiuser/apps/dummyapp/Android.mk b/apct-tests/perftests/multiuser/apps/dummyapp/Android.mk new file mode 100644 index 000000000000..31e799ede3c7 --- /dev/null +++ b/apct-tests/perftests/multiuser/apps/dummyapp/Android.mk @@ -0,0 +1,26 @@ +# Copyright (C) 2019 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. + +LOCAL_PATH:= $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_SDK_VERSION := current +LOCAL_MIN_SDK_VERSION := 19 + +LOCAL_PACKAGE_NAME := PerftestMultiuserDummyApp + +LOCAL_SRC_FILES := $(call all-java-files-under, src) + +include $(BUILD_PACKAGE)
\ No newline at end of file diff --git a/apct-tests/perftests/multiuser/apps/dummyapp/AndroidManifest.xml b/apct-tests/perftests/multiuser/apps/dummyapp/AndroidManifest.xml new file mode 100644 index 000000000000..d4d37f97b00a --- /dev/null +++ b/apct-tests/perftests/multiuser/apps/dummyapp/AndroidManifest.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2019 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="perftests.multiuser.apps.dummyapp" > + + <application android:label="Perftest Multiuser Dummy App"> + + <activity + android:name=".DummyForegroundActivity" + android:label="Perftest Multiuser Dummy App" + android:exported="true" + android:launchMode="singleTop" > + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + + </application> + +</manifest>
\ No newline at end of file diff --git a/apct-tests/perftests/multiuser/apps/dummyapp/src/com/android/multiuser/test/DummyForegroundActivity.java b/apct-tests/perftests/multiuser/apps/dummyapp/src/com/android/multiuser/test/DummyForegroundActivity.java new file mode 100644 index 000000000000..497d242b2531 --- /dev/null +++ b/apct-tests/perftests/multiuser/apps/dummyapp/src/com/android/multiuser/test/DummyForegroundActivity.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2019 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 perftests.multiuser.apps.dummyapp; + +import android.app.Activity; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.SystemClock; + +/** An activity. */ +public class DummyForegroundActivity extends Activity { + private static final String TAG = DummyForegroundActivity.class.getSimpleName(); + + public static final int TOP_SLEEP_TIME_MS = 2_000; + + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + doSleepWhileTop(TOP_SLEEP_TIME_MS); + } + + /** Does nothing, but asynchronously. */ + private void doSleepWhileTop(int sleepTime) { + new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... params) { + SystemClock.sleep(sleepTime); + return null; + } + + @Override + protected void onPostExecute(Void nothing) { + finish(); + } + }.execute(); + } +} diff --git a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java index 28079408d59b..6b09a9f150cf 100644 --- a/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java +++ b/apct-tests/perftests/multiuser/src/android/multiuser/UserLifecycleTests.java @@ -16,18 +16,30 @@ package android.multiuser; import android.app.ActivityManager; +import android.app.ActivityTaskManager; +import android.app.AppGlobals; import android.app.IActivityManager; import android.app.IStopUserCallback; import android.app.UserSwitchObserver; +import android.app.WaitResult; import android.content.BroadcastReceiver; import android.content.Context; +import android.content.IIntentReceiver; +import android.content.IIntentSender; import android.content.Intent; import android.content.IntentFilter; +import android.content.IntentSender; +import android.content.pm.IPackageInstaller; +import android.content.pm.PackageManager; import android.content.pm.UserInfo; +import android.os.Bundle; +import android.os.IBinder; +import android.os.IProgressListener; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; +import android.view.WindowManagerGlobal; import androidx.test.InstrumentationRegistry; import androidx.test.filters.LargeTest; @@ -66,8 +78,10 @@ import java.util.concurrent.TimeUnit; public class UserLifecycleTests { private static final String TAG = UserLifecycleTests.class.getSimpleName(); - private final int TIMEOUT_IN_SECOND = 30; - private final int CHECK_USER_REMOVED_INTERVAL_MS = 200; + private static final int TIMEOUT_IN_SECOND = 30; + private static final int CHECK_USER_REMOVED_INTERVAL_MS = 200; + + private static final String DUMMY_PACKAGE_NAME = "perftests.multiuser.apps.dummyapp"; private UserManager mUm; private ActivityManager mAm; @@ -101,15 +115,16 @@ public class UserLifecycleTests { @Test public void createAndStartUser() throws Exception { while (mRunner.keepRunning()) { - final UserInfo userInfo = mUm.createUser("TestUser", 0); + final int userId = createUser(); final CountDownLatch latch = new CountDownLatch(1); - registerBroadcastReceiver(Intent.ACTION_USER_STARTED, latch, userInfo.id); - mIam.startUserInBackground(userInfo.id); + registerBroadcastReceiver(Intent.ACTION_USER_STARTED, latch, userId); + // Don't use this.startUserInBackground() since only waiting until ACTION_USER_STARTED. + mIam.startUserInBackground(userId); latch.await(TIMEOUT_IN_SECOND, TimeUnit.SECONDS); mRunner.pauseTiming(); - removeUser(userInfo.id); + removeUser(userId); mRunner.resumeTiming(); } } @@ -119,14 +134,14 @@ public class UserLifecycleTests { while (mRunner.keepRunning()) { mRunner.pauseTiming(); final int startUser = mAm.getCurrentUser(); - final UserInfo userInfo = mUm.createUser("TestUser", 0); + final int userId = createUser(); mRunner.resumeTiming(); - switchUser(userInfo.id); + switchUser(userId); mRunner.pauseTiming(); switchUser(startUser); - removeUser(userInfo.id); + removeUser(userId); mRunner.resumeTiming(); } } @@ -176,17 +191,17 @@ public class UserLifecycleTests { public void stopUser() throws Exception { while (mRunner.keepRunning()) { mRunner.pauseTiming(); - final UserInfo userInfo = mUm.createUser("TestUser", 0); + final int userId = createUser(); final CountDownLatch latch = new CountDownLatch(1); - registerBroadcastReceiver(Intent.ACTION_USER_STARTED, latch, userInfo.id); - mIam.startUserInBackground(userInfo.id); + registerBroadcastReceiver(Intent.ACTION_USER_STARTED, latch, userId); + mIam.startUserInBackground(userId); latch.await(TIMEOUT_IN_SECOND, TimeUnit.SECONDS); mRunner.resumeTiming(); - stopUser(userInfo.id, false); + stopUser(userId, false); mRunner.pauseTiming(); - removeUser(userInfo.id); + removeUser(userId); mRunner.resumeTiming(); } } @@ -196,17 +211,17 @@ public class UserLifecycleTests { while (mRunner.keepRunning()) { mRunner.pauseTiming(); final int startUser = mAm.getCurrentUser(); - final UserInfo userInfo = mUm.createUser("TestUser", 0); + final int userId = createUser(); final CountDownLatch latch = new CountDownLatch(1); - registerUserSwitchObserver(null, latch, userInfo.id); + registerUserSwitchObserver(null, latch, userId); mRunner.resumeTiming(); - mAm.switchUser(userInfo.id); + mAm.switchUser(userId); latch.await(TIMEOUT_IN_SECOND, TimeUnit.SECONDS); mRunner.pauseTiming(); switchUser(startUser); - removeUser(userInfo.id); + removeUser(userId); mRunner.resumeTiming(); } } @@ -216,15 +231,14 @@ public class UserLifecycleTests { while (mRunner.keepRunning()) { mRunner.pauseTiming(); final int startUser = mAm.getCurrentUser(); - final UserInfo userInfo = mUm.createUser("TestUser", - UserInfo.FLAG_EPHEMERAL | UserInfo.FLAG_DEMO); - switchUser(userInfo.id); + final int userId = createUser(UserInfo.FLAG_EPHEMERAL | UserInfo.FLAG_DEMO); + switchUser(userId); final CountDownLatch latch = new CountDownLatch(1); InstrumentationRegistry.getContext().registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (Intent.ACTION_USER_STOPPED.equals(intent.getAction()) && intent.getIntExtra( - Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL) == userInfo.id) { + Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL) == userId) { latch.countDown(); } } @@ -238,79 +252,173 @@ public class UserLifecycleTests { mRunner.pauseTiming(); switchLatch.await(TIMEOUT_IN_SECOND, TimeUnit.SECONDS); - removeUser(userInfo.id); + removeUser(userId); + mRunner.resumeTiming(); + } + } + + /** Tests creating a new profile. */ + @Test + public void managedProfileCreate() throws Exception { + while (mRunner.keepRunning()) { + final int userId = createManagedProfile(); + + mRunner.pauseTiming(); + attestTrue("Failed creating profile " + userId, mUm.isManagedProfile(userId)); + removeUser(userId); mRunner.resumeTiming(); } } + /** Tests starting (unlocking) a newly-created profile. */ @Test public void managedProfileUnlock() throws Exception { while (mRunner.keepRunning()) { mRunner.pauseTiming(); - final UserInfo userInfo = mUm.createProfileForUser("TestUser", - UserInfo.FLAG_MANAGED_PROFILE, mAm.getCurrentUser()); - final CountDownLatch latch = new CountDownLatch(1); - registerBroadcastReceiver(Intent.ACTION_USER_UNLOCKED, latch, userInfo.id); + final int userId = createManagedProfile(); mRunner.resumeTiming(); - mIam.startUserInBackground(userInfo.id); - latch.await(TIMEOUT_IN_SECOND, TimeUnit.SECONDS); + startUserInBackground(userId); mRunner.pauseTiming(); - removeUser(userInfo.id); + removeUser(userId); mRunner.resumeTiming(); } } - /** Tests starting an already-created, but no-longer-running, profile. */ + /** Tests starting (unlocking) an already-created, but no-longer-running, profile. */ @Test public void managedProfileUnlock_stopped() throws Exception { while (mRunner.keepRunning()) { mRunner.pauseTiming(); - final UserInfo userInfo = mUm.createProfileForUser("TestUser", - UserInfo.FLAG_MANAGED_PROFILE, mAm.getCurrentUser()); + final int userId = createManagedProfile(); // Start the profile initially, then stop it. Similar to setQuietModeEnabled. - final CountDownLatch latch1 = new CountDownLatch(1); - registerBroadcastReceiver(Intent.ACTION_USER_UNLOCKED, latch1, userInfo.id); - mIam.startUserInBackground(userInfo.id); - latch1.await(TIMEOUT_IN_SECOND, TimeUnit.SECONDS); - stopUser(userInfo.id, true); - - // Now we restart the profile. - final CountDownLatch latch2 = new CountDownLatch(1); - registerBroadcastReceiver(Intent.ACTION_USER_UNLOCKED, latch2, userInfo.id); + startUserInBackground(userId); + stopUser(userId, true); + mRunner.resumeTiming(); + + startUserInBackground(userId); + + mRunner.pauseTiming(); + removeUser(userId); + mRunner.resumeTiming(); + } + } + + /** + * Tests starting (unlocking) and launching an already-installed app in a newly-created profile. + */ + @Test + public void managedProfileUnlockAndLaunchApp() throws Exception { + while (mRunner.keepRunning()) { + mRunner.pauseTiming(); + final int userId = createManagedProfile(); + WindowManagerGlobal.getWindowManagerService().dismissKeyguard(null, null); + installPreexistingApp(userId, DUMMY_PACKAGE_NAME); + mRunner.resumeTiming(); + + startUserInBackground(userId); + startApp(userId, DUMMY_PACKAGE_NAME); + + mRunner.pauseTiming(); + removeUser(userId); + mRunner.resumeTiming(); + } + } + + /** Tests installing a pre-existing app in a newly-created profile. */ + @Test + public void managedProfileInstall() throws Exception { + while (mRunner.keepRunning()) { + mRunner.pauseTiming(); + final int userId = createManagedProfile(); mRunner.resumeTiming(); - mIam.startUserInBackground(userInfo.id); - latch2.await(TIMEOUT_IN_SECOND, TimeUnit.SECONDS); + installPreexistingApp(userId, DUMMY_PACKAGE_NAME); mRunner.pauseTiming(); - removeUser(userInfo.id); + removeUser(userId); mRunner.resumeTiming(); } } + /** + * Tests creating a new profile, starting (unlocking) it, installing an app, + * and launching that app in it. + */ + @Test + public void managedProfileCreateUnlockInstallAndLaunchApp() throws Exception { + final String packageName = "perftests.multiuser.apps.dummyapp"; + while (mRunner.keepRunning()) { + mRunner.pauseTiming(); + WindowManagerGlobal.getWindowManagerService().dismissKeyguard(null, null); + mRunner.resumeTiming(); + + final int userId = createManagedProfile(); + startUserInBackground(userId); + installPreexistingApp(userId, DUMMY_PACKAGE_NAME); + startApp(userId, DUMMY_PACKAGE_NAME); + + mRunner.pauseTiming(); + removeUser(userId); + mRunner.resumeTiming(); + } + } + /** Tests stopping a profile. */ @Test public void managedProfileStopped() throws Exception { while (mRunner.keepRunning()) { mRunner.pauseTiming(); - final UserInfo userInfo = mUm.createProfileForUser("TestUser", - UserInfo.FLAG_MANAGED_PROFILE, mAm.getCurrentUser()); - final CountDownLatch latch = new CountDownLatch(1); - registerBroadcastReceiver(Intent.ACTION_USER_UNLOCKED, latch, userInfo.id); - mIam.startUserInBackground(userInfo.id); - latch.await(TIMEOUT_IN_SECOND, TimeUnit.SECONDS); + final int userId = createManagedProfile(); + startUserInBackground(userId); mRunner.resumeTiming(); - stopUser(userInfo.id, true); + stopUser(userId, true); mRunner.pauseTiming(); - removeUser(userInfo.id); + removeUser(userId); mRunner.resumeTiming(); } } + /** Creates a new user, returning its userId. */ + private int createUser() { + return createUser(0); + } + + /** Creates a new user with the given flags, returning its userId. */ + private int createUser(int flags) { + int userId = mUm.createUser("TestUser", flags).id; + mUsersToRemove.add(userId); + return userId; + } + + /** Creates a managed (work) profile under the current user, returning its userId. */ + private int createManagedProfile() { + final UserInfo userInfo = mUm.createProfileForUser("TestProfile", + UserInfo.FLAG_MANAGED_PROFILE, mAm.getCurrentUser()); + mUsersToRemove.add(userInfo.id); + return userInfo.id; + } + + /** + * Start user in background and wait for it to unlock (equivalent to ACTION_USER_UNLOCKED). + * To start in foreground instead, see {@link #switchUser(int)}. + * This should always be used for profiles since profiles cannot be started in foreground. + */ + private void startUserInBackground(int userId) { + final ProgressWaiter waiter = new ProgressWaiter(); + try { + mIam.startUserInBackgroundWithListener(userId, waiter); + boolean success = waiter.waitForFinish(TIMEOUT_IN_SECOND); + attestTrue("Failed to start user " + userId + " in background.", success); + } catch (RemoteException e) { + Log.e(TAG, "startUserInBackground failed", e); + } + } + + /** Starts the given user in the foreground. */ private void switchUser(int userId) throws Exception { final CountDownLatch latch = new CountDownLatch(1); registerUserSwitchObserver(latch, null, userId); @@ -343,7 +451,7 @@ public class UserLifecycleTests { private int initializeNewUserAndSwitchBack(boolean stopNewUser) throws Exception { final int origUser = mAm.getCurrentUser(); // First, create and switch to testUser, waiting for its ACTION_USER_UNLOCKED - final int testUser = mUm.createUser("TestUser", 0).id; + final int testUser = createUser(); final CountDownLatch latch1 = new CountDownLatch(1); registerBroadcastReceiver(Intent.ACTION_USER_UNLOCKED, latch1, testUser); mAm.switchUser(testUser); @@ -362,6 +470,45 @@ public class UserLifecycleTests { return testUser; } + /** + * Installs the given package in the given user. + */ + private void installPreexistingApp(int userId, String packageName) throws RemoteException { + final CountDownLatch latch = new CountDownLatch(1); + + final IntentSender sender = new IntentSender((IIntentSender) new IIntentSender.Stub() { + @Override + public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken, + IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) { + latch.countDown(); + } + }); + + final IPackageInstaller installer = AppGlobals.getPackageManager().getPackageInstaller(); + installer.installExistingPackage(packageName, 0, PackageManager.INSTALL_REASON_UNKNOWN, + sender, userId); + + try { + latch.await(TIMEOUT_IN_SECOND, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Log.e(TAG, "Thread interrupted unexpectedly.", e); + } + } + + /** + * Launches the given package in the given user. + * Make sure the keyguard has been dismissed prior to calling. + */ + private void startApp(int userId, String packageName) throws RemoteException { + final Context context = InstrumentationRegistry.getContext(); + final WaitResult result = ActivityTaskManager.getService().startActivityAndWait(null, null, + context.getPackageManager().getLaunchIntentForPackage(packageName), + null, null, null, 0, 0, null, null, + userId); + attestTrue("User " + userId + " failed to start " + packageName, + result.result == ActivityManager.START_SUCCESS); + } + private void registerUserSwitchObserver(final CountDownLatch switchLatch, final CountDownLatch bootCompleteLatch, final int userId) throws Exception { ActivityManager.getService().registerUserSwitchObserver( @@ -395,6 +542,30 @@ public class UserLifecycleTests { }, UserHandle.of(userId), new IntentFilter(action), null, null); } + private class ProgressWaiter extends IProgressListener.Stub { + private final CountDownLatch mFinishedLatch = new CountDownLatch(1); + + @Override + public void onStarted(int id, Bundle extras) {} + + @Override + public void onProgress(int id, int progress, Bundle extras) {} + + @Override + public void onFinished(int id, Bundle extras) { + mFinishedLatch.countDown(); + } + + public boolean waitForFinish(long timeoutSecs) { + try { + return mFinishedLatch.await(timeoutSecs, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Log.e(TAG, "Thread interrupted unexpectedly.", e); + return false; + } + } + } + private void removeUser(int userId) { try { mUm.removeUser(userId); @@ -414,13 +585,13 @@ public class UserLifecycleTests { } } - private void attestTrue(String message, boolean attestion) { - if (!attestion) { + private void attestTrue(String message, boolean assertion) { + if (!assertion) { Log.w(TAG, message); } } - private void attestFalse(String message, boolean attestion) { - attestTrue(message, !attestion); + private void attestFalse(String message, boolean assertion) { + attestTrue(message, !assertion); } } |