Commit f77983ba authored by Adam Barth's avatar Adam Barth

Adds experimental `pub run sky_tools:sky_test` command

This command uses package:test to run Dart tests with sky_shell.  For this to
work, we need https://github.com/dart-lang/test/tree/hacky-loader-hook to land.
We're also not smart enough to find sky_shell ourselves yet. Instead, we take
the path as input using an environment variable. Eventually, we'll be able to
get the sky_shell executable from package:sky_engine, but we don't yet ship
that executable.
parent f7fa689d
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/src/executable.dart' as executable;
import 'package:sky_tools/src/test/loader.dart' as loader;
main(List<String> args) {
loader.installHook();
return executable.main(args);
}
// Copyright 2015 The Chromium Authors. 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:convert';
import 'dart:io';
class JSONSocket {
JSONSocket(WebSocket socket)
: _socket = socket, stream = socket.map(JSON.decode).asBroadcastStream();
final WebSocket _socket;
final Stream stream;
void send(dynamic data) {
_socket.add(JSON.encode(data));
}
}
// Copyright 2015 The Chromium Authors. 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:convert';
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:sky_tools/src/test/json_socket.dart';
import 'package:sky_tools/src/test/remote_test.dart';
import 'package:stack_trace/stack_trace.dart';
import 'package:test/src/backend/metadata.dart';
import 'package:test/src/backend/test_platform.dart';
import 'package:test/src/runner/configuration.dart';
import 'package:test/src/runner/load_exception.dart';
import 'package:test/src/runner/loader.dart';
import 'package:test/src/runner/runner_suite.dart';
import 'package:test/src/runner/vm/environment.dart';
import 'package:test/src/util/io.dart';
import 'package:test/src/util/remote_exception.dart';
void installHook() {
Loader.loadVMFileHook = _loadVMFile;
}
final String _kSkyShell = Platform.environment['SKY_SHELL'];
const String _kHost = '127.0.0.1';
const String _kPath = '/runner';
class _ServerInfo {
final String url;
final Future<WebSocket> socket;
final HttpServer server;
_ServerInfo(this.server, this.url, this.socket);
}
Future<_ServerInfo> _createServer() async {
HttpServer server = await HttpServer.bind(_kHost, 0);
Completer<WebSocket> socket = new Completer<WebSocket>();
server.listen((HttpRequest request) {
if (request.uri.path == _kPath)
socket.complete(WebSocketTransformer.upgrade(request));
});
return new _ServerInfo(server, 'ws://$_kHost:${server.port}$_kPath', socket.future);
}
Future<Process> _startProcess(String path, { String packageRoot }) {
assert(_kSkyShell != null); // Please provide the path to the shell in the SKY_SHELL environment variable.
return Process.start(_kSkyShell, [
'--enable-checked-mode',
'--non-interactive',
'--package-root=$packageRoot',
path,
]);
}
Future<RunnerSuite> _loadVMFile(String path,
Metadata metadata,
Configuration config) async {
String encodedMetadata = Uri.encodeComponent(JSON.encode(
metadata.serialize()));
_ServerInfo info = await _createServer();
Directory tempDir = await Directory.systemTemp.createTemp(
'dart_test_listener');
File listenerFile = new File('${tempDir.path}/listener.dart');
await listenerFile.create();
await listenerFile.writeAsString('''
import 'dart:convert';
import 'package:test/src/backend/metadata.dart';
import 'package:sky_tools/src/test/remote_listener.dart';
import '${p.toUri(p.absolute(path))}' as test;
void main() {
String server = Uri.decodeComponent('${Uri.encodeComponent(info.url)}');
Metadata metadata = new Metadata.deserialize(
JSON.decode(Uri.decodeComponent('$encodedMetadata')));
RemoteListener.start(server, metadata, () => test.main);
}
''');
Process process = await _startProcess(listenerFile.path,
packageRoot: p.absolute(config.packageRoot));
JSONSocket socket = new JSONSocket(await info.socket);
await tempDir.delete(recursive: true);
void shutdown() {
process.kill();
info.server.close(force: true);
}
var completer = new Completer();
StreamSubscription subscription;
subscription = socket.stream.listen((response) {
if (response["type"] == "print") {
print(response["line"]);
} else if (response["type"] == "loadException") {
shutdown();
completer.completeError(
new LoadException(path, response["message"]),
new Trace.current());
} else if (response["type"] == "error") {
shutdown();
var asyncError = RemoteException.deserialize(response["error"]);
completer.completeError(
new LoadException(path, asyncError.error),
asyncError.stackTrace);
} else {
assert(response["type"] == "success");
subscription.cancel();
completer.complete(response["tests"]);
}
});
return new RunnerSuite(const VMEnvironment(),
(await completer.future).map((test) {
var testMetadata = new Metadata.deserialize(test['metadata']);
return new RemoteTest(test['name'], testMetadata, socket, test['index']);
}),
metadata: metadata,
path: path,
platform: TestPlatform.vm,
os: currentOS,
onClose: shutdown);
}
// Copyright 2015 The Chromium Authors. 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:convert';
import 'dart:io';
import 'dart:isolate';
import 'package:stack_trace/stack_trace.dart';
import 'package:test/src/backend/declarer.dart';
import 'package:test/src/backend/live_test.dart';
import 'package:test/src/backend/metadata.dart';
import 'package:test/src/backend/operating_system.dart';
import 'package:test/src/backend/suite.dart';
import 'package:test/src/backend/test_platform.dart';
import 'package:test/src/backend/test.dart';
import 'package:test/src/util/remote_exception.dart';
final OperatingSystem currentOS = (() {
var name = Platform.operatingSystem;
var os = OperatingSystem.findByIoName(name);
if (os != null) return os;
throw new UnsupportedError('Unsupported operating system "$name".');
})();
typedef AsyncFunction();
class RemoteListener {
final Suite _suite;
final WebSocket _socket;
LiveTest _liveTest;
static Future start(String server, Metadata metadata, Function getMain()) async {
WebSocket socket = await WebSocket.connect(server);
// Capture any top-level errors (mostly lazy syntax errors, since other are
// caught below) and report them to the parent isolate. We set errors
// non-fatal because otherwise they'll be double-printed.
var errorPort = new ReceivePort();
Isolate.current.setErrorsFatal(false);
Isolate.current.addErrorListener(errorPort.sendPort);
errorPort.listen((message) {
// Masquerade as an IsoalteSpawnException because that's what this would
// be if the error had been detected statically.
var error = new IsolateSpawnException(message[0]);
var stackTrace =
message[1] == null ? new Trace([]) : new Trace.parse(message[1]);
socket.add(JSON.encode({
"type": "error",
"error": RemoteException.serialize(error, stackTrace)
}));
});
var main;
try {
main = getMain();
} on NoSuchMethodError catch (_) {
_sendLoadException(socket, "No top-level main() function defined.");
return;
}
if (main is! Function) {
_sendLoadException(socket, "Top-level main getter is not a function.");
return;
} else if (main is! AsyncFunction) {
_sendLoadException(
socket, "Top-level main() function takes arguments.");
return;
}
var declarer = new Declarer();
try {
await runZoned(() => new Future.sync(main), zoneValues: {
#test.declarer: declarer
}, zoneSpecification: new ZoneSpecification(print: (_, __, ___, line) {
socket.add(JSON.encode({"type": "print", "line": line}));
}));
} catch (error, stackTrace) {
socket.add(JSON.encode({
"type": "error",
"error": RemoteException.serialize(error, stackTrace)
}));
return;
}
Suite suite = new Suite(declarer.tests,
platform: TestPlatform.vm, os: currentOS, metadata: metadata);
new RemoteListener._(suite, socket)._listen();
}
static void _sendLoadException(WebSocket socket, String message) {
socket.add(JSON.encode({"type": "loadException", "message": message}));
}
RemoteListener._(this._suite, this._socket);
void _send(data) {
_socket.add(JSON.encode(data));
}
void _listen() {
List tests = [];
for (var i = 0; i < _suite.tests.length; i++) {
Test test = _suite.tests[i];
tests.add({
"name": test.name,
"metadata": test.metadata.serialize(),
"index": i,
});
}
_send({"type": "success", "tests": tests});
_socket.listen(_handleCommand);
}
void _handleCommand(String data) {
var message = JSON.decode(data);
if (message['command'] == 'run') {
assert(_liveTest == null);
Test test = _suite.tests[message['index']];
_liveTest = test.load(_suite);
_liveTest.onStateChange.listen((state) {
_send({
"type": "state-change",
"status": state.status.name,
"result": state.result.name
});
});
_liveTest.onError.listen((asyncError) {
_send({
"type": "error",
"error": RemoteException.serialize(
asyncError.error, asyncError.stackTrace)
});
});
_liveTest.onPrint.listen((line) {
_send({"type": "print", "line": line});
});
_liveTest.run().then((_) {
_send({"type": "complete"});
_liveTest = null;
});
} else if (message['command'] == 'close') {
_liveTest.close();
_liveTest = null;
}
}
}
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/src/backend/live_test.dart';
import 'package:test/src/backend/live_test_controller.dart';
import 'package:test/src/backend/metadata.dart';
import 'package:test/src/backend/state.dart';
import 'package:test/src/backend/suite.dart';
import 'package:test/src/backend/test.dart';
import 'package:test/src/util/remote_exception.dart';
import 'package:sky_tools/src/test/json_socket.dart';
class RemoteTest implements Test {
final String name;
final Metadata metadata;
final JSONSocket _socket;
final int _index;
RemoteTest(this.name, this.metadata, this._socket, this._index);
LiveTest load(Suite suite) {
var controller;
var subscription;
controller = new LiveTestController(suite, this, () {
controller.setState(const State(Status.running, Result.success));
_socket.send({'command': 'run', 'index': _index});
subscription = _socket.stream.listen((message) {
if (message['type'] == 'error') {
var asyncError = RemoteException.deserialize(message['error']);
controller.addError(asyncError.error, asyncError.stackTrace);
} else if (message['type'] == 'state-change') {
controller.setState(
new State(
new Status.parse(message['status']),
new Result.parse(message['result'])));
} else if (message['type'] == 'print') {
controller.print(message['line']);
} else {
assert(message['type'] == 'complete');
subscription.cancel();
subscription = null;
controller.completer.complete();
}
});
}, () {
_socket.send({'command': 'close'});
if (subscription != null) {
subscription.cancel();
subscription = null;
}
});
return controller.liveTest;
}
Test change({String name, Metadata metadata}) {
if (name == name && metadata == this.metadata) return this;
if (name == null) name = this.name;
if (metadata == null) metadata = this.metadata;
return new RemoteTest(name, metadata, _socket, _index);
}
}
......@@ -14,8 +14,6 @@ dependencies:
shelf: ^0.6.2
shelf_route: ^0.13.4
shelf_static: ^0.2.3
dev_dependencies:
test: ^0.12.0
# Add the bin/sky_tools.dart script to the scripts pub installs.
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment