Compare sets order-independently in pkg/matcher.

Also release matcher 0.10.1.

R=kevmoo@google.com
BUG= https://code.google.com/p/dart/issues/detail?id=19376

Review URL: https://codereview.chromium.org//327213003

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/matcher@37286 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 98cd9f4..3852190 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.10.1
+
+* Compare sets order-independently when using `equals()`.
+
 ## 0.10.0+3
 
 * Removed `@deprecated` annotation on matchers due to 
diff --git a/lib/src/core_matchers.dart b/lib/src/core_matchers.dart
index 356476b..cb76d68 100644
--- a/lib/src/core_matchers.dart
+++ b/lib/src/core_matchers.dart
@@ -85,7 +85,7 @@
 ///
 /// For [Iterable]s and [Map]s, this will recursively match the elements. To
 /// handle cyclic structures a recursion depth [limit] can be provided. The
-/// default limit is 100.
+/// default limit is 100. [Set]s will be compared order-independently.
 Matcher equals(expected, [int limit=100]) =>
     expected is String
         ? new _StringEqualsMatcher(expected)
@@ -124,6 +124,26 @@
     }
   }
 
+  List _compareSets(Set expected, actual, matcher, depth, location) {
+    if (actual is! Iterable) return ['is not Iterable', location];
+    actual = actual.toSet();
+
+    for (var expectedElement in expected) {
+      if (actual.every((actualElement) =>
+          matcher(expectedElement, actualElement, location, depth) != null)) {
+        return ['does not contain $expectedElement', location];
+      }
+    }
+
+    if (actual.length > expected.length) {
+      return ['larger than expected', location];
+    } else if (actual.length < expected.length) {
+      return ['smaller than expected', location];
+    } else {
+      return null;
+    }
+  }
+
   List _recursiveMatch(expected, actual, String location, int depth) {
     // If the expected value is a matcher, try to match it.
     if (expected is Matcher) {
@@ -146,37 +166,38 @@
     if (depth > _limit) return ['recursion depth limit exceeded', location];
 
     // If _limit is 1 we can only recurse one level into object.
-    bool canRecurse = depth == 0 || _limit > 1;
+    if (depth == 0 || _limit > 1) {
+      if (expected is Set) {
+        return _compareSets(expected, actual, _recursiveMatch, depth + 1,
+            location);
+      } else if (expected is Iterable) {
+        return _compareIterables(expected, actual, _recursiveMatch, depth + 1,
+            location);
+      } else if (expected is Map) {
+        if (actual is! Map) return ['expected a map', location];
 
-    if (expected is Iterable && canRecurse) {
-      return _compareIterables(expected, actual, _recursiveMatch, depth + 1,
-          location);
-    }
-
-    if (expected is Map && canRecurse) {
-      if (actual is! Map) return ['expected a map', location];
-
-      var err = (expected.length == actual.length) ? '' :
-                'has different length and ';
-      for (var key in expected.keys) {
-        if (!actual.containsKey(key)) {
-          return ["${err}is missing map key '$key'", location];
+        var err = (expected.length == actual.length) ? '' :
+                  'has different length and ';
+        for (var key in expected.keys) {
+          if (!actual.containsKey(key)) {
+            return ["${err}is missing map key '$key'", location];
+          }
         }
-      }
 
-      for (var key in actual.keys) {
-        if (!expected.containsKey(key)) {
-          return ["${err}has extra map key '$key'", location];
+        for (var key in actual.keys) {
+          if (!expected.containsKey(key)) {
+            return ["${err}has extra map key '$key'", location];
+          }
         }
-      }
 
-      for (var key in expected.keys) {
-        var rp = _recursiveMatch(expected[key], actual[key],
-            "${location}['${key}']", depth + 1);
-        if (rp != null) return rp;
-      }
+        for (var key in expected.keys) {
+          var rp = _recursiveMatch(expected[key], actual[key],
+              "${location}['${key}']", depth + 1);
+          if (rp != null) return rp;
+        }
 
-      return null;
+        return null;
+      }
     }
 
     var description = new StringDescription();
diff --git a/pubspec.yaml b/pubspec.yaml
index 87ab0cf..becc452 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: matcher
-version: 0.10.0+3
+version: 0.10.1
 author: Dart Team <misc@dartlang.org>
 description: Support for specifying test expectations
 homepage: http://www.dartlang.org
diff --git a/test/core_matchers_test.dart b/test/core_matchers_test.dart
index 3f694f3..c7644a2 100644
--- a/test/core_matchers_test.dart
+++ b/test/core_matchers_test.dart
@@ -47,6 +47,24 @@
     shouldPass(a, equals(b));
   });
 
+  test('equals with a set', () {
+    var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
+    var set1 = numbers.toSet();
+    numbers.shuffle();
+    var set2 = numbers.toSet();
+
+    shouldPass(set2, equals(set1));
+    shouldPass(numbers, equals(set1));
+    shouldFail([1, 2, 3, 4, 5, 6, 7, 8, 9], equals(set1),
+        "Expected: ?:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\n"
+        "  Actual: [1, 2, 3, 4, 5, 6, 7, 8, 9]\n"
+        "   Which: does not contain 10");
+    shouldFail([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], equals(set1),
+        "Expected: ?:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\n"
+        "  Actual: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]\n"
+        "   Which: larger than expected");
+  });
+
   test('anything', () {
     var a = new Map();
     shouldPass(0, anything);