Merge pull request #132 from srawlins/better-multiline

Better multiline call string representations
diff --git a/lib/src/mock.dart b/lib/src/mock.dart
index 7bb81a5..b8f6ab5 100644
--- a/lib/src/mock.dart
+++ b/lib/src/mock.dart
@@ -133,6 +133,18 @@
 
   @override
   String toString() => _givenName != null ? _givenName : runtimeType.toString();
+
+  String _realCallsToString() {
+    var stringRepresentations = _realCalls.map((call) => call.toString());
+    if (stringRepresentations.any((s) => s.contains('\n'))) {
+      // As each call contains newlines, put each on its own line, for better
+      // readability.
+      return stringRepresentations.join(',\n');
+    } else {
+      // A compact String should be perfect.
+      return stringRepresentations.join(', ');
+    }
+  }
 }
 
 typedef CallPair _ReturnsCannedResponse();
@@ -456,32 +468,50 @@
 
   @override
   String toString() {
+    var argString = '';
     var args = invocation.positionalArguments
-        .map((v) => v == null ? "null" : v.toString())
-        .join(", ");
+        .map((v) => v == null ? "null" : v.toString());
+    if (args.any((arg) => arg.contains('\n'))) {
+      // As one or more arg contains newlines, put each on its own line, and
+      // indent each, for better readability.
+      argString += '\n' +
+          args
+              .map((arg) => arg.splitMapJoin('\n', onNonMatch: (m) => '    $m'))
+              .join(',\n');
+    } else {
+      // A compact String should be perfect.
+      argString += args.join(', ');
+    }
     if (invocation.namedArguments.isNotEmpty) {
-      var namedArgs = invocation.namedArguments.keys
-          .map((key) =>
-              "${_symbolToString(key)}: ${invocation.namedArguments[key]}")
-          .join(", ");
-      if (args.isNotEmpty) args += ", ";
-      args += "{$namedArgs}";
+      if (argString.isNotEmpty) argString += ', ';
+      var namedArgs = invocation.namedArguments.keys.map((key) =>
+          '${_symbolToString(key)}: ${invocation.namedArguments[key]}');
+      if (namedArgs.any((arg) => arg.contains('\n'))) {
+        // As one or more arg contains newlines, put each on its own line, and
+        // indent each, for better readability.
+        namedArgs = namedArgs
+            .map((arg) => arg.splitMapJoin('\n', onNonMatch: (m) => '    $m'));
+        argString += '{\n${namedArgs.join(',\n')}}';
+      } else {
+        // A compact String should be perfect.
+        argString += '{${namedArgs.join(', ')}}';
+      }
     }
 
     var method = _symbolToString(invocation.memberName);
     if (invocation.isMethod) {
-      method = "$method($args)";
+      method = '$method($argString)';
     } else if (invocation.isGetter) {
-      method = "$method";
+      method = '$method';
     } else if (invocation.isSetter) {
-      method = "$method=$args";
+      method = '$method=$argString';
     } else {
       throw new StateError(
           'Invocation should be getter, setter or a method call.');
     }
 
-    var verifiedText = verified ? "[VERIFIED] " : "";
-    return "$verifiedText$mock.$method";
+    var verifiedText = verified ? '[VERIFIED] ' : '';
+    return '$verifiedText$mock.$method';
   }
 
   // This used to use MirrorSystem, which cleans up the Symbol() wrapper.
@@ -551,7 +581,7 @@
       if (mock._realCalls.isEmpty) {
         message = "No matching calls (actually, no calls at all).";
       } else {
-        var otherCalls = mock._realCalls.join(", ");
+        var otherCalls = mock._realCallsToString();
         message = "No matching calls. All calls: $otherCalls";
       }
       fail("$message\n"
@@ -559,7 +589,7 @@
           "`verifyNever(...);`.)");
     }
     if (never && matchingInvocations.isNotEmpty) {
-      var calls = mock._realCalls.join(", ");
+      var calls = mock._realCallsToString();
       fail("Unexpected calls. All calls: $calls");
     }
     matchingInvocations.forEach((inv) {
diff --git a/test/verify_test.dart b/test/verify_test.dart
index eab950a..9c1bcf1 100644
--- a/test/verify_test.dart
+++ b/test/verify_test.dart
@@ -21,6 +21,7 @@
   String methodWithoutArgs() => 'Real';
   String methodWithNormalArgs(int x) => 'Real';
   String methodWithListArgs(List<int> x) => 'Real';
+  String methodWithOptionalArg([int x]) => 'Real';
   String methodWithPositionalArgs(int x, [int y]) => 'Real';
   String methodWithNamedArgs(int x, {int y}) => 'Real';
   String methodWithOnlyNamedArgs({int y, int z}) => 'Real';
@@ -29,6 +30,24 @@
   set setter(String arg) {
     throw new StateError('I must be mocked');
   }
+
+  String methodWithLongArgs(LongToString a, LongToString b,
+          {LongToString c, LongToString d}) =>
+      'Real';
+}
+
+class LongToString {
+  final List aList;
+  final Map aMap;
+  final String aString;
+
+  LongToString(this.aList, this.aMap, this.aString);
+
+  String toString() => 'LongToString<\n'
+      '    aList: $aList\n'
+      '    aMap: $aMap\n'
+      '    aString: $aString\n'
+      '>';
 }
 
 class MockedClass extends Mock implements RealClass {}
@@ -179,51 +198,6 @@
       verify(mock.setter = 'A');
     });
 
