Cleanup and move test-only code into the test utils library (#1878)
diff --git a/lib/src/io_utils.dart b/lib/src/io_utils.dart
index 92ce509..3c99b3a 100644
--- a/lib/src/io_utils.dart
+++ b/lib/src/io_utils.dart
@@ -6,10 +6,8 @@
library dartdoc.io_utils;
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
@@ -82,7 +80,6 @@
final partOfRegexp = new RegExp('part of ');
final newLinePartOfRegexp = new RegExp('\npart of ');
-final RegExp quotables = new RegExp(r'[ "\r\n\$]');
/// Best used with Future<void>.
class MultiFutureTracker<T> {
@@ -117,204 +114,4 @@
Future<void> 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<String, String> 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<Tuple2<String, Future<Iterable<Map>>>> 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<void> 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', // TODO(jcollins-g): use pub after dart-lang/coverage#240 is landed
- '--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<Iterable<Map>> runStreamed(String executable, List<String> 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<Iterable<Map>> 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<String, String> environment;
-
- String get prefix => context.isNotEmpty ? '$context: ' : '';
-
- // from flutter:dev/tools/dartdoc.dart, modified
- static Future<void> _printStream(Stream<List<int>> stream, Stdout output,
- {String prefix: '', Iterable<String> Function(String line) filter}) {
- assert(prefix != null);
- if (filter == null) filter = (line) => [line];
- return stream
- .transform(utf8.decoder)
- .transform(const LineSplitter())
- .expand(filter)
- .listen((String line) {
- if (line != null) {
- output.write('$prefix$line'.trim());
- output.write('\n');
- }
- }).asFuture();
- }
-
- SubprocessLauncher(this.context, [Map<String, String> environment])
- : this.environment = environment ?? <String, String>{};
-
- /// 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, or if DRY_RUN is set to 1 in the execution environment.
- ///
- /// 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<Iterable<Map>> runStreamed(String executable, List<String> arguments,
- {String workingDirectory}) async {
- List<Map> 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<String> 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');
-
- if (Platform.environment.containsKey('DRY_RUN')) return null;
-
- String realExecutable = executable;
- final List<String> realArguments = [];
- if (Platform.isLinux) {
- // Use GNU coreutils to force line buffering. This makes sure that
- // subprocesses that die due to fatal signals do not chop off the
- // last few lines of their output.
- //
- // Dart does not actually do this (seems to flush manually) unless
- // the VM crashes.
- realExecutable = 'stdbuf';
- realArguments.addAll(['-o', 'L', '-e', 'L']);
- realArguments.add(executable);
- }
- realArguments.addAll(arguments);
-
- Process process = await Process.start(realExecutable, realArguments,
- workingDirectory: workingDirectory, environment: environment);
- Future<void> stdoutFuture = _printStream(process.stdout, stdout,
- prefix: prefix, filter: jsonCallback);
- Future<void> stderrFuture = _printStream(process.stderr, stderr,
- prefix: prefix, filter: jsonCallback);
- await Future.wait([stderrFuture, stdoutFuture, process.exitCode]);
-
- int exitCode = await process.exitCode;
- if (exitCode != 0) {
- throw new ProcessException(executable, arguments,
- "SubprocessLauncher got non-zero exitCode: $exitCode", exitCode);
- }
- return jsonObjects;
- }
-}
diff --git a/lib/src/model.dart b/lib/src/model.dart
index 4659baf..dbbce53 100644
--- a/lib/src/model.dart
+++ b/lib/src/model.dart
@@ -388,7 +388,7 @@
} else {
_overriddenElement = foundField.setter;
}
- assert(!(_overriddenElement as Accessor).isInherited);
+ assert(!(_overriddenElement as InheritableAccessor).isInherited);
break;
}
}
@@ -413,16 +413,7 @@
}
GetterSetterCombo get enclosingCombo {
- if (_enclosingCombo == null) {
- if (enclosingElement is Class) {
- // TODO(jcollins-g): this side effect is rather hacky. Make sure
- // enclosingCombo always gets set at accessor creation time, somehow, to
- // avoid this.
- // TODO(jcollins-g): This also doesn't work for private accessors sometimes.
- (enclosingElement as Class).allInstanceFields;
- }
- assert(_enclosingCombo != null);
- }
+ assert(_enclosingCombo != null);
return _enclosingCombo;
}
@@ -473,19 +464,12 @@
@override
void warn(PackageWarning kind,
{String message,
- Iterable<Locatable> referredFrom,
- Iterable<String> extendedDebug}) {
- if (enclosingCombo != null) {
- enclosingCombo.warn(kind,
- message: message,
- referredFrom: referredFrom,
- extendedDebug: extendedDebug);
- } else {
- super.warn(kind,
- message: message,
- referredFrom: referredFrom,
- extendedDebug: extendedDebug);
- }
+ Iterable<Locatable> referredFrom,
+ Iterable<String> extendedDebug}) {
+ enclosingCombo.warn(kind,
+ message: message,
+ referredFrom: referredFrom,
+ extendedDebug: extendedDebug);
}
@override
@@ -510,8 +494,6 @@
bool get isCovariant => isSetter && parameters.first.isCovariant;
- bool get isInherited => false;
-
@override
String get href {
return enclosingCombo.href;
@@ -596,7 +578,7 @@
with TypeParameters, Categorization
implements EnclosedElement {
List<DefinedElementType> _mixins;
- DefinedElementType _supertype;
+ DefinedElementType supertype;
List<DefinedElementType> _interfaces;
List<Constructor> _constructors;
List<Method> _allMethods;
@@ -623,7 +605,7 @@
.toList(growable: false);
if (_cls.supertype != null && _cls.supertype.element.supertype != null) {
- _supertype = new ElementType.from(_cls.supertype, packageGraph);
+ supertype = new ElementType.from(_cls.supertype, packageGraph);
}
_interfaces = _cls.interfaces
@@ -1032,7 +1014,7 @@
List<DefinedElementType> get superChain {
List<DefinedElementType> typeChain = [];
- DefinedElementType parent = _supertype;
+ DefinedElementType parent = supertype;
while (parent != null) {
typeChain.add(parent);
if (parent.type is InterfaceType) {
@@ -1045,7 +1027,7 @@
(parent.type as InterfaceType).superclass, packageGraph);
}
} else {
- parent = (parent.element as Class)._supertype;
+ parent = (parent.element as Class).supertype;
}
}
return typeChain;
@@ -1056,8 +1038,6 @@
Iterable<DefinedElementType> get publicSuperChainReversed =>
publicSuperChain.toList().reversed;
- DefinedElementType get supertype => _supertype;
-
List<ExecutableElement> __inheritedElements;
List<ExecutableElement> get _inheritedElements {
if (__inheritedElements == null) {
@@ -1133,9 +1113,9 @@
PropertyAccessorElement setterElement,
Set<PropertyAccessorElement> inheritedAccessors,
[FieldElement f]) {
- Accessor getter =
+ InheritableAccessor getter =
new InheritableAccessor.from(getterElement, inheritedAccessors, this);
- Accessor setter =
+ InheritableAccessor setter =
new InheritableAccessor.from(setterElement, inheritedAccessors, this);
// Rebind getterElement/setterElement as ModelElement.from can resolve
// MultiplyInheritedExecutableElements or resolve Members.
@@ -1154,7 +1134,7 @@
// different places in the inheritance chain, there are two FieldElements
// for this single Field we're trying to compose. Pick the one closest
// to this class on the inheritance chain.
- if ((setter.enclosingElement as Class)
+ if ((setter.enclosingElement)
.isInheritingFrom(getter.enclosingElement)) {
f = setterElement.variable;
} else {
@@ -2033,10 +2013,7 @@
}
bool get hasExplicitGetter => hasPublicGetter && !getter.isSynthetic;
-
bool get hasExplicitSetter => hasPublicSetter && !setter.isSynthetic;
- bool get hasImplicitSetter => hasPublicSetter && setter.isSynthetic;
- bool get hasImplicitGetter => hasPublicGetter && getter.isSynthetic;
bool get hasGetter => getter != null;
@@ -2054,10 +2031,7 @@
if (writeOnly) return r'←';
// ↔
if (readWrite) return r'↔';
- // A GetterSetterCombo should always be one of readOnly, writeOnly,
- // or readWrite (if documented).
- assert(!isPublic);
- return null;
+ throw UnsupportedError('GetterSetterCombo must be one of readOnly, writeOnly, or readWrite');
}
bool get readOnly => hasPublicGetter && !hasPublicSetter;
@@ -2201,12 +2175,6 @@
return true;
}
- /// A special case where the SDK has defined that we should not document
- /// this library. This is implemented by tweaking canonicalization so
- /// even though the library is public and part of the Package's list,
- /// we don't count it as a candidate for canonicalization.
- bool get isSdkUndocumented => (sdkLib != null && !sdkLib.isDocumented);
-
@override
Iterable<TopLevelVariable> get constants {
if (_constants == null) {
@@ -2462,8 +2430,6 @@
return _packageMeta;
}
- String get path => _libraryElement.definingCompilationUnit.name;
-
/// All variables ("properties") except constants.
@override
Iterable<TopLevelVariable> get properties {
@@ -2530,18 +2496,6 @@
return _allClasses.firstWhere((it) => it.name == name, orElse: () => null);
}
- bool hasInExportedNamespace(Element element) {
- Element found = _exportedNamespace.get(element.name);
- if (found == null) return false;
- if (found == element) return true; // this checks more than just the name
-
- // Fix for #587, comparison between elements isn't reliable on windows.
- // for some reason. sigh.
-
- return found.runtimeType == element.runtimeType &&
- found.nameOffset == element.nameOffset;
- }
-
List<TopLevelVariable> _getVariables() {
if (_variables != null) return _variables;
@@ -2591,21 +2545,8 @@
} else {
hidePackage = package.packageGraph.packageMeta;
}
- if (name.startsWith('file:')) {
- // restoreUri doesn't do anything for the package we're documenting.
- String canonicalPackagePath =
- '${pathLib.canonicalize(hidePackage.dir.path)}${pathLib.separator}lib${pathLib.separator}';
- String canonicalElementPath =
- pathLib.canonicalize(element.source.uri.toFilePath());
- assert(canonicalElementPath.startsWith(canonicalPackagePath));
- List<String> pathSegments = [hidePackage.name]..addAll(pathLib
- .split(canonicalElementPath.replaceFirst(canonicalPackagePath, '')));
- Uri libraryUri = new Uri(
- scheme: 'package',
- pathSegments: pathSegments,
- );
- name = libraryUri.toString();
- }
+ // restoreUri must not result in another file URI.
+ assert(!name.startsWith('file:'));
String defaultPackagePrefix = 'package:$hidePackage/';
if (name.startsWith(defaultPackagePrefix)) {
@@ -3391,8 +3332,7 @@
if (!hasPublicName(element)) {
_canonicalLibrary = null;
} else if (!packageGraph.localPublicLibraries.contains(definingLibrary)) {
- List<Library> candidateLibraries = packageGraph
- .libraryElementReexportedBy[definingLibrary.element]
+ List<Library> candidateLibraries = definingLibrary.exportedInLibraries
?.where((l) =>
l.isPublic &&
l.package.documentedWhere != DocumentLocation.missing)
@@ -4609,6 +4549,9 @@
}
return _namePart;
}
+
+ @override
+ String toString() => name;
}
/// Something able to be indexed.
@@ -4881,17 +4824,6 @@
PackageWarningCounter get packageWarningCounter => _packageWarningCounter;
- /// Returns colon-stripped name and location of the given locatable.
- static Tuple2<String, String> nameAndLocation(Locatable locatable) {
- String locatableName = '<unknown>';
- String locatableLocation = '';
- if (locatable != null) {
- locatableName = locatable.fullyQualifiedName.replaceFirst(':', '-');
- locatableLocation = locatable.location;
- }
- return new Tuple2(locatableName, locatableLocation);
- }
-
final Set<Tuple3<Element, PackageWarning, String>> _warnAlreadySeen =
new Set();
void warnOnElement(Warnable warnable, PackageWarning kind,
@@ -5074,8 +5006,6 @@
return locatable.fullyQualifiedName.replaceFirst(':', '-');
}
- bool get hasMultiplePackages => localPackages.length > 1;
-
List<Package> get packages => packageMap.values.toList();
List<Package> _publicPackages;
@@ -5197,8 +5127,8 @@
_checkAndAddClass(t.element, c);
});
}
- if (c._supertype != null) {
- _checkAndAddClass(c._supertype.element, c);
+ if (c.supertype != null) {
+ _checkAndAddClass(c.supertype.element, c);
}
if (!c.interfaces.isEmpty) {
c.interfaces.forEach((t) {
@@ -5571,9 +5501,6 @@
Iterable<TopLevelVariable> get publicProperties =>
filterNonPublic(properties);
Iterable<Typedef> get publicTypedefs => filterNonPublic(typedefs);
-
- @override
- String toString() => name;
}
/// A set of libraries, initialized after construction by accessing [_libraries].
@@ -5598,9 +5525,6 @@
/// Sorting key. [containerOrder] should contain these.
String get sortKey => name;
- @override
- String toString() => name;
-
/// Does this container represent the SDK? This can be false for containers
/// that only represent a part of the SDK.
bool get isSdk => false;
@@ -6133,9 +6057,6 @@
PackageMeta get packageMeta => _packageMeta;
@override
- String toString() => name;
-
- @override
Element get element => null;
@override
@@ -6195,9 +6116,6 @@
String get kind => 'parameter';
ParameterElement get _parameter => element as ParameterElement;
-
- @override
- String toString() => element.name;
}
abstract class SourceCodeMixin implements Documentable {
@@ -6427,9 +6345,6 @@
}
TypeParameterElement get _typeParameter => element as TypeParameterElement;
-
- @override
- String toString() => element.name;
}
/// Everything you need to instantiate a PackageGraph object for documenting.
@@ -6556,22 +6471,11 @@
options);
driver.results.listen((_) {});
driver.exceptions.listen((_) {});
- _session = driver.currentSession;
scheduler.start();
}
return _driver;
}
- AnalysisSession _session;
- AnalysisSession get session {
- if (_session == null) {
- // The current session is initialized as a side-effect of initializing the
- // driver.
- driver;
- }
- return _session;
- }
-
PackageWarningOptions getWarningOptions() {
PackageWarningOptions warningOptions =
new PackageWarningOptions(config.verboseWarnings);
@@ -6594,12 +6498,10 @@
/// Return an Iterable with the sdk files we should parse.
/// Filter can be String or RegExp (technically, anything valid for
/// [String.contains])
- Iterable<String> getSdkFilesToDocument([dynamic filter]) sync* {
+ Iterable<String> getSdkFilesToDocument() sync* {
for (var sdkLib in sdk.sdkLibraries) {
Source source = sdk.mapDartUri(sdkLib.shortName);
- if (filter == null || source.uri.toString().contains(filter)) {
- yield source.fullName;
- }
+ yield source.fullName;
}
}
@@ -6631,7 +6533,7 @@
if (sourceKind == SourceKind.LIBRARY) {
// Loading libraryElements from part files works, but is painfully slow
// and creates many duplicates.
- return await session.getResolvedLibrary(source.fullName);
+ return await driver.currentSession.getResolvedLibrary(source.fullName);
}
return null;
}
diff --git a/test/src/utils.dart b/test/src/utils.dart
index 2002660..17ae32e 100644
--- a/test/src/utils.dart
+++ b/test/src/utils.dart
@@ -5,12 +5,17 @@
library test_utils;
import 'dart:async';
+import 'dart:convert';
import 'dart:io';
import 'package:dartdoc/dartdoc.dart';
import 'package:dartdoc/src/html/html_generator.dart';
import 'package:dartdoc/src/model.dart';
import 'package:dartdoc/src/package_meta.dart';
+import 'package:dartdoc/src/tuple.dart';
+import 'package:path/path.dart' as pathLib;
+
+final RegExp quotables = new RegExp(r'[ "\r\n\$]');
Directory sdkDir;
PackageMeta sdkPackageMeta;
@@ -59,10 +64,6 @@
return new DartdocOptionContext(optionSet, Directory.current);
}
-void delete(Directory dir) {
- if (dir.existsSync()) dir.deleteSync(recursive: true);
-}
-
void init({List<String> additionalArguments}) async {
sdkDir = defaultSdkDir;
sdkPackageMeta = new PackageMeta.fromDir(sdkDir);
@@ -108,3 +109,205 @@
additionalArguments))
.buildPackageGraph();
}
+
+/// 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<String, String> 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<Tuple2<String, Future<Iterable<Map>>>> 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<void> 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', // TODO(jcollins-g): use pub after dart-lang/coverage#240 is landed
+ '--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<Iterable<Map>> runStreamed(String executable, List<String> 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<Iterable<Map>> 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<String, String> environment;
+
+ String get prefix => context.isNotEmpty ? '$context: ' : '';
+
+ // from flutter:dev/tools/dartdoc.dart, modified
+ static Future<void> _printStream(Stream<List<int>> stream, Stdout output,
+ {String prefix: '', Iterable<String> Function(String line) filter}) {
+ assert(prefix != null);
+ if (filter == null) filter = (line) => [line];
+ return stream
+ .transform(utf8.decoder)
+ .transform(const LineSplitter())
+ .expand(filter)
+ .listen((String line) {
+ if (line != null) {
+ output.write('$prefix$line'.trim());
+ output.write('\n');
+ }
+ }).asFuture();
+ }
+
+ SubprocessLauncher(this.context, [Map<String, String> environment])
+ : this.environment = environment ?? <String, String>{};
+
+ /// 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, or if DRY_RUN is set to 1 in the execution environment.
+ ///
+ /// 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<Iterable<Map>> runStreamed(String executable, List<String> arguments,
+ {String workingDirectory}) async {
+ List<Map> 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<String> 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');
+
+ if (Platform.environment.containsKey('DRY_RUN')) return null;
+
+ String realExecutable = executable;
+ final List<String> realArguments = [];
+ if (Platform.isLinux) {
+ // Use GNU coreutils to force line buffering. This makes sure that
+ // subprocesses that die due to fatal signals do not chop off the
+ // last few lines of their output.
+ //
+ // Dart does not actually do this (seems to flush manually) unless
+ // the VM crashes.
+ realExecutable = 'stdbuf';
+ realArguments.addAll(['-o', 'L', '-e', 'L']);
+ realArguments.add(executable);
+ }
+ realArguments.addAll(arguments);
+
+ Process process = await Process.start(realExecutable, realArguments,
+ workingDirectory: workingDirectory, environment: environment);
+ Future<void> stdoutFuture = _printStream(process.stdout, stdout,
+ prefix: prefix, filter: jsonCallback);
+ Future<void> stderrFuture = _printStream(process.stderr, stderr,
+ prefix: prefix, filter: jsonCallback);
+ await Future.wait([stderrFuture, stdoutFuture, process.exitCode]);
+
+ int exitCode = await process.exitCode;
+ if (exitCode != 0) {
+ throw new ProcessException(executable, arguments,
+ "SubprocessLauncher got non-zero exitCode: $exitCode", exitCode);
+ }
+ return jsonObjects;
+ }
+}
\ No newline at end of file
diff --git a/tool/grind.dart b/tool/grind.dart
index 9c8d7ee..d07ebd6 100644
--- a/tool/grind.dart
+++ b/tool/grind.dart
@@ -11,6 +11,8 @@
import 'package:path/path.dart' as pathLib;
import 'package:yaml/yaml.dart' as yaml;
+import '../test/src/utils.dart';
+
void main([List<String> args]) => grind(args);
/// Thrown on failure to find something in a file.