Add deeplyEqual() method for use in replay (#25)

Part of #11
diff --git a/lib/src/backends/record_replay/common.dart b/lib/src/backends/record_replay/common.dart
index afd95e0..d41abeb 100644
--- a/lib/src/backends/record_replay/common.dart
+++ b/lib/src/backends/record_replay/common.dart
@@ -51,6 +51,10 @@
 /// named arguments that were passed to the method.
 const String kManifestNamedArgumentsKey = 'namedArguments';
 
+/// The key in a serialized [InvocationEvent] map that is used to store whether
+/// the invocation has been replayed already.
+const String kManifestReplayedKey = 'replayed';
+
 /// The serialized [kManifestTypeKey] for property retrievals.
 const String kGetType = 'get';
 
@@ -83,3 +87,41 @@
   /// Returns `true` if the given object is of type `T`.
   bool matches(dynamic object) => object is T;
 }
+
+/// Tells whether two objects are equal using deep equality checking.
+///
+/// Two lists are deeply equal if they have the same runtime type, the same
+/// length, and every element in list A is pairwise deeply equal with the
+/// corresponding element in list B.
+///
+/// Two maps are deeply equal if they have the same runtime type, the same
+/// length, the same set of keys, and the value for every key in map A is
+/// deeply equal to the corresponding value in map B.
+///
+/// All other types of objects are deeply equal if they have the same runtime
+/// type and are logically equal (according to `operator==`).
+bool deeplyEqual(dynamic object1, dynamic object2) {
+  if (object1.runtimeType != object2.runtimeType) {
+    return false;
+  } else if (object1 is List<dynamic>) {
+    return _areListsEqual(object1, object2);
+  } else if (object1 is Map<dynamic, dynamic>) {
+    return _areMapsEqual(object1, object2);
+  } else {
+    return object1 == object2;
+  }
+}
+
+bool _areListsEqual<T>(List<T> list1, List<T> list2) {
+  int i = 0;
+  return list1.length == list2.length &&
+      list1.every((T element) => deeplyEqual(element, list2[i++]));
+}
+
+bool _areMapsEqual<K, V>(Map<K, V> map1, Map<K, V> map2) {
+  return map1.length == map2.length &&
+      map1.keys.every((K key) {
+        return map1.containsKey(key) == map2.containsKey(key) &&
+            deeplyEqual(map1[key], map2[key]);
+      });
+}
diff --git a/test/recording_test.dart b/test/recording_test.dart
index c2472ab..919700c 100644
--- a/test/recording_test.dart
+++ b/test/recording_test.dart
@@ -20,7 +20,7 @@
 import 'common_tests.dart';
 
 void main() {
-  group('SupportingClasses', () {
+  group('SupportingCode', () {
     _BasicClass delegate;
     _RecordingClass rc;
     MutableRecording recording;
@@ -185,7 +185,7 @@
       });
     });
 
-    group('Encode', () {
+    group('encode', () {
       test('performsDeepEncoding', () async {
         rc.basicProperty = 'foo';
         rc.basicProperty;
@@ -245,6 +245,68 @@
         });
       });
     });
+
+    group('deeplyEqual', () {
+      Map<String, dynamic> newMap({
+        String stringValue: 'foo',
+        bool boolValue: true,
+        String lastListValue: 'c',
+        int lastMapValue: 2,
+      }) {
+        return <String, dynamic>{
+          'string': stringValue,
+          'bool': boolValue,
+          'list': <String>['a', 'b', lastListValue],
+          'map': <Symbol, int>{
+            #foo: 1,
+            #bar: lastMapValue,
+          },
+        };
+      }
+
+      test('primitives', () {
+        expect(deeplyEqual(1, 1), isTrue);
+        expect(deeplyEqual(1, 2), isFalse);
+        expect(deeplyEqual('1', '1'), isTrue);
+        expect(deeplyEqual('1', '2'), isFalse);
+        expect(deeplyEqual(true, true), isTrue);
+        expect(deeplyEqual(true, false), isFalse);
+        expect(deeplyEqual(null, null), isTrue);
+        expect(deeplyEqual(1, '1'), isFalse);
+      });
+
+      test('listOfPrimitives', () {
+        expect(deeplyEqual(<int>[], <int>[]), isTrue);
+        expect(deeplyEqual(<int>[1, 2, 3], <int>[1, 2, 3]), isTrue);
+        expect(deeplyEqual(<int>[1, 2, 3], <int>[1, 3, 2]), isFalse);
+        expect(deeplyEqual(<int>[1, 2, 3], <int>[1, 2]), isFalse);
+        expect(deeplyEqual(<int>[1, 2, 3], <int>[1, 2, 3, 4]), isFalse);
+        expect(deeplyEqual(<String>['a', 'b'], <String>['a', 'b']), isTrue);
+        expect(deeplyEqual(<String>['a', 'b'], <String>['b', 'a']), isFalse);
+        expect(deeplyEqual(<String>['a', 'b'], <String>['a']), isFalse);
+        expect(deeplyEqual(<int>[], <dynamic>[]), isFalse);
+        expect(deeplyEqual(<int>[], null), isFalse);
+      });
+
+      test('mapOfPrimitives', () {
+        expect(deeplyEqual(<String, int>{}, <String, int>{}), isTrue);
+        expect(deeplyEqual(<int, int>{1: 2}, <int, int>{1: 2}), isTrue);
+        expect(deeplyEqual(<int, int>{1: 2}, <int, int>{1: 3}), isFalse);
+        expect(deeplyEqual(<int, int>{1: 2}, <int, int>{}), isFalse);
+        expect(deeplyEqual(<int, int>{}, <int, int>{1: 2}), isFalse);
+        expect(deeplyEqual(<String, int>{}, <int, int>{}), isFalse);
+        expect(deeplyEqual(<String, int>{}, <dynamic, dynamic>{}), isFalse);
+        expect(deeplyEqual(<String, int>{}, null), isFalse);
+      });
+
+      test('listOfMaps', () {
+        expect(deeplyEqual(newMap(), newMap()), isTrue);
+        expect(deeplyEqual(newMap(), newMap(stringValue: 'bar')), isFalse);
+        expect(deeplyEqual(newMap(), newMap(boolValue: false)), isFalse);
+        expect(deeplyEqual(newMap(), newMap(lastListValue: 'd')), isFalse);
+        expect(deeplyEqual(newMap(), newMap(lastMapValue: 3)), isFalse);
+      });
+    });
   });
 
   group('RecordingFileSystem', () {