From cb12c4147bb1bcd5b7c07892a4bfd36058898bd3 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Fri, 15 Aug 2025 10:47:52 -0400 Subject: [PATCH 1/2] implemented hot restart over websocket test --- .../hot_reload_websocket_test.dart | 135 ++----------- .../hot_restart_websocket_test.dart | 88 +++++++++ .../test_data/websocket_dwds_test_common.dart | 182 ++++++++++++++++++ 3 files changed, 283 insertions(+), 122 deletions(-) create mode 100644 packages/flutter_tools/test/integration.shard/hot_restart_websocket_test.dart create mode 100644 packages/flutter_tools/test/integration.shard/test_data/websocket_dwds_test_common.dart diff --git a/packages/flutter_tools/test/integration.shard/hot_reload_websocket_test.dart b/packages/flutter_tools/test/integration.shard/hot_reload_websocket_test.dart index c28ca8361ab91..91dcc0cf420df 100644 --- a/packages/flutter_tools/test/integration.shard/hot_reload_websocket_test.dart +++ b/packages/flutter_tools/test/integration.shard/hot_reload_websocket_test.dart @@ -6,14 +6,12 @@ library; import 'dart:async'; -import 'dart:io' as io; import 'package:file/file.dart'; -import 'package:flutter_tools/src/web/chrome.dart'; -import 'package:flutter_tools/src/web/web_device.dart' show WebServerDevice; import '../src/common.dart'; import 'test_data/hot_reload_project.dart'; +import 'test_data/websocket_dwds_test_common.dart'; import 'test_driver.dart'; import 'test_utils.dart'; import 'transition_test_utils.dart'; @@ -26,8 +24,6 @@ void testAll({List additionalCommandArgs = const []}) { group('WebSocket DWDS connection' '${additionalCommandArgs.isEmpty ? '' : ' with args: $additionalCommandArgs'}', () { // Test configuration constants - const debugUrlTimeout = Duration(seconds: 20); - const appStartTimeout = Duration(seconds: 15); const hotReloadTimeout = Duration(seconds: 10); late Directory tempDir; @@ -48,62 +44,17 @@ void testAll({List additionalCommandArgs = const []}) { testWithoutContext( 'hot reload with headless Chrome WebSocket connection', () async { - debugPrint('Starting WebSocket DWDS test with headless Chrome...'); - - // Set up listening for app output before starting - final stdout = StringBuffer(); - final sawDebugUrl = Completer(); - final StreamSubscription subscription = flutter.stdout.listen((String e) { - stdout.writeln(e); - // Extract the debug connection URL - if (e.contains('Waiting for connection from Dart debug extension at http://')) { - final debugUrlPattern = RegExp( - r'Waiting for connection from Dart debug extension at (http://[^\s]+)', - ); - final Match? match = debugUrlPattern.firstMatch(e); - if (match != null && !sawDebugUrl.isCompleted) { - sawDebugUrl.complete(match.group(1)!); - } - } - }); - - io.Process? chromeProcess; - try { - // Step 1: Start Flutter app with web-server device (will wait for debug connection) - debugPrint('Step 1: Starting Flutter app with web-server device...'); - // Start the app but don't wait for it to complete - it won't complete until Chrome connects - final Future appStartFuture = runFlutterWithWebServerDevice( - flutter, - additionalCommandArgs: [...additionalCommandArgs, '--no-web-resources-cdn'], - ); + debugPrint('Starting WebSocket DWDS test with headless Chrome for hot reload...'); - // Step 2: Wait for DWDS debug URL to be available - debugPrint('Step 2: Waiting for DWDS debug service URL...'); - final String debugUrl = await sawDebugUrl.future.timeout( - debugUrlTimeout, - onTimeout: () { - throw Exception('DWDS debug URL not found - app may not have started correctly'); - }, - ); - debugPrint('✓ DWDS debug service available at: $debugUrl'); - - // Step 3: Launch headless Chrome to connect to DWDS - debugPrint('Step 3: Launching headless Chrome to connect to DWDS...'); - chromeProcess = await _launchHeadlessChrome(debugUrl); - debugPrint('✓ Headless Chrome launched and connecting to DWDS'); - - // Step 4: Wait for app to start (Chrome connection established) - debugPrint('Step 4: Waiting for Flutter app to start after Chrome connection...'); - await appStartFuture.timeout( - appStartTimeout, - onTimeout: () { - throw Exception('App startup did not complete after Chrome connection'); - }, - ); - debugPrint('✓ Flutter app started successfully with WebSocket connection'); + // Set up WebSocket connection + final WebSocketDwdsTestSetup setup = await WebSocketDwdsTestUtils.setupWebSocketConnection( + flutter, + additionalCommandArgs: additionalCommandArgs, + ); - // Step 5: Test hot reload functionality - debugPrint('Step 5: Testing hot reload with WebSocket connection...'); + try { + // Test hot reload functionality + debugPrint('Step 6: Testing hot reload with WebSocket connection...'); await flutter.hotReload().timeout( hotReloadTimeout, onTimeout: () { @@ -114,80 +65,20 @@ void testAll({List additionalCommandArgs = const []}) { // Give some time for logs to capture await Future.delayed(const Duration(seconds: 2)); - final output = stdout.toString(); + final output = setup.stdout.toString(); expect(output, contains('Reloaded'), reason: 'Hot reload should complete successfully'); debugPrint('✓ Hot reload completed successfully with WebSocket connection'); // Verify the correct infrastructure was used - expect( - output, - contains('Waiting for connection from Dart debug extension'), - reason: 'Should wait for debug connection (WebSocket infrastructure)', - ); - expect(output, contains('web-server'), reason: 'Should use web-server device'); + WebSocketDwdsTestUtils.verifyWebSocketInfrastructure(output); debugPrint('✓ WebSocket DWDS test completed successfully'); debugPrint('✓ Verified: web-server device + DWDS + WebSocket connection + hot reload'); } finally { - await _cleanupResources(chromeProcess, subscription); + await cleanupWebSocketTestResources(setup.chromeProcess, setup.subscription); } }, skip: !platform.isMacOS, // Skip on non-macOS platforms where Chrome paths may differ ); }); } - -/// Launches headless Chrome with the given debug URL. -/// Uses findChromeExecutable to locate Chrome on the current platform. -Future _launchHeadlessChrome(String debugUrl) async { - const chromeArgs = [ - '--headless', - '--disable-gpu', - '--no-sandbox', - '--disable-extensions', - '--disable-dev-shm-usage', - '--remote-debugging-port=0', - ]; - - final String chromePath = findChromeExecutable(platform, fileSystem); - - try { - return await io.Process.start(chromePath, [...chromeArgs, debugUrl]); - } on Exception catch (e) { - throw Exception( - 'Could not launch Chrome at $chromePath: $e. Please ensure Chrome is installed.', - ); - } -} - -/// Cleans up test resources (Chrome process and stdout subscription). -Future _cleanupResources( - io.Process? chromeProcess, - StreamSubscription subscription, -) async { - if (chromeProcess != null) { - try { - chromeProcess.kill(); - await chromeProcess.exitCode; - debugPrint('Chrome process cleaned up'); - } on Exception catch (e) { - debugPrint('Warning: Failed to clean up Chrome process: $e'); - } - } - await subscription.cancel(); -} - -// Helper to run flutter with web-server device using WebSocket connection. -Future runFlutterWithWebServerDevice( - FlutterRunTestDriver flutter, { - bool verbose = false, - bool withDebugger = true, // Enable debugger by default for WebSocket connection - bool startPaused = false, // Don't start paused for this test - List additionalCommandArgs = const [], -}) => flutter.run( - verbose: verbose, - withDebugger: withDebugger, // Enable debugger to establish WebSocket connection - startPaused: startPaused, // Let the app start normally after debugger connects - device: WebServerDevice.kWebServerDeviceId, - additionalCommandArgs: additionalCommandArgs, -); diff --git a/packages/flutter_tools/test/integration.shard/hot_restart_websocket_test.dart b/packages/flutter_tools/test/integration.shard/hot_restart_websocket_test.dart new file mode 100644 index 0000000000000..2bf1fa928de85 --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/hot_restart_websocket_test.dart @@ -0,0 +1,88 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@Tags(['flutter-test-driver']) +library; + +import 'dart:async'; + +import 'package:file/file.dart'; + +import '../src/common.dart'; +import 'test_data/hot_reload_project.dart'; +import 'test_data/websocket_dwds_test_common.dart'; +import 'test_driver.dart'; +import 'test_utils.dart'; +import 'transition_test_utils.dart'; + +void main() { + testAll(); +} + +void testAll({List additionalCommandArgs = const []}) { + group('WebSocket DWDS connection for hot restart' + '${additionalCommandArgs.isEmpty ? '' : ' with args: $additionalCommandArgs'}', () { + // Test configuration constants + const hotRestartTimeout = Duration(seconds: 15); + + late Directory tempDir; + final project = HotReloadProject(); + late FlutterRunTestDriver flutter; + + setUp(() async { + tempDir = createResolvedTempDirectorySync('hot_restart_websocket_test.'); + await project.setUpIn(tempDir); + flutter = FlutterRunTestDriver(tempDir); + }); + + tearDown(() async { + await flutter.stop(); + tryToDelete(tempDir); + }); + + testWithoutContext( + 'hot restart with headless Chrome WebSocket connection', + () async { + debugPrint('Starting WebSocket DWDS test with headless Chrome for hot restart...'); + + // Set up WebSocket connection + final WebSocketDwdsTestSetup setup = await WebSocketDwdsTestUtils.setupWebSocketConnection( + flutter, + additionalCommandArgs: additionalCommandArgs, + ); + + try { + // Test hot restart functionality + debugPrint('Step 6: Testing hot restart with WebSocket connection...'); + await flutter.hotRestart().timeout( + hotRestartTimeout, + onTimeout: () { + throw Exception('Hot restart timed out'); + }, + ); + + // Give some time for logs to capture + await Future.delayed(const Duration(seconds: 2)); + + final output = setup.stdout.toString(); + expect( + output, + contains('Restarted application'), + reason: 'Hot restart should complete successfully', + ); + debugPrint('✓ Hot restart completed successfully with WebSocket connection'); + + // Verify the correct infrastructure was used + WebSocketDwdsTestUtils.verifyWebSocketInfrastructure(output); + + debugPrint('✓ WebSocket DWDS test completed successfully'); + debugPrint('✓ Verified: web-server device + DWDS + WebSocket connection + hot restart'); + } finally { + await cleanupWebSocketTestResources(setup.chromeProcess, setup.subscription); + } + }, + skip: !platform.isMacOS, // Skip on non-macOS platforms where Chrome paths may differ + ); + }); +} diff --git a/packages/flutter_tools/test/integration.shard/test_data/websocket_dwds_test_common.dart b/packages/flutter_tools/test/integration.shard/test_data/websocket_dwds_test_common.dart new file mode 100644 index 0000000000000..afcda01bb150d --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/test_data/websocket_dwds_test_common.dart @@ -0,0 +1,182 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io' as io; + +import 'package:flutter_tools/src/web/chrome.dart'; +import 'package:flutter_tools/src/web/web_device.dart' show WebServerDevice; + +import '../../src/common.dart'; +import '../test_driver.dart'; +import '../test_utils.dart'; +import '../transition_test_utils.dart'; + +/// Configuration for WebSocket DWDS tests. +class WebSocketDwdsTestConfig { + const WebSocketDwdsTestConfig({ + this.debugUrlTimeout = const Duration(seconds: 20), + this.appStartTimeout = const Duration(seconds: 15), + }); + + final Duration debugUrlTimeout; + final Duration appStartTimeout; +} + +/// Result of setting up a WebSocket DWDS connection. +class WebSocketDwdsTestSetup { + const WebSocketDwdsTestSetup({ + required this.stdout, + required this.chromeProcess, + required this.subscription, + }); + + final StringBuffer stdout; + final io.Process chromeProcess; + final StreamSubscription subscription; +} + +/// Common utilities for WebSocket DWDS tests. +class WebSocketDwdsTestUtils { + /// Sets up WebSocket DWDS connection with headless Chrome. + /// + /// This method handles the complete setup flow: + /// 1. Start Flutter app with web-server device + /// 2. Wait for DWDS debug URL + /// 3. Launch headless Chrome to connect to DWDS + /// 4. Wait for app startup after Chrome connection + static Future setupWebSocketConnection( + FlutterRunTestDriver flutter, { + required List additionalCommandArgs, + WebSocketDwdsTestConfig config = const WebSocketDwdsTestConfig(), + }) async { + debugPrint('Step 1: Starting WebSocket DWDS connection setup...'); + + // Set up listening for app output before starting + final stdout = StringBuffer(); + final sawDebugUrl = Completer(); + final StreamSubscription subscription = flutter.stdout.listen((String e) { + stdout.writeln(e); + // Extract the debug connection URL + if (e.contains('Waiting for connection from Dart debug extension at http://')) { + final debugUrlPattern = RegExp( + r'Waiting for connection from Dart debug extension at (http://[^\s]+)', + ); + final Match? match = debugUrlPattern.firstMatch(e); + if (match != null && !sawDebugUrl.isCompleted) { + sawDebugUrl.complete(match.group(1)!); + } + } + }); + + try { + // Step 1: Start Flutter app with web-server device (will wait for debug connection) + debugPrint('Step 2: Starting Flutter app with web-server device...'); + final Future appStartFuture = runFlutterWithWebServerDevice( + flutter, + additionalCommandArgs: [...additionalCommandArgs, '--no-web-resources-cdn'], + ); + + // Step 2: Wait for DWDS debug URL to be available + debugPrint('Step 3: Waiting for DWDS debug service URL...'); + final String debugUrl = await sawDebugUrl.future.timeout( + config.debugUrlTimeout, + onTimeout: () { + throw Exception('DWDS debug URL not found - app may not have started correctly'); + }, + ); + debugPrint('✓ DWDS debug service available at: $debugUrl'); + + // Step 3: Launch headless Chrome to connect to DWDS + debugPrint('Step 4: Launching headless Chrome to connect to DWDS...'); + final io.Process chromeProcess = await launchHeadlessChrome(debugUrl); + debugPrint('✓ Headless Chrome launched and connecting to DWDS'); + + // Step 4: Wait for app to start (Chrome connection established) + debugPrint('Step 5: Waiting for Flutter app to start after Chrome connection...'); + await appStartFuture.timeout( + config.appStartTimeout, + onTimeout: () { + throw Exception('App startup did not complete after Chrome connection'); + }, + ); + debugPrint('✓ Flutter app started successfully with WebSocket connection'); + + return WebSocketDwdsTestSetup( + stdout: stdout, + chromeProcess: chromeProcess, + subscription: subscription, + ); + } catch (e) { + // Clean up on error + await subscription.cancel(); + rethrow; + } + } + + /// Verifies WebSocket DWDS infrastructure is being used. + static void verifyWebSocketInfrastructure(String output) { + expect( + output, + contains('Waiting for connection from Dart debug extension'), + reason: 'Should wait for debug connection (WebSocket infrastructure)', + ); + expect(output, contains('web-server'), reason: 'Should use web-server device'); + } +} + +/// Launches headless Chrome with the given debug URL. +/// Uses findChromeExecutable to locate Chrome on the current platform. +Future launchHeadlessChrome(String debugUrl) async { + const chromeArgs = [ + '--headless', + '--disable-gpu', + '--no-sandbox', + '--disable-extensions', + '--disable-dev-shm-usage', + '--remote-debugging-port=0', + ]; + + final String chromePath = findChromeExecutable(platform, fileSystem); + + try { + return await io.Process.start(chromePath, [...chromeArgs, debugUrl]); + } on Exception catch (e) { + throw Exception( + 'Could not launch Chrome at $chromePath: $e. Please ensure Chrome is installed.', + ); + } +} + +/// Cleans up test resources (Chrome process and stdout subscription). +Future cleanupWebSocketTestResources( + io.Process? chromeProcess, + StreamSubscription subscription, +) async { + if (chromeProcess != null) { + try { + chromeProcess.kill(); + await chromeProcess.exitCode; + debugPrint('Chrome process cleaned up'); + } on Exception catch (e) { + debugPrint('Warning: Failed to clean up Chrome process: $e'); + } + } + await subscription.cancel(); +} + +/// Helper to run flutter with web-server device using WebSocket connection. +Future runFlutterWithWebServerDevice( + FlutterRunTestDriver flutter, { + bool verbose = false, + bool withDebugger = true, // Enable debugger by default for WebSocket connection + bool startPaused = false, // Don't start paused for this test + List additionalCommandArgs = const [], +}) => flutter.run( + verbose: verbose, + withDebugger: withDebugger, // Enable debugger to establish WebSocket connection + startPaused: startPaused, // Let the app start normally after debugger connects + device: WebServerDevice.kWebServerDeviceId, + additionalCommandArgs: additionalCommandArgs, +); From e881a556437f21ea8e46fc3d4dcceff5aebed2b1 Mon Sep 17 00:00:00 2001 From: Jessy Yameogo Date: Fri, 15 Aug 2025 11:28:58 -0400 Subject: [PATCH 2/2] addressed gemini code assist comment --- .../test_data/websocket_dwds_test_common.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/flutter_tools/test/integration.shard/test_data/websocket_dwds_test_common.dart b/packages/flutter_tools/test/integration.shard/test_data/websocket_dwds_test_common.dart index afcda01bb150d..aceb33b35b681 100644 --- a/packages/flutter_tools/test/integration.shard/test_data/websocket_dwds_test_common.dart +++ b/packages/flutter_tools/test/integration.shard/test_data/websocket_dwds_test_common.dart @@ -70,6 +70,7 @@ class WebSocketDwdsTestUtils { } }); + io.Process? chromeProcess; try { // Step 1: Start Flutter app with web-server device (will wait for debug connection) debugPrint('Step 2: Starting Flutter app with web-server device...'); @@ -90,7 +91,7 @@ class WebSocketDwdsTestUtils { // Step 3: Launch headless Chrome to connect to DWDS debugPrint('Step 4: Launching headless Chrome to connect to DWDS...'); - final io.Process chromeProcess = await launchHeadlessChrome(debugUrl); + chromeProcess = await launchHeadlessChrome(debugUrl); debugPrint('✓ Headless Chrome launched and connecting to DWDS'); // Step 4: Wait for app to start (Chrome connection established) @@ -111,6 +112,10 @@ class WebSocketDwdsTestUtils { } catch (e) { // Clean up on error await subscription.cancel(); + if (chromeProcess != null) { + chromeProcess.kill(); + await chromeProcess.exitCode; + } rethrow; } }