Skip to content

Commit a1ad8f3

Browse files
committed
Support dart2wasm in node.js
1 parent d0dc833 commit a1ad8f3

File tree

10 files changed

+152
-59
lines changed

10 files changed

+152
-59
lines changed

integration_tests/wasm/dart_test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
platforms: [chrome, firefox]
1+
platforms: [chrome, firefox, node]
22
compilers: [dart2wasm]

pkgs/test/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## 1.25.9-wip
22

33
* Increase SDK constraint to ^3.5.0-259.0.dev.
4+
* Support running Node.js tests compiled with dart2wasm.
45

56
## 1.25.8
67

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: 101 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@ import 'package:test_core/src/runner/plugin/environment.dart'; // ignore: implem
2323
import 'package:test_core/src/runner/plugin/platform_helpers.dart'; // ignore: implementation_imports
2424
import 'package:test_core/src/runner/runner_suite.dart'; // ignore: implementation_imports
2525
import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
26+
import 'package:test_core/src/runner/wasm_compiler_pool.dart'; // ignore: implementation_imports
2627
import 'package:test_core/src/util/errors.dart'; // ignore: implementation_imports
2728
import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports
2829
import 'package:test_core/src/util/package_config.dart'; // ignore: implementation_imports
29-
import 'package:test_core/src/util/pair.dart'; // ignore: implementation_imports
3030
import 'package:test_core/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports
3131
import 'package:yaml/yaml.dart';
3232

@@ -40,7 +40,8 @@ class NodePlatform extends PlatformPlugin
4040
final Configuration _config;
4141

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

4546
/// The temporary directory in which compiled JS is emitted.
4647
final _compiledDir = createTempDir();
@@ -75,15 +76,17 @@ class NodePlatform extends PlatformPlugin
7576
@override
7677
Future<RunnerSuite> load(String path, SuitePlatform platform,
7778
SuiteConfiguration suiteConfig, Map<String, Object?> message) async {
78-
if (platform.compiler != Compiler.dart2js) {
79+
if (platform.compiler != Compiler.dart2js &&
80+
platform.compiler != Compiler.dart2wasm) {
7981
throw StateError(
8082
'Unsupported compiler for the Node platform ${platform.compiler}.');
8183
}
82-
var pair = await _loadChannel(path, platform, suiteConfig);
84+
var (channel, stackMapper) =
85+
await _loadChannel(path, platform, suiteConfig);
8386
var controller = deserializeSuite(path, platform, suiteConfig,
84-
const PluginEnvironment(), pair.first, message);
87+
const PluginEnvironment(), channel, message);
8588

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

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

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

106106
// Forward Node's standard IO to the print handler so it's associated with
107107
// the load test.
@@ -120,7 +120,7 @@ class NodePlatform extends PlatformPlugin
120120
sink.close();
121121
}));
122122

123-
return Pair(channel, pair.last);
123+
return (channel, stackMapper);
124124
} finally {
125125
unawaited(Future.wait<void>(servers.map((s) =>
126126
s.close().then<ServerSocket?>((v) => v).onError((_, __) => null))));
@@ -131,23 +131,28 @@ class NodePlatform extends PlatformPlugin
131131
///
132132
/// Returns that channel along with a [StackTraceMapper] representing the
133133
/// source map for the compiled suite.
134-
Future<Pair<Process, StackTraceMapper?>> _spawnProcess(String path,
135-
Runtime runtime, SuiteConfiguration suiteConfig, int socketPort) async {
134+
Future<(Process, StackTraceMapper?)> _spawnProcess(
135+
String path,
136+
SuitePlatform platform,
137+
SuiteConfiguration suiteConfig,
138+
int socketPort) async {
136139
if (_config.suiteDefaults.precompiledPath != null) {
137-
return _spawnPrecompiledProcess(path, runtime, suiteConfig, socketPort,
138-
_config.suiteDefaults.precompiledPath!);
140+
return _spawnPrecompiledProcess(path, platform.runtime, suiteConfig,
141+
socketPort, _config.suiteDefaults.precompiledPath!);
139142
} else {
140-
return _spawnNormalProcess(path, runtime, suiteConfig, socketPort);
143+
return switch (platform.compiler) {
144+
Compiler.dart2js => _spawnNormalJsProcess(
145+
path, platform.runtime, suiteConfig, socketPort),
146+
Compiler.dart2wasm => _spawnNormalWasmProcess(
147+
path, platform.runtime, suiteConfig, socketPort),
148+
_ => throw StateError('Unsupported compiler ${platform.compiler}'),
149+
};
141150
}
142151
}
143152

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('''
153+
Future<String> _entrypointScriptForTest(
154+
String testPath, SuiteConfiguration suiteConfig) async {
155+
return '''
151156
${suiteConfig.metadata.languageVersionComment ?? await rootPackageLanguageVersionComment}
152157
import "package:test/src/bootstrap/node.dart";
153158
@@ -156,7 +161,20 @@ class NodePlatform extends PlatformPlugin
156161
void main() {
157162
internalBootstrapNodeTest(() => test.main);
158163
}
159-
''', jsPath, suiteConfig);
164+
''';
165+
}
166+
167+
/// Compiles [testPath] with dart2js, adds the node preamble, and then spawns
168+
/// a Node.js process that loads that Dart test suite.
169+
Future<(Process, StackTraceMapper?)> _spawnNormalJsProcess(String testPath,
170+
Runtime runtime, SuiteConfiguration suiteConfig, int socketPort) async {
171+
var dir = Directory(_compiledDir).createTempSync('test_').path;
172+
var jsPath = p.join(dir, '${p.basename(testPath)}.node_test.dart.js');
173+
await _jsCompilers.compile(
174+
await _entrypointScriptForTest(testPath, suiteConfig),
175+
jsPath,
176+
suiteConfig,
177+
);
160178

161179
// Add the Node.js preamble to ensure that the dart2js output is
162180
// compatible. Use the minified version so the source map remains valid.
@@ -173,12 +191,63 @@ class NodePlatform extends PlatformPlugin
173191
packageMap: (await currentPackageConfig).toPackageMap());
174192
}
175193

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

179248
/// Spawns a Node.js process that loads the Dart test suite at [testPath]
180249
/// under [precompiledPath].
181-
Future<Pair<Process, StackTraceMapper?>> _spawnPrecompiledProcess(
250+
Future<(Process, StackTraceMapper?)> _spawnPrecompiledProcess(
182251
String testPath,
183252
Runtime runtime,
184253
SuiteConfiguration suiteConfig,
@@ -195,7 +264,7 @@ class NodePlatform extends PlatformPlugin
195264
.toPackageMap());
196265
}
197266

198-
return Pair(await _startProcess(runtime, jsPath, socketPort), mapper);
267+
return (await _startProcess(runtime, jsPath, socketPort), mapper);
199268
}
200269

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

