Move FlickerTests to frameworks/base/tests 2/2

platform_testing is moving to AOSP first development. FlickerTests may require access to
in development test apis and cannot be developed in AOSP.

Other minor changes:
- remove FlickerTests from APCT. FlickerTests will use TEST_MAPPING
- use @FlakyTest instead of @Ignore annotation to skip tests

Test: atest FlickerTests
Test: make -j32 continuous_instrumentation_tests
Test: cd frameworks/base/tests/FlickerTests/ && atest :postsubmit

Bug: 112044297
Change-Id: Icc3194ddd9b801034d8797bf2ec60ebff071612c
This commit is contained in:
Vishnu Nair
2018-08-01 10:13:36 -07:00
parent 7c79a07c34
commit 8248b7c995
67 changed files with 6465 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
#
# Copyright (C) 2018 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_PACKAGE_NAME := FlickerTests
LOCAL_MODULE_TAGS := tests optional
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_PRIVATE_PLATFORM_APIS := true
LOCAL_CERTIFICATE := platform
LOCAL_COMPATIBILITY_SUITE := device-tests
LOCAL_JAVA_LIBRARIES := android.test.runner
LOCAL_STATIC_JAVA_LIBRARIES := \
flickertestapplib \
flickerlib \
truth-prebuilt \
app-helpers-core
include $(BUILD_PACKAGE)
include $(call all-makefiles-under,$(LOCAL_PATH))

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 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="com.android.server.wm.flicker">
<uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27"/>
<!-- Read and write traces from external storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- Capture screen contents -->
<uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" />
<!-- Run layers trace -->
<uses-permission android:name="android.permission.HARDWARE_TEST"/>
<application>
<uses-library android:name="android.test.runner"/>
</application>
<instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.server.wm.flicker"
android:label="WindowManager Flicker Tests">
</instrumentation>
</manifest>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
* Copyright 2018 Google Inc. All Rights Reserved.
-->
<configuration description="Runs WindowManager Flicker Tests">
<option name="test-tag" value="FlickerTests" />
<target_preparer class="com.google.android.tradefed.targetprep.GoogleDeviceSetup">
<!-- keeps the screen on during tests -->
<option name="screen-always-on" value="on" />
<!-- prevents the phone from restarting -->
<option name="force-skip-system-props" value="true" />
</target_preparer>
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true"/>
<option name="test-file-name" value="FlickerTests.apk"/>
<option name="test-file-name" value="FlickerTestApp.apk" />
</target_preparer>
<test class="com.android.tradefed.testtype.AndroidJUnitTest">
<option name="package" value="com.android.server.wm.flicker"/>
<option name="shell-timeout" value="6600s" />
<option name="test-timeout" value="6000s" />
<option name="hidden-api-checks" value="false" />
</test>
<metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
<option name="directory-keys" value="/sdcard/flicker" />
<option name="collect-on-run-ended-only" value="true" />
</metrics_collector>
</configuration>

View File

@@ -0,0 +1,146 @@
# Flicker Test Library
## Motivation
Detect *flicker* &mdash; any discontinuous, or unpredictable behavior seen during UI transitions that is not due to performance. This is often the result of a logic error in the code and difficult to identify because the issue is transient and at times difficult to reproduce. This library helps create integration tests between `SurfaceFlinger`, `WindowManager` and `SystemUI` to identify flicker.
## Adding a Test
The library builds and runs UI transitions, captures Winscope traces and exposes common assertions that can be tested against each trace.
### Building Transitions
Start by defining common or error prone transitions using `TransitionRunner`.
```java
// Example: Build a transition that cold launches an app from launcher
TransitionRunner transition = TransitionRunner.newBuilder()
// Specify a tag to identify the transition (optional)
.withTag("OpenAppCold_" + testApp.getLauncherName())
// Specify preconditions to setup the device
// Wake up device and go to home screen
.runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen)
// Setup transition under test
// Press the home button and close the app to test a cold start
.runBefore(device::pressHome)
.runBefore(testApp::exit)
// Run the transition under test
// Open the app and wait for UI to be idle
// This is the part of the transition that will be tested.
.run(testApp::open)
.run(device::waitForIdle)
// Perform any tear downs
// Close the app
.runAfterAll(testApp::exit)
// Number of times to repeat the transition to catch any flaky issues
.repeat(5);
```
Run the transition to get a list of `TransitionResult` for each time the transition is repeated.
```java
List<TransitionResult> results = transition.run();
```
`TransitionResult` contains paths to test artifacts such as Winscope traces and screen recordings.
### Checking Assertions
Each `TransitionResult` can be tested using an extension of the Google Truth library, `LayersTraceSubject` and `WmTraceSubject`. They try to balance test principles set out by Google Truth (not supporting nested assertions, keeping assertions simple) with providing support for common assertion use cases.
Each trace can be represented as a ordered collection of trace entries, with an associated timestamp. Each trace entry has common assertion checks. The trace subjects expose methods to filter the range of entries and test for changing assertions.
```java
TransitionResult result = results.get(0);
Rect displayBounds = getDisplayBounds();
// check all trace entries
assertThat(result).coversRegion(displayBounds).forAllEntries();
// check a range of entries
assertThat(result).coversRegion(displayBounds).forRange(startTime, endTime);
// check first entry
assertThat(result).coversRegion(displayBounds).inTheBeginning();
// check last entry
assertThat(result).coversRegion(displayBounds).atTheEnd();
// check a change in assertions, e.g. wallpaper window is visible,
// then wallpaper window becomes and stays invisible
assertThat(result)
.showsBelowAppWindow("wallpaper")
.then()
.hidesBelowAppWindow("wallpaper")
.forAllEntries();
```
All assertions return `Result` which contains a `success` flag, `assertionName` string identifier, and `reason` string to provide actionable details to the user. The `reason` string is build along the way with all the details as to why the assertions failed and any hints which might help the user determine the root cause. Failed assertion message will also contain a path to the trace that was tested. Example of a failed test:
```
java.lang.AssertionError: Not true that <com.android.server.wm.flicker.LayersTrace@65da4cc>
Layers Trace can be found in: /layers_trace_emptyregion.pb
Timestamp: 2308008331271
Assertion: coversRegion
Reason: Region to test: Rect(0, 0 - 1440, 2880)
first empty point: 0, 99
visible regions:
StatusBar#0Rect(0, 0 - 1440, 98)
NavigationBar#0Rect(0, 2712 - 1440, 2880)
ScreenDecorOverlay#0Rect(0, 0 - 1440, 91)
...
at com.google.common.truth.FailureStrategy.fail(FailureStrategy.java:24)
...
```
---
## Running Tests
The tests can be run as any other Android JUnit tests. `platform_testing/tests/flicker` uses the library to test common UI transitions. Run `atest FlickerTest` to execute these tests.
---
## Other Topics
### Monitors
Monitors capture test artifacts for each transition run. They are started before each iteration of the test transition (after the `runBefore` calls) and stopped after the transition is completed. Each iteration will produce a new test artifact. The following monitors are available:
#### LayersTraceMonitor
Captures Layers trace. This monitor is started by default. Build a transition with `skipLayersTrace()` to disable this monitor.
#### WindowManagerTraceMonitor
Captures Window Manager trace. This monitor is started by default. Build a transition with `skipWindowManagerTrace()` to disable this monitor.
#### WindowAnimationFrameStatsMonitor
Captures WindowAnimationFrameStats for the transition. This monitor is started by default and is used to eliminate *janky* runs. If an iteration has skipped frames, as determined by WindowAnimationFrameStats, the results for the iteration is skipped. If the list of results is empty after all iterations are completed, then the test should fail. Build a transition with `includeJankyRuns()` to disable this monitor.
#### ScreenRecorder
Captures screen to a video file. This monitor is disabled by default. Build a transition with `recordEachRun()` to capture each transition or build with `recordAllRuns()` to capture every transition including setup and teardown.
---
### Extending Assertions
To add a new assertion, add a function to one of the trace entry classes, `LayersTrace.Entry` or `WindowManagerTrace.Entry`.
```java
// Example adds an assertion to the check if layer is hidden by parent.
Result isHiddenByParent(String layerName) {
// Result should contain a details if assertion fails for any reason
// such as if layer is not found or layer is not hidden by parent
// or layer has no parent.
// ...
}
```
Then add a function to the trace subject `LayersTraceSubject` or `WmTraceSubject` which will add the assertion for testing. When the assertion is evaluated, the trace will first be filtered then the assertion will be applied to the remaining entries.
```java
public LayersTraceSubject isHiddenByParent(String layerName) {
mChecker.add(entry -> entry.isHiddenByParent(layerName),
"isHiddenByParent(" + layerName + ")");
return this;
}
```
To use the new assertion:
```java
// Check if "Chrome" layer is hidden by parent in the first trace entry.
assertThat(result).isHiddenByParent("Chrome").inTheBeginning();
```

View File

@@ -0,0 +1,7 @@
{
"postsubmit": [
{
"name": "FlickerTests"
}
]
}

View File

