Add scaffolding, expect, hooks libraries (#1460)

Shuffles code around to make a more clear distinction between the
different types of use cases that are satisfied by the "frontend"
classes. Exposing an import which does not include `expect` and the
matchers will make it possible to write alternative matching frameworks
without worrying about name conflicts.

- `scaffolding` includes the code to define test cases and high level
  utilities that user level test code might need. A test might import
  `scaffolding` instead of `test` if they want to use a different
  matching framework and don't need `expect`.
- `expect` includes the synchronous and asynchronous matchers, as well
  as `expect` and `expectAsync*`.

Add a new library `test_api/hooks` which has lower level APIs for
interacting with the test runner. `expect` is written against only
`hooks`, and an alternative matching framework could have equivalent
capabilities using the same APIs. The primary capability which was
previously only available in `Invoker` is the ability to report that
some async work has not completed, and must complete before the test can
be considered done.

- Add `hooks.dart` and refactor all of `lib/src/expect/*` to use it.
- Move most code from `src/frontend` to either `src/scaffolding` or
  `src/expect` as appropriate.
- Change the original entrypoints to export the new ones. Avoid exporting
  deprecated members, and `registerException` which is not yet deprecated
  but may not be quite the API we want in `hooks.dart`.

Fixes #1468. The new API in `hooks.dart` takes a `Future` instead of adding
and removing from a counter of unfinished callbacks. Since the handler on the
future won't run in the same Zone as the wrapped callback does, it doesn't
suffer the same issues of passing a callback to a non-test Zone.

TODO:
We fold stack frames stemming from the `test*` packages specifically,
which means that frames beneath `expect` get handled for free. An
alternative matching framework defined outside this package would not
have the same advantage, so we may need to find an API to bring that to
parity.
diff --git a/pkgs/test/CHANGELOG.md b/pkgs/test/CHANGELOG.md
index b988a2d..2e15457 100644
--- a/pkgs/test/CHANGELOG.md
+++ b/pkgs/test/CHANGELOG.md
@@ -8,6 +8,8 @@
   passing the `--chain-stack-traces` flag.
 * Remove `phantomjs` support completely, it was previously broken.
 * Fix `expectAsync` function type checks.
+* Add libraries `scaffolding.dart`, and `expect.dart` to allow importing a
+  subset of the normal surface area.
 
 ## 1.16.8
 
diff --git a/pkgs/test/lib/expect.dart b/pkgs/test/lib/expect.dart
new file mode 100644
index 0000000..28f64f7
--- /dev/null
+++ b/pkgs/test/lib/expect.dart
@@ -0,0 +1,5 @@
+// Copyright (c) 2021, 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.
+
+export 'package:test_api/expect.dart';
diff --git a/pkgs/test/lib/scaffolding.dart b/pkgs/test/lib/scaffolding.dart
new file mode 100644
index 0000000..2712d29
--- /dev/null
+++ b/pkgs/test/lib/scaffolding.dart
@@ -0,0 +1,6 @@
+// Copyright (c) 2021, 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.
+
+// ignore: deprecated_member_use
+export 'package:test_core/scaffolding.dart';
diff --git a/pkgs/test/test/runner/signal_test.dart b/pkgs/test/test/runner/signal_test.dart
index 15133f2..d141329 100644
--- a/pkgs/test/test/runner/signal_test.dart
+++ b/pkgs/test/test/runner/signal_test.dart
@@ -181,41 +181,6 @@
       await signalAndQuit(test);
     });
 
