feat(mdtest run/auto): support specifying test script paths from command
line arguments.

mdtest now supports reading test suites paths from command line
arguments.  It will treat anything besides the supported command line
flag/option as the path pattern for the test scripts.  Users can specify
multiple glob patterns for their test scripts.

Now mdtest is able to find test script paths from both test spec and
command line arguments.  Users can specify glob patterns in both test spec
or command line.  The convention is that relatively stable test script
paths should be specified in the test spec and other frequently modified
test script should be specified from the command line.

The behavior of the current implementation is that test paths in the
test spec will be executed before test paths specified in the command line.
Duplicated test paths will be ignored and only the first test path
position is kept.  If a user specifies multiple glob patterns, the test
paths corresponding to each glob pattern will be executed in the order
of their appearance.  However, the order of executions for test paths
within the same glob pattern is not guaranteed.

Change-Id: Ibefc789e4c17cbf12ecd054171e95c1099d6f0de
diff --git a/mdtest/lib/src/commands/auto.dart b/mdtest/lib/src/commands/auto.dart
index 83dfe9a..88323fe 100644
--- a/mdtest/lib/src/commands/auto.dart
+++ b/mdtest/lib/src/commands/auto.dart
@@ -32,7 +32,7 @@
   Future<int> runCore() async {
     printInfo('Running "mdtest auto command" ...');
 
-    this._specs = await loadSpecs(argResults['specs']);
+    this._specs = await loadSpecs(argResults);
 
     this._devices = await getDevices();
     if (_devices.isEmpty) {
diff --git a/mdtest/lib/src/commands/run.dart b/mdtest/lib/src/commands/run.dart
index 089ac43..07d332e 100644
--- a/mdtest/lib/src/commands/run.dart
+++ b/mdtest/lib/src/commands/run.dart
@@ -29,7 +29,7 @@
   Future<int> runCore() async {
     printInfo('Running "mdtest run command" ...');
 
-    this._specs = await loadSpecs(argResults['specs']);
+    this._specs = await loadSpecs(argResults);
     printTrace(_specs.toString());
 
     this._devices = await getDevices();
diff --git a/mdtest/lib/src/mobile/device_spec.dart b/mdtest/lib/src/mobile/device_spec.dart
index 59a18c8..8522159 100644
--- a/mdtest/lib/src/mobile/device_spec.dart
+++ b/mdtest/lib/src/mobile/device_spec.dart
@@ -6,6 +6,8 @@
 import 'dart:convert';
 import 'dart:io';
 
+import 'package:args/args.dart';
+
 import 'device.dart';
 import 'key_provider.dart';
 import '../globals.dart';
@@ -62,18 +64,25 @@
                        'app path: $appPath>';
 }
 
-Future<dynamic> loadSpecs(String specsPath) async {
+Future<dynamic> loadSpecs(ArgResults argResults) async {
+  String specsPath = argResults['specs'];
   try {
     // Read specs file into json format
     dynamic newSpecs = JSON.decode(await new File(specsPath).readAsString());
     // Get the parent directory of the specs file
     String rootPath = new File(specsPath).parent.absolute.path;
-    // Normalize the 'test-path' in the specs file
-    // newSpecs['test-path'] = normalizePath(rootPath, newSpecs['test-path']);
-    newSpecs['test-paths']
-      = newSpecs['test-paths'].map(
-        (String testPath) => normalizePath(rootPath, testPath)
-      );
+    // Normalize the 'test-path' in the specs file and add extra test paths
+    // from the command line argument
+    List<String> testPathsFromSpec
+      = listFilePathsFromGlobPatterns(rootPath, newSpecs['test-paths']);
+    print('Test paths from spec: $testPathsFromSpec');
+    List<String> testPathsFromCommandLine
+      = listFilePathsFromGlobPatterns(Directory.current.path, argResults.rest);
+    print('Test paths from command line: $testPathsFromCommandLine');
+    newSpecs['test-paths'] = mergeWithoutDuplicate(
+      testPathsFromSpec,
+      testPathsFromCommandLine
+    );
     // Normalize the 'app-path' in the specs file
     newSpecs['devices'].forEach((String name, Map<String, String> map) {
       map['app-path'] = normalizePath(rootPath, map['app-path']);
diff --git a/mdtest/lib/src/test/reporter.dart b/mdtest/lib/src/test/reporter.dart
index 0a87559..d7f8a43 100644
--- a/mdtest/lib/src/test/reporter.dart
+++ b/mdtest/lib/src/test/reporter.dart
@@ -38,7 +38,6 @@
       '1..$currentTestNum\n'
       '# tests $currentTestNum\n'
       '# pass $passingTestsNum\n'
-      '\n'
     );
   }
 
diff --git a/mdtest/lib/src/util.dart b/mdtest/lib/src/util.dart
index f195760..2840353 100644
--- a/mdtest/lib/src/util.dart
+++ b/mdtest/lib/src/util.dart
@@ -6,6 +6,7 @@
 import 'dart:math';
 
 import 'package:path/path.dart' as path;
+import 'package:glob/glob.dart';
 
 import 'globals.dart';
 
@@ -70,3 +71,41 @@
     i++;
   }
 }
+
+/// Return the absolute paths of a list of files based on a list of glob
+/// patterns.  The order of the result follow the order of the given glob
+/// patterns, but the order of file paths corresponding to the same glob
+/// pattern is not guranteed.
+List<String> listFilePathsFromGlobPatterns(
+  String rootPath,
+  Iterable<String> globPatterns
+) {
+  List<String> result = <String>[];
+  if (globPatterns == null) {
+    return result;
+  }
+  Set<String> seen = new Set<String>();
+  for (String globPattern in globPatterns) {
+    Glob fileGlob = new Glob(globPattern);
+    Iterable<String> filePaths = fileGlob.listSync().map(
+      (FileSystemEntity file) => normalizePath(rootPath, file.path)
+    );
+    Set<String> neverSeen = new Set.from(filePaths).difference(seen);
+    result.addAll(neverSeen);
+    seen.addAll(neverSeen);
+  }
+  return result;
+}
+
+/// Merge two iterables into a list and remove duplicates.  The order is kept
+/// where elements in [first] appear before elements in [second] and the order
+/// inside each iterable is also kept.  [first] and [second] must not contain
+/// any duplicate item.
+List<String> mergeWithoutDuplicate(
+  Iterable<String> first,
+  Iterable<String> second
+) {
+  List<String> result = new List.from(first);
+  result.addAll(second.where((String e) => !first.contains(e)));
+  return result;
+}
diff --git a/mdtest/pubspec.yaml b/mdtest/pubspec.yaml
index ac93572..4ad70a0 100644
--- a/mdtest/pubspec.yaml
+++ b/mdtest/pubspec.yaml
@@ -5,6 +5,7 @@
   args: ^0.13.4
   stack_trace: ^1.4.0
   coverage: ^0.7.9
+  glob: ">=1.1.3"
   dlog:
     path: ../../../../third_party/dart/dlog
   flutter_driver: