Add --name and --plain-name to 'flutter test' (#11020)

This adds a way to run only a subset of the tests.
(The new flags do the same thing as 'pub run test'.)
diff --git a/dev/automated_tests/flutter_test/filtering_test.dart b/dev/automated_tests/flutter_test/filtering_test.dart
new file mode 100644
index 0000000..1cf3277
--- /dev/null
+++ b/dev/automated_tests/flutter_test/filtering_test.dart
@@ -0,0 +1,14 @@
+// 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 'package:test/test.dart';
+
+void main() {
+  test('included', () {
+    expect(2 + 2, 4);
+  });
+  test('excluded', () {
+    throw "this test should have been filtered out";
+  });
+}
diff --git a/packages/flutter_tools/lib/src/commands/test.dart b/packages/flutter_tools/lib/src/commands/test.dart
index 2f2e58e..91de53f 100644
--- a/packages/flutter_tools/lib/src/commands/test.dart
+++ b/packages/flutter_tools/lib/src/commands/test.dart
@@ -22,6 +22,18 @@
 class TestCommand extends FlutterCommand {
   TestCommand({ bool verboseHelp: false }) {
     usesPubOption();
+    argParser.addOption('name',
+      help: 'A regular expression matching substrings of the names of tests to run.',
+      valueHelp: 'regexp',
+      allowMultiple: true,
+      splitCommas: false,
+    );
+    argParser.addOption('plain-name',
+      help: 'A plain-text substring of the names of tests to run.',
+      valueHelp: 'substring',
+      allowMultiple: true,
+      splitCommas: false,
+    );
     argParser.addFlag('start-paused',
         defaultsTo: false,
         negatable: false,
@@ -141,6 +153,8 @@
     }
 
     commandValidator();
+    final List<String> names = argResults['name'];
+    final List<String> plainNames = argResults['plain-name'];
 
     Iterable<String> files = argResults.rest.map<String>((String testPath) => fs.path.absolute(testPath)).toList();
 
@@ -189,6 +203,8 @@
 
     final int result = await runTests(files,
         workDir: workDir,
+        names: names,
+        plainNames: plainNames,
         watcher: watcher,
         enableObservatory: collector != null || startPaused,
         startPaused: startPaused,
diff --git a/packages/flutter_tools/lib/src/test/runner.dart b/packages/flutter_tools/lib/src/test/runner.dart
index c376313..0d127d8 100644
--- a/packages/flutter_tools/lib/src/test/runner.dart
+++ b/packages/flutter_tools/lib/src/test/runner.dart
@@ -21,6 +21,8 @@
 Future<int> runTests(
     List<String> testFiles, {
     Directory workDir,
+    List<String> names: const <String>[],
+    List<String> plainNames: const <String>[],
     bool enableObservatory: false,
     bool startPaused: false,
     bool ipv6: false,
@@ -41,6 +43,14 @@
     testArgs.addAll(<String>['-r', 'json']);
   }
 
+  for (String name in names) {
+    testArgs..add("--name")..add(name);
+  }
+
+  for (String plainName in plainNames) {
+    testArgs..add("--plain-name")..add(plainName);
+  }
+
   testArgs.add('--');
   testArgs.addAll(testFiles);
 
diff --git a/packages/flutter_tools/test/commands/test_test.dart b/packages/flutter_tools/test/commands/test_test.dart
index bd3c043..450e126 100644
--- a/packages/flutter_tools/test/commands/test_test.dart
+++ b/packages/flutter_tools/test/commands/test_test.dart
@@ -18,63 +18,63 @@
 Future<Null> _testExclusionLock;
 
 void main() {
-  group('test', () {
+  group('flutter test should', () {
 
     final String automatedTestsDirectory = fs.path.join('..', '..', 'dev', 'automated_tests');
     final String flutterTestDirectory = fs.path.join(automatedTestsDirectory, 'flutter_test');
 
-    testUsingContext('Exception handling in test harness', () async {
+    testUsingContext('report nice errors for exceptions thrown within testWidgets()', () async {
       Cache.flutterRoot = '../..';
       return _testFile('exception_handling', automatedTestsDirectory, flutterTestDirectory);
     });
 
-    testUsingContext('TestAsyncUtils guarded function test', () async {
+    testUsingContext('report a nice error when a guarded function was called without await', () async {
       Cache.flutterRoot = '../..';
       return _testFile('test_async_utils_guarded', automatedTestsDirectory, flutterTestDirectory);
     });
 
-    testUsingContext('TestAsyncUtils unguarded function test', () async {
+    testUsingContext('report a nice error when an async function was called without await', () async {
       Cache.flutterRoot = '../..';
       return _testFile('test_async_utils_unguarded', automatedTestsDirectory, flutterTestDirectory);
     });
 
-    testUsingContext('Missing flutter_test dependency', () async {
+    testUsingContext('report a nice error when a pubspec.yaml is missing a flutter_test dependency', () async {
       final String missingDependencyTests = fs.path.join('..', '..', 'dev', 'missing_dependency_tests');
       Cache.flutterRoot = '../..';
       return _testFile('trivial', missingDependencyTests, missingDependencyTests);
     });
+
+    testUsingContext('run a test when its name matches a regexp', () async {
+      Cache.flutterRoot = '../..';
+      final ProcessResult result = await _runFlutterTest('filtering', automatedTestsDirectory, flutterTestDirectory,
+        extraArgs: const <String>["--name", "inc.*de"]);
+      if (!result.stdout.contains("+1: All tests passed"))
+        fail("unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n");        
+      expect(result.exitCode, 0);
+    });
+
+    testUsingContext('run a test when its name contains a string', () async {
+      Cache.flutterRoot = '../..';
+      final ProcessResult result = await _runFlutterTest('filtering', automatedTestsDirectory, flutterTestDirectory,
+        extraArgs: const <String>["--plain-name", "include"]);
+      if (!result.stdout.contains("+1: All tests passed"))
+        fail("unexpected output from test:\n\n${result.stdout}\n-- end stdout --\n\n");        
+      expect(result.exitCode, 0);
+    });
+
   }, skip: io.Platform.isWindows); // TODO(goderbauer): enable when sky_shell is available
 }
 
 Future<Null> _testFile(String testName, String workingDirectory, String testDirectory) async {
-  final String fullTestName = fs.path.join(testDirectory, '${testName}_test.dart');
-  final File testFile = fs.file(fullTestName);
-  expect(testFile.existsSync(), true);
   final String fullTestExpectation = fs.path.join(testDirectory, '${testName}_expectation.txt');
   final File expectationFile = fs.file(fullTestExpectation);
-  expect(expectationFile.existsSync(), true);
+  if (!expectationFile.existsSync())
+    fail("missing expectation file: $expectationFile");
 
   while (_testExclusionLock != null)
     await _testExclusionLock;
 
-  ProcessResult exec;
-  final Completer<Null> testExclusionCompleter = new Completer<Null>();
-  _testExclusionLock = testExclusionCompleter.future;
-  try {
-    exec = await Process.run(
-      fs.path.join(dartSdkPath, 'bin', 'dart'),
-      <String>[
-        fs.path.absolute(fs.path.join('bin', 'flutter_tools.dart')),
-        'test',
-        '--no-color',
-        fullTestName,
-      ],
-      workingDirectory: workingDirectory,
-    );
-  } finally {
-    _testExclusionLock = null;
-    testExclusionCompleter.complete();
-  }
+  final ProcessResult exec = await _runFlutterTest(testName, workingDirectory, testDirectory);
 
   expect(exec.exitCode, isNonZero);
   final List<String> output = exec.stdout.split('\n');
@@ -115,3 +115,34 @@
   if (!haveSeenStdErrMarker)
     expect(exec.stderr, '');
 }
+
+Future<ProcessResult> _runFlutterTest(String testName, String workingDirectory, String testDirectory,
+  {List<String> extraArgs = const <String>[]}) async {
+    
+  final String testFilePath = fs.path.join(testDirectory, '${testName}_test.dart');
+  final File testFile = fs.file(testFilePath);
+  if (!testFile.existsSync())
+    fail("missing test file: $testFile");
+
+  final List<String> args = <String>[
+    fs.path.absolute(fs.path.join('bin', 'flutter_tools.dart')),
+    'test',
+    '--no-color'
+  ]..addAll(extraArgs)..add(testFilePath);
+
+  while (_testExclusionLock != null)
+    await _testExclusionLock;
+
+  final Completer<Null> testExclusionCompleter = new Completer<Null>();
+  _testExclusionLock = testExclusionCompleter.future;
+  try {
+    return await Process.run(
+      fs.path.join(dartSdkPath, 'bin', 'dart'),
+      args,
+      workingDirectory: workingDirectory,
+    );
+  } finally {
+    _testExclusionLock = null;
+    testExclusionCompleter.complete();
+  }
+}