// 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 socket; final HttpServer server; _ServerInfo(this.server, this.url, this.socket); } Future<_ServerInfo> _createServer() async { HttpServer server = await HttpServer.bind(_kHost, 0); Completer socket = new Completer(); 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 _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 _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> completer = new Completer>(); Completer 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 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); } ); }