Merge pull request #721 from dart-lang/never-called

Add flushEventQueue() and neverCalled
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f0e14a4..cd365d5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 0.12.28
+
+* Add a `pumpEventQueue()` function to make it easy to wait until all
+  asynchronous tasks are complete.
+
+* Add a `neverCalled` getter that returns a function that causes the test to
+  fail if it's ever called.
+
 ## 0.12.27+1
 
 * Increase the timeout for loading tests to 12 minutes.
diff --git a/lib/src/frontend/expect_async.dart b/lib/src/frontend/expect_async.dart
index 9a37bdf..85f4d0a 100644
--- a/lib/src/frontend/expect_async.dart
+++ b/lib/src/frontend/expect_async.dart
@@ -5,11 +5,9 @@
 import 'dart:async';
 
 import '../backend/invoker.dart';
+import '../util/placeholder.dart';
 import 'expect.dart';
 
-/// An object used to detect unpassed arguments.
-const _PLACEHOLDER = const Object();
-
 // Function types returned by expectAsync# methods.
 
 typedef T Func0<T>();
@@ -157,39 +155,39 @@
   // argument count of zero.
   T max0() => max6();
 
-  T max1([Object a0 = _PLACEHOLDER]) => max6(a0);
+  T max1([Object a0 = placeholder]) => max6(a0);
 
