tool_coverage.dart 6.68 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
6
import 'dart:developer';
7
import 'dart:io';
8
import 'dart:isolate';
9

10 11
import 'package:async/async.dart';
import 'package:coverage/coverage.dart';
12
import 'package:flutter_tools/src/base/common.dart';
13
import 'package:flutter_tools/src/context_runner.dart';
14
import 'package:flutter_tools/src/test/test_wrapper.dart';
15
import 'package:path/path.dart' as path;
16 17
import 'package:stream_channel/isolate_channel.dart';
import 'package:stream_channel/stream_channel.dart';
18
import 'package:vm_service_client/vm_service_client.dart'; // ignore: deprecated_member_use
19 20 21 22 23
import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/runner_suite.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/plugin/platform_helpers.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/environment.dart'; // ignore: implementation_imports
24 25
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/test/coverage_collector.dart';
26 27

/// Generates an lcov report for the flutter tool unit tests.
28 29 30
///
/// Example invocation:
///
31
///     dart tool/tool_coverage.dart
32
Future<void> main(List<String> arguments) async {
33 34
  return runInContext(() async {
    final VMPlatform vmPlatform = VMPlatform();
35 36
    const TestWrapper test = TestWrapper();
    test.registerPlatformPlugin(
37 38
      <Runtime>[Runtime.vm],
      () => vmPlatform,
39
    );
40 41 42 43 44 45 46 47 48 49 50 51
    if (arguments.isEmpty) {
      arguments = <String>[
        path.join('test', 'general.shard'),
        path.join('test', 'commands.shard', 'hermetic'),
      ];
    }
    await test.main(<String>[
      '--no-color',
      '-r', 'compact',
      '-j', '1',
      ...arguments
    ]);
52
    exit(exitCode);
53 54 55
  });
}

56 57 58
/// A platform that loads tests in isolates spawned within this Dart process.
class VMPlatform extends PlatformPlugin {
  final CoverageCollector coverageCollector = CoverageCollector(
59
    libraryPredicate: (String libraryName) => libraryName.contains(FlutterProject.current().manifest.appName),
60
  );
61
  final Map<String, Future<void>> _pending = <String, Future<void>>{};
62
  final String precompiledPath = path.join('.dart_tool', 'build', 'generated', 'flutter_tools');
63

64
  @override
65
  StreamChannel<void> loadChannel(String codePath, SuitePlatform platform) =>
66
      throw UnimplementedError();
67

68
  @override
69 70 71 72 73 74
  Future<RunnerSuite> load(
    String codePath,
    SuitePlatform platform,
    SuiteConfiguration suiteConfig,
    Object message,
  ) async {
75 76 77
    final ReceivePort receivePort = ReceivePort();
    Isolate isolate;
    try {
78
      isolate = await _spawnIsolate(codePath, receivePort.sendPort);
79 80 81
    } catch (error) {
      receivePort.close();
      rethrow;
82
    }
83 84 85 86
    final Completer<void> completer = Completer<void>();
    // When this is completed we remove it from the map of pending so we can
    // log the futures that get "stuck".
    unawaited(completer.future.whenComplete(() {
87
      _pending.remove(codePath);
88 89
    }));
    final ServiceProtocolInfo info = await Service.controlWebServer(enable: true);
90
    final StreamChannel<Object> channel = IsolateChannel<Object>.connectReceive(receivePort)
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
      .transformStream(StreamTransformer<Object, Object>.fromHandlers(
        handleDone: (EventSink<Object> sink) async {
          try {
            // this will throw if collection fails.
            await coverageCollector.collectCoverageIsolate(info.serverUri);
          } finally {
            isolate.kill(priority: Isolate.immediate);
            isolate = null;
            sink.close();
            completer.complete();
          }
        },
        handleError: (dynamic error, StackTrace stackTrace, EventSink<Object> sink) {
          isolate.kill(priority: Isolate.immediate);
          isolate = null;
          sink.close();
          completer.complete();
        },
      ));
110

111 112
    VMEnvironment environment;
    final RunnerSuiteController controller = deserializeSuite(
113
      codePath,
114 115 116 117 118 119
      platform,
      suiteConfig,
      environment,
      channel,
      message,
    );
120
    _pending[codePath] = completer.future;
121
    return await controller.suite;
122 123
  }

124 125 126 127
  /// Spawns an isolate and passes it [message].
  ///
  /// This isolate connects an [IsolateChannel] to [message] and sends the
  /// serialized tests over that channel.
128 129
  Future<Isolate> _spawnIsolate(String codePath, SendPort message) async {
    String testPath = path.absolute(path.join(precompiledPath, codePath) + '.vm_test.dart');
130
    testPath = testPath.substring(0, testPath.length - '.dart'.length) + '.vm.app.dill';
131 132
    return await Isolate.spawnUri(path.toUri(testPath), <String>[], message,
      packageConfig: path.toUri('.packages'),
133 134
      checked: true,
    );
135 136
  }

137 138
  @override
  Future<void> close() async {
139
    try {
140
      await Future.wait(_pending.values).timeout(const Duration(minutes: 1));
141
    } on TimeoutException {
142 143 144
      // TODO(jonahwilliams): resolve whether there are any specific tests that
      // get stuck or if it is a general infra issue with how we are collecting
      // coverage.
145 146
      // Log tests that are "Stuck" waiting for coverage.
      print('The following tests timed out waiting for coverage:');
147
      print(_pending.keys.join(', '));
148
    }
149 150 151 152 153 154 155 156
    final String packagePath = Directory.current.path;
    final Resolver resolver = Resolver(packagesPath: '.packages');
    final Formatter formatter = LcovFormatter(resolver, reportOn: <String>[
      'lib',
    ], basePath: packagePath);
    final String result = await coverageCollector.finalizeCoverage(
      formatter: formatter,
    );
157
    final String outputLcovPath = path.join('coverage', 'lcov.info');
158 159 160
    File(outputLcovPath)
      ..createSync(recursive: true)
      ..writeAsStringSync(result);
161
  }
162
}
163

164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
class VMEnvironment implements Environment {
  VMEnvironment(this.observatoryUrl, this._isolate);

  @override
  final bool supportsDebugging = false;

  @override
  final Uri observatoryUrl;

  /// The VM service isolate object used to control this isolate.
  final VMIsolateRef _isolate;

  @override
  Uri get remoteDebuggerUrl => null;

  @override
  Stream<void> get onRestart => StreamController<dynamic>.broadcast().stream;

  @override
  CancelableOperation<void> displayPause() {
    final CancelableCompleter<dynamic> completer = CancelableCompleter<dynamic>(onCancel: () => _isolate.resume());

    completer.complete(_isolate.pause().then((dynamic _) => _isolate.onPauseOrResume
        .firstWhere((VMPauseEvent event) => event is VMResumeEvent)));

    return completer.operation;
190 191
  }
}