loader.dart 6.78 KB
Newer Older
1 2 3 4 5 6 7 8
// 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';

9
import 'package:path/path.dart' as path;
10
import 'package:stack_trace/stack_trace.dart';
11
import 'package:test/src/backend/group.dart';
12 13 14
import 'package:test/src/backend/metadata.dart';
import 'package:test/src/backend/test_platform.dart';
import 'package:test/src/runner/configuration.dart';
15
import 'package:test/src/runner/hack_load_vm_file_hook.dart' as hack;
16 17 18 19 20 21
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';

22 23 24
import 'json_socket.dart';
import 'remote_test.dart';

25
void installHook() {
26
  hack.loadVMFileHook = _loadVMFile;
27 28 29 30 31 32
}

final String _kSkyShell = Platform.environment['SKY_SHELL'];
const String _kHost = '127.0.0.1';
const String _kPath = '/runner';

33 34
String shellPath;

35
// Right now a bunch of our tests crash or assert after the tests have finished running.
36
// Mostly this is just because the test puts the framework in an inconsistent state with
37 38 39 40
// 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;

41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
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);
}

59
Future<Process> _startProcess(String mainPath, { String packageRoot }) {
60 61
  assert(shellPath != null || _kSkyShell != null); // Please provide the path to the shell in the SKY_SHELL environment variable.
  return Process.start(shellPath ?? _kSkyShell, [
62 63 64
    '--enable-checked-mode',
    '--non-interactive',
    '--package-root=$packageRoot',
65
    mainPath,
66 67 68
  ]);
}

69
Future<RunnerSuite> _loadVMFile(String mainPath,
70 71 72 73 74 75 76 77 78 79 80 81 82
                                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';
83
import 'package:flutter_tools/src/test/remote_listener.dart';
84

85
import '${path.toUri(path.absolute(mainPath))}' as test;
86 87 88 89 90 91 92 93 94

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);
}
''');

95
  Completer<Iterable<RemoteTest>> completer = new Completer<Iterable<RemoteTest>>();
Hixie's avatar
Hixie committed
96
  Completer<String> deathCompleter = new Completer();
97

98 99
  Process process = await _startProcess(
    listenerFile.path,
100
    packageRoot: path.absolute(config.packageRoot)
101
  );
102

103 104 105 106 107 108
  Future cleanupTempDirectory() async {
    if (tempDir == null)
      return;
    Directory dirToDelete = tempDir;
    tempDir = null;
    await dirToDelete.delete(recursive: true);
109 110
  }

111
  process.exitCode.then((int exitCode) async {
112 113 114 115 116 117 118 119 120 121
    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
122
            output += 'Segmentation fault in subprocess for: $mainPath\n';
123
            break;
124
          case -0x06: // ProcessSignal.SIGABRT
125
            output += 'Aborted while running: $mainPath\n';
126
            break;
127
          default:
128
            output += 'Unexpected exit code $exitCode from subprocess for: $mainPath\n';
129 130 131 132 133 134 135 136 137 138 139 140
        }
      }
      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(
141
          new LoadException(mainPath, output),
142 143 144 145 146 147
          new Trace.current()
        );
      } else {
        if (kExpectAllTestsToCloseCleanly && output != '')
          print('Unexpected failure after test claimed to pass:\n$output');
      }
Hixie's avatar
Hixie committed
148
      deathCompleter.complete(output);
149 150 151 152
    } 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");
153 154 155
    }
  });

Hixie's avatar
Hixie committed
156
  JSONSocket socket = new JSONSocket(await info.socket, deathCompleter.future);
157

158
  await cleanupTempDirectory();
159

160 161 162 163 164
  StreamSubscription subscription;
  subscription = socket.stream.listen((response) {
    if (response["type"] == "print") {
      print(response["line"]);
    } else if (response["type"] == "loadException") {
165
      process.kill(ProcessSignal.SIGTERM);
166
      completer.completeError(
167
          new LoadException(mainPath, response["message"]),
168 169
          new Trace.current());
    } else if (response["type"] == "error") {
170
      process.kill(ProcessSignal.SIGTERM);
171 172
      AsyncError asyncError = RemoteException.deserialize(response["error"]);
      completer.completeError(
173
          new LoadException(mainPath, asyncError.error),
174 175 176 177 178
          asyncError.stackTrace);
    } else {
      assert(response["type"] == "success");
      subscription.cancel();
      completer.complete(response["tests"].map((test) {
179 180
        var testMetadata = new Metadata.deserialize(test['metadata']);
        return new RemoteTest(test['name'], testMetadata, socket, test['index']);
181
      }));
182 183 184
    }
  });

185
  Iterable<RemoteTest> entries = await completer.future;
186 187 188 189

  return new RunnerSuite(
    const VMEnvironment(),
    new Group.root(entries, metadata: metadata),
190
    path: mainPath,
191 192
    platform: TestPlatform.vm,
    os: currentOS,
193
    onClose: () { process.kill(ProcessSignal.SIGTERM); }
194
  );
195
}