Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support dart2wasm in node.js tests #2259

Merged
merged 10 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 81 additions & 81 deletions .github/workflows/dart.yml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion integration_tests/regression/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: regression_tests
publish_to: none
environment:
sdk: ^3.5.0-259.0.dev
sdk: ^3.5.0-311.0.dev
resolution: workspace
dependencies:
test: any
2 changes: 1 addition & 1 deletion integration_tests/spawn_hybrid/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: spawn_hybrid
publish_to: none
environment:
sdk: ^3.5.0-259.0.dev
sdk: ^3.5.0-311.0.dev
resolution: workspace
dependencies:
async: ^2.9.0
Expand Down
3 changes: 3 additions & 0 deletions integration_tests/wasm/dart_test.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
platforms: [chrome, firefox]
# Node doesn't work because the version available in the current Ubuntu GitHub runners is too
# old to support WASM+GC, which would be required to run Dart tests.
#platforms: [chrome, firefox, node]
compilers: [dart2wasm]
2 changes: 1 addition & 1 deletion integration_tests/wasm/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: wasm_tests
publish_to: none
environment:
sdk: ^3.5.0-259.0.dev
sdk: ^3.5.0-311.0.dev
resolution: workspace
dev_dependencies:
test: any
2 changes: 1 addition & 1 deletion pkgs/checks/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ repository: https://github.com/dart-lang/test/tree/master/pkgs/checks
resolution: workspace

environment:
sdk: ^3.5.0-259.0.dev
sdk: ^3.5.0-311.0.dev

dependencies:
async: ^2.8.0
Expand Down
3 changes: 2 additions & 1 deletion pkgs/test/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
## 1.25.9-wip

* Fix dart2wasm tests on windows.
* Increase SDK constraint to ^3.5.0-259.0.dev.
* Increase SDK constraint to ^3.5.0-311.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));
}
147 changes: 115 additions & 32 deletions pkgs/test/lib/src/runner/node/platform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,18 @@ import 'package:test_api/backend.dart'
import 'package:test_core/src/runner/application_exception.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/dart2js_compiler_pool.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/load_exception.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/package_version.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/platform.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/plugin/customizable_platform.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/plugin/environment.dart'; // ignore: implementation_imports
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 +41,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 +77,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 +96,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 @@ -110,7 +111,19 @@ class NodePlatform extends PlatformPlugin
process.stdout.transform(lineSplitter).listen(print);
process.stderr.transform(lineSplitter).listen(print);

var socket = await StreamGroup.merge(servers).first;
// Wait for the first connection (either over ipv4 or v6). If the proccess
// exits before it connects, throw instead of waiting for a connection
// indefinitely.
var socket = await Future.any([
StreamGroup.merge(servers).first,
process.exitCode.then((_) => null),
]);

if (socket == null) {
throw LoadException(
path, 'Node exited before connecting to the test channel.');
}

var channel = StreamChannel(socket.cast<List<int>>(), socket)
.transform(StreamChannelTransformer.fromCodec(utf8))
.transform(_chunksToLines)
Expand All @@ -120,7 +133,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 +144,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 +174,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 +204,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 +277,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 +306,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
2 changes: 1 addition & 1 deletion pkgs/test/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ repository: https://github.com/dart-lang/test/tree/master/pkgs/test
resolution: workspace

environment:
sdk: ^3.5.0-259.0.dev
sdk: ^3.5.0-311.0.dev

dependencies:
analyzer: '>=5.12.0 <7.0.0'
Expand Down
Loading
Loading