Merge branch 'master' into pre-nnbd--merge
diff --git a/.travis.yml b/.travis.yml
index 3bebaa1..2e503fb 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,73 +1,73 @@
-# Created with package:mono_repo v2.3.0
+# Created with package:mono_repo v3.0.0
 language: dart
 
 # Custom configuration
-sudo: required
 addons:
   chrome: stable
 env:
   global: FORCE_TEST_EXIT=true
 after_failure:
   - tool/report_failure.sh
-branches:
-  - "pre-nnbd"
-  - master
 
 jobs:
   include:
-    - stage: analyze_and_format
-      name: "SDK: 2.7.0; PKGS: pkgs/test, pkgs/test_api, pkgs/test_core; TASKS: `dartanalyzer --fatal-warnings .`"
-      dart: "2.7.0"
+    - stage: mono_repo_self_validate
+      name: mono_repo self validate
       os: linux
-      env: PKGS="pkgs/test pkgs/test_api pkgs/test_core"
-      script: ./tool/travis.sh dartanalyzer_1
+      script: "pub global activate mono_repo 3.0.0 && pub global run mono_repo travis --validate"
     - stage: analyze_and_format
-      name: "SDK: dev; PKGS: pkgs/test, pkgs/test_api, pkgs/test_core; TASKS: [`dartfmt -n --set-exit-if-changed .`, `dartanalyzer --fatal-infos --fatal-warnings .`]"
+      name: "SDK: dev; PKGS: pkgs/test, pkgs/test_api, pkgs/test_core; TASKS: [`dartfmt -n --set-exit-if-changed .`, `dartanalyzer --enable-experiment=non-nullable --fatal-infos --fatal-warnings .`]"
       dart: dev
       os: linux
       env: PKGS="pkgs/test pkgs/test_api pkgs/test_core"
-      script: ./tool/travis.sh dartfmt dartanalyzer_0
+      script: tool/travis.sh dartfmt dartanalyzer
     - stage: unit_test
-      name: "SDK: dev; PKG: pkgs/test; TASKS: `xvfb-run -s \"-screen 0 1024x768x24\" pub run test --preset travis -x phantomjs --total-shards 5 --shard-index 0`"
+      name: "SDK: dev; PKG: pkgs/test; TASKS: `xvfb-run -s \"-screen 0 1024x768x24\" pub run --enable-experiment=non-nullable test --preset travis -x phantomjs --total-shards 5 --shard-index 0`"
       dart: dev
       os: linux
       env: PKGS="pkgs/test"
-      script: ./tool/travis.sh command_0
+      script: tool/travis.sh command_0
     - stage: unit_test
-      name: "SDK: dev; PKG: pkgs/test; TASKS: `xvfb-run -s \"-screen 0 1024x768x24\" pub run test --preset travis -x phantomjs --total-shards 5 --shard-index 1`"
+      name: "SDK: dev; PKG: pkgs/test; TASKS: `xvfb-run -s \"-screen 0 1024x768x24\" pub run --enable-experiment=non-nullable test --preset travis -x phantomjs --total-shards 5 --shard-index 1`"
       dart: dev
       os: linux
       env: PKGS="pkgs/test"
-      script: ./tool/travis.sh command_1
+      script: tool/travis.sh command_1
     - stage: unit_test
-      name: "SDK: dev; PKG: pkgs/test; TASKS: `xvfb-run -s \"-screen 0 1024x768x24\" pub run test --preset travis -x phantomjs --total-shards 5 --shard-index 2`"
+      name: "SDK: dev; PKG: pkgs/test; TASKS: `xvfb-run -s \"-screen 0 1024x768x24\" pub run --enable-experiment=non-nullable test --preset travis -x phantomjs --total-shards 5 --shard-index 2`"
       dart: dev
       os: linux
       env: PKGS="pkgs/test"
-      script: ./tool/travis.sh command_2
+      script: tool/travis.sh command_2
     - stage: unit_test
-      name: "SDK: dev; PKG: pkgs/test; TASKS: `xvfb-run -s \"-screen 0 1024x768x24\" pub run test --preset travis -x phantomjs --total-shards 5 --shard-index 3`"
+      name: "SDK: dev; PKG: pkgs/test; TASKS: `xvfb-run -s \"-screen 0 1024x768x24\" pub run --enable-experiment=non-nullable test --preset travis -x phantomjs --total-shards 5 --shard-index 3`"
       dart: dev
       os: linux
       env: PKGS="pkgs/test"
-      script: ./tool/travis.sh command_3
+      script: tool/travis.sh command_3
     - stage: unit_test
-      name: "SDK: dev; PKG: pkgs/test; TASKS: `xvfb-run -s \"-screen 0 1024x768x24\" pub run test --preset travis -x phantomjs --total-shards 5 --shard-index 4`"
+      name: "SDK: dev; PKG: pkgs/test; TASKS: `xvfb-run -s \"-screen 0 1024x768x24\" pub run --enable-experiment=non-nullable test --preset travis -x phantomjs --total-shards 5 --shard-index 4`"
       dart: dev
       os: linux
       env: PKGS="pkgs/test"
-      script: ./tool/travis.sh command_4
+      script: tool/travis.sh command_4
     - stage: unit_test
-      name: "SDK: dev; PKG: pkgs/test_api; TASKS: `pub run test --preset travis`"
+      name: "SDK: dev; PKG: pkgs/test_api; TASKS: `pub run --enable-experiment=non-nullable test --preset travis -x browser`"
       dart: dev
       os: linux
       env: PKGS="pkgs/test_api"
-      script: ./tool/travis.sh test
+      script: tool/travis.sh command_5
 
 stages:
+  - mono_repo_self_validate
   - analyze_and_format
   - unit_test
 
+# Only building master means that we don't run two builds for each pull request.
+branches:
+  only:
+    - master
+
 cache:
   directories:
     - "$HOME/.pub-cache"
diff --git a/analysis_options.yaml b/analysis_options.yaml
index db0a85f..54dec20 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -12,6 +12,8 @@
     dead_code: error
     # There are a number of deprecated members used through this package
     deprecated_member_use_from_same_package: ignore
+  enable-experiment:
+    - non-nullable
 linter:
   rules:
     - avoid_private_typedef_functions
diff --git a/mono_repo.yaml b/mono_repo.yaml
index 59bb3f5..266bfe4 100644
--- a/mono_repo.yaml
+++ b/mono_repo.yaml
@@ -1,6 +1,6 @@
 # See with https://github.com/dart-lang/mono_repo for details on this file
+self_validate: true
 travis:
-  sudo: required
   addons:
     chrome: stable
   env:
@@ -9,9 +9,5 @@
   after_failure:
   - tool/report_failure.sh
 
-  branches:
-  - pre-nnbd
-  - master
-
 merge_stages:
 - analyze_and_format
diff --git a/pkgs/test/CHANGELOG.md b/pkgs/test/CHANGELOG.md
index 941164d..fcc12da 100644
--- a/pkgs/test/CHANGELOG.md
+++ b/pkgs/test/CHANGELOG.md
@@ -1,3 +1,71 @@
+## 1.16.0-nullsafety.12
+
+* Fix `spawnHybridUri` on windows.
+* Fix failures running tests on the `node` platform.
+
+## 1.16.0-nullsafety.11
+
+* Set up a stack trace mapper in precompiled mode if source maps exist. If
+  the stack traces are already mapped then this has no effect, otherwise it
+  will try to map any JS lines it sees.
+
+## 1.16.0-nullsafety.10
+
+* Allow injecting a test channel for browser tests.
+* Allow `package:analyzer` version `0.41.x`.
+
+## 1.16.0-nullsafety.9
+
+* Fix `spawnHybridUri` to respect language versioning of the spawned uri.
+
+## 1.16.0-nullsafety.8
+
+* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release
+  guidelines.
+
+## 1.16.0-nullsafety.7
+
+* Allow prerelease versions of the 2.12 sdk.
+
+## 1.16.0-nullsafety.6
+
+* Add `markTestSkipped` API.
+
+## 1.16.0-nullsafety.5
+
+* Allow `2.10` stable and `2.11.0-dev` SDKs.
+* Annotate the classes used as annotations to restrict their usage to library
+  level.
+* Stop required a `SILENT_OBSERVATORY` environment variable to run with
+  debugging and the JSON reporter.
+
+## 1.16.0-nullsafety.4
+
+* Depend on the latest test_core.
+
+## 1.16.0-nullsafety.3
+
+* Clean up `--help` output.
+
+## 1.16.0-nullsafety.2
+
+* Allow version `0.40.x` of `analyzer`.
+
+## 1.16.0-nullsafety.1
+
+* Depend on the latest test_core.
+
+## 1.16.0-nullsafety
+
+* Support running tests with null safety.
+  * Note that the test runner itself is not fully migrated yet.
+* Add the `Fake` class, available through `package:test_api/fake.dart`.  This
+  was previously part of the Mockito package, but with null safety it is useful
+  enough that we decided to make it available through `package:test`.  In a
+  future release it will be made available directly through
+  `package:test_api/test_api.dart` (and hence through
+  `package:test_core/test_core.dart` and `package:test/test.dart`).
+
 ## 1.15.6 (Backport)
 
 * Support `package:analyzer` version `0.41.x`.
@@ -53,7 +121,7 @@
 
 ## 1.14.3
 
-* Fix an issue where coverage tests could not run in Chrome headless. 
+* Fix an issue where coverage tests could not run in Chrome headless.
 * Fix an issue where coverage collection would not work with source
   maps that contained absolute file URIs.
 * Fix error messages for incorrect string literals in test annotations.
diff --git a/pkgs/test/README.md b/pkgs/test/README.md
index d7c49c0..87cf5c3 100644
--- a/pkgs/test/README.md
+++ b/pkgs/test/README.md
@@ -550,7 +550,7 @@
 ### Providing a custom HTML template
 
 If you want to share the same HTML file across all tests, you can provide a
-`custom-html-template-path` configuration option to your configuration file.
+`custom_html_template_path` configuration option to your configuration file.
 This file should follow the rules above, except that instead of the link tag
 add exactly one `{{testScript}}` in the place where you want the template processor to insert it.
 
@@ -562,7 +562,7 @@
 For example:
 
 ```yaml
-custom-html-template-path: html_template.html.tpl
+custom_html_template_path: html_template.html.tpl
 ```
 
 ```html
diff --git a/pkgs/test/bin/test.dart b/pkgs/test/bin/test.dart
index 0b40282..6c4a320 100644
--- a/pkgs/test/bin/test.dart
+++ b/pkgs/test/bin/test.dart
@@ -1,5 +1,7 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 export 'package:test/src/executable.dart';
diff --git a/pkgs/test/lib/fake.dart b/pkgs/test/lib/fake.dart
new file mode 100644
index 0000000..fc28bef
--- /dev/null
+++ b/pkgs/test/lib/fake.dart
@@ -0,0 +1,10 @@
+// Copyright (c) 2020, 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.
+
+// Note: eventually we would like to fold this into test.dart, but we can't do
+// so until Mockito stops implementing its own version of `Fake`, because there
+// is code in the wild that imports both test_api.dart and Mockito.
+
+// ignore: deprecated_member_use
+export 'package:test_api/fake.dart';
diff --git a/pkgs/test/lib/src/bootstrap/browser.dart b/pkgs/test/lib/src/bootstrap/browser.dart
index 171840e..8a5f0a5 100644
--- a/pkgs/test/lib/src/bootstrap/browser.dart
+++ b/pkgs/test/lib/src/bootstrap/browser.dart
@@ -2,19 +2,21 @@
 // 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:stream_channel/stream_channel.dart';
 import 'package:test_core/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports
 import 'package:test_core/src/runner/plugin/remote_platform_helpers.dart'; // ignore: implementation_imports
 
 import '../runner/browser/post_message_channel.dart';
 
 /// Bootstraps a browser test to communicate with the test runner.
-void internalBootstrapBrowserTest(Function Function() getMain) {
+void internalBootstrapBrowserTest(Function Function() getMain,
+    {StreamChannel<Object?>? testChannel}) {
   var channel =
       serializeSuite(getMain, hidePrints: false, beforeLoad: () async {
     var serialized =
-        await suiteChannel('test.browser.mapper').stream.first as Map;
+        await suiteChannel('test.browser.mapper').stream.first as Map?;
     if (serialized == null) return;
-    setStackTraceMapper(JSStackTraceMapper.deserialize(serialized));
+    setStackTraceMapper(JSStackTraceMapper.deserialize(serialized)!);
   });
-  postMessageChannel().pipe(channel);
+  (testChannel ?? postMessageChannel()).pipe(channel);
 }
diff --git a/pkgs/test/lib/src/bootstrap/node.dart b/pkgs/test/lib/src/bootstrap/node.dart
index ca002ba..23f284a 100644
--- a/pkgs/test/lib/src/bootstrap/node.dart
+++ b/pkgs/test/lib/src/bootstrap/node.dart
@@ -11,8 +11,8 @@
 void internalBootstrapNodeTest(Function Function() getMain) {
   var channel = serializeSuite(getMain, beforeLoad: () async {
     var serialized = await suiteChannel('test.node.mapper').stream.first;
-    if (serialized == null || serialized is! Map) return;
-    setStackTraceMapper(JSStackTraceMapper.deserialize(serialized as Map));
+    if (serialized is! Map) return;
+    setStackTraceMapper(JSStackTraceMapper.deserialize(serialized)!);
   });
   socketChannel().pipe(channel);
 }
diff --git a/pkgs/test/lib/src/executable.dart b/pkgs/test/lib/src/executable.dart
index 75b2d58..f5a435a 100644
--- a/pkgs/test/lib/src/executable.dart
+++ b/pkgs/test/lib/src/executable.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 // ignore: implementation_imports
 import 'package:test_core/src/executable.dart' as executable;
diff --git a/pkgs/test/lib/src/runner/browser/browser.dart b/pkgs/test/lib/src/runner/browser/browser.dart
index 57b0fcc..c65962c 100644
--- a/pkgs/test/lib/src/runner/browser/browser.dart
+++ b/pkgs/test/lib/src/runner/browser/browser.dart
@@ -1,12 +1,13 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 import 'dart:async';
 import 'dart:convert';
 import 'dart:io';
 
-import 'package:stack_trace/stack_trace.dart';
 import 'package:typed_data/typed_data.dart';
 
 import 'package:test_api/src/utils.dart'; // ignore: implementation_imports
@@ -64,7 +65,7 @@
     // Don't return a Future here because there's no need for the caller to wait
     // for the process to actually start. They should just wait for the HTTP
     // request instead.
-    runZoned(() async {
+    runZonedGuarded(() async {
       var process = await startBrowser();
       _processCompleter.complete(process);
 
@@ -107,15 +108,13 @@
       }
 
       _onExitCompleter.complete();
-      // ignore: deprecated_member_use
-    }, onError: (error, StackTrace stackTrace) {
+    }, (error, stackTrace) {
       // Ignore any errors after the browser has been closed.
       if (_closed) return;
 
       // Make sure the process dies even if the error wasn't fatal.
       _process.then((process) => process.kill());
 
-      stackTrace ??= Trace.current();
       if (_onExitCompleter.isCompleted) return;
       _onExitCompleter.completeError(
           ApplicationException(
diff --git a/pkgs/test/lib/src/runner/browser/browser_manager.dart b/pkgs/test/lib/src/runner/browser/browser_manager.dart
index 0fc3025..1323346 100644
--- a/pkgs/test/lib/src/runner/browser/browser_manager.dart
+++ b/pkgs/test/lib/src/runner/browser/browser_manager.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 import 'dart:async';
 import 'dart:convert';
diff --git a/pkgs/test/lib/src/runner/browser/chrome.dart b/pkgs/test/lib/src/runner/browser/chrome.dart
index a00985e..19cd5e6 100644
--- a/pkgs/test/lib/src/runner/browser/chrome.dart
+++ b/pkgs/test/lib/src/runner/browser/chrome.dart
@@ -1,13 +1,14 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 import 'dart:async';
 import 'dart:convert';
 import 'dart:io';
 
 import 'package:coverage/coverage.dart';
-import 'package:http/http.dart' as http;
 import 'package:path/path.dart' as p;
 import 'package:pedantic/pedantic.dart';
 import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
@@ -105,12 +106,14 @@
     var response = await tabConnection.debugger.connection
         .sendCommand('Profiler.takePreciseCoverage', {});
     var result = response.result['result'];
+    var httpClient = HttpClient();
     var coverage = await parseChromeCoverage(
       (result as List).cast(),
-      _sourceProvider,
-      _sourceMapProvider,
+      (scriptId) => _sourceProvider(scriptId, httpClient),
+      (scriptId) => _sourceMapProvider(scriptId, httpClient),
       _sourceUriProvider,
     );
+    httpClient.close();
     return coverage;
   }
 
@@ -133,20 +136,17 @@
         : null;
   }
 
-  Future<String> _sourceMapProvider(String scriptId) async {
+  Future<String> _sourceMapProvider(
+      String scriptId, HttpClient httpClient) async {
     var script = _idToUrl[scriptId];
     if (script == null) return null;
-    var mapResponse = await http.get('$script.map');
-    if (mapResponse.statusCode != HttpStatus.ok) return null;
-    return mapResponse.body;
+    return await httpClient.getString('$script.map');
   }
 
-  Future<String> _sourceProvider(String scriptId) async {
+  Future<String> _sourceProvider(String scriptId, HttpClient httpClient) async {
     var script = _idToUrl[scriptId];
     if (script == null) return null;
-    var scriptResponse = await http.get(script);
-    if (scriptResponse.statusCode != HttpStatus.ok) return null;
-    return scriptResponse.body;
+    return await httpClient.getString(script);
   }
 }
 
@@ -191,3 +191,13 @@
 
   return tabConnection;
 }
+
+extension on HttpClient {
+  Future<String> getString(String url) async {
+    final request = await getUrl(Uri.parse(url));
+    final response = await request.close();
+    if (response.statusCode != HttpStatus.ok) return null;
+    var bytes = [await for (var chunk in response) ...chunk];
+    return utf8.decode(bytes);
+  }
+}
diff --git a/pkgs/test/lib/src/runner/browser/default_settings.dart b/pkgs/test/lib/src/runner/browser/default_settings.dart
index 9ff1fd4..7818713 100644
--- a/pkgs/test/lib/src/runner/browser/default_settings.dart
+++ b/pkgs/test/lib/src/runner/browser/default_settings.dart
@@ -1,6 +1,8 @@
 // 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.
+//
+// @dart=2.7
 
 import 'dart:collection';
 
diff --git a/pkgs/test/lib/src/runner/browser/firefox.dart b/pkgs/test/lib/src/runner/browser/firefox.dart
index cf0efe6..caabe1f 100644
--- a/pkgs/test/lib/src/runner/browser/firefox.dart
+++ b/pkgs/test/lib/src/runner/browser/firefox.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 import 'dart:async';
 import 'dart:io';
diff --git a/pkgs/test/lib/src/runner/browser/internet_explorer.dart b/pkgs/test/lib/src/runner/browser/internet_explorer.dart
index d79c9e4..c191adc 100644
--- a/pkgs/test/lib/src/runner/browser/internet_explorer.dart
+++ b/pkgs/test/lib/src/runner/browser/internet_explorer.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 import 'dart:async';
 import 'dart:io';
diff --git a/pkgs/test/lib/src/runner/browser/phantom_js.dart b/pkgs/test/lib/src/runner/browser/phantom_js.dart
index 7e3536d..ed49a2a 100644
--- a/pkgs/test/lib/src/runner/browser/phantom_js.dart
+++ b/pkgs/test/lib/src/runner/browser/phantom_js.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 import 'dart:async';
 import 'dart:io';
diff --git a/pkgs/test/lib/src/runner/browser/platform.dart b/pkgs/test/lib/src/runner/browser/platform.dart
index cd8347c..1af5dab 100644
--- a/pkgs/test/lib/src/runner/browser/platform.dart
+++ b/pkgs/test/lib/src/runner/browser/platform.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.7
 
 import 'dart:async';
 import 'dart:convert';
@@ -23,6 +25,7 @@
 import 'package:test_core/src/runner/compiler_pool.dart'; // ignore: implementation_imports
 import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports
 import 'package:test_core/src/runner/load_exception.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/package_version.dart'; // ignore: implementation_imports
 import 'package:test_core/src/runner/platform.dart'; // ignore: implementation_imports
 import 'package:test_core/src/runner/plugin/customizable_platform.dart'; // ignore: implementation_imports
 import 'package:test_core/src/runner/runner_suite.dart'; // ignore: implementation_imports
@@ -256,6 +259,8 @@
       if (browser.isJS) {
         if (suiteConfig.precompiledPath == null) {
           await _compileSuite(path, suiteConfig);
+        } else {
+          await _addPrecompiledStackTraceMapper(path, suiteConfig);
         }
       }
 
@@ -407,6 +412,20 @@
     });
   }
 
+  Future<void> _addPrecompiledStackTraceMapper(
+      String dartPath, SuiteConfiguration suiteConfig) async {
+    if (suiteConfig.jsTrace) return;
+    var mapPath = p.join(
+        suiteConfig.precompiledPath, dartPath + '.browser_test.dart.js.map');
+    var mapFile = File(mapPath);
+    if (mapFile.existsSync()) {
+      _mappers[dartPath] = JSStackTraceMapper(mapFile.readAsStringSync(),
+          mapUrl: p.toUri(mapPath),
+          sdkRoot: Uri.parse(r'/packages/$sdk'),
+          packageMap: (await currentPackageConfig).toPackageMap());
+    }
+  }
+
   /// Returns the [BrowserManager] for [runtime], which should be a browser.
   ///
   /// If no browser manager is running yet, starts one.
diff --git a/pkgs/test/lib/src/runner/browser/post_message_channel.dart b/pkgs/test/lib/src/runner/browser/post_message_channel.dart
index 595c2ec..d05c045 100644
--- a/pkgs/test/lib/src/runner/browser/post_message_channel.dart
+++ b/pkgs/test/lib/src/runner/browser/post_message_channel.dart
@@ -17,8 +17,8 @@
 
 /// Constructs a [StreamChannel] wrapping [MessageChannel] communication with
 /// the host page.
-StreamChannel postMessageChannel() {
-  var controller = StreamChannelController(sync: true);
+StreamChannel<Object?> postMessageChannel() {
+  var controller = StreamChannelController<Object?>(sync: true);
 
   // Listen for a message from the host that transfers a message port. Using
   // `firstWhere` automatically unsubscribes from further messages. This is
@@ -47,7 +47,8 @@
   // Send a ready message once we're listening so the host knows it's safe to
   // start sending events.
   // TODO(nweiz): Stop manually adding href here once issue 22554 is fixed.
-  _postParentMessage(jsify({'href': window.location.href, 'ready': true}),
+  _postParentMessage(
+      jsify({'href': window.location.href, 'ready': true}) as Object,
       window.location.origin);
 
   return controller.foreign;
diff --git a/pkgs/test/lib/src/runner/browser/safari.dart b/pkgs/test/lib/src/runner/browser/safari.dart
index a47bb09..a8eb8f0 100644
--- a/pkgs/test/lib/src/runner/browser/safari.dart
+++ b/pkgs/test/lib/src/runner/browser/safari.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 import 'dart:async';
 import 'dart:convert';
diff --git a/pkgs/test/lib/src/runner/executable_settings.dart b/pkgs/test/lib/src/runner/executable_settings.dart
index 5aa9b8d..e89f36e 100644
--- a/pkgs/test/lib/src/runner/executable_settings.dart
+++ b/pkgs/test/lib/src/runner/executable_settings.dart
@@ -1,6 +1,8 @@
 // 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.
+//
+// @dart=2.7
 
 import 'dart:io';
 
diff --git a/pkgs/test/lib/src/runner/node/platform.dart b/pkgs/test/lib/src/runner/node/platform.dart
index 671e7ad..a677fa6 100644
--- a/pkgs/test/lib/src/runner/node/platform.dart
+++ b/pkgs/test/lib/src/runner/node/platform.dart
@@ -1,6 +1,8 @@
 // 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.
+//
+// @dart=2.7
 
 import 'dart:async';
 import 'dart:io';
@@ -17,6 +19,7 @@
 import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
 import 'package:test_api/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports
 import 'package:test_api/src/utils.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/package_version.dart'; // ignore: implementation_imports
 import 'package:test_core/src/runner/platform.dart'; // ignore: implementation_imports
 import 'package:test_core/src/runner/runner_suite.dart'; // ignore: implementation_imports
 import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
diff --git a/pkgs/test/lib/src/runner/node/socket_channel.dart b/pkgs/test/lib/src/runner/node/socket_channel.dart
index 0c2036a..1d9055e 100644
--- a/pkgs/test/lib/src/runner/node/socket_channel.dart
+++ b/pkgs/test/lib/src/runner/node/socket_channel.dart
@@ -5,9 +5,11 @@
 @JS()
 library node;
 
+import 'dart:async';
+import 'dart:convert';
+
 import 'package:js/js.dart';
 import 'package:stream_channel/stream_channel.dart';
-import 'package:test_api/src/utils.dart'; // ignore: implementation_imports
 
 @JS('require')
 external _Net _require(String module);
@@ -29,15 +31,18 @@
 
 /// Returns a [StreamChannel] of JSON-encodable objects that communicates over a
 /// socket whose port is given by `process.argv[2]`.
-StreamChannel<Object> socketChannel() {
-  var controller =
-      StreamChannelController<String>(allowForeignErrors: false, sync: true);
+StreamChannel<Object?> socketChannel() {
   var net = _require('net');
   var socket = net.connect(int.parse(_args[2]));
   socket.setEncoding('utf8');
 
-  controller.local.stream.listen((chunk) => socket.write(chunk));
-  socket.on('data', allowInterop(controller.local.sink.add));
+  var socketSink = StreamController<Object?>(sync: true)
+    ..stream.listen((event) => socket.write('${jsonEncode(event)}\n'));
 
-  return controller.foreign.transform(chunksToLines).transform(jsonDocument);
+  var socketStream = StreamController<String>(sync: true);
+  socket.on('data', allowInterop(socketStream.add));
+
+  return StreamChannel.withCloseGuarantee(
+      socketStream.stream.transform(const LineSplitter()).map(jsonDecode),
+      socketSink);
 }
diff --git a/pkgs/test/lib/src/util/one_off_handler.dart b/pkgs/test/lib/src/util/one_off_handler.dart
index 7667df3..c07799a 100644
--- a/pkgs/test/lib/src/util/one_off_handler.dart
+++ b/pkgs/test/lib/src/util/one_off_handler.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 import 'dart:async';
 import 'package:path/path.dart' as p;
diff --git a/pkgs/test/lib/src/util/package_map.dart b/pkgs/test/lib/src/util/package_map.dart
index 6b30afa..fd39842 100644
--- a/pkgs/test/lib/src/util/package_map.dart
+++ b/pkgs/test/lib/src/util/package_map.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2020, 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.
+//
+// @dart=2.7
 
 import 'package:package_config/package_config.dart';
 
diff --git a/pkgs/test/lib/src/util/path_handler.dart b/pkgs/test/lib/src/util/path_handler.dart
index 2fe31c4..4b94f1d 100644
--- a/pkgs/test/lib/src/util/path_handler.dart
+++ b/pkgs/test/lib/src/util/path_handler.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 import 'dart:async';
 import 'package:path/path.dart' as p;
diff --git a/pkgs/test/mono_pkg.yaml b/pkgs/test/mono_pkg.yaml
index ef401af..98dd59a 100644
--- a/pkgs/test/mono_pkg.yaml
+++ b/pkgs/test/mono_pkg.yaml
@@ -5,14 +5,10 @@
     - analyze_and_format:
       - group:
         - dartfmt: sdk
-        - dartanalyzer: --fatal-infos --fatal-warnings .
-        dart: dev
-      - group:
-        - dartanalyzer: --fatal-warnings .
-        dart: 2.7.0
+        - dartanalyzer: --enable-experiment=non-nullable --fatal-infos --fatal-warnings .
     - unit_test:
-      - command: xvfb-run -s "-screen 0 1024x768x24" pub run test --preset travis -x phantomjs --total-shards 5 --shard-index 0
-      - command: xvfb-run -s "-screen 0 1024x768x24" pub run test --preset travis -x phantomjs --total-shards 5 --shard-index 1
-      - command: xvfb-run -s "-screen 0 1024x768x24" pub run test --preset travis -x phantomjs --total-shards 5 --shard-index 2
-      - command: xvfb-run -s "-screen 0 1024x768x24" pub run test --preset travis -x phantomjs --total-shards 5 --shard-index 3
-      - command: xvfb-run -s "-screen 0 1024x768x24" pub run test --preset travis -x phantomjs --total-shards 5 --shard-index 4
+      - command: xvfb-run -s "-screen 0 1024x768x24" pub run --enable-experiment=non-nullable test --preset travis -x phantomjs --total-shards 5 --shard-index 0
+      - command: xvfb-run -s "-screen 0 1024x768x24" pub run --enable-experiment=non-nullable test --preset travis -x phantomjs --total-shards 5 --shard-index 1
+      - command: xvfb-run -s "-screen 0 1024x768x24" pub run --enable-experiment=non-nullable test --preset travis -x phantomjs --total-shards 5 --shard-index 2
+      - command: xvfb-run -s "-screen 0 1024x768x24" pub run --enable-experiment=non-nullable test --preset travis -x phantomjs --total-shards 5 --shard-index 3
+      - command: xvfb-run -s "-screen 0 1024x768x24" pub run --enable-experiment=non-nullable test --preset travis -x phantomjs --total-shards 5 --shard-index 4
diff --git a/pkgs/test/pubspec.yaml b/pkgs/test/pubspec.yaml
index a578c35..fb73aa9 100644
--- a/pkgs/test/pubspec.yaml
+++ b/pkgs/test/pubspec.yaml
@@ -1,39 +1,38 @@
 name: test
-version: 1.15.6
+version: 1.16.0-nullsafety.12
 description: A full featured library for writing and running Dart tests.
 homepage: https://github.com/dart-lang/test/blob/master/pkgs/test
 
 environment:
-  sdk: '>=2.7.0 <3.0.0'
+  sdk: ">=2.12.0-0 <3.0.0"
 
 dependencies:
   analyzer: '>=0.36.0 <0.42.0'
-  async: ^2.0.0
-  boolean_selector: '>=1.0.0 <3.0.0'
+  async: '>=2.5.0-nullsafety <2.5.0'
+  boolean_selector: '>=2.1.0-nullsafety <2.1.0'
   coverage: '>=0.13.4 < 0.15.0'
-  http: ^0.12.0
   http_multi_server: ^2.0.0
   io: ^0.3.0
-  js: ^0.6.0
+  js: '>=0.6.3-nullsafety <0.6.3'
   node_preamble: ^1.3.0
   package_config: ^1.9.0
-  path: ^1.2.0
-  pedantic: ^1.1.0
-  pool: ^1.3.0
+  path: '>=1.8.0-nullsafety <1.8.0'
+  pedantic: '>=1.10.0-nullsafety <1.10.0'
+  pool: '>=1.5.0-nullsafety <1.5.0'
   shelf: ^0.7.0
   shelf_packages_handler: ">=1.0.0 <3.0.0"
   shelf_static: ^0.2.6
   shelf_web_socket: ^0.2.0
-  source_span: ^1.4.0
-  stack_trace: ^1.9.0
-  stream_channel: '>=1.7.0 <3.0.0'
-  typed_data: ^1.0.0
+  source_span: '>=1.8.0-nullsafety <1.8.0'
+  stack_trace: '>=1.10.0-nullsafety <1.10.0'
+  stream_channel: '>=2.1.0-nullsafety <2.1.0'
+  typed_data: '>=1.3.0-nullsafety <1.3.0'
   web_socket_channel: ^1.0.0
   webkit_inspection_protocol: ">=0.5.0 <0.8.0"
   yaml: ^2.0.0
   # Use an exact version until the test_api and test_core package are stable.
-  test_api: 0.2.18+1
-  test_core: 0.3.11+3
+  test_api: 0.2.19-nullsafety.6
+  test_core: 0.3.12-nullsafety.11
 
 dev_dependencies:
   fake_async: ^1.0.0
diff --git a/pkgs/test/test/common.dart b/pkgs/test/test/common.dart
index ddc1fe5..4d397da 100644
--- a/pkgs/test/test/common.dart
+++ b/pkgs/test/test/common.dart
@@ -1,3 +1,8 @@
+// Copyright (c) 2020, 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.
+//
+// @dart=2.7
 import 'package:test/test.dart';
 
 dynamic myTest(String name, Function() testFn) => test(name, testFn);
diff --git a/pkgs/test/test/io.dart b/pkgs/test/test/io.dart
index 3094380..5e12407 100644
--- a/pkgs/test/test/io.dart
+++ b/pkgs/test/test/io.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 import 'dart:async';
 import 'dart:io';
@@ -15,7 +17,11 @@
 /// The path to the root directory of the `test` package.
 final Future<String> packageDir =
     Isolate.resolvePackageUri(Uri(scheme: 'package', path: 'test/'))
-        .then((uri) => p.dirname(uri.path));
+        .then((uri) {
+  var dir = p.dirname(uri.path);
+  if (dir.startsWith('/C:')) dir = dir.substring(1);
+  return dir;
+});
 
 /// The path to the `pub` executable in the current Dart SDK.
 final _pubPath = p.absolute(p.join(p.dirname(Platform.resolvedExecutable),
@@ -76,7 +82,7 @@
 
   var allArgs = [
     ...?vmArgs,
-    p.absolute(p.join(await packageDir, 'bin/test.dart')),
+    Uri.file(p.url.join(await packageDir, 'bin', 'test.dart')).toString(),
     '--concurrency=$concurrency',
     if (reporter != null) '--reporter=$reporter',
     if (fileReporter != null) '--file-reporter=$fileReporter',
diff --git a/pkgs/test/test/runner/browser/chrome_test.dart b/pkgs/test/test/runner/browser/chrome_test.dart
index a60bdf4..7912aed 100644
--- a/pkgs/test/test/runner/browser/chrome_test.dart
+++ b/pkgs/test/test/runner/browser/chrome_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 @Tags(['chrome'])
diff --git a/pkgs/test/test/runner/browser/code_server.dart b/pkgs/test/test/runner/browser/code_server.dart
index 384f871..55ed281 100644
--- a/pkgs/test/test/runner/browser/code_server.dart
+++ b/pkgs/test/test/runner/browser/code_server.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 import 'dart:async';
 
diff --git a/pkgs/test/test/runner/browser/compact_reporter_test.dart b/pkgs/test/test/runner/browser/compact_reporter_test.dart
index 4a65f92..3539007 100644
--- a/pkgs/test/test/runner/browser/compact_reporter_test.dart
+++ b/pkgs/test/test/runner/browser/compact_reporter_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
diff --git a/pkgs/test/test/runner/browser/expanded_reporter_test.dart b/pkgs/test/test/runner/browser/expanded_reporter_test.dart
index c334843..fbb4d28 100644
--- a/pkgs/test/test/runner/browser/expanded_reporter_test.dart
+++ b/pkgs/test/test/runner/browser/expanded_reporter_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
diff --git a/pkgs/test/test/runner/browser/firefox_html_test.dart b/pkgs/test/test/runner/browser/firefox_html_test.dart
index 271f147..48fb79b 100644
--- a/pkgs/test/test/runner/browser/firefox_html_test.dart
+++ b/pkgs/test/test/runner/browser/firefox_html_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('firefox')
 import 'dart:html';
diff --git a/pkgs/test/test/runner/browser/firefox_test.dart b/pkgs/test/test/runner/browser/firefox_test.dart
index 748eafe..d49e20b 100644
--- a/pkgs/test/test/runner/browser/firefox_test.dart
+++ b/pkgs/test/test/runner/browser/firefox_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 @Tags(['firefox'])
diff --git a/pkgs/test/test/runner/browser/internet_explorer_test.dart b/pkgs/test/test/runner/browser/internet_explorer_test.dart
index e240755..692162a 100644
--- a/pkgs/test/test/runner/browser/internet_explorer_test.dart
+++ b/pkgs/test/test/runner/browser/internet_explorer_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 @Tags(['ie'])
diff --git a/pkgs/test/test/runner/browser/loader_test.dart b/pkgs/test/test/runner/browser/loader_test.dart
index 84b42d4..9ffae9a 100644
--- a/pkgs/test/test/runner/browser/loader_test.dart
+++ b/pkgs/test/test/runner/browser/loader_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 @Tags(['chrome'])
diff --git a/pkgs/test/test/runner/browser/phantom_js_test.dart b/pkgs/test/test/runner/browser/phantom_js_test.dart
index f0c4c10..3c371b6 100644
--- a/pkgs/test/test/runner/browser/phantom_js_test.dart
+++ b/pkgs/test/test/runner/browser/phantom_js_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 @Tags(['phantomjs'])
diff --git a/pkgs/test/test/runner/browser/runner_test.dart b/pkgs/test/test/runner/browser/runner_test.dart
index ccd12ce..577e8a5 100644
--- a/pkgs/test/test/runner/browser/runner_test.dart
+++ b/pkgs/test/test/runner/browser/runner_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 import 'dart:convert';
diff --git a/pkgs/test/test/runner/browser/safari_test.dart b/pkgs/test/test/runner/browser/safari_test.dart
index e1f5a1a..ed37252 100644
--- a/pkgs/test/test/runner/browser/safari_test.dart
+++ b/pkgs/test/test/runner/browser/safari_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 @Tags(['safari'])
diff --git a/pkgs/test/test/runner/compact_reporter_test.dart b/pkgs/test/test/runner/compact_reporter_test.dart
index 3794af8..fb84de8 100644
--- a/pkgs/test/test/runner/compact_reporter_test.dart
+++ b/pkgs/test/test/runner/compact_reporter_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
diff --git a/pkgs/test/test/runner/configuration/configuration_test.dart b/pkgs/test/test/runner/configuration/configuration_test.dart
index 913325d..dc06315 100644
--- a/pkgs/test/test/runner/configuration/configuration_test.dart
+++ b/pkgs/test/test/runner/configuration/configuration_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 import 'package:path/path.dart' as p;
diff --git a/pkgs/test/test/runner/configuration/custom_platform_test.dart b/pkgs/test/test/runner/configuration/custom_platform_test.dart
index 5682551..66d5956 100644
--- a/pkgs/test/test/runner/configuration/custom_platform_test.dart
+++ b/pkgs/test/test/runner/configuration/custom_platform_test.dart
@@ -1,6 +1,8 @@
 // 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
diff --git a/pkgs/test/test/runner/configuration/global_test.dart b/pkgs/test/test/runner/configuration/global_test.dart
index e8e73c0..9a59523 100644
--- a/pkgs/test/test/runner/configuration/global_test.dart
+++ b/pkgs/test/test/runner/configuration/global_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 import 'dart:convert';
diff --git a/pkgs/test/test/runner/configuration/include_test.dart b/pkgs/test/test/runner/configuration/include_test.dart
index c6d605d..92495ee 100644
--- a/pkgs/test/test/runner/configuration/include_test.dart
+++ b/pkgs/test/test/runner/configuration/include_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2018, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 import 'package:path/path.dart' as p;
diff --git a/pkgs/test/test/runner/configuration/platform_test.dart b/pkgs/test/test/runner/configuration/platform_test.dart
index d9bcede..0244d47 100644
--- a/pkgs/test/test/runner/configuration/platform_test.dart
+++ b/pkgs/test/test/runner/configuration/platform_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 import 'dart:convert';
diff --git a/pkgs/test/test/runner/configuration/presets_test.dart b/pkgs/test/test/runner/configuration/presets_test.dart
index e802a87..fca9bf4 100644
--- a/pkgs/test/test/runner/configuration/presets_test.dart
+++ b/pkgs/test/test/runner/configuration/presets_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 import 'dart:convert';
diff --git a/pkgs/test/test/runner/configuration/randomize_order_test.dart b/pkgs/test/test/runner/configuration/randomize_order_test.dart
index 6aaf8c4..b6ff6e5 100644
--- a/pkgs/test/test/runner/configuration/randomize_order_test.dart
+++ b/pkgs/test/test/runner/configuration/randomize_order_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
diff --git a/pkgs/test/test/runner/configuration/suite_test.dart b/pkgs/test/test/runner/configuration/suite_test.dart
index a1b4eb7..bd54b4b 100644
--- a/pkgs/test/test/runner/configuration/suite_test.dart
+++ b/pkgs/test/test/runner/configuration/suite_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 import 'package:boolean_selector/boolean_selector.dart';
diff --git a/pkgs/test/test/runner/configuration/tags_test.dart b/pkgs/test/test/runner/configuration/tags_test.dart
index b9e9255..3eb6834 100644
--- a/pkgs/test/test/runner/configuration/tags_test.dart
+++ b/pkgs/test/test/runner/configuration/tags_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
diff --git a/pkgs/test/test/runner/configuration/top_level_error_test.dart b/pkgs/test/test/runner/configuration/top_level_error_test.dart
index eb08350..ce4c3c8 100644
--- a/pkgs/test/test/runner/configuration/top_level_error_test.dart
+++ b/pkgs/test/test/runner/configuration/top_level_error_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 import 'dart:convert';
diff --git a/pkgs/test/test/runner/configuration/top_level_test.dart b/pkgs/test/test/runner/configuration/top_level_test.dart
index 5e7208c..26705d1 100644
--- a/pkgs/test/test/runner/configuration/top_level_test.dart
+++ b/pkgs/test/test/runner/configuration/top_level_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
diff --git a/pkgs/test/test/runner/coverage_test.dart b/pkgs/test/test/runner/coverage_test.dart
index 36177e4..4b200f7 100644
--- a/pkgs/test/test/runner/coverage_test.dart
+++ b/pkgs/test/test/runner/coverage_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
diff --git a/pkgs/test/test/runner/engine_test.dart b/pkgs/test/test/runner/engine_test.dart
index b70e2a6..f85745e 100644
--- a/pkgs/test/test/runner/engine_test.dart
+++ b/pkgs/test/test/runner/engine_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 import 'dart:async';
 import 'dart:math';
diff --git a/pkgs/test/test/runner/expanded_reporter_test.dart b/pkgs/test/test/runner/expanded_reporter_test.dart
index f4495cf..2003fc5 100644
--- a/pkgs/test/test/runner/expanded_reporter_test.dart
+++ b/pkgs/test/test/runner/expanded_reporter_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
diff --git a/pkgs/test/test/runner/hybrid_test.dart b/pkgs/test/test/runner/hybrid_test.dart
index 827611a..8bc7327 100644
--- a/pkgs/test/test/runner/hybrid_test.dart
+++ b/pkgs/test/test/runner/hybrid_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
@@ -9,9 +11,8 @@
 
 import 'package:package_config/package_config.dart';
 import 'package:path/path.dart' as p;
-import 'package:test_descriptor/test_descriptor.dart' as d;
-
 import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
 
 import '../io.dart';
 
@@ -315,6 +316,7 @@
 
     test('closes the channel when the test finishes by default', () async {
       await d.file('test.dart', '''
+        // @dart=2.7
         import "package:stream_channel/stream_channel.dart";
         import "package:test/test.dart";
 
@@ -357,8 +359,8 @@
         import "package:test/test.dart";
 
         void main() {
-          StreamQueue queue;
-          StreamSink sink;
+          late StreamQueue queue;
+          late StreamSink sink;
           setUpAll(() {
             var channel = spawnHybridCode("""
               import "package:stream_channel/stream_channel.dart";
diff --git a/pkgs/test/test/runner/json_file_reporter_test.dart b/pkgs/test/test/runner/json_file_reporter_test.dart
index 85ab331..5f8d6e4 100644
--- a/pkgs/test/test/runner/json_file_reporter_test.dart
+++ b/pkgs/test/test/runner/json_file_reporter_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2020, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
diff --git a/pkgs/test/test/runner/json_reporter_test.dart b/pkgs/test/test/runner/json_reporter_test.dart
index 3b500c7..e39bccc 100644
--- a/pkgs/test/test/runner/json_reporter_test.dart
+++ b/pkgs/test/test/runner/json_reporter_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
@@ -523,7 +525,7 @@
               groupJson(2, testCount: 2),
               testStartJson(3, 'success 1',
                   line: 3,
-                  column: 50,
+                  column: 60,
                   url: p.toUri(p.join(d.sandbox, 'common.dart')).toString(),
                   root_column: 7,
                   root_line: 7,
@@ -538,7 +540,7 @@
             'common.dart': '''
 import 'package:test/test.dart';
 
-void customTest(String name, Function testFn) => test(name, testFn);
+void customTest(String name, dynamic Function() testFn) => test(name, testFn);
 ''',
           });
     });
diff --git a/pkgs/test/test/runner/json_reporter_utils.dart b/pkgs/test/test/runner/json_reporter_utils.dart
index 8677ece..aa7bba9 100644
--- a/pkgs/test/test/runner/json_reporter_utils.dart
+++ b/pkgs/test/test/runner/json_reporter_utils.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2020, 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:convert';
+// BSD-style license that can be found in the LICENSE file.
+//
+// @dart=2.7
 
 import 'dart:convert';
 
@@ -94,7 +96,7 @@
     'type': 'group',
     'group': {
       'id': id,
-      'name': name,
+      'name': name ?? '',
       'suiteID': suiteID ?? 0,
       'parentID': parentID,
       'metadata': metadataJson(skip: skip),
diff --git a/pkgs/test/test/runner/load_suite_test.dart b/pkgs/test/test/runner/load_suite_test.dart
index 2b79835..4129d8d 100644
--- a/pkgs/test/test/runner/load_suite_test.dart
+++ b/pkgs/test/test/runner/load_suite_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 import 'dart:async';
@@ -64,7 +66,7 @@
   test('a load test forwards errors and completes LoadSuite.suite to null',
       () async {
     var suite = LoadSuite('name', SuiteConfiguration.empty, suitePlatform, () {
-      fail('error');
+      return fail('error');
     });
     expect(suite.group.entries, hasLength(1));
 
diff --git a/pkgs/test/test/runner/loader_test.dart b/pkgs/test/test/runner/loader_test.dart
index 9b2e7ac..18f3cdf 100644
--- a/pkgs/test/test/runner/loader_test.dart
+++ b/pkgs/test/test/runner/loader_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 import 'dart:async';
diff --git a/pkgs/test/test/runner/name_test.dart b/pkgs/test/test/runner/name_test.dart
index ffe92ed..2717fca 100644
--- a/pkgs/test/test/runner/name_test.dart
+++ b/pkgs/test/test/runner/name_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
diff --git a/pkgs/test/test/runner/node/runner_test.dart b/pkgs/test/test/runner/node/runner_test.dart
index d732497..16d8a6c 100644
--- a/pkgs/test/test/runner/node/runner_test.dart
+++ b/pkgs/test/test/runner/node/runner_test.dart
@@ -1,6 +1,8 @@
 // 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 @Tags(['node'])
diff --git a/pkgs/test/test/runner/parse_metadata_test.dart b/pkgs/test/test/runner/parse_metadata_test.dart
index 635d881..c26500e 100644
--- a/pkgs/test/test/runner/parse_metadata_test.dart
+++ b/pkgs/test/test/runner/parse_metadata_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 import 'package:test/test.dart';
diff --git a/pkgs/test/test/runner/pause_after_load_test.dart b/pkgs/test/test/runner/pause_after_load_test.dart
index c1cd315..810f7e7 100644
--- a/pkgs/test/test/runner/pause_after_load_test.dart
+++ b/pkgs/test/test/runner/pause_after_load_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
@@ -172,28 +174,6 @@
     await test.shouldExit(0);
   }, tags: ['firefox', 'chrome', 'vm']);
 
-  test("warns if SILENT_OBSERVATORY isn't set when trying to debug the vm",
-      () async {
-    await d.file('test.dart', '''
-import 'package:test/test.dart';
-
-void main() {
-  test("success", () {});
-}
-''').create();
-
-    var test = await runTest(['--pause-after-load', '-p', 'vm', 'test.dart']);
-    await expectLater(
-        test.stderr,
-        emits('Warning: You should set `SILENT_OBSERVATORY` to true when '
-            'debugging the VM as it will output the observatory URL by '
-            'default.'));
-    test.stdin.writeln();
-    await expectLater(
-        test.stdout, emitsThrough(contains('+1: All tests passed!')));
-    await test.shouldExit(0);
-  });
-
   test('stops immediately if killed while paused', () async {
     await d.file('test.dart', '''
 import 'package:test/test.dart';
diff --git a/pkgs/test/test/runner/precompiled_test.dart b/pkgs/test/test/runner/precompiled_test.dart
index b324e2d..2184640 100644
--- a/pkgs/test/test/runner/precompiled_test.dart
+++ b/pkgs/test/test/runner/precompiled_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 import 'dart:async';
@@ -23,17 +25,11 @@
   group('browser tests', () {
     setUp(() async {
       await d.file('to_precompile.dart', '''
-        import "package:stream_channel/stream_channel.dart";
-
-        import "package:test_core/src/runner/plugin/remote_platform_helpers.dart";
-        import "package:test/src/runner/browser/post_message_channel.dart";
+        import "package:test/bootstrap/browser.dart";
         import "package:test/test.dart";
 
-        main(_) async {
-          var channel = serializeSuite(() {
-            return () => test("success", () {});
-          }, hidePrints: false);
-          postMessageChannel().pipe(channel);
+        main(_) {
+          internalBootstrapBrowserTest(() => () => test("success", () {}));
         }
       ''').create();
 
@@ -52,6 +48,7 @@
       var dart2js = await TestProcess.start(
           p.join(sdkDir, 'bin', 'dart2js'),
           [
+            ...Platform.executableArguments,
             '--packages=${await Isolate.packageConfig}',
             'to_precompile.dart',
             '--out=precompiled/test.dart.browser_test.dart.js'
@@ -109,6 +106,7 @@
       var dart2js = await TestProcess.start(
           p.join(sdkDir, 'bin', 'dart2js'),
           [
+            ...Platform.executableArguments,
             '--packages=${await Isolate.packageConfig}',
             p.join('test', 'test.dart'),
             '--out=$jsPath',
@@ -238,6 +236,7 @@
   // TODO: remove try/catch when this issue is resolved:
   // https://github.com/dart-lang/package_config/issues/66
   try {
+    await d.dir('.dart_tool').create();
     await savePackageConfig(config, Directory(d.sandbox));
   } catch (_) {
     // If it fails, just write a `.packages` file.
diff --git a/pkgs/test/test/runner/pub_serve_test.dart b/pkgs/test/test/runner/pub_serve_test.dart
index 744385c..22b0d21 100644
--- a/pkgs/test/test/runner/pub_serve_test.dart
+++ b/pkgs/test/test/runner/pub_serve_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 @Tags(['pub'])
diff --git a/pkgs/test/test/runner/retry_test.dart b/pkgs/test/test/runner/retry_test.dart
index 8a3585f..c49902f 100644
--- a/pkgs/test/test/runner/retry_test.dart
+++ b/pkgs/test/test/runner/retry_test.dart
@@ -1,6 +1,8 @@
 // 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 import 'package:test_descriptor/test_descriptor.dart' as d;
diff --git a/pkgs/test/test/runner/runner_test.dart b/pkgs/test/test/runner/runner_test.dart
index f1227aa..39b6ffe 100644
--- a/pkgs/test/test/runner/runner_test.dart
+++ b/pkgs/test/test/runner/runner_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
@@ -11,7 +13,6 @@
 
 import 'package:package_config/package_config.dart';
 import 'package:path/path.dart' as p;
-
 import 'package:test/test.dart';
 import 'package:test_core/src/util/exit_codes.dart' as exit_codes;
 import 'package:test_descriptor/test_descriptor.dart' as d;
@@ -57,10 +58,10 @@
 final _usage = '''
 Usage: pub run test [files or directories...]
 
--h, --help                            Shows this usage information.
-    --version                         Shows the package's version.
+-h, --help                            Show this usage information.
+    --version                         Show the package:test version.
 
-======== Selecting Tests
+Selecting Tests:
 -n, --name                            A substring of the name of the test to run.
                                       Regular expression syntax is supported.
                                       If passed multiple times, tests must match all substrings.
@@ -72,7 +73,7 @@
                                       Supports boolean selector syntax.
     --[no-]run-skipped                Run skipped tests instead of skipping them.
 
-======== Running Tests
+Running Tests:
 -p, --platform                        The platform(s) on which to run the tests.
                                       $_browsers
 -P, --preset                          The configuration preset(s) to use.
@@ -83,36 +84,37 @@
     --pub-serve=<port>                The port of a pub serve instance serving "test/".
     --timeout                         The default test timeout. For example: 15s, 2x, none
                                       (defaults to "30s")
-    --pause-after-load                Pauses for debugging before any tests execute.
+    --pause-after-load                Pause for debugging before any tests execute.
                                       Implies --concurrency=1, --debug, and --timeout=none.
                                       Currently only supported for browser tests.
-    --debug                           Runs the VM and Chrome tests in debug mode.
-    --coverage=<directory>            Gathers coverage and outputs it to the specified directory.
+    --debug                           Run the VM and Chrome tests in debug mode.
+    --coverage=<directory>            Gather coverage and output it to the specified directory.
                                       Implies --debug.
-    --[no-]chain-stack-traces         Chained stack traces to provide greater exception details
+    --[no-]chain-stack-traces         Use chained stack traces to provide greater exception details
                                       especially for asynchronous code. It may be useful to disable
                                       to provide improved test performance but at the cost of
                                       debuggability.
                                       (defaults to on)
-    --no-retry                        Don't re-run tests that have retry set.
-    --test-randomize-ordering-seed    The seed to randomize the execution order of test cases.
+    --no-retry                        Don't rerun tests that have retry set.
+    --test-randomize-ordering-seed    Use the specified seed to randomize the execution order of test cases.
                                       Must be a 32bit unsigned integer or "random".
                                       If "random", pick a random seed to use.
                                       If not passed, do not randomize test case execution order.
 
-======== Output
--r, --reporter                        The runner used to print test results.
+Output:
+-r, --reporter                        Set how to print test results.
 
           [compact]                   A single line, updated continuously.
           [expanded] (default)        A separate line for each update.
-          [json]                      A machine-readable format (see https://bit.ly/2Z7J0OH).
+          [json]                      A machine-readable format (see https://dart.dev/go/test-docs/json_reporter.md).
 
-    --file-reporter                   The reporter used to write test results to a file.
-                                      Should be in the form <reporter>:<filepath>, e.g. "json:reports/tests.json"
-    --verbose-trace                   Whether to emit stack traces with core library frames.
-    --js-trace                        Whether to emit raw JavaScript stack traces for browser tests.
-    --[no-]color                      Whether to use terminal colors.
+    --file-reporter                   Set the reporter used to write test results to a file.
+                                      Should be in the form <reporter>:<filepath>, Example: "json:reports/tests.json"
+    --verbose-trace                   Emit stack traces with core library frames.
+    --js-trace                        Emit raw JavaScript stack traces for browser tests.
+    --[no-]color                      Use terminal colors.
                                       (auto-detected by default)
+
 ''';
 
 final _browsers = '[vm (default), chrome, phantomjs, firefox' +
@@ -257,16 +259,22 @@
     });
 
     test('a test file has a non-function main', () async {
-      await d.file('test.dart', 'int main;').create();
+      await d.file('test.dart', 'int main = 0;').create();
       var test = await runTest(['test.dart']);
 
+      expect(test.stdout, emitsThrough(contains('-1: loading test.dart [E]')));
       expect(
           test.stdout,
-          containsInOrder([
-            '-1: loading test.dart [E]',
-            "A value of type 'int' can't be assigned to a "
-                "variable of type 'Function'",
-          ]));
+          emitsThrough(anyOf([
+            contains(
+              "A value of type 'int' can't be assigned to a variable of type "
+              "'Function'",
+            ),
+            contains(
+              "A value of type 'int' can't be returned from a function with "
+              "return type 'Function'",
+            ),
+          ])));
 
       await test.shouldExit(1);
     });
@@ -354,6 +362,34 @@
     });
   });
 
+  group('runs successful tests with async setup', () {
+    setUp(() async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() async {
+          test("success 1", () {});
+
+          await () async {};
+
+          test("success 2", () {});
+        }
+      ''').create();
+    });
+
+    test('defined in a single file', () async {
+      var test = await runTest(['test.dart']);
+      expect(test.stdout, emitsThrough(contains('+2: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('directly', () async {
+      var test = await runDart(['test.dart']);
+      expect(test.stdout, emitsThrough(contains('All tests passed!')));
+      await test.shouldExit(0);
+    });
+  });
+
   group('runs failing tests', () {
     test('defaults to chaining stack traces', () async {
       await d.file('test.dart', _asyncFailure).create();
@@ -672,6 +708,7 @@
     test('defined in a single file', () async {
       await d.file('test.dart', _success).create();
       await d.file('runner.dart', '''
+// @dart=2.8
 import 'package:test_core/src/executable.dart' as test;
 
 void main(List<String> args) async {
@@ -705,80 +742,105 @@
   group('nnbd', () {
     final _testContents = '''
 import 'package:test/test.dart';
-import 'opted_in.dart';
+import 'opted_out.dart';
 
 void main() {
   test("success", () {
-    foo = true;
     expect(foo, true);
   });
 }''';
 
     setUp(() async {
-      await d.file('opted_in.dart', '''
-// @dart=2.10
-bool? foo;''').create();
+      await d.file('opted_out.dart', '''
+// @dart=2.8
+final foo = true;''').create();
     });
 
-    test('nnbd can be enabled in deps', () async {
+    test('sound null safety is enabled if the entrypoint opts in explicitly',
+        () async {
+      await d.file('test.dart', '''
+// @dart=2.11
+$_testContents
+''').create();
+      var test = await runTest(['test.dart']);
+
+      expect(
+          test.stdout,
+          emitsThrough(contains(
+              'Error: A library can\'t opt out of null safety by default, '
+              'when using sound null safety.')));
+      await test.shouldExit(1);
+    });
+
+    test('sound null safety is disabled if the entrypoint opts out explicitly',
+        () async {
       await d.file('test.dart', '''
 // @dart=2.8
 $_testContents''').create();
-      var test = await runTest(['test.dart'],
-          packageConfig: (await Isolate.packageConfig).path,
-          vmArgs: ['--enable-experiment=non-nullable']);
+      var test = await runTest(['test.dart']);
 
       expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
       await test.shouldExit(0);
     });
 
-    test('sound null safety is enabled if the entrypoint opts in', () async {
-      await d.file('test.dart', '''
-// @dart=2.10
-$_testContents''').create();
-      var test = await runTest(['test.dart'],
-          packageConfig: (await Isolate.packageConfig).path,
-          vmArgs: ['--enable-experiment=non-nullable']);
+    group('defaults', () {
+      PackageConfig currentPackageConfig;
 
-      expect(
-          test.stdout,
-          containsInOrder([
-            'Unable to spawn isolate: Error: Cannot run with sound null '
-                'safety as one or more dependencies do not',
-            'support null safety:',
-          ]));
-      await test.shouldExit(1);
-    });
+      setUpAll(() async {
+        currentPackageConfig =
+            await loadPackageConfigUri(await Isolate.packageConfig);
+      });
 
-    test('sound null safety is enabled if the package is opted in', () async {
-      var currentPackageConfig =
-          await loadPackageConfigUri(await Isolate.packageConfig);
-      var newPackageConfig = PackageConfig([
-        ...currentPackageConfig.packages,
-        Package('example', Uri.file('${d.sandbox}/'),
-            languageVersion: LanguageVersion(2, 10),
-            // TODO: https://github.com/dart-lang/package_config/issues/81
-            packageUriRoot: Uri.file('${d.sandbox}/')),
-      ]);
+      setUp(() async {
+        await d.file('test.dart', _testContents).create();
+      });
 
-      await d.file('test.dart', _testContents).create();
-      await d
-          .file('package_config.json',
-              jsonEncode(PackageConfig.toJson(newPackageConfig)))
-          .create();
+      test('sound null safety is enabled if the package is opted in', () async {
+        var newPackageConfig = PackageConfig([
+          ...currentPackageConfig.packages,
+          Package('example', Uri.file('${d.sandbox}/'),
+              languageVersion: LanguageVersion(2, 11),
+              // TODO: https://github.com/dart-lang/package_config/issues/81
+              packageUriRoot: Uri.file('${d.sandbox}/')),
+        ]);
 
-      var test = await runTest(['test.dart'],
-          packageConfig: p.join(d.sandbox, 'package_config.json'),
-          vmArgs: ['--enable-experiment=non-nullable']);
+        await d
+            .file('package_config.json',
+                jsonEncode(PackageConfig.toJson(newPackageConfig)))
+            .create();
 
-      expect(
-          test.stdout,
-          containsInOrder([
-            'Unable to spawn isolate: Error: Cannot run with sound null '
-                'safety as one or more dependencies do not',
-            'support null safety:',
-          ]));
-      await test.shouldExit(1);
+        var test = await runTest(['test.dart'],
+            packageConfig: p.join(d.sandbox, 'package_config.json'));
+
+        expect(
+            test.stdout,
+            emitsThrough(contains(
+                'Error: A library can\'t opt out of null safety by default, '
+                'when using sound null safety.')));
+        await test.shouldExit(1);
+      });
+
+      test('sound null safety is disabled if the package is opted out',
+          () async {
+        var newPackageConfig = PackageConfig([
+          ...currentPackageConfig.packages,
+          Package('example', Uri.file('${d.sandbox}/'),
+              languageVersion: LanguageVersion(2, 8),
+              // TODO: https://github.com/dart-lang/package_config/issues/81
+              packageUriRoot: Uri.file('${d.sandbox}/')),
+        ]);
+
+        await d
+            .file('package_config.json',
+                jsonEncode(PackageConfig.toJson(newPackageConfig)))
+            .create();
+
+        var test = await runTest(['test.dart'],
+            packageConfig: p.join(d.sandbox, 'package_config.json'));
+
+        expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+        await test.shouldExit(0);
+      });
     });
   });
 }
diff --git a/pkgs/test/test/runner/set_up_all_test.dart b/pkgs/test/test/runner/set_up_all_test.dart
index 5b19afa..21f5dad 100644
--- a/pkgs/test/test/runner/set_up_all_test.dart
+++ b/pkgs/test/test/runner/set_up_all_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
diff --git a/pkgs/test/test/runner/shard_test.dart b/pkgs/test/test/runner/shard_test.dart
index 05603d1..a174d33 100644
--- a/pkgs/test/test/runner/shard_test.dart
+++ b/pkgs/test/test/runner/shard_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
diff --git a/pkgs/test/test/runner/signal_test.dart b/pkgs/test/test/runner/signal_test.dart
index 32f6972..648c8ee 100644
--- a/pkgs/test/test/runner/signal_test.dart
+++ b/pkgs/test/test/runner/signal_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 // Windows doesn't support sending signals.
 @TestOn('vm && !windows')
diff --git a/pkgs/test/test/runner/skip_expect_test.dart b/pkgs/test/test/runner/skip_expect_test.dart
index e21442a..d342511 100644
--- a/pkgs/test/test/runner/skip_expect_test.dart
+++ b/pkgs/test/test/runner/skip_expect_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
@@ -115,6 +117,96 @@
     });
   });
 
+  group('markTestSkipped', () {
+    test('prints the skip reason', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test('skipped', () {
+            markTestSkipped('some reason');
+          });
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '+0: skipped',
+            '  some reason',
+            '~1: All tests skipped.',
+          ]));
+      await test.shouldExit(0);
+    });
+
+    test('still allows the test to fail', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test('failing', () {
+            markTestSkipped('some reason');
+            expect(1, equals(2));
+          });
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '+0: failing',
+            '  some reason',
+            '+0 -1: failing [E]',
+            '  Expected: <2>',
+            '    Actual: <1>',
+            '+0 -1: Some tests failed.'
+          ]));
+      await test.shouldExit(1);
+    });
+
+    test('error when called after the test succeeded', () async {
+      await d.file('test.dart', '''
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          var skipCompleter = Completer();
+          var waitCompleter = Completer();
+          test('skip', () {
+            skipCompleter.future.then((_) {
+              waitCompleter.complete();
+              markTestSkipped('some reason');
+            });
+          });
+
+          // Trigger the skip completer in a following test to ensure that it
+          // only fires after skip has completed successfully.
+          test('wait', () async {
+            skipCompleter.complete();
+            await waitCompleter.future;
+          });
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '+0: skip',
+            '+1: wait',
+            '+0 -1: skip',
+            'This test was marked as skipped after it had already completed. '
+                'Make sure to use',
+            '[expectAsync] or the [completes] matcher when testing async code.',
+            '+1 -1: Some tests failed.'
+          ]));
+      await test.shouldExit(1);
+    });
+  });
+
   group('errors', () {
     test('when called after the test succeeded', () async {
       await d.file('test.dart', '''
diff --git a/pkgs/test/test/runner/solo_test.dart b/pkgs/test/test/runner/solo_test.dart
index 4bdb199..f87740f 100644
--- a/pkgs/test/test/runner/solo_test.dart
+++ b/pkgs/test/test/runner/solo_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2018, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
diff --git a/pkgs/test/test/runner/tag_test.dart b/pkgs/test/test/runner/tag_test.dart
index 2136d86..39088d6 100644
--- a/pkgs/test/test/runner/tag_test.dart
+++ b/pkgs/test/test/runner/tag_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
diff --git a/pkgs/test/test/runner/tear_down_all_test.dart b/pkgs/test/test/runner/tear_down_all_test.dart
index 60dd1d9..10982b3 100644
--- a/pkgs/test/test/runner/tear_down_all_test.dart
+++ b/pkgs/test/test/runner/tear_down_all_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
diff --git a/pkgs/test/test/runner/test_chain_test.dart b/pkgs/test/test/runner/test_chain_test.dart
index 5b83acb..314c739 100644
--- a/pkgs/test/test/runner/test_chain_test.dart
+++ b/pkgs/test/test/runner/test_chain_test.dart
@@ -1,6 +1,8 @@
 // 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
diff --git a/pkgs/test/test/runner/test_on_test.dart b/pkgs/test/test/runner/test_on_test.dart
index 8e69998..fa9aa4f 100644
--- a/pkgs/test/test/runner/test_on_test.dart
+++ b/pkgs/test/test/runner/test_on_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
diff --git a/pkgs/test/test/runner/timeout_test.dart b/pkgs/test/test/runner/timeout_test.dart
index a9756da..5620e84 100644
--- a/pkgs/test/test/runner/timeout_test.dart
+++ b/pkgs/test/test/runner/timeout_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 
diff --git a/pkgs/test/test/util/one_off_handler_test.dart b/pkgs/test/test/util/one_off_handler_test.dart
index ec97419..31f9fa6 100644
--- a/pkgs/test/test/util/one_off_handler_test.dart
+++ b/pkgs/test/test/util/one_off_handler_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 import 'dart:async';
 
diff --git a/pkgs/test/test/util/path_handler_test.dart b/pkgs/test/test/util/path_handler_test.dart
index c4a195f..1d99337 100644
--- a/pkgs/test/test/util/path_handler_test.dart
+++ b/pkgs/test/test/util/path_handler_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 import 'dart:async';
 
diff --git a/pkgs/test/test/util/string_literal_iterator_test.dart b/pkgs/test/test/util/string_literal_iterator_test.dart
index 21322b3..73963d7 100644
--- a/pkgs/test/test/util/string_literal_iterator_test.dart
+++ b/pkgs/test/test/util/string_literal_iterator_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @TestOn('vm')
 import 'package:analyzer/dart/analysis/utilities.dart';
diff --git a/pkgs/test/test/utils.dart b/pkgs/test/test/utils.dart
index 09b608d..bf8a947 100644
--- a/pkgs/test/test/utils.dart
+++ b/pkgs/test/test/utils.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 import 'dart:async';
 import 'dart:collection';
diff --git a/pkgs/test/tool/host.dart b/pkgs/test/tool/host.dart
index 269aa07..a59f2f9 100644
--- a/pkgs/test/tool/host.dart
+++ b/pkgs/test/tool/host.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.7
 
 @JS()
 library test.host;
@@ -115,7 +117,7 @@
     document.body.classes.add('debug');
   }
 
-  runZoned(() {
+  runZonedGuarded(() {
     var serverChannel = _connectToServer();
     serverChannel.stream.listen((message) {
       if (message['command'] == 'loadSuite') {
@@ -155,8 +157,7 @@
     }), restartCurrent: allowInterop(() {
       serverChannel.sink.add({'command': 'restart'});
     }));
-    // ignore: deprecated_member_use
-  }, onError: (error, StackTrace stackTrace) {
+  }, (error, stackTrace) {
     print('$error\n${Trace.from(stackTrace).terse}');
   });
 }
diff --git a/pkgs/test_api/CHANGELOG.md b/pkgs/test_api/CHANGELOG.md
index 232a62a..c4f2672 100644
--- a/pkgs/test_api/CHANGELOG.md
+++ b/pkgs/test_api/CHANGELOG.md
@@ -1,3 +1,41 @@
+## 0.2.19-nullsafety.6
+
+* Fix `spawnHybridUri` to respect language versioning of the spawned uri.
+
+## 0.2.19-nullsafety.5
+
+* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release
+  guidelines.
+
+## 0.2.19-nullsafety.4
+
+* Allow prerelease versions of the 2.12 sdk.
+
+## 0.2.19-nullsafety.3
+
+* Add capability to filter to a single exact test name in `Declarer`.
+* Add `markTestSkipped` API.
+
+## 0.2.19-nullsafety.2
+
+* Allow `2.10` stable and `2.11.0-dev` SDKs.
+* Annotate the classes used as annotations to restrict their usage to library
+  level.
+
+## 0.2.19-nullsafety
+
+* Migrate to NNBD.
+  * The vast majority of changes are intended to express the pre-existing
+    behavior of the code regarding to handling of nulls.
+  * **Breaking Change**: `GroupEntry.name` is no longer nullable, the root
+    group now has the empty string as its name.
+* Add the `Fake` class, available through `package:test_api/fake.dart`.  This
+  was previously part of the Mockito package, but with null safety it is useful
+  enough that we decided to make it available through `package:test`.  In a
+  future release it will be made available directly through
+  `package:test_api/test_api.dart` (and hence through
+  `package:test_core/test_core.dart` and `package:test/test.dart`).
+
 ## 0.2.18+1 (Backport)
 
 * Fix `spawnHybridUri` to respect language versioning of the spawned uri.
@@ -9,7 +47,7 @@
 ## 0.2.17
 
 * Add `languageVersionComment` on the `MetaData` class. This should only be
-  presen for test suites.
+  present for test suites.
 
 ## 0.2.16
 
diff --git a/pkgs/test_api/lib/fake.dart b/pkgs/test_api/lib/fake.dart
new file mode 100644
index 0000000..702bb5f
--- /dev/null
+++ b/pkgs/test_api/lib/fake.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2020, 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.
+
+// Note: eventually we would like to fold this into test_api.dart, but we can't
+// do so until Mockito stops implementing its own version of `Fake`, because
+// there is code in the wild that imports both test_api.dart and Mockito.
+
+@Deprecated('package:test_api is not intended for general use. '
+    'Please use package:test.')
+library test_api.fake;
+
+export 'src/frontend/fake.dart';
diff --git a/pkgs/test_api/lib/src/backend/declarer.dart b/pkgs/test_api/lib/src/backend/declarer.dart
index c27ce80..9211776 100644
--- a/pkgs/test_api/lib/src/backend/declarer.dart
+++ b/pkgs/test_api/lib/src/backend/declarer.dart
@@ -24,13 +24,13 @@
 /// [Declarer.current].
 class Declarer {
   /// The parent declarer, or `null` if this corresponds to the root group.
-  final Declarer _parent;
+  final Declarer? _parent;
 
   /// The name of the current test group, including the name of any parent
   /// groups.
   ///
   /// This is `null` if this is the root group.
-  final String _name;
+  final String? _name;
 
   /// The metadata for this group, including the metadata of any parent groups
   /// and of the test suite.
@@ -41,7 +41,9 @@
   final Set<String> _platformVariables;
 
   /// The stack trace for this group.
-  final Trace _trace;
+  ///
+  /// This is `null` for the root (implicit) group.
+  final Trace? _trace;
 
   /// Whether to collect stack traces for [GroupEntry]s.
   final bool _collectTraces;
@@ -66,7 +68,7 @@
   /// All [setUpAll]s are run in a single logical test, so they can only have
   /// one trace. The first trace is most often correct, since the first
   /// [setUpAll] is always run and the rest are only run if that one succeeds.
-  Trace _setUpAllTrace;
+  Trace? _setUpAllTrace;
 
   /// The tear-down functions to run once for this group.
   final _tearDownAlls = <Function()>[];
@@ -75,7 +77,7 @@
   ///
   /// All [tearDownAll]s are run in a single logical test, so they can only have
   /// one trace. The first trace matches [_setUpAllTrace].
-  Trace _tearDownAllTrace;
+  Trace? _tearDownAllTrace;
 
   /// The children of this group, either tests or sub-groups.
   final _entries = <GroupEntry>[];
@@ -89,8 +91,19 @@
   /// Whether any tests and/or groups have been flagged as solo.
   bool get _solo => _soloEntries.isNotEmpty;
 
+  /// An exact full test name to match.
+  ///
+  /// When non-null only tests with exactly this name will be considered. The
+  /// full test name is the combination of the test case name with all group
+  /// prefixes. All other tests, including their metadata like `solo`, is
+  /// ignored. Uniqueness is not guaranteed so this may match more than one
+  /// test.
+  ///
+  /// Groups which are not a strict prefix of this name will be ignored.
+  final String? _fullTestName;
+
   /// The current zone-scoped declarer.
-  static Declarer get current => Zone.current[#test.declarer] as Declarer;
+  static Declarer? get current => Zone.current[#test.declarer] as Declarer?;
 
   /// Creates a new declarer for the root group.
   ///
@@ -108,10 +121,11 @@
   ///
   /// If [noRetry] is `true` tests will be run at most once.
   Declarer(
-      {Metadata metadata,
-      Set<String> platformVariables,
+      {Metadata? metadata,
+      Set<String>? platformVariables,
       bool collectTraces = false,
-      bool noRetry = false})
+      bool noRetry = false,
+      String? fullTestName})
       : this._(
             null,
             null,
@@ -119,10 +133,19 @@
             platformVariables ?? const UnmodifiableSetView.empty(),
             collectTraces,
             null,
-            noRetry);
+            noRetry,
+            fullTestName);
 
-  Declarer._(this._parent, this._name, this._metadata, this._platformVariables,
-      this._collectTraces, this._trace, this._noRetry);
+  Declarer._(
+    this._parent,
+    this._name,
+    this._metadata,
+    this._platformVariables,
+    this._collectTraces,
+    this._trace,
+    this._noRetry,
+    this._fullTestName,
+  );
 
   /// Runs [body] with this declarer as [Declarer.current].
   ///
@@ -132,15 +155,20 @@
 
   /// Defines a test case with the given name and body.
   void test(String name, dynamic Function() body,
-      {String testOn,
-      Timeout timeout,
+      {String? testOn,
+      Timeout? timeout,
       skip,
-      Map<String, dynamic> onPlatform,
+      Map<String, dynamic>? onPlatform,
       tags,
-      int retry,
+      int? retry,
       bool solo = false}) {
     _checkNotBuilt('test');
 
+    final fullName = _prefix(name);
+    if (_fullTestName != null && fullName != _fullTestName) {
+      return;
+    }
+
     var newMetadata = Metadata.parse(
         testOn: testOn,
         timeout: timeout,
@@ -150,10 +178,11 @@
         retry: _noRetry ? 0 : retry);
     newMetadata.validatePlatformSelectors(_platformVariables);
     var metadata = _metadata.merge(newMetadata);
-
-    _entries.add(LocalTest(_prefix(name), metadata, () async {
+    _entries.add(LocalTest(fullName, metadata, () async {
       var parents = <Declarer>[];
-      for (var declarer = this; declarer != null; declarer = declarer._parent) {
+      for (Declarer? declarer = this;
+          declarer != null;
+          declarer = declarer._parent) {
         parents.add(declarer);
       }
 
@@ -162,7 +191,7 @@
       // they were declared in source.
       for (var declarer in parents.reversed) {
         for (var tearDown in declarer._tearDowns) {
-          Invoker.current.addTearDown(tearDown);
+          Invoker.current!.addTearDown(tearDown);
         }
       }
 
@@ -182,15 +211,20 @@
 
   /// Creates a group of tests.
   void group(String name, void Function() body,
-      {String testOn,
-      Timeout timeout,
+      {String? testOn,
+      Timeout? timeout,
       skip,
-      Map<String, dynamic> onPlatform,
+      Map<String, dynamic>? onPlatform,
       tags,
-      int retry,
+      int? retry,
       bool solo = false}) {
     _checkNotBuilt('group');
 
+    final fullTestPrefix = _prefix(name);
+    if (_fullTestName != null && !_fullTestName!.startsWith(fullTestPrefix)) {
+      return;
+    }
+
     var newMetadata = Metadata.parse(
         testOn: testOn,
         timeout: timeout,
@@ -202,8 +236,8 @@
     var metadata = _metadata.merge(newMetadata);
     var trace = _collectTraces ? Trace.current(2) : null;
 
-    var declarer = Declarer._(this, _prefix(name), metadata, _platformVariables,
-        _collectTraces, trace, _noRetry);
+    var declarer = Declarer._(this, fullTestPrefix, metadata,
+        _platformVariables, _collectTraces, trace, _noRetry, _fullTestName);
     declarer.declare(() {
       // Cast to dynamic to avoid the analyzer complaining about us using the
       // result of a void method.
@@ -266,12 +300,12 @@
             entry.name,
             entry.metadata
                 .change(skip: true, skipReason: 'does not have "solo"'),
-            null);
+            () {});
       }
       return entry;
     }).toList();
 
-    return Group(_name, entries,
+    return Group(_name ?? '', entries,
         metadata: _metadata,
         trace: _trace,
         setUpAll: _setUpAll,
@@ -291,25 +325,27 @@
   /// If no set-up functions are declared, this returns a [Future] that
   /// completes immediately.
   Future _runSetUps() async {
-    if (_parent != null) await _parent._runSetUps();
-    await Future.forEach(_setUps, (setUp) => setUp());
+    if (_parent != null) await _parent!._runSetUps();
+    // TODO: why does type inference not work here?
+    await Future.forEach<Function>(_setUps, (setUp) => setUp());
   }
 
-  /// Returns a [Test] that runs the callbacks in [_setUpAll].
-  Test get _setUpAll {
+  /// Returns a [Test] that runs the callbacks in [_setUpAll], or `null`.
+  Test? get _setUpAll {
     if (_setUpAlls.isEmpty) return null;
 
     return LocalTest(_prefix('(setUpAll)'), _metadata.change(timeout: _timeout),
         () {
-      return runZoned(() => Future.forEach(_setUpAlls, (setUp) => setUp()),
+      return runZoned(
+          () => Future.forEach<Function>(_setUpAlls, (setUp) => setUp()),
           // Make the declarer visible to running scaffolds so they can add to
           // the declarer's `tearDownAll()` list.
           zoneValues: {#test.declarer: this});
     }, trace: _setUpAllTrace, guarded: false, isScaffoldAll: true);
   }
 
-  /// Returns a [Test] that runs the callbacks in [_tearDownAll].
-  Test get _tearDownAll {
+  /// Returns a [Test] that runs the callbacks in [_tearDownAll], or `null`.
+  Test? get _tearDownAll {
     // We have to create a tearDownAll if there's a setUpAll, since it might
     // dynamically add tear-down code using [addTearDownAll].
     if (_setUpAlls.isEmpty && _tearDownAlls.isEmpty) return null;
@@ -317,7 +353,7 @@
     return LocalTest(
         _prefix('(tearDownAll)'), _metadata.change(timeout: _timeout), () {
       return runZoned(() {
-        return Invoker.current.unclosable(() async {
+        return Invoker.current!.unclosable(() async {
           while (_tearDownAlls.isNotEmpty) {
             await errorsDontStopTest(_tearDownAlls.removeLast());
           }
diff --git a/pkgs/test_api/lib/src/backend/group.dart b/pkgs/test_api/lib/src/backend/group.dart
index 30f5238..9a28875 100644
--- a/pkgs/test_api/lib/src/backend/group.dart
+++ b/pkgs/test_api/lib/src/backend/group.dart
@@ -20,42 +20,42 @@
   final Metadata metadata;
 
   @override
-  final Trace trace;
+  final Trace? trace;
 
   /// The children of this group.
   final List<GroupEntry> entries;
 
   /// Returns a new root-level group.
-  Group.root(Iterable<GroupEntry> entries, {Metadata metadata})
-      : this(null, entries, metadata: metadata);
+  Group.root(Iterable<GroupEntry> entries, {Metadata? metadata})
+      : this('', entries, metadata: metadata);
 
   /// A test to run before all tests in the group.
   ///
   /// This is `null` if no `setUpAll` callbacks were declared.
-  final Test setUpAll;
+  final Test? setUpAll;
 
   /// A test to run after all tests in the group.
   ///
   /// This is `null` if no `tearDown` callbacks were declared.
-  final Test tearDownAll;
+  final Test? tearDownAll;
 
   /// The number of tests (recursively) in this group.
   int get testCount {
-    if (_testCount != null) return _testCount;
-    _testCount = entries.fold(
+    if (_testCount != null) return _testCount!;
+    _testCount = entries.fold<int>(
         0, (count, entry) => count + (entry is Group ? entry.testCount : 1));
-    return _testCount;
+    return _testCount!;
   }
 
-  int _testCount;
+  int? _testCount;
 
   Group(this.name, Iterable<GroupEntry> entries,
-      {Metadata metadata, this.trace, this.setUpAll, this.tearDownAll})
+      {Metadata? metadata, this.trace, this.setUpAll, this.tearDownAll})
       : entries = List<GroupEntry>.unmodifiable(entries),
         metadata = metadata ?? Metadata();
 
   @override
-  Group forPlatform(SuitePlatform platform) {
+  Group? forPlatform(SuitePlatform platform) {
     if (!metadata.testOn.evaluate(platform)) return null;
     var newMetadata = metadata.forPlatform(platform);
     var filtered = _map((entry) => entry.forPlatform(platform));
@@ -68,7 +68,7 @@
   }
 
   @override
-  Group filter(bool Function(Test) callback) {
+  Group? filter(bool Function(Test) callback) {
     var filtered = _map((entry) => entry.filter(callback));
     if (filtered.isEmpty && entries.isNotEmpty) return null;
     return Group(name, filtered,
@@ -81,10 +81,10 @@
   /// Returns the entries of this group mapped using [callback].
   ///
   /// Any `null` values returned by [callback] will be removed.
-  List<GroupEntry> _map(GroupEntry Function(GroupEntry) callback) {
+  List<GroupEntry> _map(GroupEntry? Function(GroupEntry) callback) {
     return entries
         .map((entry) => callback(entry))
-        .where((entry) => entry != null)
+        .whereType<GroupEntry>()
         .toList();
   }
 }
diff --git a/pkgs/test_api/lib/src/backend/group_entry.dart b/pkgs/test_api/lib/src/backend/group_entry.dart
index 44b3054..a6f30bd 100644
--- a/pkgs/test_api/lib/src/backend/group_entry.dart
+++ b/pkgs/test_api/lib/src/backend/group_entry.dart
@@ -13,7 +13,7 @@
   /// The name of the entry, including the prefixes from any containing
   /// [Group]s.
   ///
-  /// This will be `null` for the root group.
+  /// This will be empty for the root group.
   String get name;
 
   /// The metadata for the entry, including the metadata from any containing
@@ -22,18 +22,18 @@
 
   /// The stack trace for the call to `test()` or `group()` that defined this
   /// entry, or `null` if the entry was defined in a different way.
-  Trace get trace;
+  Trace? get trace;
 
   /// Returns a copy of [this] with all platform-specific metadata resolved.
   ///
   /// Removes any tests and groups with [Metadata.testOn] selectors that don't
   /// match [platform]. Returns `null` if this entry's selector doesn't match.
-  GroupEntry forPlatform(SuitePlatform platform);
+  GroupEntry? forPlatform(SuitePlatform platform);
 
   /// Returns a copy of [this] with all tests that don't match [callback]
   /// removed.
   ///
   /// Returns `null` if this is a test that doesn't match [callback] or a group
   /// where no child tests match [callback].
-  GroupEntry filter(bool Function(Test) callback);
+  GroupEntry? filter(bool Function(Test) callback);
 }
diff --git a/pkgs/test_api/lib/src/backend/invoker.dart b/pkgs/test_api/lib/src/backend/invoker.dart
index c915770..2f5acff 100644
--- a/pkgs/test_api/lib/src/backend/invoker.dart
+++ b/pkgs/test_api/lib/src/backend/invoker.dart
@@ -25,10 +25,12 @@
 class LocalTest extends Test {
   @override
   final String name;
+
   @override
   final Metadata metadata;
+
   @override
-  final Trace trace;
+  final Trace? trace;
 
   /// Whether this is a test defined using `setUpAll()` or `tearDownAll()`.
   final bool isScaffoldAll;
@@ -54,13 +56,13 @@
 
   /// Loads a single runnable instance of this test.
   @override
-  LiveTest load(Suite suite, {Iterable<Group> groups}) {
+  LiveTest load(Suite suite, {Iterable<Group>? groups}) {
     var invoker = Invoker._(suite, this, groups: groups, guarded: _guarded);
     return invoker.liveTest;
   }
 
   @override
-  Test forPlatform(SuitePlatform platform) {
+  Test? forPlatform(SuitePlatform platform) {
     if (!metadata.testOn.evaluate(platform)) return null;
     return LocalTest._(name, metadata.forPlatform(platform), _body, trace,
         _guarded, isScaffoldAll);
@@ -77,7 +79,7 @@
   ///
   /// This provides a view into the state of the test being executed.
   LiveTest get liveTest => _controller;
-  LiveTestController _controller;
+  late final LiveTestController _controller;
 
   /// Whether to run this test in its own error zone.
   final bool _guarded;
@@ -112,7 +114,7 @@
 
   /// The outstanding callback counter for the current zone.
   _AsyncCounter get _outstandingCallbacks {
-    var counter = Zone.current[_counterKey] as _AsyncCounter;
+    var counter = Zone.current[_counterKey] as _AsyncCounter?;
     if (counter != null) return counter;
     throw StateError("Can't add or remove outstanding callbacks outside "
         'of a test body.');
@@ -137,31 +139,31 @@
   /// The current invoker, or `null` if none is defined.
   ///
   /// An invoker is only set within the zone scope of a running test.
-  static Invoker get current {
+  static Invoker? get current {
     // TODO(nweiz): Use a private symbol when dart2js supports it (issue 17526).
-    return Zone.current[#test.invoker] as Invoker;
+    return Zone.current[#test.invoker] as Invoker?;
   }
 
   /// Runs [callback] in a zone where unhandled errors from [LiveTest]s are
   /// caught and dispatched to the appropriate [Invoker].
-  static T guard<T>(T Function() callback) =>
-      runZoned(callback, zoneSpecification: ZoneSpecification(
+  static T? guard<T>(T Function() callback) =>
+      runZoned<T?>(callback, zoneSpecification: ZoneSpecification(
           // Use [handleUncaughtError] rather than [onError] so we can
           // capture [zone] and with it the outstanding callback counter for
           // the zone in which [error] was thrown.
           handleUncaughtError: (self, _, zone, error, stackTrace) {
-        var invoker = zone[#test.invoker] as Invoker;
+        var invoker = zone[#test.invoker] as Invoker?;
         if (invoker != null) {
-          self.parent.run(() => invoker._handleError(zone, error, stackTrace));
+          self.parent!.run(() => invoker._handleError(zone, error, stackTrace));
         } else {
-          self.parent.handleUncaughtError(error, stackTrace);
+          self.parent!.handleUncaughtError(error, stackTrace);
         }
       }));
 
   /// The timer for tracking timeouts.
   ///
   /// This will be `null` until the test starts running.
-  Timer _timeoutTimer;
+  Timer? _timeoutTimer;
 
   /// The tear-down functions to run when this test finishes.
   final _tearDowns = <Function()>[];
@@ -170,7 +172,7 @@
   final _printsOnFailure = <String>[];
 
   Invoker._(Suite suite, LocalTest test,
-      {Iterable<Group> groups, bool guarded = true})
+      {Iterable<Group>? groups, bool guarded = true})
       : _guarded = guarded {
     _controller = LiveTestController(
         suite, test, _onRun, _onCloseCompleter.complete,
@@ -185,7 +187,7 @@
     if (closed) throw ClosedException();
 
     if (_test.isScaffoldAll) {
-      Declarer.current.addTearDownAll(callback);
+      Declarer.current!.addTearDownAll(callback);
     } else {
       _tearDowns.add(callback);
     }
@@ -221,17 +223,17 @@
   Future<void> waitForOutstandingCallbacks(FutureOr<void> Function() fn) {
     heartbeat();
 
-    Zone zone;
+    Zone? zone;
     var counter = _AsyncCounter();
     runZoned(() async {
       zone = Zone.current;
-      _outstandingCallbackZones.add(zone);
+      _outstandingCallbackZones.add(zone!);
       await fn();
       counter.decrement();
     }, zoneValues: {_counterKey: counter});
 
     return counter.onZero.whenComplete(() {
-      _outstandingCallbackZones.remove(zone);
+      _outstandingCallbackZones.remove(zone!);
     });
   }
 
@@ -252,7 +254,7 @@
   /// long-running tests that still make progress don't time out.
   void heartbeat() {
     if (liveTest.isComplete) return;
-    if (_timeoutTimer != null) _timeoutTimer.cancel();
+    if (_timeoutTimer != null) _timeoutTimer!.cancel();
 
     const defaultTimeout = Duration(seconds: 30);
     var timeout = liveTest.test.metadata.timeout.apply(defaultTimeout);
@@ -278,7 +280,7 @@
   ///
   /// Note that this *does not* mark the test as complete. That is, it sets
   /// the result to [Result.skipped], but doesn't change the state.
-  void skip([String message]) {
+  void skip([String? message]) {
     if (liveTest.state.shouldBeDone) {
       // Set the state explicitly so we don't get an extra error about the test
       // failing after being complete.
@@ -306,7 +308,7 @@
   /// Notifies the invoker of an asynchronous error.
   ///
   /// The [zone] is the zone in which the error was thrown.
-  void _handleError(Zone zone, error, [StackTrace stackTrace]) {
+  void _handleError(Zone zone, Object error, [StackTrace? stackTrace]) {
     // Ignore errors propagated from previous test runs
     if (_runCount != zone[#runCount]) return;
 
@@ -315,7 +317,7 @@
       if (stackTrace == null) {
         stackTrace = Chain.current();
       } else {
-        stackTrace = Chain.forTrace(stackTrace);
+        stackTrace = Chain.forTrace(stackTrace!);
       }
     });
 
@@ -328,7 +330,7 @@
       _controller.setState(const State(Status.complete, Result.failure));
     }
 
-    _controller.addError(error, stackTrace);
+    _controller.addError(error, stackTrace!);
     zone.run(() => _outstandingCallbacks.complete());
 
     if (!liveTest.test.metadata.chainStackTraces) {
@@ -381,7 +383,7 @@
           await waitForOutstandingCallbacks(_test._body);
           await waitForOutstandingCallbacks(() => unclosable(_runTearDowns));
 
-          if (_timeoutTimer != null) _timeoutTimer.cancel();
+          if (_timeoutTimer != null) _timeoutTimer!.cancel();
 
           if (liveTest.state.result != Result.success &&
               _runCount < liveTest.test.metadata.retry + 1) {
diff --git a/pkgs/test_api/lib/src/backend/live_test.dart b/pkgs/test_api/lib/src/backend/live_test.dart
index 875b4d6..7de4c4f 100644
--- a/pkgs/test_api/lib/src/backend/live_test.dart
+++ b/pkgs/test_api/lib/src/backend/live_test.dart
@@ -104,7 +104,7 @@
   /// The name of this live test without any group prefixes.
   String get individualName {
     var group = groups.last;
-    if (group.name == null) return test.name;
+    if (group.name.isEmpty) return test.name;
     if (!test.name.startsWith(group.name)) return test.name;
 
     // The test will have the same name as the group for virtual tests created
diff --git a/pkgs/test_api/lib/src/backend/live_test_controller.dart b/pkgs/test_api/lib/src/backend/live_test_controller.dart
index 2f0438c..94994c1 100644
--- a/pkgs/test_api/lib/src/backend/live_test_controller.dart
+++ b/pkgs/test_api/lib/src/backend/live_test_controller.dart
@@ -99,7 +99,7 @@
   /// If [groups] is passed, it's used to populate the list of groups that
   /// contain this test. Otherwise, `suite.group` is used.
   LiveTestController(this.suite, this.test, this._onRun, this._onClose,
-      {Iterable<Group> groups})
+      {Iterable<Group>? groups})
       : groups = groups == null ? [suite.group] : List.unmodifiable(groups);
 
   /// Adds an error to the [LiveTest].
@@ -107,10 +107,11 @@
   /// This both adds the error to [LiveTest.errors] and emits it via
   /// [LiveTest.onError]. [stackTrace] is automatically converted into a [Chain]
   /// if it's not one already.
-  void addError(error, StackTrace stackTrace) {
+  void addError(Object error, StackTrace? stackTrace) {
     if (_isClosed) return;
 
-    var asyncError = AsyncError(error, Chain.forTrace(stackTrace));
+    var asyncError = AsyncError(
+        error, Chain.forTrace(stackTrace ?? StackTrace.fromString('')));
     _errors.add(asyncError);
     _onError.add(asyncError);
   }
diff --git a/pkgs/test_api/lib/src/backend/metadata.dart b/pkgs/test_api/lib/src/backend/metadata.dart
index 69322d5..f63bcf7 100644
--- a/pkgs/test_api/lib/src/backend/metadata.dart
+++ b/pkgs/test_api/lib/src/backend/metadata.dart
@@ -32,25 +32,25 @@
 
   /// Whether the test or suite should be skipped.
   bool get skip => _skip ?? false;
-  final bool _skip;
+  final bool? _skip;
 
   /// The reason the test or suite should be skipped, if given.
-  final String skipReason;
+  final String? skipReason;
 
   /// Whether to use verbose stack traces.
   bool get verboseTrace => _verboseTrace ?? false;
-  final bool _verboseTrace;
+  final bool? _verboseTrace;
 
   /// Whether to chain stack traces.
   bool get chainStackTraces => _chainStackTraces ?? true;
-  final bool _chainStackTraces;
+  final bool? _chainStackTraces;
 
   /// The user-defined tags attached to the test or suite.
   final Set<String> tags;
 
   /// The number of times to re-run a test before being marked as a failure.
   int get retry => _retry ?? 0;
-  final int _retry;
+  final int? _retry;
 
   /// Platform-specific metadata.
   ///
@@ -71,11 +71,11 @@
   /// The language version comment, if one is present.
   ///
   /// Only available for test suites and not individual tests.
-  final String languageVersionComment;
+  final String? languageVersionComment;
 
   /// Parses a user-provided map into the value for [onPlatform].
   static Map<PlatformSelector, Metadata> _parseOnPlatform(
-      Map<String, dynamic> onPlatform) {
+      Map<String, dynamic>? onPlatform) {
     if (onPlatform == null) return {};
 
     var result = <PlatformSelector, Metadata>{};
@@ -89,7 +89,7 @@
 
       var selector = PlatformSelector.parse(platform);
 
-      Timeout timeout;
+      Timeout? timeout;
       dynamic skip;
       for (var metadatum in metadata) {
         if (metadatum is Timeout) {
@@ -128,11 +128,11 @@
           tags, 'tags', 'must be either a String or an Iterable.');
     }
 
-    if ((tags as Iterable).any((tag) => tag is! String)) {
+    if (tags.any((tag) => tag is! String)) {
       throw ArgumentError.value(tags, 'tags', 'must contain only Strings.');
     }
 
-    return Set.from(tags as Iterable);
+    return Set.from(tags);
   }
 
   /// Creates new Metadata.
@@ -143,17 +143,17 @@
   /// included inline in the returned value. The values directly passed to the
   /// constructor take precedence over tag-specific metadata.
   factory Metadata(
-      {PlatformSelector testOn,
-      Timeout timeout,
-      bool skip,
-      bool verboseTrace,
-      bool chainStackTraces,
-      int retry,
-      String skipReason,
-      Iterable<String> tags,
-      Map<PlatformSelector, Metadata> onPlatform,
-      Map<BooleanSelector, Metadata> forTag,
-      String languageVersionComment}) {
+      {PlatformSelector? testOn,
+      Timeout? timeout,
+      bool? skip,
+      bool? verboseTrace,
+      bool? chainStackTraces,
+      int? retry,
+      String? skipReason,
+      Iterable<String>? tags,
+      Map<PlatformSelector, Metadata>? onPlatform,
+      Map<BooleanSelector, Metadata>? forTag,
+      String? languageVersionComment}) {
     // Returns metadata without forTag resolved at all.
     Metadata _unresolved() => Metadata._(
         testOn: testOn,
@@ -179,8 +179,8 @@
     // doing it for every test individually.
     var empty = Metadata._();
     var merged = forTag.keys.toList().fold(empty, (Metadata merged, selector) {
-      if (!selector.evaluate(tags.contains)) return merged;
-      return merged.merge(forTag.remove(selector));
+      if (!selector.evaluate(tags!.contains)) return merged;
+      return merged.merge(forTag!.remove(selector)!);
     });
 
     if (merged == empty) return _unresolved();
@@ -190,19 +190,19 @@
   /// Creates new Metadata.
   ///
   /// Unlike [new Metadata], this assumes [forTag] is already resolved.
-  Metadata._(
-      {PlatformSelector testOn,
-      Timeout timeout,
-      bool skip,
-      this.skipReason,
-      bool verboseTrace,
-      bool chainStackTraces,
-      int retry,
-      Iterable<String> tags,
-      Map<PlatformSelector, Metadata> onPlatform,
-      Map<BooleanSelector, Metadata> forTag,
-      this.languageVersionComment})
-      : testOn = testOn ?? PlatformSelector.all,
+  Metadata._({
+    PlatformSelector? testOn,
+    Timeout? timeout,
+    bool? skip,
+    this.skipReason,
+    bool? verboseTrace,
+    bool? chainStackTraces,
+    int? retry,
+    Iterable<String>? tags,
+    Map<PlatformSelector, Metadata>? onPlatform,
+    Map<BooleanSelector, Metadata>? forTag,
+    this.languageVersionComment,
+  })  : testOn = testOn ?? PlatformSelector.all,
         timeout = timeout ?? const Timeout.factor(1),
         _skip = skip,
         _verboseTrace = verboseTrace,
@@ -221,13 +221,13 @@
   ///
   /// Throws a [FormatException] if any field is invalid.
   Metadata.parse(
-      {String testOn,
-      Timeout timeout,
+      {String? testOn,
+      Timeout? timeout,
       dynamic skip,
-      bool verboseTrace,
-      bool chainStackTraces,
-      int retry,
-      Map<String, dynamic> onPlatform,
+      bool? verboseTrace,
+      bool? chainStackTraces,
+      int? retry,
+      Map<String, dynamic>? onPlatform,
       tags,
       this.languageVersionComment})
       : testOn = testOn == null
@@ -257,11 +257,11 @@
             ? PlatformSelector.all
             : PlatformSelector.parse(serialized['testOn'] as String),
         timeout = _deserializeTimeout(serialized['timeout']),
-        _skip = serialized['skip'] as bool,
-        skipReason = serialized['skipReason'] as String,
-        _verboseTrace = serialized['verboseTrace'] as bool,
-        _chainStackTraces = serialized['chainStackTraces'] as bool,
-        _retry = serialized['retry'] as int,
+        _skip = serialized['skip'] as bool?,
+        skipReason = serialized['skipReason'] as String?,
+        _verboseTrace = serialized['verboseTrace'] as bool?,
+        _chainStackTraces = serialized['chainStackTraces'] as bool?,
+        _retry = serialized['retry'] as int?,
         tags = Set.from(serialized['tags'] as Iterable),
         onPlatform = {
           for (var pair in serialized['onPlatform'])
@@ -271,7 +271,8 @@
         forTag = (serialized['forTag'] as Map).map((key, nested) => MapEntry(
             BooleanSelector.parse(key as String),
             Metadata.deserialize(nested))),
-        languageVersionComment = serialized['languageVersionComment'] as String;
+        languageVersionComment =
+            serialized['languageVersionComment'] as String?;
 
   /// Deserializes timeout from the format returned by [_serializeTimeout].
   static Timeout _deserializeTimeout(serialized) {
@@ -330,17 +331,17 @@
 
   /// Returns a copy of [this] with the given fields changed.
   Metadata change(
-      {PlatformSelector testOn,
-      Timeout timeout,
-      bool skip,
-      bool verboseTrace,
-      bool chainStackTraces,
-      int retry,
-      String skipReason,
-      Map<PlatformSelector, Metadata> onPlatform,
-      Set<String> tags,
-      Map<BooleanSelector, Metadata> forTag,
-      String languageVersionComment}) {
+      {PlatformSelector? testOn,
+      Timeout? timeout,
+      bool? skip,
+      bool? verboseTrace,
+      bool? chainStackTraces,
+      int? retry,
+      String? skipReason,
+      Map<PlatformSelector, Metadata>? onPlatform,
+      Set<String>? tags,
+      Map<BooleanSelector, Metadata>? forTag,
+      String? languageVersionComment}) {
     testOn ??= this.testOn;
     timeout ??= this.timeout;
     skip ??= _skip;
@@ -408,8 +409,7 @@
   dynamic _serializeTimeout(Timeout timeout) {
     if (timeout == Timeout.none) return 'none';
     return {
-      'duration':
-          timeout.duration == null ? null : timeout.duration.inMicroseconds,
+      'duration': timeout.duration?.inMicroseconds,
       'scaleFactor': timeout.scaleFactor
     };
   }
diff --git a/pkgs/test_api/lib/src/backend/operating_system.dart b/pkgs/test_api/lib/src/backend/operating_system.dart
index f2b7639..f33e739 100644
--- a/pkgs/test_api/lib/src/backend/operating_system.dart
+++ b/pkgs/test_api/lib/src/backend/operating_system.dart
@@ -43,7 +43,7 @@
   /// If no operating system is found, returns [none].
   static OperatingSystem find(String identifier) =>
       all.firstWhere((platform) => platform.identifier == identifier,
-          orElse: () => null);
+          orElse: () => none);
 
   /// Finds an operating system by the return value from `dart:io`'s
   /// `Platform.operatingSystem`.
diff --git a/pkgs/test_api/lib/src/backend/platform_selector.dart b/pkgs/test_api/lib/src/backend/platform_selector.dart
index f2ab5a9..26760cb 100644
--- a/pkgs/test_api/lib/src/backend/platform_selector.dart
+++ b/pkgs/test_api/lib/src/backend/platform_selector.dart
@@ -35,13 +35,13 @@
   final BooleanSelector _inner;
 
   /// The source span from which this selector was parsed.
-  final SourceSpan _span;
+  final SourceSpan? _span;
 
   /// Parses [selector].
   ///
   /// If [span] is passed, it indicates the location of the text for [selector]
   /// in a larger document. It's used for error reporting.
-  PlatformSelector.parse(String selector, [SourceSpan span])
+  PlatformSelector.parse(String selector, [SourceSpan? span])
       : _inner =
             _wrapFormatException(() => BooleanSelector.parse(selector), span),
         _span = span;
@@ -52,7 +52,7 @@
   /// [SourceSpanFormatException] using [span].
   ///
   /// If [span] is `null`, runs [body] as-is.
-  static T _wrapFormatException<T>(T Function() body, SourceSpan span) {
+  static T _wrapFormatException<T>(T Function() body, [SourceSpan? span]) {
     if (span == null) return body();
 
     try {
diff --git a/pkgs/test_api/lib/src/backend/runtime.dart b/pkgs/test_api/lib/src/backend/runtime.dart
index 380be9c..8496f0c 100644
--- a/pkgs/test_api/lib/src/backend/runtime.dart
+++ b/pkgs/test_api/lib/src/backend/runtime.dart
@@ -52,7 +52,7 @@
 
   /// The parent platform that this is based on, or `null` if there is no
   /// parent.
-  final Runtime parent;
+  final Runtime? parent;
 
   /// Returns whether this is a child of another platform.
   bool get isChild => parent != null;
@@ -86,12 +86,13 @@
       this.isHeadless = false})
       : parent = null;
 
-  Runtime._child(this.name, this.identifier, this.parent)
+  Runtime._child(this.name, this.identifier, Runtime parent)
       : isDartVM = parent.isDartVM,
         isBrowser = parent.isBrowser,
         isJS = parent.isJS,
         isBlink = parent.isBlink,
-        isHeadless = parent.isHeadless;
+        isHeadless = parent.isHeadless,
+        parent = parent;
 
   /// Converts a JSON-safe representation generated by [serialize] back into a
   /// [Runtime].
@@ -109,7 +110,7 @@
       // since we only deserialize platforms in the remote execution context
       // where they're only used to evaluate platform selectors.
       return Runtime._child(map['name'] as String, map['identifier'] as String,
-          Runtime.deserialize(parent));
+          Runtime.deserialize(parent as Object));
     }
 
     return Runtime(map['name'] as String, map['identifier'] as String,
@@ -129,7 +130,7 @@
       return {
         'name': name,
         'identifier': identifier,
-        'parent': parent.serialize()
+        'parent': parent!.serialize()
       };
     }
 
diff --git a/pkgs/test_api/lib/src/backend/stack_trace_formatter.dart b/pkgs/test_api/lib/src/backend/stack_trace_formatter.dart
index 9d9dfeb..8d621f5 100644
--- a/pkgs/test_api/lib/src/backend/stack_trace_formatter.dart
+++ b/pkgs/test_api/lib/src/backend/stack_trace_formatter.dart
@@ -20,7 +20,7 @@
 class StackTraceFormatter {
   /// A class that converts [trace] into a Dart stack trace, or `null` to use it
   /// as-is.
-  StackTraceMapper _mapper;
+  StackTraceMapper? _mapper;
 
   /// The set of packages to fold when producing terse [Chain]s.
   var _except = {'test', 'stream_channel', 'test_api'};
@@ -31,8 +31,8 @@
 
   /// Returns the current manager, or `null` if this isn't called within a call
   /// to [asCurrent].
-  static StackTraceFormatter get current =>
-      Zone.current[_currentKey] as StackTraceFormatter;
+  static StackTraceFormatter? get current =>
+      Zone.current[_currentKey] as StackTraceFormatter?;
 
   /// Runs [body] with this as [StackTraceFormatter.current].
   ///
@@ -48,7 +48,7 @@
   /// [only] is non-empty, it indicates packages whose frames should *not* be
   /// folded away.
   void configure(
-      {StackTraceMapper mapper, Set<String> except, Set<String> only}) {
+      {StackTraceMapper? mapper, Set<String>? except, Set<String>? only}) {
     if (mapper != null) _mapper = mapper;
     if (except != null) _except = except;
     if (only != null) _only = only;
@@ -60,9 +60,8 @@
   /// If [verbose] is `true`, this doesn't fold out irrelevant stack frames. It
   /// defaults to the current test's [Metadata.verboseTrace] configuration, or
   /// `false` if there is no current test.
-  Chain formatStackTrace(StackTrace stackTrace, {bool verbose}) {
-    verbose ??=
-        Invoker.current?.liveTest?.test?.metadata?.verboseTrace ?? false;
+  Chain formatStackTrace(StackTrace stackTrace, {bool? verbose}) {
+    verbose ??= Invoker.current?.liveTest.test.metadata.verboseTrace ?? false;
 
     var chain =
         Chain.forTrace(_mapper?.mapStackTrace(stackTrace) ?? stackTrace);
diff --git a/pkgs/test_api/lib/src/backend/suite.dart b/pkgs/test_api/lib/src/backend/suite.dart
index 695d305..9321352 100644
--- a/pkgs/test_api/lib/src/backend/suite.dart
+++ b/pkgs/test_api/lib/src/backend/suite.dart
@@ -16,7 +16,7 @@
   final SuitePlatform platform;
 
   /// The path to the Dart test suite, or `null` if that path is unknown.
-  final String path;
+  final String? path;
 
   /// The metadata associated with this test suite.
   ///
diff --git a/pkgs/test_api/lib/src/backend/suite_platform.dart b/pkgs/test_api/lib/src/backend/suite_platform.dart
index 5f80997..bcdb6a5 100644
--- a/pkgs/test_api/lib/src/backend/suite_platform.dart
+++ b/pkgs/test_api/lib/src/backend/suite_platform.dart
@@ -24,9 +24,9 @@
   ///
   /// Throws an [ArgumentError] if [runtime] is a browser and [os] is not
   /// `null` or [OperatingSystem.none].
-  SuitePlatform(this.runtime, {OperatingSystem os, this.inGoogle = false})
-      : os = os ?? OperatingSystem.none {
-    if (runtime.isBrowser && this.os != OperatingSystem.none) {
+  SuitePlatform(this.runtime,
+      {this.os = OperatingSystem.none, this.inGoogle = false}) {
+    if (runtime.isBrowser && os != OperatingSystem.none) {
       throw ArgumentError('No OS should be passed for runtime "$runtime".');
     }
   }
@@ -35,7 +35,7 @@
   /// [SuitePlatform].
   factory SuitePlatform.deserialize(Object serialized) {
     var map = serialized as Map;
-    return SuitePlatform(Runtime.deserialize(map['runtime']),
+    return SuitePlatform(Runtime.deserialize(map['runtime'] as Object),
         os: OperatingSystem.find(map['os'] as String),
         inGoogle: map['inGoogle'] as bool);
   }
diff --git a/pkgs/test_api/lib/src/backend/test.dart b/pkgs/test_api/lib/src/backend/test.dart
index 870f5e1..5b5e73a 100644
--- a/pkgs/test_api/lib/src/backend/test.dart
+++ b/pkgs/test_api/lib/src/backend/test.dart
@@ -24,7 +24,7 @@
   Metadata get metadata;
 
   @override
-  Trace get trace;
+  Trace? get trace;
 
   /// Loads a live version of this test, which can be used to run it a single
   /// time.
@@ -32,11 +32,11 @@
   /// [suite] is the suite within which this test is being run. If [groups] is
   /// passed, it's the list of groups containing this test; otherwise, it
   /// defaults to just containing `suite.group`.
-  LiveTest load(Suite suite, {Iterable<Group> groups});
+  LiveTest load(Suite suite, {Iterable<Group>? groups});
 
   @override
-  Test forPlatform(SuitePlatform platform);
+  Test? forPlatform(SuitePlatform platform);
 
   @override
-  Test filter(bool Function(Test) callback) => callback(this) ? this : null;
+  Test? filter(bool Function(Test) callback) => callback(this) ? this : null;
 }
diff --git a/pkgs/test_api/lib/src/frontend/async_matcher.dart b/pkgs/test_api/lib/src/frontend/async_matcher.dart
index e956aec..880901d 100644
--- a/pkgs/test_api/lib/src/frontend/async_matcher.dart
+++ b/pkgs/test_api/lib/src/frontend/async_matcher.dart
@@ -37,12 +37,12 @@
         reason: 'matchAsync() may only return a String, a Future, or null.');
 
     if (result is Future) {
-      Invoker.current.addOutstandingCallback();
+      Invoker.current!.addOutstandingCallback();
       result.then((realResult) {
         if (realResult != null) {
           fail(formatFailure(this, item, realResult as String));
         }
-        Invoker.current.removeOutstandingCallback();
+        Invoker.current!.removeOutstandingCallback();
       });
     } else if (result is String) {
       matchState[this] = result;
diff --git a/pkgs/test_api/lib/src/frontend/expect.dart b/pkgs/test_api/lib/src/frontend/expect.dart
index b69dc2f..cf3d927 100644
--- a/pkgs/test_api/lib/src/frontend/expect.dart
+++ b/pkgs/test_api/lib/src/frontend/expect.dart
@@ -5,7 +5,6 @@
 import 'dart:async';
 
 import 'package:matcher/matcher.dart';
-import 'package:meta/meta.dart';
 
 import '../backend/closed_exception.dart';
 import '../backend/invoker.dart';
@@ -26,7 +25,7 @@
 /// upon failures in [expect].
 @Deprecated('Will be removed in 0.13.0.')
 typedef ErrorFormatter = String Function(dynamic actual, Matcher matcher,
-    String reason, Map matchState, bool verbose);
+    String? reason, Map matchState, bool verbose);
 
 /// Assert that [actual] matches [matcher].
 ///
@@ -53,10 +52,10 @@
 /// you want to wait for the matcher to complete before continuing the test, you
 /// can call [expectLater] instead and `await` the result.
 void expect(actual, matcher,
-    {String reason,
+    {String? reason,
     skip,
     @Deprecated('Will be removed in 0.13.0.') bool verbose = false,
-    @Deprecated('Will be removed in 0.13.0.') ErrorFormatter formatter}) {
+    @Deprecated('Will be removed in 0.13.0.') ErrorFormatter? formatter}) {
   _expect(actual, matcher,
       reason: reason, skip: skip, verbose: verbose, formatter: formatter);
 }
@@ -72,12 +71,15 @@
 ///
 /// If the matcher fails asynchronously, that failure is piped to the returned
 /// future where it can be handled by user code.
-Future expectLater(actual, matcher, {String reason, skip}) =>
+Future expectLater(actual, matcher, {String? reason, skip}) =>
     _expect(actual, matcher, reason: reason, skip: skip);
 
 /// The implementation of [expect] and [expectLater].
+///
+// TODO: why is this necessary? Is @alwaysThrows not working in NNBD?
+// ignore: body_might_complete_normally
 Future _expect(actual, matcher,
-    {String reason, skip, bool verbose = false, ErrorFormatter formatter}) {
+    {String? reason, skip, bool verbose = false, ErrorFormatter? formatter}) {
   formatter ??= (actual, matcher, reason, matchState, verbose) {
     var mismatchDescription = StringDescription();
     matcher.describeMismatch(actual, mismatchDescription, matchState, verbose);
@@ -90,7 +92,7 @@
     throw StateError('expect() may only be called within a test.');
   }
 
-  if (Invoker.current.closed) throw ClosedException();
+  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 +110,7 @@
       message = 'Skip expect ($description).';
     }
 
-    Invoker.current.skip(message);
+    Invoker.current!.skip(message);
     return Future.sync(() {});
   }
 
@@ -120,9 +122,9 @@
         reason: 'matchAsync() may only return a String, a Future, or null.');
 
     if (result is String) {
-      fail(formatFailure(matcher as Matcher, actual, result, reason: reason));
+      fail(formatFailure(matcher, actual, result, reason: reason));
     } else if (result is Future) {
-      Invoker.current.addOutstandingCallback();
+      Invoker.current!.addOutstandingCallback();
       return result.then((realResult) {
         if (realResult == null) return;
         fail(formatFailure(matcher as Matcher, actual, realResult as String,
@@ -130,7 +132,7 @@
       }).whenComplete(() {
         // Always remove this, in case the failure is caught and handled
         // gracefully.
-        Invoker.current.removeOutstandingCallback();
+        Invoker.current!.removeOutstandingCallback();
       });
     }
 
@@ -150,12 +152,11 @@
 
 /// Convenience method for throwing a new [TestFailure] with the provided
 /// [message].
-@alwaysThrows
-Null fail(String message) => throw TestFailure(message);
+Never fail(String message) => throw TestFailure(message);
 
 // The default error formatter.
 @Deprecated('Will be removed in 0.13.0.')
-String formatFailure(Matcher expected, actual, String which, {String reason}) {
+String formatFailure(Matcher expected, actual, String which, {String? reason}) {
   var buffer = StringBuffer();
   buffer.writeln(indent(prettyPrint(expected), first: 'Expected: '));
   buffer.writeln(indent(prettyPrint(actual), first: '  Actual: '));
diff --git a/pkgs/test_api/lib/src/frontend/expect_async.dart b/pkgs/test_api/lib/src/frontend/expect_async.dart
index 10abf03..17f0e54 100644
--- a/pkgs/test_api/lib/src/frontend/expect_async.dart
+++ b/pkgs/test_api/lib/src/frontend/expect_async.dart
@@ -48,7 +48,7 @@
   ///
   /// This may be `null`. If so, the function is considered to be done after
   /// it's been run once.
-  final bool Function() _isDone;
+  final bool Function()? _isDone;
 
   /// A descriptive name for the function.
   final String _id;
@@ -62,13 +62,13 @@
   int _actualCalls = 0;
 
   /// The test invoker in which this function was wrapped.
-  Invoker get _invoker => _zone[#test.invoker] as Invoker;
+  Invoker? get _invoker => _zone[#test.invoker] as Invoker?;
 
   /// The zone in which this function was wrapped.
   final Zone _zone;
 
   /// Whether this function has been called the requisite number of times.
-  bool _complete;
+  late bool _complete;
 
   /// Wraps [callback] in a function that asserts that it's called at least
   /// [minExpected] times and no more than [maxExpected] times.
@@ -77,7 +77,7 @@
   /// as a reason it's expected to be called. If [isDone] is passed, the test
   /// won't be allowed to complete until it returns `true`.
   _ExpectedFunction(Function callback, int minExpected, int maxExpected,
-      {String id, String reason, bool Function() isDone})
+      {String? id, String? reason, bool Function()? isDone})
       : _callback = callback,
         _minExpectedCalls = minExpected,
         _maxExpectedCalls =
@@ -94,7 +94,7 @@
     }
 
     if (isDone != null || minExpected > 0) {
-      _invoker.addOutstandingCallback();
+      _invoker!.addOutstandingCallback();
       _complete = false;
     } else {
       _complete = true;
@@ -105,7 +105,7 @@
   ///
   /// If [id] is passed, uses that. Otherwise, tries to determine a name from
   /// calling `toString`. If no name can be found, returns the empty string.
-  static String _makeCallbackId(String id, Function callback) {
+  static String _makeCallbackId(String? id, Function callback) {
     if (id != null) return '$id ';
 
     // If the callback is not an anonymous closure, try to get the
@@ -132,7 +132,7 @@
     if (_callback is Function(Null)) return max1;
     if (_callback is Function()) return max0;
 
-    _invoker.removeOutstandingCallback();
+    _invoker!.removeOutstandingCallback();
     throw ArgumentError(
         'The wrapped function has more than 6 required arguments');
   }
@@ -141,38 +141,38 @@
   // argument count of zero.
   T max0() => max6();
 
-  T max1([Object a0 = placeholder]) => max6(a0);
+  T max1([Object? a0 = placeholder]) => max6(a0);
 
-  T max2([Object a0 = placeholder, Object a1 = placeholder]) => max6(a0, a1);
+  T max2([Object? a0 = placeholder, Object? a1 = placeholder]) => max6(a0, a1);
 
   T max3(
-          [Object a0 = placeholder,
-          Object a1 = placeholder,
-          Object a2 = placeholder]) =>
+          [Object? a0 = placeholder,
+          Object? a1 = placeholder,
+          Object? a2 = placeholder]) =>
       max6(a0, a1, a2);
 
   T max4(
-          [Object a0 = placeholder,
-          Object a1 = placeholder,
-          Object a2 = placeholder,
-          Object a3 = placeholder]) =>
+          [Object? a0 = placeholder,
+          Object? a1 = placeholder,
+          Object? a2 = placeholder,
+          Object? a3 = placeholder]) =>
       max6(a0, a1, a2, a3);
 
   T max5(
-          [Object a0 = placeholder,
-          Object a1 = placeholder,
-          Object a2 = placeholder,
-          Object a3 = placeholder,
-          Object a4 = placeholder]) =>
+          [Object? a0 = placeholder,
+          Object? a1 = placeholder,
+          Object? a2 = placeholder,
+          Object? a3 = placeholder,
+          Object? a4 = placeholder]) =>
       max6(a0, a1, a2, a3, a4);
 
   T max6(
-          [Object a0 = placeholder,
-          Object a1 = placeholder,
-          Object a2 = placeholder,
-          Object a3 = placeholder,
-          Object a4 = placeholder,
-          Object a5 = placeholder]) =>
+          [Object? a0 = placeholder,
+          Object? a1 = placeholder,
+          Object? a2 = placeholder,
+          Object? a3 = placeholder,
+          Object? a4 = placeholder,
+          Object? a5 = placeholder]) =>
       _run([a0, a1, a2, a3, a4, a5].where((a) => a != placeholder));
 
   /// Runs the wrapped function with [args] and returns its return value.
@@ -182,9 +182,9 @@
     // pass it to the invoker anyway.
     try {
       _actualCalls++;
-      if (_invoker.liveTest.state.shouldBeDone) {
+      if (_invoker!.liveTest.state.shouldBeDone) {
         throw 'Callback ${_id}called ($_actualCalls) after test case '
-            '${_invoker.liveTest.test.name} had already completed.$_reason';
+            '${_invoker!.liveTest.test.name} had already completed.$_reason';
       } else if (_maxExpectedCalls >= 0 && _actualCalls > _maxExpectedCalls) {
         throw TestFailure('Callback ${_id}called more times than expected '
             '($_maxExpectedCalls).$_reason');
@@ -200,12 +200,12 @@
   void _afterRun() {
     if (_complete) return;
     if (_minExpectedCalls > 0 && _actualCalls < _minExpectedCalls) return;
-    if (_isDone != null && !_isDone()) return;
+    if (_isDone != null && !_isDone!()) return;
 
     // 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();
+    _invoker!.removeOutstandingCallback();
   }
 }
 
@@ -215,7 +215,7 @@
 /// [expectAsync6] instead.
 @Deprecated('Will be removed in 0.13.0')
 Function expectAsync(Function callback,
-    {int count = 1, int max = 0, String id, String reason}) {
+    {int count = 1, int max = 0, String? id, String? reason}) {
   if (Invoker.current == null) {
     throw StateError('expectAsync() may only be called within a test.');
   }
@@ -245,7 +245,7 @@
 /// [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}) {
+    {int count = 1, int max = 0, String? id, String? reason}) {
   if (Invoker.current == null) {
     throw StateError('expectAsync0() may only be called within a test.');
   }
@@ -276,7 +276,7 @@
 /// [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}) {
+    {int count = 1, int max = 0, String? id, String? reason}) {
   if (Invoker.current == null) {
     throw StateError('expectAsync1() may only be called within a test.');
   }
@@ -307,7 +307,7 @@
 /// [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}) {
+    {int count = 1, int max = 0, String? id, String? reason}) {
   if (Invoker.current == null) {
     throw StateError('expectAsync2() may only be called within a test.');
   }
@@ -338,7 +338,7 @@
 /// [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}) {
+    {int count = 1, int max = 0, String? id, String? reason}) {
   if (Invoker.current == null) {
     throw StateError('expectAsync3() may only be called within a test.');
   }
@@ -372,8 +372,8 @@
     T Function(A, B, C, D) callback,
     {int count = 1,
     int max = 0,
-    String id,
-    String reason}) {
+    String? id,
+    String? reason}) {
   if (Invoker.current == null) {
     throw StateError('expectAsync4() may only be called within a test.');
   }
@@ -407,8 +407,8 @@
     T Function(A, B, C, D, E) callback,
     {int count = 1,
     int max = 0,
-    String id,
-    String reason}) {
+    String? id,
+    String? reason}) {
   if (Invoker.current == null) {
     throw StateError('expectAsync5() may only be called within a test.');
   }
@@ -442,8 +442,8 @@
     T Function(A, B, C, D, E, F) callback,
     {int count = 1,
     int max = 0,
-    String id,
-    String reason}) {
+    String? id,
+    String? reason}) {
   if (Invoker.current == null) {
     throw StateError('expectAsync6() may only be called within a test.');
   }
@@ -458,7 +458,7 @@
 /// [expectAsyncUntil5], or [expectAsyncUntil6] instead.
 @Deprecated('Will be removed in 0.13.0')
 Function expectAsyncUntil(Function callback, bool Function() isDone,
-    {String id, String reason}) {
+    {String? id, String? reason}) {
   if (Invoker.current == null) {
     throw StateError('expectAsyncUntil() may only be called within a test.');
   }
@@ -486,7 +486,7 @@
 /// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for
 /// callbacks with different arity.
 Func0<T> expectAsyncUntil0<T>(T Function() callback, bool Function() isDone,
-    {String id, String reason}) {
+    {String? id, String? reason}) {
   if (Invoker.current == null) {
     throw StateError('expectAsyncUntil0() may only be called within a test.');
   }
@@ -515,7 +515,7 @@
 /// callbacks with different arity.
 Func1<T, A> expectAsyncUntil1<T, A>(
     T Function(A) callback, bool Function() isDone,
-    {String id, String reason}) {
+    {String? id, String? reason}) {
   if (Invoker.current == null) {
     throw StateError('expectAsyncUntil1() may only be called within a test.');
   }
@@ -544,7 +544,7 @@
 /// callbacks with different arity.
 Func2<T, A, B> expectAsyncUntil2<T, A, B>(
     T Function(A, B) callback, bool Function() isDone,
-    {String id, String reason}) {
+    {String? id, String? reason}) {
   if (Invoker.current == null) {
     throw StateError('expectAsyncUntil2() may only be called within a test.');
   }
@@ -573,7 +573,7 @@
 /// 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}) {
+    {String? id, String? reason}) {
   if (Invoker.current == null) {
     throw StateError('expectAsyncUntil3() may only be called within a test.');
   }
@@ -602,7 +602,7 @@
 /// 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}) {
+    {String? id, String? reason}) {
   if (Invoker.current == null) {
     throw StateError('expectAsyncUntil4() may only be called within a test.');
   }
@@ -631,7 +631,7 @@
 /// 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}) {
+    {String? id, String? reason}) {
   if (Invoker.current == null) {
     throw StateError('expectAsyncUntil5() may only be called within a test.');
   }
@@ -660,7 +660,7 @@
 /// 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}) {
+    {String? id, String? reason}) {
   if (Invoker.current == null) {
     throw StateError('expectAsyncUntil() may only be called within a test.');
   }
diff --git a/pkgs/test_api/lib/src/frontend/fake.dart b/pkgs/test_api/lib/src/frontend/fake.dart
new file mode 100644
index 0000000..27e8c6d
--- /dev/null
+++ b/pkgs/test_api/lib/src/frontend/fake.dart
@@ -0,0 +1,52 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// A stand-in for another object which cannot be used except for specifically
+/// overridden methods.
+///
+/// A fake has a default behavior for every field and method of throwing
+/// [UnimplementedError]. Fields and methods that are excersized by the code
+/// under test should be manually overridden in the implementing class.
+///
+/// A fake does not have any support for verification or defining behavior from
+/// the test, it cannot be used as a [Mock].
+///
+/// In most cases a shared full fake implementation without a `noSuchMethod` is
+/// preferable to `extends Fake`, however `extends Fake` is preferred against
+/// `extends Mock` mixed with manual `@override` implementations.
+///
+/// __Example use__:
+///
+///     // Real class.
+///     class Cat {
+///       String meow(String suffix) => 'Meow$suffix';
+///       String hiss(String suffix) => 'Hiss$suffix';
+///     }
+///
+///     // Fake class.
+///     class FakeCat extends Fake implements Cat {
+///       @override
+///       String meow(String suffix) => 'FakeMeow$suffix';
+///     }
+///
+///     void main() {
+///       // Create a new fake Cat at runtime.
+///       var cat = new FakeCat();
+///
+///       // Try making a Cat sound...
+///       print(cat.meow('foo')); // Prints 'FakeMeowfoo'
+///       print(cat.hiss('foo')); // Throws
+///     }
+///
+/// **WARNING**: [Fake] uses [noSuchMethod](goo.gl/r3IQUH), which is a _form_ of
+/// runtime reflection, and causes sub-standard code to be generated. As such,
+/// [Fake] should strictly _not_ be used in any production code, especially if
+/// used within the context of Dart for Web (dart2js, DDC) and Dart for Mobile
+/// (Flutter).
+abstract class Fake {
+  @override
+  dynamic noSuchMethod(Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString().split('"')[1]);
+  }
+}
diff --git a/pkgs/test_api/lib/src/frontend/format_stack_trace.dart b/pkgs/test_api/lib/src/frontend/format_stack_trace.dart
index a3cd356..b6eee2b 100644
--- a/pkgs/test_api/lib/src/frontend/format_stack_trace.dart
+++ b/pkgs/test_api/lib/src/frontend/format_stack_trace.dart
@@ -17,6 +17,6 @@
 ///
 /// 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}) =>
+Chain formatStackTrace(StackTrace stackTrace, {bool? verbose}) =>
     (StackTraceFormatter.current ?? _defaultFormatter)
         .formatStackTrace(stackTrace, verbose: verbose);
diff --git a/pkgs/test_api/lib/src/frontend/future_matchers.dart b/pkgs/test_api/lib/src/frontend/future_matchers.dart
index c456c30..efd9e76 100644
--- a/pkgs/test_api/lib/src/frontend/future_matchers.dart
+++ b/pkgs/test_api/lib/src/frontend/future_matchers.dart
@@ -34,11 +34,11 @@
 ///
 /// To test that a Future completes with an exception, you can use [throws] and
 /// [throwsA].
-Matcher completion(matcher, [@deprecated String description]) =>
+Matcher completion(matcher, [@deprecated String? description]) =>
     _Completes(wrapMatcher(matcher));
 
 class _Completes extends AsyncMatcher {
-  final Matcher _matcher;
+  final Matcher? _matcher;
 
   const _Completes(this._matcher);
 
@@ -50,14 +50,14 @@
     return item.then((value) async {
       if (_matcher == null) return null;
 
-      String result;
+      String? result;
       if (_matcher is AsyncMatcher) {
-        result = await (_matcher as AsyncMatcher).matchAsync(value) as String;
+        result = await (_matcher as AsyncMatcher).matchAsync(value) as String?;
         if (result == null) return null;
       } else {
         var matchState = {};
-        if (_matcher.matches(value, matchState)) return null;
-        result = _matcher
+        if (_matcher!.matches(value, matchState)) return null;
+        result = _matcher!
             .describeMismatch(value, StringDescription(), matchState, false)
             .toString();
       }
diff --git a/pkgs/test_api/lib/src/frontend/never_called.dart b/pkgs/test_api/lib/src/frontend/never_called.dart
index 0ea5351..cd6f004 100644
--- a/pkgs/test_api/lib/src/frontend/never_called.dart
+++ b/pkgs/test_api/lib/src/frontend/never_called.dart
@@ -25,16 +25,16 @@
 /// This also ensures that the test doesn't complete until a call to
 /// [pumpEventQueue] finishes, so that the callback has a chance to be called.
 Null Function(
-    [Object,
-    Object,
-    Object,
-    Object,
-    Object,
-    Object,
-    Object,
-    Object,
-    Object,
-    Object]) get neverCalled {
+    [Object?,
+    Object?,
+    Object?,
+    Object?,
+    Object?,
+    Object?,
+    Object?,
+    Object?,
+    Object?,
+    Object?]) get neverCalled {
   // Make sure the test stays alive long enough to call the function if it's
   // going to.
   expect(pumpEventQueue(), completes);
diff --git a/pkgs/test_api/lib/src/frontend/on_platform.dart b/pkgs/test_api/lib/src/frontend/on_platform.dart
index c3620a4..d88848d 100644
--- a/pkgs/test_api/lib/src/frontend/on_platform.dart
+++ b/pkgs/test_api/lib/src/frontend/on_platform.dart
@@ -2,11 +2,14 @@
 // 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:meta/meta_meta.dart';
+
 /// An annotation for platform-specific customizations for a test suite.
 ///
 /// See [the README][onPlatform].
 ///
 /// [onPlatform]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-specific-configuration
+@Target({TargetKind.library})
 class OnPlatform {
   final Map<String, dynamic> annotationsByPlatform;
 
diff --git a/pkgs/test_api/lib/src/frontend/prints_matcher.dart b/pkgs/test_api/lib/src/frontend/prints_matcher.dart
index 48cf8a9..d6426a4 100644
--- a/pkgs/test_api/lib/src/frontend/prints_matcher.dart
+++ b/pkgs/test_api/lib/src/frontend/prints_matcher.dart
@@ -35,7 +35,7 @@
     if (item is! Function()) return 'was not a unary Function';
 
     var buffer = StringBuffer();
-    var result = runZoned(item as Function(),
+    var result = runZoned(item,
         zoneSpecification: ZoneSpecification(print: (_, __, ____, line) {
       buffer.writeln(line);
     }));
@@ -51,7 +51,7 @@
 
   /// Verifies that [actual] matches [_matcher] and returns a [String]
   /// description of the failure if it doesn't.
-  String _check(String actual) {
+  String? _check(String actual) {
     var matchState = {};
     if (_matcher.matches(actual, matchState)) return null;
 
diff --git a/pkgs/test_api/lib/src/frontend/retry.dart b/pkgs/test_api/lib/src/frontend/retry.dart
index 0c57f67..681fcce 100644
--- a/pkgs/test_api/lib/src/frontend/retry.dart
+++ b/pkgs/test_api/lib/src/frontend/retry.dart
@@ -2,10 +2,13 @@
 // 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.
 
-/// An annotation for marking a test to be retried.
+import 'package:meta/meta_meta.dart';
+
+/// An annotation for marking a test suite to be retried.
 ///
 /// A test with retries enabled will be re-run if it fails for a reason
 /// other than [TestFailure].
+@Target({TargetKind.library})
 class Retry {
   /// The number of times the test will be retried.
   final int count;
diff --git a/pkgs/test_api/lib/src/frontend/skip.dart b/pkgs/test_api/lib/src/frontend/skip.dart
index 787b224..8be4b73 100644
--- a/pkgs/test_api/lib/src/frontend/skip.dart
+++ b/pkgs/test_api/lib/src/frontend/skip.dart
@@ -2,10 +2,13 @@
 // 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:meta/meta_meta.dart';
+
 /// An annotation for marking a test suite as skipped.
+@Target({TargetKind.library})
 class Skip {
   /// The reason the test suite is skipped, or `null` if no reason is given.
-  final String reason;
+  final String? reason;
 
   /// Marks a suite as skipped.
   ///
diff --git a/pkgs/test_api/lib/src/frontend/spawn_hybrid.dart b/pkgs/test_api/lib/src/frontend/spawn_hybrid.dart
index cab7ff2..102298e 100644
--- a/pkgs/test_api/lib/src/frontend/spawn_hybrid.dart
+++ b/pkgs/test_api/lib/src/frontend/spawn_hybrid.dart
@@ -89,7 +89,7 @@
 ///
 /// **Note**: If you use this API, be sure to add a dependency on the
 /// **`stream_channel` package, since you're using its API as well!
-StreamChannel spawnHybridUri(uri, {Object message, bool stayAlive = false}) {
+StreamChannel spawnHybridUri(uri, {Object? message, bool stayAlive = false}) {
   if (uri is String) {
     // Ensure that it can be parsed as a uri.
     Uri.parse(uri);
@@ -140,16 +140,15 @@
 /// **Note**: If you use this API, be sure to add a dependency on the
 /// **`stream_channel` package, since you're using its API as well!
 StreamChannel spawnHybridCode(String dartCode,
-    {Object message, bool stayAlive = false}) {
+    {Object? message, bool stayAlive = false}) {
   var uri = Uri.dataFromString(dartCode,
       encoding: utf8, mimeType: 'application/dart');
   return _spawn(uri.toString(), message, stayAlive: stayAlive);
 }
 
-/// Like [spawnHybridUri], but doesn't take [Uri] objects and doesn't handle
-/// relative URLs.
-StreamChannel _spawn(String uri, Object message, {bool stayAlive = false}) {
-  var channel = Zone.current[#test.runner.test_channel] as MultiChannel;
+/// Like [spawnHybridUri], but doesn't take [Uri] objects.
+StreamChannel _spawn(String uri, Object? message, {bool stayAlive = false}) {
+  var channel = Zone.current[#test.runner.test_channel] as MultiChannel?;
   if (channel == null) {
     throw UnsupportedError("Can't connect to the test runner.\n"
         'spawnHybridUri() is currently only supported within "pub run test".');
diff --git a/pkgs/test_api/lib/src/frontend/stream_matcher.dart b/pkgs/test_api/lib/src/frontend/stream_matcher.dart
index b1b96e9..57dcf56 100644
--- a/pkgs/test_api/lib/src/frontend/stream_matcher.dart
+++ b/pkgs/test_api/lib/src/frontend/stream_matcher.dart
@@ -84,9 +84,8 @@
   /// The [description] should be in the subjunctive mood. This means that it
   /// should be grammatically valid when used after the word "should". For
   /// example, it might be "emit the right events".
-  factory StreamMatcher(
-          Future<String> Function(StreamQueue) matchQueue, String description) =
-      _StreamMatcher;
+  factory StreamMatcher(Future<String?> Function(StreamQueue) matchQueue,
+      String description) = _StreamMatcher;
 
   /// Tries to match events emitted by [queue].
   ///
@@ -104,7 +103,7 @@
   ///
   /// If the queue emits an error, that error is re-thrown unless otherwise
   /// indicated by the matcher.
-  Future<String> matchQueue(StreamQueue queue);
+  Future<String?> matchQueue(StreamQueue queue);
 }
 
 /// A concrete implementation of [StreamMatcher].
@@ -116,12 +115,12 @@
   final String description;
 
   /// The callback used to implement [matchQueue].
-  final Future<String> Function(StreamQueue) _matchQueue;
+  final Future<String?> Function(StreamQueue) _matchQueue;
 
   _StreamMatcher(this._matchQueue, this.description);
 
   @override
-  Future<String> matchQueue(StreamQueue queue) => _matchQueue(queue);
+  Future<String?> matchQueue(StreamQueue queue) => _matchQueue(queue);
 
   @override
   dynamic /*FutureOr<String>*/ matchAsync(item) {
@@ -151,9 +150,9 @@
       // Get a list of events emitted by the stream so we can emit them as part
       // of the error message.
       var replay = transaction.newQueue();
-      var events = <Result>[];
+      var events = <Result?>[];
       var subscription = Result.captureStreamTransformer
-          .bind(replay.rest)
+          .bind(replay.rest.cast())
           .listen(events.add, onDone: () => events.add(null));
 
       // Wait on a timer tick so all buffered events are emitted.
@@ -164,9 +163,9 @@
         if (event == null) {
           return 'x Stream closed.';
         } else if (event.isValue) {
-          return addBullet(event.asValue.value.toString());
+          return addBullet(event.asValue!.value.toString());
         } else {
-          var error = event.asError;
+          var error = event.asError!;
           var chain = formatStackTrace(error.stackTrace);
           var text = '${error.error}\n$chain';
           return prefixLines(text, '  ', first: '! ');
@@ -180,7 +179,7 @@
       buffer.writeln(indent(eventsString, first: 'emitted '));
       if (result.isNotEmpty) buffer.writeln(indent(result, first: '  which '));
       return buffer.toString().trimRight();
-    }, onError: (error) {
+    }, onError: (Object error) {
       transaction.reject();
       throw error;
     }).then((result) {
diff --git a/pkgs/test_api/lib/src/frontend/stream_matchers.dart b/pkgs/test_api/lib/src/frontend/stream_matchers.dart
index 53f7c8c..e60bc58 100644
--- a/pkgs/test_api/lib/src/frontend/stream_matchers.dart
+++ b/pkgs/test_api/lib/src/frontend/stream_matchers.dart
@@ -56,7 +56,7 @@
   var throwsMatcher = throwsA(wrapped) as AsyncMatcher;
 
   return StreamMatcher(
-      (queue) => throwsMatcher.matchAsync(queue.next) as Future<String>,
+      (queue) => throwsMatcher.matchAsync(queue.next) as Future<String?>,
       // TODO(nweiz): add "should" once matcher#42 is fixed.
       'emit an error that $matcherDescription');
 }
@@ -100,20 +100,20 @@
     // Allocate the failures list ahead of time so that its order matches the
     // order of [matchers], and thus the order the matchers will be listed in
     // the description.
-    var failures = List<String>(matchers.length);
+    var failures = List<String?>.filled(matchers.length, null);
 
     // The first error thrown. If no matchers match and this exists, we rethrow
     // it.
-    Object firstError;
-    StackTrace firstStackTrace;
+    Object? firstError;
+    StackTrace? firstStackTrace;
 
     var futures = <Future>[];
-    StreamQueue consumedMost;
+    StreamQueue? consumedMost;
     for (var i = 0; i < matchers.length; i++) {
       futures.add(() async {
         var copy = transaction.newQueue();
 
-        String result;
+        String? result;
         try {
           result = await streamMatchers[i].matchQueue(copy);
         } catch (error, stackTrace) {
@@ -127,7 +127,7 @@
         if (result != null) {
           failures[i] = result;
         } else if (consumedMost == null ||
-            consumedMost.eventsDispatched < copy.eventsDispatched) {
+            consumedMost!.eventsDispatched < copy.eventsDispatched) {
           consumedMost = copy;
         }
       }());
@@ -138,13 +138,13 @@
     if (consumedMost == null) {
       transaction.reject();
       if (firstError != null) {
-        await Future.error(firstError, firstStackTrace);
+        await Future.error(firstError!, firstStackTrace);
       }
 
       var failureMessages = <String>[];
       for (var i = 0; i < matchers.length; i++) {
         var message = 'failed to ${streamMatchers[i].description}';
-        if (failures[i].isNotEmpty) {
+        if ((failures[i])!.isNotEmpty) {
           message += message.contains('\n') ? '\n' : ' ';
           message += 'because it ${failures[i]}';
         }
@@ -154,7 +154,7 @@
 
       return 'failed all options:\n${bullet(failureMessages)}';
     } else {
-      transaction.commit(consumedMost);
+      transaction.commit(consumedMost!);
       return null;
     }
   }, description);
@@ -328,12 +328,12 @@
   }
 
   var transaction = queue.startTransaction();
-  StreamQueue consumedMost;
+  StreamQueue? consumedMost;
 
   // The first error thrown. If no matchers match and this exists, we rethrow
   // it.
-  Object firstError;
-  StackTrace firstStackTrace;
+  Object? firstError;
+  StackTrace? firstStackTrace;
 
   await Future.wait(matchers.map((matcher) async {
     var copy = transaction.newQueue();
@@ -361,17 +361,17 @@
     }
 
     if (consumedMost == null ||
-        consumedMost.eventsDispatched < copy.eventsDispatched) {
+        consumedMost!.eventsDispatched < copy.eventsDispatched) {
       consumedMost = copy;
     }
   }));
 
   if (consumedMost == null) {
     transaction.reject();
-    if (firstError != null) await Future.error(firstError, firstStackTrace);
+    if (firstError != null) await Future.error(firstError!, firstStackTrace);
     return false;
   } else {
-    transaction.commit(consumedMost);
+    transaction.commit(consumedMost!);
     return true;
   }
 }
diff --git a/pkgs/test_api/lib/src/frontend/tags.dart b/pkgs/test_api/lib/src/frontend/tags.dart
index 8a7d4db..6ad5a22 100644
--- a/pkgs/test_api/lib/src/frontend/tags.dart
+++ b/pkgs/test_api/lib/src/frontend/tags.dart
@@ -2,11 +2,14 @@
 // 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:meta/meta_meta.dart';
+
 /// An annotation for applying a set of user-defined tags to a test suite.
 ///
 /// See [the documentation on tagging tests][tagging tests].
 ///
 /// [tagging tests]: https://github.com/dart-lang/test/blob/master/README.md#tagging-tests
+@Target({TargetKind.library})
 class Tags {
   /// The tags for the test suite.
   Set<String> get tags => _tags.toSet();
diff --git a/pkgs/test_api/lib/src/frontend/throws_matcher.dart b/pkgs/test_api/lib/src/frontend/throws_matcher.dart
index cdf9c6e..ff164d0 100644
--- a/pkgs/test_api/lib/src/frontend/throws_matcher.dart
+++ b/pkgs/test_api/lib/src/frontend/throws_matcher.dart
@@ -2,8 +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 'dart:async';
-
 import 'package:matcher/matcher.dart';
 
 import '../utils.dart';
@@ -40,9 +38,9 @@
 /// Use the [throwsA] function instead.
 @Deprecated('Will be removed in 0.13.0')
 class Throws extends AsyncMatcher {
-  final Matcher _matcher;
+  final Matcher? _matcher;
 
-  const Throws([Matcher matcher]) : _matcher = matcher;
+  const Throws([Matcher? matcher]) : _matcher = matcher;
 
   // Avoid async/await so we synchronously fail if we match a synchronous
   // function.
@@ -53,17 +51,13 @@
     }
 
     if (item is Future) {
-      return item.then((value) => indent(prettyPrint(value), first: 'emitted '),
-          onError: _check);
+      return _matchFuture(item, 'emitted ');
     }
 
     try {
       var value = item();
       if (value is Future) {
-        return value.then(
-            (value) => indent(prettyPrint(value),
-                first: 'returned a Future that emitted '),
-            onError: _check);
+        return _matchFuture(value, 'returned a Future that emitted ');
       }
 
       return indent(prettyPrint(value), first: 'returned ');
@@ -72,6 +66,18 @@
     }
   }
 
+  /// Matches [future], using try/catch since `onError` doesn't seem to work
+  /// properly in nnbd.
+  Future<String?> _matchFuture(
+      Future<dynamic> future, String messagePrefix) async {
+    try {
+      var value = await future;
+      return indent(prettyPrint(value), first: messagePrefix);
+    } catch (error, trace) {
+      return _check(error, trace);
+    }
+  }
+
   @override
   Description describe(Description description) {
     if (_matcher == null) {
@@ -83,13 +89,13 @@
 
   /// Verifies that [error] matches [_matcher] and returns a [String]
   /// description of the failure if it doesn't.
-  String _check(error, StackTrace trace) {
+  String? _check(error, StackTrace? trace) {
     if (_matcher == null) return null;
 
     var matchState = {};
-    if (_matcher.matches(error, matchState)) return null;
+    if (_matcher!.matches(error, matchState)) return null;
 
-    var result = _matcher
+    var result = _matcher!
         .describeMismatch(error, StringDescription(), matchState, false)
         .toString();
 
diff --git a/pkgs/test_api/lib/src/frontend/timeout.dart b/pkgs/test_api/lib/src/frontend/timeout.dart
index 9c1e02e..075d986 100644
--- a/pkgs/test_api/lib/src/frontend/timeout.dart
+++ b/pkgs/test_api/lib/src/frontend/timeout.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 import 'package:string_scanner/string_scanner.dart';
+import 'package:meta/meta_meta.dart';
 
 /// A regular expression that matches text until a letter or whitespace.
 ///
@@ -22,6 +23,7 @@
 /// By default, a test will time out after 30 seconds. With [new Timeout], that
 /// can be overridden entirely; with [Timeout.factor], it can be scaled
 /// relative to the default.
+@Target({TargetKind.library})
 class Timeout {
   /// A constant indicating that a test should never time out.
   static const none = Timeout._none();
@@ -30,7 +32,7 @@
   ///
   /// If set, this overrides the default duration entirely. It's `null` for
   /// timeouts with a non-null [scaleFactor] and for [Timeout.none].
-  final Duration duration;
+  final Duration? duration;
 
   /// The timeout factor.
   ///
@@ -40,7 +42,7 @@
   ///
   /// This is `null` for timeouts with a non-null [duration] and for
   /// [Timeout.none].
-  final num scaleFactor;
+  final num? scaleFactor;
 
   /// Declares an absolute timeout that overrides the default.
   const Timeout(this.duration) : scaleFactor = null;
@@ -77,7 +79,7 @@
 
     // Scan a number. This will be either a time unit or a scale factor.
     scanner.expect(_untilUnit, name: 'number');
-    var number = double.parse(scanner.lastMatch[0]);
+    var number = double.parse((scanner.lastMatch![0])!);
 
     // A number followed by "x" is a scale factor.
     if (scanner.scan('x') || scanner.scan('X')) {
@@ -90,13 +92,13 @@
     var microseconds = 0.0;
     while (true) {
       scanner.expect(_unit, name: 'unit');
-      microseconds += _microsecondsFor(number, scanner.lastMatch[0]);
+      microseconds += _microsecondsFor(number, (scanner.lastMatch![0])!);
 
       scanner.scan(_whitespace);
 
       // Scan the next number, if it's avaialble.
       if (!scanner.scan(_untilUnit)) break;
-      number = double.parse(scanner.lastMatch[0]);
+      number = double.parse((scanner.lastMatch![0])!);
     }
 
     scanner.expectDone();
@@ -132,16 +134,16 @@
   Timeout merge(Timeout other) {
     if (this == none || other == none) return none;
     if (other.duration != null) return Timeout(other.duration);
-    if (duration != null) return Timeout(duration * other.scaleFactor);
-    return Timeout.factor(scaleFactor * other.scaleFactor);
+    if (duration != null) return Timeout(duration! * other.scaleFactor!);
+    return Timeout.factor(scaleFactor! * other.scaleFactor!);
   }
 
   /// Returns a new [Duration] from applying [this] to [base].
   ///
   /// If this is [none], returns `null`.
-  Duration apply(Duration base) {
+  Duration? apply(Duration base) {
     if (this == none) return null;
-    return duration ?? base * scaleFactor;
+    return duration ?? base * scaleFactor!;
   }
 
   @override
diff --git a/pkgs/test_api/lib/src/frontend/utils.dart b/pkgs/test_api/lib/src/frontend/utils.dart
index 17dc2bd..a2314c7 100644
--- a/pkgs/test_api/lib/src/frontend/utils.dart
+++ b/pkgs/test_api/lib/src/frontend/utils.dart
@@ -11,8 +11,7 @@
 ///
 /// Awaiting this approximates waiting until all asynchronous work (other than
 /// work that's waiting for external resources) completes.
-Future pumpEventQueue({int times}) {
-  times ??= 20;
+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/remote_listener.dart b/pkgs/test_api/lib/src/remote_listener.dart
index 785ad3a..7eab7fe 100644
--- a/pkgs/test_api/lib/src/remote_listener.dart
+++ b/pkgs/test_api/lib/src/remote_listener.dart
@@ -26,7 +26,7 @@
   final Suite _suite;
 
   /// The zone to forward prints to, or `null` if prints shouldn't be forwarded.
-  final Zone _printZone;
+  final Zone? _printZone;
 
   /// Extracts metadata about all the tests in the function returned by
   /// [getMain] and returns a channel that will send information about them.
@@ -44,14 +44,13 @@
   ///
   /// If [beforeLoad] is passed, it's called before the tests have been declared
   /// for this worker.
-  static StreamChannel start(Function Function() getMain,
-      {bool hidePrints = true, Future Function() beforeLoad}) {
-    // This has to be synchronous to work around sdk#25745. Otherwise, there'll
-    // be an asynchronous pause before a syntax error notification is sent,
-    // which will cause the send to fail entirely.
+  static StreamChannel<Object?> start(Function Function() getMain,
+      {bool hidePrints = true, Future Function()? beforeLoad}) {
+    // Synchronous in order to allow `print` output to show up immediately, even
+    // if they are followed by long running synchronous work.
     var controller =
-        StreamChannelController<Object>(allowForeignErrors: false, sync: true);
-    var channel = MultiChannel(controller.local);
+        StreamChannelController<Object?>(allowForeignErrors: false, sync: true);
+    var channel = MultiChannel<Object?>(controller.local);
 
     var verboseChain = true;
 
@@ -63,8 +62,8 @@
 
     SuiteChannelManager().asCurrent(() {
       StackTraceFormatter().asCurrent(() {
-        runZoned(() async {
-          dynamic main;
+        runZonedGuarded(() async {
+          Function? main;
           try {
             main = getMain();
           } on NoSuchMethodError catch (_) {
@@ -76,32 +75,28 @@
             return;
           }
 
-          if (main is! Function) {
-            _sendLoadException(
-                channel, 'Top-level main getter is not a function.');
-            return;
-          } else if (main is! Function()) {
+          if (main is! Function()) {
             _sendLoadException(
                 channel, 'Top-level main() function takes arguments.');
             return;
           }
 
           var queue = StreamQueue(channel.stream);
-          var message = await queue.next;
+          var message = await queue.next as Map;
           assert(message['type'] == 'initial');
 
-          queue.rest.listen((message) {
+          queue.rest.cast<Map>().listen((message) {
             if (message['type'] == 'close') {
               controller.local.sink.close();
               return;
             }
 
             assert(message['type'] == 'suiteChannel');
-            SuiteChannelManager.current.connectIn(message['name'] as String,
+            SuiteChannelManager.current!.connectIn(message['name'] as String,
                 channel.virtualChannel(message['id'] as int));
           });
 
-          if ((message['asciiGlyphs'] as bool) ?? false) glyph.ascii = true;
+          if ((message['asciiGlyphs'] as bool?) ?? false) glyph.ascii = true;
           var metadata = Metadata.deserialize(message['metadata']);
           verboseChain = metadata.verboseTrace;
           var declarer = Declarer(
@@ -110,17 +105,16 @@
                   Set.from(message['platformVariables'] as Iterable),
               collectTraces: message['collectTraces'] as bool,
               noRetry: message['noRetry'] as bool);
-
-          StackTraceFormatter.current.configure(
+          StackTraceFormatter.current!.configure(
               except: _deserializeSet(message['foldTraceExcept'] as List),
               only: _deserializeSet(message['foldTraceOnly'] as List));
 
           if (beforeLoad != null) await beforeLoad();
 
-          await declarer.declare(main as Function());
+          await declarer.declare(main);
 
-          var suite = Suite(
-              declarer.build(), SuitePlatform.deserialize(message['platform']),
+          var suite = Suite(declarer.build(),
+              SuitePlatform.deserialize(message['platform'] as Object),
               path: message['path'] as String);
 
           runZoned(() {
@@ -131,8 +125,7 @@
               // useful errors when calling `test()` and `group()` within a test,
               // and so they can add to the declarer's `tearDownAll()` list.
               zoneValues: {#test.declarer: declarer});
-          // ignore: deprecated_member_use
-        }, onError: (error, StackTrace stackTrace) {
+        }, (error, stackTrace) {
           _sendError(channel, error, stackTrace, verboseChain);
         }, zoneSpecification: spec);
       });
@@ -141,8 +134,9 @@
     return controller.foreign;
   }
 
-  /// Returns a [Set] from a JSON serialized list of strings.
-  static Set<String> _deserializeSet(List list) {
+  /// Returns a [Set] from a JSON serialized list of strings, or `null` if the
+  /// list is empty or `null`.
+  static Set<String>? _deserializeSet(List? list) {
     if (list == null) return null;
     if (list.isEmpty) return null;
     return Set.from(list);
@@ -156,13 +150,13 @@
   }
 
   /// Sends a message over [channel] indicating an error from user code.
-  static void _sendError(
-      StreamChannel channel, error, StackTrace stackTrace, bool verboseChain) {
+  static void _sendError(StreamChannel channel, Object error,
+      StackTrace stackTrace, bool verboseChain) {
     channel.sink.add({
       'type': 'error',
       'error': RemoteException.serialize(
           error,
-          StackTraceFormatter.current
+          StackTraceFormatter.current!
               .formatStackTrace(stackTrace, verbose: verboseChain))
     });
   }
@@ -203,7 +197,8 @@
   ///
   /// [groups] lists the groups that contain [test]. Returns `null` if [test]
   /// is `null`.
-  Map _serializeTest(MultiChannel channel, Test test, Iterable<Group> groups) {
+  Map? _serializeTest(
+      MultiChannel channel, Test? test, Iterable<Group>? groups) {
     if (test == null) return null;
 
     var testChannel = channel.virtualChannel();
@@ -242,13 +237,13 @@
         'type': 'error',
         'error': RemoteException.serialize(
             asyncError.error,
-            StackTraceFormatter.current.formatStackTrace(asyncError.stackTrace,
+            StackTraceFormatter.current!.formatStackTrace(asyncError.stackTrace,
                 verbose: liveTest.test.metadata.verboseTrace))
       });
     });
 
     liveTest.onMessage.listen((message) {
-      if (_printZone != null) _printZone.print(message.text);
+      if (_printZone != null) _printZone!.print(message.text);
       channel.sink.add({
         'type': 'message',
         'message-type': message.type.name,
diff --git a/pkgs/test_api/lib/src/suite_channel_manager.dart b/pkgs/test_api/lib/src/suite_channel_manager.dart
index 1f70d5f..8c88df2 100644
--- a/pkgs/test_api/lib/src/suite_channel_manager.dart
+++ b/pkgs/test_api/lib/src/suite_channel_manager.dart
@@ -13,19 +13,19 @@
 class SuiteChannelManager {
   /// Connections from the test runner that have yet to connect to corresponding
   /// calls to [suiteChannel] within this worker.
-  final _incomingConnections = <String, StreamChannel>{};
+  final _incomingConnections = <String, StreamChannel<Object?>>{};
 
   /// Connections from calls to [suiteChannel] that have yet to connect to
   /// corresponding connections from the test runner.
-  final _outgoingConnections = <String, StreamChannelCompleter>{};
+  final _outgoingConnections = <String, StreamChannelCompleter<Object?>>{};
 
   /// The channel names that have already been used.
   final _names = <String>{};
 
   /// Returns the current manager, or `null` if this isn't called within a call
   /// to [asCurrent].
-  static SuiteChannelManager get current =>
-      Zone.current[_currentKey] as SuiteChannelManager;
+  static SuiteChannelManager? get current =>
+      Zone.current[_currentKey] as SuiteChannelManager?;
 
   /// Runs [body] with [this] as [SuiteChannelManager.current].
   ///
@@ -35,23 +35,23 @@
       runZoned(body, zoneValues: {_currentKey: this});
 
   /// Creates a connection to the test runnner's channel with the given [name].
-  StreamChannel connectOut(String name) {
+  StreamChannel<Object?> connectOut(String name) {
     if (_incomingConnections.containsKey(name)) {
-      return _incomingConnections[name];
+      return (_incomingConnections[name])!;
     } else if (_names.contains(name)) {
       throw StateError('Duplicate suiteChannel() connection "$name".');
     } else {
       _names.add(name);
-      var completer = StreamChannelCompleter();
+      var completer = StreamChannelCompleter<Object?>();
       _outgoingConnections[name] = completer;
       return completer.channel;
     }
   }
 
   /// Connects [channel] to this worker's channel with the given [name].
-  void connectIn(String name, StreamChannel channel) {
+  void connectIn(String name, StreamChannel<Object?> channel) {
     if (_outgoingConnections.containsKey(name)) {
-      _outgoingConnections.remove(name).setChannel(channel);
+      _outgoingConnections.remove(name)!.setChannel(channel);
     } else if (_incomingConnections.containsKey(name)) {
       throw StateError('Duplicate RunnerSuite.channel() connection "$name".');
     } else {
diff --git a/pkgs/test_api/lib/src/util/iterable_set.dart b/pkgs/test_api/lib/src/util/iterable_set.dart
index bba6433..19bdb3e 100644
--- a/pkgs/test_api/lib/src/util/iterable_set.dart
+++ b/pkgs/test_api/lib/src/util/iterable_set.dart
@@ -29,11 +29,14 @@
   IterableSet(this._base);
 
   @override
-  bool contains(Object element) => _base.contains(element);
+  bool contains(Object? element) => _base.contains(element);
 
   @override
-  E lookup(Object needle) =>
-      _base.firstWhere((element) => element == needle, orElse: () => null);
+  E? lookup(Object? needle) {
+    for (var e in _base) {
+      if (e == needle) return e;
+    }
+  }
 
   @override
   Set<E> toSet() => _base.toSet();
diff --git a/pkgs/test_api/lib/src/util/remote_exception.dart b/pkgs/test_api/lib/src/util/remote_exception.dart
index bb6fc62..996bb35 100644
--- a/pkgs/test_api/lib/src/util/remote_exception.dart
+++ b/pkgs/test_api/lib/src/util/remote_exception.dart
@@ -30,7 +30,7 @@
   /// Other than JSON- and isolate-safety, no guarantees are made about the
   /// serialized format.
   static Map<String, dynamic> serialize(error, StackTrace stackTrace) {
-    String message;
+    String? message;
     if (error is String) {
       message = error;
     } else {
diff --git a/pkgs/test_api/lib/src/util/test.dart b/pkgs/test_api/lib/src/util/test.dart
index de36fbf..8e051d6 100644
--- a/pkgs/test_api/lib/src/util/test.dart
+++ b/pkgs/test_api/lib/src/util/test.dart
@@ -16,10 +16,10 @@
 Future errorsDontStopTest(dynamic Function() body) {
   var completer = Completer();
 
-  Invoker.current.addOutstandingCallback();
-  Invoker.current.waitForOutstandingCallbacks(() {
+  Invoker.current!.addOutstandingCallback();
+  Invoker.current!.waitForOutstandingCallbacks(() {
     Future.sync(body).whenComplete(completer.complete);
-  }).then((_) => Invoker.current.removeOutstandingCallback());
+  }).then((_) => Invoker.current!.removeOutstandingCallback());
 
   return completer.future;
 }
diff --git a/pkgs/test_api/lib/src/utils.dart b/pkgs/test_api/lib/src/utils.dart
index ee1cde6..d0bb22e 100644
--- a/pkgs/test_api/lib/src/utils.dart
+++ b/pkgs/test_api/lib/src/utils.dart
@@ -106,7 +106,7 @@
 ///
 /// If [first] is passed, it's used in place of the first line's indentation and
 /// [size] defaults to `first.length`. Otherwise, [size] defaults to 2.
-String indent(String string, {int size, String first}) {
+String indent(String string, {int? size, String? first}) {
   size ??= first == null ? 2 : first.length;
   return prefixLines(string, ' ' * size, first: first);
 }
@@ -116,19 +116,19 @@
 /// This converts each element of [iter] to a string and separates them with
 /// commas and/or [conjunction] where appropriate. The [conjunction] defaults to
 /// "and".
-String toSentence(Iterable iter, {String conjunction}) {
+String toSentence(Iterable iter, {String conjunction = 'and'}) {
   if (iter.length == 1) return iter.first.toString();
 
   var result = iter.take(iter.length - 1).join(', ');
   if (iter.length > 2) result += ',';
-  return "$result ${conjunction ?? 'and'} ${iter.last}";
+  return '$result $conjunction ${iter.last}';
 }
 
 /// 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}) {
+String pluralize(String name, int number, {String? plural}) {
   if (number == 1) return name;
   if (plural != null) return plural;
   return '${name}s';
@@ -149,7 +149,7 @@
 ///
 /// The return value *may or may not* be unmodifiable.
 Map<K, V> mergeUnmodifiableMaps<K, V>(Map<K, V> map1, Map<K, V> map2,
-    {V Function(V, V) value}) {
+    {V Function(V, V)? value}) {
   if (map1.isEmpty) return map2;
   if (map2.isEmpty) return map1;
   return mergeMaps(map1, map2, value: value);
@@ -244,7 +244,7 @@
 /// Returns a random base64 string containing [bytes] bytes of data.
 ///
 /// [seed] is passed to [math.Random].
-String randomBase64(int bytes, {int seed}) {
+String randomBase64(int bytes, {int? seed}) {
   var random = math.Random(seed);
   var data = Uint8List(bytes);
   for (var i = 0; i < bytes; i++) {
@@ -254,7 +254,7 @@
 }
 
 /// Throws an [ArgumentError] if [message] isn't recursively JSON-safe.
-void ensureJsonEncodable(Object message) {
+void ensureJsonEncodable(Object? message) {
   if (message == null ||
       message is String ||
       message is num ||
@@ -291,10 +291,10 @@
 /// only a single line; otherwise, [first], [last], or [prefix] is used, in that
 /// order of precedence.
 String prefixLines(String text, String prefix,
-    {String first, String last, String single}) {
+    {String? first, String? last, String? single}) {
+  single ??= first ?? last ?? prefix;
   first ??= prefix;
   last ??= prefix;
-  single ??= first ?? last ?? prefix;
 
   var lines = text.split('\n');
   if (lines.length == 1) return '$single$text';
diff --git a/pkgs/test_api/lib/test_api.dart b/pkgs/test_api/lib/test_api.dart
index 2927580..1857896 100644
--- a/pkgs/test_api/lib/test_api.dart
+++ b/pkgs/test_api/lib/test_api.dart
@@ -95,12 +95,12 @@
 /// filter tests by name.
 @isTest
 void test(description, dynamic Function() body,
-    {String testOn,
-    Timeout timeout,
+    {String? testOn,
+    Timeout? timeout,
     skip,
     tags,
-    Map<String, dynamic> onPlatform,
-    int retry,
+    Map<String, dynamic>? onPlatform,
+    int? retry,
     @deprecated bool solo = false}) {
   _declarer.test(description.toString(), body,
       testOn: testOn,
@@ -173,12 +173,12 @@
 /// filter tests by name.
 @isTestGroup
 void group(description, dynamic Function() body,
-    {String testOn,
-    Timeout timeout,
+    {String? testOn,
+    Timeout? timeout,
     skip,
     tags,
-    Map<String, dynamic> onPlatform,
-    int retry,
+    Map<String, dynamic>? onPlatform,
+    int? retry,
     @deprecated bool solo = false}) {
   _declarer.group(description.toString(), body,
       testOn: testOn,
@@ -240,7 +240,7 @@
     throw StateError('addTearDown() may only be called within a test.');
   }
 
-  Invoker.current.addTearDown(callback);
+  Invoker.current!.addTearDown(callback);
 }
 
 /// Registers a function to be run once before all tests.
@@ -273,7 +273,8 @@
     _declarer.tearDownAll(callback);
 
 /// Registers an exception that was caught for the current test.
-void registerException(error, [StackTrace stackTrace]) {
+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);
@@ -285,4 +286,11 @@
 /// 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);
+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/mono_pkg.yaml b/pkgs/test_api/mono_pkg.yaml
index 4a2bb76..c3e2834 100644
--- a/pkgs/test_api/mono_pkg.yaml
+++ b/pkgs/test_api/mono_pkg.yaml
@@ -1,13 +1,11 @@
+dart:
+  - dev
+
 stages:
   - analyze_and_format:
     - group:
       - dartfmt: sdk
-      - dartanalyzer: --fatal-infos --fatal-warnings .
-      dart: dev
-    - group:
-      - dartanalyzer: --fatal-warnings .
-      dart: 2.7.0
+      - dartanalyzer: --enable-experiment=non-nullable --fatal-infos --fatal-warnings .
   - unit_test:
     - group:
-      - test: --preset travis
-      dart: dev
+      - command: pub run --enable-experiment=non-nullable test --preset travis -x browser
diff --git a/pkgs/test_api/pubspec.yaml b/pkgs/test_api/pubspec.yaml
index 186a3a2..ac62ee1 100644
--- a/pkgs/test_api/pubspec.yaml
+++ b/pkgs/test_api/pubspec.yaml
@@ -1,29 +1,29 @@
 name: test_api
-version: 0.2.18+1
+version: 0.2.19-nullsafety.6
 description: A library for writing Dart tests.
 homepage: https://github.com/dart-lang/test/blob/master/pkgs/test_api
 
 environment:
-  sdk: ">=2.7.0 <3.0.0"
+  sdk: ">=2.12.0-0 <3.0.0"
 
 dependencies:
-  async: ^2.0.0
-  boolean_selector: ">=1.0.0 <3.0.0"
-  collection: ^1.8.0
-  meta: ^1.1.5
-  path: ^1.2.0
-  source_span: ^1.4.0
-  stack_trace: ^1.9.0
-  stream_channel: ">=1.7.0 <3.0.0"
-  string_scanner: ^1.0.0
-  term_glyph: ^1.0.0
+  async: '>=2.5.0-nullsafety <2.5.0'
+  boolean_selector: ">=2.1.0-nullsafety <2.1.0"
+  collection: '>=1.15.0-nullsafety <1.15.0'
+  meta: '>=1.3.0-nullsafety <1.3.0'
+  path: '>=1.8.0-nullsafety <1.8.0'
+  source_span: '>=1.8.0-nullsafety <1.8.0'
+  stack_trace: '>=1.10.0-nullsafety <1.10.0'
+  stream_channel: '>=2.1.0-nullsafety <2.1.0'
+  string_scanner: '>=1.1.0-nullsafety <1.1.0'
+  term_glyph: '>=1.2.0-nullsafety <1.2.0'
 
   # Use a tight version constraint to ensure that a constraint on matcher
   # properly constrains all features it provides.
-  matcher: 0.12.9
+  matcher: '>=0.12.10-nullsafety <0.12.10'
 
 dev_dependencies:
-  pedantic: ^1.0.0
+  pedantic: '>=1.10.0-nullsafety <1.10.0'
   test_descriptor: ^1.0.0
   test_process: ^1.0.0
   test: any
@@ -31,6 +31,6 @@
 
 dependency_overrides:
   test:
-    path: ./../test
+    path: ../test
   test_core:
-    path: ./../test_core
+    path: ../test_core
diff --git a/pkgs/test_api/test/backend/declarer_test.dart b/pkgs/test_api/test/backend/declarer_test.dart
index d8b311d..69531d7 100644
--- a/pkgs/test_api/test/backend/declarer_test.dart
+++ b/pkgs/test_api/test/backend/declarer_test.dart
@@ -13,7 +13,7 @@
 
 import '../utils.dart';
 
-Suite _suite;
+late Suite _suite;
 
 void main() {
   setUp(() {
@@ -136,7 +136,7 @@
 
   group('.tearDown()', () {
     test('is run after all tests', () async {
-      bool tearDownRun;
+      late bool tearDownRun;
       var tests = declare(() {
         setUp(() => tearDownRun = false);
         tearDown(() => tearDownRun = true);
@@ -161,7 +161,7 @@
     });
 
     test('is run after an out-of-band failure', () async {
-      bool tearDownRun;
+      late bool tearDownRun;
       var tests = declare(() {
         setUp(() => tearDownRun = false);
         tearDown(() => tearDownRun = true);
@@ -169,7 +169,7 @@
         test(
             'description 1',
             expectAsync0(() {
-              Invoker.current.addOutstandingCallback();
+              Invoker.current!.addOutstandingCallback();
               Future(() => throw TestFailure('oh no'));
             }, max: 1));
       });
@@ -205,10 +205,10 @@
         });
 
         test('description', () {
-          Invoker.current.addOutstandingCallback();
+          Invoker.current!.addOutstandingCallback();
           pumpEventQueue().then((_) {
             outstandingCallbackRemoved = true;
-            Invoker.current.removeOutstandingCallback();
+            Invoker.current!.removeOutstandingCallback();
           });
         });
       });
@@ -221,10 +221,10 @@
       var outstandingCallbackRemoved = false;
       var tests = declare(() {
         tearDown(() {
-          Invoker.current.addOutstandingCallback();
+          Invoker.current!.addOutstandingCallback();
           pumpEventQueue().then((_) {
             outstandingCallbackRemoved = true;
-            Invoker.current.removeOutstandingCallback();
+            Invoker.current!.removeOutstandingCallback();
           });
         });
 
@@ -286,7 +286,7 @@
 
     test('runs in the same error zone as the test', () {
       return expectTestsPass(() {
-        Zone testBodyZone;
+        late Zone testBodyZone;
 
         tearDown(() {
           final tearDownZone = Zone.current;
@@ -513,7 +513,7 @@
 
     group('.tearDown()', () {
       test('is scoped to the group', () async {
-        bool tearDownRun;
+        late bool tearDownRun;
         var entries = declare(() {
           setUp(() => tearDownRun = false);
 
diff --git a/pkgs/test_api/test/backend/invoker_test.dart b/pkgs/test_api/test/backend/invoker_test.dart
index 5725385..d2dd6d0 100644
--- a/pkgs/test_api/test/backend/invoker_test.dart
+++ b/pkgs/test_api/test/backend/invoker_test.dart
@@ -17,7 +17,7 @@
 import '../utils.dart';
 
 void main() {
-  Suite suite;
+  late Suite suite;
   setUp(() {
     lastState = null;
     suite = Suite(Group.root([]), suitePlatform);
@@ -30,9 +30,9 @@
     });
 
     test('returns the current invoker in a test body', () async {
-      Invoker invoker;
+      late Invoker invoker;
       var liveTest = _localTest(() {
-        invoker = Invoker.current;
+        invoker = Invoker.current!;
       }).load(suite);
       liveTest.onError.listen(expectAsync1((_) {}, count: 0));
 
@@ -42,13 +42,13 @@
 
     test('returns the current invoker in a test body after the test completes',
         () async {
-      Status status;
+      Status? status;
       var completer = Completer();
       var liveTest = _localTest(() {
         // Use the event loop to wait longer than a microtask for the test to
         // complete.
         Future(() {
-          status = Invoker.current.liveTest.state.status;
+          status = Invoker.current!.liveTest.state.status;
           completer.complete(Invoker.current);
         });
       }).load(suite);
@@ -63,8 +63,8 @@
 
   group('in a successful test,', () {
     test('the state changes from pending to running to complete', () async {
-      State stateInTest;
-      LiveTest liveTest;
+      late State stateInTest;
+      late LiveTest liveTest;
       liveTest = _localTest(() {
         stateInTest = liveTest.state;
       }).load(suite);
@@ -141,7 +141,7 @@
     test('a failure reported asynchronously during the test causes it to fail',
         () {
       var liveTest = _localTest(() {
-        Invoker.current.addOutstandingCallback();
+        Invoker.current!.addOutstandingCallback();
         Future(() => registerException(TestFailure('oh no')));
       }).load(suite);
 
@@ -152,7 +152,7 @@
     test('a failure thrown asynchronously during the test causes it to fail',
         () {
       var liveTest = _localTest(() {
-        Invoker.current.addOutstandingCallback();
+        Invoker.current!.addOutstandingCallback();
         Future(() => throw TestFailure('oh no'));
       }).load(suite);
 
@@ -195,7 +195,7 @@
 
     test('multiple asynchronous failures are reported', () {
       var liveTest = _localTest(() {
-        Invoker.current.addOutstandingCallback();
+        Invoker.current!.addOutstandingCallback();
         Future(() => throw TestFailure('one'));
         Future(() => throw TestFailure('two'));
         Future(() => throw TestFailure('three'));
@@ -209,7 +209,7 @@
 
       expectErrors(liveTest, [
         (error) {
-          expect(lastState.status, equals(Status.complete));
+          expect(lastState?.status, equals(Status.complete));
           expect(error, isTestFailure('one'));
         },
         (error) {
@@ -273,7 +273,7 @@
     test('an error reported asynchronously during the test causes it to error',
         () {
       var liveTest = _localTest(() {
-        Invoker.current.addOutstandingCallback();
+        Invoker.current!.addOutstandingCallback();
         Future(() => registerException('oh no'));
       }).load(suite);
 
@@ -284,7 +284,7 @@
     test('an error thrown asynchronously during the test causes it to error',
         () {
       var liveTest = _localTest(() {
-        Invoker.current.addOutstandingCallback();
+        Invoker.current!.addOutstandingCallback();
         Future(() => throw 'oh no');
       }).load(suite);
 
@@ -324,7 +324,7 @@
 
     test('multiple asynchronous errors are reported', () {
       var liveTest = _localTest(() {
-        Invoker.current.addOutstandingCallback();
+        Invoker.current!.addOutstandingCallback();
         Future(() => throw 'one');
         Future(() => throw 'two');
         Future(() => throw 'three');
@@ -338,7 +338,7 @@
 
       expectErrors(liveTest, [
         (error) {
-          expect(lastState.status, equals(Status.complete));
+          expect(lastState?.status, equals(Status.complete));
           expect(error, equals('one'));
         },
         (error) {
@@ -387,13 +387,13 @@
       () async {
     var outstandingCallbackRemoved = false;
     var liveTest = _localTest(() {
-      Invoker.current.addOutstandingCallback();
+      Invoker.current!.addOutstandingCallback();
 
       // Pump the event queue to make sure the test isn't coincidentally
       // completing after the outstanding callback is removed.
       pumpEventQueue().then((_) {
         outstandingCallbackRemoved = true;
-        Invoker.current.removeOutstandingCallback();
+        Invoker.current!.removeOutstandingCallback();
       });
     }).load(suite);
 
@@ -426,7 +426,7 @@
   group('timeout:', () {
     test('A test can be timed out', () {
       var liveTest = _localTest(() {
-        Invoker.current.addOutstandingCallback();
+        Invoker.current!.addOutstandingCallback();
       }, metadata: Metadata(timeout: Timeout(Duration(milliseconds: 100))))
           .load(suite);
 
@@ -437,7 +437,7 @@
 
       expectErrors(liveTest, [
         (error) {
-          expect(lastState.status, equals(Status.complete));
+          expect(lastState!.status, equals(Status.complete));
           expect(error, TypeMatcher<TimeoutException>());
         }
       ]);
@@ -449,7 +449,7 @@
   group('waitForOutstandingCallbacks:', () {
     test('waits for the wrapped function to complete', () async {
       var functionCompleted = false;
-      await Invoker.current.waitForOutstandingCallbacks(() async {
+      await Invoker.current!.waitForOutstandingCallbacks(() async {
         await pumpEventQueue();
         functionCompleted = true;
       });
@@ -460,7 +460,7 @@
     test('waits for registered callbacks in the wrapped function to run',
         () async {
       var callbackRun = false;
-      await Invoker.current.waitForOutstandingCallbacks(() {
+      await Invoker.current!.waitForOutstandingCallbacks(() {
         pumpEventQueue().then(expectAsync1((_) {
           callbackRun = true;
         }));
@@ -471,8 +471,8 @@
 
     test("doesn't automatically block the enclosing context", () async {
       var innerFunctionCompleted = false;
-      await Invoker.current.waitForOutstandingCallbacks(() {
-        Invoker.current.waitForOutstandingCallbacks(() async {
+      await Invoker.current!.waitForOutstandingCallbacks(() {
+        Invoker.current!.waitForOutstandingCallbacks(() async {
           await pumpEventQueue();
           innerFunctionCompleted = true;
         });
@@ -485,8 +485,8 @@
         "forwards errors to the enclosing test but doesn't remove its "
         'outstanding callbacks', () async {
       var liveTest = _localTest(() async {
-        Invoker.current.addOutstandingCallback();
-        await Invoker.current.waitForOutstandingCallbacks(() {
+        Invoker.current!.addOutstandingCallback();
+        await Invoker.current!.waitForOutstandingCallbacks(() {
           throw 'oh no';
         });
       }).load(suite);
@@ -527,7 +527,7 @@
     test("doesn't print anything if the test succeeds", () {
       expect(() async {
         var liveTest = _localTest(() {
-          Invoker.current.printOnFailure('only on failure');
+          Invoker.current!.printOnFailure('only on failure');
         }).load(suite);
         liveTest.onError.listen(expectAsync1((_) {}, count: 0));
 
@@ -538,7 +538,7 @@
     test('prints if the test fails', () {
       expect(() async {
         var liveTest = _localTest(() {
-          Invoker.current.printOnFailure('only on failure');
+          Invoker.current!.printOnFailure('only on failure');
           expect(true, isFalse);
         }).load(suite);
         liveTest.onError.listen(expectAsync1((_) {}, count: 1));
@@ -549,7 +549,7 @@
   });
 }
 
-LocalTest _localTest(dynamic Function() body, {Metadata metadata}) {
+LocalTest _localTest(dynamic Function() body, {Metadata? metadata}) {
   metadata ??= Metadata();
   return LocalTest('test', metadata, body);
 }
diff --git a/pkgs/test_api/test/backend/metadata_test.dart b/pkgs/test_api/test/backend/metadata_test.dart
index cb96b41..44f1784 100644
--- a/pkgs/test_api/test/backend/metadata_test.dart
+++ b/pkgs/test_api/test/backend/metadata_test.dart
@@ -60,7 +60,7 @@
       expect(metadata.verboseTrace, isTrue);
       expect(metadata.skip, isFalse);
       expect(metadata.forTag, contains(BooleanSelector.parse('foo')));
-      expect(metadata.forTag[BooleanSelector.parse('foo')].skip, isTrue);
+      expect(metadata.forTag[BooleanSelector.parse('foo')]?.skip, isTrue);
     });
 
     test("returns the normal metadata if forTag doesn't match tags", () {
@@ -73,7 +73,7 @@
       expect(metadata.skip, isFalse);
       expect(metadata.tags, unorderedEquals(['bar', 'baz']));
       expect(metadata.forTag, contains(BooleanSelector.parse('foo')));
-      expect(metadata.forTag[BooleanSelector.parse('foo')].skip, isTrue);
+      expect(metadata.forTag[BooleanSelector.parse('foo')]?.skip, isTrue);
     });
 
     test('resolves forTags that match tags', () {
diff --git a/pkgs/test_api/test/frontend/expect_async_test.dart b/pkgs/test_api/test/frontend/expect_async_test.dart
index 7d8cc6b..2bf2766 100644
--- a/pkgs/test_api/test/frontend/expect_async_test.dart
+++ b/pkgs/test_api/test/frontend/expect_async_test.dart
@@ -2,8 +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 'dart:async';
-
 import 'package:test_api/src/backend/live_test.dart';
 import 'package:test_api/src/backend/state.dart';
 import 'package:test/test.dart';
@@ -168,8 +166,8 @@
     test(
         "won't allow the test to complete until it's called at least that "
         'many times', () async {
-      LiveTest liveTest;
-      Future future;
+      late LiveTest liveTest;
+      late Future future;
       liveTest = createTest(() {
         var callback = expectAsync0(() {}, count: 3);
 
@@ -267,8 +265,8 @@
   group('expectAsyncUntil()', () {
     test("won't allow the test to complete until isDone returns true",
         () async {
-      LiveTest liveTest;
-      Future future;
+      late LiveTest liveTest;
+      late Future future;
       liveTest = createTest(() {
         var done = false;
         var callback = expectAsyncUntil0(() {}, () => done);
diff --git a/pkgs/test_api/test/frontend/fake_test.dart b/pkgs/test_api/test/frontend/fake_test.dart
new file mode 100644
index 0000000..bb82a7c
--- /dev/null
+++ b/pkgs/test_api/test/frontend/fake_test.dart
@@ -0,0 +1,37 @@
+// Copyright (c) 2020, 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:test/test.dart';
+import 'package:test_api/fake.dart' as test_api;
+
+void main() {
+  late _FakeSample fake;
+  setUp(() {
+    fake = _FakeSample();
+  });
+  test('method invocation', () {
+    expect(() => fake.f(), throwsA(TypeMatcher<UnimplementedError>()));
+  });
+  test('getter', () {
+    expect(() => fake.x, throwsA(TypeMatcher<UnimplementedError>()));
+  });
+  test('setter', () {
+    expect(() => fake.x = 0, throwsA(TypeMatcher<UnimplementedError>()));
+  });
+  test('operator', () {
+    expect(() => fake + 1, throwsA(TypeMatcher<UnimplementedError>()));
+  });
+}
+
+class _Sample {
+  void f() {}
+
+  int get x => 0;
+
+  set x(int value) {}
+
+  int operator +(int other) => 0;
+}
+
+class _FakeSample extends test_api.Fake implements _Sample {}
diff --git a/pkgs/test_api/test/frontend/matcher/throws_type_test.dart b/pkgs/test_api/test/frontend/matcher/throws_type_test.dart
index 1086a02..bf6a850 100644
--- a/pkgs/test_api/test/frontend/matcher/throws_type_test.dart
+++ b/pkgs/test_api/test/frontend/matcher/throws_type_test.dart
@@ -107,7 +107,9 @@
 
   group('[throwsNullThrownError]', () {
     test('passes when a NullThrownError is thrown', () {
-      expect(() => throw null, throwsNullThrownError);
+      // Throwing null is no longer allowed with NNBD, but we do want to allow
+      // it from legacy code and should be able to catch those errors.
+      expect(() => throw NullThrownError(), throwsNullThrownError);
     });
 
     test('fails when a non-NullThrownError is thrown', () async {
diff --git a/pkgs/test_api/test/frontend/stream_matcher_test.dart b/pkgs/test_api/test/frontend/stream_matcher_test.dart
index 1ea59da..deb21fb 100644
--- a/pkgs/test_api/test/frontend/stream_matcher_test.dart
+++ b/pkgs/test_api/test/frontend/stream_matcher_test.dart
@@ -15,10 +15,10 @@
     glyph.ascii = true;
   });
 
-  Stream stream;
-  StreamQueue queue;
-  Stream errorStream;
-  StreamQueue errorQueue;
+  late Stream stream;
+  late StreamQueue queue;
+  late Stream errorStream;
+  late StreamQueue errorQueue;
   setUp(() {
     stream = Stream.fromIterable([1, 2, 3, 4, 5]);
     queue = StreamQueue(Stream.fromIterable([1, 2, 3, 4, 5]));
diff --git a/pkgs/test_api/test/utils.dart b/pkgs/test_api/test/utils.dart
index f1105dc..73e8c84 100644
--- a/pkgs/test_api/test/utils.dart
+++ b/pkgs/test_api/test/utils.dart
@@ -26,7 +26,7 @@
 final suitePlatform = SuitePlatform(Runtime.vm);
 
 // The last state change detected via [expectStates].
-State lastState;
+State? lastState;
 
 /// Asserts that exactly [states] will be emitted via [liveTest.onStateChange].
 ///
@@ -57,7 +57,7 @@
 
   expectErrors(liveTest, [
     (error) {
-      expect(lastState.status, equals(Status.complete));
+      expect(lastState?.status, equals(Status.complete));
       expect(error, isTestFailure('oh no'));
     }
   ]);
@@ -72,7 +72,7 @@
 
   expectErrors(liveTest, [
     (error) {
-      expect(lastState.status, equals(Status.complete));
+      expect(lastState?.status, equals(Status.complete));
       expect(error, equals('oh no'));
     }
   ]);
@@ -146,8 +146,8 @@
 /// [stopBlocking] is passed the return value of [test].
 Future expectTestBlocks(
     dynamic Function() test, dynamic Function(dynamic) stopBlocking) async {
-  LiveTest liveTest;
-  Future future;
+  late LiveTest liveTest;
+  late Future future;
   liveTest = createTest(() {
     var value = test();
     future = pumpEventQueue().then((_) {
diff --git a/pkgs/test_core/CHANGELOG.md b/pkgs/test_core/CHANGELOG.md
index a21061c..07730aa 100644
--- a/pkgs/test_core/CHANGELOG.md
+++ b/pkgs/test_core/CHANGELOG.md
@@ -1,3 +1,62 @@
+## 0.3.12-nullsafety.11
+
+* Fix `spawnHybridUri` on windows.
+
+## 0.3.12-nullsafety.10
+
+* Allow `package:analyzer` version `0.41.x`.
+
+## 0.3.12-nullsafety.9
+
+* Fix `spawnHybridUri` to respect language versioning of the spawned uri.
+* Pre-emptively fix legacy library import lint violations, and unmigrate some
+  libraries as necessary.
+
+## 0.3.12-nullsafety.8
+
+* Fix a bug where the test runner could crash when printing the elapsed time.
+* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release
+  guidelines.
+
+
+## 0.3.12-nullsafety.7
+
+* Allow prerelease versions of the 2.12 sdk.
+
+## 0.3.12-nullsafety.6
+
+* Add experimental `directRunTests`, `directRunSingle`, and `enumerateTestCases`
+  APIs to enable test runners written around a single executable that can report
+  and run any single test case.
+
+## 0.3.12-nullsafety.5
+
+* Allow `2.10` stable and `2.11.0-dev` SDKs.
+* Add `src/platform.dart` library to consolidate the necessary imports required
+  to write a custom platform.
+* Stop required a `SILENT_OBSERVATORY` environment variable to run with
+  debugging and the JSON reporter.
+
+## 0.3.12-nullsafety.4
+
+* Support latest `package:vm_service`.
+
+## 0.3.12-nullsafety.3
+
+* Clean up `--help` output.
+
+## 0.3.12-nullsafety.2
+
+* Allow version `0.40.x` of `analyzer`.
+
+## 0.3.12-nullsafety.1
+
+* Update source_maps constraint.
+
+## 0.3.12-nullsafety
+
+* Migrate to null safety.
+
 ## 0.3.11+3 (Backport)
 
 * Support `package:analyzer` version `0.41.x`.
@@ -42,6 +101,11 @@
 * Add additional information to an exception when we end up with a null
   `RunnerSuite`.
 
+* Update vm bootstrapping logic to ensure the bootstrap library has the same
+  language version as the test.
+* Populate `languageVersionComment` in the `Metadata` returned from
+  `parseMetadata`.
+
 ## 0.3.4
 
 * Fix error messages for incorrect string literals in test annotations.
diff --git a/pkgs/test_core/lib/backend.dart b/pkgs/test_core/lib/backend.dart
index 3a1506b..dca038f 100644
--- a/pkgs/test_core/lib/backend.dart
+++ b/pkgs/test_core/lib/backend.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2019, 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.
+//
+// @dart=2.8
 
 @Deprecated('package:test_core is not intended for general use. '
     'Please use package:test.')
diff --git a/pkgs/test_core/lib/src/bootstrap/vm.dart b/pkgs/test_core/lib/src/bootstrap/vm.dart
index 41b9269..9a75e59 100644
--- a/pkgs/test_core/lib/src/bootstrap/vm.dart
+++ b/pkgs/test_core/lib/src/bootstrap/vm.dart
@@ -11,5 +11,5 @@
 /// Bootstraps a vm test to communicate with the test runner.
 void internalBootstrapVmTest(Function Function() getMain, SendPort sendPort) {
   var channel = serializeSuite(getMain);
-  IsolateChannel.connectSend(sendPort).pipe(channel);
+  IsolateChannel<Object?>.connectSend(sendPort).pipe(channel);
 }
diff --git a/pkgs/test_core/lib/src/direct_run.dart b/pkgs/test_core/lib/src/direct_run.dart
new file mode 100644
index 0000000..c4f7612
--- /dev/null
+++ b/pkgs/test_core/lib/src/direct_run.dart
@@ -0,0 +1,141 @@
+// Copyright (c) 2020, 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.
+//
+// @dart=2.9
+
+import 'dart:async';
+import 'dart:collection';
+
+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/group.dart'; //ignore: implementation_imports
+import 'package:test_api/src/backend/group_entry.dart'; //ignore: implementation_imports
+import 'package:test_api/src/backend/invoker.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/test.dart'; //ignore: implementation_imports
+import 'package:test_api/src/utils.dart'; // ignore: implementation_imports
+
+import 'runner/configuration.dart';
+import 'runner/engine.dart';
+import 'runner/plugin/environment.dart';
+import 'runner/reporter.dart';
+import 'runner/reporter/expanded.dart';
+import 'runner/runner_suite.dart';
+import 'runner/suite.dart';
+import 'util/print_sink.dart';
+
+/// Runs all unskipped test cases declared in [testMain].
+///
+/// Test suite level metadata defined in annotations is not read. No filtering
+/// is applied except for the filtering defined by `solo` or `skip` arguments to
+/// `group` and `test`. Returns [true] if all tests passed.
+Future<bool> directRunTests(FutureOr<void> Function() testMain,
+        {Reporter Function(Engine) /*?*/ reporterFactory}) =>
+    _directRunTests(testMain, reporterFactory: reporterFactory);
+
+/// Runs a single test declared in [testMain] matched by it's full test name.
+///
+/// There must be exactly one test defined with the name [fullTestName]. Note
+/// that not all tests and groups are checked, so a test case that is not be
+/// intended to be run (due to a `solo` on a different test) may still be run
+/// with this API. Only the test names returned by [enumerateTestCases] should
+/// be used to prevent running skipped tests.
+///
+/// Return [true] if the test passes.
+///
+/// If there are no tests matching [fullTestName] a [MissingTestException] is
+/// thrown. If there is more than one test with the name [fullTestName] they
+/// will both be run, then a [DuplicateTestnameException] will be thrown.
+Future<bool> directRunSingleTest(
+        FutureOr<void> Function() testMain, String fullTestName,
+        {Reporter Function(Engine) /*?*/ reporterFactory}) =>
+    _directRunTests(testMain,
+        reporterFactory: reporterFactory, fullTestName: fullTestName);
+
+Future<bool> _directRunTests(FutureOr<void> Function() testMain,
+    {Reporter Function(Engine) /*?*/ reporterFactory,
+    String /*?*/ fullTestName}) async {
+  reporterFactory ??= (engine) => ExpandedReporter.watch(engine, PrintSink(),
+      color: Configuration.empty.color, printPath: false, printPlatform: false);
+  final declarer = Declarer(fullTestName: fullTestName);
+  await declarer.declare(testMain);
+
+  final suite = RunnerSuite(const PluginEnvironment(), SuiteConfiguration.empty,
+      declarer.build(), SuitePlatform(Runtime.vm, os: currentOSGuess),
+      path: p.prettyUri(Uri.base));
+
+  final engine = Engine()
+    ..suiteSink.add(suite)
+    ..suiteSink.close();
+
+  reporterFactory(engine);
+
+  final success = await runZoned(() => Invoker.guard(engine.run),
+      zoneValues: {#test.declarer: declarer});
+
+  if (fullTestName != null) {
+    final testCount = engine.liveTests.length;
+    if (testCount > 1) {
+      throw DuplicateTestNameException(fullTestName);
+    }
+    if (testCount == 0) {
+      throw MissingTestException(fullTestName);
+    }
+  }
+  return success;
+}
+
+/// Runs [testMain] and returns the names of all declared tests.
+///
+/// Test names declared must be unique. If any test repeats the full name,
+/// including group prefixes, of a prior test a [DuplicateTestNameException]
+/// will be thrown.
+///
+/// Skipped tests are ignored.
+Future<Set<String>> enumerateTestCases(
+    FutureOr<void> Function() testMain) async {
+  final declarer = Declarer();
+  await declarer.declare(testMain);
+
+  final toVisit = Queue<GroupEntry>.of([declarer.build()]);
+  final allTestNames = <String>{};
+  final unskippedTestNames = <String>{};
+  while (toVisit.isNotEmpty) {
+    final current = toVisit.removeLast();
+    if (current is Group) {
+      toVisit.addAll(current.entries.reversed);
+    } else if (current is Test) {
+      if (!allTestNames.add(current.name)) {
+        throw DuplicateTestNameException(current.name);
+      }
+      if (current.metadata.skip) continue;
+      unskippedTestNames.add(current.name);
+    } else {
+      throw StateError('Unandled Group Entry: ${current.runtimeType}');
+    }
+  }
+  return unskippedTestNames;
+}
+
+/// An exception thrown when two test cases in the same test suite (same `main`)
+/// have an identical name.
+class DuplicateTestNameException implements Exception {
+  final String name;
+  DuplicateTestNameException(this.name);
+
+  @override
+  String toString() => 'A test with the name "$name" was already declared. '
+      'Test cases must have unique names.';
+}
+
+/// An exception thrown when a specific test was requested by name that does not
+/// exist.
+class MissingTestException implements Exception {
+  final String name;
+  MissingTestException(this.name);
+
+  @override
+  String toString() =>
+      'A test with the name "$name" was not declared in the test suite.';
+}
diff --git a/pkgs/test_core/lib/src/executable.dart b/pkgs/test_core/lib/src/executable.dart
index 56e9661..c054674 100644
--- a/pkgs/test_core/lib/src/executable.dart
+++ b/pkgs/test_core/lib/src/executable.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.9
 
 import 'dart:async';
 import 'dart:io';
@@ -18,7 +20,7 @@
 import 'util/exit_codes.dart' as exit_codes;
 import 'util/io.dart';
 
-StreamSubscription signalSubscription;
+StreamSubscription /*?*/ signalSubscription;
 bool isShutdown = false;
 
 /// Returns the path to the global test configuration file.
@@ -135,7 +137,7 @@
     return;
   }
 
-  Runner runner;
+  Runner /*?*/ runner;
 
   signalSubscription ??= _signals.listen((signal) async {
     completeShutdown();
@@ -178,7 +180,7 @@
 ///
 /// If [error] is passed, it's used in place of the usage message and the whole
 /// thing is printed to stderr instead of stdout.
-void _printUsage([String error]) {
+void _printUsage([String /*?*/ error]) {
   var output = stdout;
 
   var message = 'Runs tests in this package.';
diff --git a/pkgs/test_core/lib/src/platform.dart b/pkgs/test_core/lib/src/platform.dart
new file mode 100644
index 0000000..24d8fb4
--- /dev/null
+++ b/pkgs/test_core/lib/src/platform.dart
@@ -0,0 +1,19 @@
+// Copyright (c) 2020, 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.
+//
+// @dart=2.9
+
+// ignore: deprecated_member_use
+export 'package:test_api/backend.dart' show SuitePlatform, Runtime;
+export 'package:test_core/src/runner/configuration.dart' show Configuration;
+export 'package:test_core/src/runner/environment.dart'
+    show PluginEnvironment, Environment;
+export 'package:test_core/src/runner/hack_register_platform.dart'
+    show registerPlatformPlugin;
+export 'package:test_core/src/runner/platform.dart' show PlatformPlugin;
+export 'package:test_core/src/runner/plugin/platform_helpers.dart'
+    show deserializeSuite;
+export 'package:test_core/src/runner/runner_suite.dart'
+    show RunnerSuite, RunnerSuiteController;
+export 'package:test_core/src/runner/suite.dart' show SuiteConfiguration;
diff --git a/pkgs/test_core/lib/src/runner.dart b/pkgs/test_core/lib/src/runner.dart
index 89ed8bd..ede16c1 100644
--- a/pkgs/test_core/lib/src/runner.dart
+++ b/pkgs/test_core/lib/src/runner.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.9
 
 import 'dart:async';
 import 'dart:io';
@@ -31,8 +33,6 @@
 import 'runner/reporter/compact.dart';
 import 'runner/reporter/expanded.dart';
 
-final _silentObservatory = const bool.fromEnvironment('SILENT_OBSERVATORY');
-
 /// A class that loads and runs tests based on a [Configuration].
 ///
 /// This maintains a [Loader] and an [Engine] and passes test suites from one to
@@ -52,7 +52,7 @@
   final Reporter _reporter;
 
   /// The subscription to the stream returned by [_loadSuites].
-  StreamSubscription _suiteSubscription;
+  StreamSubscription /*?*/ _suiteSubscription;
 
   /// The set of suite paths for which [_warnForUnknownTags] has already been
   /// called.
@@ -64,7 +64,7 @@
   /// The current debug operation, if any.
   ///
   /// This is stored so that we can cancel it when the runner is closed.
-  CancelableOperation _debugOperation;
+  CancelableOperation /*?*/ _debugOperation;
 
   /// The memoizer for ensuring [close] only runs once.
   final _closeMemo = AsyncMemoizer();
@@ -114,25 +114,11 @@
 
         var suites = _loadSuites();
 
-        var runTimes = _config.suiteDefaults.runtimes.map(_loader.findRuntime);
-
-        // TODO(grouma) - Remove this check when
-        // https://github.com/dart-lang/sdk/issues/31308 is resolved.
-        if (!_silentObservatory &&
-            runTimes.contains(Runtime.vm) &&
-            _config.debug) {
-          warn('You should set `SILENT_OBSERVATORY` to true when debugging the '
-              'VM as it will output the observatory URL by '
-              'default.\nThis breaks the various reporter contracts.'
-              '\nTo set the value define '
-              '`DART_VM_OPTIONS=-DSILENT_OBSERVATORY=true`.');
-        }
-
         if (_config.coverage != null) {
           await Directory(_config.coverage).create(recursive: true);
         }
 
-        bool success;
+        bool /*?*/ success;
         if (_config.pauseAfterLoad) {
           success = await _loadThenPause(suites);
         } else {
@@ -143,7 +129,7 @@
                 .then((_) => _engine.suiteSink.close()),
             _engine.run()
           ], eagerError: true);
-          success = results.last as bool;
+          success = results.last as bool /*?*/;
         }
 
         if (_closed) return false;
@@ -172,8 +158,8 @@
 
     var unsupportedRuntimes = _config.suiteDefaults.runtimes
         .map(_loader.findRuntime)
-        .where((runtime) =>
-            runtime != null && !testOn.evaluate(currentPlatform(runtime)))
+        .whereType<Runtime>()
+        .where((runtime) => !testOn.evaluate(currentPlatform(runtime)))
         .toList();
     if (unsupportedRuntimes.isEmpty) return;
 
@@ -222,7 +208,7 @@
   /// currently-running VM tests, in case they have stuff to clean up on the
   /// filesystem.
   Future close() => _closeMemo.runOnce(() async {
-        Timer timer;
+        Timer /*?*/ timer;
         if (!_engine.isIdle) {
           // Wait a bit to print this message, since printing it eagerly looks weird
           // if the tests then finish immediately.
@@ -236,9 +222,9 @@
           });
         }
 
-        if (_debugOperation != null) await _debugOperation.cancel();
+        await _debugOperation?.cancel();
+        await _suiteSubscription?.cancel();
 
-        if (_suiteSubscription != null) await _suiteSubscription.cancel();
         _suiteSubscription = null;
 
         // Make sure we close the engine *before* the loader. Otherwise,
@@ -248,7 +234,7 @@
         // browser tests don't store any state we care about and we want them to
         // shut down without waiting for their tear-downs.
         await Future.wait([_loader.closeEphemeral(), _engine.close()]);
-        if (timer != null) timer.cancel();
+        timer?.cancel();
         await _loader.close();
 
         // Flush any IOSinks created for file reporters.
@@ -371,7 +357,7 @@
   /// Returns a human-readable description of [entry], including its type.
   String _entryDescription(GroupEntry entry) {
     if (entry is Test) return 'the test "${entry.name}"';
-    if (entry.name != null) return 'the group "${entry.name}"';
+    if (entry.name.isNotEmpty) return 'the group "${entry.name}"';
     return 'the suite itself';
   }
 
@@ -386,8 +372,9 @@
     if (_config.totalShards == null) return suite;
 
     var shardSize = suite.group.testCount / _config.totalShards;
-    var shardStart = (shardSize * _config.shardIndex).round();
-    var shardEnd = (shardSize * (_config.shardIndex + 1)).round();
+    var shardIndex = _config.shardIndex;
+    var shardStart = (shardSize * shardIndex).round();
+    var shardEnd = (shardSize * (shardIndex + 1)).round();
 
     var count = -1;
     var filtered = suite.filter((test) {
diff --git a/pkgs/test_core/lib/src/runner/compiler_pool.dart b/pkgs/test_core/lib/src/runner/compiler_pool.dart
index 7556ab5..9bc4f7b 100644
--- a/pkgs/test_core/lib/src/runner/compiler_pool.dart
+++ b/pkgs/test_core/lib/src/runner/compiler_pool.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.9
 
 import 'dart:async';
 import 'dart:convert';
@@ -42,7 +44,7 @@
   final List<String> _extraArgs;
 
   /// Creates a compiler pool that multiple instances of `dart2js` at once.
-  CompilerPool([Iterable<String> extraArgs])
+  CompilerPool([Iterable<String> /*?*/ extraArgs])
       : _pool = Pool(Configuration.current.concurrency),
         _extraArgs = extraArgs?.toList() ?? const [];
 
@@ -64,6 +66,8 @@
         if (Platform.isWindows) dart2jsPath += '.bat';
 
         var args = [
+          for (var experiment in _enabledExperiments)
+            '--enable-experiment=$experiment',
           '--enable-asserts',
           wrapperPath,
           '--out=$jsPath',
@@ -139,3 +143,23 @@
     });
   }
 }
+
+/// Parses and returns the currently enabled experiments from
+/// [Platform.executableArguments].
+final List<String> _enabledExperiments = () {
+  var experiments = <String>[];
+  var itr = Platform.executableArguments.iterator;
+  while (itr.moveNext()) {
+    var arg = itr.current;
+    if (arg == '--enable-experiment') {
+      if (!itr.moveNext()) break;
+      experiments.add(itr.current);
+    } else if (arg.startsWith('--enable-experiment=')) {
+      var parts = arg.split('=');
+      if (parts.length == 2) {
+        experiments.addAll(parts[1].split(','));
+      }
+    }
+  }
+  return experiments;
+}();
diff --git a/pkgs/test_core/lib/src/runner/configuration.dart b/pkgs/test_core/lib/src/runner/configuration.dart
index cd4b1f6..b5a6ee8 100644
--- a/pkgs/test_core/lib/src/runner/configuration.dart
+++ b/pkgs/test_core/lib/src/runner/configuration.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.9
 
 import 'dart:async';
 
@@ -41,40 +43,40 @@
 
   /// Whether `--help` was passed.
   bool get help => _help ?? false;
-  final bool _help;
+  final bool /*?*/ _help;
 
   /// Custom HTML template file.
-  final String customHtmlTemplatePath;
+  final String /*?*/ customHtmlTemplatePath;
 
   /// Whether `--version` was passed.
   bool get version => _version ?? false;
-  final bool _version;
+  final bool /*?*/ _version;
 
   /// Whether to pause for debugging after loading each test suite.
   bool get pauseAfterLoad => _pauseAfterLoad ?? false;
-  final bool _pauseAfterLoad;
+  final bool /*?*/ _pauseAfterLoad;
 
   /// Whether to run browsers in their respective debug modes
   bool get debug => pauseAfterLoad || (_debug ?? false) || _coverage != null;
-  final bool _debug;
+  final bool /*?*/ _debug;
 
   /// The output folder for coverage gathering
-  String get coverage => _coverage;
-  final String _coverage;
+  String /*?*/ get coverage => _coverage;
+  final String /*?*/ _coverage;
 
   /// The path to the file from which to load more configuration information.
   ///
   /// This is *not* resolved automatically.
   String get configurationPath => _configurationPath ?? 'dart_test.yaml';
-  final String _configurationPath;
+  final String /*?*/ _configurationPath;
 
   /// The path to dart2js.
   String get dart2jsPath => _dart2jsPath ?? p.join(sdkDir, 'bin', 'dart2js');
-  final String _dart2jsPath;
+  final String /*?*/ _dart2jsPath;
 
   /// The name of the reporter to use to display results.
   String get reporter => _reporter ?? defaultReporter;
-  final String _reporter;
+  final String /*?*/ _reporter;
 
   /// The map of file reporters where the key is the name of the reporter and
   /// the value is the filepath to which its output should be written.
@@ -82,20 +84,20 @@
 
   /// Whether to disable retries of tests.
   bool get noRetry => _noRetry ?? false;
-  final bool _noRetry;
+  final bool /*?*/ _noRetry;
 
   /// The URL for the `pub serve` instance from which to load tests, or `null`
   /// if tests should be loaded from the filesystem.
-  final Uri pubServeUrl;
+  final Uri /*?*/ pubServeUrl;
 
   /// Whether to use command-line color escapes.
   bool get color => _color ?? canUseSpecialChars;
-  final bool _color;
+  final bool /*?*/ _color;
 
   /// How many tests to run concurrently.
   int get concurrency =>
       pauseAfterLoad ? 1 : (_concurrency ?? defaultConcurrency);
-  final int _concurrency;
+  final int /*?*/ _concurrency;
 
   /// The index of the current shard, if sharding is in use, or `null` if it's
   /// not.
@@ -111,25 +113,25 @@
   /// * Across all shards, each test must be run exactly once.
   ///
   /// In addition, tests should be balanced across shards as much as possible.
-  final int shardIndex;
+  final int /*?*/ shardIndex;
 
   /// The total number of shards, if sharding is in use, or `null` if it's not.
   ///
   /// See [shardIndex] for details.
-  final int totalShards;
+  final int /*?*/ totalShards;
 
   /// The list of packages to fold when producing [StackTrace]s.
   Set<String> get foldTraceExcept => _foldTraceExcept ?? {};
-  final Set<String> _foldTraceExcept;
+  final Set<String> /*?*/ _foldTraceExcept;
 
   /// If non-empty, all packages not in this list will be folded when producing
   /// [StackTrace]s.
   Set<String> get foldTraceOnly => _foldTraceOnly ?? {};
-  final Set<String> _foldTraceOnly;
+  final Set<String> /*?*/ _foldTraceOnly;
 
   /// The paths from which to load tests.
   List<String> get paths => _paths ?? ['test'];
-  final List<String> _paths;
+  final List<String> /*?*/ _paths;
 
   /// Whether the load paths were passed explicitly or the default was used.
   bool get explicitPaths => _paths != null;
@@ -138,7 +140,7 @@
   ///
   /// This is used to find tests within a directory.
   Glob get filename => _filename ?? defaultFilename;
-  final Glob _filename;
+  final Glob /*?*/ _filename;
 
   /// The set of presets to use.
   ///
@@ -149,19 +151,11 @@
   final Set<String> chosenPresets;
 
   /// The set of tags that have been declared in any way in this configuration.
-  Set<String> get knownTags {
-    if (_knownTags != null) return _knownTags;
-
-    var known = suiteDefaults.knownTags.toSet();
-    for (var configuration in presets.values) {
-      known.addAll(configuration.knownTags);
-    }
-
-    _knownTags = UnmodifiableSetView(known);
-    return _knownTags;
-  }
-
-  Set<String> _knownTags;
+  Set<String> get knownTags => _knownTags ??= UnmodifiableSetView({
+        ...suiteDefaults.knownTags,
+        for (var configuration in presets.values) ...configuration.knownTags
+      });
+  Set<String> /*?*/ _knownTags;
 
   /// Configuration presets.
   ///
@@ -176,19 +170,11 @@
   /// All preset names that are known to be valid.
   ///
   /// This includes presets that have already been resolved.
-  Set<String> get knownPresets {
-    if (_knownPresets != null) return _knownPresets;
-
-    var known = presets.keys.toSet();
-    for (var configuration in presets.values) {
-      known.addAll(configuration.knownPresets);
-    }
-
-    _knownPresets = UnmodifiableSetView(known);
-    return _knownPresets;
-  }
-
-  Set<String> _knownPresets;
+  Set<String> get knownPresets => _knownPresets ??= UnmodifiableSetView({
+        ...presets.keys,
+        for (var configuration in presets.values) ...configuration.knownPresets
+      });
+  Set<String> /*?*/ _knownPresets;
 
   /// Built-in runtimes whose settings are overridden by the user.
   final Map<String, RuntimeSettings> overrideRuntimes;
@@ -204,7 +190,7 @@
   ///
   /// The current configuration is set using [asCurrent].
   static Configuration get current =>
-      Zone.current[_currentKey] as Configuration ?? Configuration();
+      Zone.current[_currentKey] as Configuration /*?*/ ?? Configuration();
 
   /// Parses the configuration from [args].
   ///
@@ -231,57 +217,57 @@
   ///
   /// Throws a [FormatException] if its contents are invalid.
   factory Configuration.loadFromString(String source,
-          {bool global = false, Uri sourceUrl}) =>
+          {bool global = false, Uri /*?*/ sourceUrl}) =>
       loadFromString(source, global: global, sourceUrl: sourceUrl);
 
   factory Configuration(
-      {bool help,
-      String customHtmlTemplatePath,
-      bool version,
-      bool pauseAfterLoad,
-      bool debug,
-      bool color,
-      String configurationPath,
-      String dart2jsPath,
-      String reporter,
-      Map<String, String> fileReporters,
-      String coverage,
-      int pubServePort,
-      int concurrency,
-      int shardIndex,
-      int totalShards,
-      Iterable<String> paths,
-      Iterable<String> foldTraceExcept,
-      Iterable<String> foldTraceOnly,
-      Glob filename,
-      Iterable<String> chosenPresets,
-      Map<String, Configuration> presets,
-      Map<String, RuntimeSettings> overrideRuntimes,
-      Map<String, CustomRuntime> defineRuntimes,
-      bool noRetry,
+      {bool /*?*/ help,
+      String /*?*/ customHtmlTemplatePath,
+      bool /*?*/ version,
+      bool /*?*/ pauseAfterLoad,
+      bool /*?*/ debug,
+      bool /*?*/ color,
+      String /*?*/ configurationPath,
+      String /*?*/ dart2jsPath,
+      String /*?*/ reporter,
+      Map<String, String> /*?*/ fileReporters,
+      String /*?*/ coverage,
+      int /*?*/ pubServePort,
+      int /*?*/ concurrency,
+      int /*?*/ shardIndex,
+      int /*?*/ totalShards,
+      Iterable<String> /*?*/ paths,
+      Iterable<String> /*?*/ foldTraceExcept,
+      Iterable<String> /*?*/ foldTraceOnly,
+      Glob /*?*/ filename,
+      Iterable<String> /*?*/ chosenPresets,
+      Map<String, Configuration> /*?*/ presets,
+      Map<String, RuntimeSettings> /*?*/ overrideRuntimes,
+      Map<String, CustomRuntime> /*?*/ defineRuntimes,
+      bool /*?*/ noRetry,
 
       // Suite-level configuration
-      bool jsTrace,
-      bool runSkipped,
-      Iterable<String> dart2jsArgs,
-      String precompiledPath,
-      Iterable<Pattern> patterns,
-      Iterable<RuntimeSelection> runtimes,
-      BooleanSelector includeTags,
-      BooleanSelector excludeTags,
-      Map<BooleanSelector, SuiteConfiguration> tags,
-      Map<PlatformSelector, SuiteConfiguration> onPlatform,
-      int testRandomizeOrderingSeed,
+      bool /*?*/ jsTrace,
+      bool /*?*/ runSkipped,
+      Iterable<String> /*?*/ dart2jsArgs,
+      String /*?*/ precompiledPath,
+      Iterable<Pattern> /*?*/ patterns,
+      Iterable<RuntimeSelection> /*?*/ runtimes,
+      BooleanSelector /*?*/ includeTags,
+      BooleanSelector /*?*/ excludeTags,
+      Map<BooleanSelector, SuiteConfiguration> /*?*/ tags,
+      Map<PlatformSelector, SuiteConfiguration> /*?*/ onPlatform,
+      int /*?*/ testRandomizeOrderingSeed,
 
       // Test-level configuration
-      Timeout timeout,
-      bool verboseTrace,
-      bool chainStackTraces,
-      bool skip,
-      int retry,
-      String skipReason,
-      PlatformSelector testOn,
-      Iterable<String> addTags}) {
+      Timeout /*?*/ timeout,
+      bool /*?*/ verboseTrace,
+      bool /*?*/ chainStackTraces,
+      bool /*?*/ skip,
+      int /*?*/ retry,
+      String /*?*/ skipReason,
+      PlatformSelector /*?*/ testOn,
+      Iterable<String> /*?*/ addTags}) {
     var chosenPresetSet = chosenPresets?.toSet();
     var configuration = Configuration._(
         help: help,
@@ -333,8 +319,8 @@
     return configuration._resolvePresets();
   }
 
-  static Map<String, Configuration> _withChosenPresets(
-      Map<String, Configuration> map, Set<String> chosenPresets) {
+  static Map<String, Configuration> /*?*/ _withChosenPresets(
+      Map<String, Configuration> /*?*/ map, Set<String> /*?*/ chosenPresets) {
     if (map == null || chosenPresets == null) return map;
     return map.map((key, config) => MapEntry(
         key,
@@ -346,31 +332,31 @@
   ///
   /// Unlike [new Configuration], this assumes [presets] is already resolved.
   Configuration._(
-      {bool help,
-      String customHtmlTemplatePath,
-      bool version,
-      bool pauseAfterLoad,
-      bool debug,
-      bool color,
-      String configurationPath,
-      String dart2jsPath,
-      String reporter,
-      Map<String, String> fileReporters,
-      String coverage,
-      int pubServePort,
-      int concurrency,
+      {bool /*?*/ help,
+      String /*?*/ customHtmlTemplatePath,
+      bool /*?*/ version,
+      bool /*?*/ pauseAfterLoad,
+      bool /*?*/ debug,
+      bool /*?*/ color,
+      String /*?*/ configurationPath,
+      String /*?*/ dart2jsPath,
+      String /*?*/ reporter,
+      Map<String, String> /*?*/ fileReporters,
+      String /*?*/ coverage,
+      int /*?*/ pubServePort,
+      int /*?*/ concurrency,
       this.shardIndex,
       this.totalShards,
-      Iterable<String> paths,
-      Iterable<String> foldTraceExcept,
-      Iterable<String> foldTraceOnly,
-      Glob filename,
-      Iterable<String> chosenPresets,
-      Map<String, Configuration> presets,
-      Map<String, RuntimeSettings> overrideRuntimes,
-      Map<String, CustomRuntime> defineRuntimes,
-      bool noRetry,
-      SuiteConfiguration suiteDefaults})
+      Iterable<String> /*?*/ paths,
+      Iterable<String> /*?*/ foldTraceExcept,
+      Iterable<String> /*?*/ foldTraceOnly,
+      Glob /*?*/ filename,
+      Iterable<String> /*?*/ chosenPresets,
+      Map<String, Configuration> /*?*/ presets,
+      Map<String, RuntimeSettings> /*?*/ overrideRuntimes,
+      Map<String, CustomRuntime> /*?*/ defineRuntimes,
+      bool /*?*/ noRetry,
+      SuiteConfiguration /*?*/ suiteDefaults})
       : _help = help,
         customHtmlTemplatePath = customHtmlTemplatePath,
         _version = version,
@@ -423,7 +409,7 @@
   /// Returns an unmodifiable copy of [input].
   ///
   /// If [input] is `null` or empty, this returns `null`.
-  static List<T> _list<T>(Iterable<T> input) {
+  static List<T> /*?*/ _list<T>(Iterable<T> /*?*/ input) {
     if (input == null) return null;
     var list = List<T>.unmodifiable(input);
     if (list.isEmpty) return null;
@@ -431,7 +417,9 @@
   }
 
   /// Returns a set from [input].
-  static Set<T> _set<T>(Iterable<T> input) {
+  ///
+  /// If [input] is `null` or empty, this returns `null`.
+  static Set<T> /*?*/ _set<T>(Iterable<T> /*?*/ input) {
     if (input == null) return null;
     var set = Set<T>.from(input);
     if (set.isEmpty) return null;
@@ -439,7 +427,7 @@
   }
 
   /// Returns an unmodifiable copy of [input] or an empty unmodifiable map.
-  static Map<K, V> _map<K, V>(Map<K, V> input) {
+  static Map<K, V> _map<K, V>(Map<K, V> /*?*/ input) {
     input ??= {};
     return Map.unmodifiable(input);
   }
@@ -523,7 +511,7 @@
             value: (settings1, settings2) => RuntimeSettings(
                 settings1.identifier,
                 settings1.identifierSpan,
-                settings1.settings.toList()..addAll(settings2.settings))),
+                [...settings1.settings, ...settings2.settings])),
         defineRuntimes:
             mergeUnmodifiableMaps(defineRuntimes, other.defineRuntimes),
         noRetry: other._noRetry ?? _noRetry,
@@ -541,50 +529,50 @@
   /// Note that unlike [merge], this has no merging behavior—the old value is
   /// always replaced by the new one.
   Configuration change(
-      {bool help,
-      String customHtmlTemplatePath,
-      bool version,
-      bool pauseAfterLoad,
-      bool color,
-      String configurationPath,
-      String dart2jsPath,
-      String reporter,
-      Map<String, String> fileReporters,
-      int pubServePort,
-      int concurrency,
-      int shardIndex,
-      int totalShards,
-      Iterable<String> paths,
-      Iterable<String> exceptPackages,
-      Iterable<String> onlyPackages,
-      Glob filename,
-      Iterable<String> chosenPresets,
-      Map<String, Configuration> presets,
-      Map<String, RuntimeSettings> overrideRuntimes,
-      Map<String, CustomRuntime> defineRuntimes,
-      bool noRetry,
+      {bool /*?*/ help,
+      String /*?*/ customHtmlTemplatePath,
+      bool /*?*/ version,
+      bool /*?*/ pauseAfterLoad,
+      bool /*?*/ color,
+      String /*?*/ configurationPath,
+      String /*?*/ dart2jsPath,
+      String /*?*/ reporter,
+      Map<String, String> /*?*/ fileReporters,
+      int /*?*/ pubServePort,
+      int /*?*/ concurrency,
+      int /*?*/ shardIndex,
+      int /*?*/ totalShards,
+      Iterable<String> /*?*/ paths,
+      Iterable<String> /*?*/ exceptPackages,
+      Iterable<String> /*?*/ onlyPackages,
+      Glob /*?*/ filename,
+      Iterable<String> /*?*/ chosenPresets,
+      Map<String, Configuration> /*?*/ presets,
+      Map<String, RuntimeSettings> /*?*/ overrideRuntimes,
+      Map<String, CustomRuntime> /*?*/ defineRuntimes,
+      bool /*?*/ noRetry,
 
       // Suite-level configuration
-      bool jsTrace,
-      bool runSkipped,
-      Iterable<String> dart2jsArgs,
-      String precompiledPath,
-      Iterable<Pattern> patterns,
-      Iterable<RuntimeSelection> runtimes,
-      BooleanSelector includeTags,
-      BooleanSelector excludeTags,
-      Map<BooleanSelector, SuiteConfiguration> tags,
-      Map<PlatformSelector, SuiteConfiguration> onPlatform,
-      int testRandomizeOrderingSeed,
+      bool /*?*/ jsTrace,
+      bool /*?*/ runSkipped,
+      Iterable<String> /*?*/ dart2jsArgs,
+      String /*?*/ precompiledPath,
+      Iterable<Pattern> /*?*/ patterns,
+      Iterable<RuntimeSelection> /*?*/ runtimes,
+      BooleanSelector /*?*/ includeTags,
+      BooleanSelector /*?*/ excludeTags,
+      Map<BooleanSelector, SuiteConfiguration> /*?*/ tags,
+      Map<PlatformSelector, SuiteConfiguration> /*?*/ onPlatform,
+      int /*?*/ testRandomizeOrderingSeed,
 
       // Test-level configuration
-      Timeout timeout,
-      bool verboseTrace,
-      bool chainStackTraces,
-      bool skip,
-      String skipReason,
-      PlatformSelector testOn,
-      Iterable<String> addTags}) {
+      Timeout /*?*/ timeout,
+      bool /*?*/ verboseTrace,
+      bool /*?*/ chainStackTraces,
+      bool /*?*/ skip,
+      String /*?*/ skipReason,
+      PlatformSelector /*?*/ testOn,
+      Iterable<String> /*?*/ addTags}) {
     var config = Configuration._(
         help: help ?? _help,
         customHtmlTemplatePath:
@@ -657,7 +645,7 @@
     // Make sure the configuration knows about presets that were selected and
     // thus removed from [newPresets].
     result._knownPresets =
-        UnmodifiableSetView(result.knownPresets.toSet()..addAll(presets.keys));
+        UnmodifiableSetView({...result.knownPresets, ...presets.keys});
 
     return result;
   }
diff --git a/pkgs/test_core/lib/src/runner/configuration/args.dart b/pkgs/test_core/lib/src/runner/configuration/args.dart
index 02e5384..ab7b79f 100644
--- a/pkgs/test_core/lib/src/runner/configuration/args.dart
+++ b/pkgs/test_core/lib/src/runner/configuration/args.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.9
 
 import 'dart:io';
 import 'dart:math';
@@ -25,15 +27,15 @@
   if (!Platform.isWindows) allRuntimes.remove(Runtime.internetExplorer);
 
   parser.addFlag('help',
-      abbr: 'h', negatable: false, help: 'Shows this usage information.');
+      abbr: 'h', negatable: false, help: 'Show this usage information.');
   parser.addFlag('version',
-      negatable: false, help: "Shows the package's version.");
+      negatable: false, help: 'Show the package:test version.');
 
   // Note that defaultsTo declarations here are only for documentation purposes.
   // We pass null instead of the default so that it merges properly with the
   // config file.
 
-  parser.addSeparator('======== Selecting Tests');
+  parser.addSeparator('Selecting Tests:');
   parser.addMultiOption('name',
       abbr: 'n',
       help: 'A substring of the name of the test to run.\n'
@@ -58,7 +60,7 @@
   parser.addFlag('run-skipped',
       help: 'Run skipped tests instead of skipping them.');
 
-  parser.addSeparator('======== Running Tests');
+  parser.addSeparator('Running Tests:');
 
   // The UI term "platform" corresponds with the implementation term "runtime".
   // The [Runtime] class used to be called [TestPlatform], but it was changed to
@@ -87,28 +89,29 @@
       help: 'The default test timeout. For example: 15s, 2x, none',
       defaultsTo: '30s');
   parser.addFlag('pause-after-load',
-      help: 'Pauses for debugging before any tests execute.\n'
+      help: 'Pause for debugging before any tests execute.\n'
           'Implies --concurrency=1, --debug, and --timeout=none.\n'
           'Currently only supported for browser tests.',
       negatable: false);
   parser.addFlag('debug',
-      help: 'Runs the VM and Chrome tests in debug mode.', negatable: false);
+      help: 'Run the VM and Chrome tests in debug mode.', negatable: false);
   parser.addOption('coverage',
-      help: 'Gathers coverage and outputs it to the specified directory.\n'
+      help: 'Gather coverage and output it to the specified directory.\n'
           'Implies --debug.',
       valueHelp: 'directory');
   parser.addFlag('chain-stack-traces',
-      help: 'Chained stack traces to provide greater exception details\n'
+      help: 'Use chained stack traces to provide greater exception details\n'
           'especially for asynchronous code. It may be useful to disable\n'
           'to provide improved test performance but at the cost of\n'
           'debuggability.',
       defaultsTo: true);
   parser.addFlag('no-retry',
-      help: "Don't re-run tests that have retry set.",
+      help: "Don't rerun tests that have retry set.",
       defaultsTo: false,
       negatable: false);
   parser.addOption('test-randomize-ordering-seed',
-      help: 'The seed to randomize the execution order of test cases.\n'
+      help: 'Use the specified seed to randomize the execution order of test'
+          ' cases.\n'
           'Must be a 32bit unsigned integer or "random".\n'
           'If "random", pick a random seed to use.\n'
           'If not passed, do not randomize test case execution order.');
@@ -118,25 +121,24 @@
     reporterDescriptions[reporter] = allReporters[reporter].description;
   }
 
-  parser.addSeparator('======== Output');
+  parser.addSeparator('Output:');
   parser.addOption('reporter',
       abbr: 'r',
-      help: 'The runner used to print test results.',
+      help: 'Set how to print test results.',
       defaultsTo: defaultReporter,
       allowed: reporterDescriptions.keys.toList(),
       allowedHelp: reporterDescriptions);
   parser.addOption('file-reporter',
-      help: 'The reporter used to write test results to a file.\n'
+      help: 'Set the reporter used to write test results to a file.\n'
           'Should be in the form <reporter>:<filepath>, '
-          'e.g. "json:reports/tests.json"');
+          'Example: "json:reports/tests.json"');
   parser.addFlag('verbose-trace',
-      negatable: false,
-      help: 'Whether to emit stack traces with core library frames.');
+      negatable: false, help: 'Emit stack traces with core library frames.');
   parser.addFlag('js-trace',
       negatable: false,
-      help: 'Whether to emit raw JavaScript stack traces for browser tests.');
+      help: 'Emit raw JavaScript stack traces for browser tests.');
   parser.addFlag('color',
-      help: 'Whether to use terminal colors.\n(auto-detected by default)');
+      help: 'Use terminal colors.\n(auto-detected by default)');
 
   /// The following options are used only by the internal Google test runner.
   /// They're hidden and not supported as stable API surface outside Google.
@@ -183,8 +185,8 @@
         .toList()
           ..addAll(_options['plain-name'] as List<String>);
 
-    var includeTagSet = Set.from(_options['tags'] as Iterable ?? [])
-      ..addAll(_options['tag'] as Iterable ?? []);
+    var includeTagSet = Set.from(_options['tags'] as Iterable /*?*/ ?? [])
+      ..addAll(_options['tag'] as Iterable /*?*/ ?? []);
 
     var includeTags = includeTagSet.fold(BooleanSelector.all,
         (BooleanSelector selector, tag) {
@@ -192,8 +194,9 @@
       return selector.intersection(tagSelector);
     });
 
-    var excludeTagSet = Set.from(_options['exclude-tags'] as Iterable ?? [])
-      ..addAll(_options['exclude-tag'] as Iterable ?? []);
+    var excludeTagSet =
+        Set.from(_options['exclude-tags'] as Iterable /*?*/ ?? [])
+          ..addAll(_options['exclude-tag'] as Iterable /*?*/ ?? []);
 
     var excludeTags = excludeTagSet.fold(BooleanSelector.none,
         (BooleanSelector selector, tag) {
@@ -220,9 +223,8 @@
       var seed = value == 'random'
           ? Random().nextInt(4294967295)
           : int.parse(value).toUnsigned(32);
-      if (seed != null) {
-        print('Shuffling test order with --test-randomize-ordering-seed=$seed');
-      }
+      print('Shuffling test order with --test-randomize-ordering-seed=$seed');
+
       return seed;
     });
 
@@ -277,12 +279,12 @@
   /// If the user hasn't explicitly chosen a value, we want to pass null values
   /// to [new Configuration] so that it considers those fields unset when
   /// merging with configuration from the config file.
-  T _ifParsed<T>(String name) =>
+  T /*?*/ _ifParsed<T>(String name) =>
       _options.wasParsed(name) ? _options[name] as T : null;
 
   /// Runs [parse] on the value of the option [name], and wraps any
   /// [FormatException] it throws with additional information.
-  T _parseOption<T>(String name, T Function(String) parse) {
+  T /*?*/ _parseOption<T>(String name, T Function(String) parse) {
     if (!_options.wasParsed(name)) return null;
 
     var value = _options[name];
@@ -291,7 +293,7 @@
     return _wrapFormatException(name, () => parse(value as String));
   }
 
-  Map<String, String> _parseFileReporterOption() =>
+  Map<String, String> /*?*/ _parseFileReporterOption() =>
       _parseOption('file-reporter', (value) {
         if (!value.contains(':')) {
           throw FormatException(
diff --git a/pkgs/test_core/lib/src/runner/configuration/custom_runtime.dart b/pkgs/test_core/lib/src/runner/configuration/custom_runtime.dart
index 0c99191..65384ba 100644
--- a/pkgs/test_core/lib/src/runner/configuration/custom_runtime.dart
+++ b/pkgs/test_core/lib/src/runner/configuration/custom_runtime.dart
@@ -1,6 +1,8 @@
 // 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.
+//
+// @dart=2.9
 
 import 'package:source_span/source_span.dart';
 import 'package:yaml/yaml.dart';
diff --git a/pkgs/test_core/lib/src/runner/configuration/load.dart b/pkgs/test_core/lib/src/runner/configuration/load.dart
index 208a3c5..8c5419b 100644
--- a/pkgs/test_core/lib/src/runner/configuration/load.dart
+++ b/pkgs/test_core/lib/src/runner/configuration/load.dart
@@ -1,11 +1,14 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.9
 
 import 'dart:io';
 
 import 'package:boolean_selector/boolean_selector.dart';
 import 'package:glob/glob.dart';
+import 'package:meta/meta.dart';
 import 'package:path/path.dart' as p;
 import 'package:source_span/source_span.dart';
 import 'package:yaml/yaml.dart';
@@ -55,7 +58,7 @@
 ///
 /// Throws a [FormatException] if the configuration is invalid.
 Configuration loadFromString(String source,
-    {bool global = false, Uri sourceUrl}) {
+    {bool global = false, Uri /*?*/ sourceUrl}) {
   var document = loadYamlNode(source, sourceUrl: sourceUrl);
 
   if (document.value == null) return Configuration.empty;
@@ -140,7 +143,7 @@
               (value) => value is String);
 
           var os = OperatingSystem.find(keyNode.value as String);
-          if (os != null) return os;
+          if (os != OperatingSystem.none) return os;
 
           throw SourceSpanFormatException(
               'Invalid on_os key: No such operating system.',
@@ -184,8 +187,8 @@
     }
 
     var skipRaw = _getValue('skip', 'boolean or string',
-        (value) => value is bool || value is String);
-    String skipReason;
+        (value) => (value is bool /*?*/) || value is String /*?*/);
+    String /*?*/ skipReason;
     bool skip;
     if (skipRaw is String) {
       skipReason = skipRaw;
@@ -294,13 +297,13 @@
   Map<String, RuntimeSettings> _loadOverrideRuntimes() {
     var runtimesNode =
         _getNode('override_platforms', 'map', (value) => value is Map)
-            as YamlMap;
+            as YamlMap /*?*/;
     if (runtimesNode == null) return const {};
 
     var runtimes = <String, RuntimeSettings>{};
     runtimesNode.nodes.forEach((identifierNode, valueNode) {
-      var identifier = _parseIdentifierLike(
-          identifierNode as YamlNode, 'Platform identifier');
+      var yamlNode = identifierNode as YamlNode;
+      var identifier = _parseIdentifierLike(yamlNode, 'Platform identifier');
 
       _validate(valueNode, 'Platform definition must be a map.',
           (value) => value is Map);
@@ -309,8 +312,8 @@
       var settings = _expect(map, 'settings');
       _validate(settings, 'Must be a map.', (value) => value is Map);
 
-      runtimes[identifier] = RuntimeSettings(
-          identifier, (identifierNode as YamlNode).span, [settings as YamlMap]);
+      runtimes[identifier] =
+          RuntimeSettings(identifier, yamlNode.span, [settings as YamlMap]);
     });
     return runtimes;
   }
@@ -411,13 +414,14 @@
   /// Loads the `define_platforms` field.
   Map<String, CustomRuntime> _loadDefineRuntimes() {
     var runtimesNode =
-        _getNode('define_platforms', 'map', (value) => value is Map) as YamlMap;
+        _getNode('define_platforms', 'map', (value) => value is Map)
+            as YamlMap /*?*/;
     if (runtimesNode == null) return const {};
 
     var runtimes = <String, CustomRuntime>{};
     runtimesNode.nodes.forEach((identifierNode, valueNode) {
-      var identifier = _parseIdentifierLike(
-          identifierNode as YamlNode, 'Platform identifier');
+      var yamlNode = identifierNode as YamlNode;
+      var identifier = _parseIdentifierLike(yamlNode, 'Platform identifier');
 
       _validate(valueNode, 'Platform definition must be a map.',
           (value) => value is Map);
@@ -433,14 +437,8 @@
       var settings = _expect(map, 'settings');
       _validate(settings, 'Must be a map.', (value) => value is Map);
 
-      runtimes[identifier] = CustomRuntime(
-          name,
-          nameNode.span,
-          identifier,
-          (identifierNode as YamlNode).span,
-          parent,
-          parentNode.span,
-          settings as YamlMap);
+      runtimes[identifier] = CustomRuntime(name, nameNode.span, identifier,
+          yamlNode.span, parent, parentNode.span, settings as YamlMap);
     });
     return runtimes;
   }
@@ -456,7 +454,7 @@
   ///
   /// If [typeTest] returns `false` for that value, instead throws an error
   /// complaining that the field is not a [typeName].
-  dynamic _getValue(
+  Object /*?*/ _getValue(
       String field, String typeName, bool Function(dynamic) typeTest) {
     var value = _document[field];
     if (value == null || typeTest(value)) return value;
@@ -467,7 +465,9 @@
   ///
   /// If [typeTest] returns `false` for that node's value, instead throws an
   /// error complaining that the field is not a [typeName].
-  YamlNode _getNode(
+  ///
+  /// Returns `null` if [field] does not have a node in [_document].
+  YamlNode /*?*/ _getNode(
       String field, String typeName, bool Function(dynamic) typeTest) {
     var node = _document.nodes[field];
     if (node == null) return null;
@@ -476,27 +476,32 @@
   }
 
   /// Asserts that [field] is an int and returns its value.
-  int _getInt(String field) =>
-      _getValue(field, 'int', (value) => value is int) as int;
+  int /*?*/ _getInt(String field) =>
+      _getValue(field, 'int', (value) => value is int /*?*/) as int /*?*/;
 
   /// Asserts that [field] is a non-negative int and returns its value.
-  int _getNonNegativeInt(String field) => _getValue(
-      field, 'non-negative int', (value) => value is int && value >= 0) as int;
+  int /*?*/ _getNonNegativeInt(String field) =>
+      _getValue(field, 'non-negative int', (value) {
+        if (value == null) return true;
+        return value is int && value >= 0;
+      }) as int /*?*/;
 
   /// Asserts that [field] is a boolean and returns its value.
-  bool _getBool(String field) =>
-      _getValue(field, 'boolean', (value) => value is bool) as bool;
+  bool /*?*/ _getBool(String field) =>
+      _getValue(field, 'boolean', (value) => value is bool /*?*/) as bool /*?*/;
 
   /// Asserts that [field] is a string and returns its value.
-  String _getString(String field) =>
-      _getValue(field, 'string', (value) => value is String) as String;
+  String /*?*/ _getString(String field) =>
+      _getValue(field, 'string', (value) => value is String /*?*/)
+          as String /*?*/;
 
   /// Asserts that [field] is a list and runs [forElement] for each element it
   /// contains.
   ///
   /// Returns a list of values returned by [forElement].
   List<T> _getList<T>(String field, T Function(YamlNode) forElement) {
-    var node = _getNode(field, 'list', (value) => value is List) as YamlList;
+    var node =
+        _getNode(field, 'list', (value) => value is List) as YamlList /*?*/;
     if (node == null) return [];
     return node.nodes.map(forElement).toList();
   }
@@ -506,8 +511,8 @@
   /// Returns a map with the keys and values returned by [key] and [value]. Each
   /// of these defaults to asserting that the value is a string.
   Map<K, V> _getMap<K, V>(String field,
-      {K Function(YamlNode) key, V Function(YamlNode) value}) {
-    var node = _getNode(field, 'map', (value) => value is Map) as YamlMap;
+      {K Function(YamlNode) /*?*/ key, V Function(YamlNode) /*?*/ value}) {
+    var node = _getNode(field, 'map', (value) => value is Map) as YamlMap /*?*/;
     if (node == null) return {};
 
     key ??= (keyNode) {
@@ -538,11 +543,11 @@
   }
 
   /// Parses [node]'s value as a boolean selector.
-  BooleanSelector _parseBooleanSelector(String name) =>
+  BooleanSelector /*?*/ _parseBooleanSelector(String name) =>
       _parseValue(name, (value) => BooleanSelector.parse(value));
 
   /// Parses [node]'s value as a platform selector.
-  PlatformSelector _parsePlatformSelector(String field) {
+  PlatformSelector /*?*/ _parsePlatformSelector(String field) {
     var node = _document.nodes[field];
     if (node == null) return null;
     return _parseNode(
@@ -570,7 +575,7 @@
   ///
   /// If [parse] throws a [FormatException], it's wrapped to include [field]'s
   /// span.
-  T _parseValue<T>(String field, T Function(String) parse) {
+  T /*?*/ _parseValue<T>(String field, T Function(String) parse) {
     var node = _document.nodes[field];
     if (node == null) return null;
     return _parseNode(node, field, parse);
@@ -581,7 +586,8 @@
   /// [name] is the name of the field, which is used for error-handling.
   /// [runnerConfig] controls whether runner configuration is allowed in the
   /// nested configuration. It defaults to [_runnerConfig].
-  Configuration _nestedConfig(YamlNode node, String name, {bool runnerConfig}) {
+  Configuration _nestedConfig(YamlNode /*?*/ node, String name,
+      {bool /*?*/ runnerConfig}) {
     if (node == null || node.value == null) return Configuration.empty;
 
     _validate(node, '$name must be a map.', (value) => value is Map);
@@ -643,6 +649,7 @@
   }
 
   /// Throws a [SourceSpanFormatException] with [message] about [field].
+  @alwaysThrows
   void _error(String message, String field) {
     throw SourceSpanFormatException(
         message, _document.nodes[field].span, _source);
diff --git a/pkgs/test_core/lib/src/runner/configuration/reporters.dart b/pkgs/test_core/lib/src/runner/configuration/reporters.dart
index 94a4ca1..231b7ba 100644
--- a/pkgs/test_core/lib/src/runner/configuration/reporters.dart
+++ b/pkgs/test_core/lib/src/runner/configuration/reporters.dart
@@ -1,6 +1,8 @@
 // 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.
+//
+// @dart=2.9
 
 import 'dart:collection';
 import 'dart:io';
@@ -36,10 +38,16 @@
           printPath: config.paths.length > 1 ||
               Directory(config.paths.single).existsSync(),
           printPlatform: config.suiteDefaults.runtimes.length > 1)),
-  'compact': ReporterDetails('A single line, updated continuously.',
-      (_, engine, sink) => CompactReporter.watch(engine, sink)),
+  'compact': ReporterDetails(
+      'A single line, updated continuously.',
+      (config, engine, sink) => CompactReporter.watch(engine, sink,
+          color: config.color,
+          printPath: config.paths.length > 1 ||
+              Directory(config.paths.single).existsSync(),
+          printPlatform: config.suiteDefaults.runtimes.length > 1)),
   'json': ReporterDetails(
-      'A machine-readable format (see https://bit.ly/2Z7J0OH).',
+      'A machine-readable format (see '
+      'https://dart.dev/go/test-docs/json_reporter.md).',
       (_, engine, sink) => JsonReporter.watch(engine, sink)),
 };
 
diff --git a/pkgs/test_core/lib/src/runner/configuration/runtime_settings.dart b/pkgs/test_core/lib/src/runner/configuration/runtime_settings.dart
index 6e910a3..a798499 100644
--- a/pkgs/test_core/lib/src/runner/configuration/runtime_settings.dart
+++ b/pkgs/test_core/lib/src/runner/configuration/runtime_settings.dart
@@ -1,6 +1,8 @@
 // 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.
+//
+// @dart=2.9
 
 import 'package:source_span/source_span.dart';
 import 'package:yaml/yaml.dart';
diff --git a/pkgs/test_core/lib/src/runner/configuration/values.dart b/pkgs/test_core/lib/src/runner/configuration/values.dart
index 6e2f50a..360a503 100644
--- a/pkgs/test_core/lib/src/runner/configuration/values.dart
+++ b/pkgs/test_core/lib/src/runner/configuration/values.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.9
 
 import 'dart:io';
 import 'dart:math' as math;
diff --git a/pkgs/test_core/lib/src/runner/console.dart b/pkgs/test_core/lib/src/runner/console.dart
index 1592d6d..cf59dac 100644
--- a/pkgs/test_core/lib/src/runner/console.dart
+++ b/pkgs/test_core/lib/src/runner/console.dart
@@ -16,7 +16,7 @@
   final _commands = <String, _Command>{};
 
   /// The pending next line of standard input, if we're waiting on one.
-  CancelableOperation _nextLine;
+  CancelableOperation? _nextLine;
 
   /// Whether the console is currently running.
   bool _running = false;
@@ -66,7 +66,7 @@
       while (_running) {
         stdout.write('> ');
         _nextLine = stdinLines.cancelable((queue) => queue.next);
-        var commandName = await _nextLine.value;
+        var commandName = await _nextLine!.value;
         _nextLine = null;
 
         var command = _commands[commandName];
@@ -86,7 +86,7 @@
     _running = false;
     if (_nextLine != null) {
       stdout.writeln();
-      _nextLine.cancel();
+      _nextLine!.cancel();
     }
   }
 
diff --git a/pkgs/test_core/lib/src/runner/debugger.dart b/pkgs/test_core/lib/src/runner/debugger.dart
index 524f08e..30daef7 100644
--- a/pkgs/test_core/lib/src/runner/debugger.dart
+++ b/pkgs/test_core/lib/src/runner/debugger.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.9
 
 import 'dart:async';
 
@@ -27,7 +29,7 @@
 /// any resources it allocated.
 CancelableOperation debug(
     Engine engine, Reporter reporter, LoadSuite loadSuite) {
-  _Debugger debugger;
+  _Debugger /*?*/ debugger;
   var canceled = false;
   return CancelableOperation.fromFuture(() async {
     // Make the underlying suite null so that the engine doesn't start running
@@ -40,8 +42,7 @@
     var suite = await loadSuite.suite;
     if (canceled || suite == null) return;
 
-    debugger = _Debugger(engine, reporter, suite);
-    await debugger.run();
+    await (debugger = _Debugger(engine, reporter, suite)).run();
   }(), onCancel: () {
     canceled = true;
     // Make sure the load test finishes so the engine can close.
@@ -77,10 +78,10 @@
   final _pauseCompleter = CancelableCompleter();
 
   /// The subscription to [_suite.onDebugging].
-  StreamSubscription<bool> _onDebuggingSubscription;
+  StreamSubscription<bool> /*?*/ _onDebuggingSubscription;
 
   /// The subscription to [_suite.environment.onRestart].
-  StreamSubscription _onRestartSubscription;
+  /*late final*/ StreamSubscription _onRestartSubscription;
 
   /// Whether [close] has been called.
   bool _closed = false;
@@ -124,7 +125,6 @@
   /// Prints URLs for the [_suite]'s debugger and waits for the user to tell the
   /// suite to run.
   Future _pause() async {
-    if (_suite.platform == null) return;
     if (!_suite.environment.supportsDebugging) return;
 
     try {
diff --git a/pkgs/test_core/lib/src/runner/engine.dart b/pkgs/test_core/lib/src/runner/engine.dart
index 93bd8fb..0ea98b6 100644
--- a/pkgs/test_core/lib/src/runner/engine.dart
+++ b/pkgs/test_core/lib/src/runner/engine.dart
@@ -65,10 +65,10 @@
   /// This is `null` if close hasn't been called and the tests are still
   /// running, `true` if close was called before the tests finished running, and
   /// `false` if the tests finished running before close was called.
-  bool _closedBeforeDone;
+  bool? _closedBeforeDone;
 
   /// The coverage output directory.
-  String _coverage;
+  String? _coverage;
 
   /// A pool that limits the number of test suites running concurrently.
   final Pool _runPool;
@@ -76,22 +76,22 @@
   /// A completer that will complete when this engine is unpaused.
   ///
   /// `null` if this engine is not paused.
-  Completer _pauseCompleter;
+  Completer? _pauseCompleter;
 
   /// A future that completes once this is unpaused.
   ///
   /// If this engine isn't paused, this future completes immediately.
   Future get _onUnpaused =>
-      _pauseCompleter == null ? Future.value() : _pauseCompleter.future;
+      _pauseCompleter == null ? Future.value() : _pauseCompleter!.future;
 
   /// Whether all tests passed or were skipped.
   ///
   /// This fires once all tests have completed and [suiteSink] has been closed.
   /// This will be `null` if [close] was called before all the tests finished
   /// running.
-  Future<bool> get success async {
+  Future<bool?> get success async {
     await Future.wait(<Future>[_group.future, _runPool.done], eagerError: true);
-    if (_closedBeforeDone) return null;
+    if (_closedBeforeDone!) return null;
     return liveTests.every((liveTest) =>
         liveTest.state.result.isPassing &&
         liveTest.state.status == Status.complete);
@@ -210,7 +210,7 @@
   ///
   /// [concurrency] controls how many suites are loaded and ran at once, and
   /// defaults to 1.
-  Engine({int concurrency, String coverage})
+  Engine({int? concurrency, String? coverage})
       : _runPool = Pool(concurrency ?? 1),
         _coverage = coverage {
     _group.future.then((_) {
@@ -230,7 +230,7 @@
   /// [concurrency] controls how many suites are run at once. If [runSkipped] is
   /// `true`, skipped tests will be run as though they weren't skipped.
   factory Engine.withSuites(List<RunnerSuite> suites,
-      {int concurrency, String coverage}) {
+      {int? concurrency, String? coverage}) {
     var engine = Engine(concurrency: concurrency, coverage: coverage);
     for (var suite in suites) {
       engine.suiteSink.add(suite);
@@ -244,20 +244,22 @@
   /// This returns `true` if all tests succeed, and `false` otherwise. It will
   /// only return once all tests have finished running and [suiteSink] has been
   /// closed.
-  Future<bool> run() {
+  ///
+  /// If [success] completes with `null` this will complete with `null`.
+  Future<bool?> run() {
     if (_runCalled) {
       throw StateError('Engine.run() may not be called more than once.');
     }
     _runCalled = true;
 
-    StreamSubscription subscription;
+    late StreamSubscription subscription;
     subscription = _suiteController.stream.listen((suite) {
       _addedSuites.add(suite);
       _onSuiteAddedController.add(suite);
 
       _group.add(() async {
         var resource = await _runPool.request();
-        LiveSuiteController controller;
+        LiveSuiteController? controller;
         try {
           if (suite is LoadSuite) {
             await _onUnpaused;
@@ -272,7 +274,7 @@
           if (_closed) return;
           await _runGroup(controller, controller.liveSuite.suite.group, []);
           controller.noMoreLiveTests();
-          if (_coverage != null) await writeCoverage(_coverage, controller);
+          if (_coverage != null) await writeCoverage(_coverage!, controller);
         } finally {
           resource.allowRelease(() => controller?.close());
         }
@@ -302,7 +304,7 @@
       var skipGroup = !suiteConfig.runSkipped && group.metadata.skip;
       var setUpAllSucceeded = true;
       if (!skipGroup && group.setUpAll != null) {
-        var liveTest = group.setUpAll
+        var liveTest = group.setUpAll!
             .load(suiteController.liveSuite.suite, groups: parents);
         await _runLiveTest(suiteController, liveTest, countSuccess: false);
         setUpAllSucceeded = liveTest.state.result.isPassing;
@@ -312,7 +314,7 @@
         // shuffle the group entries
         var entries = group.entries.toList();
         if (suiteConfig.testRandomizeOrderingSeed != null &&
-            suiteConfig.testRandomizeOrderingSeed > 0) {
+            suiteConfig.testRandomizeOrderingSeed! > 0) {
           entries.shuffle(Random(suiteConfig.testRandomizeOrderingSeed));
         }
 
@@ -334,7 +336,7 @@
       // Even if we're closed or setUpAll failed, we want to run all the
       // teardowns to ensure that any state is properly cleaned up.
       if (!skipGroup && group.tearDownAll != null) {
-        var liveTest = group.tearDownAll
+        var liveTest = group.tearDownAll!
             .load(suiteController.liveSuite.suite, groups: parents);
         await _runLiveTest(suiteController, liveTest, countSuccess: false);
         if (_closed) await liveTest.close();
@@ -358,7 +360,7 @@
     // non-load test to add.
     if (_active.first.suite is LoadSuite) _active.removeFirst();
 
-    StreamSubscription subscription;
+    late StreamSubscription subscription;
     subscription = liveTest.onStateChange.listen((state) {
       if (state.status != Status.complete) return;
       _active.remove(liveTest);
@@ -397,7 +399,7 @@
     await _onUnpaused;
     var skipped = LocalTest(test.name, test.metadata, () {}, trace: test.trace);
 
-    LiveTestController controller;
+    late LiveTestController controller;
     controller =
         LiveTestController(suiteController.liveSuite.suite, skipped, () {
       controller.setState(const State(Status.running, Result.success));
@@ -437,7 +439,7 @@
   /// Runs [suite] and returns the [LiveSuiteController] for the suite it loads.
   ///
   /// Returns `null` if the suite fails to load.
-  Future<LiveSuiteController> _addLoadSuite(LoadSuite suite) async {
+  Future<LiveSuiteController?> _addLoadSuite(LoadSuite suite) async {
     var controller = LiveSuiteController(suite);
     _addLiveSuite(controller.liveSuite);
 
@@ -447,7 +449,7 @@
     // Only surface the load test if there are no other tests currently running.
     if (_active.isEmpty) _active.add(liveTest);
 
-    StreamSubscription subscription;
+    late StreamSubscription subscription;
     subscription = liveTest.onStateChange.listen((state) {
       if (state.status != Status.complete) return;
       _activeLoadTests.remove(liveTest);
@@ -516,7 +518,7 @@
 
   void resume() {
     if (_pauseCompleter == null) return;
-    _pauseCompleter.complete();
+    _pauseCompleter!.complete();
     _pauseCompleter = null;
     for (var subscription in _subscriptions) {
       subscription.resume();
diff --git a/pkgs/test_core/lib/src/runner/environment.dart b/pkgs/test_core/lib/src/runner/environment.dart
index 2ddfdb3..6b28faa 100644
--- a/pkgs/test_core/lib/src/runner/environment.dart
+++ b/pkgs/test_core/lib/src/runner/environment.dart
@@ -14,11 +14,11 @@
 
   /// The URL of the Dart VM Observatory for this environment, or `null` if this
   /// environment doesn't run the Dart VM or the URL couldn't be detected.
-  Uri get observatoryUrl;
+  Uri? get observatoryUrl;
 
   /// The URL of the remote debugger for this environment, or `null` if it isn't
   /// enabled.
-  Uri get remoteDebuggerUrl;
+  Uri? get remoteDebuggerUrl;
 
   /// A broadcast stream that emits a `null` event whenever the user tells the
   /// environment to restart the current test once it's finished.
@@ -44,10 +44,10 @@
   const PluginEnvironment();
 
   @override
-  Uri get observatoryUrl => null;
+  Uri? get observatoryUrl => null;
 
   @override
-  Uri get remoteDebuggerUrl => null;
+  Uri? get remoteDebuggerUrl => null;
 
   @override
   CancelableOperation displayPause() => throw UnsupportedError(
diff --git a/pkgs/test_core/lib/src/runner/hybrid_listener.dart b/pkgs/test_core/lib/src/runner/hybrid_listener.dart
index 022405b..2ee672b 100644
--- a/pkgs/test_core/lib/src/runner/hybrid_listener.dart
+++ b/pkgs/test_core/lib/src/runner/hybrid_listener.dart
@@ -53,7 +53,7 @@
         _sendError(channel, 'Top-level hybridMain is not a function.');
         return;
       } else if (main is! Function(StreamChannel) &&
-          main is! Function(StreamChannel, Null)) {
+          main is! Function(StreamChannel, Never)) {
         _sendError(channel,
             'Top-level hybridMain() function must take one or two arguments.');
         return;
@@ -79,7 +79,7 @@
 }
 
 /// Sends a message over [channel] indicating an error from user code.
-void _sendError(StreamChannel channel, error, [StackTrace stackTrace]) {
+void _sendError(StreamChannel channel, error, [StackTrace? stackTrace]) {
   channel.sink.add({
     'type': 'error',
     'error': RemoteException.serialize(error, stackTrace ?? Chain.current())
diff --git a/pkgs/test_core/lib/src/runner/live_suite.dart b/pkgs/test_core/lib/src/runner/live_suite.dart
index e580845..6be0ab6 100644
--- a/pkgs/test_core/lib/src/runner/live_suite.dart
+++ b/pkgs/test_core/lib/src/runner/live_suite.dart
@@ -2,8 +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 'dart:async';
-
 import 'package:collection/collection.dart';
 
 import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports
@@ -63,7 +61,7 @@
         passed,
         skipped,
         failed,
-        if (active != null) {active}
+        if (active != null) {active!}
       ]);
 
   /// A stream that emits each [LiveTest] in this suite as it's about to start
@@ -86,5 +84,5 @@
   Set<LiveTest> get failed;
 
   /// The currently running test in this suite, or `null` if no test is running.
-  LiveTest get active;
+  LiveTest? get active;
 }
diff --git a/pkgs/test_core/lib/src/runner/live_suite_controller.dart b/pkgs/test_core/lib/src/runner/live_suite_controller.dart
index b8f8cc3..f11b53d 100644
--- a/pkgs/test_core/lib/src/runner/live_suite_controller.dart
+++ b/pkgs/test_core/lib/src/runner/live_suite_controller.dart
@@ -47,7 +47,7 @@
   Set<LiveTest> get failed => UnmodifiableSetView(_controller._failed);
 
   @override
-  LiveTest get active => _controller._active;
+  LiveTest? get active => _controller._active;
 
   _LiveSuite(this._controller);
 }
@@ -63,7 +63,7 @@
 class LiveSuiteController {
   /// The [LiveSuite] being controlled.
   LiveSuite get liveSuite => _liveSuite;
-  LiveSuite _liveSuite;
+  late final LiveSuite _liveSuite;
 
   /// The suite that's being run.
   final RunnerSuite _suite;
@@ -95,7 +95,7 @@
   final _failed = <LiveTest>{};
 
   /// The test exposed through [LiveTest.active].
-  LiveTest _active;
+  LiveTest? _active;
 
   /// Creates a controller for a live suite representing running the tests in
   /// [suite].
diff --git a/pkgs/test_core/lib/src/runner/load_suite.dart b/pkgs/test_core/lib/src/runner/load_suite.dart
index fd6dfb0..d975e31 100644
--- a/pkgs/test_core/lib/src/runner/load_suite.dart
+++ b/pkgs/test_core/lib/src/runner/load_suite.dart
@@ -14,11 +14,10 @@
 import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
 import 'package:test_api/src/backend/test.dart'; // ignore: implementation_imports
 import 'package:test_api/src/utils.dart'; // ignore: implementation_imports
+// ignore: deprecated_member_use
+import 'package:test_api/test_api.dart' show Timeout;
 
-import '../../test_core.dart';
-import '../util/io_stub.dart'
-    // ignore: uri_does_not_exist
-    if (dart.library.io) '../util/io.dart';
+import '../util/io_stub.dart' if (dart.library.io) '../util/io.dart';
 import 'load_exception.dart';
 import 'plugin/environment.dart';
 import 'runner_suite.dart';
@@ -64,19 +63,19 @@
   ///
   /// This will return `null` if the suite is unavailable for some reason (for
   /// example if an error occurred while loading it).
-  Future<RunnerSuite> get suite async => (await _suiteAndZone)?.first;
+  Future<RunnerSuite?> get suite async => (await _suiteAndZone)?.first;
 
   /// A future that completes to a pair of [suite] and the load test's [Zone].
   ///
   /// This will return `null` if the suite is unavailable for some reason (for
   /// example if an error occurred while loading it).
-  final Future<Pair<RunnerSuite, Zone>> _suiteAndZone;
+  final Future<Pair<RunnerSuite, Zone>?> _suiteAndZone;
 
   /// Returns the test that loads the suite.
   ///
   /// Load suites are guaranteed to only contain one test. This is a utility
   /// method for accessing it directly.
-  Test get test => this.group.entries.single as Test;
+  Test get test => group.entries.single as Test;
 
   /// Creates a load suite named [name] on [platform].
   ///
@@ -87,12 +86,12 @@
   /// If the the load test is closed before [body] is complete, it will close
   /// the suite returned by [body] once it completes.
   factory LoadSuite(String name, SuiteConfiguration config,
-      SuitePlatform platform, FutureOr<RunnerSuite> Function() body,
-      {String path}) {
+      SuitePlatform platform, FutureOr<RunnerSuite?> Function() body,
+      {String? path}) {
     var completer = Completer<Pair<RunnerSuite, Zone>>.sync();
     return LoadSuite._(name, config, platform, () {
       var invoker = Invoker.current;
-      invoker.addOutstandingCallback();
+      invoker!.addOutstandingCallback();
 
       unawaited(() async {
         var suite = await body();
@@ -124,8 +123,8 @@
   ///
   /// The suite's name will be based on [exception]'s path.
   factory LoadSuite.forLoadException(
-      LoadException exception, SuiteConfiguration config,
-      {SuitePlatform platform, StackTrace stackTrace}) {
+      LoadException exception, SuiteConfiguration? config,
+      {SuitePlatform? platform, StackTrace? stackTrace}) {
     stackTrace ??= Trace.current();
 
     return LoadSuite(
@@ -144,7 +143,7 @@
   }
 
   LoadSuite._(String name, this.config, SuitePlatform platform,
-      void Function() body, this._suiteAndZone, {String path})
+      void Function() body, this._suiteAndZone, {String? path})
       : super(
             Group.root(
                 [LocalTest(name, Metadata(timeout: Timeout(_timeout)), body)]),
@@ -173,11 +172,11 @@
       if (pair == null) return null;
 
       var zone = pair.last;
-      RunnerSuite newSuite;
+      RunnerSuite? newSuite;
       zone.runGuarded(() {
         newSuite = change(pair.first);
       });
-      return newSuite == null ? null : Pair(newSuite, zone);
+      return newSuite == null ? null : Pair(newSuite!, zone);
     }));
   }
 
@@ -185,7 +184,7 @@
   ///
   /// Rather than emitting errors through a [LiveTest], this just pipes them
   /// through the return value.
-  Future<RunnerSuite> getSuite() async {
+  Future<RunnerSuite?> getSuite() async {
     var liveTest = test.load(this);
     liveTest.onMessage.listen((message) => print(message.text));
     await liveTest.run();
@@ -199,7 +198,7 @@
 
   @override
   LoadSuite filter(bool Function(Test) callback) {
-    var filtered = this.group.filter(callback);
+    var filtered = group.filter(callback);
     filtered ??= Group.root([], metadata: metadata);
     return LoadSuite._filtered(this, filtered);
   }
diff --git a/pkgs/test_core/lib/src/runner/loader.dart b/pkgs/test_core/lib/src/runner/loader.dart
index 1e13341..9e93945 100644
--- a/pkgs/test_core/lib/src/runner/loader.dart
+++ b/pkgs/test_core/lib/src/runner/loader.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.9
 
 import 'dart:async';
 import 'dart:io';
@@ -119,17 +121,14 @@
   void _registerRuntimeOverrides() {
     for (var settings in _config.overrideRuntimes.values) {
       var runtime = _runtimesByIdentifier[settings.identifier];
-
-      // This is officially validated in [Configuration.validateRuntimes].
-      assert(runtime != null);
-
       _runtimeSettings.putIfAbsent(runtime, () => []).addAll(settings.settings);
     }
   }
 
   /// Returns the [Runtime] registered with this loader that's identified
   /// by [identifier], or `null` if none can be found.
-  Runtime findRuntime(String identifier) => _runtimesByIdentifier[identifier];
+  Runtime /*?*/ findRuntime(String identifier) =>
+      _runtimesByIdentifier[identifier];
 
   /// Loads all test suites in [dir] according to [suiteConfig].
   ///
@@ -142,14 +141,10 @@
   Stream<LoadSuite> loadDir(String dir, SuiteConfiguration suiteConfig) {
     return StreamGroup.merge(
         Directory(dir).listSync(recursive: true).map((entry) {
-      if (entry is! File) return Stream.fromIterable([]);
-
-      if (!_config.filename.matches(p.basename(entry.path))) {
-        return Stream.fromIterable([]);
-      }
-
-      if (p.split(entry.path).contains('packages')) {
-        return Stream.fromIterable([]);
+      if (entry is! File ||
+          !_config.filename.matches(p.basename(entry.path)) ||
+          p.split(entry.path).contains('packages')) {
+        return Stream.empty();
       }
 
       return loadFile(entry.path, suiteConfig);
@@ -231,7 +226,7 @@
                 {'platformVariables': _runtimeVariables.toList()});
             if (suite != null) _suites.add(suite);
             return suite;
-          } catch (error, stackTrace) {
+          } on Object catch (error, stackTrace) {
             if (retriesLeft > 0) {
               retriesLeft--;
               print('Retrying load of $path in 1s ($retriesLeft remaining)');
diff --git a/pkgs/test_core/lib/src/runner/package_version.dart b/pkgs/test_core/lib/src/runner/package_version.dart
new file mode 100644
index 0000000..788e7de
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/package_version.dart
@@ -0,0 +1,26 @@
+// Copyright (c) 2020, 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.
+//
+// @dart=2.8
+
+import 'dart:isolate';
+
+import 'package:package_config/package_config.dart';
+import 'package:path/path.dart' as p;
+
+import '../util/detaching_future.dart';
+
+/// A comment which forces the language version to be that of the current
+/// packages default.
+///
+/// If the cwd is not a package, this returns an empty string which ends up
+/// defaulting to the current sdk version.
+Future<String> get rootPackageLanguageVersionComment =>
+    _rootPackageLanguageVersionComment.asFuture;
+final _rootPackageLanguageVersionComment = DetachingFuture(() async {
+  var packageConfig = await loadPackageConfigUri(await Isolate.packageConfig);
+  var rootPackage = packageConfig.packageOf(Uri.file(p.absolute('foo.dart')));
+  if (rootPackage == null) return '';
+  return '// @dart=${rootPackage.languageVersion}';
+}());
diff --git a/pkgs/test_core/lib/src/runner/parse_metadata.dart b/pkgs/test_core/lib/src/runner/parse_metadata.dart
index 7f912e1..43a76cc 100644
--- a/pkgs/test_core/lib/src/runner/parse_metadata.dart
+++ b/pkgs/test_core/lib/src/runner/parse_metadata.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.8
 
 import 'package:analyzer/dart/analysis/utilities.dart';
 import 'package:analyzer/dart/ast/ast.dart';
@@ -43,7 +45,7 @@
   /// The actual contents of the file.
   final String _contents;
 
-  /// The language version override comment if one was present
+  /// The language version override comment if one was present, otherwise null.
   String _languageVersionComment;
 
   _Parser(this._path, this._contents, this._platformVariables) {
@@ -63,7 +65,7 @@
             return null;
           }
         })
-        .where((prefix) => prefix != null)
+        .whereType<String>()
         .toSet();
   }
 
@@ -84,21 +86,21 @@
 
       if (name == 'TestOn') {
         _assertSingle(testOn, 'TestOn', annotation);
-        testOn = _parseTestOn(annotation, constructorName);
+        testOn = _parseTestOn(annotation);
       } else if (name == 'Timeout') {
         _assertSingle(timeout, 'Timeout', annotation);
         timeout = _parseTimeout(annotation, constructorName);
       } else if (name == 'Skip') {
         _assertSingle(skip, 'Skip', annotation);
-        skip = _parseSkip(annotation, constructorName);
+        skip = _parseSkip(annotation);
       } else if (name == 'OnPlatform') {
         _assertSingle(onPlatform, 'OnPlatform', annotation);
-        onPlatform = _parseOnPlatform(annotation, constructorName);
+        onPlatform = _parseOnPlatform(annotation);
       } else if (name == 'Tags') {
         _assertSingle(tags, 'Tags', annotation);
-        tags = _parseTags(annotation, constructorName);
+        tags = _parseTags(annotation);
       } else if (name == 'Retry') {
-        retry = _parseRetry(annotation, constructorName);
+        retry = _parseRetry(annotation);
       }
     }
 
@@ -115,10 +117,8 @@
 
   /// Parses a `@TestOn` annotation.
   ///
-  /// [annotation] is the annotation. [constructorName] is the name of the named
-  /// constructor for the annotation, if any.
-  PlatformSelector _parseTestOn(
-          Annotation annotation, String constructorName) =>
+  /// [annotation] is the annotation.
+  PlatformSelector _parseTestOn(Annotation annotation) =>
       _parsePlatformSelector(annotation.arguments.arguments.first);
 
   /// Parses an [expression] that should contain a string representing a
@@ -133,9 +133,8 @@
 
   /// Parses a `@Retry` annotation.
   ///
-  /// [annotation] is the annotation. [constructorName] is the name of the named
-  /// constructor for the annotation, if any.
-  int _parseRetry(Annotation annotation, String constructorName) =>
+  /// [annotation] is the annotation.
+  int _parseRetry(Annotation annotation) =>
       _parseInt(annotation.arguments.arguments.first);
 
   /// Parses a `@Timeout` annotation.
@@ -163,11 +162,10 @@
 
   /// Parses a `@Skip` annotation.
   ///
-  /// [annotation] is the annotation. [constructorName] is the name of the named
-  /// constructor for the annotation, if any.
+  /// [annotation] is the annotation.
   ///
   /// Returns either `true` or a reason string.
-  dynamic _parseSkip(Annotation annotation, String constructorName) {
+  dynamic _parseSkip(Annotation annotation) {
     var args = annotation.arguments.arguments;
     return args.isEmpty ? true : _parseString(args.first).stringValue;
   }
@@ -183,9 +181,8 @@
 
   /// Parses a `@Tags` annotation.
   ///
-  /// [annotation] is the annotation. [constructorName] is the name of the named
-  /// constructor for the annotation, if any.
-  Set<String> _parseTags(Annotation annotation, String constructorName) {
+  /// [annotation] is the annotation.
+  Set<String> _parseTags(Annotation annotation) {
     return _parseList(annotation.arguments.arguments.first)
         .map((tagExpression) {
       var name = _parseString(tagExpression).stringValue;
@@ -200,10 +197,8 @@
 
   /// Parses an `@OnPlatform` annotation.
   ///
-  /// [annotation] is the annotation. [constructorName] is the name of the named
-  /// constructor for the annotation, if any.
-  Map<PlatformSelector, Metadata> _parseOnPlatform(
-      Annotation annotation, String constructorName) {
+  /// [annotation] is the annotation.
+  Map<PlatformSelector, Metadata> _parseOnPlatform(Annotation annotation) {
     return _parseMap(annotation.arguments.arguments.first, key: (key) {
       return _parsePlatformSelector(key);
     }, value: (value) {
diff --git a/pkgs/test_core/lib/src/runner/platform.dart b/pkgs/test_core/lib/src/runner/platform.dart
index aa894c2..1787bf7 100644
--- a/pkgs/test_core/lib/src/runner/platform.dart
+++ b/pkgs/test_core/lib/src/runner/platform.dart
@@ -2,8 +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 'dart:async';
-
 import 'package:stream_channel/stream_channel.dart';
 
 import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
@@ -56,7 +54,7 @@
   /// Subclasses overriding this method must call [deserializeSuite] in
   /// `platform_helpers.dart` to obtain a [RunnerSuiteController]. They must
   /// pass the opaque [message] parameter to the [deserializeSuite] call.
-  Future<RunnerSuite> load(String path, SuitePlatform platform,
+  Future<RunnerSuite?> load(String path, SuitePlatform platform,
       SuiteConfiguration suiteConfig, Object message);
 
   Future closeEphemeral() async {}
diff --git a/pkgs/test_core/lib/src/runner/plugin/customizable_platform.dart b/pkgs/test_core/lib/src/runner/plugin/customizable_platform.dart
index 354a2ca..058eb95 100644
--- a/pkgs/test_core/lib/src/runner/plugin/customizable_platform.dart
+++ b/pkgs/test_core/lib/src/runner/plugin/customizable_platform.dart
@@ -1,6 +1,8 @@
 // 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.
+//
+// @dart=2.9
 
 import 'package:yaml/yaml.dart';
 
diff --git a/pkgs/test_core/lib/src/runner/plugin/environment.dart b/pkgs/test_core/lib/src/runner/plugin/environment.dart
index 40e88b2..a097842 100644
--- a/pkgs/test_core/lib/src/runner/plugin/environment.dart
+++ b/pkgs/test_core/lib/src/runner/plugin/environment.dart
@@ -18,10 +18,10 @@
   const PluginEnvironment();
 
   @override
-  Uri get observatoryUrl => null;
+  Uri? get observatoryUrl => null;
 
   @override
-  Uri get remoteDebuggerUrl => null;
+  Uri? get remoteDebuggerUrl => null;
 
   @override
   CancelableOperation displayPause() => throw UnsupportedError(
diff --git a/pkgs/test_core/lib/src/runner/plugin/platform_helpers.dart b/pkgs/test_core/lib/src/runner/plugin/platform_helpers.dart
index bd11058..6a8ee9e 100644
--- a/pkgs/test_core/lib/src/runner/plugin/platform_helpers.dart
+++ b/pkgs/test_core/lib/src/runner/plugin/platform_helpers.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.9
 
 import 'dart:async';
 import 'dart:io';
@@ -44,11 +46,11 @@
     Environment environment,
     StreamChannel channel,
     Object message,
-    {Future<Map<String, dynamic>> Function() gatherCoverage}) {
+    {Future<Map<String, dynamic>> Function() /*?*/ gatherCoverage}) {
   var disconnector = Disconnector();
   var suiteChannel = MultiChannel(channel.transform(disconnector));
 
-  suiteChannel.sink.add({
+  suiteChannel.sink.add(<String, dynamic>{
     'type': 'initial',
     'platform': platform.serialize(),
     'metadata': suiteConfig.metadata.serialize(),
@@ -64,7 +66,7 @@
   var completer = Completer<Group>();
 
   var loadSuiteZone = Zone.current;
-  void handleError(error, StackTrace stackTrace) {
+  void handleError(Object error, StackTrace stackTrace) {
     disconnector.disconnect();
 
     if (completer.isCompleted) {
@@ -85,8 +87,8 @@
             break;
 
           case 'loadException':
-            handleError(
-                LoadException(path, response['message']), Trace.current());
+            handleError(LoadException(path, response['message'] as Object),
+                Trace.current());
             break;
 
           case 'error':
@@ -145,7 +147,7 @@
   /// Deserializes [test] into a concrete [Test] class.
   ///
   /// Returns `null` if [test] is `null`.
-  Test _deserializeTest(Map test) {
+  Test /*?*/ _deserializeTest(Map /*?*/ test) {
     if (test == null) return null;
 
     var metadata = Metadata.deserialize(test['metadata']);
diff --git a/pkgs/test_core/lib/src/runner/plugin/remote_platform_helpers.dart b/pkgs/test_core/lib/src/runner/plugin/remote_platform_helpers.dart
index 17c6d4c..9abdbb5 100644
--- a/pkgs/test_core/lib/src/runner/plugin/remote_platform_helpers.dart
+++ b/pkgs/test_core/lib/src/runner/plugin/remote_platform_helpers.dart
@@ -2,8 +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 'dart:async';
-
 import 'package:stream_channel/stream_channel.dart';
 
 import 'package:test_api/src/backend/stack_trace_formatter.dart'; // ignore: implementation_imports
@@ -30,10 +28,13 @@
 ///
 /// If [beforeLoad] is passed, it's called before the tests have been declared
 /// for this worker.
-StreamChannel serializeSuite(Function Function() getMain,
-        {bool hidePrints = true, Future Function() beforeLoad}) =>
-    RemoteListener.start(getMain,
-        hidePrints: hidePrints, beforeLoad: beforeLoad);
+StreamChannel<Object?> serializeSuite(Function Function() getMain,
+        {bool hidePrints = true, Future Function()? beforeLoad}) =>
+    RemoteListener.start(
+      getMain,
+      hidePrints: hidePrints,
+      beforeLoad: beforeLoad,
+    );
 
 /// Returns a channel that communicates with a plugin in the test runner.
 ///
@@ -44,7 +45,7 @@
 /// Throws a [StateError] if [name] has already been used for a channel, or if
 /// this is called outside a worker context (such as within a running test or
 /// `serializeSuite()`'s `onLoad()` function).
-StreamChannel suiteChannel(String name) {
+StreamChannel<Object?> suiteChannel(String name) {
   var manager = SuiteChannelManager.current;
   if (manager == null) {
     throw StateError('suiteChannel() may only be called within a test worker.');
diff --git a/pkgs/test_core/lib/src/runner/reporter.dart b/pkgs/test_core/lib/src/runner/reporter.dart
index ddb938b..7caa944 100644
--- a/pkgs/test_core/lib/src/runner/reporter.dart
+++ b/pkgs/test_core/lib/src/runner/reporter.dart
@@ -19,7 +19,4 @@
   /// Subclasses should ensure that this does nothing if the reporter isn't
   /// paused.
   void resume();
-
-  /// Cancels the reporter's output.
-  void cancel();
 }
diff --git a/pkgs/test_core/lib/src/runner/reporter/compact.dart b/pkgs/test_core/lib/src/runner/reporter/compact.dart
index 4c3e69a..c18b1f7 100644
--- a/pkgs/test_core/lib/src/runner/reporter/compact.dart
+++ b/pkgs/test_core/lib/src/runner/reporter/compact.dart
@@ -14,7 +14,6 @@
 import 'package:test_api/src/utils.dart' as utils;
 
 import '../../util/io.dart';
-import '../configuration.dart';
 import '../engine.dart';
 import '../load_exception.dart';
 import '../load_suite.dart';
@@ -23,40 +22,41 @@
 /// A reporter that prints test results to the console in a single
 /// continuously-updating line.
 class CompactReporter implements Reporter {
-  final _config = Configuration.current;
+  /// Whether the reporter should emit terminal color escapes.
+  final bool _color;
 
   /// The terminal escape for green text, or the empty string if this is Windows
   /// or not outputting to a terminal.
-  String get _green => _config.color ? '\u001b[32m' : '';
+  final String _green;
 
   /// The terminal escape for red text, or the empty string if this is Windows
   /// or not outputting to a terminal.
-  String get _red => _config.color ? '\u001b[31m' : '';
+  final String _red;
 
   /// The terminal escape for yellow text, or the empty string if this is
   /// Windows or not outputting to a terminal.
-  String get _yellow => _config.color ? '\u001b[33m' : '';
+  final String _yellow;
 
   /// The terminal escape for gray text, or the empty string if this is
   /// Windows or not outputting to a terminal.
-  String get _gray => _config.color ? '\u001b[1;30m' : '';
+  final String _gray;
 
   /// The terminal escape for bold text, or the empty string if this is
   /// Windows or not outputting to a terminal.
-  String get _bold => _config.color ? '\u001b[1m' : '';
+  final String _bold;
 
   /// The terminal escape for removing test coloring, or the empty string if
   /// this is Windows or not outputting to a terminal.
-  String get _noColor => _config.color ? '\u001b[0m' : '';
-
-  /// Whether the path to each test's suite should be printed.
-  final bool _printPath = Configuration.current.paths.length > 1 ||
-      Directory(Configuration.current.paths.single).existsSync();
+  final String _noColor;
 
   /// The engine used to run the tests.
   final Engine _engine;
 
-  final StringSink _sink;
+  /// Whether the path to each test's suite should be printed.
+  final bool _printPath;
+
+  /// Whether the platform each test is running on should be printed.
+  final bool _printPlatform;
 
   /// A stopwatch that tracks the duration of the full run.
   final _stopwatch = Stopwatch();
@@ -69,28 +69,29 @@
 
   /// The size of `_engine.passed` last time a progress notification was
   /// printed.
-  int _lastProgressPassed;
+  int _lastProgressPassed = 0;
 
-  /// The size of `_engine.skipped` last time a progress notification was printed.
-  int _lastProgressSkipped;
+  /// The size of `_engine.skipped` last time a progress notification was
+  /// printed.
+  int? _lastProgressSkipped;
 
   /// The size of `_engine.failed` last time a progress notification was
   /// printed.
-  int _lastProgressFailed;
+  int? _lastProgressFailed;
 
   /// The duration of the test run in seconds last time a progress notification
   /// was printed.
-  int _lastProgressElapsed;
+  int? _lastProgressElapsed;
 
   /// The message printed for the last progress notification.
-  String _lastProgressMessage;
+  String? _lastProgressMessage;
 
   /// The suffix added to the last progress notification.
-  String _lastProgressSuffix;
+  String? _lastProgressSuffix;
 
   /// Whether the message printed for the last progress notification was
   /// truncated.
-  bool _lastProgressTruncated;
+  bool? _lastProgressTruncated;
 
   // Whether a newline has been printed since the last progress line.
   var _printedNewline = true;
@@ -101,16 +102,39 @@
   /// The set of all subscriptions to various streams.
   final _subscriptions = <StreamSubscription>{};
 
+  final StringSink _sink;
+
   /// Watches the tests run by [engine] and prints their results to the
   /// terminal.
-  static CompactReporter watch(Engine engine, StringSink sink) =>
-      CompactReporter._(engine, sink);
+  ///
+  /// If [color] is `true`, this will use terminal colors; if it's `false`, it
+  /// won't. If [printPath] is `true`, this will print the path name as part of
+  /// the test description. Likewise, if [printPlatform] is `true`, this will
+  /// print the platform as part of the test description.
+  static CompactReporter watch(Engine engine, StringSink sink,
+          {required bool color,
+          required bool printPath,
+          required bool printPlatform}) =>
+      CompactReporter._(engine, sink,
+          color: color, printPath: printPath, printPlatform: printPlatform);
 
-  CompactReporter._(this._engine, this._sink) {
+  CompactReporter._(this._engine, this._sink,
+      {required bool color,
+      required bool printPath,
+      required bool printPlatform})
+      : _printPath = printPath,
+        _printPlatform = printPlatform,
+        _color = color,
+        _green = color ? '\u001b[32m' : '',
+        _red = color ? '\u001b[31m' : '',
+        _yellow = color ? '\u001b[33m' : '',
+        _gray = color ? '\u001b[1;30m' : '',
+        _bold = color ? '\u001b[1m' : '',
+        _noColor = color ? '\u001b[0m' : '' {
     _subscriptions.add(_engine.onTestStarted.listen(_onTestStarted));
 
-    /// Convert the future to a stream so that the subscription can be paused or
-    /// canceled.
+    // Convert the future to a stream so that the subscription can be paused or
+    // canceled.
     _subscriptions.add(_engine.success.asStream().listen(_onDone));
   }
 
@@ -145,8 +169,7 @@
     }
   }
 
-  @override
-  void cancel() {
+  void _cancel() {
     for (var subscription in _subscriptions) {
       subscription.cancel();
     }
@@ -159,9 +182,9 @@
       _stopwatchStarted = true;
       _stopwatch.start();
 
-      /// Keep updating the time even when nothing else is happening.
+      // Keep updating the time even when nothing else is happening.
       _subscriptions.add(Stream.periodic(Duration(seconds: 1))
-          .listen((_) => _progressLine(_lastProgressMessage)));
+          .listen((_) => _progressLine(_lastProgressMessage ?? '')));
     }
 
     // If this is the first test to start, print a progress line so the user
@@ -221,7 +244,7 @@
     }
 
     // TODO - what type is this?
-    _sink.writeln(indent(error.toString(color: _config.color) as String));
+    _sink.writeln(indent(error.toString(color: _color)));
 
     // Only print stack traces for load errors that come from the user's code.
     if (error.innerError is! IOException &&
@@ -236,8 +259,8 @@
   ///
   /// [success] will be `true` if all tests passed, `false` if some tests
   /// failed, and `null` if the engine was closed prematurely.
-  void _onDone(bool success) {
-    cancel();
+  void _onDone(bool? success) {
+    _cancel();
     _stopwatch.stop();
 
     // A null success value indicates that the engine was closed before the
@@ -283,7 +306,7 @@
   /// color for [message]. If [suffix] is passed, it's added to the end of
   /// [message].
   bool _progressLine(String message,
-      {String color, bool truncate = true, String suffix}) {
+      {String? color, bool truncate = true, String? suffix}) {
     var elapsed = _stopwatch.elapsed.inSeconds;
 
     // Print nothing if nothing has changed since the last progress line.
@@ -295,7 +318,7 @@
         (suffix == null || suffix == _lastProgressSuffix) &&
         // Don't re-print just because the message became re-truncated, because
         // that doesn't add information.
-        (truncate || !_lastProgressTruncated) &&
+        (truncate || !_lastProgressTruncated!) &&
         // If we printed a newline, that means the last line *wasn't* a progress
         // line. In that case, we don't want to print a new progress line just
         // because the elapsed time changed.
@@ -375,8 +398,7 @@
       name = '${liveTest.suite.path}: $name';
     }
 
-    if (_config.suiteDefaults.runtimes.length > 1 &&
-        liveTest.suite.platform != null) {
+    if (_printPlatform) {
       name = '[${liveTest.suite.platform.runtime.name}] $name';
     }
 
diff --git a/pkgs/test_core/lib/src/runner/reporter/expanded.dart b/pkgs/test_core/lib/src/runner/reporter/expanded.dart
index 2785ae9..a72ec78 100644
--- a/pkgs/test_core/lib/src/runner/reporter/expanded.dart
+++ b/pkgs/test_core/lib/src/runner/reporter/expanded.dart
@@ -62,21 +62,21 @@
 
   /// The size of `_engine.passed` last time a progress notification was
   /// printed.
-  int _lastProgressPassed;
+  int _lastProgressPassed = 0;
 
   /// The size of `_engine.skipped` last time a progress notification was
   /// printed.
-  int _lastProgressSkipped;
+  int _lastProgressSkipped = 0;
 
   /// The size of `_engine.failed` last time a progress notification was
   /// printed.
-  int _lastProgressFailed;
+  int _lastProgressFailed = 0;
 
   /// The message printed for the last progress notification.
-  String _lastProgressMessage;
+  String _lastProgressMessage = '';
 
   /// The suffix added to the last progress notification.
-  String _lastProgressSuffix;
+  String? _lastProgressSuffix;
 
   /// Whether the reporter is paused.
   var _paused = false;
@@ -86,8 +86,6 @@
 
   final StringSink _sink;
 
-  // TODO(nweiz): Get configuration from [Configuration.current] once we have
-  // cross-platform imports.
   /// Watches the tests run by [engine] and prints their results to the
   /// terminal.
   ///
@@ -96,13 +94,16 @@
   /// the test description. Likewise, if [printPlatform] is `true`, this will
   /// print the platform as part of the test description.
   static ExpandedReporter watch(Engine engine, StringSink sink,
-      {bool color = true, bool printPath = true, bool printPlatform = true}) {
-    return ExpandedReporter._(engine, sink,
-        color: color, printPath: printPath, printPlatform: printPlatform);
-  }
+          {required bool color,
+          required bool printPath,
+          required bool printPlatform}) =>
+      ExpandedReporter._(engine, sink,
+          color: color, printPath: printPath, printPlatform: printPlatform);
 
   ExpandedReporter._(this._engine, this._sink,
-      {bool color = true, bool printPath = true, bool printPlatform = true})
+      {required bool color,
+      required bool printPath,
+      required bool printPlatform})
       : _printPath = printPath,
         _printPlatform = printPlatform,
         _color = color,
@@ -114,8 +115,8 @@
         _noColor = color ? '\u001b[0m' : '' {
     _subscriptions.add(_engine.onTestStarted.listen(_onTestStarted));
 
-    /// Convert the future to a stream so that the subscription can be paused or
-    /// canceled.
+    // Convert the future to a stream so that the subscription can be paused or
+    // canceled.
     _subscriptions.add(_engine.success.asStream().listen(_onDone));
   }
 
@@ -134,6 +135,7 @@
   @override
   void resume() {
     if (!_paused) return;
+
     _stopwatch.start();
 
     for (var subscription in _subscriptions) {
@@ -141,8 +143,7 @@
     }
   }
 
-  @override
-  void cancel() {
+  void _cancel() {
     for (var subscription in _subscriptions) {
       subscription.cancel();
     }
@@ -205,7 +206,7 @@
     }
 
     // TODO - what type is this?
-    _sink.writeln(indent((error as dynamic).toString(color: _color) as String));
+    _sink.writeln(indent(error.toString(color: _color)));
 
     // Only print stack traces for load errors that come from the user's code.
     if (error.innerError is! FormatException && error.innerError is! String) {
@@ -217,7 +218,8 @@
   ///
   /// [success] will be `true` if all tests passed, `false` if some tests
   /// failed, and `null` if the engine was closed prematurely.
-  void _onDone(bool success) {
+  void _onDone(bool? success) {
+    _cancel();
     // A null success value indicates that the engine was closed before the
     // tests finished running, probably because of a signal from the user, in
     // which case we shouldn't print summary information.
@@ -243,7 +245,7 @@
   /// [message] goes after the progress report. If [color] is passed, it's used
   /// as the color for [message]. If [suffix] is passed, it's added to the end
   /// of [message].
-  void _progressLine(String message, {String color, String suffix}) {
+  void _progressLine(String message, {String? color, String? suffix}) {
     // Print nothing if nothing has changed since the last progress line.
     if (_engine.passed.length == _lastProgressPassed &&
         _engine.skipped.length == _lastProgressSkipped &&
diff --git a/pkgs/test_core/lib/src/runner/reporter/json.dart b/pkgs/test_core/lib/src/runner/reporter/json.dart
index ff55fc1..11710da 100644
--- a/pkgs/test_core/lib/src/runner/reporter/json.dart
+++ b/pkgs/test_core/lib/src/runner/reporter/json.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.9
 
 import 'dart:async';
 import 'dart:convert';
@@ -8,6 +10,7 @@
 
 import 'package:path/path.dart' as p;
 
+import 'package:stack_trace/stack_trace.dart';
 import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports
 import 'package:test_api/src/backend/group_entry.dart'; // ignore: implementation_imports
 import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports
@@ -33,8 +36,6 @@
   /// The engine used to run the tests.
   final Engine _engine;
 
-  final StringSink _sink;
-
   /// A stopwatch that tracks the duration of the full run.
   final _stopwatch = Stopwatch();
 
@@ -44,12 +45,6 @@
   /// when the reporter is paused.
   var _stopwatchStarted = false;
 
-  /// Whether the reporter is paused.
-  var _paused = false;
-
-  /// The set of all subscriptions to various streams.
-  final _subscriptions = <StreamSubscription>{};
-
   /// An expando that associates unique IDs with [LiveTest]s.
   final _liveTestIDs = <LiveTest, int>{};
 
@@ -62,6 +57,14 @@
   /// The next ID to associate with a [LiveTest].
   var _nextID = 0;
 
+  /// Whether the reporter is paused.
+  var _paused = false;
+
+  /// The set of all subscriptions to various streams.
+  final _subscriptions = <StreamSubscription>{};
+
+  final StringSink _sink;
+
   /// Watches the tests run by [engine] and prints their results as JSON.
   static JsonReporter watch(Engine engine, StringSink sink) =>
       JsonReporter._(engine, sink);
@@ -69,8 +72,8 @@
   JsonReporter._(this._engine, this._sink) : _config = Configuration.current {
     _subscriptions.add(_engine.onTestStarted.listen(_onTestStarted));
 
-    /// Convert the future to a stream so that the subscription can be paused or
-    /// canceled.
+    // Convert the future to a stream so that the subscription can be paused or
+    // canceled.
     _subscriptions.add(_engine.success.asStream().listen(_onDone));
 
     _subscriptions.add(_engine.onSuiteAdded.listen(null, onDone: () {
@@ -105,8 +108,7 @@
     }
   }
 
-  @override
-  void cancel() {
+  void _cancel() {
     for (var subscription in _subscriptions) {
       subscription.cancel();
     }
@@ -146,8 +148,8 @@
           liveTest.suite.path)
     });
 
-    /// Convert the future to a stream so that the subscription can be paused or
-    /// canceled.
+    // Convert the future to a stream so that the subscription can be paused or
+    // canceled.
     _subscriptions.add(
         liveTest.onComplete.asStream().listen((_) => _onComplete(liveTest)));
 
@@ -193,7 +195,7 @@
     }
 
     _emit('suite', {
-      'suite': {
+      'suite': <String, Object /*?*/ >{
         'id': id,
         'platform': suite.platform.runtime.identifier,
         'path': suite.path
@@ -208,7 +210,7 @@
   /// If a group doesn't have an ID yet, this assigns one and emits a new event
   /// for that group.
   List<int> _idsForGroups(Iterable<Group> groups, Suite suite) {
-    int parentID;
+    int /*?*/ parentID;
     return groups.map((group) {
       if (_groupIDs.containsKey(group)) {
         parentID = _groupIDs[group];
@@ -272,8 +274,8 @@
   ///
   /// [success] will be `true` if all tests passed, `false` if some tests
   /// failed, and `null` if the engine was closed prematurely.
-  void _onDone(bool success) {
-    cancel();
+  void _onDone(bool /*?*/ success) {
+    _cancel();
     _stopwatch.stop();
 
     _emit('done', {'success': success});
@@ -304,11 +306,15 @@
       Runtime runtime,
       String suitePath) {
     var frame = entry.trace?.frames?.first;
-    var rootFrame = entry.trace?.frames?.firstWhere(
-        (frame) =>
-            frame.uri.scheme == 'file' &&
-            frame.uri.toFilePath() == p.absolute(suitePath),
-        orElse: () => null);
+    Frame /*?*/ rootFrame;
+    for (var frame in entry.trace?.frames ?? <Frame>[]) {
+      if (frame.uri.scheme == 'file' &&
+          frame.uri.toFilePath() == p.absolute(suitePath)) {
+        rootFrame = frame;
+        break;
+      }
+    }
+
     if (suiteConfig.jsTrace && runtime.isJS) {
       frame = null;
       rootFrame = null;
diff --git a/pkgs/test_core/lib/src/runner/reporter/multiplex.dart b/pkgs/test_core/lib/src/runner/reporter/multiplex.dart
index e13c1b4..59a9841 100644
--- a/pkgs/test_core/lib/src/runner/reporter/multiplex.dart
+++ b/pkgs/test_core/lib/src/runner/reporter/multiplex.dart
@@ -10,13 +10,6 @@
   MultiplexReporter(this.delegates);
 
   @override
-  void cancel() {
-    for (var d in delegates) {
-      d.cancel();
-    }
-  }
-
-  @override
   void pause() {
     for (var d in delegates) {
       d.pause();
diff --git a/pkgs/test_core/lib/src/runner/runner_suite.dart b/pkgs/test_core/lib/src/runner/runner_suite.dart
index 1b166d3..4e0cb02 100644
--- a/pkgs/test_core/lib/src/runner/runner_suite.dart
+++ b/pkgs/test_core/lib/src/runner/runner_suite.dart
@@ -55,23 +55,23 @@
   /// debugging mode and doesn't support suite channels.
   factory RunnerSuite(Environment environment, SuiteConfiguration config,
       Group group, SuitePlatform platform,
-      {String path, Function() onClose}) {
+      {String? path, Function()? onClose}) {
     var controller =
         RunnerSuiteController._local(environment, config, onClose: onClose);
-    var suite = RunnerSuite._(controller, group, path, platform);
+    var suite = RunnerSuite._(controller, group, platform, path: path);
     controller._suite = Future.value(suite);
     return suite;
   }
 
-  RunnerSuite._(
-      this._controller, Group group, String path, SuitePlatform platform)
+  RunnerSuite._(this._controller, Group group, SuitePlatform platform,
+      {String? path})
       : super(group, platform, path: path);
 
   @override
   RunnerSuite filter(bool Function(Test) callback) {
     var filtered = group.filter(callback);
     filtered ??= Group.root([], metadata: metadata);
-    return RunnerSuite._(_controller, filtered, path, platform);
+    return RunnerSuite._(_controller, filtered, platform, path: path);
   }
 
   /// Closes the suite and releases any resources associated with it.
@@ -81,15 +81,20 @@
   ///
   /// Result is suitable for input to the coverage formatters provided by
   /// `package:coverage`.
-  Future<Map<String, dynamic>> gatherCoverage() async =>
-      (await _controller._gatherCoverage?.call()) ?? {};
+  Future<Map<String, dynamic>> gatherCoverage() async {
+    // TODO(https://github.com/dart-lang/sdk/issues/41108): Remove cast
+    var coverage =
+        // ignore: unnecessary_cast
+        await _controller._gatherCoverage?.call() as Map<String, dynamic>?;
+    return coverage ?? {};
+  }
 }
 
 /// A class that exposes and controls a [RunnerSuite].
 class RunnerSuiteController {
   /// The suite controlled by this controller.
   Future<RunnerSuite> get suite => _suite;
-  Future<RunnerSuite> _suite;
+  late final Future<RunnerSuite> _suite;
 
   /// The backing value for [suite.environment].
   final Environment _environment;
@@ -98,10 +103,10 @@
   final SuiteConfiguration _config;
 
   /// A channel that communicates with the remote suite.
-  final MultiChannel _suiteChannel;
+  final MultiChannel? _suiteChannel;
 
   /// The function to call when the suite is closed.
-  final Function() _onClose;
+  final Function()? _onClose;
 
   /// The backing value for [suite.isDebugging].
   bool _isDebugging = false;
@@ -113,24 +118,24 @@
   final _channelNames = <String>{};
 
   /// Collects a hit-map containing merged coverage.
-  final Future<Map<String, dynamic>> Function() _gatherCoverage;
+  final Future<Map<String, dynamic>> Function()? _gatherCoverage;
 
   RunnerSuiteController(this._environment, this._config, this._suiteChannel,
       Future<Group> groupFuture, SuitePlatform platform,
-      {String path,
-      Function() onClose,
-      Future<Map<String, dynamic>> Function() gatherCoverage})
+      {String? path,
+      Function()? onClose,
+      Future<Map<String, dynamic>> Function()? gatherCoverage})
       : _onClose = onClose,
         _gatherCoverage = gatherCoverage {
-    _suite =
-        groupFuture.then((group) => RunnerSuite._(this, group, path, platform));
+    _suite = groupFuture
+        .then((group) => RunnerSuite._(this, group, platform, path: path));
   }
 
   /// Used by [new RunnerSuite] to create a runner suite that's not loaded from
   /// an external source.
   RunnerSuiteController._local(this._environment, this._config,
-      {Function() onClose,
-      Future<Map<String, dynamic>> Function() gatherCoverage})
+      {Function()? onClose,
+      Future<Map<String, dynamic>> Function()? gatherCoverage})
       : _suiteChannel = null,
         _onClose = onClose,
         _gatherCoverage = gatherCoverage;
@@ -159,8 +164,13 @@
       throw StateError('Duplicate RunnerSuite.channel() connection "$name".');
     }
 
-    var channel = _suiteChannel.virtualChannel();
-    _suiteChannel.sink
+    var suiteChannel = _suiteChannel;
+    if (suiteChannel == null) {
+      throw StateError('No suite channel set up but one was requested.');
+    }
+
+    var channel = suiteChannel.virtualChannel();
+    suiteChannel.sink
         .add({'type': 'suiteChannel', 'name': name, 'id': channel.id});
     return channel;
   }
@@ -168,7 +178,8 @@
   /// The backing function for [suite.close].
   Future _close() => _closeMemo.runOnce(() async {
         await _onDebuggingController.close();
-        if (_onClose != null) await _onClose();
+        var onClose = _onClose;
+        if (onClose != null) await onClose();
       });
   final _closeMemo = AsyncMemoizer();
 }
diff --git a/pkgs/test_core/lib/src/runner/runner_test.dart b/pkgs/test_core/lib/src/runner/runner_test.dart
index d910cac..156e528 100644
--- a/pkgs/test_core/lib/src/runner/runner_test.dart
+++ b/pkgs/test_core/lib/src/runner/runner_test.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.9
 
 import 'package:pedantic/pedantic.dart';
 import 'package:stack_trace/stack_trace.dart';
@@ -25,7 +27,7 @@
   @override
   final Metadata metadata;
   @override
-  final Trace trace;
+  final Trace /*?*/ trace;
 
   /// The channel used to communicate with the test's [IframeListener].
   final MultiChannel _channel;
@@ -35,9 +37,9 @@
   RunnerTest._(this.name, this.metadata, this.trace, this._channel);
 
   @override
-  LiveTest load(Suite suite, {Iterable<Group> groups}) {
-    LiveTestController controller;
-    VirtualChannel testChannel;
+  LiveTest load(Suite suite, {Iterable<Group> /*?*/ groups}) {
+    /*late final*/ LiveTestController controller;
+    /*late final*/ VirtualChannel testChannel;
     controller = LiveTestController(suite, this, () {
       controller.setState(const State(Status.running, Result.success));
 
@@ -101,7 +103,7 @@
   }
 
   @override
-  Test forPlatform(SuitePlatform platform) {
+  Test /*?*/ forPlatform(SuitePlatform platform) {
     if (!metadata.testOn.evaluate(platform)) return null;
     return RunnerTest._(name, metadata.forPlatform(platform), trace, _channel);
   }
diff --git a/pkgs/test_core/lib/src/runner/runtime_selection.dart b/pkgs/test_core/lib/src/runner/runtime_selection.dart
index 8105dcd..e607400 100644
--- a/pkgs/test_core/lib/src/runner/runtime_selection.dart
+++ b/pkgs/test_core/lib/src/runner/runtime_selection.dart
@@ -12,7 +12,7 @@
   /// The location in the configuration file of this runtime string, or `null`
   /// if it was defined outside a configuration file (for example, on the
   /// command line).
-  final SourceSpan span;
+  final SourceSpan? span;
 
   RuntimeSelection(this.name, [this.span]);
 
diff --git a/pkgs/test_core/lib/src/runner/spawn_hybrid.dart b/pkgs/test_core/lib/src/runner/spawn_hybrid.dart
index 76e448a..e9cacd6 100644
--- a/pkgs/test_core/lib/src/runner/spawn_hybrid.dart
+++ b/pkgs/test_core/lib/src/runner/spawn_hybrid.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.9
 
 import 'dart:async';
 import 'dart:io';
@@ -84,7 +86,7 @@
 /// Normalizes [url] to an absolute url, or returns it as is if it has a
 /// scheme.
 ///
-/// Follows the rules for relatives/absolute paths outlit
+/// Follows the rules for relative/absolute paths outlined in [spawnHybridUri].
 String _normalizeUrl(String url, Suite suite) {
   final parsedUri = Uri.parse(url);
 
@@ -123,7 +125,7 @@
   // Returns the explicit language version comment if one exists.
   var result = parseString(
       content: await _readUri(parsedUri),
-      path: parsedUri.path,
+      path: parsedUri.scheme == 'data' ? null : p.fromUri(parsedUri),
       throwIfDiagnostics: false);
   var languageVersionComment = result.unit.languageVersionToken?.value();
   if (languageVersionComment != null) return languageVersionComment.toString();
diff --git a/pkgs/test_core/lib/src/runner/suite.dart b/pkgs/test_core/lib/src/runner/suite.dart
index 33e748b..2e8ed80 100644
--- a/pkgs/test_core/lib/src/runner/suite.dart
+++ b/pkgs/test_core/lib/src/runner/suite.dart
@@ -27,18 +27,18 @@
   /// Whether JavaScript stack traces should be left as-is or converted to
   /// Dart-like traces.
   bool get jsTrace => _jsTrace ?? false;
-  final bool _jsTrace;
+  final bool? _jsTrace;
 
   /// Whether skipped tests should be run.
   bool get runSkipped => _runSkipped ?? false;
-  final bool _runSkipped;
+  final bool? _runSkipped;
 
   /// The path to a mirror of this package containing HTML that points to
   /// precompiled JS.
   ///
   /// This is used by the internal Google test runner so that test compilation
   /// can more effectively make use of Google's build tools.
-  final String precompiledPath;
+  final String? precompiledPath;
 
   /// Additional arguments to pass to dart2js.
   ///
@@ -47,17 +47,18 @@
   /// suite's arguments will be used.
   final List<String> dart2jsArgs;
 
-  /// The patterns to match against test names to decide which to run, or `null`
-  /// if all tests should be run.
+  /// The patterns to match against test names to decide which to run.
   ///
   /// All patterns must match in order for a test to be run.
+  ///
+  /// If empty, all tests should be run.
   final Set<Pattern> patterns;
 
   /// The set of runtimes on which to run tests.
   List<String> get runtimes => _runtimes == null
       ? const ['vm']
-      : List.unmodifiable(_runtimes.map((runtime) => runtime.name));
-  final List<RuntimeSelection> _runtimes;
+      : List.unmodifiable(_runtimes!.map((runtime) => runtime.name));
+  final List<RuntimeSelection>? _runtimes;
 
   /// Only run tests whose tags match this selector.
   ///
@@ -87,7 +88,7 @@
   /// The seed with which to shuffle the test order.
   /// Default value is null if not provided and will not change the test order.
   /// The same seed will shuffle the tests in the same way every time.
-  final int testRandomizeOrderingSeed;
+  final int? testRandomizeOrderingSeed;
 
   /// The global test metadata derived from this configuration.
   Metadata get metadata {
@@ -102,7 +103,7 @@
 
   /// The set of tags that have been declared in any way in this configuration.
   Set<String> get knownTags {
-    if (_knownTags != null) return _knownTags;
+    if (_knownTags != null) return _knownTags!;
 
     var known = includeTags.variables.toSet()
       ..addAll(excludeTags.variables)
@@ -116,11 +117,10 @@
       known.addAll(configuration.knownTags);
     }
 
-    _knownTags = UnmodifiableSetView(known);
-    return _knownTags;
+    return _knownTags = UnmodifiableSetView(known);
   }
 
-  Set<String> _knownTags;
+  Set<String>? _knownTags;
 
   /// All child configurations that may be selected under various circumstances.
   Iterable<SuiteConfiguration> get _children sync* {
@@ -129,27 +129,27 @@
   }
 
   factory SuiteConfiguration(
-      {bool jsTrace,
-      bool runSkipped,
-      Iterable<String> dart2jsArgs,
-      String precompiledPath,
-      Iterable<Pattern> patterns,
-      Iterable<RuntimeSelection> runtimes,
-      BooleanSelector includeTags,
-      BooleanSelector excludeTags,
-      Map<BooleanSelector, SuiteConfiguration> tags,
-      Map<PlatformSelector, SuiteConfiguration> onPlatform,
-      int testRandomizeOrderingSeed,
+      {bool? jsTrace,
+      bool? runSkipped,
+      Iterable<String>? dart2jsArgs,
+      String? precompiledPath,
+      Iterable<Pattern>? patterns,
+      Iterable<RuntimeSelection>? runtimes,
+      BooleanSelector? includeTags,
+      BooleanSelector? excludeTags,
+      Map<BooleanSelector, SuiteConfiguration>? tags,
+      Map<PlatformSelector, SuiteConfiguration>? onPlatform,
+      int? testRandomizeOrderingSeed,
 
       // Test-level configuration
-      Timeout timeout,
-      bool verboseTrace,
-      bool chainStackTraces,
-      bool skip,
-      int retry,
-      String skipReason,
-      PlatformSelector testOn,
-      Iterable<String> addTags}) {
+      Timeout? timeout,
+      bool? verboseTrace,
+      bool? chainStackTraces,
+      bool? skip,
+      int? retry,
+      String? skipReason,
+      PlatformSelector? testOn,
+      Iterable<String>? addTags}) {
     var config = SuiteConfiguration._(
         jsTrace: jsTrace,
         runSkipped: runSkipped,
@@ -179,18 +179,18 @@
   /// Unlike [new SuiteConfiguration], this assumes [tags] is already
   /// resolved.
   SuiteConfiguration._(
-      {bool jsTrace,
-      bool runSkipped,
-      Iterable<String> dart2jsArgs,
+      {bool? jsTrace,
+      bool? runSkipped,
+      Iterable<String>? dart2jsArgs,
       this.precompiledPath,
-      Iterable<Pattern> patterns,
-      Iterable<RuntimeSelection> runtimes,
-      BooleanSelector includeTags,
-      BooleanSelector excludeTags,
-      Map<BooleanSelector, SuiteConfiguration> tags,
-      Map<PlatformSelector, SuiteConfiguration> onPlatform,
-      int testRandomizeOrderingSeed,
-      Metadata metadata})
+      Iterable<Pattern>? patterns,
+      Iterable<RuntimeSelection>? runtimes,
+      BooleanSelector? includeTags,
+      BooleanSelector? excludeTags,
+      Map<BooleanSelector, SuiteConfiguration>? tags,
+      Map<PlatformSelector, SuiteConfiguration>? onPlatform,
+      int? testRandomizeOrderingSeed,
+      Metadata? metadata})
       : _jsTrace = jsTrace,
         _runSkipped = runSkipped,
         dart2jsArgs = _list(dart2jsArgs) ?? const [],
@@ -216,7 +216,7 @@
   /// Returns an unmodifiable copy of [input].
   ///
   /// If [input] is `null` or empty, this returns `null`.
-  static List<T> _list<T>(Iterable<T> input) {
+  static List<T>? _list<T>(Iterable<T>? input) {
     if (input == null) return null;
     var list = List<T>.unmodifiable(input);
     if (list.isEmpty) return null;
@@ -224,8 +224,8 @@
   }
 
   /// Returns an unmodifiable copy of [input] or an empty unmodifiable map.
-  static Map<K, V> _map<K, V>(Map<K, V> input) {
-    if (input == null || input.isEmpty) return const {};
+  static Map<K, V> _map<K, V>(Map<K, V>? input) {
+    if (input == null || input.isEmpty) return const <Never, Never>{};
     return Map.unmodifiable(input);
   }
 
@@ -260,33 +260,34 @@
   /// Note that unlike [merge], this has no merging behavior—the old value is
   /// always replaced by the new one.
   SuiteConfiguration change(
-      {bool jsTrace,
-      bool runSkipped,
-      Iterable<String> dart2jsArgs,
-      String precompiledPath,
-      Iterable<Pattern> patterns,
-      Iterable<RuntimeSelection> runtimes,
-      BooleanSelector includeTags,
-      BooleanSelector excludeTags,
-      Map<BooleanSelector, SuiteConfiguration> tags,
-      Map<PlatformSelector, SuiteConfiguration> onPlatform,
-      int testRandomizeOrderingSeed,
+      {bool? jsTrace,
+      bool? runSkipped,
+      Iterable<String>? dart2jsArgs,
+      String? precompiledPath,
+      Iterable<Pattern>? patterns,
+      Iterable<RuntimeSelection>? runtimes,
+      BooleanSelector? includeTags,
+      BooleanSelector? excludeTags,
+      Map<BooleanSelector, SuiteConfiguration>? tags,
+      Map<PlatformSelector, SuiteConfiguration>? onPlatform,
+      int? testRandomizeOrderingSeed,
 
       // Test-level configuration
-      Timeout timeout,
-      bool verboseTrace,
-      bool chainStackTraces,
-      bool skip,
-      int retry,
-      String skipReason,
-      PlatformSelector testOn,
-      Iterable<String> addTags}) {
+      Timeout? timeout,
+      bool? verboseTrace,
+      bool? chainStackTraces,
+      bool? skip,
+      int? retry,
+      String? skipReason,
+      PlatformSelector? testOn,
+      Iterable<String>? addTags}) {
     var config = SuiteConfiguration._(
         jsTrace: jsTrace ?? _jsTrace,
         runSkipped: runSkipped ?? _runSkipped,
         dart2jsArgs: dart2jsArgs?.toList() ?? this.dart2jsArgs,
         precompiledPath: precompiledPath ?? this.precompiledPath,
         patterns: patterns ?? this.patterns,
+        // TODO(https://github.com/dart-lang/sdk/issues/41114): Remove cast
         runtimes: runtimes ?? _runtimes,
         includeTags: includeTags ?? this.includeTags,
         excludeTags: excludeTags ?? this.excludeTags,
@@ -312,8 +313,9 @@
         allRuntimes.map((runtime) => runtime.identifier).toSet();
     _metadata.validatePlatformSelectors(validVariables);
 
-    if (_runtimes != null) {
-      for (var selection in _runtimes) {
+    var runtimes = _runtimes;
+    if (runtimes != null) {
+      for (var selection in runtimes) {
         if (!allRuntimes
             .any((runtime) => runtime.identifier == selection.name)) {
           if (selection.span != null) {
@@ -363,7 +365,7 @@
     var newTags = Map<BooleanSelector, SuiteConfiguration>.from(tags);
     var merged = tags.keys.fold(empty, (SuiteConfiguration merged, selector) {
       if (!selector.evaluate(_metadata.tags.contains)) return merged;
-      return merged.merge(newTags.remove(selector));
+      return merged.merge(newTags.remove(selector)!);
     });
 
     if (merged == empty) return this;
diff --git a/pkgs/test_core/lib/src/runner/version.dart b/pkgs/test_core/lib/src/runner/version.dart
index b10aefa..5c617cf 100644
--- a/pkgs/test_core/lib/src/runner/version.dart
+++ b/pkgs/test_core/lib/src/runner/version.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.9
 
 import 'dart:io';
 
@@ -10,7 +12,7 @@
 ///
 /// This is a semantic version, optionally followed by a space and additional
 /// data about its source.
-final String testVersion = (() {
+final String /*?*/ testVersion = (() {
   dynamic lockfile;
   try {
     lockfile = loadYaml(File('pubspec.lock').readAsStringSync());
diff --git a/pkgs/test_core/lib/src/runner/vm/environment.dart b/pkgs/test_core/lib/src/runner/vm/environment.dart
index 6f919bf..fb75c66 100644
--- a/pkgs/test_core/lib/src/runner/vm/environment.dart
+++ b/pkgs/test_core/lib/src/runner/vm/environment.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2018, 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.
+//
+// @dart=2.9
 
 import 'dart:async';
 
@@ -22,7 +24,7 @@
   VMEnvironment(this.observatoryUrl, this._isolate, this._client);
 
   @override
-  Uri get remoteDebuggerUrl => null;
+  Uri /*?*/ get remoteDebuggerUrl => null;
 
   @override
   Stream get onRestart => StreamController.broadcast().stream;
diff --git a/pkgs/test_core/lib/src/runner/vm/platform.dart b/pkgs/test_core/lib/src/runner/vm/platform.dart
index aa89dbe..dcdb849 100644
--- a/pkgs/test_core/lib/src/runner/vm/platform.dart
+++ b/pkgs/test_core/lib/src/runner/vm/platform.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2016, 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.
+//
+// @dart=2.9
 
 import 'dart:async';
 import 'dart:developer';
@@ -25,7 +27,7 @@
 import '../../runner/runner_suite.dart';
 import '../../runner/suite.dart';
 import '../../util/dart.dart' as dart;
-import '../../util/io.dart';
+import '../package_version.dart';
 import 'environment.dart';
 
 /// A platform that loads tests in isolates spawned within this Dart process.
@@ -54,8 +56,8 @@
       rethrow;
     }
 
-    VmService client;
-    StreamSubscription<Event> eventSub;
+    VmService /*?*/ client;
+    StreamSubscription<Event> /*?*/ eventSub;
     var channel = IsolateChannel.connectReceive(receivePort)
         .transformStream(StreamTransformer.fromHandlers(handleDone: (sink) {
       isolate.kill();
@@ -64,15 +66,11 @@
       sink.close();
     }));
 
-    Environment environment;
-    IsolateRef isolateRef;
+    Environment /*?*/ environment;
+    IsolateRef /*?*/ isolateRef;
     if (_config.debug) {
-      // Print an empty line because the VM prints an "Observatory listening on"
-      // line and we don't want that to end up on the same line as the reporter
-      // info.
-      if (_config.reporter == 'compact') stdout.writeln();
-
-      var info = await Service.controlWebServer(enable: true);
+      var info =
+          await Service.controlWebServer(enable: true, silenceOutput: true);
       var isolateID = Service.getIsolateID(isolate);
 
       var libraryPath = p.toUri(p.absolute(path)).toString();
@@ -135,15 +133,12 @@
     ${suiteMetadata.languageVersionComment ?? await rootPackageLanguageVersionComment}
     import "dart:isolate";
 
-    import "package:stream_channel/isolate_channel.dart";
-
-    import "package:test_core/src/runner/plugin/remote_platform_helpers.dart";
+    import "package:test_core/src/bootstrap/vm.dart";
 
     import "${p.toUri(p.absolute(path))}" as test;
 
-    void main(_, SendPort message) {
-      var channel = serializeSuite(() => test.main);
-      IsolateChannel.connectSend(message).pipe(channel);
+    void main(_, SendPort sendPort) {
+      internalBootstrapVmTest(() => test.main, sendPort);
     }
   ''', message);
 }
diff --git a/pkgs/test_core/lib/src/util/dart.dart b/pkgs/test_core/lib/src/util/dart.dart
index 07f178a..fd8aeed 100644
--- a/pkgs/test_core/lib/src/util/dart.dart
+++ b/pkgs/test_core/lib/src/util/dart.dart
@@ -1,8 +1,9 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.9
 
-import 'dart:async';
 import 'dart:convert';
 import 'dart:isolate';
 
@@ -18,7 +19,7 @@
 /// passed to the [main] method of the code being run; the caller is responsible
 /// for using this to establish communication with the isolate.
 Future<Isolate> runInIsolate(String code, Object message,
-        {SendPort onExit}) async =>
+        {SendPort /*?*/ onExit}) async =>
     Isolate.spawnUri(
         Uri.dataFromString(code, mimeType: 'application/dart', encoding: utf8),
         [],
@@ -52,7 +53,7 @@
 ///
 /// This will return `null` if [context] contains an invalid string or does not
 /// contain [span].
-SourceSpan contextualizeSpan(
+SourceSpan /*?*/ contextualizeSpan(
     SourceSpan span, StringLiteral context, SourceFile file) {
   var contextRunes = StringLiteralIterator(context)..moveNext();
 
diff --git a/pkgs/test_core/lib/src/util/detaching_future.dart b/pkgs/test_core/lib/src/util/detaching_future.dart
new file mode 100644
index 0000000..594f313
--- /dev/null
+++ b/pkgs/test_core/lib/src/util/detaching_future.dart
@@ -0,0 +1,32 @@
+// Copyright (c) 2020, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// A handle on a [Future] that detaches from the original evaluation context
+/// after it has resolved.
+///
+/// A [Future] holds the Zone it was created in and may hold references to
+/// `async` functions that `await` it. When a future is stored in a static
+/// variable it won't be garbage collected which means all zone variables and
+/// variables in async functions get leaked. [DetachingFuture] works around this
+/// by forgetting the original [Future] after it has resolved, and wrapping the
+/// resolved value with `Future.value` for later calls.
+///
+/// https://github.com/dart-lang/sdk/issues/42457
+/// https://github.com/dart-lang/sdk/issues/42458
+///
+/// In the case of a future that resolves to an error the original future is
+/// retained.
+class DetachingFuture<T> {
+  late T _value;
+  Future<T>? _inProgress;
+
+  DetachingFuture(Future<T> inProgress) : _inProgress = inProgress {
+    inProgress.then((result) {
+      _value = result;
+      _inProgress = null;
+    });
+  }
+
+  Future<T> get asFuture => _inProgress ?? Future.value(_value);
+}
diff --git a/pkgs/test_core/lib/src/util/io.dart b/pkgs/test_core/lib/src/util/io.dart
index 7822dbe..347ca89 100644
--- a/pkgs/test_core/lib/src/util/io.dart
+++ b/pkgs/test_core/lib/src/util/io.dart
@@ -7,10 +7,8 @@
 import 'dart:core';
 import 'dart:convert';
 import 'dart:io';
-import 'dart:isolate';
 
 import 'package:async/async.dart';
-import 'package:package_config/package_config.dart';
 import 'package:path/path.dart' as p;
 
 import 'package:test_api/src/backend/operating_system.dart'; // ignore: implementation_imports
@@ -38,18 +36,6 @@
   }
 }();
 
-/// A comment which forces the language version to be that of the current
-/// packages default.
-///
-/// If the cwd is not a package, this returns an empty string which ends up
-/// defaulting to the current sdk version.
-final Future<String> rootPackageLanguageVersionComment = () async {
-  var packageConfig = await loadPackageConfigUri(await Isolate.packageConfig);
-  var rootPackage = packageConfig.packageOf(Uri.file(p.absolute('foo.dart')));
-  if (rootPackage == null) return '';
-  return '// @dart=${rootPackage.languageVersion}';
-}();
-
 /// The root directory of the Dart SDK.
 final String sdkDir = p.dirname(p.dirname(Platform.resolvedExecutable));
 
@@ -80,7 +66,7 @@
 /// This is configurable so that the test code can validate that the runner
 /// cleans up after itself fully.
 final _tempDir = Platform.environment.containsKey('_UNITTEST_TEMP_DIR')
-    ? Platform.environment['_UNITTEST_TEMP_DIR']
+    ? Platform.environment['_UNITTEST_TEMP_DIR']!
     : Directory.systemTemp.path;
 
 /// Whether or not the current terminal supports ansi escape codes.
@@ -147,7 +133,7 @@
 ///
 /// If [print] is `true`, this prints the message using [print] to associate it
 /// with the current test. Otherwise, it prints it using [stderr].
-void warn(String message, {bool color, bool print = false}) {
+void warn(String message, {bool? color, bool print = false}) {
   color ??= canUseSpecialChars;
   var header = color ? '\u001b[33mWarning:\u001b[0m' : 'Warning:';
   (print ? core.print : stderr.writeln)(wordWrap('$header $message\n'));
@@ -163,12 +149,12 @@
 /// This is necessary for ensuring that our port binding isn't flaky for
 /// applications that don't print out the bound port.
 Future<T> getUnusedPort<T>(FutureOr<T> Function(int port) tryPort) async {
-  T value;
+  T? value;
   await Future.doWhile(() async {
     value = await tryPort(await getUnsafeUnusedPort());
     return value == null;
   });
-  return value;
+  return value!;
 }
 
 /// Whether this computer supports binding to IPv6 addresses.
@@ -180,7 +166,7 @@
 /// any time after this call has returned. If at all possible, callers should
 /// use [getUnusedPort] instead.
 Future<int> getUnsafeUnusedPort() async {
-  int port;
+  late int port;
   if (_maySupportIPv6) {
     try {
       final socket = await ServerSocket.bind(InternetAddress.loopbackIPv6, 0,
diff --git a/pkgs/test_core/lib/src/util/print_sink.dart b/pkgs/test_core/lib/src/util/print_sink.dart
index 3409770..ebb2fe8 100644
--- a/pkgs/test_core/lib/src/util/print_sink.dart
+++ b/pkgs/test_core/lib/src/util/print_sink.dart
@@ -6,14 +6,14 @@
   final _buffer = StringBuffer();
 
   @override
-  void write(Object obj) {
+  void write(Object? obj) {
     _buffer.write(obj);
     _flush();
   }
 
   @override
   void writeAll(Iterable objects, [String separator = '']) {
-    _buffer.writeAll(objects, separator ?? '');
+    _buffer.writeAll(objects, separator);
     _flush();
   }
 
@@ -24,7 +24,7 @@
   }
 
   @override
-  void writeln([Object obj = '']) {
+  void writeln([Object? obj = '']) {
     _buffer.writeln(obj ?? '');
     _flush();
   }
diff --git a/pkgs/test_core/lib/src/util/stack_trace_mapper.dart b/pkgs/test_core/lib/src/util/stack_trace_mapper.dart
index 4f4a821..f55de3b 100644
--- a/pkgs/test_core/lib/src/util/stack_trace_mapper.dart
+++ b/pkgs/test_core/lib/src/util/stack_trace_mapper.dart
@@ -11,22 +11,22 @@
   /// The parsed source map.
   ///
   /// This is initialized lazily in `mapStackTrace()`.
-  Mapping _mapping;
+  Mapping? _mapping;
 
   /// The same package resolution information as was passed to dart2js.
-  final Map<String, Uri> _packageMap;
+  final Map<String, Uri>? _packageMap;
 
   /// The URL of the SDK root from which dart2js loaded its sources.
-  final Uri _sdkRoot;
+  final Uri? _sdkRoot;
 
   /// The contents of the source map.
   final String _mapContents;
 
   /// The URL of the source map.
-  final Uri _mapUrl;
+  final Uri? _mapUrl;
 
   JSStackTraceMapper(this._mapContents,
-      {Uri mapUrl, Map<String, Uri> packageMap, Uri sdkRoot})
+      {Uri? mapUrl, Map<String, Uri>? packageMap, Uri? sdkRoot})
       : _mapUrl = mapUrl,
         _packageMap = packageMap,
         _sdkRoot = sdkRoot;
@@ -34,8 +34,8 @@
   /// Converts [trace] into a Dart stack trace.
   @override
   StackTrace mapStackTrace(StackTrace trace) {
-    _mapping ??= parseExtended(_mapContents, mapUrl: _mapUrl);
-    return mapper.mapStackTrace(_mapping, trace,
+    var mapping = _mapping ??= parseExtended(_mapContents, mapUrl: _mapUrl);
+    return mapper.mapStackTrace(mapping, trace,
         packageMap: _packageMap, sdkRoot: _sdkRoot);
   }
 
@@ -52,7 +52,7 @@
 
   /// Returns a [StackTraceMapper] contained in the provided serialized
   /// representation.
-  static StackTraceMapper deserialize(Map serialized) {
+  static StackTraceMapper? deserialize(Map? serialized) {
     if (serialized == null) return null;
     var deserialized = _deserializePackageConfigMap(
         (serialized['packageConfigMap'] as Map).cast<String, String>());
@@ -65,16 +65,16 @@
 
   /// Converts a [packageConfigMap] into a format suitable for JSON
   /// serialization.
-  static Map<String, String> _serializePackageConfigMap(
-      Map<String, Uri> packageConfigMap) {
+  static Map<String, String>? _serializePackageConfigMap(
+      Map<String, Uri>? packageConfigMap) {
     if (packageConfigMap == null) return null;
     return packageConfigMap.map((key, value) => MapEntry(key, '$value'));
   }
 
   /// Converts a serialized package config map into a format suitable for
   /// the [PackageResolver]
-  static Map<String, Uri> _deserializePackageConfigMap(
-      Map<String, String> serialized) {
+  static Map<String, Uri>? _deserializePackageConfigMap(
+      Map<String, String>? serialized) {
     if (serialized == null) return null;
     return serialized.map((key, value) => MapEntry(key, Uri.parse(value)));
   }
diff --git a/pkgs/test_core/lib/src/util/string_literal_iterator.dart b/pkgs/test_core/lib/src/util/string_literal_iterator.dart
index bc1e284..5058028 100644
--- a/pkgs/test_core/lib/src/util/string_literal_iterator.dart
+++ b/pkgs/test_core/lib/src/util/string_literal_iterator.dart
@@ -1,6 +1,8 @@
 // Copyright (c) 2015, 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.
+//
+// @dart=2.9
 
 import 'dart:collection';
 
@@ -39,7 +41,7 @@
 class StringLiteralIterator extends Iterator<int> {
   @override
   int get current => _current;
-  int _current;
+  int /*?*/ _current;
 
   /// The offset of the beginning of [current] in the Dart source file that
   /// contains the string literal.
@@ -47,14 +49,14 @@
   /// Before iteration begins, this points to the character before the first
   /// rune.
   int get offset => _offset;
-  int _offset;
+  int /*?*/ _offset;
 
   /// The offset of the next rune.
   ///
   /// This isn't necessarily just `offset + 1`, since a single rune may be
   /// represented by multiple characters in the source file, or a string literal
   /// may be composed of several adjacent string literals.
-  int _nextOffset;
+  int /*?*/ _nextOffset;
 
   /// All [SimpleStringLiteral]s that compose the input literal.
   ///
@@ -66,13 +68,13 @@
   /// Whether this is a raw string that begins with `r`.
   ///
   /// This is necessary for knowing how to parse escape sequences.
-  bool _isRaw;
+  bool /*?*/ _isRaw;
 
   /// The iterator over the runes in the Dart source file.
   ///
   /// When switching to a new string in [_strings], this is updated to point to
   /// that string's component runes.
-  Iterator<int> _runes;
+  Iterator<int> /*?*/ _runes;
 
   /// Creates a new [StringLiteralIterator] iterating over the contents of
   /// [literal].
@@ -101,7 +103,7 @@
   bool moveNext() {
     // If we're at beginning of a [SimpleStringLiteral], move forward until
     // there's actually text to consume.
-    while (_runes == null || _runes.current == null || _runes.current < 0) {
+    while (_runes == null || _runes.current == -1) {
       if (_strings.isEmpty) {
         // Move the offset past the end of the text.
         _offset = _nextOffset;
@@ -135,7 +137,7 @@
   }
 
   /// Consume and return the next rune.
-  int _nextRune() {
+  int /*?*/ _nextRune() {
     if (_isRaw || _runes.current != _backslash) {
       var rune = _runes.current;
       _moveRunesNext();
@@ -150,7 +152,7 @@
   ///
   /// This assumes that a backslash has already been consumed. It leaves the
   /// [_runes] cursor on the first character after the escape sequence.
-  int _parseEscapeSequence() {
+  int /*?*/ _parseEscapeSequence() {
     switch (_runes.current) {
       case _n:
         _moveRunesNext();
@@ -194,7 +196,7 @@
   ///
   /// This parses digits as they appear in a unicode escape sequence: one to six
   /// hex digits.
-  int _parseHexSequence() {
+  int /*?*/ _parseHexSequence() {
     var number = _parseHexDigit(_runes.current);
     if (number == null) return null;
     if (!_moveRunesNext()) return null;
@@ -210,10 +212,10 @@
   }
 
   /// Parses [digits] hexadecimal digits and returns their value as an [int].
-  int _parseHex(int digits) {
+  int /*?*/ _parseHex(int digits) {
     var number = 0;
     for (var i = 0; i < digits; i++) {
-      if (_runes.current == null) return null;
+      if (_runes.current == -1) return null;
       var digit = _parseHexDigit(_runes.current);
       if (digit == null) return null;
       number = number * 16 + digit;
@@ -223,7 +225,7 @@
   }
 
   /// Parses a single hexadecimal digit.
-  int _parseHexDigit(int rune) {
+  int /*?*/ _parseHexDigit(int rune) {
     if (rune < _zero) return null;
     if (rune <= _nine) return rune - _zero;
     if (rune < _capitalA) return null;
@@ -236,7 +238,7 @@
   /// Move [_runes] to the next rune and update [_nextOffset].
   bool _moveRunesNext() {
     var result = _runes.moveNext();
-    _nextOffset++;
+    _nextOffset = _nextOffset + 1;
     return result;
   }
 }
diff --git a/pkgs/test_core/lib/test_core.dart b/pkgs/test_core/lib/test_core.dart
index 21f5ea2..af2357e 100644
--- a/pkgs/test_core/lib/test_core.dart
+++ b/pkgs/test_core/lib/test_core.dart
@@ -14,6 +14,7 @@
 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 'package:test_api/src/utils.dart'; // ignore: implementation_imports
 
 import 'src/runner/engine.dart';
@@ -34,7 +35,7 @@
 /// The global declarer.
 ///
 /// This is used if a test file is run directly, rather than through the runner.
-Declarer _globalDeclarer;
+Declarer? _globalDeclarer;
 
 /// Gets the declarer for the current scope.
 ///
@@ -44,16 +45,19 @@
 Declarer get _declarer {
   var declarer = Declarer.current;
   if (declarer != null) return declarer;
-  if (_globalDeclarer != null) return _globalDeclarer;
+  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 schedule a microtask to run the tests once they're
-  // finished being defined.
+  // [_globalDeclarer], and pump the event queue as a best effort to wait for
+  // all tests to be defined before starting them.
   _globalDeclarer = Declarer();
-  scheduleMicrotask(() async {
+
+  () async {
+    await pumpEventQueue();
+
     var suite = RunnerSuite(const PluginEnvironment(), SuiteConfiguration.empty,
-        _globalDeclarer.build(), SuitePlatform(Runtime.vm, os: currentOSGuess),
+        _globalDeclarer!.build(), SuitePlatform(Runtime.vm, os: currentOSGuess),
         path: p.prettyUri(Uri.base));
 
     var engine = Engine();
@@ -64,11 +68,12 @@
 
     var success = await runZoned(() => Invoker.guard(engine.run),
         zoneValues: {#test.declarer: _globalDeclarer});
-    if (success) return null;
+    if (success == true) return null;
     print('');
     unawaited(Future.error('Dummy exception to set exit code.'));
-  });
-  return _globalDeclarer;
+  }();
+
+  return _globalDeclarer!;
 }
 
 // TODO(nweiz): This and other top-level functions should throw exceptions if
@@ -128,12 +133,12 @@
 /// filter tests by name.
 @isTest
 void test(description, dynamic Function() body,
-    {String testOn,
-    Timeout timeout,
+    {String? testOn,
+    Timeout? timeout,
     skip,
     tags,
-    Map<String, dynamic> onPlatform,
-    int retry,
+    Map<String, dynamic>? onPlatform,
+    int? retry,
     @deprecated bool solo = false}) {
   _declarer.test(description.toString(), body,
       testOn: testOn,
@@ -206,12 +211,12 @@
 /// filter tests by name.
 @isTestGroup
 void group(description, dynamic Function() body,
-    {String testOn,
-    Timeout timeout,
+    {String? testOn,
+    Timeout? timeout,
     skip,
     tags,
-    Map<String, dynamic> onPlatform,
-    int retry,
+    Map<String, dynamic>? onPlatform,
+    int? retry,
     @deprecated bool solo = false}) {
   _declarer.group(description.toString(), body,
       testOn: testOn,
diff --git a/pkgs/test_core/mono_pkg.yaml b/pkgs/test_core/mono_pkg.yaml
index f6da98d..15b2c8e 100644
--- a/pkgs/test_core/mono_pkg.yaml
+++ b/pkgs/test_core/mono_pkg.yaml
@@ -1,9 +1,8 @@
+dart:
+  - dev
+
 stages:
   - analyze_and_format:
     - group:
       - dartfmt: sdk
-      - dartanalyzer: --fatal-infos --fatal-warnings .
-      dart: dev
-    - group:
-      - dartanalyzer: --fatal-warnings .
-      dart: 2.7.0
+      - dartanalyzer: --enable-experiment=non-nullable --fatal-infos --fatal-warnings .
diff --git a/pkgs/test_core/pubspec.yaml b/pkgs/test_core/pubspec.yaml
index 71a92ff..5c15c4d 100644
--- a/pkgs/test_core/pubspec.yaml
+++ b/pkgs/test_core/pubspec.yaml
@@ -1,36 +1,36 @@
 name: test_core
-version: 0.3.11+3
+version: 0.3.12-nullsafety.11
 description: A basic library for writing tests and running them on the VM.
 homepage: https://github.com/dart-lang/test/blob/master/pkgs/test_core
 
 environment:
-  sdk: ">=2.7.0 <3.0.0"
+  sdk: ">=2.12.0-0 <3.0.0"
 
 dependencies:
   analyzer: ">=0.39.5 <0.42.0"
-  async: ^2.0.0
+  async: '>=2.5.0-nullsafety <2.5.0'
   args: ^1.4.0
-  boolean_selector: ">=1.0.0 <3.0.0"
-  collection: ^1.8.0
-  coverage: ">=0.13.3 < 0.15.0"
+  boolean_selector: ">=2.1.0-nullsafety <2.1.0"
+  collection: '>=1.15.0-nullsafety <1.15.0'
+  coverage: ">=0.13.3 <0.15.0"
   glob: ^1.0.0
   io: ^0.3.0
-  meta: ^1.1.5
+  meta: '>=1.3.0-nullsafety <1.3.0'
   package_config: ^1.9.2
-  path: ^1.2.0
-  pedantic: ^1.0.0
-  pool: ^1.3.0
-  source_map_stack_trace: ^2.0.0
-  source_maps: ^0.10.2
-  source_span: ^1.4.0
-  stack_trace: ^1.9.0
-  stream_channel: ">=1.7.0 <3.0.0"
-  vm_service: '>=1.0.0 <5.0.0'
+  path: '>=1.8.0-nullsafety <1.8.0'
+  pedantic: '>=1.10.0-nullsafety <1.10.0'
+  pool: '>=1.5.0-nullsafety <1.5.0'
+  source_map_stack_trace: '>=2.1.0-nullsafety <2.1.0'
+  source_maps: '>=0.10.10-nullsafety <0.10.10'
+  source_span: '>=1.8.0-nullsafety <1.8.0'
+  stack_trace: '>=1.10.0-nullsafety <1.10.0'
+  stream_channel: ">=2.1.0-nullsafety <2.1.0"
+  vm_service: '>=1.0.0 <6.0.0'
   yaml: ^2.0.0
   # matcher is tightly constrained by test_api
   matcher: any
   # Use an exact version until the test_api package is stable.
-  test_api: 0.2.18+1
+  test_api: 0.2.19-nullsafety.6
 
 dependency_overrides:
   test_api:
diff --git a/tool/mono_repo_self_validate.sh b/tool/mono_repo_self_validate.sh
new file mode 100755
index 0000000..ec70a28
--- /dev/null
+++ b/tool/mono_repo_self_validate.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+# Created with package:mono_repo v2.5.0
+
+# Support built in commands on windows out of the box.
+function pub {
+       if [[ $TRAVIS_OS_NAME == "windows" ]]; then
+        command pub.bat "$@"
+    else
+        command pub "$@"
+    fi
+}
+
+set -v -e
+pub global activate mono_repo 2.5.0
+pub global run mono_repo travis --validate
diff --git a/tool/report_failure.sh b/tool/report_failure.sh
index 39d87a9..3a0d925 100755
--- a/tool/report_failure.sh
+++ b/tool/report_failure.sh
@@ -1,6 +1,5 @@
-TRAVIS_BUILDS_URI="https://travis-ci.org/dart-lang/test/builds"
 if [ "$TRAVIS_EVENT_TYPE" != "pull_request" -a "$TRAVIS_ALLOW_FAILURE" != "true" ]; then
   curl -H "Content-Type: application/json" -X POST -d \
-    "{'text':'Build failed! ${TRAVIS_BUILDS_URI}/${TRAVIS_BUILD_ID}'}" \
+    "{'text':'Build failed! ${TRAVIS_BUILD_WEB_URL}'}" \
     "${CHAT_HOOK_URI}"
 fi
diff --git a/tool/travis.sh b/tool/travis.sh
index 7ac5921..765947a 100755
--- a/tool/travis.sh
+++ b/tool/travis.sh
@@ -1,103 +1,126 @@
 #!/bin/bash
-# Created with package:mono_repo v2.3.0
+# Created with package:mono_repo v3.0.0
 
 # Support built in commands on windows out of the box.
-function pub {
-       if [[ $TRAVIS_OS_NAME == "windows" ]]; then
-        command pub.bat "$@"
-    else
-        command pub "$@"
-    fi
+function pub() {
+  if [[ $TRAVIS_OS_NAME == "windows" ]]; then
+    command pub.bat "$@"
+  else
+    command pub "$@"
+  fi
 }
-function dartfmt {
-       if [[ $TRAVIS_OS_NAME == "windows" ]]; then
-        command dartfmt.bat "$@"
-    else
-        command dartfmt "$@"
-    fi
+function dartfmt() {
+  if [[ $TRAVIS_OS_NAME == "windows" ]]; then
+    command dartfmt.bat "$@"
+  else
+    command dartfmt "$@"
+  fi
 }
-function dartanalyzer {
-       if [[ $TRAVIS_OS_NAME == "windows" ]]; then
-        command dartanalyzer.bat "$@"
-    else
-        command dartanalyzer "$@"
-    fi
+function dartanalyzer() {
+  if [[ $TRAVIS_OS_NAME == "windows" ]]; then
+    command dartanalyzer.bat "$@"
+  else
+    command dartanalyzer "$@"
+  fi
 }
 
 if [[ -z ${PKGS} ]]; then
-  echo -e '\033[31mPKGS environment variable must be set!\033[0m'
-  exit 1
+  echo -e '\033[31mPKGS environment variable must be set! - TERMINATING JOB\033[0m'
+  exit 64
 fi
 
 if [[ "$#" == "0" ]]; then
-  echo -e '\033[31mAt least one task argument must be provided!\033[0m'
-  exit 1
+  echo -e '\033[31mAt least one task argument must be provided! - TERMINATING JOB\033[0m'
+  exit 64
 fi
 
-EXIT_CODE=0
+SUCCESS_COUNT=0
+declare -a FAILURES
 
 for PKG in ${PKGS}; do
   echo -e "\033[1mPKG: ${PKG}\033[22m"
-  pushd "${PKG}" || exit $?
+  EXIT_CODE=0
+  pushd "${PKG}" >/dev/null || EXIT_CODE=$?
 
-  PUB_EXIT_CODE=0
-  pub upgrade --no-precompile || PUB_EXIT_CODE=$?
-
-  if [[ ${PUB_EXIT_CODE} -ne 0 ]]; then
-    EXIT_CODE=1
-    echo -e '\033[31mpub upgrade failed\033[0m'
-    popd
-    continue
+  if [[ ${EXIT_CODE} -ne 0 ]]; then
+    echo -e "\033[31mPKG: '${PKG}' does not exist - TERMINATING JOB\033[0m"
+    exit 64
   fi
 
-  for TASK in "$@"; do
-    echo
-    echo -e "\033[1mPKG: ${PKG}; TASK: ${TASK}\033[22m"
-    case ${TASK} in
-    command_0)
-      echo 'xvfb-run -s "-screen 0 1024x768x24" pub run test --preset travis -x phantomjs --total-shards 5 --shard-index 0'
-      xvfb-run -s "-screen 0 1024x768x24" pub run test --preset travis -x phantomjs --total-shards 5 --shard-index 0 || EXIT_CODE=$?
-      ;;
-    command_1)
-      echo 'xvfb-run -s "-screen 0 1024x768x24" pub run test --preset travis -x phantomjs --total-shards 5 --shard-index 1'
-      xvfb-run -s "-screen 0 1024x768x24" pub run test --preset travis -x phantomjs --total-shards 5 --shard-index 1 || EXIT_CODE=$?
-      ;;
-    command_2)
-      echo 'xvfb-run -s "-screen 0 1024x768x24" pub run test --preset travis -x phantomjs --total-shards 5 --shard-index 2'
-      xvfb-run -s "-screen 0 1024x768x24" pub run test --preset travis -x phantomjs --total-shards 5 --shard-index 2 || EXIT_CODE=$?
-      ;;
-    command_3)
-      echo 'xvfb-run -s "-screen 0 1024x768x24" pub run test --preset travis -x phantomjs --total-shards 5 --shard-index 3'
-      xvfb-run -s "-screen 0 1024x768x24" pub run test --preset travis -x phantomjs --total-shards 5 --shard-index 3 || EXIT_CODE=$?
-      ;;
-    command_4)
-      echo 'xvfb-run -s "-screen 0 1024x768x24" pub run test --preset travis -x phantomjs --total-shards 5 --shard-index 4'
-      xvfb-run -s "-screen 0 1024x768x24" pub run test --preset travis -x phantomjs --total-shards 5 --shard-index 4 || EXIT_CODE=$?
-      ;;
-    dartanalyzer_0)
-      echo 'dartanalyzer --fatal-infos --fatal-warnings .'
-      dartanalyzer --fatal-infos --fatal-warnings . || EXIT_CODE=$?
-      ;;
-    dartanalyzer_1)
-      echo 'dartanalyzer --fatal-warnings .'
-      dartanalyzer --fatal-warnings . || EXIT_CODE=$?
-      ;;
-    dartfmt)
-      echo 'dartfmt -n --set-exit-if-changed .'
-      dartfmt -n --set-exit-if-changed . || EXIT_CODE=$?
-      ;;
-    test)
-      echo 'pub run test --preset travis'
-      pub run test --preset travis || EXIT_CODE=$?
-      ;;
-    *)
-      echo -e "\033[31mNot expecting TASK '${TASK}'. Error!\033[0m"
-      EXIT_CODE=1
-      ;;
-    esac
-  done
+  pub upgrade --no-precompile || EXIT_CODE=$?
 
-  popd
+  if [[ ${EXIT_CODE} -ne 0 ]]; then
+    echo -e "\033[31mPKG: ${PKG}; 'pub upgrade' - FAILED  (${EXIT_CODE})\033[0m"
+    FAILURES+=("${PKG}; 'pub upgrade'")
+  else
+    for TASK in "$@"; do
+      EXIT_CODE=0
+      echo
+      echo -e "\033[1mPKG: ${PKG}; TASK: ${TASK}\033[22m"
+      case ${TASK} in
+      command_0)
+        echo 'xvfb-run -s "-screen 0 1024x768x24" pub run --enable-experiment=non-nullable test --preset travis -x phantomjs --total-shards 5 --shard-index 0'
+        xvfb-run -s "-screen 0 1024x768x24" pub run --enable-experiment=non-nullable test --preset travis -x phantomjs --total-shards 5 --shard-index 0 || EXIT_CODE=$?
+        ;;
+      command_1)
+        echo 'xvfb-run -s "-screen 0 1024x768x24" pub run --enable-experiment=non-nullable test --preset travis -x phantomjs --total-shards 5 --shard-index 1'
+        xvfb-run -s "-screen 0 1024x768x24" pub run --enable-experiment=non-nullable test --preset travis -x phantomjs --total-shards 5 --shard-index 1 || EXIT_CODE=$?
+        ;;
+      command_2)
+        echo 'xvfb-run -s "-screen 0 1024x768x24" pub run --enable-experiment=non-nullable test --preset travis -x phantomjs --total-shards 5 --shard-index 2'
+        xvfb-run -s "-screen 0 1024x768x24" pub run --enable-experiment=non-nullable test --preset travis -x phantomjs --total-shards 5 --shard-index 2 || EXIT_CODE=$?
+        ;;
+      command_3)
+        echo 'xvfb-run -s "-screen 0 1024x768x24" pub run --enable-experiment=non-nullable test --preset travis -x phantomjs --total-shards 5 --shard-index 3'
+        xvfb-run -s "-screen 0 1024x768x24" pub run --enable-experiment=non-nullable test --preset travis -x phantomjs --total-shards 5 --shard-index 3 || EXIT_CODE=$?
+        ;;
+      command_4)
+        echo 'xvfb-run -s "-screen 0 1024x768x24" pub run --enable-experiment=non-nullable test --preset travis -x phantomjs --total-shards 5 --shard-index 4'
+        xvfb-run -s "-screen 0 1024x768x24" pub run --enable-experiment=non-nullable test --preset travis -x phantomjs --total-shards 5 --shard-index 4 || EXIT_CODE=$?
+        ;;
+      command_5)
+        echo 'pub run --enable-experiment=non-nullable test --preset travis -x browser'
+        pub run --enable-experiment=non-nullable test --preset travis -x browser || EXIT_CODE=$?
+        ;;
+      dartanalyzer)
+        echo 'dartanalyzer --enable-experiment=non-nullable --fatal-infos --fatal-warnings .'
+        dartanalyzer --enable-experiment=non-nullable --fatal-infos --fatal-warnings . || EXIT_CODE=$?
+        ;;
+      dartfmt)
+        echo 'dartfmt -n --set-exit-if-changed .'
+        dartfmt -n --set-exit-if-changed . || EXIT_CODE=$?
+        ;;
+      *)
+        echo -e "\033[31mUnknown TASK '${TASK}' - TERMINATING JOB\033[0m"
+        exit 64
+        ;;
+      esac
+
+      if [[ ${EXIT_CODE} -ne 0 ]]; then
+        echo -e "\033[31mPKG: ${PKG}; TASK: ${TASK} - FAILED (${EXIT_CODE})\033[0m"
+        FAILURES+=("${PKG}; TASK: ${TASK}")
+      else
+        echo -e "\033[32mPKG: ${PKG}; TASK: ${TASK} - SUCCEEDED\033[0m"
+        SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
+      fi
+
+    done
+  fi
+
+  echo
+  echo -e "\033[32mSUCCESS COUNT: ${SUCCESS_COUNT}\033[0m"
+
+  if [ ${#FAILURES[@]} -ne 0 ]; then
+    echo -e "\033[31mFAILURES: ${#FAILURES[@]}\033[0m"
+    for i in "${FAILURES[@]}"; do
+      echo -e "\033[31m  $i\033[0m"
+    done
+  fi
+
+  popd >/dev/null || exit 70
+  echo
 done
 
-exit ${EXIT_CODE}
+if [ ${#FAILURES[@]} -ne 0 ]; then
+  exit 1
+fi