-    test('causes expect() to always throw an error immediately', () async {
-      await d.file('test.dart', '''
-import 'dart:async';
-import 'dart:io';
-
-import 'package:test/test.dart';
-
-void main() {
-  var expectThrewError = false;
-
-  tearDown(() {
-    File("output").writeAsStringSync(expectThrewError.toString());
-  });
-
-  test("test", () async {
-    print("running test");
-
-    await Future.delayed(Duration(seconds: 1));
-    try {
-      expect(true, isTrue);
-    } catch (_) {
-      expectThrewError = true;
-    }
-  });
-}
-''').create();
-
-      var test = await _runTest(['test.dart']);
-      await expectLater(test.stdout, emitsThrough('running test'));
-      await signalAndQuit(test);
-
-      await d.file('output', 'true').validate();
-      expectTempDirEmpty();
-    });
-
     test('causes expectAsync() to always throw an error immediately', () async {
       await d.file('test.dart', '''
 import 'dart:async';
diff --git a/pkgs/test_api/CHANGELOG.md b/pkgs/test_api/CHANGELOG.md
index bc661ec..ec25191 100644
--- a/pkgs/test_api/CHANGELOG.md
+++ b/pkgs/test_api/CHANGELOG.md
@@ -1,5 +1,9 @@
 ## 0.4.0-dev
 
+* Add libraries `scaffolding.dart`, and `expect.dart` to allow importing as
+  subset of the normal surface area.
+* Add new APIs in `hooks.dart` to allow writing custom expectation frameworks
+  which integrate with the test runner.
 * Add examples to `throwsA` and make top-level `throws...` matchers refer to it.
 * Disable stack trace chaining by default.
 * Fix `expectAsync` function type checks.
diff --git a/pkgs/test_api/lib/expect.dart b/pkgs/test_api/lib/expect.dart
new file mode 100644
index 0000000..30a8071
--- /dev/null
+++ b/pkgs/test_api/lib/expect.dart
@@ -0,0 +1,61 @@
+// Copyright (c) 2021, 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.
+
+export 'package:matcher/matcher.dart';
+
+export 'src/expect/expect.dart' show expect, expectLater, fail;
+export 'src/expect/expect_async.dart'
+    show
+        Func0,
+        Func1,
+        Func2,
+        Func3,
+        Func4,
+        Func5,
+        Func6,
+        expectAsync0,
+        expectAsync1,
+        expectAsync2,
+        expectAsync3,
+        expectAsync4,
+        expectAsync5,
+        expectAsync6,
+        expectAsyncUntil0,
+        expectAsyncUntil1,
+        expectAsyncUntil2,
+        expectAsyncUntil3,
+        expectAsyncUntil4,
+        expectAsyncUntil5,
+        expectAsyncUntil6;
+export 'src/expect/future_matchers.dart'
+    show completes, completion, doesNotComplete;
+export 'src/expect/never_called.dart' show neverCalled;
+export 'src/expect/prints_matcher.dart' show prints;
+export 'src/expect/stream_matcher.dart' show StreamMatcher;
+export 'src/expect/stream_matchers.dart'
+    show
+        emitsDone,
+        emits,
+        emitsError,
+        mayEmit,
+        emitsAnyOf,
+        emitsInOrder,
+        emitsInAnyOrder,
+        emitsThrough,
+        mayEmitMultiple,
+        neverEmits;
+export 'src/expect/throws_matcher.dart' show throwsA;
+export 'src/expect/throws_matchers.dart'
+    show
+        throwsArgumentError,
+        throwsConcurrentModificationError,
+        throwsCyclicInitializationError,
+        throwsException,
+        throwsFormatException,
+        throwsNoSuchMethodError,
+        throwsNullThrownError,
+        throwsRangeError,
+        throwsStateError,
+        throwsUnimplementedError,
+        throwsUnsupportedError;
diff --git a/pkgs/test_api/lib/hooks.dart b/pkgs/test_api/lib/hooks.dart
new file mode 100644
index 0000000..c22fe55
--- /dev/null
+++ b/pkgs/test_api/lib/hooks.dart
@@ -0,0 +1,74 @@
+// Copyright (c) 2021, 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 'package:stack_trace/stack_trace.dart';
+
+import 'src/backend/closed_exception.dart';
+import 'src/backend/invoker.dart';
+import 'src/backend/stack_trace_formatter.dart';
+
+class TestHandle {
+  /// Returns handle for the currently running test.
+  ///
+  /// This must be called from within the zone that the test is running in. If
+  /// the current zone is not a test's zone throws [OutsideTestException].
+  static TestHandle get current {
+    final invoker = Invoker.current;
+    if (invoker == null) throw OutsideTestException();
+    return TestHandle._(
+        invoker, StackTraceFormatter.current ?? _defaultFormatter);
+  }
+
+  static final _defaultFormatter = StackTraceFormatter();
+
+  final Invoker _invoker;
+  final StackTraceFormatter _stackTraceFormatter;
+  TestHandle._(this._invoker, this._stackTraceFormatter);
+
+  String get name => _invoker.liveTest.test.name;
+
+  /// Whether this test has already completed successfully.
+  ///
+  /// If a callback originating from a test case is invoked after the test has
+  /// already passed it may be an indication of a test that fails to wait for
+  /// all work to be finished, or of an asynchronous callback that is called
+  /// more times or later than expected.
+  bool get shouldBeDone => _invoker.liveTest.state.shouldBeDone;
+
+  /// Marks this test as skipped.
+  ///
+  /// A skipped test may still fail if any exception is thrown, including
+  /// uncaught asynchronous errors.
+  void markSkipped(String message) {
+    if (_invoker.closed) throw ClosedException();
+    _invoker.skip(message);
+  }
+
+  /// Indicates that this test should not be considered done until [future]
+  /// completes.
+  ///
+  /// The test may time out before [future] completes.
+  Future<T> mustWaitFor<T>(Future<T> future) {
+    if (_invoker.closed) throw ClosedException();
+    _invoker.addOutstandingCallback();
+    return future.whenComplete(_invoker.removeOutstandingCallback);
+  }
+
+  /// Converts [stackTrace] to a [Chain] according to the current test's
+  /// configuration.
+  Chain formatStackTrace(StackTrace stackTrace) =>
+      _stackTraceFormatter.formatStackTrace(stackTrace);
+}
+
+class OutsideTestException implements Exception {}
+
+/// An exception thrown when a test assertion fails.
+class TestFailure {
+  final String? message;
+
+  TestFailure(this.message);
+
+  @override
+  String toString() => message.toString();
+}
diff --git a/pkgs/test_api/lib/scaffolding.dart b/pkgs/test_api/lib/scaffolding.dart
new file mode 100644
index 0000000..4979ba3
--- /dev/null
+++ b/pkgs/test_api/lib/scaffolding.dart
@@ -0,0 +1,19 @@
+// Copyright (c) 2021, 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.
+
+@Deprecated('package:test_api is not intended for general use. '
+    'Please use package:test.')
+library test_api.scaffolding;
+
+export 'src/scaffolding/on_platform.dart' show OnPlatform;
+export 'src/scaffolding/retry.dart' show Retry;
+export 'src/scaffolding/skip.dart' show Skip;
+export 'src/scaffolding/spawn_hybrid.dart' show spawnHybridUri, spawnHybridCode;
+export 'src/scaffolding/tags.dart' show Tags;
+export 'src/scaffolding/test_on.dart' show TestOn;
+export 'src/scaffolding/timeout.dart' show Timeout;
+export 'src/scaffolding/utils.dart'
+    show pumpEventQueue, printOnFailure, markTestSkipped;
+export 'src/scaffolding/test_structure.dart'
+    show group, test, setUp, setUpAll, tearDown, tearDownAll, addTearDown;
diff --git a/pkgs/test_api/lib/src/backend/declarer.dart b/pkgs/test_api/lib/src/backend/declarer.dart
index d327699..1c299ce 100644
--- a/pkgs/test_api/lib/src/backend/declarer.dart
+++ b/pkgs/test_api/lib/src/backend/declarer.dart
@@ -7,7 +7,7 @@
 import 'package:collection/collection.dart';
 import 'package:stack_trace/stack_trace.dart';
 
-import '../frontend/timeout.dart';
+import '../scaffolding/timeout.dart';
 import 'group.dart';
 import 'group_entry.dart';
 import 'invoker.dart';
diff --git a/pkgs/test_api/lib/src/backend/invoker.dart b/pkgs/test_api/lib/src/backend/invoker.dart
index 784e160..cfdc4b5 100644
--- a/pkgs/test_api/lib/src/backend/invoker.dart
+++ b/pkgs/test_api/lib/src/backend/invoker.dart
@@ -6,7 +6,7 @@
 
 import 'package:stack_trace/stack_trace.dart';
 
-import '../frontend/expect.dart';
+import '../../hooks.dart' show TestFailure;
 import '../util/pretty_print.dart';
 import 'closed_exception.dart';
 import 'declarer.dart';
diff --git a/pkgs/test_api/lib/src/backend/metadata.dart b/pkgs/test_api/lib/src/backend/metadata.dart
index 1dd90d5..ef93153 100644
--- a/pkgs/test_api/lib/src/backend/metadata.dart
+++ b/pkgs/test_api/lib/src/backend/metadata.dart
@@ -5,8 +5,8 @@
 import 'package:boolean_selector/boolean_selector.dart';
 import 'package:collection/collection.dart';
 
-import '../frontend/skip.dart';
-import '../frontend/timeout.dart';
+import '../scaffolding/skip.dart';
+import '../scaffolding/timeout.dart';
 import '../util/pretty_print.dart';
 import '../utils.dart';
 import 'platform_selector.dart';
diff --git a/pkgs/test_api/lib/src/frontend/async_matcher.dart b/pkgs/test_api/lib/src/expect/async_matcher.dart
similarity index 91%
rename from pkgs/test_api/lib/src/frontend/async_matcher.dart
rename to pkgs/test_api/lib/src/expect/async_matcher.dart
index 56ad776..236e0fd 100644
--- a/pkgs/test_api/lib/src/frontend/async_matcher.dart
+++ b/pkgs/test_api/lib/src/expect/async_matcher.dart
@@ -3,8 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:matcher/matcher.dart';
+import 'package:test_api/hooks.dart';
 
-import '../backend/invoker.dart';
 import 'expect.dart';
 
 /// A matcher that does asynchronous computation.
@@ -35,13 +35,11 @@
         reason: 'matchAsync() may only return a String, a Future, or null.');
 
     if (result is Future) {
-      Invoker.current!.addOutstandingCallback();
-      result.then((realResult) {
+      TestHandle.current.mustWaitFor(result.then((realResult) {
         if (realResult != null) {
           fail(formatFailure(this, item, realResult as String));
         }
-        Invoker.current!.removeOutstandingCallback();
-      });
+      }));
     } else if (result is String) {
       matchState[this] = result;
       return false;
diff --git a/pkgs/test_api/lib/src/frontend/expect.dart b/pkgs/test_api/lib/src/expect/expect.dart
similarity index 87%
rename from pkgs/test_api/lib/src/frontend/expect.dart
rename to pkgs/test_api/lib/src/expect/expect.dart
index 408846a..0342e87 100644
--- a/pkgs/test_api/lib/src/frontend/expect.dart
+++ b/pkgs/test_api/lib/src/expect/expect.dart
@@ -3,21 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:matcher/matcher.dart';
+import 'package:test_api/hooks.dart';
 
-import '../backend/closed_exception.dart';
-import '../backend/invoker.dart';
-import '../util/pretty_print.dart';
 import 'async_matcher.dart';
-
-/// An exception thrown when a test assertion fails.
-class TestFailure {
-  final String? message;
-
-  TestFailure(this.message);
-
-  @override
-  String toString() => message.toString();
-}
+import 'util/pretty_print.dart';
 
 /// The type used for functions that can be used to build up error reports
 /// upon failures in [expect].
@@ -78,6 +67,7 @@
 // ignore: body_might_complete_normally
 Future _expect(actual, matcher,
     {String? reason, skip, bool verbose = false, ErrorFormatter? formatter}) {
+  final test = TestHandle.current;
   formatter ??= (actual, matcher, reason, matchState, verbose) {
     var mismatchDescription = StringDescription();
     matcher.describeMismatch(actual, mismatchDescription, matchState, verbose);
@@ -86,12 +76,6 @@
         reason: reason);
   };
 
-  if (Invoker.current == null) {
-    throw StateError('expect() may only be called within a test.');
-  }
-
-  if (Invoker.current!.closed) throw ClosedException();
-
   if (skip != null && skip is! bool && skip is! String) {
     throw ArgumentError.value(skip, 'skip', 'must be a bool or a String');
   }
@@ -108,7 +92,7 @@
       message = 'Skip expect ($description).';
     }
 
-    Invoker.current!.skip(message);
+    test.markSkipped(message);
     return Future.sync(() {});
   }
 
@@ -122,16 +106,11 @@
     if (result is String) {
       fail(formatFailure(matcher, actual, result, reason: reason));
     } else if (result is Future) {
-      Invoker.current!.addOutstandingCallback();
-      return result.then((realResult) {
+      return test.mustWaitFor(result.then((realResult) {
         if (realResult == null) return;
         fail(formatFailure(matcher as Matcher, actual, realResult as String,
             reason: reason));
-      }).whenComplete(() {
-        // Always remove this, in case the failure is caught and handled
-        // gracefully.
-        Invoker.current!.removeOutstandingCallback();
-      });
+      }));
     }
 
     return Future.sync(() {});
diff --git a/pkgs/test_api/lib/src/frontend/expect_async.dart b/pkgs/test_api/lib/src/expect/expect_async.dart
similarity index 80%
rename from pkgs/test_api/lib/src/frontend/expect_async.dart
rename to pkgs/test_api/lib/src/expect/expect_async.dart
index 95ab80a..e326e47 100644
--- a/pkgs/test_api/lib/src/frontend/expect_async.dart
+++ b/pkgs/test_api/lib/src/expect/expect_async.dart
@@ -4,9 +4,9 @@
 
 import 'dart:async';
 
-import '../backend/invoker.dart';
-import '../util/placeholder.dart';
-import 'expect.dart';
+import 'package:test_api/hooks.dart';
+
+import 'util/placeholder.dart';
 
 // Function types returned by expectAsync# methods.
 
@@ -61,15 +61,14 @@
   /// The number of times the function has been called.
   int _actualCalls = 0;
 
-  /// The test invoker in which this function was wrapped.
-  Invoker? get _invoker => _zone[#test.invoker] as Invoker?;
-
-  /// The zone in which this function was wrapped.
-  final Zone _zone;
+  /// The test in which this function was wrapped.
+  late final TestHandle _test;
 
   /// Whether this function has been called the requisite number of times.
   late bool _complete;
 
+  Completer<void>? _expectationSatisfied;
+
   /// Wraps [callback] in a function that asserts that it's called at least
   /// [minExpected] times and no more than [maxExpected] times.
   ///
@@ -84,17 +83,21 @@
             (maxExpected == 0 && minExpected > 0) ? minExpected : maxExpected,
         _isDone = isDone,
         _reason = reason == null ? '' : '\n$reason',
-        _zone = Zone.current,
         _id = _makeCallbackId(id, callback) {
-    if (_invoker == null) {
-      throw StateError('[expectAsync] was called outside of a test.');
-    } else if (maxExpected > 0 && minExpected > maxExpected) {
+    try {
+      _test = TestHandle.current;
+    } on OutsideTestException {
+      throw StateError('`expectAsync` must be called within a test.');
+    }
+
+    if (maxExpected > 0 && minExpected > maxExpected) {
       throw ArgumentError('max ($maxExpected) may not be less than count '
           '($minExpected).');
     }
 
     if (isDone != null || minExpected > 0) {
-      _invoker!.addOutstandingCallback();
+      var completer = _expectationSatisfied = Completer<void>();
+      _test.mustWaitFor(completer.future);
       _complete = false;
     } else {
       _complete = true;
@@ -134,7 +137,7 @@
     if (_callback is Function(Never)) return max1;
     if (_callback is Function()) return max0;
 
-    _invoker!.removeOutstandingCallback();
+    _expectationSatisfied?.complete();
     throw ArgumentError(
         'The wrapped function has more than 6 required arguments');
   }
@@ -184,9 +187,9 @@
     // pass it to the invoker anyway.
     try {
       _actualCalls++;
-      if (_invoker!.liveTest.state.shouldBeDone) {
+      if (_test.shouldBeDone) {
         throw 'Callback ${_id}called ($_actualCalls) after test case '
-            '${_invoker!.liveTest.test.name} had already completed.$_reason';
+            '${_test.name} had already completed.$_reason';
       } else if (_maxExpectedCalls >= 0 && _actualCalls > _maxExpectedCalls) {
         throw TestFailure('Callback ${_id}called more times than expected '
             '($_maxExpectedCalls).$_reason');
@@ -207,7 +210,7 @@
     // Mark this callback as complete and remove it from the test case's
     // oustanding callback count; if that hits zero the test is done.
     _complete = true;
-    _invoker!.removeOutstandingCallback();
+    _expectationSatisfied?.complete();
   }
 }
 
@@ -217,13 +220,8 @@
 /// [expectAsync6] instead.
 @Deprecated('Will be removed in 0.13.0')
 Function expectAsync(Function callback,
-    {int count = 1, int max = 0, String? id, String? reason}) {
-  if (Invoker.current == null) {
-    throw StateError('expectAsync() may only be called within a test.');
-  }
-
-  return _ExpectedFunction(callback, count, max, id: id, reason: reason).func;
-}
+        {int count = 1, int max = 0, String? id, String? reason}) =>
+    _ExpectedFunction(callback, count, max, id: id, reason: reason).func;
 
 /// Informs the framework that the given [callback] of arity 0 is expected to be
 /// called [count] number of times (by default 1).
@@ -247,14 +245,8 @@
 /// [expectAsync1], [expectAsync2], [expectAsync3], [expectAsync4],
 /// [expectAsync5], and [expectAsync6] for callbacks with different arity.
 Func0<T> expectAsync0<T>(T Function() callback,
-    {int count = 1, int max = 0, String? id, String? reason}) {
-  if (Invoker.current == null) {
-    throw StateError('expectAsync0() may only be called within a test.');
-  }
-
-  return _ExpectedFunction<T>(callback, count, max, id: id, reason: reason)
-      .max0;
-}
+        {int count = 1, int max = 0, String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max0;
 
 /// Informs the framework that the given [callback] of arity 1 is expected to be
 /// called [count] number of times (by default 1).
@@ -278,14 +270,8 @@
 /// [expectAsync0], [expectAsync2], [expectAsync3], [expectAsync4],
 /// [expectAsync5], and [expectAsync6] for callbacks with different arity.
 Func1<T, A> expectAsync1<T, A>(T Function(A) callback,
-    {int count = 1, int max = 0, String? id, String? reason}) {
-  if (Invoker.current == null) {
-    throw StateError('expectAsync1() may only be called within a test.');
-  }
-
-  return _ExpectedFunction<T>(callback, count, max, id: id, reason: reason)
-      .max1;
-}
+        {int count = 1, int max = 0, String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max1;
 
 /// Informs the framework that the given [callback] of arity 2 is expected to be
 /// called [count] number of times (by default 1).
@@ -309,14 +295,8 @@
 /// [expectAsync0], [expectAsync1], [expectAsync3], [expectAsync4],
 /// [expectAsync5], and [expectAsync6] for callbacks with different arity.
 Func2<T, A, B> expectAsync2<T, A, B>(T Function(A, B) callback,
-    {int count = 1, int max = 0, String? id, String? reason}) {
-  if (Invoker.current == null) {
-    throw StateError('expectAsync2() may only be called within a test.');
-  }
-
-  return _ExpectedFunction<T>(callback, count, max, id: id, reason: reason)
-      .max2;
-}
+        {int count = 1, int max = 0, String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max2;
 
 /// Informs the framework that the given [callback] of arity 3 is expected to be
 /// called [count] number of times (by default 1).
@@ -340,14 +320,8 @@
 /// [expectAsync0], [expectAsync1], [expectAsync2], [expectAsync4],
 /// [expectAsync5], and [expectAsync6] for callbacks with different arity.
 Func3<T, A, B, C> expectAsync3<T, A, B, C>(T Function(A, B, C) callback,
-    {int count = 1, int max = 0, String? id, String? reason}) {
-  if (Invoker.current == null) {
-    throw StateError('expectAsync3() may only be called within a test.');
-  }
-
-  return _ExpectedFunction<T>(callback, count, max, id: id, reason: reason)
-      .max3;
-}
+        {int count = 1, int max = 0, String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max3;
 
 /// Informs the framework that the given [callback] of arity 4 is expected to be
 /// called [count] number of times (by default 1).
@@ -371,18 +345,12 @@
 /// [expectAsync0], [expectAsync1], [expectAsync2], [expectAsync3],
 /// [expectAsync5], and [expectAsync6] for callbacks with different arity.
 Func4<T, A, B, C, D> expectAsync4<T, A, B, C, D>(
-    T Function(A, B, C, D) callback,
-    {int count = 1,
-    int max = 0,
-    String? id,
-    String? reason}) {
-  if (Invoker.current == null) {
-    throw StateError('expectAsync4() may only be called within a test.');
-  }
-
-  return _ExpectedFunction<T>(callback, count, max, id: id, reason: reason)
-      .max4;
-}
+        T Function(A, B, C, D) callback,
+        {int count = 1,
+        int max = 0,
+        String? id,
+        String? reason}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max4;
 
 /// Informs the framework that the given [callback] of arity 5 is expected to be
 /// called [count] number of times (by default 1).
@@ -406,18 +374,12 @@
 /// [expectAsync0], [expectAsync1], [expectAsync2], [expectAsync3],
 /// [expectAsync4], and [expectAsync6] for callbacks with different arity.
 Func5<T, A, B, C, D, E> expectAsync5<T, A, B, C, D, E>(
-    T Function(A, B, C, D, E) callback,
-    {int count = 1,
-    int max = 0,
-    String? id,
-    String? reason}) {
-  if (Invoker.current == null) {
-    throw StateError('expectAsync5() may only be called within a test.');
-  }
-
-  return _ExpectedFunction<T>(callback, count, max, id: id, reason: reason)
-      .max5;
-}
+        T Function(A, B, C, D, E) callback,
+        {int count = 1,
+        int max = 0,
+        String? id,
+        String? reason}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max5;
 
 /// Informs the framework that the given [callback] of arity 6 is expected to be
 /// called [count] number of times (by default 1).
@@ -441,18 +403,12 @@
 /// [expectAsync0], [expectAsync1], [expectAsync2], [expectAsync3],
 /// [expectAsync4], and [expectAsync5] for callbacks with different arity.
 Func6<T, A, B, C, D, E, F> expectAsync6<T, A, B, C, D, E, F>(
-    T Function(A, B, C, D, E, F) callback,
-    {int count = 1,
-    int max = 0,
-    String? id,
-    String? reason}) {
-  if (Invoker.current == null) {
-    throw StateError('expectAsync6() may only be called within a test.');
-  }
-
-  return _ExpectedFunction<T>(callback, count, max, id: id, reason: reason)
-      .max6;
-}
+        T Function(A, B, C, D, E, F) callback,
+        {int count = 1,
+        int max = 0,
+        String? id,
+        String? reason}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max6;
 
 /// This function is deprecated because it doesn't work well with strong mode.
 /// Use [expectAsyncUntil0], [expectAsyncUntil1],
@@ -460,15 +416,9 @@
 /// [expectAsyncUntil5], or [expectAsyncUntil6] instead.
 @Deprecated('Will be removed in 0.13.0')
 Function expectAsyncUntil(Function callback, bool Function() isDone,
-    {String? id, String? reason}) {
-  if (Invoker.current == null) {
-    throw StateError('expectAsyncUntil() may only be called within a test.');
-  }
-
-  return _ExpectedFunction(callback, 0, -1,
-          id: id, reason: reason, isDone: isDone)
-      .func;
-}
+        {String? id, String? reason}) =>
+    _ExpectedFunction(callback, 0, -1, id: id, reason: reason, isDone: isDone)
+        .func;
 
 /// Informs the framework that the given [callback] of arity 0 is expected to be
 /// called until [isDone] returns true.
@@ -488,15 +438,10 @@
 /// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for
 /// callbacks with different arity.
 Func0<T> expectAsyncUntil0<T>(T Function() callback, bool Function() isDone,
-    {String? id, String? reason}) {
-  if (Invoker.current == null) {
-    throw StateError('expectAsyncUntil0() may only be called within a test.');
-  }
-
-  return _ExpectedFunction<T>(callback, 0, -1,
-          id: id, reason: reason, isDone: isDone)
-      .max0;
-}
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max0;
 
 /// Informs the framework that the given [callback] of arity 1 is expected to be
 /// called until [isDone] returns true.
@@ -516,16 +461,11 @@
 /// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for
 /// callbacks with different arity.
 Func1<T, A> expectAsyncUntil1<T, A>(
-    T Function(A) callback, bool Function() isDone,
-    {String? id, String? reason}) {
-  if (Invoker.current == null) {
-    throw StateError('expectAsyncUntil1() may only be called within a test.');
-  }
-
-  return _ExpectedFunction<T>(callback, 0, -1,
-          id: id, reason: reason, isDone: isDone)
-      .max1;
-}
+        T Function(A) callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max1;
 
 /// Informs the framework that the given [callback] of arity 2 is expected to be
 /// called until [isDone] returns true.
@@ -545,16 +485,11 @@
 /// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for
 /// callbacks with different arity.
 Func2<T, A, B> expectAsyncUntil2<T, A, B>(
-    T Function(A, B) callback, bool Function() isDone,
-    {String? id, String? reason}) {
-  if (Invoker.current == null) {
-    throw StateError('expectAsyncUntil2() may only be called within a test.');
-  }
-
-  return _ExpectedFunction<T>(callback, 0, -1,
-          id: id, reason: reason, isDone: isDone)
-      .max2;
-}
+        T Function(A, B) callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max2;
 
 /// Informs the framework that the given [callback] of arity 3 is expected to be
 /// called until [isDone] returns true.
@@ -574,16 +509,11 @@
 /// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for
 /// callbacks with different arity.
 Func3<T, A, B, C> expectAsyncUntil3<T, A, B, C>(
-    T Function(A, B, C) callback, bool Function() isDone,
-    {String? id, String? reason}) {
-  if (Invoker.current == null) {
-    throw StateError('expectAsyncUntil3() may only be called within a test.');
-  }
-
-  return _ExpectedFunction<T>(callback, 0, -1,
-          id: id, reason: reason, isDone: isDone)
-      .max3;
-}
+        T Function(A, B, C) callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max3;
 
 /// Informs the framework that the given [callback] of arity 4 is expected to be
 /// called until [isDone] returns true.
@@ -603,16 +533,11 @@
 /// [expectAsyncUntil3], [expectAsyncUntil5], and [expectAsyncUntil6] for
 /// callbacks with different arity.
 Func4<T, A, B, C, D> expectAsyncUntil4<T, A, B, C, D>(
-    T Function(A, B, C, D) callback, bool Function() isDone,
-    {String? id, String? reason}) {
-  if (Invoker.current == null) {
-    throw StateError('expectAsyncUntil4() may only be called within a test.');
-  }
-
-  return _ExpectedFunction<T>(callback, 0, -1,
-          id: id, reason: reason, isDone: isDone)
-      .max4;
-}
+        T Function(A, B, C, D) callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max4;
 
 /// Informs the framework that the given [callback] of arity 5 is expected to be
 /// called until [isDone] returns true.
@@ -632,16 +557,11 @@
 /// [expectAsyncUntil3], [expectAsyncUntil4], and [expectAsyncUntil6] for
 /// callbacks with different arity.
 Func5<T, A, B, C, D, E> expectAsyncUntil5<T, A, B, C, D, E>(
-    T Function(A, B, C, D, E) callback, bool Function() isDone,
-    {String? id, String? reason}) {
-  if (Invoker.current == null) {
-    throw StateError('expectAsyncUntil5() may only be called within a test.');
-  }
-
-  return _ExpectedFunction<T>(callback, 0, -1,
-          id: id, reason: reason, isDone: isDone)
-      .max5;
-}
+        T Function(A, B, C, D, E) callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max5;
 
 /// Informs the framework that the given [callback] of arity 6 is expected to be
 /// called until [isDone] returns true.
@@ -661,13 +581,8 @@
 /// [expectAsyncUntil3], [expectAsyncUntil4], and [expectAsyncUntil5] for
 /// callbacks with different arity.
 Func6<T, A, B, C, D, E, F> expectAsyncUntil6<T, A, B, C, D, E, F>(
-    T Function(A, B, C, D, E, F) callback, bool Function() isDone,
-    {String? id, String? reason}) {
-  if (Invoker.current == null) {
-    throw StateError('expectAsyncUntil() may only be called within a test.');
-  }
-
-  return _ExpectedFunction<T>(callback, 0, -1,
-          id: id, reason: reason, isDone: isDone)
-      .max6;
-}
+        T Function(A, B, C, D, E, F) callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max6;
diff --git a/pkgs/test_api/lib/src/frontend/future_matchers.dart b/pkgs/test_api/lib/src/expect/future_matchers.dart
similarity index 97%
rename from pkgs/test_api/lib/src/frontend/future_matchers.dart
rename to pkgs/test_api/lib/src/expect/future_matchers.dart
index 2a2e186..d6d39c0 100644
--- a/pkgs/test_api/lib/src/frontend/future_matchers.dart
+++ b/pkgs/test_api/lib/src/expect/future_matchers.dart
@@ -3,11 +3,11 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:matcher/matcher.dart';
+import 'package:test_api/test_api.dart' show pumpEventQueue;
 
-import '../util/pretty_print.dart';
 import 'async_matcher.dart';
 import 'expect.dart';
-import 'utils.dart';
+import 'util/pretty_print.dart';
 
 /// Matches a [Future] that completes successfully with any value.
 ///
diff --git a/pkgs/test_api/lib/src/frontend/never_called.dart b/pkgs/test_api/lib/src/expect/never_called.dart
similarity index 91%
rename from pkgs/test_api/lib/src/frontend/never_called.dart
rename to pkgs/test_api/lib/src/expect/never_called.dart
index fadbeff..d950650 100644
--- a/pkgs/test_api/lib/src/frontend/never_called.dart
+++ b/pkgs/test_api/lib/src/expect/never_called.dart
@@ -5,12 +5,13 @@
 import 'dart:async';
 
 import 'package:stack_trace/stack_trace.dart';
+import 'package:test_api/hooks.dart';
+import 'package:test_api/test_api.dart' show pumpEventQueue;
 
-import '../util/placeholder.dart';
-import '../util/pretty_print.dart';
 import 'expect.dart';
 import 'future_matchers.dart';
-import 'utils.dart';
+import 'util/placeholder.dart';
+import 'util/pretty_print.dart';
 
 /// Returns a function that causes the test to fail if it's called.
 ///
diff --git a/pkgs/test_api/lib/src/frontend/prints_matcher.dart b/pkgs/test_api/lib/src/expect/prints_matcher.dart
similarity index 98%
rename from pkgs/test_api/lib/src/frontend/prints_matcher.dart
rename to pkgs/test_api/lib/src/expect/prints_matcher.dart
index 5c6fd52..5e9769b 100644
--- a/pkgs/test_api/lib/src/frontend/prints_matcher.dart
+++ b/pkgs/test_api/lib/src/expect/prints_matcher.dart
@@ -6,9 +6,9 @@
 
 import 'package:matcher/matcher.dart';
 
-import '../util/pretty_print.dart';
 import 'async_matcher.dart';
 import 'expect.dart';
+import 'util/pretty_print.dart';
 
 /// Matches a [Function] that prints text that matches [matcher].
 ///
diff --git a/pkgs/test_api/lib/src/frontend/stream_matcher.dart b/pkgs/test_api/lib/src/expect/stream_matcher.dart
similarity index 97%
rename from pkgs/test_api/lib/src/frontend/stream_matcher.dart
rename to pkgs/test_api/lib/src/expect/stream_matcher.dart
index adc2a40..78d5a3a 100644
--- a/pkgs/test_api/lib/src/frontend/stream_matcher.dart
+++ b/pkgs/test_api/lib/src/expect/stream_matcher.dart
@@ -4,10 +4,10 @@
 
 import 'package:async/async.dart';
 import 'package:matcher/matcher.dart';
+import 'package:test_api/hooks.dart';
 
-import '../util/pretty_print.dart';
 import 'async_matcher.dart';
-import 'format_stack_trace.dart';
+import 'util/pretty_print.dart';
 
 /// A matcher that matches events from [Stream]s or [StreamQueue]s.
 ///
@@ -164,7 +164,7 @@
           return addBullet(event.asValue!.value.toString());
         } else {
           var error = event.asError!;
-          var chain = formatStackTrace(error.stackTrace);
+          var chain = TestHandle.current.formatStackTrace(error.stackTrace);
           var text = '${error.error}\n$chain';
           return indent(text, first: '! ');
         }
diff --git a/pkgs/test_api/lib/src/frontend/stream_matchers.dart b/pkgs/test_api/lib/src/expect/stream_matchers.dart
similarity index 99%
rename from pkgs/test_api/lib/src/frontend/stream_matchers.dart
rename to pkgs/test_api/lib/src/expect/stream_matchers.dart
index 1e4ce96..95495ff 100644
--- a/pkgs/test_api/lib/src/frontend/stream_matchers.dart
+++ b/pkgs/test_api/lib/src/expect/stream_matchers.dart
@@ -5,10 +5,10 @@
 import 'package:async/async.dart';
 import 'package:matcher/matcher.dart';
 
-import '../util/pretty_print.dart';
 import 'async_matcher.dart';
 import 'stream_matcher.dart';
 import 'throws_matcher.dart';
+import 'util/pretty_print.dart';
 
 /// Returns a [StreamMatcher] that asserts that the stream emits a "done" event.
 final emitsDone = StreamMatcher(
diff --git a/pkgs/test_api/lib/src/frontend/throws_matcher.dart b/pkgs/test_api/lib/src/expect/throws_matcher.dart
similarity index 95%
rename from pkgs/test_api/lib/src/frontend/throws_matcher.dart
rename to pkgs/test_api/lib/src/expect/throws_matcher.dart
index ab97408..b4f081e 100644
--- a/pkgs/test_api/lib/src/frontend/throws_matcher.dart
+++ b/pkgs/test_api/lib/src/expect/throws_matcher.dart
@@ -3,10 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:matcher/matcher.dart';
+import 'package:test_api/hooks.dart';
 
-import '../util/pretty_print.dart';
 import 'async_matcher.dart';
-import 'format_stack_trace.dart';
+import 'util/pretty_print.dart';
 
 /// This function is deprecated.
 ///
@@ -125,8 +125,9 @@
     var buffer = StringBuffer();
     buffer.writeln(indent(prettyPrint(error), first: 'threw '));
     if (trace != null) {
-      buffer
-          .writeln(indent(formatStackTrace(trace).toString(), first: 'stack '));
+      buffer.writeln(indent(
+          TestHandle.current.formatStackTrace(trace).toString(),
+          first: 'stack '));
     }
     if (result.isNotEmpty) buffer.writeln(indent(result, first: 'which '));
     return buffer.toString().trimRight();
diff --git a/pkgs/test_api/lib/src/frontend/throws_matchers.dart b/pkgs/test_api/lib/src/expect/throws_matchers.dart
similarity index 100%
rename from pkgs/test_api/lib/src/frontend/throws_matchers.dart
rename to pkgs/test_api/lib/src/expect/throws_matchers.dart
diff --git a/pkgs/test_api/lib/src/util/placeholder.dart b/pkgs/test_api/lib/src/expect/util/placeholder.dart
similarity index 100%
rename from pkgs/test_api/lib/src/util/placeholder.dart
rename to pkgs/test_api/lib/src/expect/util/placeholder.dart
diff --git a/pkgs/test_api/lib/src/expect/util/pretty_print.dart b/pkgs/test_api/lib/src/expect/util/pretty_print.dart
new file mode 100644
index 0000000..f21399d
--- /dev/null
+++ b/pkgs/test_api/lib/src/expect/util/pretty_print.dart
@@ -0,0 +1,47 @@
+// Copyright (c) 2021, 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 'package:matcher/matcher.dart';
+import 'package:term_glyph/term_glyph.dart' as glyph;
+
+/// Indent each line in [string] by [first.length] spaces.
+///
+/// [first] is used in place of the first line's indentation.
+String indent(String text, {required String first}) {
+  final prefix = ' ' * first.length;
+  var lines = text.split('\n');
+  if (lines.length == 1) return '$first$text';
+
+  var buffer = StringBuffer('$first${lines.first}\n');
+
+  // Write out all but the first and last lines with [prefix].
+  for (var line in lines.skip(1).take(lines.length - 2)) {
+    buffer.writeln('$prefix$line');
+  }
+  buffer.write('$prefix${lines.last}');
+  return buffer.toString();
+}
+
+/// Returns a pretty-printed representation of [value].
+///
+/// The matcher package doesn't expose its pretty-print function directly, but
+/// we can use it through StringDescription.
+String prettyPrint(value) =>
+    StringDescription().addDescriptionOf(value).toString();
+
+/// Indents [text], and adds a bullet at the beginning.
+String addBullet(String text) => indent(text, first: '${glyph.bullet} ');
+
+/// Converts [strings] to a bulleted list.
+String bullet(Iterable<String> strings) => strings.map(addBullet).join('\n');
+
+/// Returns [name] if [number] is 1, or the plural of [name] otherwise.
+///
+/// By default, this just adds "s" to the end of [name] to get the plural. If
+/// [plural] is passed, that's used instead.
+String pluralize(String name, int number, {String? plural}) {
+  if (number == 1) return name;
+  if (plural != null) return plural;
+  return '${name}s';
+}
diff --git a/pkgs/test_api/lib/src/frontend/format_stack_trace.dart b/pkgs/test_api/lib/src/frontend/format_stack_trace.dart
deleted file mode 100644
index b6eee2b..0000000
--- a/pkgs/test_api/lib/src/frontend/format_stack_trace.dart
+++ /dev/null
@@ -1,22 +0,0 @@
-// 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 'package:stack_trace/stack_trace.dart';
-
-import '../backend/stack_trace_formatter.dart';
-
-/// The default formatter to use for formatting stack traces.
-///
-/// This is used in situations where the zone-scoped formatter is unavailable,
-/// such as when running via `dart path/to/test.dart'.
-final _defaultFormatter = StackTraceFormatter();
-
-/// Converts [stackTrace] to a [Chain] according to the current test's
-/// configuration.
-///
-/// If [verbose] is `true`, this doesn't fold out irrelevant stack frames. It
-/// defaults to the current test's `verbose_trace` configuration.
-Chain formatStackTrace(StackTrace stackTrace, {bool? verbose}) =>
-    (StackTraceFormatter.current ?? _defaultFormatter)
-        .formatStackTrace(stackTrace, verbose: verbose);
diff --git a/pkgs/test_api/lib/src/frontend/utils.dart b/pkgs/test_api/lib/src/frontend/utils.dart
deleted file mode 100644
index ac023e5..0000000
--- a/pkgs/test_api/lib/src/frontend/utils.dart
+++ /dev/null
@@ -1,16 +0,0 @@
-// 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.
-
-/// Returns a [Future] that completes after the [event loop][] has run the given
-/// number of [times] (20 by default).
-///
-/// [event loop]: https://medium.com/dartlang/dart-asynchronous-programming-isolates-and-event-loops-bffc3e296a6a
-///
-/// Awaiting this approximates waiting until all asynchronous work (other than
-/// work that's waiting for external resources) completes.
-Future pumpEventQueue({int times = 20}) {
-  if (times == 0) return Future.value();
-  // Use the event loop to allow the microtask queue to finish.
-  return Future(() => pumpEventQueue(times: times - 1));
-}
diff --git a/pkgs/test_api/lib/src/frontend/on_platform.dart b/pkgs/test_api/lib/src/scaffolding/on_platform.dart
similarity index 100%
rename from pkgs/test_api/lib/src/frontend/on_platform.dart
rename to pkgs/test_api/lib/src/scaffolding/on_platform.dart
diff --git a/pkgs/test_api/lib/src/frontend/retry.dart b/pkgs/test_api/lib/src/scaffolding/retry.dart
similarity index 100%
rename from pkgs/test_api/lib/src/frontend/retry.dart
rename to pkgs/test_api/lib/src/scaffolding/retry.dart
diff --git a/pkgs/test_api/lib/src/frontend/skip.dart b/pkgs/test_api/lib/src/scaffolding/skip.dart
similarity index 100%
rename from pkgs/test_api/lib/src/frontend/skip.dart
rename to pkgs/test_api/lib/src/scaffolding/skip.dart
diff --git a/pkgs/test_api/lib/src/frontend/spawn_hybrid.dart b/pkgs/test_api/lib/src/scaffolding/spawn_hybrid.dart
similarity index 100%
rename from pkgs/test_api/lib/src/frontend/spawn_hybrid.dart
rename to pkgs/test_api/lib/src/scaffolding/spawn_hybrid.dart
diff --git a/pkgs/test_api/lib/src/frontend/tags.dart b/pkgs/test_api/lib/src/scaffolding/tags.dart
similarity index 100%
rename from pkgs/test_api/lib/src/frontend/tags.dart
rename to pkgs/test_api/lib/src/scaffolding/tags.dart
diff --git a/pkgs/test_api/lib/src/frontend/test_on.dart b/pkgs/test_api/lib/src/scaffolding/test_on.dart
similarity index 100%
rename from pkgs/test_api/lib/src/frontend/test_on.dart
rename to pkgs/test_api/lib/src/scaffolding/test_on.dart
diff --git a/pkgs/test_api/lib/src/scaffolding/test_structure.dart b/pkgs/test_api/lib/src/scaffolding/test_structure.dart
new file mode 100644
index 0000000..1a99435
--- /dev/null
+++ b/pkgs/test_api/lib/src/scaffolding/test_structure.dart
@@ -0,0 +1,249 @@
+// Copyright (c) 2021, 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:meta/meta.dart';
+
+import '../backend/declarer.dart';
+import '../backend/invoker.dart';
+import '../scaffolding/timeout.dart';
+
+// test_core does not support running tests directly, so the Declarer should
+// always be on the Zone.
+Declarer get _declarer => Zone.current[#test.declarer] as Declarer;
+
+// TODO(nweiz): This and other top-level functions should throw exceptions if
+// they're called after the declarer has finished declaring.
+/// Creates a new test case with the given description (converted to a string)
+/// and body.
+///
+/// The description will be added to the descriptions of any surrounding
+/// [group]s. If [testOn] is passed, it's parsed as a [platform selector][]; the
+/// test will only be run on matching platforms.
+///
+/// [platform selector]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors
+///
+/// If [timeout] is passed, it's used to modify or replace the default timeout
+/// of 30 seconds. Timeout modifications take precedence in suite-group-test
+/// order, so [timeout] will also modify any timeouts set on the group or suite.
+///
+/// If [skip] is a String or `true`, the test is skipped. If it's a String, it
+/// should explain why the test is skipped; this reason will be printed instead
+/// of running the test.
+///
+/// If [tags] is passed, it declares user-defined tags that are applied to the
+/// test. These tags can be used to select or skip the test on the command line,
+/// or to do bulk test configuration. All tags should be declared in the
+/// [package configuration file][configuring tags]. The parameter can be an
+/// [Iterable] of tag names, or a [String] representing a single tag.
+///
+/// If [retry] is passed, the test will be retried the provided number of times
+/// before being marked as a failure.
+///
+/// [configuring tags]: https://github.com/dart-lang/test/blob/master/doc/package_config.md#configuring-tags
+///
+/// [onPlatform] allows tests to be configured on a platform-by-platform
+/// basis. It's a map from strings that are parsed as [PlatformSelector]s to
+/// annotation classes: [Timeout], [Skip], or lists of those. These
+/// annotations apply only on the given platforms. For example:
+///
+///     test('potentially slow test', () {
+///       // ...
+///     }, onPlatform: {
+///       // This test is especially slow on Windows.
+///       'windows': Timeout.factor(2),
+///       'browser': [
+///         Skip('TODO: add browser support'),
+///         // This will be slow on browsers once it works on them.
+///         Timeout.factor(2)
+///       ]
+///     });
+///
+/// If multiple platforms match, the annotations apply in order as through
+/// they were in nested groups.
+///
+/// If the `solo` flag is `true`, only tests and groups marked as
+/// "solo" will be be run. This only restricts tests *within this test
+/// suite*—tests in other suites will run as normal. We recommend that users
+/// avoid this flag if possible and instead use the test runner flag `-n` to
+/// filter tests by name.
+@isTest
+void test(description, dynamic Function() body,
+    {String? testOn,
+    Timeout? timeout,
+    skip,
+    tags,
+    Map<String, dynamic>? onPlatform,
+    int? retry,
+    @deprecated bool solo = false}) {
+  _declarer.test(description.toString(), body,
+      testOn: testOn,
+      timeout: timeout,
+      skip: skip,
+      onPlatform: onPlatform,
+      tags: tags,
+      retry: retry,
+      solo: solo);
+
+  // Force dart2js not to inline this function. We need it to be separate from
+  // `main()` in JS stack traces in order to properly determine the line and
+  // column where the test was defined. See sdk#26705.
+  return;
+  return; // ignore: dead_code
+}
+
+/// Creates a group of tests.
+///
+/// A group's description (converted to a string) is included in the descriptions
+/// of any tests or sub-groups it contains. [setUp] and [tearDown] are also scoped
+/// to the containing group.
+///
+/// If [testOn] is passed, it's parsed as a [platform selector][]; the test will
+/// only be run on matching platforms.
+///
+/// [platform selector]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors
+///
+/// If [timeout] is passed, it's used to modify or replace the default timeout
+/// of 30 seconds. Timeout modifications take precedence in suite-group-test
+/// order, so [timeout] will also modify any timeouts set on the suite, and will
+/// be modified by any timeouts set on individual tests.
+///
+/// If [skip] is a String or `true`, the group is skipped. If it's a String, it
+/// should explain why the group is skipped; this reason will be printed instead
+/// of running the group's tests.
+///
+/// If [tags] is passed, it declares user-defined tags that are applied to the
+/// test. These tags can be used to select or skip the test on the command line,
+/// or to do bulk test configuration. All tags should be declared in the
+/// [package configuration file][configuring tags]. The parameter can be an
+/// [Iterable] of tag names, or a [String] representing a single tag.
+///
+/// [configuring tags]: https://github.com/dart-lang/test/blob/master/doc/package_config.md#configuring-tags
+///
+/// [onPlatform] allows groups to be configured on a platform-by-platform
+/// basis. It's a map from strings that are parsed as [PlatformSelector]s to
+/// annotation classes: [Timeout], [Skip], or lists of those. These
+/// annotations apply only on the given platforms. For example:
+///
+///     group('potentially slow tests', () {
+///       // ...
+///     }, onPlatform: {
+///       // These tests are especially slow on Windows.
+///       'windows': Timeout.factor(2),
+///       'browser': [
+///         Skip('TODO: add browser support'),
+///         // They'll be slow on browsers once it works on them.
+///         Timeout.factor(2)
+///       ]
+///     });
+///
+/// If multiple platforms match, the annotations apply in order as through
+/// they were in nested groups.
+///
+/// If the `solo` flag is `true`, only tests and groups marked as
+/// "solo" will be be run. This only restricts tests *within this test
+/// suite*—tests in other suites will run as normal. We recommend that users
+/// avoid this flag if possible, and instead use the test runner flag `-n` to
+/// filter tests by name.
+@isTestGroup
+void group(description, dynamic Function() body,
+    {String? testOn,
+    Timeout? timeout,
+    skip,
+    tags,
+    Map<String, dynamic>? onPlatform,
+    int? retry,
+    @deprecated bool solo = false}) {
+  _declarer.group(description.toString(), body,
+      testOn: testOn,
+      timeout: timeout,
+      skip: skip,
+      tags: tags,
+      onPlatform: onPlatform,
+      retry: retry,
+      solo: solo);
+
+  // Force dart2js not to inline this function. We need it to be separate from
+  // `main()` in JS stack traces in order to properly determine the line and
+  // column where the test was defined. See sdk#26705.
+  return;
+  return; // ignore: dead_code
+}
+
+/// Registers a function to be run before tests.
+///
+/// This function will be called before each test is run. [callback] may be
+/// asynchronous; if so, it must return a [Future].
+///
+/// If this is called within a test group, it applies only to tests in that
+/// group. [callback] will be run after any set-up callbacks in parent groups or
+/// at the top level.
+///
+/// Each callback at the top level or in a given group will be run in the order
+/// they were declared.
+void setUp(dynamic Function() callback) => _declarer.setUp(callback);
+
+/// Registers a function to be run after tests.
+///
+/// This function will be called after each test is run. [callback] may be
+/// asynchronous; if so, it must return a [Future].
+///
+/// If this is called within a test group, it applies only to tests in that
+/// group. [callback] will be run before any tear-down callbacks in parent
+/// groups or at the top level.
+///
+/// Each callback at the top level or in a given group will be run in the
+/// reverse of the order they were declared.
+///
+/// See also [addTearDown], which adds tear-downs to a running test.
+void tearDown(dynamic Function() callback) => _declarer.tearDown(callback);
+
+/// Registers a function to be run after the current test.
+///
+/// This is called within a running test, and adds a tear-down only for the
+/// current test. It allows testing libraries to add cleanup logic as soon as
+/// there's something to clean up.
+///
+/// The [callback] is run before any callbacks registered with [tearDown]. Like
+/// [tearDown], the most recently registered callback is run first.
+///
+/// If this is called from within a [setUpAll] or [tearDownAll] callback, it
+/// instead runs the function after *all* tests in the current test suite.
+void addTearDown(dynamic Function() callback) {
+  if (Invoker.current == null) {
+    throw StateError('addTearDown() may only be called within a test.');
+  }
+
+  Invoker.current!.addTearDown(callback);
+}
+
+/// Registers a function to be run once before all tests.
+///
+/// [callback] may be asynchronous; if so, it must return a [Future].
+///
+/// If this is called within a test group, [callback] will run before all tests
+/// in that group. It will be run after any [setUpAll] callbacks in parent
+/// groups or at the top level. It won't be run if none of the tests in the
+/// group are run.
+///
+/// **Note**: This function makes it very easy to accidentally introduce hidden
+/// dependencies between tests that should be isolated. In general, you should
+/// prefer [setUp], and only use [setUpAll] if the callback is prohibitively
+/// slow.
+void setUpAll(dynamic Function() callback) => _declarer.setUpAll(callback);
+
+/// Registers a function to be run once after all tests.
+///
+/// If this is called within a test group, [callback] will run after all tests
+/// in that group. It will be run before any [tearDownAll] callbacks in parent
+/// groups or at the top level. It won't be run if none of the tests in the
+/// group are run.
+///
+/// **Note**: This function makes it very easy to accidentally introduce hidden
+/// dependencies between tests that should be isolated. In general, you should
+/// prefer [tearDown], and only use [tearDownAll] if the callback is
+/// prohibitively slow.
+void tearDownAll(dynamic Function() callback) =>
+    _declarer.tearDownAll(callback);
diff --git a/pkgs/test_api/lib/src/frontend/timeout.dart b/pkgs/test_api/lib/src/scaffolding/timeout.dart
similarity index 100%
rename from pkgs/test_api/lib/src/frontend/timeout.dart
rename to pkgs/test_api/lib/src/scaffolding/timeout.dart
diff --git a/pkgs/test_api/lib/src/scaffolding/utils.dart b/pkgs/test_api/lib/src/scaffolding/utils.dart
new file mode 100644
index 0000000..1f2fa5d
--- /dev/null
+++ b/pkgs/test_api/lib/src/scaffolding/utils.dart
@@ -0,0 +1,43 @@
+// Copyright (c) 2021, 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 '../backend/invoker.dart';
+
+/// Returns a [Future] that completes after the [event loop][] has run the given
+/// number of [times] (20 by default).
+///
+/// [event loop]: https://medium.com/dartlang/dart-asynchronous-programming-isolates-and-event-loops-bffc3e296a6a
+///
+/// Awaiting this approximates waiting until all asynchronous work (other than
+/// work that's waiting for external resources) completes.
+Future pumpEventQueue({int times = 20}) {
+  if (times == 0) return Future.value();
+  // Use the event loop to allow the microtask queue to finish.
+  return Future(() => pumpEventQueue(times: times - 1));
+}
+
+/// Registers an exception that was caught for the current test.
+void registerException(Object error,
+    [StackTrace stackTrace = StackTrace.empty]) {
+  // This will usually forward directly to [Invoker.current.handleError], but
+  // going through the zone API allows other zones to consistently see errors.
+  Zone.current.handleUncaughtError(error, stackTrace);
+}
+
+/// Prints [message] if and when the current test fails.
+///
+/// This is intended for test infrastructure to provide debugging information
+/// without cluttering the output for successful tests. Note that unlike
+/// [print], each individual message passed to [printOnFailure] will be
+/// separated by a blank line.
+void printOnFailure(String message) => Invoker.current!.printOnFailure(message);
+
+/// Marks the current test as skipped.
+///
+/// A skipped test may still fail if any exception is thrown, including uncaught
+/// asynchronous errors. If the entire test should be skipped `return` from the
+/// test body after marking it as skipped.
+void markTestSkipped(String message) => Invoker.current!.skip(message);
diff --git a/pkgs/test_api/lib/src/util/pretty_print.dart b/pkgs/test_api/lib/src/util/pretty_print.dart
index 621f1f6..5c011c5 100644
--- a/pkgs/test_api/lib/src/util/pretty_print.dart
+++ b/pkgs/test_api/lib/src/util/pretty_print.dart
@@ -2,9 +2,6 @@
 // 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 'package:matcher/matcher.dart';
-import 'package:term_glyph/term_glyph.dart' as glyph;
-
 /// Returns [name] if [number] is 1, or the plural of [name] otherwise.
 ///
 /// By default, this just adds "s" to the end of [name] to get the plural. If
@@ -15,37 +12,6 @@
   return '${name}s';
 }
 
-/// Indent each line in [string] by [first.length] spaces.
-///
-/// [first] is used in place of the first line's indentation.
-String indent(String text, {required String first}) {
-  final prefix = ' ' * first.length;
-  var lines = text.split('\n');
-  if (lines.length == 1) return '$first$text';
-
-  var buffer = StringBuffer('$first${lines.first}\n');
-
-  // Write out all but the first and last lines with [prefix].
-  for (var line in lines.skip(1).take(lines.length - 2)) {
-    buffer.writeln('$prefix$line');
-  }
-  buffer.write('$prefix${lines.last}');
-  return buffer.toString();
-}
-
-/// Indents [text], and adds a bullet at the beginning.
-String addBullet(String text) => indent(text, first: '${glyph.bullet} ');
-
-/// Converts [strings] to a bulleted list.
-String bullet(Iterable<String> strings) => strings.map(addBullet).join('\n');
-
-/// Returns a pretty-printed representation of [value].
-///
-/// The matcher package doesn't expose its pretty-print function directly, but
-/// we can use it through StringDescription.
-String prettyPrint(value) =>
-    StringDescription().addDescriptionOf(value).toString();
-
 /// Returns a sentence fragment listing the elements of [iter].
 ///
 /// This converts each element of [iter] to a string and separates them with
diff --git a/pkgs/test_api/lib/src/util/remote_exception.dart b/pkgs/test_api/lib/src/util/remote_exception.dart
index 8f2e45a..a38104e 100644
--- a/pkgs/test_api/lib/src/util/remote_exception.dart
+++ b/pkgs/test_api/lib/src/util/remote_exception.dart
@@ -6,7 +6,7 @@
 
 import 'package:stack_trace/stack_trace.dart';
 
-import '../frontend/expect.dart';
+import '../../hooks.dart' show TestFailure;
 
 /// An exception that was thrown remotely.
 ///
diff --git a/pkgs/test_api/lib/test_api.dart b/pkgs/test_api/lib/test_api.dart
index 7ab4b13..3ee5866 100644
--- a/pkgs/test_api/lib/test_api.dart
+++ b/pkgs/test_api/lib/test_api.dart
@@ -6,291 +6,12 @@
     'Please use package:test.')
 library test_api;
 
-import 'dart:async';
-
-import 'package:meta/meta.dart';
-
-import 'src/backend/declarer.dart';
-import 'src/backend/invoker.dart';
-import 'src/frontend/timeout.dart';
-
-export 'package:matcher/matcher.dart';
-
-export 'src/frontend/expect.dart' hide formatFailure;
-export 'src/frontend/expect_async.dart';
-export 'src/frontend/future_matchers.dart';
-export 'src/frontend/never_called.dart';
-export 'src/frontend/on_platform.dart';
-export 'src/frontend/prints_matcher.dart';
-export 'src/frontend/retry.dart';
-export 'src/frontend/skip.dart';
-export 'src/frontend/spawn_hybrid.dart';
-export 'src/frontend/stream_matcher.dart';
-export 'src/frontend/stream_matchers.dart';
-export 'src/frontend/tags.dart';
-export 'src/frontend/test_on.dart';
-export 'src/frontend/throws_matcher.dart';
-export 'src/frontend/throws_matchers.dart';
-export 'src/frontend/timeout.dart';
-export 'src/frontend/utils.dart';
-
-// test_core does not support running tests directly, so the Declarer should
-// always be on the Zone.
-Declarer get _declarer => Zone.current[#test.declarer] as Declarer;
-
-// TODO(nweiz): This and other top-level functions should throw exceptions if
-// they're called after the declarer has finished declaring.
-/// Creates a new test case with the given description (converted to a string)
-/// and body.
-///
-/// The description will be added to the descriptions of any surrounding
-/// [group]s. If [testOn] is passed, it's parsed as a [platform selector][]; the
-/// test will only be run on matching platforms.
-///
-/// [platform selector]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors
-///
-/// If [timeout] is passed, it's used to modify or replace the default timeout
-/// of 30 seconds. Timeout modifications take precedence in suite-group-test
-/// order, so [timeout] will also modify any timeouts set on the group or suite.
-///
-/// If [skip] is a String or `true`, the test is skipped. If it's a String, it
-/// should explain why the test is skipped; this reason will be printed instead
-/// of running the test.
-///
-/// If [tags] is passed, it declares user-defined tags that are applied to the
-/// test. These tags can be used to select or skip the test on the command line,
-/// or to do bulk test configuration. All tags should be declared in the
-/// [package configuration file][configuring tags]. The parameter can be an
-/// [Iterable] of tag names, or a [String] representing a single tag.
-///
-/// If [retry] is passed, the test will be retried the provided number of times
-/// before being marked as a failure.
-///
-/// [configuring tags]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#configuring-tags
-///
-/// [onPlatform] allows tests to be configured on a platform-by-platform
-/// basis. It's a map from strings that are parsed as [PlatformSelector]s to
-/// annotation classes: [Timeout], [Skip], or lists of those. These
-/// annotations apply only on the given platforms. For example:
-///
-///     test('potentially slow test', () {
-///       // ...
-///     }, onPlatform: {
-///       // This test is especially slow on Windows.
-///       'windows': Timeout.factor(2),
-///       'browser': [
-///         Skip('TODO: add browser support'),
-///         // This will be slow on browsers once it works on them.
-///         Timeout.factor(2)
-///       ]
-///     });
-///
-/// If multiple platforms match, the annotations apply in order as through
-/// they were in nested groups.
-///
-/// If the `solo` flag is `true`, only tests and groups marked as
-/// "solo" will be be run. This only restricts tests *within this test
-/// suite*—tests in other suites will run as normal. We recommend that users
-/// avoid this flag if possible and instead use the test runner flag `-n` to
-/// filter tests by name.
-@isTest
-void test(description, dynamic Function() body,
-    {String? testOn,
-    Timeout? timeout,
-    skip,
-    tags,
-    Map<String, dynamic>? onPlatform,
-    int? retry,
-    @deprecated bool solo = false}) {
-  _declarer.test(description.toString(), body,
-      testOn: testOn,
-      timeout: timeout,
-      skip: skip,
-      onPlatform: onPlatform,
-      tags: tags,
-      retry: retry,
-      solo: solo);
-
-  // Force dart2js not to inline this function. We need it to be separate from
-  // `main()` in JS stack traces in order to properly determine the line and
-  // column where the test was defined. See sdk#26705.
-  return;
-  return; // ignore: dead_code
-}
-
-/// Creates a group of tests.
-///
-/// A group's description (converted to a string) is included in the descriptions
-/// of any tests or sub-groups it contains. [setUp] and [tearDown] are also scoped
-/// to the containing group.
-///
-/// If [testOn] is passed, it's parsed as a [platform selector][]; the test will
-/// only be run on matching platforms.
-///
-/// [platform selector]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors
-///
-/// If [timeout] is passed, it's used to modify or replace the default timeout
-/// of 30 seconds. Timeout modifications take precedence in suite-group-test
-/// order, so [timeout] will also modify any timeouts set on the suite, and will
-/// be modified by any timeouts set on individual tests.
-///
-/// If [skip] is a String or `true`, the group is skipped. If it's a String, it
-/// should explain why the group is skipped; this reason will be printed instead
-/// of running the group's tests.
-///
-/// If [tags] is passed, it declares user-defined tags that are applied to the
-/// test. These tags can be used to select or skip the test on the command line,
-/// or to do bulk test configuration. All tags should be declared in the
-/// [package configuration file][configuring tags]. The parameter can be an
-/// [Iterable] of tag names, or a [String] representing a single tag.
-///
-/// [configuring tags]: https://github.com/dart-lang/test/blob/master/doc/package_config.md#configuring-tags
-///
-/// [onPlatform] allows groups to be configured on a platform-by-platform
-/// basis. It's a map from strings that are parsed as [PlatformSelector]s to
-/// annotation classes: [Timeout], [Skip], or lists of those. These
-/// annotations apply only on the given platforms. For example:
-///
-///     group('potentially slow tests', () {
-///       // ...
-///     }, onPlatform: {
-///       // These tests are especially slow on Windows.
-///       'windows': Timeout.factor(2),
-///       'browser': [
-///         Skip('TODO: add browser support'),
-///         // They'll be slow on browsers once it works on them.
-///         Timeout.factor(2)
-///       ]
-///     });
-///
-/// If multiple platforms match, the annotations apply in order as through
-/// they were in nested groups.
-///
-/// If the `solo` flag is `true`, only tests and groups marked as
-/// "solo" will be be run. This only restricts tests *within this test
-/// suite*—tests in other suites will run as normal. We recommend that users
-/// avoid this flag if possible, and instead use the test runner flag `-n` to
-/// filter tests by name.
-@isTestGroup
-void group(description, dynamic Function() body,
-    {String? testOn,
-    Timeout? timeout,
-    skip,
-    tags,
-    Map<String, dynamic>? onPlatform,
-    int? retry,
-    @deprecated bool solo = false}) {
-  _declarer.group(description.toString(), body,
-      testOn: testOn,
-      timeout: timeout,
-      skip: skip,
-      tags: tags,
-      onPlatform: onPlatform,
-      retry: retry,
-      solo: solo);
-
-  // Force dart2js not to inline this function. We need it to be separate from
-  // `main()` in JS stack traces in order to properly determine the line and
-  // column where the test was defined. See sdk#26705.
-  return;
-  return; // ignore: dead_code
-}
-
-/// Registers a function to be run before tests.
-///
-/// This function will be called before each test is run. [callback] may be
-/// asynchronous; if so, it must return a [Future].
-///
-/// If this is called within a test group, it applies only to tests in that
-/// group. [callback] will be run after any set-up callbacks in parent groups or
-/// at the top level.
-///
-/// Each callback at the top level or in a given group will be run in the order
-/// they were declared.
-void setUp(dynamic Function() callback) => _declarer.setUp(callback);
-
-/// Registers a function to be run after tests.
-///
-/// This function will be called after each test is run. [callback] may be
-/// asynchronous; if so, it must return a [Future].
-///
-/// If this is called within a test group, it applies only to tests in that
-/// group. [callback] will be run before any tear-down callbacks in parent
-/// groups or at the top level.
-///
-/// Each callback at the top level or in a given group will be run in the
-/// reverse of the order they were declared.
-///
-/// See also [addTearDown], which adds tear-downs to a running test.
-void tearDown(dynamic Function() callback) => _declarer.tearDown(callback);
-
-/// Registers a function to be run after the current test.
-///
-/// This is called within a running test, and adds a tear-down only for the
-/// current test. It allows testing libraries to add cleanup logic as soon as
-/// there's something to clean up.
-///
-/// The [callback] is run before any callbacks registered with [tearDown]. Like
-/// [tearDown], the most recently registered callback is run first.
-///
-/// If this is called from within a [setUpAll] or [tearDownAll] callback, it
-/// instead runs the function after *all* tests in the current test suite.
-void addTearDown(dynamic Function() callback) {
-  if (Invoker.current == null) {
-    throw StateError('addTearDown() may only be called within a test.');
-  }
-
-  Invoker.current!.addTearDown(callback);
-}
-
-/// Registers a function to be run once before all tests.
-///
-/// [callback] may be asynchronous; if so, it must return a [Future].
-///
-/// If this is called within a test group, [callback] will run before all tests
-/// in that group. It will be run after any [setUpAll] callbacks in parent
-/// groups or at the top level. It won't be run if none of the tests in the
-/// group are run.
-///
-/// **Note**: This function makes it very easy to accidentally introduce hidden
-/// dependencies between tests that should be isolated. In general, you should
-/// prefer [setUp], and only use [setUpAll] if the callback is prohibitively
-/// slow.
-void setUpAll(dynamic Function() callback) => _declarer.setUpAll(callback);
-
-/// Registers a function to be run once after all tests.
-///
-/// If this is called within a test group, [callback] will run after all tests
-/// in that group. It will be run before any [tearDownAll] callbacks in parent
-/// groups or at the top level. It won't be run if none of the tests in the
-/// group are run.
-///
-/// **Note**: This function makes it very easy to accidentally introduce hidden
-/// dependencies between tests that should be isolated. In general, you should
-/// prefer [tearDown], and only use [tearDownAll] if the callback is
-/// prohibitively slow.
-void tearDownAll(dynamic Function() callback) =>
-    _declarer.tearDownAll(callback);
-
-/// Registers an exception that was caught for the current test.
-void registerException(Object error,
-    [StackTrace stackTrace = StackTrace.empty]) {
-  // This will usually forward directly to [Invoker.current.handleError], but
-  // going through the zone API allows other zones to consistently see errors.
-  Zone.current.handleUncaughtError(error, stackTrace);
-}
-
-/// Prints [message] if and when the current test fails.
-///
-/// This is intended for test infrastructure to provide debugging information
-/// without cluttering the output for successful tests. Note that unlike
-/// [print], each individual message passed to [printOnFailure] will be
-/// separated by a blank line.
-void printOnFailure(String message) => Invoker.current!.printOnFailure(message);
-
-/// Marks the current test as skipped.
-///
-/// A skipped test may still fail if any exception is thrown, including uncaught
-/// asynchronous errors. If the entire test should be skipped `return` from the
-/// test body after marking it as skipped.
-void markTestSkipped(String message) => Invoker.current!.skip(message);
+export 'expect.dart';
+export 'hooks.dart' show TestFailure;
+export 'scaffolding.dart';
+// Not yet deprecated, but not exposed through focused libraries.
+export 'src/scaffolding/utils.dart' show registerException;
+// Deprecated exports not surfaced through focused libraries.
+export 'src/expect/expect.dart' show ErrorFormatter;
+export 'src/expect/expect_async.dart' show expectAsync;
+export 'src/expect/throws_matcher.dart' show throws, Throws;
diff --git a/pkgs/test_api/test/backend/declarer_test.dart b/pkgs/test_api/test/backend/declarer_test.dart
index 69531d7..8426857 100644
--- a/pkgs/test_api/test/backend/declarer_test.dart
+++ b/pkgs/test_api/test/backend/declarer_test.dart
@@ -4,12 +4,11 @@
 
 import 'dart:async';
 
+import 'package:test/test.dart';
 import 'package:test_api/src/backend/group.dart';
 import 'package:test_api/src/backend/invoker.dart';
 import 'package:test_api/src/backend/suite.dart';
 import 'package:test_api/src/backend/test.dart';
-import 'package:test_api/src/frontend/timeout.dart';
-import 'package:test/test.dart';
 
 import '../utils.dart';
 
diff --git a/pkgs/test_api/test/backend/metadata_test.dart b/pkgs/test_api/test/backend/metadata_test.dart
index 44f1784..5a099df 100644
--- a/pkgs/test_api/test/backend/metadata_test.dart
+++ b/pkgs/test_api/test/backend/metadata_test.dart
@@ -8,8 +8,6 @@
 import 'package:test_api/src/backend/platform_selector.dart';
 import 'package:test_api/src/backend/runtime.dart';
 import 'package:test_api/src/backend/suite_platform.dart';
-import 'package:test_api/src/frontend/skip.dart';
-import 'package:test_api/src/frontend/timeout.dart';
 import 'package:test/test.dart';
 
 void main() {
diff --git a/pkgs/test_api/test/frontend/expect_async_test.dart b/pkgs/test_api/test/frontend/expect_async_test.dart
index 539ce2d..fb68246 100644
--- a/pkgs/test_api/test/frontend/expect_async_test.dart
+++ b/pkgs/test_api/test/frontend/expect_async_test.dart
@@ -2,6 +2,8 @@
 // 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:test_api/src/backend/live_test.dart';
 import 'package:test_api/src/backend/state.dart';
 import 'package:test/test.dart';
@@ -306,6 +308,14 @@
     expectTestPassed(liveTest);
   });
 
+  test('may be called in a non-test zone', () async {
+    var liveTest = await runTestBody(() {
+      var callback = expectAsync0(() {});
+      Zone.root.run(callback);
+    });
+    expectTestPassed(liveTest);
+  });
+
   group('old-style expectAsync()', () {
     test('works with no arguments', () async {
       var callbackRun = false;
diff --git a/pkgs/test_core/CHANGELOG.md b/pkgs/test_core/CHANGELOG.md
index 3602465..e733f23 100644
--- a/pkgs/test_core/CHANGELOG.md
+++ b/pkgs/test_core/CHANGELOG.md
@@ -1,5 +1,7 @@
 ## 0.3.20-dev
 
+* Add library `scaffolding.dart` to allow importing a subset of the normal
+  surface area.
 * Remove `suiteChannel`. This is now handled by an additional argument to the
   `beforeLoad` callback in `serializeSuite`.
 * Disable stack trace chaining by default.
diff --git a/pkgs/test_core/lib/scaffolding.dart b/pkgs/test_core/lib/scaffolding.dart
new file mode 100644
index 0000000..ec4dd03
--- /dev/null
+++ b/pkgs/test_core/lib/scaffolding.dart
@@ -0,0 +1,290 @@
+// Copyright (c) 2021, 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.
+
+@Deprecated('package:test_core is not intended for general use. '
+    'Please use package:test.')
+library test_core.scaffolding;
+
+import 'dart:async';
+
+import 'package:meta/meta.dart' show isTest, isTestGroup;
+import 'package:path/path.dart' as p;
+import 'package:test_api/backend.dart'; //ignore: deprecated_member_use
+import 'package:test_api/scaffolding.dart' show Timeout, pumpEventQueue;
+import 'package:test_api/src/backend/declarer.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/invoker.dart'; // ignore: implementation_imports
+
+import 'src/runner/engine.dart';
+import 'src/runner/plugin/environment.dart';
+import 'src/runner/reporter/expanded.dart';
+import 'src/runner/runner_suite.dart';
+import 'src/runner/suite.dart';
+import 'src/util/async.dart';
+import 'src/util/os.dart';
+import 'src/util/print_sink.dart';
+
+// Hide implementations which don't support being run directly.
+// This file is an almost direct copy of import below, but with the global
+// declarer added.
+export 'package:test_api/scaffolding.dart'
+    hide test, group, setUp, setUpAll, tearDown, tearDownAll;
+
+/// The global declarer.
+///
+/// This is used if a test file is run directly, rather than through the runner.
+Declarer? _globalDeclarer;
+
+/// Gets the declarer for the current scope.
+///
+/// When using the runner, this returns the [Zone]-scoped declarer that's set by
+/// [IsolateListener] or [IframeListener]. If the test file is run directly,
+/// this returns [_globalDeclarer] (and sets it up on the first call).
+Declarer get _declarer {
+  var declarer = Declarer.current;
+  if (declarer != null) return declarer;
+  if (_globalDeclarer != null) return _globalDeclarer!;
+
+  // Since there's no Zone-scoped declarer, the test file is being run directly.
+  // In order to run the tests, we set up our own Declarer via
+  // [_globalDeclarer], and pump the event queue as a best effort to wait for
+  // all tests to be defined before starting them.
+  _globalDeclarer = Declarer();
+
+  () async {
+    await pumpEventQueue();
+
+    var suite = RunnerSuite(const PluginEnvironment(), SuiteConfiguration.empty,
+        _globalDeclarer!.build(), SuitePlatform(Runtime.vm, os: currentOSGuess),
+        path: p.prettyUri(Uri.base));
+
+    var engine = Engine();
+    engine.suiteSink.add(suite);
+    engine.suiteSink.close();
+    ExpandedReporter.watch(engine, PrintSink(),
+        color: true, printPath: false, printPlatform: false);
+
+    var success = await runZoned(() => Invoker.guard(engine.run),
+        zoneValues: {#test.declarer: _globalDeclarer});
+    if (success == true) return null;
+    print('');
+    unawaited(Future.error('Dummy exception to set exit code.'));
+  }();
+
+  return _globalDeclarer!;
+}
+
+// TODO(nweiz): This and other top-level functions should throw exceptions if
+// they're called after the declarer has finished declaring.
+/// Creates a new test case with the given description (converted to a string)
+/// and body.
+///
+/// The description will be added to the descriptions of any surrounding
+/// [group]s. If [testOn] is passed, it's parsed as a [platform selector][]; the
+/// test will only be run on matching platforms.
+///
+/// [platform selector]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors
+///
+/// If [timeout] is passed, it's used to modify or replace the default timeout
+/// of 30 seconds. Timeout modifications take precedence in suite-group-test
+/// order, so [timeout] will also modify any timeouts set on the group or suite.
+///
+/// If [skip] is a String or `true`, the test is skipped. If it's a String, it
+/// should explain why the test is skipped; this reason will be printed instead
+/// of running the test.
+///
+/// If [tags] is passed, it declares user-defined tags that are applied to the
+/// test. These tags can be used to select or skip the test on the command line,
+/// or to do bulk test configuration. All tags should be declared in the
+/// [package configuration file][configuring tags]. The parameter can be an
+/// [Iterable] of tag names, or a [String] representing a single tag.
+///
+/// If [retry] is passed, the test will be retried the provided number of times
+/// before being marked as a failure.
+///
+/// [configuring tags]: https://github.com/dart-lang/test/blob/master/doc/package_config.md#configuring-tags
+///
+/// [onPlatform] allows tests to be configured on a platform-by-platform
+/// basis. It's a map from strings that are parsed as [PlatformSelector]s to
+/// annotation classes: [Timeout], [Skip], or lists of those. These
+/// annotations apply only on the given platforms. For example:
+///
+///     test('potentially slow test', () {
+///       // ...
+///     }, onPlatform: {
+///       // This test is especially slow on Windows.
+///       'windows': Timeout.factor(2),
+///       'browser': [
+///         Skip('TODO: add browser support'),
+///         // This will be slow on browsers once it works on them.
+///         Timeout.factor(2)
+///       ]
+///     });
+///
+/// If multiple platforms match, the annotations apply in order as through
+/// they were in nested groups.
+///
+/// If the `solo` flag is `true`, only tests and groups marked as
+/// "solo" will be be run. This only restricts tests *within this test
+/// suite*—tests in other suites will run as normal. We recommend that users
+/// avoid this flag if possible and instead use the test runner flag `-n` to
+/// filter tests by name.
+@isTest
+void test(description, dynamic Function() body,
+    {String? testOn,
+    Timeout? timeout,
+    skip,
+    tags,
+    Map<String, dynamic>? onPlatform,
+    int? retry,
+    @deprecated bool solo = false}) {
+  _declarer.test(description.toString(), body,
+      testOn: testOn,
+      timeout: timeout,
+      skip: skip,
+      onPlatform: onPlatform,
+      tags: tags,
+      retry: retry,
+      solo: solo);
+
+  // Force dart2js not to inline this function. We need it to be separate from
+  // `main()` in JS stack traces in order to properly determine the line and
+  // column where the test was defined. See sdk#26705.
+  return;
+  return; // ignore: dead_code
+}
+
+/// Creates a group of tests.
+///
+/// A group's description (converted to a string) is included in the descriptions
+/// of any tests or sub-groups it contains. [setUp] and [tearDown] are also scoped
+/// to the containing group.
+///
+/// If [testOn] is passed, it's parsed as a [platform selector][]; the test will
+/// only be run on matching platforms.
+///
+/// [platform selector]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors
+///
+/// If [timeout] is passed, it's used to modify or replace the default timeout
+/// of 30 seconds. Timeout modifications take precedence in suite-group-test
+/// order, so [timeout] will also modify any timeouts set on the suite, and will
+/// be modified by any timeouts set on individual tests.
+///
+/// If [skip] is a String or `true`, the group is skipped. If it's a String, it
+/// should explain why the group is skipped; this reason will be printed instead
+/// of running the group's tests.
+///
+/// If [tags] is passed, it declares user-defined tags that are applied to the
+/// test. These tags can be used to select or skip the test on the command line,
+/// or to do bulk test configuration. All tags should be declared in the
+/// [package configuration file][configuring tags]. The parameter can be an
+/// [Iterable] of tag names, or a [String] representing a single tag.
+///
+/// [configuring tags]: https://github.com/dart-lang/test/blob/master/doc/package_config.md#configuring-tags
+///
+/// [onPlatform] allows groups to be configured on a platform-by-platform
+/// basis. It's a map from strings that are parsed as [PlatformSelector]s to
+/// annotation classes: [Timeout], [Skip], or lists of those. These
+/// annotations apply only on the given platforms. For example:
+///
+///     group('potentially slow tests', () {
+///       // ...
+///     }, onPlatform: {
+///       // These tests are especially slow on Windows.
+///       'windows': Timeout.factor(2),
+///       'browser': [
+///         Skip('TODO: add browser support'),
+///         // They'll be slow on browsers once it works on them.
+///         Timeout.factor(2)
+///       ]
+///     });
+///
+/// If multiple platforms match, the annotations apply in order as through
+/// they were in nested groups.
+///
+/// If the `solo` flag is `true`, only tests and groups marked as
+/// "solo" will be be run. This only restricts tests *within this test
+/// suite*—tests in other suites will run as normal. We recommend that users
+/// avoid this flag if possible, and instead use the test runner flag `-n` to
+/// filter tests by name.
+@isTestGroup
+void group(description, dynamic Function() body,
+    {String? testOn,
+    Timeout? timeout,
+    skip,
+    tags,
+    Map<String, dynamic>? onPlatform,
+    int? retry,
+    @deprecated bool solo = false}) {
+  _declarer.group(description.toString(), body,
+      testOn: testOn,
+      timeout: timeout,
+      skip: skip,
+      tags: tags,
+      onPlatform: onPlatform,
+      retry: retry,
+      solo: solo);
+
+  // Force dart2js not to inline this function. We need it to be separate from
+  // `main()` in JS stack traces in order to properly determine the line and
+  // column where the test was defined. See sdk#26705.
+  return;
+  return; // ignore: dead_code
+}
+
+/// Registers a function to be run before tests.
+///
+/// This function will be called before each test is run. [callback] may be
+/// asynchronous; if so, it must return a [Future].
+///
+/// If this is called within a test group, it applies only to tests in that
+/// group. [callback] will be run after any set-up callbacks in parent groups or
+/// at the top level.
+///
+/// Each callback at the top level or in a given group will be run in the order
+/// they were declared.
+void setUp(dynamic Function() callback) => _declarer.setUp(callback);
+
+/// Registers a function to be run after tests.
+///
+/// This function will be called after each test is run. [callback] may be
+/// asynchronous; if so, it must return a [Future].
+///
+/// If this is called within a test group, it applies only to tests in that
+/// group. [callback] will be run before any tear-down callbacks in parent
+/// groups or at the top level.
+///
+/// Each callback at the top level or in a given group will be run in the
+/// reverse of the order they were declared.
+///
+/// See also [addTearDown], which adds tear-downs to a running test.
+void tearDown(dynamic Function() callback) => _declarer.tearDown(callback);
+
+/// Registers a function to be run once before all tests.
+///
+/// [callback] may be asynchronous; if so, it must return a [Future].
+///
+/// If this is called within a test group, [callback] will run before all tests
+/// in that group. It will be run after any [setUpAll] callbacks in parent
+/// groups or at the top level. It won't be run if none of the tests in the
+/// group are run.
+///
+/// **Note**: This function makes it very easy to accidentally introduce hidden
+/// dependencies between tests that should be isolated. In general, you should
+/// prefer [setUp], and only use [setUpAll] if the callback is prohibitively
+/// slow.
+void setUpAll(dynamic Function() callback) => _declarer.setUpAll(callback);
+
+/// Registers a function to be run once after all tests.
+///
+/// If this is called within a test group, [callback] will run after all tests
+/// in that group. It will be run before any [tearDownAll] callbacks in parent
+/// groups or at the top level. It won't be run if none of the tests in the
+/// group are run.
+///
+/// **Note**: This function makes it very easy to accidentally introduce hidden
+/// dependencies between tests that should be isolated. In general, you should
+/// prefer [tearDown], and only use [tearDownAll] if the callback is
+/// prohibitively slow.
+void tearDownAll(dynamic Function() callback) =>
+    _declarer.tearDownAll(callback);
diff --git a/pkgs/test_core/lib/src/runner/configuration.dart b/pkgs/test_core/lib/src/runner/configuration.dart
index 28874d6..cdf1f75 100644
--- a/pkgs/test_core/lib/src/runner/configuration.dart
+++ b/pkgs/test_core/lib/src/runner/configuration.dart
@@ -9,10 +9,11 @@
 import 'package:glob/glob.dart';
 import 'package:path/path.dart' as p;
 import 'package:source_span/source_span.dart';
-
+import 'package:test_api/scaffolding.dart' // ignore: deprecated_member_use
+    show
+        Timeout;
 import 'package:test_api/src/backend/platform_selector.dart'; // ignore: implementation_imports
 import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
-import 'package:test_api/src/frontend/timeout.dart'; // ignore: implementation_imports
 
 import '../util/io.dart';
 import 'configuration/args.dart' as args;
diff --git a/pkgs/test_core/lib/src/runner/configuration/args.dart b/pkgs/test_core/lib/src/runner/configuration/args.dart
index c1ff9fb..efdc2ae 100644
--- a/pkgs/test_core/lib/src/runner/configuration/args.dart
+++ b/pkgs/test_core/lib/src/runner/configuration/args.dart
@@ -7,8 +7,10 @@
 
 import 'package:args/args.dart';
 import 'package:boolean_selector/boolean_selector.dart';
+import 'package:test_api/scaffolding.dart' // ignore: deprecated_member_use
+    show
+        Timeout;
 import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
-import 'package:test_api/src/frontend/timeout.dart'; // ignore: implementation_imports
 
 import '../../util/io.dart';
 import '../configuration.dart';
diff --git a/pkgs/test_core/lib/src/runner/configuration/load.dart b/pkgs/test_core/lib/src/runner/configuration/load.dart
index 7a5116a..9a23915 100644
--- a/pkgs/test_core/lib/src/runner/configuration/load.dart
+++ b/pkgs/test_core/lib/src/runner/configuration/load.dart
@@ -9,9 +9,11 @@
 import 'package:meta/meta.dart';
 import 'package:path/path.dart' as p;
 import 'package:source_span/source_span.dart';
+import 'package:test_api/scaffolding.dart' // ignore: deprecated_member_use
+    show
+        Timeout;
 import 'package:test_api/src/backend/operating_system.dart'; // ignore: implementation_imports
 import 'package:test_api/src/backend/platform_selector.dart'; // ignore: implementation_imports
-import 'package:test_api/src/frontend/timeout.dart'; // ignore: implementation_imports
 import 'package:test_api/src/utils.dart'; // ignore: implementation_imports
 import 'package:yaml/yaml.dart';
 
diff --git a/pkgs/test_core/lib/src/runner/parse_metadata.dart b/pkgs/test_core/lib/src/runner/parse_metadata.dart
index 4576d76..a68422b 100644
--- a/pkgs/test_core/lib/src/runner/parse_metadata.dart
+++ b/pkgs/test_core/lib/src/runner/parse_metadata.dart
@@ -6,9 +6,11 @@
 import 'package:analyzer/dart/ast/ast.dart';
 import 'package:path/path.dart' as p;
 import 'package:source_span/source_span.dart';
+import 'package:test_api/scaffolding.dart' // ignore: deprecated_member_use
+    show
+        Timeout;
 import 'package:test_api/src/backend/metadata.dart'; // ignore: implementation_imports
 import 'package:test_api/src/backend/platform_selector.dart'; // ignore: implementation_imports
-import 'package:test_api/src/frontend/timeout.dart'; // ignore: implementation_imports
 import 'package:test_api/src/utils.dart'; // ignore: implementation_imports
 
 import '../util/dart.dart';
diff --git a/pkgs/test_core/lib/src/runner/reporter/json.dart b/pkgs/test_core/lib/src/runner/reporter/json.dart
index 4be3734..c16c420 100644
--- a/pkgs/test_core/lib/src/runner/reporter/json.dart
+++ b/pkgs/test_core/lib/src/runner/reporter/json.dart
@@ -15,7 +15,9 @@
 import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
 import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports
 import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports
-import 'package:test_api/src/frontend/expect.dart'; // ignore: implementation_imports
+import 'package:test_api/hooks.dart' // ignore: implementation_imports
+    show
+        TestFailure;
 
 import '../configuration.dart';
 import '../engine.dart';
diff --git a/pkgs/test_core/lib/src/runner/suite.dart b/pkgs/test_core/lib/src/runner/suite.dart
index b656041..2cd1e07 100644
--- a/pkgs/test_core/lib/src/runner/suite.dart
+++ b/pkgs/test_core/lib/src/runner/suite.dart
@@ -5,12 +5,13 @@
 import 'package:boolean_selector/boolean_selector.dart';
 import 'package:collection/collection.dart';
 import 'package:source_span/source_span.dart';
-
+import 'package:test_api/scaffolding.dart' // ignore: deprecated_member_use
+    show
+        Timeout;
 import 'package:test_api/src/backend/metadata.dart'; // ignore: implementation_imports
 import 'package:test_api/src/backend/platform_selector.dart'; // ignore: implementation_imports
-import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
 import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
-import 'package:test_api/src/frontend/timeout.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
 
 import 'runtime_selection.dart';
 
diff --git a/pkgs/test_core/lib/test_core.dart b/pkgs/test_core/lib/test_core.dart
index f0f8625..428580a 100644
--- a/pkgs/test_core/lib/test_core.dart
+++ b/pkgs/test_core/lib/test_core.dart
@@ -6,288 +6,11 @@
     'Please use package:test.')
 library test_core;
 
-import 'dart:async';
-
-import 'package:meta/meta.dart' show isTest, isTestGroup;
-import 'package:path/path.dart' as p;
-import 'package:test_api/backend.dart'; //ignore: deprecated_member_use
-import 'package:test_api/src/backend/declarer.dart'; // ignore: implementation_imports
-import 'package:test_api/src/backend/invoker.dart'; // ignore: implementation_imports
-import 'package:test_api/src/frontend/timeout.dart'; // ignore: implementation_imports
-import 'package:test_api/src/frontend/utils.dart'; // ignore: implementation_imports
-
-import 'src/runner/engine.dart';
-import 'src/runner/plugin/environment.dart';
-import 'src/runner/reporter/expanded.dart';
-import 'src/runner/runner_suite.dart';
-import 'src/runner/suite.dart';
-import 'src/util/async.dart';
-import 'src/util/os.dart';
-import 'src/util/print_sink.dart';
-
-export 'package:matcher/matcher.dart';
-// Hide implementations which don't support being run directly.
-// This file is an almost direct copy of import below, but with the global
-// declarer added.
-//ignore: deprecated_member_use
+export 'scaffolding.dart';
+export 'package:test_api/expect.dart';
+export 'package:test_api/hooks.dart' show TestFailure;
+// Not yet deprecated, but not exposed through focused libraries.
+export 'package:test_api/test_api.dart' show registerException;
+// Deprecated exports not surfaced through focused libraries.
 export 'package:test_api/test_api.dart'
-    hide test, group, setUp, setUpAll, tearDown, tearDownAll;
-
-/// The global declarer.
-///
-/// This is used if a test file is run directly, rather than through the runner.
-Declarer? _globalDeclarer;
-
-/// Gets the declarer for the current scope.
-///
-/// When using the runner, this returns the [Zone]-scoped declarer that's set by
-/// [IsolateListener] or [IframeListener]. If the test file is run directly,
-/// this returns [_globalDeclarer] (and sets it up on the first call).
-Declarer get _declarer {
-  var declarer = Declarer.current;
-  if (declarer != null) return declarer;
-  if (_globalDeclarer != null) return _globalDeclarer!;
-
-  // Since there's no Zone-scoped declarer, the test file is being run directly.
-  // In order to run the tests, we set up our own Declarer via
-  // [_globalDeclarer], and pump the event queue as a best effort to wait for
-  // all tests to be defined before starting them.
-  _globalDeclarer = Declarer();
-
-  () async {
-    await pumpEventQueue();
-
-    var suite = RunnerSuite(const PluginEnvironment(), SuiteConfiguration.empty,
-        _globalDeclarer!.build(), SuitePlatform(Runtime.vm, os: currentOSGuess),
-        path: p.prettyUri(Uri.base));
-
-    var engine = Engine();
-    engine.suiteSink.add(suite);
-    engine.suiteSink.close();
-    ExpandedReporter.watch(engine, PrintSink(),
-        color: true, printPath: false, printPlatform: false);
-
-    var success = await runZoned(() => Invoker.guard(engine.run),
-        zoneValues: {#test.declarer: _globalDeclarer});
-    if (success == true) return null;
-    print('');
-    unawaited(Future.error('Dummy exception to set exit code.'));
-  }();
-
-  return _globalDeclarer!;
-}
-
-// TODO(nweiz): This and other top-level functions should throw exceptions if
-// they're called after the declarer has finished declaring.
-/// Creates a new test case with the given description (converted to a string)
-/// and body.
-///
-/// The description will be added to the descriptions of any surrounding
-/// [group]s. If [testOn] is passed, it's parsed as a [platform selector][]; the
-/// test will only be run on matching platforms.
-///
-/// [platform selector]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors
-///
-/// If [timeout] is passed, it's used to modify or replace the default timeout
-/// of 30 seconds. Timeout modifications take precedence in suite-group-test
-/// order, so [timeout] will also modify any timeouts set on the group or suite.
-///
-/// If [skip] is a String or `true`, the test is skipped. If it's a String, it
-/// should explain why the test is skipped; this reason will be printed instead
-/// of running the test.
-///
-/// If [tags] is passed, it declares user-defined tags that are applied to the
-/// test. These tags can be used to select or skip the test on the command line,
-/// or to do bulk test configuration. All tags should be declared in the
-/// [package configuration file][configuring tags]. The parameter can be an
-/// [Iterable] of tag names, or a [String] representing a single tag.
-///
-/// If [retry] is passed, the test will be retried the provided number of times
-/// before being marked as a failure.
-///
-/// [configuring tags]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#configuring-tags
-///
-/// [onPlatform] allows tests to be configured on a platform-by-platform
-/// basis. It's a map from strings that are parsed as [PlatformSelector]s to
-/// annotation classes: [Timeout], [Skip], or lists of those. These
-/// annotations apply only on the given platforms. For example:
-///
-///     test('potentially slow test', () {
-///       // ...
-///     }, onPlatform: {
-///       // This test is especially slow on Windows.
-///       'windows': Timeout.factor(2),
-///       'browser': [
-///         Skip('TODO: add browser support'),
-///         // This will be slow on browsers once it works on them.
-///         Timeout.factor(2)
-///       ]
-///     });
-///
-/// If multiple platforms match, the annotations apply in order as through
-/// they were in nested groups.
-///
-/// If the `solo` flag is `true`, only tests and groups marked as
-/// "solo" will be be run. This only restricts tests *within this test
-/// suite*—tests in other suites will run as normal. We recommend that users
-/// avoid this flag if possible and instead use the test runner flag `-n` to
-/// filter tests by name.
-@isTest
-void test(description, dynamic Function() body,
-    {String? testOn,
-    Timeout? timeout,
-    skip,
-    tags,
-    Map<String, dynamic>? onPlatform,
-    int? retry,
-    @deprecated bool solo = false}) {
-  _declarer.test(description.toString(), body,
-      testOn: testOn,
-      timeout: timeout,
-      skip: skip,
-      onPlatform: onPlatform,
-      tags: tags,
-      retry: retry,
-      solo: solo);
-
-  // Force dart2js not to inline this function. We need it to be separate from
-  // `main()` in JS stack traces in order to properly determine the line and
-  // column where the test was defined. See sdk#26705.
-  return;
-  return; // ignore: dead_code
-}
-
-/// Creates a group of tests.
-///
-/// A group's description (converted to a string) is included in the descriptions
-/// of any tests or sub-groups it contains. [setUp] and [tearDown] are also scoped
-/// to the containing group.
-///
-/// If [testOn] is passed, it's parsed as a [platform selector][]; the test will
-/// only be run on matching platforms.
-///
-/// [platform selector]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors
-///
-/// If [timeout] is passed, it's used to modify or replace the default timeout
-/// of 30 seconds. Timeout modifications take precedence in suite-group-test
-/// order, so [timeout] will also modify any timeouts set on the suite, and will
-/// be modified by any timeouts set on individual tests.
-///
-/// If [skip] is a String or `true`, the group is skipped. If it's a String, it
-/// should explain why the group is skipped; this reason will be printed instead
-/// of running the group's tests.
-///
-/// If [tags] is passed, it declares user-defined tags that are applied to the
-/// test. These tags can be used to select or skip the test on the command line,
-/// or to do bulk test configuration. All tags should be declared in the
-/// [package configuration file][configuring tags]. The parameter can be an
-/// [Iterable] of tag names, or a [String] representing a single tag.
-///
-/// [configuring tags]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#configuring-tags
-///
-/// [onPlatform] allows groups to be configured on a platform-by-platform
-/// basis. It's a map from strings that are parsed as [PlatformSelector]s to
-/// annotation classes: [Timeout], [Skip], or lists of those. These
-/// annotations apply only on the given platforms. For example:
-///
-///     group('potentially slow tests', () {
-///       // ...
-///     }, onPlatform: {
-///       // These tests are especially slow on Windows.
-///       'windows': Timeout.factor(2),
-///       'browser': [
-///         Skip('TODO: add browser support'),
-///         // They'll be slow on browsers once it works on them.
-///         Timeout.factor(2)
-///       ]
-///     });
-///
-/// If multiple platforms match, the annotations apply in order as through
-/// they were in nested groups.
-///
-/// If the `solo` flag is `true`, only tests and groups marked as
-/// "solo" will be be run. This only restricts tests *within this test
-/// suite*—tests in other suites will run as normal. We recommend that users
-/// avoid this flag if possible, and instead use the test runner flag `-n` to
-/// filter tests by name.
-@isTestGroup
-void group(description, dynamic Function() body,
-    {String? testOn,
-    Timeout? timeout,
-    skip,
-    tags,
-    Map<String, dynamic>? onPlatform,
-    int? retry,
-    @deprecated bool solo = false}) {
-  _declarer.group(description.toString(), body,
-      testOn: testOn,
-      timeout: timeout,
-      skip: skip,
-      tags: tags,
-      onPlatform: onPlatform,
-      retry: retry,
-      solo: solo);
-
-  // Force dart2js not to inline this function. We need it to be separate from
-  // `main()` in JS stack traces in order to properly determine the line and
-  // column where the test was defined. See sdk#26705.
-  return;
-  return; // ignore: dead_code
-}
-
-/// Registers a function to be run before tests.
-///
-/// This function will be called before each test is run. [callback] may be
-/// asynchronous; if so, it must return a [Future].
-///
-/// If this is called within a test group, it applies only to tests in that
-/// group. [callback] will be run after any set-up callbacks in parent groups or
-/// at the top level.
-///
-/// Each callback at the top level or in a given group will be run in the order
-/// they were declared.
-void setUp(dynamic Function() callback) => _declarer.setUp(callback);
-
-/// Registers a function to be run after tests.
-///
-/// This function will be called after each test is run. [callback] may be
-/// asynchronous; if so, it must return a [Future].
-///
-/// If this is called within a test group, it applies only to tests in that
-/// group. [callback] will be run before any tear-down callbacks in parent
-/// groups or at the top level.
-///
-/// Each callback at the top level or in a given group will be run in the
-/// reverse of the order they were declared.
-///
-/// See also [addTearDown], which adds tear-downs to a running test.
-void tearDown(dynamic Function() callback) => _declarer.tearDown(callback);
-
-/// Registers a function to be run once before all tests.
-///
-/// [callback] may be asynchronous; if so, it must return a [Future].
-///
-/// If this is called within a test group, [callback] will run before all tests
-/// in that group. It will be run after any [setUpAll] callbacks in parent
-/// groups or at the top level. It won't be run if none of the tests in the
-/// group are run.
-///
-/// **Note**: This function makes it very easy to accidentally introduce hidden
-/// dependencies between tests that should be isolated. In general, you should
-/// prefer [setUp], and only use [setUpAll] if the callback is prohibitively
-/// slow.
-void setUpAll(dynamic Function() callback) => _declarer.setUpAll(callback);
-
-/// Registers a function to be run once after all tests.
-///
-/// If this is called within a test group, [callback] will run after all tests
-/// in that group. It will be run before any [tearDownAll] callbacks in parent
-/// groups or at the top level. It won't be run if none of the tests in the
-/// group are run.
-///
-/// **Note**: This function makes it very easy to accidentally introduce hidden
-/// dependencies between tests that should be isolated. In general, you should
-/// prefer [tearDown], and only use [tearDownAll] if the callback is
-/// prohibitively slow.
-void tearDownAll(dynamic Function() callback) =>
-    _declarer.tearDownAll(callback);
+    show ErrorFormatter, expectAsync, throws, Throws;