1 /* <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 package com.android.launcher3.util.rule 17 18 import android.app.Activity 19 import android.app.Application 20 import android.media.permission.SafeCloseable 21 import android.os.Bundle 22 import androidx.test.core.app.ApplicationProvider 23 import androidx.test.platform.app.InstrumentationRegistry 24 import com.android.app.viewcapture.SimpleViewCapture 25 import com.android.app.viewcapture.ViewCapture.MAIN_EXECUTOR 26 import com.android.app.viewcapture.data.ExportedData 27 import com.android.launcher3.tapl.TestHelpers 28 import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter 29 import com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT 30 import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer 31 import java.io.BufferedOutputStream 32 import java.io.FileOutputStream 33 import java.io.IOException 34 import java.io.OutputStreamWriter 35 import java.util.function.Supplier 36 import org.junit.Assert.assertTrue 37 import org.junit.Assert.fail 38 import org.junit.rules.TestRule 39 import org.junit.runner.Description 40 import org.junit.runners.model.Statement 41 42 /** 43 * This JUnit TestRule registers a listener for activity lifecycle events to attach a ViewCapture 44 * instance that other test rules use to dump the timelapse hierarchy upon an error during a test. 45 * 46 * This rule will not work in OOP tests that don't have access to the activity under test. 47 */ 48 class ViewCaptureRule(var alreadyOpenActivitySupplier: Supplier<Activity?>) : TestRule { 49 private val viewCapture = SimpleViewCapture("test-view-capture") 50 var viewCaptureData: ExportedData? = null 51 private set 52 53 override fun apply(base: Statement, description: Description): Statement { 54 // Skip view capture collection in Launcher3 tests to avoid hidden API check exception. 55 if ( 56 "com.android.launcher3.tests" == 57 InstrumentationRegistry.getInstrumentation().context.packageName 58 ) 59 return base 60 61 return object : Statement() { 62 override fun evaluate() { 63 viewCaptureData = null 64 val windowListenerCloseables = mutableListOf<SafeCloseable>() 65 66 startCapturingExistingActivity(windowListenerCloseables) 67 68 val lifecycleCallbacks = 69 object : ActivityLifecycleCallbacksAdapter { 70 override fun onActivityCreated(activity: Activity, bundle: Bundle?) { 71 startCapture(windowListenerCloseables, activity) 72 } 73 74 override fun onActivityDestroyed(activity: Activity) { 75 viewCapture.stopCapture(activity.window.decorView) 76 } 77 } 78 79 val application = ApplicationProvider.getApplicationContext<Application>() 80 application.registerActivityLifecycleCallbacks(lifecycleCallbacks) 81 82 try { 83 base.evaluate() 84 } finally { 85 application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks) 86 87 viewCaptureData = 88 viewCapture.getExportedData(ApplicationProvider.getApplicationContext()) 89 90 // Clean up ViewCapture references here rather than in onActivityDestroyed so 91 // test code can access view hierarchy capture. onActivityDestroyed would delete 92 // view capture data before FailureWatcher could output it as a test artifact. 93 // This is on the main thread to avoid a race condition where the onDrawListener 94 // is removed while onDraw is running, resulting in an IllegalStateException. 95 MAIN_EXECUTOR.execute { windowListenerCloseables.onEach(SafeCloseable::close) } 96 } 97 98 analyzeViewCapture(description) 99 } 100 101 private fun startCapturingExistingActivity( 102 windowListenerCloseables: MutableCollection<SafeCloseable> 103 ) { 104 val alreadyOpenActivity = alreadyOpenActivitySupplier.get() 105 if (alreadyOpenActivity != null) { 106 startCapture(windowListenerCloseables, alreadyOpenActivity) 107 } 108 } 109 110 private fun startCapture( 111 windowListenerCloseables: MutableCollection<SafeCloseable>, 112 activity: Activity 113 ) { 114 windowListenerCloseables.add( 115 viewCapture.startCapture( 116 activity.window.decorView, 117 "${description.testClass?.simpleName}.${description.methodName}" 118 ) 119 ) 120 } 121 } 122 } 123 124 private fun analyzeViewCapture(description: Description) { 125 // OOP tests don't produce ViewCapture data 126 if (!TestHelpers.isInLauncherProcess()) return 127 128 // Due to flakiness of ViewCapture verifier, don't run the check in presubmit 129 if (TestStabilityRule.getRunFlavor() != PLATFORM_POSTSUBMIT) return 130 131 var frameCount = 0 132 for (i in 0 until viewCaptureData!!.windowDataCount) { 133 frameCount += viewCaptureData!!.getWindowData(i).frameDataCount 134 } 135 136 val mayProduceNoFrames = description.getAnnotation(MayProduceNoFrames::class.java) != null 137 assertTrue("Empty ViewCapture data", mayProduceNoFrames || frameCount > 0) 138 139 val anomalies: Map<String, String> = ViewCaptureAnalyzer.getAnomalies(viewCaptureData) 140 if (!anomalies.isEmpty()) { 141 val diagFile = FailureWatcher.diagFile(description, "ViewAnomalies", "txt") 142 try { 143 OutputStreamWriter(BufferedOutputStream(FileOutputStream(diagFile))).use { writer -> 144 writer.write("View animation anomalies detected.\r\n") 145 writer.write( 146 "To suppress an anomaly for a view, add its full path to the PATHS_TO_IGNORE list in the corresponding AnomalyDetector.\r\n" 147 ) 148 writer.write("List of views with animation anomalies:\r\n") 149 150 for ((viewPath, message) in anomalies) { 151 writer.write("View: $viewPath\r\n $message\r\n") 152 } 153 } 154 } catch (ex: IOException) { 155 throw RuntimeException(ex) 156 } 157 158 val (viewPath, message) = anomalies.entries.first() 159 fail( 160 "${anomalies.size} view(s) had animation anomalies during the test, including view: $viewPath: $message\r\nSee ${diagFile.name} for details." 161 ) 162 } 163 } 164 165 @Retention(AnnotationRetention.RUNTIME) 166 @Target(AnnotationTarget.FUNCTION) 167 annotation class MayProduceNoFrames 168 } 169