blob: fda1d6c31c36ad2138f80a958ea9f666278b93c5 [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:isolate';
import 'dart:convert';
import 'package:async/async.dart';
import 'package:multi_server_socket/multi_server_socket.dart';
import 'package:node_preamble/preamble.dart' as preamble;
import 'package:package_resolver/package_resolver.dart';
import 'package:path/path.dart' as p;
import 'package:stream_channel/stream_channel.dart';
import 'package:yaml/yaml.dart';
import '../../backend/compiler.dart';
import '../../backend/test_platform.dart';
import '../../util/io.dart';
import '../../util/stack_trace_mapper.dart';
import '../../utils.dart';
import '../application_exception.dart';
import '../build.dart' as build;
import '../compiler_pool.dart';
import '../configuration.dart';
import '../configuration/suite.dart';
import '../executable_settings.dart';
import '../load_exception.dart';
import '../plugin/customizable_platform.dart';
import '../plugin/environment.dart';
import '../plugin/platform.dart';
import '../plugin/platform_helpers.dart';
import '../runner_suite.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 = new CompilerPool(["-Dnode=true"]);
/// 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 [TestPlatform.nodeJS] and platforms that extend
/// it.
final _settings = {
TestPlatform.nodeJS: new ExecutableSettings(
linuxExecutable: "node",
macOSExecutable: "node",
windowsExecutable: "node.exe")
};
NodePlatform()
: _config = Configuration.current,
_http =
Configuration.current.pubServeUrl == null ? null : new HttpClient();
ExecutableSettings parsePlatformSettings(YamlMap settings) =>
new ExecutableSettings.parse(settings);
ExecutableSettings mergePlatformSettings(
ExecutableSettings settings1, ExecutableSettings settings2) =>
settings1.merge(settings2);
void customizePlatform(TestPlatform platform, ExecutableSettings settings) {
var oldSettings = _settings[platform] ?? _settings[platform.root];
if (oldSettings != null) settings = oldSettings.merge(settings);
_settings[platform] = settings;
}
StreamChannel loadChannel(String path, TestPlatform platform) =>
throw new UnimplementedError();
Future<RunnerSuite> load(String path, TestPlatform platform,
SuiteConfiguration suiteConfig, Object message) async {
assert(platform == TestPlatform.nodeJS);
var compiler = Compiler.find((message as Map)['compiler'] as String);
var pair = await _loadChannel(path, platform, compiler, suiteConfig);
var controller = await deserializeSuite(path, platform, suiteConfig,
new PluginEnvironment(), pair.first, message,
mapper: pair.last);
return 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,
TestPlatform platform,
Compiler compiler,
SuiteConfiguration suiteConfig) async {
var server = await MultiServerSocket.loopback(0);
var pair =
await _spawnProcess(path, platform, compiler, 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;
// TODO(nweiz): Remove the DelegatingStreamSink wrapper when sdk#31504 is
// fixed.
var channel = new StreamChannel(socket, new DelegatingStreamSink(socket))
.transform(new StreamChannelTransformer.fromCodec(UTF8))
.transform(chunksToLines)
.transform(jsonDocument)
.transformStream(new StreamTransformer.fromHandlers(handleDone: (sink) {
if (process != null) process.kill();
sink.close();
}));
return new Pair(channel, pair.last);
}
/// 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,
TestPlatform platform,
Compiler compiler,
SuiteConfiguration suiteConfig,
int socketPort) async {
var dir = new Directory(_compiledDir).createTempSync('test_').path;
var jsPath = p.join(dir, p.basename(path) + ".node_test.dart.js");
StackTraceMapper mapper;
if (_config.pubServeUrl != null) {
mapper = await _downloadPubServeSuite(path, jsPath, suiteConfig);
} else if (compiler == Compiler.build) {
await _writeBuildSuite(path, jsPath);
} else {
mapper = await _compileSuite(path, jsPath, suiteConfig);
}
return new Pair(await _startProcess(platform, jsPath, socketPort), mapper);
}
/// Compiles the test suite at [dartPath] to JavaScript at [jsPath] from `pub
/// serve`.
///
/// If [suiteConfig.jsTrace] is `true`, returns a [StackTraceMapper] that will
/// convert JS stack traces to Dart.
Future<StackTraceMapper> _downloadPubServeSuite(
String dartPath, String jsPath, SuiteConfiguration suiteConfig) async {
var url = _config.pubServeUrl.resolveUri(
p.toUri(p.relative(dartPath, from: 'test') + '.node_test.dart.js'));
var js = await _get(url, dartPath);
await new File(jsPath)
.writeAsString(preamble.getPreamble(minified: true) + js);
if (suiteConfig.jsTrace) return null;
var mapUrl = url.replace(path: url.path + '.map');
return new StackTraceMapper(await _get(mapUrl, dartPath),
mapUrl: mapUrl,
packageResolver: new SyncPackageResolver.root('packages'),
sdkRoot: p.toUri('packages/\$sdk'));
}
Future _writeBuildSuite(String dartPath, String jsPath) async {
var bootstrapPath = await _compileBootstrap();
var modulePaths = {
"dart_sdk": p.join(sdkDir, 'lib/dev_compiler/amd/dart_sdk'),
"packages/test/src/bootstrap/node": p.withoutExtension(bootstrapPath)
};
for (var path in build.ddcModules) {
var components = p.split(p.relative(path, from: build.generatedDir));
Uri module;
if (components[1] == 'lib') {
var package = components.first;
var pathInLib = p.joinAll(components.skip(2));
module = p.toUri(p.join('packages', package, pathInLib));
} else {
assert(components.first == rootPackageName);
module = p.toUri(p.joinAll(components.skip(1)));
}
modulePaths[module.toString()] = p.absolute("$path.ddc");
}
var moduleName = p.withoutExtension(dartPath);
var requires = [moduleName, "packages/test/src/bootstrap/node", "dart_sdk"];
var moduleIdentifier =
p.url.split(moduleName).skip(1).join('__').replaceAll('.', '\$46');
var requirejsPath = p.fromUri(await Isolate.resolvePackageUri(
Uri.parse('package:test/src/runner/node/vendor/r.js')));
new File(jsPath).writeAsStringSync('''
${preamble.getPreamble()}
(function() {
let requirejs = require(${JSON.encode(requirejsPath)});
requirejs.config({waitSeconds: 0, paths: ${JSON.encode(modulePaths)}});
requirejs(${JSON.encode(requires)}, function(app, bootstrap, dart_sdk) {
dart_sdk._isolate_helper.startRootIsolate(() => {}, []);
dart_sdk._debugger.registerDevtoolsFormatter();
dart_sdk.dart.global = self;
global.self = self;
self.\$dartTestGetSourceMap = dart_sdk.dart.getSourceMap;
bootstrap.src__bootstrap__node.internalBootstrapNodeTest(
function() {
return app.$moduleIdentifier.main;
});
});
})();
''');
}
/// Compiles the test suite at [dartPath] to JavaScript at [jsPath] using
/// dart2js.
///
/// If [suiteConfig.jsTrace] is `true`, returns a [StackTraceMapper] that will
/// convert JS stack traces to Dart.
Future<StackTraceMapper> _compileSuite(
String dartPath, String jsPath, SuiteConfiguration suiteConfig) async {
await _compilers.compile('''
import "package:test/src/bootstrap/node.dart";
import "${p.toUri(p.absolute(dartPath))}" 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 = new File(jsPath);
await jsFile.writeAsString(
preamble.getPreamble(minified: true) + await jsFile.readAsString());
if (suiteConfig.jsTrace) return null;
var mapPath = jsPath + '.map';
return new StackTraceMapper(await new File(mapPath).readAsString(),
mapUrl: p.toUri(mapPath),
packageResolver: await PackageResolver.current.asSync,
sdkRoot: p.toUri(sdkDir));
}
/// Starts the Node.js process for [platform] with [jsPath].
Future<Process> _startProcess(
TestPlatform platform, String jsPath, int socketPort) async {
var settings = _settings[platform];
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 new Future.error(
new ApplicationException(
"Failed to run ${platform.name}: ${getErrorMessage(error)}"),
stackTrace);
return null;
}
}
/// Compiles "package:test/src/bootstrap/node.dart" to JS using DDC and
/// returns the path to the compiled result.
Future<String> _compileBootstrap() async {
// Pass the error through a result to avoid cross-zone issues.
var result = await _compileBootstrapMemo.runOnce(() {
return Result.capture(new Future(() async {
var js = await build.compile("package:test/src/bootstrap/node.dart");
var jsPath = p.join(_compiledDir, 'bootstrap.dart.js');
new File(jsPath).writeAsStringSync(js);
return jsPath;
}));
});
return await result.asFuture;
}
final _compileBootstrapMemo = new AsyncMemoizer<Result<String>>();
/// 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 new 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 new LoadException(
suitePath,
"Error getting $url: $message\n"
'Make sure "pub serve" is running.');
}
}
Future close() => _closeMemo.runOnce(() async {
await _compilers.close();
if (_config.pubServeUrl == null) {
new Directory(_compiledDir).deleteSync(recursive: true);
} else {
_http.close();
}
});
final _closeMemo = new AsyncMemoizer();
}