Skip to content

Commit 032ef1d

Browse files
authored
Support dart2wasm in node.js tests (#2259)
Support `dart2wasm` as a compiler for tests running in Node.js. `dart2wasm` emits a `.mjs` file exporting definitions to load generated wasm modules, the only additional thing we have to do is wrap that in a simple entrypoint file compiling the wasm module and invoking the startup wrapper. There's no support for stack trace maps yet. We also don't support precompiled node wasm tests yet (`dart2wasm` is also not currently supported by `build_web_compilers`, so we're blocked on that either way). In the test runtime, I had to migrate off `require` as that function is not in the global context for `.mjs` files. It looks like we can use `await import` instead though. If we need to support ancient Node versions that lack `import` support, I can adapt that to still use `require` when compiled with `dart2js` for compatibility.
1 parent 90481cf commit 032ef1d

File tree

20 files changed

+356
-151
lines changed

20 files changed

+356
-151
lines changed

.github/workflows/dart.yml

Lines changed: 81 additions & 81 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: regression_tests
22
publish_to: none
33
environment:
4-
sdk: ^3.5.0-259.0.dev
4+
sdk: ^3.5.0-311.0.dev
55
resolution: workspace
66
dependencies:
77
test: any

integration_tests/spawn_hybrid/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: spawn_hybrid
22
publish_to: none
33
environment:
4-
sdk: ^3.5.0-259.0.dev
4+
sdk: ^3.5.0-311.0.dev
55
resolution: workspace
66
dependencies:
77
async: ^2.9.0

integration_tests/wasm/dart_test.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
platforms: [chrome, firefox]
2+
# Node doesn't work because the version available in the current Ubuntu GitHub runners is too
3+
# old to support WASM+GC, which would be required to run Dart tests.
4+
#platforms: [chrome, firefox, node]
25
compilers: [dart2wasm]

integration_tests/wasm/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: wasm_tests
22
publish_to: none
33
environment:
4-
sdk: ^3.5.0-259.0.dev
4+
sdk: ^3.5.0-311.0.dev
55
resolution: workspace
66
dev_dependencies:
77
test: any

pkgs/checks/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ repository: https://github.com/dart-lang/test/tree/master/pkgs/checks
77
resolution: workspace
88

99
environment:
10-
sdk: ^3.5.0-259.0.dev
10+
sdk: ^3.5.0-311.0.dev
1111

1212
dependencies:
1313
async: ^2.8.0

pkgs/test/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
## 1.25.9-wip
22

33
* Fix dart2wasm tests on windows.
4-
* Increase SDK constraint to ^3.5.0-259.0.dev.
4+
* Increase SDK constraint to ^3.5.0-311.0.dev.
5+
* Support running Node.js tests compiled with dart2wasm.
56

67
## 1.25.8
78

pkgs/test/lib/src/bootstrap/node.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@ void internalBootstrapNodeTest(Function Function() getMain) {
1414
if (serialized is! Map) return;
1515
setStackTraceMapper(JSStackTraceMapper.deserialize(serialized)!);
1616
});
17-
socketChannel().pipe(channel);
17+
socketChannel().then((socket) => socket.pipe(channel));
1818
}

pkgs/test/lib/src/runner/node/platform.dart

Lines changed: 115 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,18 @@ import 'package:test_api/backend.dart'
1616
import 'package:test_core/src/runner/application_exception.dart'; // ignore: implementation_imports
1717
import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports
1818
import 'package:test_core/src/runner/dart2js_compiler_pool.dart'; // ignore: implementation_imports
19+
import 'package:test_core/src/runner/load_exception.dart'; // ignore: implementation_imports
1920
import 'package:test_core/src/runner/package_version.dart'; // ignore: implementation_imports
2021
import 'package:test_core/src/runner/platform.dart'; // ignore: implementation_imports
2122
import 'package:test_core/src/runner/plugin/customizable_platform.dart'; // ignore: implementation_imports
2223
import 'package:test_core/src/runner/plugin/environment.dart'; // ignore: implementation_imports
2324
import 'package:test_core/src/runner/plugin/platform_helpers.dart'; // ignore: implementation_imports
2425
import 'package:test_core/src/runner/runner_suite.dart'; // ignore: implementation_imports
2526
import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
27+
import 'package:test_core/src/runner/wasm_compiler_pool.dart'; // ignore: implementation_imports
2628
import 'package:test_core/src/util/errors.dart'; // ignore: implementation_imports
2729
import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports
2830
import 'package:test_core/src/util/package_config.dart'; // ignore: implementation_imports
29-
import 'package:test_core/src/util/pair.dart'; // ignore: implementation_imports
3031
import 'package:test_core/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports
3132
import 'package:yaml/yaml.dart';
3233