225294
@override
226295
Future<void> close() => _closeMemo.runOnce(() async {
227-
await _compilers.close();
296+
await _jsCompilers.close();
297+
await _wasmCompilers.close();
228298
await Directory(_compiledDir).deleteWithRetry();
229299
});
230300
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),

pkgs/test/test/runner/node/runner_test.dart

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@ void main() {
116116
expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
117117
await test.shouldExit(0);
118118
});
119+
120+
test('compiled with dart2wasm', () async {
121+
await d.file('test.dart', _success).create();
122+
var test =
123+
await runTest(['-p', 'node', '--compiler', 'dart2wasm', 'test.dart']);
124+
125+
expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
126+
await test.shouldExit(0);
127+
});
119128
});
120129

121130
test('defines a node environment constant', () async {
@@ -148,8 +157,18 @@ void main() {
148157
}
149158
''').create();
150159

151-
var test = await runTest(['-p', 'node', '-p', 'vm', 'test.dart']);
152-
expect(test.stdout, emitsThrough(contains('+1 -1: Some tests failed.')));
160+
var test = await runTest([
161+
'-p',
162+
'node',
163+
'-p',
164+
'vm',
165+
'-c',
166+
'dart2js',
167+
'-c',
168+
'dart2wasm',
169+
'test.dart'
170+
]);
171+
expect(test.stdout, emitsThrough(contains('+1 -2: Some tests failed.')));
153172
await test.shouldExit(1);
154173
});
155174

pkgs/test_api/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## 0.7.4-wip
22

33
* Increase SDK constraint to ^3.5.0-259.0.dev.
4+
* Support running Node.js tests compiled with dart2wasm.
45

56
## 0.7.3
67

pkgs/test_api/lib/src/backend/runtime.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ final class Runtime {
4141
isBrowser: true, isBlink: true);
4242

4343
/// The command-line Node.js VM.
44-
static const Runtime nodeJS =
45-
Runtime('Node.js', 'node', Compiler.dart2js, [Compiler.dart2js]);
44+
static const Runtime nodeJS = Runtime('Node.js', 'node', Compiler.dart2js,
45+
[Compiler.dart2js, Compiler.dart2wasm]);
4646

4747
/// The platforms that are supported by the test runner by default.
4848
static const List<Runtime> builtIn = [

pkgs/test_core/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## 0.6.6-wip
22

33
* Increase SDK constraint to ^3.5.0-259.0.dev.
4+
* Allow passing additional arguments to `dart compile wasm`.
45

56
## 0.6.5
67

pkgs/test_core/lib/src/runner/wasm_compiler_pool.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@ import 'suite.dart';
1717
///
1818
/// This limits the number of compiler instances running concurrently.
1919
class WasmCompilerPool extends CompilerPool {
20+
/// Extra arguments to pass to `dart compile js`.
21+
final List<String> _extraArgs;
22+
2023
/// The currently-active dart2wasm processes.
2124
final _processes = <Process>{};
2225

26+
WasmCompilerPool([this._extraArgs = const []]);
27+
2328
/// Compiles [code] to [path].
2429
///
2530
/// This wraps the Dart code in the standard browser-testing wrapper.
@@ -41,6 +46,7 @@ class WasmCompilerPool extends CompilerPool {
4146
for (var experiment in enabledExperiments)
4247
'--enable-experiment=$experiment',
4348
'-O0',
49+
..._extraArgs,
4450
'-o',
4551
outWasmPath,
4652
wrapperPath,

0 commit comments

Comments
 (0)