-    test(
-        'should fail when no matching call is found, '
-        'and there are no unmatched calls', () {
-      expectFail(
-          'No matching calls (actually, no calls at all).\n'
-          '$noMatchingCallsFooter', () {
-        verify(mock.methodWithNormalArgs(43));
-      });
-    });
-
-    test(
-        'should fail when no matching call is found, '
-        'and there is one unmatched call', () {
-      mock.methodWithNormalArgs(42);
-      expectFail(
-          'No matching calls. All calls: MockedClass.methodWithNormalArgs(42)\n'
-          '$noMatchingCallsFooter', () {
-        verify(mock.methodWithNormalArgs(43));
-      });
-    });
-
-    test(
-        'should fail when no matching call is found, '
-        'and there are multiple unmatched calls', () {
-      mock.methodWithNormalArgs(41);
-      mock.methodWithNormalArgs(42);
-      expectFail(
-          'No matching calls. All calls: '
-          'MockedClass.methodWithNormalArgs(41), MockedClass.methodWithNormalArgs(42)\n'
-          '$noMatchingCallsFooter', () {
-        verify(mock.methodWithNormalArgs(43));
-      });
-    });
-
-    test(
-        'should fail when no matching call is found, '
-        'and unmatched calls have only named args', () {
-      mock.methodWithOnlyNamedArgs(y: 1);
-      expectFail(
-          'No matching calls. All calls: MockedClass.methodWithOnlyNamedArgs({y: 1})\n'
-          '$noMatchingCallsFooter', () {
-        verify(mock.methodWithOnlyNamedArgs());
-      });
-    });
-
     test('should throw meaningful errors when verification is interrupted', () {
       var badHelper = () => throw 'boo';
       try {
@@ -248,6 +222,56 @@
     });
   });
 
+  group('verify should fail when no matching call is found', () {
+    test('and there are no unmatched calls', () {
+      expectFail(
+          'No matching calls (actually, no calls at all).\n'
+          '$noMatchingCallsFooter', () {
+        verify(mock.methodWithNormalArgs(43));
+      });
+    });
+
+    test('and there is one unmatched call', () {
+      mock.methodWithNormalArgs(42);
+      expectFail(
+          'No matching calls. All calls: MockedClass.methodWithNormalArgs(42)\n'
+          '$noMatchingCallsFooter', () {
+        verify(mock.methodWithNormalArgs(43));
+      });
+    });
+
+    test('and there is one unmatched call without args', () {
+      mock.methodWithOptionalArg();
+      expectFail(
+          'No matching calls. All calls: MockedClass.methodWithOptionalArg()\n'
+          '$noMatchingCallsFooter', () {
+        verify(mock.methodWithOptionalArg(43));
+      });
+    });
+
+    test('and there are multiple unmatched calls', () {
+      mock.methodWithNormalArgs(41);
+      mock.methodWithNormalArgs(42);
+      expectFail(
+          'No matching calls. All calls: '
+          'MockedClass.methodWithNormalArgs(41), '
+          'MockedClass.methodWithNormalArgs(42)\n'
+          '$noMatchingCallsFooter', () {
+        verify(mock.methodWithNormalArgs(43));
+      });
+    });
+
+    test('and unmatched calls have only named args', () {
+      mock.methodWithOnlyNamedArgs(y: 1);
+      expectFail(
+          'No matching calls. All calls: '
+          'MockedClass.methodWithOnlyNamedArgs({y: 1})\n'
+          '$noMatchingCallsFooter', () {
+        verify(mock.methodWithOnlyNamedArgs());
+      });
+    });
+  });
+
   group('verify qualifies', () {
     group('unqualified as at least one', () {
       test('zero fails', () {
@@ -475,4 +499,43 @@
       });
     });
   });
+
+  group('multiline toStrings on objects', () {
+    test(
+        '"No matching calls" message visibly separates unmatched calls, '
+        'if an arg\'s string representations is multiline', () {
+      mock.methodWithLongArgs(new LongToString([1, 2], {1: 'a', 2: 'b'}, 'c'),
+          new LongToString([4, 5], {3: 'd', 4: 'e'}, 'f'));
+      mock.methodWithLongArgs(null, null,
+          c: new LongToString([5, 6], {5: 'g', 6: 'h'}, 'i'),
+          d: new LongToString([7, 8], {7: 'j', 8: 'k'}, 'l'));
+      expectFail(
+          'No matching calls. All calls: '
+          'MockedClass.methodWithLongArgs(\n'
+          '    LongToString<\n'
+          '        aList: [1, 2]\n'
+          '        aMap: {1: a, 2: b}\n'
+          '        aString: c\n'
+          '    >,\n'
+          '    LongToString<\n'
+          '        aList: [4, 5]\n'
+          '        aMap: {3: d, 4: e}\n'
+          '        aString: f\n'
+          '    >),\n'
+          'MockedClass.methodWithLongArgs(null, null, {\n'
+          '    c: LongToString<\n'
+          '        aList: [5, 6]\n'
+          '        aMap: {5: g, 6: h}\n'
+          '        aString: i\n'
+          '    >,\n'
+          '    d: LongToString<\n'
+          '        aList: [7, 8]\n'
+          '        aMap: {7: j, 8: k}\n'
+          '        aString: l\n'
+          '    >})\n'
+          '$noMatchingCallsFooter', () {
+        verify(mock.methodWithNormalArgs(43));
+      });
+    });
+  });
 }