From 337bbfbe042a89c0a887c895fa3aa364decc7077 Mon Sep 17 00:00:00 2001 From: Matthew Kosarek Date: Mon, 24 Nov 2025 08:47:20 -0500 Subject: [PATCH] Potentially fixing the flakiness in win32 windowing tests, but it needs some running (#178499) Adding some more logging and retrying mechanisms to the win32 windowing tests to see if it fixes the flakiness. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --- .../windowing_test/lib/main.dart | 257 +++++++++--------- .../windowing_test/test_driver/main_test.dart | 145 +++++++--- 2 files changed, 244 insertions(+), 158 deletions(-) diff --git a/dev/integration_tests/windowing_test/lib/main.dart b/dev/integration_tests/windowing_test/lib/main.dart index e20c08d096d..4024ec063f6 100644 --- a/dev/integration_tests/windowing_test/lib/main.dart +++ b/dev/integration_tests/windowing_test/lib/main.dart @@ -32,138 +32,149 @@ void main() { final Completer windowCreated = Completer(); enableFlutterDriverExtension( handler: (String? message) async { - await windowCreated.future; - if (message == null) { - return ''; - } - - final jsonMap = jsonDecode(message); - if (!jsonMap.containsKey('type')) { - throw ArgumentError('Message must contain a "type" field.'); - } - - /// This helper method registers a listener on the controller, - /// calls [act] to perform some action on the controller, waits for - /// the [predicate] to be satisified, and finally cleans up the listener. - Future awaitNotification( - VoidCallback act, - bool Function() predicate, - ) async { - final StreamController streamController = StreamController(); - void notificationHandler() { - streamController.add(true); + try { + await windowCreated.future; + if (message == null) { + return ''; } - controller.addListener(notificationHandler); - - act(); - - // On macOS, the predicate is immediately true, even though - // the animation is in progress and next request for state change - // will be ignored. Easiest way to handle this is to just wait. - if (defaultTargetPlatform == TargetPlatform.macOS) { - await Future.delayed(Duration(seconds: 1)); + final jsonMap = jsonDecode(message); + if (!jsonMap.containsKey('type')) { + throw ArgumentError('Message must contain a "type" field.'); } - await for (final _ in streamController.stream) { - if (predicate()) { - break; + /// This helper method registers a listener on the controller, + /// calls [act] to perform some action on the controller, waits for + /// the [predicate] to be satisified, and finally cleans up the listener. + Future awaitNotification( + VoidCallback act, + bool Function() predicate, + ) async { + final StreamController streamController = StreamController(); + void notificationHandler() { + streamController.add(true); + } + + controller.addListener(notificationHandler); + + try { + act(); + + // On macOS, the predicate is immediately true, even though + // the animation is in progress and next request for state change + // will be ignored. Easiest way to handle this is to just wait. + if (defaultTargetPlatform == TargetPlatform.macOS) { + await Future.delayed(Duration(seconds: 1)); + } + + // Add a timeout to avoid hanging indefinitely + await for (final _ in streamController.stream.timeout( + Duration(seconds: 10), + )) { + if (predicate()) { + break; + } + } + } finally { + controller.removeListener(notificationHandler); + await streamController.close(); } } - controller.removeListener(notificationHandler); - } - if (jsonMap['type'] == 'ping') { - return jsonEncode({'type': 'pong'}); - } else if (jsonMap['type'] == 'get_size') { - return jsonEncode({ - 'width': controller.contentSize.width, - 'height': controller.contentSize.height, - }); - } else if (jsonMap['type'] == 'set_size') { - final Size size = Size( - jsonMap['width'].toDouble(), - jsonMap['height'].toDouble(), - ); - await awaitNotification(() { - controller.setSize(size); - }, () => controller.contentSize == size); - } else if (jsonMap['type'] == 'set_constraints') { - final BoxConstraints constraints = BoxConstraints( - minWidth: jsonMap['min_width'].toDouble(), - minHeight: jsonMap['min_height'].toDouble(), - maxWidth: jsonMap['max_width'].toDouble(), - maxHeight: jsonMap['max_height'].toDouble(), - ); - // We assume that this will cause a resize, which the current tests do. - final initialSize = controller.contentSize; - await awaitNotification(() { - controller.setConstraints(constraints); - }, () => controller.contentSize != initialSize); - } else if (jsonMap['type'] == 'set_fullscreen') { - await awaitNotification(() { - controller.setFullscreen(true); - }, () => controller.isFullscreen); - } else if (jsonMap['type'] == 'unset_fullscreen') { - await awaitNotification(() { - controller.setFullscreen(false); - }, () => !controller.isFullscreen); - } else if (jsonMap['type'] == 'get_fullscreen') { - return jsonEncode({'isFullscreen': controller.isFullscreen}); - } else if (jsonMap['type'] == 'set_maximized') { - await awaitNotification(() { - controller.setMaximized(true); - }, () => controller.isMaximized); - } else if (jsonMap['type'] == 'unset_maximized') { - await awaitNotification(() { - controller.setMaximized(false); - }, () => !controller.isMaximized); - } else if (jsonMap['type'] == 'get_maximized') { - return jsonEncode({'isMaximized': controller.isMaximized}); - } else if (jsonMap['type'] == 'set_minimized') { - await awaitNotification(() { - controller.setMinimized(true); - }, () => controller.isMinimized); - } else if (jsonMap['type'] == 'unset_minimized') { - await awaitNotification(() { - controller.setMinimized(false); - }, () => !controller.isMinimized); - } else if (jsonMap['type'] == 'get_minimized') { - return jsonEncode({'isMinimized': controller.isMinimized}); - } else if (jsonMap['type'] == 'set_title') { - final String title = jsonMap['title']; - await awaitNotification(() { - controller.setTitle(title); - }, () => controller.title == title); - } else if (jsonMap['type'] == 'get_title') { - return jsonEncode({'title': controller.title}); - } else if (jsonMap['type'] == 'set_activated') { - await awaitNotification(() { - controller.activate(); - }, () => controller.isActivated); - } else if (jsonMap['type'] == 'get_activated') { - return jsonEncode({'isActivated': controller.isActivated}); - } else if (jsonMap['type'] == 'open_dialog') { - if (dialogController.value != null) { - return jsonEncode({'result': false}); + if (jsonMap['type'] == 'ping') { + return jsonEncode({'type': 'pong'}); + } else if (jsonMap['type'] == 'get_size') { + return jsonEncode({ + 'width': controller.contentSize.width, + 'height': controller.contentSize.height, + }); + } else if (jsonMap['type'] == 'set_size') { + final Size size = Size( + jsonMap['width'].toDouble(), + jsonMap['height'].toDouble(), + ); + await awaitNotification(() { + controller.setSize(size); + }, () => controller.contentSize == size); + } else if (jsonMap['type'] == 'set_constraints') { + final BoxConstraints constraints = BoxConstraints( + minWidth: jsonMap['min_width'].toDouble(), + minHeight: jsonMap['min_height'].toDouble(), + maxWidth: jsonMap['max_width'].toDouble(), + maxHeight: jsonMap['max_height'].toDouble(), + ); + // We assume that this will cause a resize, which the current tests do. + final initialSize = controller.contentSize; + await awaitNotification(() { + controller.setConstraints(constraints); + }, () => controller.contentSize != initialSize); + } else if (jsonMap['type'] == 'set_fullscreen') { + await awaitNotification(() { + controller.setFullscreen(true); + }, () => controller.isFullscreen); + } else if (jsonMap['type'] == 'unset_fullscreen') { + await awaitNotification(() { + controller.setFullscreen(false); + }, () => !controller.isFullscreen); + } else if (jsonMap['type'] == 'get_fullscreen') { + return jsonEncode({'isFullscreen': controller.isFullscreen}); + } else if (jsonMap['type'] == 'set_maximized') { + await awaitNotification(() { + controller.setMaximized(true); + }, () => controller.isMaximized); + } else if (jsonMap['type'] == 'unset_maximized') { + await awaitNotification(() { + controller.setMaximized(false); + }, () => !controller.isMaximized); + } else if (jsonMap['type'] == 'get_maximized') { + return jsonEncode({'isMaximized': controller.isMaximized}); + } else if (jsonMap['type'] == 'set_minimized') { + await awaitNotification(() { + controller.setMinimized(true); + }, () => controller.isMinimized); + } else if (jsonMap['type'] == 'unset_minimized') { + await awaitNotification(() { + controller.setMinimized(false); + }, () => !controller.isMinimized); + } else if (jsonMap['type'] == 'get_minimized') { + return jsonEncode({'isMinimized': controller.isMinimized}); + } else if (jsonMap['type'] == 'set_title') { + final String title = jsonMap['title']; + await awaitNotification(() { + controller.setTitle(title); + }, () => controller.title == title); + } else if (jsonMap['type'] == 'get_title') { + return jsonEncode({'title': controller.title}); + } else if (jsonMap['type'] == 'set_activated') { + await awaitNotification(() { + controller.activate(); + }, () => controller.isActivated); + } else if (jsonMap['type'] == 'get_activated') { + return jsonEncode({'isActivated': controller.isActivated}); + } else if (jsonMap['type'] == 'open_dialog') { + if (dialogController.value != null) { + return jsonEncode({'result': false}); + } + dialogController.value = DialogWindowController( + preferredSize: const Size(200, 200), + parent: controller, + delegate: MyDialogWindowControllerDelegate( + onDestroyed: () { + dialogController.value = null; + }, + ), + ); + return jsonEncode({'result': true}); + } else if (jsonMap['type'] == 'close_dialog') { + dialogController.value?.destroy(); + return jsonEncode({'result': true}); + } else { + throw ArgumentError('Unknown message type: ${jsonMap['type']}'); } - dialogController.value = DialogWindowController( - preferredSize: const Size(200, 200), - parent: controller, - delegate: MyDialogWindowControllerDelegate( - onDestroyed: () { - dialogController.value = null; - }, - ), - ); - return jsonEncode({'result': true}); - } else if (jsonMap['type'] == 'close_dialog') { - dialogController.value?.destroy(); - return jsonEncode({'result': true}); - } else { - throw ArgumentError('Unknown message type: ${jsonMap['type']}'); + return ''; + } catch (e) { + return jsonEncode({'error': e.toString()}); } - return ''; }, ); controller = RegularWindowController( diff --git a/dev/integration_tests/windowing_test/test_driver/main_test.dart b/dev/integration_tests/windowing_test/test_driver/main_test.dart index cb71d929414..296f0021a20 100644 --- a/dev/integration_tests/windowing_test/test_driver/main_test.dart +++ b/dev/integration_tests/windowing_test/test_driver/main_test.dart @@ -7,13 +7,47 @@ import 'dart:convert'; import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart' hide TypeMatcher, isInstanceOf; +Future _requestDataWithRetry( + FlutterDriver driver, + String message, { + int maxRetries = 3, +}) async { + for (int attempt = 1; attempt <= maxRetries; attempt++) { + try { + final String response = await driver.requestData(message); + if (response.isNotEmpty) { + // Check for an error returned from the driver extension handler. + try { + final Map data = + jsonDecode(response) as Map; + if (data.containsKey('error')) { + throw Exception( + 'Driver extension handler reported an error: ${data['error']}', + ); + } + } on FormatException { + // Not a JSON map, which is fine for some responses. + } + } + return response; + } catch (e) { + if (attempt == maxRetries) { + rethrow; + } + + await Future.delayed(Duration(milliseconds: 100 * attempt)); + } + } + throw StateError('Should not reach here'); +} + void main() { group('end-to-end test', () { late final FlutterDriver driver; setUpAll(() async { driver = await FlutterDriver.connect(); - await driver.requestData(jsonEncode({'type': 'ping'})); + await _requestDataWithRetry(driver, jsonEncode({'type': 'ping'})); }); tearDownAll(() async { @@ -21,10 +55,12 @@ void main() { }); test('Can set and get title', () async { - await driver.requestData( + await _requestDataWithRetry( + driver, jsonEncode({'type': 'set_title', 'title': 'Hello World'}), ); - final response = await driver.requestData( + final response = await _requestDataWithRetry( + driver, jsonEncode({'type': 'get_title'}), ); final data = jsonDecode(response); @@ -32,7 +68,8 @@ void main() { }, timeout: Timeout.none); test('Initial controller size is correct', () async { - final response = await driver.requestData( + final response = await _requestDataWithRetry( + driver, jsonEncode({'type': 'get_size'}), ); final data = jsonDecode(response); @@ -41,10 +78,12 @@ void main() { }, timeout: Timeout.none); test('Can set and get size', () async { - await driver.requestData( + await _requestDataWithRetry( + driver, jsonEncode({'type': 'set_size', 'width': 800, 'height': 600}), ); - final response = await driver.requestData( + final response = await _requestDataWithRetry( + driver, jsonEncode({'type': 'get_size'}), ); final data = jsonDecode(response); @@ -53,53 +92,77 @@ void main() { }, timeout: Timeout.none); test('Can set and get fullscreen', () async { - await driver.requestData(jsonEncode({'type': 'set_fullscreen'})); - var response = await driver.requestData( + await _requestDataWithRetry( + driver, + jsonEncode({'type': 'set_fullscreen'}), + ); + var response = await _requestDataWithRetry( + driver, jsonEncode({'type': 'get_fullscreen'}), ); var data = jsonDecode(response); - expect(data["isFullscreen"], true); + expect(data['isFullscreen'], true); - await driver.requestData(jsonEncode({'type': 'unset_fullscreen'})); - response = await driver.requestData( + await _requestDataWithRetry( + driver, + jsonEncode({'type': 'unset_fullscreen'}), + ); + response = await _requestDataWithRetry( + driver, jsonEncode({'type': 'get_fullscreen'}), ); data = jsonDecode(response); - expect(data["isFullscreen"], false); + expect(data['isFullscreen'], false); }, timeout: Timeout.none); test('Can set and get maximized', () async { - await driver.requestData(jsonEncode({'type': 'set_maximized'})); - var response = await driver.requestData( + await _requestDataWithRetry( + driver, + jsonEncode({'type': 'set_maximized'}), + ); + var response = await _requestDataWithRetry( + driver, jsonEncode({'type': 'get_maximized'}), ); var data = jsonDecode(response); - expect(data["isMaximized"], true); + expect(data['isMaximized'], true); - await driver.requestData(jsonEncode({'type': 'unset_maximized'})); - response = await driver.requestData( + await _requestDataWithRetry( + driver, + jsonEncode({'type': 'unset_maximized'}), + ); + response = await _requestDataWithRetry( + driver, jsonEncode({'type': 'get_maximized'}), ); data = jsonDecode(response); - expect(data["isMaximized"], false); + expect(data['isMaximized'], false); }, timeout: Timeout.none); test( 'Can set and get minimized', () async { - await driver.requestData(jsonEncode({'type': 'set_minimized'})); - var response = await driver.requestData( + await _requestDataWithRetry( + driver, + jsonEncode({'type': 'set_minimized'}), + ); + var response = await _requestDataWithRetry( + driver, jsonEncode({'type': 'get_minimized'}), ); var data = jsonDecode(response); - expect(data["isMinimized"], true); + expect(data['isMinimized'], true); - await driver.requestData(jsonEncode({'type': 'unset_minimized'})); - response = await driver.requestData( + await _requestDataWithRetry( + driver, + jsonEncode({'type': 'unset_minimized'}), + ); + response = await _requestDataWithRetry( + driver, jsonEncode({'type': 'get_minimized'}), ); data = jsonDecode(response); - expect(data["isMinimized"], false); + expect(data['isMinimized'], false); }, timeout: Timeout.none, onPlatform: {'linux': Skip('isMinimized is not supported on Wayland')}, @@ -108,30 +171,39 @@ void main() { test( 'Can set and get activated', () async { - await driver.requestData( + await _requestDataWithRetry( + driver, jsonEncode({'type': 'set_minimized'}), ); // Minimize first so that the window is not active - await driver.requestData(jsonEncode({'type': 'set_activated'})); - final response = await driver.requestData( + await _requestDataWithRetry( + driver, + jsonEncode({'type': 'set_activated'}), + ); + final response = await _requestDataWithRetry( + driver, jsonEncode({'type': 'get_activated'}), ); final data = jsonDecode(response); - expect(data["isActivated"], true); + expect(data['isActivated'], true); }, timeout: Timeout.none, onPlatform: {'linux': Skip('isMinimized is not supported on Wayland')}, ); test('Can open dialog', () async { - await driver.requestData(jsonEncode({'type': 'open_dialog'})); - await driver.waitFor(find.byValueKey('close_dialog')); - await driver.requestData(jsonEncode({'type': 'close_dialog'})); + await _requestDataWithRetry(driver, jsonEncode({'type': 'open_dialog'})); + await driver.waitFor( + find.byValueKey('close_dialog'), + timeout: Duration(seconds: 10), + ); + await _requestDataWithRetry(driver, jsonEncode({'type': 'close_dialog'})); }, timeout: Timeout.none); test( 'Can set constraints and see the resize', () async { - await driver.requestData( + await _requestDataWithRetry( + driver, jsonEncode({ 'type': 'set_constraints', 'min_width': 0, @@ -140,7 +212,8 @@ void main() { 'max_height': 501, }), ); - final response = await driver.requestData( + final response = await _requestDataWithRetry( + driver, jsonEncode({'type': 'get_size'}), ); final data = jsonDecode(response); @@ -154,7 +227,8 @@ void main() { test( 'Can set constraints and see the resize (Linux)', () async { - await driver.requestData( + await _requestDataWithRetry( + driver, jsonEncode({ 'type': 'set_constraints', 'min_width': 0, @@ -163,7 +237,8 @@ void main() { 'max_height': 501, }), ); - final response = await driver.requestData( + final response = await _requestDataWithRetry( + driver, jsonEncode({'type': 'get_size'}), ); final data = jsonDecode(response);