test(mdtest algorithms): add tests for the key algorithms in mdtest.

Add test suites for some key algorithms in mdtest.  Test the device spec
matches() function, the grouping algorithm, matching algorithm and
coverage algorithm.

Change-Id: I0f1b43e004443200b14aff2a0d915108fbe3c5c6
diff --git a/mdtest/lib/src/algorithms/matching.dart b/mdtest/lib/src/algorithms/matching.dart
index 1951b68..3072c21 100644
--- a/mdtest/lib/src/algorithms/matching.dart
+++ b/mdtest/lib/src/algorithms/matching.dart
@@ -14,7 +14,8 @@
 /// Find all matched devices for each device spec
 Map<DeviceSpec, Set<Device>> findIndividualMatches(
   List<DeviceSpec> deviceSpecs,
-  List<Device> devices) {
+  List<Device> devices
+) {
   Map<DeviceSpec, Set<Device>> individualMatches
     = new Map<DeviceSpec, Set<Device>>();
   for(DeviceSpec deviceSpecs in deviceSpecs) {
@@ -31,7 +32,8 @@
 /// Return the first device spec to device matching, null if no such matching
 Map<DeviceSpec, Device> findMatchingDeviceMapping(
   List<DeviceSpec> deviceSpecs,
-  Map<DeviceSpec, Set<Device>> individualMatches) {
+  Map<DeviceSpec, Set<Device>> individualMatches
+) {
   Map<DeviceSpec, Device> deviceMapping = <DeviceSpec, Device>{};
   Set<Device> visited = new Set<Device>();
   if (!_findMatchingDeviceMapping(0, deviceSpecs, individualMatches,
diff --git a/mdtest/lib/assets/locator.dart b/mdtest/lib/src/report/locator.dart
similarity index 87%
rename from mdtest/lib/assets/locator.dart
rename to mdtest/lib/src/report/locator.dart
index 5692bd8..a75a290 100644
--- a/mdtest/lib/assets/locator.dart
+++ b/mdtest/lib/src/report/locator.dart
@@ -4,8 +4,9 @@
 
 import 'dart:io';
 
-import '../src/util.dart';
+import 'package:mdtest/src/util.dart';
 
+// Provide paths which point to the assets directory
 String mdtestScriptPath = Platform.script.toFilePath();
 int binStart = mdtestScriptPath.lastIndexOf('bin');
 String mdtestRootPath = mdtestScriptPath.substring(0, binStart);
diff --git a/mdtest/lib/src/report/test_report.dart b/mdtest/lib/src/report/test_report.dart
index 9934a4c..9a3a43d 100644
--- a/mdtest/lib/src/report/test_report.dart
+++ b/mdtest/lib/src/report/test_report.dart
@@ -8,7 +8,7 @@
 import 'report.dart';
 import '../globals.dart';
 import '../util.dart';
-import '../../assets/locator.dart';
+import '../report/locator.dart';
 
 class TestReport extends Report {
   HitmapInfo hitmapInfo;
diff --git a/mdtest/pubspec.yaml b/mdtest/pubspec.yaml
index 193478b..0bf4533 100644
--- a/mdtest/pubspec.yaml
+++ b/mdtest/pubspec.yaml
@@ -7,8 +7,11 @@
   coverage: ^0.7.9
   glob: ">=1.1.3"
   intl: ">=0.12.4+2"
-  pub_semver: "1.3.0"
+  pub_semver: 1.3.0
   dlog:
     path: ../../../../third_party/dart/dlog
   flutter_driver:
     path: ../deps/flutter/packages/flutter_driver
+dev_dependencies:
+   test: ^0.12.15+1
+   mockito: 1.0.0
diff --git a/mdtest/test/coverage_test.dart b/mdtest/test/coverage_test.dart
new file mode 100644
index 0000000..974fd18
--- /dev/null
+++ b/mdtest/test/coverage_test.dart
@@ -0,0 +1,399 @@
+// 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 'package:mdtest/src/mobile/device_spec.dart';
+import 'package:mdtest/src/mobile/device.dart';
+import 'package:mdtest/src/mobile/key_provider.dart';
+import 'package:mdtest/src/algorithms/coverage.dart';
+import 'package:mdtest/src/algorithms/matching.dart';
+
+import 'package:test/test.dart';
+import 'package:mockito/mockito.dart';
+
+import 'src/mocks.dart';
+
+void main() {
+  group('coverage matrix', () {
+    test('0 reachable paths', () {
+      MockCoverageMatrix baseMatrix = new MockCoverageMatrix();
+      when(baseMatrix.matrix).thenReturn(
+        [
+          [cannotBeCovered, cannotBeCovered, cannotBeCovered],
+          [cannotBeCovered, cannotBeCovered, cannotBeCovered],
+          [cannotBeCovered, cannotBeCovered, cannotBeCovered]
+        ]
+      );
+      MockCoverageMatrix newMatrix = new MockCoverageMatrix();
+      when(newMatrix.matrix).thenReturn(
+        [
+          [cannotBeCovered, cannotBeCovered, cannotBeCovered],
+          [cannotBeCovered, cannotBeCovered, cannotBeCovered],
+          [cannotBeCovered, cannotBeCovered, cannotBeCovered]
+        ]
+      );
+      int reward = computeReward(baseMatrix, newMatrix);
+      expect(reward, equals(0));
+    });
+
+    test('3 reachable paths', () {
+      MockCoverageMatrix baseMatrix = new MockCoverageMatrix();
+      when(baseMatrix.matrix).thenReturn(
+        [
+          [cannotBeCovered, cannotBeCovered, cannotBeCovered],
+          [cannotBeCovered, cannotBeCovered, cannotBeCovered],
+          [cannotBeCovered, cannotBeCovered, cannotBeCovered]
+        ]
+      );
+      MockCoverageMatrix newMatrix = new MockCoverageMatrix();
+      when(newMatrix.matrix).thenReturn(
+        [
+          [cannotBeCovered, cannotBeCovered, isNotCovered],
+          [cannotBeCovered, isNotCovered, cannotBeCovered],
+          [cannotBeCovered, isNotCovered, cannotBeCovered]
+        ]
+      );
+      int reward = computeReward(baseMatrix, newMatrix);
+      expect(reward, equals(3));
+    });
+
+    test('all reachable paths', () {
+      MockCoverageMatrix baseMatrix = new MockCoverageMatrix();
+      when(baseMatrix.matrix).thenReturn(
+        [
+          [cannotBeCovered, cannotBeCovered, cannotBeCovered],
+          [cannotBeCovered, cannotBeCovered, cannotBeCovered],
+          [cannotBeCovered, cannotBeCovered, cannotBeCovered]
+        ]
+      );
+      MockCoverageMatrix newMatrix = new MockCoverageMatrix();
+      when(newMatrix.matrix).thenReturn(
+        [
+          [isNotCovered, isNotCovered, isNotCovered],
+          [isNotCovered, isNotCovered, isNotCovered],
+          [isNotCovered, isNotCovered, isNotCovered]
+        ]
+      );
+      int reward = computeReward(baseMatrix, newMatrix);
+      expect(reward, equals(9));
+    });
+  });
+
+  group('coverage algorithm', () {
+    test('with device id as device key', () {
+      List<DeviceSpec> specs = <DeviceSpec>[
+        new DeviceSpec(
+          'Alice',
+          specProperties: {
+            'app-root': 'xxx',
+            'app-path': 'yyy'
+          }
+        ),
+        new DeviceSpec(
+          'Bob',
+          specProperties: {
+            'app-root': 'xxx',
+            'app-path': 'zzz'
+          }
+        )
+      ];
+      List<Device> devices = <Device>[
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '123',
+            'model-name': 'Nexus 9',
+            'os-version': '6.0.1',
+            'screen-size': 'xlarge'
+          },
+          groupKey: 'device-id'
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'ios',
+            'device-id': '456',
+            'model-name': 'iPhone 6S',
+            'os-version': '9.3.2',
+            'screen-size': 'large'
+          },
+          groupKey: 'device-id'
+        )
+      ];
+      Map<DeviceSpec, Set<Device>> individualMatches
+        = findIndividualMatches(specs, devices);
+      List<Map<DeviceSpec, Device>> allDeviceMappings
+        = findAllMatchingDeviceMappings(specs, individualMatches);
+      Map<String, List<Device>> deviceGroups = buildGroups(devices);
+      Map<String, List<DeviceSpec>> specGroups = buildGroups(specs);
+      GroupInfo groupInfo = new GroupInfo(deviceGroups, specGroups);
+      Map<CoverageMatrix, Map<DeviceSpec, Device>> cov2match
+        = buildCoverage2MatchMapping(allDeviceMappings, groupInfo);
+      CoverageMatrix appDeviceCoverageMatrix = new CoverageMatrix(groupInfo);
+      findMinimumMappings(cov2match, appDeviceCoverageMatrix);
+      List<List<int>> matrix = appDeviceCoverageMatrix.matrix;
+      for (List<int> row in matrix) {
+        for (int e in row) {
+          expect(e, equals(0));
+        }
+      }
+    });
+
+    test('with platform as device key', () {
+      List<DeviceSpec> specs = <DeviceSpec>[
+        new DeviceSpec(
+          'Alice',
+          specProperties: {
+            'platform': 'android',
+            'app-root': 'xxx',
+            'app-path': 'yyy'
+          }
+        ),
+        new DeviceSpec(
+          'Bob',
+          specProperties: {
+            'app-root': 'xxx',
+            'app-path': 'zzz'
+          }
+        )
+      ];
+      List<Device> devices = <Device>[
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '123',
+            'model-name': 'Nexus 9',
+            'os-version': '6.0.1',
+            'screen-size': 'xlarge'
+          },
+          groupKey: 'platform'
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'ios',
+            'device-id': '456',
+            'model-name': 'iPhone 6S',
+            'os-version': '9.3.2',
+            'screen-size': 'large'
+          },
+          groupKey: 'platform'
+        )
+      ];
+      Map<DeviceSpec, Set<Device>> individualMatches
+        = findIndividualMatches(specs, devices);
+      List<Map<DeviceSpec, Device>> allDeviceMappings
+        = findAllMatchingDeviceMappings(specs, individualMatches);
+      Map<String, List<Device>> deviceGroups = buildGroups(devices);
+      Map<String, List<DeviceSpec>> specGroups = buildGroups(specs);
+      GroupInfo groupInfo = new GroupInfo(deviceGroups, specGroups);
+      Map<CoverageMatrix, Map<DeviceSpec, Device>> cov2match
+        = buildCoverage2MatchMapping(allDeviceMappings, groupInfo);
+      CoverageMatrix appDeviceCoverageMatrix = new CoverageMatrix(groupInfo);
+      findMinimumMappings(cov2match, appDeviceCoverageMatrix);
+      List<List<int>> matrix = appDeviceCoverageMatrix.matrix;
+      expect(matrix[0][0], equals(0));
+      expect(matrix[0][1], equals(-1));
+      expect(matrix[1][0], equals(-1));
+      expect(matrix[1][1], equals(0));
+    });
+
+    test('with model name as device key', () {
+      List<DeviceSpec> specs = <DeviceSpec>[
+        new DeviceSpec(
+          'Alice',
+          specProperties: {
+            'app-root': 'xxx',
+            'app-path': 'yyy'
+          }
+        ),
+        new DeviceSpec(
+          'Bob',
+          specProperties: {
+            'model-name': 'Nexus 9',
+            'app-root': 'xxx',
+            'app-path': 'zzz'
+          }
+        )
+      ];
+      List<Device> devices = <Device>[
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '123',
+            'model-name': 'Nexus 9',
+            'os-version': '6.0.1',
+            'screen-size': 'xlarge'
+          },
+          groupKey: 'model-name'
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'ios',
+            'device-id': '456',
+            'model-name': 'iPhone 6S',
+            'os-version': '9.3.2',
+            'screen-size': 'large'
+          },
+          groupKey: 'model-name'
+        )
+      ];
+      Map<DeviceSpec, Set<Device>> individualMatches
+        = findIndividualMatches(specs, devices);
+      List<Map<DeviceSpec, Device>> allDeviceMappings
+        = findAllMatchingDeviceMappings(specs, individualMatches);
+      Map<String, List<Device>> deviceGroups = buildGroups(devices);
+      Map<String, List<DeviceSpec>> specGroups = buildGroups(specs);
+      GroupInfo groupInfo = new GroupInfo(deviceGroups, specGroups);
+      Map<CoverageMatrix, Map<DeviceSpec, Device>> cov2match
+        = buildCoverage2MatchMapping(allDeviceMappings, groupInfo);
+      CoverageMatrix appDeviceCoverageMatrix = new CoverageMatrix(groupInfo);
+      findMinimumMappings(cov2match, appDeviceCoverageMatrix);
+      List<List<int>> matrix = appDeviceCoverageMatrix.matrix;
+      expect(matrix[0][0], equals(-1));
+      expect(matrix[0][1], equals(0));
+      expect(matrix[1][0], equals(0));
+      expect(matrix[1][1], equals(-1));
+    });
+
+    test('with OS version as device key', () {
+      List<DeviceSpec> specs = <DeviceSpec>[
+        new DeviceSpec(
+          'Alice',
+          specProperties: {
+            'app-root': 'xxx',
+            'app-path': 'yyy'
+          }
+        ),
+        new DeviceSpec(
+          'Bob',
+          specProperties: {
+            'platform': 'ios',
+            'os-version': '^9.0.0',
+            'app-root': 'xxx',
+            'app-path': 'zzz'
+          }
+        )
+      ];
+      List<Device> devices = <Device>[
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '123',
+            'model-name': 'Nexus 9',
+            'os-version': '6.0.1',
+            'screen-size': 'xlarge'
+          },
+          groupKey: 'os-version'
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'ios',
+            'device-id': '456',
+            'model-name': 'iPhone 6S',
+            'os-version': '9.3.2',
+            'screen-size': 'large'
+          },
+          groupKey: 'os-version'
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'ios',
+            'device-id': '456',
+            'model-name': 'iPhone 5',
+            'os-version': '8.4.2',
+            'screen-size': 'normal'
+          },
+          groupKey: 'os-version'
+        )
+      ];
+      Map<DeviceSpec, Set<Device>> individualMatches
+        = findIndividualMatches(specs, devices);
+      List<Map<DeviceSpec, Device>> allDeviceMappings
+        = findAllMatchingDeviceMappings(specs, individualMatches);
+      Map<String, List<Device>> deviceGroups = buildGroups(devices);
+      Map<String, List<DeviceSpec>> specGroups = buildGroups(specs);
+      GroupInfo groupInfo = new GroupInfo(deviceGroups, specGroups);
+      Map<CoverageMatrix, Map<DeviceSpec, Device>> cov2match
+        = buildCoverage2MatchMapping(allDeviceMappings, groupInfo);
+      CoverageMatrix appDeviceCoverageMatrix = new CoverageMatrix(groupInfo);
+      findMinimumMappings(cov2match, appDeviceCoverageMatrix);
+      List<List<int>> matrix = appDeviceCoverageMatrix.matrix;
+      expect(matrix[0][0], equals(0));
+      expect(matrix[0][1], equals(-1));
+      expect(matrix[0][2], equals(0));
+      expect(matrix[1][0], equals(-1));
+      expect(matrix[1][1], equals(0));
+      expect(matrix[1][2], equals(-1));
+    });
+
+    test('with screen size as device key', () {
+      List<DeviceSpec> specs = <DeviceSpec>[
+        new DeviceSpec(
+          'Alice',
+          specProperties: {
+            'screen-size': 'normal',
+            'app-root': 'xxx',
+            'app-path': 'yyy'
+          }
+        ),
+        new DeviceSpec(
+          'Bob',
+          specProperties: {
+            'screen-size': 'large',
+            'app-root': 'xxx',
+            'app-path': 'zzz'
+          }
+        )
+      ];
+      List<Device> devices = <Device>[
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '123',
+            'model-name': 'Nexus 9',
+            'os-version': '6.0.1',
+            'screen-size': 'xlarge'
+          },
+          groupKey: 'os-version'
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'ios',
+            'device-id': '456',
+            'model-name': 'iPhone 6S',
+            'os-version': '9.3.2',
+            'screen-size': 'large'
+          },
+          groupKey: 'os-version'
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'ios',
+            'device-id': '456',
+            'model-name': 'iPhone 5',
+            'os-version': '8.4.2',
+            'screen-size': 'normal'
+          },
+          groupKey: 'os-version'
+        )
+      ];
+      Map<DeviceSpec, Set<Device>> individualMatches
+        = findIndividualMatches(specs, devices);
+      List<Map<DeviceSpec, Device>> allDeviceMappings
+        = findAllMatchingDeviceMappings(specs, individualMatches);
+      Map<String, List<Device>> deviceGroups = buildGroups(devices);
+      Map<String, List<DeviceSpec>> specGroups = buildGroups(specs);
+      GroupInfo groupInfo = new GroupInfo(deviceGroups, specGroups);
+      Map<CoverageMatrix, Map<DeviceSpec, Device>> cov2match
+        = buildCoverage2MatchMapping(allDeviceMappings, groupInfo);
+      CoverageMatrix appDeviceCoverageMatrix = new CoverageMatrix(groupInfo);
+      findMinimumMappings(cov2match, appDeviceCoverageMatrix);
+      List<List<int>> matrix = appDeviceCoverageMatrix.matrix;
+      expect(matrix[0][0], equals(-1));
+      expect(matrix[0][1], equals(-1));
+      expect(matrix[0][2], equals(0));
+      expect(matrix[1][0], equals(-1));
+      expect(matrix[1][1], equals(0));
+      expect(matrix[1][2], equals(-1));
+    });
+  });
+}
diff --git a/mdtest/test/device_spec_test.dart b/mdtest/test/device_spec_test.dart
new file mode 100644
index 0000000..1fa1016
--- /dev/null
+++ b/mdtest/test/device_spec_test.dart
@@ -0,0 +1,150 @@
+// 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 'package:mdtest/src/mobile/device_spec.dart';
+import 'package:mdtest/src/mobile/device.dart';
+
+import 'package:test/test.dart';
+
+void main() {
+  group('device spec matches', () {
+    Device device;
+
+    setUpAll(() {
+      device = new Device(
+        properties: <String, String>{
+          'platform': 'ios',
+          'device-id': '123',
+          'model-name': 'iPhone 6S Plus',
+          'os-version': '9.3.4',
+          'screen-size': 'large',
+          'app-root': 'xxx',
+          'app-path': 'yyy'
+        }
+      );
+    });
+
+    test('platform matches', () {
+      DeviceSpec spec = new DeviceSpec(
+        'Alice',
+        specProperties: <String, String>{
+          'platform': 'ios',
+          'app-root': 'xxx',
+          'app-path': 'yyy'
+        }
+      );
+      expect(spec.matches(device), equals(true));
+    });
+
+    test('platform does not match', () {
+      DeviceSpec spec = new DeviceSpec(
+        'Alice',
+        specProperties: <String, String>{
+          'platform': 'android',
+          'app-root': 'xxx',
+          'app-path': 'yyy'
+        }
+      );
+      expect(spec.matches(device), equals(false));
+    });
+
+    test('device id matches', () {
+      DeviceSpec spec = new DeviceSpec(
+        'Alice',
+        specProperties: <String, String>{
+          'device-id': '123',
+          'app-root': 'xxx',
+          'app-path': 'yyy'
+        }
+      );
+      expect(spec.matches(device), equals(true));
+    });
+
+    test('device id does not match', () {
+      DeviceSpec spec = new DeviceSpec(
+        'Alice',
+        specProperties: <String, String>{
+          'device-id': '456',
+          'app-root': 'xxx',
+          'app-path': 'yyy'
+        }
+      );
+      expect(spec.matches(device), equals(false));
+    });
+
+    test('model name matches', () {
+      DeviceSpec spec = new DeviceSpec(
+        'Alice',
+        specProperties: <String, String>{
+          'model-name': 'iPhone 6S Plus',
+          'app-root': 'xxx',
+          'app-path': 'yyy'
+        }
+      );
+      expect(spec.matches(device), equals(true));
+    });
+
+    test('model name does not match', () {
+      DeviceSpec spec = new DeviceSpec(
+        'Alice',
+        specProperties: <String, String>{
+          'model-name': 'iPhone 5',
+          'app-root': 'xxx',
+          'app-path': 'yyy'
+        }
+      );
+      expect(spec.matches(device), equals(false));
+    });
+
+    test('os version matches', () {
+      DeviceSpec spec = new DeviceSpec(
+        'Alice',
+        specProperties: <String, String>{
+          'platform': 'ios',
+          'os-version': '>=9.3.4',
+          'app-root': 'xxx',
+          'app-path': 'yyy'
+        }
+      );
+      expect(spec.matches(device), equals(true));
+    });
+
+    test('os version does not match', () {
+      DeviceSpec spec = new DeviceSpec(
+        'Alice',
+        specProperties: <String, String>{
+          'platform': 'ios',
+          'os-version': '>9.4.0',
+          'app-root': 'xxx',
+          'app-path': 'yyy'
+        }
+      );
+      expect(spec.matches(device), equals(false));
+    });
+
+    test('os version matches', () {
+      DeviceSpec spec = new DeviceSpec(
+        'Alice',
+        specProperties: <String, String>{
+          'screen-size': 'large',
+          'app-root': 'xxx',
+          'app-path': 'yyy'
+        }
+      );
+      expect(spec.matches(device), equals(true));
+    });
+
+    test('os version does not match', () {
+      DeviceSpec spec = new DeviceSpec(
+        'Alice',
+        specProperties: <String, String>{
+          'screen-size': 'normal',
+          'app-root': 'xxx',
+          'app-path': 'yyy'
+        }
+      );
+      expect(spec.matches(device), equals(false));
+    });
+  });
+}
diff --git a/mdtest/test/group_test.dart b/mdtest/test/group_test.dart
new file mode 100644
index 0000000..d7347cd
--- /dev/null
+++ b/mdtest/test/group_test.dart
@@ -0,0 +1,326 @@
+// 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 'package:mdtest/src/mobile/device_spec.dart';
+import 'package:mdtest/src/mobile/device.dart';
+import 'package:mdtest/src/mobile/key_provider.dart';
+
+import 'package:test/test.dart';
+
+void main() {
+  group('build device groups', () {
+    test('by device id', () {
+      List<Device> devices = <Device>[
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '123',
+            'model-name': 'Nexus 9',
+            'os-version': '6.0.1',
+            'screen-size': 'xlarge'
+          }
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '456',
+            'model-name': 'Nexus 9',
+            'os-version': '6.0.1',
+            'screen-size': 'xlarge'
+          }
+        )
+      ];
+      Map<String, List<Device>> deviceGroups = buildGroups(devices);
+      expect(deviceGroups['123'].length, equals(1));
+      expect(deviceGroups['123'][0], equals(devices[0]));
+      expect(deviceGroups['456'].length, equals(1));
+      expect(deviceGroups['456'][0], equals(devices[1]));
+    });
+
+    test('by platform, same platform', () {
+      List<Device> devices = <Device>[
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '123',
+            'model-name': 'Nexus 9',
+            'os-version': '6.0.1',
+            'screen-size': 'xlarge'
+          },
+          groupKey: 'platform'
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '456',
+            'model-name': 'Nexus 9',
+            'os-version': '6.0.1',
+            'screen-size': 'xlarge'
+          },
+          groupKey: 'platform'
+        )
+      ];
+      Map<String, List<Device>> deviceGroups = buildGroups(devices);
+      expect(deviceGroups['android'].length, equals(2));
+      expect(deviceGroups['android'][0], equals(devices[0]));
+      expect(deviceGroups['android'][1], equals(devices[1]));
+    });
+
+    test('by platform, different platforms', () {
+      List<Device> devices = <Device>[
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '123',
+            'model-name': 'Nexus 9',
+            'os-version': '6.0.1',
+            'screen-size': 'xlarge'
+          },
+          groupKey: 'platform'
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'ios',
+            'device-id': '456',
+            'model-name': 'iPhone 6S',
+            'os-version': '9.3.2',
+            'screen-size': 'large'
+          },
+          groupKey: 'platform'
+        )
+      ];
+      Map<String, List<Device>> deviceGroups = buildGroups(devices);
+      expect(deviceGroups['android'].length, equals(1));
+      expect(deviceGroups['android'][0], equals(devices[0]));
+      expect(deviceGroups['ios'].length, equals(1));
+      expect(deviceGroups['ios'][0], equals(devices[1]));
+    });
+
+    test('by model name, same model name', () {
+      List<Device> devices = <Device>[
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '123',
+            'model-name': 'Nexus 9',
+            'os-version': '6.0.1',
+            'screen-size': 'xlarge'
+          },
+          groupKey: 'model-name'
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '456',
+            'model-name': 'Nexus 9',
+            'os-version': '6.0.1',
+            'screen-size': 'xlarge'
+          },
+          groupKey: 'model-name'
+        )
+      ];
+      Map<String, List<Device>> deviceGroups = buildGroups(devices);
+      expect(deviceGroups['Nexus 9'].length, equals(2));
+      expect(deviceGroups['Nexus 9'][0], equals(devices[0]));
+      expect(deviceGroups['Nexus 9'][1], equals(devices[1]));
+    });
+
+    test('by model name, different model names', () {
+      List<Device> devices = <Device>[
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '123',
+            'model-name': 'Nexus 9',
+            'os-version': '6.0.1',
+            'screen-size': 'xlarge'
+          },
+          groupKey: 'model-name'
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'ios',
+            'device-id': '456',
+            'model-name': 'iPhone 6S',
+            'os-version': '9.3.2',
+            'screen-size': 'large'
+          },
+          groupKey: 'model-name'
+        )
+      ];
+      Map<String, List<Device>> deviceGroups = buildGroups(devices);
+      expect(deviceGroups['Nexus 9'].length, equals(1));
+      expect(deviceGroups['Nexus 9'][0], equals(devices[0]));
+      expect(deviceGroups['iPhone 6S'].length, equals(1));
+      expect(deviceGroups['iPhone 6S'][0], equals(devices[1]));
+    });
+
+    test('by OS version, same OS version', () {
+      List<Device> devices = <Device>[
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '123',
+            'model-name': 'Nexus 9',
+            'os-version': '6.0.1',
+            'screen-size': 'xlarge'
+          },
+          groupKey: 'os-version'
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '456',
+            'model-name': 'Nexus 9',
+            'os-version': '6.0.1',
+            'screen-size': 'xlarge'
+          },
+          groupKey: 'os-version'
+        )
+      ];
+      Map<String, List<Device>> deviceGroups = buildGroups(devices);
+      expect(deviceGroups['android 6.x.x'].length, equals(2));
+      expect(deviceGroups['android 6.x.x'][0], equals(devices[0]));
+      expect(deviceGroups['android 6.x.x'][1], equals(devices[1]));
+    });
+
+    test('group by OS version, different OS versions', () {
+      List<Device> devices = <Device>[
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '123',
+            'model-name': 'Nexus 9',
+            'os-version': '6.0.1',
+            'screen-size': 'xlarge'
+          },
+          groupKey: 'os-version'
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'ios',
+            'device-id': '456',
+            'model-name': 'iPhone 6S',
+            'os-version': '9.3.2',
+            'screen-size': 'large'
+          },
+          groupKey: 'os-version'
+        )
+      ];
+      Map<String, List<Device>> deviceGroups = buildGroups(devices);
+      expect(deviceGroups['android 6.x.x'].length, equals(1));
+      expect(deviceGroups['android 6.x.x'][0], equals(devices[0]));
+      expect(deviceGroups['ios 9.x.x'].length, equals(1));
+      expect(deviceGroups['ios 9.x.x'][0], equals(devices[1]));
+    });
+
+    test('by screen size, same screen size', () {
+      List<Device> devices = <Device>[
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '123',
+            'model-name': 'Nexus 9',
+            'os-version': '6.0.1',
+            'screen-size': 'xlarge'
+          },
+          groupKey: 'screen-size'
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '456',
+            'model-name': 'Nexus 9',
+            'os-version': '6.0.1',
+            'screen-size': 'xlarge'
+          },
+          groupKey: 'screen-size'
+        )
+      ];
+      Map<String, List<Device>> deviceGroups = buildGroups(devices);
+      expect(deviceGroups['xlarge'].length, equals(2));
+      expect(deviceGroups['xlarge'][0], equals(devices[0]));
+      expect(deviceGroups['xlarge'][1], equals(devices[1]));
+    });
+
+    test('by screen size, different screen sizes', () {
+      List<Device> devices = <Device>[
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '123',
+            'model-name': 'Nexus 9',
+            'os-version': '6.0.1',
+            'screen-size': 'xlarge'
+          },
+          groupKey: 'screen-size'
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'ios',
+            'device-id': '456',
+            'model-name': 'iPhone 6S',
+            'os-version': '9.3.2',
+            'screen-size': 'large'
+          },
+          groupKey: 'screen-size'
+        )
+      ];
+      Map<String, List<Device>> deviceGroups = buildGroups(devices);
+      expect(deviceGroups['xlarge'].length, equals(1));
+      expect(deviceGroups['xlarge'][0], equals(devices[0]));
+      expect(deviceGroups['large'].length, equals(1));
+      expect(deviceGroups['large'][0], equals(devices[1]));
+    });
+  });
+
+  group('build spec groups', () {
+    test('by app path, same app path', () {
+      List<DeviceSpec> specs = <DeviceSpec>[
+        new DeviceSpec(
+          'Alice',
+          specProperties: {
+            'app-root': 'xxx',
+            'app-path': 'yyy'
+          }
+        ),
+        new DeviceSpec(
+          'Bob',
+          specProperties: {
+            'app-root': 'xxx',
+            'app-path': 'yyy'
+          }
+        )
+      ];
+      Map<String, List<DeviceSpec>> specGroups = buildGroups(specs);
+      expect(specGroups['yyy'].length, 2);
+      expect(specGroups['yyy'][0], equals(specs[0]));
+      expect(specGroups['yyy'][1], equals(specs[1]));
+    });
+
+    test('by app path, different app paths', () {
+      List<DeviceSpec> specs = <DeviceSpec>[
+        new DeviceSpec(
+          'Alice',
+          specProperties: {
+            'app-root': 'xxx',
+            'app-path': 'yyy'
+          }
+        ),
+        new DeviceSpec(
+          'Bob',
+          specProperties: {
+            'app-root': 'xxx',
+            'app-path': 'zzz'
+          }
+        )
+      ];
+      Map<String, List<DeviceSpec>> specGroups = buildGroups(specs);
+      expect(specGroups['yyy'].length, equals(1));
+      expect(specGroups['yyy'][0], equals(specs[0]));
+      expect(specGroups['zzz'].length, equals(1));
+      expect(specGroups['zzz'][0], equals(specs[1]));
+    });
+  });
+}
diff --git a/mdtest/test/matching_test.dart b/mdtest/test/matching_test.dart
new file mode 100644
index 0000000..7f00acc
--- /dev/null
+++ b/mdtest/test/matching_test.dart
@@ -0,0 +1,421 @@
+// 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 'package:mdtest/src/mobile/device_spec.dart';
+import 'package:mdtest/src/mobile/device.dart';
+import 'package:mdtest/src/algorithms/matching.dart';
+
+import 'package:test/test.dart';
+
+void main() {
+  group('individual matches', () {
+    test('return empty matches if specs is empty', () {
+      List<DeviceSpec> specs = <DeviceSpec>[];
+      List<Device> devices = <Device>[
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '123',
+            'model-name': 'Nexus 9',
+            'os-version': '6.0.1',
+            'screen-size': 'xlarge'
+          }
+        )
+      ];
+      Map<DeviceSpec, Set<Device>> individualMatches
+        = findIndividualMatches(specs, devices);
+      expect(individualMatches.isEmpty, equals(true));
+    });
+
+    test('spec match empty set if devices is empty', () {
+      List<DeviceSpec> specs = <DeviceSpec>[
+        new DeviceSpec(
+          'Alice',
+          specProperties: <String, String>{
+            'platform': 'ios',
+            'device-id': 'abc',
+            'model-name': 'iPhone 6S Plus',
+            'os-version': '^9.0.0',
+            'screen-size': 'large'
+          }
+        ),
+        new DeviceSpec(
+          'Bob',
+          specProperties: <String, String>{
+            'platform': 'android',
+            'device-id': 'def',
+            'model-name': 'Nexus 6',
+            'os-version': '>6.0.2 <7.0.0',
+            'screen-size': 'large'
+          }
+        )
+      ];
+      List<Device> devices = <Device>[];
+      Map<DeviceSpec, Set<Device>> individualMatches
+        = findIndividualMatches(specs, devices);
+      individualMatches.forEach(
+        (DeviceSpec spec, Set<Device> matchedDevices)
+          => expect(matchedDevices.isEmpty, equals(true))
+      );
+    });
+
+    test('common cases', () {
+      List<DeviceSpec> specs = <DeviceSpec>[
+        new DeviceSpec(
+          'Alice',
+          specProperties: <String, String>{
+            'platform': 'ios',
+            'model-name': 'iPhone 6S Plus',
+            'os-version': '^9.0.0',
+            'screen-size': 'large'
+          }
+        ),
+        new DeviceSpec(
+          'Bob',
+          specProperties: <String, String>{
+            'platform': 'android',
+            'os-version': '>6.0.2 <7.0.0'
+          }
+        )
+      ];
+      List<Device> devices = <Device>[
+        new Device(
+          properties: <String, String>{
+            'platform': 'ios',
+            'device-id': '123',
+            'model-name': 'iPhone 5',
+            'os-version': '8.3.2',
+            'screen-size': 'normal'
+          }
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'ios',
+            'device-id': '456',
+            'model-name': 'iPhone 6S Plus',
+            'os-version': '9.3.4',
+            'screen-size': 'large'
+          }
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '789',
+            'model-name': 'Nexus 9',
+            'os-version': '6.5.1',
+            'screen-size': 'xlarge'
+          }
+        )
+      ];
+      Map<DeviceSpec, Set<Device>> individualMatches
+        = findIndividualMatches(specs, devices);
+      expect(
+        individualMatches[specs[0]].containsAll([devices[1]]),
+        equals(true)
+      );
+      expect(
+        individualMatches[specs[1]].containsAll([devices[2]]),
+        equals(true)
+      );
+    });
+  });
+
+  group('first app-device mapping', () {
+    test('no matching should return null', () {
+      List<DeviceSpec> specs = <DeviceSpec>[
+        new DeviceSpec(
+          'Alice',
+          specProperties: <String, String>{
+            'platform': 'ios',
+            'model-name': 'iPhone 6S Plus',
+            'os-version': '^9.0.0',
+            'screen-size': 'large'
+          }
+        ),
+        new DeviceSpec(
+          'Bob',
+          specProperties: <String, String>{
+            'platform': 'ios',
+            'model-name': 'iPhone 6S Plus'
+          }
+        )
+      ];
+      List<Device> devices = <Device>[
+        new Device(
+          properties: <String, String>{
+            'platform': 'ios',
+            'device-id': '123',
+            'model-name': 'iPhone 5',
+            'os-version': '8.3.2',
+            'screen-size': 'normal'
+          }
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'ios',
+            'device-id': '456',
+            'model-name': 'iPhone 6S Plus',
+            'os-version': '9.3.4',
+            'screen-size': 'large'
+          }
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '789',
+            'model-name': 'Nexus 9',
+            'os-version': '6.5.1',
+            'screen-size': 'xlarge'
+          }
+        )
+      ];
+      Map<DeviceSpec, Set<Device>> individualMatches
+        = findIndividualMatches(specs, devices);
+      Map<DeviceSpec, Device> appDeviceMapping
+        = findMatchingDeviceMapping(specs, individualMatches);
+      expect(appDeviceMapping, equals(null));
+    });
+
+    test('return first match if matches exist', () {
+      List<DeviceSpec> specs = <DeviceSpec>[
+        new DeviceSpec(
+          'Alice',
+          specProperties: <String, String>{
+            'platform': 'ios',
+            'model-name': 'iPhone 6S Plus',
+            'os-version': '^9.0.0',
+            'screen-size': 'large'
+          }
+        ),
+        new DeviceSpec(
+          'Bob',
+          specProperties: <String, String>{
+            'platform': 'android',
+            'model-name': 'Nexus 9',
+            'os-version': '>6.0.0'
+          }
+        ),
+        new DeviceSpec(
+          'Susan',
+          specProperties: <String, String>{
+            'platform': 'android',
+            'screen-size': 'normal'
+          }
+        )
+      ];
+      List<Device> devices = <Device>[
+        new Device(
+          properties: <String, String>{
+            'platform': 'ios',
+            'device-id': '123',
+            'model-name': 'iPhone 6S Plus',
+            'os-version': '8.3.2',
+            'screen-size': 'large'
+          }
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'ios',
+            'device-id': '456',
+            'model-name': 'iPhone 6S Plus',
+            'os-version': '9.3.4',
+            'screen-size': 'large'
+          }
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '789',
+            'model-name': 'Nexus 9',
+            'os-version': '6.5.1',
+            'screen-size': 'xlarge'
+          }
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '101112',
+            'model-name': 'Nexus 4',
+            'os-version': '4.4.2',
+            'screen-size': 'normal'
+          }
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '131415',
+            'model-name': 'Nexus 5',
+            'os-version': '4.6.2',
+            'screen-size': 'normal'
+          }
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '16171718',
+            'model-name': 'Nexus 9',
+            'os-version': '6.5.1',
+            'screen-size': 'xlarge'
+          }
+        )
+      ];
+      Map<DeviceSpec, Set<Device>> individualMatches
+        = findIndividualMatches(specs, devices);
+      Map<DeviceSpec, Device> appDeviceMapping
+        = findMatchingDeviceMapping(specs, individualMatches);
+      expect(appDeviceMapping[specs[0]], equals(devices[1]));
+      expect(appDeviceMapping[specs[1]], equals(devices[2]));
+      expect(appDeviceMapping[specs[2]], equals(devices[3]));
+    });
+  });
+
+  group('all app-device mappings', () {
+    test('return empty mapping if no mapping found', () {
+      List<DeviceSpec> specs = <DeviceSpec>[
+        new DeviceSpec(
+          'Alice',
+          specProperties: <String, String>{
+            'platform': 'ios',
+            'model-name': 'iPhone 6S Plus',
+            'os-version': '^9.0.0',
+            'screen-size': 'large'
+          }
+        ),
+        new DeviceSpec(
+          'Bob',
+          specProperties: <String, String>{
+            'platform': 'android',
+            'os-version': '>6.0.2 <7.0.0'
+          }
+        )
+      ];
+      List<Device> devices = <Device>[
+        new Device(
+          properties: <String, String>{
+            'platform': 'ios',
+            'device-id': '123',
+            'model-name': 'iPhone 5',
+            'os-version': '8.3.2',
+            'screen-size': 'normal'
+          }
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '789',
+            'model-name': 'Nexus 9',
+            'os-version': '6.5.1',
+            'screen-size': 'xlarge'
+          }
+        )
+      ];
+      Map<DeviceSpec, Set<Device>> individualMatches
+        = findIndividualMatches(specs, devices);
+      List<Map<DeviceSpec, Device>> allAppDeviceMapping
+        = findAllMatchingDeviceMappings(specs, individualMatches);
+      expect(allAppDeviceMapping.isEmpty, equals(true));
+    });
+
+    test('return all mappings if exist', () {
+      List<DeviceSpec> specs = <DeviceSpec>[
+        new DeviceSpec(
+          'Alice',
+          specProperties: <String, String>{
+            'platform': 'ios',
+            'model-name': 'iPhone 6S Plus',
+            'os-version': '^9.0.0',
+            'screen-size': 'large'
+          }
+        ),
+        new DeviceSpec(
+          'Bob',
+          specProperties: <String, String>{
+            'platform': 'android',
+            'model-name': 'Nexus 9',
+            'os-version': '>6.0.0'
+          }
+        ),
+        new DeviceSpec(
+          'Susan',
+          specProperties: <String, String>{
+            'platform': 'android',
+            'screen-size': 'normal'
+          }
+        )
+      ];
+      List<Device> devices = <Device>[
+        new Device(
+          properties: <String, String>{
+            'platform': 'ios',
+            'device-id': '123',
+            'model-name': 'iPhone 6S Plus',
+            'os-version': '8.3.2',
+            'screen-size': 'large'
+          }
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'ios',
+            'device-id': '456',
+            'model-name': 'iPhone 6S Plus',
+            'os-version': '9.3.4',
+            'screen-size': 'large'
+          }
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '789',
+            'model-name': 'Nexus 9',
+            'os-version': '6.5.1',
+            'screen-size': 'xlarge'
+          }
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '101112',
+            'model-name': 'Nexus 4',
+            'os-version': '4.4.2',
+            'screen-size': 'normal'
+          }
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '131415',
+            'model-name': 'Nexus 5',
+            'os-version': '4.6.2',
+            'screen-size': 'normal'
+          }
+        ),
+        new Device(
+          properties: <String, String>{
+            'platform': 'android',
+            'device-id': '16171718',
+            'model-name': 'Nexus 9',
+            'os-version': '6.5.1',
+            'screen-size': 'xlarge'
+          }
+        )
+      ];
+      Map<DeviceSpec, Set<Device>> individualMatches
+        = findIndividualMatches(specs, devices);
+      List<Map<DeviceSpec, Device>> allAppDeviceMapping
+        = findAllMatchingDeviceMappings(specs, individualMatches);
+      expect(allAppDeviceMapping[0][specs[0]], equals(devices[1]));
+      expect(allAppDeviceMapping[0][specs[1]], equals(devices[2]));
+      expect(allAppDeviceMapping[0][specs[2]], equals(devices[3]));
+      expect(allAppDeviceMapping[1][specs[0]], equals(devices[1]));
+      expect(allAppDeviceMapping[1][specs[1]], equals(devices[2]));
+      expect(allAppDeviceMapping[1][specs[2]], equals(devices[4]));
+      expect(allAppDeviceMapping[2][specs[0]], equals(devices[1]));
+      expect(allAppDeviceMapping[2][specs[1]], equals(devices[5]));
+      expect(allAppDeviceMapping[2][specs[2]], equals(devices[3]));
+      expect(allAppDeviceMapping[3][specs[0]], equals(devices[1]));
+      expect(allAppDeviceMapping[3][specs[1]], equals(devices[5]));
+      expect(allAppDeviceMapping[3][specs[2]], equals(devices[4]));
+    });
+  });
+}
diff --git a/mdtest/test/src/mocks.dart b/mdtest/test/src/mocks.dart
new file mode 100644
index 0000000..2db8a4c
--- /dev/null
+++ b/mdtest/test/src/mocks.dart
@@ -0,0 +1,9 @@
+// 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 'package:mdtest/src/algorithms/coverage.dart';
+import 'package:mockito/mockito.dart';
+
+class MockCoverageMatrix extends Mock implements CoverageMatrix {
+}