Loading tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt +33 −5 Original line number Diff line number Diff line Loading @@ -26,8 +26,13 @@ import com.android.app.viewcapture.data.ExportedData import com.android.launcher3.tapl.TestHelpers import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer import org.junit.Assert.assertTrue import java.io.BufferedOutputStream import java.io.FileOutputStream import java.io.IOException import java.io.OutputStreamWriter import java.util.function.Supplier import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement Loading Loading @@ -81,7 +86,7 @@ class ViewCaptureRule(var alreadyOpenActivitySupplier: Supplier<Activity?>) : Te MAIN_EXECUTOR.execute { windowListenerCloseables.onEach(SafeCloseable::close) } } analyzeViewCapture() analyzeViewCapture(description) } private fun startCapturingExistingActivity( Loading @@ -107,16 +112,39 @@ class ViewCaptureRule(var alreadyOpenActivitySupplier: Supplier<Activity?>) : Te } } private fun analyzeViewCapture() { private fun analyzeViewCapture(description: Description) { // OOP tests don't produce ViewCapture data if (!TestHelpers.isInLauncherProcess()) return ViewCaptureAnalyzer.assertNoAnomalies(viewCaptureData) var frameCount = 0 for (i in 0 until viewCaptureData!!.windowDataCount) { frameCount += viewCaptureData!!.getWindowData(i).frameDataCount } assertTrue("Empty ViewCapture data", frameCount > 0) val anomalies: Map<String, String> = ViewCaptureAnalyzer.getAnomalies(viewCaptureData) if (!anomalies.isEmpty()) { val diagFile = FailureWatcher.diagFile(description, "ViewAnomalies", "txt") try { OutputStreamWriter(BufferedOutputStream(FileOutputStream(diagFile))).use { writer -> writer.write("View animation anomalies detected.\r\n") writer.write( "To suppress an anomaly for a view, add its full path to the PATHS_TO_IGNORE list in the corresponding AnomalyDetector.\r\n" ) writer.write("List of views with animation anomalies:\r\n") for ((viewPath, message) in anomalies) { writer.write("View: $viewPath\r\n $message\r\n") } } } catch (ex: IOException) { throw RuntimeException(ex) } val (viewPath, message) = anomalies.entries.first() fail( "${anomalies.size} view(s) had animation anomalies during the test, including view: $viewPath: $message\r\nSee ${diagFile.name} for details." ) } } } tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java +10 −8 Original line number Diff line number Diff line Loading @@ -212,24 +212,26 @@ final class AlphaJumpDetector extends AnomalyDetector { } @Override void detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN) { String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN) { // If the view was previously seen, proceed with analysis only if it was present in the // view hierarchy in the previous frame. if (oldInfo != null && oldInfo.frameN != frameN) return; if (oldInfo != null && oldInfo.frameN != frameN) return null; final AnalysisNode latestInfo = newInfo != null ? newInfo : oldInfo; if (getNodeData(latestInfo).ignoreAlphaJumps) return; final NodeData nodeData = getNodeData(latestInfo); if (nodeData.ignoreAlphaJumps) return null; final float oldAlpha = oldInfo != null ? oldInfo.alpha : 0; final float newAlpha = newInfo != null ? newInfo.alpha : 0; final float alphaDeltaAbs = Math.abs(newAlpha - oldAlpha); if (alphaDeltaAbs >= ALPHA_JUMP_THRESHOLD) { throw new AssertionError( String.format( nodeData.ignoreAlphaJumps = true; // No need to report alpha jump in children. return String.format( "Alpha jump detected in ViewCapture data: alpha change: %s (%s -> %s)" + ", threshold: %s, view: %s", alphaDeltaAbs, oldAlpha, newAlpha, ALPHA_JUMP_THRESHOLD, latestInfo)); + ", threshold: %s, %s", // ----------- no need to include view? alphaDeltaAbs, oldAlpha, newAlpha, ALPHA_JUMP_THRESHOLD, latestInfo); } return null; } } tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java +52 −21 Original line number Diff line number Diff line Loading @@ -61,8 +61,9 @@ public class ViewCaptureAnalyzer { * the view is not present in the 'currentFrame', but was present in earlier * frames. * @param frameN number of the current frame. * @return Anomaly diagnostic message if an anomaly has been detected; null otherwise. */ abstract void detectAnomalies( abstract String detectAnomalies( @Nullable AnalysisNode oldInfo, @Nullable AnalysisNode newInfo, int frameN); } Loading Loading @@ -101,25 +102,31 @@ public class ViewCaptureAnalyzer { @Override public String toString() { return String.format("window coordinates: (%s, %s), class path from the root: %s", left, top, diagPathFromRoot(this)); return String.format("view window coordinates: (%s, %s)", left, top); } } /** * Scans a view capture record and throws an error if an anomaly is found. * Scans a view capture record and searches for view animation anomalies. Can find anomalies for * multiple views. * Returns a map from the view path to the anomaly message for the view. Non-empty map means * that anomalies were detected. */ public static void assertNoAnomalies(ExportedData viewCaptureData) { public static Map<String, String> getAnomalies(ExportedData viewCaptureData) { final Map<String, String> anomalies = new HashMap<>(); final int scrimClassIndex = viewCaptureData.getClassnameList().indexOf(SCRIM_VIEW_CLASS); final int windowDataCount = viewCaptureData.getWindowDataCount(); for (int i = 0; i < windowDataCount; ++i) { analyzeWindowData(viewCaptureData, viewCaptureData.getWindowData(i), scrimClassIndex); analyzeWindowData( viewCaptureData, viewCaptureData.getWindowData(i), scrimClassIndex, anomalies); } return anomalies; } private static void analyzeWindowData(ExportedData viewCaptureData, WindowData windowData, int scrimClassIndex) { int scrimClassIndex, Map<String, String> anomalies) { // View hash code => Last seen node with this hash code. // The view is added when we analyze the first frame where it's visible. // After that, it gets updated for every frame where it's visible. Loading @@ -128,12 +135,13 @@ public class ViewCaptureAnalyzer { for (int frameN = 0; frameN < windowData.getFrameDataCount(); ++frameN) { analyzeFrame(frameN, windowData.getFrameData(frameN), viewCaptureData, lastSeenNodes, scrimClassIndex); scrimClassIndex, anomalies); } } private static void analyzeFrame(int frameN, FrameData frame, ExportedData viewCaptureData, Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex) { Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex, Map<String, String> anomalies) { // Analyze the node tree starting from the root. analyzeView( frame.getNode(), Loading @@ -143,7 +151,8 @@ public class ViewCaptureAnalyzer { /* topShift = */ 0, viewCaptureData, lastSeenNodes, scrimClassIndex); scrimClassIndex, anomalies); // Analyze transitions when a view visible in the last frame become invisible in the // current one. Loading @@ -151,10 +160,14 @@ public class ViewCaptureAnalyzer { if (info.frameN == frameN - 1) { if (!info.viewCaptureNode.getWillNotDraw()) { Arrays.stream(ANOMALY_DETECTORS).forEach( detector -> detector.detectAnomalies( detector -> detectAnomaly( detector, frameN, /* oldInfo = */ info, /* newInfo = */ null, frameN)); anomalies) ); } } } Loading @@ -162,7 +175,8 @@ public class ViewCaptureAnalyzer { private static void analyzeView(ViewNode viewCaptureNode, AnalysisNode parent, int frameN, float leftShift, float topShift, ExportedData viewCaptureData, Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex) { Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex, Map<String, String> anomalies) { // Skip analysis of invisible views final float parentAlpha = parent != null ? parent.alpha : 1; final float alpha = getVisibleAlpha(viewCaptureNode, parentAlpha); Loading Loading @@ -205,7 +219,10 @@ public class ViewCaptureAnalyzer { final AnalysisNode oldAnalysisNode = lastSeenNodes.get(hashcode); // may be null if (frameN != 0 && !viewCaptureNode.getWillNotDraw()) { Arrays.stream(ANOMALY_DETECTORS).forEach( detector -> detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN)); detector -> detectAnomaly(detector, frameN, oldAnalysisNode, newAnalysisNode, anomalies) ); } lastSeenNodes.put(hashcode, newAnalysisNode); Loading @@ -220,8 +237,20 @@ public class ViewCaptureAnalyzer { if (child.getClassnameIndex() == scrimClassIndex) break; analyzeView(child, newAnalysisNode, frameN, leftShiftForChildren, topShiftForChildren, viewCaptureData, lastSeenNodes, scrimClassIndex); viewCaptureData, lastSeenNodes, scrimClassIndex, anomalies); } } private static void detectAnomaly(AnomalyDetector detector, int frameN, AnalysisNode oldAnalysisNode, AnalysisNode newAnalysisNode, Map<String, String> anomalies) { final String maybeAnomaly = detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN); if (maybeAnomaly != null) { final String viewDiagPath = diagPathFromRoot(newAnalysisNode); if (!anomalies.containsKey(viewDiagPath)) { anomalies.put(viewDiagPath, maybeAnomaly); } } } Loading @@ -235,9 +264,11 @@ public class ViewCaptureAnalyzer { return className.substring(className.lastIndexOf(".") + 1); } private static String diagPathFromRoot(AnalysisNode nodeBox) { final StringBuilder path = new StringBuilder(nodeBox.nodeIdentity); for (AnalysisNode ancestor = nodeBox.parent; ancestor != null; ancestor = ancestor.parent) { private static String diagPathFromRoot(AnalysisNode analysisNode) { final StringBuilder path = new StringBuilder(analysisNode.nodeIdentity); for (AnalysisNode ancestor = analysisNode.parent; ancestor != null; ancestor = ancestor.parent) { path.insert(0, ancestor.nodeIdentity + "|"); } return path.toString(); Loading Loading
tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt +33 −5 Original line number Diff line number Diff line Loading @@ -26,8 +26,13 @@ import com.android.app.viewcapture.data.ExportedData import com.android.launcher3.tapl.TestHelpers import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter import com.android.launcher3.util.viewcapture_analysis.ViewCaptureAnalyzer import org.junit.Assert.assertTrue import java.io.BufferedOutputStream import java.io.FileOutputStream import java.io.IOException import java.io.OutputStreamWriter import java.util.function.Supplier import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement Loading Loading @@ -81,7 +86,7 @@ class ViewCaptureRule(var alreadyOpenActivitySupplier: Supplier<Activity?>) : Te MAIN_EXECUTOR.execute { windowListenerCloseables.onEach(SafeCloseable::close) } } analyzeViewCapture() analyzeViewCapture(description) } private fun startCapturingExistingActivity( Loading @@ -107,16 +112,39 @@ class ViewCaptureRule(var alreadyOpenActivitySupplier: Supplier<Activity?>) : Te } } private fun analyzeViewCapture() { private fun analyzeViewCapture(description: Description) { // OOP tests don't produce ViewCapture data if (!TestHelpers.isInLauncherProcess()) return ViewCaptureAnalyzer.assertNoAnomalies(viewCaptureData) var frameCount = 0 for (i in 0 until viewCaptureData!!.windowDataCount) { frameCount += viewCaptureData!!.getWindowData(i).frameDataCount } assertTrue("Empty ViewCapture data", frameCount > 0) val anomalies: Map<String, String> = ViewCaptureAnalyzer.getAnomalies(viewCaptureData) if (!anomalies.isEmpty()) { val diagFile = FailureWatcher.diagFile(description, "ViewAnomalies", "txt") try { OutputStreamWriter(BufferedOutputStream(FileOutputStream(diagFile))).use { writer -> writer.write("View animation anomalies detected.\r\n") writer.write( "To suppress an anomaly for a view, add its full path to the PATHS_TO_IGNORE list in the corresponding AnomalyDetector.\r\n" ) writer.write("List of views with animation anomalies:\r\n") for ((viewPath, message) in anomalies) { writer.write("View: $viewPath\r\n $message\r\n") } } } catch (ex: IOException) { throw RuntimeException(ex) } val (viewPath, message) = anomalies.entries.first() fail( "${anomalies.size} view(s) had animation anomalies during the test, including view: $viewPath: $message\r\nSee ${diagFile.name} for details." ) } } }
tests/src/com/android/launcher3/util/viewcapture_analysis/AlphaJumpDetector.java +10 −8 Original line number Diff line number Diff line Loading @@ -212,24 +212,26 @@ final class AlphaJumpDetector extends AnomalyDetector { } @Override void detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN) { String detectAnomalies(AnalysisNode oldInfo, AnalysisNode newInfo, int frameN) { // If the view was previously seen, proceed with analysis only if it was present in the // view hierarchy in the previous frame. if (oldInfo != null && oldInfo.frameN != frameN) return; if (oldInfo != null && oldInfo.frameN != frameN) return null; final AnalysisNode latestInfo = newInfo != null ? newInfo : oldInfo; if (getNodeData(latestInfo).ignoreAlphaJumps) return; final NodeData nodeData = getNodeData(latestInfo); if (nodeData.ignoreAlphaJumps) return null; final float oldAlpha = oldInfo != null ? oldInfo.alpha : 0; final float newAlpha = newInfo != null ? newInfo.alpha : 0; final float alphaDeltaAbs = Math.abs(newAlpha - oldAlpha); if (alphaDeltaAbs >= ALPHA_JUMP_THRESHOLD) { throw new AssertionError( String.format( nodeData.ignoreAlphaJumps = true; // No need to report alpha jump in children. return String.format( "Alpha jump detected in ViewCapture data: alpha change: %s (%s -> %s)" + ", threshold: %s, view: %s", alphaDeltaAbs, oldAlpha, newAlpha, ALPHA_JUMP_THRESHOLD, latestInfo)); + ", threshold: %s, %s", // ----------- no need to include view? alphaDeltaAbs, oldAlpha, newAlpha, ALPHA_JUMP_THRESHOLD, latestInfo); } return null; } }
tests/src/com/android/launcher3/util/viewcapture_analysis/ViewCaptureAnalyzer.java +52 −21 Original line number Diff line number Diff line Loading @@ -61,8 +61,9 @@ public class ViewCaptureAnalyzer { * the view is not present in the 'currentFrame', but was present in earlier * frames. * @param frameN number of the current frame. * @return Anomaly diagnostic message if an anomaly has been detected; null otherwise. */ abstract void detectAnomalies( abstract String detectAnomalies( @Nullable AnalysisNode oldInfo, @Nullable AnalysisNode newInfo, int frameN); } Loading Loading @@ -101,25 +102,31 @@ public class ViewCaptureAnalyzer { @Override public String toString() { return String.format("window coordinates: (%s, %s), class path from the root: %s", left, top, diagPathFromRoot(this)); return String.format("view window coordinates: (%s, %s)", left, top); } } /** * Scans a view capture record and throws an error if an anomaly is found. * Scans a view capture record and searches for view animation anomalies. Can find anomalies for * multiple views. * Returns a map from the view path to the anomaly message for the view. Non-empty map means * that anomalies were detected. */ public static void assertNoAnomalies(ExportedData viewCaptureData) { public static Map<String, String> getAnomalies(ExportedData viewCaptureData) { final Map<String, String> anomalies = new HashMap<>(); final int scrimClassIndex = viewCaptureData.getClassnameList().indexOf(SCRIM_VIEW_CLASS); final int windowDataCount = viewCaptureData.getWindowDataCount(); for (int i = 0; i < windowDataCount; ++i) { analyzeWindowData(viewCaptureData, viewCaptureData.getWindowData(i), scrimClassIndex); analyzeWindowData( viewCaptureData, viewCaptureData.getWindowData(i), scrimClassIndex, anomalies); } return anomalies; } private static void analyzeWindowData(ExportedData viewCaptureData, WindowData windowData, int scrimClassIndex) { int scrimClassIndex, Map<String, String> anomalies) { // View hash code => Last seen node with this hash code. // The view is added when we analyze the first frame where it's visible. // After that, it gets updated for every frame where it's visible. Loading @@ -128,12 +135,13 @@ public class ViewCaptureAnalyzer { for (int frameN = 0; frameN < windowData.getFrameDataCount(); ++frameN) { analyzeFrame(frameN, windowData.getFrameData(frameN), viewCaptureData, lastSeenNodes, scrimClassIndex); scrimClassIndex, anomalies); } } private static void analyzeFrame(int frameN, FrameData frame, ExportedData viewCaptureData, Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex) { Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex, Map<String, String> anomalies) { // Analyze the node tree starting from the root. analyzeView( frame.getNode(), Loading @@ -143,7 +151,8 @@ public class ViewCaptureAnalyzer { /* topShift = */ 0, viewCaptureData, lastSeenNodes, scrimClassIndex); scrimClassIndex, anomalies); // Analyze transitions when a view visible in the last frame become invisible in the // current one. Loading @@ -151,10 +160,14 @@ public class ViewCaptureAnalyzer { if (info.frameN == frameN - 1) { if (!info.viewCaptureNode.getWillNotDraw()) { Arrays.stream(ANOMALY_DETECTORS).forEach( detector -> detector.detectAnomalies( detector -> detectAnomaly( detector, frameN, /* oldInfo = */ info, /* newInfo = */ null, frameN)); anomalies) ); } } } Loading @@ -162,7 +175,8 @@ public class ViewCaptureAnalyzer { private static void analyzeView(ViewNode viewCaptureNode, AnalysisNode parent, int frameN, float leftShift, float topShift, ExportedData viewCaptureData, Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex) { Map<Integer, AnalysisNode> lastSeenNodes, int scrimClassIndex, Map<String, String> anomalies) { // Skip analysis of invisible views final float parentAlpha = parent != null ? parent.alpha : 1; final float alpha = getVisibleAlpha(viewCaptureNode, parentAlpha); Loading Loading @@ -205,7 +219,10 @@ public class ViewCaptureAnalyzer { final AnalysisNode oldAnalysisNode = lastSeenNodes.get(hashcode); // may be null if (frameN != 0 && !viewCaptureNode.getWillNotDraw()) { Arrays.stream(ANOMALY_DETECTORS).forEach( detector -> detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN)); detector -> detectAnomaly(detector, frameN, oldAnalysisNode, newAnalysisNode, anomalies) ); } lastSeenNodes.put(hashcode, newAnalysisNode); Loading @@ -220,8 +237,20 @@ public class ViewCaptureAnalyzer { if (child.getClassnameIndex() == scrimClassIndex) break; analyzeView(child, newAnalysisNode, frameN, leftShiftForChildren, topShiftForChildren, viewCaptureData, lastSeenNodes, scrimClassIndex); viewCaptureData, lastSeenNodes, scrimClassIndex, anomalies); } } private static void detectAnomaly(AnomalyDetector detector, int frameN, AnalysisNode oldAnalysisNode, AnalysisNode newAnalysisNode, Map<String, String> anomalies) { final String maybeAnomaly = detector.detectAnomalies(oldAnalysisNode, newAnalysisNode, frameN); if (maybeAnomaly != null) { final String viewDiagPath = diagPathFromRoot(newAnalysisNode); if (!anomalies.containsKey(viewDiagPath)) { anomalies.put(viewDiagPath, maybeAnomaly); } } } Loading @@ -235,9 +264,11 @@ public class ViewCaptureAnalyzer { return className.substring(className.lastIndexOf(".") + 1); } private static String diagPathFromRoot(AnalysisNode nodeBox) { final StringBuilder path = new StringBuilder(nodeBox.nodeIdentity); for (AnalysisNode ancestor = nodeBox.parent; ancestor != null; ancestor = ancestor.parent) { private static String diagPathFromRoot(AnalysisNode analysisNode) { final StringBuilder path = new StringBuilder(analysisNode.nodeIdentity); for (AnalysisNode ancestor = analysisNode.parent; ancestor != null; ancestor = ancestor.parent) { path.insert(0, ancestor.nodeIdentity + "|"); } return path.toString(); Loading