diff --git a/integration_tests/wasm/dart_test.yaml b/integration_tests/wasm/dart_test.yaml index c53292fc7..2c8098ae5 100644 --- a/integration_tests/wasm/dart_test.yaml +++ b/integration_tests/wasm/dart_test.yaml @@ -1,2 +1,2 @@ -platforms: [chrome, firefox] +platforms: [chrome, firefox, node] compilers: [dart2wasm] diff --git a/pkgs/test/CHANGELOG.md b/pkgs/test/CHANGELOG.md index 60100aa8b..3c20665f9 100644 --- a/pkgs/test/CHANGELOG.md +++ b/pkgs/test/CHANGELOG.md @@ -1,6 +1,7 @@ ## 1.25.9-wip * Increase SDK constraint to ^3.5.0-259.0.dev. +* Support running Node.js tests compiled with dart2wasm. ## 1.25.8 diff --git a/pkgs/test/lib/src/bootstrap/node.dart b/pkgs/test/lib/src/bootstrap/node.dart index 8612dbcc0..21089a47b 100644 --- a/pkgs/test/lib/src/bootstrap/node.dart +++ b/pkgs/test/lib/src/bootstrap/node.dart @@ -14,5 +14,5 @@ void internalBootstrapNodeTest(Function Function() getMain) { if (serialized is! Map) return; setStackTraceMapper(JSStackTraceMapper.deserialize(serialized)!); }); - socketChannel().pipe(channel); + socketChannel().then((socket) => socket.pipe(channel)); } diff --git a/pkgs/test/lib/src/runner/node/platform.dart b/pkgs/test/lib/src/runner/node/platform.dart index b27009e07..2bde2ee16 100644 --- a/pkgs/test/lib/src/runner/node/platform.dart +++ b/pkgs/test/lib/src/runner/node/platform.dart @@ -23,10 +23,10 @@ import 'package:test_core/src/runner/plugin/environment.dart'; // ignore: implem import 'package:test_core/src/runner/plugin/platform_helpers.dart'; // ignore: implementation_imports import 'package:test_core/src/runner/runner_suite.dart'; // ignore: implementation_imports import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/wasm_compiler_pool.dart'; // ignore: implementation_imports import 'package:test_core/src/util/errors.dart'; // ignore: implementation_imports import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports import 'package:test_core/src/util/package_config.dart'; // ignore: implementation_imports -import 'package:test_core/src/util/pair.dart'; // ignore: implementation_imports import 'package:test_core/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports import 'package:yaml/yaml.dart'; @@ -40,7 +40,8 @@ class NodePlatform extends PlatformPlugin final Configuration _config; /// The [Dart2JsCompilerPool] managing active instances of `dart2js`. - final _compilers = Dart2JsCompilerPool(['-Dnode=true', '--server-mode']); + final _jsCompilers = Dart2JsCompilerPool(['-Dnode=true', '--server-mode']); + final _wasmCompilers = WasmCompilerPool(['-Dnode=true']); /// The temporary directory in which compiled JS is emitted. final _compiledDir = createTempDir(); @@ -75,15 +76,17 @@ class NodePlatform extends PlatformPlugin @override Future load(String path, SuitePlatform platform, SuiteConfiguration suiteConfig, Map message) async { - if (platform.compiler != Compiler.dart2js) { + if (platform.compiler != Compiler.dart2js && + platform.compiler != Compiler.dart2wasm) { throw StateError( 'Unsupported compiler for the Node platform ${platform.compiler}.'); } - var pair = await _loadChannel(path, platform, suiteConfig); + var (channel, stackMapper) = + await _loadChannel(path, platform, suiteConfig); var controller = deserializeSuite(path, platform, suiteConfig, - const PluginEnvironment(), pair.first, message); + const PluginEnvironment(), channel, message); - controller.channel('test.node.mapper').sink.add(pair.last?.serialize()); + controller.channel('test.node.mapper').sink.add(stackMapper?.serialize()); return await controller.suite; } @@ -92,16 +95,13 @@ class NodePlatform extends PlatformPlugin /// /// Returns that channel along with a [StackTraceMapper] representing the /// source map for the compiled suite. - Future, StackTraceMapper?>> _loadChannel( - String path, - SuitePlatform platform, - SuiteConfiguration suiteConfig) async { + Future<(StreamChannel, StackTraceMapper?)> _loadChannel(String path, + SuitePlatform platform, SuiteConfiguration suiteConfig) async { final servers = await _loopback(); try { - var pair = await _spawnProcess( - path, platform.runtime, suiteConfig, servers.first.port); - var process = pair.first; + var (process, stackMapper) = + await _spawnProcess(path, platform, suiteConfig, servers.first.port); // Forward Node's standard IO to the print handler so it's associated with // the load test. @@ -120,7 +120,7 @@ class NodePlatform extends PlatformPlugin sink.close(); })); - return Pair(channel, pair.last); + return (channel, stackMapper); } finally { unawaited(Future.wait(servers.map((s) => s.close().then((v) => v).onError((_, __) => null)))); @@ -131,23 +131,28 @@ class NodePlatform extends PlatformPlugin /// /// Returns that channel along with a [StackTraceMapper] representing the /// source map for the compiled suite. - Future> _spawnProcess(String path, - Runtime runtime, SuiteConfiguration suiteConfig, int socketPort) async { + Future<(Process, StackTraceMapper?)> _spawnProcess( + String path, + SuitePlatform platform, + SuiteConfiguration suiteConfig, + int socketPort) async { if (_config.suiteDefaults.precompiledPath != null) { - return _spawnPrecompiledProcess(path, runtime, suiteConfig, socketPort, - _config.suiteDefaults.precompiledPath!); + return _spawnPrecompiledProcess(path, platform.runtime, suiteConfig, + socketPort, _config.suiteDefaults.precompiledPath!); } else { - return _spawnNormalProcess(path, runtime, suiteConfig, socketPort); + return switch (platform.compiler) { + Compiler.dart2js => _spawnNormalJsProcess( + path, platform.runtime, suiteConfig, socketPort), + Compiler.dart2wasm => _spawnNormalWasmProcess( + path, platform.runtime, suiteConfig, socketPort), + _ => throw StateError('Unsupported compiler ${platform.compiler}'), + }; } } - /// Compiles [testPath] with dart2js, adds the node preamble, and then spawns - /// a Node.js process that loads that Dart test suite. - Future> _spawnNormalProcess(String testPath, - Runtime runtime, SuiteConfiguration suiteConfig, int socketPort) async { - var dir = Directory(_compiledDir).createTempSync('test_').path; - var jsPath = p.join(dir, '${p.basename(testPath)}.node_test.dart.js'); - await _compilers.compile(''' + Future _entrypointScriptForTest( + String testPath, SuiteConfiguration suiteConfig) async { + return ''' ${suiteConfig.metadata.languageVersionComment ?? await rootPackageLanguageVersionComment} import "package:test/src/bootstrap/node.dart"; @@ -156,7 +161,20 @@ class NodePlatform extends PlatformPlugin void main() { internalBootstrapNodeTest(() => test.main); } - ''', jsPath, suiteConfig); + '''; + } + + /// Compiles [testPath] with dart2js, adds the node preamble, and then spawns + /// a Node.js process that loads that Dart test suite. + Future<(Process, StackTraceMapper?)> _spawnNormalJsProcess(String testPath, + Runtime runtime, SuiteConfiguration suiteConfig, int socketPort) async { + var dir = Directory(_compiledDir).createTempSync('test_').path; + var jsPath = p.join(dir, '${p.basename(testPath)}.node_test.dart.js'); + await _jsCompilers.compile( + await _entrypointScriptForTest(testPath, suiteConfig), + jsPath, + suiteConfig, + ); // Add the Node.js preamble to ensure that the dart2js output is // compatible. Use the minified version so the source map remains valid. @@ -173,12 +191,63 @@ class NodePlatform extends PlatformPlugin packageMap: (await currentPackageConfig).toPackageMap()); } - return Pair(await _startProcess(runtime, jsPath, socketPort), mapper); + return (await _startProcess(runtime, jsPath, socketPort), mapper); + } + + /// Compiles [testPath] with dart2wasm, adds a JS entrypoint and then spawns + /// a Node.js process loading the compiled test suite. + Future<(Process, StackTraceMapper?)> _spawnNormalWasmProcess(String testPath, + Runtime runtime, SuiteConfiguration suiteConfig, int socketPort) async { + var dir = Directory(_compiledDir).createTempSync('test_').path; + // dart2wasm will emit a .wasm file and a .mjs file responsible for loading + // that file. + var wasmPath = p.join(dir, '${p.basename(testPath)}.node_test.dart.wasm'); + var loader = '${p.basename(testPath)}.node_test.dart.wasm.mjs'; + + // We need to create an additional entrypoint file loading the wasm module. + var jsPath = p.join(dir, '${p.basename(testPath)}.node_test.dart.js'); + + await _wasmCompilers.compile( + await _entrypointScriptForTest(testPath, suiteConfig), + wasmPath, + suiteConfig, + ); + + await File(jsPath).writeAsString(''' +const { createReadStream } = require('fs'); +const { once } = require('events'); +const { PassThrough } = require('stream'); + +const main = async () => { + const { instantiate, invoke } = await import("./$loader"); + + const wasmContents = createReadStream("$wasmPath.wasm"); + const stream = new PassThrough(); + wasmContents.pipe(stream); + + await once(wasmContents, 'open'); + const response = new Response( + stream, + { + headers: { + "Content-Type": "application/wasm" + } + } + ); + const instancePromise = WebAssembly.compileStreaming(response); + const module = await instantiate(instancePromise, {}); + invoke(module); +}; + +main(); +'''); + + return (await _startProcess(runtime, jsPath, socketPort), null); } /// Spawns a Node.js process that loads the Dart test suite at [testPath] /// under [precompiledPath]. - Future> _spawnPrecompiledProcess( + Future<(Process, StackTraceMapper?)> _spawnPrecompiledProcess( String testPath, Runtime runtime, SuiteConfiguration suiteConfig, @@ -195,7 +264,7 @@ class NodePlatform extends PlatformPlugin .toPackageMap()); } - return Pair(await _startProcess(runtime, jsPath, socketPort), mapper); + return (await _startProcess(runtime, jsPath, socketPort), mapper); } /// Starts the Node.js process for [runtime] with [jsPath]. @@ -224,7 +293,8 @@ class NodePlatform extends PlatformPlugin @override Future close() => _closeMemo.runOnce(() async { - await _compilers.close(); + await _jsCompilers.close(); + await _wasmCompilers.close(); await Directory(_compiledDir).deleteWithRetry(); }); final _closeMemo = AsyncMemoizer(); diff --git a/pkgs/test/lib/src/runner/node/socket_channel.dart b/pkgs/test/lib/src/runner/node/socket_channel.dart index 4e8a35036..95e81de96 100644 --- a/pkgs/test/lib/src/runner/node/socket_channel.dart +++ b/pkgs/test/lib/src/runner/node/socket_channel.dart @@ -1,46 +1,41 @@ // Copyright (c) 2017, 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. - -@JS() -library; - import 'dart:async'; import 'dart:convert'; +import 'dart:js_interop'; -import 'package:js/js.dart'; import 'package:stream_channel/stream_channel.dart'; -@JS('require') -external _Net _require(String module); - @JS('process.argv') -external List get _args; +external JSArray get _args; -@JS() -class _Net { +extension type _Net._(JSObject _) { external _Socket connect(int port); } -@JS() -class _Socket { - external void setEncoding(String encoding); - external void on(String event, void Function(String chunk) callback); - external void write(String data); +extension type _Socket._(JSObject _) { + external void setEncoding(JSString encoding); + external void on(JSString event, JSFunction callback); + external void write(JSString data); } /// Returns a [StreamChannel] of JSON-encodable objects that communicates over a /// socket whose port is given by `process.argv[2]`. -StreamChannel socketChannel() { - var net = _require('net'); - var socket = net.connect(int.parse(_args[2])); - socket.setEncoding('utf8'); +Future> socketChannel() async { + final net = (await importModule('node:net'.toJS).toDart) as _Net; + + var socket = net.connect(int.parse(_args.toDart[2].toDart)); + socket.setEncoding('utf8'.toJS); var socketSink = StreamController(sync: true) - ..stream.listen((event) => socket.write('${jsonEncode(event)}\n')); + ..stream.listen((event) => socket.write('${jsonEncode(event)}\n'.toJS)); var socketStream = StreamController(sync: true); - socket.on('data', allowInterop(socketStream.add)); + socket.on( + 'data'.toJS, + ((JSString chunk) => socketStream.add(chunk.toDart)).toJS, + ); return StreamChannel.withCloseGuarantee( socketStream.stream.transform(const LineSplitter()).map(jsonDecode), diff --git a/pkgs/test/test/runner/node/runner_test.dart b/pkgs/test/test/runner/node/runner_test.dart index aba260440..22e253e64 100644 --- a/pkgs/test/test/runner/node/runner_test.dart +++ b/pkgs/test/test/runner/node/runner_test.dart @@ -116,6 +116,15 @@ void main() { expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); await test.shouldExit(0); }); + + test('compiled with dart2wasm', () async { + await d.file('test.dart', _success).create(); + var test = + await runTest(['-p', 'node', '--compiler', 'dart2wasm', 'test.dart']); + + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); }); test('defines a node environment constant', () async { @@ -148,8 +157,18 @@ void main() { } ''').create(); - var test = await runTest(['-p', 'node', '-p', 'vm', 'test.dart']); - expect(test.stdout, emitsThrough(contains('+1 -1: Some tests failed.'))); + var test = await runTest([ + '-p', + 'node', + '-p', + 'vm', + '-c', + 'dart2js', + '-c', + 'dart2wasm', + 'test.dart' + ]); + expect(test.stdout, emitsThrough(contains('+1 -2: Some tests failed.'))); await test.shouldExit(1); }); diff --git a/pkgs/test_api/CHANGELOG.md b/pkgs/test_api/CHANGELOG.md index cb9a01226..98ebc10fb 100644 --- a/pkgs/test_api/CHANGELOG.md +++ b/pkgs/test_api/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.7.4-wip * Increase SDK constraint to ^3.5.0-259.0.dev. +* Support running Node.js tests compiled with dart2wasm. ## 0.7.3 diff --git a/pkgs/test_api/lib/src/backend/runtime.dart b/pkgs/test_api/lib/src/backend/runtime.dart index d875632b2..ceef2277e 100644 --- a/pkgs/test_api/lib/src/backend/runtime.dart +++ b/pkgs/test_api/lib/src/backend/runtime.dart @@ -41,8 +41,8 @@ final class Runtime { isBrowser: true, isBlink: true); /// The command-line Node.js VM. - static const Runtime nodeJS = - Runtime('Node.js', 'node', Compiler.dart2js, [Compiler.dart2js]); + static const Runtime nodeJS = Runtime('Node.js', 'node', Compiler.dart2js, + [Compiler.dart2js, Compiler.dart2wasm]); /// The platforms that are supported by the test runner by default. static const List builtIn = [ diff --git a/pkgs/test_core/CHANGELOG.md b/pkgs/test_core/CHANGELOG.md index fd88f3329..97c458a74 100644 --- a/pkgs/test_core/CHANGELOG.md +++ b/pkgs/test_core/CHANGELOG.md @@ -1,6 +1,7 @@ ## 0.6.6-wip * Increase SDK constraint to ^3.5.0-259.0.dev. +* Allow passing additional arguments to `dart compile wasm`. ## 0.6.5 diff --git a/pkgs/test_core/lib/src/runner/wasm_compiler_pool.dart b/pkgs/test_core/lib/src/runner/wasm_compiler_pool.dart index 7290da49d..2cde8731c 100644 --- a/pkgs/test_core/lib/src/runner/wasm_compiler_pool.dart +++ b/pkgs/test_core/lib/src/runner/wasm_compiler_pool.dart @@ -17,9 +17,14 @@ import 'suite.dart'; /// /// This limits the number of compiler instances running concurrently. class WasmCompilerPool extends CompilerPool { + /// Extra arguments to pass to `dart compile js`. + final List _extraArgs; + /// The currently-active dart2wasm processes. final _processes = {}; + WasmCompilerPool([this._extraArgs = const []]); + /// Compiles [code] to [path]. /// /// This wraps the Dart code in the standard browser-testing wrapper. @@ -41,6 +46,7 @@ class WasmCompilerPool extends CompilerPool { for (var experiment in enabledExperiments) '--enable-experiment=$experiment', '-O0', + ..._extraArgs, '-o', outWasmPath, wrapperPath,