@@ -40,7 +41,8 @@ class NodePlatform extends PlatformPlugin
4041
final Configuration _config;
4142

4243
/// The [Dart2JsCompilerPool] managing active instances of `dart2js`.
43-
final _compilers = Dart2JsCompilerPool(['-Dnode=true', '--server-mode']);
44+
final _jsCompilers = Dart2JsCompilerPool(['-Dnode=true', '--server-mode']);
45+
final _wasmCompilers = WasmCompilerPool(['-Dnode=true']);
4446

4547
/// The temporary directory in which compiled JS is emitted.
4648
final _compiledDir = createTempDir();
@@ -75,15 +77,17 @@ class NodePlatform extends PlatformPlugin
7577
@override
7678
Future<RunnerSuite> load(String path, SuitePlatform platform,
7779
SuiteConfiguration suiteConfig, Map<String, Object?> message) async {
78-
if (platform.compiler != Compiler.dart2js) {
80+
if (platform.compiler != Compiler.dart2js &&
81+
platform.compiler != Compiler.dart2wasm) {
7982
throw StateError(
8083
'Unsupported compiler for the Node platform ${platform.compiler}.');
8184
}
82-
var pair = await _loadChannel(path, platform, suiteConfig);
85+
var (channel, stackMapper) =
86+
await _loadChannel(path, platform, suiteConfig);
8387
var controller = deserializeSuite(path, platform, suiteConfig,
84-
const PluginEnvironment(), pair.first, message);
88+
const PluginEnvironment(), channel, message);
8589

86-
controller.channel('test.node.mapper').sink.add(pair.last?.serialize());
90+
controller.channel('test.node.mapper').sink.add(stackMapper?.serialize());
8791

8892
return await controller.suite;
8993
}
@@ -92,16 +96,13 @@ class NodePlatform extends PlatformPlugin
9296
///
9397
/// Returns that channel along with a [StackTraceMapper] representing the
9498
/// source map for the compiled suite.
95-
Future<Pair<StreamChannel<Object?>, StackTraceMapper?>> _loadChannel(
96-
String path,
97-
SuitePlatform platform,
98-
SuiteConfiguration suiteConfig) async {
99+
Future<(StreamChannel<Object?>, StackTraceMapper?)> _loadChannel(String path,
100+
SuitePlatform platform, SuiteConfiguration suiteConfig) async {
99101
final servers = await _loopback();
100102

101103
try {
102-
var pair = await _spawnProcess(
103-
path, platform.runtime, suiteConfig, servers.first.port);
104-
var process = pair.first;
104+
var (process, stackMapper) =
105+
await _spawnProcess(path, platform, suiteConfig, servers.first.port);
105106

106107
// Forward Node's standard IO to the print handler so it's associated with
107108
// the load test.
@@ -110,7 +111,19 @@ class NodePlatform extends PlatformPlugin
110111
process.stdout.transform(lineSplitter).listen(print);
111112
process.stderr.transform(lineSplitter).listen(print);
112113

113-
var socket = await StreamGroup.merge(servers).first;
114+
// Wait for the first connection (either over ipv4 or v6). If the proccess
115+
// exits before it connects, throw instead of waiting for a connection
116+
// indefinitely.
117+
var socket = await Future.any([
118+
StreamGroup.merge(servers).first,
119+
process.exitCode.then((_) => null),
120+
]);
121+
122+
if (socket == null) {
123+
throw LoadException(
124+
path, 'Node exited before connecting to the test channel.');
125+
}
126+
114127
var channel = StreamChannel(socket.cast<List<int>>(), socket)
115128
.transform(StreamChannelTransformer.fromCodec(utf8))
116129
.transform(_chunksToLines)
@@ -120,7 +133,7 @@ class NodePlatform extends PlatformPlugin
120133
sink.close();
121134
}));
122135

