diff --git a/examples/hello_world/hello_world.iml b/examples/hello_world/hello_world.iml
index 9d5dae19540..c82504f3177 100644
--- a/examples/hello_world/hello_world.iml
+++ b/examples/hello_world/hello_world.iml
@@ -7,6 +7,7 @@
+
diff --git a/packages/flutter_tools/bin/fuchsia_tester.dart b/packages/flutter_tools/bin/fuchsia_tester.dart
index fb4ef775973..d4bd938eece 100644
--- a/packages/flutter_tools/bin/fuchsia_tester.dart
+++ b/packages/flutter_tools/bin/fuchsia_tester.dart
@@ -88,7 +88,6 @@ Future run(List args) async {
}
loader.installHook(
shellPath: shellPath,
- debuggerMode: false,
);
PackageMap.globalPackagesPath =
diff --git a/packages/flutter_tools/flutter_tools.iml b/packages/flutter_tools/flutter_tools.iml
index 367b72461cd..22fe5c309c0 100644
--- a/packages/flutter_tools/flutter_tools.iml
+++ b/packages/flutter_tools/flutter_tools.iml
@@ -7,6 +7,12 @@
+
+
+
+
+
+
@@ -30,16 +36,10 @@
+
-
-
-
-
-
-
-
-
+
diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart
index 9b3c208c384..380641270d1 100644
--- a/packages/flutter_tools/lib/executable.dart
+++ b/packages/flutter_tools/lib/executable.dart
@@ -83,7 +83,7 @@ Future main(List args) async {
new RunCommand(verboseHelp: verboseHelp),
new ScreenshotCommand(),
new StopCommand(),
- new TestCommand(),
+ new TestCommand(verboseHelp: verboseHelp),
new TraceCommand(),
new UpdatePackagesCommand(hidden: !verboseHelp),
new UpgradeCommand(),
diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart
index e3285010a7b..6171abcc88b 100644
--- a/packages/flutter_tools/lib/src/commands/test.dart
+++ b/packages/flutter_tools/lib/src/commands/test.dart
@@ -4,9 +4,6 @@
import 'dart:async';
-import 'package:test/src/executable.dart' as test; // ignore: implementation_imports
-
-import '../artifacts.dart';
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
@@ -14,16 +11,16 @@ import '../base/logger.dart';
import '../base/os.dart';
import '../base/platform.dart';
import '../base/process_manager.dart';
-import '../base/terminal.dart';
import '../cache.dart';
-import '../dart/package_map.dart';
import '../globals.dart';
import '../runner/flutter_command.dart';
import '../test/coverage_collector.dart';
-import '../test/flutter_platform.dart' as loader;
+import '../test/event_printer.dart';
+import '../test/runner.dart';
+import '../test/watcher.dart';
class TestCommand extends FlutterCommand {
- TestCommand() {
+ TestCommand({ bool verboseHelp: false }) {
usesPubOption();
argParser.addFlag('start-paused',
defaultsTo: false,
@@ -53,6 +50,11 @@ class TestCommand extends FlutterCommand {
defaultsTo: 'coverage/lcov.info',
help: 'Where to store coverage information (if coverage is enabled).'
);
+ argParser.addFlag('machine',
+ hide: !verboseHelp,
+ negatable: false,
+ help: 'Handle machine structured JSON command input\n'
+ 'and provide output and progress in machine friendly format.');
commandValidator = () {
if (!fs.isFileSync('pubspec.yaml')) {
throwToolExit(
@@ -70,37 +72,12 @@ class TestCommand extends FlutterCommand {
@override
String get description => 'Run Flutter unit tests for the current project.';
- Iterable _findTests(Directory directory) {
- return directory.listSync(recursive: true, followLinks: false)
- .where((FileSystemEntity entity) => entity.path.endsWith('_test.dart') &&
- fs.isFileSync(entity.path))
- .map((FileSystemEntity entity) => fs.path.absolute(entity.path));
- }
-
Directory get _currentPackageTestDir {
// We don't scan the entire package, only the test/ subdirectory, so that
// files with names like like "hit_test.dart" don't get run.
return fs.directory('test');
}
- Future _runTests(List testArgs, Directory testDirectory) async {
- final Directory currentDirectory = fs.currentDirectory;
- try {
- if (testDirectory != null) {
- printTrace('switching to directory $testDirectory to run tests');
- PackageMap.globalPackagesPath = fs.path.normalize(fs.path.absolute(PackageMap.globalPackagesPath));
- fs.currentDirectory = testDirectory;
- }
- printTrace('running test package with arguments: $testArgs');
- await test.main(testArgs);
- // test.main() sets dart:io's exitCode global.
- printTrace('test package returned with exit code $exitCode');
- return exitCode;
- } finally {
- fs.currentDirectory = currentDirectory;
- }
- }
-
Future _collectCoverageData(CoverageCollector collector, { bool mergeCoverageData: false }) async {
final Status status = logger.startProgress('Collecting coverage information...');
final String coverageData = await collector.finalizeCoverage(
@@ -161,7 +138,7 @@ class TestCommand extends FlutterCommand {
}
@override
- Future runCommand() async {
+ Future runCommand() async {
if (platform.isWindows) {
throwToolExit(
'The test command is currently not supported on Windows: '
@@ -169,57 +146,57 @@ class TestCommand extends FlutterCommand {
);
}
- final List testArgs = [];
-
commandValidator();
- if (!terminal.supportsColor)
- testArgs.addAll(['--no-color', '-rexpanded']);
+ Iterable files = argResults.rest.map((String testPath) => fs.path.absolute(testPath)).toList();
+
+ final bool startPaused = argResults['start-paused'];
+ if (startPaused && files.length != 1) {
+ throwToolExit(
+ 'When using --start-paused, you must specify a single test file to run.',
+ exitCode: 1);
+ }
+
+ Directory workDir;
+ if (files.isEmpty) {
+ workDir = _currentPackageTestDir;
+ if (!workDir.existsSync())
+ throwToolExit('Test directory "${workDir.path}" not found.');
+ files = _findTests(workDir);
+ if (files.isEmpty) {
+ throwToolExit(
+ 'Test directory "${workDir.path}" does not appear to contain any test files.\n'
+ 'Test files must be in that directory and end with the pattern "_test.dart".'
+ );
+ }
+ }
CoverageCollector collector;
if (argResults['coverage'] || argResults['merge-coverage']) {
collector = new CoverageCollector();
- testArgs.add('--concurrency=1');
}
- testArgs.add('--');
-
- Directory testDir;
- Iterable files = argResults.rest.map((String testPath) => fs.path.absolute(testPath)).toList();
- if (argResults['start-paused']) {
- if (files.length != 1)
- throwToolExit('When using --start-paused, you must specify a single test file to run.', exitCode: 1);
- } else if (files.isEmpty) {
- testDir = _currentPackageTestDir;
- if (!testDir.existsSync())
- throwToolExit('Test directory "${testDir.path}" not found.');
- files = _findTests(testDir);
- if (files.isEmpty) {
- throwToolExit(
- 'Test directory "${testDir.path}" does not appear to contain any test files.\n'
- 'Test files must be in that directory and end with the pattern "_test.dart".'
- );
- }
+ final bool wantEvents = argResults['machine'];
+ if (collector != null && wantEvents) {
+ throwToolExit(
+ "The test command doesn't support --machine and coverage together");
}
- testArgs.addAll(files);
- final InternetAddressType serverType = argResults['ipv6']
- ? InternetAddressType.IP_V6
- : InternetAddressType.IP_V4;
-
- final String shellPath = artifacts.getArtifactPath(Artifact.flutterTester);
- if (!fs.isFileSync(shellPath))
- throwToolExit('Cannot find Flutter shell at $shellPath');
- loader.installHook(
- shellPath: shellPath,
- collector: collector,
- debuggerMode: argResults['start-paused'],
- serverType: serverType,
- );
+ TestWatcher watcher;
+ if (collector != null) {
+ watcher = collector;
+ } else if (wantEvents) {
+ watcher = new EventPrinter();
+ }
Cache.releaseLockEarly();
- final int result = await _runTests(testArgs, testDir);
+ final int result = await runTests(files,
+ workDir: workDir,
+ watcher: watcher,
+ enableObservatory: collector != null || startPaused,
+ startPaused: startPaused,
+ ipv6: argResults['ipv6']);
if (collector != null) {
if (!await _collectCoverageData(collector, mergeCoverageData: argResults['merge-coverage']))
@@ -228,5 +205,13 @@ class TestCommand extends FlutterCommand {
if (result != 0)
throwToolExit(null);
+ return const FlutterCommandResult(ExitStatus.success);
}
}
+
+Iterable _findTests(Directory directory) {
+ return directory.listSync(recursive: true, followLinks: false)
+ .where((FileSystemEntity entity) => entity.path.endsWith('_test.dart') &&
+ fs.isFileSync(entity.path))
+ .map((FileSystemEntity entity) => fs.path.absolute(entity.path));
+}
diff --git a/packages/flutter_tools/lib/src/test/coverage_collector.dart b/packages/flutter_tools/lib/src/test/coverage_collector.dart
index 3b7ded4a501..21b80e0befd 100644
--- a/packages/flutter_tools/lib/src/test/coverage_collector.dart
+++ b/packages/flutter_tools/lib/src/test/coverage_collector.dart
@@ -11,10 +11,18 @@ import '../base/io.dart';
import '../dart/package_map.dart';
import '../globals.dart';
+import 'watcher.dart';
+
/// A class that's used to collect coverage data during tests.
-class CoverageCollector {
+class CoverageCollector extends TestWatcher {
Map _globalHitmap;
+ @override
+ Future onFinishedTests(ProcessEvent event) async {
+ printTrace('test ${event.childIndex}: collecting coverage');
+ await collectCoverage(event.process, event.observatoryUri);
+ }
+
void _addHitmap(Map hitmap) {
if (_globalHitmap == null)
_globalHitmap = hitmap;
diff --git a/packages/flutter_tools/lib/src/test/event_printer.dart b/packages/flutter_tools/lib/src/test/event_printer.dart
new file mode 100644
index 00000000000..4be6b77fe1f
--- /dev/null
+++ b/packages/flutter_tools/lib/src/test/event_printer.dart
@@ -0,0 +1,41 @@
+// Copyright 2017 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:convert' show JSON;
+
+import '../base/io.dart' show stdout;
+import 'watcher.dart';
+
+/// Prints JSON events when running a test in --machine mode.
+class EventPrinter extends TestWatcher {
+ EventPrinter({StringSink out}) : this._out = out == null ? stdout: out;
+
+ final StringSink _out;
+
+ @override
+ void onStartedProcess(ProcessEvent event) {
+ _sendEvent("test.startedProcess",
+ {"observatoryUri": event.observatoryUri.toString()});
+ }
+
+ void _sendEvent(String name, [dynamic params]) {
+ final Map map = { 'event': name};
+ if (params != null) {
+ map['params'] = params;
+ }
+ _send(map);
+ }
+
+ void _send(Map command) {
+ final String encoded = JSON.encode(command, toEncodable: _jsonEncodeObject);
+ _out.writeln('\n[$encoded]');
+ }
+
+ dynamic _jsonEncodeObject(dynamic object) {
+ if (object is Uri) {
+ return object.toString();
+ }
+ return object;
+ }
+}
diff --git a/packages/flutter_tools/lib/src/test/flutter_platform.dart b/packages/flutter_tools/lib/src/test/flutter_platform.dart
index 58de703b7ce..8af0147f391 100644
--- a/packages/flutter_tools/lib/src/test/flutter_platform.dart
+++ b/packages/flutter_tools/lib/src/test/flutter_platform.dart
@@ -19,6 +19,7 @@ import '../base/process_manager.dart';
import '../dart/package_map.dart';
import '../globals.dart';
import 'coverage_collector.dart';
+import 'watcher.dart';
/// The timeout we give the test process to connect to the test harness
/// once the process has entered its main method.
@@ -53,17 +54,22 @@ final Map _kHosts = [TestPlatform.vm],
() => new _FlutterPlatform(
shellPath: shellPath,
- collector: collector,
- debuggerMode: debuggerMode,
+ watcher: watcher,
+ enableObservatory: enableObservatory,
+ startPaused: startPaused,
explicitObservatoryPort: observatoryPort,
explicitDiagnosticPort: diagnosticPort,
host: _kHosts[serverType],
@@ -78,8 +84,9 @@ typedef Future _Finalizer();
class _FlutterPlatform extends PlatformPlugin {
_FlutterPlatform({
@required this.shellPath,
- this.collector,
- this.debuggerMode,
+ this.watcher,
+ this.enableObservatory,
+ this.startPaused,
this.explicitObservatoryPort,
this.explicitDiagnosticPort,
this.host,
@@ -88,8 +95,9 @@ class _FlutterPlatform extends PlatformPlugin {
}
final String shellPath;
- final CoverageCollector collector;
- final bool debuggerMode;
+ final TestWatcher watcher;
+ final bool enableObservatory;
+ final bool startPaused;
final int explicitObservatoryPort;
final int explicitDiagnosticPort;
final InternetAddress host;
@@ -105,7 +113,7 @@ class _FlutterPlatform extends PlatformPlugin {
@override
StreamChannel loadChannel(String testPath, TestPlatform platform) {
- if (explicitObservatoryPort != null || explicitDiagnosticPort != null || debuggerMode) {
+ if (enableObservatory || explicitDiagnosticPort != null) {
if (_testCount > 0)
throwToolExit('installHook() was called with an observatory port, a diagnostic port, both, or debugger mode enabled, but then more than one test suite was run.');
}
@@ -190,8 +198,8 @@ class _FlutterPlatform extends PlatformPlugin {
shellPath,
listenerFile.path,
packages: PackageMap.globalPackagesPath,
- enableObservatory: collector != null || debuggerMode,
- startPaused: debuggerMode,
+ enableObservatory: enableObservatory,
+ startPaused: startPaused,
observatoryPort: explicitObservatoryPort,
diagnosticPort: explicitDiagnosticPort,
);
@@ -216,19 +224,23 @@ class _FlutterPlatform extends PlatformPlugin {
// Pipe stdout and stderr from the subprocess to our printStatus console.
// We also keep track of what observatory port the engine used, if any.
Uri processObservatoryUri;
+
_pipeStandardStreamsToConsole(
process,
reportObservatoryUri: (Uri detectedUri) {
assert(processObservatoryUri == null);
assert(explicitObservatoryPort == null ||
explicitObservatoryPort == detectedUri.port);
- if (debuggerMode) {
+ if (startPaused) {
printStatus('The test process has been started.');
printStatus('You can now connect to it using observatory. To connect, load the following Web site in your browser:');
printStatus(' $detectedUri');
printStatus('You should first set appropriate breakpoints, then resume the test in the debugger.');
} else {
- printTrace('test $ourTestCount: using observatory uri $detectedUri from pid ${process.pid} to collect coverage');
+ printTrace('test $ourTestCount: using observatory uri $detectedUri from pid ${process.pid}');
+ }
+ if (watcher != null) {
+ watcher.onStartedProcess(new ProcessEvent(ourTestCount, process, detectedUri));
}
processObservatoryUri = detectedUri;
},
@@ -341,9 +353,9 @@ class _FlutterPlatform extends PlatformPlugin {
break;
}
- if (subprocessActive && collector != null) {
- printTrace('test $ourTestCount: collecting coverage');
- await collector.collectCoverage(process, processObservatoryUri);
+ if (subprocessActive && watcher != null) {
+ await watcher.onFinishedTests(
+ new ProcessEvent(ourTestCount, process, processObservatoryUri));
}
} catch (error, stack) {
printTrace('test $ourTestCount: error caught during test; ${controllerSinkClosed ? "reporting to console" : "sending to test framework"}');
diff --git a/packages/flutter_tools/lib/src/test/runner.dart b/packages/flutter_tools/lib/src/test/runner.dart
new file mode 100644
index 00000000000..cd7ad43ec84
--- /dev/null
+++ b/packages/flutter_tools/lib/src/test/runner.dart
@@ -0,0 +1,81 @@
+// Copyright 2017 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';
+
+// ignore: implementation_imports
+import 'package:test/src/executable.dart' as test;
+
+import '../artifacts.dart';
+import '../base/common.dart';
+import '../base/file_system.dart';
+import '../base/io.dart';
+import '../base/terminal.dart';
+import '../dart/package_map.dart';
+import '../globals.dart';
+import '../test/flutter_platform.dart' as loader;
+import 'watcher.dart';
+
+/// Runs tests using package:test and the Flutter engine.
+Future runTests(
+ List testFiles, {
+ Directory workDir,
+ bool enableObservatory: false,
+ bool startPaused: false,
+ bool ipv6: false,
+ TestWatcher watcher,
+ }) async {
+ // Compute the command-line arguments for package:test.
+ final List testArgs = [];
+ if (!terminal.supportsColor)
+ testArgs.addAll(['--no-color', '-rexpanded']);
+
+ if (enableObservatory) {
+ testArgs.add('--concurrency=1');
+ }
+
+ testArgs.add('--');
+ testArgs.addAll(testFiles);
+
+ // Configure package:test to use the Flutter engine for child processes.
+ final String shellPath = artifacts.getArtifactPath(Artifact.flutterTester);
+ if (!fs.isFileSync(shellPath))
+ throwToolExit('Cannot find Flutter shell at $shellPath');
+
+ final InternetAddressType serverType =
+ ipv6 ? InternetAddressType.IP_V6 : InternetAddressType.IP_V4;
+
+ loader.installHook(
+ shellPath: shellPath,
+ watcher: watcher,
+ enableObservatory: enableObservatory,
+ startPaused: startPaused,
+ serverType: serverType,
+ );
+
+ // Set the package path used for child processes.
+ // TODO(skybrian): why is this global? Move to installHook?
+ PackageMap.globalPackagesPath =
+ fs.path.normalize(fs.path.absolute(PackageMap.globalPackagesPath));
+
+ // Call package:test's main method in the appropriate directory.
+ final Directory saved = fs.currentDirectory;
+ try {
+ if (workDir != null) {
+ printTrace('switching to directory $workDir to run tests');
+ fs.currentDirectory = workDir;
+ }
+
+ printTrace('running test package with arguments: $testArgs');
+ await test.main(testArgs);
+
+ // test.main() sets dart:io's exitCode global.
+ // TODO(skybrian): restore previous value?
+ printTrace('test package returned with exit code $exitCode');
+
+ return exitCode;
+ } finally {
+ fs.currentDirectory = saved;
+ }
+}
diff --git a/packages/flutter_tools/lib/src/test/watcher.dart b/packages/flutter_tools/lib/src/test/watcher.dart
new file mode 100644
index 00000000000..114f456151d
--- /dev/null
+++ b/packages/flutter_tools/lib/src/test/watcher.dart
@@ -0,0 +1,39 @@
+// Copyright 2017 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 '../base/io.dart' show Process;
+
+/// Callbacks for reporting progress while running tests.
+class TestWatcher {
+
+ /// Called after a child process starts.
+ ///
+ /// If startPaused was true, the caller needs to resume in Observatory to
+ /// start running the tests.
+ void onStartedProcess(ProcessEvent event) {}
+
+ /// Called after the tests finish but before the process exits.
+ ///
+ /// The child process won't exit until this method completes.
+ /// Not called if the process died.
+ Future onFinishedTests(ProcessEvent event) async {}
+}
+
+/// Describes a child process started during testing.
+class ProcessEvent {
+ ProcessEvent(this.childIndex, this.process, this.observatoryUri);
+
+ /// The index assigned when the child process was launched.
+ ///
+ /// Indexes are assigned consecutively starting from zero.
+ /// When debugging, there should only be one child process so this will
+ /// always be zero.
+ final int childIndex;
+
+ final Process process;
+
+ /// The observatory Uri or null if not debugging.
+ final Uri observatoryUri;
+}