-  T max2([Object a0 = _PLACEHOLDER, Object a1 = _PLACEHOLDER]) => max6(a0, a1);
+  T max2([Object a0 = placeholder, Object a1 = placeholder]) => max6(a0, a1);
 
   T max3(
-          [Object a0 = _PLACEHOLDER,
-          Object a1 = _PLACEHOLDER,
-          Object a2 = _PLACEHOLDER]) =>
+          [Object a0 = placeholder,
+          Object a1 = placeholder,
+          Object a2 = placeholder]) =>
       max6(a0, a1, a2);
 
   T max4(
-          [Object a0 = _PLACEHOLDER,
-          Object a1 = _PLACEHOLDER,
-          Object a2 = _PLACEHOLDER,
-          Object a3 = _PLACEHOLDER]) =>
+          [Object a0 = placeholder,
+          Object a1 = placeholder,
+          Object a2 = placeholder,
+          Object a3 = placeholder]) =>
       max6(a0, a1, a2, a3);
 
   T max5(
-          [Object a0 = _PLACEHOLDER,
-          Object a1 = _PLACEHOLDER,
-          Object a2 = _PLACEHOLDER,
-          Object a3 = _PLACEHOLDER,
-          Object a4 = _PLACEHOLDER]) =>
+          [Object a0 = placeholder,
+          Object a1 = placeholder,
+          Object a2 = placeholder,
+          Object a3 = placeholder,
+          Object a4 = placeholder]) =>
       max6(a0, a1, a2, a3, a4);
 
   T max6(
-          [Object a0 = _PLACEHOLDER,
-          Object a1 = _PLACEHOLDER,
-          Object a2 = _PLACEHOLDER,
-          Object a3 = _PLACEHOLDER,
-          Object a4 = _PLACEHOLDER,
-          Object a5 = _PLACEHOLDER]) =>
-      _run([a0, a1, a2, a3, a4, a5].where((a) => a != _PLACEHOLDER));
+          [Object a0 = placeholder,
+          Object a1 = placeholder,
+          Object a2 = placeholder,
+          Object a3 = placeholder,
+          Object a4 = placeholder,
+          Object a5 = placeholder]) =>
+      _run([a0, a1, a2, a3, a4, a5].where((a) => a != placeholder));
 
   /// Runs the wrapped function with [args] and returns its return value.
   T _run(Iterable args) {
diff --git a/lib/src/frontend/future_matchers.dart b/lib/src/frontend/future_matchers.dart
index 0284a4d..31f78ac 100644
--- a/lib/src/frontend/future_matchers.dart
+++ b/lib/src/frontend/future_matchers.dart
@@ -9,6 +9,7 @@
 import '../utils.dart';
 import 'async_matcher.dart';
 import 'expect.dart';
+import 'utils.dart';
 
 /// Matches a [Future] that completes successfully with a value.
 ///
@@ -86,31 +87,23 @@
 /// Note that this creates an asynchronous expectation. The call to
 /// `expect()` that includes this will return immediately and execution will
 /// continue.
-final Matcher doesNotComplete = const _DoesNotComplete(20);
+final Matcher doesNotComplete = const _DoesNotComplete();
 
 class _DoesNotComplete extends Matcher {
-  final int _timesToPump;
-  const _DoesNotComplete(this._timesToPump);
-
-  // TODO(grouma) - Make this a top level function
-  Future _pumpEventQueue(times) {
-    if (times == 0) return new Future.value();
-    return new Future(() => _pumpEventQueue(times - 1));
-  }
+  const _DoesNotComplete();
 
   Description describe(Description description) {
     description.add("does not complete");
     return description;
   }
 
-  @override
   bool matches(item, Map matchState) {
     if (item is! Future) return false;
     item.then((value) {
       fail('Future was not expected to complete but completed with a value of '
           '$value');
     });
-    expect(_pumpEventQueue(_timesToPump), completes);
+    expect(pumpEventQueue(), completes);
     return true;
   }
 
diff --git a/lib/src/frontend/never_called.dart b/lib/src/frontend/never_called.dart
new file mode 100644
index 0000000..f56029e
--- /dev/null
+++ b/lib/src/frontend/never_called.dart
@@ -0,0 +1,67 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+
+import '../util/placeholder.dart';
+import '../utils.dart';
+import 'expect.dart';
+import 'future_matchers.dart';
+import 'utils.dart';
+
+/// Returns a function that causes the test to fail if it's called.
+///
+/// This can safely be passed in place of any callback that takes ten or fewer
+/// positional parameters. For example:
+///
+/// ```
+/// // Asserts that the stream never emits an event.
+/// stream.listen(neverCalled);
+/// ```
+///
+/// This also ensures that the test doesn't complete until a call to
+/// [pumpEventQueue] finishes, so that the callback has a chance to be called.
+T Function<T>(
+    [Object,
+    Object,
+    Object,
+    Object,
+    Object,
+    Object,
+    Object,
+    Object,
+    Object,
+    Object]) get neverCalled {
+  // Make sure the test stays alive long enough to call the function if it's
+  // going to.
+  expect(pumpEventQueue(), completes);
+
+  var zone = Zone.current;
+  return <T>(
+      [a1 = placeholder,
+      a2 = placeholder,
+      a3 = placeholder,
+      a4 = placeholder,
+      a5 = placeholder,
+      a6 = placeholder,
+      a7 = placeholder,
+      a8 = placeholder,
+      a9 = placeholder,
+      a10 = placeholder]) {
+    var arguments = [a1, a2, a3, a4, a5, a6, a7, a8, a9, a10]
+        .where((argument) => argument != placeholder)
+        .toList();
+
+    zone.handleUncaughtError(
+        new TestFailure(
+            "Callback should never have been called, but it was called with" +
+                (arguments.isEmpty
+                    ? " no arguments."
+                    : ":\n${bullet(arguments.map(prettyPrint))}")),
+        zone.run(() => new Chain.current()));
+    return null as T;
+  };
+}
diff --git a/lib/src/frontend/utils.dart b/lib/src/frontend/utils.dart
new file mode 100644
index 0000000..209c4d1
--- /dev/null
+++ b/lib/src/frontend/utils.dart
@@ -0,0 +1,22 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+/// Returns a [Future] that completes after the [event loop][] has run the given
+/// number of [times] (20 by default).
+///
+/// [event loop]: https://webdev.dartlang.org/articles/performance/event-loop#darts-event-loop-and-queues
+///
+/// Awaiting this approximates waiting until all asynchronous work (other than
+/// work that's waiting for external resources) completes.
+Future pumpEventQueue({int times}) {
+  times ??= 20;
+  if (times == 0) return new Future.value();
+  // Use [new Future] future to allow microtask events to finish. The [new
+  // Future.value] constructor uses scheduleMicrotask itself and would therefore
+  // not wait for microtask callbacks that are scheduled after invoking this
+  // method.
+  return new Future(() => pumpEventQueue(times: times - 1));
+}
diff --git a/lib/src/util/placeholder.dart b/lib/src/util/placeholder.dart
new file mode 100644
index 0000000..862964f
--- /dev/null
+++ b/lib/src/util/placeholder.dart
@@ -0,0 +1,16 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// A class that's used as a default argument to detect whether an argument was
+/// passed.
+///
+/// We use a custom class for this rather than just `const Object()` so that
+/// callers can't accidentally pass the placeholder value.
+class _Placeholder {
+  const _Placeholder();
+}
+
+/// A placeholder to use as a default argument value to detect whether an
+/// argument was passed.
+const placeholder = const _Placeholder();
diff --git a/lib/test.dart b/lib/test.dart
index 8d159ad..39e4cbc 100644
--- a/lib/test.dart
+++ b/lib/test.dart
@@ -23,6 +23,7 @@
 export 'src/frontend/expect_async.dart';
 export 'src/frontend/future_matchers.dart';
 export 'src/frontend/on_platform.dart';
+export 'src/frontend/never_called.dart';
 export 'src/frontend/prints_matcher.dart';
 export 'src/frontend/skip.dart';
 export 'src/frontend/spawn_hybrid.dart';
@@ -33,6 +34,7 @@
 export 'src/frontend/throws_matcher.dart';
 export 'src/frontend/throws_matchers.dart';
 export 'src/frontend/timeout.dart';
+export 'src/frontend/utils.dart';
 
 /// The global declarer.
 ///
diff --git a/pubspec.yaml b/pubspec.yaml
index 96ac403..8c006dc 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: test
-version: 0.12.27+1
+version: 0.12.28
 author: Dart Team <misc@dartlang.org>
 description: A library for writing dart unit tests.
 homepage: https://github.com/dart-lang/test
diff --git a/test/frontend/matcher/completion_test.dart b/test/frontend/matcher/completion_test.dart
index 5054e1f..ca89152 100644
--- a/test/frontend/matcher/completion_test.dart
+++ b/test/frontend/matcher/completion_test.dart
@@ -55,7 +55,7 @@
         var completer = new Completer();
         expect(completer.future, doesNotComplete);
         new Future(() async {
-          await pumpEventQueue(10);
+          await pumpEventQueue(times: 10);
         }).then(completer.complete);
       });
 
