diff --git a/lib/src/io_utils.dart b/lib/src/io_utils.dart index b0d02225a6..207fb3dc8a 100644 --- a/lib/src/io_utils.dart +++ b/lib/src/io_utils.dart @@ -5,6 +5,8 @@ /// This is a helper library to make working with io easier. library dartdoc.io_utils; +import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as path; @@ -60,3 +62,158 @@ String getFileNameFor(String name) => final libraryNameRegexp = new RegExp('[.:]'); final partOfRegexp = new RegExp('part of '); final newLinePartOfRegexp = new RegExp('\npart of '); + +final RegExp quotables = new RegExp(r'[ "\r\n\$]'); + +class SubprocessLauncher { + final String context; + Map _environment; + + Map get environment => _environment; + + String get prefix => context.isNotEmpty ? '$context: ' : ''; + + // from flutter:dev/tools/dartdoc.dart, modified + static void _printStream(Stream> stream, Stdout output, + {String prefix: '', Iterable Function(String line) filter}) { + assert(prefix != null); + if (filter == null) filter = (line) => [line]; + stream + .transform(UTF8.decoder) + .transform(const LineSplitter()) + .expand(filter) + .listen((String line) { + if (line != null) { + output.write('$prefix$line'.trim()); + output.write('\n'); + } + }); + } + + SubprocessLauncher(this.context, [Map environment]) { + if (environment == null) this._environment = new Map(); + } + + /// A wrapper around start/await process.exitCode that will display the + /// output of the executable continuously and fail on non-zero exit codes. + /// It will also parse any valid JSON objects (one per line) it encounters + /// on stdout/stderr, and return them. Returns null if no JSON objects + /// were encountered. + /// + /// Makes running programs in grinder similar to set -ex for bash, even on + /// Windows (though some of the bashisms will no longer make sense). + /// TODO(jcollins-g): move this to grinder? + Future> runStreamed(String executable, List arguments, + {String workingDirectory}) async { + List jsonObjects; + + /// Allow us to pretend we didn't pass the JSON flag in to dartdoc by + /// printing what dartdoc would have printed without it, yet storing + /// json objects into [jsonObjects]. + Iterable jsonCallback(String line) { + Map result; + try { + result = json.decoder.convert(line); + } catch (FormatException) {} + if (result != null) { + if (jsonObjects == null) { + jsonObjects = new List(); + } + jsonObjects.add(result); + if (result.containsKey('message')) { + line = result['message']; + } else if (result.containsKey('data')) { + line = result['data']['text']; + } + } + return line.split('\n'); + } + + stderr.write('$prefix+ '); + if (workingDirectory != null) stderr.write('(cd "$workingDirectory" && '); + if (environment != null) { + stderr.write(environment.keys.map((String key) { + if (environment[key].contains(quotables)) { + return "$key='${environment[key]}'"; + } else { + return "$key=${environment[key]}"; + } + }).join(' ')); + stderr.write(' '); + } + stderr.write('$executable'); + if (arguments.isNotEmpty) { + for (String arg in arguments) { + if (arg.contains(quotables)) { + stderr.write(" '$arg'"); + } else { + stderr.write(" $arg"); + } + } + } + if (workingDirectory != null) stderr.write(')'); + stderr.write('\n'); + Process process = await Process.start(executable, arguments, + workingDirectory: workingDirectory, environment: environment); + + _printStream(process.stdout, stdout, prefix: prefix, filter: jsonCallback); + _printStream(process.stderr, stderr, prefix: prefix, filter: jsonCallback); + await process.exitCode; + + int exitCode = await process.exitCode; + if (exitCode != 0) { + throw new ProcessException(executable, arguments, + "SubprocessLauncher got non-zero exitCode", exitCode); + } + return jsonObjects; + } +} + +/// Output formatter for comparing warnings. +String printWarningDelta(String title, String dartdocOriginalBranch, + Map original, Map current) { + StringBuffer printBuffer = new StringBuffer(); + Set quantityChangedOuts = new Set(); + Set onlyOriginal = new Set(); + Set onlyCurrent = new Set(); + Set allKeys = + new Set.from([]..addAll(original.keys)..addAll(current.keys)); + + for (String key in allKeys) { + if (original.containsKey(key) && !current.containsKey(key)) { + onlyOriginal.add(key); + } else if (!original.containsKey(key) && current.containsKey(key)) { + onlyCurrent.add(key); + } else if (original.containsKey(key) && + current.containsKey(key) && + original[key] != current[key]) { + quantityChangedOuts.add(key); + } + } + + if (onlyOriginal.isNotEmpty) { + printBuffer.writeln( + '*** $title : ${onlyOriginal.length} warnings from original ($dartdocOriginalBranch) missing in current:'); + onlyOriginal.forEach((warning) => printBuffer.writeln(warning)); + } + if (onlyCurrent.isNotEmpty) { + printBuffer.writeln( + '*** $title : ${onlyCurrent.length} new warnings not in original ($dartdocOriginalBranch)'); + onlyCurrent.forEach((warning) => printBuffer.writeln(warning)); + } + if (quantityChangedOuts.isNotEmpty) { + printBuffer.writeln('*** $title : Identical warning quantity changed'); + for (String key in quantityChangedOuts) { + printBuffer.writeln( + "* Appeared ${original[key]} times in original ($dartdocOriginalBranch), now ${current[key]}:"); + printBuffer.writeln(key); + } + } + if (onlyOriginal.isEmpty && + onlyCurrent.isEmpty && + quantityChangedOuts.isEmpty) { + printBuffer.writeln( + '*** $title : No difference in warning output from original ($dartdocOriginalBranch)${allKeys.isEmpty ? "" : " (${allKeys.length} warnings found)"}'); + } + return printBuffer.toString(); +} diff --git a/test/io_utils_test.dart b/test/io_utils_test.dart index 5b0548c4ca..a550e249fe 100644 --- a/test/io_utils_test.dart +++ b/test/io_utils_test.dart @@ -17,4 +17,36 @@ void main() { expect(getFileNameFor('dartdoc.generator'), 'dartdoc-generator.html'); }); }); + + group('printWarningDelta', () { + Map original; + Map current; + setUp(() { + original = new Map.fromIterables( + ["originalwarning", "morewarning", "duplicateoriginalwarning"], + [1, 1, 2]); + current = new Map.fromIterables( + ["newwarning", "morewarning", "duplicateoriginalwarning"], [1, 1, 1]); + }); + + test('verify output of printWarningDelta', () { + expect( + printWarningDelta('Diff Title', 'oldbranch', original, current), + equals( + '*** Diff Title : 1 warnings from original (oldbranch) missing in current:\n' + 'originalwarning\n' + '*** Diff Title : 1 new warnings not in original (oldbranch)\n' + 'newwarning\n' + '*** Diff Title : Identical warning quantity changed\n' + '* Appeared 2 times in original (oldbranch), now 1:\n' + 'duplicateoriginalwarning\n')); + }); + + test('verify output when nothing changes', () { + expect( + printWarningDelta('Diff Title 2', 'oldbranch2', original, original), + equals( + '*** Diff Title 2 : No difference in warning output from original (oldbranch2) (3 warnings found)\n')); + }); + }); } diff --git a/tool/grind.dart b/tool/grind.dart index d84e372b72..f7278379d6 100644 --- a/tool/grind.dart +++ b/tool/grind.dart @@ -3,7 +3,6 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; -import 'dart:convert'; import 'dart:io' hide ProcessException; import 'package:dartdoc/src/io_utils.dart'; @@ -25,23 +24,19 @@ Directory get dartdocDocsDir => Directory get sdkDocsDir => tempdirsCache.memoized1(createTempSync, 'sdkdocs'); Directory get flutterDir => tempdirsCache.memoized1(createTempSync, 'flutter'); +/// Version of dartdoc we should use when making comparisons. +String get dartdocOriginalBranch { + String branch = 'master'; + if (Platform.environment.containsKey('DARTDOC_ORIGINAL')) { + branch = Platform.environment['DARTDOC_ORIGINAL']; + log('using branch/tag: $branch for comparison from \$DARTDOC_ORIGINAL'); + } + return branch; +} + final Directory flutterDirDevTools = new Directory(path.join(flutterDir.path, 'dev', 'tools')); -final RegExp quotables = new RegExp(r'[ "\r\n\$]'); -// from flutter:dev/tools/dartdoc.dart, modified -void _printStream(Stream> stream, Stdout output, - {String prefix: ''}) { - assert(prefix != null); - stream - .transform(UTF8.decoder) - .transform(const LineSplitter()) - .listen((String line) { - output.write('$prefix$line'.trim()); - output.write('\n'); - }); -} - /// Creates a throwaway pub cache and returns the environment variables /// necessary to use it. Map _createThrowawayPubCache() { @@ -57,66 +52,9 @@ Map _createThrowawayPubCache() { ]); } -class _SubprocessLauncher { - final String context; - Map _environment; - - Map get environment => _environment; - - String get prefix => context.isNotEmpty ? '$context: ' : ''; - - _SubprocessLauncher(this.context, [Map environment]) { - if (environment == null) this._environment = new Map(); - } - - /// A wrapper around start/await process.exitCode that will display the - /// output of the executable continuously and fail on non-zero exit codes. - /// Makes running programs in grinder similar to set -ex for bash, even on - /// Windows (though some of the bashisms will no longer make sense). - /// TODO(jcollins-g): move this to grinder? - Future runStreamed(String executable, List arguments, - {String workingDirectory}) async { - stderr.write('$prefix+ '); - if (workingDirectory != null) stderr.write('cd "$workingDirectory" && '); - if (environment != null) { - stderr.write(environment.keys.map((String key) { - if (environment[key].contains(quotables)) { - return "$key='${environment[key]}'"; - } else { - return "$key=${environment[key]}"; - } - }).join(' ')); - stderr.write(' '); - } - stderr.write('$executable'); - if (arguments.isNotEmpty) { - for (String arg in arguments) { - if (arg.contains(quotables)) { - stderr.write(" '$arg'"); - } else { - stderr.write(" $arg"); - } - } - } - if (workingDirectory != null) stderr.write(')'); - stderr.write('\n'); - Process process = await Process.start(executable, arguments, - workingDirectory: workingDirectory, environment: environment); - - _printStream(process.stdout, stdout, prefix: prefix); - _printStream(process.stderr, stderr, prefix: prefix); - await process.exitCode; - - int exitCode = await process.exitCode; - if (exitCode != 0) { - fail("exitCode: $exitCode"); - } - } -} - @Task('Analyze dartdoc to ensure there are no errors and warnings') analyze() async { - await new _SubprocessLauncher('analyze').runStreamed( + await new SubprocessLauncher('analyze').runStreamed( sdkBin('dartanalyzer'), [ '--fatal-warnings', @@ -135,22 +73,84 @@ buildbot() => null; @Task('Generate docs for the Dart SDK') Future buildSdkDocs() async { log('building SDK docs'); - var launcher = new _SubprocessLauncher('build-sdk-docs'); - await launcher.runStreamed(Platform.resolvedExecutable, [ - '--checked', - 'bin/dartdoc.dart', - '--output', - '${sdkDocsDir.path}', - '--sdk-docs', - '--show-progress' - ]); + await _buildSdkDocs( + sdkDocsDir.path, new Future.value(Directory.current.path)); +} + +/// Returns a map of warning texts to the number of times each has been seen. +Map jsonMessageIterableToWarnings(Iterable messageIterable) { + Map warningTexts = new Map(); + for (Map message in messageIterable) { + if (message.containsKey('level') && + message['level'] == 'WARNING' && + message.containsKey('data')) { + warningTexts.putIfAbsent(message['data']['text'], () => 0); + warningTexts[message['data']['text']]++; + } + } + return warningTexts; +} + +@Task('Display delta in SDK warnings') +Future compareSdkWarnings() async { + Directory originalDartdocSdkDocs = + Directory.systemTemp.createTempSync('dartdoc-comparison-sdkdocs'); + Future originalDartdoc = createComparisonDartdoc(); + Future currentDartdocSdkBuild = _buildSdkDocs( + sdkDocsDir.path, new Future.value(Directory.current.path), 'current'); + Future originalDartdocSdkBuild = + _buildSdkDocs(originalDartdocSdkDocs.path, originalDartdoc, 'original'); + Map currentDartdocWarnings = + jsonMessageIterableToWarnings(await currentDartdocSdkBuild); + Map originalDartdocWarnings = + jsonMessageIterableToWarnings(await originalDartdocSdkBuild); + + print(printWarningDelta('SDK docs', dartdocOriginalBranch, + originalDartdocWarnings, currentDartdocWarnings)); +} + +/// Helper function to create a clean version of dartdoc (based on the current +/// directory, assumed to be a git repository). Uses [dartdocOriginalBranch] +/// to checkout a branch or tag. +Future createComparisonDartdoc() async { + var launcher = new SubprocessLauncher('create-comparison-dartdoc'); + Directory dartdocClean = + Directory.systemTemp.createTempSync('dartdoc-comparison'); + await launcher + .runStreamed('git', ['clone', Directory.current.path, dartdocClean.path]); + await launcher.runStreamed('git', ['checkout', dartdocOriginalBranch], + workingDirectory: dartdocClean.path); + await launcher.runStreamed(sdkBin('pub'), ['get'], + workingDirectory: dartdocClean.path); + return dartdocClean.path; +} + +Future> _buildSdkDocs(String sdkDocsPath, Future futureCwd, + [String label]) async { + if (label == null) label = ''; + if (label != '') label = '-$label'; + var launcher = new SubprocessLauncher('build-sdk-docs$label'); + String cwd = await futureCwd; + await launcher.runStreamed(sdkBin('pub'), ['get'], workingDirectory: cwd); + return await launcher.runStreamed( + Platform.resolvedExecutable, + [ + '--checked', + 'bin/dartdoc.dart', + '--output', + '${sdkDocsDir.path}', + '--sdk-docs', + '--json', + '--show-progress', + ], + workingDirectory: cwd); } @Task('Serve generated SDK docs locally with dhttpd on port 8000') @Depends(buildSdkDocs) Future serveSdkDocs() async { log('launching dhttpd on port 8000 for SDK'); - var launcher = new _SubprocessLauncher('serve-sdk-docs'); + var launcher = new SubprocessLauncher('serve-sdk-docs'); await launcher.runStreamed(sdkBin('pub'), [ 'run', 'dhttpd', @@ -165,7 +165,7 @@ Future serveSdkDocs() async { @Depends(buildFlutterDocs) Future serveFlutterDocs() async { log('launching dhttpd on port 8001 for Flutter'); - var launcher = new _SubprocessLauncher('serve-flutter-docs'); + var launcher = new SubprocessLauncher('serve-flutter-docs'); await launcher.runStreamed(sdkBin('pub'), ['get']); await launcher.runStreamed(sdkBin('pub'), [ 'run', @@ -180,42 +180,47 @@ Future serveFlutterDocs() async { @Task('Build flutter docs') Future buildFlutterDocs() async { log('building flutter docs into: $flutterDir'); - var launcher = - new _SubprocessLauncher('build-flutter-docs', _createThrowawayPubCache()); + await _buildFlutterDocs(flutterDir.path); + String index = + new File(path.join(flutterDir.path, 'dev', 'docs', 'doc', 'index.html')) + .readAsStringSync(); + stdout.write(index); +} + +Future _buildFlutterDocs(String flutterPath, [String label]) async { + var launcher = new SubprocessLauncher( + 'build-flutter-docs${label == null ? "" : "-$label"}', + _createThrowawayPubCache()); await launcher.runStreamed('git', ['clone', '--depth', '1', 'https://github.com/flutter/flutter.git', '.'], - workingDirectory: flutterDir.path); + workingDirectory: flutterPath); String flutterBin = path.join('bin', 'flutter'); String flutterCacheDart = - path.join(flutterDir.path, 'bin', 'cache', 'dart-sdk', 'bin', 'dart'); + path.join(flutterPath, 'bin', 'cache', 'dart-sdk', 'bin', 'dart'); String flutterCachePub = - path.join(flutterDir.path, 'bin', 'cache', 'dart-sdk', 'bin', 'pub'); + path.join(flutterPath, 'bin', 'cache', 'dart-sdk', 'bin', 'pub'); await launcher.runStreamed( flutterBin, ['--version'], - workingDirectory: flutterDir.path, + workingDirectory: flutterPath, ); await launcher.runStreamed( flutterBin, ['precache'], - workingDirectory: flutterDir.path, + workingDirectory: flutterPath, ); await launcher.runStreamed( flutterCachePub, ['get'], - workingDirectory: path.join(flutterDir.path, 'dev', 'tools'), + workingDirectory: path.join(flutterPath, 'dev', 'tools'), ); await launcher .runStreamed(flutterCachePub, ['global', 'activate', '-spath', '.']); await launcher.runStreamed( flutterCacheDart, [path.join('dev', 'tools', 'dartdoc.dart')], - workingDirectory: flutterDir.path, + workingDirectory: flutterPath, ); - String index = - new File(path.join(flutterDir.path, 'dev', 'docs', 'doc', 'index.html')) - .readAsStringSync(); - stdout.write(index); } @Task('Checks that CHANGELOG mentions current version') @@ -326,13 +331,13 @@ publish() async { test() async { // `pub run test` is a bit slower than running an `test_all.dart` script // But it provides more useful output in the case of failures. - await new _SubprocessLauncher('test') + await new SubprocessLauncher('test') .runStreamed(sdkBin('pub'), ['run', 'test']); } @Task('Generate docs for dartdoc') testDartdoc() async { - var launcher = new _SubprocessLauncher('test-dartdoc'); + var launcher = new SubprocessLauncher('test-dartdoc'); await launcher.runStreamed(Platform.resolvedExecutable, ['--checked', 'bin/dartdoc.dart', '--output', dartdocDocsDir.path]); File indexHtml = joinFile(dartdocDocsDir, ['index.html']); @@ -341,7 +346,7 @@ testDartdoc() async { @Task('update test_package_docs') updateTestPackageDocs() async { - var launcher = new _SubprocessLauncher('update-test-package-docs'); + var launcher = new SubprocessLauncher('update-test-package-docs'); var testPackageDocs = new Directory(path.join('testing', 'test_package_docs')); var testPackage = new Directory(path.join('testing', 'test_package'));