diff --git a/.gitignore b/.gitignore index 9a076ca277..a144cd77ce 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ .settings/ build/ doc/ +lcov.info packages pub.dartlang.org/ testing/test_package/doc diff --git a/.travis.yml b/.travis.yml index 4e638d3690..f74c7551e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,18 @@ language: dart sudo: false dart: - - stable + #- stable - "dev/raw/latest" env: - - DARTDOC_BOT=flutter - - DARTDOC_BOT=sdk-analyzer + #- DARTDOC_BOT=sdk-analyzer - DARTDOC_BOT=main - - DARTDOC_BOT=packages - - DARTDOC_BOT=sdk-docs + #- DARTDOC_BOT=flutter + #- DARTDOC_BOT=packages + #- DARTDOC_BOT=sdk-docs script: ./tool/travis.sh os: - - osx + #- osx - linux install: diff --git a/README.md b/README.md index f42227486e..7225a024aa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # dartdoc [![Build Status](https://travis-ci.org/dart-lang/dartdoc.svg?branch=master)](https://travis-ci.org/dart-lang/dartdoc) +[![Coverage Status](https://coveralls.io/repos/github/dart-lang/dartdoc/badge.svg?branch=master)](https://coveralls.io/github/dart-lang/dartdoc?branch=master) + Use `dartdoc` to generate HTML documentaton for your Dart package. diff --git a/bin/dartdoc.dart b/bin/dartdoc.dart index 2b70ffab9d..3626038747 100644 --- a/bin/dartdoc.dart +++ b/bin/dartdoc.dart @@ -38,7 +38,7 @@ Future> createDartdocProgramOptions() async { /// Analyzes Dart files and generates a representation of included libraries, /// classes, and members. Uses the current directory to look for libraries. -void main(List arguments) async { +Future main(List arguments) async { DartdocOptionSet optionSet = await DartdocOptionSet.fromOptionGenerators('dartdoc', [ createDartdocOptions, @@ -52,17 +52,27 @@ void main(List arguments) async { } on FormatException catch (e) { stderr.writeln(' fatal error: ${e.message}'); stderr.writeln(''); - _printUsageAndExit(optionSet.argParser, exitCode: 64); + _printUsage(optionSet.argParser); + // Do not use exit() as this bypasses --pause-isolates-on-exit + // TODO(jcollins-g): use exit once dart-lang/sdk#31747 is fixed. + exitCode = 64; + return; } on DartdocOptionError catch (e) { stderr.writeln(' fatal error: ${e.message}'); stderr.writeln(''); - _printUsageAndExit(optionSet.argParser, exitCode: 64); + _printUsage(optionSet.argParser); + exitCode = 64; + return; } if (optionSet['help'].valueAt(Directory.current)) { - _printHelpAndExit(optionSet.argParser); + _printHelp(optionSet.argParser); + exitCode = 0; + return; } if (optionSet['version'].valueAt(Directory.current)) { - _printVersionAndExit(optionSet.argParser); + _printVersion(optionSet.argParser); + exitCode = 0; + return; } DartdocProgramOptionContext config = @@ -88,34 +98,37 @@ void main(List arguments) async { }, onError: (e, Chain chain) { if (e is DartdocFailure) { stderr.writeln('\nGeneration failed: ${e}.'); - exit(1); + exitCode = 1; + return; } else { stderr.writeln('\nGeneration failed: ${e}\n${chain.terse}'); - exit(255); + exitCode = 255; + return; } }, when: config.asyncStackTraces); } finally { // Clear out any cached tool snapshots and temporary directories. + // ignore: unawaited_futures SnapshotCache.instance.dispose(); + // ignore: unawaited_futures ToolTempFileTracker.instance.dispose(); } + exitCode = 0; + return; } /// Print help if we are passed the help option. -void _printHelpAndExit(ArgParser parser, {int exitCode: 0}) { +void _printHelp(ArgParser parser) { print('Generate HTML documentation for Dart libraries.\n'); - _printUsageAndExit(parser, exitCode: exitCode); } /// Print usage information on invalid command lines. -void _printUsageAndExit(ArgParser parser, {int exitCode: 0}) { +void _printUsage(ArgParser parser) { print('Usage: dartdoc [OPTIONS]\n'); print(parser.usage); - exit(exitCode); } /// Print version information. -void _printVersionAndExit(ArgParser parser) { +void _printVersion(ArgParser parser) { print('dartdoc version: ${dartdocVersion}'); - exit(exitCode); } diff --git a/lib/src/io_utils.dart b/lib/src/io_utils.dart index 79fc42ad75..507bd0e612 100644 --- a/lib/src/io_utils.dart +++ b/lib/src/io_utils.dart @@ -9,6 +9,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:dartdoc/src/tuple.dart'; import 'package:path/path.dart' as pathLib; /// Return a resolved path including the home directory in place of tilde @@ -116,6 +117,90 @@ class MultiFutureTracker { Future wait() async => await _waitUntil(0); } +/// Keeps track of coverage data automatically for any processes run by this +/// [CoverageSubprocessLauncher]. Requires that these be dart processes. +class CoverageSubprocessLauncher extends SubprocessLauncher { + CoverageSubprocessLauncher(String context, [Map environment]) + : super(context, environment); + + static int nextObservatoryPort = 9292; + + /// Set this to true to enable coverage runs. + static bool coverageEnabled = false; + + /// A list of all coverage results picked up by all launchers. + static List>>> coverageResults = []; + + static Directory _tempDir; + static Directory get tempDir => + _tempDir ??= Directory.systemTemp.createTempSync('dartdoc_coverage_data'); + + int _observatoryPort; + // TODO(jcollins-g): use ephemeral ports + int get observatoryPort => _observatoryPort ??= nextObservatoryPort++; + + String _outCoverageFilename; + String get outCoverageFilename => _outCoverageFilename ??= + pathLib.join(tempDir.path, 'dart-cov-0-${observatoryPort}.json'); + + /// Call once all coverage runs have been generated by calling runStreamed + /// on all [CoverageSubprocessLaunchers]. + static Future generateCoverageToFile(File outputFile) async { + if (!coverageEnabled) return Future.value(null); + var currentCoverageResults = coverageResults; + coverageResults = []; + var launcher = SubprocessLauncher('format_coverage'); + + /// Wait for all coverage runs to finish. + await Future.wait(currentCoverageResults.map((t) => t.item2)); + + return launcher.runStreamed( + Platform.executable, + [ + 'tool/format_coverage.dart', + '--lcov', + '-v', + '-b', '.', + '--packages=.packages', + '--sdk-root=${pathLib.canonicalize(pathLib.join(pathLib.dirname(Platform.executable), '..'))}', + '--out=${pathLib.canonicalize(outputFile.path)}', + '--report-on=bin,lib', + '-i', tempDir.path, + ]); + } + + @override + Future> runStreamed(String executable, List arguments, + {String workingDirectory}) { + assert(executable == Platform.executable, + 'Must use dart executable for tracking coverage'); + + if (coverageEnabled) { + arguments = [ + '--enable-vm-service=${observatoryPort}', + '--pause-isolates-on-exit' + ]..addAll(arguments); + } + + Future> results = super + .runStreamed(executable, arguments, workingDirectory: workingDirectory); + + if (coverageEnabled) { + coverageResults.add(new Tuple2( + outCoverageFilename, + super.runStreamed('pub', [ + 'run', + 'coverage:collect_coverage', + '--wait-paused', + '--resume-isolates', + '--port=${observatoryPort}', + '--out=${outCoverageFilename}', + ]))); + } + return results; + } +} + class SubprocessLauncher { final String context; final Map environment; diff --git a/pubspec.yaml b/pubspec.yaml index a5e7482b67..0e14685774 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,6 +34,7 @@ dev_dependencies: build: ^1.0.1 build_runner: ^1.0.0 build_version: ^1.0.0 + coverage: any dhttpd: ^3.0.0 glob: ^1.1.5 grinder: ^0.8.2 diff --git a/tool/format_coverage.dart b/tool/format_coverage.dart new file mode 100644 index 0000000000..4e6512907f --- /dev/null +++ b/tool/format_coverage.dart @@ -0,0 +1,232 @@ +// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file +// for details. 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:io'; + +import 'package:args/args.dart'; +import 'package:coverage/coverage.dart'; +import 'package:path/path.dart' as p; + +/// [Environment] stores gathered arguments information. +class Environment { + String sdkRoot; + String pkgRoot; + String packagesPath; + String baseDirectory; + String input; + IOSink output; + List reportOn; + String bazelWorkspace; + bool bazel; + int workers; + bool prettyPrint; + bool lcov; + bool expectMarkers; + bool verbose; +} + +Future main(List arguments) async { + final env = parseArgs(arguments); + + List files = filesToProcess(env.input); + if (env.verbose) { + print('Environment:'); + print(' # files: ${files.length}'); + print(' # workers: ${env.workers}'); + print(' sdk-root: ${env.sdkRoot}'); + print(' package-root: ${env.pkgRoot}'); + print(' package-spec: ${env.packagesPath}'); + print(' report-on: ${env.reportOn}'); + } + + var clock = new Stopwatch()..start(); + var hitmap = await parseCoverage(files, env.workers); + + // All workers are done. Process the data. + if (env.verbose) { + print('Done creating global hitmap. Took ${clock.elapsedMilliseconds} ms.'); + } + + String output; + var resolver = env.bazel + ? new BazelResolver(workspacePath: env.bazelWorkspace) + : new Resolver( + packagesPath: env.packagesPath, + packageRoot: env.pkgRoot, + sdkRoot: env.sdkRoot); + var loader = new Loader(); + if (env.prettyPrint) { + output = + await new PrettyPrintFormatter(resolver, loader, reportOn: env.reportOn) + .format(hitmap); + } else { + assert(env.lcov); + output = await new LcovFormatter(resolver, + reportOn: env.reportOn, basePath: env.baseDirectory) + .format(hitmap); + } + + env.output.write(output); + await env.output.flush(); + if (env.verbose) { + print('Done flushing output. Took ${clock.elapsedMilliseconds} ms.'); + } + + if (env.verbose) { + if (resolver.failed.length > 0) { + print('Failed to resolve:'); + for (String error in resolver.failed.toSet()) { + print(' $error'); + } + } + if (loader.failed.length > 0) { + print('Failed to load:'); + for (String error in loader.failed.toSet()) { + print(' $error'); + } + } + } + await env.output.close(); +} + +/// Checks the validity of the provided arguments. Does not initialize actual +/// processing. +Environment parseArgs(List arguments) { + final env = new Environment(); + var parser = new ArgParser(); + + parser.addOption('sdk-root', abbr: 's', help: 'path to the SDK root'); + parser.addOption('package-root', abbr: 'p', help: 'path to the package root'); + parser.addOption('packages', help: 'path to the package spec file'); + parser.addOption('in', abbr: 'i', help: 'input(s): may be file or directory'); + parser.addOption('out', + abbr: 'o', defaultsTo: 'stdout', help: 'output: may be file or stdout'); + parser.addMultiOption('report-on', + help: 'which directories or files to report coverage on'); + parser.addOption('workers', + abbr: 'j', defaultsTo: '1', help: 'number of workers'); + parser.addOption('bazel-workspace', + defaultsTo: '', help: 'Bazel workspace directory'); + parser.addOption('base-directory', + abbr: 'b', + help: 'the base directory relative to which source paths are output'); + parser.addFlag('bazel', + defaultsTo: false, help: 'use Bazel-style path resolution'); + parser.addFlag('pretty-print', + abbr: 'r', + negatable: false, + help: 'convert coverage data to pretty print format'); + parser.addFlag('lcov', + abbr: 'l', + negatable: false, + help: 'convert coverage data to lcov format'); + parser.addFlag('verbose', + abbr: 'v', negatable: false, help: 'verbose output'); + parser.addFlag('help', abbr: 'h', negatable: false, help: 'show this help'); + + var args = parser.parse(arguments); + + void printUsage() { + print('Usage: dart format_coverage.dart [OPTION...]\n'); + print(parser.usage); + } + + void fail(String msg) { + print('\n$msg\n'); + printUsage(); + exit(1); + } + + if (args['help']) { + printUsage(); + exit(0); + } + + env.sdkRoot = args['sdk-root']; + if (env.sdkRoot != null) { + env.sdkRoot = p.normalize(p.join(p.absolute(env.sdkRoot), 'lib')); + if (!FileSystemEntity.isDirectorySync(env.sdkRoot)) { + fail('Provided SDK root "${args["sdk-root"]}" is not a valid SDK ' + 'top-level directory'); + } + } + + if (args['package-root'] != null && args['packages'] != null) { + fail('Only one of --package-root or --packages may be specified.'); + } + + env.packagesPath = args['packages']; + if (env.packagesPath != null) { + if (!FileSystemEntity.isFileSync(env.packagesPath)) { + fail('Package spec "${args["packages"]}" not found, or not a file.'); + } + } + + env.pkgRoot = args['package-root']; + if (env.pkgRoot != null) { + env.pkgRoot = p.absolute(p.normalize(args['package-root'])); + if (!FileSystemEntity.isDirectorySync(env.pkgRoot)) { + fail('Package root "${args["package-root"]}" is not a directory.'); + } + } + + if (args['in'] == null) fail('No input files given.'); + env.input = p.absolute(p.normalize(args['in'])); + if (!FileSystemEntity.isDirectorySync(env.input) && + !FileSystemEntity.isFileSync(env.input)) { + fail('Provided input "${args["in"]}" is neither a directory nor a file.'); + } + + if (args['out'] == 'stdout') { + env.output = stdout; + } else { + var outpath = p.absolute(p.normalize(args['out'])); + var outfile = new File(outpath)..createSync(recursive: true); + env.output = outfile.openWrite(); + } + + env.reportOn = args['report-on'].isNotEmpty ? args['report-on'] : null; + + env.bazel = args['bazel']; + env.bazelWorkspace = args['bazel-workspace']; + if (env.bazelWorkspace.isNotEmpty && !env.bazel) { + stderr.writeln('warning: ignoring --bazel-workspace: --bazel not set'); + } + + if (args['base-directory'] != null) { + env.baseDirectory = p.absolute(args['base-directory']); + } + + env.lcov = args['lcov']; + if (args['pretty-print'] && env.lcov) { + fail('Choose one of pretty-print or lcov output'); + } + // Use pretty-print either explicitly or by default. + env.prettyPrint = !env.lcov; + + try { + env.workers = int.parse('${args["workers"]}'); + } catch (e) { + fail('Invalid worker count: $e'); + } + + env.verbose = args['verbose']; + return env; +} + +/// Given an absolute path absPath, this function returns a [List] of files +/// are contained by it if it is a directory, or a [List] containing the file if +/// it is a file. +List filesToProcess(String absPath) { + var filePattern = new RegExp(r'^dart-cov-\d+-\d+.json$'); + if (FileSystemEntity.isDirectorySync(absPath)) { + return new Directory(absPath) + .listSync(recursive: true) + .whereType() + .where((e) => filePattern.hasMatch(p.basename(e.path))) + .toList(); + } + return [new File(absPath)]; +} diff --git a/tool/grind.dart b/tool/grind.dart index 614bc94a5b..9f2546e6ce 100644 --- a/tool/grind.dart +++ b/tool/grind.dart @@ -852,18 +852,20 @@ List get testFiles => new Directory('test') Future testDart2() async { List parameters = ['--enable-asserts']; + CoverageSubprocessLauncher.coverageEnabled = + Platform.environment.containsKey('COVERAGE_TOKEN'); for (File dartFile in testFiles) { - await testFutures.addFutureFromClosure(() => - new SubprocessLauncher('dart2-${pathLib.basename(dartFile.path)}') - .runStreamed( - Platform.resolvedExecutable, - [] - ..addAll(parameters) - ..add(dartFile.path))); + await testFutures.addFutureFromClosure(() => new CoverageSubprocessLauncher( + 'dart2-${pathLib.basename(dartFile.path)}') + .runStreamed( + Platform.resolvedExecutable, + [] + ..addAll(parameters) + ..add(dartFile.path))); } for (File dartFile in binFiles) { - await testFutures.addFutureFromClosure(() => new SubprocessLauncher( + await testFutures.addFutureFromClosure(() => new CoverageSubprocessLauncher( 'dart2-bin-${pathLib.basename(dartFile.path)}-help') .runStreamed( Platform.resolvedExecutable, @@ -872,6 +874,9 @@ Future testDart2() async { ..add(dartFile.path) ..add('--help'))); } + + return await CoverageSubprocessLauncher.generateCoverageToFile( + new File('lcov.info')); } @Task('Generate docs for dartdoc') diff --git a/tool/install_travis.sh b/tool/install_travis.sh index c3bdc15b8e..91ff42c2e7 100755 --- a/tool/install_travis.sh +++ b/tool/install_travis.sh @@ -8,7 +8,10 @@ set -x if uname | grep -q Linux ; then + sudo apt-get update sudo apt-get install -y gdb + sudo gem install coveralls-lcov + coveralls-lcov --help fi exit 0 diff --git a/tool/travis.sh b/tool/travis.sh index 4bdc78b734..c26fcef0e6 100755 --- a/tool/travis.sh +++ b/tool/travis.sh @@ -9,6 +9,7 @@ set -ex # add globally activated packages to the path export PATH="$PATH":"~/.pub-cache/bin" +DART_VERSION=`dart --version 2>&1 | awk '{print $4}'` if [ "$DARTDOC_BOT" = "sdk-docs" ]; then # Build the SDK docs @@ -22,7 +23,6 @@ elif [ "$DARTDOC_BOT" = "flutter" ]; then pub run grinder validate-flutter-docs elif [ "$DARTDOC_BOT" = "packages" ]; then echo "Running packages dartdoc bot" - DART_VERSION=`dart --version 2>&1 | awk '{print $4}'` if [ ${DART_VERSION} != 2.0.0 ] ; then PACKAGE_NAME=angular PACKAGE_VERSION=">=5.1.0" DARTDOC_PARAMS="--include=angular,angular.security" pub run grinder build-pub-package else @@ -34,8 +34,13 @@ elif [ "$DARTDOC_BOT" = "packages" ]; then PACKAGE_NAME=shelf_exception_handler PACKAGE_VERSION=">=0.2.0" pub run grinder build-pub-package elif [ "$DARTDOC_BOT" = "sdk-analyzer" ]; then echo "Running main dartdoc bot against the SDK analyzer" + unset COVERAGE_TOKEN DARTDOC_GRIND_STEP=buildbot-no-publish pub run grinder test-with-analyzer-sdk else echo "Running main dartdoc bot" pub run grinder buildbot + if [ -n "$COVERAGE_TOKEN" ] && [ "${DART_VERSION}" != "2.1.0" ] && uname | grep -q Linux ; then + # Only attempt to upload coverage data for dev builds. + coveralls-lcov --repo-token="${COVERAGE_TOKEN}" lcov.info + fi fi