loader.dart 6.84 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 11
import 'package:flutter_tools/src/test/json_socket.dart';
import 'package:flutter_tools/src/test/remote_test.dart';
12
import 'package:stack_trace/stack_trace.dart';
13
import 'package:test/src/backend/group.dart';
14 15 16
import 'package:test/src/backend/metadata.dart';
import 'package:test/src/backend/test_platform.dart';
import 'package:test/src/runner/configuration.dart';
17
import 'package:test/src/runner/hack_load_vm_file_hook.dart' as hack;
18 19 20 21 22 23 24
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() {
25
  hack.loadVMFileHook = _loadVMFile;
26 27 28 29 30 31
}

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

32 33
String shellPath;

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

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

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

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

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

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

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

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

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

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

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

157
  await cleanupTempDirectory();
158

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

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

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