Skip to content

Commit

Permalink
Support dart2wasm in node.js
Browse files Browse the repository at this point in the history
  • Loading branch information
simolus3 committed Jul 26, 2024
1 parent d0dc833 commit a1ad8f3
Show file tree
Hide file tree
Showing 10 changed files with 152 additions and 59 deletions.
2 changes: 1 addition & 1 deletion integration_tests/wasm/dart_test.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
platforms: [chrome, firefox]
platforms: [chrome, firefox, node]
compilers: [dart2wasm]
1 change: 1 addition & 0 deletions pkgs/test/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion pkgs/test/lib/src/bootstrap/node.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
132 changes: 101 additions & 31 deletions pkgs/test/lib/src/runner/node/platform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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();
Expand Down Expand Up @@ -75,15 +76,17 @@ class NodePlatform extends PlatformPlugin
@override
Future<RunnerSuite> load(String path, SuitePlatform platform,
SuiteConfiguration suiteConfig, Map<String, Object?> 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;
}
Expand All @@ -92,16 +95,13 @@ class NodePlatform extends PlatformPlugin
///
/// Returns that channel along with a [StackTraceMapper] representing the
/// source map for the compiled suite.
Future<Pair<StreamChannel<Object?>, StackTraceMapper?>> _loadChannel(
String path,
SuitePlatform platform,
SuiteConfiguration suiteConfig) async {
Future<(StreamChannel<Object?>, 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.
Expand All @@ -120,7 +120,7 @@ class NodePlatform extends PlatformPlugin
sink.close();
}));

return Pair(channel, pair.last);
return (channel, stackMapper);
} finally {
unawaited(Future.wait<void>(servers.map((s) =>
s.close().then<ServerSocket?>((v) => v).onError((_, __) => null))));
Expand All @@ -131,23 +131,28 @@ class NodePlatform extends PlatformPlugin
///
/// Returns that channel along with a [StackTraceMapper] representing the
/// source map for the compiled suite.
Future<Pair<Process, StackTraceMapper?>> _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<Pair<Process, StackTraceMapper?>> _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<String> _entrypointScriptForTest(
String testPath, SuiteConfiguration suiteConfig) async {
return '''
${suiteConfig.metadata.languageVersionComment ?? await rootPackageLanguageVersionComment}
import "package:test/src/bootstrap/node.dart";
Expand All @@ -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.
Expand All @@ -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<Pair<Process, StackTraceMapper?>> _spawnPrecompiledProcess(
Future<(Process, StackTraceMapper?)> _spawnPrecompiledProcess(
String testPath,
Runtime runtime,
SuiteConfiguration suiteConfig,
Expand All @@ -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].
Expand Down Expand Up @@ -224,7 +293,8 @@ class NodePlatform extends PlatformPlugin

@override
Future<void> close() => _closeMemo.runOnce(() async {
await _compilers.close();
await _jsCompilers.close();
await _wasmCompilers.close();
await Directory(_compiledDir).deleteWithRetry();
});
final _closeMemo = AsyncMemoizer<void>();
Expand Down
39 changes: 17 additions & 22 deletions pkgs/test/lib/src/runner/node/socket_channel.dart
Original file line number Diff line number Diff line change
@@ -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<String> get _args;
external JSArray<JSString> 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<Object?> socketChannel() {
var net = _require('net');
var socket = net.connect(int.parse(_args[2]));
socket.setEncoding('utf8');
Future<StreamChannel<Object?>> 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<Object?>(sync: true)
..stream.listen((event) => socket.write('${jsonEncode(event)}\n'));
..stream.listen((event) => socket.write('${jsonEncode(event)}\n'.toJS));

var socketStream = StreamController<String>(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),
Expand Down
23 changes: 21 additions & 2 deletions pkgs/test/test/runner/node/runner_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
});

Expand Down
1 change: 1 addition & 0 deletions pkgs/test_api/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
4 changes: 2 additions & 2 deletions pkgs/test_api/lib/src/backend/runtime.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<Runtime> builtIn = [
Expand Down
1 change: 1 addition & 0 deletions pkgs/test_core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
6 changes: 6 additions & 0 deletions pkgs/test_core/lib/src/runner/wasm_compiler_pool.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> _extraArgs;

/// The currently-active dart2wasm processes.
final _processes = <Process>{};

WasmCompilerPool([this._extraArgs = const []]);

/// Compiles [code] to [path].
///
/// This wraps the Dart code in the standard browser-testing wrapper.
Expand All @@ -41,6 +46,7 @@ class WasmCompilerPool extends CompilerPool {
for (var experiment in enabledExperiments)
'--enable-experiment=$experiment',
'-O0',
..._extraArgs,
'-o',
outWasmPath,
wrapperPath,
Expand Down

0 comments on commit a1ad8f3

Please sign in to comment.