feat(mdtest auto): add more options to group devices

mdtest supports --groupby option which allows users to provide grouping
keyword for mdtest to group devices and run tests under different device
settings based on app-device coverage.  --groupby supports options like
'device-id', 'model-name', 'os-version', 'api-level' and 'screen-size'.

Fix minor bug for mdtest script to deal with the first run.

Change-Id: I54a6b91fcd40d995e6413b3d556c3c1fc526c6a5
diff --git a/mdtest/bin/mdtest b/mdtest/bin/mdtest
index 3c2f60f..574e1f4 100755
--- a/mdtest/bin/mdtest
+++ b/mdtest/bin/mdtest
@@ -38,6 +38,8 @@
   exit 0
 fi
 
+mkdir -p "$MDTEST_TOOL/bin/cache"
+
 REVISION=`(cd "$MDTEST_TOOL"; git rev-parse HEAD)`
 if [ ! -f "$SNAPSHOT_PATH" ] || [ ! -f "$STAMP_PATH" ] || [ `cat "$STAMP_PATH"` != "$REVISION" ] || [ "$MDTEST_TOOL/pubspec.yaml" -nt "$MDTEST_TOOL/pubspec.lock" ]; then
   echo Building mdtest ...
diff --git a/mdtest/lib/src/algorithms/coverage.dart b/mdtest/lib/src/algorithms/coverage.dart
index 6791af3..32e79bc 100644
--- a/mdtest/lib/src/algorithms/coverage.dart
+++ b/mdtest/lib/src/algorithms/coverage.dart
@@ -8,13 +8,13 @@
 import '../mobile/device_spec.dart' show DeviceSpec;
 import '../util.dart';
 
