feat(mdtest doctor): mdtest is able to detect if any dependent tool is
not installed and provide tool installation instructions

mdtest provide doctor command which checks if Dart, Pub, Flutter, ADB,
lcov are installed and reports error as well as installation instruction
if any of those tools is not installed.  The doctor command also checks
if mobiledevice tool is installed if mdtest is running on Mac OS.
mobiledevice is a commandline tool similar to ADB but does not provide
as many features as ADB does.  However, it is the best commandline tool
that meets mdtest's need.  genhtml is used to generate code coverage
report and comes along with lcov.

Change-Id: Iea4c7d3227d3c9bca9f7b10025b7cb54e94ec617
diff --git a/mdtest/lib/executable.dart b/mdtest/lib/executable.dart
index d390341..f3a7e6b 100644
--- a/mdtest/lib/executable.dart
+++ b/mdtest/lib/executable.dart
@@ -8,6 +8,7 @@
 import 'package:args/command_runner.dart';
 import 'package:stack_trace/stack_trace.dart';
 
+import 'src/commands/doctor.dart';
 import 'src/commands/create.dart';
 import 'src/commands/run.dart';
 import 'src/commands/auto.dart';
@@ -17,6 +18,7 @@
 
 Future<Null> main(List<String> args) async {
   MDTestCommandRunner runner = new MDTestCommandRunner()
+    ..addCommand(new DoctorCommand())
     ..addCommand(new CreateCommand())
     ..addCommand(new RunCommand())
     ..addCommand(new AutoCommand())
diff --git a/mdtest/lib/src/commands/auto.dart b/mdtest/lib/src/commands/auto.dart
index 8101b2c..bf36e90 100644
--- a/mdtest/lib/src/commands/auto.dart
+++ b/mdtest/lib/src/commands/auto.dart
@@ -23,8 +23,10 @@
 
   @override
   final String description
-    = 'Automatically run applications based on a subset of spec to device '
-      'settings that maximize the device coverage';
+    = 'Automatically install and launch flutter applications on different '
+      'device groups which satisfy the test spec.  mdtest combines test result '
+      'for each round and report it to the user.  Each application is '
+      'guaranteed to be executed on each device group.';
 
   dynamic _specs;
 
@@ -83,7 +85,7 @@
       MDTestRunner runner = new MDTestRunner();
 
       if (await runner.runAllApps(deviceMapping) != 0) {
-        printError('Error when running applications on #Round $roundNum');
+        printError('Error when running applications on Round #$roundNum');
         await uninstallTestingApps(deviceMapping);
         errRounds.add(roundNum);
         printInfo('End of Round #$roundNum\n');
@@ -172,6 +174,7 @@
   }
 
   AutoCommand() {
+    usesBriefFlag();
     usesSpecsOption();
     usesCoverageFlag();
     usesTAPReportOption();
diff --git a/mdtest/lib/src/commands/doctor.dart b/mdtest/lib/src/commands/doctor.dart
new file mode 100644
index 0000000..961255d
--- /dev/null
+++ b/mdtest/lib/src/commands/doctor.dart
@@ -0,0 +1,98 @@
+// Copyright 2016 The Vanadium 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 'dart:io';
+
+import '../runner/mdtest_command.dart';
+import '../globals.dart';
+
+class DoctorCommand extends MDTestCommand {
+
+  @override
+  final String name = 'doctor';
+
+  @override
+  final String description = 'Check if all dependent tools are installed.';
+
+  @override
+  Future<int> runCore() async {
+    printInfo('Running "mdtest doctor command" ...');
+    if (os.isWindows) {
+      print('Windows platform is not supported.');
+      return 1;
+    }
+    int result = 0;
+    result += printDiagnoseMessage(
+      'dart',
+      'Please install dart following the link https://www.dartlang.org/install '
+      'and add dart/bin to your environment variable PATH.'
+    );
+    result += printDiagnoseMessage(
+      'pub',
+      'Please install dart following the link https://www.dartlang.org/install '
+      'and add dart/bin to your environment variable PATH.'
+    );
+    result += printDiagnoseMessage(
+      'flutter',
+      'Please install flutter following the link https://flutter.io/setup/ '
+      'and add flutter/bin to your environment variable PATH.  Ideally '
+      '`flutter doctor` should not report any problem.'
+    );
+    result += printDiagnoseMessage(
+      'adb',
+      'Please install Android SDK following the link '
+      'https://developer.android.com/studio/intro/update.html '
+      'and add Android/Sdk/platform-tools to your environment variable PATH.'
+    );
+
+    if (os.isMacOS) {
+      result += printDiagnoseMessage(
+        'brew',
+        'Please install homebrew following the link http://brew.sh/.'
+      );
+      result += printDiagnoseMessage(
+        'lcov',
+        'Please install lcov using `brew install lcov`.'
+      );
+      result += printDiagnoseMessage(
+        'mobiledevice',
+        'Please install mobiledevice following the link '
+        'https://github.com/imkira/mobiledevice.'
+      );
+    }
+
+    if (os.isLinux) {
+      result += printDiagnoseMessage(
+        'lcov',
+        'Please install lcov using `sudo apt-get lcov`.'
+      );
+    }
+
+    if (result > 0) {
+      bool singleSyntax = result == 1;
+      printError(
+        'Some tool${singleSyntax ? '' : 's'} that mdtest '
+        'depend${singleSyntax ? 's' : ''} on ${singleSyntax ? 'is' : 'are'} '
+        'not installed.  Please follow the instructions above and resolve '
+        'all problems before using mdtest.'
+      );
+      return 1;
+    }
+    return 0;
+  }
+}
+
+/// Print diagnose message for the given executable and instructions on how
+/// to install the tool if it is not detected or installed.  Returns 0 if
+/// the tool is installed and properly configured, 1 otherwise.
+int printDiagnoseMessage(String exec, String instructions) {
+  File execFile = os.which(exec);
+  if (execFile == null) {
+    print('[x] $exec is not found.  $instructions');
+    return 1;
+  }
+  print('[✓] $exec found in ${execFile.path}.');
+  return 0;
+}
diff --git a/mdtest/lib/src/commands/run.dart b/mdtest/lib/src/commands/run.dart
index 852259e..b640545 100644
--- a/mdtest/lib/src/commands/run.dart
+++ b/mdtest/lib/src/commands/run.dart
@@ -21,7 +21,9 @@
   final String name = 'run';
 
   @override
-  final String description = 'Run multi-device driver tests';
+  final String description
+    = 'Install and launch flutter applications on multiple devices and '
+      'execute test script to collect and report test results to the user.';
 
   dynamic _specs;
 
@@ -112,6 +114,7 @@
   }
 
   RunCommand() {
+    usesBriefFlag();
     usesSpecsOption();
     usesCoverageFlag();
     usesTAPReportOption();
diff --git a/mdtest/lib/src/runner/mdtest_command.dart b/mdtest/lib/src/runner/mdtest_command.dart
index 7a5b3f0..b666227 100644
--- a/mdtest/lib/src/runner/mdtest_command.dart
+++ b/mdtest/lib/src/runner/mdtest_command.dart
@@ -7,6 +7,7 @@
 
 import 'package:args/command_runner.dart';
 
+import '../base/logger.dart';
 import '../globals.dart';
 import 'mdtest_command_runner.dart';
 
@@ -21,12 +22,23 @@
   @override
   MDTestCommandRunner get runner => super.runner;
 
+  bool _usesBriefFlag = false;
   bool _usesSpecsOption = false;
   bool _usesSpecTemplateOption = false;
   bool _usesTestTemplateOption = false;
   bool _usesSaveTestReportOption = false;
   bool _usesReportTypeOption = false;
 
+  void usesBriefFlag() {
+    argParser.addFlag(
+      'brief',
+      abbr: 'b',
+      negatable: false,
+      help: 'Disable logging, only report test execution result.'
+    );
+    _usesBriefFlag = true;
+  }
+
   void usesSpecsOption() {
     argParser.addOption(
       'spec',
@@ -120,6 +132,17 @@
   Validator commandValidator;
 
   bool _commandValidator() {
+    if (_usesBriefFlag) {
+      if (globalResults['verbose'] && argResults['brief']) {
+        printError('--verbose flag conflicts with --brief flag');
+        return false;
+      }
+      if (argResults['brief']) {
+        defaultLogger = new DumbLogger();
+        briefMode = true;
+      }
+    }
+
     if (_usesSpecsOption) {
       String specsPath = argResults['spec'];
       if (specsPath == null) {
diff --git a/mdtest/lib/src/runner/mdtest_command_runner.dart b/mdtest/lib/src/runner/mdtest_command_runner.dart
index e5c9981..965653a 100644
--- a/mdtest/lib/src/runner/mdtest_command_runner.dart
+++ b/mdtest/lib/src/runner/mdtest_command_runner.dart
@@ -22,12 +22,6 @@
       help: 'Noisy logging, including detailed information '
             'through the entire execution.'
     );
-    argParser.addFlag(
-      'brief',
-      abbr: 'b',
-      negatable: false,
-      help: 'Disable logging, only report test execution output.'
-    );
   }
 
   @override
@@ -39,24 +33,10 @@
 
   @override
   Future<int> runCommand(ArgResults globalResults) async {
-    if (!_commandValidator(globalResults)) {
-      return 1;
-    }
     if (globalResults['verbose']) {
       defaultLogger = new VerboseLogger();
     }
-    if (globalResults['brief']) {
-      defaultLogger = new DumbLogger();
-      briefMode = true;
-    }
-    return await super.runCommand(globalResults);
-  }
 
-  bool _commandValidator(ArgResults globalResults) {
-    if (globalResults['verbose'] && globalResults['brief']) {
-      printError('--verbose flag conflicts with --brief flag');
-      return false;
-    }
-    return true;
+    return await super.runCommand(globalResults);
   }
 }