195 lines
6.8 KiB
Dart

// 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/group.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/hack_load_vm_file_hook.dart' as hack;
import 'package:test/src/runner/load_exception.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() {
hack.loadVMFileHook = _loadVMFile;
}
final String _kSkyShell = Platform.environment['SKY_SHELL'];
const String _kHost = '127.0.0.1';
const String _kPath = '/runner';
String shellPath;
// Right now a bunch of our tests crash or assert after the tests have finished running.
// Mostly this is just because the test puts the framework in an inconsistent state with
// a scheduled microtask that verifies that state. Eventually we should fix all these
// problems but for now we'll just paper over them.
const bool kExpectAllTestsToCloseCleanly = false;
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(shellPath != null || _kSkyShell != null); // Please provide the path to the shell in the SKY_SHELL environment variable.
return Process.start(shellPath ?? _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);
}
''');
Completer<Iterable<RemoteTest>> completer = new Completer<Iterable<RemoteTest>>();
Completer<String> deathCompleter = new Completer();
Process process = await _startProcess(
listenerFile.path,
packageRoot: p.absolute(config.packageRoot)
);
Future cleanupTempDirectory() async {
if (tempDir == null)
return;
Directory dirToDelete = tempDir;
tempDir = null;
await dirToDelete.delete(recursive: true);
}
process.exitCode.then((int exitCode) async {
try {
info.server.close(force: true);
await cleanupTempDirectory();
String output = '';
if (exitCode < 0) {
// Abnormal termination (high bit of signed 8-bit exitCode is set)
switch (exitCode) {
case -0x0f: // ProcessSignal.SIGTERM
break; // we probably killed it ourselves
case -0x0b: // ProcessSignal.SIGSEGV
output += 'Segmentation fault in subprocess for: $path\n';
break;
case -0x06: // ProcessSignal.SIGABRT
output += 'Aborted while running: $path\n';
break;
default:
output += 'Unexpected exit code $exitCode from subprocess for: $path\n';
}
}
String stdout = await process.stdout.transform(UTF8.decoder).join('\n');
String stderr = await process.stderr.transform(UTF8.decoder).join('\n');
if (stdout != '')
output += '\nstdout:\n$stdout';
if (stderr != '')
output += '\nstderr:\n$stderr';
if (!completer.isCompleted) {
if (output == '')
output = 'No output.';
completer.completeError(
new LoadException(path, output),
new Trace.current()
);
} else {
if (kExpectAllTestsToCloseCleanly && output != '')
print('Unexpected failure after test claimed to pass:\n$output');
}
deathCompleter.complete(output);
} catch (e) {
// Throwing inside this block causes all kinds of hard-to-debug issues
// like stack overflows and hangs. So catch everything just in case.
print("exception while handling subprocess termination: $e");
}
});
JSONSocket socket = new JSONSocket(await info.socket, deathCompleter.future);
await cleanupTempDirectory();
StreamSubscription subscription;
subscription = socket.stream.listen((response) {
if (response["type"] == "print") {
print(response["line"]);
} else if (response["type"] == "loadException") {
process.kill(ProcessSignal.SIGTERM);
completer.completeError(
new LoadException(path, response["message"]),
new Trace.current());
} else if (response["type"] == "error") {
process.kill(ProcessSignal.SIGTERM);
AsyncError 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"].map((test) {
var testMetadata = new Metadata.deserialize(test['metadata']);
return new RemoteTest(test['name'], testMetadata, socket, test['index']);
}));
}
});
Iterable<RemoteTest> entries = await completer.future;
return new RunnerSuite(
const VMEnvironment(),
new Group.root(entries, metadata: metadata),
path: path,
platform: TestPlatform.vm,
os: currentOS,
onClose: () { process.kill(ProcessSignal.SIGTERM); }
);
}