-class ClusterInfo {
+class GroupInfo {
   Map<String, List<Device>> _deviceClusters;
   Map<String, List<DeviceSpec>> _deviceSpecClusters;
   List<String> _deviceClustersOrder;
   List<String> _deviceSpecClustersOrder;
 
-  ClusterInfo(
+  GroupInfo(
     Map<String, List<Device>> deviceClusters,
     Map<String, List<DeviceSpec>> deviceSpecClusters
   ) {
@@ -39,7 +39,7 @@
     }
   }
 
-  ClusterInfo clusterInfo;
+  GroupInfo clusterInfo;
   // Coverage matrix, where a row indicats an app cluster and a column
   // indicates a device cluster
   List<List<int>> matrix;
@@ -47,9 +47,9 @@
   void fill(Map<DeviceSpec, Device> match) {
     match.forEach((DeviceSpec spec, Device device) {
       int rowNum = clusterInfo.deviceSpecClustersOrder
-                              .indexOf(spec.clusterKey());
+                              .indexOf(spec.groupKey());
       int colNum = clusterInfo.deviceClustersOrder
-                              .indexOf(device.clusterKey());
+                              .indexOf(device.groupKey());
       matrix[rowNum][colNum] = 1;
     });
   }
@@ -78,7 +78,7 @@
 
 Map<CoverageMatrix, Map<DeviceSpec, Device>> buildCoverage2MatchMapping(
   List<Map<DeviceSpec, Device>> allMatches,
-  ClusterInfo clusterInfo
+  GroupInfo clusterInfo
 ) {
   Map<CoverageMatrix, Map<DeviceSpec, Device>> cov2match
     = <CoverageMatrix, Map<DeviceSpec, Device>>{};
@@ -97,7 +97,7 @@
 /// [ref link]: https://en.wikipedia.org/wiki/Set_cover_problem
 Set<Map<DeviceSpec, Device>> findMinimumMappings(
   Map<CoverageMatrix, Map<DeviceSpec, Device>> cov2match,
-  ClusterInfo clusterInfo
+  GroupInfo clusterInfo
 ) {
   Set<CoverageMatrix> minSet = new Set<CoverageMatrix>();
   CoverageMatrix base = new CoverageMatrix(clusterInfo);
diff --git a/mdtest/lib/src/algorithms/matching.dart b/mdtest/lib/src/algorithms/matching.dart
index 5f97fc8..447cfea 100644
--- a/mdtest/lib/src/algorithms/matching.dart
+++ b/mdtest/lib/src/algorithms/matching.dart
@@ -142,9 +142,9 @@
   for (Map<DeviceSpec, Device> match in matches) {
     sb.writeln('Round $roundNum:');
     match.forEach((DeviceSpec spec, Device device) {
-      sb.writeln('[Spec Cluster Key: ${spec.clusterKey()}]'
+      sb.writeln('[Spec Cluster Key: ${spec.groupKey()}]'
                  ' -> '
-                 '[Device Cluster Key: ${device.clusterKey()}]');
+                 '[Device Cluster Key: ${device.groupKey()}]');
     });
     roundNum++;
   }
diff --git a/mdtest/lib/src/commands/auto.dart b/mdtest/lib/src/commands/auto.dart
index 088d8c2..1fe236d 100644
--- a/mdtest/lib/src/commands/auto.dart
+++ b/mdtest/lib/src/commands/auto.dart
@@ -33,12 +33,12 @@
     printInfo('Running "mdtest auto command" ...');
 
     this._specs = await loadSpecs(argResults);
-    if (sanityCheckSpecs(_specs, argResults['spec']) != 0) {
+    if (sanityCheckSpecs(_specs, argResults['specs']) != 0) {
       printError('Test spec does not meet requirements.');
       return 1;
     }
 
-    this._devices = await getDevices();
+    this._devices = await getDevices(groupKey: argResults['groupby']);
     if (_devices.isEmpty) {
       printError('No device found.');
       return 1;
@@ -59,7 +59,7 @@
     Map<String, List<DeviceSpec>> deviceSpecClusters
       = buildCluster(allDeviceSpecs);
 
-    ClusterInfo clusterInfo = new ClusterInfo(deviceClusters, deviceSpecClusters);
+    GroupInfo clusterInfo = new GroupInfo(deviceClusters, deviceSpecClusters);
     Map<CoverageMatrix, Map<DeviceSpec, Device>> cov2match
       = buildCoverage2MatchMapping(allDeviceMappings, clusterInfo);
     Set<Map<DeviceSpec, Device>> chosenMappings
@@ -132,5 +132,11 @@
     usesSpecsOption();
     usesCoverageFlag();
     usesTAPReportOption();
+    argParser.addOption('groupby',
+      defaultsTo: 'device-id',
+      allowed: ['device-id', 'model-name', 'os-version', 'api-level', 'screen-size'],
+      help: 'Device property used to group devices to'
+            'adjust app-device coverage criterion.'
+    );
   }
 }
diff --git a/mdtest/lib/src/mobile/device.dart b/mdtest/lib/src/mobile/device.dart
index 6629eec..c69fd74 100644
--- a/mdtest/lib/src/mobile/device.dart
+++ b/mdtest/lib/src/mobile/device.dart
@@ -8,32 +8,40 @@
 import 'key_provider.dart';
 import 'android.dart';
 
-class Device implements ClusterKeyProvider {
+class Device implements GroupKeyProvider {
   Device({
-    this.properties
-  });
+    this.properties,
+    String groupKey
+  }) {
+    this._groupKey = groupKey;
+  }
 
   Map<String, String> properties;
+  String _groupKey;
 
   String get id => properties['device-id'];
   String get modelName => properties['model-name'];
   String get screenSize => properties['screen-size'];
+  String get osVersion => properties['os-version'];
+  String get apiLevel => properties['api-level'];
 
+  /// default to 'device-id'
   @override
-  String clusterKey() {
-    return id;
+  String groupKey() {
+    return properties[_groupKey ?? 'device-id'];
   }
 
   @override
   String toString()
-    => '<id: $id, model-name: $modelName, screen-size: $screenSize>';
+    => '<device-id: $id, model-name: $modelName, screen-size: $screenSize, '
+       'os-version: $osVersion, api-level: $apiLevel>';
 }
 
-Future<List<Device>> getDevices() async {
+Future<List<Device>> getDevices({String groupKey}) async {
   List<Device> devices = <Device>[];
   await _getDeviceIDs().then((List<String> ids) async {
     for(String id in ids) {
-      devices.add(await _collectDeviceProps(id));
+      devices.add(await _collectDeviceProps(id, groupKey: groupKey));
     }
   });
   return devices;
@@ -73,12 +81,15 @@
   return deviceIDs;
 }
 
-Future<Device> _collectDeviceProps(String deviceID) async {
+Future<Device> _collectDeviceProps(String deviceID, {String groupKey}) async {
   return new Device(
     properties: <String, String> {
       'device-id': deviceID,
       'model-name': await getProperty(deviceID, 'ro.product.model'),
+      'os-version': await getProperty(deviceID, 'ro.build.version.release'),
+      'api-level': await getProperty(deviceID, 'ro.build.version.sdk'),
       'screen-size': await getScreenSize(deviceID)
-    }
+    },
+    groupKey: groupKey
   );
 }
diff --git a/mdtest/lib/src/mobile/device_spec.dart b/mdtest/lib/src/mobile/device_spec.dart
index 2f7e924..77f604f 100644
--- a/mdtest/lib/src/mobile/device_spec.dart
+++ b/mdtest/lib/src/mobile/device_spec.dart
@@ -13,7 +13,7 @@
 import '../globals.dart';
 import '../util.dart';
 
-class DeviceSpec implements ClusterKeyProvider {
+class DeviceSpec implements GroupKeyProvider {
   DeviceSpec(String nickname, { this.specProperties }) {
     specProperties['nickname'] = nickname;
   }
@@ -51,7 +51,7 @@
   }
 
   @override
-  String clusterKey() {
+  String groupKey() {
     return appPath;
   }
 
diff --git a/mdtest/lib/src/mobile/key_provider.dart b/mdtest/lib/src/mobile/key_provider.dart
index 3974539..d0a9dc1 100644
--- a/mdtest/lib/src/mobile/key_provider.dart
+++ b/mdtest/lib/src/mobile/key_provider.dart
@@ -2,14 +2,15 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-abstract class ClusterKeyProvider {
-  String clusterKey();
+abstract class GroupKeyProvider {
+  /// key to group devices or specs
+  String groupKey();
 }
 
 Map<String, List<dynamic>> buildCluster(List<dynamic> elements) {
   Map<String, List<dynamic>> clusters = <String, List<dynamic>>{};
   elements.forEach((dynamic element) {
-    clusters.putIfAbsent(element.clusterKey(), () => <dynamic>[])
+    clusters.putIfAbsent(element.groupKey(), () => <dynamic>[])
             .add(element);
   });
   return clusters;