diff --git a/test/frontend/never_called_test.dart b/test/frontend/never_called_test.dart
new file mode 100644
index 0000000..f830e8f
--- /dev/null
+++ b/test/frontend/never_called_test.dart
@@ -0,0 +1,77 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:term_glyph/term_glyph.dart' as glyph;
+import 'package:test/src/backend/state.dart';
+import 'package:test/test.dart';
+
+import '../utils.dart';
+
+void main() {
+  setUpAll(() {
+    glyph.ascii = true;
+  });
+
+  test("doesn't throw if it isn't called", () async {
+    var liveTest = await runTestBody(() {
+      const Stream.empty().listen(neverCalled);
+    });
+
+    expectTestPassed(liveTest);
+  });
+
+  group("if it's called", () {
+    test("throws", () async {
+      var liveTest = await runTestBody(() {
+        neverCalled();
+      });
+
+      expectTestFailed(
+          liveTest,
+          "Callback should never have been called, but it was called with no "
+          "arguments.");
+    });
+
+    test("pretty-prints arguments", () async {
+      var liveTest = await runTestBody(() {
+        neverCalled(1, "foo\nbar");
+      });
+
+      expectTestFailed(
+          liveTest,
+          "Callback should never have been called, but it was called with:\n"
+          "* <1>\n"
+          "* 'foo\\n'\n"
+          "    'bar'");
+    });
+
+    test("keeps the test alive", () async {
+      var liveTest = await runTestBody(() {
+        pumpEventQueue(times: 10).then(neverCalled);
+      });
+
+      expectTestFailed(
+          liveTest,
+          'Callback should never have been called, but it was called with:\n'
+          '* <null>');
+    });
+
+    test("can't be caught", () async {
+      var liveTest = await runTestBody(() {
+        try {
+          neverCalled();
+        } catch (_) {
+          // Do nothing.
+        }
+      });
+
+      expectTestFailed(
+          liveTest,
+          'Callback should never have been called, but it was called with '
+          'no arguments.');
+    });
+  });
+}
diff --git a/test/utils.dart b/test/utils.dart
index c2b062a..ccc5c13 100644
--- a/test/utils.dart
+++ b/test/utils.dart
@@ -223,20 +223,6 @@
   }
 }
 
-/// Returns a [Future] that completes after pumping the event queue [times]
-/// times.
-///
-/// By default, this should pump the event queue enough times to allow any code
-/// to run, as long as it's not waiting on some external event.
-Future pumpEventQueue([int times = 20]) {
-  if (times == 0) return new Future.value();
-  // Use [new Future] future to allow microtask events to finish. The [new
-  // Future.value] constructor uses scheduleMicrotask itself and would therefore
-  // not wait for microtask callbacks that are scheduled after invoking this
-  // method.
-  return new Future(() => pumpEventQueue(times - 1));
-}
-
 /// Returns a local [LiveTest] that runs [body].
 LiveTest createTest(body()) {
   var test = new LocalTest("test", new Metadata(), body);