@@ -0,0 +1,48 @@
#
# Copyright (C) 2018 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_MODULE := flickerlib
LOCAL_MODULE_TAGS := tests optional
# sign this with platform cert, so this test is allowed to call private platform apis
LOCAL_CERTIFICATE := platform
LOCAL_PRIVATE_PLATFORM_APIS := true
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_STATIC_JAVA_LIBRARIES := \
ub-janktesthelper \
cts-amwm-util \
platformprotosnano \
layersprotosnano \
truth-prebuilt \
sysui-helper \
launcher-helper-lib \
include $(BUILD_STATIC_JAVA_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := flickerautomationhelperlib
LOCAL_SDK_VERSION := current
LOCAL_SRC_FILES := src/com/android/server/wm/flicker/AutomationUtils.java \
src/com/android/server/wm/flicker/WindowUtils.java
LOCAL_STATIC_JAVA_LIBRARIES := sysui-helper \
launcher-helper-lib \
compatibility-device-util
include $(BUILD_STATIC_JAVA_LIBRARY)
include $(call all-makefiles-under,$(LOCAL_PATH))

View File

@@ -0,0 +1,134 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
/**
* Collection of functional interfaces and classes representing assertions and their associated
* results. Assertions are functions that are applied over a single trace entry and returns a
* result which includes a detailed reason if the assertion fails.
*/
class Assertions {
/**
* Checks assertion on a single trace entry.
*
* @param <T> trace entry type to perform the assertion on.
*/
@FunctionalInterface
interface TraceAssertion<T> extends Function<T, Result> {
/**
* Returns an assertion that represents the logical negation of this assertion.
*
* @return a assertion that represents the logical negation of this assertion
*/
default TraceAssertion<T> negate() {
return (T t) -> apply(t).negate();
}
}
/**
* Checks assertion on a single layers trace entry.
*/
@FunctionalInterface
interface LayersTraceAssertion extends TraceAssertion<LayersTrace.Entry> {
}
/**
* Utility class to store assertions with an identifier to help generate more useful debug
* data when dealing with multiple assertions.
*/
static class NamedAssertion<T> {
final TraceAssertion<T> assertion;
final String name;
NamedAssertion(TraceAssertion<T> assertion, String name) {
this.assertion = assertion;
this.name = name;
}
}
/**
* Contains the result of an assertion including the reason for failed assertions.
*/
static class Result {
static final String NEGATION_PREFIX = "!";
final boolean success;
final long timestamp;
final String assertionName;
final String reason;
Result(boolean success, long timestamp, String assertionName, String reason) {
this.success = success;
this.timestamp = timestamp;
this.assertionName = assertionName;
this.reason = reason;
}
Result(boolean success, String reason) {
this.success = success;
this.reason = reason;
this.assertionName = "";
this.timestamp = 0;
}
/**
* Returns the negated {@code Result} and adds a negation prefix to the assertion name.
*/
Result negate() {
String negatedAssertionName;
if (this.assertionName.startsWith(NEGATION_PREFIX)) {
negatedAssertionName = this.assertionName.substring(NEGATION_PREFIX.length() + 1);
} else {
negatedAssertionName = NEGATION_PREFIX + this.assertionName;
}
return new Result(!this.success, this.timestamp, negatedAssertionName, this.reason);
}
boolean passed() {
return this.success;
}
boolean failed() {
return !this.success;
}
@Override
public String toString() {
return "Timestamp: " + prettyTimestamp(timestamp)
+ "\nAssertion: " + assertionName
+ "\nReason: " + reason;
}
private String prettyTimestamp(long timestamp_ns) {
StringBuilder prettyTimestamp = new StringBuilder();
TimeUnit[] timeUnits = {TimeUnit.HOURS, TimeUnit.MINUTES, TimeUnit.SECONDS, TimeUnit
.MILLISECONDS};
String[] unitSuffixes = {"h", "m", "s", "ms"};
for (int i = 0; i < timeUnits.length; i++) {
long convertedTime = timeUnits[i].convert(timestamp_ns, TimeUnit.NANOSECONDS);
timestamp_ns -= TimeUnit.NANOSECONDS.convert(convertedTime, timeUnits[i]);
prettyTimestamp.append(convertedTime).append(unitSuffixes[i]);
}
return prettyTimestamp.toString();
}
}
}

View File

@@ -0,0 +1,183 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import com.android.server.wm.flicker.Assertions.NamedAssertion;
import com.android.server.wm.flicker.Assertions.Result;
import com.android.server.wm.flicker.Assertions.TraceAssertion;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
/**
* Captures some of the common logic in {@link LayersTraceSubject} and {@link WmTraceSubject}
* used to filter trace entries and combine multiple assertions.
*
* @param <T> trace entry type
*/
public class AssertionsChecker<T extends ITraceEntry> {
private boolean mFilterEntriesByRange = false;
private long mFilterStartTime = 0;
private long mFilterEndTime = 0;
private AssertionOption mOption = AssertionOption.NONE;
private List<NamedAssertion<T>> mAssertions = new LinkedList<>();
void add(Assertions.TraceAssertion<T> assertion, String name) {
mAssertions.add(new NamedAssertion<>(assertion, name));
}
void filterByRange(long startTime, long endTime) {
mFilterEntriesByRange = true;
mFilterStartTime = startTime;
mFilterEndTime = endTime;
}
private void setOption(AssertionOption option) {
if (mOption != AssertionOption.NONE && option != mOption) {
throw new IllegalArgumentException("Cannot use " + mOption + " option with "
+ option + " option.");
}
mOption = option;
}
public void checkFirstEntry() {
setOption(AssertionOption.CHECK_FIRST_ENTRY);
}
public void checkLastEntry() {
setOption(AssertionOption.CHECK_LAST_ENTRY);
}
public void checkChangingAssertions() {
setOption(AssertionOption.CHECK_CHANGING_ASSERTIONS);
}
/**
* Filters trace entries then runs assertions returning a list of failures.
*
* @param entries list of entries to perform assertions on
* @return list of failed assertion results
*/
List<Result> test(List<T> entries) {
List<T> filteredEntries;
List<Result> failures;
if (mFilterEntriesByRange) {
filteredEntries = entries.stream()
.filter(e -> ((e.getTimestamp() >= mFilterStartTime)
&& (e.getTimestamp() <= mFilterEndTime)))
.collect(Collectors.toList());
} else {
filteredEntries = entries;
}
switch (mOption) {
case CHECK_CHANGING_ASSERTIONS:
return assertChanges(filteredEntries);
case CHECK_FIRST_ENTRY:
return assertEntry(filteredEntries.get(0));
case CHECK_LAST_ENTRY:
return assertEntry(filteredEntries.get(filteredEntries.size() - 1));
}
return assertAll(filteredEntries);
}
/**
* Steps through each trace entry checking if provided assertions are true in the order they
* are added. Each assertion must be true for at least a single trace entry.
*
* This can be used to check for asserting a change in property over a trace. Such as visibility
* for a window changes from true to false or top-most window changes from A to Bb and back to A
* again.
*/
private List<Result> assertChanges(List<T> entries) {
List<Result> failures = new ArrayList<>();
int entryIndex = 0;
int assertionIndex = 0;
int lastPassedAssertionIndex = -1;
if (mAssertions.size() == 0) {
return failures;
}
while (assertionIndex < mAssertions.size() && entryIndex < entries.size()) {
TraceAssertion<T> currentAssertion = mAssertions.get(assertionIndex).assertion;
Result result = currentAssertion.apply(entries.get(entryIndex));
if (result.passed()) {
lastPassedAssertionIndex = assertionIndex;
entryIndex++;
continue;
}
if (lastPassedAssertionIndex != assertionIndex) {
failures.add(result);
break;
}
assertionIndex++;
if (assertionIndex == mAssertions.size()) {
failures.add(result);
break;
}
}
if (failures.isEmpty()) {
if (assertionIndex != mAssertions.size() - 1) {
String reason = "\nAssertion " + mAssertions.get(assertionIndex).name
+ " never became false";
reason += "\nPassed assertions: " + mAssertions.stream().limit(assertionIndex)
.map(assertion -> assertion.name).collect(Collectors.joining(","));
reason += "\nUntested assertions: " + mAssertions.stream().skip(assertionIndex + 1)
.map(assertion -> assertion.name).collect(Collectors.joining(","));
Result result = new Result(false /* success */, 0 /* timestamp */,
"assertChanges", "Not all assertions passed." + reason);
failures.add(result);
}
}
return failures;
}
private List<Result> assertEntry(T entry) {
List<Result> failures = new ArrayList<>();
for (NamedAssertion<T> assertion : mAssertions) {
Result result = assertion.assertion.apply(entry);
if (result.failed()) {
failures.add(result);
}
}
return failures;
}
private List<Result> assertAll(List<T> entries) {
return mAssertions.stream().flatMap(
assertion -> entries.stream()
.map(assertion.assertion)
.filter(Result::failed))
.collect(Collectors.toList());
}
private enum AssertionOption {
NONE,
CHECK_CHANGING_ASSERTIONS,
CHECK_FIRST_ENTRY,
CHECK_LAST_ENTRY,
}
}

View File

@@ -0,0 +1,260 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static android.os.SystemClock.sleep;
import static android.system.helpers.OverviewHelper.isRecentsInLauncher;
import static android.view.Surface.ROTATION_0;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.RemoteException;
import android.support.test.InstrumentationRegistry;
import android.support.test.launcherhelper.LauncherStrategyFactory;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.BySelector;
import android.support.test.uiautomator.Configurator;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject2;
import android.support.test.uiautomator.Until;
import android.util.Log;
import android.util.Rational;
import android.view.View;
import android.view.ViewConfiguration;
/**
* Collection of UI Automation helper functions.
*/
public class AutomationUtils {
private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
private static final long FIND_TIMEOUT = 10000;
private static final long LONG_PRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout() * 2L;
private static final String TAG = "FLICKER";
public static void wakeUpAndGoToHomeScreen() {
UiDevice device = UiDevice.getInstance(InstrumentationRegistry
.getInstrumentation());
try {
device.wakeUp();
} catch (RemoteException e) {
throw new RuntimeException(e);
}
device.pressHome();
}
/**
* Sets {@link android.app.UiAutomation#waitForIdle(long, long)} global timeout to 0 causing
* the {@link android.app.UiAutomation#waitForIdle(long, long)} function to timeout instantly.
* This removes some delays when using the UIAutomator library required to create fast UI
* transitions.
*/
static void setFastWait() {
Configurator.getInstance().setWaitForIdleTimeout(0);
}
/**
* Reverts {@link android.app.UiAutomation#waitForIdle(long, long)} to default behavior.
*/
static void setDefaultWait() {
Configurator.getInstance().setWaitForIdleTimeout(10000);
}
public static boolean isQuickstepEnabled(UiDevice device) {
return device.findObject(By.res(SYSTEMUI_PACKAGE, "recent_apps")) == null;
}
public static void openQuickstep(UiDevice device) {
if (isQuickstepEnabled(device)) {
int height = device.getDisplayHeight();
UiObject2 navBar = device.findObject(By.res(SYSTEMUI_PACKAGE, "navigation_bar_frame"));
Rect navBarVisibleBounds;
// TODO(vishnun) investigate why this object cannot be found.
if (navBar != null) {
navBarVisibleBounds = navBar.getVisibleBounds();
} else {
Log.e(TAG, "Could not find nav bar, infer location");
navBarVisibleBounds = WindowUtils.getNavigationBarPosition(ROTATION_0);
}
// Swipe from nav bar to 2/3rd down the screen.
device.swipe(
navBarVisibleBounds.centerX(), navBarVisibleBounds.centerY(),
navBarVisibleBounds.centerX(), height * 2 / 3,
(navBarVisibleBounds.centerY() - height * 2 / 3) / 100); // 100 px/step
} else {
try {
device.pressRecentApps();
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
BySelector RECENTS = By.res(SYSTEMUI_PACKAGE, "recents_view");
// use a long timeout to wait until recents populated
if (device.wait(
Until.findObject(isRecentsInLauncher()
? getLauncherOverviewSelector(device) : RECENTS),
10000) == null) {
fail("Recents didn't appear");
}
device.waitForIdle();
}
static void clearRecents(UiDevice device) {
if (isQuickstepEnabled(device)) {
openQuickstep(device);
for (int i = 0; i < 5; i++) {
device.swipe(device.getDisplayWidth() / 2,
device.getDisplayHeight() / 2, device.getDisplayWidth(),
device.getDisplayHeight() / 2,
5);
BySelector clearAllSelector = By.res("com.google.android.apps.nexuslauncher",
"clear_all_button");
UiObject2 clearAllButton = device.wait(Until.findObject(clearAllSelector), 100);
if (clearAllButton != null) {
clearAllButton.click();
return;
}
}
}
}
private static BySelector getLauncherOverviewSelector(UiDevice device) {
return By.res(device.getLauncherPackageName(), "overview_panel");
}
private static void longPressRecents(UiDevice device) {
BySelector recentsSelector = By.res(SYSTEMUI_PACKAGE, "recent_apps");
UiObject2 recentsButton = device.wait(Until.findObject(recentsSelector), FIND_TIMEOUT);
assertNotNull("Unable to find recents button", recentsButton);
recentsButton.click(LONG_PRESS_TIMEOUT);
}
public static void launchSplitScreen(UiDevice device) {
String mLauncherPackage = LauncherStrategyFactory.getInstance(device)
.getLauncherStrategy().getSupportedLauncherPackage();
if (isQuickstepEnabled(device)) {
// Quickstep enabled
openQuickstep(device);
BySelector overviewIconSelector = By.res(mLauncherPackage, "icon")
.clazz(View.class);
UiObject2 overviewIcon = device.wait(Until.findObject(overviewIconSelector),
FIND_TIMEOUT);
assertNotNull("Unable to find app icon in Overview", overviewIcon);
overviewIcon.click();
BySelector splitscreenButtonSelector = By.text("Split screen");
UiObject2 splitscreenButton = device.wait(Until.findObject(splitscreenButtonSelector),
FIND_TIMEOUT);
assertNotNull("Unable to find Split screen button in Overview", overviewIcon);
splitscreenButton.click();
} else {
// Classic long press recents
longPressRecents(device);
}
// Wait for animation to complete.
sleep(2000);
}
public static void exitSplitScreen(UiDevice device) {
if (isQuickstepEnabled(device)) {
// Quickstep enabled
BySelector dividerSelector = By.res(SYSTEMUI_PACKAGE, "docked_divider_handle");
UiObject2 divider = device.wait(Until.findObject(dividerSelector), FIND_TIMEOUT);
assertNotNull("Unable to find Split screen divider", divider);
// Drag the split screen divider to the top of the screen
divider.drag(new Point(device.getDisplayWidth() / 2, 0), 400);
} else {
// Classic long press recents
longPressRecents(device);
}
// Wait for animation to complete.
sleep(2000);
}
static void resizeSplitScreen(UiDevice device, Rational windowHeightRatio) {
BySelector dividerSelector = By.res(SYSTEMUI_PACKAGE, "docked_divider_handle");
UiObject2 divider = device.wait(Until.findObject(dividerSelector), FIND_TIMEOUT);
assertNotNull("Unable to find Split screen divider", divider);
int destHeight =
(int) (WindowUtils.getDisplayBounds().height() * windowHeightRatio.floatValue());
// Drag the split screen divider to so that the ratio of top window height and bottom
// window height is windowHeightRatio
device.drag(divider.getVisibleBounds().centerX(), divider.getVisibleBounds().centerY(),
device.getDisplayWidth() / 2, destHeight, 10);
//divider.drag(new Point(device.getDisplayWidth() / 2, destHeight), 400)
divider = device.wait(Until.findObject(dividerSelector), FIND_TIMEOUT);
// Wait for animation to complete.
sleep(2000);
}
static void closePipWindow(UiDevice device) {
UiObject2 pipWindow = device.findObject(
By.res(SYSTEMUI_PACKAGE, "background"));
pipWindow.click();
UiObject2 exitPipObject = device.findObject(
By.res(SYSTEMUI_PACKAGE, "dismiss"));
exitPipObject.click();
// Wait for animation to complete.
sleep(2000);
}
static void expandPipWindow(UiDevice device) {
UiObject2 pipWindow = device.findObject(
By.res(SYSTEMUI_PACKAGE, "background"));
pipWindow.click();
pipWindow.click();
}
public static void stopPackage(Context context, String packageName) {
runShellCommand("am force-stop " + packageName);
int packageUid;
try {
packageUid = context.getPackageManager().getPackageUid(packageName, /* flags= */0);
} catch (PackageManager.NameNotFoundException e) {
return;
}
while (targetPackageIsRunning(packageUid)) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
//ignore
}
}
}
private static boolean targetPackageIsRunning(int uid) {
final String result = runShellCommand(
String.format("cmd activity get-uid-state %d", uid));
return !result.contains("(NONEXISTENT)");
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright (C) 2018 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.wm.flicker;
/**
* Common interface for Layer and WindowManager trace entries.
*/
interface ITraceEntry {
/**
* @return timestamp of current entry
*/
long getTimestamp();
}

View File

@@ -0,0 +1,412 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import android.annotation.Nullable;
import android.graphics.Rect;
import android.surfaceflinger.nano.Layers.LayerProto;
import android.surfaceflinger.nano.Layers.RectProto;
import android.surfaceflinger.nano.Layers.RegionProto;
import android.surfaceflinger.nano.Layerstrace.LayersTraceFileProto;
import android.surfaceflinger.nano.Layerstrace.LayersTraceProto;
import android.util.SparseArray;
import com.android.server.wm.flicker.Assertions.Result;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Contains a collection of parsed Layers trace entries and assertions to apply over
* a single entry.
*
* Each entry is parsed into a list of {@link LayersTrace.Entry} objects.
*/
public class LayersTrace {
final private List<Entry> mEntries;
@Nullable
final private Path mSource;
private LayersTrace(List<Entry> entries, Path source) {
this.mEntries = entries;
this.mSource = source;
}
/**
* Parses {@code LayersTraceFileProto} from {@code data} and uses the proto to generates a list
* of trace entries, storing the flattened layers into its hierarchical structure.
*
* @param data binary proto data
* @param source Path to source of data for additional debug information
*/
static LayersTrace parseFrom(byte[] data, Path source) {
List<Entry> entries = new ArrayList<>();
LayersTraceFileProto fileProto;
try {
fileProto = LayersTraceFileProto.parseFrom(data);
} catch (Exception e) {
throw new RuntimeException(e);
}
for (LayersTraceProto traceProto : fileProto.entry) {
Entry entry = Entry.fromFlattenedLayers(traceProto.elapsedRealtimeNanos,
traceProto.layers.layers);
entries.add(entry);
}
return new LayersTrace(entries, source);
}
/**
* Parses {@code LayersTraceFileProto} from {@code data} and uses the proto to generates a list
* of trace entries, storing the flattened layers into its hierarchical structure.
*
* @param data binary proto data
*/
static LayersTrace parseFrom(byte[] data) {
return parseFrom(data, null);
}
List<Entry> getEntries() {
return mEntries;
}
Entry getEntry(long timestamp) {
Optional<Entry> entry = mEntries.stream()
.filter(e -> e.getTimestamp() == timestamp)
.findFirst();
if (!entry.isPresent()) {
throw new RuntimeException("Entry does not exist for timestamp " + timestamp);
}
return entry.get();
}
Optional<Path> getSource() {
return Optional.ofNullable(mSource);
}
/**
* Represents a single Layer trace entry.
*/
static class Entry implements ITraceEntry {
private long mTimestamp;
private List<Layer> mRootLayers; // hierarchical representation of layers
private List<Layer> mFlattenedLayers = null;
private Entry(long timestamp, List<Layer> rootLayers) {
this.mTimestamp = timestamp;
this.mRootLayers = rootLayers;
}
/**
* Constructs the layer hierarchy from a flattened list of layers.
*/
static Entry fromFlattenedLayers(long timestamp, LayerProto[] protos) {
SparseArray<Layer> layerMap = new SparseArray<>();
ArrayList<Layer> orphans = new ArrayList<>();
for (LayerProto proto : protos) {
int id = proto.id;
int parentId = proto.parent;
Layer newLayer = layerMap.get(id);
if (newLayer == null) {
newLayer = new Layer(proto);
layerMap.append(id, newLayer);
} else if (newLayer.mProto != null) {
throw new RuntimeException("Duplicate layer id found:" + id);
} else {
newLayer.mProto = proto;
orphans.remove(newLayer);
}
// add parent placeholder
if (layerMap.get(parentId) == null) {
Layer orphanLayer = new Layer(null);
layerMap.append(parentId, orphanLayer);
orphans.add(orphanLayer);
}
layerMap.get(parentId).addChild(newLayer);
newLayer.addParent(layerMap.get(parentId));
}
// Fail if we find orphan layers.
orphans.remove(layerMap.get(-1));
orphans.forEach(orphan -> {
String childNodes = orphan.mChildren.stream().map(node ->
Integer.toString(node.getId())).collect(Collectors.joining(", "));
int orphanId = orphan.mChildren.get(0).mProto.parent;
throw new RuntimeException(
"Failed to parse layers trace. Found orphan layers with parent "
+ "layer id:" + orphanId + " : " + childNodes);
});
return new Entry(timestamp, layerMap.get(-1).mChildren);
}
/**
* Extracts {@link Rect} from {@link RectProto}.
*/
private static Rect extract(RectProto proto) {
return new Rect(proto.left, proto.top, proto.right, proto.bottom);
}
/**
* Extracts {@link Rect} from {@link RegionProto} by returning a rect that encompasses all
* the rects making up the region.
*/
private static Rect extract(RegionProto regionProto) {
Rect region = new Rect();
for (RectProto proto : regionProto.rect) {
region.union(proto.left, proto.top, proto.right, proto.bottom);
}
return region;
}
/**
* Checks if a region specified by {@code testRect} is covered by all visible layers.
*/
Result coversRegion(Rect testRect) {
String assertionName = "coversRegion";
Collection<Layer> layers = asFlattenedLayers();
for (int x = testRect.left; x < testRect.right; x++) {
for (int y = testRect.top; y < testRect.bottom; y++) {
boolean emptyRegionFound = true;
for (Layer layer : layers) {
if (layer.isInvisible() || layer.isHiddenByParent()) {
continue;
}
for (RectProto rectProto : layer.mProto.visibleRegion.rect) {
Rect r = extract(rectProto);
if (r.contains(x, y)) {
y = r.bottom;
emptyRegionFound = false;
}
}
}
if (emptyRegionFound) {
String reason = "Region to test: " + testRect
+ "\nfirst empty point: " + x + ", " + y;
reason += "\nvisible regions:";
for (Layer layer : layers) {
if (layer.isInvisible() || layer.isHiddenByParent()) {
continue;
}
Rect r = extract(layer.mProto.visibleRegion);
reason += "\n" + layer.mProto.name + r.toString();
}
return new Result(false /* success */, this.mTimestamp, assertionName,
reason);
}
}
}
String info = "Region covered: " + testRect;
return new Result(true /* success */, this.mTimestamp, assertionName, info);
}
/**
* Checks if a layer with name {@code layerName} has a visible region
* {@code expectedVisibleRegion}.
*/
Result hasVisibleRegion(String layerName, Rect expectedVisibleRegion) {
String assertionName = "hasVisibleRegion";
String reason = "Could not find " + layerName;
for (Layer layer : asFlattenedLayers()) {
if (layer.mProto.name.contains(layerName)) {
if (layer.isHiddenByParent()) {
reason = layer.getHiddenByParentReason();
continue;
}
if (layer.isInvisible()) {
reason = layer.getVisibilityReason();
continue;
}
Rect visibleRegion = extract(layer.mProto.visibleRegion);
if (visibleRegion.equals(expectedVisibleRegion)) {
return new Result(true /* success */, this.mTimestamp, assertionName,
layer.mProto.name + "has visible region " + expectedVisibleRegion);
}
reason = layer.mProto.name + " has visible region:" + visibleRegion + " "
+ "expected:" + expectedVisibleRegion;
}
}
return new Result(false /* success */, this.mTimestamp, assertionName, reason);
}
/**
* Checks if a layer with name {@code layerName} is visible.
*/
Result isVisible(String layerName) {
String assertionName = "isVisible";
String reason = "Could not find " + layerName;
for (Layer layer : asFlattenedLayers()) {
if (layer.mProto.name.contains(layerName)) {
if (layer.isHiddenByParent()) {
reason = layer.getHiddenByParentReason();
continue;
}
if (layer.isInvisible()) {
reason = layer.getVisibilityReason();
continue;
}
return new Result(true /* success */, this.mTimestamp, assertionName,
layer.mProto.name + " is visible");
}
}
return new Result(false /* success */, this.mTimestamp, assertionName, reason);
}
@Override
public long getTimestamp() {
return mTimestamp;
}
List<Layer> getRootLayers() {
return mRootLayers;
}
List<Layer> asFlattenedLayers() {
if (mFlattenedLayers == null) {
mFlattenedLayers = new ArrayList<>();
ArrayList<Layer> pendingLayers = new ArrayList<>(this.mRootLayers);
while (!pendingLayers.isEmpty()) {
Layer layer = pendingLayers.remove(0);
mFlattenedLayers.add(layer);
pendingLayers.addAll(layer.mChildren);
}
}
return mFlattenedLayers;
}
Rect getVisibleBounds(String layerName) {
List<Layer> layers = asFlattenedLayers();
for (Layer layer : layers) {
if (layer.mProto.name.contains(layerName) && layer.isVisible()) {
return extract(layer.mProto.visibleRegion);
}
}
return new Rect(0, 0, 0, 0);
}
}
/**
* Represents a single layer with links to its parent and child layers.
*/
static class Layer {
@Nullable
LayerProto mProto;
List<Layer> mChildren;
@Nullable
Layer mParent = null;
private Layer(LayerProto proto) {
this.mProto = proto;
this.mChildren = new ArrayList<>();
}
private void addChild(Layer childLayer) {
this.mChildren.add(childLayer);
}
private void addParent(Layer parentLayer) {
this.mParent = parentLayer;
}
int getId() {
return mProto.id;
}
boolean isActiveBufferEmpty() {
return this.mProto.activeBuffer == null || this.mProto.activeBuffer.height == 0
|| this.mProto.activeBuffer.width == 0;
}
boolean isVisibleRegionEmpty() {
if (this.mProto.visibleRegion == null) {
return true;
}
Rect visibleRect = Entry.extract(this.mProto.visibleRegion);
return visibleRect.height() == 0 || visibleRect.width() == 0;
}
boolean isHidden() {
return (this.mProto.flags & /* FLAG_HIDDEN */ 0x1) != 0x0;
}
boolean isVisible() {
return (!isActiveBufferEmpty() || isColorLayer()) &&
!isHidden() && this.mProto.color.a > 0 && !isVisibleRegionEmpty();
}
boolean isColorLayer() {
return this.mProto.type.equals("ColorLayer");
}
boolean isRootLayer() {
return mParent == null || mParent.mProto == null;
}
boolean isInvisible() {
return !isVisible();
}
boolean isHiddenByParent() {
return !isRootLayer() && (mParent.isHidden() || mParent.isHiddenByParent());
}
String getHiddenByParentReason() {
String reason = "Layer " + mProto.name;
if (isHiddenByParent()) {
reason += " is hidden by parent: " + mParent.mProto.name;
} else {
reason += " is not hidden by parent: " + mParent.mProto.name;
}
return reason;
}
String getVisibilityReason() {
String reason = "Layer " + mProto.name;
if (isVisible()) {
reason += " is visible:";
} else {
reason += " is invisible:";
if (this.mProto.activeBuffer == null) {
reason += " activeBuffer=null";
} else if (this.mProto.activeBuffer.height == 0) {
reason += " activeBuffer.height=0";
} else if (this.mProto.activeBuffer.width == 0) {
reason += " activeBuffer.width=0";
}
if (!isColorLayer()) {
reason += " type != ColorLayer";
}
if (isHidden()) {
reason += " flags=" + this.mProto.flags + " (FLAG_HIDDEN set)";
}
if (this.mProto.color.a == 0) {
reason += " color.a=0";
}
if (isVisibleRegionEmpty()) {
reason += " visible region is empty";
}
}
return reason;
}
}
}

View File

@@ -0,0 +1,140 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static com.google.common.truth.Truth.assertAbout;
import static com.google.common.truth.Truth.assertWithMessage;
import android.annotation.Nullable;
import android.graphics.Rect;
import com.android.server.wm.flicker.Assertions.Result;
import com.android.server.wm.flicker.LayersTrace.Entry;
import com.android.server.wm.flicker.TransitionRunner.TransitionResult;
import com.google.common.truth.FailureStrategy;
import com.google.common.truth.Subject;
import com.google.common.truth.SubjectFactory;
import java.util.List;
import java.util.stream.Collectors;
/**
* Truth subject for {@link LayersTrace} objects.
*/
public class LayersTraceSubject extends Subject<LayersTraceSubject, LayersTrace> {
// Boiler-plate Subject.Factory for LayersTraceSubject
private static final SubjectFactory<LayersTraceSubject, LayersTrace> FACTORY =
new SubjectFactory<LayersTraceSubject, LayersTrace>() {
@Override
public LayersTraceSubject getSubject(
FailureStrategy fs, @Nullable LayersTrace target) {
return new LayersTraceSubject(fs, target);
}
};
private AssertionsChecker<Entry> mChecker = new AssertionsChecker<>();
private LayersTraceSubject(FailureStrategy fs, @Nullable LayersTrace subject) {
super(fs, subject);
}
// User-defined entry point
public static LayersTraceSubject assertThat(@Nullable LayersTrace entry) {
return assertAbout(FACTORY).that(entry);
}
// User-defined entry point
public static LayersTraceSubject assertThat(@Nullable TransitionResult result) {
LayersTrace entries = LayersTrace.parseFrom(result.getLayersTrace(),
result.getLayersTracePath());
return assertWithMessage(result.toString()).about(FACTORY).that(entries);
}
// Static method for getting the subject factory (for use with assertAbout())
public static SubjectFactory<LayersTraceSubject, LayersTrace> entries() {
return FACTORY;
}
public void forAllEntries() {
test();
}
public void forRange(long startTime, long endTime) {
mChecker.filterByRange(startTime, endTime);
test();
}
public LayersTraceSubject then() {
mChecker.checkChangingAssertions();
return this;
}
public void inTheBeginning() {
if (getSubject().getEntries().isEmpty()) {
fail("No entries found.");
}
mChecker.checkFirstEntry();
test();
}
public void atTheEnd() {
if (getSubject().getEntries().isEmpty()) {
fail("No entries found.");
}
mChecker.checkLastEntry();
test();
}
private void test() {
List<Result> failures = mChecker.test(getSubject().getEntries());
if (!failures.isEmpty()) {
String failureLogs = failures.stream().map(Result::toString)
.collect(Collectors.joining("\n"));
String tracePath = "";
if (getSubject().getSource().isPresent()) {
tracePath = "\nLayers Trace can be found in: "
+ getSubject().getSource().get().toAbsolutePath() + "\n";
}
fail(tracePath + failureLogs);
}
}
public LayersTraceSubject coversRegion(Rect rect) {
mChecker.add(entry -> entry.coversRegion(rect),
"coversRegion(" + rect + ")");
return this;
}
public LayersTraceSubject hasVisibleRegion(String layerName, Rect size) {
mChecker.add(entry -> entry.hasVisibleRegion(layerName, size),
"hasVisibleRegion(" + layerName + size + ")");
return this;
}
public LayersTraceSubject showsLayer(String layerName) {
mChecker.add(entry -> entry.isVisible(layerName),
"showsLayer(" + layerName + ")");
return this;
}
public LayersTraceSubject hidesLayer(String layerName) {
mChecker.add(entry -> entry.isVisible(layerName).negate(),
"hidesLayer(" + layerName + ")");
return this;
}
}

View File

@@ -0,0 +1,423 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import android.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.test.InstrumentationRegistry;
import android.util.Log;
import com.android.server.wm.flicker.monitor.ITransitionMonitor;
import com.android.server.wm.flicker.monitor.LayersTraceMonitor;
import com.android.server.wm.flicker.monitor.ScreenRecorder;
import com.android.server.wm.flicker.monitor.WindowAnimationFrameStatsMonitor;
import com.android.server.wm.flicker.monitor.WindowManagerTraceMonitor;
import com.google.common.io.Files;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* Builds and runs UI transitions capturing test artifacts.
*
* User can compose a transition from simpler steps, specifying setup and teardown steps. During
* a transition, Layers trace, WindowManager trace, screen recordings and window animation frame
* stats can be captured.
*
* <pre>
* Transition builder options:
* {@link TransitionBuilder#run(Runnable)} run transition under test. Monitors will be started
* before the transition and stopped after the transition is completed.
* {@link TransitionBuilder#repeat(int)} repeat transitions under test multiple times recording
* result for each run.
* {@link TransitionBuilder#withTag(String)} specify a string identifier used to prefix logs and
* artifacts generated.
* {@link TransitionBuilder#runBeforeAll(Runnable)} run setup transitions once before all other
* transition are run to set up an initial state on device.
* {@link TransitionBuilder#runBefore(Runnable)} run setup transitions before each test transition
* run.
* {@link TransitionBuilder#runAfter(Runnable)} run teardown transitions after each test
* transition.
* {@link TransitionBuilder#runAfter(Runnable)} run teardown transitions once after all
* other transition are run.
* {@link TransitionBuilder#includeJankyRuns()} disables {@link WindowAnimationFrameStatsMonitor}
* to monitor janky frames. If janky frames are detected, then the test run is skipped. This
* monitor is enabled by default.
* {@link TransitionBuilder#skipLayersTrace()} disables {@link LayersTraceMonitor} used to
* capture Layers trace during a transition. This monitor is enabled by default.
* {@link TransitionBuilder#skipWindowManagerTrace()} disables {@link WindowManagerTraceMonitor}
* used to capture WindowManager trace during a transition. This monitor is enabled by
* default.
* {@link TransitionBuilder#recordAllRuns()} records the screen contents and saves it to a file.
* All the runs including setup and teardown transitions are included in the recording. This
* monitor is used for debugging purposes.
* {@link TransitionBuilder#recordEachRun()} records the screen contents during test transitions
* and saves it to a file for each run. This monitor is used for debugging purposes.
*
* Example transition to capture WindowManager and Layers trace when opening a test app:
* {@code
* TransitionRunner.newBuilder()
* .withTag("OpenTestAppFast")
* .runBeforeAll(UiAutomationLib::wakeUp)
* .runBeforeAll(UiAutomationLib::UnlockDevice)
* .runBeforeAll(UiAutomationLib::openTestApp)
* .runBefore(UiAutomationLib::closeTestApp)
* .run(UiAutomationLib::openTestApp)
* .runAfterAll(UiAutomationLib::closeTestApp)
* .repeat(5)
* .build()
* .run();
* }
* </pre>
*/
class TransitionRunner {
private static final String TAG = "FLICKER";
private final ScreenRecorder mScreenRecorder;
private final WindowManagerTraceMonitor mWmTraceMonitor;
private final LayersTraceMonitor mLayersTraceMonitor;
private final WindowAnimationFrameStatsMonitor mFrameStatsMonitor;
private final List<ITransitionMonitor> mAllRunsMonitors;
private final List<ITransitionMonitor> mPerRunMonitors;
private final List<Runnable> mBeforeAlls;
private final List<Runnable> mBefores;
private final List<Runnable> mTransitions;
private final List<Runnable> mAfters;
private final List<Runnable> mAfterAlls;
private final int mIterations;
private final String mTestTag;
@Nullable
private List<TransitionResult> mResults = null;
private TransitionRunner(TransitionBuilder builder) {
mScreenRecorder = builder.mScreenRecorder;
mWmTraceMonitor = builder.mWmTraceMonitor;
mLayersTraceMonitor = builder.mLayersTraceMonitor;
mFrameStatsMonitor = builder.mFrameStatsMonitor;
mAllRunsMonitors = builder.mAllRunsMonitors;
mPerRunMonitors = builder.mPerRunMonitors;
mBeforeAlls = builder.mBeforeAlls;
mBefores = builder.mBefores;
mTransitions = builder.mTransitions;
mAfters = builder.mAfters;
mAfterAlls = builder.mAfterAlls;
mIterations = builder.mIterations;
mTestTag = builder.mTestTag;
}
static TransitionBuilder newBuilder() {
return new TransitionBuilder();
}
/**
* Runs the composed transition and calls monitors at the appropriate stages. If jank monitor
* is enabled, transitions with jank are skipped.
*
* @return itself
*/
TransitionRunner run() {
mResults = new ArrayList<>();
mAllRunsMonitors.forEach(ITransitionMonitor::start);
mBeforeAlls.forEach(Runnable::run);
for (int iteration = 0; iteration < mIterations; iteration++) {
mBefores.forEach(Runnable::run);
mPerRunMonitors.forEach(ITransitionMonitor::start);
mTransitions.forEach(Runnable::run);
mPerRunMonitors.forEach(ITransitionMonitor::stop);
mAfters.forEach(Runnable::run);
if (runJankFree() && mFrameStatsMonitor.jankyFramesDetected()) {
String msg = String.format("Skipping iteration %d/%d for test %s due to jank. %s",
iteration, mIterations - 1, mTestTag, mFrameStatsMonitor.toString());
Log.e(TAG, msg);
continue;
}
mResults.add(saveResult(iteration));
}
mAfterAlls.forEach(Runnable::run);
mAllRunsMonitors.forEach(monitor -> {
monitor.stop();
Path path = monitor.save(mTestTag);
Log.e(TAG, "Video saved to " + path.toString());
});
return this;
}
/**
* Returns a list of transition results.
*
* @return list of transition results.
*/
List<TransitionResult> getResults() {
if (mResults == null) {
throw new IllegalStateException("Results do not exist!");
}
return mResults;
}
/**
* Deletes all transition results that are not marked for saving.
*
* @return list of transition results.
*/
void deleteResults() {
if (mResults == null) {
return;
}
mResults.stream()
.filter(TransitionResult::canDelete)
.forEach(TransitionResult::delete);
mResults = null;
}
/**
* Saves monitor results to file.
*
* @return object containing paths to test artifacts
*/
private TransitionResult saveResult(int iteration) {
Path windowTrace = null;
Path layerTrace = null;
Path screenCaptureVideo = null;
if (mPerRunMonitors.contains(mWmTraceMonitor)) {
windowTrace = mWmTraceMonitor.save(mTestTag, iteration);
}
if (mPerRunMonitors.contains(mLayersTraceMonitor)) {
layerTrace = mLayersTraceMonitor.save(mTestTag, iteration);
}
if (mPerRunMonitors.contains(mScreenRecorder)) {
screenCaptureVideo = mScreenRecorder.save(mTestTag, iteration);
}
return new TransitionResult(layerTrace, windowTrace, screenCaptureVideo);
}
private boolean runJankFree() {
return mPerRunMonitors.contains(mFrameStatsMonitor);
}
public String getTestTag() {
return mTestTag;
}
/**
* Stores paths to all test artifacts.
*/
@VisibleForTesting
public static class TransitionResult {
@Nullable
final Path layersTrace;
@Nullable
final Path windowManagerTrace;
@Nullable
final Path screenCaptureVideo;
private boolean flaggedForSaving;
TransitionResult(@Nullable Path layersTrace, @Nullable Path windowManagerTrace,
@Nullable Path screenCaptureVideo) {
this.layersTrace = layersTrace;
this.windowManagerTrace = windowManagerTrace;
this.screenCaptureVideo = screenCaptureVideo;
}
void flagForSaving() {
flaggedForSaving = true;
}
boolean canDelete() {
return !flaggedForSaving;
}
boolean layersTraceExists() {
return layersTrace != null && layersTrace.toFile().exists();
}
byte[] getLayersTrace() {
try {
return Files.toByteArray(this.layersTrace.toFile());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Path getLayersTracePath() {
return layersTrace;
}
boolean windowManagerTraceExists() {
return windowManagerTrace != null && windowManagerTrace.toFile().exists();
}
public byte[] getWindowManagerTrace() {
try {
return Files.toByteArray(this.windowManagerTrace.toFile());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
Path getWindowManagerTracePath() {
return windowManagerTrace;
}
boolean screenCaptureVideoExists() {
return screenCaptureVideo != null && screenCaptureVideo.toFile().exists();
}
Path screenCaptureVideoPath() {
return screenCaptureVideo;
}
void delete() {
if (layersTraceExists()) layersTrace.toFile().delete();
if (windowManagerTraceExists()) windowManagerTrace.toFile().delete();
if (screenCaptureVideoExists()) screenCaptureVideo.toFile().delete();
}
}
/**
* Builds a {@link TransitionRunner} instance.
*/
static class TransitionBuilder {
private ScreenRecorder mScreenRecorder;
private WindowManagerTraceMonitor mWmTraceMonitor;
private LayersTraceMonitor mLayersTraceMonitor;
private WindowAnimationFrameStatsMonitor mFrameStatsMonitor;
private List<ITransitionMonitor> mAllRunsMonitors = new LinkedList<>();
private List<ITransitionMonitor> mPerRunMonitors = new LinkedList<>();
private List<Runnable> mBeforeAlls = new LinkedList<>();
private List<Runnable> mBefores = new LinkedList<>();
private List<Runnable> mTransitions = new LinkedList<>();
private List<Runnable> mAfters = new LinkedList<>();
private List<Runnable> mAfterAlls = new LinkedList<>();
private boolean mRunJankFree = true;
private boolean mCaptureWindowManagerTrace = true;
private boolean mCaptureLayersTrace = true;
private boolean mRecordEachRun = false;
private int mIterations = 1;
private String mTestTag = "";
private boolean mRecordAllRuns = false;
TransitionBuilder() {
mScreenRecorder = new ScreenRecorder();
mWmTraceMonitor = new WindowManagerTraceMonitor();
mLayersTraceMonitor = new LayersTraceMonitor();
mFrameStatsMonitor = new
WindowAnimationFrameStatsMonitor(InstrumentationRegistry.getInstrumentation());
}
TransitionRunner build() {
if (mCaptureWindowManagerTrace) {
mPerRunMonitors.add(mWmTraceMonitor);
}
if (mCaptureLayersTrace) {
mPerRunMonitors.add(mLayersTraceMonitor);
}
if (mRunJankFree) {
mPerRunMonitors.add(mFrameStatsMonitor);
}
if (mRecordAllRuns) {
mAllRunsMonitors.add(mScreenRecorder);
}
if (mRecordEachRun) {
mPerRunMonitors.add(mScreenRecorder);
}
return new TransitionRunner(this);
}
TransitionBuilder runBeforeAll(Runnable runnable) {
mBeforeAlls.add(runnable);
return this;
}
TransitionBuilder runBefore(Runnable runnable) {
mBefores.add(runnable);
return this;
}
TransitionBuilder run(Runnable runnable) {
mTransitions.add(runnable);
return this;
}
TransitionBuilder runAfter(Runnable runnable) {
mAfters.add(runnable);
return this;
}
TransitionBuilder runAfterAll(Runnable runnable) {
mAfterAlls.add(runnable);
return this;
}
TransitionBuilder repeat(int iterations) {
mIterations = iterations;
return this;
}
TransitionBuilder skipWindowManagerTrace() {
mCaptureWindowManagerTrace = false;
return this;
}
TransitionBuilder skipLayersTrace() {
mCaptureLayersTrace = false;
return this;
}
TransitionBuilder includeJankyRuns() {
mRunJankFree = false;
return this;
}
TransitionBuilder recordEachRun() {
if (mRecordAllRuns) {
throw new IllegalArgumentException("Invalid option with recordAllRuns");
}
mRecordEachRun = true;
return this;
}
TransitionBuilder recordAllRuns() {
if (mRecordEachRun) {
throw new IllegalArgumentException("Invalid option with recordEachRun");
}
mRecordAllRuns = true;
return this;
}
TransitionBuilder withTag(String testTag) {
mTestTag = testTag;
return this;
}
}
}

View File

@@ -0,0 +1,240 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import android.annotation.Nullable;
import com.android.server.wm.flicker.Assertions.Result;
import com.android.server.wm.nano.AppWindowTokenProto;
import com.android.server.wm.nano.StackProto;
import com.android.server.wm.nano.TaskProto;
import com.android.server.wm.nano.WindowManagerTraceFileProto;
import com.android.server.wm.nano.WindowManagerTraceProto;
import com.android.server.wm.nano.WindowStateProto;
import com.android.server.wm.nano.WindowTokenProto;
import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* Contains a collection of parsed WindowManager trace entries and assertions to apply over
* a single entry.
*
* Each entry is parsed into a list of {@link WindowManagerTrace.Entry} objects.
*/
public class WindowManagerTrace {
private static final int DEFAULT_DISPLAY = 0;
private final List<Entry> mEntries;
@Nullable
final private Path mSource;
private WindowManagerTrace(List<Entry> entries, Path source) {
this.mEntries = entries;
this.mSource = source;
}
/**
* Parses {@code WindowManagerTraceFileProto} from {@code data} and uses the proto to
* generates a list of trace entries.
*
* @param data binary proto data
* @param source Path to source of data for additional debug information
*/
static WindowManagerTrace parseFrom(byte[] data, Path source) {
List<Entry> entries = new ArrayList<>();
WindowManagerTraceFileProto fileProto;
try {
fileProto = WindowManagerTraceFileProto.parseFrom(data);
} catch (InvalidProtocolBufferNanoException e) {
throw new RuntimeException(e);
}
for (WindowManagerTraceProto entryProto : fileProto.entry) {
entries.add(new Entry(entryProto));
}
return new WindowManagerTrace(entries, source);
}
static WindowManagerTrace parseFrom(byte[] data) {
return parseFrom(data, null);
}
public List<Entry> getEntries() {
return mEntries;
}
Entry getEntry(long timestamp) {
Optional<Entry> entry = mEntries.stream()
.filter(e -> e.getTimestamp() == timestamp)
.findFirst();
if (!entry.isPresent()) {
throw new RuntimeException("Entry does not exist for timestamp " + timestamp);
}
return entry.get();
}
Optional<Path> getSource() {
return Optional.ofNullable(mSource);
}
/**
* Represents a single WindowManager trace entry.
*/
static class Entry implements ITraceEntry {
private final WindowManagerTraceProto mProto;
Entry(WindowManagerTraceProto proto) {
mProto = proto;
}
private static Result isWindowVisible(String windowTitle,
WindowTokenProto[] windowTokenProtos) {
boolean titleFound = false;
for (WindowTokenProto windowToken : windowTokenProtos) {
for (WindowStateProto windowState : windowToken.windows) {
if (windowState.identifier.title.contains(windowTitle)) {
titleFound = true;
if (isVisible(windowState)) {
return new Result(true /* success */,
windowState.identifier.title + " is visible");
}
}
}
}
String reason;
if (!titleFound) {
reason = windowTitle + " cannot be found";
} else {
reason = windowTitle + " is invisible";
}
return new Result(false /* success */, reason);
}
private static boolean isVisible(WindowStateProto windowState) {
return windowState.windowContainer.visible;
}
@Override
public long getTimestamp() {
return mProto.elapsedRealtimeNanos;
}
/**
* Returns window title of the top most visible app window.
*/
private String getTopVisibleAppWindow() {
StackProto[] stacks = mProto.windowManagerService.rootWindowContainer
.displays[DEFAULT_DISPLAY].stacks;
for (StackProto stack : stacks) {
for (TaskProto task : stack.tasks) {
for (AppWindowTokenProto token : task.appWindowTokens) {
for (WindowStateProto windowState : token.windowToken.windows) {
if (windowState.windowContainer.visible) {
return task.appWindowTokens[0].name;
}
}
}
}
}
return "";
}
/**
* Checks if aboveAppWindow with {@code windowTitle} is visible.
*/
Result isAboveAppWindowVisible(String windowTitle) {
WindowTokenProto[] windowTokenProtos = mProto.windowManagerService
.rootWindowContainer
.displays[DEFAULT_DISPLAY].aboveAppWindows;
Result result = isWindowVisible(windowTitle, windowTokenProtos);
return new Result(result.success, getTimestamp(), "showsAboveAppWindow", result.reason);
}
/**
* Checks if belowAppWindow with {@code windowTitle} is visible.
*/
Result isBelowAppWindowVisible(String windowTitle) {
WindowTokenProto[] windowTokenProtos = mProto.windowManagerService
.rootWindowContainer
.displays[DEFAULT_DISPLAY].belowAppWindows;
Result result = isWindowVisible(windowTitle, windowTokenProtos);
return new Result(result.success, getTimestamp(), "isBelowAppWindowVisible",
result.reason);
}
/**
* Checks if imeWindow with {@code windowTitle} is visible.
*/
Result isImeWindowVisible(String windowTitle) {
WindowTokenProto[] windowTokenProtos = mProto.windowManagerService
.rootWindowContainer
.displays[DEFAULT_DISPLAY].imeWindows;
Result result = isWindowVisible(windowTitle, windowTokenProtos);
return new Result(result.success, getTimestamp(), "isImeWindowVisible",
result.reason);
}
/**
* Checks if app window with {@code windowTitle} is on top.
*/
Result isVisibleAppWindowOnTop(String windowTitle) {
String topAppWindow = getTopVisibleAppWindow();
boolean success = topAppWindow.contains(windowTitle);
String reason = "wanted=" + windowTitle + " found=" + topAppWindow;
return new Result(success, getTimestamp(), "isAppWindowOnTop", reason);
}
/**
* Checks if app window with {@code windowTitle} is visible.
*/
Result isAppWindowVisible(String windowTitle) {
final String assertionName = "isAppWindowVisible";
boolean titleFound = false;
StackProto[] stacks = mProto.windowManagerService.rootWindowContainer
.displays[DEFAULT_DISPLAY].stacks;
for (StackProto stack : stacks) {
for (TaskProto task : stack.tasks) {
for (AppWindowTokenProto token : task.appWindowTokens) {
if (token.name.contains(windowTitle)) {
titleFound = true;
for (WindowStateProto windowState : token.windowToken.windows) {
if (windowState.windowContainer.visible) {
return new Result(true /* success */, getTimestamp(),
assertionName, "Window " + token.name +
"is visible");
}
}
}
}
}
}
String reason;
if (!titleFound) {
reason = "Window " + windowTitle + " cannot be found";
} else {
reason = "Window " + windowTitle + " is invisible";
}
return new Result(false /* success */, getTimestamp(), assertionName, reason);
}
}
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.support.test.InstrumentationRegistry;
import android.view.Surface;
import android.view.WindowManager;
/**
* Helper functions to retrieve system window sizes and positions.
*/
class WindowUtils {
static Rect getDisplayBounds() {
Point display = new Point();
WindowManager wm =
(WindowManager) InstrumentationRegistry.getContext().getSystemService(
Context.WINDOW_SERVICE);
wm.getDefaultDisplay().getRealSize(display);
return new Rect(0, 0, display.x, display.y);
}
private static int getCurrentRotation() {
WindowManager wm =
(WindowManager) InstrumentationRegistry.getContext().getSystemService(
Context.WINDOW_SERVICE);
return wm.getDefaultDisplay().getRotation();
}
static Rect getDisplayBounds(int requestedRotation) {
Rect displayBounds = getDisplayBounds();
int currentDisplayRotation = getCurrentRotation();
boolean displayIsRotated = (currentDisplayRotation == Surface.ROTATION_90 ||
currentDisplayRotation == Surface.ROTATION_270);
boolean requestedDisplayIsRotated = requestedRotation == Surface.ROTATION_90 ||
requestedRotation == Surface.ROTATION_270;
// if the current orientation changes with the requested rotation,
// flip height and width of display bounds.
if (displayIsRotated != requestedDisplayIsRotated) {
return new Rect(0, 0, displayBounds.height(), displayBounds.width());
}
return new Rect(0, 0, displayBounds.width(), displayBounds.height());
}
static Rect getAppPosition(int requestedRotation) {
Rect displayBounds = getDisplayBounds();
int currentDisplayRotation = getCurrentRotation();
boolean displayIsRotated = currentDisplayRotation == Surface.ROTATION_90 ||
currentDisplayRotation == Surface.ROTATION_270;
boolean requestedAppIsRotated = requestedRotation == Surface.ROTATION_90 ||
requestedRotation == Surface.ROTATION_270;
// display size will change if the display is reflected. Flip height and width of app if the
// requested rotation is different from the current rotation.
if (displayIsRotated != requestedAppIsRotated) {
return new Rect(0, 0, displayBounds.height(), displayBounds.width());
}
return new Rect(0, 0, displayBounds.width(), displayBounds.height());
}
static Rect getStatusBarPosition(int requestedRotation) {
Resources resources = InstrumentationRegistry.getContext().getResources();
String resourceName;
Rect displayBounds = getDisplayBounds();
int width;
if (requestedRotation == Surface.ROTATION_0 || requestedRotation == Surface.ROTATION_180) {
resourceName = "status_bar_height_portrait";
width = Math.min(displayBounds.width(), displayBounds.height());
} else {
resourceName = "status_bar_height_landscape";
width = Math.max(displayBounds.width(), displayBounds.height());
}
int resourceId = resources.getIdentifier(resourceName, "dimen", "android");
int height = resources.getDimensionPixelSize(resourceId);
return new Rect(0, 0, width, height);
}
static Rect getNavigationBarPosition(int requestedRotation) {
Resources resources = InstrumentationRegistry.getContext().getResources();
Rect displayBounds = getDisplayBounds();
int displayWidth = Math.min(displayBounds.width(), displayBounds.height());
int displayHeight = Math.max(displayBounds.width(), displayBounds.height());
int resourceId;
if (requestedRotation == Surface.ROTATION_0 || requestedRotation == Surface.ROTATION_180) {
resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
int height = resources.getDimensionPixelSize(resourceId);
return new Rect(0, displayHeight - height, displayWidth, displayHeight);
} else {
resourceId = resources.getIdentifier("navigation_bar_width", "dimen", "android");
int width = resources.getDimensionPixelSize(resourceId);
// swap display dimensions in landscape or seascape mode
int temp = displayHeight;
displayHeight = displayWidth;
displayWidth = temp;
if (requestedRotation == Surface.ROTATION_90) {
return new Rect(0, 0, width, displayHeight);
} else {
return new Rect(displayWidth - width, 0, displayWidth, displayHeight);
}
}
}
static int getNavigationBarHeight() {
Resources resources = InstrumentationRegistry.getContext().getResources();
int resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android");
return resources.getDimensionPixelSize(resourceId);
}
static int getDockedStackDividerInset() {
Resources resources = InstrumentationRegistry.getContext().getResources();
int resourceId = resources.getIdentifier("docked_stack_divider_insets", "dimen",
"android");
return resources.getDimensionPixelSize(resourceId);
}
}

View File

@@ -0,0 +1,193 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static com.google.common.truth.Truth.assertAbout;
import static com.google.common.truth.Truth.assertWithMessage;
import android.annotation.Nullable;
import com.android.server.wm.flicker.Assertions.Result;
import com.android.server.wm.flicker.TransitionRunner.TransitionResult;
import com.google.common.truth.FailureStrategy;
import com.google.common.truth.Subject;
import com.google.common.truth.SubjectFactory;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Truth subject for {@link WindowManagerTrace} objects.
*/
public class WmTraceSubject extends Subject<WmTraceSubject, WindowManagerTrace> {
// Boiler-plate Subject.Factory for WmTraceSubject
private static final SubjectFactory<WmTraceSubject, WindowManagerTrace> FACTORY =
new SubjectFactory<WmTraceSubject, WindowManagerTrace>() {
@Override
public WmTraceSubject getSubject(
FailureStrategy fs, @Nullable WindowManagerTrace target) {
return new WmTraceSubject(fs, target);
}
};
private AssertionsChecker<WindowManagerTrace.Entry> mChecker = new AssertionsChecker<>();
private WmTraceSubject(FailureStrategy fs, @Nullable WindowManagerTrace subject) {
super(fs, subject);
}
// User-defined entry point
public static WmTraceSubject assertThat(@Nullable WindowManagerTrace entry) {
return assertAbout(FACTORY).that(entry);
}
// User-defined entry point
public static WmTraceSubject assertThat(@Nullable TransitionResult result) {
WindowManagerTrace entries = WindowManagerTrace.parseFrom(result.getWindowManagerTrace(),
result.getWindowManagerTracePath());
return assertWithMessage(result.toString()).about(FACTORY).that(entries);
}
// Static method for getting the subject factory (for use with assertAbout())
public static SubjectFactory<WmTraceSubject, WindowManagerTrace> entries() {
return FACTORY;
}
public void forAllEntries() {
test();
}
public void forRange(long startTime, long endTime) {
mChecker.filterByRange(startTime, endTime);
test();
}
public WmTraceSubject then() {
mChecker.checkChangingAssertions();
return this;
}
public void inTheBeginning() {
if (getSubject().getEntries().isEmpty()) {
fail("No entries found.");
}
mChecker.checkFirstEntry();
test();
}
public void atTheEnd() {
if (getSubject().getEntries().isEmpty()) {
fail("No entries found.");
}
mChecker.checkLastEntry();
test();
}
private void test() {
List<Result> failures = mChecker.test(getSubject().getEntries());
if (!failures.isEmpty()) {
Optional<Path> failureTracePath = getSubject().getSource();
String failureLogs = failures.stream().map(Result::toString)
.collect(Collectors.joining("\n"));
String tracePath = "";
if (failureTracePath.isPresent()) {
tracePath = "\nWindowManager Trace can be found in: "
+ failureTracePath.get().toAbsolutePath() + "\n";
}
fail(tracePath + failureLogs);
}
}
public WmTraceSubject showsAboveAppWindow(String partialWindowTitle) {
mChecker.add(entry -> entry.isAboveAppWindowVisible(partialWindowTitle),
"showsAboveAppWindow(" + partialWindowTitle + ")");
return this;
}
public WmTraceSubject hidesAboveAppWindow(String partialWindowTitle) {
mChecker.add(entry -> entry.isAboveAppWindowVisible(partialWindowTitle).negate(),
"hidesAboveAppWindow" + "(" + partialWindowTitle + ")");
return this;
}
public WmTraceSubject showsBelowAppWindow(String partialWindowTitle) {
mChecker.add(entry -> entry.isBelowAppWindowVisible(partialWindowTitle),
"showsBelowAppWindow(" + partialWindowTitle + ")");
return this;
}
public WmTraceSubject hidesBelowAppWindow(String partialWindowTitle) {
mChecker.add(entry -> entry.isBelowAppWindowVisible(partialWindowTitle).negate(),
"hidesBelowAppWindow" + "(" + partialWindowTitle + ")");
return this;
}
public WmTraceSubject showsImeWindow(String partialWindowTitle) {
mChecker.add(entry -> entry.isImeWindowVisible(partialWindowTitle),
"showsBelowAppWindow(" + partialWindowTitle + ")");
return this;
}
public WmTraceSubject hidesImeWindow(String partialWindowTitle) {
mChecker.add(entry -> entry.isImeWindowVisible(partialWindowTitle).negate(),
"hidesImeWindow" + "(" + partialWindowTitle + ")");
return this;
}
public WmTraceSubject showsAppWindowOnTop(String partialWindowTitle) {
mChecker.add(
entry -> {
Result result = entry.isAppWindowVisible(partialWindowTitle);
if (result.passed()) {
result = entry.isVisibleAppWindowOnTop(partialWindowTitle);
}
return result;
},
"showsAppWindowOnTop(" + partialWindowTitle + ")"
);
return this;
}
public WmTraceSubject hidesAppWindowOnTop(String partialWindowTitle) {
mChecker.add(
entry -> {
Result result = entry.isAppWindowVisible(partialWindowTitle).negate();
if (result.failed()) {
result = entry.isVisibleAppWindowOnTop(partialWindowTitle).negate();
}
return result;
},
"hidesAppWindowOnTop(" + partialWindowTitle + ")"
);
return this;
}
public WmTraceSubject showsAppWindow(String partialWindowTitle) {
mChecker.add(entry -> entry.isAppWindowVisible(partialWindowTitle),
"showsAppWindow(" + partialWindowTitle + ")");
return this;
}
public WmTraceSubject hidesAppWindow(String partialWindowTitle) {
mChecker.add(entry -> entry.isAppWindowVisible(partialWindowTitle).negate(),
"hidesAppWindow(" + partialWindowTitle + ")");
return this;
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright (C) 2018 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.wm.flicker.monitor;
import android.os.Environment;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Collects test artifacts during a UI transition.
*/
public interface ITransitionMonitor {
Path OUTPUT_DIR = Paths.get(Environment.getExternalStorageDirectory().toString(), "flicker");
/**
* Starts monitor.
*/
void start();
/**
* Stops monitor.
*/
void stop();
/**
* Saves any monitor artifacts to file adding {@code testTag} and {@code iteration}
* to the file name.
*
* @param testTag suffix added to artifact name
* @param iteration suffix added to artifact name
*
* @return Path to saved artifact
*/
default Path save(String testTag, int iteration) {
return save(testTag + "_" + iteration);
}
/**
* Saves any monitor artifacts to file adding {@code testTag} to the file name.
*
* @param testTag suffix added to artifact name
*
* @return Path to saved artifact
*/
default Path save(String testTag) {
throw new UnsupportedOperationException("Save not implemented for this monitor");
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (C) 2018 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.wm.flicker.monitor;
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.Log;
/**
* Captures Layers trace from SurfaceFlinger.
*/
public class LayersTraceMonitor extends TraceMonitor {
private static final String TAG = "LayersTraceMonitor";
private IBinder mSurfaceFlinger = ServiceManager.getService("SurfaceFlinger");
public LayersTraceMonitor() {
traceFileName = "layers_trace.pb";
}
@Override
public void start() {
setEnabled(true);
}
@Override
public void stop() {
setEnabled(false);
}
@Override
public boolean isEnabled() throws RemoteException {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeInterfaceToken("android.ui.ISurfaceComposer");
mSurfaceFlinger.transact(/* LAYER_TRACE_STATUS_CODE */ 1026,
data, reply, 0 /* flags */);
return reply.readBoolean();
}
private void setEnabled(boolean isEnabled) {
Parcel data = null;
try {
if (mSurfaceFlinger != null) {
data = Parcel.obtain();
data.writeInterfaceToken("android.ui.ISurfaceComposer");
data.writeInt(isEnabled ? 1 : 0);
mSurfaceFlinger.transact( /* LAYER_TRACE_CONTROL_CODE */ 1025,
data, null, 0 /* flags */);
}
} catch (RemoteException e) {
Log.e(TAG, "Could not set layer tracing." + e.toString());
} finally {
if (data != null) {
data.recycle();
}
}
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright (C) 2018 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.wm.flicker.monitor;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import android.support.annotation.VisibleForTesting;
import android.util.Log;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* Captures screen contents and saves it as a mp4 video file.
*/
public class ScreenRecorder implements ITransitionMonitor {
@VisibleForTesting
static final Path DEFAULT_OUTPUT_PATH = OUTPUT_DIR.resolve("transition.mp4");
private static final String TAG = "FLICKER";
private Thread recorderThread;
@VisibleForTesting
static Path getPath(String testTag) {
return OUTPUT_DIR.resolve(testTag + ".mp4");
}
@Override
public void start() {
OUTPUT_DIR.toFile().mkdirs();
String command = "screenrecord " + DEFAULT_OUTPUT_PATH;
recorderThread = new Thread(() -> {
try {
Runtime.getRuntime().exec(command);
} catch (IOException e) {
Log.e(TAG, "Error executing " + command, e);
}
});
recorderThread.start();
}
@Override
public void stop() {
runShellCommand("killall -s 2 screenrecord");
try {
recorderThread.join();
} catch (InterruptedException e) {
// ignore
}
}
@Override
public Path save(String testTag) {
try {
return Files.move(DEFAULT_OUTPUT_PATH, getPath(testTag),
REPLACE_EXISTING);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright (C) 2018 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.wm.flicker.monitor;
import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
import android.os.RemoteException;
import com.android.internal.annotations.VisibleForTesting;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Locale;
/**
* Base class for monitors containing common logic to read the trace
* as a byte array and save the trace to another location.
*/
public abstract class TraceMonitor implements ITransitionMonitor {
public static final String TAG = "FLICKER";
private static final String TRACE_DIR = "/data/misc/wmtrace/";
String traceFileName;
abstract boolean isEnabled() throws RemoteException;
/**
* Saves trace file to the external storage directory suffixing the name with the testtag
* and iteration.
*
* Moves the trace file from the default location via a shell command since the test app
* does not have security privileges to access /data/misc/wmtrace.
*
* @param testTag suffix added to trace name used to identify trace
*
* @return Path to saved trace file
*/
@Override
public Path save(String testTag) {
OUTPUT_DIR.toFile().mkdirs();
Path traceFileCopy = getOutputTraceFilePath(testTag);
String copyCommand = String.format(Locale.getDefault(), "mv %s%s %s", TRACE_DIR,
traceFileName, traceFileCopy.toString());
runShellCommand(copyCommand);
return traceFileCopy;
}
@VisibleForTesting
Path getOutputTraceFilePath(String testTag) {
return OUTPUT_DIR.resolve(traceFileName + "_" + testTag);
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright (C) 2018 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.wm.flicker.monitor;
import static android.view.FrameStats.UNDEFINED_TIME_NANO;
import android.app.Instrumentation;
import android.util.Log;
import android.view.FrameStats;
/**
* Monitors {@link android.view.WindowAnimationFrameStats} to detect janky frames.
*
* Adapted from {@link android.support.test.jank.internal.WindowAnimationFrameStatsMonitorImpl}
* using the same threshold to determine jank.
*/
public class WindowAnimationFrameStatsMonitor implements ITransitionMonitor {
private static final String TAG = "FLICKER";
// Maximum normalized error in frame duration before the frame is considered janky
private static final double MAX_ERROR = 0.5f;
// Maximum normalized frame duration before the frame is considered a pause
private static final double PAUSE_THRESHOLD = 15.0f;
private Instrumentation mInstrumentation;
private FrameStats stats;
private int numJankyFrames;
private long mLongestFrameNano = 0L;
/**
* Constructs a WindowAnimationFrameStatsMonitor instance.
*/
public WindowAnimationFrameStatsMonitor(Instrumentation instrumentation) {
mInstrumentation = instrumentation;
}
private void analyze() {
int frameCount = stats.getFrameCount();
long refreshPeriodNano = stats.getRefreshPeriodNano();
// Skip first frame
for (int i = 2; i < frameCount; i++) {
// Handle frames that have not been presented.
if (stats.getFramePresentedTimeNano(i) == UNDEFINED_TIME_NANO) {
// The animation must not have completed. Warn and break out of the loop.
Log.w(TAG, "Skipping fenced frame.");
break;
}
long frameDurationNano = stats.getFramePresentedTimeNano(i) -
stats.getFramePresentedTimeNano(i - 1);
double normalized = (double) frameDurationNano / refreshPeriodNano;
if (normalized < PAUSE_THRESHOLD) {
if (normalized > 1.0f + MAX_ERROR) {
numJankyFrames++;
}
mLongestFrameNano = Math.max(mLongestFrameNano, frameDurationNano);
}
}
}
@Override
public void start() {
// Clear out any previous data
numJankyFrames = 0;
mLongestFrameNano = 0;
mInstrumentation.getUiAutomation().clearWindowAnimationFrameStats();
}
@Override
public void stop() {
stats = mInstrumentation.getUiAutomation().getWindowAnimationFrameStats();
analyze();
}
public boolean jankyFramesDetected() {
return stats.getFrameCount() > 0 && numJankyFrames > 0;
}
@Override
public String toString() {
return stats.toString() +
" RefreshPeriodNano:" + stats.getRefreshPeriodNano() +
" NumJankyFrames:" + numJankyFrames +
" LongestFrameNano:" + mLongestFrameNano;
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (C) 2018 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.wm.flicker.monitor;
import android.os.RemoteException;
import android.view.IWindowManager;
import android.view.WindowManagerGlobal;
/**
* Captures WindowManager trace from WindowManager.
*/
public class WindowManagerTraceMonitor extends TraceMonitor {
private IWindowManager wm = WindowManagerGlobal.getWindowManagerService();
public WindowManagerTraceMonitor() {
traceFileName = "wm_trace.pb";
}
@Override
public void start() {
try {
wm.startWindowTrace();
} catch (RemoteException e) {
throw new RuntimeException("Could not start trace", e);
}
}
@Override
public void stop() {
try {
wm.stopWindowTrace();
} catch (RemoteException e) {
throw new RuntimeException("Could not stop trace", e);
}
}
@Override
public boolean isEnabled() throws RemoteException{
return wm.isWindowTraceEnabled();
}
}

View File

@@ -0,0 +1,36 @@
#
# Copyright (C) 2018 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_PACKAGE_NAME := FlickerLibTest
LOCAL_MODULE_TAGS := tests optional
# sign this with platform cert, so this test is allowed to call private platform apis
LOCAL_CERTIFICATE := platform
LOCAL_PRIVATE_PLATFORM_APIS := true
LOCAL_COMPATIBILITY_SUITE := tests
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_JAVA_LIBRARIES := android.test.runner
LOCAL_STATIC_JAVA_LIBRARIES := \
android-support-test \
platform-test-annotations \
truth-prebuilt \
platformprotosnano \
layersprotosnano \
flickerlib
include $(BUILD_PACKAGE)
include $(call all-makefiles-under,$(LOCAL_PATH))

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
* Copyright 2018 Google Inc. All Rights Reserved.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.server.wm.flicker">
<uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27"/>
<!-- Read and write traces from external storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- Capture screen contents -->
<uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" />
<!-- Run layers trace -->
<uses-permission android:name="android.permission.HARDWARE_TEST"/>
<application android:label="FlickerLibTest">
<uses-library android:name="android.test.runner"/>
</application>
<instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.server.wm.flicker"
android:label="WindowManager Flicker Lib Test">
</instrumentation>
</manifest>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
* Copyright 2018 Google Inc. All Rights Reserved.
-->
<configuration description="Config for WindowManager Flicker Tests">
<target_preparer class="com.google.android.tradefed.targetprep.GoogleDeviceSetup">
<!-- keeps the screen on during tests -->
<option name="screen-always-on" value="on" />
<!-- prevents the phone from restarting -->
<option name="force-skip-system-props" value="true" />
</target_preparer>
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true"/>
<option name="test-file-name" value="FlickerLibTest.apk"/>
</target_preparer>
<test class="com.android.tradefed.testtype.AndroidJUnitTest">
<option name="package" value="com.android.server.wm.flicker"/>
<option name="hidden-api-checks" value="false" />
</test>
</configuration>

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,181 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static com.google.common.truth.Truth.assertThat;
import com.android.server.wm.flicker.Assertions.Result;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
/**
* Contains {@link AssertionsChecker} tests.
* To run this test: {@code atest FlickerLibTest:AssertionsCheckerTest}
*/
public class AssertionsCheckerTest {
/**
* Returns a list of SimpleEntry objects with {@code data} and incremental timestamps starting
* at 0.
*/
private static List<SimpleEntry> getTestEntries(int... data) {
List<SimpleEntry> entries = new ArrayList<>();
for (int i = 0; i < data.length; i++) {
entries.add(new SimpleEntry(i, data[i]));
}
return entries;
}
@Test
public void canCheckAllEntries() {
AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
checker.add(SimpleEntry::isData42, "isData42");
List<Result> failures = checker.test(getTestEntries(1, 1, 1, 1, 1));
assertThat(failures).hasSize(5);
}
@Test
public void canCheckFirstEntry() {
AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
checker.checkFirstEntry();
checker.add(SimpleEntry::isData42, "isData42");
List<Result> failures = checker.test(getTestEntries(1, 1, 1, 1, 1));
assertThat(failures).hasSize(1);
assertThat(failures.get(0).timestamp).isEqualTo(0);
}
@Test
public void canCheckLastEntry() {
AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
checker.checkLastEntry();
checker.add(SimpleEntry::isData42, "isData42");
List<Result> failures = checker.test(getTestEntries(1, 1, 1, 1, 1));
assertThat(failures).hasSize(1);
assertThat(failures.get(0).timestamp).isEqualTo(4);
}
@Test
public void canCheckRangeOfEntries() {
AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
checker.filterByRange(1, 2);
checker.add(SimpleEntry::isData42, "isData42");
List<Result> failures = checker.test(getTestEntries(1, 42, 42, 1, 1));
assertThat(failures).hasSize(0);
}
@Test
public void emptyRangePasses() {
AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
checker.filterByRange(9, 10);
checker.add(SimpleEntry::isData42, "isData42");
List<Result> failures = checker.test(getTestEntries(1, 1, 1, 1, 1));
assertThat(failures).isEmpty();
}
@Test
public void canCheckChangingAssertions() {
AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
checker.add(SimpleEntry::isData42, "isData42");
checker.add(SimpleEntry::isData0, "isData0");
checker.checkChangingAssertions();
List<Result> failures = checker.test(getTestEntries(42, 0, 0, 0, 0));
assertThat(failures).isEmpty();
}
@Test
public void canCheckChangingAssertions_withNoAssertions() {
AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
checker.checkChangingAssertions();
List<Result> failures = checker.test(getTestEntries(42, 0, 0, 0, 0));
assertThat(failures).isEmpty();
}
@Test
public void canCheckChangingAssertions_withSingleAssertion() {
AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
checker.add(SimpleEntry::isData42, "isData42");
checker.checkChangingAssertions();
List<Result> failures = checker.test(getTestEntries(42, 42, 42, 42, 42));
assertThat(failures).isEmpty();
}
@Test
public void canFailCheckChangingAssertions_ifStartingAssertionFails() {
AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
checker.add(SimpleEntry::isData42, "isData42");
checker.add(SimpleEntry::isData0, "isData0");
checker.checkChangingAssertions();
List<Result> failures = checker.test(getTestEntries(0, 0, 0, 0, 0));
assertThat(failures).hasSize(1);
}
@Test
public void canFailCheckChangingAssertions_ifStartingAssertionAlwaysPasses() {
AssertionsChecker<SimpleEntry> checker = new AssertionsChecker<>();
checker.add(SimpleEntry::isData42, "isData42");
checker.add(SimpleEntry::isData0, "isData0");
checker.checkChangingAssertions();
List<Result> failures = checker.test(getTestEntries(0, 0, 0, 0, 0));
assertThat(failures).hasSize(1);
}
static class SimpleEntry implements ITraceEntry {
long timestamp;
int data;
SimpleEntry(long timestamp, int data) {
this.timestamp = timestamp;
this.data = data;
}
@Override
public long getTimestamp() {
return timestamp;
}
Result isData42() {
return new Result(this.data == 42, this.timestamp, "is42", "");
}
Result isData0() {
return new Result(this.data == 0, this.timestamp, "is42", "");
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static com.google.common.truth.Truth.assertThat;
import com.android.server.wm.flicker.Assertions.Result;
import org.junit.Test;
/**
* Contains {@link Assertions} tests.
* To run this test: {@code atest FlickerLibTest:AssertionsTest}
*/
public class AssertionsTest {
@Test
public void traceEntryAssertionCanNegateResult() {
Assertions.TraceAssertion<Integer> assertNumEquals42 =
getIntegerTraceEntryAssertion();
assertThat(assertNumEquals42.apply(1).success).isFalse();
assertThat(assertNumEquals42.negate().apply(1).success).isTrue();
assertThat(assertNumEquals42.apply(42).success).isTrue();
assertThat(assertNumEquals42.negate().apply(42).success).isFalse();
}
@Test
public void resultCanBeNegated() {
String reason = "Everything is fine!";
Result result = new Result(true, 0, "TestAssert", reason);
Result negatedResult = result.negate();
assertThat(negatedResult.success).isFalse();
assertThat(negatedResult.reason).isEqualTo(reason);
assertThat(negatedResult.assertionName).isEqualTo("!TestAssert");
}
private Assertions.TraceAssertion<Integer> getIntegerTraceEntryAssertion() {
return (num) -> {
if (num == 42) {
return new Result(true, "Num equals 42");
}
return new Result(false, "Num doesn't equal 42, actual:" + num);
};
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static com.android.server.wm.flicker.LayersTraceSubject.assertThat;
import static com.android.server.wm.flicker.TestFileUtils.readTestFile;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.fail;
import android.graphics.Rect;
import org.junit.Test;
import java.nio.file.Paths;
/**
* Contains {@link LayersTraceSubject} tests.
* To run this test: {@code atest FlickerLibTest:LayersTraceSubjectTest}
*/
public class LayersTraceSubjectTest {
private static final Rect displayRect = new Rect(0, 0, 1440, 2880);
private static LayersTrace readLayerTraceFromFile(String relativePath) {
try {
return LayersTrace.parseFrom(readTestFile(relativePath), Paths.get(relativePath));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Test
public void testCanDetectEmptyRegionFromLayerTrace() {
LayersTrace layersTraceEntries = readLayerTraceFromFile("layers_trace_emptyregion.pb");
try {
assertThat(layersTraceEntries).coversRegion(displayRect).forAllEntries();
fail("Assertion passed");
} catch (AssertionError e) {
assertWithMessage("Contains path to trace")
.that(e.getMessage()).contains("layers_trace_emptyregion.pb");
assertWithMessage("Contains timestamp")
.that(e.getMessage()).contains("0h38m28s8ms");
assertWithMessage("Contains assertion function")
.that(e.getMessage()).contains("coversRegion");
assertWithMessage("Contains debug info")
.that(e.getMessage()).contains("Region to test: " + displayRect);
assertWithMessage("Contains debug info")
.that(e.getMessage()).contains("first empty point: 0, 99");
}
}
@Test
public void testCanDetectIncorrectVisibilityFromLayerTrace() {
LayersTrace layersTraceEntries = readLayerTraceFromFile(
"layers_trace_invalid_layer_visibility.pb");
try {
assertThat(layersTraceEntries).showsLayer("com.android.server.wm.flicker.testapp")
.then().hidesLayer("com.android.server.wm.flicker.testapp").forAllEntries();
fail("Assertion passed");
} catch (AssertionError e) {
assertWithMessage("Contains path to trace")
.that(e.getMessage()).contains("layers_trace_invalid_layer_visibility.pb");
assertWithMessage("Contains timestamp")
.that(e.getMessage()).contains("70h13m14s303ms");
assertWithMessage("Contains assertion function")
.that(e.getMessage()).contains("!isVisible");
assertWithMessage("Contains debug info")
.that(e.getMessage()).contains(
"com.android.server.wm.flicker.testapp/com.android.server.wm.flicker.testapp"
+ ".SimpleActivity#0 is visible");
}
}
}

View File

@@ -0,0 +1,229 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static com.android.server.wm.flicker.TestFileUtils.readTestFile;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.fail;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.support.test.InstrumentationRegistry;
import android.view.WindowManager;
import org.junit.Test;
import java.util.List;
import java.util.stream.Collectors;
/**
* Contains {@link LayersTrace} tests.
* To run this test: {@code atest FlickerLibTest:LayersTraceTest}
*/
public class LayersTraceTest {
private static LayersTrace readLayerTraceFromFile(String relativePath) {
try {
return LayersTrace.parseFrom(readTestFile(relativePath));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static Rect getDisplayBounds() {
Point display = new Point();
WindowManager wm =
(WindowManager) InstrumentationRegistry.getContext().getSystemService(
Context.WINDOW_SERVICE);
wm.getDefaultDisplay().getRealSize(display);
return new Rect(0, 0, display.x, display.y);
}
@Test
public void canParseAllLayers() {
LayersTrace trace = readLayerTraceFromFile(
"layers_trace_emptyregion.pb");
assertThat(trace.getEntries()).isNotEmpty();
assertThat(trace.getEntries().get(0).getTimestamp()).isEqualTo(2307984557311L);
assertThat(trace.getEntries().get(trace.getEntries().size() - 1).getTimestamp())
.isEqualTo(2308521813510L);
List<LayersTrace.Layer> flattenedLayers = trace.getEntries().get(0).asFlattenedLayers();
String msg = "Layers:\n" + flattenedLayers.stream().map(layer -> layer.mProto.name)
.collect(Collectors.joining("\n\t"));
assertWithMessage(msg).that(flattenedLayers).hasSize(47);
}
@Test
public void canParseVisibleLayers() {
LayersTrace trace = readLayerTraceFromFile(
"layers_trace_emptyregion.pb");
assertThat(trace.getEntries()).isNotEmpty();
assertThat(trace.getEntries().get(0).getTimestamp()).isEqualTo(2307984557311L);
assertThat(trace.getEntries().get(trace.getEntries().size() - 1).getTimestamp())
.isEqualTo(2308521813510L);
List<LayersTrace.Layer> flattenedLayers = trace.getEntries().get(0).asFlattenedLayers();
List<LayersTrace.Layer> visibleLayers = flattenedLayers.stream()
.filter(layer -> layer.isVisible() && !layer.isHiddenByParent())
.collect(Collectors.toList());
String msg = "Visible Layers:\n" + visibleLayers.stream()
.map(layer -> layer.mProto.name)
.collect(Collectors.joining("\n\t"));
assertWithMessage(msg).that(visibleLayers).hasSize(9);
}
@Test
public void canParseLayerHierarchy() {
LayersTrace trace = readLayerTraceFromFile(
"layers_trace_emptyregion.pb");
assertThat(trace.getEntries()).isNotEmpty();
assertThat(trace.getEntries().get(0).getTimestamp()).isEqualTo(2307984557311L);
assertThat(trace.getEntries().get(trace.getEntries().size() - 1).getTimestamp())
.isEqualTo(2308521813510L);
List<LayersTrace.Layer> layers = trace.getEntries().get(0).getRootLayers();
assertThat(layers).hasSize(2);
assertThat(layers.get(0).mChildren).hasSize(layers.get(0).mProto.children.length);
assertThat(layers.get(1).mChildren).hasSize(layers.get(1).mProto.children.length);
}
// b/76099859
@Test
public void canDetectOrphanLayers() {
try {
readLayerTraceFromFile(
"layers_trace_orphanlayers.pb");
fail("Failed to detect orphaned layers.");
} catch (RuntimeException exception) {
assertThat(exception.getMessage()).contains(
"Failed to parse layers trace. Found orphan layers "
+ "with parent layer id:1006 : 49");
}
}
// b/75276931
@Test
public void canDetectUncoveredRegion() {
LayersTrace trace = readLayerTraceFromFile(
"layers_trace_emptyregion.pb");
LayersTrace.Entry entry = trace.getEntry(2308008331271L);
Assertions.Result result = entry.coversRegion(getDisplayBounds());
assertThat(result.failed()).isTrue();
assertThat(result.reason).contains("Region to test: Rect(0, 0 - 1440, 2880)");
assertThat(result.reason).contains("first empty point: 0, 99");
assertThat(result.reason).contains("visible regions:");
assertWithMessage("Reason contains list of visible regions")
.that(result.reason).contains("StatusBar#0Rect(0, 0 - 1440, 98");
}
// Visible region tests
@Test
public void canTestLayerVisibleRegion_layerDoesNotExist() {
LayersTrace trace = readLayerTraceFromFile(
"layers_trace_emptyregion.pb");
LayersTrace.Entry entry = trace.getEntry(2308008331271L);
final Rect expectedVisibleRegion = new Rect(0, 0, 1, 1);
Assertions.Result result = entry.hasVisibleRegion("ImaginaryLayer",
expectedVisibleRegion);
assertThat(result.failed()).isTrue();
assertThat(result.reason).contains("Could not find ImaginaryLayer");
}
@Test
public void canTestLayerVisibleRegion_layerDoesNotHaveExpectedVisibleRegion() {
LayersTrace trace = readLayerTraceFromFile(
"layers_trace_emptyregion.pb");
LayersTrace.Entry entry = trace.getEntry(2307993020072L);
final Rect expectedVisibleRegion = new Rect(0, 0, 1, 1);
Assertions.Result result = entry.hasVisibleRegion("NexusLauncherActivity#2",
expectedVisibleRegion);
assertThat(result.failed()).isTrue();
assertThat(result.reason).contains(
"Layer com.google.android.apps.nexuslauncher/com.google.android.apps"
+ ".nexuslauncher.NexusLauncherActivity#2 is invisible: activeBuffer=null"
+ " type != ColorLayer flags=1 (FLAG_HIDDEN set) visible region is empty");
}
@Test
public void canTestLayerVisibleRegion_layerIsHiddenByParent() {
LayersTrace trace = readLayerTraceFromFile(
"layers_trace_emptyregion.pb");
LayersTrace.Entry entry = trace.getEntry(2308455948035L);
final Rect expectedVisibleRegion = new Rect(0, 0, 1, 1);
Assertions.Result result = entry.hasVisibleRegion(
"SurfaceView - com.android.chrome/com.google.android.apps.chrome.Main",
expectedVisibleRegion);
assertThat(result.failed()).isTrue();
assertThat(result.reason).contains(
"Layer SurfaceView - com.android.chrome/com.google.android.apps.chrome.Main#0 is "
+ "hidden by parent: com.android.chrome/com.google.android.apps.chrome"
+ ".Main#0");
}
@Test
public void canTestLayerVisibleRegion_incorrectRegionSize() {
LayersTrace trace = readLayerTraceFromFile(
"layers_trace_emptyregion.pb");
LayersTrace.Entry entry = trace.getEntry(2308008331271L);
final Rect expectedVisibleRegion = new Rect(0, 0, 1440, 99);
Assertions.Result result = entry.hasVisibleRegion(
"StatusBar",
expectedVisibleRegion);
assertThat(result.failed()).isTrue();
assertThat(result.reason).contains("StatusBar#0 has visible "
+ "region:Rect(0, 0 - 1440, 98) expected:Rect(0, 0 - 1440, 99)");
}
@Test
public void canTestLayerVisibleRegion() {
LayersTrace trace = readLayerTraceFromFile(
"layers_trace_emptyregion.pb");
LayersTrace.Entry entry = trace.getEntry(2308008331271L);
final Rect expectedVisibleRegion = new Rect(0, 0, 1440, 98);
Assertions.Result result = entry.hasVisibleRegion("StatusBar", expectedVisibleRegion);
assertThat(result.passed()).isTrue();
}
@Test
public void canTestLayerVisibleRegion_layerIsNotVisible() {
LayersTrace trace = readLayerTraceFromFile(
"layers_trace_invalid_layer_visibility.pb");
LayersTrace.Entry entry = trace.getEntry(252794268378458L);
Assertions.Result result = entry.isVisible("com.android.server.wm.flicker.testapp");
assertThat(result.failed()).isTrue();
assertThat(result.reason).contains(
"Layer com.android.server.wm.flicker.testapp/com.android.server.wm.flicker"
+ ".testapp.SimpleActivity#0 is invisible: type != ColorLayer visible "
+ "region is empty");
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import com.google.common.io.ByteStreams;
import java.io.InputStream;
/**
* Helper functions for test file resources.
*/
class TestFileUtils {
static byte[] readTestFile(String relativePath) throws Exception {
Context context = InstrumentationRegistry.getContext();
InputStream in = context.getResources().getAssets().open("testdata/" + relativePath);
return ByteStreams.toByteArray(in);
}
}

View File

@@ -0,0 +1,258 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.os.Environment;
import com.android.server.wm.flicker.TransitionRunner.TransitionBuilder;
import com.android.server.wm.flicker.TransitionRunner.TransitionResult;
import com.android.server.wm.flicker.monitor.LayersTraceMonitor;
import com.android.server.wm.flicker.monitor.ScreenRecorder;
import com.android.server.wm.flicker.monitor.WindowAnimationFrameStatsMonitor;
import com.android.server.wm.flicker.monitor.WindowManagerTraceMonitor;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InOrder;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.List;
/**
* Contains {@link TransitionRunner} tests.
* {@code atest FlickerLibTest:TransitionRunnerTest}
*/
public class TransitionRunnerTest {
@Mock
private SimpleUiTransitions mTransitionsMock;
@Mock
private ScreenRecorder mScreenRecorderMock;
@Mock
private WindowManagerTraceMonitor mWindowManagerTraceMonitorMock;
@Mock
private LayersTraceMonitor mLayersTraceMonitorMock;
@Mock
private WindowAnimationFrameStatsMonitor mWindowAnimationFrameStatsMonitor;
@InjectMocks
private TransitionBuilder mTransitionBuilder;
@Before
public void init() {
MockitoAnnotations.initMocks(this);
}
@Test
public void transitionsRunInOrder() {
TransitionRunner.newBuilder()
.runBeforeAll(mTransitionsMock::turnOnDevice)
.runBefore(mTransitionsMock::openApp)
.run(mTransitionsMock::performMagic)
.runAfter(mTransitionsMock::closeApp)
.runAfterAll(mTransitionsMock::cleanUpTracks)
.skipLayersTrace()
.skipWindowManagerTrace()
.build()
.run();
InOrder orderVerifier = inOrder(mTransitionsMock);
orderVerifier.verify(mTransitionsMock).turnOnDevice();
orderVerifier.verify(mTransitionsMock).openApp();
orderVerifier.verify(mTransitionsMock).performMagic();
orderVerifier.verify(mTransitionsMock).closeApp();
orderVerifier.verify(mTransitionsMock).cleanUpTracks();
}
@Test
public void canCombineTransitions() {
TransitionRunner.newBuilder()
.runBeforeAll(mTransitionsMock::turnOnDevice)
.runBeforeAll(mTransitionsMock::turnOnDevice)
.runBefore(mTransitionsMock::openApp)
.runBefore(mTransitionsMock::openApp)
.run(mTransitionsMock::performMagic)
.run(mTransitionsMock::performMagic)
.runAfter(mTransitionsMock::closeApp)
.runAfter(mTransitionsMock::closeApp)
.runAfterAll(mTransitionsMock::cleanUpTracks)
.runAfterAll(mTransitionsMock::cleanUpTracks)
.skipLayersTrace()
.skipWindowManagerTrace()
.build()
.run();
final int wantedNumberOfInvocations = 2;
verify(mTransitionsMock, times(wantedNumberOfInvocations)).turnOnDevice();
verify(mTransitionsMock, times(wantedNumberOfInvocations)).openApp();
verify(mTransitionsMock, times(wantedNumberOfInvocations)).performMagic();
verify(mTransitionsMock, times(wantedNumberOfInvocations)).closeApp();
verify(mTransitionsMock, times(wantedNumberOfInvocations)).cleanUpTracks();
}
@Test
public void emptyTransitionPasses() {
List<TransitionResult> results = TransitionRunner.newBuilder()
.skipLayersTrace()
.skipWindowManagerTrace()
.build()
.run()
.getResults();
assertThat(results).hasSize(1);
assertThat(results.get(0).layersTraceExists()).isFalse();
assertThat(results.get(0).windowManagerTraceExists()).isFalse();
assertThat(results.get(0).screenCaptureVideoExists()).isFalse();
}
@Test
public void canRepeatTransitions() {
final int wantedNumberOfInvocations = 10;
TransitionRunner.newBuilder()
.runBeforeAll(mTransitionsMock::turnOnDevice)
.runBefore(mTransitionsMock::openApp)
.run(mTransitionsMock::performMagic)
.runAfter(mTransitionsMock::closeApp)
.runAfterAll(mTransitionsMock::cleanUpTracks)
.repeat(wantedNumberOfInvocations)
.skipLayersTrace()
.skipWindowManagerTrace()
.build()
.run();
verify(mTransitionsMock).turnOnDevice();
verify(mTransitionsMock, times(wantedNumberOfInvocations)).openApp();
verify(mTransitionsMock, times(wantedNumberOfInvocations)).performMagic();
verify(mTransitionsMock, times(wantedNumberOfInvocations)).closeApp();
verify(mTransitionsMock).cleanUpTracks();
}
private void emptyTask() {
}
@Test
public void canCaptureWindowManagerTrace() {
mTransitionBuilder
.run(this::emptyTask)
.includeJankyRuns()
.skipLayersTrace()
.withTag("mCaptureWmTraceTransitionRunner")
.build().run();
InOrder orderVerifier = inOrder(mWindowManagerTraceMonitorMock);
orderVerifier.verify(mWindowManagerTraceMonitorMock).start();
orderVerifier.verify(mWindowManagerTraceMonitorMock).stop();
orderVerifier.verify(mWindowManagerTraceMonitorMock)
.save("mCaptureWmTraceTransitionRunner", 0);
verifyNoMoreInteractions(mWindowManagerTraceMonitorMock);
}
@Test
public void canCaptureLayersTrace() {
mTransitionBuilder
.run(this::emptyTask)
.includeJankyRuns()
.skipWindowManagerTrace()
.withTag("mCaptureLayersTraceTransitionRunner")
.build().run();
InOrder orderVerifier = inOrder(mLayersTraceMonitorMock);
orderVerifier.verify(mLayersTraceMonitorMock).start();
orderVerifier.verify(mLayersTraceMonitorMock).stop();
orderVerifier.verify(mLayersTraceMonitorMock)
.save("mCaptureLayersTraceTransitionRunner", 0);
verifyNoMoreInteractions(mLayersTraceMonitorMock);
}
@Test
public void canRecordEachRun() throws IOException {
mTransitionBuilder
.run(this::emptyTask)
.withTag("mRecordEachRun")
.recordEachRun()
.includeJankyRuns()
.skipLayersTrace()
.skipWindowManagerTrace()
.repeat(2)
.build().run();
InOrder orderVerifier = inOrder(mScreenRecorderMock);
orderVerifier.verify(mScreenRecorderMock).start();
orderVerifier.verify(mScreenRecorderMock).stop();
orderVerifier.verify(mScreenRecorderMock).save("mRecordEachRun", 0);
orderVerifier.verify(mScreenRecorderMock).start();
orderVerifier.verify(mScreenRecorderMock).stop();
orderVerifier.verify(mScreenRecorderMock).save("mRecordEachRun", 1);
verifyNoMoreInteractions(mScreenRecorderMock);
}
@Test
public void canRecordAllRuns() throws IOException {
doReturn(Paths.get(Environment.getExternalStorageDirectory().getAbsolutePath(),
"mRecordAllRuns.mp4")).when(mScreenRecorderMock).save("mRecordAllRuns");
mTransitionBuilder
.run(this::emptyTask)
.recordAllRuns()
.includeJankyRuns()
.skipLayersTrace()
.skipWindowManagerTrace()
.withTag("mRecordAllRuns")
.repeat(2)
.build().run();
InOrder orderVerifier = inOrder(mScreenRecorderMock);
orderVerifier.verify(mScreenRecorderMock).start();
orderVerifier.verify(mScreenRecorderMock).stop();
orderVerifier.verify(mScreenRecorderMock).save("mRecordAllRuns");
verifyNoMoreInteractions(mScreenRecorderMock);
}
@Test
public void canSkipJankyRuns() {
doReturn(false).doReturn(true).doReturn(false)
.when(mWindowAnimationFrameStatsMonitor).jankyFramesDetected();
List<TransitionResult> results = mTransitionBuilder
.run(this::emptyTask)
.skipLayersTrace()
.skipWindowManagerTrace()
.repeat(3)
.build().run().getResults();
assertThat(results).hasSize(2);
}
public static class SimpleUiTransitions {
public void turnOnDevice() {
}
public void openApp() {
}
public void performMagic() {
}
public void closeApp() {
}
public void cleanUpTracks() {
}
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static com.android.server.wm.flicker.TestFileUtils.readTestFile;
import static com.google.common.truth.Truth.assertThat;
import com.android.server.wm.flicker.Assertions.Result;
import org.junit.Before;
import org.junit.Test;
/**
* Contains {@link WindowManagerTrace} tests.
* To run this test: {@code atest FlickerLibTest:WindowManagerTraceTest}
*/
public class WindowManagerTraceTest {
private WindowManagerTrace mTrace;
private static WindowManagerTrace readWindowManagerTraceFromFile(String relativePath) {
try {
return WindowManagerTrace.parseFrom(readTestFile(relativePath));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Before
public void setup() {
mTrace = readWindowManagerTraceFromFile("wm_trace_openchrome.pb");
}
@Test
public void canParseAllEntries() {
assertThat(mTrace.getEntries().get(0).getTimestamp()).isEqualTo(241777211939236L);
assertThat(mTrace.getEntries().get(mTrace.getEntries().size() - 1).getTimestamp()).isEqualTo
(241779809471942L);
}
@Test
public void canDetectAboveAppWindowVisibility() {
WindowManagerTrace.Entry entry = mTrace.getEntry(241777211939236L);
Result result = entry.isAboveAppWindowVisible("NavigationBar");
assertThat(result.passed()).isTrue();
}
@Test
public void canDetectBelowAppWindowVisibility() {
WindowManagerTrace.Entry entry = mTrace.getEntry(241777211939236L);
Result result = entry.isBelowAppWindowVisible("wallpaper");
assertThat(result.passed()).isTrue();
}
@Test
public void canDetectAppWindowVisibility() {
WindowManagerTrace.Entry entry = mTrace.getEntry(241777211939236L);
Result result = entry.isAppWindowVisible("com.google.android.apps.nexuslauncher");
assertThat(result.passed()).isTrue();
}
@Test
public void canFailWithReasonForVisibilityChecks_windowNotFound() {
WindowManagerTrace.Entry entry = mTrace.getEntry(241777211939236L);
Result result = entry.isAboveAppWindowVisible("ImaginaryWindow");
assertThat(result.failed()).isTrue();
assertThat(result.reason).contains("ImaginaryWindow cannot be found");
}
@Test
public void canFailWithReasonForVisibilityChecks_windowNotVisible() {
WindowManagerTrace.Entry entry = mTrace.getEntry(241777211939236L);
Result result = entry.isAboveAppWindowVisible("AssistPreviewPanel");
assertThat(result.failed()).isTrue();
assertThat(result.reason).contains("AssistPreviewPanel is invisible");
}
@Test
public void canDetectAppZOrder() {
WindowManagerTrace.Entry entry = mTrace.getEntry(241778130296410L);
Result result = entry.isVisibleAppWindowOnTop("com.google.android.apps.chrome");
assertThat(result.passed()).isTrue();
}
@Test
public void canFailWithReasonForZOrderChecks_windowNotOnTop() {
WindowManagerTrace.Entry entry = mTrace.getEntry(241778130296410L);
Result result = entry.isVisibleAppWindowOnTop("com.google.android.apps.nexuslauncher");
assertThat(result.failed()).isTrue();
assertThat(result.reason).contains("wanted=com.google.android.apps.nexuslauncher");
assertThat(result.reason).contains("found=com.android.chrome/"
+ "com.google.android.apps.chrome.Main");
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static com.android.server.wm.flicker.TestFileUtils.readTestFile;
import static com.android.server.wm.flicker.WmTraceSubject.assertThat;
import org.junit.Test;
/**
* Contains {@link WmTraceSubject} tests.
* To run this test: {@code atest FlickerLibTest:WmTraceSubjectTest}
*/
public class WmTraceSubjectTest {
private static WindowManagerTrace readWmTraceFromFile(String relativePath) {
try {
return WindowManagerTrace.parseFrom(readTestFile(relativePath));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Test
public void testCanTransitionInAppWindow() {
WindowManagerTrace trace = readWmTraceFromFile("wm_trace_openchrome2.pb");
assertThat(trace).showsAppWindowOnTop("com.google.android.apps.nexuslauncher/"
+ ".NexusLauncherActivity").forRange(174684850717208L, 174685957511016L);
assertThat(trace).showsAppWindowOnTop(
"com.google.android.apps.nexuslauncher/.NexusLauncherActivity")
.then()
.showsAppWindowOnTop("com.android.chrome")
.forAllEntries();
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright (C) 2018 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.wm.flicker.monitor;
import static android.surfaceflinger.nano.Layerstrace.LayersTraceFileProto.MAGIC_NUMBER_H;
import static android.surfaceflinger.nano.Layerstrace.LayersTraceFileProto.MAGIC_NUMBER_L;
import static com.google.common.truth.Truth.assertThat;
import android.surfaceflinger.nano.Layerstrace.LayersTraceFileProto;
import com.google.common.io.Files;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
/**
* Contains {@link LayersTraceMonitor} tests.
* To run this test: {@code atest FlickerLibTest:LayersTraceMonitorTest}
*/
public class LayersTraceMonitorTest {
private LayersTraceMonitor mLayersTraceMonitor;
@Before
public void setup() {
mLayersTraceMonitor = new LayersTraceMonitor();
}
@After
public void teardown() {
mLayersTraceMonitor.stop();
mLayersTraceMonitor.getOutputTraceFilePath("captureLayersTrace").toFile().delete();
}
@Test
public void canStartLayersTrace() throws Exception {
mLayersTraceMonitor.start();
assertThat(mLayersTraceMonitor.isEnabled()).isTrue();
}
@Test
public void canStopLayersTrace() throws Exception {
mLayersTraceMonitor.start();
assertThat(mLayersTraceMonitor.isEnabled()).isTrue();
mLayersTraceMonitor.stop();
assertThat(mLayersTraceMonitor.isEnabled()).isFalse();
}
@Test
public void captureLayersTrace() throws Exception {
mLayersTraceMonitor.start();
mLayersTraceMonitor.stop();
File testFile = mLayersTraceMonitor.save("captureLayersTrace").toFile();
assertThat(testFile.exists()).isTrue();
byte[] trace = Files.toByteArray(testFile);
assertThat(trace.length).isGreaterThan(0);
LayersTraceFileProto mLayerTraceFileProto = LayersTraceFileProto.parseFrom(trace);
assertThat(mLayerTraceFileProto.magicNumber).isEqualTo(
(long) MAGIC_NUMBER_H << 32 | MAGIC_NUMBER_L);
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) 2018 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.wm.flicker.monitor;
import static android.os.SystemClock.sleep;
import static com.android.server.wm.flicker.monitor.ScreenRecorder.DEFAULT_OUTPUT_PATH;
import static com.android.server.wm.flicker.monitor.ScreenRecorder.getPath;
import static com.google.common.truth.Truth.assertThat;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
import java.io.IOException;
/**
* Contains {@link ScreenRecorder} tests.
* To run this test: {@code atest FlickerLibTest:ScreenRecorderTest}
*/
public class ScreenRecorderTest {
private static final String TEST_VIDEO_FILENAME = "test.mp4";
private ScreenRecorder mScreenRecorder;
@Before
public void setup() {
mScreenRecorder = new ScreenRecorder();
}
@After
public void teardown() {
DEFAULT_OUTPUT_PATH.toFile().delete();
getPath(TEST_VIDEO_FILENAME).toFile().delete();
}
@Test
public void videoIsRecorded() {
mScreenRecorder.start();
sleep(100);
mScreenRecorder.stop();
File file = DEFAULT_OUTPUT_PATH.toFile();
assertThat(file.exists()).isTrue();
}
@Test
public void videoCanBeSaved() {
mScreenRecorder.start();
sleep(100);
mScreenRecorder.stop();
mScreenRecorder.save(TEST_VIDEO_FILENAME);
File file = getPath(TEST_VIDEO_FILENAME).toFile();
assertThat(file.exists()).isTrue();
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (C) 2018 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.wm.flicker.monitor;
import static com.android.server.wm.flicker.AutomationUtils.wakeUpAndGoToHomeScreen;
import static com.google.common.truth.Truth.assertThat;
import android.support.test.InstrumentationRegistry;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
/**
* Contains {@link WindowAnimationFrameStatsMonitor} tests.
* To run this test: {@code atest FlickerLibTest:WindowAnimationFrameStatsMonitorTest}
*/
public class WindowAnimationFrameStatsMonitorTest {
private WindowAnimationFrameStatsMonitor mWindowAnimationFrameStatsMonitor;
@Before
public void setup() {
mWindowAnimationFrameStatsMonitor = new WindowAnimationFrameStatsMonitor(
InstrumentationRegistry.getInstrumentation());
wakeUpAndGoToHomeScreen();
}
// TODO(vishnun)
@Ignore("Disabled until app-helper libraries are available.")
@Test
public void captureWindowAnimationFrameStats() throws Exception {
mWindowAnimationFrameStatsMonitor.start();
//AppHelperWrapper.getInstance().getHelper(CHROME).open();
//AppHelperWrapper.getInstance().getHelper(CHROME).exit();
mWindowAnimationFrameStatsMonitor.stop();
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright (C) 2018 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.wm.flicker.monitor;
import static com.android.server.wm.nano.WindowManagerTraceFileProto.MAGIC_NUMBER_H;
import static com.android.server.wm.nano.WindowManagerTraceFileProto.MAGIC_NUMBER_L;
import static com.google.common.truth.Truth.assertThat;
import com.android.server.wm.nano.WindowManagerTraceFileProto;
import com.google.common.io.Files;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.File;
/**
* Contains {@link WindowManagerTraceMonitor} tests.
* To run this test: {@code atest FlickerLibTest:WindowManagerTraceMonitorTest}
*/
public class WindowManagerTraceMonitorTest {
private WindowManagerTraceMonitor mWindowManagerTraceMonitor;
@Before
public void setup() {
mWindowManagerTraceMonitor = new WindowManagerTraceMonitor();
}
@After
public void teardown() {
mWindowManagerTraceMonitor.stop();
mWindowManagerTraceMonitor.getOutputTraceFilePath("captureWindowTrace").toFile().delete();
}
@Test
public void canStartWindowTrace() throws Exception {
mWindowManagerTraceMonitor.start();
assertThat(mWindowManagerTraceMonitor.isEnabled()).isTrue();
}
@Test
public void canStopWindowTrace() throws Exception {
mWindowManagerTraceMonitor.start();
assertThat(mWindowManagerTraceMonitor.isEnabled()).isTrue();
mWindowManagerTraceMonitor.stop();
assertThat(mWindowManagerTraceMonitor.isEnabled()).isFalse();
}
@Test
public void captureWindowTrace() throws Exception {
mWindowManagerTraceMonitor.start();
mWindowManagerTraceMonitor.stop();
File testFile = mWindowManagerTraceMonitor.save("captureWindowTrace").toFile();
assertThat(testFile.exists()).isTrue();
byte[] trace = Files.toByteArray(testFile);
assertThat(trace.length).isGreaterThan(0);
WindowManagerTraceFileProto mWindowTraceFileProto = WindowManagerTraceFileProto.parseFrom(
trace);
assertThat(mWindowTraceFileProto.magicNumber).isEqualTo(
(long) MAGIC_NUMBER_H << 32 | MAGIC_NUMBER_L);
}
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static android.view.Surface.rotationToString;
import static com.android.server.wm.flicker.CommonTransitions.changeAppRotation;
import static com.android.server.wm.flicker.WindowUtils.getAppPosition;
import static com.android.server.wm.flicker.WindowUtils.getNavigationBarPosition;
import static com.android.server.wm.flicker.WindowUtils.getStatusBarPosition;
import static com.android.server.wm.flicker.WmTraceSubject.assertThat;
import android.graphics.Rect;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.util.Log;
import android.view.Surface;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import java.util.ArrayList;
import java.util.Collection;
/**
* Cycle through supported app rotations.
* To run this test: {@code atest FlickerTest:ChangeAppRotationTest}
*/
@RunWith(Parameterized.class)
@LargeTest
public class ChangeAppRotationTest extends FlickerTestBase {
private int beginRotation;
private int endRotation;
public ChangeAppRotationTest(String beginRotationName, String endRotationName,
int beginRotation, int endRotation) {
this.testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(),
"com.android.server.wm.flicker.testapp", "SimpleApp");
this.beginRotation = beginRotation;
this.endRotation = endRotation;
}
@Parameters(name = "{0}-{1}")
public static Collection<Object[]> getParams() {
int[] supportedRotations =
{Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_270};
Collection<Object[]> params = new ArrayList<>();
for (int begin : supportedRotations) {
for (int end : supportedRotations) {
if (begin != end) {
params.add(new Object[]{rotationToString(begin), rotationToString(end), begin,
end});
}
}
}
return params;
}
@Before
public void runTransition() {
super.runTransition(
changeAppRotation(testApp, uiDevice, beginRotation, endRotation).build());
}
@Test
public void checkVisibility_navBarWindowIsAlwaysVisible() {
checkResults(result -> assertThat(result)
.showsAboveAppWindow(NAVIGATION_BAR_WINDOW_TITLE).forAllEntries());
}
@Test
public void checkVisibility_statusBarWindowIsAlwaysVisible() {
checkResults(result -> assertThat(result)
.showsAboveAppWindow(STATUS_BAR_WINDOW_TITLE).forAllEntries());
}
@Test
public void checkPosition_navBarLayerRotatesAndScales() {
Rect startingPos = getNavigationBarPosition(beginRotation);
Rect endingPos = getNavigationBarPosition(endRotation);
checkResults(result -> {
LayersTraceSubject.assertThat(result)
.hasVisibleRegion(NAVIGATION_BAR_WINDOW_TITLE, startingPos)
.inTheBeginning();
LayersTraceSubject.assertThat(result)
.hasVisibleRegion(NAVIGATION_BAR_WINDOW_TITLE, endingPos).atTheEnd();
}
);
}
@Test
public void checkPosition_appLayerRotates() {
Rect startingPos = getAppPosition(beginRotation);
Rect endingPos = getAppPosition(endRotation);
Log.e(TAG, "startingPos=" + startingPos + " endingPos=" + endingPos);
checkResults(result -> {
LayersTraceSubject.assertThat(result)
.hasVisibleRegion(testApp.getPackage(), startingPos).inTheBeginning();
LayersTraceSubject.assertThat(result)
.hasVisibleRegion(testApp.getPackage(), endingPos).atTheEnd();
}
);
}
@Test
public void checkPosition_statusBarLayerScales() {
Rect startingPos = getStatusBarPosition(beginRotation);
Rect endingPos = getStatusBarPosition(endRotation);
checkResults(result -> {
LayersTraceSubject.assertThat(result)
.hasVisibleRegion(STATUS_BAR_WINDOW_TITLE, startingPos)
.inTheBeginning();
LayersTraceSubject.assertThat(result)
.hasVisibleRegion(STATUS_BAR_WINDOW_TITLE, endingPos).atTheEnd();
}
);
}
@Test
public void checkVisibility_navBarLayerIsAlwaysVisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer(NAVIGATION_BAR_WINDOW_TITLE).forAllEntries());
}
@Test
public void checkVisibility_statusBarLayerIsAlwaysVisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer(STATUS_BAR_WINDOW_TITLE).forAllEntries());
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static com.android.server.wm.flicker.CommonTransitions.editTextLoseFocusToApp;
import static com.android.server.wm.flicker.WindowUtils.getDisplayBounds;
import android.platform.helpers.IAppHelper;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Test IME window closing back to app window transitions.
* To run this test: {@code atest FlickerTests:CloseImeWindowToAppTest}
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
public class CloseImeWindowToAppTest extends FlickerTestBase {
private static final String IME_WINDOW_TITLE = "InputMethod";
private IAppHelper mImeTestApp = new StandardAppHelper(
InstrumentationRegistry.getInstrumentation(),
"com.android.server.wm.flicker.testapp", "ImeApp");
@Before
public void runTransition() {
super.runTransition(editTextLoseFocusToApp(uiDevice)
.includeJankyRuns().build());
}
@Test
public void checkVisibility_imeLayerBecomesInvisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer(IME_WINDOW_TITLE)
.then()
.hidesLayer(IME_WINDOW_TITLE)
.forAllEntries());
}
@Test
public void checkVisibility_imeAppLayerIsAlwaysVisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer(mImeTestApp.getPackage())
.forAllEntries());
}
@Test
public void checkVisibility_imeAppWindowIsAlwaysVisible() {
checkResults(result -> WmTraceSubject.assertThat(result)
.showsAppWindowOnTop(mImeTestApp.getPackage())
.forAllEntries());
}
@Test
public void checkCoveredRegion_noUncoveredRegions() {
checkResults(result -> LayersTraceSubject.assertThat(result).coversRegion(
getDisplayBounds()).forAllEntries());
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static com.android.server.wm.flicker.CommonTransitions.editTextLoseFocusToHome;
import static com.android.server.wm.flicker.WindowUtils.getDisplayBounds;
import android.platform.helpers.IAppHelper;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Test IME window closing to home transitions.
* To run this test: {@code atest FlickerTests:CloseImeWindowToHomeTest}
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
public class CloseImeWindowToHomeTest extends FlickerTestBase {
private static final String IME_WINDOW_TITLE = "InputMethod";
private IAppHelper mImeTestApp = new StandardAppHelper(
InstrumentationRegistry.getInstrumentation(),
"com.android.server.wm.flicker.testapp", "ImeApp");
@Before
public void runTransition() {
super.runTransition(editTextLoseFocusToHome(uiDevice)
.includeJankyRuns().build());
}
@Test
public void checkVisibility_imeWindowBecomesInvisible() {
checkResults(result -> WmTraceSubject.assertThat(result)
.showsImeWindow(IME_WINDOW_TITLE)
.then()
.hidesImeWindow(IME_WINDOW_TITLE)
.forAllEntries());
}
@Test
public void checkVisibility_imeLayerBecomesInvisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer(IME_WINDOW_TITLE)
.then()
.hidesLayer(IME_WINDOW_TITLE)
.forAllEntries());
}
@Test
public void checkVisibility_imeAppLayerBecomesInvisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer(mImeTestApp.getPackage())
.then()
.hidesLayer(mImeTestApp.getPackage())
.forAllEntries());
}
@Test
public void checkVisibility_imeAppWindowBecomesInvisible() {
checkResults(result -> WmTraceSubject.assertThat(result)
.showsAppWindowOnTop(mImeTestApp.getPackage())
.then()
.hidesAppWindowOnTop(mImeTestApp.getPackage())
.forAllEntries());
}
@Test
public void checkCoveredRegion_noUncoveredRegions() {
checkResults(result -> LayersTraceSubject.assertThat(result).coversRegion(
getDisplayBounds()).forAllEntries());
}
}

View File

@@ -0,0 +1,317 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static android.os.SystemClock.sleep;
import static android.view.Surface.rotationToString;
import static com.android.server.wm.flicker.AutomationUtils.clearRecents;
import static com.android.server.wm.flicker.AutomationUtils.closePipWindow;
import static com.android.server.wm.flicker.AutomationUtils.exitSplitScreen;
import static com.android.server.wm.flicker.AutomationUtils.expandPipWindow;
import static com.android.server.wm.flicker.AutomationUtils.launchSplitScreen;
import static com.android.server.wm.flicker.AutomationUtils.stopPackage;
import android.content.Context;
import android.content.Intent;
import android.os.RemoteException;
import android.platform.helpers.IAppHelper;
import android.support.test.InstrumentationRegistry;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject2;
import android.support.test.uiautomator.Until;
import android.util.Rational;
import android.view.Surface;
import com.android.server.wm.flicker.TransitionRunner.TransitionBuilder;
/**
* Collection of common transitions which can be used to test different apps or scenarios.
*/
class CommonTransitions {
public static final int ITERATIONS = 1;
private static final String TAG = "FLICKER";
private static final long APP_LAUNCH_TIMEOUT = 10000;
private static void setRotation(UiDevice device, int rotation) {
try {
switch (rotation) {
case Surface.ROTATION_270:
device.setOrientationLeft();
break;
case Surface.ROTATION_90:
device.setOrientationRight();
break;
case Surface.ROTATION_0:
default:
device.setOrientationNatural();
}
// Wait for animation to complete
sleep(3000);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
private static void clickEditTextWidget(UiDevice device, IAppHelper testApp) {
UiObject2 editText = device.findObject(By.res(testApp.getPackage(), "plain_text_input"));
editText.click();
sleep(500);
}
private static void clickEnterPipButton(UiDevice device, IAppHelper testApp) {
UiObject2 enterPipButton = device.findObject(By.res(testApp.getPackage(), "enter_pip"));
enterPipButton.click();
sleep(500);
}
static TransitionBuilder openAppWarm(IAppHelper testApp, UiDevice
device) {
return TransitionRunner.newBuilder()
.withTag("OpenAppWarm_" + testApp.getLauncherName())
.runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen)
.runBeforeAll(testApp::open)
.runBefore(device::pressHome)
.runBefore(device::waitForIdle)
.run(testApp::open)
.runAfterAll(testApp::exit)
.runAfterAll(AutomationUtils::setDefaultWait)
.repeat(ITERATIONS);
}
static TransitionBuilder closeAppWithBackKey(IAppHelper testApp, UiDevice
device) {
return TransitionRunner.newBuilder()
.withTag("closeAppWithBackKey_" + testApp.getLauncherName())
.runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen)
.runBefore(testApp::open)
.runBefore(device::waitForIdle)
.run(device::pressBack)
.run(device::waitForIdle)
.runAfterAll(testApp::exit)
.runAfterAll(AutomationUtils::setDefaultWait)
.repeat(ITERATIONS);
}
static TransitionBuilder closeAppWithHomeKey(IAppHelper testApp, UiDevice
device) {
return TransitionRunner.newBuilder()
.withTag("closeAppWithHomeKey_" + testApp.getLauncherName())
.runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen)
.runBefore(testApp::open)
.runBefore(device::waitForIdle)
.run(device::pressHome)
.run(device::waitForIdle)
.runAfterAll(testApp::exit)
.runAfterAll(AutomationUtils::setDefaultWait)
.repeat(ITERATIONS);
}
static TransitionBuilder getOpenAppCold(IAppHelper testApp,
UiDevice device) {
return TransitionRunner.newBuilder()
.withTag("OpenAppCold_" + testApp.getLauncherName())
.runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen)
.runBefore(device::pressHome)
.runBefore(testApp::exit)
.runBefore(device::waitForIdle)
.run(testApp::open)
.runAfterAll(testApp::exit)
.repeat(ITERATIONS);
}
static TransitionBuilder changeAppRotation(IAppHelper testApp, UiDevice
device, int beginRotation, int endRotation) {
return TransitionRunner.newBuilder()
.withTag("changeAppRotation_" + testApp.getLauncherName()
+ rotationToString(beginRotation) + "_" +
rotationToString(endRotation))
.runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen)
.runBeforeAll(testApp::open)
.runBefore(() -> setRotation(device, beginRotation))
.run(() -> setRotation(device, endRotation))
.runAfterAll(testApp::exit)
.runAfterAll(() -> setRotation(device, Surface.ROTATION_0))
.repeat(ITERATIONS);
}
static TransitionBuilder changeAppRotation(Intent intent, String intentId, Context context,
UiDevice
device, int beginRotation, int endRotation) {
final String testTag = "changeAppRotation_" + intentId + "_" +
rotationToString(beginRotation) + "_" + rotationToString(endRotation);
return TransitionRunner.newBuilder()
.withTag(testTag)
.runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen)
.runBeforeAll(() -> {
context.startActivity(intent);
device.wait(Until.hasObject(By.pkg(intent.getComponent()
.getPackageName()).depth(0)), APP_LAUNCH_TIMEOUT);
}
)
.runBefore(() -> setRotation(device, beginRotation))
.run(() -> setRotation(device, endRotation))
.runAfterAll(() -> stopPackage(context, intent.getComponent().getPackageName()))
.runAfterAll(() -> setRotation(device, Surface.ROTATION_0))
.repeat(ITERATIONS);
}
static TransitionBuilder appToSplitScreen(IAppHelper testApp, UiDevice device) {
return TransitionRunner.newBuilder()
.withTag("appToSplitScreen_" + testApp.getLauncherName())
.runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen)
.runBefore(testApp::open)
.runBefore(device::waitForIdle)
.runBefore(() -> sleep(500))
.run(() -> launchSplitScreen(device))
.runAfter(() -> exitSplitScreen(device))
.runAfterAll(testApp::exit)
.repeat(ITERATIONS);
}
static TransitionBuilder splitScreenToLauncher(IAppHelper testApp, UiDevice device) {
return TransitionRunner.newBuilder()
.withTag("splitScreenToLauncher_" + testApp.getLauncherName())
.runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen)
.runBefore(testApp::open)
.runBefore(device::waitForIdle)
.runBefore(() -> launchSplitScreen(device))
.run(() -> exitSplitScreen(device))
.runAfterAll(testApp::exit)
.repeat(ITERATIONS);
}
static TransitionBuilder editTextSetFocus(UiDevice device) {
IAppHelper testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(),
"com.android.server.wm.flicker.testapp", "ImeApp");
return TransitionRunner.newBuilder()
.withTag("editTextSetFocus_" + testApp.getLauncherName())
.runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen)
.runBefore(device::pressHome)
.runBefore(testApp::open)
.run(() -> clickEditTextWidget(device, testApp))
.runAfterAll(testApp::exit)
.repeat(ITERATIONS);
}
static TransitionBuilder resizeSplitScreen(IAppHelper testAppTop, IAppHelper testAppBottom,
UiDevice device, Rational startRatio, Rational stopRatio) {
String testTag = "resizeSplitScreen_" + testAppTop.getLauncherName() + "_" +
testAppBottom.getLauncherName() + "_" +
startRatio.toString().replace("/", ":") + "_to_" +
stopRatio.toString().replace("/", ":");
return TransitionRunner.newBuilder()
.withTag(testTag)
.runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen)
.runBeforeAll(() -> clearRecents(device))
.runBefore(testAppBottom::open)
.runBefore(device::pressHome)
.runBefore(testAppTop::open)
.runBefore(device::waitForIdle)
.runBefore(() -> launchSplitScreen(device))
.runBefore(() -> {
UiObject2 snapshot = device.findObject(
By.res("com.google.android.apps.nexuslauncher", "snapshot"));
snapshot.click();
})
.runBefore(() -> AutomationUtils.resizeSplitScreen(device, startRatio))
.run(() -> AutomationUtils.resizeSplitScreen(device, stopRatio))
.runAfter(() -> exitSplitScreen(device))
.runAfter(device::pressHome)
.runAfterAll(testAppTop::exit)
.runAfterAll(testAppBottom::exit)
.repeat(ITERATIONS);
}
static TransitionBuilder editTextLoseFocusToHome(UiDevice device) {
IAppHelper testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(),
"com.android.server.wm.flicker.testapp", "ImeApp");
return TransitionRunner.newBuilder()
.withTag("editTextLoseFocusToHome_" + testApp.getLauncherName())
.runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen)
.runBefore(device::pressHome)
.runBefore(testApp::open)
.runBefore(() -> clickEditTextWidget(device, testApp))
.run(device::pressHome)
.run(device::waitForIdle)
.runAfterAll(testApp::exit)
.repeat(ITERATIONS);
}
static TransitionBuilder editTextLoseFocusToApp(UiDevice device) {
IAppHelper testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(),
"com.android.server.wm.flicker.testapp", "ImeApp");
return TransitionRunner.newBuilder()
.withTag("editTextLoseFocusToApp_" + testApp.getLauncherName())
.runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen)
.runBefore(device::pressHome)
.runBefore(testApp::open)
.runBefore(() -> clickEditTextWidget(device, testApp))
.run(device::pressBack)
.run(device::waitForIdle)
.runAfterAll(testApp::exit)
.repeat(ITERATIONS);
}
static TransitionBuilder enterPipMode(UiDevice device) {
IAppHelper testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(),
"com.android.server.wm.flicker.testapp", "PipApp");
return TransitionRunner.newBuilder()
.withTag("enterPipMode_" + testApp.getLauncherName())
.runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen)
.runBefore(device::pressHome)
.runBefore(testApp::open)
.run(() -> clickEnterPipButton(device, testApp))
.runAfter(() -> closePipWindow(device))
.runAfterAll(testApp::exit)
.repeat(ITERATIONS);
}
static TransitionBuilder exitPipModeToHome(UiDevice device) {
IAppHelper testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(),
"com.android.server.wm.flicker.testapp", "PipApp");
return TransitionRunner.newBuilder()
.withTag("exitPipModeToHome_" + testApp.getLauncherName())
.runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen)
.runBefore(device::pressHome)
.runBefore(testApp::open)
.runBefore(() -> clickEnterPipButton(device, testApp))
.run(() -> closePipWindow(device))
.run(device::waitForIdle)
.runAfterAll(testApp::exit)
.repeat(ITERATIONS);
}
static TransitionBuilder exitPipModeToApp(UiDevice device) {
IAppHelper testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(),
"com.android.server.wm.flicker.testapp", "PipApp");
return TransitionRunner.newBuilder()
.withTag("exitPipModeToApp_" + testApp.getLauncherName())
.runBeforeAll(AutomationUtils::wakeUpAndGoToHomeScreen)
.runBefore(device::pressHome)
.runBefore(testApp::open)
.runBefore(() -> clickEnterPipButton(device, testApp))
.run(() -> expandPipWindow(device))
.run(device::waitForIdle)
.runAfterAll(testApp::exit)
.repeat(ITERATIONS);
}
}

View File

@@ -0,0 +1,167 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import android.platform.helpers.IAppHelper;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
import android.support.test.uiautomator.UiDevice;
import android.util.Rational;
import android.view.Surface;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Tests to help debug individual transitions, capture video recordings and create test cases.
*/
@Ignore("Used for debugging transitions used in FlickerTests.")
@RunWith(AndroidJUnit4.class)
public class DebugTest {
private IAppHelper testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(),
"com.android.server.wm.flicker.testapp", "SimpleApp");
private UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
/**
* atest FlickerTest:DebugTests#openAppCold
*/
@Test
public void openAppCold() {
CommonTransitions.getOpenAppCold(testApp, uiDevice).recordAllRuns().build().run();
}
/**
* atest FlickerTest:DebugTests#openAppWarm
*/
@Test
public void openAppWarm() {
CommonTransitions.openAppWarm(testApp, uiDevice).recordAllRuns().build().run();
}
/**
* atest FlickerTest:DebugTests#changeOrientationFromNaturalToLeft
*/
@Test
public void changeOrientationFromNaturalToLeft() {
CommonTransitions.changeAppRotation(testApp, uiDevice, Surface.ROTATION_0,
Surface.ROTATION_270).recordAllRuns().build().run();
}
/**
* atest FlickerTest:DebugTests#closeAppWithBackKey
*/
@Test
public void closeAppWithBackKey() {
CommonTransitions.closeAppWithBackKey(testApp, uiDevice).recordAllRuns().build().run();
}
/**
* atest FlickerTest:DebugTests#closeAppWithHomeKey
*/
@Test
public void closeAppWithHomeKey() {
CommonTransitions.closeAppWithHomeKey(testApp, uiDevice).recordAllRuns().build().run();
}
/**
* atest FlickerTest:DebugTests#openAppToSplitScreen
*/
@Test
public void openAppToSplitScreen() {
CommonTransitions.appToSplitScreen(testApp, uiDevice).includeJankyRuns().recordAllRuns()
.build().run();
}
/**
* atest FlickerTest:DebugTests#splitScreenToLauncher
*/
@Test
public void splitScreenToLauncher() {
CommonTransitions.splitScreenToLauncher(testApp,
uiDevice).includeJankyRuns().recordAllRuns()
.build().run();
}
/**
* atest FlickerTest:DebugTests#resizeSplitScreen
*/
@Test
public void resizeSplitScreen() {
IAppHelper bottomApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(),
"com.android.server.wm.flicker.testapp", "ImeApp");
CommonTransitions.resizeSplitScreen(testApp, bottomApp, uiDevice, new Rational(1, 3),
new Rational(2, 3)).includeJankyRuns().recordEachRun().build().run();
}
// IME tests
/**
* atest FlickerTest:DebugTests#editTextSetFocus
*/
@Test
public void editTextSetFocus() {
CommonTransitions.editTextSetFocus(uiDevice).includeJankyRuns().recordEachRun()
.build().run();
}
/**
* atest FlickerTest:DebugTests#editTextLoseFocusToHome
*/
@Test
public void editTextLoseFocusToHome() {
CommonTransitions.editTextLoseFocusToHome(uiDevice).includeJankyRuns().recordEachRun()
.build().run();
}
/**
* atest FlickerTest:DebugTests#editTextLoseFocusToApp
*/
@Test
public void editTextLoseFocusToApp() {
CommonTransitions.editTextLoseFocusToHome(uiDevice).includeJankyRuns().recordEachRun()
.build().run();
}
// PIP tests
/**
* atest FlickerTest:DebugTests#enterPipMode
*/
@Test
public void enterPipMode() {
CommonTransitions.enterPipMode(uiDevice).includeJankyRuns().recordEachRun().build().run();
}
/**
* atest FlickerTest:DebugTests#exitPipModeToHome
*/
@Test
public void exitPipModeToHome() {
CommonTransitions.exitPipModeToHome(uiDevice).includeJankyRuns().recordEachRun()
.build().run();
}
/**
* atest FlickerTest:DebugTests#exitPipModeToApp
*/
@Test
public void exitPipModeToApp() {
CommonTransitions.exitPipModeToApp(uiDevice).includeJankyRuns().recordEachRun()
.build().run();
}
}

View File

@@ -0,0 +1,125 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static com.android.server.wm.flicker.AutomationUtils.setDefaultWait;
import static com.google.common.truth.Truth.assertWithMessage;
import android.platform.helpers.IAppHelper;
import android.support.test.InstrumentationRegistry;
import android.support.test.uiautomator.UiDevice;
import android.util.Log;
import com.android.server.wm.flicker.TransitionRunner.TransitionResult;
import org.junit.After;
import org.junit.AfterClass;
import java.util.HashMap;
import java.util.List;
import java.util.function.Consumer;
/**
* Base class of all Flicker test that performs common functions for all flicker tests:
* <p>
* - Caches transitions so that a transition is run once and the transition results are used by
* tests multiple times. This is needed for parameterized tests which call the BeforeClass methods
* multiple times.
* - Keeps track of all test artifacts and deletes ones which do not need to be reviewed.
* - Fails tests if results are not available for any test due to jank.
*/
public class FlickerTestBase {
public static final String TAG = "FLICKER";
static final String NAVIGATION_BAR_WINDOW_TITLE = "NavigationBar";
static final String STATUS_BAR_WINDOW_TITLE = "StatusBar";
static final String DOCKED_STACK_DIVIDER = "DockedStackDivider";
private static HashMap<String, List<TransitionResult>> transitionResults =
new HashMap<>();
IAppHelper testApp;
UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
private List<TransitionResult> results;
private TransitionResult lastResult = null;
/**
* Teardown any system settings and clean up test artifacts from the file system.
*
* Note: test artifacts for failed tests will remain on the device.
*/
@AfterClass
public static void teardown() {
setDefaultWait();
transitionResults.values().stream()
.flatMap(List::stream)
.forEach(result -> {
if (result.canDelete()) {
result.delete();
} else {
if (result.layersTraceExists()) {
Log.e(TAG, "Layers trace saved to " + result.getLayersTracePath());
}
if (result.windowManagerTraceExists()) {
Log.e(TAG, "WindowManager trace saved to " + result
.getWindowManagerTracePath
());
}
if (result.screenCaptureVideoExists()) {
Log.e(TAG, "Screen capture video saved to " + result
.screenCaptureVideo.toString());
}
}
});
}
/**
* Runs a transition, returns a cached result if the transition has run before.
*/
void runTransition(TransitionRunner transition) {
if (transitionResults.containsKey(transition.getTestTag())) {
results = transitionResults.get(transition.getTestTag());
return;
}
results = transition.run().getResults();
/* Fail if we don't have any results due to jank */
assertWithMessage("No results to test because all transition runs were invalid because "
+ "of Jank").that(results).isNotEmpty();
transitionResults.put(transition.getTestTag(), results);
}
/**
* Goes through a list of transition results and checks assertions on each result.
*/
void checkResults(Consumer<TransitionResult> assertion) {
for (TransitionResult result : results) {
lastResult = result;
assertion.accept(result);
}
lastResult = null;
}
/**
* Kludge to mark a file for saving. If {@code checkResults} fails, the last result is not
* cleared. This indicates the assertion failed for the result, so mark it for saving.
*/
@After
public void markArtifactsForSaving() {
if (lastResult != null) {
lastResult.flagForSaving();
}
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static com.android.server.wm.flicker.CommonTransitions.getOpenAppCold;
import static com.android.server.wm.flicker.WindowUtils.getDisplayBounds;
import static com.android.server.wm.flicker.WmTraceSubject.assertThat;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Test cold launch app from launcher.
* To run this test: {@code atest FlickerTests:OpenAppColdTest}
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
public class OpenAppColdTest extends FlickerTestBase {
public OpenAppColdTest() {
this.testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(),
"com.android.server.wm.flicker.testapp", "SimpleApp");
}
@Before
public void runTransition() {
super.runTransition(getOpenAppCold(testApp, uiDevice).build());
}
@Test
public void checkVisibility_navBarWindowIsAlwaysVisible() {
checkResults(result -> assertThat(result)
.showsAboveAppWindow(NAVIGATION_BAR_WINDOW_TITLE).forAllEntries());
}
@Test
public void checkVisibility_statusBarWindowIsAlwaysVisible() {
checkResults(result -> assertThat(result)
.showsAboveAppWindow(STATUS_BAR_WINDOW_TITLE).forAllEntries());
}
@Test
public void checkVisibility_wallpaperWindowBecomesInvisible() {
checkResults(result -> assertThat(result)
.showsBelowAppWindow("wallpaper")
.then()
.hidesBelowAppWindow("wallpaper")
.forAllEntries());
}
@Test
public void checkZOrder_appWindowReplacesLauncherAsTopWindow() {
checkResults(result -> assertThat(result)
.showsAppWindowOnTop(
"com.google.android.apps.nexuslauncher/.NexusLauncherActivity")
.then()
.showsAppWindowOnTop(testApp.getPackage())
.forAllEntries());
}
@Test
public void checkCoveredRegion_noUncoveredRegions() {
checkResults(result -> LayersTraceSubject.assertThat(result).coversRegion(
getDisplayBounds()).forAllEntries());
}
@Test
public void checkVisibility_navBarLayerIsAlwaysVisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer(NAVIGATION_BAR_WINDOW_TITLE).forAllEntries());
}
@Test
public void checkVisibility_statusBarLayerIsAlwaysVisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer(STATUS_BAR_WINDOW_TITLE).forAllEntries());
}
@Test
public void checkVisibility_wallpaperLayerBecomesInvisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer("wallpaper")
.then()
.hidesLayer("wallpaper")
.forAllEntries());
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static com.android.server.wm.flicker.CommonTransitions.appToSplitScreen;
import static com.android.server.wm.flicker.WindowUtils.getDisplayBounds;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Test open app to split screen.
* To run this test: {@code atest FlickerTests:OpenAppToSplitScreenTest}
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
public class OpenAppToSplitScreenTest extends FlickerTestBase {
public OpenAppToSplitScreenTest() {
this.testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(),
"com.android.server.wm.flicker.testapp", "SimpleApp");
}
@Before
public void runTransition() {
super.runTransition(appToSplitScreen(testApp, uiDevice).includeJankyRuns().build());
}
@Test
public void checkVisibility_navBarWindowIsAlwaysVisible() {
checkResults(result -> WmTraceSubject.assertThat(result)
.showsAboveAppWindow(NAVIGATION_BAR_WINDOW_TITLE).forAllEntries());
}
@Test
public void checkVisibility_statusBarWindowIsAlwaysVisible() {
checkResults(result -> WmTraceSubject.assertThat(result)
.showsAboveAppWindow(STATUS_BAR_WINDOW_TITLE).forAllEntries());
}
@Test
public void checkVisibility_dividerWindowBecomesVisible() {
checkResults(result -> WmTraceSubject.assertThat(result)
.hidesAboveAppWindow(DOCKED_STACK_DIVIDER)
.then()
.showsAboveAppWindow(DOCKED_STACK_DIVIDER)
.forAllEntries());
}
@Test
public void checkCoveredRegion_noUncoveredRegions() {
checkResults(result ->
LayersTraceSubject.assertThat(result)
.coversRegion(getDisplayBounds()).forAllEntries());
}
@Test
public void checkVisibility_navBarLayerIsAlwaysVisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer(NAVIGATION_BAR_WINDOW_TITLE).forAllEntries());
}
@Test
public void checkVisibility_statusBarLayerIsAlwaysVisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer(STATUS_BAR_WINDOW_TITLE).forAllEntries());
}
@Test
public void checkVisibility_dividerLayerBecomesVisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.hidesLayer(DOCKED_STACK_DIVIDER)
.then()
.showsLayer(DOCKED_STACK_DIVIDER)
.forAllEntries());
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static com.android.server.wm.flicker.CommonTransitions.openAppWarm;
import static com.android.server.wm.flicker.WindowUtils.getDisplayBounds;
import static com.android.server.wm.flicker.WmTraceSubject.assertThat;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Test warm launch app.
* To run this test: {@code atest FlickerTests:OpenAppWarmTest}
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
public class OpenAppWarmTest extends FlickerTestBase {
public OpenAppWarmTest() {
this.testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(),
"com.android.server.wm.flicker.testapp", "SimpleApp");
}
@Before
public void runTransition() {
super.runTransition(openAppWarm(testApp, uiDevice).build());
}
@Test
public void checkVisibility_navBarIsAlwaysVisible() {
checkResults(result -> assertThat(result)
.showsAboveAppWindow(NAVIGATION_BAR_WINDOW_TITLE).forAllEntries());
}
@Test
public void checkVisibility_statusBarIsAlwaysVisible() {
checkResults(result -> assertThat(result)
.showsAboveAppWindow(STATUS_BAR_WINDOW_TITLE).forAllEntries());
}
@Test
public void checkVisibility_wallpaperBecomesInvisible() {
checkResults(result -> assertThat(result)
.showsBelowAppWindow("wallpaper")
.then()
.hidesBelowAppWindow("wallpaper")
.forAllEntries());
}
@Test
public void checkZOrder_appWindowReplacesLauncherAsTopWindow() {
checkResults(result -> assertThat(result)
.showsAppWindowOnTop(
"com.google.android.apps.nexuslauncher/.NexusLauncherActivity")
.then()
.showsAppWindowOnTop(testApp.getPackage())
.forAllEntries());
}
@Test
public void checkCoveredRegion_noUncoveredRegions() {
checkResults(result -> LayersTraceSubject.assertThat(result).coversRegion(
getDisplayBounds()).forAllEntries());
}
@Test
public void checkVisibility_navBarLayerIsAlwaysVisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer(NAVIGATION_BAR_WINDOW_TITLE).forAllEntries());
}
@Test
public void checkVisibility_statusBarLayerIsAlwaysVisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer(STATUS_BAR_WINDOW_TITLE).forAllEntries());
}
@Test
public void checkVisibility_wallpaperLayerBecomesInvisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer("wallpaper")
.then()
.hidesLayer("wallpaper")
.forAllEntries());
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static com.android.server.wm.flicker.CommonTransitions.editTextSetFocus;
import static com.android.server.wm.flicker.WindowUtils.getDisplayBounds;
import android.support.test.filters.LargeTest;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Test IME window opening transitions.
* To run this test: {@code atest FlickerTests:OpenImeWindowTest}
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
public class OpenImeWindowTest extends FlickerTestBase {
private static final String IME_WINDOW_TITLE = "InputMethod";
@Before
public void runTransition() {
super.runTransition(editTextSetFocus(uiDevice)
.includeJankyRuns().build());
}
@Test
public void checkVisibility_imeWindowBecomesVisible() {
checkResults(result -> WmTraceSubject.assertThat(result)
.hidesImeWindow(IME_WINDOW_TITLE)
.then()
.showsImeWindow(IME_WINDOW_TITLE)
.forAllEntries());
}
@Test
public void checkVisibility_imeLayerBecomesVisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.hidesLayer(IME_WINDOW_TITLE)
.then()
.showsLayer(IME_WINDOW_TITLE)
.forAllEntries());
}
@Test
public void checkCoveredRegion_noUncoveredRegions() {
checkResults(result -> LayersTraceSubject.assertThat(result).coversRegion(
getDisplayBounds()).forAllEntries());
}
}

View File

@@ -0,0 +1,187 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static com.android.server.wm.flicker.CommonTransitions.resizeSplitScreen;
import static com.android.server.wm.flicker.WindowUtils.getDisplayBounds;
import static com.android.server.wm.flicker.WindowUtils.getDockedStackDividerInset;
import static com.android.server.wm.flicker.WindowUtils.getNavigationBarHeight;
import static com.google.common.truth.Truth.assertThat;
import android.graphics.Rect;
import android.platform.helpers.IAppHelper;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.support.test.runner.AndroidJUnit4;
import android.util.Rational;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Test split screen resizing window transitions.
* To run this test: {@code atest FlickerTests:ResizeSplitScreenTest}
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
public class ResizeSplitScreenTest extends FlickerTestBase {
public ResizeSplitScreenTest() {
this.testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(),
"com.android.server.wm.flicker.testapp", "SimpleApp");
}
@Before
public void runTransition() {
IAppHelper bottomApp = new StandardAppHelper(InstrumentationRegistry
.getInstrumentation(),
"com.android.server.wm.flicker.testapp", "ImeApp");
super.runTransition(resizeSplitScreen(testApp, bottomApp, uiDevice, new Rational(1, 3),
new Rational(2, 3)).includeJankyRuns().build());
}
@Test
public void checkVisibility_navBarLayerIsAlwaysVisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer(NAVIGATION_BAR_WINDOW_TITLE)
.forAllEntries());
}
@Test
public void checkVisibility_statusBarLayerIsAlwaysVisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer(STATUS_BAR_WINDOW_TITLE)
.forAllEntries());
}
@Test
public void checkVisibility_topAppLayerIsAlwaysVisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer("SimpleActivity")
.forAllEntries());
}
@Test
public void checkVisibility_bottomAppLayerIsAlwaysVisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer("ImeActivity")
.forAllEntries());
}
@Test
public void checkVisibility_dividerLayerIsAlwaysVisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer(DOCKED_STACK_DIVIDER)
.forAllEntries());
}
@Test
public void checkPosition_appsStartingBounds() {
Rect displayBounds = getDisplayBounds();
checkResults(result -> {
LayersTrace entries = LayersTrace.parseFrom(result.getLayersTrace(),
result.getLayersTracePath());
assertThat(entries.getEntries()).isNotEmpty();
Rect startingDividerBounds = entries.getEntries().get(0).getVisibleBounds
(DOCKED_STACK_DIVIDER);
Rect startingTopAppBounds = new Rect(0, 0, startingDividerBounds.right,
startingDividerBounds.top + getDockedStackDividerInset());
Rect startingBottomAppBounds = new Rect(0,
startingDividerBounds.bottom - getDockedStackDividerInset(),
displayBounds.right,
displayBounds.bottom - getNavigationBarHeight());
LayersTraceSubject.assertThat(result)
.hasVisibleRegion("SimpleActivity", startingTopAppBounds)
.inTheBeginning();
LayersTraceSubject.assertThat(result)
.hasVisibleRegion("ImeActivity", startingBottomAppBounds)
.inTheBeginning();
});
}
@Test
public void checkPosition_appsEndingBounds() {
Rect displayBounds = getDisplayBounds();
checkResults(result -> {
LayersTrace entries = LayersTrace.parseFrom(result.getLayersTrace(),
result.getLayersTracePath());
assertThat(entries.getEntries()).isNotEmpty();
Rect endingDividerBounds = entries.getEntries().get(
entries.getEntries().size() - 1).getVisibleBounds(
DOCKED_STACK_DIVIDER);
Rect startingTopAppBounds = new Rect(0, 0, endingDividerBounds.right,
endingDividerBounds.top + getDockedStackDividerInset());
Rect startingBottomAppBounds = new Rect(0,
endingDividerBounds.bottom - getDockedStackDividerInset(),
displayBounds.right,
displayBounds.bottom - getNavigationBarHeight());
LayersTraceSubject.assertThat(result)
.hasVisibleRegion("SimpleActivity", startingTopAppBounds)
.atTheEnd();
LayersTraceSubject.assertThat(result)
.hasVisibleRegion("ImeActivity", startingBottomAppBounds)
.atTheEnd();
});
}
@Test
public void checkVisibility_navBarWindowIsAlwaysVisible() {
checkResults(result -> WmTraceSubject.assertThat(result)
.showsAboveAppWindow(NAVIGATION_BAR_WINDOW_TITLE)
.forAllEntries());
}
@Test
public void checkVisibility_statusBarWindowIsAlwaysVisible() {
checkResults(result -> WmTraceSubject.assertThat(result)
.showsAboveAppWindow(STATUS_BAR_WINDOW_TITLE)
.forAllEntries());
}
@Test
public void checkVisibility_topAppWindowIsAlwaysVisible() {
checkResults(result -> WmTraceSubject.assertThat(result)
.showsAppWindow("SimpleActivity")
.forAllEntries());
}
@Test
public void checkVisibility_bottomAppWindowIsAlwaysVisible() {
checkResults(result -> WmTraceSubject.assertThat(result)
.showsAppWindow("ImeActivity")
.forAllEntries());
}
@Test
public void checkVisibility_dividerWindowIsAlwaysVisible() {
checkResults(result -> WmTraceSubject.assertThat(result)
.showsAboveAppWindow(DOCKED_STACK_DIVIDER)
.forAllEntries());
}
}

View File

@@ -0,0 +1,169 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static android.view.Surface.rotationToString;
import static com.android.server.wm.flicker.CommonTransitions.changeAppRotation;
import static com.android.server.wm.flicker.WindowUtils.getAppPosition;
import static com.android.server.wm.flicker.WindowUtils.getDisplayBounds;
import static com.android.server.wm.flicker.WindowUtils.getNavigationBarPosition;
import static com.android.server.wm.flicker.testapp.ActivityOptions.EXTRA_STARVE_UI_THREAD;
import static com.android.server.wm.flicker.testapp.ActivityOptions.SEAMLESS_ACTIVITY_COMPONENT_NAME;
import android.content.Intent;
import android.graphics.Rect;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.view.Surface;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import java.util.ArrayList;
import java.util.Collection;
/**
* Cycle through supported app rotations using seamless rotations.
* To run this test: {@code atest FlickerTests:SeamlessAppRotationTest}
*/
@LargeTest
@RunWith(Parameterized.class)
public class SeamlessAppRotationTest extends FlickerTestBase {
private int mBeginRotation;
private int mEndRotation;
private Intent mIntent;
public SeamlessAppRotationTest(String testId, Intent intent, int beginRotation,
int endRotation) {
this.mIntent = intent;
this.mBeginRotation = beginRotation;
this.mEndRotation = endRotation;
}
@Parameters(name = "{0}")
public static Collection<Object[]> getParams() {
int[] supportedRotations =
{Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_270};
Collection<Object[]> params = new ArrayList<>();
ArrayList<Intent> testIntents = new ArrayList<>();
// launch test activity that supports seamless rotation
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setComponent(SEAMLESS_ACTIVITY_COMPONENT_NAME);
testIntents.add(intent);
// launch test activity that supports seamless rotation with a busy UI thread to miss frames
// when the app is asked to redraw
intent = new Intent(intent);
intent.putExtra(EXTRA_STARVE_UI_THREAD, true);
testIntents.add(intent);
for (Intent testIntent : testIntents) {
for (int begin : supportedRotations) {
for (int end : supportedRotations) {
if (begin != end) {
String testId = rotationToString(begin) + "_" + rotationToString(end);
if (testIntent.getExtras() != null &&
testIntent.getExtras().getBoolean(EXTRA_STARVE_UI_THREAD)) {
testId += "_" + "BUSY_UI_THREAD";
}
params.add(new Object[]{testId, testIntent, begin, end});
}
}
}
}
return params;
}
@Before
public void runTransition() {
String intentId = "";
if (mIntent.getExtras() != null &&
mIntent.getExtras().getBoolean(EXTRA_STARVE_UI_THREAD)) {
intentId = "BUSY_UI_THREAD";
}
super.runTransition(
changeAppRotation(mIntent, intentId, InstrumentationRegistry.getContext(),
uiDevice, mBeginRotation, mEndRotation).repeat(5).build());
}
@Test
public void checkVisibility_navBarWindowIsAlwaysVisible() {
checkResults(result -> WmTraceSubject.assertThat(result)
.showsAboveAppWindow(NAVIGATION_BAR_WINDOW_TITLE).forAllEntries());
}
@Test
public void checkPosition_navBarLayerRotatesAndScales() {
Rect startingPos = getNavigationBarPosition(mBeginRotation);
Rect endingPos = getNavigationBarPosition(mEndRotation);
if (startingPos.equals(endingPos)) {
checkResults(result -> LayersTraceSubject.assertThat(result)
.hasVisibleRegion(NAVIGATION_BAR_WINDOW_TITLE, startingPos)
.forAllEntries());
} else {
checkResults(result -> LayersTraceSubject.assertThat(result)
.hasVisibleRegion(NAVIGATION_BAR_WINDOW_TITLE, startingPos)
.inTheBeginning());
checkResults(result -> LayersTraceSubject.assertThat(result)
.hasVisibleRegion(NAVIGATION_BAR_WINDOW_TITLE, endingPos)
.atTheEnd());
}
}
@Test
public void checkPosition_appLayerRotates() {
Rect startingPos = getAppPosition(mBeginRotation);
Rect endingPos = getAppPosition(mEndRotation);
if (startingPos.equals(endingPos)) {
checkResults(result -> LayersTraceSubject.assertThat(result)
.hasVisibleRegion(mIntent.getComponent().getPackageName(), startingPos)
.forAllEntries());
} else {
checkResults(result -> LayersTraceSubject.assertThat(result)
.hasVisibleRegion(mIntent.getComponent().getPackageName(), startingPos)
.then()
.hasVisibleRegion(mIntent.getComponent().getPackageName(), endingPos)
.forAllEntries());
}
}
@Test
public void checkCoveredRegion_noUncoveredRegions() {
Rect startingBounds = getDisplayBounds(mBeginRotation);
Rect endingBounds = getDisplayBounds(mEndRotation);
if (startingBounds.equals(endingBounds)) {
checkResults(result ->
LayersTraceSubject.assertThat(result)
.coversRegion(startingBounds)
.forAllEntries());
} else {
checkResults(result ->
LayersTraceSubject.assertThat(result)
.coversRegion(startingBounds)
.then()
.coversRegion(endingBounds)
.forAllEntries());
}
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import static com.android.server.wm.flicker.CommonTransitions.splitScreenToLauncher;
import static com.android.server.wm.flicker.WindowUtils.getDisplayBounds;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.FlakyTest;
import android.support.test.filters.LargeTest;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Test open app to split screen.
* To run this test: {@code atest FlickerTests:SplitScreenToLauncherTest}
*/
@LargeTest
@RunWith(AndroidJUnit4.class)
public class SplitScreenToLauncherTest extends FlickerTestBase {
public SplitScreenToLauncherTest() {
this.testApp = new StandardAppHelper(InstrumentationRegistry.getInstrumentation(),
"com.android.server.wm.flicker.testapp", "SimpleApp");
}
@Before
public void runTransition() {
super.runTransition(splitScreenToLauncher(testApp, uiDevice).includeJankyRuns().build());
}
@Test
public void checkCoveredRegion_noUncoveredRegions() {
checkResults(result ->
LayersTraceSubject.assertThat(result)
.coversRegion(getDisplayBounds()).forAllEntries());
}
@Test
public void checkVisibility_dividerLayerBecomesInVisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer(DOCKED_STACK_DIVIDER)
.then()
.hidesLayer(DOCKED_STACK_DIVIDER)
.forAllEntries());
}
@FlakyTest(bugId = 79686616)
@Test
public void checkVisibility_appLayerBecomesInVisible() {
checkResults(result -> LayersTraceSubject.assertThat(result)
.showsLayer(testApp.getPackage())
.then()
.hidesLayer(testApp.getPackage())
.forAllEntries());
}
@Test
public void checkVisibility_navBarWindowIsAlwaysVisible() {
checkResults(result -> WmTraceSubject.assertThat(result)
.showsAboveAppWindow(NAVIGATION_BAR_WINDOW_TITLE).forAllEntries());
}
@Test
public void checkVisibility_statusBarWindowIsAlwaysVisible() {
checkResults(result -> WmTraceSubject.assertThat(result)
.showsAboveAppWindow(STATUS_BAR_WINDOW_TITLE).forAllEntries());
}
@Test
public void checkVisibility_dividerWindowBecomesInVisible() {
checkResults(result -> WmTraceSubject.assertThat(result)
.showsAboveAppWindow(DOCKED_STACK_DIVIDER)
.then()
.hidesAboveAppWindow(DOCKED_STACK_DIVIDER)
.forAllEntries());
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright (C) 2018 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.wm.flicker;
import android.app.Instrumentation;
import android.platform.helpers.AbstractStandardAppHelper;
/**
* Class to take advantage of {@code IAppHelper} interface so the same test can be run against
* first party and third party apps.
*/
public class StandardAppHelper extends AbstractStandardAppHelper {
private final String mPackageName;
private final String mLauncherName;
public StandardAppHelper(Instrumentation instr, String packageName, String launcherName) {
super(instr);
mPackageName = packageName;
mLauncherName = launcherName;
}
/**
* {@inheritDoc}
*/
@Override
public String getPackage() {
return mPackageName;
}
/**
* {@inheritDoc}
*/
@Override
public String getLauncherName() {
return mLauncherName;
}
/**
* {@inheritDoc}
*/
@Override
public void dismissInitialDialogs() {
}
}

View File

@@ -0,0 +1,15 @@
# Copyright (C) 2018 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.
include $(call all-subdir-makefiles)

View File

@@ -0,0 +1,31 @@
# Copyright (C) 2018 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_PACKAGE_NAME := FlickerTestApp
LOCAL_MODULE_TAGS := tests optional
LOCAL_SRC_FILES := $(call all-subdir-java-files)
LOCAL_SDK_VERSION := current
LOCAL_COMPATIBILITY_SUITE := device-tests
include $(BUILD_PACKAGE)
include $(CLEAR_VARS)
LOCAL_MODULE := flickertestapplib
LOCAL_MODULE_TAGS := tests optional
LOCAL_SDK_VERSION := current
LOCAL_SRC_FILES := src/com/android/server/wm/flicker/testapp/ActivityOptions.java
include $(BUILD_STATIC_JAVA_LIBRARY)
include $(call all-makefiles-under,$(LOCAL_PATH))

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 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="com.android.server.wm.flicker.testapp">
<uses-sdk android:minSdkVersion="17" android:targetSdkVersion="27"/>
<application
android:allowBackup="false"
android:supportsRtl="true">
<activity android:name=".SimpleActivity"
android:taskAffinity="com.android.server.wm.flicker.testapp.SimpleActivity"
android:label="SimpleApp">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".ImeActivity"
android:taskAffinity="com.android.server.wm.flicker.testapp.ImeActivity"
android:label="ImeApp">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".PipActivity"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
android:configChanges=
"screenSize|smallestScreenSize|screenLayout|orientation"
android:taskAffinity="com.android.server.wm.flicker.testapp.PipActivity"
android:label="PipApp">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".SeamlessRotationActivity"
android:taskAffinity=
"com.android.server.wm.flicker.testapp.SeamlessRotationActivity"
android:configChanges="orientation|screenSize"
android:label="SeamlessApp">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2018 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_green_light">
<EditText android:id="@+id/plain_text_input"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:inputType="text"/>
</LinearLayout>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2018 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_blue_bright">
<Button android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/enter_pip"
android:text="Enter PIP"/>
</LinearLayout>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2018 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/holo_orange_light">
</LinearLayout>

View File

@@ -0,0 +1,26 @@
/*
* Copyright (C) 2018 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.wm.flicker.testapp;
import android.content.ComponentName;
public class ActivityOptions {
public static final String EXTRA_STARVE_UI_THREAD = "StarveUiThread";
public static final ComponentName SEAMLESS_ACTIVITY_COMPONENT_NAME =
new ComponentName("com.android.server.wm.flicker.testapp",
"com.android.server.wm.flicker.testapp.SeamlessRotationActivity");
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2018 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.wm.flicker.testapp;
import android.app.Activity;
import android.os.Bundle;
import android.view.WindowManager;
public class ImeActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
WindowManager.LayoutParams p = getWindow().getAttributes();
p.layoutInDisplayCutoutMode = WindowManager.LayoutParams
.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(p);
setContentView(R.layout.activity_ime);
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (C) 2018 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.wm.flicker.testapp;
import android.app.Activity;
import android.app.PictureInPictureParams;
import android.graphics.Rect;
import android.os.Bundle;
import android.util.Rational;
import android.view.WindowManager;
import android.widget.Button;
public class PipActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
WindowManager.LayoutParams p = getWindow().getAttributes();
p.layoutInDisplayCutoutMode = WindowManager.LayoutParams
.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(p);
setContentView(R.layout.activity_pip);
Button enterPip = (Button) findViewById(R.id.enter_pip);
PictureInPictureParams params = new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(1, 1))
.setSourceRectHint(new Rect(0, 0, 100, 100))
.build();
enterPip.setOnClickListener((v) -> enterPictureInPictureMode(params));
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2018 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.wm.flicker.testapp;
import static android.os.SystemClock.sleep;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
import static com.android.server.wm.flicker.testapp.ActivityOptions.EXTRA_STARVE_UI_THREAD;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.view.Window;
import android.view.WindowManager;
import java.util.Timer;
import java.util.TimerTask;
public class SeamlessRotationActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
enableSeamlessRotation();
setContentView(R.layout.activity_simple);
boolean starveUiThread = getIntent().getExtras() != null &&
getIntent().getExtras().getBoolean(EXTRA_STARVE_UI_THREAD);
if (starveUiThread) {
starveUiThread();
}
}
private void starveUiThread() {
Handler handler = new Handler(Looper.getMainLooper(), (Message unused) -> {
sleep(20);
return true;
});
new Timer().schedule(new TimerTask() {
@Override
public void run() {
handler.sendEmptyMessage(0);
}
}, 0, 21);
}
private void enableSeamlessRotation() {
WindowManager.LayoutParams p = getWindow().getAttributes();
p.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS;
p.layoutInDisplayCutoutMode = WindowManager.LayoutParams
.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().setAttributes(p);
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2018 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.wm.flicker.testapp;
import android.app.Activity;
import android.os.Bundle;
import android.view.WindowManager;
public class SimpleActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
WindowManager.LayoutParams p = getWindow().getAttributes();
p.layoutInDisplayCutoutMode = WindowManager.LayoutParams
.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(p);
setContentView(R.layout.activity_simple);
}
}