diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md index bedef0c93..f0922afee 100644 --- a/dwds/CHANGELOG.md +++ b/dwds/CHANGELOG.md @@ -1,6 +1,7 @@ ## 23.2.0-wip - Send untruncated `dart:developer` logs to debugging clients. - [#2333](https://github.com/dart-lang/webdev/pull/2333) +- Enabling tests that run with the DDC module system and exposing `utilities/ddc_names.dart` - [#2295](https://github.com/dart-lang/webdev/pull/2295) ## 23.1.1 diff --git a/dwds/lib/dwds.dart b/dwds/lib/dwds.dart index a9016398c..d0898d792 100644 --- a/dwds/lib/dwds.dart +++ b/dwds/lib/dwds.dart @@ -41,5 +41,6 @@ export 'src/services/expression_compiler.dart' CompilerOptions; export 'src/services/expression_compiler_service.dart' show ExpressionCompilerService; +export 'src/utilities/ddc_names.dart'; export 'src/utilities/sdk_configuration.dart' show SdkLayout, SdkConfiguration, SdkConfigurationProvider; diff --git a/dwds/lib/expression_compiler.dart b/dwds/lib/expression_compiler.dart index 19283a719..674841406 100644 --- a/dwds/lib/expression_compiler.dart +++ b/dwds/lib/expression_compiler.dart @@ -7,4 +7,5 @@ export 'src/services/expression_compiler.dart' ExpressionCompilationResult, ExpressionCompiler, CompilerOptions, + ModuleFormat, ModuleInfo; diff --git a/dwds/lib/src/loaders/legacy.dart b/dwds/lib/src/loaders/legacy.dart index bfaa50315..fb35c0937 100644 --- a/dwds/lib/src/loaders/legacy.dart +++ b/dwds/lib/src/loaders/legacy.dart @@ -181,7 +181,11 @@ class LegacyStrategy extends LoadStrategy { return ''' $_baseUrlScript var scripts = ${const JsonEncoder.withIndent(" ").convert(scripts)}; -window.\$dartLoader.loadScripts(scripts); +window.\$dartLoader.loadConfig.loadScriptFn = function(loader) { + loader.addScriptsToQueue(scripts, null); + loader.loadEnqueuedModules(); +}; +window.\$dartLoader.loader.nextAttempt(); '''; } diff --git a/dwds/lib/src/services/chrome_proxy_service.dart b/dwds/lib/src/services/chrome_proxy_service.dart index 3f9f45807..9a6051cce 100644 --- a/dwds/lib/src/services/chrome_proxy_service.dart +++ b/dwds/lib/src/services/chrome_proxy_service.dart @@ -187,7 +187,7 @@ class ChromeProxyService implements VmServiceInterface { 'with sound null safety: $soundNullSafety'); final compilerOptions = CompilerOptions( - moduleFormat: moduleFormat, + moduleFormat: ModuleFormat.values.byName(moduleFormat), soundNullSafety: soundNullSafety, canaryFeatures: canaryFeatures, experiments: experiments, diff --git a/dwds/lib/src/services/expression_compiler.dart b/dwds/lib/src/services/expression_compiler.dart index 6135a707e..c1b7306b3 100644 --- a/dwds/lib/src/services/expression_compiler.dart +++ b/dwds/lib/src/services/expression_compiler.dart @@ -4,7 +4,7 @@ /// Options passed to DDC and the expression compiler. class CompilerOptions { - final String moduleFormat; + final ModuleFormat moduleFormat; final bool soundNullSafety; final bool canaryFeatures; final List experiments; @@ -17,6 +17,9 @@ class CompilerOptions { }); } +/// Indicates the module system DDC is targeting. +enum ModuleFormat { amd, ddc, es6 } + /// Result of compilation of dart expression to JavaScript class ExpressionCompilationResult { final bool isError; diff --git a/dwds/lib/src/services/expression_compiler_service.dart b/dwds/lib/src/services/expression_compiler_service.dart index 36a51f4d6..4865cb1d3 100644 --- a/dwds/lib/src/services/expression_compiler_service.dart +++ b/dwds/lib/src/services/expression_compiler_service.dart @@ -89,7 +89,7 @@ class _Compiler { '--asset-server-port', '$port', '--module-format', - compilerOptions.moduleFormat, + compilerOptions.moduleFormat.name, if (verbose) '--verbose', compilerOptions.soundNullSafety ? '--sound-null-safety' diff --git a/dwds/lib/utilities.dart b/dwds/lib/utilities.dart new file mode 100644 index 000000000..02e84d8cc --- /dev/null +++ b/dwds/lib/utilities.dart @@ -0,0 +1,5 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +export 'src/utilities/ddc_names.dart'; diff --git a/dwds/test/devtools_test.dart b/dwds/test/devtools_test.dart index b7e1ddf1b..8e9fb4868 100644 --- a/dwds/test/devtools_test.dart +++ b/dwds/test/devtools_test.dart @@ -32,110 +32,115 @@ void main() { final context = TestContext(TestProject.testWithSoundNullSafety, provider); - group('Injected client', () { - setUp(() async { - await context.setUp( - debugSettings: TestDebugSettings.withDevTools(context), - ); - await context.webDriver.driver.keyboard.sendChord([Keyboard.alt, 'd']); - // Wait for DevTools to actually open. - await Future.delayed(const Duration(seconds: 2)); - }); - - tearDown(() async { - await context.tearDown(); - }); - - test( - 'can launch devtools', - () async { - final windows = await context.webDriver.windows.toList(); - await context.webDriver.driver.switchTo.window(windows.last); - expect(await context.webDriver.pageSource, contains('DevTools')); - expect(await context.webDriver.currentUrl, contains('ide=Dwds')); - // TODO(https://github.com/dart-lang/webdev/issues/1888): Re-enable. - }, - skip: Platform.isWindows, - ); - - test('can not launch devtools for the same app in multiple tabs', () async { - final appUrl = await context.webDriver.currentUrl; - // Open a new tab, select it, and navigate to the app - await context.webDriver.driver - .execute("window.open('$appUrl', '_blank');", []); - await Future.delayed(const Duration(seconds: 2)); - final newAppWindow = await context.webDriver.windows.last; - await newAppWindow.setAsActive(); + group( + 'Injected client', + () { + setUp(() async { + await context.setUp( + debugSettings: TestDebugSettings.withDevTools(context), + ); + await context.webDriver.driver.keyboard.sendChord([Keyboard.alt, 'd']); + // Wait for DevTools to actually open. + await Future.delayed(const Duration(seconds: 2)); + }); - // Wait for the page to be ready before trying to open DevTools again. - await _waitForPageReady(context); + tearDown(() async { + await context.tearDown(); + }); - // Try to open devtools and check for the alert. - await context.webDriver.driver.keyboard.sendChord([Keyboard.alt, 'd']); - await Future.delayed(const Duration(seconds: 2)); - final alert = context.webDriver.driver.switchTo.alert; - expect(alert, isNotNull); - expect( - await alert.text, - contains('This app is already being debugged in a different tab'), + test( + 'can launch devtools', + () async { + final windows = await context.webDriver.windows.toList(); + await context.webDriver.driver.switchTo.window(windows.last); + expect(await context.webDriver.pageSource, contains('DevTools')); + expect(await context.webDriver.currentUrl, contains('ide=Dwds')); + // TODO(https://github.com/dart-lang/webdev/issues/1888): Re-enable. + }, + skip: Platform.isWindows, ); - await alert.accept(); - var windows = await context.webDriver.windows.toList(); - for (final window in windows) { - if (window.id != newAppWindow.id) { - await window.setAsActive(); - await window.close(); - } - } + test('can not launch devtools for the same app in multiple tabs', + () async { + final appUrl = await context.webDriver.currentUrl; + // Open a new tab, select it, and navigate to the app + await context.webDriver.driver + .execute("window.open('$appUrl', '_blank');", []); + await Future.delayed(const Duration(seconds: 2)); + final newAppWindow = await context.webDriver.windows.last; + await newAppWindow.setAsActive(); - await newAppWindow.setAsActive(); - await context.webDriver.driver.keyboard.sendChord([Keyboard.alt, 'd']); - await Future.delayed(const Duration(seconds: 2)); - windows = await context.webDriver.windows.toList(); - final devToolsWindow = - windows.firstWhere((window) => window != newAppWindow); - await devToolsWindow.setAsActive(); - expect(await context.webDriver.pageSource, contains('DevTools')); - }); + // Wait for the page to be ready before trying to open DevTools again. + await _waitForPageReady(context); - test( - 'destroys and recreates the isolate during a page refresh', - () async { - // This test is the same as one in reload_test, but runs here when there - // is a connected client (DevTools) since it can behave differently. - // https://github.com/dart-lang/webdev/pull/901#issuecomment-586438132 - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - context.makeEditToDartEntryFile( - toReplace: 'Hello World!', - replaceWith: 'Bonjour le monde!', - ); - await context.waitForSuccessfulBuild(propagateToBrowser: true); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), + // Try to open devtools and check for the alert. + await context.webDriver.driver.keyboard.sendChord([Keyboard.alt, 'd']); + await Future.delayed(const Duration(seconds: 2)); + final alert = context.webDriver.driver.switchTo.alert; + expect(alert, isNotNull); + expect( + await alert.text, + contains('This app is already being debugged in a different tab'), ); + await alert.accept(); - await context.webDriver.driver.refresh(); + var windows = await context.webDriver.windows.toList(); + for (final window in windows) { + if (window.id != newAppWindow.id) { + await window.setAsActive(); + await window.close(); + } + } - await eventsDone; - // Re-set the edited file: - context.makeEditToDartEntryFile( - toReplace: 'Bonjour le monde!', - replaceWith: 'Hello World!', - ); - }, - skip: 'https://github.com/dart-lang/webdev/issues/1888', - ); - }); + await newAppWindow.setAsActive(); + await context.webDriver.driver.keyboard.sendChord([Keyboard.alt, 'd']); + await Future.delayed(const Duration(seconds: 2)); + windows = await context.webDriver.windows.toList(); + final devToolsWindow = + windows.firstWhere((window) => window != newAppWindow); + await devToolsWindow.setAsActive(); + expect(await context.webDriver.pageSource, contains('DevTools')); + }); + + test( + 'destroys and recreates the isolate during a page refresh', + () async { + // This test is the same as one in reload_test, but runs here when there + // is a connected client (DevTools) since it can behave differently. + // https://github.com/dart-lang/webdev/pull/901#issuecomment-586438132 + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); + context.makeEditToDartEntryFile( + toReplace: 'Hello World!', + replaceWith: 'Bonjour le monde!', + ); + await context.waitForSuccessfulBuild(propagateToBrowser: true); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + await context.webDriver.driver.refresh(); + + await eventsDone; + // Re-set the edited file: + context.makeEditToDartEntryFile( + toReplace: 'Bonjour le monde!', + replaceWith: 'Hello World!', + ); + }, + skip: 'https://github.com/dart-lang/webdev/issues/1888', + ); + }, + timeout: Timeout.factor(2), + ); group('Injected client without a DevTools server', () { setUp(() async { @@ -193,6 +198,7 @@ void main() { }, tags: ['extension'], skip: 'https://github.com/dart-lang/webdev/issues/2114', + timeout: Timeout.factor(2), ); } diff --git a/dwds/test/evaluate_common.dart b/dwds/test/evaluate_common.dart index 61025b5be..7c3c9c984 100644 --- a/dwds/test/evaluate_common.dart +++ b/dwds/test/evaluate_common.dart @@ -71,6 +71,7 @@ void testAll({ await context.setUp( testSettings: TestSettings( compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, enableExpressionEvaluation: true, useDebuggerModuleNames: useDebuggerModuleNames, verboseCompiler: debug, @@ -821,6 +822,7 @@ void testAll({ await context.setUp( testSettings: TestSettings( compilationMode: compilationMode, + moduleFormat: provider.ddcModuleFormat, enableExpressionEvaluation: false, verboseCompiler: debug, ), diff --git a/dwds/test/expression_compiler_service_test.dart b/dwds/test/expression_compiler_service_test.dart index d791aebd6..0531764b7 100644 --- a/dwds/test/expression_compiler_service_test.dart +++ b/dwds/test/expression_compiler_service_test.dart @@ -80,7 +80,7 @@ void main() async { ); final compilerOptions = CompilerOptions( - moduleFormat: 'amd', + moduleFormat: ModuleFormat.amd, soundNullSafety: true, canaryFeatures: false, experiments: const [], diff --git a/dwds/test/fixtures/context.dart b/dwds/test/fixtures/context.dart index 3842aa03c..34a45e76e 100644 --- a/dwds/test/fixtures/context.dart +++ b/dwds/test/fixtures/context.dart @@ -2,6 +2,7 @@ // for details. 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:convert'; import 'dart:io'; @@ -9,15 +10,16 @@ import 'package:build_daemon/client.dart'; import 'package:build_daemon/data/build_status.dart'; import 'package:build_daemon/data/build_target.dart'; import 'package:dwds/asset_reader.dart'; -import 'package:dwds/expression_compiler.dart'; import 'package:dwds/src/connections/app_connection.dart'; import 'package:dwds/src/connections/debug_connection.dart'; import 'package:dwds/src/debugging/webkit_debugger.dart'; import 'package:dwds/src/loaders/build_runner_require.dart'; +import 'package:dwds/src/loaders/frontend_server_legacy.dart'; import 'package:dwds/src/loaders/frontend_server_require.dart'; -import 'package:dwds/src/loaders/require.dart'; +import 'package:dwds/src/loaders/strategy.dart'; import 'package:dwds/src/readers/proxy_server_asset_reader.dart'; import 'package:dwds/src/services/chrome_proxy_service.dart'; +import 'package:dwds/src/services/expression_compiler.dart'; import 'package:dwds/src/services/expression_compiler_service.dart'; import 'package:dwds/src/utilities/dart_uri.dart'; import 'package:dwds/src/utilities/server.dart'; @@ -204,7 +206,7 @@ class TestContext { ExpressionCompiler? expressionCompiler; AssetReader assetReader; Stream buildResults; - RequireStrategy requireStrategy; + LoadStrategy loadStrategy; String basePath = ''; String filePathToServe = project.filePathToServe; @@ -268,7 +270,7 @@ class TestContext { expressionCompiler = ddcService; } - requireStrategy = BuildRunnerRequireStrategyProvider( + loadStrategy = BuildRunnerRequireStrategyProvider( assetHandler, testSettings.reloadConfiguration, assetReader, @@ -301,6 +303,7 @@ class TestContext { nullSafety: project.nullSafety, experiments: buildSettings.experiments, canaryFeatures: buildSettings.canaryFeatures, + moduleFormat: testSettings.moduleFormat, ); _webRunner = ResidentWebRunner( @@ -332,14 +335,25 @@ class TestContext { basePath = webRunner.devFS.assetServer.basePath; assetReader = webRunner.devFS.assetServer; _assetHandler = webRunner.devFS.assetServer.handleRequest; - requireStrategy = FrontendServerRequireStrategyProvider( - testSettings.reloadConfiguration, - assetReader, - packageUriMapper, - () async => {}, - buildSettings, - ).strategy; - + loadStrategy = switch (testSettings.moduleFormat) { + ModuleFormat.amd => FrontendServerRequireStrategyProvider( + testSettings.reloadConfiguration, + assetReader, + packageUriMapper, + () async => {}, + buildSettings, + ).strategy, + ModuleFormat.ddc => FrontendServerLegacyStrategyProvider( + testSettings.reloadConfiguration, + assetReader, + packageUriMapper, + () async => {}, + buildSettings, + ).strategy, + _ => throw Exception( + 'Unsupported DDC module format ${testSettings.moduleFormat.name}.', + ) + }; buildResults = const Stream.empty(); } break; @@ -380,6 +394,11 @@ class TestContext { ), ); } + + // The debugger tab must be enabled and connected before certain + // listeners in DWDS or `main` is run. + final tabConnectionCompleter = Completer(); + final appConnectionCompleter = Completer(); final connection = ChromeConnection('localhost', debugPort); _testServer = await TestServer.start( @@ -389,13 +408,27 @@ class TestContext { port: port, assetHandler: assetHandler, assetReader: assetReader, - strategy: requireStrategy, + strategy: loadStrategy, target: project.directoryToServe, buildResults: buildResults, chromeConnection: () async => connection, - autoRun: testSettings.autoRun, ); + _testServer!.dwds.connectedApps.listen((connection) async { + // Ensure that we've established a tab connection before running main. + await tabConnectionCompleter.future; + if (testSettings.autoRun) { + connection.runMain(); + } + + // We may reuse the app connection, so only save it the first time + // it's encountered. + if (!appConnectionCompleter.isCompleted) { + appConnection = connection; + appConnectionCompleter.complete(); + } + }); + _appUrl = basePath.isEmpty ? 'http://localhost:$port/$filePathToServe' : 'http://localhost:$port/$basePath/$filePathToServe'; @@ -406,7 +439,9 @@ class TestContext { if (tab != null) { _tabConnection = await tab.connect(); await tabConnection.runtime.enable(); - await tabConnection.debugger.enable(); + await tabConnection.debugger + .enable() + .then((_) => tabConnectionCompleter.complete()); } else { throw StateError('Unable to connect to tab.'); } @@ -417,10 +452,13 @@ class TestContext { await extensionConnection.runtime.enable(); } - appConnection = await testServer.dwds.connectedApps.first; + await appConnectionCompleter.future; if (debugSettings.enableDebugging && !testSettings.waitToDebug) { await startDebugging(); } + } else { + // No tab needs to be dicovered, so fulfill the relevant completer. + tabConnectionCompleter.complete(); } } catch (e, s) { _logger.severe('Failed to setup the service, $e:$s'); diff --git a/dwds/test/fixtures/server.dart b/dwds/test/fixtures/server.dart index 88562e73f..9de66c3f0 100644 --- a/dwds/test/fixtures/server.dart +++ b/dwds/test/fixtures/server.dart @@ -9,7 +9,7 @@ import 'package:dwds/asset_reader.dart'; import 'package:dwds/dart_web_debug_service.dart'; import 'package:dwds/data/build_result.dart'; import 'package:dwds/src/config/tool_configuration.dart'; -import 'package:dwds/src/loaders/require.dart'; +import 'package:dwds/src/loaders/strategy.dart'; import 'package:dwds/src/utilities/server.dart'; import 'package:logging/logging.dart'; import 'package:shelf/shelf.dart'; @@ -39,15 +39,8 @@ class TestServer { this._server, this.dwds, this.buildResults, - bool autoRun, this.assetReader, - ) { - if (autoRun) { - dwds.connectedApps.listen((connection) { - connection.runMain(); - }); - } - } + ); String get host => _server.address.host; int get port => _server.port; @@ -62,11 +55,10 @@ class TestServer { required AppMetadata appMetadata, required Handler assetHandler, required AssetReader assetReader, - required RequireStrategy strategy, + required LoadStrategy strategy, required String target, required Stream buildResults, required Future Function() chromeConnection, - required bool autoRun, int? port, }) async { var pipeline = const Pipeline(); @@ -121,7 +113,6 @@ class TestServer { server, dwds, filteredBuildResults, - autoRun, assetReader, ); } diff --git a/dwds/test/fixtures/utilities.dart b/dwds/test/fixtures/utilities.dart index 5c722e068..45c508ac3 100644 --- a/dwds/test/fixtures/utilities.dart +++ b/dwds/test/fixtures/utilities.dart @@ -242,6 +242,7 @@ class TestSettings { // Build settings. final CompilationMode compilationMode; + final ModuleFormat moduleFormat; final bool canaryFeatures; final bool isFlutterApp; final List experiments; @@ -255,6 +256,7 @@ class TestSettings { this.verboseCompiler = false, this.launchChrome = true, this.compilationMode = CompilationMode.buildDaemon, + this.moduleFormat = ModuleFormat.amd, this.canaryFeatures = false, this.isFlutterApp = false, this.experiments = const [], @@ -296,8 +298,8 @@ class TestCompilerOptions extends CompilerOptions { required NullSafety nullSafety, required super.canaryFeatures, required List experiments, + super.moduleFormat = ModuleFormat.amd, }) : super( - moduleFormat: 'amd', soundNullSafety: nullSafety == NullSafety.sound, experiments: const [], ); diff --git a/dwds/test/frontend_server_ddc_evaluate_sound_test.dart b/dwds/test/frontend_server_ddc_evaluate_sound_test.dart new file mode 100644 index 000000000..8506896e4 --- /dev/null +++ b/dwds/test/frontend_server_ddc_evaluate_sound_test.dart @@ -0,0 +1,53 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +@Tags(['daily']) +@TestOn('vm') +@Timeout(Duration(minutes: 5)) + +import 'dart:io'; + +import 'package:dwds/expression_compiler.dart'; +import 'package:test/test.dart'; +import 'package:test_common/test_sdk_configuration.dart'; + +import 'evaluate_common.dart'; +import 'fixtures/context.dart'; +import 'fixtures/project.dart'; + +void main() async { + // Enable verbose logging for debugging. + final debug = false; + + final provider = TestSdkConfigurationProvider( + verbose: debug, + ddcModuleFormat: ModuleFormat.ddc, + ); + tearDownAll(provider.dispose); + + for (var useDebuggerModuleNames in [false, true]) { + group('Debugger module names: $useDebuggerModuleNames |', () { + final nullSafety = NullSafety.sound; + group('${nullSafety.name} null safety | DDC module system |', () { + for (var indexBaseMode in IndexBaseMode.values) { + group( + 'with ${indexBaseMode.name} |', + () { + testAll( + provider: provider, + compilationMode: CompilationMode.frontendServer, + indexBaseMode: indexBaseMode, + nullSafety: nullSafety, + useDebuggerModuleNames: useDebuggerModuleNames, + debug: debug, + ); + }, + // https://github.com/dart-lang/sdk/issues/49277 + skip: indexBaseMode == IndexBaseMode.base && Platform.isWindows, + ); + } + }); + }); + } +} diff --git a/dwds/test/reload_test.dart b/dwds/test/reload_test.dart index 6c425eaff..8a0d8eb10 100644 --- a/dwds/test/reload_test.dart +++ b/dwds/test/reload_test.dart @@ -42,438 +42,290 @@ void main() { ); } - group('Injected client with live reload', () { - group('and with debugging', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.liveReload, - ), - ); + group( + 'Injected client with live reload', + () { + group('and with debugging', () { + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + reloadConfiguration: ReloadConfiguration.liveReload, + ), + ); + }); + + tearDown(() async { + undoEdit(); + await context.tearDown(); + }); + + test('can live reload changes ', () async { + await makeEditAndWaitForRebuild(); + final source = await context.webDriver.pageSource; + + // A full reload should clear the state. + expect(source.contains(originalString), isFalse); + expect(source.contains(newString), isTrue); + }); }); - tearDown(() async { - undoEdit(); - await context.tearDown(); + group('and without debugging', () { + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + reloadConfiguration: ReloadConfiguration.liveReload, + ), + debugSettings: TestDebugSettings.noDevTools().copyWith( + enableDebugging: false, + ), + ); + }); + + tearDown(() async { + undoEdit(); + await context.tearDown(); + }); + + test('can live reload changes ', () async { + await makeEditAndWaitForRebuild(); + + final source = await context.webDriver.pageSource; + + // A full reload should clear the state. + expect(source.contains(originalString), isFalse); + expect(source.contains(newString), isTrue); + }); }); - test('can live reload changes ', () async { - await makeEditAndWaitForRebuild(); - final source = await context.webDriver.pageSource; - - // A full reload should clear the state. - expect(source.contains(originalString), isFalse); - expect(source.contains(newString), isTrue); + group('and without debugging using WebSockets', () { + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + reloadConfiguration: ReloadConfiguration.liveReload, + ), + debugSettings: TestDebugSettings.noDevTools().copyWith( + enableDebugging: false, + useSse: false, + ), + ); + }); + + tearDown(() async { + await context.tearDown(); + undoEdit(); + }); + + test('can live reload changes ', () async { + await makeEditAndWaitForRebuild(); + + final source = await context.webDriver.pageSource; + + // A full reload should clear the state. + expect(source.contains(originalString), isFalse); + expect(source.contains(newString), isTrue); + }); }); - }); + }, + timeout: Timeout.factor(2), + ); - group('and without debugging', () { + group( + 'Injected client', + () { setUp(() async { setCurrentLogWriter(debug: debug); await context.setUp( testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.liveReload, - ), - debugSettings: TestDebugSettings.noDevTools().copyWith( - enableDebugging: false, + enableExpressionEvaluation: true, ), ); }); tearDown(() async { - undoEdit(); await context.tearDown(); + undoEdit(); }); - test('can live reload changes ', () async { + test('destroys and recreates the isolate during a hot restart', () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); await makeEditAndWaitForRebuild(); - final source = await context.webDriver.pageSource; + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); - // A full reload should clear the state. - expect(source.contains(originalString), isFalse); - expect(source.contains(newString), isTrue); + expect( + await client.callServiceExtension('hotRestart'), + const TypeMatcher(), + ); + + await eventsDone; }); - }); - group('and without debugging using WebSockets', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.liveReload, + test('can execute simultaneous hot restarts', () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); + await makeEditAndWaitForRebuild(); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), ), - debugSettings: TestDebugSettings.noDevTools().copyWith( - enableDebugging: false, - useSse: false, + ); + + // Execute two hot restart calls in parallel. + final done = Future.wait([ + client.callServiceExtension('hotRestart'), + client.callServiceExtension('hotRestart'), + ]); + expect( + await done, + [const TypeMatcher(), const TypeMatcher()], + ); + + // The debugger is still working. + final vm = await client.getVM(); + final isolateId = vm.isolates!.first.id!; + final isolate = await client.getIsolate(isolateId); + final library = isolate.rootLib!.uri!; + + final result = await client.evaluate(isolateId, library, 'true'); + expect( + result, + isA().having( + (instance) => instance.valueAsString, + 'valueAsString', + 'true', ), ); - }); - tearDown(() async { - await context.tearDown(); - undoEdit(); + await eventsDone; }); - test('can live reload changes ', () async { + test('destroys and recreates the isolate during a page refresh', + () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); await makeEditAndWaitForRebuild(); - final source = await context.webDriver.pageSource; - - // A full reload should clear the state. - expect(source.contains(originalString), isFalse); - expect(source.contains(newString), isTrue); - }); - }); - }); - - group('Injected client', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - enableExpressionEvaluation: true, - ), - ); - }); - - tearDown(() async { - await context.tearDown(); - undoEdit(); - }); - - test('destroys and recreates the isolate during a hot restart', () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - await makeEditAndWaitForRebuild(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - expect( - await client.callServiceExtension('hotRestart'), - const TypeMatcher(), - ); - - await eventsDone; - }); - - test('can execute simultaneous hot restarts', () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - await makeEditAndWaitForRebuild(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - // Execute two hot restart calls in parallel. - final done = Future.wait([ - client.callServiceExtension('hotRestart'), - client.callServiceExtension('hotRestart'), - ]); - expect( - await done, - [const TypeMatcher(), const TypeMatcher()], - ); - - // The debugger is still working. - final vm = await client.getVM(); - final isolateId = vm.isolates!.first.id!; - final isolate = await client.getIsolate(isolateId); - final library = isolate.rootLib!.uri!; - - final result = await client.evaluate(isolateId, library, 'true'); - expect( - result, - isA().having( - (instance) => instance.valueAsString, - 'valueAsString', - 'true', - ), - ); - - await eventsDone; - }); - - test('destroys and recreates the isolate during a page refresh', () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - await makeEditAndWaitForRebuild(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - await context.webDriver.driver.refresh(); - - await eventsDone; - }); - - test('can hot restart via the service extension', () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - await makeEditAndWaitForRebuild(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - expect( - await client.callServiceExtension('hotRestart'), - const TypeMatcher(), - ); - - await eventsDone; - - final source = await context.webDriver.pageSource; - // Main is re-invoked which shouldn't clear the state. - expect(source, contains(originalString)); - expect(source, contains(newString)); - }); - - test('can send events before and after hot restart', () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - - // The event just before hot restart might never be received, - // but the injected client continues to work and send events - // after hot restart. - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - _hasKind(EventKind.kServiceExtensionAdded) - .having((e) => e.extensionRPC, 'service', 'ext.bar'), - ), - ); - - var vm = await client.getVM(); - var isolateId = vm.isolates!.first.id!; - var isolate = await client.getIsolate(isolateId); - var library = isolate.rootLib!.uri!; - - final String callback = - '(_, __) async => ServiceExtensionResponse.result("")'; - - await client.evaluate( - isolateId, - library, - "registerExtension('ext.foo', $callback)", - ); - - expect( - await client.callServiceExtension('hotRestart'), - const TypeMatcher(), - ); - - vm = await client.getVM(); - isolateId = vm.isolates!.first.id!; - isolate = await client.getIsolate(isolateId); - library = isolate.rootLib!.uri!; - - await client.evaluate( - isolateId, - library, - "registerExtension('ext.bar', $callback)", - ); - - await eventsDone; - - final source = await context.webDriver.pageSource; - // Main is re-invoked which shouldn't clear the state. - expect(source, contains('Hello World!')); - }); - - test('can refresh the page via the fullReload service extension', () async { - final client = context.debugConnection.vmService; - await client.streamListen('Isolate'); - await makeEditAndWaitForRebuild(); - - final eventsDone = expectLater( - client.onIsolateEvent, - emitsThrough( - emitsInOrder([ - _hasKind(EventKind.kIsolateExit), - _hasKind(EventKind.kIsolateStart), - _hasKind(EventKind.kIsolateRunnable), - ]), - ), - ); - - expect(await client.callServiceExtension('fullReload'), isA()); - - await eventsDone; - - final source = await context.webDriver.pageSource; - // Should see only the new text - expect(source.contains(originalString), isFalse); - expect(source.contains(newString), isTrue); - }); - - test('can hot restart while paused', () async { - final client = context.debugConnection.vmService; - var vm = await client.getVM(); - var isolateId = vm.isolates!.first.id!; - await client.streamListen('Debug'); - final stream = client.onEvent('Debug'); - final scriptList = await client.getScripts(isolateId); - final main = scriptList.scripts! - .firstWhere((script) => script.uri!.contains('main.dart')); - final bpLine = - await context.findBreakpointLine('printCount', isolateId, main); - await client.addBreakpoint(isolateId, main.id!, bpLine); - await stream - .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); - - await makeEditAndWaitForRebuild(); - await client.callServiceExtension('hotRestart'); - final source = await context.webDriver.pageSource; - - // Main is re-invoked which shouldn't clear the state. - expect(source.contains(originalString), isTrue); - expect(source.contains(newString), isTrue); - - vm = await client.getVM(); - isolateId = vm.isolates!.first.id!; - final isolate = await client.getIsolate(isolateId); - - // Previous breakpoint should still exist. - expect(isolate.breakpoints!.isNotEmpty, isTrue); - final bp = isolate.breakpoints!.first; - - // Should pause eventually. - await stream - .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); - - expect( - await client.removeBreakpoint(isolate.id!, bp.id!), - isA(), - ); - expect(await client.resume(isolate.id!), isA()); - }); - - test('can evaluate expressions after hot restart ', () async { - final client = context.debugConnection.vmService; - var vm = await client.getVM(); - var isolateId = vm.isolates!.first.id!; - await client.streamListen('Debug'); - final stream = client.onEvent('Debug'); - final scriptList = await client.getScripts(isolateId); - final main = scriptList.scripts! - .firstWhere((script) => script.uri!.contains('main.dart')); - final bpLine = - await context.findBreakpointLine('printCount', isolateId, main); - await client.addBreakpoint(isolateId, main.id!, bpLine); - await stream - .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); - - await client.callServiceExtension('hotRestart'); - - vm = await client.getVM(); - isolateId = vm.isolates!.first.id!; - final isolate = await client.getIsolate(isolateId); - final library = isolate.rootLib!.uri!; - final bp = isolate.breakpoints!.first; - - // Should pause eventually. - final event = await stream - .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); - - // Expression evaluation while paused on a breakpoint should work. - var result = await client.evaluateInFrame( - isolate.id!, - event.topFrame!.index!, - 'count', - ); - expect( - result, - isA().having( - (instance) => instance.valueAsString, - 'valueAsString', - greaterThanOrEqualTo('0'), - ), - ); - - await client.removeBreakpoint(isolateId, bp.id!); - await client.resume(isolateId); - - // Expression evaluation while running should work. - result = await client.evaluate(isolateId, library, 'true'); - expect( - result, - isA().having( - (instance) => instance.valueAsString, - 'valueAsString', - 'true', - ), - ); - }); - }); - - group('Injected client with hot restart', () { - group('and with debugging', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.hotRestart, + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), ), ); - }); - tearDown(() async { - await context.tearDown(); - undoEdit(); + await context.webDriver.driver.refresh(); + + await eventsDone; }); - test('can hot restart changes ', () async { + test('can hot restart via the service extension', () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); await makeEditAndWaitForRebuild(); - final source = await context.webDriver.pageSource; + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + expect( + await client.callServiceExtension('hotRestart'), + const TypeMatcher(), + ); + + await eventsDone; + final source = await context.webDriver.pageSource; // Main is re-invoked which shouldn't clear the state. - expect(source.contains(originalString), isTrue); - expect(source.contains(newString), isTrue); - // The ext.flutter.disassemble callback is invoked and waited for. + expect(source, contains(originalString)); + expect(source, contains(newString)); + }); + + test('can send events before and after hot restart', () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); + + // The event just before hot restart might never be received, + // but the injected client continues to work and send events + // after hot restart. + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + _hasKind(EventKind.kServiceExtensionAdded) + .having((e) => e.extensionRPC, 'service', 'ext.bar'), + ), + ); + + var vm = await client.getVM(); + var isolateId = vm.isolates!.first.id!; + var isolate = await client.getIsolate(isolateId); + var library = isolate.rootLib!.uri!; + + final String callback = + '(_, __) async => ServiceExtensionResponse.result("")'; + + await client.evaluate( + isolateId, + library, + "registerExtension('ext.foo', $callback)", + ); + expect( - source, - contains('start disassemble end disassemble $newString'), + await client.callServiceExtension('hotRestart'), + const TypeMatcher(), + ); + + vm = await client.getVM(); + isolateId = vm.isolates!.first.id!; + isolate = await client.getIsolate(isolateId); + library = isolate.rootLib!.uri!; + + await client.evaluate( + isolateId, + library, + "registerExtension('ext.bar', $callback)", ); + + await eventsDone; + + final source = await context.webDriver.pageSource; + // Main is re-invoked which shouldn't clear the state. + expect(source, contains('Hello World!')); }); - test('fires isolate create/destroy events during hot restart', () async { + test('can refresh the page via the fullReload service extension', + () async { final client = context.debugConnection.vmService; await client.streamListen('Isolate'); + await makeEditAndWaitForRebuild(); final eventsDone = expectLater( client.onIsolateEvent, @@ -486,45 +338,208 @@ void main() { ), ); - await makeEditAndWaitForRebuild(); + expect(await client.callServiceExtension('fullReload'), isA()); await eventsDone; - }); - }); - group('and without debugging', () { - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - reloadConfiguration: ReloadConfiguration.hotRestart, - ), - debugSettings: - TestDebugSettings.noDevTools().copyWith(enableDebugging: false), - ); + final source = await context.webDriver.pageSource; + // Should see only the new text + expect(source.contains(originalString), isFalse); + expect(source.contains(newString), isTrue); }); - tearDown(() async { - await context.tearDown(); - undoEdit(); - }); + test('can hot restart while paused', () async { + final client = context.debugConnection.vmService; + var vm = await client.getVM(); + var isolateId = vm.isolates!.first.id!; + await client.streamListen('Debug'); + final stream = client.onEvent('Debug'); + final scriptList = await client.getScripts(isolateId); + final main = scriptList.scripts! + .firstWhere((script) => script.uri!.contains('main.dart')); + final bpLine = + await context.findBreakpointLine('printCount', isolateId, main); + await client.addBreakpoint(isolateId, main.id!, bpLine); + await stream + .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); - test('can hot restart changes ', () async { await makeEditAndWaitForRebuild(); - + await client.callServiceExtension('hotRestart'); final source = await context.webDriver.pageSource; // Main is re-invoked which shouldn't clear the state. expect(source.contains(originalString), isTrue); expect(source.contains(newString), isTrue); - // The ext.flutter.disassemble callback is invoked and waited for. + + vm = await client.getVM(); + isolateId = vm.isolates!.first.id!; + final isolate = await client.getIsolate(isolateId); + + // Previous breakpoint should still exist. + expect(isolate.breakpoints!.isNotEmpty, isTrue); + final bp = isolate.breakpoints!.first; + + // Should pause eventually. + await stream + .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); + expect( - source, - contains('start disassemble end disassemble $newString'), + await client.removeBreakpoint(isolate.id!, bp.id!), + isA(), ); + expect(await client.resume(isolate.id!), isA()); + }); + + test('can evaluate expressions after hot restart ', () async { + final client = context.debugConnection.vmService; + var vm = await client.getVM(); + var isolateId = vm.isolates!.first.id!; + await client.streamListen('Debug'); + final stream = client.onEvent('Debug'); + final scriptList = await client.getScripts(isolateId); + final main = scriptList.scripts! + .firstWhere((script) => script.uri!.contains('main.dart')); + final bpLine = + await context.findBreakpointLine('printCount', isolateId, main); + await client.addBreakpoint(isolateId, main.id!, bpLine); + await stream + .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); + + await client.callServiceExtension('hotRestart'); + + vm = await client.getVM(); + isolateId = vm.isolates!.first.id!; + final isolate = await client.getIsolate(isolateId); + final library = isolate.rootLib!.uri!; + final bp = isolate.breakpoints!.first; + + // Should pause eventually. + final event = await stream + .firstWhere((event) => event.kind == EventKind.kPauseBreakpoint); + + // Expression evaluation while paused on a breakpoint should work. + var result = await client.evaluateInFrame( + isolate.id!, + event.topFrame!.index!, + 'count', + ); + expect( + result, + isA().having( + (instance) => instance.valueAsString, + 'valueAsString', + greaterThanOrEqualTo('0'), + ), + ); + + await client.removeBreakpoint(isolateId, bp.id!); + await client.resume(isolateId); + + // Expression evaluation while running should work. + result = await client.evaluate(isolateId, library, 'true'); + expect( + result, + isA().having( + (instance) => instance.valueAsString, + 'valueAsString', + 'true', + ), + ); + }); + }, + timeout: Timeout.factor(2), + ); + + group( + 'Injected client with hot restart', + () { + group('and with debugging', () { + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + reloadConfiguration: ReloadConfiguration.hotRestart, + ), + ); + }); + + tearDown(() async { + await context.tearDown(); + undoEdit(); + }); + + test('can hot restart changes ', () async { + await makeEditAndWaitForRebuild(); + + final source = await context.webDriver.pageSource; + + // Main is re-invoked which shouldn't clear the state. + expect(source.contains(originalString), isTrue); + expect(source.contains(newString), isTrue); + // The ext.flutter.disassemble callback is invoked and waited for. + expect( + source, + contains('start disassemble end disassemble $newString'), + ); + }); + + test('fires isolate create/destroy events during hot restart', + () async { + final client = context.debugConnection.vmService; + await client.streamListen('Isolate'); + + final eventsDone = expectLater( + client.onIsolateEvent, + emitsThrough( + emitsInOrder([ + _hasKind(EventKind.kIsolateExit), + _hasKind(EventKind.kIsolateStart), + _hasKind(EventKind.kIsolateRunnable), + ]), + ), + ); + + await makeEditAndWaitForRebuild(); + + await eventsDone; + }); + }); + + group('and without debugging', () { + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + reloadConfiguration: ReloadConfiguration.hotRestart, + ), + debugSettings: + TestDebugSettings.noDevTools().copyWith(enableDebugging: false), + ); + }); + + tearDown(() async { + await context.tearDown(); + undoEdit(); + }); + + test('can hot restart changes ', () async { + await makeEditAndWaitForRebuild(); + + final source = await context.webDriver.pageSource; + + // Main is re-invoked which shouldn't clear the state. + expect(source.contains(originalString), isTrue); + expect(source.contains(newString), isTrue); + // The ext.flutter.disassemble callback is invoked and waited for. + expect( + source, + contains('start disassemble end disassemble $newString'), + ); + }); }); - }); - }); + }, + timeout: Timeout.factor(2), + ); } TypeMatcher _hasKind(String kind) => diff --git a/dwds/test/run_request_test.dart b/dwds/test/run_request_test.dart index 89db646a5..8efd4f0c4 100644 --- a/dwds/test/run_request_test.dart +++ b/dwds/test/run_request_test.dart @@ -24,51 +24,57 @@ void main() { final context = TestContext(TestProject.testWithSoundNullSafety, provider); - group('while debugger is attached', () { - late VmServiceInterface service; - setUp(() async { - setCurrentLogWriter(debug: debug); - await context.setUp( - testSettings: TestSettings( - autoRun: false, - verboseCompiler: debug, - ), - ); - service = context.service; - }); + group( + 'while debugger is attached', + () { + late VmServiceInterface service; + setUp(() async { + setCurrentLogWriter(debug: debug); + await context.setUp( + testSettings: TestSettings( + autoRun: false, + verboseCompiler: debug, + ), + ); + service = context.service; + }); - tearDown(() async { - await context.tearDown(); - }); + tearDown(() async { + await context.tearDown(); + }); - test('can resume while paused at the start', () async { - final vm = await service.getVM(); - final isolate = await service.getIsolate(vm.isolates!.first.id!); - expect(isolate.pauseEvent!.kind, EventKind.kPauseStart); - final stream = service.onEvent('Debug'); - final resumeCompleter = Completer(); - // The underlying stream is a broadcast stream so we need to add a - // listener before calling resume so that we don't miss events. - unawaited( - stream.firstWhere((event) => event.kind == EventKind.kResume).then((_) { - resumeCompleter.complete(); - }), - ); - await service.resume(isolate.id!); - await resumeCompleter.future; - expect(isolate.pauseEvent!.kind, EventKind.kResume); - }); + test('can resume while paused at the start', () async { + final vm = await service.getVM(); + final isolate = await service.getIsolate(vm.isolates!.first.id!); + expect(isolate.pauseEvent!.kind, EventKind.kPauseStart); + final stream = service.onEvent('Debug'); + final resumeCompleter = Completer(); + // The underlying stream is a broadcast stream so we need to add a + // listener before calling resume so that we don't miss events. + unawaited( + stream + .firstWhere((event) => event.kind == EventKind.kResume) + .then((_) { + resumeCompleter.complete(); + }), + ); + await service.resume(isolate.id!); + await resumeCompleter.future; + expect(isolate.pauseEvent!.kind, EventKind.kResume); + }); - test('correctly sets the isolate pauseEvent', () async { - final vm = await service.getVM(); - final isolate = await service.getIsolate(vm.isolates!.first.id!); - expect(isolate.pauseEvent!.kind, EventKind.kPauseStart); - final stream = service.onEvent('Debug'); - context.appConnection.runMain(); - await stream.firstWhere((event) => event.kind == EventKind.kResume); - expect(isolate.pauseEvent!.kind, EventKind.kResume); - }); - }); + test('correctly sets the isolate pauseEvent', () async { + final vm = await service.getVM(); + final isolate = await service.getIsolate(vm.isolates!.first.id!); + expect(isolate.pauseEvent!.kind, EventKind.kPauseStart); + final stream = service.onEvent('Debug'); + context.appConnection.runMain(); + await stream.firstWhere((event) => event.kind == EventKind.kResume); + expect(isolate.pauseEvent!.kind, EventKind.kResume); + }); + }, + timeout: Timeout.factor(2), + ); group('while debugger is not attached', () { setUp(() async { diff --git a/frontend_server_common/lib/src/bootstrap.dart b/frontend_server_common/lib/src/bootstrap.dart index a5d1ba0c4..206e73e51 100644 --- a/frontend_server_common/lib/src/bootstrap.dart +++ b/frontend_server_common/lib/src/bootstrap.dart @@ -4,6 +4,78 @@ // Note: this is a copy from flutter tools, updated to work with dwds tests +/// JavaScript snippet to determine the base URL of the current path. +const String _baseUrlScript = ''' +var baseUrl = (function () { + // Attempt to detect --precompiled mode for tests, and set the base url + // appropriately, otherwise set it to '/'. + var pathParts = location.pathname.split("/"); + if (pathParts[0] == "") { + pathParts.shift(); + } + if (pathParts.length > 1 && pathParts[1] == "test") { + return "/" + pathParts.slice(0, 2).join("/") + "/"; + } + // Attempt to detect base url using html tag + // base href should start and end with "/" + if (typeof document !== 'undefined') { + var el = document.getElementsByTagName('base'); + if (el && el[0] && el[0].getAttribute("href") && el[0].getAttribute + ("href").startsWith("/") && el[0].getAttribute("href").endsWith("/")){ + return el[0].getAttribute("href"); + } + } + // return default value + return "/"; +}()); +var _trimmedBaseUrl = baseUrl.endsWith('/') ? baseUrl.substring(0, baseUrl.length - 1) : baseUrl; +var _currentDirectory = window.location.origin + _trimmedBaseUrl; +'''; + +/// Used to load prerequisite scripts such as ddc_module_loader.js +const String _simpleLoaderScript = r''' +window.$dartCreateScript = (function() { + // Find the nonce value. (Note, this is only computed once.) + var scripts = Array.from(document.getElementsByTagName("script")); + var nonce; + scripts.some( + script => (nonce = script.nonce || script.getAttribute("nonce"))); + // If present, return a closure that automatically appends the nonce. + if (nonce) { + return function() { + var script = document.createElement("script"); + script.nonce = nonce; + return script; + }; + } else { + return function() { + return document.createElement("script"); + }; + } +})(); + +// Loads a module [relativeUrl] relative to [root]. +// +// If not specified, [root] defaults to the directory serving the main app. +var forceLoadModule = function (relativeUrl, root) { + var actualRoot = root ?? _currentDirectory; + var trimmedRoot = actualRoot.endsWith('/') ? actualRoot.substring(0, actualRoot.length - 1) : actualRoot; + return new Promise(function(resolve, reject) { + var script = self.$dartCreateScript(); + let policy = { + createScriptURL: function(src) {return src;} + }; + if (self.trustedTypes && self.trustedTypes.createPolicy) { + policy = self.trustedTypes.createPolicy('dartDdcModuleUrl', policy); + } + script.onload = resolve; + script.onerror = reject; + script.src = policy.createScriptURL(trimmedRoot + "/" + relativeUrl); + document.head.appendChild(script); + }); +}; +'''; + /// The JavaScript bootstrap script to support in-browser hot restart. /// /// The [requireUrl] loads our cached RequireJS script file. The [mapperUrl] @@ -68,3 +140,244 @@ define("main_module.bootstrap", ["$entrypoint", "dart_sdk"], function(app, dart_ }); '''; } + +String generateDDCBootstrapScript({ + required String ddcModuleLoaderUrl, + required String mapperUrl, + required String entrypoint, + required String bootstrapUrl, +}) { + return ''' +$_baseUrlScript +$_simpleLoaderScript + +(function() { + let appName = "$entrypoint"; + + // A uuid that identifies a subapp. + let uuid = "00000000-0000-0000-0000-000000000000"; + + window.postMessage( + {type: "DDC_STATE_CHANGE", state: "initial_load", targetUuid: uuid}, "*"); + + // Load pre-requisite DDC scripts. We intentionally use invalid names to avoid namespace clashes. + let prerequisiteScripts = [ + { + "src": "$ddcModuleLoaderUrl", + "id": "dart_library \x00" + }, + { + "src": "$mapperUrl", + "id": "dart_stack_trace_mapper \x00" + } + ]; + + // Load ddc_module_loader.js to access DDC's module loader API. + let prerequisiteLoads = []; + for (let i = 0; i < prerequisiteScripts.length; i++) { + prerequisiteLoads.push(forceLoadModule(prerequisiteScripts[i].src)); + } + Promise.all(prerequisiteLoads).then((_) => afterPrerequisiteLogic()); + + // Save the current script so we can access it in a closure. + var _currentScript = document.currentScript; + + var afterPrerequisiteLogic = function() { + window.\$dartLoader.rootDirectories.push(_currentDirectory); + let scripts = [ + { + "src": "dart_sdk.js", + "id": "dart_sdk" + }, + { + "src": "$bootstrapUrl", + "id": "data-main" + } + ]; + let loadConfig = new window.\$dartLoader.LoadConfiguration(); + loadConfig.root = _currentDirectory; + loadConfig.bootstrapScript = scripts[scripts.length - 1]; + + if (window.\$dartJITModules) { + loadConfig.loadScriptFn = function(loader) { + // Loads just the entrypoint module and required SDK modules. + let moduleSet = new Set(); + // This cache is populated by ddc_module_loader.js + let libraryCache = JSON.parse(window.localStorage.getItem(`dartLibraryCache:\${appName}`)); + if (libraryCache) { + // TODO(b/165021238) - when should this be invalidated? + moduleSet = new Set(libraryCache["modules"]) + } + loader.addScriptsToQueue(scripts, function(script) { + // Preemptively load the ddc module loader and previously executed modules. + return moduleSet.size == 0 + || script.id.includes("dart_library") + // We preemptively load the stack_trace_mapper module so that we can + // translate JS errors to Dart. + || script.id.includes("stack_trace_mapper") + || moduleSet.has(script.id); + }); + loader.loadEnqueuedModules(); + } + loadConfig.ddcEventForLoadStart = /* LOAD_ENTRYPOINT_MODULES_START */ 4; + loadConfig.ddcEventForLoadedOk = /* LOAD_ENTRYPOINT_MODULES_END_OK */ 5; + loadConfig.ddcEventForLoadedError = /* LOAD_ENTRYPOINT_MODULES_END_ERROR */ 6; + } else { + loadConfig.loadScriptFn = function(loader) { + loader.addScriptsToQueue(scripts, null); + loader.loadEnqueuedModules(); + } + loadConfig.ddcEventForLoadStart = /* LOAD_ALL_MODULES_START */ 1; + loadConfig.ddcEventForLoadedOk = /* LOAD_ALL_MODULES_END_OK */ 2; + loadConfig.ddcEventForLoadedError = /* LOAD_ALL_MODULES_END_ERROR */ 3; + } + + let loader = new window.\$dartLoader.DDCLoader(loadConfig); + + // Record prerequisite scripts' fully resolved URLs. + prerequisiteScripts.forEach(script => loader.registerScript(script)); + + // Note: these variables should only be used in non-multi-app scenarios since + // they can be arbitrarily overridden based on multi-app load order. + window.\$dartLoader.loadConfig = loadConfig; + window.\$dartLoader.loader = loader; + loader.nextAttempt(); + + let currentUri = _currentScript.src; + let fetchEtagsUri; + if (currentUri.indexOf("?") == -1) { + fetchEtagsUri = currentUri + "?fetch-etags=true"; + } else { + fetchEtagsUri = currentUri + "&fetch-etags=true"; + } + + if (!window.\$dartAppNameToMetadata) { + window.\$dartAppNameToMetadata = new Map(); + } + window.\$dartAppNameToMetadata.set(appName, { + currentDirectory: _currentDirectory, + currentUri: currentUri, + fetchEtagsUri: fetchEtagsUri, + }); + + if (!window.\$dartReloadModifiedModules) { + window.\$dartReloadModifiedModules = (function(appName, callback) { + function cb() { + window.postMessage( + { + type: "DDC_STATE_CHANGE", + state: "restart_end", + targetUuid: uuid, + }, + "*"); + callback(); + } + window.postMessage( + { + type: "DDC_STATE_CHANGE", + state: "restart_begin", + targetUuid: uuid, + }, + "*"); + var xhttp = new XMLHttpRequest(); + xhttp.withCredentials = true; + xhttp.onreadystatechange = function() { + // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState + if (this.readyState == 4 && this.status == 200 || this.status == 304) { + var scripts = JSON.parse(this.responseText); + var numToLoad = 0; + var numLoaded = 0; + for (var i = 0; i < scripts.length; i++) { + var script = scripts[i]; + if (script.id == null) continue; + var src = + window.\$dartAppNameToMetadata.get(appName).currentDirectory + + script.src.toString(); + var oldSrc = window.\$dartLoader.moduleIdToUrl.get(script.id); + // Only compare the search parameters which contain the cache + // busting portion of the uri. The path might be different if the + // script is loaded from a different application on the page. + if (new URL(oldSrc).search == new URL(src).search) continue; + + // We might actually load from a different uri, delete the old one + // just to be sure. + window.\$dartLoader.urlToModuleId.delete(oldSrc); + + window.\$dartLoader.moduleIdToUrl.set(script.id, src); + window.\$dartLoader.urlToModuleId.set(src, script.id); + + if (window.\$dartJITModules) { + // Simply invalidate the import and the corresponding module will + // be lazily loaded. + dart_library.invalidateImport(script.id); + continue; + } else { + numToLoad++; + } + + var el = document.getElementById(script.id); + if (el) el.remove(); + el = window.\$dartCreateScript(); + el.src = policy.createScriptURL(src); + el.async = false; + el.defer = true; + el.id = script.id; + el.onload = function() { + numLoaded++; + if (numToLoad == numLoaded) cb(); + }; + document.head.appendChild(el); + } + // Call `cb` right away if we found no updated scripts. + if (numToLoad == 0) cb(); + } + }; + xhttp.open("GET", + window.\$dartAppNameToMetadata.get(appName).fetchEtagsUri, true); + let sdk = dart_library.import("dart_sdk", appName); + let developer = sdk.developer; + if (developer._extensions.containsKey("ext.flutter.disassemble")) { + developer.invokeExtension("ext.flutter.disassemble", "{}").then(() => { + // TODO(b/204210914): we should really be clearing all statics for all + // apps, but for now we just do it for flutter apps which we recognize + // based on this extension. + sdk.dart.hotRestart(); + xhttp.send(); + }); + } else { + xhttp.send(); + } + }); + } + } +})(); +'''; +} + +String generateDDCMainModule( + {required String entrypoint, String? exportedMain}) { + final exportedMainName = exportedMain ?? entrypoint.split('.')[0]; + return '''/* ENTRYPOINT_EXTENTION_MARKER */ + +(function() { + let appName = "$entrypoint"; + + // A uuid that identifies a subapp. + let uuid = "00000000-0000-0000-0000-000000000000"; + + let dart_sdk = dart_library.import('dart_sdk', appName); + + dart_sdk.dart.setStartAsyncSynchronously(true); + dart_sdk._debugger.registerDevtoolsFormatter(); + dart_sdk._isolate_helper.startRootIsolate(() => {}, []); + + let child = {}; + child.main = function() { + dart_library.start(appName, uuid, "$entrypoint", "$exportedMainName"); + } + + /* MAIN_EXTENSION_MARKER */ + child.main(); +})(); +'''; +} diff --git a/frontend_server_common/lib/src/devfs.dart b/frontend_server_common/lib/src/devfs.dart index 64e3e4512..51a8fed65 100644 --- a/frontend_server_common/lib/src/devfs.dart +++ b/frontend_server_common/lib/src/devfs.dart @@ -6,6 +6,8 @@ import 'package:dwds/asset_reader.dart'; import 'package:dwds/config.dart'; +import 'package:dwds/expression_compiler.dart'; +import 'package:dwds/utilities.dart'; import 'package:file/file.dart'; import 'package:path/path.dart' as p; import 'package:test_common/test_sdk_layout.dart'; @@ -25,6 +27,7 @@ class WebDevFS { required this.soundNullSafety, this.urlTunneler, required this.sdkLayout, + required this.ddcModuleFormat, }); final FileSystem fileSystem; @@ -37,6 +40,7 @@ class WebDevFS { final UrlEncoder? urlTunneler; final bool soundNullSafety; final TestSdkLayout sdkLayout; + final ModuleFormat ddcModuleFormat; late final Directory _savedCurrentDirectory; Future create() async { @@ -44,8 +48,15 @@ class WebDevFS { fileSystem.currentDirectory = projectDirectory.toFilePath(); - assetServer = await TestAssetServer.start(sdkLayout.sdkDirectory, - fileSystem, index, hostname, port, urlTunneler, packageUriMapper); + assetServer = await TestAssetServer.start( + sdkLayout.sdkDirectory, + fileSystem, + index, + hostname, + port, + urlTunneler, + packageUriMapper, + ); return Uri.parse('http://$hostname:$port'); } @@ -64,6 +75,7 @@ class WebDevFS { final outputDirectoryPath = fileSystem.file(mainPath).parent.path; final entryPoint = mainUri.toString(); + var ddcModuleLoader = 'ddc_module_loader.js'; var require = 'require.js'; var stackMapper = 'stack_trace_mapper.js'; var main = 'main.dart.js'; @@ -73,6 +85,7 @@ class WebDevFS { // to store all files, so the paths match the requests. if (assetServer.basePath.isEmpty) { final directory = p.dirname(entryPoint); + ddcModuleLoader = '$directory/ddc_module_loader.js'; require = '$directory/require.js'; stackMapper = '$directory/stack_trace_mapper.js'; main = '$directory/main.dart.js'; @@ -81,22 +94,51 @@ class WebDevFS { assetServer.writeFile( entryPoint, fileSystem.file(mainPath).readAsStringSync()); - assetServer.writeFile(require, requireJS.readAsStringSync()); assetServer.writeFile(stackMapper, stackTraceMapper.readAsStringSync()); - assetServer.writeFile( - main, - generateBootstrapScript( - requireUrl: 'require.js', - mapperUrl: 'stack_trace_mapper.js', - entrypoint: entryPoint, - ), - ); - assetServer.writeFile( - bootstrap, - generateMainModule( - entrypoint: entryPoint, - ), - ); + + switch (ddcModuleFormat) { + case ModuleFormat.amd: + assetServer.writeFile(require, requireJS.readAsStringSync()); + assetServer.writeFile( + main, + generateBootstrapScript( + requireUrl: 'require.js', + mapperUrl: 'stack_trace_mapper.js', + entrypoint: entryPoint, + ), + ); + assetServer.writeFile( + bootstrap, + generateMainModule( + entrypoint: entryPoint, + ), + ); + break; + case ModuleFormat.ddc: + assetServer.writeFile( + ddcModuleLoader, ddcModuleLoaderJS.readAsStringSync()); + assetServer.writeFile( + main, + generateDDCBootstrapScript( + ddcModuleLoaderUrl: ddcModuleLoader, + mapperUrl: stackMapper, + entrypoint: entryPoint, + bootstrapUrl: bootstrap, + ), + ); + // DDC uses a simple heuristic to determine exported identifier names. + // The module name (entrypoint name here) has its extension removed, and + // special path elements like '/', '\', and '..' are replaced with '__'. + final exportedMainName = pathToJSIdentifier(entryPoint.split('.')[0]); + assetServer.writeFile( + bootstrap, + generateDDCMainModule( + entrypoint: entryPoint, exportedMain: exportedMainName), + ); + break; + default: + throw Exception('Unsupported DDC module format $ddcModuleFormat.'); + } assetServer.writeFile('main_module.digests', '{}'); @@ -144,11 +186,29 @@ class WebDevFS { )..invalidatedModules = modules; } + File get ddcModuleLoaderJS => + fileSystem.file(sdkLayout.ddcModuleLoaderJsPath); File get requireJS => fileSystem.file(sdkLayout.requireJsPath); - File get dartSdkWeak => fileSystem.file(sdkLayout.weakJsPath); - File get dartSdk => fileSystem.file(sdkLayout.soundJsPath); - File get dartSdkSourcemapWeak => fileSystem.file(sdkLayout.weakJsMapPath); - File get dartSdkSourcemap => fileSystem.file(sdkLayout.soundJsMapPath); + File get dartSdkWeak => fileSystem.file(switch (ddcModuleFormat) { + ModuleFormat.amd => sdkLayout.weakAmdJsPath, + ModuleFormat.ddc => sdkLayout.weakDdcJsPath, + _ => throw Exception('Unsupported DDC module format $ddcModuleFormat.') + }); + File get dartSdk => fileSystem.file(switch (ddcModuleFormat) { + ModuleFormat.amd => sdkLayout.soundAmdJsPath, + ModuleFormat.ddc => sdkLayout.soundDdcJsPath, + _ => throw Exception('Unsupported DDC module format $ddcModuleFormat.') + }); + File get dartSdkSourcemapWeak => fileSystem.file(switch (ddcModuleFormat) { + ModuleFormat.amd => sdkLayout.weakAmdJsMapPath, + ModuleFormat.ddc => sdkLayout.weakDdcJsMapPath, + _ => throw Exception('Unsupported DDC module format $ddcModuleFormat.') + }); + File get dartSdkSourcemap => fileSystem.file(switch (ddcModuleFormat) { + ModuleFormat.amd => sdkLayout.soundAmdJsMapPath, + ModuleFormat.ddc => sdkLayout.soundDdcJsMapPath, + _ => throw Exception('Unsupported DDC module format $ddcModuleFormat.') + }); File get stackTraceMapper => fileSystem.file(sdkLayout.stackTraceMapperPath); } diff --git a/frontend_server_common/lib/src/frontend_server_client.dart b/frontend_server_common/lib/src/frontend_server_client.dart index 18b15b53e..6576a0b43 100644 --- a/frontend_server_common/lib/src/frontend_server_client.dart +++ b/frontend_server_common/lib/src/frontend_server_client.dart @@ -394,6 +394,8 @@ class ResidentCompiler { '--enable-experiment=$experiment', if (compilerOptions.canaryFeatures) '--dartdevc-canary', if (verbose) '--verbose', + if (compilerOptions.moduleFormat == ModuleFormat.ddc) + '--dartdevc-module-format=ddc' ]; _logger.info(args.join(' ')); diff --git a/frontend_server_common/lib/src/resident_runner.dart b/frontend_server_common/lib/src/resident_runner.dart index 86f142739..f57954bbf 100644 --- a/frontend_server_common/lib/src/resident_runner.dart +++ b/frontend_server_common/lib/src/resident_runner.dart @@ -81,6 +81,7 @@ class ResidentWebRunner { urlTunneler: urlTunneler, soundNullSafety: compilerOptions.soundNullSafety, sdkLayout: sdkLayout, + ddcModuleFormat: compilerOptions.moduleFormat, ); uri = await devFS.create(); diff --git a/test_common/lib/sdk_asset_generator.dart b/test_common/lib/sdk_asset_generator.dart index 16dc7be8e..bcf6d7977 100644 --- a/test_common/lib/sdk_asset_generator.dart +++ b/test_common/lib/sdk_asset_generator.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:dwds/expression_compiler.dart'; import 'package:file/file.dart'; import 'package:file/local.dart'; import 'package:logging/logging.dart'; @@ -18,6 +19,7 @@ class SdkAssetGenerator { final FileSystem fileSystem; final bool canaryFeatures; + final ModuleFormat ddcModuleFormat; final bool verbose; late final TestSdkLayout sdkLayout; @@ -26,6 +28,7 @@ class SdkAssetGenerator { this.fileSystem = const LocalFileSystem(), required this.sdkLayout, required this.canaryFeatures, + required this.ddcModuleFormat, this.verbose = false, }); @@ -52,6 +55,42 @@ class SdkAssetGenerator { } } + String resolveSdkJsPath({ + required bool soundNullSafety, + required bool canaryFeatures, + }) => + switch ((soundNullSafety, ddcModuleFormat)) { + (true, ModuleFormat.amd) => sdkLayout.soundAmdJsPath, + (false, ModuleFormat.amd) => sdkLayout.weakAmdJsPath, + (true, ModuleFormat.ddc) => sdkLayout.soundDdcJsPath, + (false, ModuleFormat.ddc) => sdkLayout.weakDdcJsPath, + _ => throw Exception('Unsupported DDC module format $ddcModuleFormat.') + }; + + String resolveSdkSourcemapPath({ + required bool soundNullSafety, + required bool canaryFeatures, + }) => + switch ((soundNullSafety, ddcModuleFormat)) { + (true, ModuleFormat.amd) => sdkLayout.soundAmdJsMapPath, + (false, ModuleFormat.amd) => sdkLayout.weakAmdJsMapPath, + (true, ModuleFormat.ddc) => sdkLayout.soundDdcJsMapPath, + (false, ModuleFormat.ddc) => sdkLayout.weakDdcJsMapPath, + _ => throw Exception('Unsupported DDC module format $ddcModuleFormat.') + }; + + String resolveSdkJsFilename({ + required bool soundNullSafety, + required bool canaryFeatures, + }) => + switch ((soundNullSafety, ddcModuleFormat)) { + (true, ModuleFormat.amd) => sdkLayout.soundAmdJsFileName, + (false, ModuleFormat.amd) => sdkLayout.weakAmdJsFileName, + (true, ModuleFormat.ddc) => sdkLayout.soundDdcJsFileName, + (false, ModuleFormat.ddc) => sdkLayout.weakDdcJsFileName, + _ => throw Exception('Unsupported DDC module format $ddcModuleFormat.') + }; + Future _generateSdkJavaScript({ required bool soundNullSafety, required bool canaryFeatures, @@ -59,10 +98,10 @@ class SdkAssetGenerator { Directory? outputDir; try { // Files to copy generated files to. - final outputJsPath = - soundNullSafety ? sdkLayout.soundJsPath : sdkLayout.weakJsPath; - final outputJsMapPath = - soundNullSafety ? sdkLayout.soundJsMapPath : sdkLayout.weakJsMapPath; + final outputJsPath = resolveSdkJsPath( + soundNullSafety: soundNullSafety, canaryFeatures: canaryFeatures); + final outputJsMapPath = resolveSdkSourcemapPath( + soundNullSafety: soundNullSafety, canaryFeatures: canaryFeatures); final outputFullDillPath = soundNullSafety ? sdkLayout.soundFullDillPath : sdkLayout.weakFullDillPath; @@ -79,9 +118,11 @@ class SdkAssetGenerator { outputDir = fileSystem.systemTempDirectory.createTempSync(); // Files to generate - final jsPath = soundNullSafety - ? p.join(outputDir.path, sdkLayout.soundJsFileName) - : p.join(outputDir.path, sdkLayout.weakJsFileName); + final jsPath = p.join( + outputDir.path, + resolveSdkJsFilename( + soundNullSafety: soundNullSafety, + canaryFeatures: canaryFeatures)); final jsMapPath = p.setExtension(jsPath, '.js.map'); final fullDillPath = p.setExtension(jsPath, '.dill'); @@ -98,7 +139,7 @@ class SdkAssetGenerator { '--libraries-file', 'org-dartlang-sdk:///lib/libraries.json', '--modules', - 'amd', + ddcModuleFormat.name, soundNullSafety ? '--sound-null-safety' : '--no-sound-null-safety', 'dart:core', '-o', @@ -241,12 +282,17 @@ class SdkAssetGenerator { Future _moveAndValidate(String from, String to) async { _logger.fine('Renaming $from to $to'); + if (!_exists(from)) { + _logger.severe('Failed to generate SDK asset at $to'); + throw Exception('File "$from" does not exist.'); + } + if (_exists(to)) _delete(to); await fileSystem.file(from).rename(to); if (!_exists(to)) { _logger.severe('Failed to generate SDK asset at $to'); - throw Exception('File does not exist.'); + throw Exception('File "$to" does not exist.'); } } } diff --git a/test_common/lib/test_sdk_configuration.dart b/test_common/lib/test_sdk_configuration.dart index dbc69185f..1fb9ec0b7 100644 --- a/test_common/lib/test_sdk_configuration.dart +++ b/test_common/lib/test_sdk_configuration.dart @@ -4,6 +4,7 @@ import 'dart:io'; +import 'package:dwds/expression_compiler.dart'; import 'package:dwds/sdk_configuration.dart'; import 'package:logging/logging.dart'; @@ -25,6 +26,7 @@ class TestSdkConfigurationProvider extends SdkConfigurationProvider { final bool _verbose; final bool canaryFeatures; + final ModuleFormat ddcModuleFormat; late final Directory _sdkDirectory; SdkConfiguration? _configuration; @@ -33,6 +35,7 @@ class TestSdkConfigurationProvider extends SdkConfigurationProvider { TestSdkConfigurationProvider({ this.canaryFeatures = false, bool verbose = false, + this.ddcModuleFormat = ModuleFormat.amd, }) : _verbose = verbose { _sdkDirectory = Directory.systemTemp.createTempSync('sdk copy'); sdkLayout = TestSdkLayout.createDefault(_sdkDirectory.path); @@ -62,6 +65,7 @@ class TestSdkConfigurationProvider extends SdkConfigurationProvider { sdkLayout: sdkLayout, canaryFeatures: canaryFeatures, verbose: _verbose, + ddcModuleFormat: ddcModuleFormat, ); await assetGenerator.generateSdkAssets(); diff --git a/test_common/lib/test_sdk_layout.dart b/test_common/lib/test_sdk_layout.dart index 9064ce8e9..8d90c4894 100644 --- a/test_common/lib/test_sdk_layout.dart +++ b/test_common/lib/test_sdk_layout.dart @@ -13,8 +13,10 @@ import 'package:path/path.dart' as p; /// We keep all the path constants in one place for ease of update. class TestSdkLayout { static final defaultSdkDirectory = SdkLayout.defaultSdkDirectory; + static TestSdkLayout defaultSdkLayout = TestSdkLayout.createDefault(defaultSdkDirectory); + static SdkConfiguration defaultSdkConfiguration = createConfiguration(defaultSdkLayout); @@ -32,7 +34,7 @@ class TestSdkLayout { '_internal', 'ddc_platform.dill', ), - soundJsPath: p.join( + soundAmdJsPath: p.join( sdkLayout.sdkDirectory, 'lib', 'dev_compiler', @@ -40,7 +42,7 @@ class TestSdkLayout { 'amd', 'dart_sdk.js', ), - soundJsMapPath: p.join( + soundAmdJsMapPath: p.join( sdkLayout.sdkDirectory, 'lib', 'dev_compiler', @@ -48,6 +50,22 @@ class TestSdkLayout { 'amd', 'dart_sdk.js.map', ), + soundDdcJsPath: p.join( + sdkLayout.sdkDirectory, + 'lib', + 'dev_compiler', + 'kernel', + 'ddc', + 'dart_sdk.js', + ), + soundDdcJsMapPath: p.join( + sdkLayout.sdkDirectory, + 'lib', + 'dev_compiler', + 'kernel', + 'ddc', + 'dart_sdk.js.map', + ), weakSummaryPath: sdkLayout.weakSummaryPath, weakFullDillPath: p.join( sdkLayout.sdkDirectory, @@ -55,7 +73,7 @@ class TestSdkLayout { '_internal', 'ddc_platform_unsound.dill', ), - weakJsPath: p.join( + weakAmdJsPath: p.join( sdkLayout.sdkDirectory, 'lib', 'dev_compiler', @@ -63,7 +81,7 @@ class TestSdkLayout { 'amd', 'dart_sdk_unsound.js', ), - weakJsMapPath: p.join( + weakAmdJsMapPath: p.join( sdkLayout.sdkDirectory, 'lib', 'dev_compiler', @@ -71,6 +89,29 @@ class TestSdkLayout { 'amd', 'dart_sdk_unsound.js.map', ), + weakDdcJsPath: p.join( + sdkLayout.sdkDirectory, + 'lib', + 'dev_compiler', + 'kernel', + 'ddc', + 'dart_sdk_unsound.js', + ), + weakDdcJsMapPath: p.join( + sdkLayout.sdkDirectory, + 'lib', + 'dev_compiler', + 'kernel', + 'ddc', + 'dart_sdk_unsound.js.map', + ), + ddcModuleLoaderJsPath: p.join( + sdkLayout.sdkDirectory, + 'lib', + 'dev_compiler', + 'ddc', + 'ddc_module_loader.js', + ), requireJsPath: p.join( sdkLayout.sdkDirectory, 'lib', @@ -118,26 +159,35 @@ class TestSdkLayout { final String sdkDirectory; - String get soundJsFileName => p.basename(soundJsPath); - String get soundJsMapFileName => p.basename(soundJsMapPath); + String get soundAmdJsFileName => p.basename(soundAmdJsPath); + String get soundAmdJsMapFileName => p.basename(soundAmdJsMapPath); + String get soundDdcJsFileName => p.basename(soundDdcJsPath); + String get soundDdcJsMapFileName => p.basename(soundDdcJsMapPath); String get soundSummaryFileName => p.basename(soundSummaryPath); String get soundFullDillFileName => p.basename(soundFullDillPath); - final String soundJsPath; - final String soundJsMapPath; + final String soundAmdJsPath; + final String soundAmdJsMapPath; + final String soundDdcJsPath; + final String soundDdcJsMapPath; final String soundSummaryPath; final String soundFullDillPath; - String get weakJsFileName => p.basename(weakJsPath); - String get weakJsMapFileName => p.basename(weakJsMapPath); + String get weakAmdJsFileName => p.basename(weakAmdJsPath); + String get weakAmdJsMapFileName => p.basename(weakAmdJsMapPath); + String get weakDdcJsFileName => p.basename(weakDdcJsPath); + String get weakDdcJsMapFileName => p.basename(weakDdcJsMapPath); String get weakSummaryFileName => p.basename(weakSummaryPath); String get weakFullDillFileName => p.basename(weakFullDillPath); - final String weakJsPath; - final String weakJsMapPath; + final String weakAmdJsPath; + final String weakAmdJsMapPath; + final String weakDdcJsPath; + final String weakDdcJsMapPath; final String weakSummaryPath; final String weakFullDillPath; + final String ddcModuleLoaderJsPath; final String requireJsPath; final String stackTraceMapperPath; @@ -150,14 +200,19 @@ class TestSdkLayout { const TestSdkLayout({ required this.sdkDirectory, - required this.soundJsPath, - required this.soundJsMapPath, + required this.soundAmdJsPath, + required this.soundAmdJsMapPath, + required this.soundDdcJsPath, + required this.soundDdcJsMapPath, required this.soundSummaryPath, required this.soundFullDillPath, - required this.weakJsPath, - required this.weakJsMapPath, + required this.weakAmdJsPath, + required this.weakAmdJsMapPath, + required this.weakDdcJsPath, + required this.weakDdcJsMapPath, required this.weakSummaryPath, required this.weakFullDillPath, + required this.ddcModuleLoaderJsPath, required this.requireJsPath, required this.stackTraceMapperPath, required this.dartPath, diff --git a/test_common/test/sdk_asset_generator_test.dart b/test_common/test/sdk_asset_generator_test.dart index fcbcfa3b5..2d45cd0ea 100644 --- a/test_common/test/sdk_asset_generator_test.dart +++ b/test_common/test/sdk_asset_generator_test.dart @@ -7,6 +7,7 @@ import 'dart:io'; +import 'package:dwds/expression_compiler.dart'; import 'package:test/test.dart'; import 'package:test_common/logging.dart'; import 'package:test_common/sdk_asset_generator.dart'; @@ -23,14 +24,18 @@ void main() { // Missing sound assets late String soundSdkFullDillPath; - late String soundSdkJsPath; - late String soundSdkJsMapPath; + late String soundAmdSdkJsPath; + late String soundAmdSdkJsMapPath; + late String soundDdcSdkJsPath; + late String soundDdcSdkJsMapPath; // Missing weak assets late String weakSdkSummaryPath; late String weakSdkFullDillPath; - late String weakSdkJsPath; - late String weakSdkJsMapPath; + late String weakAmdSdkJsPath; + late String weakAmdSdkJsMapPath; + late String weakDdcSdkJsPath; + late String weakDdcSdkJsMapPath; setUp(() async { setCurrentLogWriter(debug: debug); @@ -47,30 +52,39 @@ void main() { // Simulate missing sound assets. soundSdkFullDillPath = copySdkLayout.soundFullDillPath; - soundSdkJsPath = copySdkLayout.soundJsPath; - soundSdkJsMapPath = copySdkLayout.soundJsMapPath; + soundAmdSdkJsPath = copySdkLayout.soundAmdJsPath; + soundAmdSdkJsMapPath = copySdkLayout.soundAmdJsMapPath; + soundDdcSdkJsPath = copySdkLayout.soundDdcJsPath; + soundDdcSdkJsMapPath = copySdkLayout.soundDdcJsMapPath; _deleteIfExists(soundSdkFullDillPath); - _deleteIfExists(soundSdkJsPath); - _deleteIfExists(soundSdkJsMapPath); + _deleteIfExists(soundAmdSdkJsPath); + _deleteIfExists(soundAmdSdkJsMapPath); + _deleteIfExists(soundDdcSdkJsPath); + _deleteIfExists(soundDdcSdkJsMapPath); // Simulate missing weak assets. weakSdkSummaryPath = copySdkLayout.weakSummaryPath; weakSdkFullDillPath = copySdkLayout.weakFullDillPath; - weakSdkJsPath = copySdkLayout.weakJsPath; - weakSdkJsMapPath = copySdkLayout.weakJsMapPath; + weakAmdSdkJsPath = copySdkLayout.weakAmdJsPath; + weakAmdSdkJsMapPath = copySdkLayout.weakAmdJsMapPath; + weakDdcSdkJsPath = copySdkLayout.weakDdcJsPath; + weakDdcSdkJsMapPath = copySdkLayout.weakDdcJsMapPath; _deleteIfExists(weakSdkSummaryPath); _deleteIfExists(weakSdkFullDillPath); - _deleteIfExists(weakSdkJsPath); - _deleteIfExists(weakSdkJsMapPath); + _deleteIfExists(weakAmdSdkJsPath); + _deleteIfExists(weakAmdSdkJsMapPath); + _deleteIfExists(weakDdcSdkJsPath); + _deleteIfExists(weakDdcSdkJsMapPath); }); tearDown(() { tempDir.deleteSync(recursive: true); }); - test('Can generate missing SDK assets and validate SDK configuration', + test( + 'Can generate missing SDK assets and validate SDK configuration for the AMD module system', () async { final sdkLayout = TestSdkLayout.createDefault(sdkDirectory); final configuration = TestSdkLayout.createConfiguration(sdkLayout); @@ -79,6 +93,7 @@ void main() { sdkLayout: sdkLayout, verbose: true, canaryFeatures: false, + ddcModuleFormat: ModuleFormat.amd, ); await assetGenerator.generateSdkAssets(); @@ -88,13 +103,13 @@ void main() { expect(sdkLayout.soundSummaryPath, equals(soundSdkSummaryPath)); expect(sdkLayout.soundFullDillPath, equals(soundSdkFullDillPath)); - expect(sdkLayout.soundJsPath, equals(soundSdkJsPath)); - expect(sdkLayout.soundJsMapPath, equals(soundSdkJsMapPath)); + expect(sdkLayout.soundAmdJsPath, equals(soundAmdSdkJsPath)); + expect(sdkLayout.soundAmdJsMapPath, equals(soundAmdSdkJsMapPath)); expect(sdkLayout.weakSummaryPath, equals(weakSdkSummaryPath)); expect(sdkLayout.weakFullDillPath, equals(weakSdkFullDillPath)); - expect(sdkLayout.weakJsPath, equals(weakSdkJsPath)); - expect(sdkLayout.weakJsMapPath, equals(weakSdkJsMapPath)); + expect(sdkLayout.weakAmdJsPath, equals(weakAmdSdkJsPath)); + expect(sdkLayout.weakAmdJsMapPath, equals(weakAmdSdkJsMapPath)); // Validate that configuration files exist. configuration.validateSdkDir(); @@ -103,13 +118,57 @@ void main() { // Validate all assets exist. expect(sdkLayout.soundSummaryPath, _exists); expect(sdkLayout.soundFullDillPath, _exists); - expect(sdkLayout.soundJsPath, _exists); - expect(sdkLayout.soundJsMapPath, _exists); + expect(sdkLayout.soundAmdJsPath, _exists); + expect(sdkLayout.soundAmdJsMapPath, _exists); expect(sdkLayout.weakSummaryPath, _exists); expect(sdkLayout.weakFullDillPath, _exists); - expect(sdkLayout.weakJsPath, _exists); - expect(sdkLayout.weakJsMapPath, _exists); + expect(sdkLayout.weakAmdJsPath, _exists); + expect(sdkLayout.weakAmdJsMapPath, _exists); + }); + + test( + 'Can generate missing SDK assets and validate SDK configuration for the DDC module system', + () async { + final sdkLayout = TestSdkLayout.createDefault(sdkDirectory); + final configuration = TestSdkLayout.createConfiguration(sdkLayout); + + final assetGenerator = SdkAssetGenerator( + sdkLayout: sdkLayout, + verbose: true, + canaryFeatures: false, + ddcModuleFormat: ModuleFormat.ddc, + ); + await assetGenerator.generateSdkAssets(); + + // Make sure SDK configuration and asset generator agree on the file paths. + expect(configuration.sdkDirectory, equals(sdkDirectory)); + expect(configuration.compilerWorkerPath, equals(compilerWorkerPath)); + + expect(sdkLayout.soundSummaryPath, equals(soundSdkSummaryPath)); + expect(sdkLayout.soundFullDillPath, equals(soundSdkFullDillPath)); + expect(sdkLayout.soundDdcJsPath, equals(soundDdcSdkJsPath)); + expect(sdkLayout.soundDdcJsMapPath, equals(soundDdcSdkJsMapPath)); + + expect(sdkLayout.weakSummaryPath, equals(weakSdkSummaryPath)); + expect(sdkLayout.weakFullDillPath, equals(weakSdkFullDillPath)); + expect(sdkLayout.weakDdcJsPath, equals(weakDdcSdkJsPath)); + expect(sdkLayout.weakDdcJsMapPath, equals(weakDdcSdkJsMapPath)); + + // Validate that configuration files exist. + configuration.validateSdkDir(); + configuration.validate(); + + // Validate all assets exist. + expect(sdkLayout.soundSummaryPath, _exists); + expect(sdkLayout.soundFullDillPath, _exists); + expect(sdkLayout.soundDdcJsPath, _exists); + expect(sdkLayout.soundDdcJsMapPath, _exists); + + expect(sdkLayout.weakSummaryPath, _exists); + expect(sdkLayout.weakFullDillPath, _exists); + expect(sdkLayout.weakDdcJsPath, _exists); + expect(sdkLayout.weakDdcJsMapPath, _exists); }); test('Can generate missing SDK assets with canary features enabled', @@ -120,13 +179,34 @@ void main() { sdkLayout: sdkLayout, verbose: true, canaryFeatures: true, + ddcModuleFormat: ModuleFormat.amd, + ); + await assetGenerator.generateSdkAssets(); + + final soundSdk = File(soundAmdSdkJsPath).readAsStringSync(); + expect(soundSdk, contains('canary')); + + final weakSdk = File(weakAmdSdkJsPath).readAsStringSync(); + expect(weakSdk, contains('canary')); + }); + + test( + 'Can generate missing SDK assets with canary features enabled for the DDC module system', + () async { + final sdkLayout = TestSdkLayout.createDefault(sdkDirectory); + + final assetGenerator = SdkAssetGenerator( + sdkLayout: sdkLayout, + verbose: true, + canaryFeatures: true, + ddcModuleFormat: ModuleFormat.ddc, ); await assetGenerator.generateSdkAssets(); - final soundSdk = File(soundSdkJsPath).readAsStringSync(); + final soundSdk = File(soundDdcSdkJsPath).readAsStringSync(); expect(soundSdk, contains('canary')); - final weakSdk = File(weakSdkJsPath).readAsStringSync(); + final weakSdk = File(weakDdcSdkJsPath).readAsStringSync(); expect(weakSdk, contains('canary')); }); }); diff --git a/test_common/test/test_sdk_configuration_test.dart b/test_common/test/test_sdk_configuration_test.dart index 1ff60ddee..3e95ac645 100644 --- a/test_common/test/test_sdk_configuration_test.dart +++ b/test_common/test/test_sdk_configuration_test.dart @@ -7,6 +7,7 @@ import 'dart:io'; +import 'package:dwds/expression_compiler.dart'; import 'package:test/test.dart'; import 'package:test_common/logging.dart'; import 'package:test_common/test_sdk_configuration.dart'; @@ -40,7 +41,45 @@ void main() { }); }); - group('Test SDK configuration |', () { + group('Test SDK configuration | DDC with DDC modules |', () { + setCurrentLogWriter(debug: debug); + final provider = TestSdkConfigurationProvider( + verbose: debug, ddcModuleFormat: ModuleFormat.ddc); + tearDownAll(provider.dispose); + + test('Can validate configuration with generated assets', () async { + final sdkConfiguration = await provider.configuration; + sdkConfiguration.validateSdkDir(); + sdkConfiguration.validate(); + }); + + test('SDK layout exists', () async { + await provider.configuration; + final sdkLayout = provider.sdkLayout; + + expect(sdkLayout.sdkDirectory, _directoryExists); + expect(sdkLayout.soundDdcJsPath, _fileExists); + expect(sdkLayout.soundDdcJsMapPath, _fileExists); + expect(sdkLayout.soundSummaryPath, _fileExists); + expect(sdkLayout.soundFullDillPath, _fileExists); + + expect(sdkLayout.weakDdcJsPath, _fileExists); + expect(sdkLayout.weakDdcJsMapPath, _fileExists); + expect(sdkLayout.weakSummaryPath, _fileExists); + expect(sdkLayout.weakFullDillPath, _fileExists); + + expect(sdkLayout.ddcModuleLoaderJsPath, _fileExists); + expect(sdkLayout.stackTraceMapperPath, _fileExists); + + expect(sdkLayout.dartPath, _fileExists); + expect(sdkLayout.frontendServerSnapshotPath, _fileExists); + expect(sdkLayout.dartdevcSnapshotPath, _fileExists); + expect(sdkLayout.kernelWorkerSnapshotPath, _fileExists); + expect(sdkLayout.devToolsDirectory, _directoryExists); + }); + }); + + group('Test SDK configuration | DDC with AMD modules |', () { setCurrentLogWriter(debug: debug); final provider = TestSdkConfigurationProvider(verbose: debug); tearDownAll(provider.dispose); @@ -56,13 +95,13 @@ void main() { final sdkLayout = provider.sdkLayout; expect(sdkLayout.sdkDirectory, _directoryExists); - expect(sdkLayout.soundJsPath, _fileExists); - expect(sdkLayout.soundJsMapPath, _fileExists); + expect(sdkLayout.soundAmdJsPath, _fileExists); + expect(sdkLayout.soundAmdJsMapPath, _fileExists); expect(sdkLayout.soundSummaryPath, _fileExists); expect(sdkLayout.soundFullDillPath, _fileExists); - expect(sdkLayout.weakJsPath, _fileExists); - expect(sdkLayout.weakJsMapPath, _fileExists); + expect(sdkLayout.weakAmdJsPath, _fileExists); + expect(sdkLayout.weakAmdJsMapPath, _fileExists); expect(sdkLayout.weakSummaryPath, _fileExists); expect(sdkLayout.weakFullDillPath, _fileExists);