tool_coverage.dart 5.64 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
// 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';
import 'dart:convert';
import 'dart:io';

import 'package:args/args.dart';
import 'package:flutter_tools/src/context_runner.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/test/coverage_collector.dart';
import 'package:pool/pool.dart';
import 'package:path/path.dart' as path;

final ArgParser argParser = ArgParser()
  ..addOption('output-html',
    defaultsTo: 'coverage/report.html',
    help: 'The output path for the genhtml report.'
  )
  ..addOption('output-lcov',
    defaultsTo: 'coverage/lcov.info',
    help: 'The output path for the lcov data.'
  )
  ..addOption('test-directory',
    defaultsTo: 'test/',
    help: 'The path to the test directory.'
  )
  ..addOption('packages',
    defaultsTo: '.packages',
    help: 'The path to the .packages file.'
  )
  ..addOption('genhtml',
    defaultsTo: 'genhtml',
    help: 'The genhtml executable.');


/// Generates an html coverage report for the flutter_tool.
///
/// Example invocation:
///
///    dart tool/tool_coverage.dart --packages=.packages --test-directory=test
Future<void> main(List<String> arguments) async {
  final ArgResults argResults = argParser.parse(arguments);
  await runInContext(() async {
    final CoverageCollector coverageCollector = CoverageCollector(
47
      flutterProject: FlutterProject.current(),
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
    );
    /// A temp directory to create synthetic test files in.
    final Directory tempDirectory = Directory.systemTemp.createTempSync('_flutter_coverage')
      ..createSync();
    final String flutterRoot = File(Platform.script.toFilePath()).parent.parent.parent.parent.path;
    await ToolCoverageRunner(tempDirectory, coverageCollector, flutterRoot, argResults).collectCoverage();
  });
}

class ToolCoverageRunner {
  ToolCoverageRunner(
    this.tempDirectory,
    this.coverageCollector,
    this.flutterRoot,
    this.argResults,
  );

  final ArgResults argResults;
  final Pool pool = Pool(Platform.numberOfProcessors);
  final Directory tempDirectory;
  final CoverageCollector coverageCollector;
  final String flutterRoot;

  Future<void> collectCoverage() async {
    final List<Future<void>> pending = <Future<void>>[];

    final Directory testDirectory = Directory(argResults['test-directory']);
    final List<FileSystemEntity> fileSystemEntities = testDirectory.listSync(recursive: true);
    for (FileSystemEntity fileSystemEntity in fileSystemEntities) {
      if (!fileSystemEntity.path.endsWith('_test.dart')) {
        continue;
      }
      pending.add(_runTest(fileSystemEntity));
    }
    await Future.wait(pending);

    final String lcovData = await coverageCollector.finalizeCoverage();
    final String outputLcovPath = argResults['output-lcov'];
    final String outputHtmlPath = argResults['output-html'];
    final String genHtmlExecutable = argResults['genhtml'];
    File(outputLcovPath)
      ..createSync(recursive: true)
      ..writeAsStringSync(lcovData);
    await Process.run(genHtmlExecutable, <String>[outputLcovPath, '-o', outputHtmlPath], runInShell: true);
  }

  // Creates a synthetic test file to wrap the test main in a group invocation.
  // This will set up several fields used by the test methods on the context. Normally
  // this would be handled automatically by the test runner, but since we're executing
  // the files directly with dart we need to handle it manually.
  String _createTest(File testFile) {
    final File fakeTest = File(path.join(tempDirectory.path, testFile.path))
      ..createSync(recursive: true)
      ..writeAsStringSync('''
import "package:test/test.dart";
import "${path.absolute(testFile.path)}" as entrypoint;

void main() {
  group('', entrypoint.main);
}
''');
    return fakeTest.path;
  }

  Future<void> _runTest(File testFile) async {
    final PoolResource resource = await pool.request();
    final String testPath = _createTest(testFile);
    final int port = await _findPort();
    final Uri coverageUri = Uri.parse('http://127.0.0.1:$port');
    final Completer<void> completer = Completer<void>();
    final String packagesPath = argResults['packages'];
    final Process testProcess = await Process.start(
      Platform.resolvedExecutable,
      <String>[
        '--packages=$packagesPath',
        '--pause-isolates-on-exit',
        '--enable-asserts',
        '--enable-vm-service=${coverageUri.port}',
        testPath,
      ],
      runInShell: true,
      environment: <String, String>{
        'FLUTTER_ROOT': flutterRoot,
      }).timeout(const Duration(seconds: 30));
    testProcess.stdout
      .transform(utf8.decoder)
      .transform(const LineSplitter())
      .listen((String line) {
        print(line);
        if (line.contains('All tests passed') || line.contains('Some tests failed')) {
          completer.complete(null);
        }
      });
    try {
      await completer.future;
      await coverageCollector.collectCoverage(testProcess, coverageUri).timeout(const Duration(seconds: 30));
      testProcess?.kill();
    } on TimeoutException {
      print('Failed to collect coverage for ${testFile.path} after 30 seconds');
    } finally {
      resource.release();
    }
  }

  Future<int> _findPort() async {
    int port = 0;
    ServerSocket serverSocket;
    try {
      serverSocket = await ServerSocket.bind(InternetAddress.loopbackIPv4.address, 0);
      port = serverSocket.port;
    } catch (e) {
      // Failures are signaled by a return value of 0 from this function.
      print('_findPort failed: $e');
    }
    if (serverSocket != null) {
      await serverSocket.close();
    }
    return port;
  }
}