123-
return Pair(channel, pair.last);
136+
return (channel, stackMapper);
124137
} finally {
125138
unawaited(Future.wait<void>(servers.map((s) =>
126139
s.close().then<ServerSocket?>((v) => v).onError((_, __) => null))));
@@ -131,23 +144,28 @@ class NodePlatform extends PlatformPlugin
131144
///
132145
/// Returns that channel along with a [StackTraceMapper] representing the
133146
/// source map for the compiled suite.
134-
Future<Pair<Process, StackTraceMapper?>> _spawnProcess(String path,
135-
Runtime runtime, SuiteConfiguration suiteConfig, int socketPort) async {
147+
Future<(Process, StackTraceMapper?)> _spawnProcess(
148+
String path,
149+
SuitePlatform platform,
150+
SuiteConfiguration suiteConfig,
151+
int socketPort) async {
136152
if (_config.suiteDefaults.precompiledPath != null) {
137-
return _spawnPrecompiledProcess(path, runtime, suiteConfig, socketPort,
138-
_config.suiteDefaults.precompiledPath!);
153+
return _spawnPrecompiledProcess(path, platform.runtime, suiteConfig,
154+
socketPort, _config.suiteDefaults.precompiledPath!);
139155
} else {
140-
return _spawnNormalProcess(path, runtime, suiteConfig, socketPort);
156+
return switch (platform.compiler) {
157+
Compiler.dart2js => _spawnNormalJsProcess(
158+
path, platform.runtime, suiteConfig, socketPort),
159+
Compiler.dart2wasm => _spawnNormalWasmProcess(
160+
path, platform.runtime, suiteConfig, socketPort),
161+
_ => throw StateError('Unsupported compiler ${platform.compiler}'),
162+
};
141163
}
142164
}
143165

144-
/// Compiles [testPath] with dart2js, adds the node preamble, and then spawns
145-
/// a Node.js process that loads that Dart test suite.
146-
Future<Pair<Process, StackTraceMapper?>> _spawnNormalProcess(String testPath,
147-
Runtime runtime, SuiteConfiguration suiteConfig, int socketPort) async {
148-
var dir = Directory(_compiledDir).createTempSync('test_').path;
149-
var jsPath = p.join(dir, '${p.basename(testPath)}.node_test.dart.js');
150-
await _compilers.compile('''
166+
Future<String> _entrypointScriptForTest(
167+
String testPath, SuiteConfiguration suiteConfig) async {
168+
return '''
151169
${suiteConfig.metadata.languageVersionComment ?? await rootPackageLanguageVersionComment}
152170
import "package:test/src/bootstrap/node.dart";
153171
@@ -156,7 +174,20 @@ class NodePlatform extends PlatformPlugin
156174
void main() {
157175
internalBootstrapNodeTest(() => test.main);
158176
}
159-
''', jsPath, suiteConfig);
177+
''';
178+
}
179+
180+
/// Compiles [testPath] with dart2js, adds the node preamble, and then spawns
181+
/// a Node.js process that loads that Dart test suite.
182+
Future<(Process, StackTraceMapper?)> _spawnNormalJsProcess(String testPath,
183+
Runtime runtime, SuiteConfiguration suiteConfig, int socketPort) async {
184+
var dir = Directory(_compiledDir).createTempSync('test_').path;
185+
var jsPath = p.join(dir, '${p.basename(testPath)}.node_test.dart.js');
186+
await _jsCompilers.compile(
187+
await _entrypointScriptForTest(testPath, suiteConfig),
188+
jsPath,
189+
suiteConfig,
190+
);
160191

161192
// Add the Node.js preamble to ensure that the dart2js output is
162193
// compatible. Use the minified version so the source map remains valid.
@@ -173,12 +204,63 @@ class NodePlatform extends PlatformPlugin
173204
packageMap: (await currentPackageConfig).toPackageMap());
174205
}
175206

176-
return Pair(await _startProcess(runtime, jsPath, socketPort), mapper);
207+
return (await _startProcess(runtime, jsPath, socketPort), mapper);
208+
}
209+
210+
/// Compiles [testPath] with dart2wasm, adds a JS entrypoint and then spawns
211+
/// a Node.js process loading the compiled test suite.
212+
Future<(Process, StackTraceMapper?)> _spawnNormalWasmProcess(String testPath,
213+
Runtime runtime, SuiteConfiguration suiteConfig, int socketPort) async {
214+
var dir = Directory(_compiledDir).createTempSync('test_').path;
215+
// dart2wasm will emit a .wasm file and a .mjs file responsible for loading
216+
// that file.
217+
var wasmPath = p.join(dir, '${p.basename(testPath)}.node_test.dart.wasm');
218+
var loader = '${p.basename(testPath)}.node_test.dart.wasm.mjs';
219+
220+
// We need to create an additional entrypoint file loading the wasm module.
221+
var jsPath = p.join(dir, '${p.basename(testPath)}.node_test.dart.js');
222+
223+
await _wasmCompilers.compile(
224+
await _entrypointScriptForTest(testPath, suiteConfig),
225+
wasmPath,
226+
suiteConfig,
227+
);
228+
229+
await File(jsPath).writeAsString('''
230+
const { createReadStream } = require('fs');
231+
const { once } = require('events');
232+
const { PassThrough } = require('stream');
233+
234+
const main = async () => {
235+
const { instantiate, invoke } = await import("./$loader");
236+
237+
const wasmContents = createReadStream("$wasmPath.wasm");
238+
const stream = new PassThrough();
239+
wasmContents.pipe(stream);
240+
241+
await once(wasmContents, 'open');
242+
const response = new Response(
243+
stream,
244+
{
245+
headers: {
246+
"Content-Type": "application/wasm"
247+
}
248+
}
249+
);
250+
const instancePromise = WebAssembly.compileStreaming(response);
251+
const module = await instantiate(instancePromise, {});
252+
invoke(module);
253+
};
254+
255+
main();
256+
''');
257+
258+
return (await _startProcess(runtime, jsPath, socketPort), null);
177259
}
178260

179261
/// Spawns a Node.js process that loads the Dart test suite at [testPath]
180262
/// under [precompiledPath].
181-
Future<Pair<Process, StackTraceMapper?>> _spawnPrecompiledProcess(
263+
Future<(Process, StackTraceMapper?)> _spawnPrecompiledProcess(
182264
String testPath,
183265
Runtime runtime,
184266
SuiteConfiguration suiteConfig,
@@ -195,7 +277,7 @@ class NodePlatform extends PlatformPlugin
195277
.toPackageMap());
196278
}
197279

198-
return Pair(await _startProcess(runtime, jsPath, socketPort), mapper);
280+
return (await _startProcess(runtime, jsPath, socketPort), mapper);
199281
}
200282

