tool_coverage.dart 6.8 KB
Newer Older
1 2 3 4 5
// Copyright 2019 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';
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/context_runner.dart';
13
import 'package:path/path.dart' as path;
14 15 16 17 18
import 'package:pedantic/pedantic.dart';
import 'package:stream_channel/isolate_channel.dart';
import 'package:stream_channel/stream_channel.dart';
import 'package:test_core/src/runner/hack_register_platform.dart' as hack; // ignore: implementation_imports
import 'package:test_core/src/executable.dart' as test; // ignore: implementation_imports
19
import 'package:vm_service_client/vm_service_client.dart'; // ignore: deprecated_member_use
20 21 22 23 24 25 26
import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
import 'package:test_core/src/runner/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
27 28
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/test/coverage_collector.dart';
29 30

/// Generates an lcov report for the flutter tool unit tests.
31 32 33
///
/// Example invocation:
///
34
///     dart tool/tool_coverage.dart
35
Future<void> main(List<String> arguments) async {
36 37 38 39 40
  return runInContext(() async {
    final VMPlatform vmPlatform = VMPlatform();
    hack.registerPlatformPlugin(
      <Runtime>[Runtime.vm],
      () => vmPlatform,
41
    );
42 43
    await test.main(<String>['-x', 'no_coverage', '--no-color', '-r', 'compact', '-j', '1', ...arguments]);
    exit(exitCode);
44 45 46
  });
}

47 48 49
/// A platform that loads tests in isolates spawned within this Dart process.
class VMPlatform extends PlatformPlugin {
  final CoverageCollector coverageCollector = CoverageCollector(
50
    libraryPredicate: (String libraryName) => libraryName.contains(FlutterProject.current().manifest.appName),
51
  );
52
  final Map<String, Future<void>> _pending = <String, Future<void>>{};
53
  final String precompiledPath = path.join('.dart_tool', 'build', 'generated', 'flutter_tools');
54

55
  @override
56
  StreamChannel<void> loadChannel(String codePath, SuitePlatform platform) =>
57
      throw UnimplementedError();
58

59
  @override
60
  Future<RunnerSuite> load(String codePath, SuitePlatform platform,
61 62 63 64
      SuiteConfiguration suiteConfig, Object message) async {
    final ReceivePort receivePort = ReceivePort();
    Isolate isolate;
    try {
65
      isolate = await _spawnIsolate(codePath, receivePort.sendPort);
66 67 68
    } catch (error) {
      receivePort.close();
      rethrow;
69
    }
70 71 72 73
    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(() {
74
      _pending.remove(codePath);
75 76 77
    }));
    final ServiceProtocolInfo info = await Service.controlWebServer(enable: true);
    final dynamic channel = IsolateChannel<Object>.connectReceive(receivePort)
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
      .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();
        },
      ));
97

98 99
    VMEnvironment environment;
    final RunnerSuiteController controller = deserializeSuite(
100
      codePath,
101 102 103 104 105 106
      platform,
      suiteConfig,
      environment,
      channel,
      message,
    );
107
    _pending[codePath] = completer.future;
108
    return await controller.suite;
109 110
  }

111 112 113 114
  /// Spawns an isolate and passes it [message].
  ///
  /// This isolate connects an [IsolateChannel] to [message] and sends the
  /// serialized tests over that channel.
115 116
  Future<Isolate> _spawnIsolate(String codePath, SendPort message) async {
    String testPath = path.absolute(path.join(precompiledPath, codePath) + '.vm_test.dart');
117
    testPath = testPath.substring(0, testPath.length - '.dart'.length) + '.vm.app.dill';
118 119
    return await Isolate.spawnUri(path.toUri(testPath), <String>[], message,
      packageConfig: path.toUri('.packages'),
120 121
      checked: true,
    );
122 123
  }

124 125
  @override
  Future<void> close() async {
126
    try {
127
      await Future.wait(_pending.values).timeout(const Duration(minutes: 1));
128
    } on TimeoutException {
129 130 131
      // 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.
132 133
      // Log tests that are "Stuck" waiting for coverage.
      print('The following tests timed out waiting for coverage:');
134
      print(_pending.keys.join(', '));
135
    }
136 137 138 139 140 141 142 143 144
    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,
    );
    final String prefix = Platform.environment['SUBSHARD'] ?? '';
145
    final String outputLcovPath = path.join('coverage', '$prefix.lcov.info');
146 147 148
    File(outputLcovPath)
      ..createSync(recursive: true)
      ..writeAsStringSync(result);
149
  }
150
}
151

152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
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;
178 179
  }
}