diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2022-05-08 04:37:18 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2022-05-08 04:37:18 +0000 |
commit | 560bd88fc47ae6d7bdfea05d8ffe9dfc6304a327 (patch) | |
tree | fbc94560cc9494a655df718c90255f01d59ade87 | |
parent | d2474ee3bda8f6437a9c223bbbb2beacb1c4ba43 (diff) | |
parent | f6d2a2cbd8d083ef592ac0c46a89844bfb3ed488 (diff) | |
download | platform_testing-560bd88fc47ae6d7bdfea05d8ffe9dfc6304a327.tar.gz |
Snap for 8554636 from f6d2a2cbd8d083ef592ac0c46a89844bfb3ed488 to sdk-releaseplatform-tools-33.0.2
Change-Id: I765af8e166a58b46dbf9ba294c5754174d2caef5
380 files changed, 17353 insertions, 2836 deletions
diff --git a/build/tasks/continuous_instrumentation_tests.mk b/build/tasks/continuous_instrumentation_tests.mk index 77121598a..1674d08cc 100644 --- a/build/tasks/continuous_instrumentation_tests.mk +++ b/build/tasks/continuous_instrumentation_tests.mk @@ -81,6 +81,8 @@ continuous_instrumentation_tests_api_coverage : $(coverage_report) $(call dist-for-goals, continuous_instrumentation_tests_api_coverage, \ $(coverage_report):$(name)-api_coverage.html) +ALL_TARGETS.$(coverage_report).META_LIC:=$(module_license_metadata) + # Also build this when you run "make tests". # This allow us to not change the build server config. tests : continuous_instrumentation_tests_api_coverage diff --git a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoMediaHelper.java b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoMediaHelper.java index 530fb46d9..49174abac 100644 --- a/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoMediaHelper.java +++ b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoMediaHelper.java @@ -151,4 +151,23 @@ public interface IAutoMediaHelper extends IAppHelper { * @return true if all app names in mediaAppsNames shows up in Media Apps Grid */ boolean areMediaAppsPresent(List<String> mediaAppsNames); + + /** + * Setup expectations: "Media apps" Grid is open. + * + * @param appName App name to open + */ + void openApp(String appName); + + /** + * Setup expectations: Media app is open. + */ + void openMediaAppSettingsPage(); + + /** + * Setup expectations: Media app is open. Account not logged in. + * + * @return Error message for no user login + */ + String getMediaAppUserNotLoggedInErrorMessage(); } diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerTraceExtensions.kt b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoTestMediaAppHelper.java index d6a6810ea..4ed751eba 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerTraceExtensions.kt +++ b/libraries/app-helpers/interfaces/auto/src/android/platform/helpers/IAutoTestMediaAppHelper.java @@ -14,13 +14,12 @@ * limitations under the License. */ -package com.android.server.wm.flicker.traces.layers +package android.platform.helpers; -import com.android.server.wm.traces.common.layers.LayerTraceEntry -import com.android.server.wm.traces.parser.toAndroidRegion - -fun LayerTraceEntry.getVisibleBounds(layerName: String): android.graphics.Region { - return flattenedLayers.firstOrNull { it.name.contains(layerName) && it.isVisible } - ?.visibleRegion?.toAndroidRegion() - ?: android.graphics.Region() -}
\ No newline at end of file +public interface IAutoTestMediaAppHelper extends IAppHelper { + /** + * Loads Media files in Test Media app + * Setup expectations: Test Media app Settings page is open. + */ + void loadMediaInLocalMediaTestApp(); +} diff --git a/libraries/app-helpers/interfaces/common/src/android/platform/helpers/IMapsHelper.java b/libraries/app-helpers/interfaces/common/src/android/platform/helpers/IMapsHelper.java index 894a861b7..47b7f4684 100644 --- a/libraries/app-helpers/interfaces/common/src/android/platform/helpers/IMapsHelper.java +++ b/libraries/app-helpers/interfaces/common/src/android/platform/helpers/IMapsHelper.java @@ -24,29 +24,29 @@ public interface IMapsHelper extends IAppHelper { /** * Setup expectation: On the standard Map screen in any setup. * - * Best effort attempt to go to the query screen (if not currently there), - * does a search, and selects the results. + * <p>Best effort attempt to go to the query screen (if not currently there), does a search, and + * selects the results. */ public void doSearch(String query); /** * Setup expectation: Destination is selected. * - * Best effort attempt to go to the directions screen for the selected destination. + * <p>Best effort attempt to go to the directions screen for the selected destination. */ public void getDirections(); /** * Setup expectation: On directions screen. * - * Best effort attempt to start navigation for the selected destination. + * <p>Best effort attempt to start navigation for the selected destination. */ public void startNavigation(); /** * Setup expectation: On navigation screen. * - * Best effort attempt to stop navigation, and go back to the directions screen. + * <p>Best effort attempt to stop navigation, and go back to the directions screen. */ public void stopNavigation(); @@ -107,8 +107,7 @@ public interface IMapsHelper extends IAppHelper { /** * Setup expectation: On the standard Map screen in any setup. * - * <p>Best effort attempt to go to the query screen (if not currently there), - * does a search. + * <p>Best effort attempt to go to the query screen (if not currently there), does a search. */ public default void inputSearch(String query) { throw new UnsupportedOperationException("Not yet implemented."); @@ -177,4 +176,42 @@ public interface IMapsHelper extends IAppHelper { public default void clickBaseCompassButton() { throw new UnsupportedOperationException("Not yet implemented."); } + + /** + * Setup expectation: On the home screen for foldable device. + * + * <p>This method checks that the home page has side panel view. + */ + public default boolean isSidePanelOpened() { + throw new UnsupportedOperationException("Not implemented yet."); + } + + /** + * Setup expectation: On the home screen for foldable device. + * + * <p>Click the button to close the side panel when it is opened. + */ + public default void closeSidePanel() { + throw new UnsupportedOperationException("Not implemented yet."); + } + + /** + * Setup expectation: On the home screen for foldable device. + * + * <p>Get the UiObject2 of main map container. + */ + public default UiObject2 getMainMapContainer() { + throw new UnsupportedOperationException("Not implemented yet."); + } + + /** + * Setup expectation: On the home screen. + * + * <p>This method checks if Maps is on the main page. + * + * @param sidePanelOpened Whether the side panel view should be opened or not. + */ + public default boolean isOnMapsMainPage(boolean sidePanelOpened) { + throw new UnsupportedOperationException("Not implemented yet."); + } } diff --git a/libraries/app-helpers/interfaces/common/src/android/platform/helpers/IYouTubeHelper.java b/libraries/app-helpers/interfaces/common/src/android/platform/helpers/IYouTubeHelper.java index fb84f631b..790da94e0 100644 --- a/libraries/app-helpers/interfaces/common/src/android/platform/helpers/IYouTubeHelper.java +++ b/libraries/app-helpers/interfaces/common/src/android/platform/helpers/IYouTubeHelper.java @@ -222,4 +222,18 @@ public interface IYouTubeHelper extends IAppHelper { * <p>It presses YouTube PiP view twice to return to the main app. */ public void backFromYouTubeFromPip(); + + /** + * Setup expectation: YouTube is on the library page. + * + * <p>presses Your videos tab. + */ + public void goToYourVideos(); + + /** + * Setup expectation: YouTube is on the Your videos page. + * + * <p>presses the video name to play. + */ + public void playYourVideo(String videoName); } diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IChromeHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IChromeHelper.java index d61727656..1ff75eae0 100644 --- a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IChromeHelper.java +++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IChromeHelper.java @@ -17,6 +17,7 @@ package android.platform.helpers; import android.support.test.uiautomator.Direction; +import android.support.test.uiautomator.UiObject2; public interface IChromeHelper extends IAppHelper { public enum MenuItem { @@ -253,4 +254,13 @@ public interface IChromeHelper extends IAppHelper { public default void scrollPage(Direction dir, float percent) { throw new UnsupportedOperationException("Not yet implemented."); } + + /** + * Setup expectations: Chrome is open on a page. + * + * <p>Get the UiObject2 of the page screen. + */ + public default UiObject2 getWebPage() { + throw new UnsupportedOperationException("Not yet implemented."); + } } diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGmailHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGmailHelper.java index 551c4abc8..4318b3b94 100644 --- a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGmailHelper.java +++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGmailHelper.java @@ -17,6 +17,8 @@ package android.platform.helpers; import android.support.test.uiautomator.Direction; +import android.support.test.uiautomator.UiObject2; + import java.util.List; public interface IGmailHelper extends IAppHelper { @@ -24,19 +26,17 @@ public interface IGmailHelper extends IAppHelper { /** * Setup expectations: Gmail is open and the navigation bar is visible. * - * This method will navigate to the Inbox or Primary, depending on the name. + * <p>This method will navigate to the Inbox or Primary, depending on the name. */ public void goToInbox(); - /** - * Alias method for AbstractGmailHelper#goToInbox - */ + /** Alias method for AbstractGmailHelper#goToInbox */ public void goToPrimary(); /** * Setup expectations: Gmail is open on the Inbox or Primary page. * - * This method will open a new e-mail to compose and block until complete. + * <p>This method will open a new e-mail to compose and block until complete. */ public void goToComposeEmail(); @@ -57,16 +57,16 @@ public interface IGmailHelper extends IAppHelper { /** * Setup expectations: Gmail is open and on the Inbox or Primary page. * - * This method will open the (index)'th visible e-mail in the list and block until the e-mail is - * visible in the foreground. The top-most visible e-mail will always be labeled 0. To get the - * number of visible e-mails, consult the getVisibleEmailCount() function. + * <p>This method will open the (index)'th visible e-mail in the list and block until the e-mail + * is visible in the foreground. The top-most visible e-mail will always be labeled 0. To get + * the number of visible e-mails, consult the getVisibleEmailCount() function. */ public void openEmailByIndex(int index); /** * Setup expectations: Gmail is open and on the Inbox or Primary page. * - * This method will return the number of visible e-mails for use with the #openEmailByIndex + * <p>This method will return the number of visible e-mails for use with the #openEmailByIndex * method. */ public int getVisibleEmailCount(); @@ -74,58 +74,58 @@ public interface IGmailHelper extends IAppHelper { /** * Setup expectations: Gmail is open and an e-mail is open in the foreground. * - * This method will press reply, send a reply e-mail with the given parameters, and block until - * the original message is in the foreground again. + * <p>This method will press reply, send a reply e-mail with the given parameters, and block + * until the original message is in the foreground again. */ public void sendReplyEmail(String address, String body); /** * Setup expectations: Gmail is open and composing an e-mail. * - * This method will set the e-mail's To address and block until complete. + * <p>This method will set the e-mail's To address and block until complete. */ public void setEmailToAddress(String address); /** * Setup expectations: Gmail is open and composing an e-mail. * - * This method will set the e-mail's subject and block until complete. + * <p>This method will set the e-mail's subject and block until complete. */ public void setEmailSubject(String subject); /** * Setup expectations: Gmail is open and composing an e-mail. * - * This method will set the e-mail's Body (doesn't use keyboard) and block until complete. Focus - * will remain on the e-mail body after completion. + * <p>This method will set the e-mail's Body (doesn't use keyboard) and block until complete. + * Focus will remain on the e-mail body after completion. * - * * @param body The messages to input in the e-mail body. + * <p>* @param body The messages to input in the e-mail body. */ public void setEmailBody(String body); /** * Setup expectations: Gmail is open and composing an e-mail. * - * This method inputs the e-mail body. + * <p>This method inputs the e-mail body. * * @param body The messages to input in the e-mail body. * @param useKeyboard Types out the e-mail body by keyboard or not. */ - default public void setEmailBody(String body, boolean useKeyboard) { + public default void setEmailBody(String body, boolean useKeyboard) { throw new UnsupportedOperationException("Not yet implemented."); } /** * Setup expectations: Gmail is open and composing an e-mail. * - * This method will press send and block until the device is idle on the original e-mail. + * <p>This method will press send and block until the device is idle on the original e-mail. */ public void clickSendButton(); /** * Setup expectations: Gmail is open and composing an e-mail. * - * This method will get the e-mail's composition's body and block until complete. + * <p>This method will get the e-mail's composition's body and block until complete. * * @return {String} the text contained in the email composition's body. */ @@ -134,58 +134,60 @@ public interface IGmailHelper extends IAppHelper { /** * Setup expectations: Gmail is open and the navigation drawer is visible. * - * This method will open the navigation drawer and block until complete. + * <p>This method will open the navigation drawer and block until complete. */ public void openNavigationDrawer(); /** * Setup expectations: Gmail is open and the navigation drawer is open. * - * This method will close the navigation drawer and returns true otherwise false + * <p>This method will close the navigation drawer and returns true otherwise false */ public boolean closeNavigationDrawer(); /** * Setup expectations: Gmail is open and the navigation drawer is open. * - * This method will scroll the navigation drawer and block until idle. Only accepts UP and DOWN. + * <p>This method will scroll the navigation drawer and block until idle. Only accepts UP and + * DOWN. */ public void scrollNavigationDrawer(Direction dir); /** * Setup expectations: Gmail is open and the navigation drawer is open. * - * This method will fling the navigation drawer and block until idle. Only accepts UP and DOWN. + * <p>This method will fling the navigation drawer and block until idle. Only accepts UP and + * DOWN. */ public void flingNavigationDrawer(Direction dir); /** * Setup expectations: Gmail is open and a mailbox is open. * - * This method will scroll the mailbox view. + * <p>This method will scroll the mailbox view. * - * @param direction The direction to scroll, only accepts UP and DOWN. - * @param amount The amount to scroll - * @param scrollToEnd Whether or not to scroll to the end + * @param direction The direction to scroll, only accepts UP and DOWN. + * @param amount The amount to scroll + * @param scrollToEnd Whether or not to scroll to the end */ public void scrollMailbox(Direction direction, float amount, boolean scrollToEnd); /** * Setup expectations: Gmail is open and an email is open. * - * This method will scroll the current email. + * <p>This method will scroll the current email. * - * @param direction The direction to scroll, only accepts UP and DOWN. - * @param amount The amount to scroll - * @param scrollToEnd Whether or not to scroll to the end + * @param direction The direction to scroll, only accepts UP and DOWN. + * @param amount The amount to scroll + * @param scrollToEnd Whether or not to scroll to the end */ public void scrollEmail(Direction direction, float amount, boolean scrollToEnd); /** * Setup expectations: Gmail is open and the navigation drawer is open. * - * This method will open the mailbox with the given name and block until emails in - * that mailbox have loaded. + * <p>This method will open the mailbox with the given name and block until emails in that + * mailbox have loaded. * * @param mailboxName The case insensitive name of the mailbox to open */ @@ -194,16 +196,16 @@ public interface IGmailHelper extends IAppHelper { /** * Setup expectations: Gmail is open and an email is open. * - * This method will return to the mailbox the current email was opened from. + * <p>This method will return to the mailbox the current email was opened from. */ public void returnToMailbox(); /** * Setup expectations: Gmail is open and an email is open. * - * This method starts downloading the attachment at the specified index in the current email. - * The download happens in the background. This method returns immediately after starting - * the download and does not wait for the download to complete. + * <p>This method starts downloading the attachment at the specified index in the current email. + * The download happens in the background. This method returns immediately after starting the + * download and does not wait for the download to complete. * * @param index The index of the attachment to download */ @@ -212,7 +214,7 @@ public interface IGmailHelper extends IAppHelper { /** * Setup expectations: Gmail is open and an email is open. * - * This method gets every link target in an open email by traversing the UI tree of the body + * <p>This method gets every link target in an open email by traversing the UI tree of the body * of the open message. * * @return an iterator over the links in the message @@ -222,7 +224,7 @@ public interface IGmailHelper extends IAppHelper { /** * Setup expectations: Gmail is open and an email is open. * - * This method clicks the link in the open email with the given target. + * <p>This method clicks the link in the open email with the given target. * * @param target the target of the link to click */ @@ -245,11 +247,11 @@ public interface IGmailHelper extends IAppHelper { /** * Setup expectations: Gmail is open and an email is open. * - * This method swipes the current email. + * <p>This method swipes the current email. * * @param direction The direction to swipe, only accepts LEFT and RIGHT. */ - default public void swipeEmail(Direction direction) { + public default void swipeEmail(Direction direction) { throw new UnsupportedOperationException("Not yet implemented."); } @@ -261,4 +263,53 @@ public interface IGmailHelper extends IAppHelper { public default void openAccountMenu() { throw new UnsupportedOperationException("Not yet implemented."); } + + /** + * Setup expectations: Gmail mailbox is open. + * + * <p>The UiObject2 for Gmail to get mail contents container view. + */ + public UiObject2 getGmailContentsContainer(); + + /** + * Setup expectations: Gmail primary mail is open. + * + * <p>This method will check if the current view is the email contents page. + */ + public boolean isOnGmailContentsPage(); + + /** + * Setup expectation: Gmail is open. + * + * <p>Get the UiObject2 of mail button icon. + */ + public UiObject2 getMailButton(); + + /** + * Setup expectation: Gmail is open. + * + * <p>This method will click mail button and go to the top of the mail list. + */ + public void backToTopByMailButton(); + + /** + * Setup expectation: Gmail is open. + * + * <p>This method will switch mail label and go to the top of the mail list. + */ + public void backToTopBySwitchLabel(); + + /** + * Setup expectations: Gmail mailbox is open. + * + * <p>This method will get a UiObject2 object for Gmail list container. + */ + public UiObject2 getGmailListContainer(); + + /** + * Setup expectation: Gmail is open and emails are download completed. + * + * <p>This method will check if the current view is on Gmail list page. + */ + public boolean isOnGmailListPage(); } diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGoogleCameraHelper2.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGoogleCameraHelper2.java new file mode 100644 index 000000000..a94e06919 --- /dev/null +++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IGoogleCameraHelper2.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.platform.helpers; + +public interface IGoogleCameraHelper2 extends IAppHelper { + + /** Setup expectations: Starts taking multiple photos on camera. */ + public void takeMultiplePhotos(int count, long takePhotoDelay); + + /** Setup expectations: Switch to video record mode. */ + public void clickVideoTab(); + + /** Setup expectations: Click camera video button to start recording or stop recording. */ + public void clickCameraVideoButton(); +} diff --git a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IPhotosHelper.java b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IPhotosHelper.java index 91c518677..4ebec540e 100644 --- a/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IPhotosHelper.java +++ b/libraries/app-helpers/interfaces/handheld/src/android/platform/helpers/IPhotosHelper.java @@ -169,4 +169,25 @@ public interface IPhotosHelper extends IAppHelper { * <p>Get the UiObject2 of photo scroll view pattern. */ public UiObject2 getPhotoScrollView(); + + /** + * Setup expectation: Photos is open. + * + * <p>Check if device is now in Photos main screen. + * + * @return Returns true if device is in Photos main screen, false if not. + */ + public boolean isOnMainScreen(); + + /** Setup expectation: Disable backup mode in settings. */ + public void disableBackupMode(); + + /** Setup expectation: Enable backup mode in settings. */ + public void enableBackupMode(); + + /** Setup expectation: Verify backup starts uploading new pictures in settings. */ + public void verifyContentStartedUploading(); + + /** Setup expectation: Remove photos app content. */ + public void removeContent(); } diff --git a/libraries/automotive-helpers/media-center-app-helper/src/android/platform/helpers/MediaCenterHelperImpl.java b/libraries/automotive-helpers/media-center-app-helper/src/android/platform/helpers/MediaCenterHelperImpl.java index 93e6688a0..7d2f21c70 100644 --- a/libraries/automotive-helpers/media-center-app-helper/src/android/platform/helpers/MediaCenterHelperImpl.java +++ b/libraries/automotive-helpers/media-center-app-helper/src/android/platform/helpers/MediaCenterHelperImpl.java @@ -410,7 +410,9 @@ public class MediaCenterHelperImpl extends AbstractAutoStandardAppHelper if (menuItemElements.size() == 0) { throw new UnknownUiException("Unable to find Media drop down."); } - clickAndWaitForIdleScreen(menuItemElements.get(1)); + // Media menu drop down is the last item in Media App Screen + int positionOfMenuItemDropDown = menuItemElements.size() - 1; + clickAndWaitForIdleScreen(menuItemElements.get(positionOfMenuItemDropDown)); } /** {@inheritDoc} */ @@ -439,4 +441,48 @@ public class MediaCenterHelperImpl extends AbstractAutoStandardAppHelper } return true; } + + /** + * {@inheritDoc} + */ + @Override + public void openApp(String appName) { + SystemClock.sleep(SHORT_RESPONSE_WAIT_MS); // to avoid stale object error + UiObject2 app = scrollAndFindUiObject(By.text(appName)); + if (app != null) { + clickAndWaitForIdleScreen(app); + } else { + throw new IllegalStateException(String.format("App %s cannot be found", appName)); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void openMediaAppSettingsPage() { + List<UiObject2> mediaAppMenuItem = findUiObjects(getResourceFromConfig( + AutoConfigConstants.MEDIA_CENTER, + AutoConfigConstants.MEDIA_APP, + AutoConfigConstants.MEDIA_APP_DROP_DOWN_MENU)); + if (mediaAppMenuItem.size() == 0) { + throw new UnknownUiException("Unable to find Media App menu items."); + } + // settings page is 2nd last item in Menu Item list + int settingsItemPosition = mediaAppMenuItem.size() - 2; + clickAndWaitForIdleScreen(mediaAppMenuItem.get(settingsItemPosition)); + } + + /** {@inheritDoc} */ + @Override + public String getMediaAppUserNotLoggedInErrorMessage() { + UiObject2 noLoginMsg = findUiObject(getResourceFromConfig( + AutoConfigConstants.MEDIA_CENTER, + AutoConfigConstants.MEDIA_APP, + AutoConfigConstants.MEDIA_APP_NO_LOGIN_MSG)); + if (noLoginMsg == null) { + throw new UnknownUiException("Unable to find Media app error text."); + } + return noLoginMsg.getText(); + } } diff --git a/libraries/automotive-helpers/media-center-app-helper/src/android/platform/helpers/TestMediaAppHelperImpl.java b/libraries/automotive-helpers/media-center-app-helper/src/android/platform/helpers/TestMediaAppHelperImpl.java new file mode 100644 index 000000000..c74ec9924 --- /dev/null +++ b/libraries/automotive-helpers/media-center-app-helper/src/android/platform/helpers/TestMediaAppHelperImpl.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.platform.helpers; + +import android.app.Instrumentation; +import android.os.SystemClock; +import android.platform.helpers.exceptions.UnknownUiException; +import android.support.test.uiautomator.UiObject2; + +import java.util.List; + +public class TestMediaAppHelperImpl extends AbstractAutoStandardAppHelper + implements IAutoTestMediaAppHelper { + // Wait Time + private static final int SHORT_RESPONSE_WAIT_MS = 1000; + + public TestMediaAppHelperImpl(Instrumentation instr) { + super(instr); + } + + /** + * {@inheritDoc} + */ + @Override + public void loadMediaInLocalMediaTestApp() { + // Open Account type + clickAndWait( + AutoConfigConstants.TEST_MEDIA_ACCOUNT_TYPE, "Account Type"); + // Select Paid Account type + clickAndWait( + AutoConfigConstants.TEST_MEDIA_ACCOUNT_TYPE_PAID, "Account Type: Paid"); + // open Root node type + clickAndWait( + AutoConfigConstants.TEST_MEDIA_ROOT_NODE_TYPE, "Root node type"); + // select Browsable content + clickAndWait(AutoConfigConstants.TEST_MEDIA_ROOT_NODE_TYPE_BROWSABLE, + "Browsable Content"); + // close settings + clickAndWait(AutoConfigConstants.TEST_MEDIA_APP_CLOSE_SETTING, "Close Settings"); + selectSongInTestMediaApp(); + } + + private void selectSongInTestMediaApp() { + List<UiObject2> songList = findUiObjects(getResourceFromConfig( + AutoConfigConstants.MEDIA_CENTER, + AutoConfigConstants.MEDIA_APP, + AutoConfigConstants.MEDIA_SONGS_LIST)); + if (songList.size() == 0) { + throw new UnknownUiException("Unable to find Songs in the Test Media App."); + } + clickAndWaitForIdleScreen(songList.get(1)); + SystemClock.sleep(SHORT_RESPONSE_WAIT_MS); + // minimize songs + UiObject2 goBackToSongsList = findUiObject(getResourceFromConfig( + AutoConfigConstants.MEDIA_CENTER, + AutoConfigConstants.MEDIA_APP, + AutoConfigConstants.MEDIA_APP_NAVIGATION_ICON)); + clickAndWaitForIdleScreen(goBackToSongsList); + SystemClock.sleep(SHORT_RESPONSE_WAIT_MS); + } + + private void clickAndWait(String autoConfigConstants, String fieldName) { + UiObject2 mediaTestAppField = findUiObject(getResourceFromConfig( + AutoConfigConstants.MEDIA_CENTER, + AutoConfigConstants.TEST_MEDIA_APP, + autoConfigConstants)); + if (mediaTestAppField == null) { + throw new UnknownUiException("Unable to find Test Media App field: " + fieldName); + } + clickAndWaitForIdleScreen(mediaTestAppField); + SystemClock.sleep(SHORT_RESPONSE_WAIT_MS); + } +} diff --git a/libraries/automotive-helpers/notifications-app-helper/src/android/platform/helpers/AutoNotificationMockingHelperImpl.java b/libraries/automotive-helpers/notifications-app-helper/src/android/platform/helpers/AutoNotificationMockingHelperImpl.java index c35620044..a92ad3467 100644 --- a/libraries/automotive-helpers/notifications-app-helper/src/android/platform/helpers/AutoNotificationMockingHelperImpl.java +++ b/libraries/automotive-helpers/notifications-app-helper/src/android/platform/helpers/AutoNotificationMockingHelperImpl.java @@ -70,11 +70,6 @@ public class AutoNotificationMockingHelperImpl extends AbstractAutoStandardAppHe getResourceFromConfig( AutoConfigConstants.NOTIFICATIONS, AutoConfigConstants.EXPANDED_NOTIFICATIONS_SCREEN, - AutoConfigConstants.APP_NAME)); - NOTIFICATION_REQUIRED_FIELDS.add( - getResourceFromConfig( - AutoConfigConstants.NOTIFICATIONS, - AutoConfigConstants.EXPANDED_NOTIFICATIONS_SCREEN, AutoConfigConstants.NOTIFICATION_TITLE)); NOTIFICATION_REQUIRED_FIELDS.add( getResourceFromConfig( diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfigConstants.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfigConstants.java index 161ac43e2..a5cf1edc0 100644 --- a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfigConstants.java +++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoConfigConstants.java @@ -235,6 +235,7 @@ public class AutoConfigConstants { // Media Center Screen public static final String MEDIA_CENTER_SCREEN = "MEDIA_CENTER_SCREEN"; public static final String PLAY_PAUSE_BUTTON = "PLAY_PAUSE_BUTTON"; + public static final String MEDIA_SONGS_LIST = "MEDIA_SONGS_LIST"; // NEXT_BUTTON from Account Settings public static final String PREVIOUS_BUTTON = "PREVIOUS_BUTTON"; public static final String SHUFFLE_BUTTON = "SHUFFLE_BUTTON"; @@ -253,4 +254,14 @@ public class AutoConfigConstants { public static final String MEDIA_APP = "MEDIA_APP"; public static final String MEDIA_APP_TITLE = "MEDIA_APP_TITLE"; public static final String MEDIA_APP_DROP_DOWN_MENU = "MEDIA_APP_DROP_DOWN_MENU"; + public static final String MEDIA_APP_NAVIGATION_ICON = "MEDIA_APP_NAVIGATION_ICON"; + public static final String MEDIA_APP_NO_LOGIN_MSG = "MEDIA_APP_NO_LOGIN_MSG"; + // Test Media App + public static final String TEST_MEDIA_APP = "TEST_MEDIA_APP"; + public static final String TEST_MEDIA_ACCOUNT_TYPE = "TEST_MEDIA_ACCOUNT_TYPE"; + public static final String TEST_MEDIA_ACCOUNT_TYPE_PAID = "TEST_MEDIA_ACCOUNT_TYPE_PAID"; + public static final String TEST_MEDIA_ROOT_NODE_TYPE = "TEST_MEDIA_ROOT_NODE_TYPE"; + public static final String TEST_MEDIA_ROOT_NODE_TYPE_BROWSABLE = + "TEST_MEDIA_ROOT_NODE_TYPE_BROWSABLE"; + public static final String TEST_MEDIA_APP_CLOSE_SETTING = "TEST_MEDIA_APP_CLOSE_SETTING"; } diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoMediaCenterConfigUtility.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoMediaCenterConfigUtility.java index f7deeae79..4368d2607 100644 --- a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoMediaCenterConfigUtility.java +++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoMediaCenterConfigUtility.java @@ -32,6 +32,9 @@ public class AutoMediaCenterConfigUtility implements IAutoConfigUtility { // Car Launcher For Reference Device private static final String CAR_LAUNCHER_PACKAGE = "com.android.car.carlauncher"; + // TEST Media App Package + private static final String TEST_MEDIA_APP_PACKAGE = "com.android.car.media.testmediaapp"; + // Config Map private Map<String, AutoConfiguration> mMediaCenterConfigMap; @@ -164,6 +167,9 @@ public class AutoMediaCenterConfigUtility implements IAutoConfigUtility { // Default Media Apps UI Config loadDefaultMediaApp(mMediaCenterConfigMap); + + // Load Test Media app UI Config + loadDefaultTestMediaApp(mMediaCenterConfigMap); } private void loadDefaultMediaCenterAppConfig(Map<String, String> mApplicationConfigMap) { @@ -273,7 +279,50 @@ public class AutoMediaCenterConfigUtility implements IAutoConfigUtility { new AutoConfigResource( AutoConfigConstants.RESOURCE_ID, "car_ui_toolbar_title", MEDIA_CENTER_PACKAGE)); + mediaAppConfiguration.addResource( + AutoConfigConstants.MEDIA_SONGS_LIST, + new AutoConfigResource( + AutoConfigConstants.RESOURCE_ID, + "item_container", MEDIA_CENTER_PACKAGE)); + mediaAppConfiguration.addResource( + AutoConfigConstants.MEDIA_APP_NAVIGATION_ICON, + new AutoConfigResource( + AutoConfigConstants.RESOURCE_ID, + "car_ui_toolbar_nav_icon_container", MEDIA_CENTER_PACKAGE)); + mediaAppConfiguration.addResource( + AutoConfigConstants.MEDIA_APP_NO_LOGIN_MSG, + new AutoConfigResource( + AutoConfigConstants.RESOURCE_ID, + "error_message", MEDIA_CENTER_PACKAGE)); mMediaCenterConfigMap.put( AutoConfigConstants.MEDIA_APP, mediaAppConfiguration); } + + private void loadDefaultTestMediaApp( + Map<String, AutoConfiguration> mMediaCenterConfigMap) { + AutoConfiguration testMediaAppConfiguration = new AutoConfiguration(); + testMediaAppConfiguration.addResource( + AutoConfigConstants.TEST_MEDIA_ACCOUNT_TYPE, + new AutoConfigResource( + AutoConfigConstants.TEXT, "Account Type")); + testMediaAppConfiguration.addResource( + AutoConfigConstants.TEST_MEDIA_ACCOUNT_TYPE_PAID, + new AutoConfigResource( + AutoConfigConstants.TEXT, "Paid")); + testMediaAppConfiguration.addResource( + AutoConfigConstants.TEST_MEDIA_ROOT_NODE_TYPE, + new AutoConfigResource( + AutoConfigConstants.TEXT, "Root node type")); + testMediaAppConfiguration.addResource( + AutoConfigConstants.TEST_MEDIA_ROOT_NODE_TYPE_BROWSABLE, + new AutoConfigResource( + AutoConfigConstants.TEXT, "Only browse-able content")); + testMediaAppConfiguration.addResource( + AutoConfigConstants.TEST_MEDIA_APP_CLOSE_SETTING, + new AutoConfigResource( + AutoConfigConstants.RESOURCE_ID, + "close_target", TEST_MEDIA_APP_PACKAGE)); + mMediaCenterConfigMap.put( + AutoConfigConstants.TEST_MEDIA_APP, testMediaAppConfiguration); + } } diff --git a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoNotificationsConfigUtility.java b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoNotificationsConfigUtility.java index c04c76317..8447e7f27 100644 --- a/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoNotificationsConfigUtility.java +++ b/libraries/automotive-helpers/utility-helper/src/android/platform/helpers/AutoNotificationsConfigUtility.java @@ -183,11 +183,9 @@ public class AutoNotificationsConfigUtility implements IAutoConfigUtility { expandedNotificationsConfig.addResource( AutoConfigConstants.APP_ICON, new AutoConfigResource( - AutoConfigConstants.RESOURCE_ID, "app_icon", SYSTEM_UI_PACKAGE)); - expandedNotificationsConfig.addResource( - AutoConfigConstants.APP_NAME, - new AutoConfigResource( - AutoConfigConstants.RESOURCE_ID, "header_text", SYSTEM_UI_PACKAGE)); + AutoConfigConstants.RESOURCE_ID, + "notification_body_icon", + SYSTEM_UI_PACKAGE)); expandedNotificationsConfig.addResource( AutoConfigConstants.NOTIFICATION_TITLE, new AutoConfigResource( diff --git a/libraries/collectors-helper/lyric/src/com/android/helpers/LyricMemProfilerHelper.java b/libraries/collectors-helper/lyric/src/com/android/helpers/LyricMemProfilerHelper.java new file mode 100644 index 000000000..8647039e0 --- /dev/null +++ b/libraries/collectors-helper/lyric/src/com/android/helpers/LyricMemProfilerHelper.java @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.helpers; + +import android.util.Log; +import androidx.annotation.VisibleForTesting; +import androidx.test.InstrumentationRegistry; +import androidx.test.uiautomator.UiDevice; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This is a collector helper that collects the dumpsys meminfo output for specified services and + * puts them into files. + */ +public class LyricMemProfilerHelper implements ICollectorHelper<Integer> { + + private static final String TAG = LyricMemProfilerHelper.class.getSimpleName(); + + private static final String PID_CMD = "pgrep -f -o "; + + private static final String DUMPSYS_MEMINFO_CMD = "dumpsys meminfo -s "; + + private static final String DMABUF_DUMP_CMD = "dmabuf_dump"; + + private static final int MIN_PROFILE_PERIOD_MS = 100; + + String mPidName = "camera.provider@"; + + // Extract value of "Native Heap:" and "TOTAL PSS:" from command: "dumpsys meminfo -s [pid]" + // example of "dumpsys meminfo -s [pid]": + // Applications Memory Usage (in Kilobytes): + // Uptime: 2649336 Realtime: 3041976 + // ** MEMINFO in pid 14612 [android.hardwar] ** + // App Summary + // Pss(KB) Rss(KB) + // ------ ------ + // Java Heap: 0 0 + // Native Heap: 377584 377584 + // Code: 79008 117044 + // Stack: 3364 3364 + // Graphics: 47672 47672 + // Private Other: 37188 + // System: 5307 + // Unknown: 39136 + // + // TOTAL PSS: 550123 TOTAL RSS: 584800 TOTAL SWAP (KB): + // 0 + // + // Above string example will be remove "\n" first and then extracted + // "377584" right after "Native Heap" and "550123" right after "TOTAL PSS" + // by following Regexes: + private static final Pattern METRIC_MEMINFO_PATTERN = + Pattern.compile(".+Native Heap:\\s*(\\d+)\\s*.+TOTAL PSS:\\s*(\\d+)\\s*.+"); + + // extrace value after "PROCESS TOTAL" in camera provider section + // of string from command "dmabuf_dump" + // Example: + // PROCESS TOTAL 1752 kB 1873 kB + // Above string example will be extracted 1752 as group(1) and 1832 as group(2) + private static final Pattern METRIC_DMABUF_PSS_PATTERN = + Pattern.compile("\\s*PROCESS TOTAL\\s*(\\d+)\\s*kB\\s*(\\d+)\\s*kB"); + + // Folling Regexes is for removing "\n" in string + private static final Pattern REMOVE_CR_PATTERN = Pattern.compile("\n"); + + // This Regexes is to match string format as: + // provider@2.7-se:[pid of provider] + // + // Use above pattern to find data section of camera provider + // of output string from command "dmabuf_dump" + private Pattern mDmabufProcPattern; + + private UiDevice mUiDevice; + + private String mCameraProviderPid = ""; + + private int mProfilePeriodMs = 0; + + private Timer mTimer; + + private static class MemInfo { + int mNativeHeap; + int mTotalPss; + + public MemInfo(int nativeHeap, int totalPss) { + mNativeHeap = nativeHeap; + mTotalPss = totalPss; + } + } + + private int mMaxNativeHeap; + + private int mMaxTotalPss; + + private int mMaxDmabuf; + + @VisibleForTesting + protected UiDevice initUiDevice() { + return UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + } + + @Override + public boolean startCollecting() { + if (null == mUiDevice) { + mUiDevice = initUiDevice(); + } + getCameraProviderPid(); + + // This Regexes is to match string format as: + // provider@2.7-se:[pid of provider] + // + // Use following pattern to find data section of camera provider + // of output string from command "dmabuf_dump" + mDmabufProcPattern = Pattern.compile("\\s*provider@.*:" + mCameraProviderPid + "\\s*"); + + // To avoid frequnce of polling memory data too high and interference test case + // Set minimum polling period to MIN_PROFILE_PERIOD_MS (100), period profile only + // enable when MIN_PROFILE_PERIOD_MS <= configured polling period + if (MIN_PROFILE_PERIOD_MS <= mProfilePeriodMs) { + if (null == mTimer) { + mTimer = new Timer(); + mTimer.schedule( + new TimerTask() { + @Override + public void run() { + MemInfo memInfo = processMemInfo(getMemInfoString()); + int dmabuf = processDmabufDump(getDmabufDumpString()); + mMaxNativeHeap = Math.max(mMaxNativeHeap, memInfo.mNativeHeap); + mMaxTotalPss = Math.max(mMaxTotalPss, memInfo.mTotalPss); + mMaxDmabuf = Math.max(mMaxDmabuf, dmabuf); + } + }, + MIN_PROFILE_PERIOD_MS, + mProfilePeriodMs); + } + } + return true; + } + + public void setProfilePeriodMs(int periodMs) { + mProfilePeriodMs = periodMs; + } + + public void setProfilePidName(String pidName) { + mPidName = pidName; + } + + @Override + public Map<String, Integer> getMetrics() { + String memInfoString = getMemInfoString(); + String dmabufDumpString = getDmabufDumpString(); + Map<String, Integer> metrics = processOutput(memInfoString, dmabufDumpString); + if (MIN_PROFILE_PERIOD_MS <= mProfilePeriodMs) { + metrics.put("maxNativeHeap", mMaxNativeHeap); + metrics.put("maxTotalPss", mMaxTotalPss); + metrics.put("maxDmabuf", mMaxDmabuf); + } + return metrics; + } + + @Override + public boolean stopCollecting() { + if (null != mTimer) { + mTimer.cancel(); + } + return true; + } + + private MemInfo processMemInfo(String memInfoString) { + int nativeHeap = 0, totalPss = 0; + Matcher matcher = + METRIC_MEMINFO_PATTERN.matcher( + REMOVE_CR_PATTERN.matcher(memInfoString).replaceAll("")); + if (matcher.find()) { + nativeHeap = Integer.parseInt(matcher.group(1)); + totalPss = Integer.parseInt(matcher.group(2)); + } else { + Log.e(TAG, "Failed to collect Lyric Native Heap or TOTAL PSS metrics."); + } + return new MemInfo(nativeHeap, totalPss); + } + + private int processDmabufDump(String dmabufDumpString) { + int dmabuf = 0; + Matcher matcher; + boolean procMatched = false; + for (String line : dmabufDumpString.split("\n")) { + if (procMatched) { + matcher = METRIC_DMABUF_PSS_PATTERN.matcher(line); + if (matcher.find()) { + dmabuf = Integer.parseInt(matcher.group(2)); + break; + } + } else { + matcher = mDmabufProcPattern.matcher(line); + if (matcher.find()) { + procMatched = true; + } + } + } + return dmabuf; + } + + @VisibleForTesting + Map<String, Integer> processOutput(String memInfoString, String dmabufDumpString) { + Map<String, Integer> metrics = new HashMap<>(); + MemInfo memInfo = processMemInfo(memInfoString); + int dmabuf = processDmabufDump(dmabufDumpString); + if (0 < memInfo.mNativeHeap) metrics.put("nativeHeap", memInfo.mNativeHeap); + if (0 < memInfo.mTotalPss) metrics.put("totalPss", memInfo.mTotalPss); + if (0 < dmabuf) metrics.put("dmabuf", dmabuf); + return metrics; + } + + @VisibleForTesting + public String getCameraProviderPid() { + try { + mCameraProviderPid = mUiDevice.executeShellCommand(PID_CMD + mPidName).trim(); + } catch (IOException e) { + Log.e(TAG, "Failed to get camera provider PID"); + mCameraProviderPid = ""; + } + return mCameraProviderPid; + } + + @VisibleForTesting + public String getMemInfoString() { + if (!mCameraProviderPid.isEmpty()) { + try { + String cmdString = DUMPSYS_MEMINFO_CMD + mCameraProviderPid; + return mUiDevice.executeShellCommand(cmdString).trim(); + } catch (IOException e) { + Log.e(TAG, "Failed to get Mem info string "); + } + } + return ""; + } + + @VisibleForTesting + public String getDmabufDumpString() { + if (!mCameraProviderPid.isEmpty()) { + try { + final int minDmabufStringLen = 100; + final int maxDmabufRetryCount = 3; + String dmabufString; + for (int retryCount = 0; retryCount < maxDmabufRetryCount; retryCount++) { + dmabufString = mUiDevice.executeShellCommand(DMABUF_DUMP_CMD).trim(); + // "dmabuf_dump" may not get dmabuf size information but get following string: + // "debugfs entry for dmabuf not available, using /proc/<pid>/fdinfo instead" + // Here use string length to detected above condition and retry. + // Normal dmabuf size string should larger than 100 characters. + if (minDmabufStringLen < dmabufString.length()) { + return dmabufString; + } + Log.w(TAG, "dmabuf_dump return abnormal:" + dmabufString + ",retry"); + } + } catch (IOException e) { + Log.e(TAG, "Failed to get DMA buf dump string"); + } + } + return ""; + } +} diff --git a/libraries/collectors-helper/lyric/test/src/com/android/helpers/LyricMemProfilerHelperTest.java b/libraries/collectors-helper/lyric/test/src/com/android/helpers/LyricMemProfilerHelperTest.java new file mode 100644 index 000000000..41675bfec --- /dev/null +++ b/libraries/collectors-helper/lyric/test/src/com/android/helpers/LyricMemProfilerHelperTest.java @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.helpers; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.SystemClock; +import android.util.Log; + +import androidx.test.uiautomator.UiDevice; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import java.util.Map; +import java.io.IOException; + +/** + * Android unit test for {@link LyricMemProfilerHelper} + * + * <p>To run: atest CollectorsHelperTest:com.android.helpers.LyricMemProfilerHelperTest + */ +@RunWith(AndroidJUnit4.class) +public class LyricMemProfilerHelperTest { + private static final String TAG = LyricMemProfilerHelperTest.class.getSimpleName(); + + private @Mock UiDevice mUiDevice; + + private int mMemInfoCmdCounter = 0; + + private int mDmabufCmdCounter = 0; + + private static final int MOCK_NATIVE_HEAP = 100; + + private static final int MOCK_TOTAL_PSS = 200; + + private static final int MOCK_DMABUF = 500; + + private String genMemInfoString(int nativeHeap, int totalPss) { + return ".Native Heap:" + nativeHeap + " TOTAL PSS:" + totalPss + " ."; + } + + private String genDmabufString(int dmabuf, int pid) { + String dmabufDumpString = + " provider@2.7-se:" + + pid + + "\n" + + " Name Rss Pss " + + " nr_procs Inode\n" + + " <unknown> 576 kB 576 kB " + + " 1 153387\n" + + " <unknown> 8 kB 8 kB " + + " 1 153388\n" + + " <unknown> 576 kB 576 kB " + + " 1 153389\n" + + " <unknown> 8 kB 8 kB " + + " 1 153390\n" + + " <unknown> 576 kB 576 kB " + + " 1 205579\n" + + " <unknown> 8 kB 8 kB " + + " 1 205580\n" + + " PROCESS TOTAL 1752 kB " + + dmabuf + + " kB\n" + + "----------------------\n" + + "dmabuf total: 1752 kB kernel_rss: 0 kB userspace_rss: 1752 kB" + + " userspace_pss: 1752 kB"; + + return dmabufDumpString; + } + + @Before + public void setUp() throws Throwable { + MockitoAnnotations.initMocks(this); + // Return a fake pid for our fake processes and an empty string otherwise. + doAnswer( + (inv) -> { + final int pid = 1234; + String cmd = (String) inv.getArguments()[0]; + if (cmd.startsWith("pgrep")) { + return Integer.toString(pid); + } else if (cmd.startsWith("dumpsys meminfo")) { + mMemInfoCmdCounter++; + if (10 == mMemInfoCmdCounter) { + return genMemInfoString( + MOCK_NATIVE_HEAP + 50, + MOCK_TOTAL_PSS + 50); // max value + } else { + return genMemInfoString(MOCK_NATIVE_HEAP, MOCK_TOTAL_PSS); + } + } else if (cmd.startsWith("dmabuf_dump")) { + mDmabufCmdCounter++; + if (10 == mDmabufCmdCounter) { + return genDmabufString(MOCK_DMABUF + 50, pid); // max value + } else { + return genDmabufString(MOCK_DMABUF, pid); + } + } + return ""; + }) + .when(mUiDevice) + .executeShellCommand(any()); + } + + @Test + public void testParsePid() { + LyricMemProfilerHelper helper = new LyricMemProfilerHelper(); + String memInfoString = helper.getMemInfoString(); + String dmabufDumpString = helper.getDmabufDumpString(); + // memInfo and dmabufDump get empty string due to mCameraProviderPid is empty + assertThat(memInfoString).isEmpty(); + assertThat(dmabufDumpString).isEmpty(); + + SystemClock.sleep(1000); // sleep 1 second to wait for camera provider initialize + helper.startCollecting(); + memInfoString = helper.getMemInfoString(); + dmabufDumpString = helper.getDmabufDumpString(); + Map<String, Integer> metrics = helper.processOutput(memInfoString, dmabufDumpString); + + assertThat(metrics).containsKey("nativeHeap"); + assertThat(metrics).containsKey("totalPss"); + assertThat(metrics).containsKey("dmabuf"); + + assertThat(metrics.get("nativeHeap")).isGreaterThan(0); + assertThat(metrics.get("totalPss")).isGreaterThan(0); + assertThat(metrics.get("dmabuf")).isGreaterThan(0); + } + + @Test + public void testProcessOutput() { + LyricMemProfilerHelper helper = new LyricMemProfilerHelper(); + helper.startCollecting(); + String pid = helper.getCameraProviderPid(); + String memInfoString = + "Applications Memory Usage (in Kilobytes):\n" + + "Uptime: 2649336 Realtime: 3041976\n" + + "** MEMINFO in pid 14612 [android.hardwar] **\n" + + "App Summary\n" + + " Pss(KB) Rss(KB)\n" + + " ------ ------\n" + + " Java Heap: 0 0\n" + + " Native Heap: 377584 377584\n" + + " Code: 79008 117044\n" + + " Stack: 3364 3364\n" + + " Graphics: 47672 47672\n" + + " Private Other: 37188\n" + + " System: 5307\n" + + " Unknown: 39136\n" + + "\n" + + " TOTAL PSS: 550123 TOTAL RSS: 584800 TOTAL" + + " SWAP (KB): 0"; + + String dmabufDumpString = + " provider@2.7-se:" + + pid + + "\n" + + " Name Rss Pss " + + " nr_procs Inode\n" + + " <unknown> 576 kB 576 kB " + + " 1 153387\n" + + " <unknown> 8 kB 8 kB " + + " 1 153388\n" + + " <unknown> 576 kB 576 kB " + + " 1 153389\n" + + " <unknown> 8 kB 8 kB " + + " 1 153390\n" + + " <unknown> 576 kB 576 kB " + + " 1 205579\n" + + " <unknown> 8 kB 8 kB " + + " 1 205580\n" + + " PROCESS TOTAL 1752 kB 1552 kB\n" + + "----------------------\n" + + "dmabuf total: 1752 kB kernel_rss: 0 kB userspace_rss: 1752 kB" + + " userspace_pss: 1752 kB"; + + Map<String, Integer> metrics = helper.processOutput(memInfoString, dmabufDumpString); + assertThat(metrics.get("nativeHeap")).isEqualTo(377584); + assertThat(metrics.get("totalPss")).isEqualTo(550123); + assertThat(metrics.get("dmabuf")).isEqualTo(1552); + + metrics = helper.processOutput("", ""); + assertThat(metrics).doesNotContainKey("nativeHeap"); + assertThat(metrics).doesNotContainKey("totalPss"); + assertThat(metrics).doesNotContainKey("dmabuf"); + } + + @Test + public void testProfilePeriod() { + LyricMemProfilerHelper helper = new TestableLyricMemProfilerHelper(); + helper.setProfilePeriodMs(100); + helper.startCollecting(); + SystemClock.sleep(2000); + Map<String, Integer> metrics = helper.getMetrics(); + helper.stopCollecting(); + assertThat(metrics).containsKey("nativeHeap"); + assertThat(metrics).containsKey("totalPss"); + assertThat(metrics).containsKey("dmabuf"); + assertThat(metrics).containsKey("maxNativeHeap"); + assertThat(metrics).containsKey("maxTotalPss"); + assertThat(metrics).containsKey("maxDmabuf"); + + assertThat(metrics.get("nativeHeap")).isLessThan(metrics.get("maxNativeHeap")); + assertThat(metrics.get("totalPss")).isLessThan(metrics.get("maxTotalPss")); + assertThat(metrics.get("dmabuf")).isLessThan(metrics.get("maxDmabuf")); + } + + @Test + public void testProfilePeriodLessThanMin() { + LyricMemProfilerHelper helper = new TestableLyricMemProfilerHelper(); + InOrder inOrder = inOrder(mUiDevice); + helper.setProfilePeriodMs(50); + helper.startCollecting(); + try { + inOrder.verify(mUiDevice).executeShellCommand("pgrep -f -o camera.provider@"); + } catch (IOException e) { + Log.e(TAG, "Failed to execute Shell command"); + } + SystemClock.sleep(3000); + verifyNoMoreInteractions(mUiDevice); + + Map<String, Integer> metrics = helper.getMetrics(); + helper.stopCollecting(); + assertThat(metrics).containsKey("nativeHeap"); + assertThat(metrics).containsKey("totalPss"); + assertThat(metrics).containsKey("dmabuf"); + assertThat(metrics).doesNotContainKey("maxNativeHeap"); + assertThat(metrics).doesNotContainKey("maxTotalPss"); + assertThat(metrics).doesNotContainKey("maxDmabuf"); + + assertThat(metrics.get("nativeHeap")).isEqualTo(MOCK_NATIVE_HEAP); + assertThat(metrics.get("totalPss")).isEqualTo(MOCK_TOTAL_PSS); + assertThat(metrics.get("dmabuf")).isEqualTo(MOCK_DMABUF); + } + + @Test + public void testSetNewPidName() { + LyricMemProfilerHelper helper = new TestableLyricMemProfilerHelper(); + InOrder inOrder = inOrder(mUiDevice); + final String newPidName = "new.camera.name"; + helper.setProfilePidName(newPidName); + helper.startCollecting(); + try { + inOrder.verify(mUiDevice).executeShellCommand("pgrep -f -o " + newPidName); + } catch (IOException e) { + Log.e(TAG, "Failed to execute Shell command"); + } + } + + private final class TestableLyricMemProfilerHelper extends LyricMemProfilerHelper { + @Override + protected UiDevice initUiDevice() { + return mUiDevice; + } + } +} diff --git a/libraries/collectors-helper/statsd/src/com/android/helpers/CrashHelper.java b/libraries/collectors-helper/statsd/src/com/android/helpers/CrashHelper.java index c6f14119b..99103e26f 100644 --- a/libraries/collectors-helper/statsd/src/com/android/helpers/CrashHelper.java +++ b/libraries/collectors-helper/statsd/src/com/android/helpers/CrashHelper.java @@ -66,8 +66,12 @@ public class CrashHelper implements ICollectorHelper<Integer> { appCrashResultMap.put(TOTAL_PREFIX + EVENT_NATIVE_CRASH, 0); appCrashResultMap.put(TOTAL_PREFIX + EVENT_ANR, 0); for (StatsLog.EventMetricData dataItem : eventMetricData) { - if (dataItem.atom.hasAppCrashOccurred()) { - AtomsProto.AppCrashOccurred appCrashAtom = dataItem.atom.getAppCrashOccurred(); + AtomsProto.Atom atom = dataItem.atom; + if (atom == null) { + atom = dataItem.aggregatedAtomInfo.atom; + } + if (atom.hasAppCrashOccurred()) { + AtomsProto.AppCrashOccurred appCrashAtom = atom.getAppCrashOccurred(); String eventType = appCrashAtom.eventType; String pkgName = appCrashAtom.packageName; int foregroundState = appCrashAtom.foregroundState; @@ -84,8 +88,8 @@ public class CrashHelper implements ICollectorHelper<Integer> { MetricUtility.constructKey( eventType, pkgName, String.valueOf(foregroundState)); MetricUtility.addMetric(detailKey, appCrashResultMap); - } else if (dataItem.atom.hasAnrOccurred()) { - AtomsProto.ANROccurred anrAtom = dataItem.atom.getAnrOccurred(); + } else if (atom.hasAnrOccurred()) { + AtomsProto.ANROccurred anrAtom = atom.getAnrOccurred(); String processName = anrAtom.processName; String reason = anrAtom.reason; int foregoundState = anrAtom.foregroundState; diff --git a/libraries/collectors-helper/statsd/src/com/android/helpers/StatsdHelper.java b/libraries/collectors-helper/statsd/src/com/android/helpers/StatsdHelper.java index f5e32942f..9b69a832c 100644 --- a/libraries/collectors-helper/statsd/src/com/android/helpers/StatsdHelper.java +++ b/libraries/collectors-helper/statsd/src/com/android/helpers/StatsdHelper.java @@ -182,6 +182,21 @@ public class StatsdHelper { return config; } + /** Returns accumulated StatsdStats. */ + public com.android.os.nano.StatsLog.StatsdStatsReport getStatsdStatsReport() { + com.android.os.nano.StatsLog.StatsdStatsReport report = + new com.android.os.nano.StatsLog.StatsdStatsReport(); + try { + adoptShellIdentity(); + byte[] serializedReports = getStatsManager().getStatsMetadata(); + report = com.android.os.nano.StatsLog.StatsdStatsReport.parseFrom(serializedReports); + dropShellIdentity(); + } catch (InvalidProtocolBufferNanoException | StatsUnavailableException se) { + Log.e(LOG_TAG, "Retrieving StatsdStats report failed.", se); + } + return report; + } + /** Returns the list of EventMetricData tracked under the config. */ public List<com.android.os.nano.StatsLog.EventMetricData> getEventMetrics() { List<com.android.os.nano.StatsLog.EventMetricData> eventData = new ArrayList<>(); @@ -196,7 +211,7 @@ public class StatsdHelper { dropShellIdentity(); } } catch (InvalidProtocolBufferNanoException | StatsUnavailableException se) { - Log.e(LOG_TAG, "Retreiving event metrics failed.", se); + Log.e(LOG_TAG, "Retrieving event metrics failed.", se); return eventData; } @@ -230,7 +245,7 @@ public class StatsdHelper { dropShellIdentity(); } } catch (InvalidProtocolBufferNanoException | StatsUnavailableException se) { - Log.e(LOG_TAG, "Retreiving gauge metrics failed.", se); + Log.e(LOG_TAG, "Retrieving gauge metrics failed.", se); return gaugeData; } diff --git a/libraries/collectors-helper/statsd/src/com/android/helpers/StatsdStatsHelper.java b/libraries/collectors-helper/statsd/src/com/android/helpers/StatsdStatsHelper.java new file mode 100644 index 000000000..427849487 --- /dev/null +++ b/libraries/collectors-helper/statsd/src/com/android/helpers/StatsdStatsHelper.java @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.helpers; + +import androidx.annotation.VisibleForTesting; + +import com.android.os.nano.StatsLog; + +import java.util.HashMap; +import java.util.Map; + +/** + * StatsdStatsHelper consist of helper methods to set the Statsd metadata collection and retrieve + * the necessary information from statsd. + */ +public class StatsdStatsHelper implements ICollectorHelper<Long> { + + static final String STATSDSTATS_PREFIX = "statsdstats"; + static final String ATOM_STATS_PREFIX = "atom_stats"; + static final String MATCHER_STATS_PREFIX = "matcher_stats"; + static final String CONDITION_STATS_PREFIX = "condition_stats"; + static final String METRIC_STATS_PREFIX = "metric_stats"; + static final String ALERT_STATS_PREFIX = "alert_stats"; + static final String CONFIG_STATS_PREFIX = "config_stats"; + static final String ANOMALY_ALARM_STATS_PREFIX = "anomaly_alarm_stats"; + static final String PULLED_ATOM_STATS_PREFIX = "pulled_atom_stats"; + static final String ATOM_METRIC_STATS_PREFIX = "atom_metric_stats"; + static final String DETECTED_LOG_LOSS_STATS_PREFIX = "detected_log_loss_stats"; + static final String EVENT_QUEUE_OVERFLOW_STATS_PREFIX = "event_queue_overflow_stats"; + + interface IStatsdHelper { + StatsLog.StatsdStatsReport getStatsdStatsReport(); + } + + private static class DefaultStatsdHelper implements IStatsdHelper { + + private StatsdHelper mStatsdHelper = new StatsdHelper(); + + @Override + public StatsLog.StatsdStatsReport getStatsdStatsReport() { + return mStatsdHelper.getStatsdStatsReport(); + } + } + + private IStatsdHelper mStatsdHelper = new DefaultStatsdHelper(); + + public StatsdStatsHelper() {} + + /** + * Constructor to simulate an externally provided statsd helper instance. Should not be used + * except for testing. + */ + @VisibleForTesting + StatsdStatsHelper(IStatsdHelper helper) { + mStatsdHelper = helper; + } + + /** Resets statsd metadata */ + @Override + public boolean startCollecting() { + // TODO: http://b/204890512 implement metadata reset + return true; + } + + /** Collect the statsd metadata accumulated during the test run. */ + @Override + public Map<String, Long> getMetrics() { + Map<String, Long> resultMap = new HashMap<>(); + + final StatsLog.StatsdStatsReport report = mStatsdHelper.getStatsdStatsReport(); + populateAtomStats(report.atomStats, resultMap); + populateConfigStats(report.configStats, resultMap); + populateAnomalyAlarmStats(report.anomalyAlarmStats, resultMap); + populatePulledAtomStats(report.pulledAtomStats, resultMap); + populateAtomMetricStats(report.atomMetricStats, resultMap); + populateDetectedLogLossStats(report.detectedLogLoss, resultMap); + populateEventQueueOverflowStats(report.queueOverflow, resultMap); + + return resultMap; + } + + @Override + public boolean stopCollecting() { + return true; + } + + private static void populateAtomStats( + StatsLog.StatsdStatsReport.AtomStats[] atomStats, Map<String, Long> resultMap) { + final String metricKeyPrefix = + MetricUtility.constructKey(STATSDSTATS_PREFIX, ATOM_STATS_PREFIX); + + for (final StatsLog.StatsdStatsReport.AtomStats dataItem : atomStats) { + final String metricKeyPrefixWithTag = + MetricUtility.constructKey(metricKeyPrefix, String.valueOf(dataItem.tag)); + + resultMap.put( + MetricUtility.constructKey(metricKeyPrefixWithTag, "count"), + Long.valueOf(dataItem.count)); + resultMap.put( + MetricUtility.constructKey(metricKeyPrefixWithTag, "error_count"), + Long.valueOf(dataItem.errorCount)); + } + } + + private static void populateConfigStats( + StatsLog.StatsdStatsReport.ConfigStats[] configStats, Map<String, Long> resultMap) { + final String metricKeyPrefix = + MetricUtility.constructKey(STATSDSTATS_PREFIX, CONFIG_STATS_PREFIX); + + for (final StatsLog.StatsdStatsReport.ConfigStats dataItem : configStats) { + final String metricKeyPrefixWithTag = + MetricUtility.constructKey(metricKeyPrefix, String.valueOf(dataItem.id)); + + resultMap.put( + MetricUtility.constructKey(metricKeyPrefixWithTag, "metric_count"), + Long.valueOf(dataItem.metricCount)); + resultMap.put( + MetricUtility.constructKey(metricKeyPrefixWithTag, "condition_count"), + Long.valueOf(dataItem.conditionCount)); + resultMap.put( + MetricUtility.constructKey(metricKeyPrefixWithTag, "matcher_count"), + Long.valueOf(dataItem.matcherCount)); + resultMap.put( + MetricUtility.constructKey(metricKeyPrefixWithTag, "alert_count"), + Long.valueOf(dataItem.alertCount)); + + populateMatcherStats(dataItem.matcherStats, resultMap, metricKeyPrefixWithTag); + populateConditionStats(dataItem.conditionStats, resultMap, metricKeyPrefixWithTag); + populateMetricStats(dataItem.metricStats, resultMap, metricKeyPrefixWithTag); + populateAlertStats(dataItem.alertStats, resultMap, metricKeyPrefixWithTag); + } + } + + private static void populateMetricStats( + StatsLog.StatsdStatsReport.MetricStats[] stats, + Map<String, Long> resultMap, + String metricKeyPrefix) { + for (final StatsLog.StatsdStatsReport.MetricStats dataItem : stats) { + final String metricKey = + MetricUtility.constructKey( + metricKeyPrefix, + METRIC_STATS_PREFIX, + String.valueOf(dataItem.id), + "max_tuple_counts"); + resultMap.put(metricKey, Long.valueOf(dataItem.maxTupleCounts)); + } + } + + private static void populateConditionStats( + StatsLog.StatsdStatsReport.ConditionStats[] stats, + Map<String, Long> resultMap, + String metricKeyPrefix) { + for (final StatsLog.StatsdStatsReport.ConditionStats dataItem : stats) { + final String metricKey = + MetricUtility.constructKey( + metricKeyPrefix, + CONDITION_STATS_PREFIX, + String.valueOf(dataItem.id), + "max_tuple_counts"); + resultMap.put(metricKey, Long.valueOf(dataItem.maxTupleCounts)); + } + } + + private static void populateMatcherStats( + StatsLog.StatsdStatsReport.MatcherStats[] stats, + Map<String, Long> resultMap, + String metricKeyPrefix) { + for (final StatsLog.StatsdStatsReport.MatcherStats dataItem : stats) { + final String metricKey = + MetricUtility.constructKey( + metricKeyPrefix, + MATCHER_STATS_PREFIX, + String.valueOf(dataItem.id), + "matched_times"); + resultMap.put(metricKey, Long.valueOf(dataItem.matchedTimes)); + } + } + + private static void populateAlertStats( + StatsLog.StatsdStatsReport.AlertStats[] stats, + Map<String, Long> resultMap, + String metricKeyPrefix) { + for (final StatsLog.StatsdStatsReport.AlertStats dataItem : stats) { + final String metricKey = + MetricUtility.constructKey( + metricKeyPrefix, + ALERT_STATS_PREFIX, + String.valueOf(dataItem.id), + "alerted_times"); + resultMap.put(metricKey, Long.valueOf(dataItem.alertedTimes)); + } + } + + private static void populateAnomalyAlarmStats( + StatsLog.StatsdStatsReport.AnomalyAlarmStats anomalyAlarmStats, + Map<String, Long> resultMap) { + if (anomalyAlarmStats == null) { + return; + } + final String metricKey = + MetricUtility.constructKey( + STATSDSTATS_PREFIX, ANOMALY_ALARM_STATS_PREFIX, "alarms_registered"); + resultMap.put(metricKey, Long.valueOf(anomalyAlarmStats.alarmsRegistered)); + } + + private static void populatePulledAtomStats( + StatsLog.StatsdStatsReport.PulledAtomStats[] pulledAtomStats, + Map<String, Long> resultMap) { + final String metricKeyPrefix = + MetricUtility.constructKey(STATSDSTATS_PREFIX, PULLED_ATOM_STATS_PREFIX); + + for (final StatsLog.StatsdStatsReport.PulledAtomStats dataItem : pulledAtomStats) { + final String metricKeyWithTag = + MetricUtility.constructKey(metricKeyPrefix, String.valueOf(dataItem.atomId)); + resultMap.put( + MetricUtility.constructKey(metricKeyWithTag, "total_pull"), + Long.valueOf(dataItem.totalPull)); + resultMap.put( + MetricUtility.constructKey(metricKeyWithTag, "total_pull_from_cache"), + Long.valueOf(dataItem.totalPullFromCache)); + resultMap.put( + MetricUtility.constructKey(metricKeyWithTag, "min_pull_interval_sec"), + Long.valueOf(dataItem.minPullIntervalSec)); + resultMap.put( + MetricUtility.constructKey(metricKeyWithTag, "average_pull_time_nanos"), + Long.valueOf(dataItem.averagePullTimeNanos)); + resultMap.put( + MetricUtility.constructKey(metricKeyWithTag, "max_pull_time_nanos"), + Long.valueOf(dataItem.maxPullTimeNanos)); + resultMap.put( + MetricUtility.constructKey(metricKeyWithTag, "average_pull_delay_nanos"), + Long.valueOf(dataItem.averagePullDelayNanos)); + resultMap.put( + MetricUtility.constructKey(metricKeyWithTag, "data_error"), + Long.valueOf(dataItem.dataError)); + resultMap.put( + MetricUtility.constructKey(metricKeyWithTag, "pull_timeout"), + Long.valueOf(dataItem.pullTimeout)); + resultMap.put( + MetricUtility.constructKey(metricKeyWithTag, "pull_exceed_max_delay"), + Long.valueOf(dataItem.pullExceedMaxDelay)); + resultMap.put( + MetricUtility.constructKey(metricKeyWithTag, "pull_failed"), + Long.valueOf(dataItem.pullFailed)); + resultMap.put( + MetricUtility.constructKey(metricKeyWithTag, "empty_data"), + Long.valueOf(dataItem.emptyData)); + resultMap.put( + MetricUtility.constructKey(metricKeyWithTag, "pull_registered_count"), + Long.valueOf(dataItem.registeredCount)); + resultMap.put( + MetricUtility.constructKey(metricKeyWithTag, "pull_unregistered_count"), + Long.valueOf(dataItem.unregisteredCount)); + resultMap.put( + MetricUtility.constructKey(metricKeyWithTag, "atom_error_count"), + Long.valueOf(dataItem.atomErrorCount)); + resultMap.put( + MetricUtility.constructKey(metricKeyWithTag, "binder_call_failed"), + Long.valueOf(dataItem.binderCallFailed)); + resultMap.put( + MetricUtility.constructKey(metricKeyWithTag, "failed_uid_provider_not_found"), + Long.valueOf(dataItem.failedUidProviderNotFound)); + resultMap.put( + MetricUtility.constructKey(metricKeyWithTag, "puller_not_found"), + Long.valueOf(dataItem.pullerNotFound)); + } + } + + private static void populateAtomMetricStats( + StatsLog.StatsdStatsReport.AtomMetricStats[] atomMetricStats, + Map<String, Long> resultMap) { + final String metricKeyPrefix = + MetricUtility.constructKey(STATSDSTATS_PREFIX, ATOM_METRIC_STATS_PREFIX); + + for (StatsLog.StatsdStatsReport.AtomMetricStats dataItem : atomMetricStats) { + final String metricKeyPrefixWithTag = + MetricUtility.constructKey(metricKeyPrefix, String.valueOf(dataItem.metricId)); + + resultMap.put( + MetricUtility.constructKey( + metricKeyPrefixWithTag, "hard_dimension_limit_reached"), + dataItem.hardDimensionLimitReached); + resultMap.put( + MetricUtility.constructKey(metricKeyPrefixWithTag, "late_log_event_skipped"), + dataItem.lateLogEventSkipped); + resultMap.put( + MetricUtility.constructKey(metricKeyPrefixWithTag, "skipped_forward_buckets"), + dataItem.skippedForwardBuckets); + resultMap.put( + MetricUtility.constructKey(metricKeyPrefixWithTag, "bad_value_type"), + dataItem.badValueType); + resultMap.put( + MetricUtility.constructKey( + metricKeyPrefixWithTag, "condition_change_in_next_bucket"), + dataItem.conditionChangeInNextBucket); + resultMap.put( + MetricUtility.constructKey(metricKeyPrefixWithTag, "invalidated_bucket"), + dataItem.invalidatedBucket); + resultMap.put( + MetricUtility.constructKey(metricKeyPrefixWithTag, "bucket_dropped"), + dataItem.bucketDropped); + resultMap.put( + MetricUtility.constructKey( + metricKeyPrefixWithTag, "min_bucket_boundary_delay_ns"), + dataItem.minBucketBoundaryDelayNs); + resultMap.put( + MetricUtility.constructKey( + metricKeyPrefixWithTag, "max_bucket_boundary_delay_ns"), + dataItem.maxBucketBoundaryDelayNs); + resultMap.put( + MetricUtility.constructKey(metricKeyPrefixWithTag, "bucket_unknown_condition"), + dataItem.bucketUnknownCondition); + resultMap.put( + MetricUtility.constructKey(metricKeyPrefixWithTag, "bucket_count"), + dataItem.bucketCount); + } + } + + private static void populateDetectedLogLossStats( + StatsLog.StatsdStatsReport.LogLossStats[] detectedLogLoss, + Map<String, Long> resultMap) { + final String metricKeyPrefix = + MetricUtility.constructKey(STATSDSTATS_PREFIX, DETECTED_LOG_LOSS_STATS_PREFIX); + + for (final StatsLog.StatsdStatsReport.LogLossStats dataItem : detectedLogLoss) { + final String metricKeyPrefixWithTag = + MetricUtility.constructKey( + metricKeyPrefix, String.valueOf(dataItem.detectedTimeSec)); + + resultMap.put( + MetricUtility.constructKey(metricKeyPrefixWithTag, "count"), + Long.valueOf(dataItem.count)); + resultMap.put( + MetricUtility.constructKey(metricKeyPrefixWithTag, "last_error"), + Long.valueOf(dataItem.lastError)); + resultMap.put( + MetricUtility.constructKey(metricKeyPrefixWithTag, "last_tag"), + Long.valueOf(dataItem.lastTag)); + resultMap.put( + MetricUtility.constructKey(metricKeyPrefixWithTag, "uid"), + Long.valueOf(dataItem.uid)); + resultMap.put( + MetricUtility.constructKey(metricKeyPrefixWithTag, "pid"), + Long.valueOf(dataItem.pid)); + } + } + + private static void populateEventQueueOverflowStats( + StatsLog.StatsdStatsReport.EventQueueOverflow queueOverflow, + Map<String, Long> resultMap) { + if (queueOverflow == null) { + return; + } + final String metricKeyPrefix = + MetricUtility.constructKey(STATSDSTATS_PREFIX, EVENT_QUEUE_OVERFLOW_STATS_PREFIX); + + resultMap.put( + MetricUtility.constructKey(metricKeyPrefix, "count"), + Long.valueOf(queueOverflow.count)); + resultMap.put( + MetricUtility.constructKey(metricKeyPrefix, "max_queue_history_nanos"), + Long.valueOf(queueOverflow.maxQueueHistoryNs)); + resultMap.put( + MetricUtility.constructKey(metricKeyPrefix, "min_queue_history_nanos"), + Long.valueOf(queueOverflow.minQueueHistoryNs)); + } + +} diff --git a/libraries/collectors-helper/statsd/src/com/android/helpers/UiInteractionFrameInfoHelper.java b/libraries/collectors-helper/statsd/src/com/android/helpers/UiInteractionFrameInfoHelper.java index 3f0fe1213..5a441fe02 100644 --- a/libraries/collectors-helper/statsd/src/com/android/helpers/UiInteractionFrameInfoHelper.java +++ b/libraries/collectors-helper/statsd/src/com/android/helpers/UiInteractionFrameInfoHelper.java @@ -112,6 +112,11 @@ public class UiInteractionFrameInfoHelper implements ICollectorHelper<StringBuil frameInfoMap); addMetric( + constructKey(KEY_PREFIX_CUJ, interactionType, "app_missed_frames"), + makeLogFriendly(uiInteractionFrameInfoReported.appMissedFrames), + frameInfoMap); + + addMetric( constructKey(KEY_PREFIX_CUJ, interactionType, SUFFIX_MAX_FRAME_MS), makeLogFriendly( uiInteractionFrameInfoReported.maxFrameTimeNanos / 1000000.0), diff --git a/libraries/collectors-helper/statsd/test/src/com/android/helpers/StatsdStatsHelperTest.java b/libraries/collectors-helper/statsd/test/src/com/android/helpers/StatsdStatsHelperTest.java new file mode 100644 index 000000000..d51f83dd5 --- /dev/null +++ b/libraries/collectors-helper/statsd/test/src/com/android/helpers/StatsdStatsHelperTest.java @@ -0,0 +1,573 @@ +/* + * 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.helpers; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.os.nano.StatsLog; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Map; + +/** + * Android Unit tests for {@link StatsdStatsHelper}. + * + * <p>To run: Disable SELinux: adb shell setenforce 0; if this fails with "permission denied", try + * Build and install Development apk. "adb shell su 0 setenforce 0" atest + * CollectorsHelperTest:com.android.helpers.StatsdStatsTest + */ +@RunWith(AndroidJUnit4.class) +public class StatsdStatsHelperTest { + + private static class TestNonEmptyStatsdHelper implements StatsdStatsHelper.IStatsdHelper { + + StatsLog.StatsdStatsReport testReport = new StatsLog.StatsdStatsReport(); + + static final int ATOM_STATS_COUNT = 2; + static final int CONFIG_STATS_COUNT = 1; + static final int CONFIG_STATS_METRIC_COUNT = 2; + static final int CONFIG_STATS_CONDITION_COUNT = 2; + static final int CONFIG_STATS_ALERT_COUNT = 2; + static final int CONFIG_STATS_MATCHER_COUNT = 2; + static final int PULLED_ATOM_STATS_COUNT = 2; + static final int ATOM_METRIC_STATS_COUNT = 1; + static final int DETECTED_LOG_LOSS_STATS_COUNT = 1; + + public TestNonEmptyStatsdHelper() { + populateAtomStatsTestData(testReport); + populateConfigStatsTestData(testReport); + populateAnomalyAlarmStatsTestData(testReport); + populatePulledAtomStatsTestData(testReport); + populateAtomMetricStatsTestData(testReport); + populateDetectedLogLossStatsTestData(testReport); + populateEventQueueOverflowStatsTestData(testReport); + } + + private static void populateAtomStatsTestData(StatsLog.StatsdStatsReport testReport) { + testReport.atomStats = new StatsLog.StatsdStatsReport.AtomStats[ATOM_STATS_COUNT]; + + for (int i = 0; i < ATOM_STATS_COUNT; i++) { + testReport.atomStats[i] = new StatsLog.StatsdStatsReport.AtomStats(); + int fieldValue = i + 1; + testReport.atomStats[i].tag = fieldValue++; + testReport.atomStats[i].count = fieldValue++; + testReport.atomStats[i].errorCount = fieldValue++; + } + } + + private static void populateConfigStatsTestData(StatsLog.StatsdStatsReport testReport) { + testReport.configStats = new StatsLog.StatsdStatsReport.ConfigStats[CONFIG_STATS_COUNT]; + for (int i = 0; i < CONFIG_STATS_COUNT; i++) { + testReport.configStats[i] = new StatsLog.StatsdStatsReport.ConfigStats(); + testReport.configStats[i].id = i + 1; + testReport.configStats[i].metricCount = CONFIG_STATS_METRIC_COUNT; + testReport.configStats[i].conditionCount = CONFIG_STATS_CONDITION_COUNT; + testReport.configStats[i].alertCount = CONFIG_STATS_ALERT_COUNT; + testReport.configStats[i].matcherCount = CONFIG_STATS_MATCHER_COUNT; + + testReport.configStats[i].metricStats = + populateConfigStatsMetricTestData(CONFIG_STATS_METRIC_COUNT); + testReport.configStats[i].conditionStats = + populateConfigStatsConditionTestData(CONFIG_STATS_CONDITION_COUNT); + testReport.configStats[i].matcherStats = + populateConfigStatsMatcherTestData(CONFIG_STATS_ALERT_COUNT); + testReport.configStats[i].alertStats = + populateConfigStatsAlertTestData(CONFIG_STATS_MATCHER_COUNT); + } + } + + private static StatsLog.StatsdStatsReport.AlertStats[] populateConfigStatsAlertTestData( + int configStatsAlertCount) { + StatsLog.StatsdStatsReport.AlertStats[] alertStats = + new StatsLog.StatsdStatsReport.AlertStats[configStatsAlertCount]; + + for (int i = 0; i < configStatsAlertCount; i++) { + alertStats[i] = new StatsLog.StatsdStatsReport.AlertStats(); + int fieldValue = i + 1; + alertStats[i].id = fieldValue++; + alertStats[i].alertedTimes = fieldValue++; + } + + return alertStats; + } + + private static StatsLog.StatsdStatsReport.MetricStats[] populateConfigStatsMetricTestData( + int configStatsMetricCount) { + StatsLog.StatsdStatsReport.MetricStats[] metricStats = + new StatsLog.StatsdStatsReport.MetricStats[configStatsMetricCount]; + + for (int i = 0; i < configStatsMetricCount; i++) { + metricStats[i] = new StatsLog.StatsdStatsReport.MetricStats(); + int fieldValue = i + 1; + metricStats[i].id = fieldValue++; + metricStats[i].maxTupleCounts = fieldValue++; + } + + return metricStats; + } + + private static StatsLog.StatsdStatsReport.ConditionStats[] + populateConfigStatsConditionTestData(int configStatsConditionCount) { + StatsLog.StatsdStatsReport.ConditionStats[] conditionStats = + new StatsLog.StatsdStatsReport.ConditionStats[configStatsConditionCount]; + + for (int i = 0; i < configStatsConditionCount; i++) { + conditionStats[i] = new StatsLog.StatsdStatsReport.ConditionStats(); + int fieldValue = i + 1; + conditionStats[i].id = fieldValue++; + conditionStats[i].maxTupleCounts = fieldValue++; + } + + return conditionStats; + } + + private static StatsLog.StatsdStatsReport.MatcherStats[] populateConfigStatsMatcherTestData( + int configStatsMatcherCount) { + StatsLog.StatsdStatsReport.MatcherStats[] matcherStats = + new StatsLog.StatsdStatsReport.MatcherStats[configStatsMatcherCount]; + + for (int i = 0; i < configStatsMatcherCount; i++) { + matcherStats[i] = new StatsLog.StatsdStatsReport.MatcherStats(); + int fieldValue = i + 1; + matcherStats[i].id = fieldValue++; + matcherStats[i].matchedTimes = fieldValue++; + } + + return matcherStats; + } + + private static void populateAnomalyAlarmStatsTestData( + StatsLog.StatsdStatsReport testReport) { + testReport.anomalyAlarmStats = new StatsLog.StatsdStatsReport.AnomalyAlarmStats(); + testReport.anomalyAlarmStats.alarmsRegistered = 1; + } + + private static void populatePulledAtomStatsTestData(StatsLog.StatsdStatsReport testReport) { + testReport.pulledAtomStats = + new StatsLog.StatsdStatsReport.PulledAtomStats[PULLED_ATOM_STATS_COUNT]; + + for (int i = 0; i < PULLED_ATOM_STATS_COUNT; i++) { + testReport.pulledAtomStats[i] = new StatsLog.StatsdStatsReport.PulledAtomStats(); + int fieldValue = i + 1; + testReport.pulledAtomStats[i].atomId = fieldValue++; + testReport.pulledAtomStats[i].totalPull = fieldValue++; + testReport.pulledAtomStats[i].totalPullFromCache = fieldValue++; + testReport.pulledAtomStats[i].minPullIntervalSec = fieldValue++; + testReport.pulledAtomStats[i].averagePullTimeNanos = fieldValue++; + testReport.pulledAtomStats[i].maxPullTimeNanos = fieldValue++; + testReport.pulledAtomStats[i].averagePullDelayNanos = fieldValue++; + testReport.pulledAtomStats[i].dataError = fieldValue++; + testReport.pulledAtomStats[i].pullTimeout = fieldValue++; + testReport.pulledAtomStats[i].pullExceedMaxDelay = fieldValue++; + testReport.pulledAtomStats[i].pullFailed = fieldValue++; + testReport.pulledAtomStats[i].emptyData = fieldValue++; + testReport.pulledAtomStats[i].registeredCount = fieldValue++; + testReport.pulledAtomStats[i].unregisteredCount = fieldValue++; + testReport.pulledAtomStats[i].atomErrorCount = fieldValue++; + testReport.pulledAtomStats[i].binderCallFailed = fieldValue++; + testReport.pulledAtomStats[i].failedUidProviderNotFound = fieldValue++; + testReport.pulledAtomStats[i].pullerNotFound = fieldValue++; + } + } + + private static void populateAtomMetricStatsTestData(StatsLog.StatsdStatsReport testReport) { + testReport.atomMetricStats = + new StatsLog.StatsdStatsReport.AtomMetricStats[ATOM_METRIC_STATS_COUNT]; + for (int i = 0; i < ATOM_METRIC_STATS_COUNT; i++) { + testReport.atomMetricStats[i] = new StatsLog.StatsdStatsReport.AtomMetricStats(); + int fieldValue = i + 1; + testReport.atomMetricStats[i].metricId = fieldValue++; + testReport.atomMetricStats[i].hardDimensionLimitReached = fieldValue++; + testReport.atomMetricStats[i].lateLogEventSkipped = fieldValue++; + testReport.atomMetricStats[i].skippedForwardBuckets = fieldValue++; + testReport.atomMetricStats[i].badValueType = fieldValue++; + testReport.atomMetricStats[i].conditionChangeInNextBucket = fieldValue++; + testReport.atomMetricStats[i].invalidatedBucket = fieldValue++; + testReport.atomMetricStats[i].bucketDropped = fieldValue++; + testReport.atomMetricStats[i].minBucketBoundaryDelayNs = fieldValue++; + testReport.atomMetricStats[i].maxBucketBoundaryDelayNs = fieldValue++; + testReport.atomMetricStats[i].bucketUnknownCondition = fieldValue++; + testReport.atomMetricStats[i].bucketCount = fieldValue++; + } + } + + private static void populateDetectedLogLossStatsTestData( + StatsLog.StatsdStatsReport testReport) { + testReport.detectedLogLoss = + new StatsLog.StatsdStatsReport.LogLossStats[DETECTED_LOG_LOSS_STATS_COUNT]; + + for (int i = 0; i < DETECTED_LOG_LOSS_STATS_COUNT; i++) { + testReport.detectedLogLoss[i] = new StatsLog.StatsdStatsReport.LogLossStats(); + int fieldValue = i + 1; + testReport.detectedLogLoss[i].detectedTimeSec = fieldValue++; + testReport.detectedLogLoss[i].count = fieldValue++; + testReport.detectedLogLoss[i].lastError = fieldValue++; + testReport.detectedLogLoss[i].lastTag = fieldValue++; + testReport.detectedLogLoss[i].uid = fieldValue++; + testReport.detectedLogLoss[i].pid = fieldValue++; + } + } + + private static void populateEventQueueOverflowStatsTestData( + StatsLog.StatsdStatsReport testReport) { + testReport.queueOverflow = new StatsLog.StatsdStatsReport.EventQueueOverflow(); + int fieldValue = 1; + testReport.queueOverflow.count = fieldValue++; + testReport.queueOverflow.minQueueHistoryNs = fieldValue++; + testReport.queueOverflow.maxQueueHistoryNs = fieldValue++; + } + + @Override + public StatsLog.StatsdStatsReport getStatsdStatsReport() { + return testReport; + } + } + + private static class TestEmptyStatsdHelper implements StatsdStatsHelper.IStatsdHelper { + + StatsLog.StatsdStatsReport testReport = new StatsLog.StatsdStatsReport(); + + public TestEmptyStatsdHelper() {} + + @Override + public StatsLog.StatsdStatsReport getStatsdStatsReport() { + return testReport; + } + } + + private static void verifyAtomStats(Map<String, Long> result, int atomsCount) { + for (int i = 0; i < atomsCount; i++) { + final String metricKeyPrefix = + MetricUtility.constructKey( + StatsdStatsHelper.STATSDSTATS_PREFIX, + StatsdStatsHelper.ATOM_STATS_PREFIX, + String.valueOf(i + 1)); + int fieldValue = i + 2; + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "count")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "error_count")), + Long.valueOf(fieldValue++)); + } + } + + private static void verifyConfigAlertStats( + Map<String, Long> result, String metricKeyPrefix, long count) { + for (long i = 0; i < count; i++) { + final String metricKey = + MetricUtility.constructKey( + metricKeyPrefix, + StatsdStatsHelper.ALERT_STATS_PREFIX, + String.valueOf(i + 1), + "alerted_times"); + assertEquals(result.get(metricKey), Long.valueOf(i + 2)); + } + } + + private static void verifyConfigMatcherStats( + Map<String, Long> result, String metricKeyPrefix, long count) { + for (long i = 0; i < count; i++) { + final String metricKey = + MetricUtility.constructKey( + metricKeyPrefix, + StatsdStatsHelper.MATCHER_STATS_PREFIX, + String.valueOf(i + 1), + "matched_times"); + assertEquals(result.get(metricKey), Long.valueOf(i + 2)); + } + } + + private static void verifyConfigConditionStats( + Map<String, Long> result, String metricKeyPrefix, long count) { + for (long i = 0; i < count; i++) { + final String metricKey = + MetricUtility.constructKey( + metricKeyPrefix, + StatsdStatsHelper.CONDITION_STATS_PREFIX, + String.valueOf(i + 1), + "max_tuple_counts"); + assertEquals(result.get(metricKey), Long.valueOf(i + 2)); + } + } + + private static void verifyConfigMetricStats( + Map<String, Long> result, String metricKeyPrefix, long count) { + for (long i = 0; i < count; i++) { + final String metricKey = + MetricUtility.constructKey( + metricKeyPrefix, + StatsdStatsHelper.METRIC_STATS_PREFIX, + String.valueOf(i + 1), + "max_tuple_counts"); + assertEquals(result.get(metricKey), Long.valueOf(i + 2)); + } + } + + private static void verifyConfigStats( + Map<String, Long> result, + int configStatsCount, + int configStatsMetricCount, + int configStatsConditionCount, + int configStatsMatcherCount, + int configStatsAlertCount) { + + for (int i = 0; i < configStatsCount; i++) { + final String metricKeyPrefix = + MetricUtility.constructKey( + StatsdStatsHelper.STATSDSTATS_PREFIX, + StatsdStatsHelper.CONFIG_STATS_PREFIX, + String.valueOf(i + 1)); + + final String metricCountKey = + MetricUtility.constructKey(metricKeyPrefix, "metric_count"); + assertEquals(result.get(metricCountKey), Long.valueOf(configStatsMetricCount)); + verifyConfigMetricStats(result, metricKeyPrefix, result.get(metricCountKey)); + final String conditionCountKey = + MetricUtility.constructKey(metricKeyPrefix, "condition_count"); + assertEquals(result.get(conditionCountKey), Long.valueOf(configStatsConditionCount)); + verifyConfigConditionStats(result, metricKeyPrefix, result.get(conditionCountKey)); + final String matcherCountKey = + MetricUtility.constructKey(metricKeyPrefix, "matcher_count"); + assertEquals(result.get(matcherCountKey), Long.valueOf(configStatsMatcherCount)); + verifyConfigMatcherStats(result, metricKeyPrefix, result.get(matcherCountKey)); + final String alertCountKey = MetricUtility.constructKey(metricKeyPrefix, "alert_count"); + assertEquals(result.get(alertCountKey), Long.valueOf(configStatsAlertCount)); + verifyConfigAlertStats(result, metricKeyPrefix, result.get(alertCountKey)); + } + } + + private static void verifyAnomalyAlarmStats(Map<String, Long> result) { + final String metricKeyPrefix = + MetricUtility.constructKey( + StatsdStatsHelper.STATSDSTATS_PREFIX, + StatsdStatsHelper.ANOMALY_ALARM_STATS_PREFIX); + final String metricKey = MetricUtility.constructKey(metricKeyPrefix, "alarms_registered"); + assertEquals(result.get(metricKey), Long.valueOf(1)); + } + + private static void verifyPulledAtomStats(Map<String, Long> result, int pulledAtomStatsCount) { + for (int i = 0; i < pulledAtomStatsCount; i++) { + int fieldValue = i + 1; + final String metricKeyPrefix = + MetricUtility.constructKey( + StatsdStatsHelper.STATSDSTATS_PREFIX, + StatsdStatsHelper.PULLED_ATOM_STATS_PREFIX, + String.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "total_pull")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get( + MetricUtility.constructKey(metricKeyPrefix, "total_pull_from_cache")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get( + MetricUtility.constructKey(metricKeyPrefix, "min_pull_interval_sec")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get( + MetricUtility.constructKey(metricKeyPrefix, "average_pull_time_nanos")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "max_pull_time_nanos")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get( + MetricUtility.constructKey( + metricKeyPrefix, "average_pull_delay_nanos")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "data_error")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "pull_timeout")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get( + MetricUtility.constructKey(metricKeyPrefix, "pull_exceed_max_delay")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "pull_failed")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "empty_data")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get( + MetricUtility.constructKey(metricKeyPrefix, "pull_registered_count")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get( + MetricUtility.constructKey(metricKeyPrefix, "pull_unregistered_count")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "atom_error_count")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "binder_call_failed")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get( + MetricUtility.constructKey( + metricKeyPrefix, "failed_uid_provider_not_found")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "puller_not_found")), + Long.valueOf(fieldValue++)); + } + } + + private static void verifyAtomMetricStats(Map<String, Long> result, int atomMetricCount) { + for (int i = 0; i < atomMetricCount; i++) { + int fieldValue = i + 1; + final String metricKeyPrefix = + MetricUtility.constructKey( + StatsdStatsHelper.STATSDSTATS_PREFIX, + StatsdStatsHelper.ATOM_METRIC_STATS_PREFIX, + String.valueOf(fieldValue++)); + assertEquals( + result.get( + MetricUtility.constructKey( + metricKeyPrefix, "hard_dimension_limit_reached")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get( + MetricUtility.constructKey(metricKeyPrefix, "late_log_event_skipped")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get( + MetricUtility.constructKey(metricKeyPrefix, "skipped_forward_buckets")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "bad_value_type")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get( + MetricUtility.constructKey( + metricKeyPrefix, "condition_change_in_next_bucket")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "invalidated_bucket")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "bucket_dropped")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get( + MetricUtility.constructKey( + metricKeyPrefix, "min_bucket_boundary_delay_ns")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get( + MetricUtility.constructKey( + metricKeyPrefix, "max_bucket_boundary_delay_ns")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get( + MetricUtility.constructKey( + metricKeyPrefix, "bucket_unknown_condition")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "bucket_count")), + Long.valueOf(fieldValue++)); + } + } + + private static void verifyDetectedLogLossStats( + Map<String, Long> result, int logLossStatsCount) { + for (int i = 0; i < logLossStatsCount; i++) { + int fieldValue = i + 1; + final String metricKeyPrefix = + MetricUtility.constructKey( + StatsdStatsHelper.STATSDSTATS_PREFIX, + StatsdStatsHelper.DETECTED_LOG_LOSS_STATS_PREFIX, + String.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "count")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "last_error")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "last_tag")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "uid")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "pid")), + Long.valueOf(fieldValue++)); + } + } + + private static void verifyEventQueueOverfowStats(Map<String, Long> result) { + final String metricKeyPrefix = + MetricUtility.constructKey( + StatsdStatsHelper.STATSDSTATS_PREFIX, + StatsdStatsHelper.EVENT_QUEUE_OVERFLOW_STATS_PREFIX); + + int fieldValue = 1; + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "count")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "min_queue_history_nanos")), + Long.valueOf(fieldValue++)); + assertEquals( + result.get(MetricUtility.constructKey(metricKeyPrefix, "max_queue_history_nanos")), + Long.valueOf(fieldValue++)); + } + + @Test + public void testNonEmptyReport() throws Exception { + StatsdStatsHelper.IStatsdHelper statsdHelper = new TestNonEmptyStatsdHelper(); + StatsdStatsHelper statsdStatsHelper = new StatsdStatsHelper(statsdHelper); + + assertTrue(statsdStatsHelper.startCollecting()); + final Map<String, Long> result = statsdStatsHelper.getMetrics(); + verifyAtomStats(result, TestNonEmptyStatsdHelper.ATOM_STATS_COUNT); + verifyConfigStats( + result, + TestNonEmptyStatsdHelper.CONFIG_STATS_COUNT, + TestNonEmptyStatsdHelper.CONFIG_STATS_METRIC_COUNT, + TestNonEmptyStatsdHelper.CONFIG_STATS_CONDITION_COUNT, + TestNonEmptyStatsdHelper.CONFIG_STATS_MATCHER_COUNT, + TestNonEmptyStatsdHelper.CONFIG_STATS_ALERT_COUNT); + verifyAnomalyAlarmStats(result); + verifyPulledAtomStats(result, TestNonEmptyStatsdHelper.PULLED_ATOM_STATS_COUNT); + verifyAtomMetricStats(result, TestNonEmptyStatsdHelper.ATOM_METRIC_STATS_COUNT); + verifyDetectedLogLossStats(result, TestNonEmptyStatsdHelper.DETECTED_LOG_LOSS_STATS_COUNT); + verifyEventQueueOverfowStats(result); + assertTrue(statsdStatsHelper.stopCollecting()); + } + + @Test + public void testEmptyReport() throws Exception { + StatsdStatsHelper.IStatsdHelper statsdHelper = new TestEmptyStatsdHelper(); + StatsdStatsHelper statsdStatsHelper = new StatsdStatsHelper(statsdHelper); + + assertTrue(statsdStatsHelper.startCollecting()); + final Map<String, Long> result = statsdStatsHelper.getMetrics(); + assertEquals(result.size(), 0); + assertTrue(statsdStatsHelper.stopCollecting()); + } +} diff --git a/libraries/collectors-helper/system/src/com/android/helpers/TimeInStateHelper.java b/libraries/collectors-helper/system/src/com/android/helpers/TimeInStateHelper.java new file mode 100644 index 000000000..16be8c5f1 --- /dev/null +++ b/libraries/collectors-helper/system/src/com/android/helpers/TimeInStateHelper.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.helpers; + +import static com.android.helpers.MetricUtility.constructKey; + +import android.util.Log; +import androidx.test.InstrumentationRegistry; +import androidx.test.uiautomator.UiDevice; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** TimeInStateHelper is a helper to collect time_in_state frequency stats. */ +public class TimeInStateHelper implements ICollectorHelper<Long> { + private static final String LOG_TAG = TimeInStateHelper.class.getSimpleName(); + private static final String METRIC_KEY_PREFIX = "time_in_state"; + private static final String AVG_FREQ_KEY_SUFFIX = "avg_freq"; + private static final Pattern TIME_IN_STATE_PATTERN = Pattern.compile("(\\d+)\\s+(\\d+)"); + private static final String KEY_SOURCE_SEPARATOR = "@"; + + private static final String METRICS_LOG_FMT = "metrics key: %s, value: %d"; + private static final String READ_FILE_CMD = "cat %s"; + private static final String CHECK_FILE_EXIST_CMD = "file -b %s"; + + private List<String> mMetricKeys = new ArrayList<>(); + private List<String> mSourceLocations = new ArrayList<>(); + private List<Map<String, Long>> mBeforeFreqStats; + private List<Map<String, Long>> mAfterFreqStats; + private UiDevice mDevice; + + public void setUp(String... freqKeys) { + if (freqKeys == null) { + return; + } + for (int i = 0; i < freqKeys.length; i++) { + String[] keys = freqKeys[i].split(KEY_SOURCE_SEPARATOR); + if (keys.length != 2) { + Log.e(LOG_TAG, "Failed to parse " + freqKeys[i]); + throw new RuntimeException("Failed to parse " + freqKeys[i]); + } + String key = keys[0].trim(); + String source = keys[1].trim(); + Log.i(LOG_TAG, "key: " + key + ", source: " + source); + + String cmd = String.format(CHECK_FILE_EXIST_CMD, source); + String result = null; + try { + result = getDevice().executeShellCommand(cmd); + } catch (IOException e) { + Log.e(LOG_TAG, "Error when checking source file " + source); + } + if (result == null || result.contains("No such file or directory")) { + Log.e(LOG_TAG, "Source " + source + " does not exist"); + } else { + mMetricKeys.add(key); + mSourceLocations.add(source); + } + } + } + + @Override + public boolean startCollecting() { + Log.i(LOG_TAG, "start collecting..."); + try { + mBeforeFreqStats = readAllFreqStats(); + } catch (Exception e) { + Log.e(LOG_TAG, "Failed to collect frequency stats at the start time.", e); + throw new RuntimeException(e); + } + return true; + } + + @Override + public boolean stopCollecting() { + Log.i(LOG_TAG, "stop collecting..."); + return true; + } + + protected UiDevice getDevice() { + if (mDevice == null) { + mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + } + return mDevice; + } + + @Override + public Map<String, Long> getMetrics() { + Log.i(LOG_TAG, "get metrics..."); + + try { + mAfterFreqStats = readAllFreqStats(); + return calculateFreqStatsMetrics(); + } catch (Exception e) { + Log.e(LOG_TAG, "Failed to collect frequency stats at the end time.", e); + throw new RuntimeException(e); + } + } + + private List<Map<String, Long>> readAllFreqStats() { + List<Map<String, Long>> freqStatsMapList = new ArrayList<Map<String, Long>>(); + for (String source : mSourceLocations) { + Map<String, Long> result = readFreqStats(source); + freqStatsMapList.add(result); + } + return freqStatsMapList; + } + + private Map<String, Long> readFreqStats(String source) { + Map<String, Long> freqStatsMap = new HashMap<String, Long>(); + String timeInStateData = ""; + try { + String cmd = String.format(READ_FILE_CMD, source); + timeInStateData = getDevice().executeShellCommand(cmd); + Matcher m = TIME_IN_STATE_PATTERN.matcher(timeInStateData); + while (m.find()) { + String freq = m.group(1); + String time = m.group(2); + Log.i(LOG_TAG, source + ": freq= " + freq + ", time= " + time); + + freqStatsMap.put(freq, Long.parseLong(time)); + } + } catch (Exception e) { + Log.e( + LOG_TAG, + "Failed to read time_in_state from " + + source + + ", content:\n" + + timeInStateData, + e); + throw new RuntimeException(e); + } + + if (freqStatsMap.isEmpty()) { + Log.e( + LOG_TAG, + "Can't parse time_in_state from " + source + ", content:\n" + timeInStateData); + throw new RuntimeException("Failed to collect freq metrics."); + } + return freqStatsMap; + } + + private Map<String, Long> calculateFreqStatsMetrics() { + Log.i(LOG_TAG, "Collect frequency stats during the test"); + + Map<String, Long> freqStatsMetrics = new HashMap<String, Long>(); + for (int i = 0; i < mSourceLocations.size(); i++) { + long totalWeightedFreq = 0; + long totalTime = 0; + + Map<String, Long> afterFreqStatsMap = mAfterFreqStats.get(i); + Map<String, Long> beforeFreqStatsMap = mBeforeFreqStats.get(i); + String metricsPrefixKey = constructKey(METRIC_KEY_PREFIX, mMetricKeys.get(i)); + for (Map.Entry<String, Long> entry : afterFreqStatsMap.entrySet()) { + String freq = entry.getKey(); + long endTime = entry.getValue(); + long startTime = beforeFreqStatsMap.getOrDefault(freq, 0L); + long runTime = endTime - startTime; + if (runTime == 0) { + continue; + } + long freqValue = Long.parseLong(freq); + totalWeightedFreq += runTime * freqValue; + totalTime += runTime; + String metricsKey = constructKey(metricsPrefixKey, freq); + freqStatsMetrics.put(metricsKey, runTime); + Log.i(LOG_TAG, String.format(METRICS_LOG_FMT, metricsKey, runTime)); + } + + if (totalTime > 0) { + String avgFreqMetricsKey = constructKey(metricsPrefixKey, AVG_FREQ_KEY_SUFFIX); + long avgFreq = totalWeightedFreq / totalTime; + freqStatsMetrics.put(avgFreqMetricsKey, avgFreq); + Log.i(LOG_TAG, String.format(METRICS_LOG_FMT, avgFreqMetricsKey, avgFreq)); + } + } + + return freqStatsMetrics; + } +} diff --git a/libraries/collectors-helper/system/test/src/com/android/helpers/tests/TimeInStateHelperTest.java b/libraries/collectors-helper/system/test/src/com/android/helpers/tests/TimeInStateHelperTest.java new file mode 100644 index 000000000..9625b06ad --- /dev/null +++ b/libraries/collectors-helper/system/test/src/com/android/helpers/tests/TimeInStateHelperTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.helpers.tests; + +import static org.junit.Assert.assertEquals; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.helpers.TimeInStateHelper; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Map; + +/** + * Android unit test for {@link TimeInStateHelper} + * + * <p>To run: atest CollectorsHelperTest:com.android.helpers.tests.TimeInStateHelperTest + */ +@RunWith(AndroidJUnit4.class) +public class TimeInStateHelperTest { + private static final String METRIC_KEY_PREFIX = "time_in_state"; + private static final String AVG_FREQ_KEY_SUFFIX = "avg_freq"; + + private File file1; + private File file2; + + private TimeInStateHelper mTimeInStateHelper; + + private void writeToFile(File file, String content) { + try (FileWriter writer = new FileWriter(file.getPath())) { + writer.write(content); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + private String constructKey(String key, String freq) { + return METRIC_KEY_PREFIX + "_" + key + "_" + freq; + } + + @Before + public void setUp() { + mTimeInStateHelper = new TimeInStateHelper(); + + try { + file1 = File.createTempFile("temp1", "time_in_state"); + file1.deleteOnExit(); + file2 = File.createTempFile("temp2", "time_in_state"); + file2.deleteOnExit(); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + @Test + public void testCollectTimeInState_noSource() { + mTimeInStateHelper.startCollecting(); + mTimeInStateHelper.stopCollecting(); + Map<String, Long> results = mTimeInStateHelper.getMetrics(); + + assertEquals(results.size(), 0); + } + + @Test + public void testCollectTimeInState_oneSource() { + String key1 = "key1"; + mTimeInStateHelper.setUp(key1 + "@" + file1.getPath()); + + String content = "10000 100\n" + "250000 100\n"; + + writeToFile(file1, content); + mTimeInStateHelper.startCollecting(); + + content = "10000 200\n" + "250000 100\n" + "35000 100\n"; + writeToFile(file1, content); + mTimeInStateHelper.stopCollecting(); + Map<String, Long> results = mTimeInStateHelper.getMetrics(); + + assertEquals(results.size(), 3); + assertEquals(results.get(constructKey(key1, "10000")).longValue(), 100L); + assertEquals(results.get(constructKey(key1, "35000")).longValue(), 100L); + assertEquals(results.get(constructKey(key1, AVG_FREQ_KEY_SUFFIX)).longValue(), 22500L); + } + + @Test + public void testCollectTimeInState_multipleSources() { + String key1 = "key1"; + String key2 = "key2"; + mTimeInStateHelper.setUp(key1 + "@" + file1.getPath(), key2 + "@" + file2.getPath()); + + String content1 = "10000 200\n"; + writeToFile(file1, content1); + String content2 = "250000 100\n" + "750000 100\n"; + writeToFile(file2, content2); + mTimeInStateHelper.startCollecting(); + + content1 = "10000 250\n" + "150000 50"; + writeToFile(file1, content1); + content2 = "250000 180\n" + "750000 120\n"; + writeToFile(file2, content2); + mTimeInStateHelper.stopCollecting(); + Map<String, Long> results = mTimeInStateHelper.getMetrics(); + + assertEquals(results.size(), 6); + assertEquals(results.get(constructKey(key1, "10000")).longValue(), 50L); + assertEquals(results.get(constructKey(key1, "150000")).longValue(), 50L); + assertEquals(results.get(constructKey(key1, AVG_FREQ_KEY_SUFFIX)).longValue(), 80000L); + assertEquals(results.get(constructKey(key2, "250000")).longValue(), 80L); + assertEquals(results.get(constructKey(key2, "750000")).longValue(), 20L); + assertEquals(results.get(constructKey(key2, AVG_FREQ_KEY_SUFFIX)).longValue(), 350000L); + } +} diff --git a/libraries/compatibility-common-util/OWNERS b/libraries/compatibility-common-util/OWNERS index cb3f3b8f7..882e0ecfd 100644 --- a/libraries/compatibility-common-util/OWNERS +++ b/libraries/compatibility-common-util/OWNERS @@ -1,5 +1,4 @@ # Android EngProd Approvers dshi@google.com -fdeng@google.com guangzhu@google.com jdesprez@google.com diff --git a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/GasTest.java b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/GasTest.java index 833040181..1b80bee21 100644 --- a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/GasTest.java +++ b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/GasTest.java @@ -30,8 +30,9 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.TYPE}) public @interface GasTest { - // The GAS requirement ID the GasTest applies to. + // The GAS requirement ID('s) the GasTest applies to. // Example: @GasTest(requirement = "G-0-000") + // Example: @GasTest(requirement = "G-0-000, G-0-001, G-0-002") String requirement(); // The minimum GAS software requirement version the GasTest applies to. diff --git a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/ITestResult.java b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/ITestResult.java index 33340e66f..45811d229 100644 --- a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/ITestResult.java +++ b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/ITestResult.java @@ -170,4 +170,13 @@ public interface ITestResult extends Comparable<ITestResult> { * @param resultHistories The test result histories. */ void setTestResultHistories(List<TestResultHistory> resultHistories); + + /** + * Set test screenshots metadata of test item. This method is for per-case screenshots in CTS + * Verifier. If this field is used for large test suites like CTS, it may cause performance + * issues in APFE. Thus please do not use this field in other test suites. + */ + void setTestScreenshotsMetadata(TestScreenshotsMetadata screenshotsMetadata); + + TestScreenshotsMetadata getTestScreenshotsMetadata(); } diff --git a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/ResultHandler.java b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/ResultHandler.java index ffe16b4ea..2ed9a3202 100644 --- a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/ResultHandler.java +++ b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/ResultHandler.java @@ -53,7 +53,11 @@ import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; /** * Handles conversion of results to/from files. + * + * @deprecated b/170495912 Please avoid any change in the schema which would force updates in + * classes that currently handle the XML generation for *TS. */ +@Deprecated public class ResultHandler { private static final String ENCODING = "UTF-8"; diff --git a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/ScreenshotsMetadataHandler.java b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/ScreenshotsMetadataHandler.java new file mode 100644 index 000000000..a5b0a4e84 --- /dev/null +++ b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/ScreenshotsMetadataHandler.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2015 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.compatibility.common.util; + +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Handles conversion of results to/from files. + */ +public class ScreenshotsMetadataHandler { + + private static final String ENCODING = "UTF-8"; + private static final String TYPE = "org.kxml2.io.KXmlParser,org.kxml2.io.KXmlSerializer"; + private static final String NS = null; + public static final String SCREENSHOTS_METADATA_FILE_NAME = "screenshots_metadata.xml"; + + // XML constants + private static final String CASE_TAG = "TestCase"; + private static final String MODULE_TAG = "Module"; + private static final String NAME_ATTR = "name"; + private static final String RESULT_TAG = "Result"; + private static final String TEST_TAG = "Test"; + + /** + * @param result - result of a single Compatibility invocation + * @param resultDir - directory where to write the screenshots metadata file + * @return The screenshots metadata file created. + */ + public static File writeResults(IInvocationResult result, File resultDir) + throws IOException, XmlPullParserException { + File screenshotsMetadataFile = new File(resultDir, SCREENSHOTS_METADATA_FILE_NAME); + OutputStream stream = new FileOutputStream(screenshotsMetadataFile); + XmlSerializer serializer = XmlPullParserFactory.newInstance(TYPE, null).newSerializer(); + serializer.setOutput(stream, ENCODING); + serializer.startDocument(ENCODING, false); + serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + serializer.processingInstruction( + "xml-stylesheet type=\"text/xsl\" href=\"compatibility_result.xsl\""); + serializer.startTag(NS, RESULT_TAG); + // Results + for (IModuleResult module : result.getModules()) { + serializer.startTag(NS, MODULE_TAG); + serializer.attribute(NS, NAME_ATTR, module.getName()); + for (ICaseResult cr : module.getResults()) { + serializer.startTag(NS, CASE_TAG); + serializer.attribute(NS, NAME_ATTR, cr.getName()); + for (ITestResult r : cr.getResults()) { + TestStatus status = r.getResultStatus(); + if (status == null) { + continue; // test was not executed, don't report + } + serializer.startTag(NS, TEST_TAG); + serializer.attribute(NS, NAME_ATTR, r.getName()); + + TestScreenshotsMetadata screenshotsMetadata = r.getTestScreenshotsMetadata(); + if (screenshotsMetadata != null) { + TestScreenshotsMetadata.serialize(serializer, screenshotsMetadata); + } + serializer.endTag(NS, TEST_TAG); + } + serializer.endTag(NS, CASE_TAG); + } + serializer.endTag(NS, MODULE_TAG); + } + serializer.endDocument(); + return screenshotsMetadataFile; + } +} diff --git a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/TestResult.java b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/TestResult.java index d2a9ca91a..13db332d4 100644 --- a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/TestResult.java +++ b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/TestResult.java @@ -34,6 +34,7 @@ public class TestResult implements ITestResult { private boolean mIsRetry; private boolean mSkipped; private List<TestResultHistory> mTestResultHistories; + private TestScreenshotsMetadata mTestScreenshotsMetadata; /** * Create a {@link TestResult} for the given test name. @@ -237,6 +238,7 @@ public class TestResult implements ITestResult { mIsRetry = false; mSkipped = false; mTestResultHistories = null; + mTestScreenshotsMetadata = null; } /** @@ -299,4 +301,14 @@ public class TestResult implements ITestResult { public void setTestResultHistories(List<TestResultHistory> resultHistories) { mTestResultHistories = resultHistories; } + + @Override + public void setTestScreenshotsMetadata(TestScreenshotsMetadata screenshotsMetadata) { + mTestScreenshotsMetadata = screenshotsMetadata; + } + + @Override + public TestScreenshotsMetadata getTestScreenshotsMetadata() { + return mTestScreenshotsMetadata; + } } diff --git a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/TestScreenshotsMetadata.java b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/TestScreenshotsMetadata.java new file mode 100644 index 000000000..423e7ed5f --- /dev/null +++ b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/TestScreenshotsMetadata.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2022 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.compatibility.common.util; + +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Objects; +import java.util.Set; + +/** + * Utility class to add test case screenshot metadata to the report. This class records per-case + * screenshot metadata for CTS Verifier. If this field is used for large test suites like CTS, it + * may cause performance issues in APFE. Thus please do not use this class in other test suites. + */ +public class TestScreenshotsMetadata implements Serializable { + + // XML constants + private static final String SCREENSHOTS_TAG = "Screenshots"; + private static final String SCREENSHOT_TAG = "Screenshot"; + private static final String NAME_ATTR = "name"; + private static final String DESCRIPTION_ATTR = "description"; + + private final String mTestName; + private final Set<TestScreenshotsMetadata.ScreenshotMetadata> mScreenshotMetadataSet; + + /** + * Constructor of test screenshots metadata. + * + * @param screenshots a Set of ScreenshotMetadata. + */ + public TestScreenshotsMetadata(String testName, + Set<TestScreenshotsMetadata.ScreenshotMetadata> screenshots) { + mTestName = testName; + mScreenshotMetadataSet = screenshots; + } + + /** Get test name */ + public String getTestName() { + return mTestName; + } + + /** Get a set of ScreenshotMetadata. */ + public Set<TestScreenshotsMetadata.ScreenshotMetadata> getScreenshotMetadataSet() { + return mScreenshotMetadataSet; + } + + @Override + public String toString() { + ArrayList<String> arr = new ArrayList<>(); + for (TestScreenshotsMetadata.ScreenshotMetadata e : mScreenshotMetadataSet) { + arr.add(e.toString()); + } + return String.join(", ", arr); + } + + /** {@inheritDoc} */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof TestScreenshotsMetadata)) { + return false; + } + TestScreenshotsMetadata that = (TestScreenshotsMetadata) o; + return Objects.equals(mTestName, that.mTestName) + && Objects.equals(mScreenshotMetadataSet, that.mScreenshotMetadataSet); + } + + /** {@inheritDoc} */ + @Override + public int hashCode() { + return Objects.hash(mTestName, mScreenshotMetadataSet); + } + + /** + * Serializes a given {@link TestScreenshotsMetadata} to XML. + * + * @param serializer given serializer. + * @param screenshotsMetadata test screenshots metadata. + */ + public static void serialize( + XmlSerializer serializer, TestScreenshotsMetadata screenshotsMetadata) + throws IOException { + if (screenshotsMetadata == null) { + throw new IllegalArgumentException("Test screenshots metadata was null"); + } + + serializer.startTag(null, SCREENSHOTS_TAG); + + for (TestScreenshotsMetadata.ScreenshotMetadata screenshotMetadata : + screenshotsMetadata.getScreenshotMetadataSet()) { + serializer.startTag(null, SCREENSHOT_TAG); + serializer.attribute(null, + NAME_ATTR, String.valueOf(screenshotMetadata.getScreenshotName())); + serializer.attribute( + null, DESCRIPTION_ATTR, String.valueOf(screenshotMetadata.getDescription())); + serializer.endTag(null, SCREENSHOT_TAG); + } + serializer.endTag(null, SCREENSHOTS_TAG); + } + + /** Single screenshot information */ + public static class ScreenshotMetadata implements Serializable { + String mScreenshotName; + String mDescription; + + public void setDescription(String description) { + mDescription = description; + } + + public String getDescription() { + return mDescription; + } + + public void setScreenshotName(String screenshotName) { + mScreenshotName = screenshotName; + } + + public String getScreenshotName() { + return mScreenshotName; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ScreenshotMetadata)) { + return false; + } + TestScreenshotsMetadata.ScreenshotMetadata that = + (TestScreenshotsMetadata.ScreenshotMetadata) o; + return mScreenshotName.equals(that.mScreenshotName) + && mDescription.equals(that.mDescription); + } + + @Override + public int hashCode() { + return Objects.hash(mScreenshotName, mDescription); + } + + @Override + public String toString() { + return "[" + mScreenshotName + ", " + mDescription + "]"; + } + } +} diff --git a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/VersionCodes.java b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/VersionCodes.java index 8760d40dc..cdccb7945 100644 --- a/libraries/compatibility-common-util/src/com/android/compatibility/common/util/VersionCodes.java +++ b/libraries/compatibility-common-util/src/com/android/compatibility/common/util/VersionCodes.java @@ -49,5 +49,5 @@ public class VersionCodes { public static final int Q = 29; public static final int R = 30; public static final int S = 31; - + public static final int S_V2 = 32; } diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/AppVersionListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/AppVersionListener.java index 809c9bdf3..8b6c4c6af 100644 --- a/libraries/device-collectors/src/main/java/android/device/collectors/AppVersionListener.java +++ b/libraries/device-collectors/src/main/java/android/device/collectors/AppVersionListener.java @@ -64,7 +64,8 @@ public class AppVersionListener extends BaseCollectionListener<Long> { if (pkgNamesString == null) { Log.w(TAG, "No package name provided. All packages will be collected"); mAppVersionHelper.setUp(); + } else { + mAppVersionHelper.setUp(pkgNamesString.split(PKG_NAME_SEPARATOR)); } - mAppVersionHelper.setUp(pkgNamesString.split(PKG_NAME_SEPARATOR)); } } diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/BaseMetricListener.java b/libraries/device-collectors/src/main/java/android/device/collectors/BaseMetricListener.java index fcd596354..9d4a1323f 100644 --- a/libraries/device-collectors/src/main/java/android/device/collectors/BaseMetricListener.java +++ b/libraries/device-collectors/src/main/java/android/device/collectors/BaseMetricListener.java @@ -101,6 +101,10 @@ public class BaseMetricListener extends InstrumentationRunListener { private int mCollectIterationInterval = 1; private int mSkipMetricUntilIteration = 0; + // Whether to report the results as instrumentation results. Used by metric collector rules, + // which do not have the information to invoke InstrumentationRunFinished() to report metrics. + private boolean mReportAsInstrumentationResults = false; + public BaseMetricListener() { mIncludeFilters = new ArrayList<>(); mExcludeFilters = new ArrayList<>(); @@ -190,9 +194,13 @@ public class BaseMetricListener extends InstrumentationRunListener { } if (mTestData.hasMetrics()) { // Only send the status progress if there are metrics + if (mReportAsInstrumentationResults) { + getInstrumentation().addResults(mTestData.createBundleFromMetrics()); + } else { SendToInstrumentation.sendBundle(getInstrumentation(), mTestData.createBundleFromMetrics()); } + } } super.testFinished(description); } @@ -333,15 +341,18 @@ public class BaseMetricListener extends InstrumentationRunListener { } /** - * Create a directory inside external storage, and empty it. + * Create a directory inside external storage, and optionally empty it. * * @param dir full path to the dir to be created. + * @param empty whether to empty the new dirctory. * @return directory file created */ - public File createAndEmptyDirectory(String dir) { + public File createDirectory(String dir, boolean empty) { File rootDir = Environment.getExternalStorageDirectory(); File destDir = new File(rootDir, dir); - executeCommandBlocking("rm -rf " + destDir.getAbsolutePath()); + if (empty) { + executeCommandBlocking("rm -rf " + destDir.getAbsolutePath()); + } if (!destDir.exists() && !destDir.mkdirs()) { Log.e(getTag(), "Unable to create dir: " + destDir.getAbsolutePath()); return null; @@ -350,6 +361,16 @@ public class BaseMetricListener extends InstrumentationRunListener { } /** + * Create a directory inside external storage, and empty it. + * + * @param dir full path to the dir to be created. + * @return directory file created + */ + public File createAndEmptyDirectory(String dir) { + return createDirectory(dir, true); + } + + /** * Delete a directory and all the file inside. * * @param rootDir the {@link File} directory to delete. @@ -368,6 +389,11 @@ public class BaseMetricListener extends InstrumentationRunListener { } } + /** Sets whether metrics should be reported directly to instrumentation results. */ + public final void setReportAsInstrumentationResults(boolean enabled) { + mReportAsInstrumentationResults = enabled; + } + /** * Returns the name of the current class to be used as a logging tag. */ diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/LyricMemProfilerCollector.java b/libraries/device-collectors/src/main/java/android/device/collectors/LyricMemProfilerCollector.java new file mode 100644 index 000000000..3205fa298 --- /dev/null +++ b/libraries/device-collectors/src/main/java/android/device/collectors/LyricMemProfilerCollector.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.device.collectors; + +import android.os.Bundle; +import android.device.collectors.annotations.OptionClass; + +import com.android.helpers.LyricMemProfilerHelper; + +@OptionClass(alias = "lyric-mem-profiler-collector") +public class LyricMemProfilerCollector extends BaseCollectionListener<Integer> { + + private static final String TAG = LyricMemProfilerCollector.class.getSimpleName(); + private static final String PROFILE_PERIOD_KEY = "profile-period-ms-enable"; + private static final String PROFILE_PID_NAME = "profile-pid-name"; + private LyricMemProfilerHelper mHelper = new LyricMemProfilerHelper(); + + public LyricMemProfilerCollector() { + createHelperInstance(mHelper); + } + + /** Adds the options for total pss collector. */ + @Override + public void setupAdditionalArgs() { + Bundle args = getArgsBundle(); + String argString = args.getString(PROFILE_PERIOD_KEY); + if (argString != null) { + mHelper.setProfilePeriodMs(Integer.parseInt(argString)); + } + argString = args.getString(PROFILE_PID_NAME); + if (argString != null) { + mHelper.setProfilePidName(argString); + } + } +} diff --git a/libraries/device-collectors/src/main/java/android/device/collectors/ScreenRecordCollector.java b/libraries/device-collectors/src/main/java/android/device/collectors/ScreenRecordCollector.java index 76fc1f945..da9c98c71 100644 --- a/libraries/device-collectors/src/main/java/android/device/collectors/ScreenRecordCollector.java +++ b/libraries/device-collectors/src/main/java/android/device/collectors/ScreenRecordCollector.java @@ -47,6 +47,10 @@ public class ScreenRecordCollector extends BaseMetricListener { // * "low" is 1/8 the resolution. // * Otherwise, use the resolution. @VisibleForTesting static final String QUALITY_ARG = "video-quality"; + // Option for whether to empty the output directory before collecting. Defaults to true. Setting + // to false is useful when multiple test classes need recordings and recordings are pulled at + // the end of the test run. + @VisibleForTesting static final String EMPTY_OUTPUT_DIR_ARG = "empty-output-dir"; // Maximum parts per test (each part is <= 3min). @VisibleForTesting static final int MAX_RECORDING_PARTS = 5; private static final long VIDEO_TAIL_BUFFER = 500; @@ -59,6 +63,7 @@ public class ScreenRecordCollector extends BaseMetricListener { private RecordingThread mCurrentThread; private String mVideoDimensions; + private boolean mEmptyOutputDir; // Tracks the test iterations to ensure that each failure gets unique filenames. // Key: test description; value: number of iterations. @@ -75,8 +80,15 @@ public class ScreenRecordCollector extends BaseMetricListener { } @Override - public void onTestRunStart(DataRecord runData, Description description) { - mDestDir = createAndEmptyDirectory(OUTPUT_DIR); + public void onSetUp() { + mDestDir = createDirectory(OUTPUT_DIR, mEmptyOutputDir); + } + + @Override + public void setupAdditionalArgs() { + mEmptyOutputDir = + Boolean.parseBoolean( + getArgsBundle().getString(EMPTY_OUTPUT_DIR_ARG, String.valueOf(true))); try { long scaleDown = 1; @@ -163,18 +175,24 @@ public class ScreenRecordCollector extends BaseMetricListener { /** Returns the recording's name for part {@code part} of test {@code description}. */ private File getOutputFile(Description description, int part) { - final String baseName = - String.format("%s.%s", description.getClassName(), description.getMethodName()); - // Omit the iteration number for the first iteration. + StringBuilder builder = new StringBuilder(description.getClassName()); + if (description.getMethodName() != null) { + builder.append("."); + builder.append(description.getMethodName()); + } int iteration = mTestIterations.get(description.getDisplayName()); - final String fileName = - String.format( - "%s-video%s.mp4", - iteration == 1 - ? baseName - : String.join("-", baseName, String.valueOf(iteration)), - part == 1 ? "" : part); - return Paths.get(mDestDir.getAbsolutePath(), fileName).toFile(); + // Omit the iteration number for the first iteration. + if (iteration > 1) { + builder.append("-"); + builder.append(iteration); + } + builder.append("-video"); + // Omit the part number for the first part. + if (part > 1) { + builder.append(part); + } + builder.append(".mp4"); + return Paths.get(mDestDir.getAbsolutePath(), builder.toString()).toFile(); } /** Returns a buffer duration for the end of the video. */ diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/droidfood/README.md b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/droidfood/README.md new file mode 100644 index 000000000..011e9e537 --- /dev/null +++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/droidfood/README.md @@ -0,0 +1,4 @@ +# Droidfood Configs + +These configs are used to collect many WW metrics on droidfood devices. +They are usefull for statsd regression analysis within CrystalBall and GreenDay environment. diff --git a/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/droidfood/droidfood-run-level.pb b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/droidfood/droidfood-run-level.pb Binary files differnew file mode 100644 index 000000000..752abdae7 --- /dev/null +++ b/libraries/device-collectors/src/main/platform-collectors/res/statsd-configs/droidfood/droidfood-run-level.pb diff --git a/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/StatsdMetadataListener.java b/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/StatsdMetadataListener.java new file mode 100644 index 000000000..eaebce92b --- /dev/null +++ b/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/StatsdMetadataListener.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.device.collectors; + +import android.device.collectors.annotations.OptionClass; + +import com.android.helpers.StatsdStatsHelper; + +/** + * A {@link StatsdMetadataListener} that captures statsd metadata during test method. + * + * <p>The StatsdMetadataListener with {@link StatsdStatsHelper} support collects various statsd + * metadata information. This information will be populated into metrics with a prefix + * "statsdstats_" The metrics are about statsd logged atoms statistics, matchers and atom pullers + * such as below but not limited to: - atom stats (count, errors) - config stats (metrics, + * conditions, matcher & alerts count, and many more) - pulled atoms statsd (total pull count, data + * error, pull timeouts, etc.) Full list of available metrics is defined in {@link + * StatsdStatsHelper} + * + * <p>Do NOT throw exception anywhere in this class. We don't want to halt the test when metrics + * collection fails. + */ +@OptionClass(alias = "statsdstats-collector") +public class StatsdMetadataListener extends BaseCollectionListener<Long> { + public StatsdMetadataListener() { + createHelperInstance(new StatsdStatsHelper()); + } +} diff --git a/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/TimeInStateListener.java b/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/TimeInStateListener.java new file mode 100644 index 000000000..691160787 --- /dev/null +++ b/libraries/device-collectors/src/main/platform-collectors/src/android/device/collectors/TimeInStateListener.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.device.collectors; + +import android.device.collectors.annotations.OptionClass; +import android.os.Bundle; +import android.util.Log; +import androidx.annotation.VisibleForTesting; + +import com.android.helpers.TimeInStateHelper; + +/** + * A {@link TimeInStateListener} captures time_in_state frequency stats in the format of + * "[frequency] [time]" in each line which means that the total duration in the [frequency] state is + * equal to [time]. + * + * <p>Options: + * + * <ul> + * <li>-e key-source-mapping [mapping] : a comma-separated list of mapping "[key]@[source]" to + * provide [key] as a keyword part of the metric key and [source] as the source location + * containing the time_in_state data. + * </ul> + * + * <p>Do NOT throw exception anywhere in this class. We don't want to halt the test when metrics + * collection fails. + */ +@OptionClass(alias = "time-in-state-listener") +public class TimeInStateListener extends BaseCollectionListener<Long> { + + private static final String TAG = TimeInStateListener.class.getSimpleName(); + @VisibleForTesting static final String ARG_SEPARATOR = ","; + @VisibleForTesting static final String ARG_KEY = "key-source-mapping"; + + public TimeInStateListener() { + createHelperInstance(new TimeInStateHelper()); + } + + @VisibleForTesting + public TimeInStateListener(Bundle args, TimeInStateHelper helper) { + super(args, helper); + } + + @Override + public void setupAdditionalArgs() { + Bundle args = getArgsBundle(); + String keyArgString = args.getString(ARG_KEY); + if (keyArgString == null) { + Log.w(TAG, "No time_in_state provided. Nothing will be collected"); + return; + } + ((TimeInStateHelper) mHelper).setUp(keyArgString.split(ARG_SEPARATOR)); + } +} diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/BaseMetricListenerInstrumentedTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/BaseMetricListenerInstrumentedTest.java index 5e73d15ad..c7fe14502 100644 --- a/libraries/device-collectors/src/test/java/android/device/collectors/BaseMetricListenerInstrumentedTest.java +++ b/libraries/device-collectors/src/test/java/android/device/collectors/BaseMetricListenerInstrumentedTest.java @@ -725,4 +725,34 @@ public class BaseMetricListenerInstrumentedTest { assertEquals(RUN_END_VALUE, resultBundle.getString(RUN_END_KEY)); assertEquals(2, resultBundle.size()); } + + /** Test that the report as instrumentation result option works. */ + @MetricOption(group = "testGroup") + @Test + public void testReportAsInstrumentationResultsIfEnabled() throws Exception { + mListener.setReportAsInstrumentationResults(true); + + Description runDescription = Description.createSuiteDescription("run"); + mListener.testRunStarted(runDescription); + Description testDescription = Description.createTestDescription("class", "method"); + mListener.testStarted(testDescription); + mListener.testFinished(testDescription); + mListener.testRunFinished(new Result()); + // AJUR runner is then gonna call instrumentationRunFinished + Bundle resultBundle = new Bundle(); + mListener.instrumentationRunFinished(System.out, resultBundle, new Result()); + + // Check that results are reported via Instrumentation.addResults(). + ArgumentCaptor<Bundle> capture = ArgumentCaptor.forClass(Bundle.class); + Mockito.verify(mMockInstrumentation, Mockito.times(1)).addResults(capture.capture()); + Bundle addedResult = capture.getValue(); + assertTrue(addedResult.containsKey(TEST_END_KEY)); + assertEquals(TEST_END_VALUE + "method", addedResult.getString(TEST_END_KEY)); + + // Rather than Instrumentation.sendStatus(). + Mockito.verify(mMockInstrumentation, Mockito.never()) + .sendStatus( + Mockito.eq(SendToInstrumentation.INST_STATUS_IN_PROGRESS), + Mockito.any(Bundle.class)); + } } diff --git a/libraries/device-collectors/src/test/java/android/device/collectors/ScreenRecordCollectorTest.java b/libraries/device-collectors/src/test/java/android/device/collectors/ScreenRecordCollectorTest.java index ce1bfa55c..71162f473 100644 --- a/libraries/device-collectors/src/test/java/android/device/collectors/ScreenRecordCollectorTest.java +++ b/libraries/device-collectors/src/test/java/android/device/collectors/ScreenRecordCollectorTest.java @@ -16,7 +16,10 @@ package android.device.collectors; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.AdditionalMatchers.not; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.endsWith; import static org.mockito.ArgumentMatchers.eq; @@ -95,7 +98,7 @@ public class ScreenRecordCollectorTest { listener = spy(new ScreenRecordCollector()); } listener.setInstrumentation(mInstrumentation); - doReturn(mLogDir).when(listener).createAndEmptyDirectory(anyString()); + doReturn(mLogDir).when(listener).createDirectory(anyString(), anyBoolean()); doReturn(0L).when(listener).getTailBuffer(); doReturn(mDevice).when(listener).getDevice(); doReturn("1234").when(mDevice).executeShellCommand(eq("pidof screenrecord")); @@ -113,7 +116,7 @@ public class ScreenRecordCollectorTest { // Verify output directories are created on test run start. mListener.testRunStarted(mRunDesc); - verify(mListener).createAndEmptyDirectory(ScreenRecordCollector.OUTPUT_DIR); + verify(mListener).createDirectory(ScreenRecordCollector.OUTPUT_DIR, true); // Walk through a number of test cases to simulate behavior. for (int i = 1; i <= NUM_TEST_CASE; i++) { @@ -156,7 +159,14 @@ public class ScreenRecordCollectorTest { int videoCount = 0; for (Bundle bundle : capturedBundle) { for (String key : bundle.keySet()) { - if (key.contains("mp4")) videoCount++; + if (key.contains("mp4")) { + videoCount++; + assertTrue(key.contains(mTestDesc.getClassName())); + assertTrue(key.contains(mTestDesc.getMethodName())); + String fileName = bundle.getString(key); + assertTrue(fileName.contains(mTestDesc.getClassName())); + assertTrue(fileName.contains(mTestDesc.getMethodName())); + } } } assertEquals(NUM_TEST_CASE * ScreenRecordCollector.MAX_RECORDING_PARTS, videoCount); @@ -280,4 +290,58 @@ public class ScreenRecordCollectorTest { verify(mDevice, atLeastOnce()) .executeShellCommand(not(matches("screenrecord .*video.mp4"))); } + + /** Test that the empty-output-dir works. */ + @Test + public void testEmptyrOutputDirOptionSetToFalse() throws Exception { + Bundle args = new Bundle(); + args.putString(ScreenRecordCollector.EMPTY_OUTPUT_DIR_ARG, "false"); + mListener = initListener(args); + + // Verify output directories are created on test run start. + mListener.testRunStarted(mRunDesc); + verify(mListener).createDirectory(ScreenRecordCollector.OUTPUT_DIR, false); + } + + /** + * Test that descriptions with null method names only result in class names in the video file + * names. + */ + @Test + public void testNullMethodNameDoesNotAppearInVideoName() throws Exception { + mListener = initListener(null); + + mListener.testRunStarted(mRunDesc); + + // mRunDesc does not have a method name. + mListener.testStarted(mRunDesc); + // Delay verification by 100 ms to ensure the thread was started. + SystemClock.sleep(100); + mListener.testFinished(mRunDesc); + mListener.testRunFinished(new Result()); + + Bundle resultBundle = new Bundle(); + mListener.instrumentationRunFinished(System.out, resultBundle, new Result()); + + ArgumentCaptor<Bundle> capture = ArgumentCaptor.forClass(Bundle.class); + Mockito.verify(mInstrumentation, times(1)) + .sendStatus( + Mockito.eq(SendToInstrumentation.INST_STATUS_IN_PROGRESS), + capture.capture()); + Bundle metrics = capture.getValue(); + // Ensure that we have recordings, and none of them have "null" in their file name or metric + // key. + boolean hasRecordings = false; + for (String key : metrics.keySet()) { + if (key.startsWith(mListener.getTag())) { + hasRecordings = true; + assertTrue(key.contains(mRunDesc.getClassName())); + assertFalse(key.contains("null")); + String fileName = metrics.getString(key); + assertTrue(fileName.contains(mRunDesc.getClassName())); + assertFalse(fileName.contains("null")); + } + } + assertTrue(hasRecordings); + } } diff --git a/libraries/device-collectors/src/test/platform/android/device/collectors/TimeInStateListenerTest.java b/libraries/device-collectors/src/test/platform/android/device/collectors/TimeInStateListenerTest.java new file mode 100644 index 000000000..7ab9e17ec --- /dev/null +++ b/libraries/device-collectors/src/test/platform/android/device/collectors/TimeInStateListenerTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.device.collectors; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.MockitoAnnotations.initMocks; + +import android.app.Instrumentation; +import android.os.Bundle; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.helpers.TimeInStateHelper; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +/** + * Android Unit tests for {@link TimeInStateListener}. + * + * <p>To run: atest CollectorDeviceLibPlatformTest:android.device.collectors.TimeInStateListenerTest + */ +@RunWith(AndroidJUnit4.class) +public class TimeInStateListenerTest { + + @Mock private TimeInStateHelper mTimeInStateHelper; + @Mock private Instrumentation mInstrumentation; + + private Description mRunDesc; + + @Before + public void setup() { + initMocks(this); + mRunDesc = Description.createSuiteDescription("run"); + } + + @Test + public void testListener_noSource() throws Exception { + TimeInStateListener listener = initListener(new Bundle(), mTimeInStateHelper); + listener.testRunStarted(mRunDesc); + verify(mTimeInStateHelper, never()).setUp(any()); + } + + @Test + public void testListener_withSources() throws Exception { + Bundle bundle = new Bundle(); + bundle.putString( + TimeInStateListener.ARG_KEY, + String.join(TimeInStateListener.ARG_SEPARATOR, "key1@temp1", "key2@temp2")); + TimeInStateListener listener = initListener(bundle, mTimeInStateHelper); + listener.testRunStarted(mRunDesc); + verify(mTimeInStateHelper).setUp("key1@temp1", "key2@temp2"); + } + + private TimeInStateListener initListener(Bundle bundle, TimeInStateHelper helper) { + TimeInStateListener listener = new TimeInStateListener(bundle, helper); + listener.setInstrumentation(mInstrumentation); + return listener; + } +} diff --git a/libraries/flicker/.gitignore b/libraries/flicker/.gitignore new file mode 100644 index 000000000..caa32e675 --- /dev/null +++ b/libraries/flicker/.gitignore @@ -0,0 +1,2 @@ +.idea/ +*.iml
\ No newline at end of file diff --git a/libraries/flicker/Android.bp b/libraries/flicker/Android.bp index 13484795e..f65c6de09 100644 --- a/libraries/flicker/Android.bp +++ b/libraries/flicker/Android.bp @@ -21,6 +21,9 @@ package { java_test { name: "flickerlib", platform_apis: true, + optimize: { + enabled: false + }, srcs: [ "src/**/*.java", "src/**/*.kt" @@ -34,14 +37,21 @@ java_test { java_library { name: "flickerlib-core", platform_apis: true, + optimize: { + enabled: false + }, srcs: [ "src/com/android/server/wm/flicker/**/*.java", "src/com/android/server/wm/flicker/**/*.kt" ], + java_resource_dirs: [ + "src/com/android/server/wm/flicker/service/resources/" + ], exclude_srcs: [ "**/helpers/*", ], static_libs: [ + "flickerlib-helpers", "compatibility-device-util-axt", "ub-uiautomator", "androidx.test.uiautomator_uiautomator", @@ -51,12 +61,16 @@ java_library { "wm-proto-parsers", "platform-test-annotations", "platform-test-core-rules", + "health-testing-utils", ], } java_library { name: "flickerlib-helpers", sdk_version: "test_current", + optimize: { + enabled: false + }, srcs: [ "src/**/helpers/*.java", "src/**/helpers/*.kt", @@ -72,6 +86,9 @@ java_library { java_library { name: "wm-proto-parsers", sdk_version: "test_current", + optimize: { + enabled: false + }, srcs: [ "src/com/android/server/wm/traces/**/*.java", "src/com/android/server/wm/traces/**/*.kt", @@ -81,5 +98,16 @@ java_library { "androidx.test.ext.junit", "platformprotosnano", "layersprotosnano", + "flicker-tags-proto", + ], +} + +java_library { + name: "flicker-tags-proto", + srcs: [ + "**/*.proto", ], + optimize: { + enabled: false + } } diff --git a/libraries/flicker/README.md b/libraries/flicker/README.md index 97f79b991..0b5842433 100644 --- a/libraries/flicker/README.md +++ b/libraries/flicker/README.md @@ -111,7 +111,7 @@ Example of a failed test: ## Running Tests -The tests can be run as any other Android JUnit tests. `frameworks/base/tests/FlickerTests` uses the library to test common UI transitions. Run `atest FlickerTest` to execute these tests. +The tests can be run as any other Android JUnit tests. `frameworks/base/tests/FlickerTests` uses the library to test common UI transitions. Run `atest FlickerTests` to execute these tests. --- diff --git a/libraries/flicker/src/com/android/server/wm/flicker/Flicker.kt b/libraries/flicker/src/com/android/server/wm/flicker/Flicker.kt index 7bb2074d7..cc174d9ed 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/Flicker.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/Flicker.kt @@ -137,11 +137,9 @@ class Flicker( val result = result requireNotNull(result) - val failures = result.checkAssertions(listOf(assertion)) - val failureMessage = failures.joinToString("\n") { it.message } - - if (failureMessage.isNotEmpty()) { - throw AssertionError(failureMessage) + val failures = result.checkAssertion(assertion) + if (failures.isNotEmpty()) { + throw failures.first() } } diff --git a/libraries/flicker/src/com/android/server/wm/flicker/FlickerBlockJUnit4ClassRunner.kt b/libraries/flicker/src/com/android/server/wm/flicker/FlickerBlockJUnit4ClassRunner.kt index b43eab93e..d7f7fafc7 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/FlickerBlockJUnit4ClassRunner.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/FlickerBlockJUnit4ClassRunner.kt @@ -16,7 +16,9 @@ package com.android.server.wm.flicker +import android.platform.test.util.TestFilter import android.util.Log +import androidx.test.platform.app.InstrumentationRegistry import com.android.server.wm.flicker.dsl.FlickerBuilder import org.junit.internal.runners.statements.RunAfters import org.junit.runner.notification.RunNotifier @@ -30,6 +32,16 @@ import java.lang.reflect.Modifier * Implements the JUnit 4 standard test case class model, parsing from a flicker DSL. * * Supports both assertions in {@link org.junit.Test} and assertions defined in the DSL + * + * When using this runnr the default `atest class#method` command doesn't work. + * Instead use: -- --test-arg \ + * com.android.tradefed.testtype.AndroidJUnitTest:instrumentation-arg:filter-tests:=<TEST_NAME> + * + * For example: + * `atest FlickerTests -- \ + * --test-arg com.android.tradefed.testtype.AndroidJUnitTest:instrumentation-arg:filter-tests\ + * :=com.android.server.wm.flicker.close.\ + * CloseAppBackButtonTest#launcherWindowBecomesVisible[ROTATION_90_GESTURAL_NAV]` */ class FlickerBlockJUnit4ClassRunner @JvmOverloads constructor( test: TestWithParameters, @@ -42,6 +54,18 @@ class FlickerBlockJUnit4ClassRunner @JvmOverloads constructor( /** * {@inheritDoc} */ + override fun getChildren(): MutableList<FrameworkMethod> { + val arguments = InstrumentationRegistry.getArguments() + val validChildren = super.getChildren().filter { + val childDescription = describeChild(it) + TestFilter.isFilteredOrUnspecified(arguments, childDescription) + } + return validChildren.toMutableList() + } + + /** + * {@inheritDoc} + */ override fun classBlock(notifier: RunNotifier): Statement { val statement = childrenInvoker(notifier) val cleanUpMethod = getFlickerCleanUpMethod() diff --git a/libraries/flicker/src/com/android/server/wm/flicker/FlickerResult.kt b/libraries/flicker/src/com/android/server/wm/flicker/FlickerResult.kt index 006a153f6..62da20d53 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/FlickerResult.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/FlickerResult.kt @@ -18,6 +18,7 @@ package com.android.server.wm.flicker import com.android.server.wm.flicker.assertions.AssertionData import com.android.server.wm.flicker.assertions.FlickerAssertionError +import com.android.server.wm.flicker.assertions.FlickerAssertionErrorBuilder import com.google.common.truth.Truth /** @@ -51,22 +52,23 @@ data class FlickerResult( } /** - * Run the assertions on the trace + * Run the assertion on the trace * - * @throws AssertionError If the assertions fail or the transition crashed + * @throws AssertionError If the assertion fail or the transition crashed */ - internal fun checkAssertions( - assertions: List<AssertionData> - ): List<FlickerAssertionError> { + internal fun checkAssertion(assertion: AssertionData): List<FlickerAssertionError> { checkIsExecuted() - val currFailures: List<FlickerAssertionError> = runs.flatMap { run -> - assertions.filter { it.tag == run.assertionTag }.mapNotNull { assertion -> - try { - assertion.checkAssertion(run) - null - } catch (error: Throwable) { - FlickerAssertionError(error, assertion, run) - } + val filteredRuns = runs.filter { it.assertionTag == assertion.tag } + val currFailures = filteredRuns.mapNotNull { run -> + try { + assertion.checkAssertion(run) + null + } catch (error: Throwable) { + FlickerAssertionErrorBuilder() + .fromError(error) + .atTag(assertion.tag) + .withTrace(run.traceFiles) + .build() } } failures.addAll(currFailures) diff --git a/libraries/flicker/src/com/android/server/wm/flicker/FlickerRunResult.kt b/libraries/flicker/src/com/android/server/wm/flicker/FlickerRunResult.kt index 5508a53a7..22de5b998 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/FlickerRunResult.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/FlickerRunResult.kt @@ -54,11 +54,11 @@ class FlickerRunResult private constructor( /** * Truth subject that corresponds to a [WindowManagerTrace] or [WindowManagerState] */ - private val wmSubject: FlickerSubject?, + internal val wmSubject: FlickerSubject?, /** * Truth subject that corresponds to a [LayersTrace] or [LayerTraceEntry] */ - private val layersSubject: FlickerSubject?, + internal val layersSubject: FlickerSubject?, /** * Truth subject that corresponds to a list of [FocusEvent] */ diff --git a/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.kt b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.kt index 3f63df7d8..dc10c0003 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.kt @@ -18,7 +18,9 @@ package com.android.server.wm.flicker import android.util.Log import com.android.server.wm.flicker.monitor.ITransitionMonitor -import com.android.server.wm.traces.parser.DeviceStateDump +import com.android.server.wm.traces.common.ConditionList +import com.android.server.wm.traces.common.WindowManagerConditionsFactory +import com.android.server.wm.traces.parser.DeviceDumpParser import com.android.server.wm.traces.parser.getCurrentState import java.io.IOException import java.nio.file.Files @@ -81,6 +83,10 @@ open class TransitionRunner { * @param flicker test specification */ internal open fun run(flicker: Flicker): FlickerResult { + val uiStableCondition = ConditionList(listOf( + WindowManagerConditionsFactory.isWMStateComplete(), + WindowManagerConditionsFactory.hasLayersAnimating().negate() + )) val runs = mutableListOf<FlickerRunResult>() var executionError: Throwable? = null try { @@ -89,10 +95,12 @@ open class TransitionRunner { for (iteration in 0 until flicker.repetitions) { try { flicker.runSetup.forEach { it.invoke(flicker) } + flicker.wmHelper.waitFor(uiStableCondition) flicker.traceMonitors.forEach { it.start() } flicker.frameStatsMonitor?.run { start() } flicker.transitions.forEach { it.invoke(flicker) } } finally { + flicker.wmHelper.waitFor(uiStableCondition) flicker.traceMonitors.forEach { it.tryStop() } flicker.frameStatsMonitor?.run { tryStop() } flicker.runTeardown.forEach { it.invoke(flicker) } @@ -160,7 +168,7 @@ open class TransitionRunner { tags.add(tag) val deviceStateBytes = getCurrentState(flicker.instrumentation.uiAutomation) - val deviceState = DeviceStateDump.fromDump(deviceStateBytes.first, deviceStateBytes.second) + val deviceState = DeviceDumpParser.fromDump(deviceStateBytes.first, deviceStateBytes.second) try { val wmTraceFile = flicker.outputDir.resolve( getTaggedFilePath(flicker, tag, "wm_trace")) @@ -176,8 +184,8 @@ open class TransitionRunner { val result = builder.buildStateResult( tag, - deviceState.wmTrace, - deviceState.layersTrace + deviceState.wmState?.asTrace(), + deviceState.layerState?.asTrace() ) tagsResults.add(result) } catch (e: IOException) { diff --git a/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunnerWithRules.kt b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunnerWithRules.kt index fff00c558..9919af5f1 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunnerWithRules.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunnerWithRules.kt @@ -19,7 +19,9 @@ package com.android.server.wm.flicker import android.platform.test.rule.NavigationModeRule import android.platform.test.rule.PressHomeRule import android.platform.test.rule.UnlockScreenRule +import com.android.server.wm.flicker.helpers.SampleAppHelper import com.android.server.wm.flicker.rules.ChangeDisplayOrientationRule +import com.android.server.wm.flicker.rules.LaunchAppRule import com.android.server.wm.flicker.rules.RemoveAllTasksButHomeRule import org.junit.rules.RuleChain import org.junit.runner.Description @@ -33,12 +35,26 @@ import org.junit.runners.model.Statement class TransitionRunnerWithRules(private val testConfig: Map<String, Any?>) : TransitionRunner() { private var result: FlickerResult? = null - private fun buildDefaultSetupRules(): RuleChain { - return RuleChain.outerRule(ChangeDisplayOrientationRule(testConfig.startRotation)) - .around(RemoveAllTasksButHomeRule()) + /** + * Create the default flicker test setup rules. In order: + * - unlock device + * - change orientation + * - change navigation mode + * - launch an app + * - remove all apps + * - go to home screen + * + * (b/186740751) An app should be launched because, after changing the navigation mode, + * the first app launch is handled as a screen size change (similar to a rotation), this + * causes different problems during testing (e.g. IME now shown on app launch) + */ + private fun buildDefaultSetupRules(flicker: Flicker): RuleChain { + return RuleChain.outerRule(UnlockScreenRule()) .around(NavigationModeRule(testConfig.navBarMode)) + .around(LaunchAppRule(SampleAppHelper(flicker.instrumentation))) + .around(RemoveAllTasksButHomeRule()) + .around(ChangeDisplayOrientationRule(testConfig.startRotation)) .around(PressHomeRule()) - .around(UnlockScreenRule()) } private fun buildTransitionRule(flicker: Flicker): Statement { @@ -54,7 +70,7 @@ class TransitionRunnerWithRules(private val testConfig: Map<String, Any?>) : Tra } private fun buildTransitionChain(flicker: Flicker): Statement { - val setupRules = buildDefaultSetupRules() + val setupRules = buildDefaultSetupRules(flicker) val transitionRule = buildTransitionRule(flicker) return setupRules.apply(transitionRule, Description.EMPTY) } diff --git a/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group2.kt b/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group2.kt index 79f629811..5ac3a126a 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group2.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group2.kt @@ -18,7 +18,6 @@ package com.android.server.wm.flicker.annotation /** * The group annotations enable to run tests in parallel according to the arguments of test runner. - * By default, the test without group annotation are considered to be in this group. */ @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) diff --git a/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group3.kt b/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group3.kt index e67fe0762..98c4676f3 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group3.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group3.kt @@ -18,7 +18,6 @@ package com.android.server.wm.flicker.annotation /** * The group annotations enable to run tests in parallel according to the arguments of test runner. - * By default, the test without group annotation are considered to be in this group. */ @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) diff --git a/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group4.kt b/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group4.kt new file mode 100644 index 000000000..c1cdcfc4b --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/annotation/Group4.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.annotation + +/** + * The group annotations enable to run tests in parallel according to the arguments of test runner. + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +annotation class Group4
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionData.kt b/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionData.kt index c3847601f..cbb4f1117 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionData.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionData.kt @@ -16,13 +16,14 @@ package com.android.server.wm.flicker.assertions +import androidx.annotation.VisibleForTesting import com.android.server.wm.flicker.FlickerRunResult import kotlin.reflect.KClass /** * Class containing basic data about a trace assertion for Flicker DSL */ -data class AssertionData internal constructor( +data class AssertionData @VisibleForTesting constructor( /** * Segment of the trace where the assertion will be applied (e.g., start, end). */ diff --git a/libraries/flicker/src/com/android/server/wm/flicker/assertions/Assertions.kt b/libraries/flicker/src/com/android/server/wm/flicker/assertions/Assertions.kt index 26e4404fb..5f1471a9c 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/assertions/Assertions.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/assertions/Assertions.kt @@ -26,39 +26,61 @@ typealias Assertion<T> = (T) -> Unit /** * Utility class to store assertions with an identifier to help generate more useful debug data * when dealing with multiple assertions. + * + * @param assertion Assertion to execute + * @param name Assertion name + * @param isOptional If the assertion is optional (can fail) or not (must pass) */ open class NamedAssertion<T> ( private val assertion: Assertion<T>, - open val name: String + open val name: String, + open val isOptional: Boolean = false ) : Assertion<T> { override fun invoke(target: T): Unit = assertion.invoke(target) - override fun toString(): String = "Assertion($name)" + override fun toString(): String = "Assertion($name)${if (isOptional) "[optional]" else ""}" } /** * Utility class to store assertions composed of multiple individual assertions */ -class CompoundAssertion<T>(assertion: Assertion<T>, name: String) : +class CompoundAssertion<T>(assertion: Assertion<T>, name: String, optional: Boolean) : NamedAssertion<T>(assertion, name) { private val assertions = mutableListOf<NamedAssertion<T>>() init { - add(assertion, name) + add(assertion, name, optional) } + override val isOptional: Boolean + get() = assertions.all { it.isOptional } + override val name: String get() = assertions.joinToString(" and ") { it.name } /** * Executes all [assertions] on [target] + * + * In case of failure, returns the first non-optional failure (if available) + * or the first failed assertion */ override fun invoke(target: T) { - val failure = assertions.mapNotNull { - kotlin.runCatching { it.invoke(target) }.exceptionOrNull() - }.firstOrNull() - if (failure != null) { - throw failure + val failures = assertions + .mapNotNull { assertion -> + val error = kotlin.runCatching { assertion.invoke(target) }.exceptionOrNull() + if (error != null) { + Pair(assertion, error) + } else { + null + } + } + val nonOptionalFailure = failures.firstOrNull { !it.first.isOptional } + if (nonOptionalFailure != null) { + throw nonOptionalFailure.second + } + val firstFailure = failures.firstOrNull() + if (firstFailure != null) { + throw firstFailure.second } } @@ -67,7 +89,7 @@ class CompoundAssertion<T>(assertion: Assertion<T>, name: String) : /** * Adds a new assertion to the list */ - fun add(assertion: Assertion<T>, name: String) { - assertions.add(NamedAssertion(assertion, name)) + fun add(assertion: Assertion<T>, name: String, optional: Boolean) { + assertions.add(NamedAssertion(assertion, name, optional)) } } diff --git a/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionsChecker.kt b/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionsChecker.kt index d3fb57945..a714edcfe 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionsChecker.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/assertions/AssertionsChecker.kt @@ -16,6 +16,8 @@ package com.android.server.wm.flicker.assertions +import android.util.Log +import com.android.server.wm.flicker.FLICKER_TAG import com.google.common.truth.Fact import kotlin.math.max @@ -31,15 +33,15 @@ class AssertionsChecker<T : FlickerSubject> { private val assertions = mutableListOf<CompoundAssertion<T>>() private var skipUntilFirstAssertion = false - fun add(name: String, assertion: Assertion<T>) { - assertions.add(CompoundAssertion(assertion, name)) + fun add(name: String, isOptional: Boolean = false, assertion: Assertion<T>) { + assertions.add(CompoundAssertion(assertion, name, isOptional)) } /** * Append [assertion] to the last existing set of assertions. */ - fun append(name: String, assertion: Assertion<T>) { - assertions.last().add(assertion, name) + fun append(name: String, isOptional: Boolean = false, assertion: Assertion<T>) { + assertions.last().add(assertion, name, isOptional) } /** @@ -73,19 +75,31 @@ class AssertionsChecker<T : FlickerSubject> { var entryIndex = 0 var assertionIndex = 0 var lastPassedAssertionIndex = -1 + val assertionTrace = mutableListOf<String>() while (assertionIndex < assertions.size && entryIndex < entries.size) { val currentAssertion = assertions[assertionIndex] val currEntry = entries[entryIndex] try { + val log = "${assertionIndex + 1}/${assertions.size}:[${currentAssertion.name}]\t" + + "Entry: ${entryIndex + 1}/${entries.size} $currEntry" + Log.v(FLICKER_TAG, "Checking Assertion: $log") + assertionTrace.add(log) currentAssertion.invoke(currEntry) lastPassedAssertionIndex = assertionIndex entryIndex++ } catch (e: Throwable) { + // ignore errors are the start of the trace val ignoreFailure = skipUntilFirstAssertion && lastPassedAssertionIndex == -1 if (ignoreFailure) { entryIndex++ continue } + // failure is an optional assertion, just consider it passed skip it + if (currentAssertion.isOptional) { + lastPassedAssertionIndex = assertionIndex + assertionIndex++ + continue + } if (lastPassedAssertionIndex != assertionIndex) { val prevEntry = entries[max(entryIndex - 1, 0)] prevEntry.fail(e) @@ -97,17 +111,23 @@ class AssertionsChecker<T : FlickerSubject> { } } } + // Didn't pass any assertions if (lastPassedAssertionIndex == -1 && assertions.isNotEmpty() && failures.isEmpty()) { entries.first().fail("Assertion never passed", assertions.first()) } - if (failures.isEmpty() && assertionIndex != assertions.lastIndex) { - val reason = listOf( - Fact.fact("Assertion never became false", assertions[assertionIndex]), - Fact.fact("Passed assertions", assertions.take(assertionIndex).joinToString(",")), - Fact.fact("Untested assertions", - assertions.drop(assertionIndex + 1).joinToString(",")) - ) + val untestedAssertions = assertions.drop(assertionIndex + 1) + if (failures.isEmpty() && untestedAssertions.any { !it.isOptional }) { + val passedAssertionsFacts = assertions.take(assertionIndex) + .map { Fact.fact("Passed", it) } + val untestedAssertionsFacts = untestedAssertions + .map { Fact.fact("Untested", it) } + val trace = assertionTrace.map { Fact.fact("Trace", it) } + val reason = mutableListOf<Fact>() + reason.addAll(passedAssertionsFacts) + reason.add(Fact.fact("Assertion never failed", assertions[assertionIndex])) + reason.addAll(untestedAssertionsFacts) + reason.addAll(trace) entries.first().fail(reason) } } diff --git a/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionError.kt b/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionError.kt index 9061a6ae8..7947a6fba 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionError.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionError.kt @@ -16,47 +16,11 @@ package com.android.server.wm.flicker.assertions -import com.android.server.wm.flicker.FlickerRunResult -import com.android.server.wm.flicker.traces.FlickerSubjectException import java.nio.file.Path import kotlin.AssertionError class FlickerAssertionError( - cause: Throwable, - @JvmField val assertion: AssertionData, - @JvmField val iteration: Int, - @JvmField val assertionTag: String, - @JvmField val traceFiles: List<Path> -) : AssertionError(cause) { - constructor(cause: Throwable, assertion: AssertionData, run: FlickerRunResult) - : this(cause, assertion, run.iteration, run.assertionTag, run.traceFiles) - - override val message: String - get() = buildString { - append("\n") - append("Test failed") - append("\n") - append("Iteration: ") - append(iteration) - append("\n") - append("Tag: ") - append(assertionTag) - append("\n") - append("Files: ") - append("\n") - traceFiles.forEach { - append("\t") - append(it) - append("\n") - } - // For subject exceptions, add the facts (layer/window/entry/etc) - // and the original cause of failure - if (cause is FlickerSubjectException) { - append(cause.facts) - append("\n") - cause.cause?.message?.let { append(it) } - } else { - cause?.message?.let { append(it) } - } - } -}
\ No newline at end of file + message: String, + cause: Throwable?, + val traceFiles: List<Path> +) : AssertionError(message, cause) diff --git a/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionErrorBuilder.kt b/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionErrorBuilder.kt new file mode 100644 index 000000000..74f32da84 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionErrorBuilder.kt @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.assertions + +import com.android.server.wm.flicker.dsl.AssertionTag +import com.android.server.wm.flicker.traces.FlickerSubjectException +import com.google.common.truth.Fact +import java.io.ByteArrayOutputStream +import java.io.PrintStream +import java.nio.file.Path + +class FlickerAssertionErrorBuilder { + private var error: Throwable? = null + private var traceFiles: List<Path> = emptyList() + private var tag = "" + + fun fromError(error: Throwable): FlickerAssertionErrorBuilder = apply { + this.error = error + } + + fun withTrace(traceFiles: List<Path>): FlickerAssertionErrorBuilder = apply { + this.traceFiles = traceFiles + } + + fun atTag(tag: String): FlickerAssertionErrorBuilder = apply { + this.tag = when (tag) { + AssertionTag.START -> "before transition (initial state)" + AssertionTag.END -> "after transition (final state)" + AssertionTag.ALL -> "during transition" + else -> "at user-defined location ($tag)" + } + } + + fun build(): FlickerAssertionError { + return FlickerAssertionError(errorMessage, rootCause, traceFiles) + } + + private val errorMessage get() = buildString { + val error = error + requireNotNull(error) + if (error is FlickerSubjectException) { + appendln(error.errorType) + appendln() + append(error.errorDescription) + appendln() + append(error.subjectInformation) + append("\t").appendln(Fact.fact("Location", tag)) + appendln() + } else { + appendln(error.message) + } + appendln("Trace files:") + append(traceFileMessage) + appendln() + appendln("Cause:") + append(rootCauseStackTrace) + appendln() + appendln("Full stacktrace:") + appendln() + } + + private val traceFileMessage get() = buildString { + traceFiles.forEach { + append("\t") + appendln(it) + } + } + + private val rootCauseStackTrace: String get() { + val rootCause = rootCause + return if (rootCause != null) { + val baos = ByteArrayOutputStream() + PrintStream(baos, true) + .use { ps -> rootCause.printStackTrace(ps) } + "\t$baos" + } else { + "" + } + } + + /** + * In some paths the exceptions are encapsulated by the Truth subjects + * To make sure the correct error is printed, located the first non-subject + * related exception and use that as cause. + */ + private val rootCause: Throwable? get() { + var childCause: Throwable? = this.error?.cause + if (childCause != null && childCause is FlickerSubjectException) { + childCause = childCause.cause + } + return childCause + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerSubject.kt index d2f956fb2..c54bc9d70 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerSubject.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerSubject.kt @@ -16,6 +16,7 @@ package com.android.server.wm.flicker.assertions +import androidx.annotation.VisibleForTesting import com.android.server.wm.flicker.traces.FlickerSubjectException import com.google.common.truth.Fact import com.google.common.truth.FailureMetadata @@ -29,8 +30,20 @@ abstract class FlickerSubject( protected val fm: FailureMetadata, data: Any? ) : Subject(fm, data) { - abstract val defaultFacts: String abstract fun clone(): FlickerSubject + @VisibleForTesting + abstract val timestamp: Long + protected abstract val parent: FlickerSubject? + + protected abstract val selfFacts: List<Fact> + val completeFacts: List<Fact> get() { + val facts = selfFacts.toMutableList() + parent?.run { + val ancestorFacts = this.completeFacts + facts.addAll(ancestorFacts) + } + return facts + } /** * Fails an assertion on a subject @@ -95,4 +108,10 @@ abstract class FlickerSubject( * Necessary because check is protected and final in the Truth library */ fun verify(message: String): StandardSubjectBuilder = check(message) -}
\ No newline at end of file + + companion object { + @VisibleForTesting + @JvmStatic + val ASSERTION_TAG = "Assertion" + } +} diff --git a/libraries/flicker/src/com/android/server/wm/flicker/helpers/AutomationUtils.kt b/libraries/flicker/src/com/android/server/wm/flicker/helpers/AutomationUtils.kt index fe7058d51..9113b2d19 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/helpers/AutomationUtils.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/helpers/AutomationUtils.kt @@ -24,6 +24,7 @@ import android.os.RemoteException import android.os.SystemClock import android.util.Log import android.util.Rational +import android.view.Display import android.view.Surface import android.view.View import android.view.ViewConfiguration @@ -35,7 +36,8 @@ import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until import com.android.compatibility.common.util.SystemUtil import com.android.server.wm.flicker.helpers.WindowUtils.displayBounds -import com.android.server.wm.flicker.helpers.WindowUtils.getNavigationBarPosition +import com.android.server.wm.flicker.helpers.WindowUtils.estimateNavigationBarPosition +import com.android.server.wm.traces.common.WindowManagerConditionsFactory import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper import org.junit.Assert import org.junit.Assert.assertNotNull @@ -112,7 +114,7 @@ fun UiDevice.openQuickstep( navBar.visibleBounds } else { Log.e(TAG, "Could not find nav bar, infer location") - getNavigationBarPosition(Surface.ROTATION_0).bounds + estimateNavigationBarPosition(Surface.ROTATION_0).bounds } val startX = navBarVisibleBounds.centerX() @@ -151,8 +153,11 @@ fun UiDevice.openQuickstep( recents = this.wait(Until.findObject(recentsSysUISelector), FIND_TIMEOUT) } assertNotNull("Recent items didn't appear", recents) - wmHelper.waitForNavBarStatusBarVisible() - wmHelper.waitForAppTransitionIdle() + wmHelper.waitFor( + WindowManagerConditionsFactory.isNavBarVisible(), + WindowManagerConditionsFactory.isStatusBarVisible(), + WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY) + ) } private fun getLauncherOverviewSelector(device: UiDevice): BySelector { @@ -242,7 +247,9 @@ fun UiDevice.launchSplitScreen( // Wait for animation to complete. this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT) - wmHelper.waitForSurfaceAppeared(DOCKED_STACK_DIVIDER) + wmHelper.waitFor( + WindowManagerConditionsFactory.isLayerVisible(DOCKED_STACK_DIVIDER), + WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY)) if (!this.isInSplitScreen()) { Assert.fail("Unable to find Split screen divider") @@ -270,8 +277,10 @@ fun UiDevice.isInSplitScreen(): Boolean { return this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT) != null } -fun UiDevice.waitSplitScreenGone(): Boolean { - return this.wait(Until.gone(splitScreenDividerSelector), FIND_TIMEOUT) != null +fun waitSplitScreenGone(wmHelper: WindowManagerStateHelper): Boolean { + return wmHelper.waitFor( + WindowManagerConditionsFactory.isLayerVisible(DOCKED_STACK_DIVIDER), + WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY)) } private val splitScreenDividerSelector: BySelector @@ -303,7 +312,7 @@ fun UiDevice.exitSplitScreen() { * * @throws AssertionError when unable to find the split screen divider */ -fun UiDevice.exitSplitScreenFromBottom() { +fun UiDevice.exitSplitScreenFromBottom(wmHelper: WindowManagerStateHelper) { // Quickstep enabled val divider = this.wait(Until.findObject(splitScreenDividerSelector), FIND_TIMEOUT) assertNotNull("Unable to find Split screen divider", divider) @@ -315,7 +324,7 @@ fun UiDevice.exitSplitScreenFromBottom() { Point(this.displayWidth / 2, this.displayHeight) } divider.drag(dstPoint, 400) - if (!this.waitSplitScreenGone()) { + if (!waitSplitScreenGone(wmHelper)) { Assert.fail("Split screen divider never disappeared") } } diff --git a/libraries/flicker/src/com/android/server/wm/flicker/helpers/SampleAppHelper.kt b/libraries/flicker/src/com/android/server/wm/flicker/helpers/SampleAppHelper.kt new file mode 100644 index 000000000..940c06367 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/helpers/SampleAppHelper.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.helpers + +import android.app.Instrumentation +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import com.android.server.wm.traces.common.FlickerComponentName + +/** + * Helper to launch the default browser app (compatible with AOSP) + * + * This helper has no other functionality but the app launch. + * + * This helper is used to launch an app after some operations (e.g., navigation mode change), + * so that the device is stable before executing flicker tests + */ +class SampleAppHelper( + instrumentation: Instrumentation, + private val pkgManager: PackageManager = instrumentation.context.packageManager +) : StandardAppHelper( + instrumentation, + "SampleApp", + getBrowserComponent(pkgManager) +) { + override fun getOpenAppIntent(): Intent = + pkgManager.getLaunchIntentForPackage(component.packageName) + ?: error("Unable to find intent for browser") + + companion object { + private fun getBrowserIntent(): Intent { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("http://")) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + return intent + } + + private fun getBrowserComponent(pkgManager: PackageManager): FlickerComponentName { + val intent = getBrowserIntent() + val resolveInfo = pkgManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) + ?: error("Unable to resolve browser activity") + return FlickerComponentName(resolveInfo.activityInfo.packageName, className = "") + } + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/helpers/StandardAppHelper.kt b/libraries/flicker/src/com/android/server/wm/flicker/helpers/StandardAppHelper.kt index 71338f1fb..20ebfdee4 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/helpers/StandardAppHelper.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/helpers/StandardAppHelper.kt @@ -28,9 +28,8 @@ import androidx.test.uiautomator.By import androidx.test.uiautomator.BySelector import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until +import com.android.server.wm.traces.common.FlickerComponentName import com.android.server.wm.traces.common.windowmanager.WindowManagerState -import com.android.server.wm.traces.parser.toActivityName -import com.android.server.wm.traces.parser.toWindowName import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper /** @@ -40,7 +39,7 @@ import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelpe open class StandardAppHelper @JvmOverloads constructor( instr: Instrumentation, @JvmField val appName: String, - @JvmField val component: ComponentName, + @JvmField val component: FlickerComponentName, protected val launcherStrategy: ILauncherStrategy = LauncherStrategyFactory.getInstance(instr).launcherStrategy ) : AbstractStandardAppHelper(instr) { @@ -52,10 +51,7 @@ open class StandardAppHelper @JvmOverloads constructor( launcherStrategy: ILauncherStrategy = LauncherStrategyFactory.getInstance(instr).launcherStrategy ): this(instr, appName, - ComponentName.createRelative(packageName, ".$activity"), launcherStrategy) - - val windowName: String = component.toWindowName() - val activityName: String = component.toActivityName() + FlickerComponentName(packageName, ".$activity"), launcherStrategy) private val activityManager: ActivityManager? get() = mInstrumentation.context.getSystemService(ActivityManager::class.java) @@ -88,7 +84,7 @@ open class StandardAppHelper @JvmOverloads constructor( val intent = Intent() intent.addCategory(Intent.CATEGORY_LAUNCHER) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - intent.component = component + intent.component = ComponentName(component.packageName, component.className) return intent } @@ -109,7 +105,6 @@ open class StandardAppHelper @JvmOverloads constructor( /** * Exits the activity and wait for activity destroyed */ - @JvmOverloads fun exit( wmHelper: WindowManagerStateHelper ) { @@ -177,10 +172,11 @@ open class StandardAppHelper @JvmOverloads constructor( val window = if (expectedWindowName.isNotEmpty()) { expectedWindowName } else { - windowName + component.toWindowName() } wmHelper.waitFor("App is shown") { - it.wmState.isComplete() && it.wmState.isWindowVisible(window) + it.wmState.isComplete() && it.wmState.isWindowVisible(window) && + !it.layerState.isAnimating() } wmHelper.waitForAppTransitionIdle() diff --git a/libraries/flicker/src/com/android/server/wm/flicker/helpers/WindowUtils.kt b/libraries/flicker/src/com/android/server/wm/flicker/helpers/WindowUtils.kt index 2d1e4439a..f40ca50be 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/helpers/WindowUtils.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/helpers/WindowUtils.kt @@ -23,8 +23,7 @@ import android.graphics.Region import android.view.Surface import android.view.WindowManager import androidx.test.platform.app.InstrumentationRegistry -import kotlin.math.max -import kotlin.math.min +import com.android.server.wm.traces.common.layers.Display fun Int.isRotated() = this == Surface.ROTATION_90 || this == Surface.ROTATION_270 @@ -77,32 +76,54 @@ object WindowUtils { } /** - * Gets the expected status bar position at a specific rotation + * Gets the expected status bar position for a specific display * - * @param requestedRotation Device rotation + * @param display the main display */ - fun getStatusBarPosition(requestedRotation: Int): Region { - val displayBounds = displayBounds - val resourceName: String - val width: Int - if (!requestedRotation.isRotated()) { - resourceName = "status_bar_height_portrait" - width = min(displayBounds.width(), displayBounds.height()) + fun getStatusBarPosition(display: Display): Region { + val resourceName = if (!display.transform.getRotation().isRotated()) { + "status_bar_height_portrait" } else { - resourceName = "status_bar_height_landscape" - width = max(displayBounds.width(), displayBounds.height()) + "status_bar_height_landscape" } val resourceId = resources.getIdentifier(resourceName, "dimen", "android") val height = resources.getDimensionPixelSize(resourceId) - return Region(0, 0, width, height) + return Region(0, 0, display.layerStackSpace.width, height) + } + + /** + * Gets the expected navigation bar position for a specific display + * + * @param display the main display + */ + fun getNavigationBarPosition(display: Display): Region { + val navBarWidth = getDimensionPixelSize("navigation_bar_width") + val navBarHeight = navigationBarHeight + val displayHeight = display.layerStackSpace.height + val displayWidth = display.layerStackSpace.width + val requestedRotation = display.transform.getRotation() + + return when { + // nav bar is at the bottom of the screen + requestedRotation in listOf(Surface.ROTATION_0, Surface.ROTATION_180) || + isGesturalNavigationEnabled -> + Region(0, displayHeight - navBarHeight, displayWidth, displayHeight) + // nav bar is at the right side + requestedRotation == Surface.ROTATION_90 -> + Region(displayWidth - navBarWidth, 0, displayWidth, displayHeight) + // nav bar is at the left side + requestedRotation == Surface.ROTATION_270 -> + Region(0, 0, navBarWidth, displayHeight) + else -> error("Unknown rotation $requestedRotation") + } } /** - * Gets the expected navigation bar position at a specific rotation + * Estimate the navigation bar position at a specific rotation * * @param requestedRotation Device rotation */ - fun getNavigationBarPosition(requestedRotation: Int): Region { + fun estimateNavigationBarPosition(requestedRotation: Int): Region { val displayBounds = displayBounds val displayWidth: Int val displayHeight: Int @@ -168,4 +189,4 @@ object WindowUtils { .getIdentifier("docked_stack_divider_insets", "dimen", "android") return resources.getDimensionPixelSize(resourceId) } -}
\ No newline at end of file +} diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/Extensions.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/Extensions.kt index 6a87b51c1..88075e155 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/Extensions.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/Extensions.kt @@ -17,10 +17,11 @@ @file:JvmName("Extensions") package com.android.server.wm.flicker.monitor -import com.android.server.wm.traces.parser.DeviceStateDump import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace import com.android.server.wm.flicker.getDefaultFlickerOutputDir +import com.android.server.wm.traces.common.DeviceTraceDump import com.android.server.wm.traces.common.layers.LayersTrace +import com.android.server.wm.traces.parser.DeviceDumpParser import com.android.server.wm.traces.parser.layers.LayersTraceParser import com.android.server.wm.traces.parser.windowmanager.WindowManagerTraceParser import java.nio.file.Path @@ -73,11 +74,11 @@ fun withSFTracing( fun withTracing( outputDir: Path = getDefaultFlickerOutputDir(), predicate: () -> Unit -): DeviceStateDump { +): DeviceTraceDump { val traces = recordTraces(outputDir, predicate) val wmTraceData = traces.first val layersTraceData = traces.second - return DeviceStateDump.fromTrace(wmTraceData, layersTraceData) + return DeviceDumpParser.fromTrace(wmTraceData, layersTraceData) } /** diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/ITransitionMonitor.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/ITransitionMonitor.kt index 8e5a71689..45ffcda26 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/ITransitionMonitor.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/ITransitionMonitor.kt @@ -39,12 +39,6 @@ interface ITransitionMonitor { save("${testTag}_$iteration", flickerRunResultBuilder) /** - * Saves any monitor artifacts to file adding `testTag` to the file name. - * - * @param testTag suffix added to artifact name - * @return Path to saved artifact - */ - /** * Saves trace file to the external storage directory suffixing the name with the testtag and * iteration. * @@ -58,7 +52,4 @@ interface ITransitionMonitor { fun save(testTag: String, flickerRunResultBuilder: FlickerRunResult.Builder) { throw UnsupportedOperationException("Save not implemented for this monitor") } - - val checksum: String - get() = throw UnsupportedOperationException("Checksum not implemented for this monitor") } diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.kt index 74fa38852..b9f16cdbc 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/LayersTraceMonitor.kt @@ -31,7 +31,7 @@ import java.nio.file.Path open class LayersTraceMonitor( outputDir: Path, private val traceFlags: Int -) : TransitionMonitor(outputDir, "layers_trace.pb") { +) : TransitionMonitor(outputDir, "layers_trace$WINSCOPE_EXT") { constructor(outputDir: Path) : this(outputDir, TRACE_FLAGS) diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/TraceMonitor.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/TraceMonitor.kt index 92409958a..d9418332f 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/TraceMonitor.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/TraceMonitor.kt @@ -19,14 +19,8 @@ package com.android.server.wm.flicker.monitor import androidx.annotation.VisibleForTesting import com.android.compatibility.common.util.SystemUtil import com.android.server.wm.flicker.FlickerRunResult -import com.google.common.io.BaseEncoding -import java.io.FileInputStream -import java.io.IOException -import java.nio.ByteBuffer import java.nio.file.Files import java.nio.file.Path -import java.security.MessageDigest -import java.security.NoSuchAlgorithmException /** * Base class for monitors containing common logic to read the trace as a byte array and save the @@ -36,9 +30,6 @@ abstract class TraceMonitor internal constructor( @VisibleForTesting var outputPath: Path, protected var sourceTraceFilePath: Path ) : ITransitionMonitor { - override var checksum: String = "" - protected set - abstract val isEnabled: Boolean abstract fun setResult(flickerRunResultBuilder: FlickerRunResult.Builder, traceFile: Path) @@ -50,8 +41,6 @@ abstract class TraceMonitor internal constructor( require(Files.exists(savedTrace)) { "Unable to save trace file $savedTrace" } setResult(flickerRunResultBuilder, savedTrace) - - checksum = calculateChecksum(savedTrace) } fun save(testTag: String) { @@ -59,8 +48,6 @@ abstract class TraceMonitor internal constructor( val savedTrace = outputPath.resolve("${testTag}_${sourceTraceFilePath.fileName}") moveFile(sourceTraceFilePath, savedTrace) require(Files.exists(savedTrace)) { "Unable to save trace file $savedTrace" } - - checksum = calculateChecksum(savedTrace) } private fun moveFile(src: Path, dst: Path) { @@ -74,28 +61,4 @@ abstract class TraceMonitor internal constructor( SystemUtil.runShellCommand("chmod a+r $dst") SystemUtil.runShellCommand("rm $src") } - - companion object { - @VisibleForTesting - @JvmStatic - fun calculateChecksum(traceFile: Path): String { - return try { - val messageDigest = MessageDigest.getInstance("SHA-256") - val inputStream = FileInputStream(traceFile.toFile()) - val channel = inputStream.channel - val buffer = ByteBuffer.allocate(2048) - while (channel.read(buffer) != -1) { - buffer.flip() - messageDigest.update(buffer) - buffer.clear() - } - val hash = messageDigest.digest() - BaseEncoding.base16().encode(hash).toLowerCase() - } catch (e: NoSuchAlgorithmException) { - throw IllegalArgumentException("Checksum algorithm SHA-256 not found", e) - } catch (e: IOException) { - throw IllegalArgumentException("File not found", e) - } - } - } } diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/TransitionMonitor.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/TransitionMonitor.kt index 3424aa241..3df800747 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/TransitionMonitor.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/TransitionMonitor.kt @@ -61,5 +61,6 @@ abstract class TransitionMonitor( companion object { private val TRACE_DIR = Paths.get("/data/misc/wmtrace/") + internal const val WINSCOPE_EXT = ".winscope" } } diff --git a/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.kt b/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.kt index 0e46c81ce..11858cbd2 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitor.kt @@ -30,7 +30,7 @@ import java.nio.file.Path */ open class WindowManagerTraceMonitor( outputDir: Path -) : TransitionMonitor(outputDir, "wm_trace.pb") { +) : TransitionMonitor(outputDir, "wm_trace$WINSCOPE_EXT") { private val windowManager = WindowManagerGlobal.getWindowManagerService() override fun start() { try { diff --git a/libraries/flicker/src/com/android/server/wm/flicker/rules/LaunchAppRule.kt b/libraries/flicker/src/com/android/server/wm/flicker/rules/LaunchAppRule.kt new file mode 100644 index 000000000..d425775a2 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/rules/LaunchAppRule.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.rules + +import android.app.Instrumentation +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.helpers.StandardAppHelper +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +/** + * Launched an app before the test + * + * @param instrumentation Instrumentation mechanism to use + * @param wmHelper WM/SF synchronization helper + * @param appHelper App to launch + */ +class LaunchAppRule @JvmOverloads constructor( + private val appHelper: StandardAppHelper, + private val instrumentation: Instrumentation = appHelper.mInstrumentation, + private val wmHelper: WindowManagerStateHelper = WindowManagerStateHelper() +) : TestWatcher() { + @JvmOverloads + constructor( + component: FlickerComponentName, + appName: String = "", + instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(), + wmHelper: WindowManagerStateHelper = WindowManagerStateHelper() + ): this(StandardAppHelper(instrumentation, appName, component), instrumentation, wmHelper) + + override fun starting(description: Description?) { + appHelper.launchViaIntent() + appHelper.exit(wmHelper) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/rules/WMFlickerServiceRule.kt b/libraries/flicker/src/com/android/server/wm/flicker/rules/WMFlickerServiceRule.kt new file mode 100644 index 000000000..165fa8674 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/rules/WMFlickerServiceRule.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.rules + +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.getDefaultFlickerOutputDir +import com.android.server.wm.flicker.monitor.LayersTraceMonitor +import com.android.server.wm.flicker.monitor.ScreenRecorder +import com.android.server.wm.flicker.monitor.TraceMonitor +import com.android.server.wm.flicker.monitor.WindowManagerTraceMonitor +import com.android.server.wm.flicker.service.FlickerService +import com.android.server.wm.flicker.service.FlickerService.Companion.getFassFilePath +import com.android.server.wm.traces.common.layers.LayersTrace +import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace +import com.android.server.wm.traces.parser.layers.LayersTraceParser +import com.android.server.wm.traces.parser.windowmanager.WindowManagerTraceParser +import org.junit.rules.TestWatcher +import org.junit.runner.Description +import java.nio.file.Files +import java.nio.file.Path + +/** + * Collect the WM and SF traces, parse them and call the WM Flicker Service after the test + */ +open class WMFlickerServiceRule @JvmOverloads constructor( + private val outputDir: Path = getDefaultFlickerOutputDir() +) : TestWatcher() { + private val traceMonitors = mutableListOf<TraceMonitor>() + + protected var wmTrace: WindowManagerTrace = WindowManagerTrace(emptyArray(), source = "") + protected var layersTrace: LayersTrace = LayersTrace(emptyArray(), source = "") + + override fun starting(description: Description?) { + setupMonitors() + cleanupTraceFiles() + traceMonitors.forEach { + it.start() + } + } + + override fun finished(description: Description?) { + val testTag = description?.methodName ?: "fass" + traceMonitors.forEach { + it.stop() + it.save(testTag) + } + + Files.createDirectories(outputDir) + wmTrace = getWindowManagerTrace(getFassFilePath(outputDir, testTag, "wm_trace")) + layersTrace = getLayersTrace(getFassFilePath(outputDir, testTag, "layers_trace")) + + val flickerService = FlickerService() + flickerService.process(wmTrace, layersTrace, outputDir, testTag) + } + + private fun setupMonitors() { + traceMonitors.add(WindowManagerTraceMonitor(outputDir)) + traceMonitors.add(LayersTraceMonitor(outputDir)) + traceMonitors.add(ScreenRecorder( + outputDir, + InstrumentationRegistry.getInstrumentation().targetContext) + ) + } + + /** + * Remove the WM trace and layers trace files collected from previous test runs. + */ + private fun cleanupTraceFiles() { + Files.list(outputDir).forEach { file -> + if (!Files.isDirectory(file)) { + Files.delete(file) + } + } + } + + /** + * Parse the window manager trace file. + * + * @param traceFilePath + * @return parsed window manager trace. + */ + private fun getWindowManagerTrace(traceFilePath: Path): WindowManagerTrace { + val wmTraceByteArray: ByteArray = Files.readAllBytes(traceFilePath) + return WindowManagerTraceParser.parseFromTrace(wmTraceByteArray) + } + + /** + * Parse the layers trace file. + * + * @param traceFilePath + * @return parsed layers trace. + */ + private fun getLayersTrace(traceFilePath: Path): LayersTrace { + val layersTraceByteArray: ByteArray = Files.readAllBytes(traceFilePath) + return LayersTraceParser.parseFromTrace(layersTraceByteArray) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/rules/WMFlickerServiceRuleForTestSpec.kt b/libraries/flicker/src/com/android/server/wm/flicker/rules/WMFlickerServiceRuleForTestSpec.kt new file mode 100644 index 000000000..5c3606d94 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/rules/WMFlickerServiceRuleForTestSpec.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.rules + +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.dsl.AssertionTag +import com.android.server.wm.flicker.service.FlickerService +import com.android.server.wm.flicker.service.assertors.AssertionConfigParser +import com.android.server.wm.flicker.service.assertors.AssertionData +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.errors.ErrorTrace +import org.junit.rules.TestWatcher + +/** + * A test rule reusing flicker data from [FlickerTestParameter], and fetching the traces + * to call the WM Flicker Service after the test + */ +@Deprecated("This test rule should be only used with legacy flicker tests. " + + "For new tests use WMFlickerServiceRule instead") +class WMFlickerServiceRuleForTestSpec( + private val testSpec: FlickerTestParameter +) : TestWatcher() { + private fun checkFlicker(category: String): List<ErrorTrace> { + // run flicker if it was not executed before + testSpec.flicker.result ?: testSpec.assertWm { isNotEmpty() } + + val errors = mutableListOf<ErrorTrace>() + val result = testSpec.flicker.result ?: error("No flicker results for ${testSpec.flicker}") + val assertions = AssertionData.readConfiguration().filter { it.category == category } + val flickerService = FlickerService(assertions) + + result.runs + .filter { it.assertionTag == AssertionTag.ALL } + .filter { + val hasWmTrace = it.wmSubject?.let { true } ?: false + val hasLayersTrace = it.layersSubject?.let { true } ?: false + hasWmTrace || hasLayersTrace + } + .forEach { run -> + val wmSubject = run.wmSubject as WindowManagerTraceSubject + val layersSubject = run.layersSubject as LayersTraceSubject + + val outputDir = run.traceFiles + .firstOrNull() + ?.parent + ?: error("Output dir not detected") + + val wmTrace = wmSubject.trace + val layersTrace = layersSubject.trace + errors.add(flickerService.process(wmTrace, layersTrace, outputDir, category)) + } + + return errors + } + + fun checkPresubmitAssertions() { + val errors = checkFlicker(AssertionConfigParser.PRESUBMIT_KEY) + failIfAnyError(errors) + } + + fun checkPostsubmitAssertions() { + val errors = checkFlicker(AssertionConfigParser.POSTSUBMIT_KEY) + failIfAnyError(errors) + } + + fun checkFlakyAssertions() { + val errors = checkFlicker(AssertionConfigParser.FLAKY_KEY) + failIfAnyError(errors) + } + + private fun failIfAnyError(errors: List<ErrorTrace>) { + val errorMsg = errors.joinToString("\n") { runs -> + runs.entries.joinToString { state -> + state.errors.joinToString { "${it.assertionName}\n${it.message}" } + } + } + if (errorMsg.isNotEmpty()) { + error(errorMsg) + } + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/AssertionEngine.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/AssertionEngine.kt new file mode 100644 index 000000000..c18144ed1 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/AssertionEngine.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service + +import com.android.server.wm.flicker.service.assertors.AssertionData +import com.android.server.wm.flicker.service.assertors.TransitionAssertor +import com.android.server.wm.traces.common.errors.ErrorState +import com.android.server.wm.traces.common.errors.ErrorTrace +import com.android.server.wm.traces.common.layers.LayersTrace +import com.android.server.wm.traces.common.tags.Tag +import com.android.server.wm.traces.common.tags.TagTrace +import com.android.server.wm.traces.common.tags.Transition +import com.android.server.wm.traces.common.tags.TransitionTag +import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace + +/** + * Invokes the configured assertors and summarizes the results. + */ +class AssertionEngine( + private val assertions: List<AssertionData>, + private val logger: (String) -> Unit +) { + private val knownTypes = assertions.map { it.transitionType } + + fun analyze( + wmTrace: WindowManagerTrace, + layersTrace: LayersTrace, + tagTrace: TagTrace + ): ErrorTrace { + val errors = mutableListOf<ErrorState>() + val allTransitions = getTransitionTags(tagTrace) + + allTransitions + .filter { knownTypes.contains(it.tag.transition) } + .forEach { transition -> + val (filteredWmTrace, filteredLayersTrace) = + splitTraces(transition, wmTrace, layersTrace) + + val assertionsOfType = assertions + .filter { it.transitionType == transition.tag.transition } + val assertor = TransitionAssertor(assertionsOfType, logger) + val errorTrace = assertor.analyze( + transition.tag, filteredWmTrace, filteredLayersTrace) + errors.addAll(errorTrace) + } + + /* Ensure all error states with same timestamp are merged */ + val errorStates = errors.distinct() + .groupBy({ it.timestamp }, { it.errors.asList() }) + .mapValues { (key, value) -> + ErrorState(value.flatten().toTypedArray(), key.toString()) } + .values.toTypedArray() + + return ErrorTrace(errorStates, source = "") + } + + /** + * Extracts all [TransitionTag]s from a [TagTrace]. + * + * @param tagTrace Tag Trace + * @return a list with [TransitionTag] + */ + fun getTransitionTags(tagTrace: TagTrace): List<TransitionTag> { + return tagTrace.entries.flatMap { state -> + state.tags.filter { tag -> tag.isStartTag } + .map { + TransitionTag( + tag = it, + startTimestamp = state.timestamp, + endTimestamp = getEndTagTimestamp(tagTrace, it) + ) + } + } + } + + private fun getEndTagTimestamp(tagTrace: TagTrace, tag: Tag): Long { + val finalTag = tag.copy(isStartTag = false) + return tagTrace.entries.firstOrNull { state -> state.tags.contains(finalTag) }?.timestamp + ?: throw RuntimeException("All open tags should be closed!") + } + + /** + * Splits a [WindowManagerTrace] and a [LayersTrace] by a [Transition]. + * + * @param tag a list with all [TransitionTag]s + * @param wmTrace Window Manager trace + * @param layersTrace Surface Flinger trace + * @return a list with [WindowManagerTrace] blocks + */ + fun splitTraces( + tag: TransitionTag, + wmTrace: WindowManagerTrace, + layersTrace: LayersTrace + ): Pair<WindowManagerTrace, LayersTrace> { + val filteredWmTrace = wmTrace.filter(tag.startTimestamp, tag.endTimestamp) + val filteredLayersTrace = layersTrace.filter(tag.startTimestamp, tag.endTimestamp) + return Pair(filteredWmTrace, filteredLayersTrace) + } +} diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/FlickerService.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/FlickerService.kt new file mode 100644 index 000000000..827b586ed --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/FlickerService.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service + +import android.util.Log +import com.android.server.wm.flicker.FLICKER_TAG +import com.android.server.wm.flicker.monitor.TransitionMonitor.Companion.WINSCOPE_EXT +import com.android.server.wm.flicker.service.assertors.AssertionData +import com.android.server.wm.traces.common.errors.ErrorTrace +import com.android.server.wm.traces.common.layers.LayersTrace +import com.android.server.wm.traces.common.service.TaggingEngine +import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace +import com.android.server.wm.traces.parser.errors.writeToFile +import com.android.server.wm.traces.parser.tags.writeToFile +import java.nio.file.Path + +/** + * Contains the logic for Flicker as a Service. + */ +class FlickerService @JvmOverloads constructor( + private val assertions: List<AssertionData> = AssertionData.readConfiguration() +) { + /** + * The entry point for WM Flicker Service. + * + * Calls the Tagging Engine and the Assertion Engine. + * + * @param wmTrace Window Manager trace + * @param layersTrace Surface Flinger trace + * @return A list containing all failures + */ + fun process( + wmTrace: WindowManagerTrace, + layersTrace: LayersTrace, + outputDir: Path, + testTag: String + ): ErrorTrace { + val taggingEngine = TaggingEngine(wmTrace, layersTrace) { Log.v("$FLICKER_TAG-PROC", it) } + val tagTrace = taggingEngine.run() + val tagTraceFile = getFassFilePath(outputDir, testTag, "tag_trace") + tagTrace.writeToFile(tagTraceFile) + + val assertionEngine = AssertionEngine(assertions) { Log.v("$FLICKER_TAG-ASSERT", it) } + val errorTrace = assertionEngine.analyze(wmTrace, layersTrace, tagTrace) + val errorTraceFile = getFassFilePath(outputDir, testTag, "error_trace") + errorTrace.writeToFile(errorTraceFile) + return errorTrace + } + + companion object { + /** + * Returns the computed path for the Fass files. + * + * @param outputDir the output directory for the trace file + * @param testTag the tag to identify the test + * @param file the name of the trace file + * @return the path to the trace file + */ + internal fun getFassFilePath(outputDir: Path, testTag: String, file: String): Path = + outputDir.resolve("${testTag}_$file$WINSCOPE_EXT") + } +} diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/AssertionConfigParser.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/AssertionConfigParser.kt new file mode 100644 index 000000000..cc2d83930 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/AssertionConfigParser.kt @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors + +import android.util.Log +import com.android.server.wm.flicker.FLICKER_TAG +import com.android.server.wm.traces.common.tags.Transition +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject + +object AssertionConfigParser { + private const val ASSERTORS_KEY = "assertors" + private const val CLASS_KEY = "class" + private const val ARGS_KEY = "args" + private const val TRANSITION_KEY = "transition" + private const val ASSERTIONS_KEY = "assertions" + + internal const val PRESUBMIT_KEY = "presubmit" + internal const val POSTSUBMIT_KEY = "postsubmit" + internal const val FLAKY_KEY = "flaky" + + /** + * Parses assertor config JSON file. The format expected is: + * <pre> + * { + * "assertors": [ + * { + * "transition": "ROTATION", + * "assertions": { + * "presubmit": [ + * "navBarWindowIsVisible" + * "navBarLayerIsVisible", + * "navBarLayerRotatesAndScales" + * ], + * "postsubmit": [ ], + * "flaky": [ + * "entireScreenCovered" + * ] + * } + * } + * ] + * } + * </pre> + * + * @param config string containing a json file + * @return a list of [AssertionData] assertions + */ + @JvmStatic + fun parseConfigFile(config: String): List<AssertionData> { + val assertorsConfig = mutableListOf<AssertionData>() + val jsonArray = JSONObject(config).getJSONArray(ASSERTORS_KEY) + + for (i in 0 until jsonArray.length()) { + val jsonObject = jsonArray.getJSONObject(i) + val jsonAssertions = jsonObject.getJSONObject(ASSERTIONS_KEY) + val transitionType = Transition.valueOf(jsonObject.getString(TRANSITION_KEY)) + val presubmit = parseAssertionArray( + jsonAssertions.getJSONArray(PRESUBMIT_KEY), transitionType, PRESUBMIT_KEY) + val postsubmit = parseAssertionArray( + jsonAssertions.getJSONArray(POSTSUBMIT_KEY), transitionType, POSTSUBMIT_KEY) + val flaky = parseAssertionArray( + jsonAssertions.getJSONArray(FLAKY_KEY), transitionType, FLAKY_KEY) + val assertionsList = presubmit + postsubmit + flaky + + assertorsConfig.addAll(assertionsList) + } + + return assertorsConfig + } + + /** + * Splits an assertions JSONArray into an array of [AssertionData]. + * + * @param assertionsArray a [JSONArray] with assertion names + * @param transitionType type of transition connected to this assertion + * @param category the category of the assertion (presubmit/postsubmit/flaky) + * @return an array of assertion details + */ + @JvmStatic + private fun parseAssertionArray( + assertionsArray: JSONArray, + transitionType: Transition, + category: String + ): List<AssertionData> { + val assertions = mutableListOf<AssertionData>() + try { + for (i in 0 until assertionsArray.length()) { + val assertionObj = assertionsArray.getJSONObject(i) + val assertionClass = assertionObj.getString(CLASS_KEY) + val args = mutableListOf<String>() + if (assertionObj.has(ARGS_KEY)) { + val assertionArgsArray = assertionObj.getJSONArray(ARGS_KEY) + for (j in 0 until assertionArgsArray.length()) { + val arg = assertionArgsArray.getString(j) + args.add(arg) + } + } + Log.v(FLICKER_TAG, "Creating assertion for class $assertionClass") + assertions.add( + AssertionData.fromString(transitionType, category, assertionClass, args)) + } + } catch (e: JSONException) { + throw RuntimeException(e) + } + + return assertions + } +} diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/AssertionData.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/AssertionData.kt new file mode 100644 index 000000000..768b867fe --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/AssertionData.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors + +import com.android.server.wm.flicker.service.FlickerService +import com.android.server.wm.traces.common.tags.Transition +import java.io.FileNotFoundException + +/** + * Stores data for FASS assertions. + */ +data class AssertionData( + val transitionType: Transition, + val assertion: BaseAssertion, + val category: String +) { + companion object { + /** + * Returns the name of the assertors configuration file. + */ + private const val CONFIG_FILE_NAME = "config.json" + + /** + * Creates an assertion data based on it's fully-qualified class path [cls] and set + * its category to [category] + */ + fun fromString( + transitionType: Transition, + category: String, + cls: String, + args: List<String> + ): AssertionData { + val clsDescriptor = Class.forName(cls) + + val assertionObj = if (args.isEmpty()) { + clsDescriptor.newInstance() as BaseAssertion + } else { + val ctor = clsDescriptor.constructors + .firstOrNull { it.parameterCount == args.size } + ?: error("Constructor not found") + ctor.newInstance(*args.toTypedArray()) as BaseAssertion + } + + return AssertionData(transitionType, assertionObj, category) + } + + /** + * Reads the assertions configuration for the configuration file. + * + * @param fileName the location of the configuration file + * @return a list with assertors configuration + * + * @throws FileNotFoundException when there is no config file + */ + @JvmOverloads + fun readConfiguration(fileName: String = CONFIG_FILE_NAME): List<AssertionData> { + val fileContent = FlickerService::class.java.classLoader.getResource(fileName) + ?.readText(Charsets.UTF_8) + ?: throw FileNotFoundException("A configuration file must exist!") + return AssertionConfigParser.parseConfigFile(fileContent) + } + } +} diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/BaseAssertion.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/BaseAssertion.kt new file mode 100644 index 000000000..0815e0357 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/BaseAssertion.kt @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors + +import com.android.server.wm.flicker.assertions.FlickerSubject +import com.android.server.wm.flicker.traces.FlickerSubjectException +import com.android.server.wm.flicker.traces.layers.LayerSubject +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowStateSubject +import com.android.server.wm.traces.common.layers.Layer +import com.android.server.wm.traces.common.tags.Tag +import com.android.server.wm.traces.common.tags.TransitionTag +import com.android.server.wm.traces.common.windowmanager.windows.WindowState + +/** + * Base calss for a FASS assertion + */ +abstract class BaseAssertion { + private var failureSubject: FlickerSubject? = null + + /** + * Assertion name + */ + val name: String = this::class.java.simpleName + + /** + * Run specific assertion evaluation block + * + * @param tag a list with all [TransitionTag]s + * @param wmSubject Window Manager trace subject + * @param layerSubject Surface Flinger trace subject + */ + protected abstract fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) + + /** + * Evaluate the assertion on a transition [Tag] in a [WindowManagerTraceSubject] and + * [LayersTraceSubject] + * + * @param tag a list with all [TransitionTag]s + * @param wmSubject Window Manager trace subject + * @param layerSubject Surface Flinger trace subject + */ + fun evaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + try { + doEvaluate(tag, wmSubject, layerSubject) + } catch (e: FlickerSubjectException) { + failureSubject = e.subject + throw e + } + } + + /** + * Returns the layer responsible for the failure, if any + * + * @param tag a list with all [TransitionTag]s + * @param wmSubject Window Manager trace subject + * @param layerSubject Surface Flinger trace subject + */ + open fun getFailureLayer( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ): Layer? { + val failureSubject = failureSubject + return if (failureSubject is LayerSubject) { + failureSubject.layer + } else { + null + } + } + + /** + * Returns the window responsible for the last failure, if any + * + * @param tag a list with all [TransitionTag]s + * @param wmSubject Window Manager trace subject + * @param layerSubject Surface Flinger trace subject + */ + open fun getFailureWindow( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ): WindowState? { + val failureSubject = failureSubject + return if (failureSubject is WindowStateSubject) { + failureSubject.windowState + } else { + null + } + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/Components.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/Components.kt new file mode 100644 index 000000000..c6d9a76b7 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/Components.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors + +import com.android.server.wm.traces.common.FlickerComponentName + +object Components { + val LAUNCHER = FlickerComponentName("com.google.android.apps.nexuslauncher", + "com.google.android.apps.nexuslauncher.NexusLauncherActivity") +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/TransitionAssertor.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/TransitionAssertor.kt new file mode 100644 index 000000000..9ec5a2908 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/TransitionAssertor.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors + +import android.util.Log +import com.android.server.wm.flicker.FLICKER_TAG +import com.android.server.wm.flicker.traces.FlickerSubjectException +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.errors.Error +import com.android.server.wm.traces.common.errors.ErrorState +import com.android.server.wm.traces.common.errors.ErrorTrace +import com.android.server.wm.traces.common.layers.LayersTrace +import com.android.server.wm.traces.common.service.ITransitionAssertor +import com.android.server.wm.traces.common.tags.Tag +import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace + +/** + * Class that runs FASS assertions. + */ +class TransitionAssertor( + private val assertions: List<AssertionData>, + private val logger: (String) -> Unit +) : ITransitionAssertor { + /** {@inheritDoc} */ + override fun analyze( + tag: Tag, + wmTrace: WindowManagerTrace, + layersTrace: LayersTrace + ): ErrorTrace { + val errorStates = mutableMapOf<Long, MutableList<Error>>() + + errorStates.putAll( + runCategoryAssertions(tag, wmTrace, layersTrace, AssertionConfigParser.PRESUBMIT_KEY)) + errorStates.putAll( + runCategoryAssertions(tag, wmTrace, layersTrace, AssertionConfigParser.POSTSUBMIT_KEY)) + errorStates.putAll( + runCategoryAssertions(tag, wmTrace, layersTrace, AssertionConfigParser.FLAKY_KEY)) + + return buildErrorTrace(errorStates) + } + + private fun runCategoryAssertions( + tag: Tag, + wmTrace: WindowManagerTrace, + layersTrace: LayersTrace, + categoryKey: String + ): Map<Long, MutableList<Error>> { + logger.invoke("Running assertions for $tag $categoryKey") + val wmSubject = WindowManagerTraceSubject.assertThat(wmTrace) + val layersSubject = LayersTraceSubject.assertThat(layersTrace) + val assertions = assertions.filter { it.category == categoryKey } + return runAssertionsOnSubjects(tag, wmSubject, layersSubject, assertions) + } + + private fun runAssertionsOnSubjects( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject, + assertions: List<AssertionData> + ): Map<Long, MutableList<Error>> { + val errors = mutableMapOf<Long, MutableList<Error>>() + + try { + assertions.forEach { + val assertion = it.assertion + logger.invoke("Running assertion $assertion") + val result = assertion.runCatching { evaluate(tag, wmSubject, layerSubject) } + if (result.isFailure) { + val layer = assertion.getFailureLayer(tag, wmSubject, layerSubject) + val window = assertion.getFailureWindow(tag, wmSubject, layerSubject) + val exception = result.exceptionOrNull() as FlickerSubjectException + + errors.putIfAbsent(exception.timestamp, mutableListOf()) + val errorEntry = Error( + stacktrace = exception.stackTraceToString(), + message = exception.message, + layerId = layer?.id ?: 0, + windowToken = window?.token ?: "", + assertionName = assertion.name + ) + errors.getValue(exception.timestamp).add(errorEntry) + } + } + } catch (e: NoSuchMethodException) { + Log.e("$FLICKER_TAG-ASSERT", "Assertion method not found", e) + } catch (e: SecurityException) { + Log.e("$FLICKER_TAG-ASSERT", "Unable to get assertion method", e) + } + + return errors + } + + private fun buildErrorTrace(errors: MutableMap<Long, MutableList<Error>>): ErrorTrace { + val errorStates = errors.map { entry -> + val timestamp = entry.key + val stateTags = entry.value + ErrorState(stateTags.toTypedArray(), timestamp.toString()) + } + return ErrorTrace(errorStates.toTypedArray(), source = "") + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppComponentBaseTest.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppComponentBaseTest.kt new file mode 100644 index 000000000..1fdce0c13 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppComponentBaseTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.service.assertors.BaseAssertion +import com.android.server.wm.flicker.traces.FlickerSubjectException +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.common.tags.Tag + +abstract class AppComponentBaseTest : BaseAssertion() { + protected fun getComponentName( + tag: Tag, + wmSubject: WindowManagerTraceSubject + ): FlickerComponentName { + try { + val windowName = getWindowState(tag, wmSubject).name + return FlickerComponentName.unflattenFromString(windowName) + } catch (e: Throwable) { + throw FlickerSubjectException(wmSubject, e) + } + } + + private fun getWindowState(tag: Tag, wmSubject: WindowManagerTraceSubject) = + wmSubject.subjects.last().windowState { + it.layerId == tag.layerId || it.token == tag.windowToken + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsInvisibleAtEnd.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsInvisibleAtEnd.kt new file mode 100644 index 000000000..58a4e20af --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsInvisibleAtEnd.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the [getWindowState] layer is invisible at the end of the transition + */ +class AppLayerIsInvisibleAtEnd : AppComponentBaseTest() { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + layerSubject.last().isInvisible(getComponentName(tag, wmSubject)) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsInvisibleAtStart.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsInvisibleAtStart.kt new file mode 100644 index 000000000..2c40e5713 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsInvisibleAtStart.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the [getWindowState] layer is invisible at the start of the transition + */ +class AppLayerIsInvisibleAtStart : AppComponentBaseTest() { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + layerSubject.first().isInvisible(getComponentName(tag, wmSubject)) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsVisibleAtEnd.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsVisibleAtEnd.kt new file mode 100644 index 000000000..3670d26db --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsVisibleAtEnd.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the [getWindowState] layer is visible at the end of the transition + */ +class AppLayerIsVisibleAtEnd : AppComponentBaseTest() { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + layerSubject.last().isVisible(getComponentName(tag, wmSubject)) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsVisibleAtStart.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsVisibleAtStart.kt new file mode 100644 index 000000000..479c71085 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerIsVisibleAtStart.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the [getWindowState] layer is visible at the start of the transition + */ +class AppLayerIsVisibleAtStart : AppComponentBaseTest() { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + layerSubject.first().isVisible(getComponentName(tag, wmSubject)) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerReplacesLauncher.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerReplacesLauncher.kt new file mode 100644 index 000000000..1591f7f2e --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppLayerReplacesLauncher.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.service.assertors.Components +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Asserts that: + * [Components.LAUNCHER] is visible at the start of the trace + * [Components.LAUNCHER] becomes invisible during the trace and (in the same entry) + * [getWindowState] becomes visible + * [getWindowState] remains visible until the end of the trace + */ +class AppLayerReplacesLauncher : AppComponentBaseTest() { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + layerSubject.isVisible(Components.LAUNCHER) + .then() + .isVisible(getComponentName(tag, wmSubject)) + } +} diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppWindowReplacesLauncherAsTopWindow.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppWindowReplacesLauncherAsTopWindow.kt new file mode 100644 index 000000000..958327dea --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/AppWindowReplacesLauncherAsTopWindow.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.service.assertors.Components +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks that [Components.LAUNCHER] is the top visible app window at the start of the transition + * and that it is replaced by [getWindowState] during the transition + */ +class AppWindowReplacesLauncherAsTopWindow : AppComponentBaseTest() { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + wmSubject.isAppWindowOnTop(Components.LAUNCHER) + .then() + .isAppWindowOnTop(getComponentName(tag, wmSubject)) + } +} diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/ComponentBaseTest.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/ComponentBaseTest.kt new file mode 100644 index 000000000..30288ea56 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/ComponentBaseTest.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.service.assertors.BaseAssertion +import com.android.server.wm.traces.common.FlickerComponentName + +abstract class ComponentBaseTest(windowName: String) : BaseAssertion() { + protected val component = FlickerComponentName.unflattenFromString(windowName) +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/EntireScreenCoveredAlways.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/EntireScreenCoveredAlways.kt new file mode 100644 index 000000000..9820eaac3 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/EntireScreenCoveredAlways.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.service.assertors.BaseAssertion +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the stack space of all displays is fully covered by any visible layer, + * during the whole transitions + */ +class EntireScreenCoveredAlways : BaseAssertion() { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + layerSubject.invoke("entireScreenCovered") { entry -> + entry.entry.displays.forEach { display -> + entry.visibleRegion().coversAtLeast(display.layerStackSpace) + } + } + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/EntireScreenCoveredAtEnd.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/EntireScreenCoveredAtEnd.kt new file mode 100644 index 000000000..62b25397c --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/EntireScreenCoveredAtEnd.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.service.assertors.BaseAssertion +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the stack space of all displays is fully covered by any visible layer, + * at the end of the transition + */ +class EntireScreenCoveredAtEnd : BaseAssertion() { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + val subject = layerSubject.last() + subject.entry.displays.forEach { display -> + subject.visibleRegion().coversAtLeast(display.layerStackSpace) + } + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/EntireScreenCoveredAtStart.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/EntireScreenCoveredAtStart.kt new file mode 100644 index 000000000..c51eb51e3 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/EntireScreenCoveredAtStart.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.service.assertors.BaseAssertion +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the stack space of all displays is fully covered by any visible layer, + * at the start of the transition + */ +class EntireScreenCoveredAtStart : BaseAssertion() { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + val subject = layerSubject.first() + subject.entry.displays.forEach { display -> + subject.visibleRegion().coversAtLeast(display.layerStackSpace) + } + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LauncherWindowMovesOutOfTop.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LauncherWindowMovesOutOfTop.kt new file mode 100644 index 000000000..4025a9567 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LauncherWindowMovesOutOfTop.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.service.assertors.Components + +/** + * Checks that [Components.LAUNCHER] starts on top and moves out of top during the transition + */ +class LauncherWindowMovesOutOfTop : WindowMovesOutOfTop(Components.LAUNCHER.toWindowName())
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LauncherWindowMovesToTop.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LauncherWindowMovesToTop.kt new file mode 100644 index 000000000..821510171 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LauncherWindowMovesToTop.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.service.assertors.Components + +/** + * Checks that [Components.LAUNCHER] starts not on top and moves to top during the transition + */ +class LauncherWindowMovesToTop : WindowMovesToTop(Components.LAUNCHER.toWindowName())
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LauncherWindowReplacesAppAsTopWindow.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LauncherWindowReplacesAppAsTopWindow.kt new file mode 100644 index 000000000..a62de1d7b --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LauncherWindowReplacesAppAsTopWindow.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.service.assertors.Components +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks that [getWindowState] is the top visible app window at the start of the transition and + * that it is replaced by [Components.LAUNCHER] during the transition + */ +class LauncherWindowReplacesAppAsTopWindow : AppComponentBaseTest() { + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + wmSubject.isAppWindowOnTop(getComponentName(tag, wmSubject)) + .then() + .isAppWindowOnTop(Components.LAUNCHER) + } +} diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsInvisibleAlways.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsInvisibleAlways.kt new file mode 100644 index 000000000..150c2a9f9 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsInvisibleAlways.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the [component] layer is invisible during the entire transition + */ +class LayerIsInvisibleAlways(windowName: String) : ComponentBaseTest(windowName) { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + layerSubject.isVisible(component) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsInvisibleAtEnd.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsInvisibleAtEnd.kt new file mode 100644 index 000000000..198302bee --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsInvisibleAtEnd.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the [component] layer is invisible at the end of the transition + */ +class LayerIsInvisibleAtEnd(windowName: String) : ComponentBaseTest(windowName) { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + layerSubject.last().isInvisible(component) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsInvisibleAtStart.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsInvisibleAtStart.kt new file mode 100644 index 000000000..e241b7e04 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsInvisibleAtStart.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the [component] layer is invisible at the start of the transition + */ +class LayerIsInvisibleAtStart(windowName: String) : ComponentBaseTest(windowName) { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + layerSubject.first().isInvisible(component) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsVisibleAlways.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsVisibleAlways.kt new file mode 100644 index 000000000..a4c126577 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsVisibleAlways.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the [component] layer is visible during the entire transition + */ +class LayerIsVisibleAlways(windowName: String) : ComponentBaseTest(windowName) { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + layerSubject.isVisible(component) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsVisibleAtEnd.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsVisibleAtEnd.kt new file mode 100644 index 000000000..5d012ce98 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsVisibleAtEnd.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the [component] layer is visible at the end of the transition + */ +class LayerIsVisibleAtEnd(windowName: String) : ComponentBaseTest(windowName) { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + layerSubject.last().isVisible(component) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsVisibleAtStart.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsVisibleAtStart.kt new file mode 100644 index 000000000..0d61d8779 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/LayerIsVisibleAtStart.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the [component] layer is visible at the start of the transition + */ +class LayerIsVisibleAtStart(windowName: String) : ComponentBaseTest(windowName) { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + layerSubject.first().isVisible(component) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NavBarLayerPositionAtEnd.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NavBarLayerPositionAtEnd.kt new file mode 100644 index 000000000..4ab581995 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NavBarLayerPositionAtEnd.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.server.wm.flicker.service.assertors.BaseAssertion +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the [FlickerComponentName.NAV_BAR] layer is placed at the correct position at the + * end of the transition + */ +class NavBarLayerPositionAtEnd : BaseAssertion() { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + val lastLayersSubject = layerSubject.last() + val display = lastLayersSubject.entry.displays.minByOrNull { it.id } + ?: throw RuntimeException("There is no display!") + lastLayersSubject.visibleRegion(FlickerComponentName.NAV_BAR) + .coversExactly(WindowUtils.getNavigationBarPosition(display)) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NavBarLayerPositionAtStart.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NavBarLayerPositionAtStart.kt new file mode 100644 index 000000000..eba2fbe3a --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NavBarLayerPositionAtStart.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.server.wm.flicker.service.assertors.BaseAssertion +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the [FlickerComponentName.NAV_BAR] layer is placed at the correct position at the + * start of the transition + */ +class NavBarLayerPositionAtStart : BaseAssertion() { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + val firstLayersSubject = layerSubject.first() + val display = firstLayersSubject.entry.displays.minByOrNull { it.id } + ?: throw RuntimeException("There is no display!") + firstLayersSubject.visibleRegion(FlickerComponentName.NAV_BAR) + .coversExactly(WindowUtils.getNavigationBarPosition(display)) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowBecomesVisible.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowBecomesVisible.kt new file mode 100644 index 000000000..3ec9cc741 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowBecomesVisible.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +open class NonAppWindowBecomesVisible(windowName: String) : ComponentBaseTest(windowName) { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + wmSubject.isNonAppWindowInvisible(component) + .then() + .isAppWindowVisible(component) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsInvisibleAlways.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsInvisibleAlways.kt new file mode 100644 index 000000000..2cbc23902 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsInvisibleAlways.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the [component] window is invisible during the entire transition + */ +class NonAppWindowIsInvisibleAlways(windowName: String) : ComponentBaseTest(windowName) { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + wmSubject.isNonAppWindowInvisible(component) + } +} diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsVisibleAlways.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsVisibleAlways.kt new file mode 100644 index 000000000..649219369 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsVisibleAlways.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the [component] window is visible during the entire transition + */ +class NonAppWindowIsVisibleAlways(windowName: String) : ComponentBaseTest(windowName) { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + wmSubject.isNonAppWindowVisible(component) + } +} diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsVisibleAtEnd.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsVisibleAtEnd.kt new file mode 100644 index 000000000..7471bc3fb --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsVisibleAtEnd.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the [component] window is visible at the end of the transition + */ +class NonAppWindowIsVisibleAtEnd(windowName: String) : ComponentBaseTest(windowName) { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + wmSubject.last().isNonAppWindowVisible(component) + } +} diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsVisibleAtStart.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsVisibleAtStart.kt new file mode 100644 index 000000000..c8f98c0d3 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/NonAppWindowIsVisibleAtStart.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the [component] window is visible at the end of the transition + */ +class NonAppWindowIsVisibleAtStart(windowName: String) : ComponentBaseTest(windowName) { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + wmSubject.first().isNonAppWindowVisible(component) + } +} diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/RotationLayerAppearsAndVanishes.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/RotationLayerAppearsAndVanishes.kt new file mode 100644 index 000000000..1f1c1c79f --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/RotationLayerAppearsAndVanishes.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.service.assertors.BaseAssertion +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks that the [FlickerComponentName.ROTATION] layer appears during the transition, + * doesn't flicker, and disappears before the transition is complete. + */ +class RotationLayerAppearsAndVanishes : BaseAssertion() { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + val window = wmSubject.trace.entries.first().topVisibleAppWindow + val appComponent = FlickerComponentName.unflattenFromString(window) + layerSubject.isVisible(appComponent) + .then() + .isVisible(FlickerComponentName.ROTATION) + .then() + .isVisible(appComponent) + .isInvisible(FlickerComponentName.ROTATION) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/StatusBarLayerPositionAtEnd.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/StatusBarLayerPositionAtEnd.kt new file mode 100644 index 000000000..d26d1c606 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/StatusBarLayerPositionAtEnd.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.server.wm.flicker.service.assertors.BaseAssertion +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the [FlickerComponentName.STATUS_BAR] layer is placed at the correct position at the + * end of the transition + */ +class StatusBarLayerPositionAtEnd : BaseAssertion() { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + val endDisplay = layerSubject.last().entry.displays.minByOrNull { it.id } + ?: throw RuntimeException("Display not found") + + layerSubject.last().visibleRegion(FlickerComponentName.STATUS_BAR) + .coversExactly(WindowUtils.getStatusBarPosition(endDisplay)) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/StatusBarLayerPositionAtStart.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/StatusBarLayerPositionAtStart.kt new file mode 100644 index 000000000..ad89b6a44 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/StatusBarLayerPositionAtStart.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.helpers.WindowUtils +import com.android.server.wm.flicker.service.assertors.BaseAssertion +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks if the [FlickerComponentName.STATUS_BAR] layer is placed at the correct position at the + * start of the transition + */ +class StatusBarLayerPositionAtStart : BaseAssertion() { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + val startDisplay = layerSubject.first().entry.displays.minByOrNull { it.id } + ?: throw RuntimeException("Display not found") + + layerSubject.first().visibleRegion(FlickerComponentName.STATUS_BAR) + .coversExactly(WindowUtils.getStatusBarPosition(startDisplay)) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/VisibleLayersShownMoreThanOneConsecutiveEntry.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/VisibleLayersShownMoreThanOneConsecutiveEntry.kt new file mode 100644 index 000000000..57b39d919 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/VisibleLayersShownMoreThanOneConsecutiveEntry.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.service.assertors.BaseAssertion +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks that all layers that are visible on the trace, are visible for at least 2 + * consecutive entries. + */ +class VisibleLayersShownMoreThanOneConsecutiveEntry : BaseAssertion() { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + layerSubject.visibleLayersShownMoreThanOneConsecutiveEntry() + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/VisibleWindowsShownMoreThanOneConsecutiveEntry.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/VisibleWindowsShownMoreThanOneConsecutiveEntry.kt new file mode 100644 index 000000000..c029479fe --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/VisibleWindowsShownMoreThanOneConsecutiveEntry.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.service.assertors.BaseAssertion +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks that all windows that are visible on the trace, are visible for at least 2 + * consecutive entries. + */ +class VisibleWindowsShownMoreThanOneConsecutiveEntry : BaseAssertion() { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + wmSubject.visibleWindowsShownMoreThanOneConsecutiveEntry() + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/WindowMovesOutOfTop.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/WindowMovesOutOfTop.kt new file mode 100644 index 000000000..b717e3f2c --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/WindowMovesOutOfTop.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks that [component] starts on top and moves out of top during the transition + */ +open class WindowMovesOutOfTop(windowName: String) : ComponentBaseTest(windowName) { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + wmSubject.isAppWindowOnTop(component) + .then() + .isAppWindowNotOnTop(component) + } +} diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/WindowMovesToTop.kt b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/WindowMovesToTop.kt new file mode 100644 index 000000000..baf5ef2e7 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/assertors/common/WindowMovesToTop.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors.common + +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.tags.Tag + +/** + * Checks that [component] starts not on top and moves to top during the transition + */ +open class WindowMovesToTop(windowName: String) : ComponentBaseTest(windowName) { + /** {@inheritDoc} */ + override fun doEvaluate( + tag: Tag, + wmSubject: WindowManagerTraceSubject, + layerSubject: LayersTraceSubject + ) { + wmSubject.isAppWindowNotOnTop(component) + .then() + .isAppWindowOnTop(component) + } +} diff --git a/libraries/flicker/src/com/android/server/wm/flicker/service/resources/config.json b/libraries/flicker/src/com/android/server/wm/flicker/service/resources/config.json new file mode 100644 index 000000000..4768e4523 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/flicker/service/resources/config.json @@ -0,0 +1,162 @@ +{ + "assertors": [ + { + "transition": "ROTATION", + "assertions": { + "presubmit": [ + { + "class": "com.android.server.wm.flicker.service.assertors.common.NonAppWindowIsInvisibleAlways", + "args": [ + "/StatusBar" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsInvisibleAlways", + "args": [ + "/StatusBar" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtStart" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtEnd" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.RotationLayerAppearsAndVanishes" + } + ], + "postsubmit": [], + "flaky": [ + { + "class": "com.android.server.wm.flicker.service.assertors.common.NonAppWindowIsVisibleAlways", + "args": [ + "/NavigationBar0" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtStart", + "args": [ + "/NavigationBar0" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtEnd", + "args": [ + "/NavigationBar0" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.NavBarLayerPositionAtStart" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.NavBarLayerPositionAtEnd" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAlways" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.VisibleWindowsShownMoreThanOneConsecutiveEntry" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.VisibleLayersShownMoreThanOneConsecutiveEntry" + } + ] + } + }, + { + "transition": "APP_LAUNCH", + "assertions": { + "presubmit": [ + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtStart", + "args": [ + "/NavigationBar0" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtEnd", + "args": [ + "/NavigationBar0" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.NonAppWindowIsVisibleAlways", + "args": [ + "/NavigationBar0" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.NonAppWindowIsVisibleAlways", + "args": [ + "/StatusBar" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAlways", + "args": [ + "/StatusBar" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtStart" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtEnd" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.AppLayerReplacesLauncher" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtStart", + "args": [ + "com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsInvisibleAtEnd", + "args": [ + "com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.AppLayerIsInvisibleAtStart" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.AppLayerIsVisibleAtEnd" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.AppWindowReplacesLauncherAsTopWindow" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.LauncherWindowMovesOutOfTop" + } + ], + "postsubmit": [], + "flaky": [ + { + "class": "com.android.server.wm.flicker.service.assertors.common.NavBarLayerPositionAtStart" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.NavBarLayerPositionAtEnd" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.StatusBarLayerPositionAtStart" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.StatusBarLayerPositionAtEnd" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAlways" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.VisibleWindowsShownMoreThanOneConsecutiveEntry" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.VisibleLayersShownMoreThanOneConsecutiveEntry" + } + ] + } + } + ] +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerSubjectException.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerSubjectException.kt index 7a5ea0c08..4f775ff1d 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerSubjectException.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerSubjectException.kt @@ -17,13 +17,47 @@ package com.android.server.wm.flicker.traces import com.android.server.wm.flicker.assertions.FlickerSubject +import com.android.server.wm.traces.common.prettyTimestamp /** * Exception thrown by [FlickerSubject]s */ class FlickerSubjectException( - flickerSubject: FlickerSubject, + internal val subject: FlickerSubject, cause: Throwable -) : AssertionError(flickerSubject.defaultFacts, cause) { - internal val facts = flickerSubject.defaultFacts +) : AssertionError(cause.message, if (cause is FlickerSubjectException) null else cause) { + internal val timestamp = subject.timestamp + private val prettyTimestamp = + if (timestamp > 0) "${prettyTimestamp(timestamp)} (timestamp=$timestamp)" else "" + + internal val errorType: String = + if (cause is AssertionError) "Flicker assertion error" else "Unknown error" + + internal val errorDescription = buildString { + appendln("Where? $prettyTimestamp") + val message = (cause.message ?: "").split(("\n")) + append("What? ") + if (message.size == 1) { + // Single line error message + appendln(message.first()) + } else { + // Multi line error message + appendln() + message.forEach { appendln("\t$it") } + } + } + + internal val subjectInformation = buildString { + appendln("Facts:") + subject.completeFacts.forEach { append("\t").appendln(it) } + } + + override val message: String + get() = buildString { + appendln(errorType) + appendln() + append(errorDescription) + appendln() + append(subjectInformation) + } }
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerTraceSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerTraceSubject.kt index b6d260743..b5ad41944 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerTraceSubject.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/FlickerTraceSubject.kt @@ -19,6 +19,8 @@ package com.android.server.wm.flicker.traces import com.android.server.wm.flicker.assertions.Assertion import com.android.server.wm.flicker.assertions.AssertionsChecker import com.android.server.wm.flicker.assertions.FlickerSubject +import com.android.server.wm.traces.common.prettyTimestamp +import com.google.common.truth.Fact import com.google.common.truth.FailureMetadata /** @@ -28,16 +30,37 @@ abstract class FlickerTraceSubject<EntrySubject : FlickerSubject>( fm: FailureMetadata, data: Any? ) : FlickerSubject(fm, data) { + override val timestamp: Long get() = subjects.first().timestamp + override val selfFacts by lazy { + val firstTimestamp = subjects.firstOrNull()?.timestamp ?: 0L + val lastTimestamp = subjects.lastOrNull()?.timestamp ?: 0L + val first = "${prettyTimestamp(firstTimestamp)} (timestamp=$firstTimestamp)" + val last = "${prettyTimestamp(lastTimestamp)} (timestamp=$lastTimestamp)" + listOf(Fact.fact("Trace start", first), + Fact.fact("Trace end", last)) + } + protected val assertionsChecker = AssertionsChecker<EntrySubject>() private var newAssertionBlock = true abstract val subjects: List<EntrySubject> - protected fun addAssertion(name: String, assertion: Assertion<EntrySubject>) { + /** + * Adds a new assertion block (if preceded by [then]) or appends an assertion to the + * latest existing assertion block + * + * @param name Assertion name + * @param isOptional If this assertion is optional or must pass + */ + protected fun addAssertion( + name: String, + isOptional: Boolean = false, + assertion: Assertion<EntrySubject> + ) { if (newAssertionBlock) { - assertionsChecker.add(name, assertion) + assertionsChecker.add(name, isOptional, assertion) } else { - assertionsChecker.append(name, assertion) + assertionsChecker.append(name, isOptional, assertion) } newAssertionBlock = false } @@ -68,7 +91,30 @@ abstract class FlickerTraceSubject<EntrySubject : FlickerSubject>( * Will produce two sets of assertions (checkA) and (checkB) and checkB will only be checked * after checkA passes. */ - protected fun startAssertionBlock() { + open fun then(): FlickerTraceSubject<EntrySubject> = apply { + startAssertionBlock() + } + + /** + * Ignores the first entries in the trace, until the first assertion passes. If it reaches the + * end of the trace without passing any assertion, return a failure with the name/reason from + * the first assertion + * + * @return + */ + open fun skipUntilFirstAssertion(): FlickerTraceSubject<EntrySubject> = + apply { assertionsChecker.skipUntilFirstAssertion() } + + /** + * Signal that the last assertion set is complete. The next assertion added will start a new + * set of assertions. + * + * E.g.: checkA().then().checkB() + * + * Will produce two sets of assertions (checkA) and (checkB) and checkB will only be checked + * after checkA passes. + */ + private fun startAssertionBlock() { newAssertionBlock = true } @@ -76,11 +122,14 @@ abstract class FlickerTraceSubject<EntrySubject : FlickerSubject>( * Checks whether all the trace entries on the list are visible for more than one consecutive * entry * - * @param [visibleEntries] a list of all the entries with their name and index + * @param [visibleEntriesProvider] a list of all the entries with their name and index */ protected fun visibleEntriesShownMoreThanOneConsecutiveTime( visibleEntriesProvider: (EntrySubject) -> Set<String> ) { + if (subjects.isEmpty()) { + return + } var lastVisible = visibleEntriesProvider(subjects.first()) val lastNew = lastVisible.toMutableSet() @@ -102,4 +151,7 @@ abstract class FlickerTraceSubject<EntrySubject : FlickerSubject>( lastEntry.fail("$lastNew is not visible for 2 entries") } } + + override fun toString(): String = "${this::class.simpleName}" + + "(${subjects.firstOrNull()?.timestamp ?: 0},${subjects.lastOrNull()?.timestamp ?: 0})" }
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/RegionSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/RegionSubject.kt index 6c3f81b70..d10a9f11e 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/traces/RegionSubject.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/RegionSubject.kt @@ -33,9 +33,10 @@ import com.google.common.truth.StandardSubjectBuilder */ class RegionSubject( fm: FailureMetadata, - private val subjects: List<FlickerSubject>, + override val parent: FlickerSubject?, val region: android.graphics.Region ) : FlickerSubject(fm, region) { + override val timestamp: Long get() = parent?.timestamp ?: 0 private val topPositionSubject get() = check(MSG_ERROR_TOP_POSITION).that(region.bounds.top) private val bottomPositionSubject @@ -50,15 +51,13 @@ class RegionSubject( private val android.graphics.Rect.area get() = this.width() * this.height() private val Rect.area get() = this.width * this.height - override val defaultFacts: String = buildString { - subjects.forEach { subject -> appendln(subject.defaultFacts) } - } + override val selfFacts = listOf(Fact.fact("Region - Covered", region.toString())) /** * {@inheritDoc} */ override fun clone(): FlickerSubject { - return RegionSubject(fm, subjects, region) + return RegionSubject(fm, parent, region) } /** @@ -76,6 +75,34 @@ class RegionSubject( } /** + * Subtracts [other] from this subject [region] + */ + fun minus(other: Region): RegionSubject = minus(other.toAndroidRegion()) + + /** + * Subtracts [other] from this subject [region] + */ + fun minus(other: android.graphics.Region): RegionSubject { + val remainingRegion = android.graphics.Region(this.region) + remainingRegion.op(other, android.graphics.Region.Op.XOR) + return assertThat(remainingRegion, this) + } + + /** + * Adds [other] to this subject [region] + */ + fun plus(other: Region): RegionSubject = plus(other.toAndroidRegion()) + + /** + * Adds [other] to this subject [region] + */ + fun plus(other: android.graphics.Region): RegionSubject { + val remainingRegion = android.graphics.Region(this.region) + remainingRegion.op(other, android.graphics.Region.Op.UNION) + return assertThat(remainingRegion, this) + } + + /** * Asserts that the top and bottom coordinates of [RegionSubject.region] are smaller * or equal to those of [region]. * @@ -503,26 +530,24 @@ class RegionSubject( * Boiler-plate Subject.Factory for RectSubject */ @JvmStatic - @JvmOverloads fun getFactory( - flickerSubjects: List<FlickerSubject> = emptyList() + parent: FlickerSubject? ) = Factory { fm: FailureMetadata, region: android.graphics.Region? -> val subjectRegion = region ?: android.graphics.Region() - RegionSubject(fm, flickerSubjects, subjectRegion) + RegionSubject(fm, parent, subjectRegion) } /** * User-defined entry point for existing android regions */ @JvmStatic - @JvmOverloads fun assertThat( region: android.graphics.Region?, - flickerSubjects: List<FlickerSubject> = emptyList() + parent: FlickerSubject? = null ): RegionSubject { val strategy = FlickerFailureStrategy() val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy) - .about(getFactory(flickerSubjects)) + .about(getFactory(parent)) .that(region ?: android.graphics.Region()) as RegionSubject strategy.init(subject) return subject @@ -533,59 +558,47 @@ class RegionSubject( */ @JvmStatic @JvmOverloads - fun assertThat( - rect: Array<Rect>, - flickerSubjects: List<FlickerSubject> = emptyList() - ): RegionSubject = assertThat(Region(rect), flickerSubjects) + fun assertThat(rect: Array<Rect>, parent: FlickerSubject? = null): RegionSubject = + assertThat(Region(rect), parent) /** * User-defined entry point for existing rects */ @JvmStatic @JvmOverloads - fun assertThat( - rect: Rect?, - flickerSubjects: List<FlickerSubject> = emptyList() - ): RegionSubject = assertThat(Region(rect), flickerSubjects) + fun assertThat(rect: Rect?, parent: FlickerSubject? = null): RegionSubject = + assertThat(Region(rect), parent) /** * User-defined entry point for existing rects */ @JvmStatic - fun assertThat( - rect: RectF?, - flickerSubjects: List<FlickerSubject> = emptyList() - ): RegionSubject = assertThat(rect?.toRect(), flickerSubjects) + @JvmOverloads + fun assertThat(rect: RectF?, parent: FlickerSubject? = null): RegionSubject = + assertThat(rect?.toRect(), parent) /** * User-defined entry point for existing rects */ @JvmStatic - fun assertThat( - rect: Array<RectF>, - flickerSubjects: List<FlickerSubject> = emptyList() - ): RegionSubject = assertThat( - mergeRegions(rect.map { Region(it.toRect()) }.toTypedArray()), - flickerSubjects) + @JvmOverloads + fun assertThat(rect: Array<RectF>, parent: FlickerSubject? = null): RegionSubject = + assertThat(mergeRegions(rect.map { Region(it.toRect()) }.toTypedArray()), parent) /** * User-defined entry point for existing regions */ @JvmStatic @JvmOverloads - fun assertThat( - regions: Array<Region>, - flickerSubjects: List<FlickerSubject> = emptyList() - ): RegionSubject = assertThat(mergeRegions(regions), flickerSubjects) + fun assertThat(regions: Array<Region>, parent: FlickerSubject? = null): RegionSubject = + assertThat(mergeRegions(regions), parent) /** * User-defined entry point for existing regions */ @JvmStatic @JvmOverloads - fun assertThat( - region: Region?, - flickerSubjects: List<FlickerSubject> = emptyList() - ): RegionSubject = assertThat(region?.toAndroidRegion(), flickerSubjects) + fun assertThat(region: Region?, parent: FlickerSubject? = null): RegionSubject = + assertThat(region?.toAndroidRegion(), parent) } }
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/EventLogSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/EventLogSubject.kt index 704e46b3e..8036730b8 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/EventLogSubject.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/EventLogSubject.kt @@ -18,6 +18,7 @@ package com.android.server.wm.flicker.traces.eventlog import com.android.server.wm.flicker.assertions.AssertionsChecker import com.android.server.wm.flicker.assertions.FlickerSubject +import com.android.server.wm.traces.common.prettyTimestamp import com.google.common.truth.Fact import com.google.common.truth.FailureMetadata import com.google.common.truth.Subject.Factory @@ -30,10 +31,15 @@ class EventLogSubject private constructor( failureMetadata: FailureMetadata, private val trace: List<FocusEvent> ) : FlickerSubject(failureMetadata, trace) { - override val defaultFacts: String by lazy { - val first = subjects.first().defaultFacts - val last = subjects.last().defaultFacts - "EventLogSubject($first, $last)" + override val timestamp: Long get() = 0 + override val parent: FlickerSubject? get() = null + override val selfFacts by lazy { + val firstTimestamp = subjects.first().timestamp + val lastTimestamp = subjects.last().timestamp + val first = "${prettyTimestamp(firstTimestamp)} (timestamp=$firstTimestamp)" + val last = "${prettyTimestamp(lastTimestamp)} (timestamp=$lastTimestamp)" + listOf(Fact.fact("Trace start", first), + Fact.fact("Trace end", last)) } /** {@inheritDoc} */ @@ -52,7 +58,7 @@ class EventLogSubject private constructor( focusList + trace.filter { it.hasFocus() }.map { it.window } } - fun focusChanges(windows: Array<out String>) = apply { + fun focusChanges(vararg windows: String) = apply { if (windows.isNotEmpty()) { val focusChanges = _focusChanges .dropWhile { !it.contains(windows.first()) } diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/FocusEventSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/FocusEventSubject.kt index fb6745dd5..1f691eb31 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/FocusEventSubject.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/eventlog/FocusEventSubject.kt @@ -18,19 +18,21 @@ package com.android.server.wm.flicker.traces.eventlog import com.android.server.wm.flicker.assertions.FlickerSubject import com.android.server.wm.flicker.traces.FlickerFailureStrategy +import com.google.common.truth.Fact import com.google.common.truth.FailureMetadata import com.google.common.truth.StandardSubjectBuilder class FocusEventSubject( fm: FailureMetadata, val event: FocusEvent, - val trace: EventLogSubject? + override val parent: EventLogSubject? ) : FlickerSubject(fm, event) { - override val defaultFacts by lazy { event.toString() } + override val timestamp: Long get() = 0 + override val selfFacts by lazy { listOf(Fact.simpleFact(event.toString())) } /** {@inheritDoc} */ override fun clone(): FlickerSubject { - return FocusEventSubject(fm, event, trace) + return FocusEventSubject(fm, event, parent) } fun hasFocus() { diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerSubject.kt index efa0352b3..614cd9ae7 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerSubject.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerSubject.kt @@ -20,8 +20,9 @@ import com.android.server.wm.flicker.assertions.Assertion import com.android.server.wm.flicker.traces.FlickerFailureStrategy import com.android.server.wm.flicker.assertions.FlickerSubject import com.android.server.wm.flicker.traces.RegionSubject -import com.android.server.wm.traces.common.Bounds +import com.android.server.wm.traces.common.Size import com.android.server.wm.traces.common.layers.Layer +import com.google.common.truth.Fact import com.google.common.truth.FailureMetadata import com.google.common.truth.FailureStrategy import com.google.common.truth.StandardSubjectBuilder @@ -48,8 +49,9 @@ import com.google.common.truth.Subject.Factory */ class LayerSubject private constructor( fm: FailureMetadata, + override val parent: FlickerSubject, + override val timestamp: Long, val layer: Layer?, - val entry: LayerTraceEntrySubject?, private val layerName: String? = null ) : FlickerSubject(fm, layer) { val isEmpty: Boolean get() = layer == null @@ -62,16 +64,19 @@ class LayerSubject private constructor( * Visible region calculated by the Composition Engine */ val visibleRegion: RegionSubject get() = - RegionSubject.assertThat(layer?.visibleRegion, listOf(this)) + RegionSubject.assertThat(layer?.visibleRegion, this) /** * Visible region calculated by the Composition Engine (when available) or calculated * based on the layer bounds and transform */ val screenBounds: RegionSubject get() = - RegionSubject.assertThat(layer?.screenBounds, listOf(this)) + RegionSubject.assertThat(layer?.screenBounds, this) - override val defaultFacts: String = - "${entry?.defaultFacts ?: ""}\nFrame: ${layer?.currFrame}\nLayer: ${layer?.name}" + override val selfFacts = if (layer != null) { + listOf(Fact.fact("Frame", layer.currFrame), Fact.fact("Layer", layer.name)) + } else { + listOf(Fact.fact("Layer name", layerName)) + } /** * If the [layer] exists, executes a custom [assertion] on the current subject @@ -83,7 +88,7 @@ class LayerSubject private constructor( /** {@inheritDoc} */ override fun clone(): FlickerSubject { - return LayerSubject(fm, layer, entry, layerName) + return LayerSubject(fm, parent, timestamp, layer, layerName) } /** @@ -102,7 +107,7 @@ class LayerSubject private constructor( @Deprecated("Prefer hasBufferSize(bounds)") fun hasBufferSize(size: Point): LayerSubject = apply { - val bounds = Bounds(size.x, size.y) + val bounds = Size(size.x, size.y) hasBufferSize(bounds) } @@ -112,9 +117,9 @@ class LayerSubject private constructor( * * @param size expected buffer size */ - fun hasBufferSize(size: Bounds): LayerSubject = apply { + fun hasBufferSize(size: Size): LayerSubject = apply { layer ?: return exists() - val bufferSize = layer.activeBuffer?.size ?: Bounds.EMPTY + val bufferSize = Size(layer.activeBuffer.width, layer.activeBuffer.height) check("Incorrect buffer size").that(bufferSize).isEqualTo(size) } @@ -162,50 +167,43 @@ class LayerSubject private constructor( * Boiler-plate Subject.Factory for LayerSubject */ @JvmStatic - @JvmOverloads - fun getFactory(entry: LayerTraceEntrySubject? = null) = - Factory { fm: FailureMetadata, subject: Layer? -> LayerSubject(fm, subject, entry) } + fun getFactory(parent: FlickerSubject, timestamp: Long, name: String?) = + Factory { fm: FailureMetadata, subject: Layer? -> + LayerSubject(fm, parent, timestamp, subject, name) + } /** - * User-defined entry point for existing layers + * User-defined parent point for existing layers */ @JvmStatic - @JvmOverloads fun assertThat( layer: Layer?, - entry: LayerTraceEntrySubject? = null + parent: FlickerSubject, + timestamp: Long ): LayerSubject { val strategy = FlickerFailureStrategy() val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy) - .about(getFactory(entry)) + .about(getFactory(parent, timestamp, name = null)) .that(layer) as LayerSubject strategy.init(subject) return subject } /** - * User-defined entry point for non existing layers + * User-defined parent point for non existing layers */ @JvmStatic internal fun assertThat( name: String, - entry: LayerTraceEntrySubject? + parent: FlickerSubject, + timestamp: Long ): LayerSubject { val strategy = FlickerFailureStrategy() val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy) - .about(getFactory(entry, name)) + .about(getFactory(parent, timestamp, name)) .that(null) as LayerSubject strategy.init(subject) return subject } - - /** - * Boiler-plate Subject.Factory for LayerSubject - */ - @JvmStatic - internal fun getFactory(entry: LayerTraceEntrySubject?, name: String) = - Factory { fm: FailureMetadata, subject: Layer? -> - LayerSubject(fm, subject, entry, name) - } } }
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerTraceEntrySubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerTraceEntrySubject.kt index 7a902ae3f..8f3547785 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerTraceEntrySubject.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayerTraceEntrySubject.kt @@ -18,12 +18,13 @@ package com.android.server.wm.flicker.traces.layers import com.android.server.wm.flicker.assertions.Assertion import com.android.server.wm.flicker.assertions.FlickerSubject -import com.android.server.wm.flicker.containsAny import com.android.server.wm.flicker.traces.FlickerFailureStrategy import com.android.server.wm.flicker.traces.FlickerSubjectException import com.android.server.wm.flicker.traces.RegionSubject +import com.android.server.wm.traces.common.FlickerComponentName import com.android.server.wm.traces.common.layers.Layer import com.android.server.wm.traces.common.layers.LayerTraceEntry +import com.android.server.wm.traces.common.layers.LayersTrace import com.google.common.truth.ExpectFailure import com.google.common.truth.Fact import com.google.common.truth.FailureMetadata @@ -56,12 +57,14 @@ import com.google.common.truth.Subject class LayerTraceEntrySubject private constructor( fm: FailureMetadata, val entry: LayerTraceEntry, - val trace: LayersTraceSubject? + val trace: LayersTrace?, + override val parent: FlickerSubject? ) : FlickerSubject(fm, entry) { - override val defaultFacts: String = "${trace?.defaultFacts ?: ""}\nEntry: $entry" + override val timestamp: Long get() = entry.timestamp + override val selfFacts = listOf(Fact.fact("Entry", entry)) val subjects by lazy { - entry.flattenedLayers.map { LayerSubject.assertThat(it, this) } + entry.flattenedLayers.map { LayerSubject.assertThat(it, this, timestamp) } } /** @@ -73,15 +76,7 @@ class LayerTraceEntrySubject private constructor( /** {@inheritDoc} */ override fun clone(): FlickerSubject { - return LayerTraceEntrySubject(fm, entry, trace) - } - - /** - * Asserts that current entry subject has an [LayerTraceEntry.timestamp] equals to - * [timestamp] - */ - fun hasTimestamp(timestamp: Long): LayerTraceEntrySubject = apply { - check("Wrong entry timestamp").that(entry.timestamp).isEqualTo(timestamp) + return LayerTraceEntrySubject(fm, entry, trace, parent) } /** @@ -103,112 +98,133 @@ class LayerTraceEntrySubject private constructor( } /** - * Asserts that the current SurfaceFlinger state has [numberLayers] layers - */ - fun hasLayersSize(numberLayers: Int): LayerTraceEntrySubject = apply { - check("Wrong number of layers in entry") - .that(entry.flattenedLayers.size) - .isEqualTo(numberLayers) - } - - /** - * Obtains the region occupied by all layers with name containing any of [partialLayerNames] + * Obtains the region occupied by all layers with name containing [component] * - * @param partialLayerNames Name of the layer to search + * @param component Component to search * @param useCompositionEngineRegionOnly If true, uses only the region calculated from the * Composition Engine (CE) -- visibleRegion in the proto definition. Otherwise calculates * the visible region when the information is not available from the CE */ fun visibleRegion( - vararg partialLayerNames: String, + component: FlickerComponentName? = null, useCompositionEngineRegionOnly: Boolean = true ): RegionSubject { + val layerName = component?.toLayerName() ?: "" val selectedLayers = subjects - .filter { it.name.containsAny(*partialLayerNames) } + .filter { it.name.contains(layerName) } if (selectedLayers.isEmpty()) { - fail("Could not find", partialLayerNames.joinToString(", ")) + fail(listOf( + Fact.fact(ASSERTION_TAG, "visibleRegion(${component?.toLayerName() ?: "<any>"})"), + Fact.fact("Use composition engine region", useCompositionEngineRegionOnly), + Fact.fact("Could not find", layerName)) + ) } val visibleLayers = selectedLayers.filter { it.isVisible } return if (useCompositionEngineRegionOnly) { val visibleAreas = visibleLayers.mapNotNull { it.layer?.visibleRegion }.toTypedArray() - RegionSubject.assertThat(visibleAreas, selectedLayers) + RegionSubject.assertThat(visibleAreas, this) } else { val visibleAreas = visibleLayers.mapNotNull { it.layer?.screenBounds }.toTypedArray() - RegionSubject.assertThat(visibleAreas, selectedLayers) + RegionSubject.assertThat(visibleAreas, this) } } /** - * Asserts that the SurfaceFlinger state contains a [Layer] with [Layer.name] containing any of - * [partialLayerNames]. + * Asserts the state contains a [Layer] with [Layer.name] containing [component]. * - * @param partialLayerNames Name of the layers to search + * @param component Name of the layers to search */ - fun contains(vararg partialLayerNames: String): LayerTraceEntrySubject = apply { - val found = entry.flattenedLayers.any { it.name.containsAny(*partialLayerNames) } - if (partialLayerNames.isNotEmpty() && !found) { - fail("Could not find", partialLayerNames.joinToString(", ")) + fun contains(component: FlickerComponentName): LayerTraceEntrySubject = apply { + val layerName = component.toLayerName() + val found = entry.flattenedLayers.any { it.name.contains(layerName) } + if (!found) { + fail(Fact.fact(ASSERTION_TAG, "contains(${component.toLayerName()})"), + Fact.fact("Could not find", layerName)) } } /** - * Asserts that the SurfaceFlinger state doesn't contain a [Layer] with [Layer.name] containing any of + * Asserts the state doesn't contain a [Layer] with [Layer.name] containing any of * - * @param partialLayerNames Name of the layers to search + * @param component Name of the layers to search */ - fun notContains(vararg partialLayerNames: String): LayerTraceEntrySubject = apply { - val found = entry.flattenedLayers.none { it.name.containsAny(*partialLayerNames) } - if (!found) { - fail("Could find", partialLayerNames) - } + fun notContains(component: FlickerComponentName): LayerTraceEntrySubject = apply { + val layerName = component.toLayerName() + val foundEntry = subjects.firstOrNull { it.name.contains(layerName) } + foundEntry?.fail(Fact.fact(ASSERTION_TAG, "notContains(${component.toLayerName()})"), + Fact.fact("Could find", foundEntry)) } /** - * Asserts that a [Layer] with [Layer.name] containing any of [partialLayerNames] is visible. + * Asserts that a [Layer] with [Layer.name] containing [component] is visible. * - * @param partialLayerNames Name of the layers to search + * @param component Name of the layers to search */ - fun isVisible(vararg partialLayerNames: String): LayerTraceEntrySubject = apply { - contains(*partialLayerNames) + fun isVisible(component: FlickerComponentName): LayerTraceEntrySubject = apply { + contains(component) + var target: FlickerSubject? = null var reason: Fact? = null - val filteredLayers = entry.flattenedLayers - .filter { it.name.containsAny(*partialLayerNames) } + val layerName = component.toLayerName() + val filteredLayers = subjects + .filter { it.name.contains(layerName) } for (layer in filteredLayers) { - if (layer.isHiddenByParent) { - reason = Fact.fact("Hidden by parent", layer.parent.name) + if (layer.layer?.isHiddenByParent == true) { + reason = Fact.fact("Hidden by parent", layer.layer.parent?.name) + target = layer continue } if (layer.isInvisible) { - reason = Fact.fact("Is Invisible", layer.visibilityReason) + reason = Fact.fact("Is Invisible", layer.layer?.visibilityReason) + target = layer continue } reason = null + target = null break } - if (reason != null) { - fail(reason) + reason?.run { + target?.fail(Fact.fact(ASSERTION_TAG, "isVisible(${component.toLayerName()})"), reason) } } /** - * Asserts that a [Layer] with [Layer.name] containing any of [partialLayerNames] doesn't exist or + * Asserts that a [Layer] with [Layer.name] containing [component] doesn't exist or * is invisible. * - * @param partialLayerNames Name of the layers to search + * @param component Name of the layers to search */ - fun isInvisible(vararg partialLayerNames: String): LayerTraceEntrySubject = apply { + fun isInvisible(component: FlickerComponentName): LayerTraceEntrySubject = apply { try { - isVisible(*partialLayerNames) + isVisible(component) } catch (e: FlickerSubjectException) { val cause = e.cause require(cause is AssertionError) ExpectFailure.assertThat(cause).factKeys().isNotEmpty() return@apply } - fail("Layer is visible", partialLayerNames) + val layerName = component.toLayerName() + val foundEntry = subjects + .firstOrNull { it.name.contains(layerName) && it.isVisible } + foundEntry?.fail(Fact.fact(ASSERTION_TAG, "isInvisible(${component.toLayerName()})"), + Fact.fact("Is visible", foundEntry)) + } + + /** + * Obtains a [LayerSubject] for the first occurrence of a [Layer] with [Layer.name] + * containing [component]. + * Always returns a subject, event when the layer doesn't exist. To verify if layer + * actually exists in the hierarchy use [LayerSubject.exists] or [LayerSubject.doesNotExist] + * + * @return LayerSubject that can be used to make assertions on a single layer matching + */ + fun layer(component: FlickerComponentName): LayerSubject { + val name = component.toLayerName() + return layer { + it.name.contains(name) + } } /** @@ -222,10 +238,27 @@ class LayerTraceEntrySubject private constructor( * [name] and [frameNumber]. */ fun layer(name: String, frameNumber: Long): LayerSubject { + return layer(name) { + it.name.contains(name) && it.currFrame == frameNumber + } + } + + /** + * Obtains a [LayerSubject] for the first occurrence of a [Layer] matching [predicate] + * + * Always returns a subject, event when the layer doesn't exist. To verify if layer + * actually exists in the hierarchy use [LayerSubject.exists] or [LayerSubject.doesNotExist] + * + * @param predicate to search for a layer + * @param name Name of the subject to use when not found (optional) + * + * @return [LayerSubject] that can be used to make assertions + */ + @JvmOverloads + fun layer(name: String = "", predicate: (Layer) -> Boolean): LayerSubject { return subjects.firstOrNull { - it.layer?.name?.contains(name) == true && - it.layer.currFrame == frameNumber - } ?: LayerSubject.assertThat(name, this) + it.layer?.run { predicate(this) } ?: false + } ?: LayerSubject.assertThat(name, this, timestamp) } override fun toString(): String { @@ -237,26 +270,28 @@ class LayerTraceEntrySubject private constructor( * Boiler-plate Subject.Factory for LayersTraceSubject */ private fun getFactory( - trace: LayersTraceSubject? = null + trace: LayersTrace?, + parent: FlickerSubject? ): Factory<Subject, LayerTraceEntry> = - Factory { fm, subject -> LayerTraceEntrySubject(fm, subject, trace) } + Factory { fm, subject -> LayerTraceEntrySubject(fm, subject, trace, parent) } /** * Creates a [LayerTraceEntrySubject] to representing a SurfaceFlinger state[entry], * which can be used to make assertions. * * @param entry SurfaceFlinger trace entry - * @param trace Trace that contains this entry (optional) + * @param parent Trace that contains this entry (optional) */ @JvmStatic @JvmOverloads fun assertThat( entry: LayerTraceEntry, - trace: LayersTraceSubject? = null + trace: LayersTrace? = null, + parent: FlickerSubject? = null ): LayerTraceEntrySubject { val strategy = FlickerFailureStrategy() val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy) - .about(getFactory(trace)) + .about(getFactory(trace, parent)) .that(entry) as LayerTraceEntrySubject strategy.init(subject) return subject @@ -267,8 +302,9 @@ class LayerTraceEntrySubject private constructor( */ @JvmStatic @JvmOverloads - fun entries(trace: LayersTraceSubject? = null): Factory<Subject, LayerTraceEntry> { - return getFactory(trace) - } + fun entries( + trace: LayersTrace? = null, + parent: FlickerSubject? = null + ): Factory<Subject, LayerTraceEntry> = getFactory(trace, parent) } }
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayersTraceSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayersTraceSubject.kt index 9c7ba2a03..39c8c6258 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayersTraceSubject.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/layers/LayersTraceSubject.kt @@ -22,9 +22,12 @@ import com.android.server.wm.flicker.assertions.Assertion import com.android.server.wm.flicker.assertions.FlickerSubject import com.android.server.wm.flicker.traces.FlickerFailureStrategy import com.android.server.wm.flicker.traces.FlickerTraceSubject +import com.android.server.wm.traces.common.FlickerComponentName import com.android.server.wm.traces.common.layers.Layer import com.android.server.wm.traces.common.layers.LayersTrace -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.android.server.wm.traces.parser.toAndroidRect +import com.android.server.wm.traces.parser.toAndroidRegion +import com.google.common.truth.Fact import com.google.common.truth.FailureMetadata import com.google.common.truth.FailureStrategy import com.google.common.truth.StandardSubjectBuilder @@ -56,20 +59,18 @@ import com.google.common.truth.Subject.Factory */ class LayersTraceSubject private constructor( fm: FailureMetadata, - val trace: LayersTrace + val trace: LayersTrace, + override val parent: LayersTraceSubject? ) : FlickerTraceSubject<LayerTraceEntrySubject>(fm, trace) { - override val defaultFacts: String by lazy { - buildString { - if (trace.hasSource()) { - append("Path: ${trace.source}") - append("\n") + override val selfFacts + get() = super.selfFacts.toMutableList() + .also { + if (trace.hasSource()) { + it.add(Fact.fact("Trace file", trace.source)) + } } - append("Trace: $trace") - } - } - override val subjects by lazy { - trace.entries.map { LayerTraceEntrySubject.assertThat(it, this) } + trace.entries.map { LayerTraceEntrySubject.assertThat(it, trace, this) } } /** @@ -81,21 +82,11 @@ class LayersTraceSubject private constructor( /** {@inheritDoc} */ override fun clone(): FlickerSubject { - return LayersTraceSubject(fm, trace) + return LayersTraceSubject(fm, trace, parent) } - /** - * Signal that the last assertion set is complete. The next assertion added will start a new - * set of assertions. - * - * E.g.: checkA().then().checkB() - * - * Will produce two sets of assertions (checkA) and (checkB) and checkB will only be checked - * after checkA passes. - */ - fun then(): LayersTraceSubject = apply { - startAssertionBlock() - } + /** {@inheritDoc} */ + override fun then(): LayersTraceSubject = apply { super.then() } fun isEmpty(): LayersTraceSubject = apply { check("Trace is empty").that(trace).isEmpty() @@ -113,206 +104,243 @@ class LayersTraceSubject private constructor( return subjects .map { it.layer(name, frameNumber) } .firstOrNull { it.isNotEmpty } - ?: LayerSubject.assertThat(null) + ?: LayerSubject.assertThat(null, this, timestamp = subjects.first().entry.timestamp) + } + + /** + * @return List of [LayerSubject]s matching [name] in the order they appear on the trace + */ + fun layers(name: String): List<LayerSubject> { + return subjects + .map { it.layer { layer -> layer.name.contains(name) } } + .filter { it.isNotEmpty } + } + + /** + * @return List of [LayerSubject]s matching [predicate] in the order they appear on the trace + */ + fun layers(predicate: (Layer) -> Boolean): List<LayerSubject> { + return subjects + .map { it.layer { layer -> predicate(layer) } } + .filter { it.isNotEmpty } } /** * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of - * [layerName] covers at least [testRegion], that is, if its area of the layer's visible + * [component] covers at least [testRegion], that is, if its area of the layer's visible * region covers each point in the region. * * @param testRegion Expected covered area - * @param layerName Name of the layer to search + * @param component Name of the layer to search */ + @JvmOverloads fun coversAtLeast( testRegion: Rect, - vararg layerName: String - ): LayersTraceSubject = this.coversAtLeast(testRegion, *layerName) + component: FlickerComponentName? = null + ): LayersTraceSubject = this.coversAtLeast(Region(testRegion), component) /** * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of - * [layerName] covers at least [testRegion], that is, if its area of the layer's visible + * [component] covers at least [testRegion], that is, if its area of the layer's visible * region covers each point in the region. * * @param testRegion Expected covered area - * @param layerName Name of the layer to search + * @param component Name of the layer to search */ + @JvmOverloads fun coversAtLeast( testRegion: com.android.server.wm.traces.common.Rect, - vararg layerName: String - ): LayersTraceSubject = this.coversAtLeast(testRegion, *layerName) + component: FlickerComponentName? = null + ): LayersTraceSubject = this.coversAtLeast(testRegion.toAndroidRect(), component) /** * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of - * [layerName] covers at most [testRegion], that is, if the area of any layer doesn't + * [component] covers at most [testRegion], that is, if the area of any layer doesn't * cover any point outside of [testRegion]. * * @param testRegion Expected covered area - * @param layerName Name of the layer to search + * @param component Name of the layer to search */ + @JvmOverloads fun coversAtMost( testRegion: Rect, - vararg layerName: String - ): LayersTraceSubject = this.coversAtMost(testRegion, *layerName) + component: FlickerComponentName? = null + ): LayersTraceSubject = this.coversAtMost(Region(testRegion), component) /** * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of - * [layerName] covers at most [testRegion], that is, if the area of any layer doesn't + * [component] covers at most [testRegion], that is, if the area of any layer doesn't * cover any point outside of [testRegion]. * * @param testRegion Expected covered area - * @param layerName Name of the layer to search + * @param component Name of the layer to search */ + @JvmOverloads fun coversAtMost( testRegion: com.android.server.wm.traces.common.Rect, - vararg layerName: String - ): LayersTraceSubject = this.coversAtMost(testRegion, *layerName) + component: FlickerComponentName? = null + ): LayersTraceSubject = this.coversAtMost(testRegion.toAndroidRect(), component) /** * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of - * [layerName] covers at least [testRegion], that is, if its area of the layer's visible + * [component] covers at least [testRegion], that is, if its area of the layer's visible * region covers each point in the region. * * @param testRegion Expected covered area - * @param layerName Name of the layer to search + * @param component Name of the layer to search */ + @JvmOverloads fun coversAtLeast( testRegion: Region, - vararg layerName: String + component: FlickerComponentName? = null ): LayersTraceSubject = apply { - addAssertion("coversAtLeast($testRegion, ${layerName.joinToString(", ")})") { - it.visibleRegion(*layerName).coversAtLeast(testRegion) + addAssertion("coversAtLeast($testRegion, ${component?.toLayerName()})") { + it.visibleRegion(component).coversAtLeast(testRegion) } } /** * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of - * [layerName] covers at least [testRegion], that is, if its area of the layer's visible + * [component] covers at least [testRegion], that is, if its area of the layer's visible * region covers each point in the region. * * @param testRegion Expected covered area - * @param layerName Name of the layer to search + * @param component Name of the layer to search */ + @JvmOverloads fun coversAtLeast( testRegion: com.android.server.wm.traces.common.Region, - vararg layerName: String - ): LayersTraceSubject = apply { - addAssertion("coversAtLeast($testRegion, ${layerName.joinToString(", ")})") { - it.visibleRegion(*layerName).coversAtLeast(testRegion) - } - } + component: FlickerComponentName? = null + ): LayersTraceSubject = this.coversAtLeast(testRegion.toAndroidRegion(), component) /** * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of - * [layerName] covers at most [testRegion], that is, if the area of any layer doesn't + * [component] covers at most [testRegion], that is, if the area of any layer doesn't * cover any point outside of [testRegion]. * * @param testRegion Expected covered area - * @param layerName Name of the layer to search + * @param component Name of the layer to search */ + @JvmOverloads fun coversAtMost( testRegion: Region, - vararg layerName: String + component: FlickerComponentName? = null ): LayersTraceSubject = apply { - addAssertion("coversAtMost($testRegion, ${layerName.joinToString(", ")}") { - it.visibleRegion(*layerName).coversAtMost(testRegion) + addAssertion("coversAtMost($testRegion, ${component?.toLayerName()}") { + it.visibleRegion(component).coversAtMost(testRegion) } } /** * Asserts that the visible area covered by any [Layer] with [Layer.name] containing any of - * [layerName] covers at most [testRegion], that is, if the area of any layer doesn't + * [component] covers at most [testRegion], that is, if the area of any layer doesn't * cover any point outside of [testRegion]. * * @param testRegion Expected covered area - * @param layerName Name of the layer to search + * @param component Name of the layer to search */ + @JvmOverloads fun coversAtMost( testRegion: com.android.server.wm.traces.common.Region, - vararg layerName: String - ): LayersTraceSubject = apply { - addAssertion("coversAtMost($testRegion, ${layerName.joinToString(", ")}") { - it.visibleRegion(*layerName).coversAtMost(testRegion) - } - } + component: FlickerComponentName? = null + ): LayersTraceSubject = this.coversAtMost(testRegion.toAndroidRegion(), component) /** * Checks that all visible layers are shown for more than one consecutive entry */ @JvmOverloads fun visibleLayersShownMoreThanOneConsecutiveEntry( - ignoreLayers: List<String> = listOf(WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + ignoreLayers: List<FlickerComponentName> = listOf(FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT) ): LayersTraceSubject = apply { visibleEntriesShownMoreThanOneConsecutiveTime { subject -> subject.entry.visibleLayers - .filter { ignoreLayers.none { layerName -> layerName in it.name } } + .filter { ignoreLayers.none { component -> component.toLayerName() in it.name } } .map { it.name } .toSet() } } /** - * Asserts that a [Layer] with [Layer.name] containing any of [layerName] has a visible region + * Asserts that a [Layer] with [Layer.name] containing any of [component] has a visible region * of exactly [expectedVisibleRegion] in trace entries. * - * @param layerName Name of the layer to search + * @param component Name of the layer to search * @param expectedVisibleRegion Expected visible region of the layer */ + @JvmOverloads fun coversExactly( expectedVisibleRegion: Region, - vararg layerName: String + component: FlickerComponentName? = null ): LayersTraceSubject = apply { - addAssertion("coversExactly(${layerName.joinToString(", ")}$expectedVisibleRegion)") { - it.visibleRegion(*layerName).coversExactly(expectedVisibleRegion) + addAssertion("coversExactly($component$expectedVisibleRegion)") { + it.visibleRegion(component).coversExactly(expectedVisibleRegion) } } /** * Asserts that each entry in the trace doesn't contain a [Layer] with [Layer.name] - * containing [layerName]. + * containing [component]. * - * @param layerName Name of the layer to search + * @param component Name of the layer to search + * @param isOptional If this assertion is optional or must pass */ - fun notContains(vararg layerName: String): LayersTraceSubject = + @JvmOverloads + fun notContains( + component: FlickerComponentName, + isOptional: Boolean = false + ): LayersTraceSubject = apply { - addAssertion("notContains(${layerName.joinToString(", ")})") { - it.notContains(*layerName) + addAssertion("notContains(${component.toLayerName()})", isOptional) { + it.notContains(component) } } /** * Asserts that each entry in the trace contains a [Layer] with [Layer.name] containing any of - * [layerName]. + * [component]. * - * @param layerName Name of the layer to search + * @param component Name of the layer to search + * @param isOptional If this assertion is optional or must pass */ - fun contains(vararg layerName: String): LayersTraceSubject = - apply { addAssertion("contains(${layerName.joinToString(", ")})") { - it.contains(*layerName) } + @JvmOverloads + fun contains( + component: FlickerComponentName, + isOptional: Boolean = false + ): LayersTraceSubject = + apply { addAssertion("contains(${component.toLayerName()})", isOptional) { + it.contains(component) } } /** * Asserts that each entry in the trace contains a [Layer] with [Layer.name] containing any of - * [layerName] that is visible. + * [component] that is visible. * - * @param layerName Name of the layer to search + * @param component Name of the layer to search */ - fun isVisible(vararg layerName: String): LayersTraceSubject = - apply { addAssertion("isVisible(${layerName.joinToString(", ")})") { - it.isVisible(*layerName) } + @JvmOverloads + fun isVisible( + component: FlickerComponentName, + isOptional: Boolean = false + ): LayersTraceSubject = + apply { addAssertion("isVisible(${component.toLayerName()})", isOptional) { + it.isVisible(component) } } /** * Asserts that each entry in the trace doesn't contain a [Layer] with [Layer.name] - * containing [layerName] or that the layer is not visible . + * containing [component] or that the layer is not visible . * - * @param layerName Name of the layer to search + * @param component Name of the layer to search */ - fun isInvisible(vararg layerName: String): LayersTraceSubject = + @JvmOverloads + fun isInvisible( + component: FlickerComponentName, + isOptional: Boolean = false + ): LayersTraceSubject = apply { - addAssertion("hidesLayer(${layerName.joinToString(", ")})") { - it.isInvisible(*layerName) + addAssertion("isInvisible(${component.toLayerName()})", isOptional) { + it.isInvisible(component) } } @@ -321,8 +349,9 @@ class LayersTraceSubject private constructor( */ operator fun invoke( name: String, + isOptional: Boolean = false, assertion: Assertion<LayerTraceEntrySubject> - ): LayersTraceSubject = apply { addAssertion(name, assertion) } + ): LayersTraceSubject = apply { addAssertion(name, isOptional, assertion) } fun hasFrameSequence(name: String, frameNumbers: Iterable<Long>): LayersTraceSubject = apply { val firstFrame = frameNumbers.first() @@ -370,8 +399,8 @@ class LayersTraceSubject private constructor( /** * Boiler-plate Subject.Factory for LayersTraceSubject */ - private val FACTORY: Factory<Subject, LayersTrace> = - Factory { fm, subject -> LayersTraceSubject(fm, subject) } + private fun getFactory(parent: LayersTraceSubject?): Factory<Subject, LayersTrace> = + Factory { fm, subject -> LayersTraceSubject(fm, subject, parent) } /** * Creates a [LayersTraceSubject] to representing a SurfaceFlinger trace, @@ -380,10 +409,11 @@ class LayersTraceSubject private constructor( * @param trace SurfaceFlinger trace */ @JvmStatic - fun assertThat(trace: LayersTrace): LayersTraceSubject { + @JvmOverloads + fun assertThat(trace: LayersTrace, parent: LayersTraceSubject? = null): LayersTraceSubject { val strategy = FlickerFailureStrategy() val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy) - .about(FACTORY) + .about(getFactory(parent)) .that(trace) as LayersTraceSubject strategy.init(subject) return subject @@ -393,8 +423,8 @@ class LayersTraceSubject private constructor( * Static method for getting the subject factory (for use with assertAbout()) */ @JvmStatic - fun entries(): Factory<Subject, LayersTrace> { - return FACTORY + fun entries(parent: LayersTraceSubject?): Factory<Subject, LayersTrace> { + return getFactory(parent) } } } diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerStateSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerStateSubject.kt index ea642ec2a..b7a55dde8 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerStateSubject.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerStateSubject.kt @@ -16,19 +16,18 @@ package com.android.server.wm.flicker.traces.windowmanager -import android.content.ComponentName import android.view.Display +import androidx.annotation.VisibleForTesting import com.android.server.wm.flicker.assertions.Assertion import com.android.server.wm.flicker.assertions.FlickerSubject -import com.android.server.wm.flicker.containsAny import com.android.server.wm.flicker.traces.FlickerFailureStrategy import com.android.server.wm.flicker.traces.RegionSubject +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.common.Region import com.android.server.wm.traces.common.windowmanager.WindowManagerState import com.android.server.wm.traces.common.windowmanager.windows.Activity import com.android.server.wm.traces.common.windowmanager.windows.WindowState -import com.android.server.wm.traces.parser.toActivityName import com.android.server.wm.traces.parser.toAndroidRegion -import com.android.server.wm.traces.parser.toWindowName import com.google.common.truth.Fact import com.google.common.truth.FailureMetadata import com.google.common.truth.FailureStrategy @@ -61,14 +60,31 @@ import com.google.common.truth.Subject.Factory class WindowManagerStateSubject private constructor( fm: FailureMetadata, val wmState: WindowManagerState, - val trace: WindowManagerTraceSubject? + val trace: WindowManagerTraceSubject?, + override val parent: FlickerSubject? ) : FlickerSubject(fm, wmState) { - override val defaultFacts = "${trace?.defaultFacts ?: ""}\nEntry: $wmState" + override val timestamp: Long get() = wmState.timestamp + override val selfFacts = listOf(Fact.fact("Entry", wmState)) val subjects by lazy { - wmState.windowStates.map { WindowStateSubject.assertThat(it, this) } + wmState.windowStates.map { WindowStateSubject.assertThat(it, this, timestamp) } } + val appWindows: List<WindowStateSubject> + get() = subjects.filter { wmState.appWindows.contains(it.windowState) } + + val nonAppWindows: List<WindowStateSubject> + get() = subjects.filter { wmState.nonAppWindows.contains(it.windowState) } + + val aboveAppWindows: List<WindowStateSubject> + get() = subjects.filter { wmState.aboveAppWindows.contains(it.windowState) } + + val belowAppWindows: List<WindowStateSubject> + get() = subjects.filter { wmState.belowAppWindows.contains(it.windowState) } + + val visibleWindows: List<WindowStateSubject> + get() = subjects.filter { wmState.visibleWindows.contains(it.windowState) } + /** * Executes a custom [assertion] on the current subject */ @@ -77,265 +93,188 @@ class WindowManagerStateSubject private constructor( /** {@inheritDoc} */ override fun clone(): FlickerSubject { - return WindowManagerStateSubject(fm, wmState, trace) + return WindowManagerStateSubject(fm, wmState, trace, parent) } /** - * Asserts that the current WindowManager state doesn't contain [WindowState]s + * Asserts the current WindowManager state doesn't contain [WindowState]s */ fun isEmpty(): WindowManagerStateSubject = apply { - check("State is empty") - .that(wmState.windowStates) - .isEmpty() + check("State is empty").that(subjects).isEmpty() } /** - * Asserts that the current WindowManager state contains [WindowState]s + * Asserts the current WindowManager state contains [WindowState]s */ fun isNotEmpty(): WindowManagerStateSubject = apply { - check("State is not empty") - .that(wmState.windowStates) - .isNotEmpty() + check("State is not empty").that(subjects).isNotEmpty() } /** - * Obtains the region occupied by all windows with name containing any of [partialWindowTitles] + * Obtains the region occupied by all windows with name containing any of [component] * - * @param partialWindowTitles Name of the layer to search + * @param component Component to search */ - fun frameRegion(vararg partialWindowTitles: String): RegionSubject { - val selectedWindows = subjects.filter { it.name.containsAny(*partialWindowTitles) } + fun frameRegion(component: FlickerComponentName?): RegionSubject { + val windowName = component?.toWindowName() ?: "" + val selectedWindows = subjects.filter { it.name.contains(windowName) } if (selectedWindows.isEmpty()) { - fail("Could not find", selectedWindows.joinToString(", ")) + fail(Fact.fact(ASSERTION_TAG, "frameRegion(${component?.toWindowName() ?: "<any>"})"), + Fact.fact("Could not find", windowName)) } val visibleWindows = selectedWindows.filter { it.isVisible } val frameRegions = visibleWindows.mapNotNull { it.windowState?.frameRegion }.toTypedArray() - return RegionSubject.assertThat(frameRegions, selectedWindows) - } - - /** - * Asserts that the WindowManager state contains a [WindowState] with [WindowState.title] - * containing any of [partialWindowTitles]. - * - * @param partialWindowTitles window titles to search to search - */ - fun contains(vararg partialWindowTitles: String): WindowManagerStateSubject = apply { - val found = if (partialWindowTitles.isNotEmpty()) { - wmState.windowStates.any { it.name.containsAny(*partialWindowTitles) } - } else { - wmState.windowStates.isNotEmpty() - } - - if (!found) { - fail("Could not find", partialWindowTitles.joinToString(", ")) - } - } - - /** - * Asserts that the WindowManager state doesn't contain a [WindowState] with - * [WindowState.title] containing [partialWindowTitles]. - * - * @param partialWindowTitles Title of the window to search - */ - fun notContains(vararg partialWindowTitles: String): WindowManagerStateSubject = apply { - val found = wmState.windowStates.none { it.name.containsAny(*partialWindowTitles) } - if (!found) { - fail("Could find", partialWindowTitles.joinToString(", ")) - } - } - - /** - * Asserts that a [WindowState] with [WindowState.title] containing [partialWindowTitles] is visible. - * - * @param partialWindowTitles Title of the window to search - */ - fun isVisible(vararg partialWindowTitles: String): WindowManagerStateSubject = apply { - wmState.windowStates.checkVisibility(*partialWindowTitles, isVisible = true) - } - - /** - * Asserts that a [WindowState] with [WindowState.title] containing [partialWindowTitles] doesn't - * exist or is invisible. - * - * @param partialWindowTitles Title of the window to search - */ - fun isInvisible(vararg partialWindowTitles: String): WindowManagerStateSubject = apply { - wmState.windowStates.checkVisibility(*partialWindowTitles, isVisible = false) - } - - private fun Array<WindowState>.checkIsVisible(vararg partialWindowTitles: String) { - this@WindowManagerStateSubject.contains(*partialWindowTitles) - val visibleWindows = this.filter { it.isVisible } - .filter { it.name.containsAny(*partialWindowTitles) } - - if (visibleWindows.isEmpty()) { - fail("Is Invisible", partialWindowTitles.joinToString(", ")) - } - } - - private fun Array<WindowState>.checkIsInvisible(vararg partialWindowTitles: String) { - try { - notContains(*partialWindowTitles) - } catch (e: AssertionError) { - val invisibleWindows = this.filterNot { it.isVisible } - .filter { it.name.containsAny(*partialWindowTitles) } - if (invisibleWindows.isEmpty()) { - fail("Is Visible", partialWindowTitles.joinToString(", ")) - } - } - } - - private fun Array<WindowState>.checkVisibility( - vararg partialWindowTitles: String, - isVisible: Boolean - ) { - if (isVisible) { - checkIsVisible(*partialWindowTitles) - } else { - checkIsInvisible(*partialWindowTitles) - } + return RegionSubject.assertThat(frameRegions, this) } /** - * Asserts that the non-app window ([WindowManagerState.nonAppWindows]) with title - * containing [partialWindowTitles] exists, is above all app windows ([WindowManagerState.appWindows]) - * and has a visibility equal to [isVisible] - * - * This assertion can be used, for example, to assert that the Status and Navigation bars - * are visible and shown above the app + * Asserts the state contains a [WindowState] with title matching [component] above the + * app windows * - * @param partialWindowTitles window title to search - * @param isVisible if the found window should be visible or not + * @param component Component to search */ - @JvmOverloads - fun isAboveAppWindow( - vararg partialWindowTitles: String, - isVisible: Boolean = true - ): WindowManagerStateSubject = apply { - wmState.aboveAppWindows.checkVisibility(*partialWindowTitles, isVisible = isVisible) + fun containsAboveAppWindow(component: FlickerComponentName): WindowManagerStateSubject = apply { + contains(aboveAppWindows, component) } /** - * Asserts that the non-app window ([WindowManagerState.nonAppWindows]) with title - * containing [partialWindowTitles] exists, is below all app windows ([WindowManagerState.appWindows]) - * and has a visibility equal to [isVisible] + * Asserts the state contains a [WindowState] with title matching [component] below the + * app windows * - * This assertion can be used, for example, to assert that the wallpaper is visible and - * shown below the app - * - * @param partialWindowTitles window title to search - * @param isVisible if the found window should be visible or not + * @param component Component to search */ - @JvmOverloads - fun isBelowAppWindow( - vararg partialWindowTitles: String, - isVisible: Boolean = true - ): WindowManagerStateSubject = apply { - wmState.belowAppWindows.checkVisibility(*partialWindowTitles, isVisible = isVisible) + fun containsBelowAppWindow(component: FlickerComponentName): WindowManagerStateSubject = apply { + contains(belowAppWindows, component) } /** - * Asserts that a window A with title containing [aboveWindowTitle] exists, - * a window B with title containing [belowWindowTitle] also exists, and that - * A is shown above B. + * Asserts the state contains [WindowState]s with titles matching [aboveWindowComponent] and + * [belowWindowComponent], and that [aboveWindowComponent] is above [belowWindowComponent] * * This assertion can be used, for example, to assert that a PIP window is shown above * other apps. * - * @param aboveWindowTitle name of the window that should be above - * @param belowWindowTitle name of the window that should be below + * @param aboveWindowComponent name of the window that should be above + * @param belowWindowComponent name of the window that should be below */ - fun isAboveWindow(aboveWindowTitle: String, belowWindowTitle: String) { + fun isAboveWindow( + aboveWindowComponent: FlickerComponentName, + belowWindowComponent: FlickerComponentName + ): WindowManagerStateSubject = apply { + contains(aboveWindowComponent) + contains(belowWindowComponent) + // windows are ordered by z-order, from top to bottom + val aboveWindowTitle = aboveWindowComponent.toWindowName() + val belowWindowTitle = belowWindowComponent.toWindowName() val aboveZ = wmState.windowStates.indexOfFirst { aboveWindowTitle in it.name } val belowZ = wmState.windowStates.indexOfFirst { belowWindowTitle in it.name } - - contains(aboveWindowTitle) - contains(belowWindowTitle) if (aboveZ >= belowZ) { - fail("$aboveWindowTitle is above $belowWindowTitle") + val aboveWindow = subjects.first { aboveWindowTitle in it.name } + aboveWindow.fail(Fact.fact(ASSERTION_TAG, "isAboveWindow(above=$aboveWindowTitle, " + + "below=$belowWindowTitle"), + Fact.fact("Above", aboveWindowTitle), + Fact.fact("Below", belowWindowTitle)) } } /** - * Asserts that the WindowManager state contains a non-app [WindowState] with - * [WindowState.title] containing [partialWindowTitles] and that its visibility is - * equal to [isVisible] + * Asserts the state contains a non-app [WindowState] with title matching [component] * - * @param partialWindowTitles window title to search - * @param isVisible if the found window should be visible or not + * @param component Component to search */ - @JvmOverloads - fun containsNonAppWindow( - vararg partialWindowTitles: String, - isVisible: Boolean = true - ): WindowManagerStateSubject = apply { - wmState.nonAppWindows.checkVisibility(*partialWindowTitles, isVisible = isVisible) + fun containsNonAppWindow(component: FlickerComponentName): WindowManagerStateSubject = apply { + contains(nonAppWindows, component) } /** - * Asserts that the title of the top visible app window in the state contains any - * of [partialWindowTitles] + * Asserts the title of the top visible app window in the state contains [component] * - * @param partialWindowTitles window title to search - */ - fun showsAppWindowOnTop(vararg partialWindowTitles: String): WindowManagerStateSubject = apply { - contains(*partialWindowTitles) - val windowOnTop = wmState.topVisibleAppWindow.containsAny(*partialWindowTitles) + * @param component Component to search + */ + fun isAppWindowOnTop(component: FlickerComponentName): WindowManagerStateSubject = apply { + val windowName = component.toWindowName() + if (!wmState.topVisibleAppWindow.contains(windowName)) { + val topWindow = subjects.first { it.name == wmState.topVisibleAppWindow } + topWindow.fail( + Fact.fact(ASSERTION_TAG, "isAppWindowOnTop(${component.toWindowName()})"), + Fact.fact("Not on top", component.toWindowName()), + Fact.fact("Found", wmState.topVisibleAppWindow) + ) + } + } - if (!windowOnTop) { - fail(Fact.fact("Not on top", partialWindowTitles.joinToString(", ")), - Fact.fact("Found", wmState.topVisibleAppWindow)) + /** + * Asserts the title of the top visible app window in the state contains [component] + * + * @param component Component to search + */ + fun isAppWindowNotOnTop(component: FlickerComponentName): WindowManagerStateSubject = apply { + val windowName = component.toWindowName() + if (wmState.topVisibleAppWindow.contains(windowName)) { + val topWindow = subjects.first { it.name == wmState.topVisibleAppWindow } + topWindow.fail( + Fact.fact(ASSERTION_TAG, "isAppWindowNotOnTop(${component.toWindowName()})"), + Fact.fact("On top", component.toWindowName()) + ) } } /** - * Asserts that the [WindowState.bounds] of the [WindowState] with [WindowState.title] - * contained in any of [partialWindowTitles] don't overlap. + * Asserts the bounds of the [WindowState]s title matching [component] don't overlap. * - * @param partialWindowTitles Title of the windows that should not overlap + * @param component Component to search */ - fun noWindowsOverlap(vararg partialWindowTitles: String): WindowManagerStateSubject = apply { - partialWindowTitles.forEach { contains(it) } - val foundWindows = partialWindowTitles.toSet() - .associateWith { title -> wmState.windowStates.find { it.name.contains(title) } } + fun doNotOverlap( + vararg component: FlickerComponentName + ): WindowManagerStateSubject = apply { + component.forEach { contains(it) } + val foundWindows = component.toSet() + .associateWith { act -> + wmState.windowStates.firstOrNull { it.name.contains(act.toWindowName()) } + } // keep entries only for windows that we actually found by removing nulls .filterValues { it != null } - .mapValues { (_, v) -> v!!.frameRegion } + val foundWindowsRegions = foundWindows + .mapValues { (_, v) -> v?.frameRegion ?: Region.EMPTY } - val regions = foundWindows.entries.toList() + val regions = foundWindowsRegions.entries.toList() for (i in regions.indices) { val (ourTitle, ourRegion) = regions[i] for (j in i + 1 until regions.size) { val (otherTitle, otherRegion) = regions[j] if (ourRegion.toAndroidRegion().op(otherRegion.toAndroidRegion(), android.graphics.Region.Op.INTERSECT)) { - fail(Fact.fact("Overlap", ourTitle), Fact.fact("Overlap", otherTitle)) + val window = foundWindows[ourTitle] ?: error("Window $ourTitle not found") + val windowSubject = subjects.first { it.windowState == window } + windowSubject.fail(Fact.fact(ASSERTION_TAG, + "noWindowsOverlap${component.joinToString { it.toWindowName() }}"), + Fact.fact("Overlap", ourTitle), + Fact.fact("Overlap", otherTitle)) } } } } /** - * Asserts that the WindowManager state contains an app [WindowState] with - * [WindowState.title] containing [partialWindowTitles] and that its visibility - * is equal to [isVisible] + * Asserts the state contains an app [WindowState] with title matching [component] * - * @param partialWindowTitles window title to search - * @param isVisible if the found window should be visible or not + * @param component Component to search */ - @JvmOverloads - fun containsAppWindow( - vararg partialWindowTitles: String, - isVisible: Boolean = true - ): WindowManagerStateSubject = apply { - wmState.appWindows.checkVisibility(*partialWindowTitles, isVisible = isVisible) + fun containsAppWindow(component: FlickerComponentName): WindowManagerStateSubject = apply { + val windowName = component.toWindowName() + // Check existence of activity + val activity = wmState.getActivitiesForWindow(windowName).firstOrNull() + check("Activity for window $windowName must exist.") + .that(activity).isNotNull() + // Check existence of window. + contains(component) } /** - * Asserts that the display with id [displayId] has rotation [rotation] + * Asserts the display with id [displayId] has rotation [rotation] * * @param rotation to assert * @param displayId of the target display @@ -351,72 +290,69 @@ class WindowManagerStateSubject private constructor( } /** - * Asserts that the display with id [displayId] has rotation [rotation] + * Asserts the state contains a [WindowState] with title matching [component]. * - * @param rotation to assert - * @param displayId of the target display + * @param component Component name to search */ - @JvmOverloads - fun isNotRotation( - rotation: Int, - displayId: Int = Display.DEFAULT_DISPLAY - ): WindowManagerStateSubject = apply { - check("Rotation should not be $rotation") - .that(rotation) - .isNotEqualTo(wmState.getRotation(displayId)) + fun contains(component: FlickerComponentName): WindowManagerStateSubject = apply { + contains(subjects, component) } /** - * Asserts that the WindowManager state contains a [WindowState] with [WindowState.title] - * equal to [ComponentName.toWindowName] and an [Activity] with [Activity.title] equal to - * [ComponentName.toActivityName] + * Asserts the state doesn't contain a [WindowState] nor an [Activity] with title + * matching [component]. * - * @param activity Component name to search + * @param component Component name to search */ - fun contains(activity: ComponentName): WindowManagerStateSubject = apply { - val windowName = activity.toWindowName() - val activityName = activity.toActivityName() - check("Activity=$activityName must exist.") - .that(wmState.containsActivity(activityName)).isTrue() - check("Window=$windowName must exits.") - .that(wmState.containsWindow(windowName)).isTrue() + fun notContainsAppWindow(component: FlickerComponentName): WindowManagerStateSubject = apply { + val activityName = component.toActivityName() + // system components (e.g., NavBar, StatusBar, PipOverlay) don't have a package name + // nor an activity, ignore them + check("Activity=$activityName must NOT exist.") + .that(wmState.containsActivity(activityName)).isFalse() + notContains(component) } /** - * Asserts that the WindowManager state doesn't contain a [WindowState] with [WindowState.title] - * equal to [ComponentName.toWindowName] nor an [Activity] with [Activity.title] equal to - * [ComponentName.toActivityName] + * Asserts the state doesn't contain a [WindowState] with title matching [component]. * - * @param activity Component name to search + * @param component Component name to search */ - fun notContains(activity: ComponentName): WindowManagerStateSubject = apply { - val windowName = activity.toWindowName() - val activityName = activity.toActivityName() - check("Activity=$activityName must NOT exist.") - .that(wmState.containsActivity(activityName)).isFalse() + fun notContains(component: FlickerComponentName): WindowManagerStateSubject = apply { + val windowName = component.toWindowName() check("Window=$windowName must NOT exits.") .that(wmState.containsWindow(windowName)).isFalse() } - @JvmOverloads - fun isRecentsActivityVisible(visible: Boolean = true): WindowManagerStateSubject = apply { + fun isRecentsActivityVisible(): WindowManagerStateSubject = apply { if (wmState.isHomeRecentsComponent) { isHomeActivityVisible() } else { - check("Recents activity is ${if (visible) "" else "not"} visible") + check("Recents activity visibility") .that(wmState.isRecentsActivityVisible) - .isEqualTo(visible) + .isTrue() + } + } + + fun isRecentsActivityInvisible(): WindowManagerStateSubject = apply { + if (wmState.isHomeRecentsComponent) { + isHomeActivityInvisible() + } else { + check("Recents activity visibility") + .that(wmState.isRecentsActivityVisible) + .isFalse() } } /** - * Asserts that the WindowManager state is valid, that is, if it has: + * Asserts the state is valid, that is, if it has: * - a resumed activity * - a focused activity * - a focused window * - a front window * - a focused app */ + @VisibleForTesting fun isValid(): WindowManagerStateSubject = apply { check("Must have stacks").that(wmState.stackCount).isGreaterThan(0) // TODO: Update when keyguard will be shown on multiple displays @@ -442,229 +378,171 @@ class WindowManagerStateSubject private constructor( } /** - * Asserts that the [WindowManagerState.focusedActivity] and [WindowManagerState.focusedApp] - * match [activity] + * Asserts the state contains a visible window with [WindowState.title] matching [component]. * - * @param activity Component name to search - */ - fun hasFocusedActivity(activity: ComponentName): WindowManagerStateSubject = apply { - val activityComponentName = activity.toActivityName() - check("Focused activity invalid") - .that(activityComponentName) - .isEqualTo(wmState.focusedActivity) - check("Focused app invalid") - .that(activityComponentName) - .isEqualTo(wmState.focusedApp) - } - - /** - * Asserts that the [WindowManagerState.focusedActivity] and [WindowManagerState.focusedApp] - * don't match [activity] + * Also, if [component] has a package name (i.e., is not a system component), also checks that + * it contains a visible [Activity] with [Activity.title] matching [component]. * - * @param activity Component name to search + * @param component Component name to search */ - fun hasNotFocusedActivity(activity: ComponentName): WindowManagerStateSubject = apply { - val activityComponentName = activity.toActivityName() - check("Has focused activity") - .that(wmState.focusedActivity) - .isNotEqualTo(activityComponentName) - check("Has focused app") - .that(wmState.focusedApp) - .isNotEqualTo(activityComponentName) + fun isNonAppWindowVisible(component: FlickerComponentName): WindowManagerStateSubject = apply { + checkWindowVisibility("isVisible", nonAppWindows, component, isVisible = true) } /** - * Asserts that the display [displayId] has a [WindowManagerState.focusedApp] - * matching [activity] + * Asserts the state contains a visible window with [WindowState.title] matching [component]. * - * @param activity Component name to search + * Also, if [component] has a package name (i.e., is not a system component), also checks that + * it contains a visible [Activity] with [Activity.title] matching [component]. + * + * @param component Component name to search */ - @JvmOverloads - fun hasFocusedApp( - activity: ComponentName, - displayId: Int = Display.DEFAULT_DISPLAY + fun isAppWindowVisible( + component: FlickerComponentName ): WindowManagerStateSubject = apply { - val activityComponentName = activity.toActivityName() - check("Focused app invalid") - .that(activityComponentName) - .isEqualTo(wmState.getDisplay(displayId)?.focusedApp) - } + containsAppWindow(component) - /** - * Asserts that WindowManager state has a [WindowManagerState.resumedActivities] - * matching [activity] - * - * @param activity Component name to search - */ - fun hasResumedActivity(activity: ComponentName): WindowManagerStateSubject = apply { - val activityComponentName = activity.toActivityName() - check("Invalid resumed activity") - .that(wmState.resumedActivities) - .asList() - .contains(activityComponentName) + val windowName = component.toWindowName() + // Check existence of activity + val activity = wmState.getActivitiesForWindow(windowName).firstOrNull() + // Check visibility of activity and window. + check("Activity=${activity?.name} must be visible.") + .that(activity?.isVisible ?: false).isTrue() + checkWindowVisibility("isVisible", appWindows, component, isVisible = true) } /** - * Asserts that WindowManager state [WindowManagerState.resumedActivities] doesn't - * match [activity] + * Asserts the state contains an invisible window with [WindowState.title] matching [component]. * - * @param activity Component name to search - */ - fun hasNotResumedActivity(activity: ComponentName): WindowManagerStateSubject = apply { - val activityComponentName = activity.toActivityName() - check("Has resumed activity") - .that(wmState.resumedActivities) - .asList() - .doesNotContain(activityComponentName) - } - - /** - * Asserts that title of the [WindowManagerState.focusedWindow] on the state matches - * [windowTitle] + * Also, if [component] has a package name (i.e., is not a system component), also checks that + * it contains an invisible [Activity] with [Activity.title] matching [component]. * - * @param windowTitle window title to search + * @param component Component name to search */ - fun isFocused(windowTitle: String): WindowManagerStateSubject = apply { - check("Invalid focused window") - .that(windowTitle) - .isEqualTo(wmState.focusedWindow) - } + fun isAppWindowInvisible( + component: FlickerComponentName + ): WindowManagerStateSubject = apply { + val activityName = component.toActivityName() - /** - * Asserts that [WindowManagerState.focusedWindow] on the WindowManager state doesn't - * match [windowTitle] - * - * @param windowTitle window title to search - */ - fun isWindowNotFocused(windowTitle: String): WindowManagerStateSubject = apply { - check("Has focused window") - .that(wmState.focusedWindow) - .isNotEqualTo(windowTitle) + // system components (e.g., NavBar, StatusBar, PipOverlay) don't have a package name + // nor an activity, ignore them + // activity is visible, check window + if (wmState.isActivityVisible(activityName)) { + checkWindowVisibility("isInvisible", appWindows, component, isVisible = false) + } } /** - * Asserts that the WindowManager state contains a [WindowState] with [WindowState.title] - * equal to [ComponentName.toWindowName] and an [Activity] with [Activity.title] equal to - * [ComponentName.toActivityName] and both are visible + * Asserts the state contains an invisible window with [WindowState.title] matching [component]. * - * @param activity Component name to search - */ - fun isVisible(activity: ComponentName): WindowManagerStateSubject = - hasActivityAndWindowVisibility(activity, visible = true) - - /** - * Asserts that the WindowManager state contains a [WindowState] with [WindowState.title] - * equal to [ComponentName.toWindowName] and an [Activity] with [Activity.title] equal to - * [ComponentName.toActivityName] and both are invisible + * Also, if [component] has a package name (i.e., is not a system component), also checks that + * it contains an invisible [Activity] with [Activity.title] matching [component]. * - * @param activity Component name to search + * @param component Component name to search */ - fun isInvisible(activity: ComponentName): WindowManagerStateSubject = - hasActivityAndWindowVisibility(activity, visible = false) - - private fun hasActivityAndWindowVisibility( - activity: ComponentName, - visible: Boolean + fun isNonAppWindowInvisible( + component: FlickerComponentName ): WindowManagerStateSubject = apply { - // Check existence of activity and window. - val windowName = activity.toWindowName() - val activityName = activity.toActivityName() - check("Activity=$activityName must exist.") - .that(wmState.containsActivity(activityName)).isTrue() + checkWindowVisibility("isInvisible", nonAppWindows, component, isVisible = false) + } + + private fun checkWindowVisibility( + assertionName: String, + subjectList: List<WindowStateSubject>, + component: FlickerComponentName, + isVisible: Boolean + ) { + // Check existence of window. + contains(subjectList, component) + + val windowName = component.toWindowName() + val foundWindows = subjectList.filter { it.name.contains(windowName) } + val windowsWithVisibility = foundWindows.filter { it.isVisible == isVisible } + + if (windowsWithVisibility.isEmpty()) { + val errorTag = if (isVisible) "Is Invisible" else "Is Visible" + val facts = listOf<Fact>( + Fact.fact(ASSERTION_TAG, "$assertionName(${component.toWindowName()})"), + Fact.fact(errorTag, windowName) + ) + foundWindows.first().fail(facts) + } + } + + private fun contains(subjectList: List<WindowStateSubject>, component: FlickerComponentName) { + val windowName = component.toWindowName() check("Window=$windowName must exist.") .that(wmState.containsWindow(windowName)).isTrue() - - // Check visibility of activity and window. - check("Activity=$activityName must ${if (visible) "" else " NOT"} be visible.") - .that(visible).isEqualTo(wmState.isActivityVisible(activityName)) - check("Window=$windowName must ${if (visible) "" else " NOT"} have shown surface.") - .that(visible).isEqualTo(wmState.isWindowSurfaceShown(windowName)) } /** - * Asserts that the WindowManager state home activity visibility is equal to [isVisible] - * - * @param isVisible if the home activity should be visible of not + * Asserts the state home activity is visible */ - @JvmOverloads - fun isHomeActivityVisible(isVisible: Boolean = true): WindowManagerStateSubject = apply { - if (isVisible) { - check("Home activity doesn't exist") - .that(wmState.homeActivity) - .isNotNull() - - check("Home activity is not visible") - .that(wmState.homeActivity?.isVisible) - .isTrue() - } else { - check("Home activity is visible") - .that(wmState.homeActivity?.isVisible ?: false) - .isFalse() - } + fun isHomeActivityVisible(): WindowManagerStateSubject = apply { + val homeIsVisible = wmState.homeActivity?.isVisible ?: false + check("Home activity doesn't exist").that(wmState.homeActivity).isNotNull() + check("Home activity is not visible").that(homeIsVisible).isTrue() } /** - * Asserts that the IME surface is visible in the display [displayId] + * Asserts the state home activity is invisible */ - @JvmOverloads - fun isImeWindowVisible( - displayId: Int = Display.DEFAULT_DISPLAY - ): WindowManagerStateSubject = apply { - val imeWinState = wmState.inputMethodWindowState - check("IME window must exist") - .that(imeWinState).isNotNull() - check("IME window must be shown") - .that(imeWinState?.isSurfaceShown ?: false).isTrue() - check("IME window must be on the given display") - .that(displayId).isEqualTo(imeWinState?.displayId ?: -1) + fun isHomeActivityInvisible(): WindowManagerStateSubject = apply { + val homeIsVisible = wmState.homeActivity?.isVisible ?: false + check("Home activity is visible").that(homeIsVisible).isFalse() } /** - * Asserts that the IME surface is invisible in the display [displayId] + * Asserts that [component] exists and is pinned (in PIP mode) + * + * @param component Component name to search */ - @JvmOverloads - fun isImeWindowInvisible( - displayId: Int = Display.DEFAULT_DISPLAY - ): WindowManagerStateSubject = apply { - val imeWinState = wmState.inputMethodWindowState - check("IME window must not be shown") - .that(imeWinState?.isSurfaceShown ?: false).isFalse() - if (imeWinState?.isSurfaceShown == true) { - check("IME window must not be on the given display") - .that(displayId).isNotEqualTo(imeWinState.displayId) - } + fun isPinned(component: FlickerComponentName): WindowManagerStateSubject = apply { + contains(component) + val windowName = component.toWindowName() + val pinnedWindows = wmState.pinnedWindows.map { it.title } + check("Window not in PIP mode").that(pinnedWindows).contains(windowName) } /** - * Asserts that an activity [activity] exists and is in PIP mode + * Asserts that [component] exists and is not pinned (not in PIP mode) + * + * @param component Component name to search */ - fun isInPipMode( - activity: ComponentName - ): WindowManagerStateSubject = apply { - val windowName = activity.toWindowName() - contains(windowName) - val pinnedWindows = wmState.pinnedWindows - .map { it.title } - check("Window not in PIP mode") - .that(pinnedWindows) - .contains(windowName) + fun isNotPinned(component: FlickerComponentName): WindowManagerStateSubject = apply { + contains(component) + val windowName = component.toWindowName() + val pinnedWindows = wmState.pinnedWindows.map { it.title } + check("Window not in PIP mode").that(pinnedWindows).doesNotContain(windowName) } /** - * Obtains a [WindowStateSubject] for the first occurrence of a [WindowState] with - * [WindowState.title] containing [name]. + * Obtains the first subject with [WindowState.title] containing [name]. * * Always returns a subject, event when the layer doesn't exist. To verify if layer * actually exists in the hierarchy use [WindowStateSubject.exists] or * [WindowStateSubject.doesNotExist] - * - * @return WindowStateSubject that can be used to make assertions on a single [WindowState] - * matching [name]. */ fun windowState(name: String): WindowStateSubject { return subjects.firstOrNull { it.windowState?.name?.contains(name) == true - } ?: WindowStateSubject.assertThat(name, this) + } ?: WindowStateSubject.assertThat(name, this, timestamp) + } + + /** + * Obtains the first subject matching [predicate]. + * + * Always returns a subject, event when the layer doesn't exist. To verify if layer + * actually exists in the hierarchy use [WindowStateSubject.exists] or + * [WindowStateSubject.doesNotExist] + * + * @param predicate to search for a subject + * @param name Name of the subject to use when not found (optional) + */ + fun windowState(name: String = "", predicate: (WindowState) -> Boolean): WindowStateSubject { + return subjects.firstOrNull { + it.windowState?.run { predicate(this) } ?: false + } ?: WindowStateSubject.assertThat(name, this, timestamp) } override fun toString(): String { @@ -675,28 +553,30 @@ class WindowManagerStateSubject private constructor( /** * Boiler-plate Subject.Factory for WindowManagerStateSubject * - * @param trace containing the entry + * @param parent containing the entry */ private fun getFactory( - trace: WindowManagerTraceSubject? = null + trace: WindowManagerTraceSubject?, + parent: FlickerSubject? ): Factory<Subject, WindowManagerState> = - Factory { fm, subject -> WindowManagerStateSubject(fm, subject, trace) } + Factory { fm, subject -> WindowManagerStateSubject(fm, subject, trace, parent) } /** * User-defined entry point * * @param entry to assert - * @param trace containing the entry + * @param parent containing the entry */ @JvmStatic @JvmOverloads fun assertThat( entry: WindowManagerState, - trace: WindowManagerTraceSubject? = null + trace: WindowManagerTraceSubject? = null, + parent: FlickerSubject? = null ): WindowManagerStateSubject { val strategy = FlickerFailureStrategy() val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy) - .about(getFactory(trace)) + .about(getFactory(trace, parent)) .that(entry) as WindowManagerStateSubject strategy.init(subject) return subject diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerTraceSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerTraceSubject.kt index c790f970a..ed77b20de 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerTraceSubject.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowManagerTraceSubject.kt @@ -20,11 +20,12 @@ import com.android.server.wm.flicker.assertions.Assertion import com.android.server.wm.flicker.assertions.FlickerSubject import com.android.server.wm.flicker.traces.FlickerFailureStrategy import com.android.server.wm.flicker.traces.FlickerTraceSubject +import com.android.server.wm.traces.common.FlickerComponentName import com.android.server.wm.traces.common.Rect import com.android.server.wm.traces.common.Region import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace import com.android.server.wm.traces.common.windowmanager.windows.WindowState -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.google.common.truth.Fact import com.google.common.truth.FailureMetadata import com.google.common.truth.FailureStrategy import com.google.common.truth.StandardSubjectBuilder @@ -55,46 +56,32 @@ import com.google.common.truth.Subject */ class WindowManagerTraceSubject private constructor( fm: FailureMetadata, - val trace: WindowManagerTrace + val trace: WindowManagerTrace, + override val parent: WindowManagerTraceSubject? ) : FlickerTraceSubject<WindowManagerStateSubject>(fm, trace) { - override val defaultFacts: String = buildString { - if (trace.hasSource()) { - append("Path: ${trace.source}") - append("\n") - } - append("Trace: $trace") - } + override val selfFacts + get() = super.selfFacts.toMutableList() + .also { + if (trace.hasSource()) { + it.add(Fact.fact("Trace file", trace.source)) + } + } override val subjects by lazy { - trace.entries.map { WindowManagerStateSubject.assertThat(it, this) } + trace.entries.map { WindowManagerStateSubject.assertThat(it, this, this) } } /** {@inheritDoc} */ override fun clone(): FlickerSubject { - return WindowManagerTraceSubject(fm, trace) + return WindowManagerTraceSubject(fm, trace, parent) } - /** - * Signal that the last assertion set is complete. The next assertion added will start a new - * set of assertions. - * - * E.g.: checkA().then().checkB() - * - * Will produce two sets of assertions (checkA) and (checkB) and checkB will only be checked - * after checkA passes. - */ - fun then(): WindowManagerTraceSubject = - apply { startAssertionBlock() } + /** {@inheritDoc} */ + override fun then(): WindowManagerTraceSubject = apply { super.then() } - /** - * Ignores the first entries in the trace, until the first assertion passes. If it reaches the - * end of the trace without passing any assertion, return a failure with the name/reason from - * the first assertion - * - * @return - */ - fun skipUntilFirstAssertion(): WindowManagerTraceSubject = - apply { assertionsChecker.skipUntilFirstAssertion() } + /** {@inheritDoc} */ + override fun skipUntilFirstAssertion(): WindowManagerTraceSubject = + apply { super.skipUntilFirstAssertion() } fun isEmpty(): WindowManagerTraceSubject = apply { check("Trace is empty").that(trace).isEmpty() @@ -105,353 +92,439 @@ class WindowManagerTraceSubject private constructor( } /** - * Checks if the non-app window with title containing [partialWindowTitle] exists above the app + * @return List of [WindowStateSubject]s matching [partialWindowTitle] in the order they + * appear on the trace + */ + fun windowStates(partialWindowTitle: String): List<WindowStateSubject> { + return subjects + .map { it.windowState { windows -> windows.title.contains(partialWindowTitle) } } + .filter { it.isNotEmpty } + } + + /** + * @return List of [WindowStateSubject]s matching [predicate] in the order they + * appear on the trace + */ + fun windowStates(predicate: (WindowState) -> Boolean): List<WindowStateSubject> { + return subjects + .map { it.windowState { window -> predicate(window) } } + .filter { it.isNotEmpty } + } + + /** {@inheritDoc} */ + fun notContains( + component: FlickerComponentName, + isOptional: Boolean = false + ): WindowManagerTraceSubject = apply { + addAssertion("notContains(${component.toWindowName()})", isOptional) { + it.notContains(component) + } + } + + /** + * Checks if the non-app window with title containing [component] exists above the app * windows and is visible * - * @param partialWindowTitle window title to search + * @param component Component to search + * @param isOptional If this assertion is optional or must pass */ - fun showsAboveAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply { - addAssertion("showsAboveAppWindow($partialWindowTitle)") { - it.isAboveAppWindow(*partialWindowTitle) + @JvmOverloads + fun isAboveAppWindowVisible( + component: FlickerComponentName, + isOptional: Boolean = false + ): WindowManagerTraceSubject = apply { + addAssertion("isAboveAppWindowVisible(${component.toWindowName()})", isOptional) { + it.containsAboveAppWindow(component) + .isNonAppWindowVisible(component) } } /** - * Checks if the non-app window with title containing [partialWindowTitle] exists above the app + * Checks if the non-app window with title containing [component] exists above the app * windows and is invisible * - * @param partialWindowTitle window title to search + * @param component Component to search + * @param isOptional If this assertion is optional or must pass */ - fun hidesAboveAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply { - addAssertion("hidesAboveAppWindow($partialWindowTitle)") { - it.isAboveAppWindow(*partialWindowTitle, isVisible = false) + @JvmOverloads + fun isAboveAppWindowInvisible( + component: FlickerComponentName, + isOptional: Boolean = false + ): WindowManagerTraceSubject = apply { + addAssertion("isAboveAppWindowInvisible(${component.toWindowName()})", isOptional) { + it.containsAboveAppWindow(component) + .isNonAppWindowInvisible(component) } } /** - * Checks if the non-app window with title containing [partialWindowTitle] exists below the app + * Checks if the non-app window with title containing [component] exists below the app * windows and is visible * - * @param partialWindowTitle window title to search + * @param component Component to search + * @param isOptional If this assertion is optional or must pass */ - fun showsBelowAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply { - addAssertion("showsBelowAppWindow($partialWindowTitle)") { - it.isBelowAppWindow(*partialWindowTitle) + @JvmOverloads + fun isBelowAppWindowVisible( + component: FlickerComponentName, + isOptional: Boolean = false + ): WindowManagerTraceSubject = apply { + addAssertion("isBelowAppWindowVisible(${component.toWindowName()})", isOptional) { + it.containsBelowAppWindow(component) + .isNonAppWindowVisible(component) } } /** - * Checks if the non-app window with title containing [partialWindowTitle] exists below the app + * Checks if the non-app window with title containing [component] exists below the app * windows and is invisible * - * @param partialWindowTitle window title to search + * @param component Component to search + * @param isOptional If this assertion is optional or must pass */ - fun hidesBelowAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply { - addAssertion("hidesBelowAppWindow($partialWindowTitle)") { - it.isBelowAppWindow(*partialWindowTitle, isVisible = false) + @JvmOverloads + fun isBelowAppWindowInvisible( + component: FlickerComponentName, + isOptional: Boolean = false + ): WindowManagerTraceSubject = apply { + addAssertion("isBelowAppWindowInvisible(${component.toWindowName()})", isOptional) { + it.containsBelowAppWindow(component) + .isNonAppWindowInvisible(component) } } /** - * Checks if non-app window with title containing the [partialWindowTitle] exists above or + * Checks if non-app window with title containing the [component] exists above or * below the app windows and is visible * - * @param partialWindowTitle window title to search + * @param component Component to search + * @param isOptional If this assertion is optional or must pass */ - fun showsNonAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply { - addAssertion("showsNonAppWindow($partialWindowTitle)") { - it.containsNonAppWindow(*partialWindowTitle) + @JvmOverloads + fun isNonAppWindowVisible( + component: FlickerComponentName, + isOptional: Boolean = false + ): WindowManagerTraceSubject = apply { + addAssertion("isNonAppWindowVisible(${component.toWindowName()})", isOptional) { + it.isNonAppWindowVisible(component) } } /** - * Checks if non-app window with title containing the [partialWindowTitle] exists above or + * Checks if non-app window with title containing the [component] exists above or * below the app windows and is invisible * - * @param partialWindowTitle window title to search + * @param component Component to search + * @param isOptional If this assertion is optional or must pass */ - fun hidesNonAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply { - addAssertion("hidesNonAppWindow($partialWindowTitle)") { - it.containsNonAppWindow(*partialWindowTitle, isVisible = false) + @JvmOverloads + fun isNonAppWindowInvisible( + component: FlickerComponentName, + isOptional: Boolean = false + ): WindowManagerTraceSubject = apply { + addAssertion("isNonAppWindowInvisible(${component.toWindowName()})", isOptional) { + it.isNonAppWindowInvisible(component) } } /** - * Checks if an app window with title containing the [partialWindowTitles] is on top + * Checks if app window with title containing the [component] is on top * - * @param partialWindowTitles window title to search + * @param component Component to search + * @param isOptional If this assertion is optional or must pass */ - fun showsAppWindowOnTop(vararg partialWindowTitles: String): WindowManagerTraceSubject = apply { - val assertionName = "showsAppWindowOnTop(${partialWindowTitles.joinToString(",")})" - addAssertion(assertionName) { - check("No window titles to search") - .that(partialWindowTitles) - .isNotEmpty() - it.showsAppWindowOnTop(*partialWindowTitles) + @JvmOverloads + fun isAppWindowOnTop( + component: FlickerComponentName, + isOptional: Boolean = false + ): WindowManagerTraceSubject = apply { + addAssertion("isAppWindowOnTop(${component.toWindowName()})", isOptional) { + it.isAppWindowOnTop(component) } } /** - * Checks if app window with title containing the [partialWindowTitle] is not on top + * Checks if app window with title containing the [component] is not on top * - * @param partialWindowTitle window title to search + * @param component Component to search + * @param isOptional If this assertion is optional or must pass */ - fun appWindowNotOnTop(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply { - addAssertion("hidesAppWindowOnTop($partialWindowTitle)") { - it.containsAppWindow(*partialWindowTitle, isVisible = false) + @JvmOverloads + fun isAppWindowNotOnTop( + component: FlickerComponentName, + isOptional: Boolean = false + ): WindowManagerTraceSubject = apply { + addAssertion("appWindowNotOnTop(${component.toWindowName()})", isOptional) { + it.isAppWindowNotOnTop(component) } } /** - * Checks if app window with title containing the [partialWindowTitle] is visible + * Checks if app window with title containing the [component] is visible * - * @param partialWindowTitle window title to search + * @param component Component to search + * @param isOptional If this assertion is optional or must pass */ - fun showsAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply { - addAssertion("showsAppWindow($partialWindowTitle)") { - it.containsAppWindow(*partialWindowTitle, isVisible = true) + @JvmOverloads + fun isAppWindowVisible( + component: FlickerComponentName, + isOptional: Boolean = false + ): WindowManagerTraceSubject = apply { + addAssertion("isAppWindowVisible(${component.toWindowName()})", isOptional) { + it.isAppWindowVisible(component) } } /** - * Checks if app window with title containing the [partialWindowTitle] is invisible + * Checks if app window with title containing the [component] is invisible * - * @param partialWindowTitle window title to search + * Note: This assertion have issues with the launcher window, because it contains 2 windows + * with the same name and only 1 is visible at a time. Prefer [isAppWindowOnTop] for launcher + * instead + * + * @param component Component to search + * @param isOptional If this assertion is optional or must pass */ - fun hidesAppWindow(vararg partialWindowTitle: String): WindowManagerTraceSubject = apply { - addAssertion("hidesAppWindow($partialWindowTitle)") { - it.containsAppWindow(*partialWindowTitle, isVisible = false) + @JvmOverloads + fun isAppWindowInvisible( + component: FlickerComponentName, + isOptional: Boolean = false + ): WindowManagerTraceSubject = apply { + addAssertion("isAppWindowInvisible(${component.toWindowName()})", isOptional) { + it.isAppWindowInvisible(component) } } /** - * Checks if no app windows containing the [partialWindowTitles] overlap with each other. + * Checks if no app windows containing the [component] overlap with each other. * - * @param partialWindowTitles partial titles of windows to check + * @param component Component to search */ - fun noWindowsOverlap(vararg partialWindowTitles: String): WindowManagerTraceSubject = apply { - val repr = partialWindowTitles.joinToString(", ") - require(partialWindowTitles.size > 1) { - "Must give more than one window to check! (Given $repr)" - } + fun noWindowsOverlap( + vararg component: FlickerComponentName + ): WindowManagerTraceSubject = apply { + val repr = component.joinToString(", ") { it.toWindowName() } + verify("Must give more than one window to check! (Given $repr)") + .that(component) + .hasLength(1) addAssertion("noWindowsOverlap($repr)") { - it.noWindowsOverlap(*partialWindowTitles) + it.doNotOverlap(*component) } } /** - * Checks if the window named [aboveWindowTitle] is above the one named [belowWindowTitle] in + * Checks if the window named [aboveWindow] is above the one named [belowWindow] in * z-order. * - * @param aboveWindowTitle partial name of the expected top window - * @param belowWindowTitle partial name of the expected bottom window + * @param aboveWindow Expected top window + * @param belowWindow Expected bottom window */ fun isAboveWindow( - aboveWindowTitle: String, - belowWindowTitle: String + aboveWindow: FlickerComponentName, + belowWindow: FlickerComponentName ): WindowManagerTraceSubject = apply { + val aboveWindowTitle = aboveWindow.toWindowName() + val belowWindowTitle = belowWindow.toWindowName() require(aboveWindowTitle != belowWindowTitle) addAssertion("$aboveWindowTitle is above $belowWindowTitle") { - it.isAboveWindow(aboveWindowTitle, belowWindowTitle) + it.isAboveWindow(aboveWindow, belowWindow) } } /** - * Asserts that the visible area covered by the first [WindowState] with [WindowState.title] - * containing [partialWindowTitle] covers at least [testRegion], that is, if its area of the - * window's bounds cover each point in the region. + * Asserts the visible area covered by the [WindowState]s matching [component] covers at least + * [testRegion], that is, if its area of the window's bounds cover each point in the region. * - * @param partialWindowTitle Name of the layer to search + * @param component Component to search * @param testRegion Expected visible area of the window */ fun coversAtLeast( testRegion: Region, - partialWindowTitle: String + component: FlickerComponentName? ): WindowManagerTraceSubject = apply { - addAssertion("coversAtLeastRegion($partialWindowTitle, $testRegion)") { - it.frameRegion(partialWindowTitle).coversAtLeast(testRegion) + addAssertion("coversAtLeastRegion(${component?.toWindowName()}, $testRegion)") { + it.frameRegion(component).coversAtLeast(testRegion) } } /** - * Asserts that the visible area covered by the first [WindowState] with [WindowState.title] - * containing [partialWindowTitle] covers at least [testRegion], that is, if its area of the - * window's bounds cover each point in the region. + * Asserts the visible area covered by the [WindowState]s matching [component] covers at least + * [testRegion], that is, if its area of the window's bounds cover each point in the region. * - * @param partialWindowTitle Name of the layer to search + * @param component Component to search * @param testRegion Expected visible area of the window */ fun coversAtLeast( testRegion: android.graphics.Region, - partialWindowTitle: String + component: FlickerComponentName? ): WindowManagerTraceSubject = apply { - addAssertion("coversAtLeastRegion($partialWindowTitle, $testRegion)") { - it.frameRegion(partialWindowTitle).coversAtLeast(testRegion) + addAssertion("coversAtLeastRegion(${component?.toWindowName()}, $testRegion)") { + it.frameRegion(component).coversAtLeast(testRegion) } } /** - * Asserts that the visible area covered by the first [WindowState] with [WindowState.title] - * containing [partialWindowTitle] covers at least [testRect], that is, if its area of the - * window's bounds cover each point in the region. + * Asserts the visible area covered by the [WindowState]s matching [component] covers at least + * [testRect], that is, if its area of the window's bounds cover each point in the region. * - * @param partialWindowTitle Name of the layer to search + * @param component Component to search * @param testRect Expected visible area of the window */ fun coversAtLeast( testRect: android.graphics.Rect, - partialWindowTitle: String + component: FlickerComponentName? ): WindowManagerTraceSubject = apply { - addAssertion("coversAtLeastRegion($partialWindowTitle, $testRect)") { - it.frameRegion(partialWindowTitle).coversAtLeast(testRect) + addAssertion("coversAtLeastRegion(${component?.toWindowName()}, $testRect)") { + it.frameRegion(component).coversAtLeast(testRect) } } /** - * Asserts that the visible area covered by the first [WindowState] with [WindowState.title] - * containing [partialWindowTitle] covers at least [testRect], that is, if its area of the - * window's bounds cover each point in the region. + * Asserts the visible area covered by the [WindowState]s matching [component] covers at least + * [testRect], that is, if its area of the window's bounds cover each point in the region. * - * @param partialWindowTitle Name of the layer to search + * @param component Component to search * @param testRect Expected visible area of the window */ fun coversAtLeast( testRect: Rect, - partialWindowTitle: String + component: FlickerComponentName? ): WindowManagerTraceSubject = apply { - addAssertion("coversAtLeastRegion($partialWindowTitle, $testRect)") { - it.frameRegion(partialWindowTitle).coversAtLeast(testRect) + addAssertion("coversAtLeastRegion(${component?.toWindowName()}, $testRect)") { + it.frameRegion(component).coversAtLeast(testRect) } } /** - * Asserts that the visible area covered by the first [WindowState] with [WindowState.title] - * containing [partialWindowTitle] covers at most [testRegion], that is, if the area of the - * window state bounds don't cover any point outside of [testRegion]. + * Asserts the visible area covered by the [WindowState]s matching [component] covers at most + * [testRegion], that is, if the area of the window state bounds don't cover any point outside + * of [testRegion]. * - * @param partialWindowTitle Name of the layer to search + * @param component Component to search * @param testRegion Expected visible area of the window */ fun coversAtMost( testRegion: Region, - partialWindowTitle: String + component: FlickerComponentName? ): WindowManagerTraceSubject = apply { - addAssertion("coversAtMostRegion($partialWindowTitle, $testRegion)") { - it.frameRegion(partialWindowTitle).coversAtMost(testRegion) + addAssertion("coversAtMostRegion(${component?.toWindowName()}, $testRegion)") { + it.frameRegion(component).coversAtMost(testRegion) } } /** - * Asserts that the visible area covered by the first [WindowState] with [WindowState.title] - * containing [partialWindowTitle] covers at most [testRegion], that is, if the area of the - * window state bounds don't cover any point outside of [testRegion]. + * Asserts the visible area covered by the [WindowState]s matching [component] covers at most + * [testRegion], that is, if the area of the window state bounds don't cover any point outside + * of [testRegion]. * - * @param partialWindowTitle Name of the layer to search + * @param component Component to search * @param testRegion Expected visible area of the window */ fun coversAtMost( testRegion: android.graphics.Region, - partialWindowTitle: String + component: FlickerComponentName? ): WindowManagerTraceSubject = apply { - addAssertion("coversAtMostRegion($partialWindowTitle, $testRegion)") { - it.frameRegion(partialWindowTitle).coversAtMost(testRegion) + addAssertion("coversAtMostRegion(${component?.toWindowName()}, $testRegion)") { + it.frameRegion(component).coversAtMost(testRegion) } } /** - * Asserts that the visible area covered by the first [WindowState] with [WindowState.title] - * containing [partialWindowTitle] covers at most [testRect], that is, if the area of the - * window state bounds don't cover any point outside of [testRect]. + * Asserts the visible area covered by the [WindowState]s matching [component] covers at most + * [testRect], that is, if the area of the window state bounds don't cover any point outside + * of [testRect]. * - * @param partialWindowTitle Name of the layer to search + * @param component Component to search * @param testRect Expected visible area of the window */ fun coversAtMost( testRect: Rect, - partialWindowTitle: String + component: FlickerComponentName? ): WindowManagerTraceSubject = apply { - addAssertion("coversAtMostRegion($partialWindowTitle, $testRect)") { - it.frameRegion(partialWindowTitle).coversAtMost(testRect) + addAssertion("coversAtMostRegion(${component?.toWindowName()}, $testRect)") { + it.frameRegion(component).coversAtMost(testRect) } } /** - * Asserts that the visible area covered by the first [WindowState] with [WindowState.title] - * containing [partialWindowTitle] covers at most [testRect], that is, if the area of the - * window state bounds don't cover any point outside of [testRect]. + * Asserts the visible area covered by the [WindowState]s matching [component] covers at most + * [testRect], that is, if the area of the window state bounds don't cover any point outside + * of [testRect]. * - * @param partialWindowTitle Name of the layer to search + * @param component Component to search * @param testRect Expected visible area of the window */ fun coversAtMost( testRect: android.graphics.Rect, - partialWindowTitle: String + component: FlickerComponentName? ): WindowManagerTraceSubject = apply { - addAssertion("coversAtMostRegion($partialWindowTitle, $testRect)") { - it.frameRegion(partialWindowTitle).coversAtMost(testRect) + addAssertion("coversAtMostRegion(${component?.toWindowName()}, $testRect)") { + it.frameRegion(component).coversAtMost(testRect) } } /** - * Asserts that the visible area covered by the first [WindowState] with [WindowState.title] - * containing [partialWindowTitle] covers exactly [testRegion]. + * Asserts the visible area covered by the [WindowState]s matching [component] covers exactly + * [testRegion]. * - * @param partialWindowTitle Name of the layer to search + * @param component Component to search * @param testRegion Expected visible area of the window */ fun coversExactly( testRegion: android.graphics.Region, - partialWindowTitle: String + component: FlickerComponentName? ): WindowManagerTraceSubject = apply { - addAssertion("coversExactly($partialWindowTitle, $testRegion)") { - it.frameRegion(partialWindowTitle).coversExactly(testRegion) + addAssertion("coversExactly(${component?.toWindowName()}, $testRegion)") { + it.frameRegion(component).coversExactly(testRegion) } } /** - * Asserts that the visible area covered by the first [WindowState] with [WindowState.title] - * containing [partialWindowTitle] covers exactly [testRect]. + * Asserts the visible area covered by the [WindowState]s matching [component] covers exactly + * [testRegion]. * - * @param partialWindowTitle Name of the layer to search + * @param component Component to search * @param testRect Expected visible area of the window */ fun coversExactly( testRect: Rect, - partialWindowTitle: String + component: FlickerComponentName? ): WindowManagerTraceSubject = apply { - addAssertion("coversExactly($partialWindowTitle, $testRect)") { - it.frameRegion(partialWindowTitle).coversExactly(testRect) + addAssertion("coversExactly(${component?.toWindowName()}, $testRect)") { + it.frameRegion(component).coversExactly(testRect) } } /** - * Asserts that the visible area covered by the first [WindowState] with [WindowState.title] - * containing [partialWindowTitle] covers exactly [testRect]. + * Asserts the visible area covered by the [WindowState]s matching [component] covers exactly + * [testRect]. * - * @param partialWindowTitle Name of the layer to search + * @param component Component to search * @param testRect Expected visible area of the window */ fun coversExactly( testRect: android.graphics.Rect, - partialWindowTitle: String + component: FlickerComponentName? ): WindowManagerTraceSubject = apply { - addAssertion("coversExactly($partialWindowTitle, $testRect)") { - it.frameRegion(partialWindowTitle).coversExactly(testRect) + addAssertion("coversExactly(${component?.toWindowName()}, $testRect)") { + it.frameRegion(component).coversExactly(testRect) } } /** * Checks that all visible layers are shown for more than one consecutive entry */ + @JvmOverloads fun visibleWindowsShownMoreThanOneConsecutiveEntry( - ignoreWindows: List<String> = listOf(WindowManagerStateHelper.SPLASH_SCREEN_NAME, - WindowManagerStateHelper.SNAPSHOT_WINDOW_NAME) + ignoreWindows: List<FlickerComponentName> = listOf( + FlickerComponentName.SPLASH_SCREEN, + FlickerComponentName.SNAPSHOT) ): WindowManagerTraceSubject = apply { visibleEntriesShownMoreThanOneConsecutiveTime { subject -> subject.wmState.windowStates .filter { it.isVisible } .filter { - ignoreWindows.none { windowName -> windowName in it.title } + ignoreWindows.none { windowName -> windowName.toWindowName() in it.title } } .map { it.name } .toSet() @@ -463,8 +536,9 @@ class WindowManagerTraceSubject private constructor( */ operator fun invoke( name: String, + isOptional: Boolean = false, assertion: Assertion<WindowManagerStateSubject> - ): WindowManagerTraceSubject = apply { addAssertion(name, assertion) } + ): WindowManagerTraceSubject = apply { addAssertion(name, isOptional, assertion) } /** * Run the assertions for all trace entries within the specified time range @@ -486,8 +560,10 @@ class WindowManagerTraceSubject private constructor( /** * Boiler-plate Subject.Factory for WmTraceSubject */ - private val FACTORY: Factory<Subject, WindowManagerTrace> = - Factory { fm, subject -> WindowManagerTraceSubject(fm, subject) } + private fun getFactory( + parent: WindowManagerTraceSubject? + ): Factory<Subject, WindowManagerTrace> = + Factory { fm, subject -> WindowManagerTraceSubject(fm, subject, parent) } /** * Creates a [WindowManagerTraceSubject] representing a WindowManager trace, @@ -496,10 +572,14 @@ class WindowManagerTraceSubject private constructor( * @param trace WindowManager trace */ @JvmStatic - fun assertThat(trace: WindowManagerTrace): WindowManagerTraceSubject { + @JvmOverloads + fun assertThat( + trace: WindowManagerTrace, + parent: WindowManagerTraceSubject? = null + ): WindowManagerTraceSubject { val strategy = FlickerFailureStrategy() val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy) - .about(FACTORY) + .about(getFactory(parent)) .that(trace) as WindowManagerTraceSubject strategy.init(subject) return subject @@ -509,6 +589,8 @@ class WindowManagerTraceSubject private constructor( * Static method for getting the subject factory (for use with assertAbout()) */ @JvmStatic - fun entries(): Factory<Subject, WindowManagerTrace> = FACTORY + fun entries( + parent: WindowManagerTraceSubject? + ): Factory<Subject, WindowManagerTrace> = getFactory(parent) } } diff --git a/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowStateSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowStateSubject.kt index 3666d0328..41a31192c 100644 --- a/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowStateSubject.kt +++ b/libraries/flicker/src/com/android/server/wm/flicker/traces/windowmanager/WindowStateSubject.kt @@ -21,6 +21,7 @@ import com.android.server.wm.flicker.assertions.FlickerSubject import com.android.server.wm.flicker.traces.FlickerFailureStrategy import com.android.server.wm.flicker.traces.RegionSubject import com.android.server.wm.traces.common.windowmanager.windows.WindowState +import com.google.common.truth.Fact import com.google.common.truth.FailureMetadata import com.google.common.truth.FailureStrategy import com.google.common.truth.StandardSubjectBuilder @@ -45,8 +46,9 @@ import com.google.common.truth.StandardSubjectBuilder */ class WindowStateSubject private constructor( fm: FailureMetadata, + override val parent: WindowManagerStateSubject?, + override val timestamp: Long, val windowState: WindowState?, - private val entry: WindowManagerStateSubject?, private val windowTitle: String? = null ) : FlickerSubject(fm, windowState) { val isEmpty: Boolean get() = windowState == null @@ -54,10 +56,10 @@ class WindowStateSubject private constructor( val isVisible: Boolean get() = windowState?.isVisible == true val isInvisible: Boolean get() = windowState?.isVisible == false val name: String get() = windowState?.name ?: windowTitle ?: "" - val frame: RegionSubject get() = RegionSubject.assertThat(windowState?.frame, listOf(this)) + val frame: RegionSubject get() = RegionSubject.assertThat(windowState?.frame, this) - override val defaultFacts: String = - "${entry?.defaultFacts ?: ""}\nWindowTitle: ${windowState?.title}" + override val selfFacts = listOf( + Fact.fact("Window title", "${windowState?.title ?: windowTitle}")) /** * If the [windowState] exists, executes a custom [assertion] on the current subject @@ -69,7 +71,7 @@ class WindowStateSubject private constructor( /** {@inheritDoc} */ override fun clone(): FlickerSubject { - return WindowStateSubject(fm, windowState, entry, windowTitle) + return WindowStateSubject(fm, parent, timestamp, windowState, windowTitle) } /** @@ -95,10 +97,9 @@ class WindowStateSubject private constructor( * Boiler-plate Subject.Factory for LayerSubject */ @JvmStatic - @JvmOverloads - fun getFactory(entry: WindowManagerStateSubject? = null) = + fun getFactory(parent: WindowManagerStateSubject?, timestamp: Long, name: String?) = Factory { fm: FailureMetadata, subject: WindowState? -> - WindowStateSubject(fm, subject, entry) + WindowStateSubject(fm, parent, timestamp, subject, name) } /** @@ -107,13 +108,14 @@ class WindowStateSubject private constructor( @JvmStatic @JvmOverloads fun assertThat( - layer: WindowState?, - entry: WindowManagerStateSubject? = null + state: WindowState?, + parent: WindowManagerStateSubject? = null, + timestamp: Long ): WindowStateSubject { val strategy = FlickerFailureStrategy() val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy) - .about(getFactory(entry)) - .that(layer) as WindowStateSubject + .about(getFactory(parent, timestamp, name = null)) + .that(state) as WindowStateSubject strategy.init(subject) return subject } @@ -124,23 +126,15 @@ class WindowStateSubject private constructor( @JvmStatic internal fun assertThat( name: String, - entry: WindowManagerStateSubject? + parent: WindowManagerStateSubject?, + timestamp: Long ): WindowStateSubject { val strategy = FlickerFailureStrategy() val subject = StandardSubjectBuilder.forCustomFailureStrategy(strategy) - .about(getFactory(entry, name)) + .about(getFactory(parent, timestamp, name)) .that(null) as WindowStateSubject strategy.init(subject) return subject } - - /** - * Boiler-plate Subject.Factory for LayerSubject - */ - @JvmStatic - internal fun getFactory(entry: WindowManagerStateSubject?, name: String) = - Factory { fm: FailureMetadata, subject: WindowState? -> - WindowStateSubject(fm, subject, entry, name) - } } }
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/proto/errors.proto b/libraries/flicker/src/com/android/server/wm/proto/errors.proto new file mode 100644 index 000000000..94427bb81 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/proto/errors.proto @@ -0,0 +1,35 @@ +syntax = "proto2"; + +package com.android.server.wm.flicker; + +// Each message has its own class file created. +option java_multiple_files = true; + +message FlickerErrorProto { + required string stacktrace = 1; + required string message = 2; + optional int32 layerId = 3; + optional string windowToken = 4; + optional int32 taskId = 5; + optional string assertionName = 6; +} + +message FlickerErrorStateProto { + required int64 timestamp = 1; + repeated FlickerErrorProto errors = 2; +} + +message FlickerErrorTraceProto { + /* constant; MAGIC_NUMBER = (long) MAGIC_NUMBER_H << 32 | + MagicNumber.MAGIC_NUMBER_L (this is needed because enums have to be 32 bits + and there's no nice way to put 64bit constants into .proto files. */ + enum MagicNumber { + INVALID = 0; + MAGIC_NUMBER_L = 0x54525245; /* ERRT (little-endian ASCII) */ + MAGIC_NUMBER_H = 0x45434152; /* RACE (little-endian ASCII) */ + } + + /* Must be the first field, set to value in MagicNumber */ + optional fixed64 magic_number = 1; + repeated FlickerErrorStateProto states = 2; +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/proto/tags.proto b/libraries/flicker/src/com/android/server/wm/proto/tags.proto new file mode 100644 index 000000000..dd352b50b --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/proto/tags.proto @@ -0,0 +1,45 @@ +syntax = "proto2"; + +package com.android.server.wm.flicker; + +// Each message has its own class file created. +option java_multiple_files = true; + +message FlickerTagProto { + enum Transition { + ROTATION = 0; + APP_LAUNCH = 1; + APP_CLOSE = 2; + IME_APPEAR = 3; + IME_DISAPPEAR = 4; + PIP_ENTER = 5; + PIP_RESIZE = 6; + PIP_EXIT = 7; + }; + required bool isStartTag = 1; + required Transition transition = 2; + required int32 id = 3; + optional int32 layerId = 4; + optional string windowToken = 5; + optional int32 taskId = 6; +} + +message FlickerTagStateProto { + required int64 timestamp = 1; + repeated FlickerTagProto tags = 2; +} + +message FlickerTagTraceProto { + /* constant; MAGIC_NUMBER = (long) MAGIC_NUMBER_H << 32 | + MagicNumber.MAGIC_NUMBER_L (this is needed because enums have to be 32 bits + and there's no nice way to put 64bit constants into .proto files. */ + enum MagicNumber { + INVALID = 0; + MAGIC_NUMBER_L = 0x54474154; /* TAGT (little-endian ASCII) */ + MAGIC_NUMBER_H = 0x45434152; /* RACE (little-endian ASCII) */ + } + + /* Must be the first field, set to value in MagicNumber */ + optional fixed64 magic_number = 1; + repeated FlickerTagStateProto states = 2; +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Buffer.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Buffer.kt index 25e221e76..46a6227c8 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/Buffer.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/Buffer.kt @@ -16,7 +16,7 @@ package com.android.server.wm.traces.common -class Buffer(width: Int, height: Int, val stride: Int, val format: Int) : Bounds(width, height) { +class Buffer(width: Int, height: Int, val stride: Int, val format: Int) : Size(width, height) { override fun prettyPrint(): String = prettyPrint(this) override fun equals(other: Any?): Boolean = @@ -34,6 +34,8 @@ class Buffer(width: Int, height: Int, val stride: Int, val format: Int) : Bounds return result } + override fun toString(): String = prettyPrint() + companion object { val EMPTY: Buffer = Buffer(0, 0, 0, 0) diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Color.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Color.kt index a4334feca..785977e70 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/Color.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/Color.kt @@ -27,6 +27,26 @@ data class Color(val r: Float, val g: Float, val b: Float, val a: Float) { override fun toString(): String = if (isEmpty) "[empty]" else prettyPrint() + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Color) return false + + if (r != other.r) return false + if (g != other.g) return false + if (b != other.b) return false + if (a != other.a) return false + + return true + } + + override fun hashCode(): Int { + var result = r.hashCode() + result = 31 * result + g.hashCode() + result = 31 * result + b.hashCode() + result = 31 * result + a.hashCode() + return result + } + companion object { val EMPTY = Color(r = -1f, g = -1f, b = -1f, a = 0f) diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Condition.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Condition.kt new file mode 100644 index 000000000..3645304c0 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/Condition.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common + +/** + * The utility class to wait a condition with customized options. + * The default retry policy is 5 times with interval 1 second. + * + * @param <T> The type of the object to validate. + * + * <p>Sample:</p> + * <pre> + * // Simple case. + * if (Condition.waitFor("true value", () -> true)) { + * println("Success"); + * } + * // Wait for customized result with customized validation. + * String result = Condition.waitForResult(new Condition<String>("string comparison") + * .setResultSupplier(() -> "Result string") + * .setResultValidator(str -> str.equals("Expected string")) + * .setRetryIntervalMs(500) + * .setRetryLimit(3) + * .setOnFailure(str -> println("Failed on " + str))); + * </pre> + + * @param message The message to show what is waiting for. + * @param condition If it returns true, that means the condition is satisfied. + */ +open class Condition<T>( + protected open val message: String = "", + protected open val condition: (T) -> Boolean +) { + /** + * @return if [value] satisfies the condition + */ + fun isSatisfied(value: T): Boolean { + return condition.invoke(value) + } + + /** + * @return the negation of the current assertion + */ + fun negate(): Condition<T> = Condition( + message = "!$message") { + !this.condition.invoke(it) + } + + /** + * @return a formatted message for the passing or failing condition on a state + */ + open fun getMessage(value: T): String = "$message(passed=${isSatisfied(value)})" + + override fun toString(): String = this.message +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/ConditionList.kt b/libraries/flicker/src/com/android/server/wm/traces/common/ConditionList.kt new file mode 100644 index 000000000..acc6a5fbd --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/ConditionList.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common + +/** + * The utility class to validate a set of conditions + * + * This class is used to easily integrate multiple conditions into a single + * verification, for example, during [WaitCondition], while keeping the individual + * conditions separate for better reuse + * + * @param conditions conditions to be checked + */ +class ConditionList<T>( + val conditions: List<Condition<T>> +) : Condition<T>("", { false }) { + override val message: String + get() = conditions.joinToString(" and ") { it.toString() } + + override val condition: (T) -> Boolean + get() = { + conditions.all { condition -> condition.isSatisfied(it) } + } + + override fun getMessage(value: T): String = conditions + .joinToString(" and ") { it.getMessage(value) } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/DeviceStateDump.kt b/libraries/flicker/src/com/android/server/wm/traces/common/DeviceStateDump.kt new file mode 100644 index 000000000..c96cd6cae --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/DeviceStateDump.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common + +import com.android.server.wm.traces.common.layers.LayerTraceEntry +import com.android.server.wm.traces.common.windowmanager.WindowManagerState + +/** + * Represents a state dump containing the [WindowManagerState] and the [LayerTraceEntry] both + * parsed and in raw (byte) data. + */ +class DeviceStateDump<WMType : WindowManagerState?, LayerType : LayerTraceEntry?>( + /** + * Parsed [WindowManagerState] + */ + val wmState: WMType, + /** + * Parsed [LayerTraceEntry] + */ + val layerState: LayerType +)
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/DeviceTraceDump.kt b/libraries/flicker/src/com/android/server/wm/traces/common/DeviceTraceDump.kt new file mode 100644 index 000000000..f856a449e --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/DeviceTraceDump.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common + +import com.android.server.wm.traces.common.layers.LayersTrace +import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace + +/** + * Represents a state dump containing the [WindowManagerTrace] and the [LayersTrace] both parsed + * and in raw (byte) data. + */ +class DeviceTraceDump( + /** + * Parsed [WindowManagerTrace] + */ + val wmTrace: WindowManagerTrace?, + /** + * Parsed [LayersTrace] + */ + val layersTrace: LayersTrace? +)
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Extensions.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Extensions.kt index 1b24bae36..efffd11af 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/Extensions.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/Extensions.kt @@ -22,7 +22,7 @@ private const val MINUTE_AS_NANOSECONDS: Long = 60000000000 private const val HOUR_AS_NANOSECONDS: Long = 3600000000000 private const val DAY_AS_NANOSECONDS: Long = 86400000000000 -internal fun prettyTimestamp(timestampNs: Long): String { +fun prettyTimestamp(timestampNs: Long): String { // Necessary for compatibility with JS Number var remainingNs = "$timestampNs".toLong() val prettyTimestamp = StringBuilder() diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/FlickerComponentName.kt b/libraries/flicker/src/com/android/server/wm/traces/common/FlickerComponentName.kt new file mode 100644 index 000000000..c39a3bf7b --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/FlickerComponentName.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common + +/** + * Create a new component identifier. + * + * This is a version of Android's ComponentName class for flicker. This is necessary because + * flicker codebase it also compiled into KotlinJS for use into Winscope + * + * @param packageName The name of the package that the component exists in. Can + * not be null. + * @param className The name of the class inside of <var>pkg</var> that + * implements the component. Can not be null. + */ +data class FlickerComponentName( + val packageName: String, + val className: String +) { + /** + * Obtains the activity name from the component name. + * + * See [ComponentName.toWindowName] for additional information + */ + fun toActivityName(): String { + return when { + packageName.isNotEmpty() && className.isNotEmpty() -> { + val sb = StringBuilder(packageName.length + className.length) + appendShortString(sb, packageName, className) + return sb.toString() + } + packageName.isNotEmpty() -> packageName + className.isNotEmpty() -> className + else -> error("Component name should have an activity of class name") + } + } + + /** + * Obtains the window name from the component name. + * + * [ComponentName] builds the string representation as PKG/CLASS, however this doesn't + * work for system components such as IME, NavBar and StatusBar, Toast. + * + * If the component doesn't have a package name, assume it's a system component and return only + * the class name + */ + fun toWindowName(): String { + return when { + packageName.isNotEmpty() && className.isNotEmpty() -> "$packageName/$className" + packageName.isNotEmpty() -> packageName + className.isNotEmpty() -> className + else -> error("Component name should have an activity of class name") + } + } + + /** + * Obtains the layer name from the component name. + * + * See [toWindowName] for additional information + */ + fun toLayerName(): String { + var result = this.toWindowName() + if (result.contains("/") && !result.contains("#")) { + result = "$result#" + } + + return result + } + + private fun appendShortString(sb: StringBuilder, packageName: String, className: String) { + sb.append(packageName).append('/') + appendShortClassName(sb, packageName, className) + } + + private fun appendShortClassName(sb: StringBuilder, packageName: String, className: String) { + if (className.startsWith(packageName)) { + val packageNameLength = packageName.length + val classNameLength = className.length + if (classNameLength > packageNameLength && className[packageNameLength] == '.') { + sb.append(className, packageNameLength, classNameLength) + return + } + } + sb.append(className) + } + + companion object { + val NAV_BAR = FlickerComponentName("", "NavigationBar0") + val STATUS_BAR = FlickerComponentName("", "StatusBar") + val ROTATION = FlickerComponentName("", "RotationLayer") + val BACK_SURFACE = FlickerComponentName("", "BackColorSurface") + val IME = FlickerComponentName("", "InputMethod") + val SPLASH_SCREEN = FlickerComponentName("", "Splash Screen") + val SNAPSHOT = FlickerComponentName("", "SnapshotStartingWindow") + val WALLPAPER_BBQ_WRAPPER = + FlickerComponentName("", "Wallpaper BBQ wrapper") + + fun unflattenFromString(str: String): FlickerComponentName { + val sep = str.indexOf('/') + if (sep < 0 || sep + 1 >= str.length) { + error("Missing package/class separator") + } + val pkg = str.substring(0, sep) + var cls = str.substring(sep + 1) + if (cls.isNotEmpty() && cls[0] == '.') { + cls = pkg + cls + } + return FlickerComponentName(pkg, cls) + } + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/ITrace.kt b/libraries/flicker/src/com/android/server/wm/traces/common/ITrace.kt index 6dc532db8..75af785dd 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/ITrace.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/ITrace.kt @@ -17,9 +17,8 @@ package com.android.server.wm.traces.common interface ITrace<Entry : ITraceEntry> { - val entries: List<Entry> + val entries: Array<Entry> val source: String - val sourceChecksum: String fun getEntry(timestamp: Long): Entry { return entries.firstOrNull { it.timestamp == timestamp } diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Point.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Point.kt index 3b3c2b2f0..84b7e2c83 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/Point.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/Point.kt @@ -21,6 +21,22 @@ data class Point(val x: Int, val y: Int) { override fun toString(): String = prettyPrint() + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Point) return false + + if (x != other.x) return false + if (y != other.y) return false + + return true + } + + override fun hashCode(): Int { + var result = x + result = 31 * result + y + return result + } + companion object { fun prettyPrint(point: Point): String = "(${point.x}, ${point.y})" } diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/RectF.kt b/libraries/flicker/src/com/android/server/wm/traces/common/RectF.kt index db55eda45..5830ad024 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/RectF.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/RectF.kt @@ -73,11 +73,11 @@ data class RectF( * @return A rectangle with the intersection coordinates */ fun intersection(left: Float, top: Float, right: Float, bottom: Float): RectF { - if (this.left < right && left < this.right && this.top < bottom && top < this.bottom) { - var intersectionLeft = 0f - var intersectionTop = 0f - var intersectionRight = 0f - var intersectionBottom = 0f + if (this.left < right && left < this.right && this.top <= bottom && top <= this.bottom) { + var intersectionLeft = this.left + var intersectionTop = this.top + var intersectionRight = this.right + var intersectionBottom = this.bottom if (this.left < left) { intersectionLeft = left @@ -111,6 +111,26 @@ data class RectF( override fun toString(): String = if (isEmpty) "[empty]" else prettyPrint() + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is RectF) return false + + if (left != other.left) return false + if (top != other.top) return false + if (right != other.right) return false + if (bottom != other.bottom) return false + + return true + } + + override fun hashCode(): Int { + var result = left.hashCode() + result = 31 * result + top.hashCode() + result = 31 * result + right.hashCode() + result = 31 * result + bottom.hashCode() + return result + } + companion object { val EMPTY = RectF() diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Region.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Region.kt index 0bacfe9fe..b6c173478 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/Region.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/Region.kt @@ -39,6 +39,22 @@ class Region(val rects: Array<Rect>) : Rect( override fun prettyPrint(): String = rects.joinToString(", ") { it.prettyPrint() } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Region) return false + if (!super.equals(other)) return false + + if (!rects.contentEquals(other.rects)) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + rects.contentHashCode() + return result + } + companion object { val EMPTY = Region() } diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/Bounds.kt b/libraries/flicker/src/com/android/server/wm/traces/common/Size.kt index 52b838488..d5bd20aac 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/Bounds.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/Size.kt @@ -16,22 +16,19 @@ package com.android.server.wm.traces.common -open class Bounds(val width: Int, val height: Int) { +open class Size(val width: Int, val height: Int) { open val isEmpty: Boolean get() = height == 0 || width == 0 val isNotEmpty: Boolean get() = !isEmpty - val size: Bounds - get() = Bounds(width, height) - open fun prettyPrint(): String = prettyPrint(this) override fun toString(): String = if (isEmpty) "[empty]" else prettyPrint() override fun equals(other: Any?): Boolean = - other is Bounds && + other is Size && other.height == height && other.width == width @@ -42,8 +39,8 @@ open class Bounds(val width: Int, val height: Int) { } companion object { - val EMPTY: Bounds = Bounds(0, 0) + val EMPTY: Size = Size(0, 0) - fun prettyPrint(bounds: Bounds): String = "${bounds.width} x ${bounds.height}" + fun prettyPrint(bounds: Size): String = "${bounds.width} x ${bounds.height}" } }
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/WaitCondition.kt b/libraries/flicker/src/com/android/server/wm/traces/common/WaitCondition.kt new file mode 100644 index 000000000..cd8e3161a --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/WaitCondition.kt @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common + +/** + * The utility class to wait a condition with customized options. + * The default retry policy is 5 times with interval 1 second. + * + * @param <T> The type of the object to validate. + * + * <p>Sample:</p> + * <pre> + * // Simple case. + * if (Condition.waitFor("true value", () -> true)) { + * println("Success"); + * } + * // Wait for customized result with customized validation. + * String result = WaitForCondition.Builder(supplier = () -> "Result string") + * .withCondition(str -> str.equals("Expected string")) + * .withRetryIntervalMs(500) + * .withRetryLimit(3) + * .onFailure(str -> println("Failed on " + str))) + * .build() + * .waitFor() + * </pre> + + * @param condition If it returns true, that means the condition is satisfied. + */ +class WaitCondition<T> private constructor( + private val supplier: () -> T, + private val condition: Condition<T>, + private val retryLimit: Int, + private val onLog: ((String) -> Unit)?, + private val onFailure: ((T) -> Any)?, + private val onRetry: ((T) -> Any)?, + private val onSuccess: ((T) -> Any)? +) { + /** + * @return `false` if the condition does not satisfy within the time limit. + */ + fun waitFor(): Boolean { + onLog?.invoke("***Waiting for $condition") + var currState: T? = null + for (i in 0..retryLimit) { + currState = supplier.invoke() + if (condition.isSatisfied(currState)) { + onLog?.invoke("***Waiting for $condition ... Success!") + onSuccess?.invoke(currState) + return true + } else { + val detailedMessage = condition.getMessage(currState) + onLog?.invoke("***Waiting for $detailedMessage... retry=${i + 1}") + if (i < retryLimit) { + onRetry?.invoke(currState) + } + } + } + + val detailedMessage = if (currState != null) { + condition.getMessage(currState) + } else { + condition.toString() + } + onLog?.invoke("***Waiting for $detailedMessage ... Failed!") + if (onFailure != null) { + require(currState != null) { "Missing last result for failure notification" } + onFailure.invoke(currState) + } + return false + } + + class Builder<T>( + private val supplier: () -> T, + private var retryLimit: Int + ) { + private val conditions = mutableListOf<Condition<T>>() + private var onFailure: ((T) -> Any)? = null + private var onRetry: ((T) -> Any)? = null + private var onSuccess: ((T) -> Any)? = null + private var onLog: ((String) -> Unit)? = null + + fun withCondition(condition: Condition<T>) = + apply { conditions.add(condition) } + + fun withCondition(message: String, condition: (T) -> Boolean) = + apply { withCondition(Condition(message, condition)) } + + private fun spreadConditionList(): List<Condition<T>> = + conditions.flatMap { + if (it is ConditionList<T>) { + it.conditions + } else { + listOf(it) + } + } + + /** + * Executes the action when the condition does not satisfy within the time limit. The passed + * object to the consumer will be the last result from the supplier. + */ + fun onFailure(onFailure: (T) -> Any): Builder<T> = + apply { this.onFailure = onFailure } + + fun onLog(onLog: (String) -> Unit): Builder<T> = + apply { this.onLog = onLog } + + fun onRetry(onRetry: ((T) -> Any)? = null): Builder<T> = + apply { this.onRetry = onRetry } + + fun onSuccess(onRetry: ((T) -> Any)? = null): Builder<T> = + apply { this.onSuccess = onRetry } + + fun build(): WaitCondition<T> = + WaitCondition(supplier, ConditionList(spreadConditionList()), retryLimit, + onLog, onFailure, onRetry, onSuccess) + } + + companion object { + // TODO(b/112837428): Implement a incremental retry policy to reduce the unnecessary + // constant time, currently keep the default as 5*1s because most of the original code + // uses it, and some tests might be sensitive to the waiting interval. + const val DEFAULT_RETRY_LIMIT = 10 + const val DEFAULT_RETRY_INTERVAL_MS = 500L + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/WindowManagerConditionsFactory.kt b/libraries/flicker/src/com/android/server/wm/traces/common/WindowManagerConditionsFactory.kt new file mode 100644 index 000000000..030ad1806 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/WindowManagerConditionsFactory.kt @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common + +import com.android.server.wm.traces.common.layers.Layer +import com.android.server.wm.traces.common.layers.LayerTraceEntry +import com.android.server.wm.traces.common.layers.Transform +import com.android.server.wm.traces.common.layers.Transform.Companion.isFlagSet +import com.android.server.wm.traces.common.service.PlatformConsts +import com.android.server.wm.traces.common.windowmanager.WindowManagerState +import com.android.server.wm.traces.common.windowmanager.windows.WindowState + +object WindowManagerConditionsFactory { + private val navBarWindowName = FlickerComponentName.NAV_BAR.toWindowName() + private val navBarLayerName = FlickerComponentName.NAV_BAR.toLayerName() + private val statusBarWindowName = FlickerComponentName.STATUS_BAR.toWindowName() + private val statusBarLayerName = FlickerComponentName.STATUS_BAR.toLayerName() + + /** + * Condition to check if the nav bar window is visible + */ + fun isNavBarVisible(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + ConditionList(listOf( + isNavBarWindowVisible(), isNavBarLayerVisible(), isNavBarLayerOpaque())) + + /** + * Condition to check if the nav bar window is visible + */ + fun isNavBarWindowVisible(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("isNavBarWindowVisible") { + it.wmState.isWindowVisible(navBarWindowName) + } + + /** + * Condition to check if the nav bar layer is visible + */ + fun isNavBarLayerVisible(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + isLayerVisible(navBarLayerName) + + /** + * Condition to check if the nav bar layer is opaque + */ + fun isNavBarLayerOpaque(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("isNavBarLayerOpaque") { + it.layerState.getLayerWithBuffer(navBarLayerName) + ?.color?.a ?: 0f == 1f + } + + /** + * Condition to check if the status bar window is visible + */ + fun isStatusBarVisible(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + ConditionList(listOf( + isStatusBarWindowVisible(), isStatusBarLayerVisible(), isStatusBarLayerOpaque())) + + /** + * Condition to check if the nav bar window is visible + */ + fun isStatusBarWindowVisible(): + Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("isStatusBarWindowVisible") { + it.wmState.isWindowVisible(statusBarWindowName) + } + + /** + * Condition to check if the nav bar layer is visible + */ + fun isStatusBarLayerVisible(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + isLayerVisible(statusBarLayerName) + + /** + * Condition to check if the nav bar layer is opaque + */ + fun isStatusBarLayerOpaque(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("isStatusBarLayerOpaque") { + it.layerState.getLayerWithBuffer(statusBarLayerName) + ?.color?.a ?: 0f == 1f + } + + fun isHomeActivityVisible(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("isHomeActivityVisible") { + it.wmState.homeActivity?.isVisible == true + } + + fun isAppTransitionIdle( + displayId: Int + ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("isAppTransitionIdle[$displayId]") { + it.wmState.getDisplay(displayId) + ?.appTransitionState == WindowManagerState.APP_STATE_IDLE + } + + fun containsActivity( + component: FlickerComponentName + ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("containsActivity[${component.toActivityName()}]") { + it.wmState.containsActivity(component.toActivityName()) + } + + fun containsWindow( + component: FlickerComponentName + ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("containsWindow[${component.toWindowName()}]") { + it.wmState.containsWindow(component.toWindowName()) + } + + fun isWindowSurfaceShown( + windowName: String + ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("isWindowSurfaceShown[$windowName]") { + it.wmState.isWindowSurfaceShown(windowName) + } + + fun isWindowSurfaceShown( + component: FlickerComponentName + ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + isWindowSurfaceShown(component.toWindowName()) + + fun isActivityVisible( + component: FlickerComponentName + ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("isActivityVisible") { + it.wmState.isActivityVisible(component.toActivityName()) + } + + fun isWMStateComplete(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("isWMStateComplete") { + it.wmState.isComplete() + } + + fun hasRotation( + expectedRotation: Int, + displayId: Int + ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> { + val hasRotationCondition = Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>>( + "hasRotation[$expectedRotation, display=$displayId]") { + val currRotation = it.wmState.getRotation(displayId) + currRotation == expectedRotation + } + return ConditionList(listOf( + hasRotationCondition, + isLayerVisible(FlickerComponentName.ROTATION).negate(), + isLayerVisible(FlickerComponentName.BACK_SURFACE).negate(), + hasLayersAnimating().negate() + )) + } + + fun isLayerVisible( + layerName: String + ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("isLayerVisible[$layerName]") { + it.layerState.isVisible(layerName) + } + + fun isLayerVisible( + layerId: Int + ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("isLayerVisible[$layerId]") { + it.layerState.getLayerById(layerId)?.isVisible ?: false + } + + fun isLayerColorAlphaOne( + component: FlickerComponentName + ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("isLayerColorAlphaOne[${component.toLayerName()}]") { + val layers = it.layerState.getVisibleLayersByName(component) + layers.any { layer -> layer.color.a == 1.0f } + } + + fun isLayerColorAlphaOne( + layerId: Int + ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("isLayerColorAlphaOne[$layerId]") { + val layer = it.layerState.getLayerById(layerId) + layer?.color?.a == 1.0f + } + + fun isLayerTransformFlagSet( + component: FlickerComponentName, + transform: Int + ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("isLayerTransformFlagSet[" + + "${component.toLayerName()},transform=$transform]") { + val layers = it.layerState.getVisibleLayersByName(component) + layers.any { layer -> isTransformFlagSet(layer, transform) } + } + + fun isLayerTransformFlagSet( + layerId: Int, + transform: Int + ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("isLayerTransformFlagSet[$layerId, $transform]") { + val layer = it.layerState.getLayerById(layerId) + layer?.transform?.type?.isFlagSet(transform) ?: false + } + + fun isLayerTransformIdentity( + layerId: Int + ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + ConditionList(listOf( + isLayerTransformFlagSet(layerId, Transform.SCALE_VAL).negate(), + isLayerTransformFlagSet(layerId, Transform.TRANSLATE_VAL).negate(), + isLayerTransformFlagSet(layerId, Transform.ROTATE_VAL).negate() + )) + + private fun isTransformFlagSet(layer: Layer, transform: Int): Boolean = + layer.transform.type?.isFlagSet(transform) ?: false + + fun LayerTraceEntry.getVisibleLayersByName( + component: FlickerComponentName + ): List<Layer> = visibleLayers.filter { it.name.contains(component.toLayerName()) } + + fun isLayerVisible( + component: FlickerComponentName + ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + isLayerVisible(component.toLayerName()) + + fun hasLayersAnimating(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("hasLayersAnimating") { + it.layerState.isAnimating() + } + + fun isPipWindowLayerSizeMatch( + layerId: Int + ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("isPipWindowLayerSizeMatch") { + val pipWindow = it.wmState.pinnedWindows.firstOrNull { it.layerId == layerId } + ?: error("Unable to find window with layerId $layerId") + val windowHeight = pipWindow.frame.height.toFloat() + val windowWidth = pipWindow.frame.width.toFloat() + + val pipLayer = it.layerState.getLayerById(layerId) + val layerHeight = pipLayer?.sourceBounds?.height + ?: error("Unable to find layer with id $layerId") + val layerWidth = pipLayer.sourceBounds.width + + windowHeight == layerHeight && windowWidth == layerWidth + } + + fun hasPipWindow(): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("hasPipWindow") { + it.wmState.hasPipWindow() + } + + fun isImeShown( + displayId: Int + ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + ConditionList(listOf( + isImeOnDisplay(displayId), + isLayerVisible(FlickerComponentName.IME), + isImeSurfaceShown(), + isWindowSurfaceShown(FlickerComponentName.IME.toWindowName()) + )) + + private fun isImeOnDisplay( + displayId: Int + ): Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("isImeOnDisplay[$displayId]") { + it.wmState.inputMethodWindowState?.displayId == displayId + } + + private fun isImeSurfaceShown(): + Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("isImeSurfaceShown") { + it.wmState.inputMethodWindowState?.isSurfaceShown == true + } + + fun isAppLaunchEnded(taskId: Int): + Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + Condition("containsVisibleAppLaunchWindow[$taskId]") { dump -> + val windowStates = dump.wmState.getRootTask(taskId)?.activities?.flatMap { + it.children.filterIsInstance<WindowState>() + } + windowStates != null && windowStates.none { window -> + window.attributes.type == PlatformConsts.TYPE_APPLICATION_STARTING && + window.isVisible + } + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/errors/Error.kt b/libraries/flicker/src/com/android/server/wm/traces/common/errors/Error.kt new file mode 100644 index 000000000..7663b9373 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/errors/Error.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.errors + +/** + * Flicker Error identified in a WindowManager or SurfaceFlinger trace + * @param stacktrace Stacktrace to identify source of errors + * @param message Message to explain error briefly + * @param layerId The layer which the error is associated with + * @param windowToken The window which the error is associated with + * @param taskId The task which the error is associated with + * @param assertionName The class name of the assertion that generated the error + */ +data class Error( + val stacktrace: String, + val message: String, + val layerId: Int = 0, + val windowToken: String = "", + val taskId: Int = 0, + val assertionName: String = "" +)
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/errors/ErrorState.kt b/libraries/flicker/src/com/android/server/wm/traces/common/errors/ErrorState.kt new file mode 100644 index 000000000..0d285e06a --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/errors/ErrorState.kt @@ -0,0 +1,33 @@ +package com.android.server.wm.traces.common.errors + +import com.android.server.wm.traces.common.ITraceEntry +import com.android.server.wm.traces.common.prettyTimestamp + +/** + * A state at a particular time within a trace that holds a list of errors there may be. + * @param errors Errors contained in the state + * @param _timestamp Timestamp of this state + */ +class ErrorState( + val errors: Array<Error>, + _timestamp: String +) : ITraceEntry { + override val timestamp: Long = _timestamp.toLong() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ErrorState) return false + if (timestamp != other.timestamp) return false + if (errors.contentEquals(other.errors)) return false + return true + } + + override fun hashCode(): Int { + var result = timestamp.hashCode() + result = 31 * result + errors.contentDeepHashCode() + return result + } + + override fun toString(): String = "FlickerErrorState(" + + "timestamp=${prettyTimestamp(timestamp)}, numberOfErrors=${errors.size})" +} diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/errors/ErrorTrace.kt b/libraries/flicker/src/com/android/server/wm/traces/common/errors/ErrorTrace.kt new file mode 100644 index 000000000..e851e1d19 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/errors/ErrorTrace.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.errors + +import com.android.server.wm.traces.common.ITrace + +/** + * Represents all the states with errors in an entire trace. + * @param entries States with errors contained in this trace + * @param source Source of the trace + */ +data class ErrorTrace( + override val entries: Array<ErrorState>, + override val source: String +) : ITrace<ErrorState>, + List<ErrorState> by entries.toList() { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ErrorTrace) return false + if (entries != other.entries) return false + return true + } + + override fun hashCode(): Int = entries.contentDeepHashCode() + + override fun toString(): String = "FlickerErrorTrace(First: ${entries.first()}," + + "End: ${entries.last()})" +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/layers/Display.kt b/libraries/flicker/src/com/android/server/wm/traces/common/layers/Display.kt new file mode 100644 index 000000000..1dfa94e71 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/layers/Display.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.layers + +import com.android.server.wm.traces.common.Rect +import com.android.server.wm.traces.common.Size + +/** + * Representation of a Display in the SF trace + */ +data class Display( + val id: ULong, + val name: String, + val layerStackId: Int, + val size: Size, + val layerStackSpace: Rect, + val transform: Transform +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Display) return false + + if (id != other.id) return false + if (name != other.name) return false + if (layerStackId != other.layerStackId) return false + if (size != other.size) return false + if (layerStackSpace != other.layerStackSpace) return false + if (transform != other.transform) return false + + return true + } + + override fun hashCode(): Int { + var result = id.toInt() + result = 31 * result + name.hashCode() + result = 31 * result + layerStackId + result = 31 * result + size.hashCode() + result = 31 * result + layerStackSpace.hashCode() + result = 31 * result + transform.hashCode() + return result + } + + companion object { + val EMPTY = Display( + id = 0.toULong(), + name = "EMPTY", + layerStackId = -1, + size = Size.EMPTY, + layerStackSpace = Rect.EMPTY, + transform = Transform.EMPTY + ) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/layers/Layer.kt b/libraries/flicker/src/com/android/server/wm/traces/common/layers/Layer.kt index 2dd763003..ddff3ebc8 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/layers/Layer.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/layers/Layer.kt @@ -19,8 +19,9 @@ package com.android.server.wm.traces.common.layers import com.android.server.wm.traces.common.Buffer import com.android.server.wm.traces.common.Color import com.android.server.wm.traces.common.Rect -import com.android.server.wm.traces.common.Region import com.android.server.wm.traces.common.RectF +import com.android.server.wm.traces.common.Region +import com.android.server.wm.traces.common.layers.Transform.Companion.isFlagSet /** * Represents a single layer with links to its parent and child layers. @@ -29,7 +30,7 @@ import com.android.server.wm.traces.common.RectF * access internal Java/Android functionality * **/ -open class Layer( +data class Layer( val name: String, val id: Int, val parentId: Int, @@ -37,15 +38,15 @@ open class Layer( val visibleRegion: Region?, val activeBuffer: Buffer, val flags: Int, - _bounds: RectF?, + val bounds: RectF, val color: Color, - _isOpaque: Boolean, + private val _isOpaque: Boolean, val shadowRadius: Float, val cornerRadius: Float, val type: String, - _screenBounds: RectF?, + private val _screenBounds: RectF?, val transform: Transform, - _sourceBounds: RectF?, + val sourceBounds: RectF, val currFrame: Long, val effectiveScalingMode: Int, val bufferTransform: Transform, @@ -57,7 +58,8 @@ open class Layer( val isRelativeOf: Boolean, val zOrderRelativeOfId: Int ) { - lateinit var parent: Layer + val stableId: String = "$type $id $name" + var parent: Layer? = null var zOrderRelativeOf: Layer? = null var zOrderRelativeParentOf: Int = 0 @@ -66,22 +68,32 @@ open class Layer( * * @return */ - val isRootLayer: Boolean - get() { - return !::parent.isInitialized - } - - val children = mutableListOf<Layer>() - val occludedBy = mutableListOf<Layer>() - val partiallyOccludedBy = mutableListOf<Layer>() - val coveredBy = mutableListOf<Layer>() - - fun addChild(childLayer: Layer) { - children.add(childLayer) - } - - val bounds: RectF = _bounds ?: RectF.EMPTY - val sourceBounds: RectF = _sourceBounds ?: RectF.EMPTY + val isRootLayer: Boolean get() = parent == null + + private val _children = mutableListOf<Layer>() + private val _occludedBy = mutableListOf<Layer>() + private val _partiallyOccludedBy = mutableListOf<Layer>() + private val _coveredBy = mutableListOf<Layer>() + val children: Array<Layer> + get() = _children.toTypedArray() + val occludedBy: Array<Layer> + get() = _occludedBy.toTypedArray() + val partiallyOccludedBy: Array<Layer> + get() = _partiallyOccludedBy.toTypedArray() + val coveredBy: Array<Layer> + get() = _coveredBy.toTypedArray() + var isMissing: Boolean = false + internal set + + val isScaling: Boolean + get() = isTransformFlagSet(Transform.SCALE_VAL) + val isTranslating: Boolean + get() = isTransformFlagSet(Transform.TRANSLATE_VAL) + val isRotating: Boolean + get() = isTransformFlagSet(Transform.ROTATE_VAL) + + private fun isTransformFlagSet(transform: Int): Boolean = + this.transform.type?.isFlagSet(transform) ?: false /** * Checks if the layer's active buffer is empty @@ -135,10 +147,7 @@ open class Layer( * * @return */ - val fillsColor: Boolean - get() { - return color.isNotEmpty - } + val fillsColor: Boolean get() = color.isNotEmpty /** * Checks if the [Layer] draws a shadow @@ -209,19 +218,13 @@ open class Layer( val isEffectLayer: Boolean get() = type == "EffectLayer" /** - * Checks if the [Layer] is not visible - * - * @return - */ - val isInvisible: Boolean get() = !isVisible - - /** * Checks if the [Layer] is hidden by its parent * * @return */ val isHiddenByParent: Boolean - get() = !isRootLayer && (parent.isHiddenByPolicy || parent.isHiddenByParent) + get() = !isRootLayer && + (parent?.isHiddenByPolicy == true || parent?.isHiddenByParent == true) /** * Gets a description of why the layer is (in)visible @@ -234,7 +237,7 @@ open class Layer( isVisible -> "" isContainerLayer -> "ContainerLayer" isHiddenByPolicy -> "Flag is hidden" - isHiddenByParent -> "Hidden by parent ${parent.name}" + isHiddenByParent -> "Hidden by parent ${parent?.name}" isBufferLayer && isActiveBufferEmpty -> "Buffer is empty" color.isEmpty -> "Alpha is 0" crop?.isEmpty ?: false -> "Crop is 0x0" @@ -243,8 +246,8 @@ open class Layer( isRelativeOf && zOrderRelativeOf == null -> "RelativeOf layer has been removed" isEffectLayer && !fillsColor && !drawsShadows && !hasBlur -> "Effect layer does not have color fill, shadow or blur" - occludedBy.isNotEmpty() -> { - val occludedByIds = occludedBy.joinToString(", ") { it.id.toString() } + _occludedBy.isNotEmpty() -> { + val occludedByIds = _occludedBy.joinToString(", ") { it.id.toString() } "Layer is occluded by: $occludedByIds" } visibleRegion?.isEmpty ?: false -> @@ -259,6 +262,18 @@ open class Layer( else -> transform.apply(bounds) } + val absoluteZ: String + get() { + val zOrderRelativeOf = zOrderRelativeOf + return buildString { + when { + zOrderRelativeOf != null -> append(zOrderRelativeOf.absoluteZ).append(",") + parent != null -> append(parent?.absoluteZ).append(",") + } + append(z) + } + } + fun contains(innerLayer: Layer): Boolean { return if (!this.transform.isSimpleRotation || !innerLayer.transform.isSimpleRotation) { false @@ -267,6 +282,22 @@ open class Layer( } } + fun addChild(childLayer: Layer) { + _children.add(childLayer) + } + + fun addOccludedBy(layers: Array<Layer>) { + _occludedBy.addAll(layers) + } + + fun addPartiallyOccludedBy(layers: Array<Layer>) { + _partiallyOccludedBy.addAll(layers) + } + + fun addCoveredBy(layers: Array<Layer>) { + _coveredBy.addAll(layers) + } + fun overlaps(other: Layer): Boolean = !this.screenBounds.intersection(other.screenBounds).isEmpty @@ -275,7 +306,7 @@ open class Layer( append(name) if (activeBuffer.isNotEmpty) { - append(" buffer:${activeBuffer.width}x${activeBuffer.height}") + append(" buffer:$activeBuffer") append(" frame#$currFrame") } @@ -286,13 +317,42 @@ open class Layer( } override fun equals(other: Any?): Boolean { - return other is Layer && - other.parentId == this.parentId && - other.name == this.name && - other.flags == this.flags && - other.currFrame == this.currFrame && - other.activeBuffer == this.activeBuffer && - other.screenBounds == this.screenBounds + if (this === other) return true + if (other !is Layer) return false + + if (name != other.name) return false + if (id != other.id) return false + if (parentId != other.parentId) return false + if (z != other.z) return false + if (visibleRegion != other.visibleRegion) return false + if (activeBuffer != other.activeBuffer) return false + if (flags != other.flags) return false + if (bounds != other.bounds) return false + if (color != other.color) return false + if (shadowRadius != other.shadowRadius) return false + if (cornerRadius != other.cornerRadius) return false + if (type != other.type) return false + if (transform != other.transform) return false + if (sourceBounds != other.sourceBounds) return false + if (currFrame != other.currFrame) return false + if (effectiveScalingMode != other.effectiveScalingMode) return false + if (bufferTransform != other.bufferTransform) return false + if (hwcCompositionType != other.hwcCompositionType) return false + if (hwcCrop != other.hwcCrop) return false + if (hwcFrame != other.hwcFrame) return false + if (backgroundBlurRadius != other.backgroundBlurRadius) return false + if (crop != other.crop) return false + if (isRelativeOf != other.isRelativeOf) return false + if (zOrderRelativeOfId != other.zOrderRelativeOfId) return false + if (stableId != other.stableId) return false + if (parent != other.parent) return false + if (zOrderRelativeOf != other.zOrderRelativeOf) return false + if (zOrderRelativeParentOf != other.zOrderRelativeParentOf) return false + if (isMissing != other.isMissing) return false + if (isOpaque != other.isOpaque) return false + if (screenBounds != other.screenBounds) return false + + return true } override fun hashCode(): Int { @@ -300,26 +360,33 @@ open class Layer( result = 31 * result + id result = 31 * result + parentId result = 31 * result + z - result = 31 * result + visibleRegion.hashCode() + result = 31 * result + (visibleRegion?.hashCode() ?: 0) result = 31 * result + activeBuffer.hashCode() result = 31 * result + flags result = 31 * result + bounds.hashCode() result = 31 * result + color.hashCode() - result = 31 * result + isOpaque.hashCode() result = 31 * result + shadowRadius.hashCode() result = 31 * result + cornerRadius.hashCode() result = 31 * result + type.hashCode() - result = 31 * result + screenBounds.hashCode() result = 31 * result + transform.hashCode() result = 31 * result + sourceBounds.hashCode() result = 31 * result + currFrame.hashCode() result = 31 * result + effectiveScalingMode result = 31 * result + bufferTransform.hashCode() - result = 31 * result + parent.hashCode() - result = 31 * result + children.hashCode() - result = 31 * result + occludedBy.hashCode() - result = 31 * result + partiallyOccludedBy.hashCode() - result = 31 * result + coveredBy.hashCode() + result = 31 * result + hwcCompositionType + result = 31 * result + hwcCrop.hashCode() + result = 31 * result + hwcFrame.hashCode() + result = 31 * result + backgroundBlurRadius + result = 31 * result + (crop?.hashCode() ?: 0) + result = 31 * result + isRelativeOf.hashCode() + result = 31 * result + zOrderRelativeOfId + result = 31 * result + stableId.hashCode() + result = 31 * result + (parent?.hashCode() ?: 0) + result = 31 * result + (zOrderRelativeOf?.hashCode() ?: 0) + result = 31 * result + zOrderRelativeParentOf + result = 31 * result + isMissing.hashCode() + result = 31 * result + isOpaque.hashCode() + result = 31 * result + screenBounds.hashCode() return result } }
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntry.kt b/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntry.kt index c440de9a5..47ec4bff0 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntry.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntry.kt @@ -27,13 +27,18 @@ import com.android.server.wm.traces.common.prettyTimestamp * **/ open class LayerTraceEntry constructor( - override val timestamp: Long, // hierarchical representation of layers + override val timestamp: Long, val hwcBlob: String, val where: String, + val displays: Array<Display>, _rootLayers: Array<Layer> ) : ITraceEntry { + val isVisible = true + val stableId: String get() = this::class.simpleName ?: error("Unable to determine class") + val name: String get() = prettyTimestamp(timestamp) + val flattenedLayers: Array<Layer> = fillFlattenedLayers(_rootLayers) - val rootLayers: Array<Layer> get() = flattenedLayers.filter { it.isRootLayer }.toTypedArray() + val children: Array<Layer> get() = flattenedLayers.filter { it.isRootLayer }.toTypedArray() private fun fillFlattenedLayers(rootLayers: Array<Layer>): Array<Layer> { val opaqueLayers = mutableListOf<Layer>() @@ -79,11 +84,15 @@ open class LayerTraceEntry constructor( val visible = layer.isVisible if (visible) { - layer.occludedBy.addAll(opaqueLayers - .filter { it.contains(layer) && !it.hasRoundedCorners }) - layer.partiallyOccludedBy.addAll( - opaqueLayers.filter { it.overlaps(layer) && it !in layer.occludedBy }) - layer.coveredBy.addAll(transparentLayers.filter { it.overlaps(layer) }) + val occludedBy = opaqueLayers + .filter { it.contains(layer) && !it.hasRoundedCorners }.toTypedArray() + layer.addOccludedBy(occludedBy) + val partiallyOccludedBy = opaqueLayers + .filter { it.overlaps(layer) && it !in layer.occludedBy } + .toTypedArray() + layer.addPartiallyOccludedBy(partiallyOccludedBy) + val coveredBy = transparentLayers.filter { it.overlaps(layer) }.toTypedArray() + layer.addCoveredBy(coveredBy) if (layer.isOpaque) { opaqueLayers.add(layer) @@ -102,13 +111,39 @@ open class LayerTraceEntry constructor( } } + fun getLayerById(layerId: Int): Layer? = this.flattenedLayers.firstOrNull { it.id == layerId } + + /** + * Checks the transform of any layer is not a simple rotation + */ + fun isAnimating(windowName: String = ""): Boolean { + val layers = visibleLayers.filter { it.name.contains(windowName) } + return layers.any { layer -> !layer.transform.isSimpleRotation } + } + /** * Check if at least one window which matches provided window name is visible. */ fun isVisible(windowName: String): Boolean = - visibleLayers.any { it.name == windowName } + visibleLayers.any { it.name.contains(windowName) } + + fun asTrace(): LayersTrace = LayersTrace(arrayOf(this), source = "") override fun toString(): String { - return prettyTimestamp(timestamp) + return "${prettyTimestamp(timestamp)} (timestamp=$timestamp)" + } + + override fun equals(other: Any?): Boolean { + return other is LayerTraceEntry && other.timestamp == this.timestamp + } + + override fun hashCode(): Int { + var result = timestamp.hashCode() + result = 31 * result + hwcBlob.hashCode() + result = 31 * result + where.hashCode() + result = 31 * result + displays.contentHashCode() + result = 31 * result + isVisible.hashCode() + result = 31 * result + flattenedLayers.contentHashCode() + return result } }
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntryBuilder.kt b/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntryBuilder.kt index 446678f6e..3bcd774cf 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntryBuilder.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayerTraceEntryBuilder.kt @@ -21,9 +21,10 @@ package com.android.server.wm.traces.common.layers */ class LayerTraceEntryBuilder( timestamp: Any, - layers: List<Layer>, - val hwcBlob: String = "", - val where: String = "" + layers: Array<Layer>, + private val displays: Array<Display>, + private val hwcBlob: String = "", + private val where: String = "" ) { // Necessary for compatibility with JS number type private val timestamp: Long = "$timestamp".toLong() @@ -31,7 +32,7 @@ class LayerTraceEntryBuilder( private val orphans = mutableListOf<Layer>() private val layers = setLayers(layers) - private fun setLayers(layers: List<Layer>): Map<Int, Layer> { + private fun setLayers(layers: Array<Layer>): Map<Int, Layer> { val result = mutableMapOf<Int, Layer>() layers.forEach { layer -> val id = layer.id @@ -129,6 +130,6 @@ class LayerTraceEntryBuilder( // Fail if we find orphan layers. notifyOrphansLayers() - return LayerTraceEntry(timestamp, hwcBlob, where, rootLayers.toTypedArray()) + return LayerTraceEntry(timestamp, hwcBlob, where, displays, rootLayers.toTypedArray()) } -}
\ No newline at end of file +} diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayersTrace.kt b/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayersTrace.kt index 5162b7cdc..988f35748 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayersTrace.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/layers/LayersTrace.kt @@ -27,15 +27,46 @@ import com.android.server.wm.traces.common.ITrace * access internal Java/Android functionality * */ -open class LayersTrace( - override val entries: List<LayerTraceEntry>, - override val source: String = "", - override val sourceChecksum: String = "" -) : ITrace<LayerTraceEntry>, List<LayerTraceEntry> by entries { - constructor(entry: LayerTraceEntry): this(listOf(entry)) +data class LayersTrace( + override val entries: Array<LayerTraceEntry>, + override val source: String = "" +) : ITrace<LayerTraceEntry>, List<LayerTraceEntry> by entries.toList() { + constructor(entry: LayerTraceEntry): this(arrayOf(entry)) override fun toString(): String { return "LayersTrace(Start: ${entries.first()}, " + "End: ${entries.last()})" } -}
\ No newline at end of file + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is LayersTrace) return false + + if (!entries.contentEquals(other.entries)) return false + if (source != other.source) return false + + return true + } + + override fun hashCode(): Int { + var result = entries.contentHashCode() + result = 31 * result + source.hashCode() + return result + } + + /** + * Split the trace by the start and end timestamp. + * + * @param from the start timestamp + * @param to the end timestamp + * @return the subtrace trace(from, to) + */ + fun filter(from: Long, to: Long): LayersTrace { + return LayersTrace( + this.entries + .dropWhile { it.timestamp < from } + .dropLastWhile { it.timestamp > to } + .toTypedArray(), + source = "") + } +} diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/layers/Transform.kt b/libraries/flicker/src/com/android/server/wm/traces/common/layers/Transform.kt index 144fbabb6..2c05628ee 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/layers/Transform.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/layers/Transform.kt @@ -16,7 +16,9 @@ package com.android.server.wm.traces.common.layers +import com.android.server.wm.traces.common.FloatFormatter import com.android.server.wm.traces.common.RectF +import com.android.server.wm.traces.common.service.PlatformConsts open class Transform(val type: Int?, val matrix: Matrix) { @@ -46,6 +48,20 @@ open class Transform(val type: Int?, val matrix: Matrix) { return matrix.dsdx * matrix.dtdy != matrix.dtdx * matrix.dsdy } + fun getRotation(): Int { + if (type == null) { + return PlatformConsts.ROTATION_0 + } + + return when { + type.isFlagClear(SCALE_VAL or ROTATE_VAL or TRANSLATE_VAL) -> PlatformConsts.ROTATION_0 + type.isFlagSet(ROT_90_VAL) -> PlatformConsts.ROTATION_90 + type.isFlagSet(FLIP_V_VAL or FLIP_H_VAL) -> PlatformConsts.ROTATION_180 + type.isFlagSet(ROT_90_VAL or FLIP_V_VAL or FLIP_H_VAL) -> PlatformConsts.ROTATION_270 + else -> PlatformConsts.ROTATION_0 + } + } + private val typeFlags: Array<String> get() { if (type == null) { @@ -100,6 +116,8 @@ open class Transform(val type: Int?, val matrix: Matrix) { return "$transformType ${matrix.prettyPrint()}" } + override fun toString(): String = prettyPrint() + fun apply(bounds: RectF?): RectF { return multiplyRect(matrix, bounds ?: RectF.EMPTY) } @@ -116,7 +134,17 @@ open class Transform(val type: Int?, val matrix: Matrix) { val dtdy: Float, val ty: Float ) { - fun prettyPrint(): String = "dsdx:$dsdx dtdx:$dtdx dsdy:$dsdy dtdy:$dtdy" + fun prettyPrint(): String { + val dsdx = FloatFormatter.format(dsdx) + val dtdx = FloatFormatter.format(dtdx) + val dsdy = FloatFormatter.format(dsdy) + val dtdy = FloatFormatter.format(dtdy) + return "dsdx:$dsdx dtdx:$dtdx dsdy:$dsdy dtdy:$dtdy" + } + + companion object { + val EMPTY: Matrix = Matrix(0f, 0f, 0f, 0f, 0f, 0f) + } } private data class Vec2(val x: Float, val y: Float) @@ -150,6 +178,8 @@ open class Transform(val type: Int?, val matrix: Matrix) { } companion object { + val EMPTY: Transform = Transform(type = null, matrix = Matrix.EMPTY) + /* transform type flags */ const val TRANSLATE_VAL = 0x0001 const val ROTATE_VAL = 0x0002 @@ -173,4 +203,22 @@ open class Transform(val type: Int?, val matrix: Matrix) { return this and bits == bits } } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Transform) return false + + if (type != other.type) return false + if (matrix != other.matrix) return false + if (isSimpleRotation != other.isSimpleRotation) return false + + return true + } + + override fun hashCode(): Int { + var result = type ?: 0 + result = 31 * result + matrix.hashCode() + result = 31 * result + isSimpleRotation.hashCode() + return result + } }
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/ITagProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/ITagProcessor.kt new file mode 100644 index 000000000..cb049666c --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/ITagProcessor.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.service + +import com.android.server.wm.traces.common.layers.LayersTrace +import com.android.server.wm.traces.common.tags.TagTrace +import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace + +/** + * Interface for the WM Flicker Tag Processor component. + */ +interface ITagProcessor { + /** + * Adds tags to the received traces. + * + * @param wmTrace Window Manager trace + * @param layersTrace Surface Flinger trace + */ + fun generateTags(wmTrace: WindowManagerTrace, layersTrace: LayersTrace): TagTrace +} diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/ITransitionAssertor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/ITransitionAssertor.kt new file mode 100644 index 000000000..a3dcea8b9 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/ITransitionAssertor.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.service + +import com.android.server.wm.traces.common.errors.ErrorTrace +import com.android.server.wm.traces.common.layers.LayersTrace +import com.android.server.wm.traces.common.tags.Tag +import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace + +/** + * Interface for the WM Flicker Service Assertor component. + */ +interface ITransitionAssertor { + /** + * Analyzes a [WindowManagerTrace] and/or a [LayersTrace] trace to detect flickers. + * + * @param tag Tag for the transition + * @param wmTrace Window Manager trace + * @param layersTrace Surface Flinger trace + * @return An error trace + */ + fun analyze(tag: Tag, wmTrace: WindowManagerTrace, layersTrace: LayersTrace): ErrorTrace +} diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/PlatformConsts.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/PlatformConsts.kt new file mode 100644 index 000000000..0a13329cc --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/PlatformConsts.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.service + +object PlatformConsts { + /** + * The default Display id, which is the id of the primary display assuming there is one. + * + * Duplicated from [Display.DEFAULT_DISPLAY] because this class is used by JVM and KotlinJS + */ + const val DEFAULT_DISPLAY = 0 + + /** + * Window type: an application window that serves as the "base" window + * of the overall application + * + * Duplicated from [WindowManager.LayoutParams.TYPE_BASE_APPLICATION] because this class + * is used by JVM and KotlinJS + */ + const val TYPE_BASE_APPLICATION = 1 + + /** + * Window type: special application window that is displayed while the + * application is starting + * + * Duplicated from [WindowManager.LayoutParams.TYPE_APPLICATION_STARTING] because this class + * is used by JVM and KotlinJS + */ + const val TYPE_APPLICATION_STARTING = 3 + + /** + * Rotation constant: 0 degree rotation (natural orientation) + * + * Duplicated from [Surface.ROTATION_0] because this class is used by JVM and KotlinJS + */ + const val ROTATION_0 = 0 + + /** + * Rotation constant: 90 degree rotation. + * + * Duplicated from [Surface.ROTATION_90] because this class is used by JVM and KotlinJS + */ + const val ROTATION_90 = 1 + + /** + * Rotation constant: 180 degree rotation. + * + * Duplicated from [Surface.ROTATION_180] because this class is used by JVM and KotlinJS + */ + const val ROTATION_180 = 2 + + /** + * Rotation constant: 270 degree rotation. + * + * Duplicated from [Surface.ROTATION_270] because this class is used by JVM and KotlinJS + */ + const val ROTATION_270 = 3 +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/TaggingEngine.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/TaggingEngine.kt new file mode 100644 index 000000000..ef46d8e72 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/TaggingEngine.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.service + +import com.android.server.wm.traces.common.layers.LayersTrace +import com.android.server.wm.traces.common.service.processors.AppCloseProcessor +import com.android.server.wm.traces.common.service.processors.AppLaunchProcessor +import com.android.server.wm.traces.common.service.processors.ImeAppearProcessor +import com.android.server.wm.traces.common.service.processors.ImeDisappearProcessor +import com.android.server.wm.traces.common.service.processors.PipEnterProcessor +import com.android.server.wm.traces.common.service.processors.PipExitProcessor +import com.android.server.wm.traces.common.service.processors.PipExpandProcessor +import com.android.server.wm.traces.common.service.processors.PipResizeProcessor +import com.android.server.wm.traces.common.service.processors.RotationProcessor +import com.android.server.wm.traces.common.tags.TagState +import com.android.server.wm.traces.common.tags.TagTrace +import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace + +/** + * Invokes all concrete tag producers and writes to a .winscope file + * + * The timestamp constructor must be a string due to lack of Kotlin/KotlinJS Long compatibility + * + * The traces are passed as constructor arguments due to name mangling in KotlinJS + * + * @param logger Platform dependent function for logging + * @param wmTrace WindowManager trace + * @param layersTrace SurfaceFlinger trace + */ +class TaggingEngine( + private val wmTrace: WindowManagerTrace, + private val layersTrace: LayersTrace, + private val logger: (String) -> Unit +) { + private val transitions = listOf( + // TODO: Keep adding new transition processors to invoke + RotationProcessor(logger), + AppLaunchProcessor(logger), + AppCloseProcessor(logger), + ImeAppearProcessor(logger), + ImeDisappearProcessor(logger), + PipEnterProcessor(logger), + PipResizeProcessor(logger), + PipExpandProcessor(logger), + PipExitProcessor(logger) + ) + + /** + * Generate tags denoting start and end points for all [transitions] within traces + */ + fun run(): TagTrace { + val allStates = transitions.flatMap { + logger.invoke("Generating tags for ${it::class.simpleName}") + it.generateTags(wmTrace, layersTrace).entries.asList() + } + + /** + * Ensure all tag states with the same timestamp are merged + */ + val tagStates = allStates.distinct() + .groupBy({ it.timestamp }, { it.tags.asList() }) + .mapValues { (key, value) -> TagState(key.toString(), value.flatten().toTypedArray()) } + .values.toTypedArray() + + return TagTrace(tagStates, source = "") + } +} diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/AppCloseProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/AppCloseProcessor.kt new file mode 100644 index 000000000..7b3ffffc7 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/AppCloseProcessor.kt @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.service.processors + +import com.android.server.wm.traces.common.DeviceStateDump +import com.android.server.wm.traces.common.WindowManagerConditionsFactory +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerTransformFlagSet +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerTransformIdentity +import com.android.server.wm.traces.common.layers.LayerTraceEntry +import com.android.server.wm.traces.common.layers.Transform +import com.android.server.wm.traces.common.service.PlatformConsts.TYPE_APPLICATION_STARTING +import com.android.server.wm.traces.common.service.PlatformConsts.TYPE_BASE_APPLICATION +import com.android.server.wm.traces.common.tags.Tag +import com.android.server.wm.traces.common.tags.Transition +import com.android.server.wm.traces.common.windowmanager.WindowManagerState +import com.android.server.wm.traces.common.windowmanager.windows.WindowState + +/** + * This processor creates tags when an app is closed. + * @param logger logs by invoking any event messages + */ +class AppCloseProcessor(logger: (String) -> Unit) : TransitionProcessor(logger) { + override val transition = Transition.APP_CLOSE + private val areLayersAnimating = WindowManagerConditionsFactory.hasLayersAnimating() + private val wmStateIdle = WindowManagerConditionsFactory + .isAppTransitionIdle(/* default display */ 0) + private val wmStateComplete = WindowManagerConditionsFactory.isWMStateComplete() + private val translatingWindows = + HashMap<String, DeviceStateDump<WindowManagerState, LayerTraceEntry>>() + + override fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>) = + RetrieveClosingAppLayerId(tags) + + /** + * Initial FSM state that passes the current app launch activity if any to the next state. + * Closing app is also not transforming and has transform identity + */ + inner class RetrieveClosingAppLayerId( + tags: MutableMap<Long, MutableList<Tag>> + ) : BaseState(tags) { + override fun doProcessState( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState { + val isStableState = wmStateIdle.isSatisfied(current) || + wmStateComplete.isSatisfied(current) || + areLayersAnimating.negate().isSatisfied(current) + val currentAppWindows = current.wmState.rootTasks.flatMap { task -> + // No app launch activities and only resuming activities + val activities = task.activities.filter { activity -> + activity.state == "RESUMED" && activity.isVisible && + activity.children.filterIsInstance<WindowState>().none { window -> + window.attributes.type == TYPE_APPLICATION_STARTING + } + } + activities.flatMap { activity -> + activity.children.filterIsInstance<WindowState>().filter { window -> + window.isVisible && window.attributes.type == TYPE_BASE_APPLICATION + } + } + } + + // Only one closing app. This processor ignores app pairs situations. + if (currentAppWindows.size == 1 && isStableState) { + val isNotTransforming = isLayerTransformIdentity(currentAppWindows.first().layerId) + .isSatisfied(current) + if (isNotTransforming) { + return WaitLayerAnimationComplete(tags, currentAppWindows.first()) + } + } + return this + } + } + + /** + * FSM State that waits until the closing app has finished and stopped transforming. + */ + inner class WaitLayerAnimationComplete( + tags: MutableMap<Long, MutableList<Tag>>, + private val appWindow: WindowState + ) : BaseState(tags) { + private val layerId = appWindow.layerId + private val isTranslating = isLayerTransformFlagSet(layerId, Transform.TRANSLATE_VAL) + + override fun doProcessState( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState { + if (previous == null) return this + val startedTransforming = isTranslating.isSatisfied(current) && + isTranslating.negate().isSatisfied(previous) + val isStableState = wmStateIdle.isSatisfied(current) || + wmStateComplete.isSatisfied(current) || + areLayersAnimating.negate().isSatisfied(current) + + val finishedClosing = isLayerTransformIdentity(layerId).isSatisfied(current) && + current.layerState.getLayerById(layerId)?.isHiddenByParent ?: false + + if (startedTransforming) { + translatingWindows[appWindow.token] = current + } else if (finishedClosing && isStableState) { + val deviceStateDump = translatingWindows[appWindow.token] + if (deviceStateDump != null) { + addStartTransitionTag(deviceStateDump, transition, + layerId = layerId, + windowToken = appWindow.token + ) + addEndTransitionTag(current, transition, + layerId = layerId, + windowToken = appWindow.token + ) + return RetrieveClosingAppLayerId(tags) + } + } + return this + } + } +} diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/AppLaunchProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/AppLaunchProcessor.kt new file mode 100644 index 000000000..cb759548f --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/AppLaunchProcessor.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.service.processors + +import com.android.server.wm.traces.common.DeviceStateDump +import com.android.server.wm.traces.common.layers.LayerTraceEntry +import com.android.server.wm.traces.common.tags.Tag +import com.android.server.wm.traces.common.tags.Transition +import com.android.server.wm.traces.common.windowmanager.WindowManagerState + +/** + * This processor creates tags when an app is launched. + * @param logger logs by invoking any event messages + */ +class AppLaunchProcessor(logger: (String) -> Unit) : TransitionProcessor(logger) { + override val transition = Transition.APP_LAUNCH + private val windowsBecomeVisible = + HashMap<Int, DeviceStateDump<WindowManagerState, LayerTraceEntry>>() + + override fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>) = + WaitUntilWindowIsInVisibleActivity(tags) + + /** + * FSM state that stores any newly visible window activities (start tag) + * and when their layers stop scaling (end tag). + */ + inner class WaitUntilWindowIsInVisibleActivity( + tags: MutableMap<Long, MutableList<Tag>> + ) : BaseState(tags) { + override fun doProcessState( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState { + if (previous == null) return this + val prevVisibleWindows = previous.wmState.visibleWindows + val newlyVisibleWindows = current.wmState.visibleWindows.filterNot { window -> + prevVisibleWindows.any { it.token == window.token } + } + + // Wait until layer is no longer scaling + val appLaunchedLayers = windowsBecomeVisible.filterKeys { layerId -> + val currDumpLayer = current.layerState.getLayerById(layerId) + (previous.layerState.getLayerById(layerId)?.isScaling == true && + currDumpLayer?.isScaling == false) + } + + // Only want to tag when one app is being launched. + // Other scenarios like app pairs enter are ignored. + if (newlyVisibleWindows.size == 1) { + windowsBecomeVisible[newlyVisibleWindows.first().layerId] = previous + } else if (appLaunchedLayers.isNotEmpty()) { + val firstDump = appLaunchedLayers.entries.first() + val layerId = firstDump.key + addStartTransitionTag(firstDump.value, transition, + layerId = layerId, + timestamp = firstDump.value.layerState.timestamp + ) + addEndTransitionTag(current, transition, + layerId = layerId, + timestamp = current.layerState.timestamp + ) + windowsBecomeVisible.clear() + } + return this + } + } +} diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/BaseFsmState.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/BaseFsmState.kt new file mode 100644 index 000000000..2b7bb3b7f --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/BaseFsmState.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.service.processors + +import com.android.server.wm.traces.common.DeviceStateDump +import com.android.server.wm.traces.common.layers.LayerTraceEntry +import com.android.server.wm.traces.common.tags.Tag +import com.android.server.wm.traces.common.tags.Transition +import com.android.server.wm.traces.common.windowmanager.WindowManagerState + +/** + * Base state for the FSM, check if there are more WM and SF states to process + * and ensure there is always a 1:1 correspondence between start and end tags. + * If the location of the end of the transition wasn't found, add an end tag at end of trace. + */ +abstract class BaseFsmState( + tags: MutableMap<Long, MutableList<Tag>>, + internal val logger: (String) -> Unit, + internal val transition: Transition +) : FSMState(tags) { + protected abstract fun doProcessState( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState + + override fun process( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry>? + ): FSMState? { + return if (next == null) { + // last state + val timestamp = current.layerState.timestamp + logger.invoke("($timestamp) Trace has reached the end") + if (hasOpenTag()) { + logger.invoke("($timestamp) Has an open tag, closing it on the last SF state") + addEndTransitionTag(current, transition) + } + null + } else { + doProcessState(previous, current, next) + } + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/FSMState.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/FSMState.kt new file mode 100644 index 000000000..6c661ae44 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/FSMState.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.service.processors + +import com.android.server.wm.traces.common.DeviceStateDump +import com.android.server.wm.traces.common.layers.LayerTraceEntry +import com.android.server.wm.traces.common.tags.Tag +import com.android.server.wm.traces.common.tags.Transition +import com.android.server.wm.traces.common.windowmanager.WindowManagerState +import kotlin.math.max +import kotlin.math.min + +/** + * Represents the Finite State Machine used by tagging processors and implements adding start and + * end tags. + * @param tags represents the map of timestamps associated with tag(s). + */ +abstract class FSMState(protected val tags: MutableMap<Long, MutableList<Tag>>) { + abstract fun process( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry>? + ): FSMState? + + protected fun addStartTransitionTag( + state: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + transition: Transition, + layerId: Int = 0, + windowToken: String = "", + taskId: Int = 0, + timestamp: Long = min(state.wmState.timestamp, state.layerState.timestamp) + ) { + val tagId = ++lastTagId + val startTag = Tag(id = tagId, transition, isStartTag = true, layerId = layerId, + windowToken = windowToken, taskId = taskId) + if (!tags.containsKey(timestamp)) { + tags[timestamp] = mutableListOf() + } + tags.getValue(timestamp).add(startTag) + } + + protected fun addEndTransitionTag( + state: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + transition: Transition, + layerId: Int = 0, + windowToken: String = "", + taskId: Int = 0, + timestamp: Long = max(state.wmState.timestamp, state.layerState.timestamp) + ) { + val endTag = Tag(id = lastTagId, transition, isStartTag = false, layerId = layerId, + windowToken = windowToken, taskId = taskId) + if (!tags.containsKey(timestamp)) { + tags[timestamp] = mutableListOf() + } + tags.getValue(timestamp).add(endTag) + } + + protected fun hasOpenTag() = tags.values.flatten().size % 2 != 0 + + companion object { + private var lastTagId = -1 + } +} diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/ImeAppearProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/ImeAppearProcessor.kt new file mode 100644 index 000000000..44b5c3ca1 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/ImeAppearProcessor.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.service.processors + +import com.android.server.wm.traces.common.ConditionList +import com.android.server.wm.traces.common.DeviceStateDump +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isImeShown +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerColorAlphaOne +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerTransformFlagSet +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerVisible +import com.android.server.wm.traces.common.layers.LayerTraceEntry +import com.android.server.wm.traces.common.layers.Transform +import com.android.server.wm.traces.common.service.PlatformConsts +import com.android.server.wm.traces.common.tags.Tag +import com.android.server.wm.traces.common.tags.Transition +import com.android.server.wm.traces.common.windowmanager.WindowManagerState + +/** + * This processor creates tags when the keyboard starts and finishes appearing. + * @param logger logs by invoking any event messages + */ +class ImeAppearProcessor(logger: (String) -> Unit) : TransitionProcessor(logger) { + override val transition = Transition.IME_APPEAR + override fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>) = + WaitInputMethodVisible(tags) + + /** + * FSM state that waits until the InputMethod is visible in both WM and SF. + */ + inner class WaitInputMethodVisible( + tags: MutableMap<Long, MutableList<Tag>> + ) : BaseState(tags) { + private val newImeVisible = isImeShown(PlatformConsts.DEFAULT_DISPLAY) + private val prevImeInvisible = newImeVisible.negate() + + override fun doProcessState( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState { + if (previous == null) return this + + return if (newImeVisible.isSatisfied(current) && + prevImeInvisible.isSatisfied(previous)) { + processInputMethodVisible(current) + } else { + this + } + } + + private fun processInputMethodVisible( + current: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState { + logger.invoke("(${current.layerState.timestamp}) IME appear started.") + // add factory method as well + val inputMethodLayer = current.layerState.visibleLayers.first { + it.name.contains(FlickerComponentName.IME.toLayerName()) + } + addStartTransitionTag(current, transition, layerId = inputMethodLayer.id) + return WaitImeAppearFinished(tags, inputMethodLayer.id) + } + } + + /** + * FSM state to check when the Ime Appear has finished by opaque color alpha of input method + * and it has finished transforming and scaling. + */ + inner class WaitImeAppearFinished( + tags: MutableMap<Long, MutableList<Tag>>, + private val layerId: Int + ) : BaseState(tags) { + override fun doProcessState( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState { + val isImeAppearFinished = isImeAppearFinished.isSatisfied(current) + + return if (isImeAppearFinished) { + // tag on the last complete state at the start + logger.invoke("(${current.layerState.timestamp}) Ime appear end detected.") + addEndTransitionTag(current, transition, layerId = layerId) + // return to start to wait for a second IME appear + WaitInputMethodVisible(tags) + } else { + logger.invoke("(${current.layerState.timestamp}) Ime appear hasn't finished.") + this + } + } + + private val isImeAppearFinished = ConditionList(listOf( + isLayerVisible(FlickerComponentName.IME), + isLayerColorAlphaOne(FlickerComponentName.IME), + isLayerTransformFlagSet(FlickerComponentName.IME, Transform.TRANSLATE_VAL), + isLayerTransformFlagSet(FlickerComponentName.IME, Transform.SCALE_VAL).negate() + )) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/ImeDisappearProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/ImeDisappearProcessor.kt new file mode 100644 index 000000000..3aff56f3c --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/ImeDisappearProcessor.kt @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.service.processors + +import com.android.server.wm.traces.common.ConditionList +import com.android.server.wm.traces.common.DeviceStateDump +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isImeShown +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerColorAlphaOne +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerTransformFlagSet +import com.android.server.wm.traces.common.layers.LayerTraceEntry +import com.android.server.wm.traces.common.layers.Transform +import com.android.server.wm.traces.common.service.PlatformConsts +import com.android.server.wm.traces.common.tags.Tag +import com.android.server.wm.traces.common.tags.Transition +import com.android.server.wm.traces.common.windowmanager.WindowManagerState + +/** + * This processor creates tags when the keyboard starts and finishes disappearing. + * @param logger logs by invoking any event messages + */ +class ImeDisappearProcessor(logger: (String) -> Unit) : TransitionProcessor(logger) { + override val transition = Transition.IME_DISAPPEAR + override fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>) = + WaitImeDisappearStart(tags) + + /** + * FSM state that waits until the IME begins to disappear + * Different conditions required for IME closing by gesture (layer color alpha < 1), compared + * to IME closing via app close (layer translate SCALE_VAL bit set) + */ + inner class WaitImeDisappearStart( + tags: MutableMap<Long, MutableList<Tag>> + ) : BaseState(tags) { + private val isImeShown = isImeShown(PlatformConsts.DEFAULT_DISPLAY) + private val isImeAppeared = + ConditionList(listOf( + isImeShown, + isLayerColorAlphaOne(FlickerComponentName.IME), + isLayerTransformFlagSet(FlickerComponentName.IME, Transform.TRANSLATE_VAL), + isLayerTransformFlagSet(FlickerComponentName.IME, Transform.SCALE_VAL).negate() + )) + private val isImeDisappearByGesture = + ConditionList(listOf( + isImeShown, + isLayerColorAlphaOne(FlickerComponentName.IME).negate() + )) + private val isImeDisappearByAppClose = + ConditionList(listOf( + isImeShown, + isLayerTransformFlagSet(FlickerComponentName.IME, Transform.SCALE_VAL) + )) + + override fun doProcessState( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState { + if (previous == null) return this + + return if (isImeAppeared.isSatisfied(previous) && + (isImeDisappearByGesture.isSatisfied(current) || + isImeDisappearByAppClose.isSatisfied(current))) { + processImeDisappearing(current) + } else { + logger.invoke("(${current.layerState.timestamp}) IME disappear not started.") + this + } + } + + private fun processImeDisappearing( + current: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState { + logger.invoke("(${current.layerState.timestamp}) IME disappear started.") + val inputMethodLayer = current.layerState.visibleLayers.first { + it.name.contains(FlickerComponentName.IME.toLayerName()) + } + addStartTransitionTag(current, transition, layerId = inputMethodLayer.id) + return WaitImeDisappearFinished(tags, inputMethodLayer.id) + } + } + + /** + * FSM state to check when the IME disappear has finished i.e. when the input method layer is + * no longer visible. + */ + inner class WaitImeDisappearFinished( + tags: MutableMap<Long, MutableList<Tag>>, + private val layerId: Int + ) : BaseState(tags) { + private val imeNotShown = isImeShown(PlatformConsts.DEFAULT_DISPLAY).negate() + override fun doProcessState( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState { + return if (imeNotShown.isSatisfied(current)) { + // tag on the last complete state at the start + logger.invoke("(${current.layerState.timestamp}) IME disappear end detected.") + addEndTransitionTag(current, transition, layerId = layerId) + // return to start to wait for a second IME disappear + WaitImeDisappearStart(tags) + } else { + logger.invoke("(${current.layerState.timestamp}) IME disappear not finished.") + this + } + } + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipEnterProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipEnterProcessor.kt new file mode 100644 index 000000000..4ec409952 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipEnterProcessor.kt @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.service.processors + +import com.android.server.wm.traces.common.ConditionList +import com.android.server.wm.traces.common.DeviceStateDump +import com.android.server.wm.traces.common.WindowManagerConditionsFactory +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.hasPipWindow +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isAppTransitionIdle +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isPipWindowLayerSizeMatch +import com.android.server.wm.traces.common.layers.Layer +import com.android.server.wm.traces.common.layers.LayerTraceEntry +import com.android.server.wm.traces.common.layers.Transform +import com.android.server.wm.traces.common.service.PlatformConsts +import com.android.server.wm.traces.common.tags.Tag +import com.android.server.wm.traces.common.tags.Transition +import com.android.server.wm.traces.common.windowmanager.WindowManagerState + +/** + * Processor to detect PIP mode enter. + * Waits for a window to enter pip mode, then tags transition start at the last moment before + * corresponding layer started scaling. + * Tags transition end when window and layer stop animating and their sizes match. + */ +class PipEnterProcessor(logger: (String) -> Unit) : TransitionProcessor(logger) { + override val transition = Transition.PIP_ENTER + private val allScalingLayers = mutableMapOf<Int, Long>() + + override fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>) = + WaitPipEnterStart(tags) + + /** + * FSM state that waits for a pinned window to appear. Until this occurs, it watches all + * scaling layers, recording when they started to scale in the companion object. When the + * pinned window has appeared, it adds a start tag at the timestamp at which the layer with + * the same id as the pinned window layerId started to scale. + */ + inner class WaitPipEnterStart( + tags: MutableMap<Long, MutableList<Tag>> + ) : BaseState(tags) { + + private fun getScalingLayers( + current: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): List<Layer> = current.layerState.flattenedLayers.filter { + WindowManagerConditionsFactory.isLayerTransformFlagSet( + it.id, + Transform.SCALE_VAL + ).isSatisfied(current) + } + + override fun doProcessState( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState { + if (previous == null) return this + + // get current scaling layers and add to allScalingLayers map for whole trace + val scalingLayers = getScalingLayers(current) + val scalingLayerIds = scalingLayers.map { it.id } + scalingLayerIds.forEach { + allScalingLayers.getOrPut(it) { previous.layerState.timestamp } + } + + // remove all layers that are no longer scaling from allScalingLayers map + val notScalingLayerIds = allScalingLayers.keys.filter { !scalingLayerIds.contains(it) } + notScalingLayerIds.forEach { + allScalingLayers.remove(it) + } + + return if (hasPipWindow().isSatisfied(current) && + hasPipWindow().negate().isSatisfied(previous)) { + processPipEnterStart(current) + } else { + logger.invoke("(${current.layerState.timestamp}) PIP enter not started.") + this + } + } + + private fun processPipEnterStart( + current: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState { + val pipWindow = current.wmState.pinnedWindows.first() + val startTimestamp = allScalingLayers[pipWindow.layerId] + + if (startTimestamp != null) { + addStartTransitionTag( + current, + transition, + layerId = pipWindow.layerId, + timestamp = startTimestamp + ) + } else { + addStartTransitionTag( + current, + transition, + layerId = pipWindow.layerId + ) + } + // reset all scaling layers + allScalingLayers.clear() + + logger.invoke("($startTimestamp) PIP enter started.") + return WaitPipEnterFinished(tags, pipWindow.layerId) + } + } + + /** + * FSM state to check when the PIP enter has finished. This is when the pinned window + * has the same size as the associated layer. + */ + inner class WaitPipEnterFinished( + tags: MutableMap<Long, MutableList<Tag>>, + private val layerId: Int + ) : BaseState(tags) { + override fun doProcessState( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState { + return if (isPipEnterFinished.isSatisfied(current)) { + // tag on the last complete state at the start + logger.invoke("(${current.wmState.timestamp}) PIP enter finished.") + addEndTransitionTag(current, transition, layerId = layerId) + + // return to start to wait for a second PIP enter + WaitPipEnterStart(tags) + } else { + logger.invoke("(${current.wmState.timestamp}) PIP enter not finished.") + this + } + } + + private val isPipEnterFinished = ConditionList(listOf( + isAppTransitionIdle(PlatformConsts.DEFAULT_DISPLAY), + hasPipWindow(), + isPipWindowLayerSizeMatch(layerId) + )) + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipExitProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipExitProcessor.kt new file mode 100644 index 000000000..3ed713e8d --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipExitProcessor.kt @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.service.processors + +import com.android.server.wm.traces.common.DeviceStateDump +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.hasLayersAnimating +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isAppTransitionIdle +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerColorAlphaOne +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerTransformFlagSet +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerVisible +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isWMStateComplete +import com.android.server.wm.traces.common.layers.LayerTraceEntry +import com.android.server.wm.traces.common.layers.Transform +import com.android.server.wm.traces.common.tags.Tag +import com.android.server.wm.traces.common.tags.Transition +import com.android.server.wm.traces.common.windowmanager.WindowManagerState + +/** + * This processor creates tags when the pip window is exited directly to home screen or a + * different app altogether. + * @param logger logs by invoking any event messages + */ +class PipExitProcessor(logger: (String) -> Unit) : TransitionProcessor(logger) { + override val transition = Transition.PIP_EXIT + private val scalingWindows = + HashMap<String, DeviceStateDump<WindowManagerState, LayerTraceEntry>>() + private val areLayersAnimating = hasLayersAnimating() + private val wmStateIdle = isAppTransitionIdle(/* default display */ 0) + private val wmStateComplete = isWMStateComplete() + + override fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>) = + WaitPinnedWindowSwipedOrFading(tags) + + /** + * Initial FSM state which waits until the app window in pip mode starts to change opacity. + */ + inner class WaitPinnedWindowSwipedOrFading( + tags: MutableMap<Long, MutableList<Tag>> + ) : BaseState(tags) { + override fun doProcessState( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState { + if (previous == null) return this + // There can only ever be one pinned window at a time + val nextPinnedWindow = next.wmState.pinnedWindows.firstOrNull() + val currPinnedWindow = current.wmState.pinnedWindows.firstOrNull() ?: return this + + if (nextPinnedWindow == null) { + val dump = scalingWindows[currPinnedWindow.token] + if (dump != null) { + // Pip app unpinned so let's tag. + addStartTransitionTag(dump, transition, + layerId = currPinnedWindow.layerId, + windowToken = currPinnedWindow.token + ) + addEndTransitionTag(previous, transition, + layerId = currPinnedWindow.layerId, + windowToken = currPinnedWindow.token + ) + } + return this + } + + // close pip by swiping + val isScaling = isLayerTransformFlagSet(currPinnedWindow.layerId, Transform.SCALE_VAL) + val movingPinnedWindow = + isScaling.isSatisfied(current) && isScaling.negate().isSatisfied(previous) + + // close pip by pressing dismiss button + val colorAlphaIsOne = isLayerColorAlphaOne(currPinnedWindow.layerId) + val pinnedWindowFading = colorAlphaIsOne.negate().isSatisfied(current) && + colorAlphaIsOne.isSatisfied(previous) && + isScaling.negate().isSatisfied(current) + + return when { + movingPinnedWindow -> { + // Record last time when pip app started scaling + scalingWindows[currPinnedWindow.token] = current + this + } + pinnedWindowFading -> { + addStartTransitionTag(previous, transition, + layerId = currPinnedWindow.layerId, + windowToken = currPinnedWindow.token + ) + WaitUntilPipColorAlphaIsOneAndInvisible(tags, currPinnedWindow.layerId) + } + else -> { + this + } + } + } + } + + inner class WaitUntilPipColorAlphaIsOneAndInvisible( + tags: MutableMap<Long, MutableList<Tag>>, + private val layerId: Int + ) : BaseState(tags) { + override fun doProcessState( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState { + val layerInvisible = isLayerVisible(layerId).negate().isSatisfied(current) + val layerColorAlphaOne = isLayerColorAlphaOne(layerId).isSatisfied(current) + val isStableState = wmStateIdle.isSatisfied(current) || + wmStateComplete.isSatisfied(current) || + areLayersAnimating.negate().isSatisfied(current) + + return if (layerInvisible && layerColorAlphaOne && isStableState) { + addEndTransitionTag(current, transition, layerId = layerId) + WaitPinnedWindowSwipedOrFading(tags) + } else { + this + } + } + } +} diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipExpandProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipExpandProcessor.kt new file mode 100644 index 000000000..c15b731df --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipExpandProcessor.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.service.processors + +import com.android.server.wm.traces.common.DeviceStateDump +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerTransformFlagSet +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerTransformIdentity +import com.android.server.wm.traces.common.layers.LayerTraceEntry +import com.android.server.wm.traces.common.layers.Transform +import com.android.server.wm.traces.common.tags.Tag +import com.android.server.wm.traces.common.tags.Transition +import com.android.server.wm.traces.common.windowmanager.WindowManagerState + +/** + * This processor creates tags when an pip window starts to expand from window to full screen app. + * @param logger logs by invoking any event messages + */ +class PipExpandProcessor(logger: (String) -> Unit) : TransitionProcessor(logger) { + override val transition = Transition.PIP_EXPAND + private val scalingLayers = + HashMap<Int, DeviceStateDump<WindowManagerState, LayerTraceEntry>>() + + override fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>) = + WaitUntilAppIsNoLongerPinned(tags) + + /** + * We wait until the pip app is no longer pinned and is ready to expand. + */ + inner class WaitUntilAppIsNoLongerPinned( + tags: MutableMap<Long, MutableList<Tag>> + ) : BaseState(tags) { + override fun doProcessState( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState { + // Pinned window is no longer pinned + val prevPinnedWindows = previous?.wmState?.pinnedWindows?.toList() ?: emptyList() + val currPinnedWindows = current.wmState.pinnedWindows.toList() + + if (prevPinnedWindows.isNotEmpty() && currPinnedWindows.isEmpty()) { + return WaitUntilAppCompletesExpanding(tags, prevPinnedWindows.first().layerId) + } + return this + } + } + + /** + * FSMState when app has been unpinned and we track its corresponding layer. + * We record every time the layer is scaling and check it has transform identity + * with increased bounds. + */ + inner class WaitUntilAppCompletesExpanding( + tags: MutableMap<Long, MutableList<Tag>>, + private val layerId: Int + ) : BaseState(tags) { + private val isScaling = isLayerTransformFlagSet(layerId, Transform.SCALE_VAL) + private val isIdentity = isLayerTransformIdentity(layerId) + override fun doProcessState( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState { + if (previous == null) return this + if (previous.wmState.pinnedWindows.isNotEmpty()) { + return WaitUntilAppIsNoLongerPinned(tags) + } + + val startedScaling = isScaling.isSatisfied(current) && + isScaling.negate().isSatisfied(previous) + + val currLayerBounds = current.layerState.getLayerById(layerId)?.bounds ?: return this + val prevLayerBounds = previous.layerState.getLayerById(layerId)?.bounds ?: return this + val finishedExpanding = isScaling.isSatisfied(previous) && + isIdentity.isSatisfied(current) && + currLayerBounds.height > prevLayerBounds.height && + currLayerBounds.width > prevLayerBounds.width + + if (startedScaling) { + scalingLayers[layerId] = current + } else if (finishedExpanding) { + val dump = scalingLayers[layerId] + if (dump != null) { + addStartTransitionTag(current, transition, + layerId = layerId, + timestamp = dump.layerState.timestamp + ) + addEndTransitionTag(current, transition, + layerId = layerId, + timestamp = current.layerState.timestamp + ) + scalingLayers.clear() + return WaitUntilAppIsNoLongerPinned(tags) + } + } + return this + } + } +} diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipResizeProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipResizeProcessor.kt new file mode 100644 index 000000000..7bb89d23b --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/PipResizeProcessor.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.service.processors + +import com.android.server.wm.traces.common.DeviceStateDump +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isLayerTransformFlagSet +import com.android.server.wm.traces.common.layers.LayerTraceEntry +import com.android.server.wm.traces.common.layers.Transform +import com.android.server.wm.traces.common.tags.Tag +import com.android.server.wm.traces.common.tags.Transition +import com.android.server.wm.traces.common.windowmanager.WindowManagerState + +/** + * This processor creates tags when the pip window is resized but is still in pip mode. + * @param logger logs by invoking any event messages + */ +class PipResizeProcessor(logger: (String) -> Unit) : TransitionProcessor(logger) { + override val transition = Transition.PIP_RESIZE + private val scalingWindows = + HashMap<String, DeviceStateDump<WindowManagerState, LayerTraceEntry>>() + + override fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>) = + WaitUntilAppStopsAnimatingYetStillPinned(tags) + + inner class WaitUntilAppStopsAnimatingYetStillPinned( + tags: MutableMap<Long, MutableList<Tag>> + ) : BaseState(tags) { + override fun doProcessState( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState { + val currPinnedWindow = current.wmState.pinnedWindows.firstOrNull() ?: return this + previous?.wmState?.pinnedWindows?.firstOrNull() ?: return this + + val isScaling = isLayerTransformFlagSet(currPinnedWindow.layerId, Transform.SCALE_VAL) + val startedScaling = isScaling.negate().isSatisfied(previous) && + isScaling.isSatisfied(current) + if (startedScaling) { + // remember when pinned window/layer starting scaling + scalingWindows[currPinnedWindow.token] = previous + } + + // Bounds have changed and layer no longer scaling + val currBounds = current.layerState.getLayerById(currPinnedWindow.layerId)?.bounds + val prevBounds = previous.layerState.getLayerById(currPinnedWindow.layerId)?.bounds + val finishedResizing = isScaling.isSatisfied(previous) && + isScaling.negate().isSatisfied(current) && + (currBounds?.height != prevBounds?.height) && + (currBounds?.width != prevBounds?.width) + + if (finishedResizing) { + val lastScaledDump = scalingWindows[currPinnedWindow.token] + if (lastScaledDump != null) { + addStartTransitionTag(lastScaledDump, transition, + layerId = currPinnedWindow.layerId, + windowToken = currPinnedWindow.token, + timestamp = lastScaledDump.layerState.timestamp + ) + addEndTransitionTag(lastScaledDump, transition, + layerId = currPinnedWindow.layerId, + windowToken = currPinnedWindow.token, + timestamp = current.layerState.timestamp + ) + scalingWindows.remove(currPinnedWindow.token) + } + } + return this + } + } +} diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/RotationProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/RotationProcessor.kt new file mode 100644 index 000000000..be29cedef --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/RotationProcessor.kt @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.service.processors + +import com.android.server.wm.traces.common.DeviceStateDump +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.common.RectF +import com.android.server.wm.traces.common.WindowManagerConditionsFactory +import com.android.server.wm.traces.common.layers.LayerTraceEntry +import com.android.server.wm.traces.common.tags.Tag +import com.android.server.wm.traces.common.tags.Transition +import com.android.server.wm.traces.common.windowmanager.WindowManagerState + +/** + * Processor to detect rotations. + * + * First check the WM state for a rotation change, then wait the SF rotation + * to occur and both nav and status bars to appear + */ +class RotationProcessor(logger: (String) -> Unit) : TransitionProcessor(logger) { + override val transition = Transition.ROTATION + override fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>) = InitialState(tags) + + /** + * Initial FSM state, obtains the current display size and start searching + * for display size changes + */ + inner class InitialState( + tags: MutableMap<Long, MutableList<Tag>> + ) : BaseState(tags) { + override fun doProcessState( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState { + val currDisplayRect = current.wmState.displaySize() + logger.invoke("(${current.wmState.timestamp}) Initial state. " + + "Display size $currDisplayRect") + return WaitDisplayRectChange(tags, currDisplayRect) + } + } + + /** + * FSM state when the display size has not changed since [InitialState] + */ + inner class WaitDisplayRectChange( + tags: MutableMap<Long, MutableList<Tag>>, + private val currDisplayRect: RectF + ) : BaseState(tags) { + override fun doProcessState( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState { + val newWmDisplayRect = current.wmState.displaySize() + val newLayersDisplayRect = current.layerState.screenBounds() + + return when { + // WM display changed first (Regular rotation) + // SF display changed first (Seamless rotation) + newWmDisplayRect != currDisplayRect || newLayersDisplayRect != currDisplayRect -> { + requireNotNull(previous) { "Should have a previous state" } + val rect = if (newWmDisplayRect != currDisplayRect) { + newWmDisplayRect + } else { + newLayersDisplayRect + } + processDisplaySizeChange(previous, rect) + } + else -> { + logger.invoke("(${current.wmState.timestamp}) No display size change") + this + } + } + } + + private fun processDisplaySizeChange( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + newDisplayRect: RectF + ): FSMState { + logger.invoke("(${previous.wmState.timestamp}) Display size changed " + + "to $newDisplayRect") + // tag on the last complete state at the start + logger.invoke("(${previous.wmState.timestamp}) Tagging transition start") + addStartTransitionTag(previous, transition) + return WaitRotationFinished(tags) + } + } + + /** + * FSM state for when the animation occurs in the SF trace + */ + inner class WaitRotationFinished(tags: MutableMap<Long, MutableList<Tag>>) : BaseState(tags) { + private val rotationLayerExists = WindowManagerConditionsFactory + .isLayerVisible(FlickerComponentName.ROTATION) + private val backSurfaceLayerExists = WindowManagerConditionsFactory + .isLayerVisible(FlickerComponentName.BACK_SURFACE) + private val areLayersAnimating = WindowManagerConditionsFactory.hasLayersAnimating() + private val wmStateIdle = WindowManagerConditionsFactory + .isAppTransitionIdle(/* default display */ 0) + private val wmStateComplete = WindowManagerConditionsFactory.isWMStateComplete() + + override fun doProcessState( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState { + val anyLayerAnimating = areLayersAnimating.isSatisfied(current) + val rotationLayerExists = rotationLayerExists.isSatisfied(current) + val blackSurfaceLayerExists = backSurfaceLayerExists.isSatisfied(current) + val wmStateIdle = wmStateIdle.isSatisfied(current) + val wmStateComplete = wmStateComplete.isSatisfied(current) + + val newWmDisplayRect = current.wmState.displaySize() + val newLayersDisplayRect = current.layerState.screenBounds() + val displaySizeDifferent = newWmDisplayRect != newLayersDisplayRect + + val inRotation = anyLayerAnimating || rotationLayerExists || blackSurfaceLayerExists || + displaySizeDifferent || !wmStateIdle || !wmStateComplete + logger.invoke("(${current.layerState.timestamp}) " + + "In rotation? $inRotation (" + + "anyLayerAnimating=$anyLayerAnimating, " + + "blackSurfaceLayerExists=$blackSurfaceLayerExists, " + + "rotationLayerExists=$rotationLayerExists, " + + "wmStateIdle=$wmStateIdle, " + + "wmStateComplete=$wmStateComplete, " + + "displaySizeDifferent=$displaySizeDifferent)") + return if (inRotation) { + this + } else { + // tag on the last complete state at the start + logger.invoke("(${current.layerState.timestamp}) Tagging transition end") + addEndTransitionTag(current, transition) + // return to start to wait for a second rotation + val lastDisplayRect = current.wmState.displaySize() + WaitDisplayRectChange(tags, lastDisplayRect) + } + } + } + + companion object { + private fun LayerTraceEntry.screenBounds() = this.displays.minByOrNull { it.id } + ?.layerStackSpace?.toRectF() ?: this.children + .sortedBy { it.id } + .firstOrNull { it.isRootLayer } + ?.screenBounds ?: error("Unable to identify screen bounds (display is empty in proto)") + + private fun WindowManagerState.displaySize() = getDefaultDisplay() + ?.displayRect?.toRectF() ?: RectF.EMPTY + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/TransitionProcessor.kt b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/TransitionProcessor.kt new file mode 100644 index 000000000..31b30ba97 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/service/processors/TransitionProcessor.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.service.processors + +import com.android.server.wm.traces.common.DeviceStateDump +import com.android.server.wm.traces.common.layers.LayerTraceEntry +import com.android.server.wm.traces.common.layers.LayersTrace +import com.android.server.wm.traces.common.service.ITagProcessor +import com.android.server.wm.traces.common.tags.Tag +import com.android.server.wm.traces.common.tags.TagState +import com.android.server.wm.traces.common.tags.TagTrace +import com.android.server.wm.traces.common.tags.Transition +import com.android.server.wm.traces.common.windowmanager.WindowManagerState +import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace + +/** + * This class implements the relevant methods such as generating tags, creating dumps for the + * WindowManager and SurfaceFlinger traces, and ensuring the 1:1 correspondence between the start + * and end tags invariant is maintained by [BaseFsmState]. + */ +abstract class TransitionProcessor(internal val logger: (String) -> Unit) : ITagProcessor { + abstract val transition: Transition + abstract fun getInitialState(tags: MutableMap<Long, MutableList<Tag>>): BaseState + + abstract inner class BaseState( + tags: MutableMap<Long, MutableList<Tag>> + ) : BaseFsmState(tags, logger, transition) { + abstract override fun doProcessState( + previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>?, + current: DeviceStateDump<WindowManagerState, LayerTraceEntry>, + next: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ): FSMState + } + + /** + * Add the start and end tags corresponding to the transition from + * the WindowManager and SurfaceFlinger traces + * @param wmTrace - WindowManager trace + * @param layersTrace - SurfaceFlinger trace + * @return [TagTrace] - containing all the newly generated tags in states with + * timestamps + */ + override fun generateTags( + wmTrace: WindowManagerTrace, + layersTrace: LayersTrace + ): TagTrace { + val tags = mutableMapOf<Long, MutableList<Tag>>() + var currPosition: FSMState? = getInitialState(tags) + + val dumpList = createDumpList(wmTrace, layersTrace) + val dumpIterator = dumpList.iterator() + + // keep always a reference to previous, current and next states + var previous: DeviceStateDump<WindowManagerState, LayerTraceEntry>? + var current: DeviceStateDump<WindowManagerState, LayerTraceEntry>? = null + var next: DeviceStateDump<WindowManagerState, LayerTraceEntry>? = dumpIterator.next() + while (currPosition != null) { + previous = current + current = next + next = if (dumpIterator.hasNext()) dumpIterator.next() else null + requireNotNull(current) { "Current state shouldn't be null" } + val newPosition = currPosition.process(previous, current, next) + currPosition = newPosition + } + + return buildTagTrace(tags) + } + + private fun buildTagTrace(tags: MutableMap<Long, MutableList<Tag>>): TagTrace { + val tagStates = tags.map { entry -> + val timestamp = entry.key + val stateTags = entry.value + TagState(timestamp.toString(), stateTags.toTypedArray()) + } + return TagTrace(tagStates.toTypedArray(), source = "") + } + + companion object { + internal fun createDumpList( + wmTrace: WindowManagerTrace, + layersTrace: LayersTrace + ): List<DeviceStateDump<WindowManagerState, LayerTraceEntry>> { + val wmTimestamps = wmTrace.map { it.timestamp }.toTypedArray() + val layersTimestamps = layersTrace.map { it.timestamp }.toTypedArray() + val fullTimestamps = setOf(*wmTimestamps, *layersTimestamps).sorted() + + return fullTimestamps.map { baseTimestamp -> + val wmState = wmTrace + .lastOrNull { it.timestamp <= baseTimestamp } + ?: wmTrace.first() + val layerState = layersTrace + .lastOrNull { it.timestamp <= baseTimestamp } + ?: layersTrace.first() + DeviceStateDump(wmState, layerState) + }.distinctBy { Pair(it.wmState.timestamp, it.layerState.timestamp) } + } + } +} diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/tags/Tag.kt b/libraries/flicker/src/com/android/server/wm/traces/common/tags/Tag.kt new file mode 100644 index 000000000..c5ecc7da1 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/tags/Tag.kt @@ -0,0 +1,28 @@ +package com.android.server.wm.traces.common.tags + +/** + * Tag Class relating to a particular transition event in a WindowManager + * or SurfaceFlinger trace state. + * @param id The id to match the end and start tags + * @param transition Transition the tag represents the transition + * @param isStartTag Tag represents the start or end moment in transition + * @param layerId The Layer the tag is associated with (or 0 if no taskId associated with it) + * @param windowToken The Window the tag is associated + * with (or empty string if no taskId associated with it) + * @param taskId The Task the tag is associated with (or 0 if no taskId associated with it) + */ +data class Tag( + val id: Int, + val transition: Transition, + val isStartTag: Boolean, + val layerId: Int = 0, + val windowToken: String = "", + val taskId: Int = 0 +) { + override fun toString(): String { + if (isStartTag) { + return "Start Of $transition" + } + return "End Of $transition" + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/tags/TagState.kt b/libraries/flicker/src/com/android/server/wm/traces/common/tags/TagState.kt new file mode 100644 index 000000000..adfc163c6 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/tags/TagState.kt @@ -0,0 +1,37 @@ +package com.android.server.wm.traces.common.tags + +import com.android.server.wm.traces.common.ITraceEntry +import com.android.server.wm.traces.common.prettyTimestamp + +/** + * Holds the list of tags corresponding to a particular state at a particular time in trace. + * + * The timestamp constructor must be a string due to lack of Kotlin/KotlinJS Long compatibility + * + * @param _timestamp Timestamp of the state + * @param tags Array of tags contained in the state + * @param isFallback False indicate if the tag timestamp was found or true if a default tag is made + */ +class TagState( + _timestamp: String, + val tags: Array<Tag>, + val isFallback: Boolean = false +) : ITraceEntry { + override val timestamp: Long = _timestamp.toLong() + override fun toString(): String = "FlickerTagState(timestamp=${prettyTimestamp(timestamp)}, " + + "tags=$tags)" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is TagState) return false + if (timestamp != other.timestamp) return false + if (!tags.contentEquals(other.tags)) return false + return true + } + + override fun hashCode(): Int { + var result = timestamp.hashCode() + result = 31 * result + tags.contentDeepHashCode() + return result + } +} diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/tags/TagTrace.kt b/libraries/flicker/src/com/android/server/wm/traces/common/tags/TagTrace.kt new file mode 100644 index 000000000..9aa578fac --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/tags/TagTrace.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.tags + +import com.android.server.wm.traces.common.ITrace + +/** + * Holds the entire list of [TagState]s representing an entire trace that has been tagged. + * @param entries Array of tagged states within the trace + * @param source Source of the trace file + */ +data class TagTrace( + override val entries: Array<TagState>, + override val source: String +) : ITrace<TagState>, + List<TagState> by entries.toList() { + override fun toString(): String = "FlickerTagTrace(${entries.firstOrNull()?.timestamp ?: 0}, " + + "${entries.lastOrNull()?.timestamp ?: 0})" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is TagTrace) return false + if (!entries.contentEquals(other.entries)) return false + if (source != other.source) return false + return true + } + + override fun hashCode(): Int { + var result = entries.contentDeepHashCode() + result = 31 * result + source.hashCode() + return result + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/tags/Transition.kt b/libraries/flicker/src/com/android/server/wm/traces/common/tags/Transition.kt new file mode 100644 index 000000000..204d3e253 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/tags/Transition.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.tags + +/** + * Represents all the possible transitions to be tagged. + */ +enum class Transition(private val transitionName: String) { + ROTATION("Rotation"), + APP_LAUNCH("AppLaunching"), + APP_CLOSE("AppClosing"), + PIP_ENTER("PipEntering"), + PIP_RESIZE("PipResizing"), + PIP_EXPAND("PipExpanding"), + PIP_EXIT("PipExiting"), + IME_APPEAR("ImeAppearing"), + IME_DISAPPEAR("ImeDisappearing"); +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/tags/TransitionTag.kt b/libraries/flicker/src/com/android/server/wm/traces/common/tags/TransitionTag.kt new file mode 100644 index 000000000..d2be4af41 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/tags/TransitionTag.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.tags + +/** + * Saves the information about a transition tag. + */ +data class TransitionTag( + var tag: Tag, + var startTimestamp: Long, + var endTimestamp: Long +) { + fun isEmpty(): Boolean { + return this.tag.layerId == 0 && + this.tag.taskId == 0 && + this.tag.windowToken.isEmpty() + } +} diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerState.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerState.kt index 34b7598d9..ccb31f134 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerState.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerState.kt @@ -16,15 +16,13 @@ package com.android.server.wm.traces.common.windowmanager -import com.android.server.wm.traces.common.Rect import com.android.server.wm.traces.common.ITraceEntry import com.android.server.wm.traces.common.prettyTimestamp import com.android.server.wm.traces.common.windowmanager.windows.Activity -import com.android.server.wm.traces.common.windowmanager.windows.DisplayArea import com.android.server.wm.traces.common.windowmanager.windows.DisplayContent import com.android.server.wm.traces.common.windowmanager.windows.KeyguardControllerState import com.android.server.wm.traces.common.windowmanager.windows.RootWindowContainer -import com.android.server.wm.traces.common.windowmanager.windows.ActivityTask +import com.android.server.wm.traces.common.windowmanager.windows.Task import com.android.server.wm.traces.common.windowmanager.windows.WindowContainer import com.android.server.wm.traces.common.windowmanager.windows.WindowManagerPolicy import com.android.server.wm.traces.common.windowmanager.windows.WindowState @@ -35,6 +33,8 @@ import com.android.server.wm.traces.common.windowmanager.windows.WindowState * This is a generic object that is reused by both Flicker and Winscope and cannot * access internal Java/Android functionality * + * The timestamp constructor must be a string due to lack of Kotlin/KotlinJS Long compatibility + * **/ open class WindowManagerState( val where: String, @@ -48,8 +48,9 @@ open class WindowManagerState( val pendingActivities: Array<String>, val root: RootWindowContainer, val keyguardControllerState: KeyguardControllerState, - override val timestamp: Long = 0 + _timestamp: String = "0" ) : ITraceEntry { + override val timestamp: Long = _timestamp.toLong() val isVisible: Boolean = true val stableId: String get() = this::class.simpleName ?: error("Unable to determine class") val name: String get() = prettyTimestamp(timestamp) @@ -65,7 +66,7 @@ open class WindowManagerState( get() = windowContainers.filterIsInstance<DisplayContent>().toTypedArray() // Stacks in z-order with the top most at the front of the list, starting with primary display. - val rootTasks: Array<ActivityTask> + val rootTasks: Array<Task> get() = displays.flatMap { it.rootTasks.toList() }.toTypedArray() // Windows in z-order with the top most at the front of the list. @@ -86,9 +87,17 @@ open class WindowManagerState( get() = windowStates .dropWhile { !appWindows.contains(it) }.drop(appWindows.size).toTypedArray() val visibleWindows: Array<WindowState> - get() = windowStates.filter { it.isSurfaceShown }.toTypedArray() + get() = windowStates + .filter { it.isVisible } + .filter { window -> + val activities = getActivitiesForWindow(window.title) + val activity = activities.firstOrNull { it.children.contains(window) } + activity?.isVisible ?: true + } + .toTypedArray() val topVisibleAppWindow: String - get() = appWindows.filter { it.isVisible } + get() = visibleWindows + .filter { it.isAppWindow } .map { it.title } .firstOrNull() ?: "" val pinnedWindows: Array<WindowState> @@ -96,6 +105,12 @@ open class WindowManagerState( .filter { it.windowingMode == WINDOWING_MODE_PINNED } .toTypedArray() + /** + * Checks if the device state supports rotation, i.e., if the rotation sensor is + * enabled (e.g., launcher) and if the rotation not fixed + */ + val canRotate: Boolean + get() = policy?.isFixedOrientation != true && policy?.isOrientationNoSensor != true val focusedDisplay: DisplayContent? get() = getDisplay(focusedDisplayId) val focusedStackId: Int get() = focusedDisplay?.focusedRootTaskId ?: -1 val focusedActivity: String get() { @@ -103,46 +118,19 @@ open class WindowManagerState( return if (focusedDisplay != null && focusedDisplay.resumedActivity.isNotEmpty()) { focusedDisplay.resumedActivity } else { - getActivityForWindow(focusedWindow, focusedDisplayId)?.name ?: "" + getActivitiesForWindow(focusedWindow, focusedDisplayId).firstOrNull()?.name ?: "" } } - val resumedActivitiesInDisplays: Array<String> - get() = displays.flatMap { display -> - display.rootTasks.flatMap { it.resumedActivities.toList() } - }.toTypedArray() - val defaultPinnedStackBounds: Rect - get() = displays - .lastOrNull { it.defaultPinnedStackBounds.isNotEmpty }?.defaultPinnedStackBounds - ?: Rect.EMPTY - val pinnedStackMovementBounds: Rect - get() = displays - .lastOrNull { it.defaultPinnedStackBounds.isNotEmpty }?.pinnedStackMovementBounds - ?: Rect.EMPTY - val focusedStackActivityType: Int - get() = getRootTask(focusedStackId)?.activityType ?: ACTIVITY_TYPE_UNDEFINED - val focusedStackWindowingMode: Int - get() = getRootTask(focusedStackId)?.windowingMode ?: WINDOWING_MODE_UNDEFINED val resumedActivities: Array<String> get() = rootTasks.flatMap { it.resumedActivities.toList() }.toTypedArray() val resumedActivitiesCount: Int get() = resumedActivities.size val stackCount: Int get() = rootTasks.size - val displayCount: Int get() = displays.size - val homeTask: ActivityTask? get() = getStackByActivityType(ACTIVITY_TYPE_HOME)?.topTask - val recentsTask: ActivityTask? get() = getStackByActivityType(ACTIVITY_TYPE_RECENTS)?.topTask + val homeTask: Task? get() = getStackByActivityType(ACTIVITY_TYPE_HOME)?.topTask + val recentsTask: Task? get() = getStackByActivityType(ACTIVITY_TYPE_RECENTS)?.topTask val homeActivity: Activity? get() = homeTask?.activities?.lastOrNull() val recentsActivity: Activity? get() = recentsTask?.activities?.lastOrNull() - val rootTasksCount: Int get() = rootTasks.size val isRecentsActivityVisible: Boolean get() = recentsActivity?.isVisible ?: false - val dreamTask: ActivityTask? - get() = getStackByActivityType(ACTIVITY_TYPE_DREAM)?.topTask - val defaultDisplayLastTransition: String get() = getDefaultDisplay()?.lastTransition - ?: "Default display not found" - val defaultDisplayAppTransitionState: String get() = getDefaultDisplay()?.appTransitionState - ?: "Default display not found" - val allNavigationBarStates: Array<WindowState> - get() = windowStates.filter { it.isValidNavBarType }.toTypedArray() val frontWindow: String? get() = windowStates.map { it.title }.firstOrNull() - val stableBounds: Rect get() = getDefaultDisplay()?.stableBounds ?: Rect.EMPTY val inputMethodWindowState: WindowState? get() = getWindowStateForAppToken(inputMethodWindowAppToken) @@ -152,56 +140,6 @@ open class WindowManagerState( fun getDisplay(displayId: Int): DisplayContent? = displays.firstOrNull { it.id == displayId } - fun getTaskDisplayArea(activityName: String): DisplayArea? { - val result = displays.mapNotNull { it.getTaskDisplayArea(activityName) } - - if (result.size > 1) { - throw IllegalArgumentException( - "There must be exactly one activity among all TaskDisplayAreas.") - } - - return result.firstOrNull() - } - - fun getFrontRootTaskId(displayId: Int): Int = - getDisplay(displayId)?.rootTasks?.first()?.rootTaskId ?: 0 - - fun getFrontStackActivityType(displayId: Int): Int = - getDisplay(displayId)?.rootTasks?.first()?.activityType ?: 0 - - fun getFrontStackWindowingMode(displayId: Int): Int = - getDisplay(displayId)?.rootTasks?.first()?.windowingMode ?: 0 - - fun getTopActivityName(displayId: Int): String { - return getDisplay(displayId) - ?.rootTasks?.firstOrNull() - ?.topTask - ?.activities?.firstOrNull() - ?.title - ?: "" - } - - fun getResumedActivitiesCountInPackage(packageName: String): Int { - val componentPrefix = "$packageName/" - var count = 0 - displays.forEach { display -> - display.rootTasks.forEach { task -> - count += task.resumedActivities.count { - it.isNotEmpty() && it.startsWith(componentPrefix) - } - } - } - return count - } - - fun getResumedActivity(displayId: Int): String { - return getDisplay(displayId)?.resumedActivity ?: "" - } - - fun containsStack(windowingMode: Int, activityType: Int): Boolean { - return countStacks(windowingMode, activityType) > 0 - } - fun countStacks(windowingMode: Int, activityType: Int): Int { var count = 0 for (stack in rootTasks) { @@ -216,7 +154,7 @@ open class WindowManagerState( return count } - fun getRootTask(taskId: Int): ActivityTask? = + fun getRootTask(taskId: Int): Task? = rootTasks.firstOrNull { it.rootTaskId == taskId } fun getRotation(displayId: Int): Int = @@ -225,223 +163,53 @@ open class WindowManagerState( fun getOrientation(displayId: Int): Int = getDisplay(displayId)?.lastOrientation ?: error("Default display not found") - fun getStackByActivityType(activityType: Int): ActivityTask? = + fun getStackByActivityType(activityType: Int): Task? = rootTasks.firstOrNull { it.activityType == activityType } - fun getStandardStackByWindowingMode(windowingMode: Int): ActivityTask? = + fun getStandardStackByWindowingMode(windowingMode: Int): Task? = rootTasks.firstOrNull { it.activityType == ACTIVITY_TYPE_STANDARD && it.windowingMode == windowingMode } - fun getStandardTaskCountByWindowingMode(windowingMode: Int): Int { - var count = 0 - for (stack in rootTasks) { - if (stack.activityType != ACTIVITY_TYPE_STANDARD) { - continue - } - if (stack.windowingMode == windowingMode) { - count += if (stack.tasks.isEmpty()) 1 else stack.tasks.size - } - } - return count - } - - /** Get the stack on its display. */ - fun getStackByActivity(activityName: String): ActivityTask? { - return displays.map { display -> - display.rootTasks.reversed().firstOrNull { stack -> - stack.containsActivity(activityName) - } - }.firstOrNull() - } - /** - * Get the first activity on display with id [displayId], containing a window whose title + * Get the all activities on display with id [displayId], containing a window whose title * contains [partialWindowTitle] * * @param partialWindowTitle window title to search * @param displayId display where to search the activity */ - fun getActivityForWindow( + fun getActivitiesForWindow( partialWindowTitle: String, displayId: Int = DEFAULT_DISPLAY - ): Activity? { - return displays.firstOrNull { it.id == displayId }?.rootTasks?.map { stack -> + ): List<Activity> { + return displays.firstOrNull { it.id == displayId }?.rootTasks?.mapNotNull { stack -> stack.getActivity { activity -> activity.hasWindow(partialWindowTitle) } - }?.firstOrNull() - } - - /** Get the stack position on its display. */ - fun getStackIndexByActivityType(activityType: Int): Int { - return displays - .map { it.rootTasks.indexOfFirst { p -> p.activityType == activityType } } - .firstOrNull { it > -1 } - ?: -1 - } - - /** Get the stack position on its display. */ - fun getStackIndexByActivity(activityName: String): Int { - for (display in displays) { - for (i in display.rootTasks.indices.reversed()) { - val stack = display.rootTasks[i] - if (stack.containsActivity(activityName)) return i - } - } - return -1 - } - - /** Get display id by activity on it. */ - fun getDisplayByActivity(activityComponent: String): Int { - val task = getTaskByActivity(activityComponent) ?: return -1 - return getRootTask(task.rootTaskId)?.displayId - ?: error("Task with name $activityComponent not found") + } ?: emptyList() } fun containsActivity(activityName: String): Boolean = rootTasks.any { it.containsActivity(activityName) } - fun containsNoneOf(activityNames: Iterable<String>): Boolean { - for (activityName in activityNames) { - for (stack in rootTasks) { - if (stack.containsActivity(activityName)) return false - } - } - return true - } - - fun containsActivityInWindowingMode( - activityName: String, - windowingMode: Int - ): Boolean { - for (stack in rootTasks) { - val activity = stack.getActivity(activityName) - if (activity != null && activity.windowingMode == windowingMode) { - return true - } - } - return false + fun isActivityVisible(activityName: String): Boolean { + val activity = rootTasks.mapNotNull { it.getActivity(activityName) }.firstOrNull() + return activity?.isVisible ?: false } - fun isActivityVisible(activityName: String): Boolean = - rootTasks.map { it.getActivity(activityName)?.isVisible ?: false }.firstOrNull() - ?: false - - fun isActivityTranslucent(activityName: String): Boolean = - rootTasks.map { it.getActivity(activityName)?.isTranslucent ?: false }.firstOrNull() - ?: false - - fun isBehindOpaqueActivities(activityName: String): Boolean { - for (stack in rootTasks) { - val activity = stack.getActivity { a -> a.title == activityName || !a.isTranslucent } - if (activity != null) { - if (activity.title == activityName) { - return false - } - if (!activity.isTranslucent) { - return true - } - } - } - - return false - } - - fun containsStartedActivities(): Boolean = rootTasks.map { - it.getActivity { a -> a.state != STATE_STOPPED && a.state != STATE_DESTROYED } != null - }.firstOrNull() ?: false - fun hasActivityState(activityName: String, activityState: String): Boolean = rootTasks.any { it.getActivity(activityName)?.state == activityState } - fun getActivityProcId(activityName: String): Int = - rootTasks.mapNotNull { it.getActivity(activityName)?.procId } - .firstOrNull() - ?: -1 - - fun getStackIdByActivity(activityName: String): Int = - getTaskByActivity(activityName)?.rootTaskId ?: INVALID_STACK_ID - - fun getTaskByActivity(activityName: String): ActivityTask? = - getTaskByActivity(activityName, WINDOWING_MODE_UNDEFINED) - - fun getTaskByActivity(activityName: String, windowingMode: Int): ActivityTask? { - for (stack in rootTasks) { - if (windowingMode == WINDOWING_MODE_UNDEFINED || windowingMode == stack.windowingMode) { - val task = stack.getTask { it.getActivity(activityName) != null } - if (task != null) { - return task - } - } - } - return null - } - - /** - * Get the number of activities in the task, with the option to count only activities with - * specific name. - * @param taskId Id of the task where we're looking for the number of activities. - * @param activityName Optional name of the activity we're interested in. - * @return Number of all activities in the task if activityName is `null`, otherwise will - * report number of activities that have specified name. - */ - fun getActivityCountInTask(taskId: Int, activityName: String?): Int { - // If activityName is null, count all activities in the task. - // Otherwise count activities that have specified name. - for (stack in rootTasks) { - val task = stack.getTask(taskId) ?: continue - - if (activityName == null) { - return task.activities.size - } - var count = 0 - for (activity in task.activities) { - if (activity.title == activityName) { - count++ - } - } - return count - } - return 0 - } - - fun getRootTasksCount(displayId: Int): Int { - var count = 0 - for (rootTask in rootTasks) { - if (rootTask.displayId == displayId) ++count - } - return count - } - fun pendingActivityContain(activityName: String): Boolean { return pendingActivities.contains(activityName) } - fun getMatchingVisibleWindowState(windowName: String): List<WindowState> { - return windowStates.filter { it.isSurfaceShown && it.title == windowName } + fun getMatchingVisibleWindowState(windowName: String): Array<WindowState> { + return windowStates.filter { it.isSurfaceShown && it.title.contains(windowName) } + .toTypedArray() } - fun getWindowByPackageName(packageName: String, windowType: Int): WindowState? = - getWindowsByPackageName(packageName, windowType).firstOrNull() - - fun getWindowsByPackageName( - packageName: String, - vararg restrictToTypes: Int - ): List<WindowState> = - windowStates.filter { ws -> - ((ws.title == packageName || - ws.title.startsWith("$packageName/")) && - restrictToTypes.any { type -> type == ws.attributes.type }) - } - - fun getMatchingWindowType(type: Int): List<WindowState> = - windowStates.filter { it.attributes.type == type } - - fun getMatchingWindowTokens(windowName: String): List<String> = - windowStates.filter { it.title === windowName }.map { it.token } - fun getNavBarWindow(displayId: Int): WindowState? { val navWindow = windowStates.filter { it.isValidNavBarType && it.displayId == displayId } @@ -460,21 +228,13 @@ open class WindowManagerState( * Check if there exists a window record with matching windowName. */ fun containsWindow(windowName: String): Boolean = - windowStates.any { it.title == windowName } + windowStates.any { it.title.contains(windowName) } /** * Check if at least one window which matches the specified name has shown it's surface. */ - fun isWindowSurfaceShown(windowName: String): Boolean { - for (window in windowStates) { - if (window.title == windowName) { - if (window.isSurfaceShown) { - return true - } - } - } - return false - } + fun isWindowSurfaceShown(windowName: String): Boolean = + getMatchingVisibleWindowState(windowName).isNotEmpty() /** * Check if at least one window which matches provided window name is visible. @@ -494,35 +254,8 @@ open class WindowManagerState( return pinnedWindows.any { it.title.contains(windowName) } } - /** - * Checks whether the display contains the given activity. - */ - fun hasActivityInDisplay(displayId: Int, activityName: String): Boolean { - for (stack in getDisplay(displayId)!!.rootTasks) { - if (stack.containsActivity(activityName)) { - return true - } - } - return false - } - - fun findFirstWindowWithType(type: Int): WindowState? = - windowStates.firstOrNull { it.attributes.type == type } - fun getZOrder(w: WindowState): Int = windowStates.size - windowStates.indexOf(w) - fun getStandardRootStackByWindowingMode(windowingMode: Int): ActivityTask? { - for (task in rootTasks) { - if (task.activityType != ACTIVITY_TYPE_STANDARD) { - continue - } - if (task.windowingMode == windowingMode) { - return task - } - } - return null - } - fun defaultMinimalTaskSize(displayId: Int): Int = dpToPx(DEFAULT_RESIZABLE_TASK_SIZE_DP.toFloat(), getDisplay(displayId)!!.dpi) @@ -568,8 +301,10 @@ open class WindowManagerState( !keyguardControllerState.isKeyguardShowing } + fun asTrace(): WindowManagerTrace = WindowManagerTrace(arrayOf(this), source = "") + override fun toString(): String { - return prettyTimestamp(timestamp) + return "${prettyTimestamp(timestamp)} (timestamp=$timestamp)" } companion object { @@ -583,10 +318,8 @@ open class WindowManagerState( internal const val ACTIVITY_TYPE_STANDARD = 1 internal const val DEFAULT_DISPLAY = 0 internal const val DEFAULT_MINIMAL_SPLIT_SCREEN_DISPLAY_SIZE_DP = 440 - internal const val INVALID_STACK_ID = -1 internal const val ACTIVITY_TYPE_HOME = 2 internal const val ACTIVITY_TYPE_RECENTS = 3 - internal const val ACTIVITY_TYPE_DREAM = 5 internal const val WINDOWING_MODE_UNDEFINED = 0 private const val DENSITY_DEFAULT = 160 /** @@ -595,7 +328,7 @@ open class WindowManagerState( private const val WINDOWING_MODE_PINNED = 2 /** - * @see WindowManager.LayoutParams + * @see android.view.WindowManager.LayoutParams */ internal const val TYPE_NAVIGATION_BAR_PANEL = 2024 @@ -608,4 +341,7 @@ open class WindowManagerState( return (dp * densityDpi / DENSITY_DEFAULT + 0.5f).toInt() } } + override fun equals(other: Any?): Boolean { + return other is WindowManagerState && other.timestamp == this.timestamp + } }
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerTrace.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerTrace.kt index cd937ac52..3517ba92d 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerTrace.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/WindowManagerTrace.kt @@ -28,14 +28,45 @@ import com.android.server.wm.traces.common.ITrace * access internal Java/Android functionality * */ -open class WindowManagerTrace( - override val entries: List<WindowManagerState>, - override val source: String, - override val sourceChecksum: String +data class WindowManagerTrace( + override val entries: Array<WindowManagerState>, + override val source: String ) : ITrace<WindowManagerState>, - List<WindowManagerState> by entries { + List<WindowManagerState> by entries.toList() { override fun toString(): String { return "WindowManagerTrace(Start: ${entries.first()}, " + "End: ${entries.last()})" } -}
\ No newline at end of file + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is WindowManagerTrace) return false + + if (!entries.contentEquals(other.entries)) return false + if (source != other.source) return false + + return true + } + + override fun hashCode(): Int { + var result = entries.contentHashCode() + result = 31 * result + source.hashCode() + return result + } + + /** + * Split the trace by the start and end timestamp. + * + * @param from the start timestamp + * @param to the end timestamp + * @return the subtrace trace(from, to) + */ + fun filter(from: Long, to: Long): WindowManagerTrace { + return WindowManagerTrace( + this.entries + .dropWhile { it.timestamp < from } + .dropLastWhile { it.timestamp > to } + .toTypedArray(), + source = "") + } +} diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Activity.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Activity.kt index cff1a7f28..5c082a1a6 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Activity.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Activity.kt @@ -38,10 +38,35 @@ open class Activity( * @param partialWindowTitle window title to search */ fun hasWindow(partialWindowTitle: String): Boolean { - return this.windows.any { it.title.contains(partialWindowTitle) } + return collectDescendants<WindowState> { it.title.contains(partialWindowTitle) } + .isNotEmpty() } override fun toString(): String { return "${this::class.simpleName}: {$token $title} state=$state visible=$isVisible" } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Activity) return false + + if (state != other.state) return false + if (frontOfTask != other.frontOfTask) return false + if (procId != other.procId) return false + if (isTranslucent != other.isTranslucent) return false + if (orientation != other.orientation) return false + if (title != other.title) return false + if (token != other.token) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + state.hashCode() + result = 31 * result + frontOfTask.hashCode() + result = 31 * result + procId + result = 31 * result + isTranslucent.hashCode() + return result + } }
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Configuration.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Configuration.kt index 44c64f355..13d1f9e8f 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Configuration.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Configuration.kt @@ -42,4 +42,32 @@ data class Configuration( smallestScreenWidthDp == 0 && screenLayout == 0 && uiMode == 0 + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Configuration) return false + + if (windowConfiguration != other.windowConfiguration) return false + if (densityDpi != other.densityDpi) return false + if (orientation != other.orientation) return false + if (screenHeightDp != other.screenHeightDp) return false + if (screenWidthDp != other.screenWidthDp) return false + if (smallestScreenWidthDp != other.smallestScreenWidthDp) return false + if (screenLayout != other.screenLayout) return false + if (uiMode != other.uiMode) return false + + return true + } + + override fun hashCode(): Int { + var result = windowConfiguration?.hashCode() ?: 0 + result = 31 * result + densityDpi + result = 31 * result + orientation + result = 31 * result + screenHeightDp + result = 31 * result + screenWidthDp + result = 31 * result + smallestScreenWidthDp + result = 31 * result + screenLayout + result = 31 * result + uiMode + return result + } }
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/ConfigurationContainer.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/ConfigurationContainer.kt index 4ba0b6a08..6aacdb89d 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/ConfigurationContainer.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/ConfigurationContainer.kt @@ -42,4 +42,22 @@ open class ConfigurationContainer( get() = (overrideConfiguration?.isEmpty ?: true) && (fullConfiguration?.isEmpty ?: true) && (mergedOverrideConfiguration?.isEmpty ?: true) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ConfigurationContainer) return false + + if (overrideConfiguration != other.overrideConfiguration) return false + if (fullConfiguration != other.fullConfiguration) return false + if (mergedOverrideConfiguration != other.mergedOverrideConfiguration) return false + + return true + } + + override fun hashCode(): Int { + var result = overrideConfiguration?.hashCode() ?: 0 + result = 31 * result + (fullConfiguration?.hashCode() ?: 0) + result = 31 * result + (mergedOverrideConfiguration?.hashCode() ?: 0) + return result + } }
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayArea.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayArea.kt index 3067ac601..9c42887e3 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayArea.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayArea.kt @@ -45,4 +45,23 @@ open class DisplayArea( override fun toString(): String { return "${this::class.simpleName} {$token $title} isTaskArea=$isTaskDisplayArea" } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is DisplayArea) return false + + if (isTaskDisplayArea != other.isTaskDisplayArea) return false + if (isVisible != other.isVisible) return false + if (orientation != other.orientation) return false + if (title != other.title) return false + if (token != other.token) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + isTaskDisplayArea.hashCode() + return result + } }
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayContent.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayContent.kt index cb2167238..6671aee8f 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayContent.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/DisplayContent.kt @@ -30,13 +30,13 @@ open class DisplayContent( val focusedRootTaskId: Int, val resumedActivity: String, val singleTaskInstance: Boolean, - _defaultPinnedStackBounds: Rect?, - _pinnedStackMovementBounds: Rect?, + val defaultPinnedStackBounds: Rect, + val pinnedStackMovementBounds: Rect, val displayRect: Rect, val appRect: Rect, val dpi: Int, val flags: Int, - _stableBounds: Rect?, + val stableBounds: Rect, val surfaceSize: Int, val focusedApp: String, val lastTransition: String, @@ -48,16 +48,12 @@ open class DisplayContent( override val name: String = id.toString() override val isVisible: Boolean = false - val defaultPinnedStackBounds: Rect = _defaultPinnedStackBounds ?: Rect.EMPTY - val pinnedStackMovementBounds: Rect = _pinnedStackMovementBounds ?: Rect.EMPTY - val stableBounds: Rect = _stableBounds ?: Rect.EMPTY - - val rootTasks: Array<ActivityTask> + val rootTasks: Array<Task> get() { - val tasks = this.collectDescendants<ActivityTask> { it.isRootTask }.toMutableList() + val tasks = this.collectDescendants<Task> { it.isRootTask }.toMutableList() // TODO(b/149338177): figure out how CTS tests deal with organizer. For now, // don't treat them as regular stacks - val rootOrganizedTasks = mutableListOf<ActivityTask>() + val rootOrganizedTasks = mutableListOf<Task>() val reversedTaskList = tasks.reversed() reversedTaskList.forEach { task -> // Skip tasks created by an organizer @@ -68,7 +64,7 @@ open class DisplayContent( } // Add root tasks controlled by an organizer rootOrganizedTasks.reversed().forEach { task -> - tasks.addAll(task.children.reversed().map { it as ActivityTask }) + tasks.addAll(task.children.reversed().map { it as Task }) } return tasks.toTypedArray() @@ -93,4 +89,55 @@ open class DisplayContent( return "${this::class.simpleName} #$id: name=$title mDisplayRect=$displayRect " + "mAppRect=$appRect mFlags=$flags" } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is DisplayContent) return false + if (!super.equals(other)) return false + + if (id != other.id) return false + if (focusedRootTaskId != other.focusedRootTaskId) return false + if (resumedActivity != other.resumedActivity) return false + if (defaultPinnedStackBounds != other.defaultPinnedStackBounds) return false + if (pinnedStackMovementBounds != other.pinnedStackMovementBounds) return false + if (stableBounds != other.stableBounds) return false + if (displayRect != other.displayRect) return false + if (appRect != other.appRect) return false + if (dpi != other.dpi) return false + if (flags != other.flags) return false + if (focusedApp != other.focusedApp) return false + if (lastTransition != other.lastTransition) return false + if (appTransitionState != other.appTransitionState) return false + if (rotation != other.rotation) return false + if (lastOrientation != other.lastOrientation) return false + if (name != other.name) return false + if (singleTaskInstance != other.singleTaskInstance) return false + if (surfaceSize != other.surfaceSize) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + id + result = 31 * result + focusedRootTaskId + result = 31 * result + resumedActivity.hashCode() + result = 31 * result + singleTaskInstance.hashCode() + result = 31 * result + defaultPinnedStackBounds.hashCode() + result = 31 * result + pinnedStackMovementBounds.hashCode() + result = 31 * result + displayRect.hashCode() + result = 31 * result + appRect.hashCode() + result = 31 * result + dpi + result = 31 * result + flags + result = 31 * result + stableBounds.hashCode() + result = 31 * result + surfaceSize + result = 31 * result + focusedApp.hashCode() + result = 31 * result + lastTransition.hashCode() + result = 31 * result + appTransitionState.hashCode() + result = 31 * result + rotation + result = 31 * result + lastOrientation + result = 31 * result + name.hashCode() + result = 31 * result + isVisible.hashCode() + return result + } }
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/ActivityTask.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Task.kt index 63f80a347..1c17405a3 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/ActivityTask.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/Task.kt @@ -25,14 +25,14 @@ import com.android.server.wm.traces.common.Rect * access internal Java/Android functionality * */ -open class ActivityTask( +open class Task( override val activityType: Int, override val isFullscreen: Boolean, override val bounds: Rect, val taskId: Int, val rootTaskId: Int, val displayId: Int, - _lastNonFullscreenBounds: Rect?, + val lastNonFullscreenBounds: Rect, val realActivity: String, val origActivity: String, val resizeMode: Int, @@ -48,18 +48,20 @@ open class ActivityTask( override val isVisible: Boolean = false override val name: String = taskId.toString() override val isEmpty: Boolean get() = tasks.isEmpty() && activities.isEmpty() + override val stableId: String get() = "${super.stableId} $taskId" - val lastNonFullscreenBounds: Rect = _lastNonFullscreenBounds ?: Rect.EMPTY val isRootTask: Boolean get() = taskId == rootTaskId - val tasks: List<ActivityTask> - get() = this.children.reversed().filterIsInstance<ActivityTask>() - val activities: List<Activity> - get() = this.children.reversed().filterIsInstance<Activity>() + val tasks: Array<Task> + get() = this.children.reversed().filterIsInstance<Task>().toTypedArray() + val taskFragments: Array<TaskFragment> + get() = this.children.reversed().filterIsInstance<TaskFragment>().toTypedArray() + val activities: Array<Activity> + get() = this.children.reversed().filterIsInstance<Activity>().toTypedArray() /** The top task in the stack. */ // NOTE: Unlike the WindowManager internals, we dump the state from top to bottom, // so the indices are inverted - val topTask: ActivityTask? get() = tasks.firstOrNull() + val topTask: Task? get() = tasks.firstOrNull() val resumedActivities: Array<String> get() { val result = mutableSetOf<String>() if (this._resumedActivity.isNotEmpty()) { @@ -72,23 +74,29 @@ open class ActivityTask( return result.toTypedArray() } - fun getTask(predicate: (ActivityTask) -> Boolean) = + fun getTask(predicate: (Task) -> Boolean) = tasks.firstOrNull { predicate(it) } ?: if (predicate(this)) this else null fun getTask(taskId: Int) = getTask { t -> t.taskId == taskId } - fun forAllTasks(consumer: (ActivityTask) -> Any) { + fun getActivityWithTask(predicate: (Task, Activity) -> Boolean): Activity? { + return activities.firstOrNull { predicate(this, it) } + ?: tasks.flatMap { task -> task.activities.filter { predicate(task, it) } } + .firstOrNull() + } + + fun forAllTasks(consumer: (Task) -> Any) { tasks.forEach { consumer(it) } } fun getActivity(predicate: (Activity) -> Boolean): Activity? { return activities.firstOrNull { predicate(it) } - ?: tasks.flatMap { it.activities } + ?: tasks.flatMap { it.activities.toList() } .firstOrNull { predicate(it) } } fun getActivity(activityName: String): Activity? { - return getActivity { activity -> activity.title == activityName } + return getActivity { activity -> activity.title.contains(activityName) } } fun containsActivity(activityName: String) = getActivity(activityName) != null @@ -96,4 +104,50 @@ open class ActivityTask( override fun toString(): String { return "${this::class.simpleName}: {$token $title} id=$taskId bounds=$bounds" } -}
\ No newline at end of file + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Task) return false + + if (activityType != other.activityType) return false + if (isFullscreen != other.isFullscreen) return false + if (bounds != other.bounds) return false + if (taskId != other.taskId) return false + if (rootTaskId != other.rootTaskId) return false + if (displayId != other.displayId) return false + if (realActivity != other.realActivity) return false + if (resizeMode != other.resizeMode) return false + if (minWidth != other.minWidth) return false + if (minHeight != other.minHeight) return false + if (name != other.name) return false + if (orientation != other.orientation) return false + if (title != other.title) return false + if (token != other.token) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + activityType + result = 31 * result + isFullscreen.hashCode() + result = 31 * result + bounds.hashCode() + result = 31 * result + taskId + result = 31 * result + rootTaskId + result = 31 * result + displayId + result = 31 * result + lastNonFullscreenBounds.hashCode() + result = 31 * result + realActivity.hashCode() + result = 31 * result + origActivity.hashCode() + result = 31 * result + resizeMode + result = 31 * result + _resumedActivity.hashCode() + result = 31 * result + animatingBounds.hashCode() + result = 31 * result + surfaceWidth + result = 31 * result + surfaceHeight + result = 31 * result + createdByOrganizer.hashCode() + result = 31 * result + minWidth + result = 31 * result + minHeight + result = 31 * result + isVisible.hashCode() + result = 31 * result + name.hashCode() + return result + } +} diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/TaskFragment.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/TaskFragment.kt new file mode 100644 index 000000000..d18daa0f0 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/TaskFragment.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.common.windowmanager.windows + +/** + * Represents a task fragment in the window manager hierarchy + * + * This is a generic object that is reused by both Flicker and Winscope and cannot + * access internal Java/Android functionality + * + */ +open class TaskFragment( + override val activityType: Int, + val displayId: Int, + val minWidth: Int, + val minHeight: Int, + windowContainer: WindowContainer +) : WindowContainer(windowContainer) { + val tasks: Array<Task> + get() = this.children.reversed().filterIsInstance<Task>().toTypedArray() + val taskFragments: Array<TaskFragment> + get() = this.children.reversed().filterIsInstance<TaskFragment>().toTypedArray() + val activities: Array<Activity> + get() = this.children.reversed().filterIsInstance<Activity>().toTypedArray() + + override fun toString(): String { + return "${this::class.simpleName}: {$token $title} bounds=$bounds" + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is TaskFragment) return false + + if (activityType != other.activityType) return false + if (displayId != other.displayId) return false + if (minWidth != other.minWidth) return false + if (minHeight != other.minHeight) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + activityType + result = 31 * result + displayId + result = 31 * result + minWidth + result = 31 * result + minHeight + return result + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowConfiguration.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowConfiguration.kt index ffdda8298..c2eb894e4 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowConfiguration.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowConfiguration.kt @@ -42,4 +42,26 @@ open class WindowConfiguration( maxBounds.isEmpty && windowingMode == 0 && activityType == 0 + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is WindowConfiguration) return false + + if (windowingMode != other.windowingMode) return false + if (activityType != other.activityType) return false + if (appBounds != other.appBounds) return false + if (bounds != other.bounds) return false + if (maxBounds != other.maxBounds) return false + + return true + } + + override fun hashCode(): Int { + var result = windowingMode + result = 31 * result + activityType + result = 31 * result + appBounds.hashCode() + result = 31 * result + bounds.hashCode() + result = 31 * result + maxBounds.hashCode() + return result + } }
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowContainer.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowContainer.kt index db48e0dc2..b81f2abd4 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowContainer.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowContainer.kt @@ -31,6 +31,7 @@ open class WindowContainer constructor( val title: String, val token: String, val orientation: Int, + val layerId: Int, _isVisible: Boolean, configurationContainer: ConfigurationContainer, val children: Array<WindowContainer> @@ -43,6 +44,7 @@ open class WindowContainer constructor( titleOverride ?: windowContainer.title, windowContainer.token, windowContainer.orientation, + windowContainer.layerId, isVisibleOverride ?: windowContainer.isVisible, windowContainer, windowContainer.children @@ -50,13 +52,10 @@ open class WindowContainer constructor( open val isVisible: Boolean = _isVisible open val name: String = title - val stableId: String get() = "${this::class.simpleName} $token $title" + open val stableId: String get() = "${this::class.simpleName} $token $title" open val isFullscreen: Boolean = false open val bounds: Rect = Rect.EMPTY - val windows: Array<WindowState> - get() = this.collectDescendants() - fun traverseTopDown(): List<WindowContainer> { val traverseList = mutableListOf(this) @@ -111,6 +110,33 @@ open class WindowContainer constructor( return name } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is WindowContainer) return false + + if (title != other.title) return false + if (token != other.token) return false + if (orientation != other.orientation) return false + if (isVisible != other.isVisible) return false + if (name != other.name) return false + if (isFullscreen != other.isFullscreen) return false + if (bounds != other.bounds) return false + + return true + } + + override fun hashCode(): Int { + var result = title.hashCode() + result = 31 * result + token.hashCode() + result = 31 * result + orientation + result = 31 * result + children.contentHashCode() + result = 31 * result + isVisible.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + isFullscreen.hashCode() + result = 31 * result + bounds.hashCode() + return result + } + override val isEmpty: Boolean get() = super.isEmpty && title.isEmpty() && diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowLayoutParams.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowLayoutParams.kt index def86db27..9c73e6d92 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowLayoutParams.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowLayoutParams.kt @@ -63,4 +63,78 @@ data class WindowLayoutParams( */ private const val TYPE_NAVIGATION_BAR = 2019 } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is WindowLayoutParams) return false + + if (type != other.type) return false + if (x != other.x) return false + if (y != other.y) return false + if (width != other.width) return false + if (height != other.height) return false + if (horizontalMargin != other.horizontalMargin) return false + if (verticalMargin != other.verticalMargin) return false + if (gravity != other.gravity) return false + if (softInputMode != other.softInputMode) return false + if (format != other.format) return false + if (windowAnimations != other.windowAnimations) return false + if (alpha != other.alpha) return false + if (screenBrightness != other.screenBrightness) return false + if (buttonBrightness != other.buttonBrightness) return false + if (rotationAnimation != other.rotationAnimation) return false + if (preferredRefreshRate != other.preferredRefreshRate) return false + if (preferredDisplayModeId != other.preferredDisplayModeId) return false + if (hasSystemUiListeners != other.hasSystemUiListeners) return false + if (inputFeatureFlags != other.inputFeatureFlags) return false + if (userActivityTimeout != other.userActivityTimeout) return false + if (colorMode != other.colorMode) return false + if (flags != other.flags) return false + if (privateFlags != other.privateFlags) return false + if (systemUiVisibilityFlags != other.systemUiVisibilityFlags) return false + if (subtreeSystemUiVisibilityFlags != other.subtreeSystemUiVisibilityFlags) return false + if (appearance != other.appearance) return false + if (behavior != other.behavior) return false + if (fitInsetsTypes != other.fitInsetsTypes) return false + if (fitInsetsSides != other.fitInsetsSides) return false + if (fitIgnoreVisibility != other.fitIgnoreVisibility) return false + if (isValidNavBarType != other.isValidNavBarType) return false + + return true + } + + override fun hashCode(): Int { + var result = type + result = 31 * result + x + result = 31 * result + y + result = 31 * result + width + result = 31 * result + height + result = 31 * result + horizontalMargin.hashCode() + result = 31 * result + verticalMargin.hashCode() + result = 31 * result + gravity + result = 31 * result + softInputMode + result = 31 * result + format + result = 31 * result + windowAnimations + result = 31 * result + alpha.hashCode() + result = 31 * result + screenBrightness.hashCode() + result = 31 * result + buttonBrightness.hashCode() + result = 31 * result + rotationAnimation + result = 31 * result + preferredRefreshRate.hashCode() + result = 31 * result + preferredDisplayModeId + result = 31 * result + hasSystemUiListeners.hashCode() + result = 31 * result + inputFeatureFlags + result = 31 * result + userActivityTimeout.hashCode() + result = 31 * result + colorMode + result = 31 * result + flags + result = 31 * result + privateFlags + result = 31 * result + systemUiVisibilityFlags + result = 31 * result + subtreeSystemUiVisibilityFlags + result = 31 * result + appearance + result = 31 * result + behavior + result = 31 * result + fitInsetsTypes + result = 31 * result + fitInsetsSides + result = 31 * result + fitIgnoreVisibility.hashCode() + result = 31 * result + isValidNavBarType.hashCode() + return result + } }
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowManagerPolicy.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowManagerPolicy.kt index 2bc52d073..cc06fa522 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowManagerPolicy.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowManagerPolicy.kt @@ -37,4 +37,78 @@ data class WindowManagerPolicy( val rotationMode: Int, val screenOnFully: Boolean, val windowManagerDrawComplete: Boolean -)
\ No newline at end of file +) { + val isOrientationNoSensor: Boolean + get() = orientation == SCREEN_ORIENTATION_NOSENSOR + + val isFixedOrientation: Boolean + get() = isFixedOrientationLandscape || + isFixedOrientationPortrait || + orientation == SCREEN_ORIENTATION_LOCKED + + private val isFixedOrientationLandscape + get() = orientation == SCREEN_ORIENTATION_LANDSCAPE || + orientation == SCREEN_ORIENTATION_SENSOR_LANDSCAPE || + orientation == SCREEN_ORIENTATION_REVERSE_LANDSCAPE || + orientation == SCREEN_ORIENTATION_USER_LANDSCAPE + + private val isFixedOrientationPortrait + get() = orientation == SCREEN_ORIENTATION_PORTRAIT || + orientation == SCREEN_ORIENTATION_SENSOR_PORTRAIT || + orientation == SCREEN_ORIENTATION_REVERSE_PORTRAIT || + orientation == SCREEN_ORIENTATION_USER_PORTRAIT + + companion object { + /** + * From [android.content.pm.ActivityInfo] + */ + private const val SCREEN_ORIENTATION_LANDSCAPE = 0 + private const val SCREEN_ORIENTATION_PORTRAIT = 1 + private const val SCREEN_ORIENTATION_NOSENSOR = 5 + private const val SCREEN_ORIENTATION_SENSOR_LANDSCAPE = 6 + private const val SCREEN_ORIENTATION_SENSOR_PORTRAIT = 7 + private const val SCREEN_ORIENTATION_REVERSE_LANDSCAPE = 8 + private const val SCREEN_ORIENTATION_REVERSE_PORTRAIT = 9 + private const val SCREEN_ORIENTATION_USER_LANDSCAPE = 11 + private const val SCREEN_ORIENTATION_USER_PORTRAIT = 12 + private const val SCREEN_ORIENTATION_LOCKED = 14 + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is WindowManagerPolicy) return false + + if (focusedAppToken != other.focusedAppToken) return false + if (forceStatusBar != other.forceStatusBar) return false + if (forceStatusBarFromKeyguard != other.forceStatusBarFromKeyguard) return false + if (keyguardDrawComplete != other.keyguardDrawComplete) return false + if (keyguardOccluded != other.keyguardOccluded) return false + if (keyguardOccludedChanged != other.keyguardOccludedChanged) return false + if (keyguardOccludedPending != other.keyguardOccludedPending) return false + if (lastSystemUiFlags != other.lastSystemUiFlags) return false + if (orientation != other.orientation) return false + if (rotation != other.rotation) return false + if (rotationMode != other.rotationMode) return false + if (screenOnFully != other.screenOnFully) return false + if (windowManagerDrawComplete != other.windowManagerDrawComplete) return false + + return true + } + + override fun hashCode(): Int { + var result = focusedAppToken.hashCode() + result = 31 * result + forceStatusBar.hashCode() + result = 31 * result + forceStatusBarFromKeyguard.hashCode() + result = 31 * result + keyguardDrawComplete.hashCode() + result = 31 * result + keyguardOccluded.hashCode() + result = 31 * result + keyguardOccludedChanged.hashCode() + result = 31 * result + keyguardOccludedPending.hashCode() + result = 31 * result + lastSystemUiFlags + result = 31 * result + orientation + result = 31 * result + rotation + result = 31 * result + rotationMode + result = 31 * result + screenOnFully.hashCode() + result = 31 * result + windowManagerDrawComplete.hashCode() + return result + } +} diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowState.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowState.kt index d85e860d2..f11bfc8fa 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowState.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowState.kt @@ -16,7 +16,7 @@ package com.android.server.wm.traces.common.windowmanager.windows -import com.android.server.wm.traces.common.Bounds +import com.android.server.wm.traces.common.Size import com.android.server.wm.traces.common.Rect import com.android.server.wm.traces.common.Region @@ -34,31 +34,22 @@ open class WindowState( val layer: Int, val isSurfaceShown: Boolean, val windowType: Int, - val requestedSize: Bounds, + val requestedSize: Size, val surfacePosition: Rect?, - _frame: Rect?, - _containingFrame: Rect?, - _parentFrame: Rect?, - _contentFrame: Rect?, - _contentInsets: Rect?, - _surfaceInsets: Rect?, - _givenContentInsets: Rect?, - _crop: Rect?, + val frame: Rect, + val containingFrame: Rect, + val parentFrame: Rect, + val contentFrame: Rect, + val contentInsets: Rect, + val surfaceInsets: Rect, + val givenContentInsets: Rect, + val crop: Rect, windowContainer: WindowContainer, val isAppWindow: Boolean ) : WindowContainer(windowContainer, getWindowTitle(windowContainer.title)) { override val isVisible: Boolean get() = super.isVisible && attributes.alpha > 0 - val frame: Rect = _frame ?: Rect.EMPTY - val containingFrame: Rect = _containingFrame ?: Rect.EMPTY - val parentFrame: Rect = _parentFrame ?: Rect.EMPTY - val contentFrame: Rect = _contentFrame ?: Rect.EMPTY - val contentInsets: Rect = _contentInsets ?: Rect.EMPTY - val surfaceInsets: Rect = _surfaceInsets ?: Rect.EMPTY - val givenContentInsets: Rect = _givenContentInsets ?: Rect.EMPTY - val crop: Rect = _crop ?: Rect.EMPTY override val isFullscreen: Boolean get() = this.attributes.flags.and(FLAG_FULLSCREEN) > 0 - val isStartingWindow: Boolean = windowType == WINDOW_TYPE_STARTING val isExitingWindow: Boolean = windowType == WINDOW_TYPE_EXITING val isDebuggerWindow: Boolean = windowType == WINDOW_TYPE_DEBUGGER @@ -79,13 +70,29 @@ open class WindowState( "type=${attributes.type} cf=$containingFrame pf=$parentFrame" override fun equals(other: Any?): Boolean { - return other is WindowState && - other.stableId == stableId && - other.attributes == attributes && - other.token == token && - other.title == title && - other.containingFrame == containingFrame && - other.parentFrame == parentFrame + if (this === other) return true + if (other !is WindowState) return false + + if (name != other.name) return false + if (attributes != other.attributes) return false + if (displayId != other.displayId) return false + if (stackId != other.stackId) return false + if (layer != other.layer) return false + if (isSurfaceShown != other.isSurfaceShown) return false + if (windowType != other.windowType) return false + if (requestedSize != other.requestedSize) return false + if (surfacePosition != other.surfacePosition) return false + if (frame != other.frame) return false + if (containingFrame != other.containingFrame) return false + if (parentFrame != other.parentFrame) return false + if (contentFrame != other.contentFrame) return false + if (contentInsets != other.contentInsets) return false + if (surfaceInsets != other.surfaceInsets) return false + if (givenContentInsets != other.givenContentInsets) return false + if (crop != other.crop) return false + if (isAppWindow != other.isAppWindow) return false + + return true } override fun hashCode(): Int { diff --git a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowToken.kt b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowToken.kt index cfcaafc30..9d2b911aa 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowToken.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/common/windowmanager/windows/WindowToken.kt @@ -28,4 +28,20 @@ open class WindowToken(windowContainer: WindowContainer) : WindowContainer(windo override fun toString(): String { return "${this::class.simpleName}: {$token $title}" } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is WindowToken) return false + if (!super.equals(other)) return false + + if (isVisible != other.isVisible) return false + + return true + } + + override fun hashCode(): Int { + var result = super.hashCode() + result = 31 * result + isVisible.hashCode() + return result + } } diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/Condition.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/Condition.kt deleted file mode 100644 index be03b2d6a..000000000 --- a/libraries/flicker/src/com/android/server/wm/traces/parser/Condition.kt +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm.traces.parser - -import android.os.SystemClock -import android.util.Log - -/** - * The utility class to wait a condition with customized options. - * The default retry policy is 5 times with interval 1 second. - * - * @param <T> The type of the object to validate. - * - * <p>Sample:</p> - * <pre> - * // Simple case. - * if (Condition.waitFor("true value", () -> true)) { - * println("Success"); - * } - * // Wait for customized result with customized validation. - * String result = Condition.waitForResult(new Condition<String>("string comparison") - * .setResultSupplier(() -> "Result string") - * .setResultValidator(str -> str.equals("Expected string")) - * .setRetryIntervalMs(500) - * .setRetryLimit(3) - * .setOnFailure(str -> println("Failed on " + str))); - * </pre> - */ -class Condition<T> -/** - * Constructs with a simple boolean condition. - * - * When satisfier = null, it is expected that the condition will be configured with - * [.setResultSupplier] and [.setResultValidator]. - * - * @param message The message to show what is waiting for. - * @param satisfier If it returns true, that means the condition is satisfied. - */ -@JvmOverloads constructor( - private var message: String = "", - /** - * It decides whether this condition is satisfied. - */ - private var satisfier: (() -> Boolean)? = null, - private var retryLimit: Int = DEFAULT_RETRY_LIMIT, - private var retryIntervalMs: Long = DEFAULT_RETRY_INTERVAL_MS -) { - private var returnLastResult: Boolean = false - - /** - * It is used when the condition is not a simple boolean expression, such as the caller may - * want to get the validated product as the return value. - */ - private var resultSupplier: (() -> T?)? = null - - /** - * It validates the result from [.mResultSupplier]. - */ - private var resultValidator: ((T?) -> Boolean)? = null - private var onFailure: ((T) -> Any)? = null - private var onRetry: Runnable? = null - private var lastResult: T? = null - private var validatedResult: T? = null - - /** - * Set the supplier which provides the result object to validate. - */ - fun setResultSupplier(supplier: () -> T?): Condition<T> = - apply { resultSupplier = supplier } - - /** - * Set the validator which tests the object provided by the supplier. - */ - fun setResultValidator(validator: (T?) -> Boolean): Condition<T> = - apply { resultValidator = validator } - - /** - * If true, when using [.waitForResult], the method will return the last result - * provided by [.mResultSupplier] even it is not valid (by [.mResultValidator]). - */ - fun setReturnLastResult(returnLastResult: Boolean): Condition<T> = - apply { this.returnLastResult = returnLastResult } - - /** - * Executes the action when the condition does not satisfy within the time limit. The passed - * object to the consumer will be the last result from the supplier. - */ - fun setOnFailure(onFailure: (T) -> Any): Condition<T> = apply { this.onFailure = onFailure } - - fun setOnRetry(onRetry: Runnable): Condition<T> = apply { this.onRetry = onRetry } - - fun setRetryIntervalMs(millis: Long): Condition<T> = apply { retryIntervalMs = millis } - - fun setRetryLimit(limit: Int): Condition<T> = apply { retryLimit = limit } - - /** - * Build the condition by [.mResultSupplier] and [.mResultValidator]. - */ - private fun prepareSatisfier(): () -> Boolean { - val supplier = resultSupplier - val validator = resultValidator - require(!(supplier == null || validator == null)) { "No specified condition" } - - return { - val result = supplier.invoke() - lastResult = result - if (validator.invoke(result)) { - validatedResult = result - true - } else { - false - } - }.also { - satisfier = it - } - } - - companion object { - // TODO(b/112837428): Implement a incremental retry policy to reduce the unnecessary - // constant time, currently keep the default as 5*1s because most of the original code - // uses it, and some tests might be sensitive to the waiting interval. - private const val DEFAULT_RETRY_LIMIT = 5 - private const val DEFAULT_RETRY_INTERVAL_MS = 1000L - - /** - * @see .waitFor - * @see .Condition - */ - @JvmStatic - @JvmOverloads - fun <T> waitFor( - message: String, - retryLimit: Int = DEFAULT_RETRY_LIMIT, - retryIntervalMs: Long = DEFAULT_RETRY_INTERVAL_MS, - satisfier: () -> Boolean - ): Boolean { - val condition = Condition<T>(message, satisfier, retryLimit, retryIntervalMs) - return waitFor(condition) - } - - /** - * @return `false` if the condition does not satisfy within the time limit. - */ - @JvmStatic - fun <T> waitFor(condition: Condition<T>): Boolean { - val satisfier = condition.satisfier ?: condition.prepareSatisfier() - val startTime = SystemClock.elapsedRealtime() - Log.v(LOG_TAG, "***Waiting for ${condition.message}") - for (i in 1..condition.retryLimit) { - if (satisfier.invoke()) { - Log.v(LOG_TAG, "***Waiting for ${condition.message} ... Success!") - return true - } else { - SystemClock.sleep(condition.retryIntervalMs) - Log.v(LOG_TAG, "***Waiting for ${condition.message} ... retry=$i" + - " elapsed=${SystemClock.elapsedRealtime() - startTime} ms") - val onRetry = condition.onRetry - if (onRetry != null && i < condition.retryLimit) { - onRetry.run() - } - } - } - if (satisfier.invoke()) { - Log.v(LOG_TAG, "***Waiting for ${condition.message} ... Success!") - return true - } - val onFailure = condition.onFailure - if (onFailure == null) { - Log.e(LOG_TAG, "***Waiting for ${condition.message} ... Failed!") - } else { - val result = condition.lastResult - require(result != null) { "Missing last result for failure notification" } - onFailure.invoke(result) - } - return false - } - - /** - * @see .waitForResult - */ - @JvmStatic - fun <T> waitForResult(message: String, setup: (Condition<T>) -> Any): T? { - val condition = Condition<T>(message) - setup.invoke(condition) - return waitForResult(condition) - } - - /** - * @return `null` if the condition does not satisfy within the time limit or the result - * supplier returns `null`. - */ - @JvmStatic - fun <T> waitForResult(condition: Condition<T>): T? { - condition.validatedResult = null - condition.lastResult = condition.validatedResult - condition.prepareSatisfier() - waitFor(condition) - return when { - condition.validatedResult != null -> condition.validatedResult - condition.returnLastResult -> condition.lastResult - else -> null - } - } - } -}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/DeviceStateDump.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/DeviceDumpParser.kt index 6534b3132..d47bdbc04 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/parser/DeviceStateDump.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/parser/DeviceDumpParser.kt @@ -16,6 +16,8 @@ package com.android.server.wm.traces.parser +import com.android.server.wm.traces.common.DeviceStateDump +import com.android.server.wm.traces.common.DeviceTraceDump import com.android.server.wm.traces.common.layers.LayersTrace import com.android.server.wm.traces.common.layers.LayerTraceEntry import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace @@ -27,16 +29,7 @@ import com.android.server.wm.traces.parser.windowmanager.WindowManagerTraceParse * Represents a state dump containing the [WindowManagerTrace] and the [LayersTrace] both parsed * and in raw (byte) data. */ -class DeviceStateDump( - /** - * Parsed [WindowManagerTrace] - */ - val wmTrace: WindowManagerTrace?, - /** - * Parsed [LayersTrace] - */ - val layersTrace: LayersTrace? -) { +class DeviceDumpParser { companion object { /** * Creates a device state dump containing the [WindowManagerTrace] and [LayersTrace] @@ -47,15 +40,18 @@ class DeviceStateDump( * @param layersTraceData [LayersTrace] content */ @JvmStatic - fun fromDump(wmTraceData: ByteArray, layersTraceData: ByteArray): DeviceStateDump { + fun fromDump( + wmTraceData: ByteArray, + layersTraceData: ByteArray + ): DeviceStateDump<WindowManagerState?, LayerTraceEntry?> { return DeviceStateDump( - wmTrace = if (wmTraceData.isNotEmpty()) { - WindowManagerTraceParser.parseFromDump(wmTraceData) + wmState = if (wmTraceData.isNotEmpty()) { + WindowManagerTraceParser.parseFromDump(wmTraceData).first() } else { null }, - layersTrace = if (layersTraceData.isNotEmpty()) { - LayersTraceParser.parseFromDump(layersTraceData) + layerState = if (layersTraceData.isNotEmpty()) { + LayersTraceParser.parseFromTrace(layersTraceData).first() } else { null } @@ -71,8 +67,8 @@ class DeviceStateDump( * @param layersTraceData [LayersTrace] content */ @JvmStatic - fun fromTrace(wmTraceData: ByteArray, layersTraceData: ByteArray): DeviceStateDump { - return DeviceStateDump( + fun fromTrace(wmTraceData: ByteArray, layersTraceData: ByteArray): DeviceTraceDump { + return DeviceTraceDump( wmTrace = if (wmTraceData.isNotEmpty()) { WindowManagerTraceParser.parseFromTrace(wmTraceData) } else { diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/Extensions.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/Extensions.kt index bb20b5cc6..32f22252c 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/parser/Extensions.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/parser/Extensions.kt @@ -22,8 +22,12 @@ import android.app.UiAutomation import android.content.ComponentName import android.os.ParcelFileDescriptor import android.util.Log +import com.android.server.wm.traces.common.DeviceStateDump +import com.android.server.wm.traces.common.FlickerComponentName import com.android.server.wm.traces.common.Rect import com.android.server.wm.traces.common.Region +import com.android.server.wm.traces.common.layers.LayerTraceEntry +import com.android.server.wm.traces.common.windowmanager.WindowManagerState internal const val LOG_TAG = "AMWM_FLICKER" @@ -35,9 +39,33 @@ fun Rect.toAndroidRect(): android.graphics.Rect { return android.graphics.Rect(left, top, right, bottom) } -fun ComponentName.toActivityName(): String = this.flattenToShortString() +/** + * Subtracts [other] region from this [this] region + */ +fun Region.minus(other: Region): android.graphics.Region = minus(other.toAndroidRegion()) + +/** + * Subtracts [other] region from this [this] region + */ +fun Region.minus(other: android.graphics.Region): android.graphics.Region { + val thisRegion = this.toAndroidRegion() + thisRegion.op(other, android.graphics.Region.Op.XOR) + return thisRegion +} + +/** + * Adds [other] region to this [this] region + */ +fun Region.plus(other: Region): android.graphics.Region = plus(other.toAndroidRegion()) -fun ComponentName.toWindowName(): String = this.flattenToString() +/** + * Adds [other] region to this [this] region + */ +fun Region.plus(other: android.graphics.Region): android.graphics.Region { + val thisRegion = this.toAndroidRegion() + thisRegion.op(other, android.graphics.Region.Op.XOR) + return thisRegion +} private fun executeCommand(uiAutomation: UiAutomation, cmd: String): ByteArray { val fileDescriptor = uiAutomation.executeShellCommand(cmd) @@ -80,9 +108,15 @@ fun getCurrentState( fun getCurrentStateDump( uiAutomation: UiAutomation, @WmStateDumpFlags dumpFlags: Int = FLAG_STATE_DUMP_FLAG_WM.or(FLAG_STATE_DUMP_FLAG_LAYERS) -): DeviceStateDump { +): DeviceStateDump<WindowManagerState?, LayerTraceEntry?> { val currentStateDump = getCurrentState(uiAutomation, dumpFlags) val wmTraceData = currentStateDump.first val layersTraceData = currentStateDump.second - return DeviceStateDump.fromDump(wmTraceData, layersTraceData) + return DeviceDumpParser.fromDump(wmTraceData, layersTraceData) } + +/** + * Converts an Android [ComponentName] into a flicker [FlickerComponentName] + */ +fun ComponentName.toFlickerComponent(): FlickerComponentName = + FlickerComponentName(this.packageName, this.className) diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/errors/ErrorTraceParserUtil.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/errors/ErrorTraceParserUtil.kt new file mode 100644 index 000000000..e91b956eb --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/parser/errors/ErrorTraceParserUtil.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.parser.errors + +import android.util.Log +import com.android.server.wm.flicker.FlickerErrorTraceProto +import com.android.server.wm.traces.common.errors.Error +import com.android.server.wm.traces.common.errors.ErrorState +import com.android.server.wm.traces.common.errors.ErrorTrace +import com.android.server.wm.traces.parser.LOG_TAG +import java.nio.file.Path +import kotlin.system.measureTimeMillis + +/** + * Class that holds the methods to parse error proto files into error classes. + */ +class ErrorTraceParserUtil { + companion object { + /** + * Parses [FlickerErrorTraceProto] from [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 + */ + @JvmOverloads + @JvmStatic + fun parseFromTrace( + data: ByteArray, + source: Path? = null + ): ErrorTrace { + var fileProto: FlickerErrorTraceProto? = null + try { + measureTimeMillis { + fileProto = FlickerErrorTraceProto.parseFrom(data) + }.also { + Log.v(LOG_TAG, "Parsing proto (Flicker Errors Trace): ${it}ms") + } + } catch (e: Exception) { + throw RuntimeException(e) + } + return fileProto?.let { + parseFromTrace(it, source) + } ?: error("Unable to read flicker errors trace file") + } + + /** + * Parses [FlickerErrorTraceProto] from [proto] and uses the proto to generates a list + * of trace entries, storing the flattened layers into its hierarchical structure. + * + * @param proto Parsed proto data + * @param source Path to source of data for additional debug information + */ + @JvmOverloads + @JvmStatic + fun parseFromTrace( + proto: FlickerErrorTraceProto, + source: Path? = null + ): ErrorTrace { + val states = mutableListOf<ErrorState>() + var traceParseTime = 0L + for (stateProto in proto.statesList) { + val errorParseTime = measureTimeMillis { + val errors = mutableListOf<Error>() + for (errorProto in stateProto.errorsList) { + errors.add( + Error( + stacktrace = errorProto.stacktrace, + message = errorProto.message, + layerId = errorProto.layerId, + windowToken = errorProto.windowToken, + taskId = errorProto.taskId, + assertionName = errorProto.assertionName + )) + } + states.add( + ErrorState( + _timestamp = stateProto.timestamp.toString(), + errors = errors.toTypedArray())) + } + traceParseTime += errorParseTime + } + return ErrorTrace( + entries = states.toTypedArray(), + source = source?.toString() ?: "" + ) + } + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/errors/Extensions.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/errors/Extensions.kt new file mode 100644 index 000000000..0d183ae60 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/parser/errors/Extensions.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmName("Extensions") + +package com.android.server.wm.traces.parser.errors + +import android.util.Log +import com.android.server.wm.flicker.FlickerErrorProto +import com.android.server.wm.flicker.FlickerErrorStateProto +import com.android.server.wm.flicker.FlickerErrorTraceProto +import com.android.server.wm.traces.common.errors.ErrorState +import com.android.server.wm.traces.common.errors.ErrorTrace +import com.android.server.wm.traces.common.errors.Error +import com.android.server.wm.traces.parser.LOG_TAG +import java.io.IOException +import java.nio.file.Files +import java.nio.file.Path + +/** + * Stores the error trace in a .winscope file. + */ +fun ErrorTrace.writeToFile(outputFile: Path) { + val proto = FlickerErrorTraceProto + .newBuilder() + .addAllStates(this.entries.map { it.toProto() }) + .setMagicNumber( + FlickerErrorTraceProto.MagicNumber.MAGIC_NUMBER_H.number.toLong() shl 32 or + FlickerErrorTraceProto.MagicNumber.MAGIC_NUMBER_L.number.toLong() + ) + .build() + val errorTraceBytes = proto.toByteArray() + + try { + Log.d(LOG_TAG, outputFile.toString()) + Files.createDirectories(outputFile.parent) + Files.write(outputFile, errorTraceBytes) + } catch (e: IOException) { + throw RuntimeException("Unable to create error trace file: ${e.message}", e) + } +} + +fun ErrorState.toProto(): FlickerErrorStateProto = FlickerErrorStateProto + .newBuilder() + .addAllErrors(this.errors.map { it.toProto() }) + .setTimestamp(this.timestamp) + .build() + +fun Error.toProto(): FlickerErrorProto = FlickerErrorProto + .newBuilder() + .setStacktrace(this.stacktrace) + .setMessage(this.message) + .setLayerId(this.layerId) + .setWindowToken(this.windowToken) + .setTaskId(this.taskId) + .setAssertionName(this.assertionName) + .build()
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/layers/LayersTraceParser.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/layers/LayersTraceParser.kt index 5bf48bfc4..bd473e5d9 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/parser/layers/LayersTraceParser.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/parser/layers/LayersTraceParser.kt @@ -17,8 +17,10 @@ package com.android.server.wm.traces.parser.layers import android.graphics.Rect +import android.surfaceflinger.nano.Common.RectProto +import android.surfaceflinger.nano.Common.SizeProto +import android.surfaceflinger.nano.Display.DisplayProto import android.surfaceflinger.nano.Layers -import android.surfaceflinger.nano.Layers.RectProto import android.surfaceflinger.nano.Layers.RegionProto import android.surfaceflinger.nano.Layerstrace import android.util.Log @@ -26,6 +28,8 @@ import com.android.server.wm.traces.common.Buffer import com.android.server.wm.traces.common.Color import com.android.server.wm.traces.common.RectF import com.android.server.wm.traces.common.Region +import com.android.server.wm.traces.common.Size +import com.android.server.wm.traces.common.layers.Display import com.android.server.wm.traces.common.layers.Layer import com.android.server.wm.traces.common.layers.LayerTraceEntry import com.android.server.wm.traces.common.layers.LayerTraceEntryBuilder @@ -33,6 +37,7 @@ import com.android.server.wm.traces.common.layers.LayersTrace import com.android.server.wm.traces.parser.LOG_TAG import com.google.protobuf.nano.InvalidProtocolBufferNanoException import java.nio.file.Path +import kotlin.math.max import kotlin.system.measureTimeMillis /** @@ -46,7 +51,6 @@ class LayersTraceParser { * * @param data binary proto data * @param source Path to source of data for additional debug information - * @param sourceChecksum Checksum of the source file * @param orphanLayerCallback a callback to handle any unexpected orphan layers */ @JvmOverloads @@ -54,10 +58,9 @@ class LayersTraceParser { fun parseFromTrace( data: ByteArray, source: Path? = null, - sourceChecksum: String = "", orphanLayerCallback: ((Layer) -> Boolean)? = null ): LayersTrace { - val fileProto: Layerstrace.LayersTraceFileProto + var fileProto: Layerstrace.LayersTraceFileProto? = null try { measureTimeMillis { fileProto = Layerstrace.LayersTraceFileProto.parseFrom(data) @@ -67,7 +70,9 @@ class LayersTraceParser { } catch (e: Exception) { throw RuntimeException(e) } - return parseFromTrace(fileProto, source, sourceChecksum, orphanLayerCallback) + return fileProto?.let { + parseFromTrace(it, source, orphanLayerCallback) + } ?: error("Unable to read trace file") } /** @@ -76,7 +81,6 @@ class LayersTraceParser { * * @param proto Parsed proto data * @param source Path to source of data for additional debug information - * @param sourceChecksum Checksum of the source file * @param orphanLayerCallback a callback to handle any unexpected orphan layers */ @JvmOverloads @@ -84,7 +88,6 @@ class LayersTraceParser { fun parseFromTrace( proto: Layerstrace.LayersTraceFileProto, source: Path? = null, - sourceChecksum: String = "", orphanLayerCallback: ((Layer) -> Boolean)? = null ): LayersTrace { val entries: MutableList<LayerTraceEntry> = ArrayList() @@ -92,15 +95,16 @@ class LayersTraceParser { for (traceProto: Layerstrace.LayersTraceProto in proto.entry) { val entryParseTime = measureTimeMillis { val entry = newEntry( - traceProto.elapsedRealtimeNanos, traceProto.layers.layers, - traceProto.hwcBlob, traceProto.where, orphanLayerCallback) + traceProto.elapsedRealtimeNanos, traceProto.displays, + traceProto.layers.layers, traceProto.hwcBlob, traceProto.where, + orphanLayerCallback) entries.add(entry) } traceParseTime += entryParseTime } Log.v(LOG_TAG, "Parsing duration (Layers Trace): ${traceParseTime}ms " + - "(avg ${traceParseTime / entries.size}ms per entry)") - return LayersTrace(entries, source?.toString() ?: "", sourceChecksum) + "(avg ${traceParseTime / max(entries.size, 1)}ms per entry)") + return LayersTrace(entries.toTypedArray(), source?.toString() ?: "") } /** @@ -110,8 +114,11 @@ class LayersTraceParser { * @param proto Parsed proto data */ @JvmStatic - fun parseFromDump(proto: Layers.LayersProto): LayersTrace { - val entry = newEntry(timestamp = 0, protos = proto.layers) + @Deprecated("This functions parsers old SF dumps. Now SF dumps create a " + + "single entry trace, for new dump use [parseFromTrace]") + fun parseFromLegacyDump(proto: Layers.LayersProto): LayersTrace { + val entry = newEntry(timestamp = 0, displayProtos = emptyArray(), + protos = proto.layers) return LayersTrace(entry) } @@ -122,25 +129,29 @@ class LayersTraceParser { * @param data binary proto data */ @JvmStatic - fun parseFromDump(data: ByteArray?): LayersTrace { + @Deprecated("This functions parsers old SF dumps. Now SF dumps create a " + + "single entry trace, for new dump use [parseFromTrace]") + fun parseFromLegacyDump(data: ByteArray?): LayersTrace { val traceProto = try { Layers.LayersProto.parseFrom(data) } catch (e: InvalidProtocolBufferNanoException) { throw RuntimeException(e) } - return parseFromDump(traceProto) + return parseFromLegacyDump(traceProto) } @JvmStatic private fun newEntry( timestamp: Long, + displayProtos: Array<DisplayProto>, protos: Array<Layers.LayerProto>, hwcBlob: String = "", where: String = "", orphanLayerCallback: ((Layer) -> Boolean)? = null ): LayerTraceEntry { - val layers = protos.map { newLayer(it) } - val builder = LayerTraceEntryBuilder(timestamp, layers, hwcBlob, where) + val layers = protos.map { newLayer(it) }.toTypedArray() + val displays = displayProtos.map { newDisplay(it) }.toTypedArray() + val builder = LayerTraceEntryBuilder(timestamp, layers, displays, hwcBlob, where) builder.setOrphanLayerCallback(orphanLayerCallback) return builder.build() } @@ -171,7 +182,7 @@ class LayersTraceParser { proto.type ?: "", proto.screenBounds?.toRectF(), Transform(proto.transform, proto.position), - proto.sourceBounds?.toRectF(), + proto.sourceBounds?.toRectF() ?: RectF.EMPTY, proto.currFrame, proto.effectiveScalingMode, Transform(proto.bufferTransform, position = null), @@ -185,6 +196,17 @@ class LayersTraceParser { ) } + private fun newDisplay(proto: DisplayProto): Display { + return Display( + proto.id.toULong(), + proto.name, + proto.layerStack, + proto.size.toSize(), + proto.layerStackSpaceRect.toRect(), + Transform(proto.transform, position = null) + ) + } + @JvmStatic private fun Layers.FloatRectProto?.toRectF(): RectF? { return this?.let { @@ -193,6 +215,13 @@ class LayersTraceParser { } @JvmStatic + private fun SizeProto?.toSize(): Size { + return this?.let { + Size(this.w, this.h) + } ?: Size.EMPTY + } + + @JvmStatic private fun Layers.ColorProto?.toColor(): Color { if (this == null) { return Color.EMPTY diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/layers/Transform.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/layers/Transform.kt index ca1b28fdf..856ebb2eb 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/parser/layers/Transform.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/parser/layers/Transform.kt @@ -17,6 +17,7 @@ package com.android.server.wm.traces.parser.layers import android.surfaceflinger.nano.Layers +import android.surfaceflinger.nano.Common.TransformProto import com.android.server.wm.traces.common.layers.Transform import com.android.server.wm.traces.common.layers.Transform.Companion.FLIP_H_VAL import com.android.server.wm.traces.common.layers.Transform.Companion.FLIP_V_VAL @@ -26,13 +27,13 @@ import com.android.server.wm.traces.common.layers.Transform.Companion.SCALE_VAL import com.android.server.wm.traces.common.layers.Transform.Companion.isFlagClear import com.android.server.wm.traces.common.layers.Transform.Companion.isFlagSet -class Transform(transform: Layers.TransformProto?, position: Layers.PositionProto?) : +class Transform(transform: TransformProto?, position: Layers.PositionProto?) : Transform( transform?.type, getMatrix(transform, position) ) -private fun getMatrix(transform: Layers.TransformProto?, position: Layers.PositionProto?): +private fun getMatrix(transform: TransformProto?, position: Layers.PositionProto?): Transform.Matrix { val x = position?.x ?: 0f val y = position?.y ?: 0f @@ -65,4 +66,4 @@ private fun Int?.getDefaultTransform(x: Float, y: Float): Transform.Matrix { else -> throw IllegalStateException("Unknown transform type $this") } -}
\ No newline at end of file +} diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/tags/Extensions.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/tags/Extensions.kt new file mode 100644 index 000000000..287758011 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/parser/tags/Extensions.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmName("Extensions") + +package com.android.server.wm.traces.parser.tags + +import android.util.Log +import com.android.server.wm.flicker.FlickerTagProto +import com.android.server.wm.flicker.FlickerTagStateProto +import com.android.server.wm.flicker.FlickerTagTraceProto +import com.android.server.wm.traces.common.tags.Tag +import com.android.server.wm.traces.common.tags.TagState +import com.android.server.wm.traces.common.tags.TagTrace +import com.android.server.wm.traces.parser.LOG_TAG +import java.nio.file.Files +import java.nio.file.Path + +fun TagTrace.writeToFile(outputFile: Path) { + val proto = FlickerTagTraceProto + .newBuilder() + .addAllStates(this.entries.map { it.toProto() }) + .setMagicNumber( + FlickerTagTraceProto.MagicNumber.MAGIC_NUMBER_H.number.toLong() shl 32 or + FlickerTagTraceProto.MagicNumber.MAGIC_NUMBER_L.number.toLong() + ) + .build() + + val tagTraceBytes = proto.toByteArray() + + try { + Log.d(LOG_TAG, outputFile.toString()) + Files.createDirectories(outputFile.parent) + Files.write(outputFile, tagTraceBytes) + } catch (e: Exception) { + throw RuntimeException("Unable to create error trace file: ${e.message}", e) + } +} + +fun TagState.toProto(): FlickerTagStateProto = FlickerTagStateProto + .newBuilder() + .addAllTags(this.tags.map { it.toProto() }) + .setTimestamp(this.timestamp) + .build() + +fun Tag.toProto(): FlickerTagProto = FlickerTagProto + .newBuilder() + .setIsStartTag(this.isStartTag) + .setTransition(FlickerTagProto.Transition.valueOf(this.transition.name)) + .setId(this.id) + .setTaskId(this.taskId) + .setWindowToken(this.windowToken) + .setLayerId(this.layerId) + .build() diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/tags/TagTraceParserUtil.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/tags/TagTraceParserUtil.kt new file mode 100644 index 000000000..1dd346c37 --- /dev/null +++ b/libraries/flicker/src/com/android/server/wm/traces/parser/tags/TagTraceParserUtil.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.traces.parser.tags + +import android.util.Log +import com.android.server.wm.flicker.FlickerTagTraceProto +import com.android.server.wm.traces.common.tags.Tag +import com.android.server.wm.traces.common.tags.TagState +import com.android.server.wm.traces.common.tags.TagTrace +import com.android.server.wm.traces.common.tags.Transition +import com.android.server.wm.traces.parser.LOG_TAG +import java.nio.file.Path +import kotlin.system.measureTimeMillis + +/** + * Class that holds the methods to parse tag proto files into tag classes. + */ +class TagTraceParserUtil { + companion object { + /** + * Parses [FlickerTagTraceProto] from [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 + */ + @JvmOverloads + @JvmStatic + fun parseFromTrace( + data: ByteArray, + source: Path? = null + ): TagTrace { + var fileProto: FlickerTagTraceProto? = null + try { + measureTimeMillis { + fileProto = FlickerTagTraceProto.parseFrom(data) + }.also { + Log.v(LOG_TAG, "Parsing proto (Flicker Tags Trace): ${it}ms") + } + } catch (e: Exception) { + throw RuntimeException(e) + } + return fileProto?.let { + parseFromTrace(it) + } ?: error("Unable to read flicker tags trace file") + } + + /** + * Parses [FlickerTagTraceProto] from [proto] and uses the proto to generates a list + * of trace entries, storing the flattened layers into its hierarchical structure. + * + * @param proto Parsed proto data + * @param source Path to source of data for additional debug informationy + */ + @JvmOverloads + @JvmStatic + fun parseFromTrace( + proto: FlickerTagTraceProto, + source: Path? = null + ): TagTrace { + val states = mutableListOf<TagState>() + var traceParseTime = 0L + for (stateProto in proto.statesList) { + val tagParseTime = measureTimeMillis { + val tags = mutableListOf<Tag>() + for (tagProto in stateProto.tagsList) { + tags.add( + Tag( + layerId = tagProto.layerId, + windowToken = tagProto.windowToken, + taskId = tagProto.taskId, + transition = Transition.valueOf(tagProto.transition.name), + id = tagProto.id, + isStartTag = tagProto.isStartTag + )) + } + states.add( + TagState( + _timestamp = stateProto.timestamp.toString(), + tags = tags.toTypedArray())) + } + traceParseTime += tagParseTime + } + return TagTrace( + entries = states.toTypedArray(), + source = source?.toString() ?: "" + ) + } + } +}
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WaitForValidActivityState.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WaitForValidActivityState.kt index 1a0b23c58..8889fd0c0 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WaitForValidActivityState.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WaitForValidActivityState.kt @@ -18,13 +18,11 @@ package com.android.server.wm.traces.parser.windowmanager import android.app.ActivityTaskManager import android.app.WindowConfiguration -import android.content.ComponentName -import com.android.server.wm.traces.parser.toActivityName -import com.android.server.wm.traces.parser.toWindowName +import com.android.server.wm.traces.common.FlickerComponentName data class WaitForValidActivityState( @JvmField - val activityName: ComponentName?, + val activityName: FlickerComponentName?, @JvmField val windowName: String?, @JvmField @@ -34,7 +32,7 @@ data class WaitForValidActivityState( @JvmField val activityType: Int ) { - constructor(activityName: ComponentName) : this( + constructor(activityName: FlickerComponentName) : this( activityName, windowName = activityName.toWindowName(), stackId = ActivityTaskManager.INVALID_STACK_ID, @@ -70,7 +68,7 @@ data class WaitForValidActivityState( return sb.toString() } - class Builder constructor(internal var activityName: ComponentName? = null) { + class Builder constructor(internal var activityName: FlickerComponentName? = null) { internal var windowName: String? = activityName?.toWindowName() internal var stackId = ActivityTaskManager.INVALID_STACK_ID internal var windowingMode = WindowConfiguration.WINDOWING_MODE_UNDEFINED diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerStateHelper.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerStateHelper.kt index 3d4b08395..522915bd5 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerStateHelper.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerStateHelper.kt @@ -19,25 +19,26 @@ package com.android.server.wm.traces.parser.windowmanager import android.app.ActivityTaskManager import android.app.Instrumentation import android.app.WindowConfiguration -import android.content.ComponentName import android.graphics.Rect import android.graphics.Region import android.os.SystemClock import android.util.Log import android.view.Display -import androidx.annotation.VisibleForTesting import androidx.test.platform.app.InstrumentationRegistry -import com.android.server.wm.traces.common.layers.LayerTraceEntry -import com.android.server.wm.traces.common.windowmanager.WindowManagerState -import com.android.server.wm.traces.parser.getCurrentStateDump import com.android.server.wm.traces.common.windowmanager.windows.ConfigurationContainer import com.android.server.wm.traces.common.windowmanager.windows.WindowContainer import com.android.server.wm.traces.common.windowmanager.windows.WindowState -import com.android.server.wm.traces.parser.Condition +import com.android.server.wm.traces.common.Condition +import com.android.server.wm.traces.common.DeviceStateDump +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.common.FlickerComponentName.Companion.IME import com.android.server.wm.traces.parser.LOG_TAG -import com.android.server.wm.traces.parser.toActivityName +import com.android.server.wm.traces.common.WaitCondition +import com.android.server.wm.traces.common.layers.LayerTraceEntry +import com.android.server.wm.traces.common.WindowManagerConditionsFactory +import com.android.server.wm.traces.common.windowmanager.WindowManagerState +import com.android.server.wm.traces.parser.getCurrentStateDump import com.android.server.wm.traces.parser.toAndroidRegion -import com.android.server.wm.traces.parser.toWindowName open class WindowManagerStateHelper @JvmOverloads constructor( /** @@ -47,53 +48,51 @@ open class WindowManagerStateHelper @JvmOverloads constructor( /** * Predicate to supply a new UI information */ - private val deviceDumpSupplier: () -> Dump = { - val currState = getCurrentStateDump( - instrumentation.uiAutomation) - Dump( - currState.wmTrace?.entries?.first() ?: error("Unable to parse WM trace"), - currState.layersTrace?.entries?.first() ?: error("Unable to parse Layers trace") + private val deviceDumpSupplier: () -> DeviceStateDump<WindowManagerState, LayerTraceEntry> = { + val currState = getCurrentStateDump(instrumentation.uiAutomation) + DeviceStateDump( + currState.wmState ?: error("Unable to parse WM trace"), + currState.layerState ?: error("Unable to parse Layers trace") ) }, /** * Number of attempts to satisfy a wait condition */ - private val numRetries: Int = 5, + private val numRetries: Int = WaitCondition.DEFAULT_RETRY_LIMIT, /** * Interval between wait for state dumps during wait conditions */ - private val retryIntervalMs: Long = 500L + private val retryIntervalMs: Long = WaitCondition.DEFAULT_RETRY_INTERVAL_MS ) { - /** - * Fetches the current device state - */ - val currentState: Dump - get() = computeState(ignoreInvalidStates = true) + private var internalState: DeviceStateDump<WindowManagerState, LayerTraceEntry>? = null /** * Queries the supplier for a new device state - * - * @param ignoreInvalidStates If false, retries up to [numRetries] times (with a sleep - * interval of [retryIntervalMs] ms to obtain a complete WM state, otherwise returns the - * first state */ - protected open fun computeState(ignoreInvalidStates: Boolean = false): Dump { - var newState = deviceDumpSupplier.invoke() - for (retryNr in 0..numRetries) { - val wmState = newState.wmState - if (!ignoreInvalidStates && wmState.isIncomplete()) { - Log.w(LOG_TAG, "***Incomplete AM state: ${wmState.getIsIncompleteReason()}" + - " Waiting ${retryIntervalMs}ms and retrying ($retryNr/$numRetries)...") - SystemClock.sleep(retryIntervalMs) - newState = deviceDumpSupplier.invoke() + val currentState: DeviceStateDump<WindowManagerState, LayerTraceEntry> + get() { + if (internalState == null) { + internalState = deviceDumpSupplier.invoke() } else { - break + waitForValidState() } - } + return internalState ?: error("Unable to fetch an internal state") + } - return newState + protected open fun updateCurrState( + value: DeviceStateDump<WindowManagerState, LayerTraceEntry> + ) { + internalState = value } + private fun createConditionBuilder(): + WaitCondition.Builder<DeviceStateDump<WindowManagerState, LayerTraceEntry>> = + WaitCondition.Builder(deviceDumpSupplier, numRetries) + .onSuccess { updateCurrState(it) } + .onFailure { updateCurrState(it) } + .onLog { Log.d(LOG_TAG, it) } + .onRetry { SystemClock.sleep(retryIntervalMs) } + private fun ConfigurationContainer.isWindowingModeCompatible( requestedWindowingMode: Int ): Boolean { @@ -111,16 +110,15 @@ open class WindowManagerStateHelper @JvmOverloads constructor( * @param waitForActivitiesVisible array of activity states to wait for. */ fun waitForValidState(vararg waitForActivitiesVisible: WaitForValidActivityState): Boolean { - val success = Condition.waitFor<WindowManagerState>("valid stacks and activities states", - retryLimit = numRetries, retryIntervalMs = retryIntervalMs) { - // TODO: Get state of AM and WM at the same time to avoid mismatches caused by - // requesting dump in some intermediate state. - val state = computeState() - !(shouldWaitForValidityCheck(state) || - shouldWaitForValidStacks(state) || - shouldWaitForActivities(state, *waitForActivitiesVisible) || - shouldWaitForWindows(state)) + val builder = createConditionBuilder() + .withCondition(WindowManagerConditionsFactory.isWMStateComplete()) + + if (waitForActivitiesVisible.isNotEmpty()) { + builder.withCondition("!shouldWaitForActivities") { + !shouldWaitForActivities(it, *waitForActivitiesVisible) + } } + val success = builder.build().waitFor() if (!success) { Log.e(LOG_TAG, "***Waiting for states failed: " + waitForActivitiesVisible.contentToString()) @@ -128,225 +126,140 @@ open class WindowManagerStateHelper @JvmOverloads constructor( return success } - fun waitForFullScreenApp(componentName: ComponentName): Boolean = + fun waitForFullScreenApp(component: FlickerComponentName): Boolean = waitForValidState( WaitForValidActivityState - .Builder(componentName) + .Builder(component) .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN) .setActivityType(WindowConfiguration.ACTIVITY_TYPE_STANDARD) .build()) - fun waitForHomeActivityVisible(): Boolean { - return waitFor { it.wmState.homeActivity?.isVisible == true } && - waitForNavBarStatusBarVisible() && - waitForAppTransitionIdle() - } + fun waitForHomeActivityVisible(): Boolean = + createConditionBuilder() + .withCondition(WindowManagerConditionsFactory.isHomeActivityVisible()) + .withCondition( + WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY)) + .withCondition(WindowManagerConditionsFactory.isNavBarVisible()) + .withCondition(WindowManagerConditionsFactory.isStatusBarVisible()) + .build() + .waitFor() fun waitForRecentsActivityVisible(): Boolean = - waitFor("recents activity to be visible") { - it.wmState.isRecentsActivityVisible - } - - fun waitForAodShowing(): Boolean = - waitFor("AOD showing") { - it.wmState.keyguardControllerState.isAodShowing - } - - fun waitForKeyguardGone(): Boolean = - waitFor("Keyguard gone") { - !it.wmState.keyguardControllerState.isKeyguardShowing - } + createConditionBuilder() + .withCondition("isRecentsActivityVisible") { + it.wmState.isRecentsActivityVisible + } + .build() + .waitFor() /** * Wait for specific rotation for the default display. Values are Surface#Rotation */ @JvmOverloads - fun waitForRotation(rotation: Int, displayId: Int = Display.DEFAULT_DISPLAY): Boolean = - waitFor("Rotation: $rotation") { - val currRotation = it.wmState.getRotation(displayId) - val rotationLayerExists = it.layerState.isVisible(ROTATION_LAYER_NAME) - val blackSurfaceLayerExists = it.layerState.isVisible(BLACK_SURFACE_LAYER_NAME) - val anyLayerAnimating = it.layerState.visibleLayers.any { layer -> - !layer.transform.isSimpleRotation + fun waitForRotation(rotation: Int, displayId: Int = Display.DEFAULT_DISPLAY): Boolean { + val hasRotationCondition = WindowManagerConditionsFactory.hasRotation(rotation, displayId) + return createConditionBuilder() + .withCondition("waitForRotation[$rotation]") { + if (!it.wmState.canRotate) { + Log.v(LOG_TAG, "Rotation is not allowed in the state") + true + } else { + hasRotationCondition.isSatisfied(it) + } } - Log.v(LOG_TAG, "currRotation($currRotation) " + - "anyLayerAnimating($anyLayerAnimating) " + - "blackSurfaceLayerExists($blackSurfaceLayerExists) " + - "rotationLayerExists($rotationLayerExists)") - currRotation == rotation && - !anyLayerAnimating && - !rotationLayerExists && - !blackSurfaceLayerExists - } - - /** - * Wait for specific orientation for the default display. - * Values are ActivityInfo.ScreenOrientation - */ - @JvmOverloads - fun waitForLastOrientation( - orientation: Int, - displayId: Int = Display.DEFAULT_DISPLAY - ): Boolean = - waitFor("LastOrientation: $orientation") { - val result = it.wmState.getOrientation(displayId) - Log.v(LOG_TAG, "Current: $result Expected: $orientation") - result == orientation - } + .withCondition( + WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY)) + .build() + .waitFor() + } - fun waitForActivityState(activity: ComponentName, activityState: String): Boolean { + fun waitForActivityState(activity: FlickerComponentName, activityState: String): Boolean { val activityName = activity.toActivityName() - return waitFor("state of $activityName to be $activityState") { - it.wmState.hasActivityState(activityName, activityState) - } + return createConditionBuilder() + .withCondition("state of $activityName to be $activityState") { + it.wmState.hasActivityState(activityName, activityState) + } + .build() + .waitFor() } /** * Waits until the navigation and status bars are visible (windows and layers) */ fun waitForNavBarStatusBarVisible(): Boolean = - waitFor("Navigation and Status bar to be visible") { - val navBarWindowVisible = it.wmState.isWindowVisible(NAV_BAR_WINDOW_NAME) - val statusBarWindowVisible = it.wmState.isWindowVisible(STATUS_BAR_WINDOW_NAME) - val navBarLayerVisible = it.layerState.isVisible(NAV_BAR_LAYER_NAME) - val navBarLayerAlpha = it.layerState.getLayerWithBuffer(NAV_BAR_LAYER_NAME) - ?.color?.a ?: 0f - val statusBarLayerVisible = it.layerState.isVisible(STATUS_BAR_LAYER_NAME) - val statusBarLayerAlpha = it.layerState.getLayerWithBuffer(STATUS_BAR_LAYER_NAME) - ?.color?.a ?: 0f - val result = navBarWindowVisible && - navBarLayerVisible && - statusBarWindowVisible && - statusBarLayerVisible && - navBarLayerAlpha == 1f && - statusBarLayerAlpha == 1f - - Log.v(LOG_TAG, "Current $result " + - "navBarWindowVisible($navBarWindowVisible) " + - "navBarLayerVisible($navBarLayerVisible) " + - "statusBarWindowVisible($statusBarWindowVisible) " + - "statusBarLayerVisible($statusBarLayerVisible) " + - "navBarLayerAlpha($navBarLayerAlpha) " + - "statusBarLayerAlpha($statusBarLayerAlpha)") - - result - } - - fun waitForVisibleWindow(activity: ComponentName): Boolean { - val activityName = activity.toActivityName() - val windowName = activity.toWindowName() - return waitFor("$activityName to exist") { - val containsActivity = it.wmState.containsActivity(activityName) - val containsWindow = it.wmState.containsWindow(windowName) - val activityVisible = containsActivity && it.wmState.isActivityVisible(activityName) - val windowVisible = containsWindow && it.wmState.isWindowSurfaceShown(windowName) - val result = containsActivity && - containsWindow && - activityVisible && - windowVisible - - Log.v(LOG_TAG, "Current: $result " + - "containsActivity($containsActivity) " + - "containsWindow($containsWindow) " + - "activityVisible($activityVisible) " + - "windowVisible($windowVisible)") - - result - } - } - - fun waitForActivityRemoved(activity: ComponentName): Boolean { - val activityName = activity.toActivityName() - val windowName = activity.toWindowName() - return waitFor("$activityName to be removed") { - val containsActivity = it.wmState.containsActivity(activityName) - val containsWindow = it.wmState.containsWindow(windowName) - val result = !containsActivity && !containsWindow - - Log.v(LOG_TAG, "Current: $result" + - "containsActivity($containsActivity)" + - "containsWindow($containsWindow)") - result - } - } - - fun waitForPendingActivityContain(activity: ComponentName): Boolean { - val activityName: String = activity.toActivityName() - return waitFor("$activityName in pending list") { - it.wmState.pendingActivityContain(activityName) - } - } + createConditionBuilder() + .withCondition(WindowManagerConditionsFactory.isNavBarVisible()) + .withCondition(WindowManagerConditionsFactory.isStatusBarVisible()) + .build() + .waitFor() + + fun waitForVisibleWindow(component: FlickerComponentName): Boolean = + createConditionBuilder() + .withCondition(WindowManagerConditionsFactory.containsActivity(component)) + .withCondition(WindowManagerConditionsFactory.containsWindow(component)) + .withCondition(WindowManagerConditionsFactory.isActivityVisible(component)) + .withCondition(WindowManagerConditionsFactory.isWindowSurfaceShown(component)) + .withCondition( + WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY)) + .build() + .waitFor() + + fun waitForActivityRemoved(component: FlickerComponentName): Boolean = + createConditionBuilder() + .withCondition(WindowManagerConditionsFactory.containsActivity(component).negate()) + .withCondition(WindowManagerConditionsFactory.containsWindow(component).negate()) + .withCondition( + WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY)) + .build() + .waitFor() @JvmOverloads fun waitForAppTransitionIdle(displayId: Int = Display.DEFAULT_DISPLAY): Boolean = - waitFor("app transition idle on Display $displayId") { - val result = - it.wmState.getDisplay(displayId)?.appTransitionState - Log.v(LOG_TAG, "Current: $result") - WindowManagerState.APP_STATE_IDLE == result - } - - fun waitForWindowSurfaceDisappeared(componentName: ComponentName): Boolean { - val windowName = componentName.toWindowName() - return waitFor("$windowName's surface is disappeared") { - !it.wmState.isWindowSurfaceShown(windowName) - } + createConditionBuilder() + .withCondition(WindowManagerConditionsFactory.isAppTransitionIdle(displayId)) + .build() + .waitFor() + + fun waitForWindowSurfaceDisappeared(component: FlickerComponentName): Boolean { + val condition = WindowManagerConditionsFactory.isWindowSurfaceShown(component).negate() + return createConditionBuilder() + .withCondition(condition) + .withCondition( + WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY)) + .build() + .waitFor() } - fun waitForSurfaceAppeared(surfaceName: String): Boolean { - return waitFor("$surfaceName surface is appeared") { - it.wmState.isWindowSurfaceShown(surfaceName) - } - } + fun waitForSurfaceAppeared(surfaceName: String): Boolean = + createConditionBuilder() + .withCondition(WindowManagerConditionsFactory.isWindowSurfaceShown(surfaceName)) + .withCondition( + WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY)) + .build() + .waitFor() - fun waitWindowingModeTopFocus( - windowingMode: Int, - topFocus: Boolean, - message: String - ): Boolean = waitFor(message) { - val stack = it.wmState.getStandardStackByWindowingMode(windowingMode) - (stack != null && topFocus == (it.wmState.focusedStackId == stack.rootTaskId)) + fun waitFor( + vararg conditions: Condition<DeviceStateDump<WindowManagerState, LayerTraceEntry>> + ): Boolean { + val builder = createConditionBuilder() + conditions.forEach { builder.withCondition(it) } + return builder.build().waitFor() } @JvmOverloads fun waitFor( message: String = "", - waitCondition: (Dump) -> Boolean - ): Boolean = Condition.waitFor<Dump>(message, retryLimit = numRetries, - retryIntervalMs = retryIntervalMs) { - val state = computeState() - waitCondition.invoke(state) - } - - /** - * @return true if should wait for valid stacks state. - */ - private fun shouldWaitForValidStacks(state: Dump): Boolean { - if (state.wmState.stackCount == 0) { - Log.i(LOG_TAG, "***stackCount=0") - return true - } - if (!state.wmState.keyguardControllerState.isKeyguardShowing && - state.wmState.resumedActivities.isEmpty()) { - if (!state.wmState.keyguardControllerState.isKeyguardShowing) { - Log.i(LOG_TAG, "***resumedActivitiesCount=0") - } else { - Log.i(LOG_TAG, "***isKeyguardShowing=true") - } - return true - } - if (state.wmState.focusedActivity.isEmpty()) { - Log.i(LOG_TAG, "***focusedActivity=null") - return true - } - return false - } + waitCondition: (DeviceStateDump<WindowManagerState, LayerTraceEntry>) -> Boolean + ): Boolean = createConditionBuilder() + .withCondition(message, waitCondition) + .build() + .waitFor() /** * @return true if should wait for some activities to become visible. */ private fun shouldWaitForActivities( - state: Dump, + state: DeviceStateDump<WindowManagerState, LayerTraceEntry>, vararg waitForActivitiesVisible: WaitForValidActivityState ): Boolean { if (waitForActivitiesVisible.isEmpty()) { @@ -397,65 +310,35 @@ open class WindowManagerStateHelper @JvmOverloads constructor( } /** - * @return true if should wait for the valid windows state. - */ - private fun shouldWaitForWindows(state: Dump): Boolean { - return when { - state.wmState.frontWindow == null -> { - Log.i(LOG_TAG, "***frontWindow=null") - true - } - state.wmState.focusedWindow.isEmpty() -> { - Log.i(LOG_TAG, "***focusedWindow=null") - true - } - state.wmState.focusedApp.isEmpty() -> { - Log.i(LOG_TAG, "***focusedApp=null") - true - } - else -> false - } - } - - private fun shouldWaitForValidityCheck(state: Dump): Boolean { - return !state.wmState.isComplete() - } - - /** * Waits until the IME window and layer are visible */ @JvmOverloads - fun waitImeWindowShown(displayId: Int = Display.DEFAULT_DISPLAY): Boolean = - waitFor("IME window shown") { - val imeSurfaceShown = it.wmState.inputMethodWindowState?.isSurfaceShown == true - val imeDisplay = it.wmState.inputMethodWindowState?.displayId - val imeLayerShown = it.layerState.isVisible(IME_LAYER_NAME) - val result = imeSurfaceShown && imeLayerShown && imeDisplay == displayId - - Log.v(LOG_TAG, "Current: $result " + - "imeSurfaceShown($imeSurfaceShown) " + - "imeLayerShown($imeLayerShown) " + - "imeDisplay($imeDisplay)") - result - } + fun waitImeShown(displayId: Int = Display.DEFAULT_DISPLAY): Boolean = + createConditionBuilder() + .withCondition(WindowManagerConditionsFactory.isImeShown(displayId)) + .withCondition( + WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY)) + .build() + .waitFor() /** * Waits until the IME layer is no longer visible. Cannot wait for the window as * its visibility information is updated at a later state and is not reliable in * the trace */ - fun waitImeWindowGone(): Boolean = - waitFor("IME window gone") { - val imeLayerShown = it.layerState.isVisible(IME_LAYER_NAME) - Log.v(LOG_TAG, "imeLayerShown($imeLayerShown)") - !imeLayerShown - } + fun waitImeGone(): Boolean = + createConditionBuilder() + .withCondition(WindowManagerConditionsFactory.isLayerVisible(IME).negate()) + .withCondition( + WindowManagerConditionsFactory.isAppTransitionIdle(Display.DEFAULT_DISPLAY)) + .build() + .waitFor() /** * Obtains a [WindowContainer] from the current device state, or null if the WindowContainer * doesn't exist */ - fun getWindow(activity: ComponentName): WindowState? { + fun getWindow(activity: FlickerComponentName): WindowState? { val windowName = activity.toWindowName() return this.currentState.wmState.windowStates .firstOrNull { it.title == windowName } @@ -464,40 +347,8 @@ open class WindowManagerStateHelper @JvmOverloads constructor( /** * Obtains the region of a window in the state, or an empty [Rect] is there are none */ - fun getWindowRegion(activity: ComponentName): Region { + fun getWindowRegion(activity: FlickerComponentName): Region { val window = getWindow(activity) return window?.frameRegion?.toAndroidRegion() ?: Region() } - - companion object { - @VisibleForTesting - const val NAV_BAR_WINDOW_NAME = "NavigationBar0" - @VisibleForTesting - const val STATUS_BAR_WINDOW_NAME = "StatusBar" - @VisibleForTesting - const val NAV_BAR_LAYER_NAME = "$NAV_BAR_WINDOW_NAME#0" - @VisibleForTesting - const val STATUS_BAR_LAYER_NAME = "$STATUS_BAR_WINDOW_NAME#0" - @VisibleForTesting - const val ROTATION_LAYER_NAME = "RotationLayer#0" - @VisibleForTesting - const val BLACK_SURFACE_LAYER_NAME = "BackColorSurface#0" - @VisibleForTesting - const val IME_LAYER_NAME = "InputMethod#0" - @VisibleForTesting - const val SPLASH_SCREEN_NAME = "Splash Screen" - @VisibleForTesting - const val SNAPSHOT_WINDOW_NAME = "SnapshotStartingWindow" - } - - data class Dump( - /** - * Window manager state - */ - @JvmField val wmState: WindowManagerState, - /** - * Layers state - */ - @JvmField val layerState: LayerTraceEntry - ) }
\ No newline at end of file diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerTraceParser.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerTraceParser.kt index 718b0b3ca..010f421c6 100644 --- a/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerTraceParser.kt +++ b/libraries/flicker/src/com/android/server/wm/traces/parser/windowmanager/WindowManagerTraceParser.kt @@ -22,42 +22,45 @@ import android.graphics.nano.RectProto import android.util.Log import android.view.nano.ViewProtoEnums import android.view.nano.WindowLayoutParamsProto -import com.android.server.wm.traces.common.windowmanager.windows.Configuration -import com.android.server.wm.traces.common.Rect -import com.android.server.wm.traces.common.windowmanager.windows.WindowConfiguration -import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace -import com.android.server.wm.traces.common.windowmanager.WindowManagerState -import com.android.server.wm.traces.common.windowmanager.windows.Activity -import com.android.server.wm.traces.common.windowmanager.windows.ActivityTask -import com.android.server.wm.traces.common.windowmanager.windows.ConfigurationContainer -import com.android.server.wm.traces.common.windowmanager.windows.DisplayArea -import com.android.server.wm.traces.common.windowmanager.windows.DisplayContent -import com.android.server.wm.traces.common.windowmanager.windows.KeyguardControllerState -import com.android.server.wm.traces.common.windowmanager.windows.RootWindowContainer -import com.android.server.wm.traces.common.windowmanager.windows.WindowContainer -import com.android.server.wm.traces.common.windowmanager.windows.WindowManagerPolicy -import com.android.server.wm.traces.common.windowmanager.windows.WindowState -import com.android.server.wm.traces.common.windowmanager.windows.WindowToken import com.android.server.wm.nano.ActivityRecordProto import com.android.server.wm.nano.AppTransitionProto import com.android.server.wm.nano.ConfigurationContainerProto import com.android.server.wm.nano.DisplayAreaProto import com.android.server.wm.nano.DisplayContentProto import com.android.server.wm.nano.KeyguardControllerProto -import com.android.server.wm.nano.WindowManagerPolicyProto import com.android.server.wm.nano.RootWindowContainerProto +import com.android.server.wm.nano.TaskFragmentProto import com.android.server.wm.nano.TaskProto import com.android.server.wm.nano.WindowContainerChildProto import com.android.server.wm.nano.WindowContainerProto +import com.android.server.wm.nano.WindowManagerPolicyProto import com.android.server.wm.nano.WindowManagerServiceDumpProto import com.android.server.wm.nano.WindowManagerTraceFileProto import com.android.server.wm.nano.WindowStateProto import com.android.server.wm.nano.WindowTokenProto -import com.android.server.wm.traces.common.Bounds +import com.android.server.wm.traces.common.Size +import com.android.server.wm.traces.common.Rect +import com.android.server.wm.traces.common.windowmanager.WindowManagerState +import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace +import com.android.server.wm.traces.common.windowmanager.windows.Activity +import com.android.server.wm.traces.common.windowmanager.windows.Configuration +import com.android.server.wm.traces.common.windowmanager.windows.ConfigurationContainer +import com.android.server.wm.traces.common.windowmanager.windows.DisplayArea +import com.android.server.wm.traces.common.windowmanager.windows.DisplayContent +import com.android.server.wm.traces.common.windowmanager.windows.KeyguardControllerState +import com.android.server.wm.traces.common.windowmanager.windows.RootWindowContainer +import com.android.server.wm.traces.common.windowmanager.windows.Task +import com.android.server.wm.traces.common.windowmanager.windows.TaskFragment +import com.android.server.wm.traces.common.windowmanager.windows.WindowConfiguration +import com.android.server.wm.traces.common.windowmanager.windows.WindowContainer import com.android.server.wm.traces.common.windowmanager.windows.WindowLayoutParams +import com.android.server.wm.traces.common.windowmanager.windows.WindowManagerPolicy +import com.android.server.wm.traces.common.windowmanager.windows.WindowState +import com.android.server.wm.traces.common.windowmanager.windows.WindowToken import com.android.server.wm.traces.parser.LOG_TAG import com.google.protobuf.nano.InvalidProtocolBufferNanoException import java.nio.file.Path +import kotlin.math.max import kotlin.system.measureTimeMillis object WindowManagerTraceParser { @@ -83,16 +86,14 @@ object WindowManagerTraceParser { * * @param data binary proto data * @param source Path to source of data for additional debug information - * @param checksum File SHA512 checksum */ @JvmOverloads @JvmStatic fun parseFromTrace( data: ByteArray?, - source: Path? = null, - checksum: String = "" + source: Path? = null ): WindowManagerTrace { - val fileProto: WindowManagerTraceFileProto + var fileProto: WindowManagerTraceFileProto? = null try { measureTimeMillis { fileProto = WindowManagerTraceFileProto.parseFrom(data) @@ -102,7 +103,9 @@ object WindowManagerTraceParser { } catch (e: InvalidProtocolBufferNanoException) { throw RuntimeException(e) } - return parseFromTrace(fileProto, source, checksum) + + return fileProto?.let { parseFromTrace(it, source) } + ?: error("Unable to read trace file") } /** @@ -110,14 +113,12 @@ object WindowManagerTraceParser { * * @param proto Parsed proto data * @param source Path to source of data for additional debug information - * @param checksum File SHA512 checksum */ @JvmOverloads @JvmStatic fun parseFromTrace( proto: WindowManagerTraceFileProto, - source: Path? = null, - checksum: String = "" + source: Path? = null ): WindowManagerTrace { val entries = mutableListOf<WindowManagerState>() var traceParseTime = 0L @@ -131,9 +132,8 @@ object WindowManagerTraceParser { } Log.v(LOG_TAG, "Parsing duration (WM Trace): ${traceParseTime}ms " + - "(avg ${traceParseTime / entries.size}ms per entry)") - return WindowManagerTrace(entries, source?.toAbsolutePath()?.toString() - ?: "", checksum) + "(avg ${traceParseTime / max(entries.size, 1)}ms per entry)") + return WindowManagerTrace(entries.toTypedArray(), "${source?.toAbsolutePath()}") } /** @@ -145,9 +145,8 @@ object WindowManagerTraceParser { @JvmStatic fun parseFromDump(proto: WindowManagerServiceDumpProto): WindowManagerTrace { return WindowManagerTrace( - listOf(newTraceEntry(proto, timestamp = 0, where = "")), - source = "", - sourceChecksum = "") + arrayOf(newTraceEntry(proto, timestamp = 0, where = "")), + source = "") } /** @@ -163,7 +162,7 @@ object WindowManagerTraceParser { } catch (e: InvalidProtocolBufferNanoException) { throw RuntimeException(e) } - return WindowManagerTrace(parseFromDump(fileProto), source = "", sourceChecksum = "") + return parseFromDump(fileProto) } private fun newTraceEntry( @@ -189,11 +188,11 @@ object WindowManagerTraceParser { root = newRootWindowContainer(proto.rootWindowContainer), keyguardControllerState = newKeyguardControllerState( proto.rootWindowContainer.keyguardController), - timestamp = timestamp + _timestamp = timestamp.toString() ) } - private fun newWindowManagerPolicy(proto: WindowManagerPolicyProto): WindowManagerPolicy? { + private fun newWindowManagerPolicy(proto: WindowManagerPolicyProto): WindowManagerPolicy { return WindowManagerPolicy( focusedAppToken = proto.focusedAppToken ?: "", forceStatusBar = proto.forceStatusBar, @@ -240,7 +239,9 @@ object WindowManagerTraceParser { ): WindowContainer? { return newDisplayContent(proto.displayContent, isActivityInTree) ?: newDisplayArea(proto.displayArea, isActivityInTree) - ?: newTask(proto.task, isActivityInTree) ?: newActivity(proto.activity) + ?: newTask(proto.task, isActivityInTree) + ?: newTaskFragment(proto.taskFragment, isActivityInTree) + ?: newActivity(proto.activity) ?: newWindowToken(proto.windowToken, isActivityInTree) ?: newWindowState(proto.window, isActivityInTree) ?: newWindowContainer(proto.windowContainer, children = emptyList()) @@ -258,8 +259,10 @@ object WindowManagerTraceParser { focusedRootTaskId = proto.focusedRootTaskId, resumedActivity = proto.resumedActivity?.title ?: "", singleTaskInstance = proto.singleTaskInstance, - _defaultPinnedStackBounds = proto.pinnedTaskController?.defaultBounds?.toRect(), - _pinnedStackMovementBounds = proto.pinnedTaskController?.movementBounds?.toRect(), + defaultPinnedStackBounds = proto.pinnedTaskController?.defaultBounds?.toRect() + ?: Rect.EMPTY, + pinnedStackMovementBounds = proto.pinnedTaskController?.movementBounds?.toRect() + ?: Rect.EMPTY, displayRect = Rect(0, 0, proto.displayInfo?.logicalWidth ?: 0, proto.displayInfo?.logicalHeight ?: 0), appRect = Rect(0, 0, proto.displayInfo?.appWidth ?: 0, @@ -267,7 +270,7 @@ object WindowManagerTraceParser { ?: 0), dpi = proto.dpi, flags = proto.displayInfo?.flags ?: 0, - _stableBounds = proto.displayFrames?.stableBounds?.toRect(), + stableBounds = proto.displayFrames?.stableBounds?.toRect() ?: Rect.EMPTY, surfaceSize = proto.surfaceSize, focusedApp = proto.focusedApp, lastTransition = appTransitionToString( @@ -301,18 +304,18 @@ object WindowManagerTraceParser { } } - private fun newTask(proto: TaskProto?, isActivityInTree: Boolean): ActivityTask? { + private fun newTask(proto: TaskProto?, isActivityInTree: Boolean): Task? { return if (proto == null) { null } else { - ActivityTask( - activityType = proto.activityType, + Task( + activityType = proto.taskFragment?.activityType ?: proto.activityType, isFullscreen = proto.fillsParent, bounds = proto.bounds.toRect(), taskId = proto.id, rootTaskId = proto.rootTaskId, - displayId = proto.displayId, - _lastNonFullscreenBounds = proto.lastNonFullscreenBounds?.toRect(), + displayId = proto.taskFragment?.displayId ?: proto.displayId, + lastNonFullscreenBounds = proto.lastNonFullscreenBounds?.toRect() ?: Rect.EMPTY, realActivity = proto.realActivity, origActivity = proto.origActivity, resizeMode = proto.resizeMode, @@ -321,6 +324,30 @@ object WindowManagerTraceParser { surfaceWidth = proto.surfaceWidth, surfaceHeight = proto.surfaceHeight, createdByOrganizer = proto.createdByOrganizer, + minWidth = proto.taskFragment?.minWidth ?: proto.minWidth, + minHeight = proto.taskFragment?.minHeight ?: proto.minHeight, + windowContainer = newWindowContainer( + proto.taskFragment?.windowContainer ?: proto.windowContainer, + if (proto.taskFragment != null) { + proto.taskFragment.windowContainer.children + .mapNotNull { p -> newWindowContainerChild(p, isActivityInTree) } + } else { + proto.windowContainer.children + .mapNotNull { p -> newWindowContainerChild(p, isActivityInTree) } + } + ) ?: error("Window container should not be null") + ) + } + } + + private fun newTaskFragment(proto: TaskFragmentProto?, isActivityInTree: Boolean): + TaskFragment? { + return if (proto == null) { + null + } else { + TaskFragment( + activityType = proto.activityType, + displayId = proto.displayId, minWidth = proto.minWidth, minHeight = proto.minHeight, windowContainer = newWindowContainer( @@ -385,16 +412,16 @@ object WindowManagerTraceParser { WindowState.WINDOW_TYPE_STARTING else -> 0 }, - requestedSize = Bounds(proto.requestedWidth, proto.requestedHeight), + requestedSize = Size(proto.requestedWidth, proto.requestedHeight), surfacePosition = proto.surfacePosition?.toRect(), - _frame = proto.windowFrames?.frame?.toRect(), - _containingFrame = proto.windowFrames?.containingFrame?.toRect(), - _parentFrame = proto.windowFrames?.parentFrame?.toRect(), - _contentFrame = proto.windowFrames?.contentFrame?.toRect(), - _contentInsets = proto.windowFrames?.contentInsets?.toRect(), - _surfaceInsets = proto.surfaceInsets?.toRect(), - _givenContentInsets = proto.givenContentInsets?.toRect(), - _crop = proto.animator?.lastClipRect?.toRect(), + frame = proto.windowFrames?.frame?.toRect() ?: Rect.EMPTY, + containingFrame = proto.windowFrames?.containingFrame?.toRect() ?: Rect.EMPTY, + parentFrame = proto.windowFrames?.parentFrame?.toRect() ?: Rect.EMPTY, + contentFrame = proto.windowFrames?.contentFrame?.toRect() ?: Rect.EMPTY, + contentInsets = proto.windowFrames?.contentInsets?.toRect() ?: Rect.EMPTY, + surfaceInsets = proto.surfaceInsets?.toRect() ?: Rect.EMPTY, + givenContentInsets = proto.givenContentInsets?.toRect() ?: Rect.EMPTY, + crop = proto.animator?.lastClipRect?.toRect() ?: Rect.EMPTY, windowContainer = newWindowContainer( proto.windowContainer, proto.windowContainer.children @@ -504,7 +531,8 @@ object WindowManagerTraceParser { _isVisible = proto.visible, configurationContainer = newConfigurationContainer( proto.configurationContainer), - children.toTypedArray() + layerId = proto.surfaceControl?.layerId ?: 0, + children = children.toTypedArray() ) } } @@ -551,4 +579,4 @@ object WindowManagerTraceParser { } private fun RectProto.toRect() = Rect(this.left, this.top, this.right, this.bottom) -}
\ No newline at end of file +} diff --git a/libraries/flicker/test/Android.bp b/libraries/flicker/test/Android.bp index 1af44fe2c..314f4aa32 100644 --- a/libraries/flicker/test/Android.bp +++ b/libraries/flicker/test/Android.bp @@ -29,6 +29,9 @@ android_test { test_suites: ["device-tests"], srcs: ["src/**/*.kt"], libs: ["android.test.runner"], + optimize: { + enabled: false + }, static_libs: [ "flickerlib", "launcher-aosp-tapl" diff --git a/libraries/flicker/test/AndroidManifest.xml b/libraries/flicker/test/AndroidManifest.xml index 20b8f0a52..27e1dad86 100644 --- a/libraries/flicker/test/AndroidManifest.xml +++ b/libraries/flicker/test/AndroidManifest.xml @@ -20,6 +20,10 @@ <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT"/> <!-- Enable / Disable tracing !--> <uses-permission android:name="android.permission.DUMP" /> + <!-- ATM.removeRootTasksWithActivityTypes() --> + <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" /> + <!-- Force-stop test apps --> + <uses-permission android:name="android.permission.FORCE_STOP_PACKAGES"/> <!-- Allow the test to write directly to /sdcard/ --> <application android:label="FlickerLibTest" android:requestLegacyExternalStorage="true"> diff --git a/libraries/flicker/test/assets/testdata/assertors/AppLaunchAndRotationsSurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/assertors/AppLaunchAndRotationsSurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..3648f0459 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/assertors/AppLaunchAndRotationsSurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/assertors/AppLaunchAndRotationsTagTrace.winscope b/libraries/flicker/test/assets/testdata/assertors/AppLaunchAndRotationsTagTrace.winscope Binary files differnew file mode 100644 index 000000000..2c3cb1dab --- /dev/null +++ b/libraries/flicker/test/assets/testdata/assertors/AppLaunchAndRotationsTagTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/assertors/AppLaunchAndRotationsWindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/assertors/AppLaunchAndRotationsWindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..a6d57f3a9 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/assertors/AppLaunchAndRotationsWindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/assertors/appLaunch/SurfaceFlingerInvalidTrace.winscope b/libraries/flicker/test/assets/testdata/assertors/appLaunch/SurfaceFlingerInvalidTrace.winscope Binary files differnew file mode 100644 index 000000000..d45d04a14 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/assertors/appLaunch/SurfaceFlingerInvalidTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/assertors/appLaunch/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/assertors/appLaunch/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..92d3d76a3 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/assertors/appLaunch/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/assertors/appLaunch/WindowManagerInvalidTrace.winscope b/libraries/flicker/test/assets/testdata/assertors/appLaunch/WindowManagerInvalidTrace.winscope Binary files differnew file mode 100644 index 000000000..fe3a6acc4 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/assertors/appLaunch/WindowManagerInvalidTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/assertors/appLaunch/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/assertors/appLaunch/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..91538fbae --- /dev/null +++ b/libraries/flicker/test/assets/testdata/assertors/appLaunch/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/assertors/assertionsConfig.json b/libraries/flicker/test/assets/testdata/assertors/assertionsConfig.json new file mode 100644 index 000000000..2350b7504 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/assertors/assertionsConfig.json @@ -0,0 +1,120 @@ +{ + "assertors": [ + { + "transition": "ROTATION", + "assertions": { + "presubmit": [ + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtStart", + "args": [ + "/NavigationBar0" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtEnd", + "args": [ + "/NavigationBar0" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.NonAppWindowIsVisibleAlways", + "args": [ + "/NavigationBar0" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.NonAppWindowIsInvisibleAlways", + "args": [ + "/StatusBar" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsInvisibleAlways", + "args": [ + "/StatusBar" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.VisibleWindowsShownMoreThanOneConsecutiveEntry" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtStart" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtEnd" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.VisibleLayersShownMoreThanOneConsecutiveEntry" + } + ], + "postsubmit": [], + "flaky": [ + { + "class": "com.android.server.wm.flicker.service.assertors.common.NavBarLayerPositionAtStart" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.NavBarLayerPositionAtEnd" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAlways" + } + ] + } + }, + { + "transition": "APP_LAUNCH", + "assertions": { + "presubmit": [ + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAlways", + "args": [ + "/NavigationBar0" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.VisibleWindowsShownMoreThanOneConsecutiveEntry" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtStart", + "args": [ + "/NavigationBar0" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtEnd", + "args": [ + "/NavigationBar0" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.NavBarLayerPositionAtStart" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.NavBarLayerPositionAtEnd" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.VisibleLayersShownMoreThanOneConsecutiveEntry" + } + ], + "postsubmit": [], + "flaky": [ + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAlways", + "args": [ + "/StatusBar" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtStart" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtEnd" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAlways" + } + ] + } + } + ] +}
\ No newline at end of file diff --git a/libraries/flicker/test/assets/testdata/assertors/config.json b/libraries/flicker/test/assets/testdata/assertors/config.json new file mode 100644 index 000000000..174da8e11 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/assertors/config.json @@ -0,0 +1,127 @@ +{ + "assertors": [ + { + "transition": "ROTATION", + "assertions": { + "presubmit": [ + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtStart", + "args": [ + "/NavigationBar0" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtEnd", + "args": [ + "/NavigationBar0" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.NonAppWindowIsVisibleAlways", + "args": [ + "/NavigationBar0" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.NonAppWindowIsInvisibleAlways", + "args": [ + "/StatusBar" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsInvisibleAlways", + "args": [ + "/StatusBar" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.VisibleWindowsShownMoreThanOneConsecutiveEntry" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtStart" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtEnd" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.VisibleLayersShownMoreThanOneConsecutiveEntry" + } + ], + "postsubmit": [], + "flaky": [] + } + }, + { + "transition": "APP_LAUNCH", + "assertions": { + "presubmit": [ + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtStart", + "args": [ + "/NavigationBar0" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtEnd", + "args": [ + "/NavigationBar0" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.NonAppWindowIsVisibleAlways", + "args": [ + "/NavigationBar0" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.NonAppWindowIsVisibleAlways", + "args": [ + "/StatusBar" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAlways", + "args": [ + "/StatusBar" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtStart" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.EntireScreenCoveredAtEnd" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.AppLayerReplacesLauncher" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsVisibleAtStart", + "args": [ + "com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.LayerIsInvisibleAtEnd", + "args": [ + "com.google.android.apps.nexuslauncher/com.google.android.apps.nexuslauncher.NexusLauncherActivity" + ] + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.AppLayerIsInvisibleAtStart" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.AppLayerIsVisibleAtEnd" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.AppWindowReplacesLauncherAsTopWindow" + }, + { + "class": "com.android.server.wm.flicker.service.assertors.common.LauncherWindowMovesOutOfTop" + } + ], + "postsubmit": [], + "flaky": [] + } + } + ] +}
\ No newline at end of file diff --git a/libraries/flicker/test/assets/testdata/assertors/rotation/SurfaceFlingerInvalidTrace.winscope b/libraries/flicker/test/assets/testdata/assertors/rotation/SurfaceFlingerInvalidTrace.winscope Binary files differnew file mode 100644 index 000000000..645f8be65 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/assertors/rotation/SurfaceFlingerInvalidTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/assertors/rotation/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/assertors/rotation/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..4bdb32228 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/assertors/rotation/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/assertors/rotation/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/assertors/rotation/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..f146dc266 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/assertors/rotation/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/layers_dump_with_display.pb b/libraries/flicker/test/assets/testdata/layers_dump_with_display.pb Binary files differnew file mode 100644 index 000000000..430b3cc5f --- /dev/null +++ b/libraries/flicker/test/assets/testdata/layers_dump_with_display.pb diff --git a/libraries/flicker/test/assets/testdata/layers_trace_close_app_with_rotation.winscope b/libraries/flicker/test/assets/testdata/layers_trace_close_app_with_rotation.winscope Binary files differnew file mode 100644 index 000000000..59964cc5a --- /dev/null +++ b/libraries/flicker/test/assets/testdata/layers_trace_close_app_with_rotation.winscope diff --git a/libraries/flicker/test/assets/testdata/layers_trace_occluded.pb b/libraries/flicker/test/assets/testdata/layers_trace_occluded.pb Binary files differnew file mode 100644 index 000000000..0c1693c28 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/layers_trace_occluded.pb diff --git a/libraries/flicker/test/assets/testdata/layers_trace_openchrome.pb b/libraries/flicker/test/assets/testdata/layers_trace_openchrome.pb Binary files differnew file mode 100644 index 000000000..ba58762e1 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/layers_trace_openchrome.pb diff --git a/libraries/flicker/test/assets/testdata/layers_trace_pip_wmshell.pb b/libraries/flicker/test/assets/testdata/layers_trace_pip_wmshell.pb Binary files differnew file mode 100644 index 000000000..71e3f4719 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/layers_trace_pip_wmshell.pb diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/appclose/backbutton/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/backbutton/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..b20d68cf0 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/backbutton/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/appclose/backbutton/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/backbutton/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..b83c2ab7b --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/backbutton/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/appclose/rotated/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/rotated/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..e62a755e3 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/rotated/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/appclose/rotated/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/rotated/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..26c1c8c22 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/rotated/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/appclose/swipeup/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/swipeup/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..1a43a0328 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/swipeup/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/appclose/swipeup/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/swipeup/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..2cb81491c --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/swipeup/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/appclose/switchapp/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/switchapp/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..08d2743be --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/switchapp/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/appclose/switchapp/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/switchapp/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..96d30a041 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/appclose/switchapp/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/cold/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/cold/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..1a1de6156 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/cold/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/cold/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/cold/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..8fb920091 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/cold/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/intent/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/intent/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..587154313 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/intent/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/intent/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/intent/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..3ff92c696 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/intent/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/warm/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/warm/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..ac15b8a23 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/warm/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/warm/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/warm/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..7503f8031 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/warm/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/withrot/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/withrot/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..935c7d0f8 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/withrot/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/withrot/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/withrot/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..28d77555c --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/applaunch/withrot/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/applaunchnogesture/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/applaunchnogesture/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..6bccfc10d --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/applaunchnogesture/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/applaunchnogesture/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/applaunchnogesture/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..16171b3d4 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/applaunchnogesture/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/bygesture/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/bygesture/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..00f2be7a5 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/bygesture/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/bygesture/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/bygesture/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..31a914c42 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/bygesture/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/horizontal/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/horizontal/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..8322e0b46 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/horizontal/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/horizontal/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/horizontal/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..3a38466cc --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/horizontal/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/stationary/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/stationary/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..aa5cb0b5e --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/stationary/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/stationary/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/stationary/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..d25769bd0 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/appear/stationary/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/close/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/close/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..123dae767 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/close/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/close/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/close/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..22d7826d0 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/close/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/openandclose/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/openandclose/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..130dad9b2 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/openandclose/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/openandclose/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/openandclose/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..6f7639a92 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/bygesture/openandclose/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/close/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/close/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..a09d715de --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/close/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/close/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/close/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..4059964ca --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/close/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/openandclose/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/openandclose/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..3ba3f02a2 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/openandclose/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/openandclose/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/openandclose/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..691fff885 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/ime/disappear/closeapp/openandclose/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/norotation/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/norotation/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..9f01cd229 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/norotation/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/norotation/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/norotation/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..8c75f7c5d --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/norotation/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/rotation/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/rotation/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..27ab2810c --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/rotation/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/rotation/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/rotation/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..061067240 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/rotation/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/splitscreen/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/splitscreen/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..6c919c782 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/splitscreen/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/splitscreen/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/splitscreen/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..2cf460645 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/splitscreen/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/stationary/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/stationary/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..6011ac891 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/stationary/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/stationary/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/stationary/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..3e97adee4 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/stationary/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/twice/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/twice/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..31e22dda0 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/twice/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/twice/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/twice/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..296cd455a --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/enter/twice/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/dismissbutton/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/dismissbutton/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..8859bb867 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/dismissbutton/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/dismissbutton/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/dismissbutton/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..e5b1e1116 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/dismissbutton/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/swipe/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/swipe/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..7b7c57154 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/swipe/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/swipe/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/swipe/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..353480866 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/exit/swipe/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..c833fb4a1 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..633acee27 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/expand/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/expand/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..c833fb4a1 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/expand/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/expand/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/expand/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..633acee27 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/expand/expand/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/expand/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/expand/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..351dd17ed --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/expand/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/expand/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/expand/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..55a179c18 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/expand/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/shrink/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/shrink/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..9d3bb506d --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/shrink/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/shrink/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/shrink/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..def491fa9 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/pip/resize/shrink/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/rotation/displays/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/displays/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..e89887808 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/displays/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/rotation/displays/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/displays/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..ead45aba1 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/displays/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/rotation/regular/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/regular/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..36fc6633f --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/regular/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/rotation/regular/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/regular/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..b5537ac8c --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/regular/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/rotation/seamless/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/seamless/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..388f4e831 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/seamless/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/rotation/seamless/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/seamless/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..e7c8c4cdb --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/seamless/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/rotation/verticaltohorizontal/SurfaceFlingerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/verticaltohorizontal/SurfaceFlingerTrace.winscope Binary files differnew file mode 100644 index 000000000..bcca47ef6 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/verticaltohorizontal/SurfaceFlingerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/tagprocessors/rotation/verticaltohorizontal/WindowManagerTrace.winscope b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/verticaltohorizontal/WindowManagerTrace.winscope Binary files differnew file mode 100644 index 000000000..6c276fbe9 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/tagprocessors/rotation/verticaltohorizontal/WindowManagerTrace.winscope diff --git a/libraries/flicker/test/assets/testdata/wm_trace_launcher_visible_background.pb b/libraries/flicker/test/assets/testdata/wm_trace_launcher_visible_background.pb Binary files differnew file mode 100755 index 000000000..82a84c41c --- /dev/null +++ b/libraries/flicker/test/assets/testdata/wm_trace_launcher_visible_background.pb diff --git a/libraries/flicker/test/assets/testdata/wm_trace_open_from_overview.pb b/libraries/flicker/test/assets/testdata/wm_trace_open_from_overview.pb Binary files differnew file mode 100755 index 000000000..30eb3e60e --- /dev/null +++ b/libraries/flicker/test/assets/testdata/wm_trace_open_from_overview.pb diff --git a/libraries/flicker/test/assets/testdata/wm_trace_split_screen.pb b/libraries/flicker/test/assets/testdata/wm_trace_split_screen.pb Binary files differnew file mode 100644 index 000000000..893bda988 --- /dev/null +++ b/libraries/flicker/test/assets/testdata/wm_trace_split_screen.pb diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.kt index f82d6dfda..b398ae5cb 100644 --- a/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.kt +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/AssertionsCheckerTest.kt @@ -21,6 +21,7 @@ import com.android.server.wm.flicker.assertions.FlickerSubject import com.android.server.wm.flicker.traces.FlickerFailureStrategy import com.android.server.wm.flicker.traces.FlickerSubjectException import com.android.server.wm.traces.common.ITraceEntry +import com.google.common.truth.Fact import com.google.common.truth.FailureMetadata import com.google.common.truth.StandardSubjectBuilder import com.google.common.truth.Subject @@ -50,6 +51,47 @@ class AssertionsCheckerTest { } @Test + fun canCheckChangingAssertions_IgnoreOptionalStart() { + val checker = AssertionsChecker<SimpleEntrySubject>() + checker.add("isData1", isOptional = true) { it.isData1() } + checker.add("isData42") { it.isData42() } + checker.add("isData0") { it.isData0() } + checker.test(getTestEntries(42, 0, 0, 0, 0)) + } + + @Test + fun canCheckChangingAssertions_IgnoreOptionalEnd() { + val checker = AssertionsChecker<SimpleEntrySubject>() + checker.add("isData42") { it.isData42() } + checker.add("isData0") { it.isData0() } + checker.add("isData1", isOptional = true) { it.isData1() } + checker.test(getTestEntries(42, 0, 0, 0, 0)) + } + + @Test + fun canCheckChangingAssertions_IgnoreOptionalMiddle() { + val checker = AssertionsChecker<SimpleEntrySubject>() + checker.add("isData42") { it.isData42() } + checker.add("isData1", isOptional = true) { it.isData1() } + checker.add("isData0") { it.isData0() } + checker.test(getTestEntries(42, 0, 0, 0, 0)) + } + + @Test + fun canCheckChangingAssertions_IgnoreOptionalMultiple() { + val checker = AssertionsChecker<SimpleEntrySubject>() + checker.add("isData1", isOptional = true) { it.isData1() } + checker.add("isData1", isOptional = true) { it.isData1() } + checker.add("isData42") { it.isData42() } + checker.add("isData1", isOptional = true) { it.isData1() } + checker.add("isData1", isOptional = true) { it.isData1() } + checker.add("isData0") { it.isData0() } + checker.add("isData1", isOptional = true) { it.isData1() } + checker.add("isData1", isOptional = true) { it.isData1() } + checker.test(getTestEntries(42, 0, 0, 0, 0)) + } + + @Test fun canCheckChangingAssertions_withNoAssertions() { val checker = AssertionsChecker<SimpleEntrySubject>() checker.test(getTestEntries(42, 0, 0, 0, 0)) @@ -98,7 +140,7 @@ class AssertionsCheckerTest { require(failure is FlickerSubjectException) { "Unknown failure $failure" } assertFailure(failure.cause) .hasMessageThat() - .contains("Assertion never became false: isData42") + .contains("Assertion never failed: isData42") } } @@ -121,7 +163,9 @@ class AssertionsCheckerTest { failureMetadata: FailureMetadata, private val entry: SimpleEntry ) : FlickerSubject(failureMetadata, entry) { - override val defaultFacts: String = "SimpleEntry(${entry.mData})" + override val timestamp: Long get() = 0 + override val parent: FlickerSubject? get() = null + override val selfFacts = listOf(Fact.fact("SimpleEntry", entry.mData.toString())) override fun clone(): FlickerSubject { return SimpleEntrySubject(fm, entry) } @@ -134,6 +178,10 @@ class AssertionsCheckerTest { check("is0").that(entry.mData).isEqualTo(0) } + fun isData1() = apply { + check("is1").that(entry.mData).isEqualTo(1) + } + companion object { /** * Boiler-plate Subject.Factory for LayersTraceSubject diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/CommonConstants.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/CommonConstants.kt new file mode 100644 index 000000000..07bd72ffe --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/CommonConstants.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:JvmName("CommonConstants") +package com.android.server.wm.flicker + +import com.android.server.wm.traces.common.FlickerComponentName + +val CHROME_COMPONENT = FlickerComponentName("com.android.chrome", + "org.chromium.chrome.browser.firstrun.FirstRunActivity") +val CHROME_SPLASH_SCREEN_COMPONENT = FlickerComponentName("", "Splash Screen com.android.chrome") +val DOCKER_STACK_DIVIDER_COMPONENT = FlickerComponentName("", "DockedStackDivider") +val IMAGINARY_COMPONENT = FlickerComponentName("", "ImaginaryWindow") +val IME_ACTIVITY_COMPONENT = FlickerComponentName("com.android.server.wm.flicker.testapp", + "com.android.server.wm.flicker.testapp.ImeActivity") +val LAUNCHER_COMPONENT = FlickerComponentName("com.google.android.apps.nexuslauncher", + "com.google.android.apps.nexuslauncher.NexusLauncherActivity") +val PIP_DISMISS_COMPONENT = FlickerComponentName("", "pip-dismiss-overlay") + +val SIMPLE_APP_COMPONENT = FlickerComponentName("com.android.server.wm.flicker.testapp", + "com.android.server.wm.flicker.testapp.SimpleActivity") +private const val SHELL_PKG_NAME = "com.android.wm.shell.flicker.testapp" +val SHELL_SPLIT_SCREEN_PRIMARY_COMPONENT = FlickerComponentName(SHELL_PKG_NAME, + "$SHELL_PKG_NAME.SplitScreenActivity") +val SHELL_SPLIT_SCREEN_SECONDARY_COMPONENT = FlickerComponentName(SHELL_PKG_NAME, + "$SHELL_PKG_NAME.SplitScreenSecondaryActivity") + +val SCREEN_DECOR_COMPONENT = FlickerComponentName("", "ScreenDecorOverlay") +val WALLPAPER_COMPONENT = FlickerComponentName( + "", "com.breel.wallpapers18.soundviz.wallpaper.variations.SoundVizWallpaperV2") diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/EventLogSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/EventLogSubjectTest.kt index a894632c6..e8891f401 100644 --- a/libraries/flicker/test/src/com/android/server/wm/flicker/EventLogSubjectTest.kt +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/EventLogSubjectTest.kt @@ -34,14 +34,14 @@ class EventLogSubjectTest { FocusEvent(0, "WinB", FocusEvent.Focus.LOST, "test"), FocusEvent(0, "test WinC", FocusEvent.Focus.GAINED, "test")) val result = builder.buildEventLogResult().eventLogSubject - require(result != null) { "Event log subject was not built" } - result.focusChanges(arrayOf("WinA", "WinB", "WinC")) + requireNotNull(result) { "Event log subject was not built" } + result.focusChanges("WinA", "WinB", "WinC") .forAllEntries() - result.focusChanges(arrayOf("WinA", "WinB")).forAllEntries() - result.focusChanges(arrayOf("WinB", "WinC")).forAllEntries() - result.focusChanges(arrayOf("WinA")).forAllEntries() - result.focusChanges(arrayOf("WinB")).forAllEntries() - result.focusChanges(arrayOf("WinC")).forAllEntries() + result.focusChanges("WinA", "WinB").forAllEntries() + result.focusChanges("WinB", "WinC").forAllEntries() + result.focusChanges("WinA").forAllEntries() + result.focusChanges("WinB").forAllEntries() + result.focusChanges("WinC").forAllEntries() } @Test diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerDSLTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerDSLTest.kt index dd8d15e79..ba4bb6239 100644 --- a/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerDSLTest.kt +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerDSLTest.kt @@ -228,4 +228,31 @@ class FlickerDSLTest { .contains(exceptionMessage) } } + + private val failedAssertion = AssertionData(tag = AssertionTag.END, + expectedSubjectClass = LayerTraceEntrySubject::class) { + this.fail("Expected exception") + } + + @Test + fun exceptionContainsDebugInfo() { + val builder = FlickerBuilder(instrumentation) + builder.transitions { device.pressHome() } + val flicker = builder.build() + flicker.execute() + + val error = assertThrows(AssertionError::class.java) { + flicker.checkAssertion(failedAssertion) + } + // Exception message + Truth.assertThat(error).hasMessageThat().contains("Expected exception") + // Subject facts + Truth.assertThat(error).hasMessageThat().contains("Trace start") + Truth.assertThat(error).hasMessageThat().contains("Trace start") + Truth.assertThat(error).hasMessageThat().contains("Trace files") + Truth.assertThat(error).hasMessageThat().contains("Entry") + Truth.assertThat(error).hasMessageThat().contains("Location") + // Correct stack trace point + Truth.assertThat(error).hasMessageThat().contains("failedAssertion") + } } diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceTest.kt deleted file mode 100644 index 3016a0a09..000000000 --- a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceTest.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm.flicker - -import com.android.server.wm.traces.common.layers.LayersTrace -import com.google.common.truth.Truth -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runners.MethodSorters - -/** - * Contains [LayersTrace] tests. To run this test: `atest - * FlickerLibTest:LayersTraceTest` - */ -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class LayersTraceTest { - private fun detectRootLayer(fileName: String) { - val layersTrace = readLayerTraceFromFile(fileName) - for (entry in layersTrace.entries) { - val rootLayers = entry.rootLayers - Truth.assertWithMessage("Does not have any root layer") - .that(rootLayers.size) - .isGreaterThan(0) - val firstParentId = rootLayers.first().parentId - Truth.assertWithMessage("Has multiple root layers") - .that(rootLayers.all { it.parentId == firstParentId }) - .isTrue() - } - } - - @Test - fun testCanDetectRootLayer() { - detectRootLayer("layers_trace_root.pb") - } - - @Test - fun testCanDetectRootLayerAOSP() { - detectRootLayer("layers_trace_root_aosp.pb") - } - - @Test - fun testCanParseTraceWithoutHWC() { - val layersTrace = readLayerTraceFromFile("layers_trace_no_hwc_composition.pb") - layersTrace.forEach { entry -> - Truth.assertWithMessage("Should have visible layers in all trace entries") - .that(entry.visibleLayers).asList() - .isNotEmpty() - } - } -}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/UiDeviceExtensionsTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/UiDeviceExtensionsTest.kt index 88141137e..9161a5dd9 100644 --- a/libraries/flicker/test/src/com/android/server/wm/flicker/UiDeviceExtensionsTest.kt +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/UiDeviceExtensionsTest.kt @@ -17,7 +17,9 @@ package com.android.server.wm.flicker import androidx.test.platform.app.InstrumentationRegistry -import com.android.server.wm.traces.parser.DeviceStateDump +import com.android.server.wm.traces.common.DeviceStateDump +import com.android.server.wm.traces.common.layers.LayerTraceEntry +import com.android.server.wm.traces.common.windowmanager.WindowManagerState import com.android.server.wm.traces.parser.FLAG_STATE_DUMP_FLAG_LAYERS import com.android.server.wm.traces.parser.FLAG_STATE_DUMP_FLAG_WM import com.android.server.wm.traces.parser.WmStateDumpFlags @@ -29,7 +31,7 @@ import org.junit.Test import org.junit.runners.MethodSorters /** - * Contains [UiDeviceExtensions] tests. + * Contains [com.android.server.wm.traces.parser.Extensions] tests. * * To run this test: `atest FlickerLibTest:UiDeviceExtensionsTest` */ @@ -44,7 +46,7 @@ class UiDeviceExtensionsTest { private fun getCurrStateDump( @WmStateDumpFlags dumpFlags: Int = FLAG_STATE_DUMP_FLAG_WM.or(FLAG_STATE_DUMP_FLAG_LAYERS) - ): DeviceStateDump { + ): DeviceStateDump<WindowManagerState?, LayerTraceEntry?> { val instrumentation = InstrumentationRegistry.getInstrumentation() return getCurrentStateDump(instrumentation.uiAutomation, dumpFlags) } @@ -62,8 +64,8 @@ class UiDeviceExtensionsTest { Truth.assertThat(currStateDump.first).isNotEmpty() Truth.assertThat(currStateDump.second).isEmpty() val currState = this.getCurrStateDump(FLAG_STATE_DUMP_FLAG_WM) - Truth.assertThat(currState.wmTrace).isNotNull() - Truth.assertThat(currState.layersTrace).isNull() + Truth.assertThat(currState.wmState).isNotNull() + Truth.assertThat(currState.layerState).isNull() } @Test @@ -72,16 +74,22 @@ class UiDeviceExtensionsTest { Truth.assertThat(currStateDump.first).isEmpty() Truth.assertThat(currStateDump.second).isNotEmpty() val currState = this.getCurrStateDump(FLAG_STATE_DUMP_FLAG_LAYERS) - Truth.assertThat(currState.wmTrace).isNull() - Truth.assertThat(currState.layersTrace).isNotNull() + Truth.assertThat(currState.wmState).isNull() + Truth.assertThat(currState.layerState).isNotNull() } @Test fun canParseCurrentDeviceState() { val currState = this.getCurrStateDump() - Truth.assertThat(currState.wmTrace?.entries).hasSize(1) - Truth.assertThat(currState.wmTrace?.entries?.first()?.windowStates).isNotEmpty() - Truth.assertThat(currState.layersTrace?.entries).hasSize(1) - Truth.assertThat(currState.layersTrace?.entries?.first()?.flattenedLayers).isNotEmpty() + Truth.assertThat(currState.wmState?.asTrace()?.entries ?: emptyArray()) + .asList() + .hasSize(1) + Truth.assertThat(currState.wmState?.asTrace()?.entries?.first()?.windowStates) + .isNotEmpty() + Truth.assertThat(currState.layerState?.asTrace()?.entries ?: emptyArray()) + .asList() + .hasSize(1) + Truth.assertThat(currState.layerState?.asTrace()?.entries?.first()?.flattenedLayers) + .isNotEmpty() } }
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/Utils.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/Utils.kt index bc376b205..4d2869fb5 100644 --- a/libraries/flicker/test/src/com/android/server/wm/flicker/Utils.kt +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/Utils.kt @@ -20,8 +20,10 @@ import android.content.Context import androidx.test.platform.app.InstrumentationRegistry import com.android.server.wm.flicker.traces.FlickerSubjectException import com.android.server.wm.traces.common.layers.LayersTrace +import com.android.server.wm.traces.common.tags.TagTrace import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace import com.android.server.wm.traces.parser.layers.LayersTraceParser +import com.android.server.wm.traces.parser.tags.TagTraceParserUtil import com.android.server.wm.traces.parser.windowmanager.WindowManagerTraceParser import com.google.common.io.ByteStreams import com.google.common.truth.ExpectFailure @@ -57,6 +59,15 @@ internal fun readLayerTraceFromFile( } } +internal fun readTagTraceFromFile(relativePath: String): TagTrace { + return try { + TagTraceParserUtil.parseFromTrace(readTestFile(relativePath), + source = Paths.get(relativePath)) + } catch (e: Exception) { + throw RuntimeException(e) + } +} + @Throws(Exception::class) internal fun readTestFile(relativePath: String): ByteArray { val context: Context = InstrumentationRegistry.getInstrumentation().context @@ -95,4 +106,4 @@ fun assertFailure(failure: Throwable?): TruthFailureSubject { } require(target is AssertionError) { "Unknown failure $target" } return ExpectFailure.assertThat(target) -}
\ No newline at end of file +} diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerStateSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerStateSubjectTest.kt deleted file mode 100644 index 959232a3c..000000000 --- a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerStateSubjectTest.kt +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm.flicker - -import android.graphics.Region -import com.android.server.wm.flicker.traces.FlickerSubjectException -import com.android.server.wm.flicker.traces.windowmanager.WindowManagerStateSubject -import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject.Companion.assertThat -import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runners.MethodSorters -import java.lang.AssertionError - -/** - * Contains [WindowManagerStateSubject] tests. To run this test: `atest - * FlickerLibTest:WindowManagerStateSubjectTest` - */ -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class WindowManagerStateSubjectTest { - private val trace: WindowManagerTrace by lazy { readWmTraceFromFile("wm_trace_openchrome.pb") } - - @Test - fun canDetectAboveAppWindowVisibility_isVisible() { - assertThat(trace) - .entry(9213763541297) - .isAboveAppWindow("NavigationBar") - .isAboveAppWindow("ScreenDecorOverlay") - .isAboveAppWindow("StatusBar") - } - - @Test - fun canDetectAboveAppWindowVisibility_isInvisible() { - val subject = assertThat(trace).entry(9213763541297) - var failure = assertThrows(AssertionError::class.java) { - subject.isAboveAppWindow("pip-dismiss-overlay") - } - assertFailure(failure).factValue("Is Invisible").contains("pip-dismiss-overlay") - - failure = assertThrows(AssertionError::class.java) { - subject.isAboveAppWindow("NavigationBar", isVisible = false) - } - assertFailure(failure).factValue("Is Visible").contains("NavigationBar") - } - - @Test - fun canDetectWindowCoversAtLeastRegion_exactSize() { - val entry = assertThat(trace) - .entry(9213763541297) - - entry.frameRegion("StatusBar").coversAtLeast(Region(0, 0, 1440, 171)) - entry.frameRegion("com.google.android.apps.nexuslauncher") - .coversAtLeast(Region(0, 0, 1440, 2960)) - } - - @Test - fun canDetectWindowCoversAtLeastRegion_smallerRegion() { - val entry = assertThat(trace) - .entry(9213763541297) - entry.frameRegion("StatusBar").coversAtLeast(Region(0, 0, 100, 100)) - entry.frameRegion("com.google.android.apps.nexuslauncher") - .coversAtLeast(Region(0, 0, 100, 100)) - } - - @Test - fun canDetectWindowCoversAtLeastRegion_largerRegion() { - val subject = assertThat(trace).entry(9213763541297) - var failure = assertThrows(FlickerSubjectException::class.java) { - subject.frameRegion("StatusBar").coversAtLeast(Region(0, 0, 1441, 171)) - } - assertFailure(failure).factValue("Uncovered region").contains("SkRegion((1440,0,1441,171))") - - failure = assertThrows(FlickerSubjectException::class.java) { - subject.frameRegion("com.google.android.apps.nexuslauncher") - .coversAtLeast(Region(0, 0, 1440, 2961)) - } - assertFailure(failure).factValue("Uncovered region") - .contains("SkRegion((0,2960,1440,2961))") - } - - @Test - fun canDetectWindowCoversAtMostRegion_extactSize() { - val entry = assertThat(trace) - .entry(9213763541297) - entry.frameRegion("StatusBar").coversAtMost(Region(0, 0, 1440, 171)) - entry.frameRegion("com.google.android.apps.nexuslauncher") - .coversAtMost(Region(0, 0, 1440, 2960)) - } - - @Test - fun canDetectWindowCoversAtMostRegion_smallerRegion() { - val subject = assertThat(trace).entry(9213763541297) - var failure = assertThrows(FlickerSubjectException::class.java) { - subject.frameRegion("StatusBar").coversAtMost(Region(0, 0, 100, 100)) - } - assertFailure(failure).factValue("Out-of-bounds region") - .contains("SkRegion((100,0,1440,100)(0,100,1440,171))") - - failure = assertThrows(FlickerSubjectException::class.java) { - subject.frameRegion("com.google.android.apps.nexuslauncher") - .coversAtMost(Region(0, 0, 100, 100)) - } - assertFailure(failure).factValue("Out-of-bounds region") - .contains("SkRegion((100,0,1440,100)(0,100,1440,2960))") - } - - @Test - fun canDetectWindowCoversAtMostRegion_largerRegion() { - val entry = assertThat(trace) - .entry(9213763541297) - - entry.frameRegion("StatusBar").coversAtMost(Region(0, 0, 1441, 171)) - entry.frameRegion("com.google.android.apps.nexuslauncher") - .coversAtMost(Region(0, 0, 1440, 2961)) - } - - @Test - fun canDetectBelowAppWindowVisibility() { - assertThat(trace) - .entry(9213763541297) - .containsNonAppWindow("wallpaper") - } - - @Test - fun canDetectAppWindowVisibility() { - assertThat(trace) - .entry(9213763541297) - .containsAppWindow("com.google.android.apps.nexuslauncher") - - assertThat(trace) - .entry(9215551505798) - .containsAppWindow("com.android.chrome") - } - - @Test - fun canFailWithReasonForVisibilityChecks_windowNotFound() { - val failure = assertThrows(FlickerSubjectException::class.java) { - assertThat(trace) - .entry(9213763541297) - .containsNonAppWindow("ImaginaryWindow") - } - assertFailure(failure).factValue("Could not find") - .contains("ImaginaryWindow") - } - - @Test - fun canFailWithReasonForVisibilityChecks_windowNotVisible() { - val failure = assertThrows(FlickerSubjectException::class.java) { - assertThat(trace) - .entry(9213763541297) - .containsNonAppWindow("InputMethod") - } - assertFailure(failure).factValue("Is Invisible") - .contains("InputMethod") - } - - @Test - fun canDetectAppZOrder() { - assertThat(trace) - .entry(9215551505798) - .containsAppWindow("com.google.android.apps.nexuslauncher", isVisible = true) - .showsAppWindowOnTop("com.android.chrome") - } - - @Test - fun canFailWithReasonForZOrderChecks_windowNotOnTop() { - val failure = assertThrows(FlickerSubjectException::class.java) { - assertThat(trace) - .entry(9215551505798) - .showsAppWindowOnTop("com.google.android.apps.nexuslauncher") - } - assertFailure(failure) - .factValue("Found") - .contains("Splash Screen com.android.chrome") - } -}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerTraceSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerTraceSubjectTest.kt deleted file mode 100644 index d44c8bacb..000000000 --- a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerTraceSubjectTest.kt +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm.flicker - -import com.android.server.wm.flicker.traces.FlickerSubjectException -import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject -import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject.Companion.assertThat -import org.junit.FixMethodOrder -import org.junit.Test -import org.junit.runners.MethodSorters - -/** - * Contains [WindowManagerTraceSubject] tests. To run this test: `atest - * FlickerLibTest:WindowManagerTraceSubjectTest` - */ -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -class WindowManagerTraceSubjectTest { - private val chromeTrace by lazy { readWmTraceFromFile("wm_trace_openchrome.pb") } - private val imeTrace by lazy { readWmTraceFromFile("wm_trace_ime.pb") } - - @Test - fun testVisibleAppWindowForRange() { - assertThat(chromeTrace) - .showsAppWindowOnTop("NexusLauncherActivity") - .showsAboveAppWindow("ScreenDecorOverlay") - .forRange(9213763541297L, 9215536878453L) - assertThat(chromeTrace) - .showsAppWindowOnTop("com.android.chrome") - .showsAppWindow("NexusLauncherActivity") - .showsAboveAppWindow("ScreenDecorOverlay") - .then() - .showsAppWindowOnTop("com.android.chrome") - .hidesAppWindow("NexusLauncherActivity") - .showsAboveAppWindow("ScreenDecorOverlay") - .forRange(9215551505798L, 9216093628925L) - } - - @Test - fun testCanTransitionInAppWindow() { - assertThat(chromeTrace) - .showsAppWindowOnTop("NexusLauncherActivity") - .showsAboveAppWindow("ScreenDecorOverlay") - .then() - .showsAppWindowOnTop("com.android.chrome") - .showsAboveAppWindow("ScreenDecorOverlay") - .forAllEntries() - } - - @Test - fun testCanInspectBeginning() { - assertThat(chromeTrace) - .first() - .showsAppWindowOnTop("NexusLauncherActivity") - .isAboveAppWindow("ScreenDecorOverlay") - } - - @Test - fun testCanInspectAppWindowOnTop() { - assertThat(chromeTrace) - .first() - .showsAppWindowOnTop("NexusLauncherActivity", "InvalidWindow") - - val failure = assertThrows(FlickerSubjectException::class.java) { - assertThat(chromeTrace) - .first() - .showsAppWindowOnTop("AnotherInvalidWindow", "InvalidWindow") - .fail("Could not detect the top app window") - } - assertFailure(failure).factValue("Could not find").contains("InvalidWindow") - } - - @Test - fun testCanInspectEnd() { - assertThat(chromeTrace) - .last() - .showsAppWindowOnTop("com.android.chrome") - .isAboveAppWindow("ScreenDecorOverlay") - } - - @Test - fun testCanTransitionNonAppWindow() { - assertThat(imeTrace) - .skipUntilFirstAssertion() - .hidesNonAppWindow("InputMethod") - .then() - .showsNonAppWindow("InputMethod") - .forAllEntries() - } - - @Test(expected = AssertionError::class) - fun testCanDetectOverlappingWindows() { - assertThat(imeTrace) - .noWindowsOverlap("InputMethod", "NavigationBar", "ImeActivity") - .forAllEntries() - } - - @Test - fun testCanTransitionAboveAppWindow() { - assertThat(imeTrace) - .skipUntilFirstAssertion() - .hidesAboveAppWindow("InputMethod") - .then() - .showsAboveAppWindow("InputMethod") - .forAllEntries() - } - - @Test - fun testCanTransitionBelowAppWindow() { - val trace = readWmTraceFromFile("wm_trace_open_app_cold.pb") - assertThat(trace) - .skipUntilFirstAssertion() - .showsBelowAppWindow("Wallpaper") - .then() - .hidesBelowAppWindow("Wallpaper") - .forAllEntries() - } - - @Test - fun testCanDetectVisibleWindowsMoreThanOneConsecutiveEntry() { - val trace = readWmTraceFromFile("wm_trace_valid_visible_windows.pb") - assertThat(trace).visibleWindowsShownMoreThanOneConsecutiveEntry().forAllEntries() - } -} diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/LayerSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayerSubjectTest.kt index cb99b650f..5fff30272 100644 --- a/libraries/flicker/test/src/com/android/server/wm/flicker/LayerSubjectTest.kt +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayerSubjectTest.kt @@ -14,10 +14,13 @@ * limitations under the License. */ -package com.android.server.wm.flicker +package com.android.server.wm.flicker.layers +import com.android.server.wm.flicker.assertThrows +import com.android.server.wm.flicker.readLayerTraceFromFile +import com.android.server.wm.flicker.traces.layers.LayerSubject import com.android.server.wm.flicker.traces.layers.LayersTraceSubject.Companion.assertThat -import com.android.server.wm.traces.common.Bounds +import com.android.server.wm.traces.common.Size import com.google.common.truth.Truth import org.junit.FixMethodOrder import org.junit.Test @@ -30,7 +33,7 @@ import org.junit.runners.MethodSorters @FixMethodOrder(MethodSorters.NAME_ASCENDING) class LayerSubjectTest { @Test - fun exceptionContainsDebugInfo() { + fun exceptionContainsDebugInfoImaginary() { val layersTraceEntries = readLayerTraceFromFile("layers_trace_emptyregion.pb") val error = assertThrows(AssertionError::class.java) { assertThat(layersTraceEntries) @@ -38,11 +41,33 @@ class LayerSubjectTest { .layer("ImaginaryLayer", 0) .exists() } - Truth.assertThat(error).hasMessageThat().contains("Trace:") - Truth.assertThat(error).hasMessageThat().contains("Path: ") - Truth.assertThat(error).hasMessageThat().contains("Entry:") - Truth.assertThat(error).hasMessageThat().contains("Frame:") - Truth.assertThat(error).hasMessageThat().contains("Layer:") + Truth.assertThat(error).hasMessageThat().contains("ImaginaryLayer") + Truth.assertThat(error).hasMessageThat().contains("What?") + Truth.assertThat(error).hasMessageThat().contains("Where?") + Truth.assertThat(error).hasMessageThat().contains("Entry") + Truth.assertThat(error).hasMessageThat().contains("Trace start") + Truth.assertThat(error).hasMessageThat().contains("Trace start") + Truth.assertThat(error).hasMessageThat().contains("Trace file") + Truth.assertThat(error).hasMessageThat().contains("Layer name") + } + + @Test + fun exceptionContainsDebugInfoConcrete() { + val layersTraceEntries = readLayerTraceFromFile("layers_trace_emptyregion.pb") + val error = assertThrows(AssertionError::class.java) { + assertThat(layersTraceEntries) + .first() + .subjects + .first() + .doesNotExist() + } + Truth.assertThat(error).hasMessageThat().contains("What?") + Truth.assertThat(error).hasMessageThat().contains("Where?") + Truth.assertThat(error).hasMessageThat().contains("Entry") + Truth.assertThat(error).hasMessageThat().contains("Trace start") + Truth.assertThat(error).hasMessageThat().contains("Trace start") + Truth.assertThat(error).hasMessageThat().contains("Trace file") + Truth.assertThat(error).hasMessageThat().contains("Entry") } @Test @@ -50,7 +75,7 @@ class LayerSubjectTest { val layersTraceEntries = readLayerTraceFromFile("layers_trace_emptyregion.pb") assertThat(layersTraceEntries) .layer("SoundVizWallpaperV2", 26033) - .hasBufferSize(Bounds(1440, 2960)) + .hasBufferSize(Size(1440, 2960)) .hasScalingMode(0) assertThat(layersTraceEntries) diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/LayerTraceEntrySubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayerTraceEntrySubjectTest.kt index 76375b808..b8fe45a96 100644 --- a/libraries/flicker/test/src/com/android/server/wm/flicker/LayerTraceEntrySubjectTest.kt +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayerTraceEntrySubjectTest.kt @@ -14,11 +14,20 @@ * limitations under the License. */ -package com.android.server.wm.flicker +package com.android.server.wm.flicker.layers import android.graphics.Region +import com.android.server.wm.flicker.DOCKER_STACK_DIVIDER_COMPONENT +import com.android.server.wm.flicker.IMAGINARY_COMPONENT +import com.android.server.wm.flicker.LAUNCHER_COMPONENT +import com.android.server.wm.flicker.SIMPLE_APP_COMPONENT +import com.android.server.wm.flicker.assertFailure +import com.android.server.wm.flicker.assertThrows +import com.android.server.wm.flicker.assertions.FlickerSubject +import com.android.server.wm.flicker.readLayerTraceFromFile import com.android.server.wm.flicker.traces.layers.LayerTraceEntrySubject import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.traces.common.FlickerComponentName import com.google.common.truth.Truth import org.junit.FixMethodOrder import org.junit.Test @@ -36,28 +45,31 @@ class LayerTraceEntrySubjectTest { val error = assertThrows(AssertionError::class.java) { LayersTraceSubject.assertThat(layersTraceEntries) .first() - .contains("ImaginaryLayer") + .visibleRegion(IMAGINARY_COMPONENT) } - Truth.assertThat(error).hasMessageThat().contains("Trace:") - Truth.assertThat(error).hasMessageThat().contains("Path: ") - Truth.assertThat(error).hasMessageThat().contains("Entry:") + Truth.assertThat(error).hasMessageThat().contains(IMAGINARY_COMPONENT.className) + Truth.assertThat(error).hasMessageThat().contains("Trace start") + Truth.assertThat(error).hasMessageThat().contains("Trace start") + Truth.assertThat(error).hasMessageThat().contains("Trace file") + Truth.assertThat(error).hasMessageThat().contains("Entry") + Truth.assertThat(error).hasMessageThat().contains(FlickerSubject.ASSERTION_TAG) } @Test fun testCanInspectBeginning() { val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb") LayerTraceEntrySubject.assertThat(layersTraceEntries.entries.first()) - .isVisible("NavigationBar0#0") - .notContains("DockedStackDivider#0") - .isVisible("NexusLauncherActivity#0") + .isVisible(FlickerComponentName.NAV_BAR) + .notContains(DOCKER_STACK_DIVIDER_COMPONENT) + .isVisible(LAUNCHER_COMPONENT) } @Test fun testCanInspectEnd() { val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb") LayerTraceEntrySubject.assertThat(layersTraceEntries.entries.last()) - .isVisible("NavigationBar0#0") - .isVisible("DockedStackDivider#0") + .isVisible(FlickerComponentName.NAV_BAR) + .isVisible(DOCKER_STACK_DIVIDER_COMPONENT) } // b/75276931 @@ -82,17 +94,16 @@ class LayerTraceEntrySubjectTest { // Visible region tests @Test fun canTestLayerVisibleRegion_layerDoesNotExist() { - val imaginaryLayer = "ImaginaryLayer" val trace = readLayerTraceFromFile("layers_trace_emptyregion.pb") val expectedVisibleRegion = Region(0, 0, 1, 1) val error = assertThrows(AssertionError::class.java) { LayersTraceSubject.assertThat(trace).entry(937229257165) - .visibleRegion(imaginaryLayer) + .visibleRegion(IMAGINARY_COMPONENT) .coversExactly(expectedVisibleRegion) } assertFailure(error) .factValue("Could not find") - .isEqualTo(imaginaryLayer) + .contains(IMAGINARY_COMPONENT.toWindowName()) } @Test @@ -101,7 +112,7 @@ class LayerTraceEntrySubjectTest { val expectedVisibleRegion = Region(0, 0, 1, 1) val error = assertThrows(AssertionError::class.java) { LayersTraceSubject.assertThat(trace).entry(937126074082) - .visibleRegion("DockedStackDivider#0") + .visibleRegion(DOCKER_STACK_DIVIDER_COMPONENT) .coversExactly(expectedVisibleRegion) } assertFailure(error) @@ -115,7 +126,7 @@ class LayerTraceEntrySubjectTest { val expectedVisibleRegion = Region(0, 0, 1, 1) val error = assertThrows(AssertionError::class.java) { LayersTraceSubject.assertThat(trace).entry(935346112030) - .visibleRegion("SimpleActivity#0") + .visibleRegion(SIMPLE_APP_COMPONENT) .coversExactly(expectedVisibleRegion) } assertFailure(error) @@ -129,7 +140,7 @@ class LayerTraceEntrySubjectTest { val expectedVisibleRegion = Region(0, 0, 1440, 99) val error = assertThrows(AssertionError::class.java) { LayersTraceSubject.assertThat(trace).entry(937126074082) - .visibleRegion("StatusBar") + .visibleRegion(FlickerComponentName.STATUS_BAR) .coversExactly(expectedVisibleRegion) } assertFailure(error) @@ -142,7 +153,7 @@ class LayerTraceEntrySubjectTest { val trace = readLayerTraceFromFile("layers_trace_launch_split_screen.pb") val expectedVisibleRegion = Region(0, 0, 1080, 145) LayersTraceSubject.assertThat(trace).entry(90480846872160) - .visibleRegion("StatusBar") + .visibleRegion(FlickerComponentName.STATUS_BAR) .coversExactly(expectedVisibleRegion) } @@ -151,7 +162,7 @@ class LayerTraceEntrySubjectTest { val trace = readLayerTraceFromFile("layers_trace_invalid_layer_visibility.pb") val error = assertThrows(AssertionError::class.java) { LayersTraceSubject.assertThat(trace).entry(252794268378458) - .isVisible("com.android.server.wm.flicker.testapp") + .isVisible(SIMPLE_APP_COMPONENT) } assertFailure(error) .factValue("Is Invisible") @@ -167,7 +178,8 @@ class LayerTraceEntrySubjectTest { entry.visibleRegion(useCompositionEngineRegionOnly = false) .coversExactly(Region(0, 0, 1440, 2960)) - entry.visibleRegion("InputMethod#0", useCompositionEngineRegionOnly = false) + entry.visibleRegion(FlickerComponentName.IME, + useCompositionEngineRegionOnly = false) .coversExactly(Region(0, 171, 1440, 2960)) } }
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceEntryTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayersTraceEntryTest.kt index 616fc47ee..670bba445 100644 --- a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceEntryTest.kt +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayersTraceEntryTest.kt @@ -14,15 +14,17 @@ * limitations under the License. */ -package com.android.server.wm.flicker +package com.android.server.wm.flicker.layers -import com.android.server.wm.traces.common.layers.LayerTraceEntry +import com.android.server.wm.flicker.IMAGINARY_COMPONENT +import com.android.server.wm.flicker.assertThrows +import com.android.server.wm.flicker.readLayerTraceFromFile import com.android.server.wm.flicker.traces.layers.LayersTraceSubject.Companion.assertThat +import com.android.server.wm.traces.common.layers.LayerTraceEntry import com.google.common.truth.Truth import org.junit.FixMethodOrder import org.junit.Test import org.junit.runners.MethodSorters -import kotlin.AssertionError /** * Contains [LayerTraceEntry] tests. To run this test: `atest @@ -36,11 +38,12 @@ class LayersTraceEntryTest { val error = assertThrows(AssertionError::class.java) { assertThat(layersTraceEntries) .first() - .contains("ImaginaryLayer") + .contains(IMAGINARY_COMPONENT) } - Truth.assertThat(error).hasMessageThat().contains("Trace:") - Truth.assertThat(error).hasMessageThat().contains("Path: ") - Truth.assertThat(error).hasMessageThat().contains("Entry:") + Truth.assertThat(error).hasMessageThat().contains("Trace start") + Truth.assertThat(error).hasMessageThat().contains("Trace end") + Truth.assertThat(error).hasMessageThat().contains("Entry") + Truth.assertThat(error).hasMessageThat().contains("Trace file") } @Test @@ -107,8 +110,8 @@ class LayersTraceEntryTest { Truth.assertThat(trace.entries.first().timestamp).isEqualTo(922839428857) Truth.assertThat(trace.entries.last().timestamp).isEqualTo(941432656959) Truth.assertThat(trace.entries.first().flattenedLayers).asList().hasSize(57) - val layers = trace.entries.first().rootLayers - Truth.assertThat(layers[0].children).hasSize(3) + val layers = trace.entries.first().children + Truth.assertThat(layers[0].children).asList().hasSize(3) Truth.assertThat(layers[1].children).isEmpty() } diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayersTraceSubjectTest.kt index d47820efa..9ef6a0325 100644 --- a/libraries/flicker/test/src/com/android/server/wm/flicker/LayersTraceSubjectTest.kt +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayersTraceSubjectTest.kt @@ -14,13 +14,21 @@ * limitations under the License. */ -package com.android.server.wm.flicker +package com.android.server.wm.flicker.layers -import android.graphics.Region import androidx.test.filters.FlakyTest +import com.android.server.wm.flicker.DOCKER_STACK_DIVIDER_COMPONENT +import com.android.server.wm.flicker.LAUNCHER_COMPONENT +import com.android.server.wm.flicker.SIMPLE_APP_COMPONENT +import com.android.server.wm.flicker.assertFailure +import com.android.server.wm.flicker.assertThrows +import com.android.server.wm.flicker.readLayerTraceFromFile import com.android.server.wm.flicker.traces.layers.LayersTraceSubject import com.android.server.wm.flicker.traces.layers.LayersTraceSubject.Companion.assertThat +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.common.Region import com.android.server.wm.traces.common.layers.LayersTrace +import com.android.server.wm.traces.parser.minus import com.google.common.truth.Truth import org.junit.FixMethodOrder import org.junit.Test @@ -39,10 +47,9 @@ class LayersTraceSubjectTest { assertThat(layersTraceEntries) .isEmpty() } - Truth.assertThat(error).hasMessageThat().contains("Trace:") - Truth.assertThat(error).hasMessageThat().contains("Path: ") - Truth.assertThat(error).hasMessageThat().contains("Start:") - Truth.assertThat(error).hasMessageThat().contains("End:") + Truth.assertThat(error).hasMessageThat().contains("Trace start") + Truth.assertThat(error).hasMessageThat().contains("Trace end") + Truth.assertThat(error).hasMessageThat().contains("Trace file") } @Test @@ -64,9 +71,9 @@ class LayersTraceSubjectTest { val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb") assertThat(layersTraceEntries) .first() - .isVisible("NavigationBar0#0") - .notContains("DockedStackDivider#0") - .isVisible("NexusLauncherActivity#0") + .isVisible(FlickerComponentName.NAV_BAR) + .notContains(DOCKER_STACK_DIVIDER_COMPONENT) + .isVisible(LAUNCHER_COMPONENT) } @Test @@ -74,22 +81,37 @@ class LayersTraceSubjectTest { val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb") assertThat(layersTraceEntries) .last() - .isVisible("NavigationBar0#0") - .isVisible("DockedStackDivider#0") + .isVisible(FlickerComponentName.NAV_BAR) + .isVisible(DOCKER_STACK_DIVIDER_COMPONENT) + } + + @Test + fun testAssertionsOnRange() { + val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb") + + assertThat(layersTraceEntries) + .isVisible(FlickerComponentName.NAV_BAR) + .isInvisible(DOCKER_STACK_DIVIDER_COMPONENT) + .forRange(90480846872160L, 90480994138424L) + + assertThat(layersTraceEntries) + .isVisible(FlickerComponentName.NAV_BAR) + .isVisible(DOCKER_STACK_DIVIDER_COMPONENT) + .forRange(90491795074136L, 90493757372977L) } @Test fun testCanDetectChangingAssertions() { val layersTraceEntries = readLayerTraceFromFile("layers_trace_launch_split_screen.pb") assertThat(layersTraceEntries) - .isVisible("NavigationBar0#0") - .notContains("DockedStackDivider#0") + .isVisible(FlickerComponentName.NAV_BAR) + .notContains(DOCKER_STACK_DIVIDER_COMPONENT) .then() - .isVisible("NavigationBar0#0") - .isInvisible("DockedStackDivider#0") + .isVisible(FlickerComponentName.NAV_BAR) + .isInvisible(DOCKER_STACK_DIVIDER_COMPONENT) .then() - .isVisible("NavigationBar0#0") - .isVisible("DockedStackDivider#0") + .isVisible(FlickerComponentName.NAV_BAR) + .isVisible(DOCKER_STACK_DIVIDER_COMPONENT) .forAllEntries() } @@ -99,9 +121,9 @@ class LayersTraceSubjectTest { val layersTraceEntries = readLayerTraceFromFile("layers_trace_invalid_layer_visibility.pb") val error = assertThrows(AssertionError::class.java) { assertThat(layersTraceEntries) - .isVisible("com.android.server.wm.flicker.testapp") + .isVisible(SIMPLE_APP_COMPONENT) .then() - .isInvisible("com.android.server.wm.flicker.testapp") + .isInvisible(SIMPLE_APP_COMPONENT) .forAllEntries() } @@ -156,7 +178,8 @@ class LayersTraceSubjectTest { val layersTraceEntries = readLayerTraceFromFile( "layers_trace_invalid_visible_layers.pb") assertThat(layersTraceEntries) - .visibleLayersShownMoreThanOneConsecutiveEntry(listOf("StatusBar#0")) + .visibleLayersShownMoreThanOneConsecutiveEntry( + listOf(FlickerComponentName.STATUS_BAR)) .forAllEntries() } @@ -164,15 +187,17 @@ class LayersTraceSubjectTest { fun testCanIgnoreLayerShorterNameInVisibleLayersMoreThanOneConsecutiveEntry() { val layersTraceEntries = readLayerTraceFromFile( "one_visible_layer_launcher_trace.pb") + val launcherComponent = FlickerComponentName("com.google.android.apps.nexuslauncher", + "com.google.android.apps.nexuslauncher.NexusLauncherActivity#1") assertThat(layersTraceEntries) - .visibleLayersShownMoreThanOneConsecutiveEntry(listOf("Launcher")) + .visibleLayersShownMoreThanOneConsecutiveEntry(listOf(launcherComponent)) .forAllEntries() } private fun detectRootLayer(fileName: String) { val layersTrace = readLayerTraceFromFile(fileName) for (entry in layersTrace.entries) { - val rootLayers = entry.rootLayers + val rootLayers = entry.children Truth.assertWithMessage("Does not have any root layer") .that(rootLayers.size) .isGreaterThan(0) @@ -193,7 +218,67 @@ class LayersTraceSubjectTest { detectRootLayer("layers_trace_root_aosp.pb") } + @Test + fun canTestLayerOccludedByAppLayerIsNotVisible() { + val trace = readLayerTraceFromFile("layers_trace_occluded.pb") + val entry = assertThat(trace).entry(1700382131522L) + entry.isVisible(SIMPLE_APP_COMPONENT) + } + + @Test + fun testCanDetectLayerExpanding() { + val layersTraceEntries = readLayerTraceFromFile("layers_trace_openchrome.pb") + val animation = assertThat(layersTraceEntries).layers("animation-leash of app_transition#0") + // Obtain the area of each layer and checks if the next area is + // greater or equal to the previous one + val areas = animation.map { + val region = it.layer?.visibleRegion ?: Region() + val area = region.width * region.height + area + } + val expanding = areas.zipWithNext { currentArea, nextArea -> + nextArea >= currentArea + } + + Truth.assertWithMessage("Animation leash should be expanding") + .that(expanding.all { it }) + .isTrue() + } + + @Test + fun checkVisibleRegionAppMinusPipLayer() { + val layersTraceEntries = readLayerTraceFromFile("layers_trace_pip_wmshell.pb") + val subject = assertThat(layersTraceEntries).last() + + try { + subject.visibleRegion(FIXED_APP).coversExactly(DISPLAY_REGION_ROTATED) + error("Layer is partially covered by a Pip layer and should " + + "not cover the device screen") + } catch (e: AssertionError) { + val pipRegion = subject.visibleRegion(PIP_APP).region + val expectedWithoutPip = DISPLAY_REGION_ROTATED.minus(pipRegion) + subject.visibleRegion(FIXED_APP) + .coversExactly(expectedWithoutPip) + } + } + + @Test + fun checkVisibleRegionAppPlusPipLayer() { + val layersTraceEntries = readLayerTraceFromFile("layers_trace_pip_wmshell.pb") + val subject = assertThat(layersTraceEntries).last() + val pipRegion = subject.visibleRegion(PIP_APP).region + subject.visibleRegion(FIXED_APP) + .plus(pipRegion) + .coversExactly(DISPLAY_REGION_ROTATED) + } + companion object { - private val DISPLAY_REGION = Region(0, 0, 1440, 2880) + private val DISPLAY_REGION = android.graphics.Region(0, 0, 1440, 2880) + private val DISPLAY_REGION_ROTATED = Region(0, 0, 2160, 1080) + private const val SHELL_APP_PACKAGE = "com.android.wm.shell.flicker.testapp" + private val FIXED_APP = FlickerComponentName(SHELL_APP_PACKAGE, + "$SHELL_APP_PACKAGE.FixedActivity") + private val PIP_APP = FlickerComponentName(SHELL_APP_PACKAGE, + "$SHELL_APP_PACKAGE.PipActivity") } } diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayersTraceTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayersTraceTest.kt new file mode 100644 index 000000000..bac0ac45f --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/layers/LayersTraceTest.kt @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.layers + +import com.android.server.wm.flicker.assertThrows +import com.android.server.wm.flicker.readLayerTraceFromFile +import com.android.server.wm.flicker.traces.layers.LayersTraceSubject +import com.android.server.wm.traces.common.layers.LayersTrace +import com.google.common.truth.Truth +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +/** + * Contains [LayersTrace] tests. To run this test: `atest + * FlickerLibTest:LayersTraceTest` + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class LayersTraceTest { + private fun detectRootLayer(fileName: String) { + val layersTrace = readLayerTraceFromFile(fileName) + for (entry in layersTrace.entries) { + val rootLayers = entry.children + Truth.assertWithMessage("Does not have any root layer") + .that(rootLayers.size) + .isGreaterThan(0) + val firstParentId = rootLayers.first().parentId + Truth.assertWithMessage("Has multiple root layers") + .that(rootLayers.all { it.parentId == firstParentId }) + .isTrue() + } + } + + @Test + fun testCanDetectRootLayer() { + detectRootLayer("layers_trace_root.pb") + } + + @Test + fun testCanDetectRootLayerAOSP() { + detectRootLayer("layers_trace_root_aosp.pb") + } + + @Test + fun testCanParseTraceWithoutHWC() { + val layersTrace = readLayerTraceFromFile("layers_trace_no_hwc_composition.pb") + layersTrace.forEach { entry -> + Truth.assertWithMessage("Should have visible layers in all trace entries") + .that(entry.visibleLayers).asList() + .isNotEmpty() + } + } + + @Test + fun canParseFromDumpWithDisplay() { + val trace = readLayerTraceFromFile("layers_dump_with_display.pb") + Truth.assertWithMessage("Dump is not empty") + .that(trace) + .isNotEmpty() + Truth.assertWithMessage("Dump contains display is not empty") + .that(trace.first().displays) + .asList() + .isNotEmpty() + } + + @Test + fun canTestLayerOccludedBy_appLayerHasVisibleRegion() { + val trace = readLayerTraceFromFile("layers_trace_occluded.pb") + val entry = trace.getEntry(1700382131522L) + val layer = entry.getLayerWithBuffer( + "com.android.server.wm.flicker.testapp.SimpleActivity#0") + Truth.assertWithMessage("App should be visible") + .that(layer?.visibleRegion?.isEmpty).isFalse() + Truth.assertWithMessage("App should visible region") + .that(layer?.visibleRegion?.toString()) + .contains("(346, 1583) - (1094, 2839)") + + val splashScreenLayer = entry.getLayerWithBuffer( + "Splash Screen com.android.server.wm.flicker.testapp.SimpleActivity#0") + Truth.assertWithMessage("Splash screen should be visible") + .that(layer?.visibleRegion?.isEmpty).isFalse() + Truth.assertWithMessage("Splash screen visible region") + .that(layer?.visibleRegion?.toString()) + .contains("(346, 1583) - (1094, 2839)") + } + + @Test + fun canTestLayerOccludedBy_appLayerIsOccludedBySplashScreen() { + val layerName = "com.android.server.wm.flicker.testapp.SimpleActivity#0" + val trace = readLayerTraceFromFile("layers_trace_occluded.pb") + val entry = trace.getEntry(1700382131522L) + val layer = entry.getLayerWithBuffer(layerName) + val occludedBy = layer?.occludedBy ?: emptyArray() + val partiallyOccludedBy = layer?.partiallyOccludedBy ?: emptyArray() + Truth.assertWithMessage("Layer $layerName should not be occluded") + .that(occludedBy).isEmpty() + Truth.assertWithMessage("Layer $layerName should be partially occluded") + .that(partiallyOccludedBy).isNotEmpty() + Truth.assertWithMessage("Layer $layerName should be partially occluded") + .that(partiallyOccludedBy.joinToString()) + .contains("Splash Screen com.android.server.wm.flicker.testapp#0 buffer:w:1440, " + + "h:3040, stride:1472, format:1 frame#1 visible:(346, 1583) - (1094, 2839)") + } + + @Test + fun exceptionContainsDebugInfo() { + val layersTraceEntries = readLayerTraceFromFile("layers_trace_emptyregion.pb") + val error = assertThrows(AssertionError::class.java) { + LayersTraceSubject.assertThat(layersTraceEntries) + .isEmpty() + } + Truth.assertThat(error).hasMessageThat().contains("Trace start") + Truth.assertThat(error).hasMessageThat().contains("Trace start") + Truth.assertThat(error).hasMessageThat().contains("Trace file") + } + + @Test + fun canFilter() { + val trace = readLayerTraceFromFile("layers_trace_openchrome.pb") + val splitlayersTrace = trace.filter(71607477186189, 71607812120180) + + Truth.assertThat(splitlayersTrace).isNotEmpty() + + Truth.assertThat(splitlayersTrace.entries.first().timestamp).isEqualTo(71607477186189) + Truth.assertThat(splitlayersTrace.entries.last().timestamp).isEqualTo(71607812120180) + } + + @Test + fun canFilter_wrongTimestamps() { + val trace = readLayerTraceFromFile("layers_trace_openchrome.pb") + val splitLayersTrace = trace.filter(9213763541297, 9215895891561) + + Truth.assertThat(splitLayersTrace).isEmpty() + } +}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.kt index c8ac97dcf..37001beae 100644 --- a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.kt +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.kt @@ -63,7 +63,7 @@ class ScreenRecorderTest { @Test fun videoCanBeSaved() { mScreenRecorder.start() - SystemClock.sleep(100) + SystemClock.sleep(3000) mScreenRecorder.stop() val builder = FlickerRunResult.Builder() mScreenRecorder.save("test", builder) diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/TraceMonitorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/TraceMonitorTest.kt index 39f70a5f2..fa7aee4ca 100644 --- a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/TraceMonitorTest.kt +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/TraceMonitorTest.kt @@ -20,7 +20,8 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.server.wm.flicker.FlickerRunResult import com.android.server.wm.flicker.getDefaultFlickerOutputDir -import com.android.server.wm.traces.parser.DeviceStateDump +import com.android.server.wm.traces.common.DeviceTraceDump +import com.android.server.wm.traces.parser.DeviceDumpParser import com.google.common.io.Files import com.google.common.truth.Truth import org.junit.After @@ -81,19 +82,19 @@ abstract class TraceMonitorTest<T : TransitionMonitor> { savedTrace = getTraceFile(result) ?: error("Could not find saved trace file") val testFile = savedTrace.toFile() Truth.assertThat(testFile.exists()).isTrue() - val calculatedChecksum = TraceMonitor.calculateChecksum(savedTrace) - Truth.assertThat(calculatedChecksum).isEqualTo(traceMonitor.checksum) val trace = Files.toByteArray(testFile) Truth.assertThat(trace.size).isGreaterThan(0) assertTrace(trace) } - private fun validateTrace(dump: DeviceStateDump) { + private fun validateTrace(dump: DeviceTraceDump) { Truth.assertWithMessage("Could not obtain SF trace") - .that(dump.layersTrace?.entries) + .that(dump.layersTrace?.entries ?: emptyArray()) + .asList() .isNotEmpty() Truth.assertWithMessage("Could not obtain WM trace") - .that(dump.wmTrace?.entries) + .that(dump.wmTrace?.entries ?: emptyArray()) + .asList() .isNotEmpty() } @@ -114,7 +115,7 @@ abstract class TraceMonitorTest<T : TransitionMonitor> { device.pressRecentApps() } - val dump = DeviceStateDump.fromTrace(trace.first, trace.second) + val dump = DeviceDumpParser.fromTrace(trace.first, trace.second) this.validateTrace(dump) } } diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/AssertionEngineTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/AssertionEngineTest.kt new file mode 100644 index 000000000..99c08cec6 --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/AssertionEngineTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service + +import com.android.server.wm.flicker.readLayerTraceFromFile +import com.android.server.wm.flicker.readTagTraceFromFile +import com.android.server.wm.flicker.readWmTraceFromFile +import com.android.server.wm.traces.common.tags.Transition +import com.google.common.truth.Truth +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +/** + * Contains [AssertionEngine] tests. To run this test: + * `atest FlickerLibTest:AssertionEngineTest` + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class AssertionEngineTest { + private val assertionEngine = AssertionEngine(emptyList()) { } + private val wmTrace by lazy { + readWmTraceFromFile("assertors/AppLaunchAndRotationsWindowManagerTrace.winscope") + } + private val layersTrace by lazy { + readLayerTraceFromFile("assertors/AppLaunchAndRotationsSurfaceFlingerTrace.winscope") + } + private val tagTrace by lazy { + readTagTraceFromFile("assertors/AppLaunchAndRotationsTagTrace.winscope") + } + private val transitionTags by lazy { assertionEngine.getTransitionTags(tagTrace) } + + @Test + fun canExtractTransitionTags() { + Truth.assertThat(transitionTags).isNotEmpty() + Truth.assertThat(transitionTags.size).isEqualTo(3) + } + + @Test + fun canSplitTraces_singleTag() { + val blocks = transitionTags + .filter { it.tag.transition == Transition.APP_LAUNCH } + .map { assertionEngine.splitTraces(it, wmTrace, layersTrace) } + + Truth.assertThat(blocks).isNotEmpty() + Truth.assertThat(blocks.size).isEqualTo(1) + + val entries = blocks.first().first.entries + Truth.assertThat(entries.first().timestamp).isEqualTo(294063112453765) + Truth.assertThat(entries.last().timestamp).isEqualTo(294063379330458) + } + + @Test + fun canSplitLayersTrace_mergedTags() { + val blocks = transitionTags + .filter { it.tag.transition == Transition.ROTATION } + .map { assertionEngine.splitTraces(it, wmTrace, layersTrace) } + + Truth.assertThat(blocks).isNotEmpty() + Truth.assertThat(blocks.size).isEqualTo(2) + + val entries = blocks.last().second.entries + Truth.assertThat(entries.first().timestamp).isEqualTo(294064497020048) + Truth.assertThat(entries.last().timestamp).isEqualTo(294064981192909) + } + + @Test + fun canSplitLayersTrace_noTags() { + val blocks = transitionTags + .filter { it.tag.transition == Transition.APP_CLOSE } + .map { assertionEngine.splitTraces(it, wmTrace, layersTrace) } + + Truth.assertThat(blocks).isEmpty() + } +}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/ErrorParserTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/ErrorParserTest.kt new file mode 100644 index 000000000..44d4d6e2a --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/ErrorParserTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service + +import com.android.server.wm.flicker.FlickerErrorProto +import com.android.server.wm.flicker.FlickerErrorStateProto +import com.android.server.wm.flicker.FlickerErrorTraceProto +import com.android.server.wm.traces.parser.errors.ErrorTraceParserUtil +import com.google.common.truth.Truth.assertThat +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +/** + * Contains [ErrorTraceParserUtil] and [FlickerErrorProto] tests. To run this test: `atest + * FlickerLibTest:ErrorParserTest` + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ErrorParserTest { + private val error = FlickerErrorProto.newBuilder() + .setLayerId(1) + .setTaskId(2) + .setWindowToken("token") + .setMessage("Error!") + .setStacktrace("stacktrace of error") + .setAssertionName("LayerIsVisibleAtStart") + .build() + private val state = FlickerErrorStateProto.newBuilder() + .setTimestamp(100) + .addErrors(error) + .build() + private val trace = FlickerErrorTraceProto.newBuilder() + .addStates(state) + .build() + private val traceBytes = trace.toByteArray() + + @Test + fun canParseErrors() { + val errorTrace = ErrorTraceParserUtil.parseFromTrace(traceBytes) + val errorState = errorTrace.entries.first() + val error = errorState.errors.first() + + assertThat(errorState.timestamp).isEqualTo(100) + assertThat(error.layerId).isEqualTo(1) + assertThat(error.taskId).isEqualTo(2) + assertThat(error.message).isEqualTo("Error!") + assertThat(error.stacktrace).isEqualTo("stacktrace of error") + assertThat(error.windowToken).isEqualTo("token") + } +}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/FassMockTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/FassMockTest.kt new file mode 100644 index 000000000..a69af6491 --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/FassMockTest.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.server.wm.flicker.helpers.SampleAppHelper +import com.android.server.wm.flicker.rules.WMFlickerServiceRule +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import org.junit.FixMethodOrder +import org.junit.Rule +import org.junit.Test +import org.junit.runners.MethodSorters + +/** + * Contains a mock test for [WMFlickerServiceRule]. + * + * To run this test: `atest FlickerLibTest:FassMockTest` + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class FassMockTest { + private val instrumentation = InstrumentationRegistry.getInstrumentation() + private val dummyAppHelper = SampleAppHelper(instrumentation) + + @get:Rule + val rule = WMFlickerServiceRuleTest() + + @Test + fun startServiceTest() { + val device = UiDevice.getInstance(instrumentation) + val wmHelper = WindowManagerStateHelper(instrumentation) + device.wakeUp() + device.pressHome() + wmHelper.waitForHomeActivityVisible() + dummyAppHelper.launchViaIntent(wmHelper) + } + + companion object { + private val DUMMY_APP = FlickerComponentName("com.google.android.apps.messaging", + "com.google.android.apps.messaging.ui.ConversationListActivity") + } +}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/RotationMockTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/RotationMockTest.kt new file mode 100644 index 000000000..fdc59bfe4 --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/RotationMockTest.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.server.wm.flicker.helpers.SampleAppHelper +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.rules.WMFlickerServiceRule +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import org.junit.FixMethodOrder +import org.junit.Rule +import org.junit.Test +import org.junit.runners.MethodSorters + +/** + * Contains a rotation mock test for [WMFlickerServiceRule]. + * + * To run this test: `atest FlickerLibTest:RotationMockTest` + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class RotationMockTest { + private val instrumentation = InstrumentationRegistry.getInstrumentation() + private val dummyAppHelper = SampleAppHelper(instrumentation) + + @get:Rule + val rule = WMFlickerServiceRuleTest() + + @Test + fun startRotationServiceTest() { + val device = UiDevice.getInstance(instrumentation) + val wmHelper = WindowManagerStateHelper(instrumentation) + + device.wakeUpAndGoToHomeScreen() + wmHelper.waitForHomeActivityVisible() + dummyAppHelper.launchViaIntent(wmHelper) + device.setOrientationLeft() + instrumentation.uiAutomation.syncInputTransactions() + device.setOrientationNatural() + instrumentation.uiAutomation.syncInputTransactions() + } + + companion object { + private val DUMMY_APP = FlickerComponentName("com.google.android.apps.messaging", + "com.google.android.apps.messaging.ui.ConversationListActivity") + } +} diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/TagParserTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/TagParserTest.kt new file mode 100644 index 000000000..716d7cb77 --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/TagParserTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service + +import com.android.server.wm.flicker.FlickerTagProto +import com.android.server.wm.flicker.FlickerTagStateProto +import com.android.server.wm.flicker.FlickerTagTraceProto +import com.android.server.wm.traces.common.tags.Transition +import com.android.server.wm.traces.parser.tags.TagTraceParserUtil +import com.google.common.truth.Truth.assertThat +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +/** + * Contains [TagTraceParserUtil] and [FlickerTagProto] tests. To run this test: `atest + * FlickerLibTest:TagParserTest` + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class TagParserTest { + private val tag = FlickerTagProto.newBuilder() + .setLayerId(1) + .setTaskId(2) + .setIsStartTag(true) + .setId(123) + .setTransition(FlickerTagProto.Transition.APP_CLOSE) + .setWindowToken("token") + .build() + private val state = FlickerTagStateProto.newBuilder() + .setTimestamp(100) + .addTags(tag) + .build() + private val trace = FlickerTagTraceProto.newBuilder() + .addStates(state) + .build() + private val traceBytes = trace.toByteArray() + + @Test + fun canParseTags() { + val tagTrace = TagTraceParserUtil.parseFromTrace(traceBytes) + val tagState = tagTrace.entries.first() + val tag = tagState.tags.first() + + assertThat(tagState.timestamp).isEqualTo(100) + assertThat(tag.layerId).isEqualTo(1) + assertThat(tag.taskId).isEqualTo(2) + assertThat(tag.id).isEqualTo(123) + assertThat(tag.isStartTag).isTrue() + assertThat(tag.transition).isEqualTo(Transition.APP_CLOSE) + assertThat(tag.windowToken).isEqualTo("token") + } +}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/TraceIsTaggableTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/TraceIsTaggableTest.kt new file mode 100644 index 000000000..efddbbd64 --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/TraceIsTaggableTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.server.wm.flicker.helpers.SampleAppHelper +import com.android.server.wm.flicker.monitor.withTracing +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.hasLayersAnimating +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isAppTransitionIdle +import com.android.server.wm.traces.common.WindowManagerConditionsFactory.isWMStateComplete +import com.android.server.wm.traces.common.service.TaggingEngine +import com.android.server.wm.traces.common.tags.Transition +import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper +import com.google.common.truth.Truth +import org.junit.Test + +class TraceIsTaggableTest { + private val instrumentation = InstrumentationRegistry.getInstrumentation() + private val device = UiDevice.getInstance(instrumentation) + private val wmHelper = WindowManagerStateHelper(instrumentation) + + @Test + fun canCreateTagsFromDeviceTrace() { + + // Generates trace of opening the messaging application from home screen + val trace = withTracing { + device.pressHome() + SampleAppHelper(instrumentation).launchViaIntent(wmHelper) + + // Wait until transition is fully completed + WindowManagerStateHelper().waitFor( + hasLayersAnimating().negate(), + isAppTransitionIdle(/* default display */ 0), + isWMStateComplete() + ) + } + + val engine = TaggingEngine( + requireNotNull(trace.wmTrace), + requireNotNull(trace.layersTrace) + ) { } + + val tagStates = engine.run().entries + Truth.assertThat(tagStates.size).isEqualTo(2) + + val startTag = tagStates.first().tags + val endTag = tagStates.last().tags + Truth.assertThat(startTag.size).isEqualTo(1) + Truth.assertThat(endTag.size).isEqualTo(1) + + Truth.assertThat(startTag.first().transition).isEqualTo(Transition.APP_LAUNCH) + Truth.assertThat(endTag.first().transition).isEqualTo(Transition.APP_LAUNCH) + } +} diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/WMFlickerServiceRuleForTestSpecTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/WMFlickerServiceRuleForTestSpecTest.kt new file mode 100644 index 000000000..3de48c92a --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/WMFlickerServiceRuleForTestSpecTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service + +import android.app.Instrumentation +import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.FlickerBuilderProvider +import com.android.server.wm.flicker.FlickerParametersRunnerFactory +import com.android.server.wm.flicker.FlickerTestParameter +import com.android.server.wm.flicker.FlickerTestParameterFactory +import com.android.server.wm.flicker.dsl.FlickerBuilder +import com.android.server.wm.flicker.helpers.wakeUpAndGoToHomeScreen +import com.android.server.wm.flicker.rules.WMFlickerServiceRuleForTestSpec +import org.junit.FixMethodOrder +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class WMFlickerServiceRuleForTestSpecTest(private val testSpec: FlickerTestParameter) { + @get:Rule + val flickerRule = WMFlickerServiceRuleForTestSpec(testSpec) + + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + + @FlickerBuilderProvider + fun emptyFlicker(): FlickerBuilder { + return FlickerBuilder(instrumentation).apply { + transitions { + device.wakeUpAndGoToHomeScreen() + wmHelper.waitForAppTransitionIdle() + } + } + } + + @Test + fun runAssertion() { + flickerRule.checkPresubmitAssertions() + } + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams(): Collection<FlickerTestParameter> { + return FlickerTestParameterFactory.getInstance() + .getConfigNonRotationTests(repetitions = 1) + .take(1) + } + } +}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/WMFlickerServiceRuleTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/WMFlickerServiceRuleTest.kt new file mode 100644 index 000000000..6672ad004 --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/WMFlickerServiceRuleTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service + +import com.android.server.wm.flicker.rules.WMFlickerServiceRule +import com.google.common.truth.Truth +import org.junit.runner.Description + +/** + * An extension for [WMFlickerServiceRule] checking that the traces are collected. + */ +class WMFlickerServiceRuleTest : WMFlickerServiceRule() { + override fun finished(description: Description?) { + super.finished(description) + + Truth.assertThat(wmTrace).isNotEmpty() + Truth.assertThat(layersTrace).isNotEmpty() + } +}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/assertors/AppLaunchAssertionsTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/assertors/AppLaunchAssertionsTest.kt new file mode 100644 index 000000000..ea6350ded --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/assertors/AppLaunchAssertionsTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors + +import com.android.server.wm.flicker.readLayerTraceFromFile +import com.android.server.wm.flicker.readTestFile +import com.android.server.wm.flicker.readWmTraceFromFile +import com.android.server.wm.traces.common.tags.Tag +import com.android.server.wm.traces.common.tags.Transition +import com.google.common.truth.Truth +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +/** + * Contains tests for App Launch assertions. To run this test: + * `atest FlickerLibTest:AppLaunchAssertionsTest` + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class AppLaunchAssertionsTest { + private val jsonByteArray = readTestFile("assertors/config.json") + private val assertions = + AssertionConfigParser.parseConfigFile(String(jsonByteArray)) + .filter { it.transitionType == Transition.APP_LAUNCH } + + private val appLaunchAssertor = TransitionAssertor(assertions) { } + + @Test + fun testValidAppLaunchTraces() { + val wmTrace = readWmTraceFromFile( + "assertors/appLaunch/WindowManagerTrace.winscope") + val layersTrace = readLayerTraceFromFile( + "assertors/appLaunch/SurfaceFlingerTrace.winscope") + val errorTrace = appLaunchAssertor.analyze(VALID_APP_LAUNCH_TAG, wmTrace, layersTrace) + + Truth.assertThat(errorTrace).isEmpty() + } + + @Test + fun testInvalidAppLaunchTraces() { + val wmTrace = readWmTraceFromFile( + "assertors/appLaunch/WindowManagerInvalidTrace.winscope") + val layersTrace = readLayerTraceFromFile( + "assertors/appLaunch/SurfaceFlingerInvalidTrace.winscope") + val errorTrace = appLaunchAssertor.analyze(INVALID_APP_LAUNCH_TAG, wmTrace, layersTrace) + + Truth.assertThat(errorTrace).isNotEmpty() + Truth.assertThat(errorTrace.entries.size).isEqualTo(1) + } + + companion object { + private val VALID_APP_LAUNCH_TAG = Tag(1, Transition.APP_LAUNCH, true, + windowToken = "e91fbda") + private val INVALID_APP_LAUNCH_TAG = Tag(2, Transition.APP_LAUNCH, true, + windowToken = "ffc3b1f") + } +}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/assertors/AssertionConfigParserTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/assertors/AssertionConfigParserTest.kt new file mode 100644 index 000000000..e4a3615ad --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/assertors/AssertionConfigParserTest.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors + +import com.android.server.wm.flicker.readTestFile +import com.android.server.wm.traces.common.tags.Transition +import com.google.common.truth.Truth +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +/** + * Contains [AssertionConfigParser] tests. To run this test: + * `atest FlickerLibTest:AssertionConfigParserTest` + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class AssertionConfigParserTest { + + @Test + fun canParseConfigFile() { + val jsonByteArray = readTestFile("assertors/assertionsConfig.json") + val assertionConfigurations = AssertionConfigParser.parseConfigFile(String(jsonByteArray)) + Truth.assertThat(assertionConfigurations).hasSize(23) + Truth.assertThat(assertionConfigurations.first().transitionType) + .isEqualTo(Transition.ROTATION) + Truth.assertThat(assertionConfigurations.last().transitionType) + .isEqualTo(Transition.APP_LAUNCH) + } +}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/assertors/RotationAssertionsTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/assertors/RotationAssertionsTest.kt new file mode 100644 index 000000000..d0f7ea0f1 --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/assertors/RotationAssertionsTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.assertors + +import com.android.server.wm.flicker.readLayerTraceFromFile +import com.android.server.wm.flicker.readTestFile +import com.android.server.wm.flicker.readWmTraceFromFile +import com.android.server.wm.traces.common.tags.Tag +import com.android.server.wm.traces.common.tags.Transition +import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace +import com.google.common.truth.Truth +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +/** + * Contains tests for rotation assertions. To run this test: + * `atest FlickerLibTest:RotationAssertionsTest` + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class RotationAssertionsTest { + private val jsonByteArray = readTestFile("assertors/config.json") + private val assertions = + AssertionConfigParser.parseConfigFile(String(jsonByteArray)) + .filter { it.transitionType == Transition.ROTATION } + + private val rotationAssertor = TransitionAssertor(assertions) { } + + @Test + fun testValidRotationWmTrace() { + val wmTrace = readWmTraceFromFile("assertors/rotation/WindowManagerTrace.winscope") + val layersTrace = readLayerTraceFromFile("assertors/rotation/SurfaceFlingerTrace.winscope") + val errorTrace = rotationAssertor.analyze(ROTATION_TAG, wmTrace, layersTrace) + + Truth.assertThat(errorTrace).isEmpty() + } + + @Test + fun testValidRotationLayersTrace() { + val trace = readLayerTraceFromFile("assertors/rotation/SurfaceFlingerTrace.winscope") + val errorTrace = rotationAssertor.analyze(ROTATION_TAG, EMPTY_WM_TRACE, trace) + + Truth.assertThat(errorTrace).isEmpty() + } + + @Test + fun testInvalidRotationLayersTrace() { + val trace = readLayerTraceFromFile( + "assertors/rotation/SurfaceFlingerInvalidTrace.winscope") + val errorTrace = rotationAssertor.analyze(ROTATION_TAG, EMPTY_WM_TRACE, trace) + + Truth.assertThat(errorTrace).isNotEmpty() + Truth.assertThat(errorTrace.entries.size).isEqualTo(1) + } + + companion object { + private val EMPTY_WM_TRACE = WindowManagerTrace(emptyArray(), source = "") + private val ROTATION_TAG = Tag(1, Transition.ROTATION, true) + } +}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/AppCloseProcessorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/AppCloseProcessorTest.kt new file mode 100644 index 000000000..e0b55f803 --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/AppCloseProcessorTest.kt @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.processors + +import com.android.server.wm.flicker.readLayerTraceFromFile +import com.android.server.wm.flicker.readWmTraceFromFile +import com.android.server.wm.traces.common.service.processors.AppCloseProcessor +import com.google.common.truth.Truth +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +/** + * Contains [AppCloseProcessor] tests. To run this test: + * `atest FlickerLibTest:AppCloseProcessorTest` + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class AppCloseProcessorTest { + private val processor = AppCloseProcessor { } + + private val tagAppCloseByBackButton by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/appclose/backbutton/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/appclose/backbutton/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagAppCloseBySwipeUp by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/appclose/swipeup/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/appclose/swipeup/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagAppCloseBySwitchingApps by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/appclose/switchapp/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/appclose/switchapp/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagsAppCloseByClosingRotatedApp by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/appclose/rotated/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/appclose/rotated/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagsColdAppLaunch by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/applaunch/cold/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/applaunch/cold/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagsWarmAppLaunch by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/applaunch/warm/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/applaunch/warm/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + @Test + fun generatesAppCloseTagsByPressingBackButton() { + val tagTrace = tagAppCloseByBackButton + Truth.assertWithMessage("Should have 2 app close tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 9295108146952 // 0d2h34m55s108ms + val endTagTimestamp = 9295480256103 // 0d2h34m55s480ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun generatesAppCloseTagsBySwipe() { + val tagTrace = tagAppCloseBySwipeUp + Truth.assertWithMessage("Should have 2 app close tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 9320574596259 // 0d2h35m20s578ms + val endTagTimestamp = 9321301178051 // 0d2h35m21s301ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun generatesAppCloseTagsBySwitchingApps() { + val tagTrace = tagAppCloseBySwitchingApps + Truth.assertWithMessage("Should have 2 app close tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 4129701437903 // 0d1h8m49s701ms + val endTagTimestamp = 4132063745690 // 0d1h8m52s52ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun generatesAppCloseTagsWhenAppRotated90() { + val tagTrace = tagsAppCloseByClosingRotatedApp + Truth.assertWithMessage("Should have 2 app close tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 343127388040903 // 3d23h18m47s388ms + val endTagTimestamp = 343128129419414 // 3d23h18m48s129ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun generatesNoTagsOnColdAppLaunch() { + val tagTrace = tagsColdAppLaunch + Truth.assertWithMessage("Should have 0 app launch tags") + .that(tagTrace) + .hasSize(0) + } + + @Test + fun generatesNoTagsOnWarmAppLaunch() { + val tagTrace = tagsWarmAppLaunch + Truth.assertWithMessage("Should have 0 app launch tags") + .that(tagTrace) + .hasSize(0) + } +}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/AppLaunchProcessorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/AppLaunchProcessorTest.kt new file mode 100644 index 000000000..73da69fc9 --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/AppLaunchProcessorTest.kt @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.processors + +import com.android.server.wm.flicker.readLayerTraceFromFile +import com.android.server.wm.flicker.readWmTraceFromFile +import com.android.server.wm.traces.common.service.processors.AppLaunchProcessor +import com.google.common.truth.Truth +import org.junit.Test + +/** + * Contains [AppLaunchProcessor] tests. To run this test: + * `atest FlickerLibTest:AppLaunchProcessorTest` + */ +class AppLaunchProcessorTest { + private val processor = AppLaunchProcessor { } + + /** + * Scenarios expecting tags + */ + private val tagsColdAppLaunch by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/applaunch/cold/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/applaunch/cold/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagsWarmAppLaunch by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/applaunch/warm/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/applaunch/warm/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagsAppLaunchByIntent by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/applaunch/intent/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/applaunch/intent/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagsAppLaunchWithRotation by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/applaunch/withrot/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/applaunch/withrot/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + /** + * Scenarios expecting no tags + */ + private val tagsComposeNewMessage by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/ime/appear/bygesture/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/ime/appear/bygesture/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagsRotation by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/rotation/verticaltohorizontal/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/rotation/verticaltohorizontal/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + @Test + fun tagsColdAppLaunch() { + val tagTrace = tagsColdAppLaunch + Truth.assertWithMessage("Should have 2 app launch tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 677617096496959 // Represents 7d20h13m37s96ms + val endTagTimestamp = 677617685370716 // Represents 7d20h13m37s685ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun tagsWarmAppLaunch() { + val tagTrace = tagsWarmAppLaunch + Truth.assertWithMessage("Should have 2 app launch tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 677630638463152 // Represents 7d20h13m50s638ms + val endTagTimestamp = 677631170881851 // Represents 7d20h13m51s170ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun tagsAppLaunchByIntent() { + val tagTrace = tagsAppLaunchByIntent + Truth.assertWithMessage("Should have 2 app launch tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 678152269074856 // Represents 7d20h22m32s269ms + val endTagTimestamp = 678152921944244 // Represents 7d20h22m32s921ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun tagsAppLaunchWithRotation() { + val tagTrace = tagsAppLaunchWithRotation + Truth.assertWithMessage("Should have 2 app launch tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 765902849663314 // Represents 8d20h45m2s849ms + val endTagTimestamp = 765903475287491 // Represents 8d20h45m4s139ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun doesNotTagComposeNewMessage() { + val tagTrace = tagsComposeNewMessage + Truth.assertWithMessage("Should have 0 app launch tags") + .that(tagTrace) + .isEmpty() + } + + @Test + fun doesNotTagRotation() { + val tagTrace = tagsRotation + Truth.assertWithMessage("Should have 0 app launch tags") + .that(tagTrace) + .isEmpty() + } +}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/ImeAppearProcessorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/ImeAppearProcessorTest.kt new file mode 100644 index 000000000..827a7aa67 --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/ImeAppearProcessorTest.kt @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.processors + +import com.android.server.wm.flicker.readLayerTraceFromFile +import com.android.server.wm.flicker.readWmTraceFromFile +import com.android.server.wm.traces.common.service.processors.ImeAppearProcessor +import com.google.common.truth.Truth +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +/** + * Contains [ImeAppearProcessor] tests. To run this test: + * `atest FlickerLibTest:ImeAppearProcessorTest` + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ImeAppearProcessorTest { + private val processor = ImeAppearProcessor { } + + private val tagsImeAppearWithGesture by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/ime/appear/bygesture/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/ime/appear/bygesture/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagsImeAppearWithoutGestureOnAppLaunch by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/ime/appear/applaunchnogesture/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/ime/appear/applaunchnogesture/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val doesntTagStationaryIme by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/ime/appear/stationary/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/ime/appear/stationary/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagsImeAppearHorizontal by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/ime/appear/horizontal/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/ime/appear/horizontal/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + @Test + fun generatesImeTagsOnGesture() { + val tagTrace = tagsImeAppearWithGesture + Truth.assertWithMessage("Should have 2 Ime appear tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 11120377206699 // 0d3h5m20s377ms + val endTagTimestamp = 11120645554330 // 0d3h5m20s645ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun generatesTagsOnAppLaunchWithNoGesture() { + val tagTrace = tagsImeAppearWithoutGestureOnAppLaunch + Truth.assertWithMessage("Should have 2 Ime appear tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 437392442580 // 0d0h7m17s392ms + val endTagTimestamp = 437396516487 // 0d0h7m17s396ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun generatesTagsOnHorizontalAppLaunchWithGesture() { + val tagTrace = tagsImeAppearHorizontal + Truth.assertWithMessage("Should have 2 Ime appear tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 1015548115255262 // 11d18h5m48s115ms + val endTagTimestamp = 1015548233177878 // 11d18h5m48s223ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun doNotGenerateTagsOnStationaryIme() { + val tagTrace = doesntTagStationaryIme + Truth.assertWithMessage("Should have no Ime appear tags") + .that(tagTrace) + .hasSize(0) + } +}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/ImeDisappearProcessorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/ImeDisappearProcessorTest.kt new file mode 100644 index 000000000..f5aae5add --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/ImeDisappearProcessorTest.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.processors + +import com.android.server.wm.flicker.readLayerTraceFromFile +import com.android.server.wm.flicker.readWmTraceFromFile +import com.android.server.wm.traces.common.service.processors.ImeDisappearProcessor +import com.google.common.truth.Truth +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +/** + * Contains [ImeDisappearProcessor] tests. To run this test: + * `atest FlickerLibTest:ImeDisappearProcessorTest` + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class ImeDisappearProcessorTest { + private val processor = ImeDisappearProcessor { } + + private val tagsImeDisappearWithGestureOpenAndClose by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/ime/disappear/bygesture/openandclose/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/ime/disappear/bygesture/openandclose/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagsImeDisappearOnAppOpenAndClose by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/ime/disappear/closeapp/openandclose/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/ime/disappear/closeapp/openandclose/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagsImeDisappearWithGestureClose by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/ime/disappear/bygesture/close/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/ime/disappear/bygesture/close/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagsImeDisappearOnAppClose by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/ime/disappear/closeapp/close/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/ime/disappear/closeapp/close/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + @Test + fun generatesImeDisappearTagsWithGestureOpenAndCloseIme() { + val tagTrace = tagsImeDisappearWithGestureOpenAndClose + Truth.assertWithMessage("Should have 2 IME disappear tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 69234720627579 // 19h13m54s720ms + val endTagTimestamp = 69234929459162 // 19h13m54s929ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun generatesImeDisappearTagsOnAppOpenAndClose() { + val tagTrace = tagsImeDisappearOnAppOpenAndClose + Truth.assertWithMessage("Should have 2 IME disappear tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 69524600678331 // 19h18m44s600ms + val endTagTimestamp = 69524958584304 // 19h18m44s958ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun generatesImeDisappearTagsWithGestureCloseIme() { + val tagTrace = tagsImeDisappearWithGestureClose + Truth.assertWithMessage("Should have 2 IME disappear tags") + .that(tagTrace) + .hasSize(2) + + val startTagTimestamp = 69387450340971 // 19h16m27s450ms + val endTagTimestamp = 69387644316302 // 19h16m27s644ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun generatesImeDisappearTagsOnAppClose() { + val tagTrace = tagsImeDisappearOnAppClose + Truth.assertWithMessage("Should have 2 IME disappear tags") + .that(tagTrace) + .hasSize(2) + + val startTagTimestamp = 69635457764375 // 19h20m35s457ms + val endTagTimestamp = 69635765486645 // 19h20m35s765ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } +}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipEnterProcessorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipEnterProcessorTest.kt new file mode 100644 index 000000000..32eb1bb49 --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipEnterProcessorTest.kt @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.processors + +import com.android.server.wm.flicker.readLayerTraceFromFile +import com.android.server.wm.flicker.readWmTraceFromFile +import com.android.server.wm.traces.common.service.processors.PipEnterProcessor +import com.google.common.truth.Truth +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +/** + * Contains [PipEnterProcessor] tests. To run this test: + * `atest FlickerLibTest:PipEnterProcessorTest` + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class PipEnterProcessorTest { + private val processor = PipEnterProcessor {} + + private val tagsPipEnterWithoutRotation by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/pip/enter/norotation/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/pip/enter/norotation/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagsPipEnterWithRotation by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/pip/enter/rotation/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/pip/enter/rotation/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagsPipEnterWithSplitScreen by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/pip/enter/splitscreen/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/pip/enter/splitscreen/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagsPipEnterTwice by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/pip/enter/twice/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/pip/enter/twice/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagsStationaryPip by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/pip/enter/stationary/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/pip/enter/stationary/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + @Test + fun generatesPipEnterTagsWithoutRotation() { + val tagTrace = tagsPipEnterWithoutRotation + Truth.assertWithMessage("Should have 2 PIP enter tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 224969581940 // 3m44s969ms + val endTagTimestamp = 225315964162 // 3m45s315ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun generatesPipEnterTagsWithRotation() { + val tagTrace = tagsPipEnterWithRotation + Truth.assertWithMessage("Should have 2 PIP enter tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 7546761373566 // 2h5m46s761ms + val endTagTimestamp = 7547420542538 // 2h5m47s420ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun generatesPipEnterTagsWithSplitScreen() { + val tagTrace = tagsPipEnterWithSplitScreen + Truth.assertWithMessage("Should have 2 PIP enter tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 7706716317365 // 2h8m26s716ms + val endTagTimestamp = 7707029441615 // 2h8m27s29ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun generatesPipEnterTagsTwice() { + val tagTrace = tagsPipEnterTwice + Truth.assertWithMessage("Should have 4 PIP enter tags") + .that(tagTrace) + .hasSize(4) + val firstPipEnter = arrayOf(126177655536, 126747555592) // 2m6s177ms, 2m6s747ms + val secondPipEnter = arrayOf(132780039058, 133367243856) // 2m12s780ms, 2m13s367ms + Truth.assertThat(tagTrace[0].timestamp).isEqualTo(firstPipEnter[0]) + Truth.assertThat(tagTrace[1].timestamp).isEqualTo(firstPipEnter[1]) + Truth.assertThat(tagTrace[2].timestamp).isEqualTo(secondPipEnter[0]) + Truth.assertThat(tagTrace[3].timestamp).isEqualTo(secondPipEnter[1]) + } + + @Test + fun doesNotGeneratePipEnterTagsOnStationaryPip() { + val tagTrace = tagsStationaryPip + Truth.assertWithMessage("Should have no PIP enter tags") + .that(tagTrace) + .hasSize(0) + } +}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipExitProcessorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipExitProcessorTest.kt new file mode 100644 index 000000000..a0d703887 --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipExitProcessorTest.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.processors + +import com.android.server.wm.flicker.readLayerTraceFromFile +import com.android.server.wm.flicker.readWmTraceFromFile +import com.android.server.wm.traces.common.service.processors.PipExitProcessor +import com.google.common.truth.Truth +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +/** + * Contains [PipExitProcessor] tests. To run this test: + * `atest FlickerLibTest:PipExitProcessorTest` + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class PipExitProcessorTest { + private val processor = PipExitProcessor { } + + private val tagPipExitByDismissButton by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/pip/exit/dismissbutton/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/pip/exit/dismissbutton/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagPipExitBySwipe by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/pip/exit/swipe/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/pip/exit/swipe/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagPipExpand by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/pip/expand/expand/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/pip/expand/expand/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + @Test + fun generatesPipExitTagsByDismissButton() { + val tagTrace = tagPipExitByDismissButton + Truth.assertWithMessage("Should have 2 pip exit tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 2852929744046 // 0d0h47m32s929ms + val endTagTimestamp = 2853783914340 // 0d0h47m33s783ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun generatesPipExitTagsBySwipe() { + val tagTrace = tagPipExitBySwipe + Truth.assertWithMessage("Should have 2 pip exit tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 2057767033167 // 0d0h34m17s767ms + val endTagTimestamp = 2058963092661 // 0d0h34m18s963ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun doesNotTagPipExitOnPipExpand() { + val tagTrace = tagPipExpand + Truth.assertWithMessage("Should have 0 pip exit tags") + .that(tagTrace) + .hasSize(0) + } +}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipExpandProcessorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipExpandProcessorTest.kt new file mode 100644 index 000000000..bca7f7152 --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipExpandProcessorTest.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.processors + +import com.android.server.wm.flicker.readLayerTraceFromFile +import com.android.server.wm.flicker.readWmTraceFromFile +import com.android.server.wm.traces.common.service.processors.PipExpandProcessor +import com.google.common.truth.Truth +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +/** + * Contains [PipExpandProcessor] tests. To run this test: + * `atest FlickerLibTest:PipExpandProcessorTest` + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class PipExpandProcessorTest { + private val processor = PipExpandProcessor { } + + private val tagsPipExpanding by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/pip/expand/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/pip/expand/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagsPipEnterTwice by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/pip/enter/twice/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/pip/enter/twice/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + @Test + fun generatesPipExpandTags() { + val tagTrace = tagsPipExpanding + Truth.assertWithMessage("Should have 2 pip expand tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 2881856703699 // 0d0h48m1s856ms + val endTagTimestamp = 2882177502376 // 0d0h0m48m2s177ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun doesntTagPipEnterTwice() { + val tagTrace = tagsPipEnterTwice + Truth.assertWithMessage("Should have 0 pip expand tags") + .that(tagTrace) + .hasSize(0) + } +}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipResizeProcessorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipResizeProcessorTest.kt new file mode 100644 index 000000000..34a87dfd8 --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/PipResizeProcessorTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.processors + +import com.android.server.wm.flicker.readLayerTraceFromFile +import com.android.server.wm.flicker.readWmTraceFromFile +import com.android.server.wm.traces.common.service.processors.PipResizeProcessor +import com.google.common.truth.Truth +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +/** + * Contains [PipResizeProcessor] tests. To run this test: + * `atest FlickerLibTest:PipResizeProcessorTest` + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class PipResizeProcessorTest { + private val processor = PipResizeProcessor { } + + private val tagsPipResizingToExpand by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/pip/resize/expand/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/pip/resize/expand/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + private val tagsPipResizingToShrink by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/pip/resize/shrink/WindowManagerTrace.winscope" + ) + val layersTrace = readLayerTraceFromFile( + "tagprocessors/pip/resize/shrink/SurfaceFlingerTrace.winscope" + ) + processor.generateTags(wmTrace, layersTrace) + } + + @Test + fun generatesPipResizeTagsOnExpand() { + val tagTrace = tagsPipResizingToExpand + Truth.assertWithMessage("Should have 2 pip resize tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 175188084434996 // 2d0h39m48s84ms + val endTagTimestamp = 175188414547217 // 2d0h39m48s414ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } + + @Test + fun generatesPipResizeTagsOnShrink() { + val tagTrace = tagsPipResizingToShrink + Truth.assertWithMessage("Should have 2 pip resize tags") + .that(tagTrace) + .hasSize(2) + val startTagTimestamp = 183718779433562 // 2d3h1m58s779ms + val endTagTimestamp = 183719115416407 // 2d3h1m59s115ms + Truth.assertThat(tagTrace.first().timestamp).isEqualTo(startTagTimestamp) + Truth.assertThat(tagTrace.last().timestamp).isEqualTo(endTagTimestamp) + } +}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/RotationProcessorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/RotationProcessorTest.kt new file mode 100644 index 000000000..0245a5941 --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/service/processors/RotationProcessorTest.kt @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.service.processors + +import com.android.server.wm.flicker.readLayerTraceFromFile +import com.android.server.wm.flicker.readWmTraceFromFile +import com.android.server.wm.traces.common.service.processors.RotationProcessor +import com.google.common.truth.Truth +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +/** + * Contains [RotationProcessor] tests. To run this test: + * `atest FlickerLibTest:RotationProcessorTest` + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class RotationProcessorTest { + companion object { + const val REGULAR_ROTATION1_START = 280186737540384 + const val REGULAR_ROTATION1_END = 280187243649340 + const val REGULAR_ROTATION2_START = 280188522078113 + const val REGULAR_ROTATION2_END = 280189020672174 + const val SEAMLESS_ROTATION_START = 981157456801L + const val SEAMLESS_ROTATION_END = 981560560070L + const val DISPLAYS_ROTATION1_START = 67585958089516 + const val DISPLAYS_ROTATION1_END = 67586545923169 + const val DISPLAYS_ROTATION2_START = 67587517164151 + const val DISPLAYS_ROTATION2_END = 67588122219420 + } + + private val rotationProcessor = RotationProcessor { } + private val tagsRegularRotationTagFinalState by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/rotation/regular/WindowManagerTrace.winscope") + val layersTrace = readLayerTraceFromFile( + "tagprocessors/rotation/regular/SurfaceFlingerTrace.winscope") + rotationProcessor.generateTags(wmTrace, layersTrace) + } + + private val tagsSeamlessRotation by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/rotation/seamless/WindowManagerTrace.winscope") + val layersTrace = readLayerTraceFromFile( + "tagprocessors/rotation/seamless/SurfaceFlingerTrace.winscope") + rotationProcessor.generateTags(wmTrace, layersTrace) + } + + private val tagsDisplaysRotation by lazy { + val wmTrace = readWmTraceFromFile( + "tagprocessors/rotation/displays/WindowManagerTrace.winscope") + val layersTrace = readLayerTraceFromFile( + "tagprocessors/rotation/displays/SurfaceFlingerTrace.winscope") + rotationProcessor.generateTags(wmTrace, layersTrace) + } + + @Test + fun canDetectMultipleRegularRotations() { + Truth.assertWithMessage("Number of tags") + .that(tagsRegularRotationTagFinalState) + .hasSize(4) + val tags = tagsRegularRotationTagFinalState.flatMap { it.tags.toList() } + Truth.assertWithMessage("Number of start tags") + .that(tags.filter { it.isStartTag }) + .hasSize(2) + Truth.assertWithMessage("Number of end tags") + .that(tags.filterNot { it.isStartTag }) + .hasSize(2) + } + + @Test + fun canDetectRegularRotation1Start() { + val tag = tagsRegularRotationTagFinalState + .firstOrNull { it.tags.any { tag -> tag.isStartTag } } + ?.timestamp ?: 0L + Truth.assertWithMessage("Start tag timestamp") + .that(tag) + .isEqualTo(REGULAR_ROTATION1_START) + } + + @Test + fun canDetectRegularRotation1End() { + val tag = tagsRegularRotationTagFinalState + .firstOrNull { it.tags.any { tag -> !tag.isStartTag } } + ?.timestamp ?: 0L + Truth.assertWithMessage("End tag timestamp") + .that(tag) + .isEqualTo(REGULAR_ROTATION1_END) + } + + @Test + fun canDetectRegularRotation2Start() { + val tag = tagsRegularRotationTagFinalState + .lastOrNull { it.tags.any { tag -> tag.isStartTag } } + ?.timestamp ?: 0L + Truth.assertWithMessage("Start tag timestamp") + .that(tag) + .isEqualTo(REGULAR_ROTATION2_START) + } + + @Test + fun canDetectRegularRotation2End() { + val tag = tagsRegularRotationTagFinalState + .lastOrNull { it.tags.any { tag -> !tag.isStartTag } } + ?.timestamp ?: 0L + Truth.assertWithMessage("End tag timestamp") + .that(tag) + .isEqualTo(REGULAR_ROTATION2_END) + } + + @Test + fun canDetectSeamlessRotation() { + Truth.assertWithMessage("Number of tags") + .that(tagsSeamlessRotation) + .hasSize(2) + val tags = tagsSeamlessRotation.flatMap { it.tags.toList() } + Truth.assertWithMessage("Number of start tags") + .that(tags.filter { it.isStartTag }) + .hasSize(1) + Truth.assertWithMessage("Number of end tags") + .that(tags.filterNot { it.isStartTag }) + .hasSize(1) + } + + @Test + fun canDetectSeamlessRotationStart() { + val tag = tagsSeamlessRotation + .lastOrNull { it.tags.any { tag -> tag.isStartTag } } + ?.timestamp ?: 0L + Truth.assertWithMessage("Start tag timestamp") + .that(tag) + .isEqualTo(SEAMLESS_ROTATION_START) + } + + @Test + fun canDetectSeamlessRotationEnd() { + val tag = tagsSeamlessRotation + .lastOrNull { it.tags.any { tag -> !tag.isStartTag } } + ?.timestamp ?: 0L + Truth.assertWithMessage("End tag timestamp") + .that(tag) + .isEqualTo(SEAMLESS_ROTATION_END) + } + + @Test + fun canDetectDisplaysRotation() { + Truth.assertWithMessage("Number of tags") + .that(tagsDisplaysRotation) + .hasSize(4) + val tags = tagsDisplaysRotation.flatMap { it.tags.toList() } + Truth.assertWithMessage("Number of start tags") + .that(tags.filter { it.isStartTag }) + .hasSize(2) + Truth.assertWithMessage("Number of end tags") + .that(tags.filterNot { it.isStartTag }) + .hasSize(2) + } + + @Test + fun canDetectDisplaysRotation1Start() { + val tag = tagsDisplaysRotation + .firstOrNull { it.tags.any { tag -> tag.isStartTag } } + ?.timestamp ?: 0L + Truth.assertWithMessage("Start tag timestamp") + .that(tag) + .isEqualTo(DISPLAYS_ROTATION1_START) + } + + @Test + fun canDetectDisplaysRotation1End() { + val tag = tagsDisplaysRotation + .firstOrNull { it.tags.any { tag -> !tag.isStartTag } } + ?.timestamp ?: 0L + Truth.assertWithMessage("End tag timestamp") + .that(tag) + .isEqualTo(DISPLAYS_ROTATION1_END) + } + + @Test + fun canDetectDisplaysRotation2Start() { + val tag = tagsDisplaysRotation + .lastOrNull { it.tags.any { tag -> tag.isStartTag } } + ?.timestamp ?: 0L + Truth.assertWithMessage("Start tag timestamp") + .that(tag) + .isEqualTo(DISPLAYS_ROTATION2_START) + } + + @Test + fun canDetectDisplaysRotation2End() { + val tag = tagsDisplaysRotation + .lastOrNull { it.tags.any { tag -> !tag.isStartTag } } + ?.timestamp ?: 0L + Truth.assertWithMessage("End tag timestamp") + .that(tag) + .isEqualTo(DISPLAYS_ROTATION2_END) + } +}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerStateHelperTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerStateHelperTest.kt index 4c18c68ac..dd6f5c26f 100644 --- a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerStateHelperTest.kt +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerStateHelperTest.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,22 +14,27 @@ * limitations under the License. */ -package com.android.server.wm.flicker +package com.android.server.wm.flicker.windowmanager -import android.content.ComponentName import android.view.Display import android.view.Surface import androidx.test.filters.FlakyTest import androidx.test.platform.app.InstrumentationRegistry +import com.android.server.wm.flicker.readWmTraceFromDumpFile +import com.android.server.wm.flicker.readWmTraceFromFile import com.android.server.wm.flicker.traces.windowmanager.WindowManagerStateSubject import com.android.server.wm.traces.common.Buffer import com.android.server.wm.traces.common.Color +import com.android.server.wm.traces.common.DeviceStateDump +import com.android.server.wm.traces.common.FlickerComponentName import com.android.server.wm.traces.common.Rect import com.android.server.wm.traces.common.RectF import com.android.server.wm.traces.common.Region import com.android.server.wm.traces.common.layers.Layer +import com.android.server.wm.traces.common.layers.LayerTraceEntry import com.android.server.wm.traces.common.layers.LayerTraceEntryBuilder import com.android.server.wm.traces.common.layers.Transform +import com.android.server.wm.traces.common.windowmanager.WindowManagerState import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper import com.google.common.truth.Truth @@ -44,29 +49,28 @@ import org.junit.runners.MethodSorters @FixMethodOrder(MethodSorters.NAME_ASCENDING) class WindowManagerStateHelperTest { class TestWindowManagerStateHelper( + _wmState: WindowManagerState, /** * Predicate to supply a new UI information */ - deviceDumpSupplier: () -> Dump, + deviceDumpSupplier: () -> DeviceStateDump<WindowManagerState, LayerTraceEntry>, numRetries: Int = 5, retryIntervalMs: Long = 500L ) : WindowManagerStateHelper(InstrumentationRegistry.getInstrumentation(), deviceDumpSupplier, numRetries, retryIntervalMs) { - var wmState = computeState(ignoreInvalidStates = true).wmState - override fun computeState(ignoreInvalidStates: Boolean): Dump { - val state = super.computeState(ignoreInvalidStates) - wmState = state.wmState - return state + var wmState: WindowManagerState = _wmState + private set + + override fun updateCurrState(value: DeviceStateDump<WindowManagerState, LayerTraceEntry>) { + wmState = value.wmState } } - private fun String.toComponentName() = - ComponentName.unflattenFromString(this) ?: error("Unable to extract component name") - - private val chromeComponentName = ("com.android.chrome/org.chromium.chrome.browser" + - ".firstrun.FirstRunActivity").toComponentName() - private val simpleAppComponentName = "com.android.server.wm.flicker.testapp/.SimpleActivity" - .toComponentName() + private val chromeComponent = FlickerComponentName.unflattenFromString( + "com.android.chrome/org.chromium.chrome.browser" + + ".firstrun.FirstRunActivity") + private val simpleAppComponentName = FlickerComponentName.unflattenFromString( + "com.android.server.wm.flicker.testapp/.SimpleActivity") private fun createImaginaryLayer(name: String, index: Int, id: Int, parentId: Int): Layer { val transform = Transform(0, Transform.Matrix(0f, 0f, 0f, 0f, 0f, 0f)) @@ -84,7 +88,7 @@ class WindowManagerStateHelperTest { visibleRegion = Region(rect.toRect()), activeBuffer = Buffer(1, 1, 1, 1), flags = 0, - _bounds = rect, + bounds = rect, color = Color(0f, 0f, 0f, 1f), _isOpaque = true, shadowRadius = 0f, @@ -92,7 +96,7 @@ class WindowManagerStateHelperTest { type = "", _screenBounds = rect, transform = transform, - _sourceBounds = rect, + sourceBounds = rect, currFrame = 0, effectiveScalingMode = 0, bufferTransform = transform, @@ -106,36 +110,35 @@ class WindowManagerStateHelperTest { ) } - private fun createImaginaryVisibleLayers(names: List<String>): List<Layer> { + private fun createImaginaryVisibleLayers(names: List<FlickerComponentName>): Array<Layer> { val root = createImaginaryLayer("root", -1, id = "root".hashCode(), parentId = -1) val layers = mutableListOf(root) names.forEachIndexed { index, name -> layers.add( - createImaginaryLayer(name, index, id = name.hashCode(), parentId = root.id) + createImaginaryLayer(name.toLayerName(), index, id = name.hashCode(), + parentId = root.id) ) } - return layers + return layers.toTypedArray() } private fun WindowManagerTrace.asSupplier( startingTimestamp: Long = 0 - ): () -> WindowManagerStateHelper.Dump { + ): () -> DeviceStateDump<WindowManagerState, LayerTraceEntry> { val iterator = this.dropWhile { it.timestamp < startingTimestamp }.iterator() return { if (iterator.hasNext()) { val wmState = iterator.next() - val layerList = mutableListOf(WindowManagerStateHelper.STATUS_BAR_LAYER_NAME, - WindowManagerStateHelper.NAV_BAR_LAYER_NAME) + val layerList = mutableListOf(FlickerComponentName.STATUS_BAR, + FlickerComponentName.NAV_BAR) if (wmState.inputMethodWindowState?.isSurfaceShown == true) { - layerList.add(WindowManagerStateHelper.IME_LAYER_NAME) + layerList.add(FlickerComponentName.IME) } val layerTraceEntry = LayerTraceEntryBuilder(timestamp = 0, + displays = emptyArray(), layers = createImaginaryVisibleLayers(layerList)).build() - WindowManagerStateHelper.Dump( - wmState, - layerTraceEntry - ) + DeviceStateDump(wmState, layerTraceEntry) } else { error("Reached the end of the trace") } @@ -146,35 +149,37 @@ class WindowManagerStateHelperTest { fun canWaitForIme() { val trace = readWmTraceFromFile("wm_trace_ime.pb") val supplier = trace.asSupplier() - val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size, - retryIntervalMs = 1) + val helper = TestWindowManagerStateHelper(trace.first(), supplier, + numRetries = trace.entries.size, retryIntervalMs = 1) try { WindowManagerStateSubject .assertThat(helper.wmState) - .isImeWindowVisible(Display.DEFAULT_DISPLAY) + .isNonAppWindowVisible(FlickerComponentName.IME) error("IME state should not be available") } catch (e: AssertionError) { - helper.waitImeWindowShown(Display.DEFAULT_DISPLAY) + helper.waitImeShown(Display.DEFAULT_DISPLAY) WindowManagerStateSubject .assertThat(helper.wmState) - .isImeWindowVisible(Display.DEFAULT_DISPLAY) + .isNonAppWindowVisible(FlickerComponentName.IME) } } @Test fun canFailImeNotShown() { - val supplier = readWmTraceFromFile("wm_trace_ime.pb").asSupplier() - val helper = TestWindowManagerStateHelper(supplier, retryIntervalMs = 1) + val trace = readWmTraceFromFile("wm_trace_ime.pb") + val supplier = trace.asSupplier() + val helper = TestWindowManagerStateHelper(trace.first(), supplier, + numRetries = trace.entries.size, retryIntervalMs = 1) try { WindowManagerStateSubject .assertThat(helper.wmState) - .isImeWindowVisible() + .isNonAppWindowVisible(FlickerComponentName.IME) error("IME state should not be available") } catch (e: AssertionError) { - helper.waitImeWindowShown() + helper.waitImeShown() WindowManagerStateSubject .assertThat(helper.wmState) - .isImeWindowInvisible() + .isNonAppWindowVisible(FlickerComponentName.IME) } } @@ -182,18 +187,18 @@ class WindowManagerStateHelperTest { fun canWaitForWindow() { val trace = readWmTraceFromFile("wm_trace_open_app_cold.pb") val supplier = trace.asSupplier() - val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size, - retryIntervalMs = 1) + val helper = TestWindowManagerStateHelper(trace.first(), supplier, + numRetries = trace.entries.size, retryIntervalMs = 1) try { WindowManagerStateSubject .assertThat(helper.wmState) - .contains(simpleAppComponentName) + .containsAppWindow(simpleAppComponentName) error("Chrome window should not exist in the start of the trace") } catch (e: AssertionError) { helper.waitForVisibleWindow(simpleAppComponentName) WindowManagerStateSubject .assertThat(helper.wmState) - .isVisible(simpleAppComponentName) + .isAppWindowVisible(simpleAppComponentName) } } @@ -201,11 +206,12 @@ class WindowManagerStateHelperTest { fun canFailWindowNotShown() { val trace = readWmTraceFromFile("wm_trace_open_app_cold.pb") val supplier = trace.asSupplier() - val helper = TestWindowManagerStateHelper(supplier, numRetries = 3, retryIntervalMs = 1) + val helper = TestWindowManagerStateHelper(trace.first(), supplier, + numRetries = 3, retryIntervalMs = 1) try { WindowManagerStateSubject .assertThat(helper.wmState) - .contains(simpleAppComponentName) + .containsAppWindow(simpleAppComponentName) error("SimpleActivity window should not exist in the start of the trace") } catch (e: AssertionError) { helper.waitForVisibleWindow(simpleAppComponentName) @@ -219,15 +225,15 @@ class WindowManagerStateHelperTest { fun canDetectHomeActivityVisibility() { val trace = readWmTraceFromFile("wm_trace_open_and_close_chrome.pb") val supplier = trace.asSupplier() - val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size, - retryIntervalMs = 1) + val helper = TestWindowManagerStateHelper(trace.first(), supplier, + numRetries = trace.entries.size, retryIntervalMs = 1) WindowManagerStateSubject .assertThat(helper.wmState) .isHomeActivityVisible() - helper.waitForVisibleWindow(chromeComponentName) + helper.waitForVisibleWindow(chromeComponent) WindowManagerStateSubject .assertThat(helper.wmState) - .isHomeActivityVisible(false) + .isHomeActivityInvisible() helper.waitForHomeActivityVisible() WindowManagerStateSubject .assertThat(helper.wmState) @@ -238,29 +244,31 @@ class WindowManagerStateHelperTest { fun canWaitActivityRemoved() { val trace = readWmTraceFromFile("wm_trace_open_and_close_chrome.pb") val supplier = trace.asSupplier() - val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size, - retryIntervalMs = 1) + val helper = TestWindowManagerStateHelper(trace.first(), supplier, + numRetries = trace.entries.size, retryIntervalMs = 1) WindowManagerStateSubject .assertThat(helper.wmState) .isHomeActivityVisible() - .notContains(chromeComponentName) - helper.waitForVisibleWindow(chromeComponentName) + .notContains(chromeComponent) + helper.waitForVisibleWindow(chromeComponent) WindowManagerStateSubject .assertThat(helper.wmState) - .isVisible(chromeComponentName) - helper.waitForActivityRemoved(chromeComponentName) + .isAppWindowVisible(chromeComponent) + helper.waitForActivityRemoved(chromeComponent) WindowManagerStateSubject .assertThat(helper.wmState) - .notContains(chromeComponentName) + .notContains(chromeComponent) .isHomeActivityVisible() } @Test fun canWaitAppStateIdle() { val trace = readWmTraceFromFile("wm_trace_open_and_close_chrome.pb") - val supplier = trace.asSupplier(startingTimestamp = 69443911868523) - val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size, - retryIntervalMs = 1) + val initialTimestamp = 69443911868523 + val supplier = trace.asSupplier(startingTimestamp = initialTimestamp) + val initialEntry = trace.getEntry(initialTimestamp) + val helper = TestWindowManagerStateHelper(initialEntry, supplier, + numRetries = trace.entries.size, retryIntervalMs = 1) try { WindowManagerStateSubject .assertThat(helper.wmState) @@ -280,8 +288,8 @@ class WindowManagerStateHelperTest { fun canWaitForRotation() { val trace = readWmTraceFromFile("wm_trace_rotation.pb") val supplier = trace.asSupplier() - val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size, - retryIntervalMs = 1) + val helper = TestWindowManagerStateHelper(trace.first(), supplier, + numRetries = trace.entries.size, retryIntervalMs = 1) WindowManagerStateSubject .assertThat(helper.wmState) .hasRotation(Surface.ROTATION_0) @@ -296,26 +304,6 @@ class WindowManagerStateHelperTest { } @Test - fun canFailRotationNotReached() { - val trace = readWmTraceFromFile("wm_trace_rotation.pb") - val supplier = trace.asSupplier() - val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size, - retryIntervalMs = 1) - WindowManagerStateSubject - .assertThat(helper.wmState) - .hasRotation(Surface.ROTATION_0) - try { - helper.waitForRotation(Surface.ROTATION_90) - error("Should not have reached orientation ${Surface.ROTATION_90}") - } catch (e: IllegalStateException) { - WindowManagerStateSubject - .assertThat(helper.wmState) - .isNotRotation(Surface.ROTATION_90) - .hasRotation(Surface.ROTATION_0) - } - } - - @Test fun canDetectResumedActivitiesInStacks() { val trace = readWmTraceFromDumpFile("wm_trace_resumed_activities_in_stack.pb") val entry = trace.first() @@ -330,11 +318,11 @@ class WindowManagerStateHelperTest { fun canWaitForRecents() { val trace = readWmTraceFromFile("wm_trace_open_recents.pb") val supplier = trace.asSupplier() - val helper = TestWindowManagerStateHelper(supplier, numRetries = trace.entries.size, - retryIntervalMs = 1) + val helper = TestWindowManagerStateHelper(trace.first(), supplier, + numRetries = trace.entries.size, retryIntervalMs = 1) WindowManagerStateSubject .assertThat(helper.wmState) - .isRecentsActivityVisible(visible = false) + .isRecentsActivityInvisible() helper.waitForRecentsActivityVisible() WindowManagerStateSubject .assertThat(helper.wmState) diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerStateSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerStateSubjectTest.kt new file mode 100644 index 000000000..0fe73dbdf --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerStateSubjectTest.kt @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.windowmanager + +import android.graphics.Region +import com.android.server.wm.flicker.CHROME_SPLASH_SCREEN_COMPONENT +import com.android.server.wm.flicker.IMAGINARY_COMPONENT +import com.android.server.wm.flicker.LAUNCHER_COMPONENT +import com.android.server.wm.flicker.PIP_DISMISS_COMPONENT +import com.android.server.wm.flicker.SCREEN_DECOR_COMPONENT +import com.android.server.wm.flicker.SHELL_SPLIT_SCREEN_PRIMARY_COMPONENT +import com.android.server.wm.flicker.SHELL_SPLIT_SCREEN_SECONDARY_COMPONENT +import com.android.server.wm.flicker.WALLPAPER_COMPONENT +import com.android.server.wm.flicker.assertFailure +import com.android.server.wm.flicker.assertThrows +import com.android.server.wm.flicker.assertions.FlickerSubject +import com.android.server.wm.flicker.readWmTraceFromFile +import com.android.server.wm.flicker.traces.FlickerSubjectException +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerStateSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject.Companion.assertThat +import com.android.server.wm.traces.common.FlickerComponentName +import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace +import com.google.common.truth.Truth +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters +import java.lang.AssertionError + +/** + * Contains [WindowManagerStateSubject] tests. + * To run this test: `atest FlickerLibTest:WindowManagerStateSubjectTest` + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class WindowManagerStateSubjectTest { + private val trace: WindowManagerTrace by lazy { readWmTraceFromFile("wm_trace_openchrome.pb") } + // Launcher is visible in fullscreen in the first frame of the trace + private val traceFirstFrameTimestamp = 9213763541297 + // The first frame where the chrome splash screen is shown + private val traceFirstChromeFlashScreenTimestamp = 9215551505798 + // The bounds of the display used to generate the trace [trace] + private val displayBounds = Region(0, 0, 1440, 2960) + // The region covered by the status bar in the trace + private val statusBarRegion = Region(0, 0, 1440, 171) + + @Test + fun exceptionContainsDebugInfo() { + val error = assertThrows(AssertionError::class.java) { + assertThat(trace).first().frameRegion(IMAGINARY_COMPONENT) + } + Truth.assertThat(error).hasMessageThat().contains(IMAGINARY_COMPONENT.className) + Truth.assertThat(error).hasMessageThat().contains("Trace start") + Truth.assertThat(error).hasMessageThat().contains("Trace start") + Truth.assertThat(error).hasMessageThat().contains("Trace file") + Truth.assertThat(error).hasMessageThat().contains("Entry") + Truth.assertThat(error).hasMessageThat().contains(FlickerSubject.ASSERTION_TAG) + } + + @Test + fun canDetectAboveAppWindowVisibility_isVisible() { + assertThat(trace) + .entry(traceFirstFrameTimestamp) + .containsAboveAppWindow(FlickerComponentName.NAV_BAR) + .containsAboveAppWindow(SCREEN_DECOR_COMPONENT) + .containsAboveAppWindow(FlickerComponentName.STATUS_BAR) + } + + @Test + fun canDetectAboveAppWindowVisibility_isInvisible() { + val subject = assertThat(trace).entry(traceFirstFrameTimestamp) + var failure = assertThrows(AssertionError::class.java) { + subject.containsAboveAppWindow(PIP_DISMISS_COMPONENT) + .isNonAppWindowVisible(PIP_DISMISS_COMPONENT) + } + assertFailure(failure).factValue("Is Invisible").contains("pip-dismiss-overlay") + + failure = assertThrows(AssertionError::class.java) { + subject.containsAboveAppWindow(FlickerComponentName.NAV_BAR) + .isNonAppWindowInvisible(FlickerComponentName.NAV_BAR) + } + assertFailure(failure).factValue("Is Visible").contains("NavigationBar") + } + + @Test + fun canDetectWindowCoversAtLeastRegion_exactSize() { + val entry = assertThat(trace) + .entry(traceFirstFrameTimestamp) + + entry.frameRegion(FlickerComponentName.STATUS_BAR) + .coversAtLeast(statusBarRegion) + entry.frameRegion(LAUNCHER_COMPONENT) + .coversAtLeast(displayBounds) + } + + @Test + fun canDetectWindowCoversAtLeastRegion_smallerRegion() { + val entry = assertThat(trace) + .entry(traceFirstFrameTimestamp) + entry.frameRegion(FlickerComponentName.STATUS_BAR) + .coversAtLeast(Region(0, 0, 100, 100)) + entry.frameRegion(LAUNCHER_COMPONENT) + .coversAtLeast(Region(0, 0, 100, 100)) + } + + @Test + fun canDetectWindowCoversAtLeastRegion_largerRegion() { + val subject = assertThat(trace).entry(traceFirstFrameTimestamp) + var failure = assertThrows(FlickerSubjectException::class.java) { + subject.frameRegion(FlickerComponentName.STATUS_BAR) + .coversAtLeast(Region(0, 0, 1441, 171)) + } + assertFailure(failure).factValue("Uncovered region").contains("SkRegion((1440,0,1441,171))") + + failure = assertThrows(FlickerSubjectException::class.java) { + subject.frameRegion(LAUNCHER_COMPONENT) + .coversAtLeast(Region(0, 0, 1440, 2961)) + } + assertFailure(failure).factValue("Uncovered region") + .contains("SkRegion((0,2960,1440,2961))") + } + + @Test + fun canDetectWindowCoversExactlyRegion_exactSize() { + val entry = assertThat(trace) + .entry(traceFirstFrameTimestamp) + + entry.frameRegion(FlickerComponentName.STATUS_BAR) + .coversExactly(statusBarRegion) + entry.frameRegion(LAUNCHER_COMPONENT) + .coversExactly(displayBounds) + } + + @Test + fun canDetectWindowCoversExactlyRegion_smallerRegion() { + val subject = assertThat(trace).entry(traceFirstFrameTimestamp) + var failure = assertThrows(FlickerSubjectException::class.java) { + subject.frameRegion(FlickerComponentName.STATUS_BAR) + .coversAtMost(Region(0, 0, 100, 100)) + } + assertFailure(failure).factValue("Out-of-bounds region") + .contains("SkRegion((100,0,1440,100)(0,100,1440,171))") + + failure = assertThrows(FlickerSubjectException::class.java) { + subject.frameRegion(LAUNCHER_COMPONENT) + .coversAtMost(Region(0, 0, 100, 100)) + } + assertFailure(failure).factValue("Out-of-bounds region") + .contains("SkRegion((100,0,1440,100)(0,100,1440,2960))") + } + + @Test + fun canDetectWindowCoversExactlyRegion_largerRegion() { + val subject = assertThat(trace).entry(traceFirstFrameTimestamp) + var failure = assertThrows(FlickerSubjectException::class.java) { + subject.frameRegion(FlickerComponentName.STATUS_BAR) + .coversAtLeast(Region(0, 0, 1441, 171)) + } + assertFailure(failure).factValue("Uncovered region").contains("SkRegion((1440,0,1441,171))") + + failure = assertThrows(FlickerSubjectException::class.java) { + subject.frameRegion(LAUNCHER_COMPONENT) + .coversAtLeast(Region(0, 0, 1440, 2961)) + } + assertFailure(failure).factValue("Uncovered region") + .contains("SkRegion((0,2960,1440,2961))") + } + + @Test + fun canDetectWindowCoversAtMostRegion_extactSize() { + val entry = assertThat(trace) + .entry(traceFirstFrameTimestamp) + entry.frameRegion(FlickerComponentName.STATUS_BAR) + .coversAtMost(statusBarRegion) + entry.frameRegion(LAUNCHER_COMPONENT) + .coversAtMost(displayBounds) + } + + @Test + fun canDetectWindowCoversAtMostRegion_smallerRegion() { + val subject = assertThat(trace).entry(traceFirstFrameTimestamp) + var failure = assertThrows(FlickerSubjectException::class.java) { + subject.frameRegion(FlickerComponentName.STATUS_BAR) + .coversAtMost(Region(0, 0, 100, 100)) + } + assertFailure(failure).factValue("Out-of-bounds region") + .contains("SkRegion((100,0,1440,100)(0,100,1440,171))") + + failure = assertThrows(FlickerSubjectException::class.java) { + subject.frameRegion(LAUNCHER_COMPONENT) + .coversAtMost(Region(0, 0, 100, 100)) + } + assertFailure(failure).factValue("Out-of-bounds region") + .contains("SkRegion((100,0,1440,100)(0,100,1440,2960))") + } + + @Test + fun canDetectWindowCoversAtMostRegion_largerRegion() { + val entry = assertThat(trace) + .entry(traceFirstFrameTimestamp) + + entry.frameRegion(FlickerComponentName.STATUS_BAR) + .coversAtMost(Region(0, 0, 1441, 171)) + entry.frameRegion(LAUNCHER_COMPONENT) + .coversAtMost(Region(0, 0, 1440, 2961)) + } + + @Test + fun canDetectBelowAppWindowVisibility() { + assertThat(trace) + .entry(traceFirstFrameTimestamp) + .containsNonAppWindow(WALLPAPER_COMPONENT) + } + + @Test + fun canDetectAppWindowVisibility() { + assertThat(trace) + .entry(traceFirstFrameTimestamp) + .containsAppWindow(LAUNCHER_COMPONENT) + + assertThat(trace) + .entry(traceFirstChromeFlashScreenTimestamp) + .containsAppWindow(CHROME_SPLASH_SCREEN_COMPONENT) + } + + @Test + fun canDetectAppWindowVisibilitySubject() { + val trace = readWmTraceFromFile("wm_trace_launcher_visible_background.pb") + val firstEntry = assertThat(trace).first() + val appWindowNames = firstEntry.wmState.appWindows.map { it.name } + firstEntry.verify("has1AppWindow").that(appWindowNames).hasSize(3) + firstEntry.verify("has1AppWindow").that(appWindowNames) + .contains("com.android.server.wm.flicker.testapp/" + + "com.android.server.wm.flicker.testapp.SimpleActivity") + } + + @Test + fun canDetectLauncherVisibility() { + val trace = readWmTraceFromFile("wm_trace_launcher_visible_background.pb") + val subject = assertThat(trace) + val firstTrace = subject.first() + firstTrace.isAppWindowInvisible(LAUNCHER_COMPONENT) + + // launcher is at the same time visible an invisible because it + // contains 2 windows with the exact same name + val lastTrace = subject.last() + lastTrace.isAppWindowInvisible(LAUNCHER_COMPONENT) + + subject.isAppWindowNotOnTop(LAUNCHER_COMPONENT) + .isAppWindowInvisible(LAUNCHER_COMPONENT) + .then() + .isAppWindowOnTop(LAUNCHER_COMPONENT) + .forAllEntries() + + subject.isAppWindowInvisible(LAUNCHER_COMPONENT) + .forAllEntries() + } + + @Test + fun canFailWithReasonForVisibilityChecks_windowNotFound() { + val failure = assertThrows(FlickerSubjectException::class.java) { + assertThat(trace) + .entry(traceFirstFrameTimestamp) + .containsNonAppWindow(IMAGINARY_COMPONENT) + } + assertFailure(failure).hasMessageThat() + .contains(IMAGINARY_COMPONENT.packageName) + } + + @Test + fun canFailWithReasonForVisibilityChecks_windowNotVisible() { + val failure = assertThrows(FlickerSubjectException::class.java) { + assertThat(trace) + .entry(traceFirstFrameTimestamp) + .containsNonAppWindow(FlickerComponentName.IME) + .isNonAppWindowVisible(FlickerComponentName.IME) + } + assertFailure(failure).factValue("Is Invisible") + .contains(FlickerComponentName.IME.packageName) + } + + @Test + fun canDetectAppZOrder() { + assertThat(trace) + .entry(traceFirstChromeFlashScreenTimestamp) + .containsAppWindow(LAUNCHER_COMPONENT) + .isAppWindowVisible(LAUNCHER_COMPONENT) + .isAboveWindow(CHROME_SPLASH_SCREEN_COMPONENT, LAUNCHER_COMPONENT) + .isAppWindowOnTop(LAUNCHER_COMPONENT) + } + + @Test + fun canFailWithReasonForZOrderChecks_windowNotOnTop() { + val failure = assertThrows(FlickerSubjectException::class.java) { + assertThat(trace) + .entry(traceFirstChromeFlashScreenTimestamp) + .isAppWindowOnTop(CHROME_SPLASH_SCREEN_COMPONENT) + } + assertFailure(failure) + .factValue("Found") + .contains(LAUNCHER_COMPONENT.packageName) + } + + @Test + fun canDetectActivityVisibility() { + val trace = readWmTraceFromFile("wm_trace_split_screen.pb") + val lastEntry = assertThat(trace).last() + lastEntry.isAppWindowVisible(SHELL_SPLIT_SCREEN_PRIMARY_COMPONENT) + lastEntry.isAppWindowVisible(SHELL_SPLIT_SCREEN_SECONDARY_COMPONENT) + } +}
\ No newline at end of file diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerTraceSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerTraceSubjectTest.kt new file mode 100644 index 000000000..60632fa61 --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerTraceSubjectTest.kt @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.windowmanager + +import com.android.server.wm.flicker.CHROME_COMPONENT +import com.android.server.wm.flicker.CHROME_SPLASH_SCREEN_COMPONENT +import com.android.server.wm.flicker.IMAGINARY_COMPONENT +import com.android.server.wm.flicker.IME_ACTIVITY_COMPONENT +import com.android.server.wm.flicker.LAUNCHER_COMPONENT +import com.android.server.wm.flicker.SCREEN_DECOR_COMPONENT +import com.android.server.wm.flicker.WALLPAPER_COMPONENT +import com.android.server.wm.flicker.assertFailure +import com.android.server.wm.flicker.assertThrows +import com.android.server.wm.flicker.readWmTraceFromFile +import com.android.server.wm.flicker.traces.FlickerSubjectException +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject.Companion.assertThat +import com.android.server.wm.traces.common.FlickerComponentName +import com.google.common.truth.Truth +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runners.MethodSorters + +/** + * Contains [WindowManagerTraceSubject] tests. To run this test: `atest + * FlickerLibTest:WindowManagerTraceSubjectTest` + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class WindowManagerTraceSubjectTest { + private val chromeTrace by lazy { readWmTraceFromFile("wm_trace_openchrome.pb") } + private val imeTrace by lazy { readWmTraceFromFile("wm_trace_ime.pb") } + + @Test + fun testVisibleAppWindowForRange() { + assertThat(chromeTrace) + .isAppWindowOnTop(LAUNCHER_COMPONENT) + .isAboveAppWindowVisible(SCREEN_DECOR_COMPONENT) + .forRange(9213763541297L, 9215536878453L) + + assertThat(chromeTrace) + .isAppWindowOnTop(LAUNCHER_COMPONENT) + .isAppWindowInvisible(CHROME_SPLASH_SCREEN_COMPONENT) + .isAboveAppWindowVisible(SCREEN_DECOR_COMPONENT) + .then() + .isAppWindowOnTop(CHROME_SPLASH_SCREEN_COMPONENT) + .isAppWindowInvisible(LAUNCHER_COMPONENT) + .isAboveAppWindowVisible(SCREEN_DECOR_COMPONENT) + .then() + .isAppWindowOnTop(CHROME_COMPONENT) + .isAppWindowInvisible(LAUNCHER_COMPONENT) + .isAboveAppWindowVisible(SCREEN_DECOR_COMPONENT) + .forRange(9215551505798L, 9216093628925L) + } + + @Test + fun testCanTransitionInAppWindow() { + assertThat(chromeTrace) + .isAppWindowOnTop(LAUNCHER_COMPONENT) + .isAboveAppWindowVisible(SCREEN_DECOR_COMPONENT) + .then() + .isAppWindowOnTop(CHROME_SPLASH_SCREEN_COMPONENT) + .isAboveAppWindowVisible(SCREEN_DECOR_COMPONENT) + .then() + .isAppWindowOnTop(CHROME_COMPONENT) + .isAboveAppWindowVisible(SCREEN_DECOR_COMPONENT) + .forAllEntries() + } + + @Test + fun testCanDetectTransitionWithOptionalValue() { + val trace = readWmTraceFromFile("wm_trace_open_from_overview.pb") + val subject = assertThat(trace) + subject.isAppWindowOnTop(LAUNCHER_COMPONENT) + .then() + .isAppWindowOnTop(FlickerComponentName.SNAPSHOT) + .then() + .isAppWindowOnTop(CHROME_COMPONENT) + } + + @Test + fun testCanTransitionInAppWindow_withOptional() { + assertThat(chromeTrace) + .isAppWindowOnTop(LAUNCHER_COMPONENT) + .isAboveAppWindowVisible(SCREEN_DECOR_COMPONENT) + .then() + .isAppWindowOnTop(CHROME_SPLASH_SCREEN_COMPONENT) + .isAboveAppWindowVisible(SCREEN_DECOR_COMPONENT) + .then() + .isAppWindowOnTop(CHROME_COMPONENT) + .isAboveAppWindowVisible(SCREEN_DECOR_COMPONENT) + .forAllEntries() + } + + @Test + fun testCanInspectBeginning() { + assertThat(chromeTrace) + .first() + .isAppWindowOnTop(LAUNCHER_COMPONENT) + .containsAboveAppWindow(SCREEN_DECOR_COMPONENT) + } + + @Test + fun testCanInspectAppWindowOnTop() { + assertThat(chromeTrace) + .first() + .isAppWindowOnTop(LAUNCHER_COMPONENT) + + val failure = assertThrows(FlickerSubjectException::class.java) { + assertThat(chromeTrace) + .first() + .isAppWindowOnTop(IMAGINARY_COMPONENT) + .fail("Could not detect the top app window") + } + assertFailure(failure).hasMessageThat().contains("ImaginaryWindow") + } + + @Test + fun testCanInspectEnd() { + assertThat(chromeTrace) + .last() + .isAppWindowOnTop(CHROME_COMPONENT) + .containsAboveAppWindow(SCREEN_DECOR_COMPONENT) + } + + @Test + fun testCanTransitionNonAppWindow() { + assertThat(imeTrace) + .skipUntilFirstAssertion() + .isNonAppWindowInvisible(FlickerComponentName.IME) + .then() + .isNonAppWindowVisible(FlickerComponentName.IME) + .forAllEntries() + } + + @Test(expected = AssertionError::class) + fun testCanDetectOverlappingWindows() { + assertThat(imeTrace) + .noWindowsOverlap(FlickerComponentName.IME, FlickerComponentName.NAV_BAR, + IME_ACTIVITY_COMPONENT) + .forAllEntries() + } + + @Test + fun testCanTransitionAboveAppWindow() { + assertThat(imeTrace) + .skipUntilFirstAssertion() + .isAboveAppWindowInvisible(FlickerComponentName.IME) + .then() + .isAboveAppWindowVisible(FlickerComponentName.IME) + .forAllEntries() + } + + @Test + fun testCanTransitionBelowAppWindow() { + val trace = readWmTraceFromFile("wm_trace_open_app_cold.pb") + assertThat(trace) + .skipUntilFirstAssertion() + .isBelowAppWindowVisible(WALLPAPER_COMPONENT) + .then() + .isBelowAppWindowInvisible(WALLPAPER_COMPONENT) + .forAllEntries() + } + + @Test + fun testCanDetectVisibleWindowsMoreThanOneConsecutiveEntry() { + val trace = readWmTraceFromFile("wm_trace_valid_visible_windows.pb") + assertThat(trace).visibleWindowsShownMoreThanOneConsecutiveEntry().forAllEntries() + } + + @Test + fun testCanAssertWindowStateSequence() { + val windowStates = assertThat(chromeTrace).windowStates( + "com.android.chrome/org.chromium.chrome.browser.firstrun.FirstRunActivity") + val visibilityChange = windowStates.zipWithNext { current, next -> + current.windowState?.isVisible != next.windowState?.isVisible + } + + Truth.assertWithMessage("Visibility should have changed only 1x in the trace") + .that(visibilityChange.count { it }) + .isEqualTo(1) + } + + @Test + fun exceptionContainsDebugInfo() { + val error = assertThrows(AssertionError::class.java) { + assertThat(chromeTrace).isEmpty() + } + Truth.assertThat(error).hasMessageThat().contains("Trace start") + Truth.assertThat(error).hasMessageThat().contains("Trace start") + Truth.assertThat(error).hasMessageThat().contains("Trace file") + } +} diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerTraceTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerTraceTest.kt index 8383938c5..df9d8821f 100644 --- a/libraries/flicker/test/src/com/android/server/wm/flicker/WindowManagerTraceTest.kt +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowManagerTraceTest.kt @@ -14,8 +14,10 @@ * limitations under the License. */ -package com.android.server.wm.flicker +package com.android.server.wm.flicker.windowmanager +import com.android.server.wm.flicker.readTestFile +import com.android.server.wm.flicker.readWmTraceFromFile import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace import com.android.server.wm.traces.common.windowmanager.WindowManagerState import com.android.server.wm.traces.common.windowmanager.windows.WindowContainer @@ -42,7 +44,7 @@ class WindowManagerTraceTest { val firstEntry = trace.entries[0] assertThat(firstEntry.timestamp).isEqualTo(9213763541297L) assertThat(firstEntry.windowStates.size).isEqualTo(10) - assertThat(firstEntry.visibleWindows.size).isEqualTo(6) + assertThat(firstEntry.visibleWindows.size).isEqualTo(5) assertThat(trace.entries[trace.entries.size - 1].timestamp) .isEqualTo(9216093628925L) } @@ -61,7 +63,7 @@ class WindowManagerTraceTest { } catch (e: Exception) { throw RuntimeException(e) } - assertWithMessage("Unable to parse dump").that(trace.entries).hasSize(1) + assertWithMessage("Unable to parse dump").that(trace).hasSize(1) } /** @@ -137,4 +139,21 @@ class WindowManagerTraceTest { assertThat(entry.getIsIncompleteReason()) .contains("No resumed activities found") } + + @Test + fun canFilter() { + val splitWmTrace = trace.filter(9215895891561, 9216093628925) + + assertThat(splitWmTrace).isNotEmpty() + + assertThat(splitWmTrace.entries.first().timestamp).isEqualTo(9215895891561) + assertThat(splitWmTrace.entries.last().timestamp).isEqualTo(9216093628925) + } + + @Test + fun canFilter_wrongTimestamps() { + val splitWmTrace = trace.filter(71607477186189, 71607812120180) + + assertThat(splitWmTrace).isEmpty() + } } diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowStateSubjectTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowStateSubjectTest.kt new file mode 100644 index 000000000..b7ab53515 --- /dev/null +++ b/libraries/flicker/test/src/com/android/server/wm/flicker/windowmanager/WindowStateSubjectTest.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wm.flicker.windowmanager + +import com.android.server.wm.flicker.IMAGINARY_COMPONENT +import com.android.server.wm.flicker.assertThrows +import com.android.server.wm.flicker.readWmTraceFromFile +import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject +import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace +import com.google.common.truth.Truth +import org.junit.Test + +class WindowStateSubjectTest { + private val trace: WindowManagerTrace by lazy { readWmTraceFromFile("wm_trace_openchrome.pb") } + + @Test + fun exceptionContainsDebugInfoImaginary() { + val error = assertThrows(AssertionError::class.java) { + WindowManagerTraceSubject.assertThat(trace) + .first() + .windowState(IMAGINARY_COMPONENT.className) + .exists() + } + Truth.assertThat(error).hasMessageThat().contains(IMAGINARY_COMPONENT.className) + Truth.assertThat(error).hasMessageThat().contains("What?") + Truth.assertThat(error).hasMessageThat().contains("Where?") + Truth.assertThat(error).hasMessageThat().contains("Entry") + Truth.assertThat(error).hasMessageThat().contains("Trace start") + Truth.assertThat(error).hasMessageThat().contains("Trace start") + Truth.assertThat(error).hasMessageThat().contains("Trace file") + Truth.assertThat(error).hasMessageThat().contains("Window title") + } + + @Test + fun exceptionContainsDebugInfoConcrete() { + val error = assertThrows(AssertionError::class.java) { + WindowManagerTraceSubject.assertThat(trace) + .first() + .subjects + .first() + .doesNotExist() + } + Truth.assertThat(error).hasMessageThat().contains("What?") + Truth.assertThat(error).hasMessageThat().contains("Where?") + Truth.assertThat(error).hasMessageThat().contains("Entry") + Truth.assertThat(error).hasMessageThat().contains("Trace start") + Truth.assertThat(error).hasMessageThat().contains("Trace start") + Truth.assertThat(error).hasMessageThat().contains("Trace file") + Truth.assertThat(error).hasMessageThat().contains("Entry") + } +}
\ No newline at end of file diff --git a/libraries/health/rules/src/android/platform/test/rule/ClassMetricRule.java b/libraries/health/rules/src/android/platform/test/rule/ClassMetricRule.java new file mode 100644 index 000000000..2a2ff5082 --- /dev/null +++ b/libraries/health/rules/src/android/platform/test/rule/ClassMetricRule.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.platform.test.rule; + +import android.app.Instrumentation; +import android.device.collectors.BaseMetricListener; +import android.os.Bundle; +import androidx.annotation.VisibleForTesting; +import androidx.test.InstrumentationRegistry; + +/** + * A rule that collects class-level metrics using a supplied list of metric collectors. + * + * <p>The metric collectors are passed in using the "class-metric-collectors" option, and the rule + * works by invoking the correct test-level callbacks on them at the corresponding stages of the + * test lifecycle. The metric collectors must be subclasses of {@link BaseMetricListener}, and can + * be passed in by their fully qualified class name, or simple class name if they are under the + * {@code android.device.collectors} package (but not subpackages). + * + * <p>Multiple metric collectors are supported as comma-separated values, The order they are + * triggered follows this example: for {@code -e class-metric-collectors Collector1,Collector2}, the + * evaluation order would be {@code Collector1#testStarted()}, {@code Collector2#testStarted()}, the + * entire test class, {@code Collector1#testFinished()}, {@code Collector1#testFinished()}. + * + * <p>For {@code Microbenchmark}s, this rule can be dynamically injected either inside or outside + * hardcoded rules (see {@code Microbenchmark})'s JavaDoc). + * + * <p>Note that metrics collected from this rule are reported as run metrics. Therefore, there is + * the risk of metric key collision if a run contains multiple classes that report metrics under the + * same key. At the moment, it's the responsibility of the metric collector to prevent collision + * across test classes. + * + * <p>Exceptions from metric listeners are silently logged. This behavior is in accordance with the + * approach taken by {@link BaseMetricListener}. + */ +public class ClassMetricRule extends TestMetricRule { + @VisibleForTesting static final String METRIC_COLLECTORS_OPTION = "class-metric-collectors"; + + public ClassMetricRule() { + this(InstrumentationRegistry.getArguments(), InstrumentationRegistry.getInstrumentation()); + } + + @VisibleForTesting + ClassMetricRule(Bundle args, Instrumentation instrumentation) { + super( + args, + instrumentation, + METRIC_COLLECTORS_OPTION, + ClassMetricRule.class.getSimpleName()); + for (BaseMetricListener listener : mMetricListeners) { + listener.setReportAsInstrumentationResults(true); + } + } +} diff --git a/libraries/health/rules/src/android/platform/test/rule/CoolDownRule.java b/libraries/health/rules/src/android/platform/test/rule/CoolDownRule.java index fe52efb17..78c0993de 100644 --- a/libraries/health/rules/src/android/platform/test/rule/CoolDownRule.java +++ b/libraries/health/rules/src/android/platform/test/rule/CoolDownRule.java @@ -74,7 +74,8 @@ public class CoolDownRule extends TestWatcher { CoolDownRule.unescapeOptionStr( getArguments().getString(DEVICE_TEMPERATURE_NAME_OPTION, "")); if (mDeviceTemperatureName.isEmpty()) { - throw new IllegalArgumentException("Missed device temperature name."); + Log.w(LOG_TAG, "Missed device temperature name. Skipped waiting for DUT cooling down."); + return; } mPollIntervalSecs = Long.valueOf(getArguments().getString(POLL_INTERVAL_OPTION, "30")); mMaxWaitSecs = Long.valueOf(getArguments().getString(MAX_WAIT_OPTION, "1200")); diff --git a/libraries/health/rules/src/android/platform/test/rule/FailureWatcher.java b/libraries/health/rules/src/android/platform/test/rule/FailureWatcher.java new file mode 100644 index 000000000..3b4681518 --- /dev/null +++ b/libraries/health/rules/src/android/platform/test/rule/FailureWatcher.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.platform.test.rule; + +import static androidx.test.InstrumentationRegistry.getInstrumentation; + +import android.os.FileUtils; +import android.os.ParcelFileDescriptor.AutoCloseInputStream; +import android.util.Log; + +import androidx.test.InstrumentationRegistry; +import androidx.test.uiautomator.UiDevice; + +import org.junit.runner.Description; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** A rule that generates debug artifact files for failed tests. */ +public class FailureWatcher extends TestWatcher { + private static final String TAG = "FailureWatcher"; + private final UiDevice mDevice; + + public FailureWatcher() { + mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); + } + + @Override + protected void failed(Throwable e, Description description) { + onError(mDevice, description, e); + } + + public static File diagFile(Description description, String prefix, String ext) { + return new File( + getInstrumentation().getTargetContext().getFilesDir(), + prefix + + "-" + + description.getTestClass().getSimpleName() + + "." + + description.getMethodName() + + "." + + ext); + } + + public static void onError(UiDevice device, Description description, Throwable e) { + if (device == null) return; + final File sceenshot = diagFile(description, "TestScreenshot", "png"); + final File hierarchy = diagFile(description, "Hierarchy", "zip"); + + // Dump window hierarchy + try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(hierarchy))) { + out.putNextEntry(new ZipEntry("bugreport.txt")); + dumpStringCommand("dumpsys window windows", out); + dumpStringCommand("dumpsys package", out); + out.closeEntry(); + + out.putNextEntry(new ZipEntry("visible_windows.zip")); + dumpCommand("cmd window dump-visible-window-views", out); + out.closeEntry(); + } catch (IOException ex) { + } + + Log.e( + TAG, + "Failed test " + + description.getMethodName() + + ",\nscreenshot will be saved to " + + sceenshot + + ",\nUI dump at: " + + hierarchy + + " (use go/web-hv to open the dump file)", + e); + device.takeScreenshot(sceenshot); + + // Dump accessibility hierarchy + try { + device.dumpWindowHierarchy(diagFile(description, "AccessibilityHierarchy", "uix")); + } catch (IOException ex) { + Log.e(TAG, "Failed to save accessibility hierarchy", ex); + } + } + + private static void dumpStringCommand(String cmd, OutputStream out) throws IOException { + out.write(("\n\n" + cmd + "\n").getBytes()); + dumpCommand(cmd, out); + } + + private static void dumpCommand(String cmd, OutputStream out) throws IOException { + try (AutoCloseInputStream in = + new AutoCloseInputStream( + getInstrumentation().getUiAutomation().executeShellCommand(cmd))) { + FileUtils.copy(in, out); + } + } +} diff --git a/libraries/health/rules/src/android/platform/test/rule/LandscapeOrientationRule.java b/libraries/health/rules/src/android/platform/test/rule/LandscapeOrientationRule.java index d7bad3435..11fbca466 100644 --- a/libraries/health/rules/src/android/platform/test/rule/LandscapeOrientationRule.java +++ b/libraries/health/rules/src/android/platform/test/rule/LandscapeOrientationRule.java @@ -20,6 +20,7 @@ import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; import static org.junit.Assert.assertEquals; import android.os.RemoteException; +import android.os.SystemClock; import org.junit.runner.Description; @@ -36,11 +37,18 @@ public class LandscapeOrientationRule extends TestWatcher { int currentOrientation = getContext().getResources().getConfiguration().orientation; if (currentOrientation != ORIENTATION_LANDSCAPE) { // ORIENTATION_PORTRAIT getUiDevice().setOrientationLeft(); - int rotatedOrientation = getContext().getResources().getConfiguration().orientation; - assertEquals( - "Orientation should be landscape", - ORIENTATION_LANDSCAPE, - rotatedOrientation); + for (int i = 0; i != 100; ++i) { + int rotatedOrientation = + getContext().getResources().getConfiguration().orientation; + if (rotatedOrientation == ORIENTATION_LANDSCAPE) break; + if (i == 99) { + assertEquals( + "Orientation should be landscape", + ORIENTATION_LANDSCAPE, + rotatedOrientation); + } + SystemClock.sleep(100); + } } } catch (RemoteException e) { String message = "RemoteException when forcing landscape rotation on the device"; diff --git a/libraries/health/rules/src/android/platform/test/rule/MapsPipRule.java b/libraries/health/rules/src/android/platform/test/rule/MapsPipRule.java new file mode 100644 index 000000000..703afa9d0 --- /dev/null +++ b/libraries/health/rules/src/android/platform/test/rule/MapsPipRule.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.platform.test.rule; + +import android.os.SystemClock; +import android.platform.helpers.HelperAccessor; +import android.platform.helpers.IMapsHelper; + +import androidx.annotation.VisibleForTesting; + +import org.junit.runner.Description; + +/** This rule allows to execute CUJ while Maps in pip state. */ +public class MapsPipRule extends TestWatcher { + + @VisibleForTesting static final String MAPS_SEARCH_ADDRESS = "maps-close-to-pip-address"; + String mapsAddressOption = "Golden Gate Bridge"; + + @VisibleForTesting static final String MAPS_TIMEOUT = "maps-timeout"; + long mapsTimeout = 2000; + + private static HelperAccessor<IMapsHelper> sMapsHelper = + new HelperAccessor<>(IMapsHelper.class); + + @Override + protected void starting(Description description) { + mapsAddressOption = getArguments().getString(MAPS_SEARCH_ADDRESS, "Golden Gate Bridge"); + mapsTimeout = Long.valueOf(getArguments().getString(MAPS_TIMEOUT, "2000")); + + sMapsHelper.get().open(); + sMapsHelper.get().doSearch(mapsAddressOption); + sMapsHelper.get().getDirections(); + sMapsHelper.get().startNavigation(); + sMapsHelper.get().goToNavigatePip(); + SystemClock.sleep(mapsTimeout); + } + + @Override + protected void finished(Description description) { + executeShellCommand(String.format("am force-stop %s", sMapsHelper.get().getPackage())); + } +} diff --git a/libraries/health/rules/src/android/platform/test/rule/PhotoUploadRule.java b/libraries/health/rules/src/android/platform/test/rule/PhotoUploadRule.java new file mode 100644 index 000000000..66884f58b --- /dev/null +++ b/libraries/health/rules/src/android/platform/test/rule/PhotoUploadRule.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.platform.test.rule; + +import android.os.SystemClock; +import android.platform.helpers.HelperAccessor; +import android.platform.helpers.IGoogleCameraHelper2; +import android.platform.helpers.IPhotosHelper; + +import androidx.annotation.VisibleForTesting; + +import org.junit.runner.Description; + +/** This rule allows to execute CUJ while new picures uploading in cloud. */ +public class PhotoUploadRule extends TestWatcher { + + @VisibleForTesting static final String PHOTO_COUNT = "photo-count"; + int photoCount = 5; + + @VisibleForTesting static final String TAKE_PHOTO_DELAY = "take-photo-delay"; + long takePhotoDelay = 1000; + + @VisibleForTesting static final String PHOTO_TIMEOUT = "photo-timeout"; + long photoTimeout = 10000; + + @VisibleForTesting static final String UPLOAD_PHOTO = "upload-photo"; + boolean uploadPhoto = true; + + @VisibleForTesting static final String UPLOAD_VIDEO = "upload-video"; + boolean uploadVideo = false; + + @VisibleForTesting static final String CAPTURE_VIDEO_DURATION = "capture-video-duration"; + long captureVideoDuration = 1200000; + + private static HelperAccessor<IPhotosHelper> sPhotosHelper = + new HelperAccessor<>(IPhotosHelper.class); + + private static HelperAccessor<IGoogleCameraHelper2> sGoogleCameraHelper = + new HelperAccessor<>(IGoogleCameraHelper2.class); + + @Override + protected void starting(Description description) { + photoCount = Integer.valueOf(getArguments().getString(PHOTO_COUNT, String.valueOf(5))); + photoTimeout = Long.valueOf(getArguments().getString(PHOTO_TIMEOUT, String.valueOf(10000))); + takePhotoDelay = + Long.valueOf(getArguments().getString(TAKE_PHOTO_DELAY, String.valueOf(1000))); + captureVideoDuration = + Long.valueOf( + getArguments().getString(CAPTURE_VIDEO_DURATION, String.valueOf(30000))); + uploadPhoto = Boolean.valueOf(getArguments().getString(UPLOAD_PHOTO, String.valueOf(true))); + uploadVideo = + Boolean.valueOf(getArguments().getString(UPLOAD_VIDEO, String.valueOf(false))); + + sPhotosHelper.get().open(); + sPhotosHelper.get().disableBackupMode(); + sGoogleCameraHelper.get().open(); + if (uploadPhoto) { + sGoogleCameraHelper.get().takeMultiplePhotos(photoCount, takePhotoDelay); + SystemClock.sleep(photoTimeout); + } + if (uploadVideo) { + sGoogleCameraHelper.get().clickVideoTab(); + sGoogleCameraHelper.get().clickCameraVideoButton(); + SystemClock.sleep(captureVideoDuration); + sGoogleCameraHelper.get().clickCameraVideoButton(); + } + sPhotosHelper.get().open(); + sPhotosHelper.get().enableBackupMode(); + sPhotosHelper.get().verifyContentStartedUploading(); + sPhotosHelper.get().exit(); + } + + @Override + protected void finished(Description description) { + sPhotosHelper.get().open(); + sPhotosHelper.get().removeContent(); + sPhotosHelper.get().disableBackupMode(); + sPhotosHelper.get().exit(); + } +} diff --git a/libraries/health/rules/src/android/platform/test/rule/ScreenRecordRule.java b/libraries/health/rules/src/android/platform/test/rule/ScreenRecordRule.java new file mode 100644 index 000000000..fb9d052dd --- /dev/null +++ b/libraries/health/rules/src/android/platform/test/rule/ScreenRecordRule.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.platform.test.rule; + +import static androidx.test.InstrumentationRegistry.getInstrumentation; + +import android.app.Instrumentation; +import android.app.UiAutomation; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import androidx.test.uiautomator.UiDevice; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.io.File; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Rule which captures a screen record for a test. After adding this rule to the test class, apply + * the annotation @ScreenRecord to individual tests + */ +public class ScreenRecordRule implements TestRule { + + private static final String TAG = "ScreenRecordRule"; + + @Override + public Statement apply(Statement base, Description description) { + if (description.getAnnotation(ScreenRecord.class) == null) { + return base; + } + + return new Statement() { + @Override + public void evaluate() throws Throwable { + Instrumentation inst = getInstrumentation(); + UiAutomation automation = inst.getUiAutomation(); + UiDevice device = UiDevice.getInstance(inst); + + File outputFile = FailureWatcher.diagFile(description, "ScreenRecord", "mp4"); + device.executeShellCommand("killall screenrecord"); + ParcelFileDescriptor output = + automation.executeShellCommand("screenrecord " + outputFile); + String screenRecordPid = device.executeShellCommand("pidof screenrecord"); + try { + base.evaluate(); + } finally { + device.executeShellCommand("kill -INT " + screenRecordPid); + Log.e(TAG, "Screenrecord captured at: " + outputFile); + output.close(); + } + // Delete the file if the test was successful. + automation.executeShellCommand("rm " + outputFile); + } + }; + } + + /** Interface to indicate that the test should capture screenrecord */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface ScreenRecord {} +} diff --git a/libraries/health/rules/src/android/platform/test/rule/TestMetricRule.java b/libraries/health/rules/src/android/platform/test/rule/TestMetricRule.java index 398972833..2098a4f75 100644 --- a/libraries/health/rules/src/android/platform/test/rule/TestMetricRule.java +++ b/libraries/health/rules/src/android/platform/test/rule/TestMetricRule.java @@ -15,6 +15,7 @@ */ package android.platform.test.rule; +import android.app.Instrumentation; import android.device.collectors.BaseMetricListener; import android.os.Bundle; import android.util.Log; @@ -50,12 +51,12 @@ import java.util.List; * approach taken by {@link BaseMetricListener}. */ public class TestMetricRule extends TestWatcher { - private static final String LOG_TAG = TestMetricRule.class.getSimpleName(); - @VisibleForTesting static final String METRIC_COLLECTORS_OPTION = "test-metric-collectors"; @VisibleForTesting static final String METRIC_COLLECTORS_PACKAGE = "android.device.collectors"; - private List<BaseMetricListener> mMetricListeners = new ArrayList<>(); + protected List<BaseMetricListener> mMetricListeners = new ArrayList<>(); + + private final String mLogTag; public TestMetricRule() { this(InstrumentationRegistry.getArguments()); @@ -63,8 +64,25 @@ public class TestMetricRule extends TestWatcher { @VisibleForTesting TestMetricRule(Bundle args) { + this( + args, + InstrumentationRegistry.getInstrumentation(), + METRIC_COLLECTORS_OPTION, + TestMetricRule.class.getSimpleName()); + } + + /** + * A constructor that allows subclasses to change out various components used at initialization + * time. + */ + protected TestMetricRule( + Bundle args, + Instrumentation instrumentation, + String collectorsOptionName, + String logTag) { + mLogTag = logTag; List<String> listenerNames = - Arrays.asList(args.getString(METRIC_COLLECTORS_OPTION, "").split(",")); + Arrays.asList(args.getString(collectorsOptionName, "").split(",")); for (String listenerName : listenerNames) { if (listenerName.isEmpty()) { continue; @@ -73,7 +91,7 @@ public class TestMetricRule extends TestWatcher { // We could use a regex here, but this is simpler and should work just as well. if (listenerName.contains(".")) { Log.i( - LOG_TAG, + mLogTag, String.format( "Attempting to dynamically load metric collector with fully " + "qualified name %s.", @@ -91,7 +109,7 @@ public class TestMetricRule extends TestWatcher { } else { String fullName = String.format("%s.%s", METRIC_COLLECTORS_PACKAGE, listenerName); Log.i( - LOG_TAG, + mLogTag, String.format( "Attempting to dynamically load metric collector with simple class " + "name %s (fully qualified name: %s).", @@ -111,19 +129,21 @@ public class TestMetricRule extends TestWatcher { } // Initialize each listener. for (BaseMetricListener listener : mMetricListeners) { - listener.setInstrumentation(InstrumentationRegistry.getInstrumentation()); - listener.setupAdditionalArgs(); + listener.setInstrumentation(instrumentation); } } @Override protected void starting(Description description) { for (BaseMetricListener listener : mMetricListeners) { + listener.setUp(); + } + for (BaseMetricListener listener : mMetricListeners) { try { listener.testStarted(description); } catch (Exception e) { Log.e( - LOG_TAG, + mLogTag, String.format( "Exception from listener %s during starting().", listener.getClass().getCanonicalName()), @@ -139,13 +159,16 @@ public class TestMetricRule extends TestWatcher { listener.testFinished(description); } catch (Exception e) { Log.e( - LOG_TAG, + mLogTag, String.format( "Exception from listener %s during finished().", listener.getClass().getCanonicalName()), e); } } + for (BaseMetricListener listener : mMetricListeners) { + listener.cleanUp(); + } } @Override @@ -156,7 +179,7 @@ public class TestMetricRule extends TestWatcher { listener.testFailure(failure); } catch (Exception e) { Log.e( - LOG_TAG, + mLogTag, String.format( "Exception from listener %s during failed().", listener.getClass().getCanonicalName()), diff --git a/libraries/health/rules/src/android/platform/test/rule/YouTubePipRule.java b/libraries/health/rules/src/android/platform/test/rule/YouTubePipRule.java new file mode 100644 index 000000000..e87277080 --- /dev/null +++ b/libraries/health/rules/src/android/platform/test/rule/YouTubePipRule.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.platform.test.rule; + +import android.os.SystemClock; +import android.platform.helpers.HelperAccessor; +import android.platform.helpers.IYouTubeHelper; + +import androidx.annotation.VisibleForTesting; + +import org.junit.runner.Description; + +/** This rule allows to execute CUJ while YouTube in pip state. */ +public class YouTubePipRule extends TestWatcher { + + @VisibleForTesting static final String YOUTUBE_PLAYBACK_TIMEOUT = "youtube-playback-time"; + long playbackTimeout = 2000; + + @VisibleForTesting static final String VIDEO_NAME = "video-name"; + String videoName = "test-one-hour-video"; + + private static HelperAccessor<IYouTubeHelper> sYouTubeHelper = + new HelperAccessor<>(IYouTubeHelper.class).withPrefix("YouTubeHelper"); + + @Override + protected void starting(Description description) { + playbackTimeout = Long.valueOf(getArguments().getString(YOUTUBE_PLAYBACK_TIMEOUT, "2000")); + videoName = getArguments().getString(VIDEO_NAME, "test-one-hour-video"); + + sYouTubeHelper.get().open(); + sYouTubeHelper.get().goToYourVideos(); + SystemClock.sleep(playbackTimeout); + sYouTubeHelper.get().playYourVideo(videoName); + SystemClock.sleep(playbackTimeout); + sYouTubeHelper.get().goToYouTubePip(); + SystemClock.sleep(playbackTimeout); + } + + @Override + protected void finished(Description description) { + executeShellCommand(String.format("am force-stop %s", sYouTubeHelper.get().getPackage())); + } +} diff --git a/libraries/health/rules/src/android/platform/test/rule/flicker/WindowManagerFlickerRuleCommon.java b/libraries/health/rules/src/android/platform/test/rule/flicker/WindowManagerFlickerRuleCommon.java index 032e943c4..42a4ffb42 100644 --- a/libraries/health/rules/src/android/platform/test/rule/flicker/WindowManagerFlickerRuleCommon.java +++ b/libraries/health/rules/src/android/platform/test/rule/flicker/WindowManagerFlickerRuleCommon.java @@ -19,10 +19,8 @@ package android.platform.test.rule.flicker; import android.util.Log; import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject; +import com.android.server.wm.traces.common.FlickerComponentName; import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace; -import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper; - -import java.util.LinkedList; /** * Rule used for validating the common window manager trace based flicker assertions applicable @@ -31,19 +29,23 @@ import java.util.LinkedList; public class WindowManagerFlickerRuleCommon extends WindowManagerFlickerRuleBase { private static final String TAG = WindowManagerFlickerRuleCommon.class.getSimpleName(); + private static final FlickerComponentName NAV_BAR_COMPONENT = + new FlickerComponentName("", "NavigationBar0"); + private static final FlickerComponentName STATUS_BAR_COMPONENT = + new FlickerComponentName("", "StatusBar"); protected void validateWMFlickerConditions(WindowManagerTrace wmTrace) { // Verify that there’s an non-app window with names NavigationBar, StatusBar above // the app window and is visible in all winscope log entries. WindowManagerTraceSubject.assertThat(wmTrace) - .showsAboveAppWindow(WindowManagerStateHelper.NAV_BAR_WINDOW_NAME) - .showsAboveAppWindow(WindowManagerStateHelper.STATUS_BAR_WINDOW_NAME) + .isAboveAppWindowVisible(NAV_BAR_COMPONENT) + .isAboveAppWindowVisible(STATUS_BAR_COMPONENT) .forAllEntries(); // Verify that all visible windows are visible for more than one consecutive entry // in the log entries. WindowManagerTraceSubject.assertThat(wmTrace) - .visibleWindowsShownMoreThanOneConsecutiveEntry(new LinkedList<String>()) + .visibleWindowsShownMoreThanOneConsecutiveEntry() .forAllEntries(); Log.v(TAG, "Successfully verified the window manager flicker conditions."); } diff --git a/libraries/health/rules/tests/src/android/platform/test/rule/ClassMetricRuleTest.java b/libraries/health/rules/tests/src/android/platform/test/rule/ClassMetricRuleTest.java new file mode 100644 index 000000000..ec93a37f8 --- /dev/null +++ b/libraries/health/rules/tests/src/android/platform/test/rule/ClassMetricRuleTest.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.platform.test.rule; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.MockitoAnnotations.initMocks; + +import android.app.Instrumentation; +import android.device.collectors.BaseMetricListener; +import android.device.collectors.DataRecord; +import android.os.Bundle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.Description; +import org.junit.runner.Result; +import org.junit.runners.model.Statement; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; + +import java.util.List; + +/** + * Tests for {@link ClassMetricRule}. + * + * <p>This test will focus on testing that collectors are loaded with the correct argument, and that + * they are reporting their results as run metrics. All the other logic has been tested in {@link + * TestMetricRuleTest}. + */ +public class ClassMetricRuleTest { + + private static final Description DESCRIPTION = + Description.createTestDescription("class", "method"); + + private static final Statement TEST_STATEMENT = + new Statement() { + @Override + public void evaluate() {} + }; + + @Mock private Instrumentation mMockInstrumentation; + + @Captor private ArgumentCaptor<Bundle> addResultsCaptor; + + @Before + public void setUp() { + initMocks(this); + } + + @Test + public void testRunsSpecifiedCollectorsAndReportRunMetrics() throws Throwable { + ClassMetricRule rule = + createWithMetricCollectorNames( + "android.platform.test.rule.ClassMetricRuleTest$TestableCollector2", + "android.platform.test.rule.ClassMetricRuleTest$TestableCollector1"); + rule.apply(TEST_STATEMENT, DESCRIPTION).evaluate(); + + // We have two metric collectors, hence results are reported two times. + verify(mMockInstrumentation, times(2)).addResults(addResultsCaptor.capture()); + List<Bundle> results = addResultsCaptor.getAllValues(); + boolean hasCollector1 = false, hasCollector2 = false; + for (Bundle result : results) { + if (result.containsKey("TestableCollector1-test")) { + hasCollector1 = true; + } else if (result.containsKey("TestableCollector2-test")) { + hasCollector2 = true; + } + } + assertTrue(hasCollector1); + assertTrue(hasCollector2); + } + + @Test + public void testUsesTestCallbackRatherThanRunCallback() throws Throwable { + ClassMetricRule rule = + createWithMetricCollectorNames( + "android.platform.test.rule.ClassMetricRuleTest$TestableCollector1"); + rule.apply(TEST_STATEMENT, DESCRIPTION).evaluate(); + + // We have one metric collector, hence results are reported a single time. + verify(mMockInstrumentation, times(1)).addResults(addResultsCaptor.capture()); + Bundle result = addResultsCaptor.getValue(); + assertTrue(result.containsKey("TestableCollector1-test")); + assertFalse(result.containsKey("TestableCollector1-run")); + } + + private ClassMetricRule createWithMetricCollectorNames(String... names) { + Bundle args = new Bundle(); + args.putString(ClassMetricRule.METRIC_COLLECTORS_OPTION, String.join(",", names)); + + return new ClassMetricRule(args, mMockInstrumentation); + } + + public static class BaseTestableCollector extends BaseMetricListener { + private final String mName; + + public BaseTestableCollector(String name) { + mName = name; + } + + @Override + public void onTestEnd(DataRecord testData, Description description) { + testData.addStringMetric(mName + "-test", "value"); + } + + // This method should never be used by the rule. + @Override + public void onTestRunEnd(DataRecord runData, Result result) { + runData.addStringMetric(mName + "-run", "value"); + } + } + + public static class TestableCollector1 extends BaseTestableCollector { + public TestableCollector1() { + super(TestableCollector1.class.getSimpleName()); + } + } + + public static class TestableCollector2 extends BaseTestableCollector { + public TestableCollector2() { + super(TestableCollector2.class.getSimpleName()); + } + } +} diff --git a/libraries/health/rules/tests/src/android/platform/test/rule/CoolDownRuleTest.java b/libraries/health/rules/tests/src/android/platform/test/rule/CoolDownRuleTest.java index 3bb3104b3..d2b37759b 100644 --- a/libraries/health/rules/tests/src/android/platform/test/rule/CoolDownRuleTest.java +++ b/libraries/health/rules/tests/src/android/platform/test/rule/CoolDownRuleTest.java @@ -87,6 +87,16 @@ public class CoolDownRuleTest { .inOrder(); } + /** Tests that this rule will be skipped if not all required parameters are available. */ + @Test + public void testCoolDownFallback() throws Throwable { + boolean screenOn = true; + TestableRule rule = new TestableRule(screenOn, mThermalHelper); + rule.apply(rule.getTestStatement(), Description.createTestDescription("clzz", "mthd")) + .evaluate(); + assertThat(rule.getOperations()).containsExactly(OPS_TEST).inOrder(); + } + /** Tests that this rule will fail to cool down due to timeout as expected steps. */ @Test public void testCoolDownTimeout() throws Throwable { diff --git a/libraries/health/rules/tests/src/android/platform/test/rule/TestMetricRuleTest.java b/libraries/health/rules/tests/src/android/platform/test/rule/TestMetricRuleTest.java index b8ebe1c79..506cc7e68 100644 --- a/libraries/health/rules/tests/src/android/platform/test/rule/TestMetricRuleTest.java +++ b/libraries/health/rules/tests/src/android/platform/test/rule/TestMetricRuleTest.java @@ -80,9 +80,11 @@ public class TestMetricRuleTest { .containsExactly( "TestableCollector1#setInstrumentation", "TestableCollector1#setupAdditionalArgs", + "TestableCollector1#onSetUp", String.format("Test %s: TestableCollector1#onTestStart", DESCRIPTION), "Test execution", - String.format("Test %s: TestableCollector1#onTestEnd", DESCRIPTION)) + String.format("Test %s: TestableCollector1#onTestEnd", DESCRIPTION), + "TestableCollector1#onCleanUp") .inOrder(); } @@ -98,6 +100,7 @@ public class TestMetricRuleTest { .containsExactly( "TestableCollector1#setInstrumentation", "TestableCollector1#setupAdditionalArgs", + "TestableCollector1#onSetUp", String.format("Test %s: TestableCollector1#onTestStart", DESCRIPTION), "Test execution", String.format( @@ -105,7 +108,8 @@ public class TestMetricRuleTest { DESCRIPTION, new Failure( DESCRIPTION, new RuntimeException(TEST_FAILURE_MESSAGE))), - String.format("Test %s: TestableCollector1#onTestEnd", DESCRIPTION)) + String.format("Test %s: TestableCollector1#onTestEnd", DESCRIPTION), + "TestableCollector1#onCleanUp") .inOrder(); } @@ -121,9 +125,11 @@ public class TestMetricRuleTest { assertThat(sLogs) .containsExactly( "TestableCollector1#setInstrumentation", - "TestableCollector1#setupAdditionalArgs", "TestableCollector2#setInstrumentation", + "TestableCollector1#setupAdditionalArgs", + "TestableCollector1#onSetUp", "TestableCollector2#setupAdditionalArgs", + "TestableCollector2#onSetUp", String.format("Test %s: TestableCollector1#onTestStart", DESCRIPTION), String.format("Test %s: TestableCollector2#onTestStart", DESCRIPTION), "Test execution", @@ -134,7 +140,9 @@ public class TestMetricRuleTest { "Test %s: TestableCollector2#onTestFail with failure %s", DESCRIPTION, failure), String.format("Test %s: TestableCollector1#onTestEnd", DESCRIPTION), - String.format("Test %s: TestableCollector2#onTestEnd", DESCRIPTION)) + String.format("Test %s: TestableCollector2#onTestEnd", DESCRIPTION), + "TestableCollector1#onCleanUp", + "TestableCollector2#onCleanUp") .inOrder(); } @@ -163,6 +171,29 @@ public class TestMetricRuleTest { TestMetricRule rule = createWithMetricCollectorNames(simpleName); } + @Test + public void testInitWithDifferentOptionName() throws Throwable { + String optionName = "another-" + TestMetricRule.METRIC_COLLECTORS_OPTION; + + Bundle args = new Bundle(); + args.putString( + optionName, "android.platform.test.rule.TestMetricRuleTest$TestableCollector1"); + TestMetricRule rule = + new TestMetricRule(args, new Instrumentation(), optionName, "log tag"); + + rule.apply(PASSING_STATEMENT, DESCRIPTION).evaluate(); + assertThat(sLogs) + .containsExactly( + "TestableCollector1#setInstrumentation", + "TestableCollector1#setupAdditionalArgs", + "TestableCollector1#onSetUp", + String.format("Test %s: TestableCollector1#onTestStart", DESCRIPTION), + "Test execution", + String.format("Test %s: TestableCollector1#onTestEnd", DESCRIPTION), + "TestableCollector1#onCleanUp") + .inOrder(); + } + private TestMetricRule createWithMetricCollectorNames(String... names) { Bundle args = new Bundle(); args.putString(TestMetricRule.METRIC_COLLECTORS_OPTION, String.join(",", names)); @@ -187,6 +218,16 @@ public class TestMetricRuleTest { } @Override + public void onSetUp() { + sLogs.add(String.format("%s#%s", mName, "onSetUp")); + } + + @Override + public void onCleanUp() { + sLogs.add(String.format("%s#%s", mName, "onCleanUp")); + } + + @Override public void onTestStart(DataRecord testData, Description description) { sLogs.add(String.format("Test %s: %s#%s", description, mName, "onTestStart")); } diff --git a/libraries/health/utils/src/android/platform/test/util/TestFilter.java b/libraries/health/utils/src/android/platform/test/util/TestFilter.java new file mode 100644 index 000000000..8f7c257be --- /dev/null +++ b/libraries/health/utils/src/android/platform/test/util/TestFilter.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.platform.test.util; + +import android.os.Bundle; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + +import org.junit.runner.Description; + +/** Contains a method for filtering specific tests to run. Expects the format class#method. */ +public final class TestFilter { + + private static final String LOG_TAG = TestFilter.class.getSimpleName(); + @VisibleForTesting static final String OPTION_NAME = "filter-tests"; + + private TestFilter() {} + + public static boolean isFilteredOrUnspecified(Bundle arguments, Description description) { + String testFilters = arguments.getString(OPTION_NAME); + // If the argument is unspecified, always return true. + if (testFilters == null) { + return true; + } + + String displayName = description.getDisplayName(); + + // Test the display name against all filter arguments. + for (String testFilter : testFilters.split(",")) { + String[] filterComponents = testFilter.split("#"); + if (filterComponents.length != 2) { + Log.e( + LOG_TAG, + String.format( + "Invalid filter-tests instrumentation argument supplied, %s.", + testFilter)); + continue; + } + + String displayNameFilter = + String.format( + "%s(%s)", + // method + filterComponents[1], + // class + filterComponents[0]); + if (displayNameFilter.equals(displayName)) { + return true; + } + } + // If the argument is specified and no matches, return false. + return false; + } +} diff --git a/libraries/health/utils/tests/src/android/platform/test/util/TestFilterTest.java b/libraries/health/utils/tests/src/android/platform/test/util/TestFilterTest.java new file mode 100644 index 000000000..eb8c42d24 --- /dev/null +++ b/libraries/health/utils/tests/src/android/platform/test/util/TestFilterTest.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.platform.test.util; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.Bundle; + +import org.junit.Test; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class TestFilterTest { + private static final Description DESCRIPTION1 = + Description.createTestDescription("testClassA", "method1"); + private static final Description DESCRIPTION2 = + Description.createTestDescription("testClassB", "method2"); + + @Test + public void testFilters_singleTest() { + assertThat( + TestFilter.isFilteredOrUnspecified( + buildArguments("testClassA#method1"), DESCRIPTION1)) + .isTrue(); + } + + @Test + public void testFilters_multipleTests() { + Bundle arguments = buildArguments("testClassA#method1,testClassB#method2"); + assertThat(TestFilter.isFilteredOrUnspecified(arguments, DESCRIPTION1)).isTrue(); + assertThat(TestFilter.isFilteredOrUnspecified(arguments, DESCRIPTION2)).isTrue(); + } + + @Test + public void testFilters_allTestsWhenUnspecified() { + assertThat(TestFilter.isFilteredOrUnspecified(new Bundle(), DESCRIPTION1)).isTrue(); + assertThat(TestFilter.isFilteredOrUnspecified(new Bundle(), DESCRIPTION2)).isTrue(); + } + + @Test + public void testDoesNotFilter_otherTest() { + assertThat( + TestFilter.isFilteredOrUnspecified( + buildArguments("testClassA#method1"), DESCRIPTION2)) + .isFalse(); + } + + @Test + public void testDoesNotThrow_onBadArguments() { + // If the argument is explicitly null, then it's treated as unspecified. + assertThat(TestFilter.isFilteredOrUnspecified(buildArguments(null), DESCRIPTION1)).isTrue(); + // If the argument is any other invalid input, then it just won't match. + assertThat(TestFilter.isFilteredOrUnspecified(buildArguments(",,,"), DESCRIPTION1)) + .isFalse(); + assertThat(TestFilter.isFilteredOrUnspecified(buildArguments("a#,#b"), DESCRIPTION1)) + .isFalse(); + } + + private Bundle buildArguments(String testFilterArg) { + Bundle args = new Bundle(); + args.putString(TestFilter.OPTION_NAME, testFilterArg); + return args; + } +} diff --git a/libraries/screenshot/Android.bp b/libraries/screenshot/Android.bp new file mode 100644 index 000000000..80ece3944 --- /dev/null +++ b/libraries/screenshot/Android.bp @@ -0,0 +1,69 @@ +// Copyright (C) 2022 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_test { + name: "platform-screenshot-diff-test", + platform_apis: true, + optimize: { + enabled: false + }, + srcs: [ + "src/**/*.kt" + ], + static_libs: [ + "androidx.test.core", + "androidx.test.ext.junit", + "androidx.test.runner", + "androidx.test.rules", + "launcher-helper-lib", + "metrics-helper-lib", + "platform-screenshot-diff-core", + "platform-test-annotations", + "truth-prebuilt", + "ub-uiautomator", + ], + asset_dirs: ["src/androidTest/assets"], + test_suites: ["general-tests"], +} + +java_library { + name: "platform-screenshot-diff-core", + platform_apis: true, + optimize: { + enabled: false + }, + srcs: [ + "src/main/java/platform/test/screenshot/**/*.kt", + ], + static_libs: [ + "androidx.annotation_annotation", + "androidx.test.ext.junit", + "platform-screenshot-diff-proto", + ], +} + +java_library { + name: "platform-screenshot-diff-proto", + srcs: [ + "**/*.proto", + ], + optimize: { + enabled: false + } +} diff --git a/libraries/screenshot/AndroidManifest.xml b/libraries/screenshot/AndroidManifest.xml new file mode 100644 index 000000000..7f828fb65 --- /dev/null +++ b/libraries/screenshot/AndroidManifest.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2022 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="platform.test.screenshot" > + + <application> + <uses-library android:name="android.test.runner" /> + </application> + + <uses-sdk + android:minSdkVersion="21" + android:targetSdkVersion="24" /> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:label="Android Screenshot Diff Tests" + android:targetPackage="platform.test.screenshot" /> + +</manifest> diff --git a/libraries/screenshot/OWNERS b/libraries/screenshot/OWNERS new file mode 100644 index 000000000..9dfa2ff1b --- /dev/null +++ b/libraries/screenshot/OWNERS @@ -0,0 +1,5 @@ +# Owners for /libraries/screenshot + +ihcinihsdk@google.com +ramperi@google.com +yuwu@google.com diff --git a/libraries/screenshot/TEST_MAPPING b/libraries/screenshot/TEST_MAPPING new file mode 100644 index 000000000..3eb8e569c --- /dev/null +++ b/libraries/screenshot/TEST_MAPPING @@ -0,0 +1,12 @@ +{ + "presubmit": [ + { + "name": "platform-screenshot-diff-test", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + } + ] +} diff --git a/libraries/screenshot/proto/src/main/proto/screenshot_result.proto b/libraries/screenshot/proto/src/main/proto/screenshot_result.proto new file mode 100644 index 000000000..8f6f8efba --- /dev/null +++ b/libraries/screenshot/proto/src/main/proto/screenshot_result.proto @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2022 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. + */ + +syntax = "proto3"; + +package test.screenshot.proto; +option java_package = "platform.test.screenshot.proto"; +option java_outer_classname = "ScreenshotResultProto"; + +message ComparisonOptions { + // Given an RGBA color, a failure will trigger if any channel changes beyond + // this specified threshold. + // Please only set this when actual inconsistencies are encountered. + // To find a reasonable value, consult the logs and see what real + // differences were encountered. Anything above 4 is probably too much. + // Default: 0 + int32 allowable_per_channel_difference = 5; + + // Default: 0 + int32 allowable_number_pixels_different = 3; + + // Only compare pixels that have a nonzero alpha value in the reference image. + // Default: false + bool use_masking = 4; +} + +message DiffRequest { + // PNG encoded image. + bytes image_test = 1; + oneof reference { + // Absolute file path, e.g. + // <RUNFILES>/google3/net/hostdatapath/common/statusz/scuba_goldens/header-1.png + // Use this when comparing a generated image against a golden image. + // An "Approve Changes" button will appear in the web UI. + string image_location_golden = 2; + + // PNG encoded image. Use when comparing two generated images against each + // other. + bytes image_reference = 3; + } + ComparisonOptions options = 4; + + // Additional metadata that will be copied verbatim to the DiffResult. + repeated Metadata metadata = 5; +} + +message DiffResult { + enum Status { + UNSPECIFIED = 0; + PASSED = 1; // number_pixels_different <= allowable_number_pixels_different + FAILED = 2; + // There was no file at the golden location or it was unreadable. + MISSING_REFERENCE = 3; + FLAKY = 4; // undefined behavior for single DiffResult + } + + message ComparisonStatistics { + // Copy of the ComparisonOptions from the DiffRequest. + ComparisonOptions comparison_options = 1; + + uint32 number_pixels_compared = 2; + + uint32 number_pixels_identical = 3; + uint32 number_pixels_similar = 4; // within color_allowance + // When use_masking in DiffRequest is true, number of pixels that had zero + // alpha in the reference image. Otherwise zero. + uint32 number_pixels_ignored = 5; + uint32 number_pixels_different = 6; + } + + Status result_type = 1; + + // See DiffRequest.image_location_golden + string image_location_golden = 2; + + // Locations relative to output folder. + string image_location_test = 3; + string image_location_reference = 4; + string image_location_diff = 5; + + ComparisonStatistics comparison_statistics = 6; + repeated Metadata metadata = 7; + + // MD5 hash of the difference image. + string hash_diff_image = 8; + string unique_id = 9; +} + +message Metadata { + string key = 1; + string value = 2; +} + +message DiffResultList { + repeated DiffResult results = 1; +} diff --git a/libraries/screenshot/src/androidTest/assets/checkbox_checked.png b/libraries/screenshot/src/androidTest/assets/checkbox_checked.png Binary files differnew file mode 100644 index 000000000..c0b7c9569 --- /dev/null +++ b/libraries/screenshot/src/androidTest/assets/checkbox_checked.png diff --git a/libraries/screenshot/src/androidTest/assets/fullscreen_checked_checkbox.png b/libraries/screenshot/src/androidTest/assets/fullscreen_checked_checkbox.png Binary files differnew file mode 100644 index 000000000..a2ecce9e5 --- /dev/null +++ b/libraries/screenshot/src/androidTest/assets/fullscreen_checked_checkbox.png diff --git a/libraries/screenshot/src/androidTest/assets/fullscreen_checked_checkbox_round.png b/libraries/screenshot/src/androidTest/assets/fullscreen_checked_checkbox_round.png Binary files differnew file mode 100644 index 000000000..1a2bf0121 --- /dev/null +++ b/libraries/screenshot/src/androidTest/assets/fullscreen_checked_checkbox_round.png diff --git a/libraries/screenshot/src/androidTest/assets/fullscreen_rect_gray.png b/libraries/screenshot/src/androidTest/assets/fullscreen_rect_gray.png Binary files differnew file mode 100644 index 000000000..d793f218f --- /dev/null +++ b/libraries/screenshot/src/androidTest/assets/fullscreen_rect_gray.png diff --git a/libraries/screenshot/src/androidTest/assets/fullscreen_rect_gray_dark.png b/libraries/screenshot/src/androidTest/assets/fullscreen_rect_gray_dark.png Binary files differnew file mode 100644 index 000000000..220b87d3a --- /dev/null +++ b/libraries/screenshot/src/androidTest/assets/fullscreen_rect_gray_dark.png diff --git a/libraries/screenshot/src/androidTest/assets/fullscreen_rect_gray_moved_1px.png b/libraries/screenshot/src/androidTest/assets/fullscreen_rect_gray_moved_1px.png Binary files differnew file mode 100644 index 000000000..347dd3972 --- /dev/null +++ b/libraries/screenshot/src/androidTest/assets/fullscreen_rect_gray_moved_1px.png diff --git a/libraries/screenshot/src/androidTest/assets/round_rect_gray.png b/libraries/screenshot/src/androidTest/assets/round_rect_gray.png Binary files differnew file mode 100644 index 000000000..0f7e5c8f8 --- /dev/null +++ b/libraries/screenshot/src/androidTest/assets/round_rect_gray.png diff --git a/libraries/screenshot/src/androidTest/assets/round_rect_gray_dark.png b/libraries/screenshot/src/androidTest/assets/round_rect_gray_dark.png Binary files differnew file mode 100644 index 000000000..260ec77b0 --- /dev/null +++ b/libraries/screenshot/src/androidTest/assets/round_rect_gray_dark.png diff --git a/libraries/screenshot/src/androidTest/assets/round_rect_green.png b/libraries/screenshot/src/androidTest/assets/round_rect_green.png Binary files differnew file mode 100644 index 000000000..3e3ad7832 --- /dev/null +++ b/libraries/screenshot/src/androidTest/assets/round_rect_green.png diff --git a/libraries/screenshot/src/androidTest/java/platform/test/screenshot/GoldenImagePathManagerTest.kt b/libraries/screenshot/src/androidTest/java/platform/test/screenshot/GoldenImagePathManagerTest.kt new file mode 100644 index 000000000..2845a7245 --- /dev/null +++ b/libraries/screenshot/src/androidTest/java/platform/test/screenshot/GoldenImagePathManagerTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2022 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 platform.test.screenshot + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import androidx.test.platform.app.InstrumentationRegistry + +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@MediumTest +class GoldenImagePathManagerTest { + + @Test + fun goldenWithContextTest() { + val localGoldenRoot = "/localgoldenroot/" + val remoteGoldenRoot = "http://remotegoldenroot/" + val context = InstrumentationRegistry.getInstrumentation().getContext() + val gim = GoldenImagePathManager( + context, + GoldenImageLocationConfig(localGoldenRoot, remoteGoldenRoot)) + // Test for resolving device local paths. + val localGoldenFullImagePath = gim.goldenIdentifierResolver( + testName = "test1", relativePathOnly = false, localPath = true) + assertThat(localGoldenFullImagePath).startsWith(localGoldenRoot) + assertThat(localGoldenFullImagePath).endsWith("dpi/test1.png") + assertThat(localGoldenFullImagePath.split("/").size).isEqualTo(9) + // Test for resolving repo paths. + val repoGoldenFullImagePath = gim.goldenIdentifierResolver( + testName = "test2", relativePathOnly = false, localPath = false) + assertThat(repoGoldenFullImagePath).startsWith(remoteGoldenRoot) + assertThat(repoGoldenFullImagePath).endsWith("dpi/test2.png") + assertThat(repoGoldenFullImagePath.split("/").size).isEqualTo(10) + } + + private fun pathContextExtractor(context: Context): String { + return when { + (context.resources.displayMetrics.densityDpi.toString().length > 0) -> "context" + else -> "invalidcontext" + } + } + + private fun pathNoContextExtractor() = "nocontext" + + @Test + fun pathConfigTest() { + val pc = PathConfig( + PathElementNoContext("nocontext1", true, ::pathNoContextExtractor), + PathElementNoContext("nocontext2", true, ::pathNoContextExtractor), + PathElementWithContext("context1", true, ::pathContextExtractor), + PathElementWithContext("context2", true, ::pathContextExtractor) + ) + val context = InstrumentationRegistry.getInstrumentation().getContext() + val pcResolvedRelativePath = pc.resolveRelativePath(context) + assertThat(pcResolvedRelativePath).isEqualTo("nocontext/nocontext/context/context/") + } +} diff --git a/libraries/screenshot/src/androidTest/java/platform/test/screenshot/MSSIMMatcherTest.kt b/libraries/screenshot/src/androidTest/java/platform/test/screenshot/MSSIMMatcherTest.kt new file mode 100644 index 000000000..d44e374f7 --- /dev/null +++ b/libraries/screenshot/src/androidTest/java/platform/test/screenshot/MSSIMMatcherTest.kt @@ -0,0 +1,135 @@ +/* + * Copyright 2022 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 platform.test.screenshot + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import platform.test.screenshot.matchers.MSSIMMatcher +import platform.test.screenshot.utils.loadBitmap +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@MediumTest +class MSSIMMatcherTest { + + @Test + fun performDiff_sameBitmaps() { + val first = loadBitmap("round_rect_gray") + val second = loadBitmap("round_rect_gray") + + val matcher = MSSIMMatcher() + val result = matcher.calculateSSIM( + first.toIntArray(), second.toIntArray(), + first.width, first.height + ) + + // Asserts that all compared pixels are categorized as "similar". + assertThat(result.SSIM).isEqualTo(result.numPixelsCompared) + } + + @Test + fun performDiff_checkedAgainstUnchecked() { + val first = loadBitmap("checkbox_checked") + val second = loadBitmap("round_rect_gray") + + val matcher = MSSIMMatcher() + val result = matcher.calculateSSIM( + first.toIntArray(), second.toIntArray(), + first.width, first.height + ) + + assertThat(result.SSIM / result.numPixelsCompared) + .isWithin(0.001).of(0.516) + } + + @Test + fun performDiff_differentBorders() { + val first = loadBitmap("round_rect_gray") + val second = loadBitmap("round_rect_green") + + val matcher = MSSIMMatcher() + val result = matcher.calculateSSIM( + first.toIntArray(), second.toIntArray(), + first.width, first.height + ) + + assertThat(result.SSIM / result.numPixelsCompared) + .isWithin(0.001).of(0.951) + } + + @Test + fun performDiff_fullscreen_differentBorders_dark() { + val first = loadBitmap("fullscreen_rect_gray") + val second = loadBitmap("fullscreen_rect_gray_dark") + + val matcher = MSSIMMatcher() + val result = matcher.calculateSSIM( + first.toIntArray(), second.toIntArray(), + first.width, first.height + ) + + assertThat(result.SSIM / result.numPixelsCompared) + .isWithin(0.001).of(0.990) + } + + @Test + fun performDiff_differentBorders_dark() { + val first = loadBitmap("round_rect_gray") + val second = loadBitmap("round_rect_gray_dark") + + val matcher = MSSIMMatcher() + val result = matcher.calculateSSIM( + first.toIntArray(), second.toIntArray(), + first.width, first.height + ) + + assertThat(result.SSIM / result.numPixelsCompared) + .isWithin(0.001).of(0.960) + } + + @Test + fun performDiff_fullscreen_movedToRight() { + val first = loadBitmap("fullscreen_rect_gray") + val second = loadBitmap("fullscreen_rect_gray_moved_1px") + + val matcher = MSSIMMatcher() + val result = matcher.calculateSSIM( + first.toIntArray(), second.toIntArray(), + first.width, first.height + ) + + assertThat(result.SSIM / result.numPixelsCompared) + .isWithin(0.001).of(0.695) + } + + @Test + fun performDiff_fullscreen_checkboxes_differentRadius() { + val first = loadBitmap("fullscreen_checked_checkbox") + val second = loadBitmap("fullscreen_checked_checkbox_round") + + val matcher = MSSIMMatcher() + val result = matcher.calculateSSIM( + first.toIntArray(), second.toIntArray(), + first.width, first.height + ) + + assertThat(result.SSIM / result.numPixelsCompared) + .isWithin(0.001).of(0.921) + } +} diff --git a/libraries/screenshot/src/androidTest/java/platform/test/screenshot/PixelPerfectMatcherTest.kt b/libraries/screenshot/src/androidTest/java/platform/test/screenshot/PixelPerfectMatcherTest.kt new file mode 100644 index 000000000..2ef10487a --- /dev/null +++ b/libraries/screenshot/src/androidTest/java/platform/test/screenshot/PixelPerfectMatcherTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2022 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 platform.test.screenshot + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import com.google.common.truth.Truth.assertThat +import platform.test.screenshot.matchers.PixelPerfectMatcher +import platform.test.screenshot.utils.loadBitmap +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@MediumTest +class PixelPerfectMatcherTest { + + @Test + fun performDiff_sameBitmaps() { + val first = loadBitmap("round_rect_gray") + val second = loadBitmap("round_rect_gray") + + val matcher = PixelPerfectMatcher() + val result = matcher.compareBitmaps( + first.toIntArray(), second.toIntArray(), + first.width, first.height + ) + + assertThat(result.matches).isTrue() + } + + @Test + fun performDiff_sameSize_differentBorders() { + val first = loadBitmap("round_rect_gray") + val second = loadBitmap("round_rect_green") + + val matcher = PixelPerfectMatcher() + val result = matcher.compareBitmaps( + first.toIntArray(), second.toIntArray(), + first.width, first.height + ) + + assertThat(result.matches).isFalse() + } +} diff --git a/libraries/screenshot/src/androidTest/java/platform/test/screenshot/ScreenshotTestRuleTest.kt b/libraries/screenshot/src/androidTest/java/platform/test/screenshot/ScreenshotTestRuleTest.kt new file mode 100644 index 000000000..4778b7347 --- /dev/null +++ b/libraries/screenshot/src/androidTest/java/platform/test/screenshot/ScreenshotTestRuleTest.kt @@ -0,0 +1,179 @@ +/* + * Copyright 2022 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 platform.test.screenshot + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import com.google.common.truth.Truth.assertThat +import java.lang.AssertionError +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import platform.test.screenshot.OutputFileType.IMAGE_ACTUAL +import platform.test.screenshot.OutputFileType.IMAGE_DIFF +import platform.test.screenshot.OutputFileType.IMAGE_EXPECTED +import platform.test.screenshot.OutputFileType.RESULT_BIN_PROTO +import platform.test.screenshot.OutputFileType.RESULT_PROTO +import platform.test.screenshot.matchers.PixelPerfectMatcher +import platform.test.screenshot.proto.ScreenshotResultProto +import platform.test.screenshot.utils.loadBitmap + +@RunWith(AndroidJUnit4::class) +@MediumTest +class ScreenshotTestRuleTest { + + @get:Rule + val rule = ScreenshotTestRule() + + @Before + fun setup() { + rule.setCustomGoldenIdResolver { goldenId -> + "$goldenId.png" + } + } + + @Test + fun performDiff_sameBitmaps() { + val first = loadBitmap("round_rect_gray") + + first + .assertAgainstGolden(rule, "round_rect_gray", matcher = PixelPerfectMatcher()) + + val resultProto = rule.getPathOnDeviceFor(RESULT_PROTO) + assertThat(resultProto.readText()).contains("PASS") + assertThat(rule.getPathOnDeviceFor(IMAGE_ACTUAL).exists()).isTrue() + assertThat(rule.getPathOnDeviceFor(IMAGE_DIFF).exists()).isFalse() + assertThat(rule.getPathOnDeviceFor(IMAGE_EXPECTED).exists()).isTrue() + assertThat(rule.getPathOnDeviceFor(RESULT_BIN_PROTO).exists()).isTrue() + } + + @Test + fun performDiff_sameSizes_default_noMatch() { + val first = loadBitmap("round_rect_gray") + val compStatistics = ScreenshotResultProto.DiffResult.ComparisonStatistics.newBuilder() + .setNumberPixelsCompared(17) + .setNumberPixelsDifferent(1) + .setNumberPixelsIgnored(80) + .setNumberPixelsSimilar(16) + .build() + + expectErrorMessage( + "Image mismatch! Comparison stats: '$compStatistics'" + ) { + first.assertAgainstGolden(rule, "round_rect_green") + } + + val resultProto = rule.getPathOnDeviceFor(RESULT_PROTO) + assertThat(resultProto.readText()).contains("FAILED") + assertThat(rule.getPathOnDeviceFor(IMAGE_ACTUAL).exists()).isTrue() + assertThat(rule.getPathOnDeviceFor(IMAGE_DIFF).exists()).isTrue() + assertThat(rule.getPathOnDeviceFor(IMAGE_EXPECTED).exists()).isTrue() + assertThat(rule.getPathOnDeviceFor(RESULT_BIN_PROTO).exists()).isTrue() + } + + @Test + fun performDiff_sameSizes_pixelPerfect_noMatch() { + val first = loadBitmap("round_rect_gray") + val compStatistics = ScreenshotResultProto.DiffResult.ComparisonStatistics.newBuilder() + .setNumberPixelsCompared(2304) + .setNumberPixelsDifferent(556) + .setNumberPixelsIdentical(1748) + .build() + + expectErrorMessage( + "Image mismatch! Comparison stats: '$compStatistics'" + ) { + first + .assertAgainstGolden(rule, "round_rect_green", matcher = PixelPerfectMatcher()) + } + + val resultProto = rule.getPathOnDeviceFor(RESULT_PROTO) + assertThat(resultProto.readText()).contains("FAILED") + assertThat(rule.getPathOnDeviceFor(IMAGE_ACTUAL).exists()).isTrue() + assertThat(rule.getPathOnDeviceFor(IMAGE_DIFF).exists()).isTrue() + assertThat(rule.getPathOnDeviceFor(IMAGE_EXPECTED).exists()).isTrue() + assertThat(rule.getPathOnDeviceFor(RESULT_BIN_PROTO).exists()).isTrue() + } + + @Test + fun performDiff_differentSizes() { + val first = + loadBitmap("fullscreen_rect_gray") + + expectErrorMessage("Sizes are different! Expected: [48, 48], Actual: [720, 1184]") { + first + .assertAgainstGolden(rule, "round_rect_gray") + } + + val resultProto = rule.getPathOnDeviceFor(RESULT_PROTO) + assertThat(resultProto.readText()).contains("FAILED") + assertThat(rule.getPathOnDeviceFor(IMAGE_ACTUAL).exists()).isTrue() + assertThat(rule.getPathOnDeviceFor(IMAGE_DIFF).exists()).isFalse() + assertThat(rule.getPathOnDeviceFor(IMAGE_EXPECTED).exists()).isTrue() + assertThat(rule.getPathOnDeviceFor(RESULT_BIN_PROTO).exists()).isTrue() + } + + @Test(expected = IllegalArgumentException::class) + fun performDiff_incorrectGoldenName() { + val first = + loadBitmap("fullscreen_rect_gray") + + first + .assertAgainstGolden(rule, "round_rect_gray #") + } + + @Test + fun performDiff_missingGolden() { + val first = loadBitmap("round_rect_gray") + + expectErrorMessage( + "Missing golden image 'does_not_exist.png'. Did you mean to check in " + + "a new image?" + ) { + first + .assertAgainstGolden(rule, "does_not_exist") + } + + val resultProto = rule.getPathOnDeviceFor(RESULT_PROTO) + assertThat(resultProto.readText()).contains("MISSING_REFERENCE") + assertThat(rule.getPathOnDeviceFor(IMAGE_ACTUAL).exists()).isTrue() + assertThat(rule.getPathOnDeviceFor(IMAGE_DIFF).exists()).isFalse() + assertThat(rule.getPathOnDeviceFor(IMAGE_EXPECTED).exists()).isFalse() + assertThat(rule.getPathOnDeviceFor(RESULT_BIN_PROTO).exists()).isTrue() + } + + @After + fun after() { + rule.clearCustomGoldenIdResolver() + // Clear all files we generated so we don't have dependencies between tests + rule.deviceOutputDirectory.deleteRecursively() + } + + private fun expectErrorMessage(expectedErrorMessage: String, block: () -> Unit) { + try { + block() + } catch (e: AssertionError) { + val received = e.localizedMessage!! + assertThat(received).isEqualTo(expectedErrorMessage.trim()) + return + } + + throw AssertionError("No AssertionError thrown!") + } +} diff --git a/libraries/screenshot/src/androidTest/java/platform/test/screenshot/utils/BitmapUtils.kt b/libraries/screenshot/src/androidTest/java/platform/test/screenshot/utils/BitmapUtils.kt new file mode 100644 index 000000000..8d3471b42 --- /dev/null +++ b/libraries/screenshot/src/androidTest/java/platform/test/screenshot/utils/BitmapUtils.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2022 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 platform.test.screenshot.utils + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import androidx.test.platform.app.InstrumentationRegistry + +internal fun loadBitmap(imageName: String): Bitmap { + val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext + context.assets.open("$imageName.png").use { + return BitmapFactory.decodeStream(it) + } +} diff --git a/libraries/screenshot/src/main/java/platform/test/screenshot/GoldenImagePathManager.kt b/libraries/screenshot/src/main/java/platform/test/screenshot/GoldenImagePathManager.kt new file mode 100644 index 000000000..1f4e7755d --- /dev/null +++ b/libraries/screenshot/src/main/java/platform/test/screenshot/GoldenImagePathManager.kt @@ -0,0 +1,212 @@ +/* + * Copyright 2022 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 platform.test.screenshot + +import android.content.Context +import android.os.Build + +import java.io.File + +private const val BRAND_TAG = "brand" +private const val MODEL_TAG = "model" +private const val API_TAG = "api" +private const val SIZE_TAG = "size" +private const val RESOLUTION_TAG = "resolution" + +/** + * Class to manage Directory structure of golden images. + * + * When you run a AR Diff test, different attributes/dimensions of the platform you are running + * on, such as build, screen resolution, orientation etc. will/may render differently and therefore + * may require a different golden image to compare against. You can manage these multiple golden + * images related to your test using this utility class. It supports both device-less or device based + * configurations. Please see GoldenImagePathManagerTest for detailed examples. + * + * You can configure where to find the golden images repo and local cache using [locationConfig] + * All the goldens are stored under a directory structure, which is determined by + * [pathConfig]. See getDefaultPathConfig for the current implementation. + * + * There are two ways to modify how the golden images are stored and retrieved for your test: + * A. (Recommended) Create your own PathConfig object which takes a series of [PathElement]s + * Each path element represents a dimension such as screen resolution that affects the golden + * image. This dimension will be embedded either into the directory structure or into the + * filename itself. Your test can also provide its own custom implementation of [PathElement] + * if the dimension your test needs to rely on, is not supported. + * B. If you have a completely unique way of managing your golden image repository and + * corresponding local cache, implement a derived class and override the + * goldenIdentifierResolver function. + * + * NOTE: This class does not determine what combinations of attributes / dimensions your + * test code will run for. That decision/configuration is part of your test configuration. + * + */ +open class GoldenImagePathManager( + val appContext: Context, + val locationConfig: GoldenImageLocationConfig = GoldenImageLocationConfig( + getDeviceOutputDirectory(appContext), + getRepoURL() + ), + val pathConfig: PathConfig = getDefaultPathConfig() +) { + + private val deviceLocalPath = locationConfig.deviceLocalPath + private val repoRemotePath = locationConfig.repoRemotePath + private val imageExtension = "png" + + /* + * Uses [pathConfig] and [testName] to construct the full path to the golden image. + */ + public fun goldenIdentifierResolver( + testName: String, + relativePathOnly: Boolean = true, + localPath: Boolean = true + ): String { + val relativePath = pathConfig.resolveRelativePath(appContext) + val imageFullPath = "$relativePath$testName.$imageExtension" + return when { + relativePathOnly -> imageFullPath + localPath -> "$deviceLocalPath/$imageFullPath" + else -> "$repoRemotePath/$imageFullPath" + } + } +} + +/* + * Every dimension that impacts the golden image needs to be a part of the path/filename + * that is used to access the golden. There are two types of attributes / dimensions. + * One that depend on the device context and the once that are context agnostic. + */ +abstract sealed class PathElementBase { + abstract val attr: String + abstract val isDir: Boolean +} + +/* + * For dimensions that do not need access to the device context e.g. + * Build.MODEL, please instantiate the no context class. + */ +data class PathElementNoContext( + override val attr: String, + override val isDir: Boolean, + val func: (() -> String) +) : PathElementBase() + +/* + * For dimensions that do not need to the device context e.g. + * and / or can change during run-time, please instantiate this class. + * e.g. screen orientation. + */ +data class PathElementWithContext( + override val attr: String, + override val isDir: Boolean, + val func: ((Context) -> String) +) : PathElementBase() + +/* + * Converts an ordered list of PathElements into a relative path on filesystem. + * The relative path is then combined with either repo path of local cache path + * to get the full path to golden image. + */ +class PathConfig(vararg elems: PathElementBase) { + val data = listOf(*elems) + + public fun resolveRelativePath(context: Context): String { + return data.map { + when (it) { + is PathElementWithContext -> it.func(context) + is PathElementNoContext -> it.func() + else -> "" + } + if (it.isDir) "/" else "_" + }.joinToString("") + } +} + +/* +* This configure is used to determine where to retrieve the golden images from. +* The golden images will be stored in a git repository and will be download to the +* device as needed. Both repo location and local path are part of this configuration. +*/ +data class GoldenImageLocationConfig( + // Directory on the device that is used to store the output files. + val deviceLocalPath: String = "", + + // Repo where all the golden images are checkedin. + val repoRemotePath: String = "" +) + +/* +* This is the PathConfig that will be used by default. +* An example directory structure using this config would be +* /google/pixel6/api32/600_400/ +*/ + +private fun getDefaultPathConfig(): PathConfig { + return PathConfig( + PathElementNoContext(BRAND_TAG, true, ::getDeviceBrand), + PathElementNoContext(MODEL_TAG, true, ::getDeviceModel), + PathElementNoContext(API_TAG, true, ::getAPIVersion), + PathElementWithContext(SIZE_TAG, true, ::getScreenSize), + PathElementWithContext( + RESOLUTION_TAG, + true, + ::getScreenResolution) + ) +} + +/* + * Default output directory where all images generated as part of the test are stored. + */ +fun getDeviceOutputDirectory(context: Context) = + File(context.externalCacheDir, "androidx_screenshots").toString() + +private fun getRepoURL() = "https://" + +/* Standard implementations for the usual list of dimensions that affect a golden image. */ +private fun getDeviceModel(): String { + var model = Build.MODEL.lowercase() + arrayOf("phone", "x86_64", "x86", "x64", "gms").forEach { + model = model.replace(it, "") + } + return model.trim().replace(" ", "_") +} + +private fun getDeviceBrand(): String { + var brand = Build.BRAND.lowercase() + arrayOf("phone", "x86_64", "x86", "x64", "gms").forEach { + brand = brand.replace(it, "") + } + return brand.trim().replace(" ", "_") +} + +private fun getAPIVersion() = "API" + Build.VERSION.SDK_INT.toString() + +private fun getScreenResolution(context: Context) = + context.resources.displayMetrics.densityDpi.toString() + "dpi" + +private fun getScreenOrientation(context: Context) = + context.resources.configuration.orientation.toString() + +private fun getScreenSize(context: Context): String { + val heightdp = context.resources.configuration.screenHeightDp.toString() + val widthdp = context.resources.configuration.screenWidthDp.toString() + return "${heightdp}_$widthdp" +} + +/* + * If the dimension that your golden depends on, is not supported, + * Please add its implementations here. + */ diff --git a/libraries/screenshot/src/main/java/platform/test/screenshot/PlatformScreenshotTestRule.kt b/libraries/screenshot/src/main/java/platform/test/screenshot/PlatformScreenshotTestRule.kt new file mode 100644 index 000000000..891b98d74 --- /dev/null +++ b/libraries/screenshot/src/main/java/platform/test/screenshot/PlatformScreenshotTestRule.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2022 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 platform.test.screenshot + +/** + * Rule to be used in platform project tests. Set's up the proper repository name and golden + * directory. + * + * @param moduleDirectory Directory to be used for the module that contains the tests. This is + * just a helper to avoid mixing goldens between different projects. + * Example for module directory: "compose/material/material" + * @param outputRootDir The root directory for output files. + * + * @hide + */ +class PlatformScreenshotTestRule( + moduleDirectory: String, + outputRootDir: String? = null +) : ScreenshotTestRule( + ScreenshotTestRuleConfig( + "platform/frameworks/support-golden", + moduleDirectory.trim('/') + ), + outputRootDir +) diff --git a/libraries/screenshot/src/main/java/platform/test/screenshot/ScreenshotTestRule.kt b/libraries/screenshot/src/main/java/platform/test/screenshot/ScreenshotTestRule.kt new file mode 100644 index 000000000..9f80a5345 --- /dev/null +++ b/libraries/screenshot/src/main/java/platform/test/screenshot/ScreenshotTestRule.kt @@ -0,0 +1,391 @@ +/* + * Copyright 2022 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 platform.test.screenshot + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.os.Build +import android.os.Bundle +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.rules.TestRule +import org.junit.rules.TestWatcher +import org.junit.runner.Description +import org.junit.runners.model.Statement +import platform.test.screenshot.matchers.BitmapMatcher +import platform.test.screenshot.matchers.MSSIMMatcher +import platform.test.screenshot.matchers.PixelPerfectMatcher +import platform.test.screenshot.proto.ScreenshotResultProto +import java.io.File +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException + +// TODO(b/223901506): Replace this with the more advanced config class after the CL ag/17587688 +// is submitted. +/** + * Config for [ScreenshotTestRule]. + * + * To be used to set up paths to golden images. These paths are not used to retrieve the goldens + * during the test. They are just directly stored into the result proto file. The proto file can + * then be used by CI to determined where to put the new approved goldens. Your tests assets + * directory should be pointing to exactly the same path. + * + * @param repoRootPathForGoldens Path to the repo's root that contains the goldens. To be used by + * CI. + * @param pathToGoldensInRepo Relative path to goldens inside your [repoRootPathForGoldens]. + */ +class ScreenshotTestRuleConfig( + val repoRootPathForGoldens: String = "", + val pathToGoldensInRepo: String = "" +) + +/** + * Type of file that can be produced by the [ScreenshotTestRule]. + */ +internal enum class OutputFileType { + IMAGE_ACTUAL, + IMAGE_EXPECTED, + IMAGE_DIFF, + RESULT_PROTO, + RESULT_BIN_PROTO +} + +/** + * Rule to be added to a test to facilitate screenshot testing. + * + * This rule records current test name and when instructed it will perform the given bitmap + * comparison against the given golden. All the results (including result proto file) are stored + * into the device to be retrieved later. + * + * @param config To configure where this rule should look for goldens. + * @param outputRootDir The root directory for output files. + * + * @see Bitmap.assertAgainstGolden + */ +@SuppressLint("SyntheticAccessor") +open class ScreenshotTestRule( + val config: ScreenshotTestRuleConfig = ScreenshotTestRuleConfig(), + val outputRootDir: String? = null +) : TestRule { + + val deviceOutputRootDirectory: File? = + if (outputRootDir != null) { + File(outputRootDir) + } else { + InstrumentationRegistry.getInstrumentation().getContext().externalCacheDir + } + + /** + * Directory on the device that is used to store the output files. + */ + val deviceOutputDirectory + get() = File( + deviceOutputRootDirectory, + "platform_screenshots" + ) + + private val repoRootPathForGoldens = config.repoRootPathForGoldens.trim('/') + private val pathToGoldensInRepo = config.pathToGoldensInRepo.trim('/') + private val imageExtension = ".png" + private val resultBinaryProtoFileSuffix = ".pb" + // This is used in CI to identify the files. + private val resultProtoFileSuffix = "goldResult.textproto" + + // Magic number for an in-progress status report + private val bundleStatusInProgress = 2 + private val bundleKeyPrefix = "platform_screenshots_" + + private lateinit var testIdentifier: String + private lateinit var deviceId: String + + private var goldenIdentifierResolver: ((String) -> String) = ::resolveGoldenName + + private val testWatcher = object : TestWatcher() { + override fun starting(description: Description?) { + deviceId = getDeviceModel() + testIdentifier = "${description!!.className}_${description.methodName}_$deviceId" + } + } + + override fun apply(base: Statement, description: Description?): Statement { + return ScreenshotTestStatement(base) + .run { testWatcher.apply(this, description) } + } + + class ScreenshotTestStatement(private val base: Statement) : Statement() { + override fun evaluate() { + // NOTE(ihcinihsdk@): My hunch is that we should not add these assumptions at all. + // The reason is that this framework should be served for the general platform. + // If a test is supposed to run on a specific type of device, it should be the + // test authors' duty. What we need to do is to provide guidance on how to add + // assumptions on type device and SDK version. + base.evaluate() + } + } + + internal fun setCustomGoldenIdResolver(resolver: ((String) -> String)) { + goldenIdentifierResolver = resolver + } + + internal fun clearCustomGoldenIdResolver() { + goldenIdentifierResolver = ::resolveGoldenName + } + + private fun resolveGoldenName(goldenIdentifier: String): String { + return "${goldenIdentifier}_$deviceId$imageExtension" + } + + private fun fetchExpectedImage(goldenIdentifier: String): Bitmap? { + val context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext + + try { + context.assets.open(goldenIdentifierResolver(goldenIdentifier)).use { + return BitmapFactory.decodeStream(it) + } + } catch (e: FileNotFoundException) { + // Golden not present + return null + } + } + + /** + * Asserts the given bitmap against the golden identified by the given name. + * + * Note: The golden identifier should be unique per your test module (unless you want multiple + * tests to match the same golden). The name must not contain extension. You should also avoid + * adding strings like "golden", "image" and instead describe what is the golder referring to. + * + * @param actual The bitmap captured during the test. + * @param goldenIdentifier Name of the golden. Allowed characters: 'A-Za-z0-9_-' + * @param matcher The algorithm to be used to perform the matching. + * + * @see MSSIMMatcher + * @see PixelPerfectMatcher + * @see Bitmap.assertAgainstGolden + * + * @throws IllegalArgumentException If the golden identifier contains forbidden characters or + * is empty. + */ + fun assertBitmapAgainstGolden( + actual: Bitmap, + goldenIdentifier: String, + matcher: BitmapMatcher + ) { + if (!goldenIdentifier.matches("^[A-Za-z0-9_-]+$".toRegex())) { + throw IllegalArgumentException( + "The given golden identifier '$goldenIdentifier' does not satisfy the naming " + + "requirement. Allowed characters are: '[A-Za-z0-9_-]'" + ) + } + + val expected = fetchExpectedImage(goldenIdentifier) + if (expected == null) { + reportResult( + status = ScreenshotResultProto.DiffResult.Status.MISSING_REFERENCE, + goldenIdentifier = goldenIdentifier, + actual = actual + ) + throw AssertionError( + "Missing golden image " + + "'${goldenIdentifierResolver(goldenIdentifier)}'. " + + "Did you mean to check in a new image?" + ) + } + + if (actual.width != expected.width || actual.height != expected.height) { + reportResult( + status = ScreenshotResultProto.DiffResult.Status.FAILED, + goldenIdentifier = goldenIdentifier, + actual = actual, + expected = expected + ) + throw AssertionError( + "Sizes are different! Expected: [${expected.width}, ${expected + .height}], Actual: [${actual.width}, ${actual.height}]" + ) + } + + val comparisonResult = matcher.compareBitmaps( + expected = expected.toIntArray(), + given = actual.toIntArray(), + width = actual.width, + height = actual.height + ) + + val status = if (comparisonResult.matches) { + ScreenshotResultProto.DiffResult.Status.PASSED + } else { + ScreenshotResultProto.DiffResult.Status.FAILED + } + + reportResult( + status = status, + goldenIdentifier = goldenIdentifier, + actual = actual, + comparisonStatistics = comparisonResult.comparisonStatistics, + expected = expected, + diff = comparisonResult.diff + ) + + if (!comparisonResult.matches) { + throw AssertionError( + "Image mismatch! Comparison stats: '${comparisonResult + .comparisonStatistics}'" + ) + } + } + + private fun reportResult( + status: ScreenshotResultProto.DiffResult.Status, + goldenIdentifier: String, + actual: Bitmap, + comparisonStatistics: ScreenshotResultProto.DiffResult.ComparisonStatistics? = null, + expected: Bitmap? = null, + diff: Bitmap? = null + ) { + val resultProto = ScreenshotResultProto.DiffResult + .newBuilder() + .setResultType(status) + .addMetadata( + ScreenshotResultProto.Metadata.newBuilder() + .setKey("repoRootPath") + .setValue(repoRootPathForGoldens)) + + if (comparisonStatistics != null) { + resultProto.comparisonStatistics = comparisonStatistics + } + resultProto.imageLocationGolden = + if (pathToGoldensInRepo.isEmpty()) { + goldenIdentifierResolver(goldenIdentifier) + } else { + "$pathToGoldensInRepo/${goldenIdentifierResolver(goldenIdentifier)}" + } + + val report = Bundle() + + actual.writeToDevice(OutputFileType.IMAGE_ACTUAL).also { + resultProto.imageLocationTest = it.name + report.putString(bundleKeyPrefix + OutputFileType.IMAGE_ACTUAL, it.absolutePath) + } + diff?.run { + writeToDevice(OutputFileType.IMAGE_DIFF).also { + resultProto.imageLocationDiff = it.name + report.putString(bundleKeyPrefix + OutputFileType.IMAGE_DIFF, it.absolutePath) + } + } + expected?.run { + writeToDevice(OutputFileType.IMAGE_EXPECTED).also { + resultProto.imageLocationReference = it.name + report.putString( + bundleKeyPrefix + OutputFileType.IMAGE_EXPECTED, + it.absolutePath + ) + } + } + + writeToDevice(OutputFileType.RESULT_PROTO) { + it.write(resultProto.build().toString().toByteArray()) + }.also { + report.putString(bundleKeyPrefix + OutputFileType.RESULT_PROTO, it.absolutePath) + } + + writeToDevice(OutputFileType.RESULT_BIN_PROTO) { + it.write(resultProto.build().toByteArray()) + }.also { + report.putString(bundleKeyPrefix + OutputFileType.RESULT_BIN_PROTO, it.absolutePath) + } + + InstrumentationRegistry.getInstrumentation().sendStatus(bundleStatusInProgress, report) + } + + internal fun getPathOnDeviceFor(fileType: OutputFileType): File { + val fileName = when (fileType) { + OutputFileType.IMAGE_ACTUAL -> "${testIdentifier}_actual$imageExtension" + OutputFileType.IMAGE_EXPECTED -> "${testIdentifier}_expected$imageExtension" + OutputFileType.IMAGE_DIFF -> "${testIdentifier}_diff$imageExtension" + OutputFileType.RESULT_PROTO -> "${testIdentifier}_$resultProtoFileSuffix" + OutputFileType.RESULT_BIN_PROTO -> "${testIdentifier}_$resultBinaryProtoFileSuffix" + } + return File(deviceOutputDirectory, fileName) + } + + private fun Bitmap.writeToDevice(fileType: OutputFileType): File { + return writeToDevice(fileType) { + compress(Bitmap.CompressFormat.PNG, 0 /*ignored for png*/, it) + } + } + + private fun writeToDevice( + fileType: OutputFileType, + writeAction: (FileOutputStream) -> Unit + ): File { + if (!deviceOutputDirectory.exists() && !deviceOutputDirectory.mkdir()) { + throw IOException("Could not create folder.") + } + + var file = getPathOnDeviceFor(fileType) + try { + FileOutputStream(file).use { + writeAction(it) + } + } catch (e: Exception) { + throw IOException( + "Could not write file to storage (path: ${file.absolutePath}). " + + " Stacktrace: " + e.stackTrace + ) + } + return file + } + + private fun getDeviceModel(): String { + var model = android.os.Build.MODEL.lowercase() + arrayOf("phone", "x86", "x64", "gms").forEach { + model = model.replace(it, "") + } + return model.trim().replace(" ", "_") + } +} + +internal fun Bitmap.toIntArray(): IntArray { + val bitmapArray = IntArray(width * height) + getPixels(bitmapArray, 0, width, 0, 0, width, height) + return bitmapArray +} + +/** + * Asserts this bitmap against the golden identified by the given name. + * + * Note: The golden identifier should be unique per your test module (unless you want multiple tests + * to match the same golden). The name must not contain extension. You should also avoid adding + * strings like "golden", "image" and instead describe what is the golder referring to. + * + * @param rule The screenshot test rule that provides the comparison and reporting. + * @param goldenIdentifier Name of the golden. Allowed characters: 'A-Za-z0-9_-' + * @param matcher The algorithm to be used to perform the matching. By default [MSSIMMatcher] + * is used. + * + * @see MSSIMMatcher + * @see PixelPerfectMatcher + */ +fun Bitmap.assertAgainstGolden( + rule: ScreenshotTestRule, + goldenIdentifier: String, + matcher: BitmapMatcher = MSSIMMatcher() +) { + rule.assertBitmapAgainstGolden(this, goldenIdentifier, matcher = matcher) +} diff --git a/libraries/screenshot/src/main/java/platform/test/screenshot/matchers/BitmapMatcher.kt b/libraries/screenshot/src/main/java/platform/test/screenshot/matchers/BitmapMatcher.kt new file mode 100644 index 000000000..898276a24 --- /dev/null +++ b/libraries/screenshot/src/main/java/platform/test/screenshot/matchers/BitmapMatcher.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2022 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 platform.test.screenshot.matchers + +import android.graphics.Bitmap +import platform.test.screenshot.proto.ScreenshotResultProto.DiffResult.ComparisonStatistics + +/** + * Interface to implement to provide custom bitmap matchers. + */ +interface BitmapMatcher { + /** + * Compares the given bitmaps and returns result of the operation. + * + * The images need to have same size. + * + * @param expected The reference / golden image. + * @param given The image taken during the test. + * @param width Width of both of the images. + * @param height Height of both of the images. + */ + fun compareBitmaps(expected: IntArray, given: IntArray, width: Int, height: Int): MatchResult +} + +/** + * Result of the matching performed by [BitmapMatcher]. + * + * @param matches True if bitmaps match. + * @param comparisonStatistics Matching statistics provided by this matcher that performed the + * comparison. + * @param diff Diff bitmap that highlights the differences between the images. Can be null if match + * was found. + */ +class MatchResult( + val matches: Boolean, + val comparisonStatistics: ComparisonStatistics, + val diff: Bitmap? +) diff --git a/libraries/screenshot/src/main/java/platform/test/screenshot/matchers/MSSIMMatcher.kt b/libraries/screenshot/src/main/java/platform/test/screenshot/matchers/MSSIMMatcher.kt new file mode 100644 index 000000000..d3d032463 --- /dev/null +++ b/libraries/screenshot/src/main/java/platform/test/screenshot/matchers/MSSIMMatcher.kt @@ -0,0 +1,274 @@ +/* + * Copyright 2022 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 platform.test.screenshot.matchers + +import android.graphics.Color +import androidx.annotation.FloatRange +import kotlin.math.pow +import platform.test.screenshot.proto.ScreenshotResultProto + +/** + * Image comparison using Structural Similarity Index, developed by Wang, Bovik, Sheikh, and + * Simoncelli. Details can be read in their paper: + * https://ece.uwaterloo.ca/~z70wang/publications/ssim.pdf + */ +class MSSIMMatcher( + @FloatRange(from = 0.0, to = 1.0) private val threshold: Double = 0.98 +) : BitmapMatcher { + + companion object { + // These values were taken from the publication + private const val CONSTANT_L = 254.0 + private const val CONSTANT_K1 = 0.00001 + private const val CONSTANT_K2 = 0.00003 + private val CONSTANT_C1 = (CONSTANT_L * CONSTANT_K1).pow(2.0) + private val CONSTANT_C2 = (CONSTANT_L * CONSTANT_K2).pow(2.0) + private const val WINDOW_SIZE = 10 + } + + override fun compareBitmaps( + expected: IntArray, + given: IntArray, + width: Int, + height: Int + ): MatchResult { + val calSSIMResult = calculateSSIM(expected, given, width, height) + + val stats = ScreenshotResultProto.DiffResult.ComparisonStatistics + .newBuilder() + .setNumberPixelsCompared(calSSIMResult.numPixelsCompared) + .setNumberPixelsSimilar(calSSIMResult.numPixelsSimilar) + .setNumberPixelsIgnored(calSSIMResult.numPixelsIgnored) + .setNumberPixelsDifferent( + calSSIMResult.numPixelsCompared - calSSIMResult.numPixelsSimilar) + .build() + + if (calSSIMResult.numPixelsSimilar + >= threshold * calSSIMResult.numPixelsCompared.toDouble()) { + return MatchResult( + matches = true, + diff = null, + comparisonStatistics = stats + ) + } + + // Create diff + val result = PixelPerfectMatcher() + .compareBitmaps(expected, given, width, height) + return MatchResult( + matches = false, + diff = result.diff, + comparisonStatistics = stats + ) + } + + internal fun calculateSSIM( + ideal: IntArray, + given: IntArray, + width: Int, + height: Int + ): SSIMResult { + return calculateSSIM(ideal, given, 0, width, width, height) + } + + private fun calculateSSIM( + ideal: IntArray, + given: IntArray, + offset: Int, + stride: Int, + width: Int, + height: Int + ): SSIMResult { + var SSIMTotal = 0.0 + var windows = 0 + var currentWindowY = 0 + var ignored = 0 + + while (currentWindowY < height) { + val windowHeight = computeWindowSize(currentWindowY, height) + var currentWindowX = 0 + while (currentWindowX < width) { + val windowWidth = computeWindowSize(currentWindowX, width) + val start: Int = + indexFromXAndY(currentWindowX, currentWindowY, stride, offset) + if (isWindowWhite(ideal, start, stride, windowWidth, windowHeight) && + isWindowWhite(given, start, stride, windowWidth, windowHeight) + ) { + currentWindowX += WINDOW_SIZE + ignored += WINDOW_SIZE + continue + } + windows++ + val means = + getMeans(ideal, given, start, stride, windowWidth, windowHeight) + val meanX = means[0] + val meanY = means[1] + val variances = getVariances( + ideal, given, meanX, meanY, start, stride, + windowWidth, windowHeight + ) + val varX = variances[0] + val varY = variances[1] + val stdBoth = variances[2] + val SSIM = SSIM(meanX, meanY, varX, varY, stdBoth) + SSIMTotal += SSIM + currentWindowX += WINDOW_SIZE + } + currentWindowY += WINDOW_SIZE + } + return SSIMResult( + SSIM = SSIMTotal, + numPixelsSimilar = (SSIMTotal + 0.5).toInt(), + numPixelsIgnored = ignored, + numPixelsCompared = windows + ) + } + + /** + * Compute the size of the window. The window defaults to WINDOW_SIZE, but + * must be contained within dimension. + */ + private fun computeWindowSize(coordinateStart: Int, dimension: Int): Int { + return if (coordinateStart + WINDOW_SIZE <= dimension) { + WINDOW_SIZE + } else { + dimension - coordinateStart + } + } + + private fun isWindowWhite( + colors: IntArray, + start: Int, + stride: Int, + windowWidth: Int, + windowHeight: Int + ): Boolean { + for (y in 0 until windowHeight) { + for (x in 0 until windowWidth) { + if (colors[indexFromXAndY(x, y, stride, start)] != Color.WHITE) { + return false + } + } + } + return true + } + + /** + * This calculates the position in an array that would represent a bitmap given the parameters. + */ + private fun indexFromXAndY(x: Int, y: Int, stride: Int, offset: Int): Int { + return x + y * stride + offset + } + + private fun SSIM(muX: Double, muY: Double, sigX: Double, sigY: Double, sigXY: Double): Double { + var SSIM = (2 * muX * muY + CONSTANT_C1) * (2 * sigXY + CONSTANT_C2) + val denom = ((muX * muX + muY * muY + CONSTANT_C1) * (sigX + sigY + CONSTANT_C2)) + SSIM /= denom + return SSIM + } + + /** + * This method will find the mean of a window in both sets of pixels. The return is an array + * where the first double is the mean of the first set and the second double is the mean of the + * second set. + */ + private fun getMeans( + pixels0: IntArray, + pixels1: IntArray, + start: Int, + stride: Int, + windowWidth: Int, + windowHeight: Int + ): DoubleArray { + var avg0 = 0.0 + var avg1 = 0.0 + for (y in 0 until windowHeight) { + for (x in 0 until windowWidth) { + val index: Int = indexFromXAndY(x, y, stride, start) + avg0 += getIntensity(pixels0[index]) + avg1 += getIntensity(pixels1[index]) + } + } + avg0 /= windowWidth * windowHeight.toDouble() + avg1 /= windowWidth * windowHeight.toDouble() + return doubleArrayOf(avg0, avg1) + } + + /** + * Finds the variance of the two sets of pixels, as well as the covariance of the windows. The + * return value is an array of doubles, the first is the variance of the first set of pixels, + * the second is the variance of the second set of pixels, and the third is the covariance. + */ + private fun getVariances( + pixels0: IntArray, + pixels1: IntArray, + mean0: Double, + mean1: Double, + start: Int, + stride: Int, + windowWidth: Int, + windowHeight: Int + ): DoubleArray { + var var0 = 0.0 + var var1 = 0.0 + var varBoth = 0.0 + for (y in 0 until windowHeight) { + for (x in 0 until windowWidth) { + val index: Int = indexFromXAndY(x, y, stride, start) + val v0 = getIntensity(pixels0[index]) - mean0 + val v1 = getIntensity(pixels1[index]) - mean1 + var0 += v0 * v0 + var1 += v1 * v1 + varBoth += v0 * v1 + } + } + var0 /= windowWidth * windowHeight - 1.toDouble() + var1 /= windowWidth * windowHeight - 1.toDouble() + varBoth /= windowWidth * windowHeight - 1.toDouble() + return doubleArrayOf(var0, var1, varBoth) + } + + /** + * Gets the intensity of a given pixel in RGB using luminosity formula + * + * l = 0.21R' + 0.72G' + 0.07B' + * + * The prime symbols dictate a gamma correction of 1. + */ + private fun getIntensity(pixel: Int): Double { + val gamma = 1.0 + var l = 0.0 + l += 0.21f * (Color.red(pixel) / 255f.toDouble()).pow(gamma) + l += 0.72f * (Color.green(pixel) / 255f.toDouble()).pow(gamma) + l += 0.07f * (Color.blue(pixel) / 255f.toDouble()).pow(gamma) + return l + } +} + +/** + * Result of the calculation of SSIM. + * + * @param numPixelsSimilar The number of similar pixels. + * @param numPixelsIgnored The number of ignored pixels. + * @param numPixelsCompared The number of compared pixels. + */ +class SSIMResult( + val SSIM: Double, + val numPixelsSimilar: Int, + val numPixelsIgnored: Int, + val numPixelsCompared: Int +) diff --git a/libraries/screenshot/src/main/java/platform/test/screenshot/matchers/PixelPerfectMatcher.kt b/libraries/screenshot/src/main/java/platform/test/screenshot/matchers/PixelPerfectMatcher.kt new file mode 100644 index 000000000..e1e487565 --- /dev/null +++ b/libraries/screenshot/src/main/java/platform/test/screenshot/matchers/PixelPerfectMatcher.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2022 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 platform.test.screenshot.matchers + +import android.graphics.Bitmap +import android.graphics.Color +import platform.test.screenshot.proto.ScreenshotResultProto + +/** + * Bitmap matching that does an exact comparison of pixels between bitmaps. + */ +class PixelPerfectMatcher : BitmapMatcher { + + override fun compareBitmaps( + expected: IntArray, + given: IntArray, + width: Int, + height: Int + ): MatchResult { + check(expected.size == given.size) + + var different = 0 + var same = 0 + + val diffArray = IntArray(width * height) + + for (x in 0 until width) { + for (y in 0 until height) { + val index = x + y * width + val referenceColor = expected[index] + val testColor = given[index] + if (referenceColor == testColor) { + ++same + } else { + ++different + } + diffArray[index] = + diffColor( + referenceColor, + testColor + ) + } + } + + val stats = ScreenshotResultProto.DiffResult.ComparisonStatistics + .newBuilder() + .setNumberPixelsCompared(width * height) + .setNumberPixelsIdentical(same) + .setNumberPixelsDifferent(different) + .build() + + if (different > 0) { + val diff = Bitmap.createBitmap(diffArray, width, height, Bitmap.Config.ARGB_8888) + return MatchResult(matches = false, diff = diff, comparisonStatistics = stats) + } + return MatchResult(matches = true, diff = null, comparisonStatistics = stats) + } + + private fun diffColor(referenceColor: Int, testColor: Int): Int { + return if (referenceColor != testColor) { + Color.MAGENTA + } else { + Color.TRANSPARENT + } + } +} diff --git a/tests/automotive/functional/home/Android.bp b/tests/automotive/functional/home/Android.bp index 19f5b9b3f..665953897 100644 --- a/tests/automotive/functional/home/Android.bp +++ b/tests/automotive/functional/home/Android.bp @@ -30,5 +30,5 @@ android_test { "platform-test-options", ], srcs: ["src/**/*.java"], - test_suites: ["catbox"], + test_suites: ["catbox","general-tests"], } diff --git a/tests/automotive/functional/mediacenter/src/android/platform/tests/MediaTestAppTest.java b/tests/automotive/functional/mediacenter/src/android/platform/tests/MediaTestAppTest.java new file mode 100644 index 000000000..cc73ff2ff --- /dev/null +++ b/tests/automotive/functional/mediacenter/src/android/platform/tests/MediaTestAppTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.platform.tests; + +import static junit.framework.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static junit.framework.Assert.assertTrue; + +import android.platform.helpers.AutoUtility; +import android.platform.helpers.IAutoHomeHelper; +import android.platform.helpers.IAutoMediaHelper; +import android.platform.helpers.HelperAccessor; +import android.platform.helpers.IAutoTestMediaAppHelper; +import android.platform.test.option.StringOption; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class MediaTestAppTest { + private static final String MEDIA_APP = "media-app"; + private static final String TEST_MEDIA_APP = "Test Media App"; + + @ClassRule + public static StringOption mMediaTestApp = + new StringOption(MEDIA_APP).setRequired(false); + + private static HelperAccessor<IAutoMediaHelper> sMediaCenterHelper = + new HelperAccessor<>(IAutoMediaHelper.class); + private static HelperAccessor<IAutoTestMediaAppHelper> sTestMediaAppHelper = + new HelperAccessor<>(IAutoTestMediaAppHelper.class); + private static HelperAccessor<IAutoHomeHelper> sAutoHomeHelper = + new HelperAccessor<>(IAutoHomeHelper.class); + + @BeforeClass + public static void exitSuw() { + AutoUtility.exitSuw(); + // Load songs on Test Media App + sAutoHomeHelper.get().openMediaWidget(); + sMediaCenterHelper.get().openMediaAppMenuItems(); + String mediaAppName = TEST_MEDIA_APP; + if (mMediaTestApp != null + && mMediaTestApp.get() != null && !mMediaTestApp.get().isEmpty()) { + mediaAppName = mMediaTestApp.get(); + } + sMediaCenterHelper.get().openApp(mediaAppName); + assertTrue("Not a media app", + sMediaCenterHelper.get().getMediaAppTitle().equals(mediaAppName)); + sMediaCenterHelper.get().openMediaAppSettingsPage(); + sTestMediaAppHelper.get().loadMediaInLocalMediaTestApp(); + } + + @Test + public void testPlayPauseMedia() { + sMediaCenterHelper.get().playMedia(); + assertTrue("Song not playing.", sMediaCenterHelper.get().isPlaying()); + sMediaCenterHelper.get().pauseMedia(); + assertFalse("Song not paused.", sMediaCenterHelper.get().isPlaying()); + } + + @Test + public void testNextTrack() { + String currentSong = sMediaCenterHelper.get().getMediaTrackName(); + sMediaCenterHelper.get().clickNextTrack(); + assertNotEquals( + "Song playing has not been changed", + currentSong, + sMediaCenterHelper.get().getMediaTrackName()); + } +} diff --git a/tests/automotive/functional/mediacenter/src/android/platform/tests/NoUserLoggedInTest.java b/tests/automotive/functional/mediacenter/src/android/platform/tests/NoUserLoggedInTest.java new file mode 100644 index 000000000..af58d4049 --- /dev/null +++ b/tests/automotive/functional/mediacenter/src/android/platform/tests/NoUserLoggedInTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.platform.tests; + +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import android.platform.helpers.AutoUtility; +import android.platform.helpers.HelperAccessor; +import android.platform.helpers.IAutoHomeHelper; +import android.platform.helpers.IAutoMediaHelper; +import android.platform.test.option.StringOption; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class NoUserLoggedInTest { + + @ClassRule + public static StringOption mMediaApp = + new StringOption("media-app").setRequired(false).setDefault("YouTube Music"); + private HelperAccessor<IAutoHomeHelper> mAutoHomeHelper; + private HelperAccessor<IAutoMediaHelper> mMediaCenterHelper; + + public NoUserLoggedInTest() throws Exception { + mMediaCenterHelper = new HelperAccessor<>(IAutoMediaHelper.class); + mAutoHomeHelper = new HelperAccessor<>(IAutoHomeHelper.class); + } + + @BeforeClass + public static void exitSuw() { + AutoUtility.exitSuw(); + } + + @Test + public void testNoUserLogInMessage() { + mAutoHomeHelper.get().openMediaWidget(); + mMediaCenterHelper.get().openMediaAppMenuItems(); + mMediaCenterHelper.get().openApp(mMediaApp.get()); + + assertTrue("Not a media app.", + mMediaCenterHelper.get().getMediaAppTitle().equals(mMediaApp.get())); + + String noUserLoginMsg = mMediaCenterHelper.get().getMediaAppUserNotLoggedInErrorMessage(); + assertTrue("Incorrect Sign in error message.", + noUserLoginMsg.equals("Please sign in to YouTube Music.")); + } +} diff --git a/tests/automotive/functional/notifications/Android.bp b/tests/automotive/functional/notifications/Android.bp index c962633bf..0fe19d444 100644 --- a/tests/automotive/functional/notifications/Android.bp +++ b/tests/automotive/functional/notifications/Android.bp @@ -29,5 +29,5 @@ android_test { "hamcrest-library", ], srcs: ["src/**/*.java"], - test_suites: ["catbox"], + test_suites: ["catbox","general-tests"], } diff --git a/tests/automotive/functional/settings/Android.bp b/tests/automotive/functional/settings/Android.bp index 6f575c5a8..47322f5f3 100644 --- a/tests/automotive/functional/settings/Android.bp +++ b/tests/automotive/functional/settings/Android.bp @@ -30,5 +30,5 @@ android_test { "platform-test-options", ], srcs: ["src/**/*.java"], - test_suites: ["catbox"], + test_suites: ["catbox","general-tests"], } diff --git a/tests/automotive/health/settings/src/android/platform/scenario/settings/ScrollInApp.java b/tests/automotive/health/settings/src/android/platform/scenario/settings/ScrollInApp.java index 054e8b453..1350af3a3 100644 --- a/tests/automotive/health/settings/src/android/platform/scenario/settings/ScrollInApp.java +++ b/tests/automotive/health/settings/src/android/platform/scenario/settings/ScrollInApp.java @@ -33,7 +33,7 @@ public class ScrollInApp { @Test public void testScrollDownAndUp() { - sHelper.get().scrollDownOnePage(500); - sHelper.get().scrollUpOnePage(500); + sHelper.get().scrollDownOnePage(); + sHelper.get().scrollUpOnePage(); } } diff --git a/tests/bootdoa/Android.mk b/tests/bootdoa/Android.mk index ff86b929c..28a9c74d9 100644 --- a/tests/bootdoa/Android.mk +++ b/tests/bootdoa/Android.mk @@ -15,3 +15,4 @@ LOCAL_PATH:= $(call my-dir) $(call dist-for-goals, droidcore, $(LOCAL_PATH)/fatal_allowlist) +$(call declare-1p-target,$(LOCAL_PATH)/fatal_allowlist,platform_testing) diff --git a/tests/codecoverage/native/cpp/Android.bp b/tests/codecoverage/native/cpp/Android.bp new file mode 100644 index 000000000..5f0817722 --- /dev/null +++ b/tests/codecoverage/native/cpp/Android.bp @@ -0,0 +1,18 @@ +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +cc_test { + name: "CoverageCppSmokeTest", + + srcs: ["coverage_cpp_smoke_test.cpp"], + compile_multilib: "both", + multilib: { + lib32: { + suffix: "32", + }, + lib64: { + suffix: "64", + }, + }, +} diff --git a/tests/codecoverage/native/cpp/coverage_cpp_smoke_test.cpp b/tests/codecoverage/native/cpp/coverage_cpp_smoke_test.cpp new file mode 100644 index 000000000..2aaf18acc --- /dev/null +++ b/tests/codecoverage/native/cpp/coverage_cpp_smoke_test.cpp @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 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 <gtest/gtest.h> + +void not_covered() { + return; +} + +void covered() { + return; +} + +TEST(cpp_smoke, call_covered) { + covered(); +} diff --git a/tests/codecoverage/native/rust/Android.bp b/tests/codecoverage/native/rust/Android.bp new file mode 100644 index 000000000..94d78a670 --- /dev/null +++ b/tests/codecoverage/native/rust/Android.bp @@ -0,0 +1,19 @@ +package { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +rust_test { + name: "CoverageRustSmokeTest", + srcs: [ + "coverage_rust_smoke_test.rs", + ], + compile_multilib: "both", + multilib: { + lib32: { + suffix: "32", + }, + lib64: { + suffix: "64", + }, + }, +} diff --git a/tests/codecoverage/native/rust/coverage_rust_smoke_test.rs b/tests/codecoverage/native/rust/coverage_rust_smoke_test.rs new file mode 100644 index 000000000..8b7ec56fb --- /dev/null +++ b/tests/codecoverage/native/rust/coverage_rust_smoke_test.rs @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 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. + */ + +#[allow(dead_code)] +fn not_covered() { +} + +fn covered() { +} + +#[test] +fn test() { + covered(); +} diff --git a/tests/functional/devicehealthchecks/assets/bug_map b/tests/functional/devicehealthchecks/assets/bug_map index fe49257bb..9cbe0576a 100644 --- a/tests/functional/devicehealthchecks/assets/bug_map +++ b/tests/functional/devicehealthchecks/assets/bug_map @@ -1,12 +1,14 @@ <test_name> <regex.no.spaces> <only_bug_number> system_app_anr com.google.android.apps.wellbeing*.*ContextManagerRestartBroadcastReceiver_Receiver[\s\S]*cf_x86_phone 166183732 system_app_anr com.google.android.apps.dreamliner/.dnd.DockConditionProviderService 166174264 +system_app_anr com.google.android.gms[\s\S]*executing\sservice\scom.google.android.gms/.nearby.sharing.SharingTileService 193719277 system_app_anr act=android.hardware.usb.action.USB_STATE[\S\s]*cmp=com.google.android.projection.gearhead[\s\S]*ConnectivityEventHandlerImpl\$ConnectivityEventBroadcastReceiver 130956983 -system_app_anr com.google.android.euicc[\s\S]*executing\sservice\scom.google.android.euicc/com.android.euicc.service.EuiccServiceImpl 174479972 system_app_anr com.google.android.apps.wellbeing*.*ContextManagerRestartBroadcastReceiver_Receiver[\s\S]*cf_x86_64_phone 166183732 system_app_anr executing\sservice\scom.google.android.as/com.google.android.apps.miphone.aiai.echo.notificationintelligence.scheduler.impl.NotificationJobService 192300119 system_app_anr executing\sservice\scom.google.android.as/com.google.android.apps.miphone.aiai.echo.scheduler.EchoJobService 192300119 system_app_anr executing\sservice\scom.google.android.as/com.google.android.apps.miphone.aiai.actions.service.ActionRankingDataTtlService 192300119 +system_app_anr executing\sservice\scom.android.se/.SecureElementService 199457346 +system_app_anr act=android.telephony.action.CARRIER_CONFIG_CHANGED\sflg=0x15000010\scmp=com.android.phone/.otasp.OtaspSimStateReceiver 205896452 system_app_crash -1\|android\|26\|null\|1000 155073214 system_app_crash -1\|android\|32\|null\|1000 155073214 system_app_crash android.database.sqlite.SQLiteCloseable.acquireReference 159658068 @@ -20,10 +22,12 @@ system_app_native_crash ReferenceQueueD*.*com.google.android.apps.safetyhub 1621 system_app_native_crash Binder*.*com.google.android.apps.safetyhub 162104694 system_app_native_crash Lite\sThread*.*com.google.android.apps.safetyhub 162379378 system_app_native_crash BG\sThread*.*com.google.android.apps.safetyhub 162381002 +system_app_native_crash FireflyProcMgr\s+>>>\scom.google.android.GoogleCamera\s<<<[\S\s]+SIGSEGV[\S\s]+null\spointer\sdereference 195519497 SYSTEM_TOMBSTONE Binder*.*com.google.android.apps.safetyhub 162104694 SYSTEM_TOMBSTONE ReferenceQueueD*.*com.google.android.apps.safetyhub 162103095 SYSTEM_TOMBSTONE Lite\sThread*.*com.google.android.apps.safetyhub 162379378 SYSTEM_TOMBSTONE BG\sThread*.*com.google.android.apps.safetyhub 162381002 +SYSTEM_TOMBSTONE FireflyProcMgr\s+>>>\scom.google.android.GoogleCamera\s<<<[\S\s]+SIGSEGV[\S\s]+null\spointer\sdereference 195519497 SYSTEM_TOMBSTONE AsyncTask\s+#1\s+>>>\s+com.android.nfc\s+<<<[\S\s]*nfaDeviceManagementCallback[\S\s]*nfc_ncif_cmd_timeout 172057778 SYSTEM_TOMBSTONE >>>\s+/apex/com.android.os.statsd/bin/statsd\s+<<<[\S\s]*HandleUsingDestroyedMutex 172829930 SYSTEM_TOMBSTONE audio.service\s+>>>\s+/vendor/bin/hw/android.hardware.audio.service\s+<<<[\S\s]*debuggerd\ssignal[\S\s]*audio_extn_utils_get_snd_card_num 174265816 diff --git a/tests/functional/devicehealthchecks/src/com/android/devicehealthchecks/CrashCheckBase.java b/tests/functional/devicehealthchecks/src/com/android/devicehealthchecks/CrashCheckBase.java index 191e9b885..23301a75b 100644 --- a/tests/functional/devicehealthchecks/src/com/android/devicehealthchecks/CrashCheckBase.java +++ b/tests/functional/devicehealthchecks/src/com/android/devicehealthchecks/CrashCheckBase.java @@ -115,6 +115,7 @@ abstract class CrashCheckBase { if (matcher.find()) { errorDetails.append(line); if (scanner.hasNextLine()) { + errorDetails.append("\n"); errorDetails.append(scanner.nextLine()); } break; diff --git a/tests/health/scenarios/src/android/platform/test/scenario/annotation/LargeScreenOnly.java b/tests/health/scenarios/src/android/platform/test/scenario/annotation/LargeScreenOnly.java new file mode 100644 index 000000000..3711d3cf3 --- /dev/null +++ b/tests/health/scenarios/src/android/platform/test/scenario/annotation/LargeScreenOnly.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.platform.test.scenario.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Identifies scenario that should be run only on large screen devices. Note that this annotation + * doesn't do any filtering for screen size, it's up to the test author to annotate the test + * correctly. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD}) +public @interface LargeScreenOnly {} |