blob: 9e1d67ac79e5e3c837eb32ff50e58cf905a50910 [file] [log] [blame]
/*
* 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 android.server.wm.intent;
import static android.server.wm.intent.Persistence.LaunchFromIntent.prepareSerialisation;
import static android.server.wm.intent.StateComparisonException.assertEndStatesEqual;
import static android.server.wm.intent.StateComparisonException.assertInitialStateEqual;
import static androidx.test.InstrumentationRegistry.getInstrumentation;
import static com.google.common.collect.Iterables.getLast;
import static org.junit.Assert.assertNotNull;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.SystemClock;
import android.server.wm.WindowManagerStateHelper;
import android.server.wm.WindowManagerState;
import android.server.wm.intent.LaunchSequence.LaunchSequenceExecutionInfo;
import android.server.wm.intent.Persistence.GenerationIntent;
import android.server.wm.intent.Persistence.LaunchFromIntent;
import android.server.wm.intent.Persistence.StateDump;
import android.view.Display;
import com.google.common.collect.Lists;
import java.util.List;
/**
* Launch runner is an interpreter for a {@link LaunchSequence} command object.
* It supports three main modes of operation.
*
* 1. The {@link LaunchRunner#runAndWrite} method to run a launch object and write out the
* resulting {@link Persistence.TestCase} to device storage
*
* 2. The {@link LaunchRunner#verify} method to rerun a previously recorded
* {@link Persistence.TestCase} and verify that the recorded states match the states resulting from
* the rerun.
*
* 3. The {@link LaunchRunner#run} method to run a launch object and return an {@link LaunchRecord}
* that can be used to do assertions directly in the same test.
*/
public class LaunchRunner {
private static final int ACTIVITY_LAUNCH_TIMEOUT = 10000;
private static final int BEFORE_DUMP_TIMEOUT = 3000;
/**
* Used for the waiting utilities.
*/
private IntentTestBase mTestBase;
/**
* The activities that were already present in the system when the test started.
* So they can be removed form the outputs, otherwise our tests would be system dependent.
*/
private List<WindowManagerState.ActivityTask> mBaseStacks;
public LaunchRunner(IntentTestBase testBase) {
mTestBase = testBase;
mBaseStacks = getBaseStacks();
}
/**
* Re-run a previously recorded {@link Persistence.TestCase} and verify that the recorded
* states match the states resulting from the rerun.
*
* @param initialContext the context to launch the first Activity from.
* @param testCase the {@link Persistence.TestCase} we are verifying.
*/
void verify(Context initialContext, Persistence.TestCase testCase) {
List<GenerationIntent> initialState = testCase.getSetup().getInitialIntents();
List<GenerationIntent> act = testCase.getSetup().getAct();
List<Activity> activityLog = Lists.newArrayList();
// Launch the first activity from the start context
GenerationIntent firstIntent = initialState.get(0);
activityLog.add(launchFromContext(initialContext, firstIntent.getActualIntent()));
// launch the rest from the initial intents
for (int i = 1; i < initialState.size(); i++) {
GenerationIntent generationIntent = initialState.get(i);
Activity activityToLaunchFrom = activityLog.get(generationIntent.getLaunchFromIndex(i));
Activity result = launch(activityToLaunchFrom, generationIntent.getActualIntent(),
generationIntent.startForResult());
activityLog.add(result);
}
// assert that the state after setup is the same this time as the recorded state.
StateDump setupStateDump = waitDumpAndTrimForVerification(getLast(activityLog),
testCase.getEndState());
assertInitialStateEqual(testCase.getInitialState(), setupStateDump);
// apply all the intents in the act stage
for (int i = 0; i < act.size(); i++) {
GenerationIntent generationIntent = act.get(i);
Activity activityToLaunchFrom = activityLog.get(
generationIntent.getLaunchFromIndex(initialState.size() + i));
Activity result = launch(activityToLaunchFrom, generationIntent.getActualIntent(),
generationIntent.startForResult());
activityLog.add(result);
}
// assert that the endStates are the same.
StateDump endStateDump = waitDumpAndTrimForVerification(getLast(activityLog),
testCase.getEndState());
assertEndStatesEqual(testCase.getEndState(), endStateDump);
}
/**
* Runs a launch object and writes out the resulting {@link Persistence.TestCase} to
* device storage
*
* @param startContext the context to launch the first Activity from.
* @param name the name of the directory to store the json files in.
* @param launches a list of launches to run and record.
*/
public void runAndWrite(Context startContext, String name, List<LaunchSequence> launches)
throws Exception {
for (int i = 0; i < launches.size(); i++) {
Persistence.TestCase testCase = this.runAndSerialize(launches.get(i), startContext,
Integer.toString(i));
IntentTests.writeToDocumentsStorage(testCase, i + 1, name);
// Cleanup all the activities of this testCase before going to the next
// to preserve isolation across test cases.
mTestBase.cleanUp(testCase.getSetup().componentsInCase());
}
}
private Persistence.TestCase runAndSerialize(LaunchSequence launchSequence,
Context startContext, String name) {
LaunchRecord launchRecord = run(launchSequence, startContext);
LaunchSequenceExecutionInfo executionInfo = launchSequence.fold();
List<GenerationIntent> setupIntents = prepareSerialisation(executionInfo.setup);
List<GenerationIntent> actIntents = prepareSerialisation(executionInfo.acts,
setupIntents.size());
Persistence.Setup setup = new Persistence.Setup(setupIntents, actIntents);
return new Persistence.TestCase(setup, launchRecord.initialDump, launchRecord.endDump,
name);
}
/**
* Runs a launch object and returns a {@link LaunchRecord} that can be used to do assertions
* directly in the same test.
*
* @param launch the {@link LaunchSequence}we want to run
* @param startContext the {@link android.content.Context} to launch the first Activity from.
* @return {@link LaunchRecord} that can be used to do assertions.
*/
LaunchRecord run(LaunchSequence launch, Context startContext) {
LaunchSequence.LaunchSequenceExecutionInfo work = launch.fold();
List<Activity> activityLog = Lists.newArrayList();
if (work.setup.isEmpty() || work.acts.isEmpty()) {
throw new IllegalArgumentException("no intents to start");
}
// Launch the first activity from the start context.
LaunchFromIntent firstIntent = work.setup.get(0);
Activity firstActivity = this.launchFromContext(startContext,
firstIntent.getActualIntent());
activityLog.add(firstActivity);
// launch the rest from the initial intents.
for (int i = 1; i < work.setup.size(); i++) {
LaunchFromIntent launchFromIntent = work.setup.get(i);
Intent actualIntent = launchFromIntent.getActualIntent();
Activity activity = launch(activityLog.get(launchFromIntent.getLaunchFrom()),
actualIntent, launchFromIntent.startForResult());
activityLog.add(activity);
}
// record the state after the initial intents.
StateDump initialDump = waitDumpAndTrim(getLast(activityLog));
// apply all the intents in the act stage
for (LaunchFromIntent launchFromIntent : work.acts) {
Intent actualIntent = launchFromIntent.getActualIntent();
Activity activity = launch(activityLog.get(launchFromIntent.getLaunchFrom()),
actualIntent, launchFromIntent.startForResult());
activityLog.add(activity);
}
//record the end state after all intents are launched.
StateDump endDump = waitDumpAndTrim(getLast(activityLog));
return new LaunchRecord(initialDump, endDump, activityLog);
}
/**
* Results from the running of an {@link LaunchSequence} so the user can assert on the results
* directly.
*/
class LaunchRecord {
/**
* The end state after the setup intents.
*/
public final StateDump initialDump;
/**
* The end state after the setup and act intents.
*/
public final StateDump endDump;
/**
* The activities that were started by every intent in the {@link LaunchSequence}.
*/
public final List<Activity> mActivitiesLog;
public LaunchRecord(StateDump initialDump, StateDump endDump,
List<Activity> activitiesLog) {
this.initialDump = initialDump;
this.endDump = endDump;
mActivitiesLog = activitiesLog;
}
}
public Activity launchFromContext(Context context, Intent intent) {
Instrumentation.ActivityMonitor monitor = getInstrumentation()
.addMonitor((String) null, null, false);
context.startActivity(intent);
Activity activity = monitor.waitForActivityWithTimeout(ACTIVITY_LAUNCH_TIMEOUT);
waitAndAssertActivityLaunched(activity, intent);
return activity;
}
public Activity launch(Activity activityContext, Intent intent, boolean startForResult) {
Instrumentation.ActivityMonitor monitor = getInstrumentation()
.addMonitor((String) null, null, false);
if (startForResult) {
activityContext.startActivityForResult(intent, 1);
} else {
activityContext.startActivity(intent);
}
Activity activity = monitor.waitForActivityWithTimeout(ACTIVITY_LAUNCH_TIMEOUT);
if (activity == null) {
return activityContext;
} else {
waitAndAssertActivityLaunched(activity, intent);
}
return activity;
}
private void waitAndAssertActivityLaunched(Activity activity, Intent intent) {
assertNotNull("Intent: " + intent.toString(), activity);
final ComponentName testActivityName = activity.getComponentName();
mTestBase.waitAndAssertTopResumedActivity(testActivityName,
Display.DEFAULT_DISPLAY, "Activity must be resumed");
}
/**
* After the last activity has been launched we wait for a valid state + an extra three seconds
* so have a stable state of the system. Also all previously known stacks in
* {@link LaunchRunner#mBaseStacks} is excluded from the output.
*
* @param activity The last activity to be launched before dumping the state.
* @return A stable {@link StateDump}, meaning no more {@link android.app.Activity} is in a
* life cycle transition.
*/
public StateDump waitDumpAndTrim(Activity activity) {
mTestBase.getWmState().waitForValidState(activity.getComponentName());
// The last activity that was launched before the dump could still be in an intermediate
// lifecycle state. wait an extra 3 seconds for it to settle
SystemClock.sleep(BEFORE_DUMP_TIMEOUT);
mTestBase.getWmState().computeState(activity.getComponentName());
List<WindowManagerState.ActivityTask> endStateStacks =
mTestBase.getWmState().getRootTasks();
return StateDump.fromStacks(endStateStacks, mBaseStacks);
}
/**
* Like {@link LaunchRunner#waitDumpAndTrim(Activity)} but also waits until the state becomes
* equal to the state we expect. It is therefore only used when verifying a recorded testcase.
*
* If we take a dump of an unstable state we allow it to settle into the expected state.
*
* @param activity The last activity to be launched before dumping the state.
* @param expected The state that was previously recorded for this testCase.
* @return A stable {@link StateDump}, meaning no more {@link android.app.Activity} is in a
* life cycle transition.
*/
public StateDump waitDumpAndTrimForVerification(Activity activity, StateDump expected) {
mTestBase.getWmState().waitForValidState(activity.getComponentName());
// The last activity that was launched before the dump could still be in an intermediate
// lifecycle state. wait an extra 3 seconds for it to settle
SystemClock.sleep(BEFORE_DUMP_TIMEOUT);
mTestBase.getWmState().waitForWithAmState(
am -> StateDump.fromStacks(am.getRootTasks(), mBaseStacks).equals(expected),
"the activity states match up with what we recorded");
mTestBase.getWmState().computeState(activity.getComponentName());
List<WindowManagerState.ActivityTask> endStateStacks =
mTestBase.getWmState().getRootTasks();
return StateDump.fromStacks(endStateStacks, mBaseStacks);
}
private List<WindowManagerState.ActivityTask> getBaseStacks() {
WindowManagerStateHelper amWmState = mTestBase.getWmState();
amWmState.computeState(new ComponentName[]{});
return amWmState.getRootTasks();
}
}