| // 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: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/test_platform.dart'; |
| import '../../util/io.dart'; |
| import '../../util/stack_trace_mapper.dart'; |
| import '../../utils.dart'; |
| import '../application_exception.dart'; |
| 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 pair = await _loadChannel(path, platform, 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, SuiteConfiguration suiteConfig) async { |
| var server = await MultiServerSocket.loopback(0); |
| |
| var pair = await _spawnProcess(path, platform, 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, |
| 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 _compileSuite(path, jsPath, suiteConfig); |
| } else { |
| mapper = await _downloadPubServeSuite(path, jsPath, suiteConfig); |
| } |
| |
| return new Pair(await _startProcess(platform, jsPath, socketPort), mapper); |
| } |
| |
| /// 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)); |
| } |
| |
| /// 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')); |
| } |
| |
| /// 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; |
| } |
| } |
| |
| /// 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(); |
| } |