blob: bea9aa9f24202fd50d21588e5f8f91649d254192 [file] [log] [blame]
// 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.
import 'dart:async';
import 'dart:io';
import 'dart:convert';
import 'package:async/async.dart';
import 'package:package_config/package_config.dart';
import 'package:multi_server_socket/multi_server_socket.dart';
import 'package:node_preamble/preamble.dart' as preamble;
import 'package:path/path.dart' as p;
import 'package:stream_channel/stream_channel.dart';
import 'package:yaml/yaml.dart';
import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
import 'package:test_api/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports
import 'package:test_api/src/utils.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/platform.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/util/io.dart'; // ignore: implementation_imports
import 'package:test_core/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/application_exception.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/compiler_pool.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/load_exception.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 '../executable_settings.dart';
import '../../util/package_map.dart';
/// A platform that loads tests in Node.js processes.
class NodePlatform extends PlatformPlugin
implements CustomizablePlatform<ExecutableSettings> {
/// The test runner configuration.
final Configuration _config;
/// The [CompilerPool] managing active instances of `dart2js`.
final _compilers = CompilerPool(['-Dnode=true', '--server-mode']);
/// The temporary directory in which compiled JS is emitted.
final _compiledDir = createTempDir();
/// The HTTP client to use when fetching JS files for `pub serve`.
final HttpClient _http;
/// Executable settings for [Runtime.nodeJS] and runtimes that extend
/// it.
final _settings = {
Runtime.nodeJS: ExecutableSettings(
linuxExecutable: 'node',
macOSExecutable: 'node',
windowsExecutable: 'node.exe')
};
NodePlatform()
: _config = Configuration.current,
_http = Configuration.current.pubServeUrl == null ? null : HttpClient();
@override
ExecutableSettings parsePlatformSettings(YamlMap settings) =>
ExecutableSettings.parse(settings);
@override
ExecutableSettings mergePlatformSettings(
ExecutableSettings settings1, ExecutableSettings settings2) =>
settings1.merge(settings2);
@override
void customizePlatform(Runtime runtime, ExecutableSettings settings) {
var oldSettings = _settings[runtime] ?? _settings[runtime.root];
if (oldSettings != null) settings = oldSettings.merge(settings);
_settings[runtime] = settings;
}
@override
StreamChannel loadChannel(String path, SuitePlatform platform) =>
throw UnimplementedError();
@override
Future<RunnerSuite> load(String path, SuitePlatform platform,
SuiteConfiguration suiteConfig, Object message) async {
var pair = await _loadChannel(path, platform.runtime, suiteConfig);
var controller = deserializeSuite(
path, platform, suiteConfig, PluginEnvironment(), pair.first, message);
controller.channel('test.node.mapper').sink.add(pair.last?.serialize());
return await controller.suite;
}
/// Loads a [StreamChannel] communicating with the test suite at [path].
///
/// Returns that channel along with a [StackTraceMapper] representing the
/// source map for the compiled suite.
Future<Pair<StreamChannel, StackTraceMapper>> _loadChannel(
String path, Runtime runtime, SuiteConfiguration suiteConfig) async {
var server = await MultiServerSocket.loopback(0);
try {
var pair = await _spawnProcess(path, runtime, suiteConfig, server.port);
var process = pair.first;
// Forward Node's standard IO to the print handler so it's associated with
// the load test.
//
// TODO(nweiz): Associate this with the current test being run, if any.
process.stdout.transform(lineSplitter).listen(print);
process.stderr.transform(lineSplitter).listen(print);
var socket = await server.first;
var channel = StreamChannel(socket.cast<List<int>>(), socket)
.transform(StreamChannelTransformer.fromCodec(utf8))
.transform(chunksToLines)
.transform(jsonDocument)
.transformStream(StreamTransformer.fromHandlers(handleDone: (sink) {
if (process != null) process.kill();
sink.close();
}));
return Pair(channel, pair.last);
} catch (_) {
unawaited(server.close().catchError((_) {}));
rethrow;
}
}
/// Spawns a Node.js process that loads the Dart test suite at [path].
///
/// 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 {
if (_config.suiteDefaults.precompiledPath != null) {
return _spawnPrecompiledProcess(path, runtime, suiteConfig, socketPort,
_config.suiteDefaults.precompiledPath);
} else if (_config.pubServeUrl != null) {
return _spawnPubServeProcess(path, runtime, suiteConfig, socketPort);
} else {
return _spawnNormalProcess(path, runtime, suiteConfig, socketPort);
}
}
/// 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('''
import "package:test/src/bootstrap/node.dart";
import "${p.toUri(p.absolute(testPath))}" as test;
void main() {
internalBootstrapNodeTest(() => test.main);
}
''', 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.
var jsFile = File(jsPath);
await jsFile.writeAsString(
preamble.getPreamble(minified: true) + await jsFile.readAsString());
StackTraceMapper mapper;
if (!suiteConfig.jsTrace) {
var mapPath = jsPath + '.map';
mapper = JSStackTraceMapper(await File(mapPath).readAsString(),
mapUrl: p.toUri(mapPath),
sdkRoot: Uri.parse('org-dartlang-sdk:///sdk'),
packageMap: (await currentPackageConfig).toPackageMap());
}
return Pair(await _startProcess(runtime, jsPath, socketPort), mapper);
}
/// Spawns a Node.js process that loads the Dart test suite at [testPath]
/// under [precompiledPath].
Future<Pair<Process, StackTraceMapper>> _spawnPrecompiledProcess(
String testPath,
Runtime runtime,
SuiteConfiguration suiteConfig,
int socketPort,
String precompiledPath) async {
StackTraceMapper mapper;
var jsPath = p.join(precompiledPath, '$testPath.node_test.dart.js');
if (!suiteConfig.jsTrace) {
var mapPath = jsPath + '.map';
mapper = JSStackTraceMapper(await File(mapPath).readAsString(),
mapUrl: p.toUri(mapPath),
sdkRoot: Uri.parse('org-dartlang-sdk:///sdk'),
packageMap: (await findPackageConfig(Directory(precompiledPath)))
.toPackageMap());
}
return Pair(await _startProcess(runtime, jsPath, socketPort), mapper);
}
/// Requests the compiled js for [testPath] from the pub serve url, prepends
/// the node preamble, and then spawns a Node.js process that loads that Dart
/// test suite.
Future<Pair<Process, StackTraceMapper>> _spawnPubServeProcess(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');
var url = _config.pubServeUrl.resolveUri(
p.toUri(p.relative(testPath, from: 'test') + '.node_test.dart.js'));
var js = await _get(url, testPath);
await File(jsPath).writeAsString(preamble.getPreamble(minified: true) + js);
StackTraceMapper mapper;
if (!suiteConfig.jsTrace) {
var mapUrl = url.replace(path: url.path + '.map');
mapper = JSStackTraceMapper(await _get(mapUrl, testPath),
mapUrl: mapUrl,
sdkRoot: p.toUri('packages/\$sdk'),
packageMap: (await currentPackageConfig).toPackagesDirPackageMap());
}
return Pair(await _startProcess(runtime, jsPath, socketPort), mapper);
}
/// Starts the Node.js process for [runtime] with [jsPath].
Future<Process> _startProcess(
Runtime runtime, String jsPath, int socketPort) async {
var settings = _settings[runtime];
var nodeModules = p.absolute('node_modules');
var nodePath = Platform.environment['NODE_PATH'];
nodePath = nodePath == null ? nodeModules : '$nodePath:$nodeModules';
try {
return await Process.start(settings.executable,
settings.arguments.toList()..add(jsPath)..add(socketPort.toString()),
environment: {'NODE_PATH': nodePath});
} catch (error, stackTrace) {
await Future.error(
ApplicationException(
'Failed to run ${runtime.name}: ${getErrorMessage(error)}'),
stackTrace);
return null;
}
}
/// Runs an HTTP GET on [url].
///
/// If this fails, throws a [LoadException] for [suitePath].
Future<String> _get(Uri url, String suitePath) async {
try {
var response = await (await _http.getUrl(url)).close();
if (response.statusCode != 200) {
// We don't care about the response body, but we have to drain it or
// else the process can't exit.
response.listen(null);
throw LoadException(
suitePath,
'Error getting $url: ${response.statusCode} '
'${response.reasonPhrase}\n'
'Make sure "pub serve" is serving the test/ directory.');
}
return await utf8.decodeStream(response);
} on IOException catch (error) {
var message = getErrorMessage(error);
if (error is SocketException) {
message = '${error.osError.message} '
'(errno ${error.osError.errorCode})';
}
throw LoadException(
suitePath,
'Error getting $url: $message\n'
'Make sure "pub serve" is running.');
}
}
@override
Future close() => _closeMemo.runOnce(() async {
await _compilers.close();
if (_config.pubServeUrl == null) {
Directory(_compiledDir).deleteSync(recursive: true);
} else {
_http.close();
}
});
final _closeMemo = AsyncMemoizer();
}