201283
/// Starts the Node.js process for [runtime] with [jsPath].
@@ -224,7 +306,8 @@ class NodePlatform extends PlatformPlugin
224306

225307
@override
226308
Future<void> close() => _closeMemo.runOnce(() async {
227-
await _compilers.close();
309+
await _jsCompilers.close();
310+
await _wasmCompilers.close();
228311
await Directory(_compiledDir).deleteWithRetry();
229312
});
230313
final _closeMemo = AsyncMemoizer<void>();

pkgs/test/lib/src/runner/node/socket_channel.dart

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,41 @@
11
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
22
// for details. All rights reserved. Use of this source code is governed by a
33
// BSD-style license that can be found in the LICENSE file.
4-
5-
@JS()
6-
library;
7-
84
import 'dart:async';
95
import 'dart:convert';
6+
import 'dart:js_interop';
107

11-
import 'package:js/js.dart';
128
import 'package:stream_channel/stream_channel.dart';
139

14-
@JS('require')
15-
external _Net _require(String module);
16-
1710
@JS('process.argv')
18-
external List<String> get _args;
11+
external JSArray<JSString> get _args;
1912

20-
@JS()
21-
class _Net {
13+
extension type _Net._(JSObject _) {
2214
external _Socket connect(int port);
2315
}
2416

25-
@JS()
26-
class _Socket {
27-
external void setEncoding(String encoding);
28-
external void on(String event, void Function(String chunk) callback);
29-
external void write(String data);
17+
extension type _Socket._(JSObject _) {
18+
external void setEncoding(JSString encoding);
19+
external void on(JSString event, JSFunction callback);
20+
external void write(JSString data);
3021
}
3122

3223
/// Returns a [StreamChannel] of JSON-encodable objects that communicates over a
3324
/// socket whose port is given by `process.argv[2]`.
34-
StreamChannel<Object?> socketChannel() {
35-
var net = _require('net');
36-
var socket = net.connect(int.parse(_args[2]));
37-
socket.setEncoding('utf8');
25+
Future<StreamChannel<Object?>> socketChannel() async {
26+
final net = (await importModule('node:net'.toJS).toDart) as _Net;
27+
28+
var socket = net.connect(int.parse(_args.toDart[2].toDart));
29+
socket.setEncoding('utf8'.toJS);
3830

3931
var socketSink = StreamController<Object?>(sync: true)
40-
..stream.listen((event) => socket.write('${jsonEncode(event)}\n'));
32+
..stream.listen((event) => socket.write('${jsonEncode(event)}\n'.toJS));
4133

4234
var socketStream = StreamController<String>(sync: true);
43-
socket.on('data', allowInterop(socketStream.add));
35+
socket.on(
36+
'data'.toJS,
37+
((JSString chunk) => socketStream.add(chunk.toDart)).toJS,
38+
);
4439

4540
return StreamChannel.withCloseGuarantee(
4641
socketStream.stream.transform(const LineSplitter()).map(jsonDecode),

0 commit comments

Comments
 (0)