feat(mdtest driver api): provide mdtest driver API that allows users to
access flutter drivers by device nickname

mdtest provides DriverMap class that helps users to access flutter
drivers through device nickname.  The API supports lazy initialization
which means the flutter driver instance will only be initialized the
first time the user wants to access it.  Singleton and factory pattern
is used to prevent redundant readings of the temporary spec file.

Add some code for previous submitted feature.  mdtest should now support
'os-version' and 'api-level' in the test spec.

Change code coverage report location.

Change-Id: I8a64629060071f993e15a64e6c08833211c1f96b
diff --git a/mdtest/lib/driver_util.dart b/mdtest/lib/driver_util.dart
index ae8ea2d..c53c0b6 100644
--- a/mdtest/lib/driver_util.dart
+++ b/mdtest/lib/driver_util.dart
@@ -1,39 +1,6 @@
 // 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.
+library driver_util;
 
-import 'dart:async';
-import 'dart:io';
-import 'dart:convert' show JSON;
-
-import 'package:flutter_driver/flutter_driver.dart';
-
-import 'src/base/common.dart';
-import 'src/globals.dart';
-
-class DriverUtil {
-  static Future<FlutterDriver> connectByName(String deviceNickname) async {
-    // read the temp spec file to find the device nickname -> observatory port
-    // mapping
-    Directory systemTempDir = Directory.systemTemp;
-    File tempFile = new File('${systemTempDir.path}/$defaultTempSpecsName');
-    // if temp spec file is not found, report error and exit
-    if(!await tempFile.exists()) {
-      printError('Multi-Drive temporary specs file not found.');
-      exit(1);
-    }
-    // decode specs
-    dynamic configs = JSON.decode(await tempFile.readAsString());
-    // report error if nickname is not found
-    if(!configs.containsKey(deviceNickname)) {
-      printError('Device nickname $deviceNickname not found.');
-      exit(1);
-    }
-    // read device id and observatory port
-    String deviceID = configs[deviceNickname]['device-id'];
-    String observatoryUrl = configs[deviceNickname]['observatory-url'];
-    printInfo('$deviceNickname refers to device $deviceID running on url $observatoryUrl');
-    // delegate to flutter driver connect method
-    return await FlutterDriver.connect(dartVmServiceUrl: '$observatoryUrl');
-  }
-}
+export 'src/api/driver_map.dart';
diff --git a/mdtest/lib/src/api/driver_map.dart b/mdtest/lib/src/api/driver_map.dart
new file mode 100644
index 0000000..08e1de0
--- /dev/null
+++ b/mdtest/lib/src/api/driver_map.dart
@@ -0,0 +1,88 @@
+// 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 'dart:convert' show JSON;
+import 'dart:collection';
+
+import 'package:flutter_driver/flutter_driver.dart';
+
+import '../base/common.dart';
+
+/// Singleton pattern is used to load config data only once.
+class Meta {
+  dynamic _config;
+
+  static Meta instance;
+
+  factory Meta() {
+    if (instance == null) {
+      // Read the temp spec file to find the device nickname -> observatory port
+      // mapping.
+      Directory systemTempDir = Directory.systemTemp;
+      File tempFile = new File('${systemTempDir.path}/$defaultTempSpecsName');
+      // if temp spec file is not found, report error and exit.
+      if(!tempFile.existsSync()) {
+        stderr.writeln('mdtest temporary specs file not found.');
+        exit(1);
+      }
+      // Decode specs assuming no exception, because the temp file is
+      // generated by mdtest.
+      dynamic config = JSON.decode(tempFile.readAsStringSync());
+      instance = new Meta._internal(config);
+    }
+    return instance;
+  }
+
+  dynamic get config => instance._config;
+
+  Meta._internal(this._config);
+}
+
+class DriverMap extends UnmodifiableMapBase<String, Future<FlutterDriver>> {
+
+  Meta _meta;
+  Map<String, FlutterDriver> _map;
+
+  DriverMap() {
+    _meta = new Meta();
+    _map = <String, FlutterDriver>{};
+  }
+
+  /// Lazy initializing flutter driver given the nickname.  If the nickname
+  /// does not exist in the test spec, report error and return null.  Otherwise,
+  /// if the flutter driver associated with the nickname is not initialized,
+  /// initialize the driver.  If the flutter driver is initialized, return it.
+  @override
+  Future<FlutterDriver> operator [](String nickname) async {
+    dynamic config = _meta.config;
+    if (config.containsKey(nickname)) {
+      if (!_map.containsKey(nickname)) {
+        String observatoryUrl = config[nickname]['observatory-url'];
+        // Delegate to flutter driver connect method.
+        FlutterDriver driver
+          = await FlutterDriver.connect(dartVmServiceUrl: '$observatoryUrl');
+        _map[nickname] = driver;
+        return driver;
+      }
+      return _map[nickname];
+    }
+    stderr.writeln('Device nickname $nickname not found.');
+    return null;
+  }
+
+  @override
+  Iterable<String> get keys => _meta.config.keys;
+
+  /// Close all connected drivers.
+  void closeAll() {
+    keys.forEach((String nickname) async {
+      FlutterDriver driver = await this[nickname];
+      if (driver != null) {
+        await driver.close();
+      }
+    });
+  }
+}
diff --git a/mdtest/lib/src/base/common.dart b/mdtest/lib/src/base/common.dart
index de1d4d3..a43c389 100644
--- a/mdtest/lib/src/base/common.dart
+++ b/mdtest/lib/src/base/common.dart
@@ -3,4 +3,4 @@
 // license that can be found in the LICENSE file.
 
 const String defaultTempSpecsName = 'tmp.spec';
-const String defaultCodeCoverageDirectoryPath = 'coverage/code_coverage';
+const String defaultCodeCoverageDirectoryPath = 'coverage';
diff --git a/mdtest/lib/src/mobile/device_spec.dart b/mdtest/lib/src/mobile/device_spec.dart
index 77f604f..251eba0 100644
--- a/mdtest/lib/src/mobile/device_spec.dart
+++ b/mdtest/lib/src/mobile/device_spec.dart
@@ -37,6 +37,8 @@
     List<String> checkedProperties = [
       'device-id',
       'model-name',
+      'os-version',
+      'api-level',
       'screen-size'
     ];
     return checkedProperties.every(