Add a Node platform. (#663)
This also makes CompilerPool more general, so it can be used by both
the node and the browser platforms.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3a15d73..eaf495e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.12.24
+
+* Add a `node` platform for compiling tests to JavaScript and running them on
+ Node.js.
+
## 0.12.23+1
* Remove unused imports.
diff --git a/README.md b/README.md
index 8cc47de..2519e20 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,7 @@
* [Restricting Tests to Certain Platforms](#restricting-tests-to-certain-platforms)
* [Platform Selectors](#platform-selectors)
* [Running Tests on Dartium](#running-tests-on-dartium)
+ * [Running Tests on Node.js](#running-tests-on-nodejs)
* [Asynchronous Tests](#asynchronous-tests)
* [Stream Matchers](#stream-matchers)
* [Running Tests With Custom HTML](#running-tests-with-custom-html)
@@ -219,6 +220,8 @@
* `ie`: Whether the test is running on Microsoft Internet Explorer.
+* `node`: Whether the test is running on Node.js.
+
* `dart-vm`: Whether the test is running on the Dart VM in any context,
including Dartium. It's identical to `!js`.
@@ -277,6 +280,24 @@
[issue 63]: https://github.com/dart-lang/test/issues/63
+### Running Tests on Node.js
+
+The test runner also supports compiling tests to JavaScript and running them on
+[Node.js][] by passing `--platform node`. Note that Node has access to *neither*
+`dart:html` nor `dart:io`, so any platform-specific APIs will have to be invoked
+using [the `js` package][js]. However, it may be useful when testing APIs
+that are meant to be used by JavaScript code.
+
+[Node.js]: https://nodejs.org/en/
+[js]: https://pub.dartlang.org/packages/js
+
+The test runner looks for an executable named `node` (on Mac OS or Linux) or
+`node.exe` (on Windows) on your system path. When compiling Node.js tests, it
+passes `-Dnode=true`, so tests can determine whether they're running on Node
+using [`const bool.fromEnvironment("node")`][bool.fromEnvironment].
+
+[bool.fromEnvironment]: https://api.dartlang.org/stable/1.24.2/dart-core/bool/bool.fromEnvironment.html
+
## Asynchronous Tests
Tests written with `async`/`await` will work automatically. The test runner
diff --git a/dart_test.yaml b/dart_test.yaml
index c73b23b..9af2457 100644
--- a/dart_test.yaml
+++ b/dart_test.yaml
@@ -31,3 +31,7 @@
# Tests that run pub. These tests may need to be excluded when there are local
# dependency_overrides.
pub:
+
+ # Tests that use Node.js. These tests may need to be excluded on systems that
+ # don't have Node installed.
+ node:
diff --git a/lib/pub_serve.dart b/lib/pub_serve.dart
index 546aef5..1ae4fd5 100644
--- a/lib/pub_serve.dart
+++ b/lib/pub_serve.dart
@@ -22,6 +22,7 @@
var id = transform.primaryId;
transform.declareOutput(id.addExtension('.vm_test.dart'));
transform.declareOutput(id.addExtension('.browser_test.dart'));
+ transform.declareOutput(id.addExtension('.node_test.dart'));
}
Future apply(Transform transform) async {
@@ -37,7 +38,7 @@
import "${p.url.basename(id.path)}" as test;
void main(_, SendPort message) {
- internalBootstrapVmTest(test.main, message);
+ internalBootstrapVmTest(() => test.main, message);
}
'''));
@@ -49,7 +50,19 @@
import "${p.url.basename(id.path)}" as test;
void main() {
- internalBootstrapBrowserTest(test.main);
+ internalBootstrapBrowserTest(() => test.main);
+ }
+ '''));
+
+ transform.addOutput(new Asset.fromString(
+ id.addExtension('.node_test.dart'),
+ '''
+ import "package:test/src/bootstrap/node.dart";
+
+ import "${p.url.basename(id.path)}" as test;
+
+ void main() {
+ internalBootstrapNodeTest(() => test.main);
}
'''));
diff --git a/lib/src/backend/test_platform.dart b/lib/src/backend/test_platform.dart
index dc21389..d387eba 100644
--- a/lib/src/backend/test_platform.dart
+++ b/lib/src/backend/test_platform.dart
@@ -45,6 +45,10 @@
"Internet Explorer", "ie",
isBrowser: true, isJS: true);
+ /// The command-line Node.js VM.
+ static const TestPlatform nodeJS =
+ const TestPlatform._("Node.js", "node", isJS: true);
+
/// A list of all instances of [TestPlatform].
static final UnmodifiableListView<TestPlatform> all =
new UnmodifiableListView<TestPlatform>(_allPlatforms);
@@ -95,7 +99,8 @@
TestPlatform.phantomJS,
TestPlatform.firefox,
TestPlatform.safari,
- TestPlatform.internetExplorer
+ TestPlatform.internetExplorer,
+ TestPlatform.nodeJS
];
/// **Do not call this function without express permission from the test package
diff --git a/lib/src/bootstrap/browser.dart b/lib/src/bootstrap/browser.dart
index c061711..cb79cc1 100644
--- a/lib/src/bootstrap/browser.dart
+++ b/lib/src/bootstrap/browser.dart
@@ -1,15 +1,15 @@
// 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 "../runner/browser/post_message_channel.dart";
import "../runner/plugin/remote_platform_helpers.dart";
-import "../utils.dart";
/// Bootstraps a browser test to communicate with the test runner.
///
/// This should NOT be used directly, instead use the `test/pub_serve`
/// transformer which will bootstrap your test and call this method.
-void internalBootstrapBrowserTest(AsyncFunction originalMain) {
- var channel = serializeSuite(() => originalMain);
+void internalBootstrapBrowserTest(Function getMain()) {
+ var channel = serializeSuite(getMain, hidePrints: false);
postMessageChannel().pipe(channel);
}
diff --git a/lib/src/bootstrap/node.dart b/lib/src/bootstrap/node.dart
new file mode 100644
index 0000000..961377e
--- /dev/null
+++ b/lib/src/bootstrap/node.dart
@@ -0,0 +1,15 @@
+// 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 "../runner/plugin/remote_platform_helpers.dart";
+import "../runner/node/stdio_channel.dart";
+
+/// Bootstraps a browser test to communicate with the test runner.
+///
+/// This should NOT be used directly, instead use the `test/pub_serve`
+/// transformer which will bootstrap your test and call this method.
+void internalBootstrapNodeTest(Function getMain()) {
+ var channel = serializeSuite(getMain);
+ stdioChannel().pipe(channel);
+}
diff --git a/lib/src/bootstrap/vm.dart b/lib/src/bootstrap/vm.dart
index c6cd010..3420e9b 100644
--- a/lib/src/bootstrap/vm.dart
+++ b/lib/src/bootstrap/vm.dart
@@ -1,22 +1,22 @@
// 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:isolate";
import "package:stream_channel/stream_channel.dart";
import "../runner/plugin/remote_platform_helpers.dart";
import "../runner/vm/catch_isolate_errors.dart";
-import "../utils.dart";
/// Bootstraps a vm test to communicate with the test runner.
///
/// This should NOT be used directly, instead use the `test/pub_serve`
/// transformer which will bootstrap your test and call this method.
-void internalBootstrapVmTest(AsyncFunction originalMain, SendPort sendPort) {
+void internalBootstrapVmTest(Function getMain(), SendPort sendPort) {
var channel = serializeSuite(() {
catchIsolateErrors();
- return originalMain;
+ return getMain();
});
new IsolateChannel.connectSend(sendPort).pipe(channel);
}
diff --git a/lib/src/runner/browser/platform.dart b/lib/src/runner/browser/platform.dart
index 839211c..fe66c3c 100644
--- a/lib/src/runner/browser/platform.dart
+++ b/lib/src/runner/browser/platform.dart
@@ -25,13 +25,13 @@
import '../../util/path_handler.dart';
import '../../util/stack_trace_mapper.dart';
import '../../utils.dart';
+import '../compiler_pool.dart';
import '../configuration.dart';
import '../configuration/suite.dart';
import '../load_exception.dart';
import '../plugin/platform.dart';
import '../runner_suite.dart';
import 'browser_manager.dart';
-import 'compiler_pool.dart';
import 'polymer.dart';
class BrowserPlatform extends PlatformPlugin {
@@ -70,9 +70,7 @@
final _jsHandler = new PathHandler();
/// The [CompilerPool] managing active instances of `dart2js`.
- ///
- /// This is `null` if tests are loaded from `pub serve`.
- final CompilerPool _compilers;
+ final _compilers = new CompilerPool();
/// The temporary directory in which compiled JS is emitted.
final String _compiledDir;
@@ -120,8 +118,7 @@
: _config = config,
_root = root == null ? p.current : root,
_compiledDir = config.pubServeUrl == null ? createTempDir() : null,
- _http = config.pubServeUrl == null ? null : new HttpClient(),
- _compilers = new CompilerPool() {
+ _http = config.pubServeUrl == null ? null : new HttpClient() {
var cascade = new shelf.Cascade().add(_webSocketHandler.handler);
if (_config.pubServeUrl == null) {
@@ -371,7 +368,18 @@
var dir = new Directory(_compiledDir).createTempSync('test_').path;
var jsPath = p.join(dir, p.basename(dartPath) + ".browser_test.dart.js");
- await _compilers.compile(dartPath, jsPath, suiteConfig);
+ await _compilers.compile(
+ '''
+ import "package:test/src/bootstrap/browser.dart";
+
+ import "${p.toUri(p.absolute(dartPath))}" as test;
+
+ void main() {
+ internalBootstrapBrowserTest(() => test.main);
+ }
+ ''',
+ jsPath,
+ suiteConfig);
if (_closed) return;
var jsUrl = p.toUri(p.relative(dartPath, from: _root)).path +
diff --git a/lib/src/runner/browser/compiler_pool.dart b/lib/src/runner/compiler_pool.dart
similarity index 81%
rename from lib/src/runner/browser/compiler_pool.dart
rename to lib/src/runner/compiler_pool.dart
index 736da08..677362e 100644
--- a/lib/src/runner/browser/compiler_pool.dart
+++ b/lib/src/runner/compiler_pool.dart
@@ -11,10 +11,10 @@
import 'package:path/path.dart' as p;
import 'package:pool/pool.dart';
-import '../../util/io.dart';
-import '../configuration.dart';
-import '../configuration/suite.dart';
-import '../load_exception.dart';
+import '../util/io.dart';
+import 'configuration.dart';
+import 'configuration/suite.dart';
+import 'load_exception.dart';
/// A regular expression matching the first status line printed by dart2js.
final _dart2jsStatus =
@@ -39,35 +39,27 @@
/// The memoizer for running [close] exactly once.
final _closeMemo = new AsyncMemoizer();
- /// Creates a compiler pool that multiple instances of `dart2js` at once.
- CompilerPool() : _pool = new Pool(Configuration.current.concurrency);
+ /// Extra arguments to pass to dart2js.
+ final List<String> _extraArgs;
- /// Compile the Dart code at [dartPath] to [jsPath].
+ /// Creates a compiler pool that multiple instances of `dart2js` at once.
+ CompilerPool([Iterable<String> extraArgs])
+ : _pool = new Pool(Configuration.current.concurrency),
+ _extraArgs = extraArgs?.toList() ?? const [];
+
+ /// Compiles [code] to [jsPath].
///
/// This wraps the Dart code in the standard browser-testing wrapper.
///
/// The returned [Future] will complete once the `dart2js` process completes
/// *and* all its output has been printed to the command line.
- Future compile(
- String dartPath, String jsPath, SuiteConfiguration suiteConfig) {
+ Future compile(String code, String jsPath, SuiteConfiguration suiteConfig) {
return _pool.withResource(() {
if (_closed) return null;
return withTempDir((dir) async {
var wrapperPath = p.join(dir, "runInBrowser.dart");
- new File(wrapperPath).writeAsStringSync('''
- import "package:stream_channel/stream_channel.dart";
-
- import "package:test/src/runner/plugin/remote_platform_helpers.dart";
- import "package:test/src/runner/browser/post_message_channel.dart";
-
- import "${p.toUri(p.absolute(dartPath))}" as test;
-
- main(_) async {
- var channel = serializeSuite(() => test.main, hidePrints: false);
- postMessageChannel().pipe(channel);
- }
- ''');
+ new File(wrapperPath).writeAsStringSync(code);
var dart2jsPath = _config.dart2jsPath;
if (Platform.isWindows) dart2jsPath += '.bat';
@@ -77,7 +69,9 @@
wrapperPath,
"--out=$jsPath",
await PackageResolver.current.processArgument
- ]..addAll(suiteConfig.dart2jsArgs);
+ ]
+ ..addAll(_extraArgs)
+ ..addAll(suiteConfig.dart2jsArgs);
if (_config.color) args.add("--enable-diagnostic-colors");
@@ -108,7 +102,7 @@
var output = buffer.toString().replaceFirst(_dart2jsStatus, '');
if (output.isNotEmpty) print(output);
- if (exitCode != 0) throw new LoadException(dartPath, "dart2js failed.");
+ if (exitCode != 0) throw "dart2js failed.";
_fixSourceMap(jsPath + '.map');
});
diff --git a/lib/src/runner/loader.dart b/lib/src/runner/loader.dart
index 194b78a..dc654f6 100644
--- a/lib/src/runner/loader.dart
+++ b/lib/src/runner/loader.dart
@@ -19,6 +19,7 @@
import 'configuration/suite.dart';
import 'load_exception.dart';
import 'load_suite.dart';
+import 'node/platform.dart';
import 'parse_metadata.dart';
import 'plugin/environment.dart';
import 'plugin/hack_register_platform.dart';
@@ -49,6 +50,7 @@
/// defaults to the working directory.
Loader({String root}) {
registerPlatformPlugin([TestPlatform.vm], () => new VMPlatform());
+ registerPlatformPlugin([TestPlatform.nodeJS], () => new NodePlatform());
registerPlatformPlugin([
TestPlatform.dartium,
TestPlatform.contentShell,
diff --git a/lib/src/runner/node/platform.dart b/lib/src/runner/node/platform.dart
new file mode 100644
index 0000000..e48609a
--- /dev/null
+++ b/lib/src/runner/node/platform.dart
@@ -0,0 +1,194 @@
+// 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: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 '../../backend/test_platform.dart';
+import '../../util/io.dart';
+import '../../util/stack_trace_mapper.dart';
+import '../../utils.dart';
+import '../compiler_pool.dart';
+import '../configuration.dart';
+import '../configuration/suite.dart';
+import '../load_exception.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 {
+ /// 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;
+
+ /// The Node executable to use.
+ String get _executable => Platform.isWindows ? "node.exe" : "node";
+
+ NodePlatform()
+ : _config = Configuration.current,
+ _http =
+ Configuration.current.pubServeUrl == null ? null : new HttpClient();
+
+ StreamChannel loadChannel(String path, TestPlatform platform) =>
+ throw new UnimplementedError();
+
+ Future<RunnerSuite> load(String path, TestPlatform platform,
+ SuiteConfiguration suiteConfig) async {
+ assert(platform == TestPlatform.nodeJS);
+
+ var pair = await _loadChannel(path, suiteConfig);
+ var controller = await deserializeSuite(
+ path, platform, suiteConfig, new PluginEnvironment(), pair.first,
+ 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, SuiteConfiguration suiteConfig) async {
+ var pair = await _spawnProcess(path, suiteConfig);
+ var process = pair.first;
+
+ // Node normally doesn't emit any standard error, but if it does we forward
+ // it to the print handler so it's associated with the load test.
+ process.stderr.transform(lineSplitter).listen(print);
+
+ var channel = new StreamChannel.withGuarantees(
+ process.stdout, process.stdin)
+ .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, SuiteConfiguration suiteConfig) async {
+ var dir = new Directory(_compiledDir).createTempSync('test_').path;
+ var jsPath = p.join(dir, p.basename(path) + ".node_test.dart.js");
+
+ if (_config.pubServeUrl == null) {
+ await _compilers.compile(
+ '''
+ import "package:test/src/bootstrap/node.dart";
+
+ import "${p.toUri(p.absolute(path))}" 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());
+
+ StackTraceMapper mapper;
+ if (!suiteConfig.jsTrace) {
+ var mapPath = jsPath + '.map';
+ mapper = new StackTraceMapper(await new File(mapPath).readAsString(),
+ mapUrl: p.toUri(mapPath),
+ packageResolver: await PackageResolver.current.asSync,
+ sdkRoot: p.toUri(sdkDir));
+ }
+
+ return new Pair(await Process.start(_executable, [jsPath]), mapper);
+ }
+
+ var url = _config.pubServeUrl.resolveUri(
+ p.toUri(p.relative(path, from: 'test') + '.node_test.dart.js'));
+
+ var js = await _get(url, path);
+ await new File(jsPath)
+ .writeAsString(preamble.getPreamble(minified: true) + js);
+
+ StackTraceMapper mapper;
+ if (!suiteConfig.jsTrace) {
+ var mapUrl = url.replace(path: url.path + '.map');
+ mapper = new StackTraceMapper(await _get(mapUrl, path),
+ mapUrl: mapUrl,
+ packageResolver: new SyncPackageResolver.root('packages'),
+ sdkRoot: p.toUri('packages/\$sdk'));
+ }
+
+ return new Pair(await Process.start(_executable, [jsPath]), mapper);
+ }
+
+ /// 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();
+}
diff --git a/lib/src/runner/node/stdio_channel.dart b/lib/src/runner/node/stdio_channel.dart
new file mode 100644
index 0000000..5f16f3a
--- /dev/null
+++ b/lib/src/runner/node/stdio_channel.dart
@@ -0,0 +1,47 @@
+// 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 'package:js/js.dart';
+import 'package:stream_channel/stream_channel.dart';
+
+import '../../utils.dart';
+
+@JS("require")
+external _Process _require(String module);
+
+@JS()
+class _Process {
+ external _Stdin get stdin;
+ external _Stdout get stdout;
+}
+
+@JS()
+class _Stdin {
+ external setEncoding(String encoding);
+ external on(String event, void callback(String chunk));
+}
+
+@JS()
+class _Stdout {
+ external setDefaultEncoding(String encoding);
+ external write(String chunk);
+}
+
+/// Returns a [StreamChannel] of JSON-encodable objects that communicates over
+/// the current process's stdout and stdin streams.
+StreamChannel stdioChannel() {
+ var controller = new StreamChannelController<String>(
+ allowForeignErrors: false, sync: true);
+ var process = _require("process");
+ process.stdin.setEncoding("utf8");
+ process.stdout.setDefaultEncoding("utf8");
+
+ controller.local.stream.listen((chunk) => process.stdout.write(chunk));
+ process.stdin.on("data", allowInterop(controller.local.sink.add));
+
+ return controller.foreign.transform(chunksToLines).transform(jsonDocument);
+}
diff --git a/lib/src/runner/plugin/remote_platform_helpers.dart b/lib/src/runner/plugin/remote_platform_helpers.dart
index 9920636..1fbd5c4 100644
--- a/lib/src/runner/plugin/remote_platform_helpers.dart
+++ b/lib/src/runner/plugin/remote_platform_helpers.dart
@@ -22,6 +22,5 @@
/// suite will not be forwarded to the parent zone's print handler. However, the
/// caller may want them to be forwarded in (for example) a browser context
/// where they'll be visible in the development console.
-StreamChannel serializeSuite(AsyncFunction getMain(),
- {bool hidePrints: true}) =>
+StreamChannel serializeSuite(Function getMain(), {bool hidePrints: true}) =>
RemoteListener.start(getMain, hidePrints: hidePrints);
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 0d4d818..fd17f04 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -10,6 +10,7 @@
import 'package:async/async.dart';
import 'package:matcher/matcher.dart';
import 'package:path/path.dart' as p;
+import 'package:stream_channel/stream_channel.dart';
import 'package:term_glyph/term_glyph.dart' as glyph;
import 'backend/invoker.dart';
@@ -33,6 +34,14 @@
.transform(const LineSplitter())
.listen(null, cancelOnError: cancelOnError));
+/// A [StreamChannelTransformer] that converts a chunked string channel to a
+/// line-by-line channel. Note that this is only safe for channels whose
+/// messages are guaranteed not to contain newlines.
+final chunksToLines = new StreamChannelTransformer(
+ const LineSplitter(),
+ new StreamSinkTransformer.fromHandlers(
+ handleData: (data, sink) => sink.add("$data\n")));
+
/// A regular expression to match the exception prefix that some exceptions'
/// [Object.toString] values contain.
final _exceptionPrefix = new RegExp(r'^([A-Z][a-zA-Z]*)?(Exception|Error): ');
diff --git a/pubspec.yaml b/pubspec.yaml
index 93139c3..7fc6cec 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
name: test
-version: 0.12.24-dev
+version: 0.12.24
author: Dart Team <misc@dartlang.org>
description: A library for writing dart unit tests.
homepage: https://github.com/dart-lang/test
@@ -14,7 +14,9 @@
collection: '^1.8.0'
glob: '^1.0.0'
http_multi_server: '>=1.0.0 <3.0.0'
+ js: '^0.6.0'
meta: '^1.0.0'
+ node_preamble: '^1.3.0'
package_resolver: '^1.0.0'
path: '^1.2.0'
pool: '^1.3.0'
@@ -39,7 +41,6 @@
dev_dependencies:
fake_async: '^0.1.2'
http: '^0.11.0'
- js: '^0.6.0'
shelf_test_handler: '^1.0.0'
test_descriptor: '^1.0.0'
test_process: '^1.0.0'
diff --git a/test/runner/node/runner_test.dart b/test/runner/node/runner_test.dart
new file mode 100644
index 0000000..d82ccde
--- /dev/null
+++ b/test/runner/node/runner_test.dart
@@ -0,0 +1,295 @@
+// 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.
+
+@TestOn("vm")
+@Tags(const ["node"])
+
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import 'package:test/test.dart';
+
+import '../../io.dart';
+
+final _success = """
+ import 'package:test/test.dart';
+
+ void main() {
+ test("success", () {});
+ }
+""";
+
+final _failure = """
+ import 'package:test/test.dart';
+
+ void main() {
+ test("failure", () => throw new TestFailure("oh no"));
+ }
+""";
+
+void main() {
+ group("fails gracefully if", () {
+ test("a test file fails to compile", () async {
+ await d.file("test.dart", "invalid Dart file").create();
+ var test = await runTest(["-p", "node", "test.dart"]);
+
+ expect(
+ test.stdout,
+ containsInOrder([
+ "Expected a declaration, but got 'invalid'",
+ '-1: compiling test.dart [E]',
+ 'Failed to load "test.dart": dart2js failed.'
+ ]));
+ await test.shouldExit(1);
+ });
+
+ test("a test file throws", () async {
+ await d.file("test.dart", "void main() => throw 'oh no';").create();
+
+ var test = await runTest(["-p", "node", "test.dart"]);
+ expect(
+ test.stdout,
+ containsInOrder([
+ '-1: compiling test.dart [E]',
+ 'Failed to load "test.dart": oh no'
+ ]));
+ await test.shouldExit(1);
+ });
+
+ test("a test file doesn't have a main defined", () async {
+ await d.file("test.dart", "void foo() {}").create();
+
+ var test = await runTest(["-p", "node", "test.dart"]);
+ expect(
+ test.stdout,
+ containsInOrder([
+ '-1: compiling test.dart [E]',
+ 'Failed to load "test.dart": No top-level main() function defined.'
+ ]));
+ await test.shouldExit(1);
+ });
+
+ test("a test file has a non-function main", () async {
+ await d.file("test.dart", "int main;").create();
+
+ var test = await runTest(["-p", "node", "test.dart"]);
+ expect(
+ test.stdout,
+ containsInOrder([
+ '-1: compiling test.dart [E]',
+ 'Failed to load "test.dart": Top-level main getter is not a function.'
+ ]));
+ await test.shouldExit(1);
+ });
+
+ test("a test file has a main with arguments", () async {
+ await d.file("test.dart", "void main(arg) {}").create();
+
+ var test = await runTest(["-p", "node", "test.dart"]);
+ expect(
+ test.stdout,
+ containsInOrder([
+ '-1: compiling test.dart [E]',
+ 'Failed to load "test.dart": Top-level main() function takes arguments.'
+ ]));
+ await test.shouldExit(1);
+ });
+ });
+
+ group("runs successful tests", () {
+ test("on Node and the VM", () async {
+ await d.file("test.dart", _success).create();
+ var test = await runTest(["-p", "node", "-p", "vm", "test.dart"]);
+
+ expect(test.stdout, emitsThrough(contains("+2: All tests passed!")));
+ await test.shouldExit(0);
+ });
+
+ // Regression test; this broke in 0.12.0-beta.9.
+ test("on a file in a subdirectory", () async {
+ await d.dir("dir", [d.file("test.dart", _success)]).create();
+
+ var test = await runTest(["-p", "node", "dir/test.dart"]);
+ expect(test.stdout, emitsThrough(contains("+1: All tests passed!")));
+ await test.shouldExit(0);
+ });
+ });
+
+ test("defines a node environment constant", () async {
+ await d
+ .file(
+ "test.dart",
+ """
+ import 'package:test/test.dart';
+
+ void main() {
+ test("test", () {
+ expect(const bool.fromEnvironment("node"), isTrue);
+ });
+ }
+ """)
+ .create();
+
+ var test = await runTest(["-p", "node", "test.dart"]);
+ expect(test.stdout, emitsThrough(contains("+1: All tests passed!")));
+ await test.shouldExit(0);
+ });
+
+ test("runs failing tests that fail only on node", () async {
+ await d
+ .file(
+ "test.dart",
+ """
+ import 'package:path/path.dart' as p;
+ import 'package:test/test.dart';
+
+ void main() {
+ test("test", () {
+ if (const bool.fromEnvironment("node")) {
+ throw new TestFailure("oh no");
+ }
+ });
+ }
+ """)
+ .create();
+
+ var test = await runTest(["-p", "node", "-p", "vm", "test.dart"]);
+ expect(test.stdout, emitsThrough(contains("+1 -1: Some tests failed.")));
+ await test.shouldExit(1);
+ });
+
+ test("forwards prints from the Node test", () async {
+ await d
+ .file(
+ "test.dart",
+ """
+ import 'dart:async';
+
+ import 'package:test/test.dart';
+
+ void main() {
+ test("test", () {
+ print("Hello,");
+ return new Future(() => print("world!"));
+ });
+ }
+ """)
+ .create();
+
+ var test = await runTest(["-p", "node", "test.dart"]);
+ expect(test.stdout, emitsInOrder([emitsThrough("Hello,"), "world!"]));
+ await test.shouldExit(0);
+ });
+
+ test("dartifies stack traces for JS-compiled tests by default", () async {
+ await d.file("test.dart", _failure).create();
+
+ var test = await runTest(["-p", "node", "--verbose-trace", "test.dart"]);
+ expect(
+ test.stdout,
+ containsInOrder(
+ [" main.<fn>", "package:test", "dart:async/zone.dart"]));
+ await test.shouldExit(1);
+ });
+
+ test("doesn't dartify stack traces for JS-compiled tests with --js-trace",
+ () async {
+ await d.file("test.dart", _failure).create();
+
+ var test = await runTest(
+ ["-p", "node", "--verbose-trace", "--js-trace", "test.dart"]);
+ expect(test.stdoutStream(), neverEmits(endsWith(" main.<fn>")));
+ expect(test.stdoutStream(), neverEmits(contains("package:test")));
+ expect(test.stdoutStream(), neverEmits(contains("dart:async/zone.dart")));
+ expect(test.stdout, emitsThrough(contains("-1: Some tests failed.")));
+ await test.shouldExit(1);
+ });
+
+ group("with onPlatform", () {
+ test("respects matching Skips", () async {
+ await d
+ .file(
+ "test.dart",
+ '''
+ import 'dart:async';
+
+ import 'package:test/test.dart';
+
+ void main() {
+ test("fail", () => throw 'oh no', onPlatform: {"node": new Skip()});
+ }
+ ''')
+ .create();
+
+ var test = await runTest(["-p", "node", "test.dart"]);
+ expect(test.stdout, emitsThrough(contains("+0 ~1: All tests skipped.")));
+ await test.shouldExit(0);
+ });
+
+ test("ignores non-matching Skips", () async {
+ await d
+ .file(
+ "test.dart",
+ '''
+ import 'dart:async';
+
+ import 'package:test/test.dart';
+
+ void main() {
+ test("success", () {}, onPlatform: {"browser": new Skip()});
+ }
+ ''')
+ .create();
+
+ var test = await runTest(["-p", "node", "test.dart"]);
+ expect(test.stdout, emitsThrough(contains("+1: All tests passed!")));
+ await test.shouldExit(0);
+ });
+ });
+
+ group("with an @OnPlatform annotation", () {
+ test("respects matching Skips", () async {
+ await d
+ .file(
+ "test.dart",
+ '''
+ @OnPlatform(const {"js": const Skip()})
+
+ import 'dart:async';
+
+ import 'package:test/test.dart';
+
+ void main() {
+ test("fail", () => throw 'oh no');
+ }
+ ''')
+ .create();
+
+ var test = await runTest(["-p", "node", "test.dart"]);
+ expect(test.stdout, emitsThrough(contains("~1: All tests skipped.")));
+ await test.shouldExit(0);
+ });
+
+ test("ignores non-matching Skips", () async {
+ await d
+ .file(
+ "test.dart",
+ '''
+ @OnPlatform(const {"vm": const Skip()})
+
+ import 'dart:async';
+
+ import 'package:test/test.dart';
+
+ void main() {
+ test("success", () {});
+ }
+ ''')
+ .create();
+
+ var test = await runTest(["-p", "node", "test.dart"]);
+ expect(test.stdout, emitsThrough(contains("+1: All tests passed!")));
+ await test.shouldExit(0);
+ });
+ });
+}
diff --git a/test/runner/pub_serve_test.dart b/test/runner/pub_serve_test.dart
index e79af54..1c1c3de 100644
--- a/test/runner/pub_serve_test.dart
+++ b/test/runner/pub_serve_test.dart
@@ -106,6 +106,14 @@
await pub.kill();
}, tags: 'chrome');
+ test("runs those tests on Node", () async {
+ var pub = await runPubServe();
+ var test = await runTest([_pubServeArg, '-p', 'node']);
+ expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+ await test.shouldExit(0);
+ await pub.kill();
+ }, tags: 'node');
+
test("runs those tests on content shell", () async {
var pub = await runPubServe();
var test = await runTest([_pubServeArg, '-p', 'content-shell']);
@@ -174,6 +182,26 @@
}, tags: 'content-shell');
});
+ test(
+ "gracefully handles pub serve running on the wrong directory for Node "
+ "tests", () async {
+ await d.dir("web").create();
+
+ var pub = await runPubServe(args: ['web']);
+ var test = await runTest([_pubServeArg, '-p', 'node']);
+ expect(
+ test.stdout,
+ containsInOrder([
+ '-1: compiling ${p.join("test", "my_test.dart")} [E]',
+ 'Failed to load "${p.join("test", "my_test.dart")}":',
+ '404 Not Found',
+ 'Make sure "pub serve" is serving the test/ directory.'
+ ]));
+ await test.shouldExit(1);
+
+ await pub.kill();
+ }, tags: 'node');
+
test("gracefully handles unconfigured transformers", () async {
await d
.file(
@@ -269,32 +297,64 @@
.create();
});
- test("dartifies stack traces for JS-compiled tests by default", () async {
- var pub = await runPubServe();
- var test =
- await runTest([_pubServeArg, '-p', 'chrome', '--verbose-trace']);
- expect(
- test.stdout,
- containsInOrder(
- [" main.<fn>", "package:test", "dart:async/zone.dart"]));
- await test.shouldExit(1);
- pub.kill();
- }, tags: 'chrome');
+ group("dartifies stack traces for JS-compiled tests by default", () {
+ test("on a browser", () async {
+ var pub = await runPubServe();
+ var test =
+ await runTest([_pubServeArg, '-p', 'chrome', '--verbose-trace']);
+ expect(
+ test.stdout,
+ containsInOrder(
+ [" main.<fn>", "package:test", "dart:async/zone.dart"]));
+ await test.shouldExit(1);
+ pub.kill();
+ }, tags: 'chrome');
- test("doesn't dartify stack traces for JS-compiled tests with --js-trace",
- () async {
- var pub = await runPubServe();
- var test = await runTest(
- [_pubServeArg, '-p', 'chrome', '--js-trace', '--verbose-trace']);
+ test("on Node", () async {
+ var pub = await runPubServe();
+ var test =
+ await runTest([_pubServeArg, '-p', 'node', '--verbose-trace']);
+ expect(
+ test.stdout,
+ containsInOrder(
+ [" main.<fn>", "package:test", "dart:async/zone.dart"]));
+ await test.shouldExit(1);
+ pub.kill();
+ }, tags: 'node');
+ });
- expect(test.stdoutStream(), neverEmits(endsWith(" main.<fn>")));
- expect(test.stdoutStream(), neverEmits(contains("package:test")));
- expect(test.stdoutStream(), neverEmits(contains("dart:async/zone.dart")));
- expect(test.stdout, emitsThrough(contains("-1: Some tests failed.")));
- await test.shouldExit(1);
+ group("doesn't dartify stack traces for JS-compiled tests with --js-trace",
+ () {
+ test("on a browser", () async {
+ var pub = await runPubServe();
+ var test = await runTest(
+ [_pubServeArg, '-p', 'chrome', '--js-trace', '--verbose-trace']);
- await pub.kill();
- }, tags: 'chrome');
+ expect(test.stdoutStream(), neverEmits(endsWith(" main.<fn>")));
+ expect(test.stdoutStream(), neverEmits(contains("package:test")));
+ expect(
+ test.stdoutStream(), neverEmits(contains("dart:async/zone.dart")));
+ expect(test.stdout, emitsThrough(contains("-1: Some tests failed.")));
+ await test.shouldExit(1);
+
+ await pub.kill();
+ }, tags: 'chrome');
+
+ test("on Node", () async {
+ var pub = await runPubServe();
+ var test = await runTest(
+ [_pubServeArg, '-p', 'node', '--js-trace', '--verbose-trace']);
+
+ expect(test.stdoutStream(), neverEmits(endsWith(" main.<fn>")));
+ expect(test.stdoutStream(), neverEmits(contains("package:test")));
+ expect(
+ test.stdoutStream(), neverEmits(contains("dart:async/zone.dart")));
+ expect(test.stdout, emitsThrough(contains("-1: Some tests failed.")));
+ await test.shouldExit(1);
+
+ await pub.kill();
+ }, tags: 'node');
+ });
});
test("gracefully handles pub serve not running for VM tests", () async {
@@ -329,6 +389,24 @@
await test.shouldExit(1);
}, tags: 'chrome');
+ test("gracefully handles pub serve not running for Node tests", () async {
+ var test = await runTest(['--pub-serve=54321', '-p', 'node']);
+ var message = Platform.isWindows
+ ? 'The remote computer refused the network connection.'
+ : 'Connection refused (errno ';
+
+ expect(
+ test.stdout,
+ containsInOrder([
+ '-1: compiling ${p.join("test", "my_test.dart")} [E]',
+ 'Failed to load "${p.join("test", "my_test.dart")}":',
+ 'Error getting http://localhost:54321/my_test.dart.node_test.dart.js:'
+ ' $message',
+ 'Make sure "pub serve" is running.'
+ ]));
+ await test.shouldExit(1);
+ }, tags: 'node');
+
test("gracefully handles a test file not being in test/", () async {
new File(p.join(d.sandbox, 'test/my_test.dart'))
.copySync(p.join(d.sandbox, 'my_test.dart'));
diff --git a/test/runner/runner_test.dart b/test/runner/runner_test.dart
index 085b08e..ed69cb4 100644
--- a/test/runner/runner_test.dart
+++ b/test/runner/runner_test.dart
@@ -54,7 +54,7 @@
"[vm (default), dartium, content-shell, chrome, phantomjs, firefox" +
(Platform.isMacOS ? ", safari" : "") +
(Platform.isWindows ? ", ie" : "") +
- "]";
+ ", node]";
final _usage = """
Usage: pub run test [files or directories...]