Merge package:test_descriptor into the test monorepo
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..f189cdd
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+.github/workflows/dart.yml linguist-generated=true
+tool/ci.sh linguist-generated=true
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..a53d62f
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+/pkgs/ @dart-lang/core-package-admins
diff --git a/.github/ISSUE_TEMPLATE/01_test_runner_bug.md b/.github/ISSUE_TEMPLATE/01_test_runner_bug.md
new file mode 100644
index 0000000..e49d277
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/01_test_runner_bug.md
@@ -0,0 +1,14 @@
+---
+name: I found a bug in the test runner
+about: >-
+  Report a bug in running tests, integration with a specific platform, or
+  any behavior of 'package:test'.
+title: ''
+labels: bug
+---
+Describe the bug in detail.
+Include the specific command you ran and any relevant details about the
+environment, including operating system, Dart SDK version, and the version of
+`package:test` you are using.
+Whenever possible, include a full reproduction example that we can run to
+observe the problem.
diff --git a/.github/ISSUE_TEMPLATE/02_test_runner_feature.md b/.github/ISSUE_TEMPLATE/02_test_runner_feature.md
new file mode 100644
index 0000000..56170ba
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/02_test_runner_feature.md
@@ -0,0 +1,10 @@
+---
+name: I'd like a new feature in the test runner
+about: >-
+  Propose a feature for 'package:test' that would make testing easier or more
+  powerful.
+title: ''
+labels: enhancement
+---
+Describe the feature and include specific description of the use case, or use
+cases, you have in mind.
diff --git a/.github/ISSUE_TEMPLATE/03_checks_feedback.md b/.github/ISSUE_TEMPLATE/03_checks_feedback.md
new file mode 100644
index 0000000..5eb0039
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/03_checks_feedback.md
@@ -0,0 +1,8 @@
+---
+name: I have feedback for the package:checks preview
+about: Suggest a change, new feature, or bug, for 'package:checks'.
+title: ''
+labels: package:checks
+---
+Add any feedback about `package:checks`.
+Include a description of a specific use case for any feature requests.
diff --git a/.github/ISSUE_TEMPLATE/fake_async.md b/.github/ISSUE_TEMPLATE/fake_async.md
new file mode 100644
index 0000000..c26edd2
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/fake_async.md
@@ -0,0 +1,5 @@
+---
+name: "package:fake_async"
+about: "Create a bug or file a feature request against package:fake_async."
+labels: "package:fake_async"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/matcher.md b/.github/ISSUE_TEMPLATE/matcher.md
new file mode 100644
index 0000000..f41464d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/matcher.md
@@ -0,0 +1,5 @@
+---
+name: "package:matcher"
+about: "Create a bug or file a feature request against package:matcher."
+labels: "package:matcher"
+---
\ No newline at end of file
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..8aa9b97
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,33 @@
+# Dependabot configuration file.
+# See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates
+
+version: 2
+
+updates:
+  - package-ecosystem: github-actions
+    directory: /
+    schedule:
+      interval: monthly
+    labels:
+      - autosubmit
+    groups:
+      github-actions:
+        patterns:
+          - "*"
+
+  # Check daily for major version dependency updates for packages which depend
+  # on package:analyzer.
+  - package-ecosystem: "pub"
+    directory: "pkgs/test"
+    schedule:
+      interval: "daily"
+    versioning-strategy: increase-if-necessary
+    reviewers:
+      - "natebosch"
+  - package-ecosystem: "pub"
+    directory: "pkgs/test_core"
+    schedule:
+      interval: "daily"
+    versioning-strategy: increase-if-necessary
+    reviewers:
+      - "natebosch"
diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml
new file mode 100644
index 0000000..9a3cd79
--- /dev/null
+++ b/.github/workflows/dart.yml
@@ -0,0 +1,1903 @@
+# Created with package:mono_repo v6.6.2
+name: Dart CI
+on:
+  push:
+    branches:
+      - main
+      - master
+  pull_request:
+  schedule:
+    - cron: "0 0 * * 0"
+defaults:
+  run:
+    shell: bash
+env:
+  PUB_ENVIRONMENT: bot.github
+permissions: read-all
+
+jobs:
+  job_001:
+    name: mono_repo self validate
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: stable
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - name: mono_repo self validate
+        run: dart pub global activate mono_repo 6.6.2
+      - name: mono_repo self validate
+        run: dart pub global run mono_repo generate --validate
+  job_002:
+    name: "analyze_and_format; linux; Dart 3.3.0; PKG: pkgs/fake_async; `dart analyze`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/fake_async;commands:analyze_1"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/fake_async
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.3.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_fake_async_pub_upgrade
+        name: pkgs/fake_async; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/fake_async
+      - name: pkgs/fake_async; dart analyze
+        run: dart analyze
+        if: "always() && steps.pkgs_fake_async_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/fake_async
+  job_003:
+    name: "analyze_and_format; linux; Dart 3.4.0; PKG: pkgs/matcher; `dart analyze`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.4.0;packages:pkgs/matcher;commands:analyze_1"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.4.0;packages:pkgs/matcher
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.4.0
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.4.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_matcher_pub_upgrade
+        name: pkgs/matcher; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/matcher
+      - name: pkgs/matcher; dart analyze
+        run: dart analyze
+        if: "always() && steps.pkgs_matcher_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/matcher
+  job_004:
+    name: "analyze_and_format; linux; Dart 3.5.0; PKGS: integration_tests/regression, integration_tests/wasm; `dart format --output=none --set-exit-if-changed .`, `dart analyze --fatal-infos`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:integration_tests/regression-integration_tests/wasm;commands:format-analyze_0"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:integration_tests/regression-integration_tests/wasm
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: integration_tests_regression_pub_upgrade
+        name: integration_tests/regression; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: integration_tests/regression
+      - name: "integration_tests/regression; dart format --output=none --set-exit-if-changed ."
+        run: "dart format --output=none --set-exit-if-changed ."
+        if: "always() && steps.integration_tests_regression_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/regression
+      - name: "integration_tests/regression; dart analyze --fatal-infos"
+        run: dart analyze --fatal-infos
+        if: "always() && steps.integration_tests_regression_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/regression
+      - id: integration_tests_wasm_pub_upgrade
+        name: integration_tests/wasm; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+      - name: "integration_tests/wasm; dart format --output=none --set-exit-if-changed ."
+        run: "dart format --output=none --set-exit-if-changed ."
+        if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+      - name: "integration_tests/wasm; dart analyze --fatal-infos"
+        run: dart analyze --fatal-infos
+        if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+  job_005:
+    name: "analyze_and_format; linux; Dart 3.5.0; PKGS: pkgs/checks, pkgs/test_core; `dart analyze`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/checks-pkgs/test_core;commands:analyze_1"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/checks-pkgs/test_core
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_checks_pub_upgrade
+        name: pkgs/checks; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/checks
+      - name: pkgs/checks; dart analyze
+        run: dart analyze
+        if: "always() && steps.pkgs_checks_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/checks
+      - id: pkgs_test_core_pub_upgrade
+        name: pkgs/test_core; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test_core
+      - name: pkgs/test_core; dart analyze
+        run: dart analyze
+        if: "always() && steps.pkgs_test_core_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test_core
+  job_006:
+    name: "analyze_and_format; linux; Dart dev; PKGS: integration_tests/regression, integration_tests/spawn_hybrid, integration_tests/wasm, pkgs/checks, pkgs/fake_async, pkgs/matcher, pkgs/test, pkgs/test_api, pkgs/test_core; `dart format --output=none --set-exit-if-changed .`, `dart analyze --fatal-infos`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:integration_tests/regression-integration_tests/spawn_hybrid-integration_tests/wasm-pkgs/checks-pkgs/fake_async-pkgs/matcher-pkgs/test-pkgs/test_api-pkgs/test_core;commands:format-analyze_0"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:integration_tests/regression-integration_tests/spawn_hybrid-integration_tests/wasm-pkgs/checks-pkgs/fake_async-pkgs/matcher-pkgs/test-pkgs/test_api-pkgs/test_core
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: dev
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: integration_tests_regression_pub_upgrade
+        name: integration_tests/regression; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: integration_tests/regression
+      - name: "integration_tests/regression; dart format --output=none --set-exit-if-changed ."
+        run: "dart format --output=none --set-exit-if-changed ."
+        if: "always() && steps.integration_tests_regression_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/regression
+      - name: "integration_tests/regression; dart analyze --fatal-infos"
+        run: dart analyze --fatal-infos
+        if: "always() && steps.integration_tests_regression_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/regression
+      - id: integration_tests_spawn_hybrid_pub_upgrade
+        name: integration_tests/spawn_hybrid; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: integration_tests/spawn_hybrid
+      - name: "integration_tests/spawn_hybrid; dart format --output=none --set-exit-if-changed ."
+        run: "dart format --output=none --set-exit-if-changed ."
+        if: "always() && steps.integration_tests_spawn_hybrid_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/spawn_hybrid
+      - name: "integration_tests/spawn_hybrid; dart analyze --fatal-infos"
+        run: dart analyze --fatal-infos
+        if: "always() && steps.integration_tests_spawn_hybrid_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/spawn_hybrid
+      - id: integration_tests_wasm_pub_upgrade
+        name: integration_tests/wasm; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+      - name: "integration_tests/wasm; dart format --output=none --set-exit-if-changed ."
+        run: "dart format --output=none --set-exit-if-changed ."
+        if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+      - name: "integration_tests/wasm; dart analyze --fatal-infos"
+        run: dart analyze --fatal-infos
+        if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+      - id: pkgs_checks_pub_upgrade
+        name: pkgs/checks; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/checks
+      - name: "pkgs/checks; dart format --output=none --set-exit-if-changed ."
+        run: "dart format --output=none --set-exit-if-changed ."
+        if: "always() && steps.pkgs_checks_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/checks
+      - name: "pkgs/checks; dart analyze --fatal-infos"
+        run: dart analyze --fatal-infos
+        if: "always() && steps.pkgs_checks_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/checks
+      - id: pkgs_fake_async_pub_upgrade
+        name: pkgs/fake_async; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/fake_async
+      - name: "pkgs/fake_async; dart format --output=none --set-exit-if-changed ."
+        run: "dart format --output=none --set-exit-if-changed ."
+        if: "always() && steps.pkgs_fake_async_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/fake_async
+      - name: "pkgs/fake_async; dart analyze --fatal-infos"
+        run: dart analyze --fatal-infos
+        if: "always() && steps.pkgs_fake_async_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/fake_async
+      - id: pkgs_matcher_pub_upgrade
+        name: pkgs/matcher; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/matcher
+      - name: "pkgs/matcher; dart format --output=none --set-exit-if-changed ."
+        run: "dart format --output=none --set-exit-if-changed ."
+        if: "always() && steps.pkgs_matcher_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/matcher
+      - name: "pkgs/matcher; dart analyze --fatal-infos"
+        run: dart analyze --fatal-infos
+        if: "always() && steps.pkgs_matcher_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/matcher
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; dart format --output=none --set-exit-if-changed ."
+        run: "dart format --output=none --set-exit-if-changed ."
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; dart analyze --fatal-infos"
+        run: dart analyze --fatal-infos
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+      - id: pkgs_test_api_pub_upgrade
+        name: pkgs/test_api; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test_api
+      - name: "pkgs/test_api; dart format --output=none --set-exit-if-changed ."
+        run: "dart format --output=none --set-exit-if-changed ."
+        if: "always() && steps.pkgs_test_api_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test_api
+      - name: "pkgs/test_api; dart analyze --fatal-infos"
+        run: dart analyze --fatal-infos
+        if: "always() && steps.pkgs_test_api_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test_api
+      - id: pkgs_test_core_pub_upgrade
+        name: pkgs/test_core; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test_core
+      - name: "pkgs/test_core; dart format --output=none --set-exit-if-changed ."
+        run: "dart format --output=none --set-exit-if-changed ."
+        if: "always() && steps.pkgs_test_core_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test_core
+      - name: "pkgs/test_core; dart analyze --fatal-infos"
+        run: dart analyze --fatal-infos
+        if: "always() && steps.pkgs_test_core_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test_core
+  job_007:
+    name: "analyze_and_format; windows; Dart 3.5.0; PKG: integration_tests/wasm; `dart format --output=none --set-exit-if-changed .`, `dart analyze --fatal-infos`"
+    runs-on: windows-latest
+    steps:
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: integration_tests_wasm_pub_upgrade
+        name: integration_tests/wasm; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+      - name: "integration_tests/wasm; dart format --output=none --set-exit-if-changed ."
+        run: "dart format --output=none --set-exit-if-changed ."
+        if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+      - name: "integration_tests/wasm; dart analyze --fatal-infos"
+        run: dart analyze --fatal-infos
+        if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+  job_008:
+    name: "analyze_and_format; windows; Dart dev; PKG: integration_tests/wasm; `dart format --output=none --set-exit-if-changed .`, `dart analyze --fatal-infos`"
+    runs-on: windows-latest
+    steps:
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: dev
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: integration_tests_wasm_pub_upgrade
+        name: integration_tests/wasm; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+      - name: "integration_tests/wasm; dart format --output=none --set-exit-if-changed ."
+        run: "dart format --output=none --set-exit-if-changed ."
+        if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+      - name: "integration_tests/wasm; dart analyze --fatal-infos"
+        run: dart analyze --fatal-infos
+        if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+  job_009:
+    name: "unit_test; linux; Dart 3.3.0; PKG: pkgs/fake_async; `dart test`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/fake_async;commands:command_00"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/fake_async
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.3.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_fake_async_pub_upgrade
+        name: pkgs/fake_async; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/fake_async
+      - name: pkgs/fake_async; dart test
+        run: dart test
+        if: "always() && steps.pkgs_fake_async_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/fake_async
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_010:
+    name: "unit_test; linux; Dart 3.4.0; PKG: pkgs/matcher; `dart test`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.4.0;packages:pkgs/matcher;commands:command_00"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.4.0;packages:pkgs/matcher
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.4.0
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.4.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_matcher_pub_upgrade
+        name: pkgs/matcher; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/matcher
+      - name: pkgs/matcher; dart test
+        run: dart test
+        if: "always() && steps.pkgs_matcher_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/matcher
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_011:
+    name: "unit_test; linux; Dart 3.5.0; PKG: integration_tests/regression; `dart test`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:integration_tests/regression;commands:command_00"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:integration_tests/regression
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: integration_tests_regression_pub_upgrade
+        name: integration_tests/regression; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: integration_tests/regression
+      - name: integration_tests/regression; dart test
+        run: dart test
+        if: "always() && steps.integration_tests_regression_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/regression
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_012:
+    name: "unit_test; linux; Dart 3.5.0; PKG: pkgs/checks; `dart test`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/checks;commands:command_00"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/checks
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_checks_pub_upgrade
+        name: pkgs/checks; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/checks
+      - name: pkgs/checks; dart test
+        run: dart test
+        if: "always() && steps.pkgs_checks_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/checks
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_013:
+    name: "unit_test; linux; Dart 3.5.0; PKG: pkgs/test_core; `dart test`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test_core;commands:command_00"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test_core
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_core_pub_upgrade
+        name: pkgs/test_core; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test_core
+      - name: pkgs/test_core; dart test
+        run: dart test
+        if: "always() && steps.pkgs_test_core_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test_core
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_014:
+    name: "unit_test; linux; Dart 3.5.0; PKG: integration_tests/spawn_hybrid; `dart test -p chrome,vm,node`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:integration_tests/spawn_hybrid;commands:test_1"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:integration_tests/spawn_hybrid
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: integration_tests_spawn_hybrid_pub_upgrade
+        name: integration_tests/spawn_hybrid; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: integration_tests/spawn_hybrid
+      - name: "integration_tests/spawn_hybrid; dart test -p chrome,vm,node"
+        run: "dart test -p chrome,vm,node"
+        if: "always() && steps.integration_tests_spawn_hybrid_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/spawn_hybrid
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_015:
+    name: "unit_test; linux; Dart 3.5.0; PKG: integration_tests/wasm; `dart test --timeout=60s`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:integration_tests/wasm;commands:test_2"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:integration_tests/wasm
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: integration_tests_wasm_pub_upgrade
+        name: integration_tests/wasm; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+      - name: "integration_tests/wasm; dart test --timeout=60s"
+        run: "dart test --timeout=60s"
+        if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_016:
+    name: "unit_test; linux; Dart 3.5.0; PKG: pkgs/test; `xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 0`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_01"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 0"
+        run: "xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 0"
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_017:
+    name: "unit_test; linux; Dart 3.5.0; PKG: pkgs/test; `xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 1`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_02"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 1"
+        run: "xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 1"
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_018:
+    name: "unit_test; linux; Dart 3.5.0; PKG: pkgs/test; `xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 2`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_03"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 2"
+        run: "xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 2"
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_019:
+    name: "unit_test; linux; Dart 3.5.0; PKG: pkgs/test; `xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 3`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_04"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 3"
+        run: "xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 3"
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_020:
+    name: "unit_test; linux; Dart 3.5.0; PKG: pkgs/test; `xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 4`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_05"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 4"
+        run: "xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 4"
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_021:
+    name: "unit_test; linux; Dart 3.5.0; PKG: pkgs/test_api; `dart test --preset travis -x browser`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test_api;commands:command_11"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test_api
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_api_pub_upgrade
+        name: pkgs/test_api; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test_api
+      - name: "pkgs/test_api; dart test --preset travis -x browser"
+        run: dart test --preset travis -x browser
+        if: "always() && steps.pkgs_test_api_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test_api
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_022:
+    name: "unit_test; linux; Dart dev; PKG: integration_tests/regression; `dart test`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:integration_tests/regression;commands:command_00"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:integration_tests/regression
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: dev
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: integration_tests_regression_pub_upgrade
+        name: integration_tests/regression; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: integration_tests/regression
+      - name: integration_tests/regression; dart test
+        run: dart test
+        if: "always() && steps.integration_tests_regression_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/regression
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_023:
+    name: "unit_test; linux; Dart dev; PKG: pkgs/checks; `dart test`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/checks;commands:command_00"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/checks
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: dev
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_checks_pub_upgrade
+        name: pkgs/checks; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/checks
+      - name: pkgs/checks; dart test
+        run: dart test
+        if: "always() && steps.pkgs_checks_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/checks
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_024:
+    name: "unit_test; linux; Dart dev; PKG: pkgs/fake_async; `dart test`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/fake_async;commands:command_00"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/fake_async
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: dev
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_fake_async_pub_upgrade
+        name: pkgs/fake_async; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/fake_async
+      - name: pkgs/fake_async; dart test
+        run: dart test
+        if: "always() && steps.pkgs_fake_async_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/fake_async
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_025:
+    name: "unit_test; linux; Dart dev; PKG: pkgs/matcher; `dart test`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/matcher;commands:command_00"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/matcher
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: dev
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_matcher_pub_upgrade
+        name: pkgs/matcher; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/matcher
+      - name: pkgs/matcher; dart test
+        run: dart test
+        if: "always() && steps.pkgs_matcher_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/matcher
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_026:
+    name: "unit_test; linux; Dart dev; PKG: pkgs/test_core; `dart test`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test_core;commands:command_00"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test_core
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: dev
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_core_pub_upgrade
+        name: pkgs/test_core; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test_core
+      - name: pkgs/test_core; dart test
+        run: dart test
+        if: "always() && steps.pkgs_test_core_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test_core
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_027:
+    name: "unit_test; linux; Dart dev; PKG: integration_tests/spawn_hybrid; `dart test -p chrome,vm,node`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:integration_tests/spawn_hybrid;commands:test_1"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:integration_tests/spawn_hybrid
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: dev
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: integration_tests_spawn_hybrid_pub_upgrade
+        name: integration_tests/spawn_hybrid; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: integration_tests/spawn_hybrid
+      - name: "integration_tests/spawn_hybrid; dart test -p chrome,vm,node"
+        run: "dart test -p chrome,vm,node"
+        if: "always() && steps.integration_tests_spawn_hybrid_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/spawn_hybrid
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_028:
+    name: "unit_test; linux; Dart dev; PKG: integration_tests/wasm; `dart test --timeout=60s`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:integration_tests/wasm;commands:test_2"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:integration_tests/wasm
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: dev
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: integration_tests_wasm_pub_upgrade
+        name: integration_tests/wasm; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+      - name: "integration_tests/wasm; dart test --timeout=60s"
+        run: "dart test --timeout=60s"
+        if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_029:
+    name: "unit_test; linux; Dart dev; PKG: pkgs/test; `xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 0`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test;commands:command_01"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: dev
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 0"
+        run: "xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 0"
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_030:
+    name: "unit_test; linux; Dart dev; PKG: pkgs/test; `xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 1`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test;commands:command_02"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: dev
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 1"
+        run: "xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 1"
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_031:
+    name: "unit_test; linux; Dart dev; PKG: pkgs/test; `xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 2`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test;commands:command_03"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: dev
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 2"
+        run: "xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 2"
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_032:
+    name: "unit_test; linux; Dart dev; PKG: pkgs/test; `xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 3`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test;commands:command_04"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: dev
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 3"
+        run: "xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 3"
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_033:
+    name: "unit_test; linux; Dart dev; PKG: pkgs/test; `xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 4`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test;commands:command_05"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: dev
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 4"
+        run: "xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 4"
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_034:
+    name: "unit_test; linux; Dart dev; PKG: pkgs/test_api; `dart test --preset travis -x browser`"
+    runs-on: ubuntu-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test_api;commands:command_11"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test_api
+            os:ubuntu-latest;pub-cache-hosted;sdk:dev
+            os:ubuntu-latest;pub-cache-hosted
+            os:ubuntu-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: dev
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_api_pub_upgrade
+        name: pkgs/test_api; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test_api
+      - name: "pkgs/test_api; dart test --preset travis -x browser"
+        run: dart test --preset travis -x browser
+        if: "always() && steps.pkgs_test_api_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test_api
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_035:
+    name: "unit_test; osx; Dart 3.5.0; PKG: pkgs/test; `dart test --preset travis --total-shards 5 --shard-index 0`"
+    runs-on: macos-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_06"
+          restore-keys: |
+            os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test
+            os:macos-latest;pub-cache-hosted;sdk:3.5.0
+            os:macos-latest;pub-cache-hosted
+            os:macos-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; dart test --preset travis --total-shards 5 --shard-index 0"
+        run: dart test --preset travis --total-shards 5 --shard-index 0
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_036:
+    name: "unit_test; osx; Dart 3.5.0; PKG: pkgs/test; `dart test --preset travis --total-shards 5 --shard-index 1`"
+    runs-on: macos-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_07"
+          restore-keys: |
+            os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test
+            os:macos-latest;pub-cache-hosted;sdk:3.5.0
+            os:macos-latest;pub-cache-hosted
+            os:macos-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; dart test --preset travis --total-shards 5 --shard-index 1"
+        run: dart test --preset travis --total-shards 5 --shard-index 1
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_037:
+    name: "unit_test; osx; Dart 3.5.0; PKG: pkgs/test; `dart test --preset travis --total-shards 5 --shard-index 2`"
+    runs-on: macos-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_08"
+          restore-keys: |
+            os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test
+            os:macos-latest;pub-cache-hosted;sdk:3.5.0
+            os:macos-latest;pub-cache-hosted
+            os:macos-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; dart test --preset travis --total-shards 5 --shard-index 2"
+        run: dart test --preset travis --total-shards 5 --shard-index 2
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_038:
+    name: "unit_test; osx; Dart 3.5.0; PKG: pkgs/test; `dart test --preset travis --total-shards 5 --shard-index 3`"
+    runs-on: macos-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_09"
+          restore-keys: |
+            os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test
+            os:macos-latest;pub-cache-hosted;sdk:3.5.0
+            os:macos-latest;pub-cache-hosted
+            os:macos-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; dart test --preset travis --total-shards 5 --shard-index 3"
+        run: dart test --preset travis --total-shards 5 --shard-index 3
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_039:
+    name: "unit_test; osx; Dart 3.5.0; PKG: pkgs/test; `dart test --preset travis --total-shards 5 --shard-index 4`"
+    runs-on: macos-latest
+    steps:
+      - name: Cache Pub hosted dependencies
+        uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a
+        with:
+          path: "~/.pub-cache/hosted"
+          key: "os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_10"
+          restore-keys: |
+            os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test
+            os:macos-latest;pub-cache-hosted;sdk:3.5.0
+            os:macos-latest;pub-cache-hosted
+            os:macos-latest
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; dart test --preset travis --total-shards 5 --shard-index 4"
+        run: dart test --preset travis --total-shards 5 --shard-index 4
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_040:
+    name: "unit_test; windows; Dart 3.5.0; PKG: integration_tests/spawn_hybrid; `dart test -p chrome,vm,node`"
+    runs-on: windows-latest
+    steps:
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: integration_tests_spawn_hybrid_pub_upgrade
+        name: integration_tests/spawn_hybrid; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: integration_tests/spawn_hybrid
+      - name: "integration_tests/spawn_hybrid; dart test -p chrome,vm,node"
+        run: "dart test -p chrome,vm,node"
+        if: "always() && steps.integration_tests_spawn_hybrid_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/spawn_hybrid
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_041:
+    name: "unit_test; windows; Dart 3.5.0; PKG: integration_tests/wasm; `dart test --timeout=60s`"
+    runs-on: windows-latest
+    steps:
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: integration_tests_wasm_pub_upgrade
+        name: integration_tests/wasm; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+      - name: "integration_tests/wasm; dart test --timeout=60s"
+        run: "dart test --timeout=60s"
+        if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_042:
+    name: "unit_test; windows; Dart 3.5.0; PKG: pkgs/test; `dart test --preset travis --total-shards 5 --shard-index 0`"
+    runs-on: windows-latest
+    steps:
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; dart test --preset travis --total-shards 5 --shard-index 0"
+        run: dart test --preset travis --total-shards 5 --shard-index 0
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_043:
+    name: "unit_test; windows; Dart 3.5.0; PKG: pkgs/test; `dart test --preset travis --total-shards 5 --shard-index 1`"
+    runs-on: windows-latest
+    steps:
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; dart test --preset travis --total-shards 5 --shard-index 1"
+        run: dart test --preset travis --total-shards 5 --shard-index 1
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_044:
+    name: "unit_test; windows; Dart 3.5.0; PKG: pkgs/test; `dart test --preset travis --total-shards 5 --shard-index 2`"
+    runs-on: windows-latest
+    steps:
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; dart test --preset travis --total-shards 5 --shard-index 2"
+        run: dart test --preset travis --total-shards 5 --shard-index 2
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_045:
+    name: "unit_test; windows; Dart 3.5.0; PKG: pkgs/test; `dart test --preset travis --total-shards 5 --shard-index 3`"
+    runs-on: windows-latest
+    steps:
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; dart test --preset travis --total-shards 5 --shard-index 3"
+        run: dart test --preset travis --total-shards 5 --shard-index 3
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_046:
+    name: "unit_test; windows; Dart 3.5.0; PKG: pkgs/test; `dart test --preset travis --total-shards 5 --shard-index 4`"
+    runs-on: windows-latest
+    steps:
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: "3.5.0"
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: pkgs_test_pub_upgrade
+        name: pkgs/test; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: pkgs/test
+      - name: "pkgs/test; dart test --preset travis --total-shards 5 --shard-index 4"
+        run: dart test --preset travis --total-shards 5 --shard-index 4
+        if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'"
+        working-directory: pkgs/test
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_047:
+    name: "unit_test; windows; Dart dev; PKG: integration_tests/spawn_hybrid; `dart test -p chrome,vm,node`"
+    runs-on: windows-latest
+    steps:
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: dev
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: integration_tests_spawn_hybrid_pub_upgrade
+        name: integration_tests/spawn_hybrid; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: integration_tests/spawn_hybrid
+      - name: "integration_tests/spawn_hybrid; dart test -p chrome,vm,node"
+        run: "dart test -p chrome,vm,node"
+        if: "always() && steps.integration_tests_spawn_hybrid_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/spawn_hybrid
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_048:
+    name: "unit_test; windows; Dart dev; PKG: integration_tests/wasm; `dart test --timeout=60s`"
+    runs-on: windows-latest
+    steps:
+      - name: Setup Dart SDK
+        uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: dev
+      - id: checkout
+        name: Checkout repository
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - id: integration_tests_wasm_pub_upgrade
+        name: integration_tests/wasm; dart pub upgrade
+        run: dart pub upgrade
+        if: "always() && steps.checkout.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+      - name: "integration_tests/wasm; dart test --timeout=60s"
+        run: "dart test --timeout=60s"
+        if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'"
+        working-directory: integration_tests/wasm
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+  job_049:
+    name: Notify failure
+    runs-on: ubuntu-latest
+    if: "(github.event_name == 'push' || github.event_name == 'schedule') && failure()"
+    steps:
+      - run: |
+          curl -H "Content-Type: application/json" -X POST -d \
+            "{'text':'Build failed! ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}'}" \
+            "${CHAT_WEBHOOK_URL}"
+        env:
+          CHAT_WEBHOOK_URL: "${{ secrets.BUILD_AND_TEST_TEAM_CHAT_WEBHOOK_URL }}"
+    needs:
+      - job_001
+      - job_002
+      - job_003
+      - job_004
+      - job_005
+      - job_006
+      - job_007
+      - job_008
+      - job_009
+      - job_010
+      - job_011
+      - job_012
+      - job_013
+      - job_014
+      - job_015
+      - job_016
+      - job_017
+      - job_018
+      - job_019
+      - job_020
+      - job_021
+      - job_022
+      - job_023
+      - job_024
+      - job_025
+      - job_026
+      - job_027
+      - job_028
+      - job_029
+      - job_030
+      - job_031
+      - job_032
+      - job_033
+      - job_034
+      - job_035
+      - job_036
+      - job_037
+      - job_038
+      - job_039
+      - job_040
+      - job_041
+      - job_042
+      - job_043
+      - job_044
+      - job_045
+      - job_046
+      - job_047
+      - job_048
diff --git a/.github/workflows/health.yaml b/.github/workflows/health.yaml
new file mode 100644
index 0000000..1656c16
--- /dev/null
+++ b/.github/workflows/health.yaml
@@ -0,0 +1,12 @@
+name: Health
+on:
+  pull_request:
+    branches: [ main, master ]
+    types: [opened, synchronize, reopened, labeled, unlabeled]
+jobs:
+  health:
+    uses: dart-lang/ecosystem/.github/workflows/health.yaml@main
+    with:
+      checks: "version,changelog,do-not-submit"
+    permissions:
+      pull-requests: write
diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml
new file mode 100644
index 0000000..1a6f2ec
--- /dev/null
+++ b/.github/workflows/no-response.yml
@@ -0,0 +1,35 @@
+# A workflow to close issues where the author hasn't responded to a request for
+# more information; see https://github.com/actions/stale.
+
+name: No Response
+
+# Run as a daily cron.
+on:
+  schedule:
+    # Every day at 8am
+    - cron: '0 8 * * *'
+
+# All permissions not specified are set to 'none'.
+permissions:
+  issues: write
+  pull-requests: write
+
+jobs:
+  no-response:
+    runs-on: ubuntu-latest
+    if: ${{ github.repository_owner == 'dart-lang' }}
+    steps:
+      - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e
+        with:
+          days-before-stale: -1
+          days-before-close: 14
+          stale-issue-label: "needs-info"
+          close-issue-message: >
+            Without additional information we're not able to resolve this issue.
+            Feel free to add more info or respond to any questions above and we
+            can reopen the case. Thanks for your contribution!
+          stale-pr-label: "needs-info"
+          close-pr-message: >
+            Without additional information we're not able to resolve this PR.
+            Feel free to add more info or respond to any questions above.
+            Thanks for your contribution!
diff --git a/.github/workflows/post_summaries.yaml b/.github/workflows/post_summaries.yaml
new file mode 100644
index 0000000..9be7d5d
--- /dev/null
+++ b/.github/workflows/post_summaries.yaml
@@ -0,0 +1,18 @@
+name: Comment on the pull request
+
+on:
+  # Trigger this workflow after the Health workflow completes. This workflow
+  # will have permissions to do things like create comments on the PR, even if
+  # the original workflow couldn't.
+  workflow_run:
+    workflows:
+      - Health
+      - Publish
+    types:
+      - completed
+
+jobs:
+  upload:
+    uses: dart-lang/ecosystem/.github/workflows/post_summaries.yaml@main
+    permissions:
+      pull-requests: write
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
new file mode 100644
index 0000000..7d6442c
--- /dev/null
+++ b/.github/workflows/publish.yaml
@@ -0,0 +1,17 @@
+# A CI configuration to auto-publish pub packages.
+
+name: Publish
+
+on:
+  pull_request:
+    branches: [ master ]
+    types: [opened, synchronize, reopened, labeled, unlabeled]
+  push:
+    tags: [ '[A-z]+-v[0-9]+.[0-9]+.[0-9]+*' ]
+
+jobs:
+  publish:
+    if: ${{ github.repository_owner == 'dart-lang' }}
+    uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main
+    with:
+      write-comments: false
diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml
new file mode 100644
index 0000000..3c5c10a
--- /dev/null
+++ b/.github/workflows/scorecards-analysis.yml
@@ -0,0 +1,55 @@
+name: Scorecards supply-chain security
+on:
+  # Only the default branch is supported.
+  branch_protection_rule:
+  push:
+    branches: [ master ]
+
+# Declare default permissions as read only.
+permissions: read-all
+
+jobs:
+  analysis:
+    name: Scorecards analysis
+    runs-on: ubuntu-latest
+    permissions:
+      # Needed to upload the results to code-scanning dashboard.
+      security-events: write
+      actions: read
+      contents: read
+      # Needed to access OIDC token.
+      id-token: write
+
+    steps:
+      - name: "Checkout code"
+        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+        with:
+          persist-credentials: false
+
+      - name: "Run analysis"
+        uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46
+        with:
+          results_file: results.sarif
+          results_format: sarif
+          # Read-only PAT token. To create it,
+          # follow the steps in https://github.com/ossf/scorecard-action#pat-token-creation.
+          repo_token: ${{ secrets.SCORECARD_READ_TOKEN }}
+          # Publish the results to enable scorecard badges. For more details, see
+          # https://github.com/ossf/scorecard-action#publishing-results.
+          # For private repositories, `publish_results` will automatically be set to `false`,
+          # regardless of the value entered here.
+          publish_results: true
+
+      # Upload the results as artifacts (optional).
+      - name: "Upload artifact"
+        uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882
+        with:
+          name: SARIF file
+          path: results.sarif
+          retention-days: 5
+
+      # Upload the results to GitHub's code scanning dashboard.
+      - name: "Upload to code-scanning"
+        uses: github/codeql-action/upload-sarif@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5
+        with:
+          sarif_file: results.sarif
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..56c3800
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+# Don’t commit the following directories created by pub.
+.buildlog
+.dart_tool/
+.pub/
+build/
+packages
+
+# Include when developing application packages.
+pubspec.lock
+.packages
+
+# Ignore common coverage directory
+coverage/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..90003c0
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,74 @@
+Want to contribute? Great! First, read this page (including the small print at
+the end).
+
+### Before you contribute
+
+Before we can use your code, you must sign the
+[Google Individual Contributor License Agreement][CLA] (CLA), which you can do
+online. The CLA is necessary mainly because you own the copyright to your
+changes, even after your contribution becomes part of our codebase, so we need
+your permission to use and distribute your code. We also need to be sure of
+various other things—for instance that you'll tell us if you know that your code
+infringes on other people's patents. You don't have to sign the CLA until after
+you've submitted your code for review and a member has approved it, but you must
+do it before we can put your code into our codebase.
+
+Before you start working on a larger contribution, you should get in touch with
+us first through the issue tracker with your idea so that we can help out and
+possibly guide you. Coordinating up front makes it much easier to avoid
+frustration later on.
+
+[CLA]: https://cla.developers.google.com/about/google-individual
+
+### Code reviews
+
+All submissions, including submissions by project members, require review. We
+recommend [forking the repository][fork], making changes in your fork, and
+[sending us a pull request][pr] so we can review the changes and merge them into
+this repository.
+
+[fork]: https://help.github.com/articles/about-forks/
+[pr]: https://help.github.com/articles/creating-a-pull-request/
+
+Functional changes will require tests to be added or changed. The tests live in
+the `test/` directory for each package, and are run with `dart test`. If you
+need to create new tests, use the existing tests as a guideline for what they
+should look like.
+
+You can run all additional presubmit checks locally if you wish, by using the
+`mono_repo` tool. From the root of the repository, run
+`pub global run mono_repo presubmit`.
+
+### Versioning
+
+You will also need to potentially update the pubspec.yaml and/or CHANGELOG.md of
+any package you are updating. If the current version is not a `-dev` version
+then you should update it and add a new header to the changelog. If you have no
+publicly facing change to list, it is OK for there to be no changes listed.
+
+We follow pretty strict semantic versioning, feel free to ask on the PR if you
+are unsure about what version number you should choose (or do your best, and a
+code reviewers will bring it up if it is incorrect).
+
+### File headers
+
+All files in the project must start with the following header.
+
+    // Copyright (c) 2023, 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.
+
+### Publishing
+
+Publishing is done by package owners creating a tag of the form
+`<package>-v<version>`, either manually or through the github release ui
+(preferred). The pubspec and changelog must already be updated to the desired
+release version in the master branch for this process to work.
+
+### The small print
+
+Contributions made by corporations are covered by a different agreement than the
+one above, the
+[Software Grant and Corporate Contributor License Agreement][CCLA].
+
+[CCLA]: https://developers.google.com/open-source/cla/corporate
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..25b50aa
--- /dev/null
+++ b/README.md
@@ -0,0 +1,24 @@
+[![Dart CI](https://github.com/dart-lang/test/actions/workflows/dart.yml/badge.svg)](https://github.com/dart-lang/test/actions/workflows/dart.yml)
+[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/dart-lang/test/badge)](https://deps.dev/project/github/dart-lang%2Ftest)
+
+## What's here?
+
+Welcome! [package:test](pkgs/test/) is the standard testing library for Dart and
+Flutter. If you have questions about Dart testing, please see the docs for
+[package:test](pkgs/test/). `package:test_api` and `package:test_core`
+are implementation details and generally not user-facing.
+
+[package:checks](pkgs/checks/) is a relatively new library for expressing test
+expectations. It's a more modern version of `package:matcher` and features a
+literate API.
+
+## Packages
+
+| Package | Description | Version |
+|---|---|---|
+| [checks](pkgs/checks/) | A framework for checking values against expectations and building custom expectations. | [![pub package](https://img.shields.io/pub/v/checks.svg)](https://pub.dev/packages/checks) |
+| [fake_async](pkgs/fake_async/) | Fake asynchronous events such as timers and microtasks for deterministic testing. | [![pub package](https://img.shields.io/pub/v/fake_async.svg)](https://pub.dev/packages/fake_async) |
+| [matcher](pkgs/matcher/) | Support for specifying test expectations via an extensible Matcher class. | [![pub package](https://img.shields.io/pub/v/matcher.svg)](https://pub.dev/packages/matcher) |
+| [test](pkgs/test/) | A full featured library for writing and running Dart tests across platforms. | [![pub package](https://img.shields.io/pub/v/test.svg)](https://pub.dev/packages/test) |
+| [test_api](pkgs/test_api/) |  | [![pub package](https://img.shields.io/pub/v/test_api.svg)](https://pub.dev/packages/test_api) |
+| [test_core](pkgs/test_core/) |  | [![pub package](https://img.shields.io/pub/v/test_core.svg)](https://pub.dev/packages/test_core) |
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 0000000..f6b2dc1
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1,22 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+
+analyzer:
+  language:
+    strict-casts: true
+  errors:
+    # There are a number of deprecated members used through this package
+    deprecated_member_use_from_same_package: ignore
+
+    # Ignoring a number of lints from dart_flutter_team_lints – for now
+    avoid_catching_errors: ignore
+    avoid_dynamic_calls: ignore
+    comment_references: ignore
+    lines_longer_than_80_chars: ignore
+    only_throw_errors: ignore
+    unawaited_futures: ignore
+    unsafe_html: ignore
+
+linter:
+  rules:
+    - avoid_private_typedef_functions
diff --git a/integration_tests/regression/dart_test.yaml b/integration_tests/regression/dart_test.yaml
new file mode 100644
index 0000000..07fd2f5
--- /dev/null
+++ b/integration_tests/regression/dart_test.yaml
@@ -0,0 +1,13 @@
+paths:
+- lib/issue_2142/test.dart
+
+platforms:
+- chrome
+- vm
+
+compilers:
+- dart2js
+- dart2wasm
+- exe
+- kernel
+- source
diff --git a/integration_tests/regression/lib/issue_2142/import.dart b/integration_tests/regression/lib/issue_2142/import.dart
new file mode 100644
index 0000000..987fbb1
--- /dev/null
+++ b/integration_tests/regression/lib/issue_2142/import.dart
@@ -0,0 +1,8 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ignore: prefer_relative_imports
+import 'package:regression_tests/issue_2142/test.dart';
+
+Thing newThing() => Thing();
diff --git a/integration_tests/regression/lib/issue_2142/test.dart b/integration_tests/regression/lib/issue_2142/test.dart
new file mode 100644
index 0000000..06afc5a
--- /dev/null
+++ b/integration_tests/regression/lib/issue_2142/test.dart
@@ -0,0 +1,14 @@
+// Copyright (c) 2023, 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 'import.dart';
+
+void main() {
+  test('aThing is a Thing', () {
+    expect(newThing(), isA<Thing>());
+  });
+}
+
+class Thing {}
diff --git a/integration_tests/regression/mono_pkg.yaml b/integration_tests/regression/mono_pkg.yaml
new file mode 100644
index 0000000..4a60bc1
--- /dev/null
+++ b/integration_tests/regression/mono_pkg.yaml
@@ -0,0 +1,16 @@
+# See https://pub.dev/packages/mono_repo
+
+sdk:
+- dev
+- pubspec
+
+os:
+- linux
+
+stages:
+- analyze_and_format:
+  - group:
+    - format
+    - analyze: --fatal-infos
+- unit_test:
+  - test
diff --git a/integration_tests/regression/pubspec.yaml b/integration_tests/regression/pubspec.yaml
new file mode 100644
index 0000000..6987181
--- /dev/null
+++ b/integration_tests/regression/pubspec.yaml
@@ -0,0 +1,7 @@
+name: regression_tests
+publish_to: none
+environment:
+  sdk: ^3.5.0
+resolution: workspace
+dependencies:
+  test: any
diff --git a/integration_tests/spawn_hybrid/lib/emits_numbers.dart b/integration_tests/spawn_hybrid/lib/emits_numbers.dart
new file mode 100644
index 0000000..6429a85
--- /dev/null
+++ b/integration_tests/spawn_hybrid/lib/emits_numbers.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2022, 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:stream_channel/stream_channel.dart';
+
+void hybridMain(StreamChannel channel) {
+  channel.sink
+    ..add(1)
+    ..add(2)
+    ..add(3)
+    ..close();
+}
diff --git a/integration_tests/spawn_hybrid/mono_pkg.yaml b/integration_tests/spawn_hybrid/mono_pkg.yaml
new file mode 100644
index 0000000..7d75b61
--- /dev/null
+++ b/integration_tests/spawn_hybrid/mono_pkg.yaml
@@ -0,0 +1,18 @@
+# See https://pub.dev/packages/mono_repo
+
+sdk:
+- dev
+- pubspec
+
+stages:
+- analyze_and_format:
+  - group:
+    - format
+    - analyze: --fatal-infos
+    sdk:
+    - dev
+- unit_test:
+  - test: -p chrome,vm,node
+    os:
+    - linux
+    - windows
diff --git a/integration_tests/spawn_hybrid/other_package/lib/emits_numbers.dart b/integration_tests/spawn_hybrid/other_package/lib/emits_numbers.dart
new file mode 100644
index 0000000..6429a85
--- /dev/null
+++ b/integration_tests/spawn_hybrid/other_package/lib/emits_numbers.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2022, 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:stream_channel/stream_channel.dart';
+
+void hybridMain(StreamChannel channel) {
+  channel.sink
+    ..add(1)
+    ..add(2)
+    ..add(3)
+    ..close();
+}
diff --git a/integration_tests/spawn_hybrid/other_package/pubspec.yaml b/integration_tests/spawn_hybrid/other_package/pubspec.yaml
new file mode 100644
index 0000000..b7d026f
--- /dev/null
+++ b/integration_tests/spawn_hybrid/other_package/pubspec.yaml
@@ -0,0 +1,6 @@
+name: other_package
+publish_to: none
+environment:
+  sdk: '>=2.12.0 <3.0.0'
+dependencies:
+  stream_channel: ^2.1.0
diff --git a/integration_tests/spawn_hybrid/pubspec.yaml b/integration_tests/spawn_hybrid/pubspec.yaml
new file mode 100644
index 0000000..de9e5e9
--- /dev/null
+++ b/integration_tests/spawn_hybrid/pubspec.yaml
@@ -0,0 +1,13 @@
+name: spawn_hybrid
+publish_to: none
+environment:
+  sdk: ^3.5.0
+resolution: workspace
+dependencies:
+  async: ^2.9.0
+  path: ^1.8.2
+  stream_channel: ^2.1.0
+dev_dependencies:
+  other_package:
+    path: other_package/
+  test: any
diff --git a/integration_tests/spawn_hybrid/test/hybrid_test.dart b/integration_tests/spawn_hybrid/test/hybrid_test.dart
new file mode 100644
index 0000000..1493e23
--- /dev/null
+++ b/integration_tests/spawn_hybrid/test/hybrid_test.dart
@@ -0,0 +1,357 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+import 'package:path/path.dart' as p;
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('spawnHybridUri():', () {
+    test('loads a file in a separate isolate connected via StreamChannel',
+        () async {
+      expect(spawnHybridUri('util/emits_numbers.dart').stream.toList(),
+          completion(equals([1, 2, 3])));
+    });
+
+    test('resolves root-relative URIs relative to the package root', () async {
+      expect(spawnHybridUri('/test/util/emits_numbers.dart').stream.toList(),
+          completion(equals([1, 2, 3])));
+    });
+
+    test('supports Uri objects', () async {
+      expect(
+          spawnHybridUri(Uri.parse('util/emits_numbers.dart')).stream.toList(),
+          completion(equals([1, 2, 3])));
+    });
+
+    test('supports package: uris referencing the root package', () async {
+      expect(
+          spawnHybridUri(Uri.parse('package:spawn_hybrid/emits_numbers.dart'))
+              .stream
+              .toList(),
+          completion(equals([1, 2, 3])));
+    });
+
+    test('supports package: uris referencing dependency packages', () async {
+      expect(
+          spawnHybridUri(Uri.parse('package:other_package/emits_numbers.dart'))
+              .stream
+              .toList(),
+          completion(equals([1, 2, 3])));
+    });
+
+    test('rejects non-String, non-Uri objects', () {
+      expect(() => spawnHybridUri(123), throwsArgumentError);
+    });
+
+    test('passes a message to the hybrid isolate', () async {
+      expect(
+          spawnHybridUri('util/echos_message.dart', message: 123).stream.first,
+          completion(equals(123)));
+      expect(
+          spawnHybridUri('util/echos_message.dart', message: 'wow')
+              .stream
+              .first,
+          completion(equals('wow')));
+    });
+
+    test('emits an error from the stream channel if the isolate fails to load',
+        () {
+      expect(spawnHybridUri('non existent file').stream.first,
+          throwsA(isA<Exception>()));
+    });
+  });
+
+  group('spawnHybridCode()', () {
+    test('loads the code in a separate isolate connected via StreamChannel',
+        () {
+      expect(spawnHybridCode('''
+        import "package:stream_channel/stream_channel.dart";
+
+        void hybridMain(StreamChannel channel) {
+          channel.sink..add(1)..add(2)..add(3)..close();
+        }
+      ''').stream.toList(), completion(equals([1, 2, 3])));
+    });
+
+    test('allows a first parameter with type StreamChannel<Object?>', () {
+      expect(spawnHybridCode('''
+        import "package:stream_channel/stream_channel.dart";
+
+        void hybridMain(StreamChannel<Object?> channel) {
+          channel.sink..add(1)..add(2)..add(null)..close();
+        }
+      ''').stream.toList(), completion(equals([1, 2, null])));
+    });
+
+    test('gives a good error when the StreamChannel type is not supported', () {
+      expect(
+          spawnHybridCode('''
+        import "package:stream_channel/stream_channel.dart";
+
+        void hybridMain(StreamChannel<Object> channel) {
+          channel.sink..add(1)..add(2)..add(3)..close();
+        }
+      ''').stream,
+          emitsError(isA<Exception>().having(
+              (e) => e.toString(),
+              'toString',
+              contains(
+                  'The first parameter to the top-level hybridMain() must be a '
+                  'StreamChannel<dynamic> or StreamChannel<Object?>. More specific '
+                  'types such as StreamChannel<Object> are not supported.'))));
+    });
+
+    test('can use dart:io even when run from a browser', () async {
+      var path = p.join('test', 'hybrid_test.dart');
+      expect(spawnHybridCode("""
+              import 'dart:io';
+
+              import 'package:stream_channel/stream_channel.dart';
+
+              void hybridMain(StreamChannel channel) {
+                channel.sink
+                  ..add(File(r"$path").readAsStringSync())
+                  ..close();
+              }
+            """).stream.first, completion(contains('hybrid emits numbers')));
+    }, testOn: 'browser');
+
+    test('forwards data from the test to the hybrid isolate', () async {
+      var channel = spawnHybridCode('''
+        import "package:stream_channel/stream_channel.dart";
+
+        void hybridMain(StreamChannel channel) {
+          channel.stream.listen((num) {
+            channel.sink.add(num + 1);
+          });
+        }
+      ''');
+      channel.sink
+        ..add(1)
+        ..add(2)
+        ..add(3);
+      expect(channel.stream.take(3).toList(), completion(equals([2, 3, 4])));
+    });
+
+    test('passes an initial message to the hybrid isolate', () {
+      var code = '''
+        import "package:stream_channel/stream_channel.dart";
+
+        void hybridMain(StreamChannel channel, Object message) {
+          channel.sink..add(message)..close();
+        }
+      ''';
+
+      expect(spawnHybridCode(code, message: [1, 2, 3]).stream.first,
+          completion(equals([1, 2, 3])));
+      expect(spawnHybridCode(code, message: {'a': 'b'}).stream.first,
+          completion(equals({'a': 'b'})));
+    });
+
+    test('allows the hybrid isolate to send errors across the stream channel',
+        () {
+      var channel = spawnHybridCode('''
+        import "package:stack_trace/stack_trace.dart";
+        import "package:stream_channel/stream_channel.dart";
+
+        void hybridMain(StreamChannel channel) {
+          channel.sink.addError("oh no!", Trace.current());
+        }
+      ''');
+
+      channel.stream.listen(null, onError: expectAsync2((error, stackTrace) {
+        expect(error.toString(), equals('oh no!'));
+        expect(stackTrace.toString(), contains('hybridMain'));
+      }));
+    });
+
+    test('sends an unhandled synchronous error across the stream channel', () {
+      var channel = spawnHybridCode('''
+        import "package:stream_channel/stream_channel.dart";
+
+        void hybridMain(StreamChannel channel) {
+          throw "oh no!";
+        }
+      ''');
+
+      channel.stream.listen(null, onError: expectAsync2((error, stackTrace) {
+        expect(error.toString(), equals('oh no!'));
+        expect(stackTrace.toString(), contains('hybridMain'));
+      }));
+    });
+
+    test('sends an unhandled asynchronous error across the stream channel', () {
+      var channel = spawnHybridCode('''
+        import 'dart:async';
+
+        import "package:stream_channel/stream_channel.dart";
+
+        void hybridMain(StreamChannel channel) {
+          scheduleMicrotask(() {
+            throw "oh no!";
+          });
+        }
+      ''');
+
+      channel.stream.listen(null, onError: expectAsync2((error, stackTrace) {
+        expect(error.toString(), equals('oh no!'));
+        expect(stackTrace.toString(), contains('hybridMain'));
+      }));
+    });
+
+    test('deserializes TestFailures as TestFailures', () {
+      var channel = spawnHybridCode('''
+        import "package:stream_channel/stream_channel.dart";
+
+        import "package:test/test.dart";
+
+        void hybridMain(StreamChannel channel) {
+          throw TestFailure("oh no!");
+        }
+      ''');
+
+      expect(channel.stream.first, throwsA(isA<TestFailure>()));
+    });
+
+    test('gracefully handles an unserializable message in the VM', () {
+      var channel = spawnHybridCode('''
+        import "package:stream_channel/stream_channel.dart";
+
+        void hybridMain(StreamChannel channel) {}
+      ''');
+
+      expect(() => channel.sink.add(<Object>[].iterator), throwsArgumentError);
+    });
+
+    test('gracefully handles an unserializable message in the browser',
+        () async {
+      var channel = spawnHybridCode('''
+        import 'package:stream_channel/stream_channel.dart';
+
+        void hybridMain(StreamChannel channel) {}
+      ''');
+
+      expect(() => channel.sink.add(<Object>[].iterator), throwsArgumentError);
+    }, testOn: 'browser');
+
+    test('gracefully handles an unserializable message in the hybrid isolate',
+        () {
+      var channel = spawnHybridCode('''
+        import "package:stream_channel/stream_channel.dart";
+
+        void hybridMain(StreamChannel channel) {
+          channel.sink.add([].iterator);
+        }
+      ''');
+
+      channel.stream.listen(null, onError: expectAsync1((error) {
+        expect(error.toString(), contains("can't be JSON-encoded."));
+      }));
+    });
+
+    test('forwards prints from the hybrid isolate', () {
+      expect(() async {
+        var channel = spawnHybridCode('''
+          import "package:stream_channel/stream_channel.dart";
+
+          void hybridMain(StreamChannel channel) {
+            print("hi!");
+            channel.sink.add(null);
+          }
+        ''');
+        await channel.stream.first;
+      }, prints('hi!\n'));
+    });
+
+    // This takes special handling, since the code is packed into a data: URI
+    // that's imported, URIs don't escape $ by default, and $ isn't allowed in
+    // imports.
+    test('supports a dollar character in the hybrid code', () {
+      expect(spawnHybridCode(r'''
+        import "package:stream_channel/stream_channel.dart";
+
+        void hybridMain(StreamChannel channel) {
+          var value = "bar";
+          channel.sink.add("foo${value}baz");
+        }
+      ''').stream.first, completion('foobarbaz'));
+    });
+
+    test('closes the channel when the hybrid isolate exits', () {
+      var channel = spawnHybridCode('''
+        import "dart:isolate";
+
+        hybridMain(_) {
+          Isolate.current.kill();
+        }
+      ''');
+
+      expect(channel.stream.toList(), completion(isEmpty));
+    });
+
+    group('closes the channel when the test finishes by default', () {
+      late StreamChannel channel;
+
+      test('test 1', () {
+        channel = spawnHybridCode('''
+              import 'package:stream_channel/stream_channel.dart';
+
+              void hybridMain(StreamChannel channel) {}
+            ''');
+      });
+
+      test('test 2', () async {
+        var isDone = false;
+        channel.stream.listen(null, onDone: () => isDone = true);
+        await pumpEventQueue();
+        expect(isDone, isTrue);
+      });
+    });
+
+    group('persists across multiple tests with stayAlive: true', () {
+      late StreamQueue queue;
+      late StreamSink sink;
+      setUpAll(() {
+        var channel = spawnHybridCode('''
+              import "package:stream_channel/stream_channel.dart";
+
+              void hybridMain(StreamChannel channel) {
+                channel.stream.listen((message) {
+                  channel.sink.add(message);
+                });
+              }
+            ''', stayAlive: true);
+        queue = StreamQueue(channel.stream);
+        sink = channel.sink;
+      });
+
+      test('echoes a number', () {
+        expect(queue.next, completion(equals(123)));
+        sink.add(123);
+      });
+
+      test('echoes a string', () {
+        expect(queue.next, completion(equals('wow')));
+        sink.add('wow');
+      });
+    });
+
+    test('opts in to null safety by default', () async {
+      expect(spawnHybridCode('''
+        import "package:stream_channel/stream_channel.dart";
+
+        // Use some null safety syntax
+        int? x;
+
+        void hybridMain(StreamChannel channel) {
+          channel.sink..add(1)..add(2)..add(3)..close();
+        }
+      ''').stream.toList(), completion(equals([1, 2, 3])));
+    });
+  });
+}
diff --git a/integration_tests/spawn_hybrid/test/hybrid_test_io.dart b/integration_tests/spawn_hybrid/test/hybrid_test_io.dart
new file mode 100644
index 0000000..0e454db
--- /dev/null
+++ b/integration_tests/spawn_hybrid/test/hybrid_test_io.dart
@@ -0,0 +1,47 @@
+// Copyright (c) 2022, 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+void main() {
+  test('kills the isolate when the test closes the channel', () async {
+    var channel = spawnHybridCode('''
+        import "dart:async";
+        import "dart:io";
+
+        import "package:shelf/shelf.dart" as shelf;
+        import "package:shelf/shelf_io.dart" as io;
+        import "package:stream_channel/stream_channel.dart";
+
+        hybridMain(StreamChannel channel) async {
+          var server = await ServerSocket.bind("localhost", 0);
+          server.listen(null);
+          channel.sink.add(server.port);
+        }
+      ''');
+
+    // Expect that the socket disconnects at some point (presumably when the
+    // isolate closes).
+    var port = await channel.stream.first as int;
+    var socket = await Socket.connect('localhost', port);
+    expect(socket.listen(null).asFuture<void>(), completes);
+
+    await channel.sink.close();
+  });
+
+  test('spawnHybridUri(): supports absolute file: URIs', () async {
+    expect(
+        spawnHybridUri(p.toUri(p.absolute(
+                p.relative(p.join('test', 'util', 'emits_numbers.dart')))))
+            .stream
+            .toList(),
+        completion(equals([1, 2, 3])));
+  });
+}
diff --git a/integration_tests/spawn_hybrid/test/subdir/hybrid_test.dart b/integration_tests/spawn_hybrid/test/subdir/hybrid_test.dart
new file mode 100644
index 0000000..088375c
--- /dev/null
+++ b/integration_tests/spawn_hybrid/test/subdir/hybrid_test.dart
@@ -0,0 +1,20 @@
+// Copyright (c) 2022, 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.
+
+@TestOn('vm')
+library;
+
+import 'package:test/test.dart';
+
+void main() {
+  group('spawnHybridUri():', () {
+    test('loads uris relative to the test file', () async {
+      expect(
+          spawnHybridUri(Uri.parse('../util/emits_numbers.dart'))
+              .stream
+              .toList(),
+          completion(equals([1, 2, 3])));
+    });
+  });
+}
diff --git a/integration_tests/spawn_hybrid/test/util/echos_message.dart b/integration_tests/spawn_hybrid/test/util/echos_message.dart
new file mode 100644
index 0000000..271b44f
--- /dev/null
+++ b/integration_tests/spawn_hybrid/test/util/echos_message.dart
@@ -0,0 +1,11 @@
+// Copyright (c) 2022, 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:stream_channel/stream_channel.dart';
+
+void hybridMain(StreamChannel channel, Object message) {
+  channel.sink
+    ..add(message)
+    ..close();
+}
diff --git a/integration_tests/spawn_hybrid/test/util/emits_numbers.dart b/integration_tests/spawn_hybrid/test/util/emits_numbers.dart
new file mode 100644
index 0000000..6429a85
--- /dev/null
+++ b/integration_tests/spawn_hybrid/test/util/emits_numbers.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2022, 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:stream_channel/stream_channel.dart';
+
+void hybridMain(StreamChannel channel) {
+  channel.sink
+    ..add(1)
+    ..add(2)
+    ..add(3)
+    ..close();
+}
diff --git a/integration_tests/wasm/dart_test.yaml b/integration_tests/wasm/dart_test.yaml
new file mode 100644
index 0000000..14308c4
--- /dev/null
+++ b/integration_tests/wasm/dart_test.yaml
@@ -0,0 +1,5 @@
+platforms: [chrome, firefox]
+# Node doesn't work because the version available in the current Ubuntu GitHub runners is too
+# old to support WASM+GC, which would be required to run Dart tests.
+#platforms: [chrome, firefox, node]
+compilers: [dart2wasm]
diff --git a/integration_tests/wasm/mono_pkg.yaml b/integration_tests/wasm/mono_pkg.yaml
new file mode 100644
index 0000000..a8f3410
--- /dev/null
+++ b/integration_tests/wasm/mono_pkg.yaml
@@ -0,0 +1,18 @@
+# See https://pub.dev/packages/mono_repo
+
+sdk:
+- pubspec
+- dev
+
+os:
+- linux
+- windows
+
+stages:
+- analyze_and_format:
+  - group:
+    - format
+    - analyze: --fatal-infos
+- unit_test:
+  # The config here is a regression test for https://github.com/dart-lang/test/issues/2006
+  - test: --timeout=60s
diff --git a/integration_tests/wasm/pubspec.yaml b/integration_tests/wasm/pubspec.yaml
new file mode 100644
index 0000000..370d844
--- /dev/null
+++ b/integration_tests/wasm/pubspec.yaml
@@ -0,0 +1,7 @@
+name: wasm_tests
+publish_to: none
+environment:
+  sdk: ^3.5.0
+resolution: workspace
+dev_dependencies:
+  test: any
diff --git a/integration_tests/wasm/test/hello_world_test.dart b/integration_tests/wasm/test/hello_world_test.dart
new file mode 100644
index 0000000..9ddd511
--- /dev/null
+++ b/integration_tests/wasm/test/hello_world_test.dart
@@ -0,0 +1,24 @@
+// Copyright (c) 2022, 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.
+
+@TestOn('wasm')
+// This retry is a regression test for https://github.com/dart-lang/test/issues/2006
+@Retry(2)
+library;
+
+import 'package:test/test.dart';
+
+void main() {
+  test('1 == 1', () {
+    expect(1, equals(1));
+  });
+
+  test('asserts are enabled', () {
+    expect(shouldFail, throwsA(isA<AssertionError>()));
+  });
+}
+
+void shouldFail() {
+  assert(1 == 2);
+}
diff --git a/mono_repo.yaml b/mono_repo.yaml
new file mode 100644
index 0000000..1131355
--- /dev/null
+++ b/mono_repo.yaml
@@ -0,0 +1,21 @@
+# See with https://github.com/dart-lang/mono_repo for details on this file
+self_validate: analyze_and_format
+
+github:
+  # Setting just `cron` keeps the defaults for `push` and `pull_request`
+  cron: '0 0 * * 0' # “At 00:00 (UTC) on Sunday.”
+  on_completion:
+  - name: "Notify failure"
+    runs-on: ubuntu-latest
+    # Run only if other jobs have failed and this is a push or scheduled build.
+    if: (github.event_name == 'push' || github.event_name == 'schedule') && failure()
+    steps:
+    - run: >
+        curl -H "Content-Type: application/json" -X POST -d \
+          "{'text':'Build failed! ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}'}" \
+          "${CHAT_WEBHOOK_URL}"
+      env:
+        CHAT_WEBHOOK_URL: ${{ secrets.BUILD_AND_TEST_TEAM_CHAT_WEBHOOK_URL }}
+
+merge_stages:
+- analyze_and_format
diff --git a/pkgs/checks/CHANGELOG.md b/pkgs/checks/CHANGELOG.md
new file mode 100644
index 0000000..6588352
--- /dev/null
+++ b/pkgs/checks/CHANGELOG.md
@@ -0,0 +1,54 @@
+## 0.3.1-wip
+
+-   Directly compare keys across actual and expected `Map` instances when
+    checking deep collection equality and all the keys can be directly compared
+    for equality. This maintains the path into a nested collection for typical
+    cases of checking for equality against a purely value collection.
+-   Always wrap Condition descriptions in angle brackets.
+-   Add `containsMatchingInOrder` and `containsEqualInOrder` to replace the
+    combined functionality in `containsInOrder`.
+-   Replace `pairwiseComparesTo` with `pairwiseMatches`.
+-   Increase SDK constraint to ^3.5.0.
+
+## 0.3.0
+
+-   **Breaking Changes**
+    -   Remove the `Condition` class and the `it()` utility. Replace calls to
+        `(it()..someExpectation())` with `((it) => it.someExpectation())`.
+-   Add class modifiers to restrict extension of implementation classes.
+
+## 0.2.2
+
+-   Return the first failure from `softCheck` and `softCheckAsync` as
+    documented, instead of the last failure when there are multiple failures.
+-   Add example `because` usage and mention the "reason" name in the migration
+    guide.
+-   Add `ComparableChecks` with comparison expectations for subject types that
+    implement `Comparable`.
+
+## 0.2.1
+
+-   Add a link to file issues with feedback in the README.
+
+## 0.2.0
+
+-   **Breaking Changes**
+    -   `checkThat` renamed to `check`.
+    -   `nest` and `nestAsync` take `Iterable<String> Function()` arguments for
+        `label` instead of `String`.
+    -   Async expectation extensions `completes`, `throws`, `emits`, and
+        `emitsError` no longer return a `Future<Subject>`. Instead they take an
+        optional `Condition` argument which can check expectations that would
+        have been checked on the returned subject.
+    -   `nestAsync` no longer returns a `Subject`, callers must pass the
+        followup `Condition` to the nullable argument.
+    -   Remove the `which` extension on `Future<Subject>`.
+    -   `matches` renamed to `matchesPattern` and now accepts a `Pattern`
+        argument, instead of limiting to `RegExp`.
+-   Added an example.
+-   Include a stack trace in the failure description for unexpected errors from
+    Futures or Streams.
+
+## 0.1.0
+
+-   Initial release.
diff --git a/pkgs/checks/LICENSE b/pkgs/checks/LICENSE
new file mode 100644
index 0000000..000cd7b
--- /dev/null
+++ b/pkgs/checks/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014, the Dart project authors. 
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google LLC nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/checks/README.md b/pkgs/checks/README.md
new file mode 100644
index 0000000..5415f11
--- /dev/null
+++ b/pkgs/checks/README.md
@@ -0,0 +1,220 @@
+[![pub package](https://img.shields.io/pub/v/checks.svg)](https://pub.dev/packages/checks)
+[![package publisher](https://img.shields.io/pub/publisher/checks.svg)](https://pub.dev/packages/checks/publisher)
+
+`package:checks` is a library for expressing test expectations and it features
+a literate API.
+
+## package:checks preview
+
+`package:checks` is in preview; to provide feedback on the API, please file
+[an issue][] with questions, suggestions, feature requests, or general
+feedback.
+
+For documentation about migrating from `package:matcher` to `checks`, see the
+[migration guide][].
+
+[an issue]:https://github.com/dart-lang/test/issues/new?labels=package%3Achecks&template=03_checks_feedback.md
+[migration guide]:https://github.com/dart-lang/test/blob/master/pkgs/checks/doc/migrating_from_matcher.md
+
+## Quickstart
+
+1. Add a `dev_dependency` on `checks: ^0.2.0`.
+
+1. Add an import for `package:checks/checks.dart`.
+
+1. Use `checks` in your test code:
+
+```dart
+void main() {
+  test('sample test', () {
+    // test code here
+    ...
+
+    check(actual).equals(expected);
+    check(someList).isNotEmpty();
+    check(someObject).isA<Map>();
+    check(someString)..startsWith('a')..endsWith('z')..contains('lmno');
+  });
+}
+```
+
+## Checking expectations with `checks`
+
+Expectations start with `check`. This utility returns a `Subject`, and
+expectations can be checked against the subject. Expectations are defined as
+extension methods, and different expectations will be available for subjects
+with different value types.
+
+```dart
+check(someValue).equals(expectedValue);
+check(someList).deepEquals(expectedList);
+check(someString).contains('expected pattern');
+```
+
+If a failure may not have enough context about the actual or expected values
+from the expectation calls alone, add a "Reason" in the failure message by
+passing a `because:` argument to `check()`.
+
+```dart
+check(
+  because: 'log lines must start with the severity',
+  logLines,
+).every((l) => l
+  ..anyOf([
+    (l) => l.startsWith('ERROR'),
+    (l) => l.startsWith('WARNING'),
+    (l) => l.startsWith('INFO'),
+  ]));
+```
+
+
+### Composing expectations
+
+
+Multiple expectations can be checked against the same value using cascade
+syntax. When multiple expectations are checked against a single value, a failure
+will included descriptions of the expectations that already passed.
+
+```dart
+check(someString)
+  ..startsWith('a')
+  ..endsWith('z')
+  ..contains('lmno');
+```
+
+Some nested checks may be not be possible to write with cascade syntax.
+There is a `which` utility for this use case which takes a `Condition`.
+
+```dart
+check(someString)
+  ..startsWith('a')
+  // A cascade would not be possible on `length`
+  ..length.which((l) => l
+    ..isGreatherThan(10)
+    ..isLessThan(100));
+```
+
+
+Some expectations return a `Subject` for another value derived from the original
+value, such as the `length` extension.
+
+```dart
+check(someString).length.equals(expectedLength);
+```
+
+Fields or derived values can be extracted from objects for checking further
+properties with the `has` utility.
+
+```dart
+check(someValue)
+  .has((value) => value.property, 'property')
+  .equals(expectedPropertyValue);
+```
+
+### Passing a set of expectations as an argument
+
+Some expectations take arguments which are themselves expectations to apply to
+other values. These expectations take `Condition` arguments which have the
+signature void Function(Subject)`. The conditions check expectations when they
+are called with a `Subject` argument.
+
+```dart
+check(someList).any((e) => e.isGreaterThan(0));
+```
+
+### Checking asynchronous expectations
+
+Expectation extension methods checking asynchronous behavior return a `Future`.
+The future should typically be awaited within the test body, however
+asynchronous expectations will also ensure that the test is not considered
+complete before the expectation is complete.
+Expectations with no concrete end conditions, such as an expectation that a
+future never completes, cannot be awaited and may cause a failure after the test
+has already appeared to complete.
+
+Asynchronous expectations do not return a `Subject`. When an expectation
+extracts a derived value further expectations can be checked by passing a
+`Condition`.
+
+```dart
+await check(someFuture).completes((r) => r.isGreaterThan(0));
+```
+
+Subjects for `Stream` instances must first be wrapped into a `StreamQueue` to
+allow multiple expectations to test against the stream from the same state.
+The `withQueue` extension can be used when a given stream instance only needs to
+be checked once, or if it is a broadcast stream, but if single subscription
+stream needs to have multiple expectations checked separately it should be
+wrapped with a `StreamQueue`.
+
+```dart
+await check(someStream).withQueue.inOrder([
+  (s) => s.emits((e) => e.equals(1)),
+  (s) => s.emits((e) => e.equals(2)),
+  (s) => s.emits((e) => e.equals(3)),
+  (s) => s.isDone(),
+]);
+
+var someQueue = StreamQueue(someOtherStream);
+await check(someQueue).emits((e) => e.equals(1));
+// do something
+await check(someQueue).emits((e) => e.equals(2));
+// do something
+```
+
+
+## Writing custom expectations
+
+Expectations are written as extensions on `Subject` with specific generics. The
+library `package:checks/context.dart` gives access to a `context` getter on
+`Subject` which offers capabilities for defining expectations on the subject's
+value.
+
+The `Context` allows checking an expectation with `expect`, `expectAsync` and
+`expectUnawaited`, or extracting a derived value for performing other checks
+with `nest` and `nestAsync`. Failures are reported by returning a `Rejection`,
+or an `Extracted.rejection`, extensions should avoid throwing exceptions.
+
+Descriptions of the clause checked by an expectations are passed through a
+separate callback from the predicate which checks the value. Nesting calls are
+made with a label directly. When there are no failures the clause callbacks are
+not called. When a condition callback is described, the clause callbacks are
+called, but the predicate callbacks are not called. Conditions can be checked
+against values without throwing an exception using `softCheck` or
+`softCheckAsync`.
+
+```dart
+extension CustomChecks on Subject<CustomType> {
+  void someExpectation() {
+    context.expect(() => ['meets this expectation'], (actual) {
+      if (_expectationIsMet(actual)) return null;
+      return Rejection(which: ['does not meet this expectation']);
+    });
+  }
+
+  Subject<Foo> get someDerivedValue =>
+      context.nest(() => ['has someDerivedValue'], (actual) {
+        if (_cannotReadDerivedValue(actual)) {
+          return Extracted.rejection(which: ['cannot read someDerivedValue']);
+        }
+        return Extracted.value(_readDerivedValue(actual));
+      });
+
+  // for field reads that will not get rejected, use `has`
+  Subject<Bar> get someField => has((a) => a.someField, 'someField');
+}
+```
+
+Extensions may also compose existing expectations under a single name. When
+such expectations fail, the test output will refer to the individual
+expectations that were called.
+
+```dart
+extension ComposedChecks on Subject<Iterable> {
+  void hasLengthInRange(int min, int max) {
+    length
+      ..isGreaterThan(min)
+      ..isLessThan(max);
+  }
+}
+```
diff --git a/pkgs/checks/doc/migrating_from_matcher.md b/pkgs/checks/doc/migrating_from_matcher.md
new file mode 100644
index 0000000..b358e63
--- /dev/null
+++ b/pkgs/checks/doc/migrating_from_matcher.md
@@ -0,0 +1,203 @@
+## Migrating from package:matcher
+
+`package:checks` is currently in preview. Once this package reaches a stable
+version, it will be the recommended package by the Dart team to use for most
+tests.
+
+[`package:matcher`][matcher] is the legacy package with an API exported from
+`package:test/test.dart` and `package:test/expect.dart`. 
+
+**Do I have to migrate all at once?** No. `package:matcher` will be compatible
+with `package:checks`, and old tests can continue to use matchers. Test cases
+within the same file can use a mix of `expect` and `check`.
+
+**_Should_ I migrate all at once?** Probably not, it depends on your tolerance
+for having tests use a mix of APIs. As you add new tests, or need to make
+updates to existing tests, using `checks` will make testing easier. Tests which
+are stable and passing will not get significant benefits from a migration.
+
+**Do I need to migrate at all?** No. When `package:test`stops exporting
+these members it will be possible to add a dependency on `package:matcher` and
+continue to use them. `package:matcher` will continue to be available.
+
+**Why is the Dart team adding a second framework?** The `matcher` package has a
+design which is fundamentally incompatible with using static types to validate
+correct use. With an entirely new design, the static types in `checks` give
+confidence that the expectation is appropriate for the value, and can narrow
+autocomplete choices in the IDE for a better editing experience. The clean break
+from the legacy implementation and API also gives an opportunity to make small
+behavior and signature changes to align with modern Dart idioms.
+
+**Should I start using checks right away?** There is still a
+high potential for minor or major breaking changes during the preview window.
+Once this package is stable, yes! The experience of using `checks` improves on
+`matcher`. See some of the [improvements to look forward to in checks
+below](#improvements-you-can-expect).
+
+[matcher]: https://pub.dev/packages/matcher
+
+## Trying Checks as a Preview
+
+1.  Add a `dev_dependency` on `checks: ^0.2.0`.
+
+1.  Replace the existing `package:test/test.dart` import with
+    `package:test/scaffolding.dart`.
+
+1.  Add an import to `package:checks/checks.dart`.
+
+1.  For an incremental migration within the test, add an import to
+    `package:test/expect.dart`. Remove it to surface errors in tests that still
+    need to be migrated, or keep it in so the tests work without being fully
+    migrated.
+
+1.  Migrate the test cases.
+
+## Migrating from Matchers
+
+Replace calls to `expect` or `expectLater` with a call to `check` passing the
+first argument.
+When a direct replacement is available, change the second argument from calling
+a function returning a Matcher, to calling the relevant extension method on the
+`Subject`.
+
+Whenever you see a bare non-matcher value argument for `expected`, assume it
+should use the `equals` expectation, although take care when the subject is a
+collection.
+See below, `.equals` may not always be the correct replacement in
+`package:checks`.
+
+```dart
+expect(actual, expected);
+check(actual).equals(expected);
+// or maybe
+check(actualCollection).deepEquals(expected);
+
+await expectLater(actual, completes());
+await check(actual).completes();
+```
+
+If you use the `reason` argument to `expect`, rename it to `because`.
+
+```dart
+expect(actual, expectation(), reason: 'some explanation');
+check(because: 'some explanation', actual).expectation();
+```
+
+### Differences in behavior from matcher
+
+-   The `equals` Matcher performed a deep equality check on collections.
+    `.equals()` expectation will only correspond to [operator ==] so some tests
+    may need to replace `.equals()` with `.deepEquals()`.
+-   Streams must be explicitly wrapped into a `StreamQueue` before they can be
+    tested for behavior. Use `check(actualStream).withQueue`.
+-   `emitsAnyOf` is `Subject<StreamQueue>.anyOf`. `emitsInOrder` is `inOrder`.
+    The arguments are `FutureOr<void> Function(Subject<StreamQueue>)` and match
+    a behavior of the entire stream. In `matcher` the elements to expect could
+    have been a bare value to check for equality, a matcher for the emitted
+    value, or a matcher for the entire queue which would match multiple values.
+    Use `(s) => s.emits((e) => e.interestingCheck())` to check the emitted
+    elements.
+-   In `package:matcher` the [`matches` Matcher][matches] converted a `String`
+    argument into a `Regex`, so `matches(r'\d')` would match the value `'1'`.
+    This was potentially confusing, because even though `String` is a subtype of
+    `Pattern`, it wasn't used as a pattern directly.
+    With `matchesPattern` a `String` argument is used as a `Pattern` and
+    comparison uses [`String.allMatches`][allMatches].
+    For backwards compatibility change `matches(regexString)` to
+    `matchesPattern(RegExp(regexString))`.
+-   The `TypeMatcher.having` API is replace by the more general`.has`. While
+    `.having` could only be called on a `TypeMatcher` using `.isA`, `.has` works
+    on any `Subject`. `CoreChecks.has` takes 1 fewer arguments - instead of
+    taking the last argument, a `matcher` to apply to the field, it returns a
+    `Subject` for the field.
+
+[matches]:https://pub.dev/documentation/matcher/latest/matcher/Matcher/matches.html
+[allMatches]:https://api.dart.dev/stable/2.19.1/dart-core/Pattern/allMatches.html
+
+### Matchers with replacements under a different name
+
+-   `anyElement` -> `Subject<Iterable>.any`
+-   `everyElement` -> `Subject<Iterable>.every`
+-   `completion(Matcher)` -> `completes(conditionCallback)`
+-   `containsPair(key, value)` -> Use `Subject<Map>[key].equals(value)`
+-   `hasLength(expected)` -> `length.equals(expected)`
+-   `isNot(Matcher)` -> `not(conditionCallback)`
+-   `pairwiseCompare` -> `pairwiseMatches`
+-   `same` -> `identicalTo`
+-   `stringContainsInOrder` -> `Subject<String>.containsInOrder`
+-   `containsAllInOrder(iterable)` ->
+    `Subject<Iterable>.containsMatchingInOrder(iterable)` to compare with
+    conditions other than equals,
+    `Subject<Iterable>.containsEqualInOrder(iterable)` to compare each index
+    with the equality operator (`==`).
+
+### Members from `package:test/expect.dart` without a direct replacement
+
+-   `checks` does not ship with any type checking matchers for specific types.
+    Instead of, for example,  `isArgumentError` use `isA<ArgumentError>`, and
+    similary `throws<ArgumentError>` over `throwsArgumentError`.
+-   `anything`. When a condition callback is needed that should accept any
+    value, pass `(_) {}`.
+-   Specific numeric comparison - `isNegative`, `isPositive`, `isZero` and their
+    inverses. Use `isLessThan`, `isGreaterThan`, `isLessOrEqual`, and
+    `isGreaterOrEqual` with appropriate numeric arguments.
+-   Numeric range comparison, `inClosedOpenRange`, `inExclusiveRange`,
+    `inInclusiveRange`, `inOpenClosedRange`. Use cascades to chain a check for
+    both ends of the range onto the same subject.
+-   `containsOnce`: TODO add missing expectation
+-   `emitsInAnyOrder`: TODO add missing expectation
+-   `expectAsync` and `expectAsyncUntil`. Continue to import
+    `package:test/expect.dart` for these APIs.
+-   `isIn`: TODO add missing expectation
+-   `orderedEquals`: Use `deepEquals`. If the equality needs to specifically
+    *not* be deep equality (this is unusual, nested collections are unlikely to
+    have a meaningful equality), force using `operator ==` at the first level
+    with `.deepEquals(expected.map((e) => (Subject<Object?> s) => s.equals(e)))`;
+-   `prints`: TODO add missing expectation? Is this one worth replacing?
+-   `predicate`: TODO add missing expectation
+
+## Improvements you can expect
+
+Expectations are statically restricted to those which are appropriate for the
+type. So while the following is statically allowed with `matcher` but always
+fails at runtime, the expectation cannot be written at all with `checks`.
+
+```dart
+expect(1, contains(1)); // No static error, always fails
+check(1).contains(1); // Static error. The method 'contains' isn't defined
+```
+
+These static restrictions also improve the relevance of IDE autocomplete
+suggestions. While editing with the cursor at `_`, the suggestions provided
+in the `matcher` example can include _any_ top level element including matchers
+appropriate for other types of value, type names, and top level definitions from
+other packages. With the cursor following a `.` in the `checks` example the
+suggestions will only be expectations or utilities appropriate for the value
+type.
+
+```dart
+expect(actual, _ // many unrelated suggestions
+check(actual)._ // specific suggestions
+```
+
+Asynchronous matchers in `matcher` are a subtype of synchronous matchers, but do
+not satisfy the same behavior contract. Some APIs which use a matcher could not
+validate whether it would satisfy the behavior it needs, and it could result in
+a false success, false failure, or misleading errors. APIs which correctly use
+asynchronous matchers need to do a type check and change their interaction based
+on the runtime type. Asynchronous expectations in `checks` are refused at
+runtime when a synchronous answer is required. The error will help solve the
+specific misuse, instead of resulting in a confusing error, or worse a missed
+failure. The reason for the poor compatibility in `matcher` is due to some
+history of implementation - asynchronous matchers were written in `test`
+alongside `expect`, and synchronous matchers have no dependency on the
+asynchronous implementation.
+
+Asynchronous expectations always return a `Future`, and with the
+[`unawaited_futures` lint][unawaited lint] should more safely ensure that
+asynchronous expectation work is completed within the test body. With `matcher`
+it was up to the author to correctly use `await expecLater` for asynchronous
+cases, and `expect` for synchronous cases, and if `expect` was used with an
+asynchronous matcher the expectation could fail at any point.
+
+[unawaited lint]: https://dart.dev/lints/unawaited_futures
diff --git a/pkgs/checks/example/example.dart b/pkgs/checks/example/example.dart
new file mode 100644
index 0000000..88bec0b
--- /dev/null
+++ b/pkgs/checks/example/example.dart
@@ -0,0 +1,26 @@
+// Copyright (c) 2023, 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:checks/checks.dart';
+import 'package:test/scaffolding.dart';
+
+void main() {
+  test('sample test', () {
+    final someValue = 5;
+    check(someValue).equals(5);
+
+    final someList = [1, 2, 3, 4, 5];
+    check(someList).deepEquals([1, 2, 3, 4, 5]);
+
+    final someString = 'abcdefghijklmnopqrstuvwxyz';
+
+    check(
+      because: 'it should contain the beginning, middle and end',
+      someString,
+    )
+      ..startsWith('a')
+      ..endsWith('z')
+      ..contains('lmno');
+  });
+}
diff --git a/pkgs/checks/lib/checks.dart b/pkgs/checks/lib/checks.dart
new file mode 100644
index 0000000..db8b18e
--- /dev/null
+++ b/pkgs/checks/lib/checks.dart
@@ -0,0 +1,15 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+export 'src/checks.dart'
+    show AsyncCondition, Condition, SkipExtension, Subject, check;
+export 'src/extensions/async.dart'
+    show FutureChecks, StreamChecks, WithQueueExtension;
+export 'src/extensions/core.dart'
+    show BoolChecks, ComparableChecks, CoreChecks, NullableChecks;
+export 'src/extensions/function.dart' show FunctionChecks;
+export 'src/extensions/iterable.dart' show IterableChecks;
+export 'src/extensions/map.dart' show MapChecks;
+export 'src/extensions/math.dart' show NumChecks;
+export 'src/extensions/string.dart' show StringChecks;
diff --git a/pkgs/checks/lib/context.dart b/pkgs/checks/lib/context.dart
new file mode 100644
index 0000000..cebba71
--- /dev/null
+++ b/pkgs/checks/lib/context.dart
@@ -0,0 +1,21 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+export 'src/checks.dart'
+    show
+        AsyncCondition,
+        CheckFailure,
+        Condition,
+        Context,
+        ContextExtension,
+        Extracted,
+        FailureDetail,
+        Rejection,
+        Subject,
+        describe,
+        describeAsync,
+        softCheck,
+        softCheckAsync;
+export 'src/describe.dart'
+    show escape, indent, literal, postfixLast, prefixFirst;
diff --git a/pkgs/checks/lib/src/checks.dart b/pkgs/checks/lib/src/checks.dart
new file mode 100644
index 0000000..e1e4bcb
--- /dev/null
+++ b/pkgs/checks/lib/src/checks.dart
@@ -0,0 +1,1009 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:meta/meta.dart' as meta;
+import 'package:test_api/hooks.dart';
+
+import 'describe.dart';
+import 'extensions/async.dart';
+import 'extensions/core.dart';
+import 'extensions/iterable.dart';
+
+/// A target for checking expectations against a value in a test.
+///
+/// A subject my have a real value, in which case the expectations can be
+/// validated or rejected; or it may be a placeholder, in which case
+/// expectations describe what would be checked but cannot be rejected.
+///
+/// Expectation methods are defined in extensions `on Subject`, specialized on
+/// the generic [T].
+/// Expectation extension methods can use the [ContextExtension] to interact
+/// with the [Context] for this subject.
+///
+/// Create a subject that throws an exception for missed expectations with the
+/// [check] function.
+final class Subject<T> {
+  final Context<T> _context;
+  Subject._(this._context);
+}
+
+/// A callback that synchronously checks expectations against a subject.
+///
+/// Asynchronous expectations should not be used within a `Condition` callback.
+typedef Condition<T> = void Function(Subject<T>);
+
+/// A callback that asynchronously checks expectations against a subject.
+///
+/// Any expectations may be used within an `AsyncCondition` callback.
+typedef AsyncCondition<T> = FutureOr<void> Function(Subject<T>);
+
+extension SkipExtension<T> on Subject<T> {
+  /// Mark the currently running test as skipped and return a [Subject] that
+  /// will ignore all expectations.
+  ///
+  /// Any expectations against the return value will not be checked and will not
+  /// be included in the "Expected" or "Actual" string representations of a
+  /// failure.
+  ///
+  /// ```dart
+  /// check(actual)
+  ///     ..stillChecked()
+  ///     ..skip('reason the expectation is temporarily not met').notChecked();
+  /// ```
+  ///
+  /// If `skip` is used in a callback passed to `softCheck` or `describe` it
+  /// will still mark the test as skipped, even though failing the expectation
+  /// would not have otherwise caused the test to fail.
+  Subject<T> skip(String message) {
+    TestHandle.current.markSkipped(message);
+    return Subject._(_SkippedContext());
+  }
+}
+
+/// Creates a [Subject] that can be used to validate expectations against
+/// [value], with an exception upon a failed expectation.
+///
+/// Expectations that are not satisfied throw a [TestFailure] to interrupt the
+/// currently running test and mark it as failed.
+///
+/// If [because] is passed it will be included as a "Reason:" line in failure
+/// messages.
+///
+/// ```dart
+/// check(actual).equals(expected);
+/// ```
+@meta.useResult
+Subject<T> check<T>(T value, {String? because}) => Subject._(_TestContext._root(
+      value: _Present(value),
+      // TODO - switch between "a" and "an"
+      label: 'a $T',
+      fail: (f) {
+        final which = f.rejection.which;
+        throw TestFailure([
+          ...prefixFirst('Expected: ', f.detail.expected),
+          ...prefixFirst('Actual: ', f.detail.actual),
+          ...indent(
+              prefixFirst('Actual: ', f.rejection.actual), f.detail.depth),
+          if (which != null && which.isNotEmpty)
+            ...indent(prefixFirst('Which: ', which), f.detail.depth),
+          if (because != null) 'Reason: $because',
+        ].join('\n'));
+      },
+      allowAsync: true,
+      allowUnawaited: true,
+    ));
+
+/// Checks whether [value] satisfies all expectations invoked in [condition],
+/// without throwing an exception.
+///
+/// Returns `null` if all expectations are satisfied, otherwise returns the
+/// [CheckFailure] for the first expectation that fails.
+///
+/// Asynchronous expectations are not allowed in [condition] and will cause a
+/// runtime error if they are used.
+CheckFailure? softCheck<T>(T value, Condition<T> condition) {
+  CheckFailure? failure;
+  final subject = Subject<T>._(_TestContext._root(
+    value: _Present(value),
+    fail: (f) {
+      failure ??= f;
+    },
+    allowAsync: false,
+    allowUnawaited: false,
+  ));
+  condition(subject);
+  return failure;
+}
+
+/// Checks whether [value] satisfies all expectations invoked in [condition],
+/// without throwing an exception.
+///
+/// The future will complete to `null` if all expectations are satisfied,
+/// otherwise it will complete to the [CheckFailure] for the first expectation
+/// that fails.
+///
+/// In contrast to [softCheck], asynchronous expectations are allowed in
+/// [condition].
+Future<CheckFailure?> softCheckAsync<T>(
+    T value, AsyncCondition<T> condition) async {
+  CheckFailure? failure;
+  final subject = Subject<T>._(_TestContext._root(
+    value: _Present(value),
+    fail: (f) {
+      failure ??= f;
+    },
+    allowAsync: true,
+    allowUnawaited: false,
+  ));
+  await condition(subject);
+  return failure;
+}
+
+/// Creates a description of the expectations checked by [condition].
+///
+/// The strings are individual lines of a description.
+/// The description of an expectation may be one or more adjacent lines.
+///
+/// Matches the "Expected: " lines in the output of a failure message if a value
+/// did not meet the last expectation in [condition], without the first labeled
+/// line.
+///
+/// Asynchronous expectations are not allowed in [condition], for async
+/// conditions use [describeAsync].
+Iterable<String> describe<T>(Condition<T> condition) {
+  final context = _TestContext<T>._root(
+    value: _Absent(),
+    fail: (_) {
+      throw UnimplementedError();
+    },
+    allowAsync: false,
+    allowUnawaited: true,
+  );
+  condition(Subject._(context));
+  return context.detail(context).expected.skip(1);
+}
+
+/// Creates a description of the expectations checked by [condition].
+///
+/// The strings are individual lines of a description.
+/// The description of an expectation may be one or more adjacent lines.
+///
+/// Matches the "Expected: " lines in the output of a failure message if a value
+/// did not meet the last expectation in [condition], without the first labeled
+/// line.
+///
+/// In contrast to [describe], asynchronous expectations are allowed in
+/// [condition].
+Future<Iterable<String>> describeAsync<T>(AsyncCondition<T> condition) async {
+  final context = _TestContext<T>._root(
+    value: _Absent(),
+    fail: (_) {
+      throw UnimplementedError();
+    },
+    allowAsync: true,
+    allowUnawaited: true,
+  );
+  await condition(Subject._(context));
+  return context.detail(context).expected.skip(1);
+}
+
+extension ContextExtension<T> on Subject<T> {
+  /// The expectations and nesting context for this subject.
+  Context<T> get context => _context;
+}
+
+/// The context for a [Subject] that allows asserting expectations and creating
+/// nested subjects.
+///
+/// A [Subject] is the target for checking expectations in a test.
+/// Every subject has a [Context] which holds the "actual" value, tracks how the
+/// value was obtained, and can check expectations about the value.
+///
+/// The user focused APIs called within tests are expectation extension methods
+/// written in an extension `on Subject`, typically specialized to a specific
+/// generic.
+///
+/// Expectation extension methods will make a call to one of the APIs on the
+/// subject's [Context], and can perform one of two types of operations:
+///
+/// -   Expect something of the current value (such as [CoreChecks.equals] or
+///     [IterableChecks.contains]) by calling [expect], [expectAsync], or
+///     [expectUnawaited].
+/// -   Expect that a new subject can be extracted from the current value (such
+///     as [CoreChecks.has] or [FutureChecks.completes]) by calling [nest] or
+///     [nestAsync].
+///
+///
+/// Whichever type of operation, an expectation extension method provides two
+/// callbacks.
+/// The first callback is an `Iterable<String> Function()` returning a
+/// description of the expectation.
+/// The second callback always takes the actual value as an argument, and the
+/// specific signature varies by operation.
+///
+///
+/// In expectation extension methods calling [expect], [expectAsync], or
+/// [expectUnawaited], the `predicate` callback can report a [Rejection] if the
+/// value fails to satisfy the expectation.
+/// The description will be passed in a "clause" callback.
+/// {@template clause_description}
+/// The clause callback returns a description of what is checked which stands
+/// on its own.
+/// For instance the `is equal to <1>` in:
+///
+/// ```
+/// Expected: a int that:
+///   is equal to <1>
+/// ```
+/// {@endtemplate}
+///
+///
+/// In expectation extension methods calling [nest] or [nestAsync], the
+/// `extract` callback can return a [Extracted.rejection] if the value fails to
+/// satisfy an expectation which disallows extracting the value, or an
+/// [Extracted.value] to become the value in a nested subject.
+/// The description will be passed in a "label" callback.
+/// {@template label_description}
+/// The label callback returns a description of the extracted subject as it
+/// relates to the original subject.
+/// For instance the `completes to a value` in:
+///
+/// ```
+/// Expected a Future<int> that:
+///   completes to a value that:
+///     is equal to <1>
+/// ```
+///
+/// A label should also be sensible when it is read as a clause.
+/// If no further expectations are checked on the extracted subject, or if the
+/// extraction is rejected, the "that:" is omitted in the output.
+///
+/// ```
+///   Expected a Future<int> that:
+///     completes to a value
+/// ```
+/// {@endtemplate}
+///
+///
+/// A rejection carries two descriptions, one description of the "actual" value
+/// that was tested, and an optional "which" with further details about how the
+/// result different from the expectation.
+/// If the "actual" argument is omitted it will be filled with a representation
+/// of the value passed to the expectation callback formatted with [literal].
+/// If an expectation extension method is written on a type of subject without a
+/// useful `toString()`, the rejection can provide a string representation to
+/// use instead.
+/// The "which" argument may be omitted if the reason is very obvious based on
+/// the clause and "actual" description, but most expectations should include a
+/// "which".
+///
+/// The behavior of a context following a rejection depends on the source of the
+/// [Subject].
+///
+/// When an expectation is rejected for a [check] subject, an exception is
+/// thrown to interrupt the test, so no further checks should happen. The
+/// failure message will include:
+/// -  An "Expected" section with descriptions of all the expectations that
+///    were checked, including the ones that passed, and the last one that
+///    failed.
+/// -  An "Actual" section, which may be the description directly from the
+///    [Rejection] if the failure was on the root subject, or may start with a
+///    partial version of the "Expected" description up to the label for the
+///    nesting subject that saw a failure, then the "actual" from the rejection.
+/// -  A "Which" description from the rejection, if it was included.
+///
+/// For example, if a failure happens on the root subject, the "actual" is taken
+/// directly from the rejection.
+///
+/// ```
+/// Expected: a Future<int> that:
+///   completes to a value
+/// Actual: a future that completes as an error
+/// Which: threw <UnimplementedError> at:
+/// <stack trace>
+/// ```
+///
+/// But if the failure happens on a nested subject, the actual starts with a
+/// description of the nesting or non-nesting expectations that succeeded, up
+/// to nesting point of the failure, then the "actual" and "which" from the
+/// rejection are indented to that level of nesting.
+///
+/// ```
+/// Expected: a Future<int> that:
+///   completes to a value that:
+///     equals <1>
+/// Actual: a Future<int> that:
+///   completes to a value that:
+///   Actual: <0>
+///   Which: are not equal
+/// ```
+///
+/// ```dart
+/// extension CustomChecks on Subject<CustomType> {
+///   void someExpectation() {
+///     context.expect(() => ['meets this expectation'], (actual) {
+///       if (_expectationIsMet(actual)) return null;
+///       return Rejection(which: ['does not meet this expectation']);
+///     });
+///   }
+///
+///   Subject<Foo> get someDerivedValue =>
+///       context.nest('has someDerivedValue', (actual) {
+///         if (_cannotReadDerivedValue(actual)) {
+///           return Extracted.rejection(which: ['cannot read someDerivedValue']);
+///         }
+///         return Extracted.value(_readDerivedValue(actual));
+///       });
+///
+///   // for field reads that will not get rejected, use `has`
+///   Subject<Bar> get someField => has((a) => a.someField, 'someField');
+/// }
+/// ```
+///
+/// When an expectation is rejected for a subject within a call to [softCheck]
+/// or [softCheckAsync] a [CheckFailure] will be returned with the rejection, as
+/// well as a [FailureDetail] which could be used to format the same failure
+/// message thrown by the [check] subject.
+///
+/// {@template callbacks_may_be_unused}
+/// The description of an expectation may never be shown to the user, so the
+/// callback may never be invoked.
+/// If all the conditions on a subject succeed, or if the failure detail for a
+/// failed [softCheck] is never read, the descriptions will be unused.
+/// String formatting for the descriptions should be performed in the callback,
+/// not ahead of time.
+///
+///
+/// The context for a subject may hold a real "actual" value to test against, or
+/// it may have a placeholder within a call to [describe].
+/// A context with a placeholder value will not invoke the callback to check
+/// expectations.
+///
+/// If both callbacks are invoked, the description callback will always be
+/// called strictly after the expectation callback is called.
+///
+/// Callbacks passed to a context should not throw.
+/// {@endtemplate}
+///
+///
+/// Some contexts disallow certain interactions.
+/// {@template async_limitations}
+/// Calls to [expectAsync] or [nestAsync] must not be performed by a condition
+/// callback passed to [softCheck] or [describe].
+/// Use [softCheckAsync] or [describeAsync] for any condition which checks async
+/// expectations.
+/// {@endtemplate}
+/// {@template unawaited_limitations}
+/// Calls to [expectUnawaited] may not be performed by a condition callback
+/// passed to [softCheck] or [softCheckAsync].
+/// {@endtemplate}
+///
+/// Expectation extension methods can access the context for the subject with
+/// the [ContextExtension].
+///
+/// {@template description_lines}
+/// Description callbacks return an `Iterable<String>` where each element is a
+/// line in the output. Individual elements should not contain newlines.
+/// Utilities such as [prefixFirst], [postfixLast], and [literal] may be useful
+/// to format values which are potentially multiline.
+/// {@endtemplate}
+abstract final class Context<T> {
+  /// Expect that [predicate] will not return a [Rejection] for the checked
+  /// value.
+  ///
+  /// {@macro clause_description}
+  ///
+  /// {@macro description_lines}
+  ///
+  /// {@macro callbacks_may_be_unused}
+  ///
+  /// ```dart
+  /// void someExpectation() {
+  ///   context.expect(() => ['meets this expectation'], (actual) {
+  ///     if (_expectationIsMet(actual)) return null;
+  ///     return Rejection(which: ['does not meet this expectation']);
+  ///   });
+  /// }
+  /// ```
+  void expect(
+      Iterable<String> Function() clause, Rejection? Function(T) predicate);
+
+  /// Expect that [predicate] will not result in a [Rejection] for the checked
+  /// value.
+  ///
+  /// {@macro clause_description}
+  ///
+  /// {@macro description_lines}
+  ///
+  /// {@macro callbacks_may_be_unused}
+  ///
+  /// {@macro async_limitations}
+  ///
+  /// ```dart
+  /// extension CustomChecks on Subject<CustomType> {
+  ///   Future<void> someAsyncExpectation() async {
+  ///     await context.expectAsync(() => ['meets this async expectation'],
+  ///         (actual) async {
+  ///       if (await _expectationIsMet(actual)) return null;
+  ///       return Rejection(which: ['does not meet this async expectation']);
+  ///     });
+  ///   }
+  /// }
+  /// ```
+  Future<void> expectAsync(Iterable<String> Function() clause,
+      FutureOr<Rejection?> Function(T) predicate);
+
+  /// Expect that [predicate] will not invoke the passed callback with a
+  /// [Rejection] at any point.
+  ///
+  /// In contrast to [expectAsync], a rejection is reported through a
+  /// callback instead of through a returned Future. The callback may be invoked
+  /// at any point that the failure surfaces.
+  ///
+  /// This may be useful for a condition checking that some event _never_
+  /// happens. If there is no specific point where it is know to be safe to stop
+  /// listening for the event, there is no way to complete a returned future and
+  /// consider the check "complete".
+  ///
+  /// {@macro clause_description}
+  ///
+  /// {@macro description_lines}
+  ///
+  /// {@macro callbacks_may_be_unused}
+  ///
+  /// {@macro unawaited_limitations}
+  /// The only useful effect of a late rejection is to throw a [TestFailure]
+  /// when used with a [check] subject. Most conditions should prefer to use
+  /// [expect] or [expectAsync].
+  ///
+  /// ```dart
+  /// void someUnawaitableExpectation() async {
+  ///   await context.expectUnawaited(
+  ///       () => ['meets this unawaitable expectation'], (actual, reject) {
+  ///     final failureSignal = _completeIfFailed(actual);
+  ///     unawaited(failureSignal.then((_) {
+  ///       reject(Reject(
+  ///           which: ['unexpectedly failed this unawaited expectation']));
+  ///     }));
+  ///   });
+  /// }
+  /// ```
+  void expectUnawaited(Iterable<String> Function() clause,
+      void Function(T, void Function(Rejection)) predicate);
+
+  /// Extract a property from the value for further checking.
+  ///
+  /// If the property cannot be extracted, [extract] should return an
+  /// [Extracted.rejection] describing the problem. Otherwise it should return
+  /// an [Extracted.value].
+  ///
+  /// Subsequent expectations can be checked for the extracted value on the
+  /// returned [Subject].
+  ///
+  /// {@macro label_description}
+  ///
+  /// If [atSameLevel] is true then the returned [Extracted.value] should hold
+  /// the same instance as the passed value, or an object which is is equivalent
+  /// but has a type that is more convenient to test.
+  /// In this case expectations applied to the returned [Subject] will behave as
+  /// if they were applied to the subject for this context.
+  /// The [label] will be used as if it were a "clause" argument passed to
+  /// [expect].
+  /// If the label returns an empty iterable, the clause will be omitted.
+  /// The label should only be left empty if the value extraction cannot be
+  /// rejected.
+  ///
+  /// {@macro description_lines}
+  ///
+  /// {@macro callbacks_may_be_unused}
+  ///
+  /// ```dart
+  /// Subject<Foo> get someDerivedValue =>
+  ///     context.nest(() => ['has someDerivedValue'], (actual) {
+  ///       if (_cannotReadDerivedValue(actual)) {
+  ///         return Extracted.rejection(
+  ///             which: ['cannot read someDerivedValue']);
+  ///       }
+  ///       return Extracted.value(_readDerivedValue(actual));
+  ///     });
+  /// ```
+  Subject<R> nest<R>(
+      Iterable<String> Function() label, Extracted<R> Function(T) extract,
+      {bool atSameLevel = false});
+
+  /// Extract an asynchronous property from the value for further checking.
+  ///
+  /// If the property cannot be extracted, [extract] should return an
+  /// [Extracted.rejection] describing the problem. Otherwise it should return
+  /// an [Extracted.value].
+  ///
+  /// In contrast to [nest], subsequent expectations need to be passed in
+  /// [nestedCondition] which will be applied to the subject for the extracted
+  /// value.
+  ///
+  /// {@macro label_description}
+  ///
+  /// {@macro description_lines}
+  ///
+  /// {@macro callbacks_may_be_unused}
+  ///
+  /// {@macro async_limitations}
+  ///
+  /// ```dart
+  /// Future<void> someAsyncResult(
+  ///     [AsyncCondition<Result> resultCondition]) async {
+  ///   await context.nestAsync(() => ['has someAsyncResult'], (actual) async {
+  ///     if (await _asyncOperationFailed(actual)) {
+  ///       return Extracted.rejection(which: ['cannot read someAsyncResult']);
+  ///     }
+  ///     return Extracted.value(await _readAsyncResult(actual));
+  ///   }, resultCondition);
+  /// }
+  /// ```
+  Future<void> nestAsync<R>(
+      Iterable<String> Function() label,
+      FutureOr<Extracted<R>> Function(T) extract,
+      AsyncCondition<R>? nestedCondition);
+}
+
+/// A property extracted from a value being checked, or a rejection.
+final class Extracted<T> {
+  final Rejection? _rejection;
+  final T? _value;
+
+  /// Creates a rejected extraction to indicate a failure trying to read the
+  /// value.
+  ///
+  /// When a nesting is rejected with an omitted or empty [actual] argument, it
+  /// will be filled in with the [literal] representation of the value.
+  Extracted.rejection(
+      {Iterable<String> actual = const [], Iterable<String>? which})
+      : _rejection = Rejection(actual: actual, which: which),
+        _value = null;
+  Extracted.value(T this._value) : _rejection = null;
+
+  Extracted._(Rejection this._rejection) : _value = null;
+
+  Extracted<R> _map<R>(R Function(T) transform) {
+    final rejection = _rejection;
+    if (rejection != null) return Extracted._(rejection);
+    return Extracted.value(transform(_value as T));
+  }
+
+  Extracted<T> _fillActual(Object? actual) => _rejection == null ||
+          _rejection.actual.isNotEmpty
+      ? this
+      : Extracted.rejection(actual: literal(actual), which: _rejection.which);
+}
+
+abstract interface class _Optional<T> {
+  R? apply<R extends FutureOr<Rejection?>>(R Function(T) callback);
+  Future<Extracted<_Optional<R>>> mapAsync<R>(
+      FutureOr<Extracted<R>> Function(T) transform);
+  Extracted<_Optional<R>> map<R>(Extracted<R> Function(T) transform);
+}
+
+class _Present<T> implements _Optional<T> {
+  final T value;
+  _Present(this.value);
+
+  @override
+  R? apply<R extends FutureOr<Rejection?>>(R Function(T) c) => c(value);
+
+  @override
+  Future<Extracted<_Present<R>>> mapAsync<R>(
+      FutureOr<Extracted<R>> Function(T) transform) async {
+    final transformed = await transform(value);
+    return transformed._map(_Present.new);
+  }
+
+  @override
+  Extracted<_Present<R>> map<R>(Extracted<R> Function(T) transform) =>
+      transform(value)._map(_Present.new);
+}
+
+class _Absent<T> implements _Optional<T> {
+  @override
+  R? apply<R extends FutureOr<Rejection?>>(R Function(T) c) => null;
+
+  @override
+  Future<Extracted<_Absent<R>>> mapAsync<R>(
+          FutureOr<Extracted<R>> Function(T) transform) async =>
+      Extracted.value(_Absent<R>());
+
+  @override
+  Extracted<_Absent<R>> map<R>(FutureOr<Extracted<R>> Function(T) transform) =>
+      Extracted.value(_Absent<R>());
+}
+
+final class _TestContext<T> implements Context<T>, _ClauseDescription {
+  final _Optional<T> _value;
+
+  /// A reference to find the root context which this context is nested under.
+  ///
+  /// null only for the root context.
+  final _TestContext<dynamic>? _parent;
+
+  final List<_ClauseDescription> _clauses;
+  final List<_TestContext> _aliases;
+
+  final void Function(CheckFailure) _fail;
+
+  final bool _allowAsync;
+  final bool _allowUnawaited;
+
+  /// A callback that returns a label for this context.
+  ///
+  /// If this context is the root the label should return a phrase like
+  /// "a List" in
+  ///
+  /// ```
+  /// Expected: a List that:
+  /// ```
+  ///
+  /// If this context is nested under another context the lable should return a
+  /// phrase like "completes to a value" in
+  ///
+  ///
+  /// ```
+  /// Expected: a Future<int> that:
+  ///   completes to a value that:
+  /// ```
+  ///
+  /// In cases where a nested context does not have any expectations checked on
+  /// it, the "that:" will be will be omitted.
+  final Iterable<String> Function() _label;
+
+  static Iterable<String> _emptyLabel() => const [];
+
+  /// Create a context appropriate for a subject which is not nested under any
+  /// other subject.
+  _TestContext._root({
+    required _Optional<T> value,
+    required void Function(CheckFailure) fail,
+    required bool allowAsync,
+    required bool allowUnawaited,
+    String? label,
+  })  : _value = value,
+        _label = (() => [label ?? '']),
+        _fail = fail,
+        _allowAsync = allowAsync,
+        _allowUnawaited = allowUnawaited,
+        _parent = null,
+        _clauses = [],
+        _aliases = [];
+
+  _TestContext._alias(_TestContext original, this._value)
+      : _parent = original,
+        _clauses = original._clauses,
+        _aliases = original._aliases,
+        _fail = original._fail,
+        _allowAsync = original._allowAsync,
+        _allowUnawaited = original._allowUnawaited,
+        // Never read from an aliased context because they are never present in
+        // `_clauses`.
+        _label = _emptyLabel;
+
+  /// Create a context nested under [parent].
+  ///
+  /// The [_label] callback should not return an empty iterable.
+  _TestContext._child(this._value, this._label, _TestContext<dynamic> parent)
+      : _parent = parent,
+        _fail = parent._fail,
+        _allowAsync = parent._allowAsync,
+        _allowUnawaited = parent._allowUnawaited,
+        _clauses = [],
+        _aliases = [];
+
+  @override
+  void expect(
+      Iterable<String> Function() clause, Rejection? Function(T) predicate) {
+    _clauses.add(_ExpectationClause(clause));
+    final rejection =
+        _value.apply((actual) => predicate(actual)?._fillActual(actual));
+    if (rejection != null) {
+      _fail(_failure(rejection));
+    }
+  }
+
+  @override
+  Future<void> expectAsync(Iterable<String> Function() clause,
+      FutureOr<Rejection?> Function(T) predicate) async {
+    if (!_allowAsync) {
+      throw StateError(
+          'Async expectations cannot be used on a synchronous subject');
+    }
+    _clauses.add(_ExpectationClause(clause));
+    final outstandingWork = TestHandle.current.markPending();
+    try {
+      final rejection = await _value.apply(
+          (actual) async => (await predicate(actual))?._fillActual(actual));
+      if (rejection == null) return;
+      _fail(_failure(rejection));
+    } finally {
+      outstandingWork.complete();
+    }
+  }
+
+  @override
+  void expectUnawaited(Iterable<String> Function() clause,
+      void Function(T actual, void Function(Rejection) reject) predicate) {
+    if (!_allowUnawaited) {
+      throw StateError('Late expectations cannot be used for soft checks');
+    }
+    _clauses.add(_ExpectationClause(clause));
+    _value.apply((actual) {
+      predicate(actual, (r) => _fail(_failure(r._fillActual(actual))));
+    });
+  }
+
+  @override
+  Subject<R> nest<R>(
+      Iterable<String> Function() label, Extracted<R> Function(T) extract,
+      {bool atSameLevel = false}) {
+    final result = _value.map((actual) => extract(actual)._fillActual(actual));
+    final rejection = result._rejection;
+    if (rejection != null) {
+      _clauses.add(_ExpectationClause(label));
+      _fail(_failure(rejection));
+    }
+    final value = result._value ?? _Absent<R>();
+    final _TestContext<R> context;
+    if (atSameLevel) {
+      context = _TestContext._alias(this, value);
+      _aliases.add(context);
+      _clauses.add(_ExpectationClause(label));
+    } else {
+      context = _TestContext._child(value, label, this);
+      _clauses.add(context);
+    }
+    return Subject._(context);
+  }
+
+  @override
+  Future<void> nestAsync<R>(
+      Iterable<String> Function() label,
+      FutureOr<Extracted<R>> Function(T) extract,
+      AsyncCondition<R>? nestedCondition) async {
+    if (!_allowAsync) {
+      throw StateError(
+          'Async expectations cannot be used on a synchronous subject');
+    }
+    final outstandingWork = TestHandle.current.markPending();
+    try {
+      final result = await _value.mapAsync(
+          (actual) async => (await extract(actual))._fillActual(actual));
+      final rejection = result._rejection;
+      if (rejection != null) {
+        _clauses.add(_ExpectationClause(label));
+        _fail(_failure(rejection));
+      }
+      final value = result._value ?? _Absent<R>();
+      final context = _TestContext<R>._child(value, label, this);
+      _clauses.add(context);
+      await nestedCondition?.call(Subject<R>._(context));
+    } finally {
+      outstandingWork.complete();
+    }
+  }
+
+  CheckFailure _failure(Rejection rejection) =>
+      CheckFailure(rejection, () => _root.detail(this));
+
+  _TestContext get _root {
+    _TestContext<dynamic> current = this;
+    while (current._parent != null) {
+      current = current._parent;
+    }
+    return current;
+  }
+
+  @override
+  FailureDetail detail(_TestContext failingContext) {
+    final thisContextFailed =
+        identical(failingContext, this) || _aliases.contains(failingContext);
+    var foundDepth = thisContextFailed ? 0 : -1;
+    var foundOverlap = thisContextFailed ? 0 : -1;
+    var successfulOverlap = 0;
+    final expected = <String>[];
+    if (_clauses.isEmpty) {
+      expected.addAll(_label());
+    } else {
+      expected.addAll(postfixLast(' that:', _label()));
+      for (var clause in _clauses) {
+        final details = clause.detail(failingContext);
+        expected.addAll(indent(details.expected));
+        if (details.depth >= 0) {
+          assert(foundDepth == -1);
+          assert(foundOverlap == -1);
+          foundDepth = details.depth + 1;
+          foundOverlap = details._actualOverlap + successfulOverlap + 1;
+        } else {
+          if (foundDepth == -1) {
+            successfulOverlap += details.expected.length;
+          }
+        }
+      }
+    }
+    return FailureDetail(expected, foundOverlap, foundDepth);
+  }
+}
+
+/// A context which never runs expectations and can never fail.
+final class _SkippedContext<T> implements Context<T> {
+  @override
+  void expect(
+      Iterable<String> Function() clause, Rejection? Function(T) predicate) {
+    // no-op
+  }
+
+  @override
+  Future<void> expectAsync(Iterable<String> Function() clause,
+      FutureOr<Rejection?> Function(T) predicate) async {
+    // no-op
+  }
+
+  @override
+  void expectUnawaited(Iterable<String> Function() clause,
+      void Function(T actual, void Function(Rejection) reject) predicate) {
+    // no-op
+  }
+
+  @override
+  Subject<R> nest<R>(
+      Iterable<String> Function() label, Extracted<R> Function(T p1) extract,
+      {bool atSameLevel = false}) {
+    return Subject._(_SkippedContext());
+  }
+
+  @override
+  Future<void> nestAsync<R>(
+      Iterable<String> Function() label,
+      FutureOr<Extracted<R>> Function(T p1) extract,
+      AsyncCondition<R>? nestedCondition) async {
+    // no-op
+  }
+}
+
+abstract interface class _ClauseDescription {
+  FailureDetail detail(_TestContext failingContext);
+}
+
+class _ExpectationClause implements _ClauseDescription {
+  final Iterable<String> Function() _expected;
+  _ExpectationClause(this._expected);
+  @override
+  FailureDetail detail(_TestContext failingContext) =>
+      FailureDetail(_expected(), -1, -1);
+}
+
+/// The result an expectation that failed for a subject..
+final class CheckFailure {
+  /// The specific rejected value within the overall subject that caused the
+  /// failure.
+  ///
+  /// The [Rejection.actual] may be a property derived from the value at the
+  /// root of the subject, for instance a field or an element in a collection.
+  final Rejection rejection;
+
+  /// The context within the overall subject where an expectation resulted in
+  /// the [rejection].
+  late final FailureDetail detail = _readDetail();
+
+  final FailureDetail Function() _readDetail;
+
+  CheckFailure(this.rejection, this._readDetail);
+}
+
+/// The context for a failed expectation.
+///
+/// A subject may have some number of succeeding expectations, and the failure may
+/// be for an expectation against a property derived from the value at the root
+/// of the subject. For example, in `check([]).length.equals(1)` the
+/// specific value that gets rejected is `0` from the length of the list, and
+/// the subject that sees the rejection is nested with the label "has length".
+final class FailureDetail {
+  /// A description of all the conditions the subject was expected to satisfy.
+  ///
+  /// Each subject has a label. At the root the label is typically "a
+  /// &lt;Type&gt;" and nested subjects get a label based on the condition
+  /// which extracted a property for further checks. Each level of nesting is
+  /// described as "&lt;label&gt; that:" followed by an indented list of the
+  /// expectations for that property.
+  ///
+  /// For example:
+  ///
+  ///   a List that:
+  ///     has length that:
+  ///       equals &lt;3&gt;
+  final Iterable<String> expected;
+
+  /// A description of the conditions the checked value satisfied.
+  ///
+  /// Matches the format of [expected], except it will be cut off after the
+  /// label for the subject that had a failing expectation. For example, if the
+  /// equality check for the length of a list fails:
+  ///
+  ///   a List that:
+  ///     has length that:
+  ///
+  /// If the subject with a failing expectation is the root, returns an empty
+  /// list. Instead the "Actual: " value from the rejection can be used without
+  /// indentation.
+  Iterable<String> get actual =>
+      _actualOverlap > 0 ? expected.take(_actualOverlap + 1) : const [];
+
+  /// The number of lines from [expected] which describe conditions that were
+  /// successful.
+  ///
+  /// A failed expectation on a derived property may have some number of
+  /// expectations that were checked and satisfied starting from the root
+  /// subject. This field indicates how many lines of expectations were
+  /// successful.
+  final int _actualOverlap;
+
+  /// The number of times the failing subject was nested from the root subject.
+  ///
+  /// Indicates how far the "Actual: " and "Which: " lines from the [Rejection]
+  /// should be indented so that they are at the same level of indentation as
+  /// the label for the subject where the expectation failed.
+  ///
+  /// For example, if a `List` is expected to and have a certain length
+  /// [expected] may be:
+  ///
+  ///   a List that:
+  ///     has length that:
+  ///       equals &lt;3&gt;
+  ///
+  /// If the actual value had an incorrect length, the [depth] will be `1` to
+  /// indicate that the failure occurred checking one of the expectations
+  /// against the `has length` label.
+  final int depth;
+
+  FailureDetail(this.expected, this._actualOverlap, this.depth);
+}
+
+/// A description of a value that failed an expectation.
+final class Rejection {
+  /// A description of the actual value as it relates to the expectation.
+  ///
+  /// This may use [literal] to show a String representation of the value, or it
+  /// may be a description of a specific aspect of the value. For instance an
+  /// expectation that a Future completes to a value may describe the actual as
+  /// "A Future that completes to an error".
+  ///
+  /// When a value is rejected with no [actual] argument, it will be filled in
+  /// with the [literal] representation of the value.
+  ///
+  /// Lines should be split to separate elements, and individual strings should
+  /// not contain newlines.
+  ///
+  /// This is printed following an "Actual: " label in the output of a failure
+  /// message. All lines in the message will be indented to the level of the
+  /// expectation in the description, and printed following the descriptions of
+  /// any expectations that have already passed.
+  final Iterable<String> actual;
+
+  /// A description of the way that [actual] failed to meet the expectation.
+  ///
+  /// An expectation can provide extra detail, or focus attention on a specific
+  /// part of the value. For instance when comparing multiple elements in a
+  /// collection, the rejection may describe that the value "has an unequal
+  /// value at index 3".
+  ///
+  /// Lines should be separate values in the iterable, if any element contains a
+  /// newline it may cause problems with indentation in the output.
+  ///
+  /// When provided, this is printed following a "Which: " label at the end of
+  /// the output for the failure message.
+  final Iterable<String>? which;
+
+  Rejection _fillActual(Object? value) => actual.isNotEmpty
+      ? this
+      : Rejection(actual: literal(value), which: which);
+
+  Rejection({this.actual = const [], this.which});
+}
diff --git a/pkgs/checks/lib/src/collection_equality.dart b/pkgs/checks/lib/src/collection_equality.dart
new file mode 100644
index 0000000..da6064a
--- /dev/null
+++ b/pkgs/checks/lib/src/collection_equality.dart
@@ -0,0 +1,431 @@
+// Copyright (c) 2023, 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:collection';
+
+import '../context.dart';
+
+/// Returns a descriptive `which` for a rejection if the elements of [actual]
+/// are unequal to the elements of [expected].
+///
+/// {@template deep_collection_equals}
+/// Elements, keys, or values within [expected] which are a collections are
+/// deeply compared for equality with a collection in the same position within
+/// [actual]. Elements which are collection types are not compared with the
+/// native identity based equality or custom equality operator overrides.
+///
+/// Elements, keys, or values within [expected] which are [Condition] callbacks
+/// are run against the value in the same position within [actual].
+/// Condition callbacks must take a `Subject<Object?>` or `Subject<dynamic>` and
+/// may not use a more specific generic.
+/// Use `(Subject<Object?> s) => s.isA<Type>()` to check expectations for
+/// specific element types.
+/// Note also that the argument type `Subject<Object?>` cannot be inferred and
+/// must be explicit in the function definition.
+///
+/// Elements and values within [expected] which are any other type are compared
+/// using `operator ==` equality.
+///
+/// Deep equality checks for [Map] instances depend on the structure of the
+/// expected map.
+/// If all keys of the expectation are non-collection and non-condition values
+/// the keys are compared through the map instances with [Map.containsKey].
+///
+/// If any key in the expectation is a `Condition` or a collection type the map
+/// will be compared as a [Set] of entries where both the key and value must
+/// match.
+///
+/// Comparing sets or maps with complex keys has a runtime which is polynomial
+/// on the the size of those collections.
+/// These comparisons do not use [Set.contains] or [Map.containsKey],
+/// and there will not be runtime benefits from hashing.
+/// Custom collection behavior of `contains` is ignored.
+/// For example, it is not possible to distinguish between a `Set` and a
+/// `Set.identity`.
+///
+/// Collections may be nested to a maximum depth of 1000. Recursive collections
+/// are not allowed.
+/// {@endtemplate}
+Iterable<String>? deepCollectionEquals(Object actual, Object expected) {
+  try {
+    return _deepCollectionEquals(actual, expected, 0);
+  } on _ExceededDepthError {
+    return ['exceeds the depth limit of $_maxDepth'];
+  }
+}
+
+const _maxDepth = 1000;
+
+class _ExceededDepthError extends Error {}
+
+Iterable<String>? _deepCollectionEquals(
+    Object actual, Object expected, int depth) {
+  assert(actual is Iterable || actual is Map);
+  assert(expected is Iterable || expected is Map);
+
+  final queue = Queue.of([_Search(_Path.root(), actual, expected, depth)]);
+  while (queue.isNotEmpty) {
+    final toCheck = queue.removeFirst();
+    final currentActual = toCheck.actual;
+    final currentExpected = toCheck.expected;
+    final path = toCheck.path;
+    final currentDepth = toCheck.depth;
+    Iterable<String>? rejectionWhich;
+    if (currentExpected is Set) {
+      rejectionWhich = _findSetDifference(
+          currentActual, currentExpected, path, currentDepth);
+    } else if (currentExpected is Iterable) {
+      rejectionWhich = _findIterableDifference(
+          currentActual, currentExpected, path, queue, currentDepth);
+    } else {
+      currentExpected as Map;
+      rejectionWhich = _findMapDifference(
+          currentActual, currentExpected, path, queue, currentDepth);
+    }
+    if (rejectionWhich != null) return rejectionWhich;
+  }
+  return null;
+}
+
+List<String>? _findIterableDifference(Object? actual,
+    Iterable<Object?> expected, _Path path, Queue<_Search> queue, int depth) {
+  if (actual is! Iterable) {
+    return ['${path}is not an Iterable'];
+  }
+  var actualIterator = actual.iterator;
+  var expectedIterator = expected.iterator;
+  for (var index = 0;; index++) {
+    var actualNext = actualIterator.moveNext();
+    var expectedNext = expectedIterator.moveNext();
+    if (!expectedNext && !actualNext) break;
+    if (!expectedNext) {
+      return [
+        '${path}has more elements than expected',
+        'expected an iterable with $index element(s)'
+      ];
+    }
+    if (!actualNext) {
+      return [
+        '${path}has too few elements',
+        'expected an iterable with at least ${index + 1} element(s)'
+      ];
+    }
+    final difference = _compareValue(actualIterator.current,
+        expectedIterator.current, path, index, queue, depth);
+    if (difference != null) return difference;
+  }
+  return null;
+}
+
+List<String>? _compareValue(Object? actualValue, Object? expectedValue,
+    _Path path, Object? pathAppend, Queue<_Search> queue, int depth) {
+  if (expectedValue is Iterable || expectedValue is Map) {
+    if (depth + 1 > _maxDepth) throw _ExceededDepthError();
+    queue.addLast(_Search(
+        path.append(pathAppend), actualValue, expectedValue, depth + 1));
+  } else if (expectedValue is Condition) {
+    final failure = softCheck(actualValue, expectedValue);
+    if (failure != null) {
+      final which = failure.rejection.which;
+      return [
+        'has an element ${path.append(pathAppend)}that:',
+        ...indent(failure.detail.actual.skip(1)),
+        ...indent(prefixFirst('Actual: ', failure.rejection.actual),
+            failure.detail.depth + 1),
+        if (which != null)
+          ...indent(prefixFirst('which ', which), failure.detail.depth + 1)
+      ];
+    }
+  } else {
+    if (actualValue != expectedValue) {
+      return [
+        ...prefixFirst('${path.append(pathAppend)}is ', literal(actualValue)),
+        ...prefixFirst('which does not equal ', literal(expectedValue))
+      ];
+    }
+  }
+  return null;
+}
+
+bool _elementMatches(Object? actual, Object? expected, int depth) {
+  if (expected == null) return actual == null;
+  if (expected is Iterable || expected is Map) {
+    if (++depth > _maxDepth) throw _ExceededDepthError();
+    return actual != null &&
+        _deepCollectionEquals(actual, expected, depth) == null;
+  }
+  if (expected is Condition) {
+    return softCheck(actual, expected) == null;
+  }
+  return expected == actual;
+}
+
+Iterable<String>? _findSetDifference(
+    Object? actual, Set<Object?> expected, _Path path, int depth) {
+  if (actual is! Set) {
+    return ['${path}is not a Set'];
+  }
+  return unorderedCompare(
+    actual,
+    expected,
+    (actual, expected) => _elementMatches(actual, expected, depth),
+    (expected, _, count) => [
+      ...prefixFirst('${path}has no element to match ', literal(expected)),
+      if (count > 1) 'or ${count - 1} other elements',
+    ],
+    (actual, _, count) => [
+      ...prefixFirst('${path}has an unexpected element ', literal(actual)),
+      if (count > 1) 'and ${count - 1} other unexpected elements',
+    ],
+  );
+}
+
+Iterable<String>? _findMapDifference(
+    Object? actual,
+    Map<Object?, Object?> expected,
+    _Path path,
+    Queue<_Search> queue,
+    int depth) {
+  if (actual is! Map) {
+    return ['${path}is not a Map'];
+  }
+  if (expected.keys
+      .any((key) => key is Condition || key is Iterable || key is Map)) {
+    return _findAmbiguousMapDifference(actual, expected, path, depth);
+  } else {
+    return _findUnambiguousMapDifference(actual, expected, path, queue, depth);
+  }
+}
+
+Iterable<String> _describeEntry(MapEntry<Object?, Object?> entry) {
+  final key = literal(entry.key);
+  final value = literal(entry.value);
+  return [
+    ...key.take(key.length - 1),
+    '${key.last}: ${value.first}',
+    ...value.skip(1)
+  ];
+}
+
+/// Returns a description of a difference found between [actual] and [expected]
+/// when [expected] has only direct key values and there is a 1:1 mapping
+/// between an expected value and a checked value in the map.
+Iterable<String>? _findUnambiguousMapDifference(
+    Map<Object?, Object?> actual,
+    Map<Object?, Object?> expected,
+    _Path path,
+    Queue<_Search> queue,
+    int depth) {
+  for (final entry in expected.entries) {
+    assert(entry.key is! Condition);
+    assert(entry.key is! Iterable);
+    assert(entry.key is! Map);
+    if (!actual.containsKey(entry.key)) {
+      return prefixFirst(
+          '${path}has no key matching expected entry ', _describeEntry(entry));
+    }
+    final difference = _compareValue(
+        actual[entry.key], entry.value, path, entry.key, queue, depth);
+    if (difference != null) return difference;
+  }
+  for (final entry in actual.entries) {
+    if (!expected.containsKey(entry.key)) {
+      return prefixFirst(
+          '${path}has an unexpected key for entry ', _describeEntry(entry));
+    }
+  }
+  return null;
+}
+
+Iterable<String>? _findAmbiguousMapDifference(Map<Object?, Object?> actual,
+        Map<Object?, Object?> expected, _Path path, int depth) =>
+    unorderedCompare(
+      actual.entries,
+      expected.entries,
+      (actual, expected) =>
+          _elementMatches(actual.key, expected.key, depth) &&
+          _elementMatches(actual.value, expected.value, depth),
+      (expectedEntry, _, count) => [
+        ...prefixFirst(
+            '${path}has no entry to match ', _describeEntry(expectedEntry)),
+        if (count > 1) 'or ${count - 1} other entries',
+      ],
+      (actualEntry, _, count) => [
+        ...prefixFirst(
+            '${path}has unexpected entry ', _describeEntry(actualEntry)),
+        if (count > 1) 'and ${count - 1} other unexpected entries',
+      ],
+    );
+
+class _Path {
+  final _Path? parent;
+  final Object? index;
+  _Path._(this.parent, this.index);
+  _Path.root()
+      : parent = null,
+        index = '';
+  _Path append(Object? index) => _Path._(this, index);
+
+  @override
+  String toString() {
+    if (parent == null && index == '') return '';
+    final stack = Queue.of([this]);
+    var current = parent;
+    while (current?.parent != null) {
+      stack.addLast(current!);
+      current = current.parent;
+    }
+    final result = StringBuffer('at ');
+    while (stack.isNotEmpty) {
+      result.write('[');
+      result.write(literal(stack.removeLast().index).join(r'\n'));
+      result.write(']');
+    }
+    result.write(' ');
+    return result.toString();
+  }
+}
+
+class _Search {
+  final _Path path;
+  final Object? actual;
+  final Object? expected;
+  final int depth;
+  _Search(this.path, this.actual, this.expected, this.depth);
+}
+
+/// Returns the `which` for a Rejection if there is no pairing between the
+/// elements of [actual] and [expected] using [elementsEqual].
+///
+/// If there are unmatched expected elements - either actual was too short, or
+/// has mismatched elements - returns a rejection reason from calling
+/// [unmatchedExpected] with an expected value that could not be paired, it's
+/// index, and the count of unmatched elements.
+///
+/// Otherwise, if there are unmatched actual elements - actual was too long -
+/// returns a rejection reason from calling [unmatchedActual] with an actual
+/// value that could not be paired, it's index, and the count of unmatched
+/// elements.
+///
+/// Runtime is at least `O(|actual||expected|)`, and for collections with many
+/// elements which compare as equal the runtime can reach
+/// `O((|actual| + |expected|)^2.5)`.
+Iterable<String>? unorderedCompare<T, E>(
+    Iterable<T> actual,
+    Iterable<E> expected,
+    bool Function(T, E) elementsEqual,
+    Iterable<String> Function(E, int index, int count) unmatchedExpected,
+    Iterable<String> Function(T, int index, int count) unmatchedActual) {
+  final indexedExpected = expected.toList();
+  final indexedActual = actual.toList();
+  final adjacency = <List<int>>[];
+  for (var i = 0; i < indexedExpected.length; i++) {
+    final expectedElement = indexedExpected[i];
+    final pairs = [
+      for (var j = 0; j < indexedActual.length; j++)
+        if (elementsEqual(indexedActual[j], expectedElement)) j
+    ];
+    adjacency.add(pairs);
+  }
+  final unpaired = _findUnpaired(adjacency, indexedActual.length);
+  if (unpaired.first.isNotEmpty) {
+    final firstUnmatched = indexedExpected[unpaired.first.first];
+    return unmatchedExpected(
+        firstUnmatched, unpaired.first.first, unpaired.first.length);
+  }
+  if (unpaired.last.isNotEmpty) {
+    final firstUnmatched = indexedActual[unpaired.last.first];
+    return unmatchedActual(
+        firstUnmatched, unpaired.last.first, unpaired.last.length);
+  }
+  return null;
+}
+
+/// Returns the indices which are unmatched in an optimal pairing in the
+/// bipartite graph represented by [adjacency].
+///
+/// Vertices are represented as integers. The two sets of vertices (`U` and `V`)
+/// in the biparte graph are represented as:
+/// - `U` - the indices of [adjacency].
+/// - `V` - values smaller than [rightVertexCount].
+///
+/// An edge from `U[n]` to `V[m]` is represented by the value `m` being present
+/// in the list at index `n`.
+/// The largest value within any list in [adjacency] must be smaller than
+/// [rightVertexCount].
+///
+/// Returns a List with two values, the unpaired values of `U` and `V` in the
+/// maximum-caridnality matching betweeen them.
+///
+/// If there is a perfect pairing, the returned lists will both be empty.
+///
+/// Uses the Hopcroft–Karp algorithm based on pseudocode from
+/// https://en.wikipedia.org/wiki/Hopcroft%E2%80%93Karp_algorithm
+List<List<int>> _findUnpaired(List<List<int>> adjacency, int rightVertexCount) {
+  final leftLength = adjacency.length;
+  final rightLength = rightVertexCount;
+  // The last index represents a "dummy vertex"
+  final distances = List<num>.filled(leftLength + 1, double.infinity);
+  // Initially everything is paired with the "dummy vertex" of the opposite set
+  final leftPairs = List.filled(leftLength, rightLength);
+  final rightPairs = List.filled(rightLength, leftLength);
+
+  bool bfs() {
+    final queue = Queue<int>();
+    for (var leftIndex = 0; leftIndex < leftLength; leftIndex++) {
+      if (leftPairs[leftIndex] == rightLength) {
+        distances[leftIndex] = 0;
+        queue.add(leftIndex);
+      } else {
+        distances[leftIndex] = double.infinity;
+      }
+    }
+    distances.last = double.infinity;
+    while (queue.isNotEmpty) {
+      final current = queue.removeFirst();
+      if (distances[current] < distances[leftLength]) {
+        for (final rightIndex in adjacency[current]) {
+          if (distances[rightPairs[rightIndex]].isInfinite) {
+            distances[rightPairs[rightIndex]] = distances[current] + 1;
+            queue.addLast(rightPairs[rightIndex]);
+          }
+        }
+      }
+    }
+    return !distances.last.isInfinite;
+  }
+
+  bool dfs(int leftIndex) {
+    if (leftIndex == leftLength) return true;
+    for (final rightIndex in adjacency[leftIndex]) {
+      if (distances[rightPairs[rightIndex]] == distances[leftIndex] + 1) {
+        if (dfs(rightPairs[rightIndex])) {
+          leftPairs[leftIndex] = rightIndex;
+          rightPairs[rightIndex] = leftIndex;
+          return true;
+        }
+      }
+    }
+    distances[leftIndex] = double.infinity;
+    return false;
+  }
+
+  while (bfs()) {
+    for (var leftIndex = 0; leftIndex < leftLength; leftIndex++) {
+      if (leftPairs[leftIndex] == rightLength) {
+        dfs(leftIndex);
+      }
+    }
+  }
+  return [
+    [
+      for (int i = 0; i < leftLength; i++)
+        if (leftPairs[i] == rightLength) i
+    ],
+    [
+      for (int i = 0; i < rightLength; i++)
+        if (rightPairs[i] == leftLength) i
+    ]
+  ];
+}
diff --git a/pkgs/checks/lib/src/describe.dart b/pkgs/checks/lib/src/describe.dart
new file mode 100644
index 0000000..f05a164
--- /dev/null
+++ b/pkgs/checks/lib/src/describe.dart
@@ -0,0 +1,163 @@
+// Copyright (c) 2022, 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';
+
+import 'checks.dart' show Condition, describe;
+
+/// Returns a pretty-printed representation of [object].
+///
+/// When possible, lines will be kept under [_maxLineLength]. This isn't
+/// guaranteed, since individual objects may have string representations that
+/// are too long, but most lines will be less than [_maxLineLength] long.
+///
+/// [Iterable]s and [Map]s will only print their first [_maxItems] elements or
+/// key/value pairs, respectively.
+Iterable<String> literal(Object? object) => _prettyPrint(object, 0, {}, true);
+
+const _maxLineLength = 80;
+const _maxItems = 25;
+
+Iterable<String> _prettyPrint(
+    Object? object, int indentSize, Set<Object?> seen, bool isTopLevel) {
+  if (seen.contains(object)) return ['(recursive)'];
+  seen = seen.union({object});
+  Iterable<String> prettyPrintNested(Object? child) =>
+      _prettyPrint(child, indentSize + 2, seen, false);
+
+  if (object is Iterable) {
+    String open, close;
+    if (object is List) {
+      open = '[';
+      close = ']';
+    } else if (object is Set) {
+      open = '{';
+      close = '}';
+    } else {
+      open = '(';
+      close = ')';
+    }
+    final elements = object.map(prettyPrintNested).toList();
+    return _prettyPrintCollection(
+        open, close, elements, _maxLineLength - indentSize);
+  } else if (object is Map) {
+    final entries = object.entries.map((entry) {
+      final key = prettyPrintNested(entry.key);
+      final value = prettyPrintNested(entry.value);
+      return [
+        ...key.take(key.length - 1),
+        '${key.last}: ${value.first}',
+        ...value.skip(1)
+      ];
+    }).toList();
+    return _prettyPrintCollection(
+        '{', '}', entries, _maxLineLength - indentSize);
+  } else if (object is String) {
+    if (object.isEmpty) return ["''"];
+    final escaped = const LineSplitter()
+        .convert(object)
+        .map(escape)
+        .map((line) => line.replaceAll("'", r"\'"))
+        .toList();
+    return prefixFirst("'", postfixLast("'", escaped));
+  } else if (object is Condition<Never>) {
+    return ['<A value that:', ...postfixLast('>', describe(object))];
+  } else {
+    final value = const LineSplitter().convert(object.toString());
+    return isTopLevel ? prefixFirst('<', postfixLast('>', value)) : value;
+  }
+}
+
+Iterable<String> _prettyPrintCollection(
+    String open, String close, List<Iterable<String>> elements, int maxLength) {
+  if (elements.length > _maxItems) {
+    elements.replaceRange(_maxItems - 1, elements.length, [
+      ['...']
+    ]);
+  }
+  if (elements.every((e) => e.length == 1)) {
+    final singleLine = '$open${elements.map((e) => e.single).join(', ')}$close';
+    if (singleLine.length <= maxLength) {
+      return [singleLine];
+    }
+  }
+  if (elements.length == 1) {
+    return prefixFirst(open, postfixLast(close, elements.single));
+  }
+  return [
+    ...prefixFirst(open, postfixLast(',', elements.first)),
+    for (var element in elements.skip(1).take(elements.length - 2))
+      ...postfixLast(',', element),
+    ...postfixLast(close, elements.last),
+  ];
+}
+
+Iterable<String> indent(Iterable<String> lines, [int depth = 1]) {
+  final indent = '  ' * depth;
+  return lines.map((line) => '$indent$line');
+}
+
+/// Prepends [prefix] to the first line of [lines].
+///
+/// If [lines] is empty, the result will be as well. The prefix will not be
+/// returned for an empty input.
+Iterable<String> prefixFirst(String prefix, Iterable<String> lines) sync* {
+  var isFirst = true;
+  for (var line in lines) {
+    if (isFirst) {
+      yield '$prefix$line';
+      isFirst = false;
+    } else {
+      yield line;
+    }
+  }
+}
+
+/// Append [postfix] to the last line of [lines].
+///
+/// If [lines] is empty, the result will be as well. The postfix will not be
+/// returned for an empty input.
+Iterable<String> postfixLast(String postfix, Iterable<String> lines) sync* {
+  var iterator = lines.iterator;
+  var hasNext = iterator.moveNext();
+  while (hasNext) {
+    final line = iterator.current;
+    hasNext = iterator.moveNext();
+    yield hasNext ? line : '$line$postfix';
+  }
+}
+
+/// Returns [output] with all whitespace characters represented as their escape
+/// sequences.
+///
+/// Backslash characters are escaped as `\\`
+String escape(String output) {
+  output = output.replaceAll('\\', r'\\');
+  return output.replaceAllMapped(_escapeRegExp, (match) {
+    var mapped = _escapeMap[match[0]];
+    if (mapped != null) return mapped;
+    return _hexLiteral(match[0]!);
+  });
+}
+
+/// A [RegExp] that matches whitespace characters that should be escaped.
+final _escapeRegExp = RegExp(
+    '[\\x00-\\x07\\x0E-\\x1F${_escapeMap.keys.map(_hexLiteral).join()}]');
+
+/// A [Map] between whitespace characters and their escape sequences.
+const _escapeMap = {
+  '\n': r'\n',
+  '\r': r'\r',
+  '\f': r'\f',
+  '\b': r'\b',
+  '\t': r'\t',
+  '\v': r'\v',
+  '\x7F': r'\x7F', // delete
+};
+
+/// Given single-character string, return the hex-escaped equivalent.
+String _hexLiteral(String input) {
+  var rune = input.runes.single;
+  return r'\x' + rune.toRadixString(16).toUpperCase().padLeft(2, '0');
+}
diff --git a/pkgs/checks/lib/src/extensions/async.dart b/pkgs/checks/lib/src/extensions/async.dart
new file mode 100644
index 0000000..9d9e85a
--- /dev/null
+++ b/pkgs/checks/lib/src/extensions/async.dart
@@ -0,0 +1,476 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:async/async.dart';
+
+import '../../context.dart';
+
+extension FutureChecks<T> on Subject<Future<T>> {
+  /// Expects that the `Future` completes to a value without throwing.
+  ///
+  /// Fails if the future completes as an error.
+  ///
+  /// Pass [completionCondition] to check expectations on the completion result.
+  ///
+  /// The returned future will complete when the subject future has completed,
+  /// and [completionCondition] has optionally been checked.
+  Future<void> completes([AsyncCondition<T>? completionCondition]) async {
+    await context.nestAsync<T>(() => ['completes to a value'], (actual) async {
+      try {
+        return Extracted.value(await actual);
+      } catch (e, st) {
+        return Extracted.rejection(actual: [
+          'a future that completes as an error'
+        ], which: [
+          ...prefixFirst('threw ', postfixLast(' at:', literal(e))),
+          ...const LineSplitter().convert(st.toString())
+        ]);
+      }
+    }, completionCondition);
+  }
+
+  /// Expects that the `Future` never completes as a value or an error.
+  ///
+  /// Immediately returns and does not cause the test to remain running if it
+  /// ends.
+  /// If the future completes at any time, raises a test failure. This may
+  /// happen after the test has already appeared to succeed.
+  ///
+  /// Not compatible with [softCheck] or [softCheckAsync] since there is no
+  /// concrete end point where this condition has definitely succeeded.
+  void doesNotComplete() {
+    context.expectUnawaited(() => ['does not complete'], (actual, reject) {
+      unawaited(actual.then((r) {
+        reject(Rejection(
+            actual: prefixFirst('a future that completed to ', literal(r))));
+      }, onError: (Object e, StackTrace st) {
+        reject(Rejection(actual: [
+          'a future that completed as an error:'
+        ], which: [
+          ...prefixFirst('threw ', literal(e)),
+          ...const LineSplitter().convert(st.toString())
+        ]));
+      }));
+    });
+  }
+
+  /// Expects that the `Future` completes as an error.
+  ///
+  /// Fails if the future completes to a value.
+  ///
+  /// Pass [errorCondition] to check expectations on the error thrown by the
+  /// future.
+  ///
+  /// The returned future will complete when the subject future has completed,
+  /// and [errorCondition] has optionally been checked.
+  Future<void> throws<E extends Object>(
+      [AsyncCondition<E>? errorCondition]) async {
+    await context.nestAsync<E>(
+        () => ['completes to an error${E == Object ? '' : ' of type $E'}'],
+        (actual) async {
+      try {
+        return Extracted.rejection(
+            actual: prefixFirst('completed to ', literal(await actual)),
+            which: ['did not throw']);
+      } on E catch (e) {
+        return Extracted.value(e);
+      } catch (e, st) {
+        return Extracted.rejection(
+            actual: prefixFirst('completed to error ', literal(e)),
+            which: [
+              'threw an exception that is not a $E at:',
+              ...const LineSplitter().convert(st.toString())
+            ]);
+      }
+    }, errorCondition);
+  }
+}
+
+/// Expectations on a [StreamQueue].
+///
+/// Streams should be wrapped in user test code so that any reuse of the same
+/// Stream, and the full stream lifecycle, is explicit.
+extension StreamChecks<T> on Subject<StreamQueue<T>> {
+  /// Calls [Context.expectAsync] and wraps [predicate] with a transaction.
+  ///
+  /// The transaction is committed if the check passes, or rejected if it fails.
+  Future<void> _expectAsync(Iterable<String> Function() clause,
+          FutureOr<Rejection?> Function(StreamQueue<T>) predicate) =>
+      context.expectAsync(clause, (actual) async {
+        final transaction = actual.startTransaction();
+        final copy = transaction.newQueue();
+        final result = await predicate(copy);
+        if (result == null) {
+          transaction.commit(copy);
+        } else {
+          transaction.reject();
+        }
+        return result;
+      });
+
+  /// Expect that the `Stream` emits a value without first emitting an error.
+  ///
+  /// Fails if the stream emits an error instead of a value, or closes without
+  /// emitting a value.
+  ///
+  /// If an error is emitted the queue will be left in its original state, the
+  /// error will not be consumed.
+  /// If an event is emitted, it will be consumed from the queue.
+  ///
+  /// Pass [emittedCondition] to check expectations on the value emitted by the
+  /// stream.
+  ///
+  /// The returned future will complete when the stream has emitted, errored, or
+  /// ended, and the [emittedCondition] has optionally been checked.
+  Future<void> emits([AsyncCondition<T>? emittedCondition]) async {
+    await context.nestAsync<T>(() => ['emits a value'], (actual) async {
+      if (!await actual.hasNext) {
+        return Extracted.rejection(
+            actual: ['a stream'],
+            which: ['closed without emitting enough values']);
+      }
+      try {
+        await actual.peek;
+        return Extracted.value(await actual.next);
+      } catch (e, st) {
+        return Extracted.rejection(
+            actual: prefixFirst('a stream with error ', literal(e)),
+            which: [
+              'emitted an error instead of a value at:',
+              ...const LineSplitter().convert(st.toString())
+            ]);
+      }
+    }, emittedCondition);
+  }
+
+  /// Expects that the stream emits an error of type [E].
+  ///
+  /// Fails if the stream emits any value.
+  /// Fails if the stream emits an error with an incorrect type.
+  /// Fails if the stream closes without emitting an error.
+  ///
+  /// If an event is emitted the queue will be left in its original state, the
+  /// event will not be consumed.
+  /// If an error is emitted, it will be consumed from the queue.
+  ///
+  /// Pass [errorCondition] to check expectations on the error emitted by the
+  /// stream.
+  ///
+  /// The returned future will complete when the stream has emitted, errored, or
+  /// ended, and the [errorCondition] has optionally been checked.
+  Future<void> emitsError<E extends Object>(
+      [AsyncCondition<E>? errorCondition]) async {
+    await context.nestAsync<E>(
+        () => ['emits an error${E == Object ? '' : ' of type $E'}'],
+        (actual) async {
+      if (!await actual.hasNext) {
+        return Extracted.rejection(
+            actual: ['a stream'],
+            which: ['closed without emitting an expected error']);
+      }
+      try {
+        final value = await actual.peek;
+        return Extracted.rejection(
+            actual: prefixFirst('a stream emitting value ', literal(value)),
+            which: ['closed without emitting an error']);
+      } on E catch (e) {
+        await actual.next.then<void>((_) {}, onError: (_) {});
+        return Extracted.value(e);
+      } catch (e, st) {
+        return Extracted.rejection(
+            actual: prefixFirst('a stream with error ', literal(e)),
+            which: [
+              'emitted an error which is not $E at:',
+              ...const LineSplitter().convert(st.toString())
+            ]);
+      }
+    }, errorCondition);
+  }
+
+  /// Expects that the `Stream` emits any number of events before emitting an
+  /// event that satisfies [condition].
+  ///
+  /// Returns a `Future` that completes after the stream has emitted an event
+  /// that satisfies [condition].
+  ///
+  /// Fails if the stream emits an error or closes before emitting a matching
+  /// event.
+  ///
+  /// If this expectation fails, the source queue will be left in its original
+  /// state.
+  /// If this expectation succeeds, consumes the matching event and all prior
+  /// events.
+  Future<void> emitsThrough(AsyncCondition<T> condition) async {
+    await _expectAsync(
+        () => [
+              'emits any values then emits a value that:',
+              ...describe(condition)
+            ], (actual) async {
+      var count = 0;
+      while (await actual.hasNext) {
+        if (softCheck(await actual.next, condition) == null) {
+          return null;
+        }
+        count++;
+      }
+      return Rejection(
+          actual: ['a stream'],
+          which: ['ended after emitting $count elements with none matching']);
+    });
+  }
+
+  /// Expects that the stream satisfies each condition in [conditions] serially.
+  ///
+  /// Waits for each condition to be satisfied or rejected before checking the
+  /// next. Subsequent conditions will not see any events consumed by earlier
+  /// conditions.
+  ///
+  /// ```dart
+  /// await check(someStream).withQueue.inOrder([
+  ///   (s) => s.emits((e) => e.equals(0)),
+  ///   (s) => s.emits((e) => e.equals(1)),
+  /// ]);
+  /// ```
+  ///
+  /// If this expectation fails, the source queue will be left in its original
+  /// state.
+  /// If this expectation succeeds, consumes as many events from the source
+  /// stream as are consumed by all the conditions.
+  Future<void> inOrder(
+      Iterable<AsyncCondition<StreamQueue<T>>> conditions) async {
+    conditions = conditions.toList();
+    final descriptions = <String>[];
+    await _expectAsync(
+        () => descriptions.isEmpty
+            ? ['satisfies ${conditions.length} conditions in order']
+            : descriptions, (actual) async {
+      var satisfiedCount = 0;
+      for (var condition in conditions) {
+        descriptions.addAll(await describeAsync(condition));
+        final failure = await softCheckAsync(actual, condition);
+        if (failure != null) {
+          final which = failure.rejection.which;
+          return Rejection(actual: [
+            'a stream'
+          ], which: [
+            if (satisfiedCount > 0) 'satisfied $satisfiedCount conditions then',
+            'failed to satisfy the condition at index $satisfiedCount',
+            if (failure.detail.depth > 0) ...[
+              'because it:',
+              ...indent(
+                  failure.detail.actual.skip(1), failure.detail.depth - 1),
+              ...indent(prefixFirst('Actual: ', failure.rejection.actual),
+                  failure.detail.depth),
+              if (which != null)
+                ...indent(prefixFirst('Which: ', which), failure.detail.depth),
+            ] else ...[
+              if (which != null) ...prefixFirst('because it ', which),
+            ],
+          ]);
+        }
+        satisfiedCount++;
+      }
+      return null;
+    });
+  }
+
+  /// Expects that the stream statisfies at least one condition from
+  /// [conditions].
+  ///
+  /// If this expectation fails, the source queue will be left in its original
+  /// state.
+  /// If this expectation succeeds, consumes the same events from the source
+  /// queue as the satisfied condition. If multiple conditions are satisfied,
+  /// chooses the condition which consumed the most events.
+  Future<void> anyOf(
+      Iterable<AsyncCondition<StreamQueue<T>>> conditions) async {
+    conditions = conditions.toList();
+    if (conditions.isEmpty) {
+      throw ArgumentError('conditions may not be empty');
+    }
+    final descriptions = <Iterable<String>>[];
+    await context.expectAsync(
+        () => descriptions.isEmpty
+            ? ['satisfies any of ${conditions.length} conditions']
+            : [
+                'satisfies one of:',
+                for (var i = 0; i < descriptions.length; i++) ...[
+                  ...descriptions[i],
+                  if (i < descriptions.length - 1) 'or,'
+                ]
+              ], (actual) async {
+      final transaction = actual.startTransaction();
+      StreamQueue<T>? longestAccepted;
+      final descriptionFuture = Future.wait(conditions.map(describeAsync));
+      final failures = await Future.wait(conditions.map((condition) async {
+        final copy = transaction.newQueue();
+        final failure = await softCheckAsync(copy, condition);
+        if (failure == null &&
+            (longestAccepted == null ||
+                copy.eventsDispatched > longestAccepted!.eventsDispatched)) {
+          longestAccepted = copy;
+        }
+        return failure;
+      }));
+      descriptions.addAll(await descriptionFuture);
+      if (longestAccepted != null) {
+        transaction.commit(longestAccepted!);
+        return null;
+      }
+      transaction.reject();
+      Iterable<String> failureDetails(int index, CheckFailure? failure) {
+        final actual = failure!.rejection.actual;
+        final which = failure.rejection.which;
+        final detail = failure.detail;
+        final failed = 'failed the condition at index $index';
+        if (detail.depth > 0) {
+          return [
+            '$failed because it:',
+            ...indent(detail.actual.skip(1), detail.depth - 1),
+            ...indent(prefixFirst('Actual: ', actual), detail.depth),
+            if (which != null)
+              ...indent(prefixFirst('Which: ', which), detail.depth),
+          ];
+        } else {
+          return [
+            if (which == null)
+              failed
+            else ...[
+              '$failed because it:',
+              ...indent(which),
+            ],
+          ];
+        }
+      }
+
+      return Rejection(actual: [
+        'a stream'
+      ], which: [
+        'failed to satisfy any condition',
+        for (var i = 0; i < failures.length; i++)
+          ...failureDetails(i, failures[i]),
+      ]);
+    });
+  }
+
+  /// Expects that the stream closes without emitting any event that satisfies
+  /// [condition].
+  ///
+  /// Returns a `Future` that completes after the stream has closed.
+  ///
+  /// Fails if the stream emits any even that satisfies [condition].
+  ///
+  /// If this expectation fails, the source queue will be left in its original
+  /// state.
+  /// If this expectation succeeds, consumes all the events that did not satisfy
+  /// [condition] until the end of the stream.
+  Future<void> neverEmits(AsyncCondition<T> condition) async {
+    await _expectAsync(
+        () => ['never emits a value that:', ...describe(condition)],
+        (actual) async {
+      var count = 0;
+      await for (var emitted in actual.rest) {
+        if (softCheck(emitted, condition) == null) {
+          return Rejection(actual: [
+            'a stream'
+          ], which: [
+            ...prefixFirst('emitted ', literal(emitted)),
+            if (count > 0) 'following $count other items'
+          ]);
+        }
+        count++;
+      }
+      return null;
+    });
+  }
+
+  /// Optionally consumes an event that matches [condition] from the stream.
+  ///
+  /// This expectation never fails.
+  ///
+  /// If a non-matching event is emitted, no events are consumed.
+  /// If a matching event is emitted, that event is consumed.
+  Future<void> mayEmit(AsyncCondition<T> condition) async {
+    await context
+        .expectAsync(() => ['may emit a value that:', ...describe(condition)],
+            (actual) async {
+      if (!await actual.hasNext) return null;
+      try {
+        final value = await actual.peek;
+        if (softCheck(value, condition) == null) {
+          await actual.next;
+        }
+      } catch (_) {
+        // Ignore an emitted error - it does not match he event.
+      }
+      return null;
+    });
+  }
+
+  /// Optionally consumes events that match [condition] from the stream.
+  ///
+  /// This expectation never fails.
+  ///
+  /// Consumes matching events until one of the following happens:
+  /// - A non-matching event is emitted.
+  /// - An error is emitted.
+  /// - The stream closes.
+  Future<void> mayEmitMultiple(AsyncCondition<T> condition) async {
+    await context
+        .expectAsync(() => ['may emit a value that:', ...describe(condition)],
+            (actual) async {
+      while (await actual.hasNext) {
+        try {
+          final value = await actual.peek;
+          if (softCheck(value, condition) == null) {
+            await actual.next;
+          } else {
+            return null;
+          }
+        } catch (_) {
+          return null;
+        }
+      }
+      return null;
+    });
+  }
+
+  /// Expects that the stream closes without emitting any events or errors.
+  ///
+  /// If this expectation fails, the source queue will be left in its original
+  /// state, the event or error that caused it to fail will not be consumed.
+  Future<void> isDone() async {
+    await _expectAsync(() => ['is done'], (actual) async {
+      if (!await actual.hasNext) return null;
+      try {
+        return Rejection(
+            actual: ['a stream'],
+            which: prefixFirst(
+                'emitted an unexpected value: ', literal(await actual.next)));
+      } catch (e, st) {
+        return Rejection(actual: [
+          'a stream'
+        ], which: [
+          ...prefixFirst('emitted an unexpected error: ', literal(e)),
+          ...const LineSplitter().convert(st.toString())
+        ]);
+      }
+    });
+  }
+}
+
+extension WithQueueExtension<T> on Subject<Stream<T>> {
+  /// Wrap the stream in a [StreamQueue] to allow using checks from
+  /// [StreamChecks].
+  ///
+  /// Stream expectations operate on a queue, instead of directly on the stream,
+  /// so that they can support conditional expectations and check multiple
+  /// possibilities from the same point in the stream.
+  Subject<StreamQueue<T>> get withQueue =>
+      context.nest(() => [], (actual) => Extracted.value(StreamQueue(actual)),
+          atSameLevel: true);
+}
diff --git a/pkgs/checks/lib/src/extensions/core.dart b/pkgs/checks/lib/src/extensions/core.dart
new file mode 100644
index 0000000..957e175
--- /dev/null
+++ b/pkgs/checks/lib/src/extensions/core.dart
@@ -0,0 +1,184 @@
+// Copyright (c) 2022, 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';
+
+import 'package:meta/meta.dart' as meta;
+
+import '../../context.dart';
+
+extension CoreChecks<T> on Subject<T> {
+  /// Extracts a property of the value for further expectations.
+  ///
+  /// Sets up a clause that the value "has [name] that:" followed by any
+  /// expectations applied to the returned [Subject].
+  @meta.useResult
+  Subject<R> has<R>(R Function(T) extract, String name) {
+    return context.nest(() => ['has $name'], (value) {
+      try {
+        return Extracted.value(extract(value));
+      } catch (e, st) {
+        return Extracted.rejection(which: [
+          ...prefixFirst('threw while trying to read $name: ', literal(e)),
+          ...const LineSplitter().convert(st.toString())
+        ]);
+      }
+    });
+  }
+
+  /// Applies the expectations invoked in [condition] to this subject.
+  ///
+  /// Use this method when it would otherwise not be possible to check multiple
+  /// expectations for this subject due to cascade notation already being used
+  /// in a way that would conflict.
+  ///
+  /// ```
+  /// check(something)
+  ///   ..has((s) => s.foo, 'foo').equals(expectedFoo)
+  ///   ..has((s) => s.bar, 'bar').which((b) => b
+  ///     ..isLessThan(10)
+  ///     ..isGreaterThan(0));
+  /// ```
+  void which(Condition<T> condition) => condition(this);
+
+  /// Check that the expectations invoked in [condition] are not satisfied by
+  /// this value.
+  ///
+  /// Asynchronous expectations are not allowed in [condition].
+  void not(Condition<T> condition) {
+    context.expect(
+      () => ['is not a value that:', ...indent(describe(condition))],
+      (actual) {
+        if (softCheck(actual, condition) != null) return null;
+        return Rejection(
+          which: ['is a value that: ', ...indent(describe(condition))],
+        );
+      },
+    );
+  }
+
+  /// Expects that the value satisfies the expectations invoked in at least one
+  /// condition from [conditions].
+  ///
+  /// Asynchronous expectations are not allowed in [conditions].
+  void anyOf(Iterable<Condition<T>> conditions) {
+    context.expect(
+        () => prefixFirst('matches any condition in ', literal(conditions)),
+        (actual) {
+      for (final condition in conditions) {
+        if (softCheck(actual, condition) == null) return null;
+      }
+      return Rejection(which: ['did not match any condition']);
+    });
+  }
+
+  /// Expects that the value is assignable to type [T].
+  ///
+  /// If the value is a [T], returns a [Subject] for further expectations.
+  Subject<R> isA<R>() {
+    return context.nest<R>(() => ['is a $R'], (actual) {
+      if (actual is! R) {
+        return Extracted.rejection(which: ['Is a ${actual.runtimeType}']);
+      }
+      return Extracted.value(actual);
+    }, atSameLevel: true);
+  }
+
+  /// Expects that the value is equal to [other] according to [operator ==].
+  void equals(T other) {
+    context.expect(() => prefixFirst('equals ', literal(other)), (actual) {
+      if (actual == other) return null;
+      return Rejection(which: ['are not equal']);
+    });
+  }
+
+  /// Expects that the value is [identical] to [other].
+  void identicalTo(T other) {
+    context.expect(() => prefixFirst('is identical to ', literal(other)),
+        (actual) {
+      if (identical(actual, other)) return null;
+      return Rejection(which: ['is not identical']);
+    });
+  }
+}
+
+extension BoolChecks on Subject<bool> {
+  void isTrue() {
+    context.expect(
+      () => ['is true'],
+      (actual) => actual
+          ? null // force coverage
+          : Rejection(),
+    );
+  }
+
+  void isFalse() {
+    context.expect(
+      () => ['is false'],
+      (actual) => !actual
+          ? null // force coverage
+          : Rejection(),
+    );
+  }
+}
+
+extension NullableChecks<T> on Subject<T?> {
+  Subject<T> isNotNull() {
+    return context.nest<T>(() => ['is not null'], (actual) {
+      if (actual == null) return Extracted.rejection();
+      return Extracted.value(actual);
+    }, atSameLevel: true);
+  }
+
+  void isNull() {
+    context.expect(() => const ['is null'], (actual) {
+      if (actual != null) return Rejection();
+      return null;
+    });
+  }
+}
+
+extension ComparableChecks<T> on Subject<Comparable<T>> {
+  /// Expects that this value is greater than [other].
+  void isGreaterThan(T other) {
+    context.expect(() => prefixFirst('is greater than ', literal(other)),
+        (actual) {
+      if (actual.compareTo(other) > 0) return null;
+      return Rejection(
+          which: prefixFirst('is not greater than ', literal(other)));
+    });
+  }
+
+  /// Expects that this value is greater than or equal to [other].
+  void isGreaterOrEqual(T other) {
+    context.expect(
+        () => prefixFirst('is greater than or equal to ', literal(other)),
+        (actual) {
+      if (actual.compareTo(other) >= 0) return null;
+      return Rejection(
+          which:
+              prefixFirst('is not greater than or equal to ', literal(other)));
+    });
+  }
+
+  /// Expects that this value is less than [other].
+  void isLessThan(T other) {
+    context.expect(() => prefixFirst('is less than ', literal(other)),
+        (actual) {
+      if (actual.compareTo(other) < 0) return null;
+      return Rejection(which: prefixFirst('is not less than ', literal(other)));
+    });
+  }
+
+  /// Expects that this value is less than or equal to [other].
+  void isLessOrEqual(T other) {
+    context
+        .expect(() => prefixFirst('is less than or equal to ', literal(other)),
+            (actual) {
+      if (actual.compareTo(other) <= 0) return null;
+      return Rejection(
+          which: prefixFirst('is not less than or equal to ', literal(other)));
+    });
+  }
+}
diff --git a/pkgs/checks/lib/src/extensions/function.dart b/pkgs/checks/lib/src/extensions/function.dart
new file mode 100644
index 0000000..300941c
--- /dev/null
+++ b/pkgs/checks/lib/src/extensions/function.dart
@@ -0,0 +1,56 @@
+// Copyright (c) 2022, 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 '../../context.dart';
+
+extension FunctionChecks<T> on Subject<T Function()> {
+  /// Expects that a function throws synchronously when it is called.
+  ///
+  /// If the function synchronously throws a value of type [E], return a
+  /// [Subject] to check further expectations on the error.
+  ///
+  /// If the function does not throw synchronously, or if it throws an error
+  /// that is not of type [E], this expectation will fail.
+  ///
+  /// If this function is async and returns a [Future], this expectation will
+  /// fail. Instead invoke the function and check the expectation on the
+  /// returned [Future].
+  Subject<E> throws<E>() {
+    return context.nest<E>(() => ['throws an error of type $E'], (actual) {
+      try {
+        final result = actual();
+        return Extracted.rejection(
+          actual: prefixFirst('a function that returned ', literal(result)),
+          which: ['did not throw'],
+        );
+      } catch (e) {
+        if (e is E) return Extracted.value(e as E);
+        return Extracted.rejection(
+            actual: prefixFirst('a function that threw error ', literal(e)),
+            which: ['did not throw an $E']);
+      }
+    });
+  }
+
+  /// Expects that the function returns without throwing.
+  ///
+  /// If the function runs without exception, return a [Subject] to check
+  /// further expecations on the returned value.
+  ///
+  /// If the function throws synchronously, this expectation will fail.
+  Subject<T> returnsNormally() {
+    return context.nest<T>(() => ['returns a value'], (actual) {
+      try {
+        return Extracted.value(actual());
+      } catch (e, st) {
+        return Extracted.rejection(actual: [
+          'a function that throws'
+        ], which: [
+          ...prefixFirst('threw ', literal(e)),
+          ...st.toString().split('\n')
+        ]);
+      }
+    });
+  }
+}
diff --git a/pkgs/checks/lib/src/extensions/iterable.dart b/pkgs/checks/lib/src/extensions/iterable.dart
new file mode 100644
index 0000000..ecfa9c0
--- /dev/null
+++ b/pkgs/checks/lib/src/extensions/iterable.dart
@@ -0,0 +1,372 @@
+// Copyright (c) 2022, 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 '../../context.dart';
+
+import '../collection_equality.dart';
+import 'core.dart';
+
+extension IterableChecks<T> on Subject<Iterable<T>> {
+  Subject<int> get length => has((l) => l.length, 'length');
+
+  Subject<T> get first => context.nest(() => ['has first element'], (actual) {
+        final iterator = actual.iterator;
+        if (!iterator.moveNext()) {
+          return Extracted.rejection(which: ['has no elements']);
+        }
+        return Extracted.value(iterator.current);
+      });
+
+  Subject<T> get last => context.nest(() => ['has last element'], (actual) {
+        final iterator = actual.iterator;
+        if (!iterator.moveNext()) {
+          return Extracted.rejection(which: ['has no elements']);
+        }
+        var current = iterator.current;
+        while (iterator.moveNext()) {
+          current = iterator.current;
+        }
+        return Extracted.value(current);
+      });
+
+  Subject<T> get single => context.nest(() => ['has single element'], (actual) {
+        final iterator = actual.iterator;
+        if (!iterator.moveNext()) {
+          return Extracted.rejection(which: ['has no elements']);
+        }
+        final value = iterator.current;
+        if (iterator.moveNext()) {
+          return Extracted.rejection(which: ['has more than one element']);
+        }
+        return Extracted.value(value);
+      });
+
+  void isEmpty() {
+    context.expect(() => const ['is empty'], (actual) {
+      if (actual.isEmpty) return null;
+      return Rejection(which: ['is not empty']);
+    });
+  }
+
+  void isNotEmpty() {
+    context.expect(() => const ['is not empty'], (actual) {
+      if (actual.isNotEmpty) return null;
+      return Rejection(which: ['is empty']);
+    });
+  }
+
+  /// Expects that the iterable contains [element] according to
+  /// [Iterable.contains].
+  void contains(T element) {
+    context.expect(() {
+      return prefixFirst('contains ', literal(element));
+    }, (actual) {
+      if (actual.isEmpty) return Rejection(actual: ['an empty iterable']);
+      if (actual.contains(element)) return null;
+      return Rejection(
+          which: prefixFirst('does not contain ', literal(element)));
+    });
+  }
+
+  /// Expects that the iterable contains a value matching each expected value
+  /// from [elements] in the given order, with any extra elements between
+  /// them.
+  ///
+  /// For example, the following will succeed:
+  ///
+  /// ```dart
+  /// check([1, 0, 2, 0, 3]).containsInOrder([1, 2, 3]);
+  /// ```
+  ///
+  /// Values in [elements] may be a `T`, a `Condition<T>`, or a
+  /// `Condition<Object?>`. If an expectation is a condition callback it will be
+  /// checked against the actual values, and any other expectations, including
+  /// those that are not a `T` or a condition callback, will be compared with
+  /// the equality operator.
+  ///
+  /// ```dart
+  /// check([1, 0, 2, 0, 3])
+  ///   .containsInOrder([1, (Subject<int> v) => v.isGreaterThan(1), 3]);
+  /// ```
+  @Deprecated('Use `containsEqualInOrder` for expectations with values compared'
+      ' with `==` or `containsMatchingInOrder` for other expectations')
+  void containsInOrder(Iterable<Object?> elements) {
+    context.expect(() => prefixFirst('contains, in order: ', literal(elements)),
+        (actual) {
+      final expected = elements.toList();
+      if (expected.isEmpty) {
+        throw ArgumentError('expected may not be empty');
+      }
+      var expectedIndex = 0;
+      for (final element in actual) {
+        final currentExpected = expected[expectedIndex];
+        final matches = currentExpected is Condition<T>
+            ? softCheck(element, currentExpected) == null
+            : currentExpected is Condition<dynamic>
+                ? softCheck(element, currentExpected) == null
+                : currentExpected == element;
+        if (matches && ++expectedIndex >= expected.length) return null;
+      }
+      return Rejection(which: [
+        ...prefixFirst(
+            'did not have an element matching the expectation at index '
+            '$expectedIndex ',
+            literal(expected[expectedIndex])),
+      ]);
+    });
+  }
+
+  /// Expects that the iterable contains a value matching each condition in
+  /// [conditions] in the given order, with any extra elements between them.
+  ///
+  /// For example, the following will succeed:
+  ///
+  /// ```dart
+  /// check([1, 10, 2, 10, 3]).containsMatchingInOrder([
+  ///   (it) => it.isLessThan(2),
+  ///   (it) => it.isLessThan(3),
+  ///   (it) => it.isLessThan(4),
+  /// ]);
+  /// ```
+  void containsMatchingInOrder(Iterable<Condition<T>> conditions) {
+    context
+        .expect(() => prefixFirst('contains, in order: ', literal(conditions)),
+            (actual) {
+      final expected = conditions.toList();
+      if (expected.isEmpty) {
+        throw ArgumentError('expected may not be empty');
+      }
+      var expectedIndex = 0;
+      for (final element in actual) {
+        final currentExpected = expected[expectedIndex];
+        final matches = softCheck(element, currentExpected) == null;
+        if (matches && ++expectedIndex >= expected.length) return null;
+      }
+      return Rejection(which: [
+        ...prefixFirst(
+            'did not have an element matching the expectation at index '
+            '$expectedIndex ',
+            literal(expected[expectedIndex])),
+      ]);
+    });
+  }
+
+  /// Expects that the iterable contains a value equals to each expected value
+  /// from [elements] in the given order, with any extra elements between
+  /// them.
+  ///
+  /// For example, the following will succeed:
+  ///
+  /// ```dart
+  /// check([1, 0, 2, 0, 3]).containsInOrder([1, 2, 3]);
+  /// ```
+  ///
+  /// Values, will be compared with the equality operator.
+  void containsEqualInOrder(Iterable<T> elements) {
+    context.expect(() => prefixFirst('contains, in order: ', literal(elements)),
+        (actual) {
+      final expected = elements.toList();
+      if (expected.isEmpty) {
+        throw ArgumentError('expected may not be empty');
+      }
+      var expectedIndex = 0;
+      for (final element in actual) {
+        final currentExpected = expected[expectedIndex];
+        final matches = currentExpected == element;
+        if (matches && ++expectedIndex >= expected.length) return null;
+      }
+      return Rejection(which: [
+        ...prefixFirst(
+            'did not have an element equal to the expectation at index '
+            '$expectedIndex ',
+            literal(expected[expectedIndex])),
+      ]);
+    });
+  }
+
+  /// Expects that the iterable contains at least on element such that
+  /// [elementCondition] is satisfied.
+  void any(Condition<T> elementCondition) {
+    context.expect(() {
+      final conditionDescription = describe(elementCondition);
+      assert(conditionDescription.isNotEmpty);
+      return [
+        'contains a value that:',
+        ...conditionDescription,
+      ];
+    }, (actual) {
+      if (actual.isEmpty) return Rejection(actual: ['an empty iterable']);
+      for (var e in actual) {
+        if (softCheck(e, elementCondition) == null) return null;
+      }
+      return Rejection(which: ['Contains no matching element']);
+    });
+  }
+
+  /// Expects there are no elements in the iterable which fail to satisfy
+  /// [elementCondition].
+  ///
+  /// Empty iterables will pass always pass this expectation.
+  void every(Condition<T> elementCondition) {
+    context.expect(() {
+      final conditionDescription = describe(elementCondition);
+      assert(conditionDescription.isNotEmpty);
+      return [
+        'only has values that:',
+        ...conditionDescription,
+      ];
+    }, (actual) {
+      final iterator = actual.iterator;
+      for (var i = 0; iterator.moveNext(); i++) {
+        final element = iterator.current;
+        final failure = softCheck(element, elementCondition);
+        if (failure == null) continue;
+        final which = failure.rejection.which;
+        return Rejection(which: [
+          'has an element at index $i that:',
+          ...indent(failure.detail.actual.skip(1)),
+          ...indent(prefixFirst('Actual: ', failure.rejection.actual),
+              failure.detail.depth + 1),
+          if (which != null && which.isNotEmpty)
+            ...indent(prefixFirst('Which: ', which), failure.detail.depth + 1),
+        ]);
+      }
+      return null;
+    });
+  }
+
+  /// Expects that the iterable contains elements that are deeply equal to the
+  /// elements of [expected].
+  ///
+  /// {@macro deep_collection_equals}
+  void deepEquals(Iterable<Object?> expected) => context
+          .expect(() => prefixFirst('is deeply equal to ', literal(expected)),
+              (actual) {
+        final which = deepCollectionEquals(actual, expected);
+        if (which == null) return null;
+        return Rejection(which: which);
+      });
+
+  /// Expects that the iterable contains elements which equal those of
+  /// [expected] in any order.
+  ///
+  /// Should not be used for very large collections, runtime is O(n^2.5) in the
+  /// worst case where the iterables contain many equal elements, and O(n^2) in
+  /// more typical cases.
+  void unorderedEquals(Iterable<T> expected) {
+    context.expect(() => prefixFirst('unordered equals ', literal(expected)),
+        (actual) {
+      final which = unorderedCompare(
+        actual,
+        expected,
+        (actual, expected) => expected == actual,
+        (expected, index, count) => [
+          ...prefixFirst(
+              'has no element equal to the expected element at index '
+              '$index: ',
+              literal(expected)),
+          if (count > 1) 'or ${count - 1} other elements',
+        ],
+        (actual, index, count) => [
+          ...prefixFirst(
+              'has an unexpected element at index $index: ', literal(actual)),
+          if (count > 1) 'and ${count - 1} other unexpected elements',
+        ],
+      );
+      if (which == null) return null;
+      return Rejection(which: which);
+    });
+  }
+
+  /// Expects that the iterable contains elements which match all conditions of
+  /// [expected] in any order.
+  ///
+  /// Should not be used for very large collections, runtime is O(n^2.5) in the
+  /// worst case where conditions match many elements, and O(n^2) in more
+  /// typical cases.
+  void unorderedMatches(Iterable<Condition<T>> expected) {
+    context.expect(() => prefixFirst('unordered matches ', literal(expected)),
+        (actual) {
+      final which = unorderedCompare(
+        actual,
+        expected,
+        (actual, expected) => softCheck(actual, expected) == null,
+        (expected, index, count) => [
+          'has no element matching the condition at index $index:',
+          ...describe(expected),
+          if (count > 1) 'or ${count - 1} other conditions',
+        ],
+        (actual, index, count) => [
+          ...prefixFirst(
+              'has an unmatched element at index $index: ', literal(actual)),
+          if (count > 1) 'and ${count - 1} other unmatched elements',
+        ],
+      );
+      if (which == null) return null;
+      return Rejection(which: which);
+    });
+  }
+
+  /// Expects that the iterable contains elements that correspond by the
+  /// [elementCondition] exactly to each element in [expected].
+  ///
+  /// Fails if the iterable has a different length than [expected].
+  ///
+  /// For each element in the iterable, calls [elementCondition] with the
+  /// corresponding element from [expected] to get the specific condition for
+  /// that index.
+  ///
+  /// [description] is used in the Expected clause. It should be a predicate
+  /// without the object, for example with the description 'is less than' the
+  /// full expectation will be: "pairwise is less than $expected"
+  @Deprecated('Use `pairwiseMatches`')
+  void pairwiseComparesTo<S>(List<S> expected,
+          Condition<T> Function(S) elementCondition, String description) =>
+      pairwiseMatches(expected, elementCondition, description);
+
+  /// Expects that the iterable contains elements that correspond by the
+  /// [elementCondition] exactly to each element in [expected].
+  ///
+  /// Fails if the iterable has a different length than [expected].
+  ///
+  /// For each element in the iterable, calls [elementCondition] with the
+  /// corresponding element from [expected] to get the specific condition for
+  /// that index.
+  ///
+  /// [description] is used in the Expected clause. It should be a predicate
+  /// without the object, for example with the description 'is less than' the
+  /// full expectation will be: "pairwise is less than $expected"
+  void pairwiseMatches<S>(List<S> expected,
+      Condition<T> Function(S) elementCondition, String description) {
+    context.expect(() {
+      return prefixFirst('pairwise $description ', literal(expected));
+    }, (actual) {
+      final iterator = actual.iterator;
+      for (var i = 0; i < expected.length; i++) {
+        final expectedValue = expected[i];
+        if (!iterator.moveNext()) {
+          return Rejection(which: [
+            'has too few elements, there is no element to match at index $i'
+          ]);
+        }
+        final actualValue = iterator.current;
+        final failure = softCheck(actualValue, elementCondition(expectedValue));
+        if (failure == null) continue;
+        final innerDescription = describe<T>(elementCondition(expectedValue));
+        final which = failure.rejection.which;
+        return Rejection(which: [
+          'does not have an element at index $i that:',
+          ...innerDescription,
+          ...prefixFirst(
+              'Actual element at index $i: ', failure.rejection.actual),
+          if (which != null) ...prefixFirst('Which: ', which),
+        ]);
+      }
+      if (!iterator.moveNext()) return null;
+      return Rejection(which: [
+        'has too many elements, expected exactly ${expected.length}'
+      ]);
+    });
+  }
+}
diff --git a/pkgs/checks/lib/src/extensions/map.dart b/pkgs/checks/lib/src/extensions/map.dart
new file mode 100644
index 0000000..4fc0fdf
--- /dev/null
+++ b/pkgs/checks/lib/src/extensions/map.dart
@@ -0,0 +1,109 @@
+// Copyright (c) 2022, 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 '../../context.dart';
+
+import '../collection_equality.dart';
+import 'core.dart';
+
+extension MapChecks<K, V> on Subject<Map<K, V>> {
+  Subject<Iterable<MapEntry<K, V>>> get entries =>
+      has((m) => m.entries, 'entries');
+  Subject<Iterable<K>> get keys => has((m) => m.keys, 'keys');
+  Subject<Iterable<V>> get values => has((m) => m.values, 'values');
+  Subject<int> get length => has((m) => m.length, 'length');
+  Subject<V> operator [](K key) {
+    return context.nest(
+        () => prefixFirst('contains a value for ', literal(key)), (actual) {
+      if (!actual.containsKey(key)) {
+        return Extracted.rejection(
+            which: prefixFirst('does not contain the key ', literal(key)));
+      }
+      return Extracted.value(actual[key] as V);
+    });
+  }
+
+  void isEmpty() {
+    context.expect(() => const ['is empty'], (actual) {
+      if (actual.isEmpty) return null;
+      return Rejection(which: ['is not empty']);
+    });
+  }
+
+  void isNotEmpty() {
+    context.expect(() => const ['is not empty'], (actual) {
+      if (actual.isNotEmpty) return null;
+      return Rejection(which: ['is not empty']);
+    });
+  }
+
+  /// Expects that the map contains [key] according to [Map.containsKey].
+  void containsKey(K key) {
+    context.expect(() => prefixFirst('contains key ', literal(key)), (actual) {
+      if (actual.containsKey(key)) return null;
+      return Rejection(
+          which: prefixFirst('does not contain key ', literal(key)));
+    });
+  }
+
+  /// Expects that the map contains some key such that [keyCondition] is
+  /// satisfied.
+  void containsKeyThat(Condition<K> keyCondition) {
+    context.expect(() {
+      final conditionDescription = describe(keyCondition);
+      assert(conditionDescription.isNotEmpty);
+      return [
+        'contains a key that:',
+        ...conditionDescription,
+      ];
+    }, (actual) {
+      if (actual.isEmpty) return Rejection(actual: ['an empty map']);
+      for (var k in actual.keys) {
+        if (softCheck(k, keyCondition) == null) return null;
+      }
+      return Rejection(which: ['Contains no matching key']);
+    });
+  }
+
+  /// Expects that the map contains [value] according to [Map.containsValue].
+  void containsValue(V value) {
+    context.expect(() => prefixFirst('contains value ', literal(value)),
+        (actual) {
+      if (actual.containsValue(value)) return null;
+      return Rejection(
+          which: prefixFirst('does not contain value ', literal(value)));
+    });
+  }
+
+  /// Expects that the map contains some value such that [valueCondition] is
+  /// satisfied.
+  void containsValueThat(Condition<V> valueCondition) {
+    context.expect(() {
+      final conditionDescription = describe(valueCondition);
+      assert(conditionDescription.isNotEmpty);
+      return [
+        'contains a value that:',
+        ...conditionDescription,
+      ];
+    }, (actual) {
+      if (actual.isEmpty) return Rejection(actual: ['an empty map']);
+      for (var v in actual.values) {
+        if (softCheck(v, valueCondition) == null) return null;
+      }
+      return Rejection(which: ['Contains no matching value']);
+    });
+  }
+
+  /// Expects that the map contains entries that are deeply equal to the entries
+  /// of [expected].
+  ///
+  /// {@macro deep_collection_equals}
+  void deepEquals(Map<Object?, Object?> expected) => context
+          .expect(() => prefixFirst('is deeply equal to ', literal(expected)),
+              (actual) {
+        final which = deepCollectionEquals(actual, expected);
+        if (which == null) return null;
+        return Rejection(which: which);
+      });
+}
diff --git a/pkgs/checks/lib/src/extensions/math.dart b/pkgs/checks/lib/src/extensions/math.dart
new file mode 100644
index 0000000..302f1db
--- /dev/null
+++ b/pkgs/checks/lib/src/extensions/math.dart
@@ -0,0 +1,88 @@
+// Copyright (c) 2022, 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 '../../context.dart';
+
+extension NumChecks on Subject<num> {
+  /// Expects that [num.isNaN] is true.
+  void isNaN() {
+    context.expect(() => ['is not a number (NaN)'], (actual) {
+      if (actual.isNaN) return null;
+      return Rejection(which: ['is a number']);
+    });
+  }
+
+  /// Expects that [num.isNaN] is false.
+  void isNotNaN() {
+    context.expect(() => ['is a number (not NaN)'], (actual) {
+      if (!actual.isNaN) return null;
+      return Rejection(which: ['is not a number (NaN)']);
+    });
+  }
+
+  /// Expects that [num.isNegative] is true.
+  void isNegative() {
+    context.expect(() => ['is negative'], (actual) {
+      if (actual.isNegative) return null;
+      return Rejection(which: ['is not negative']);
+    });
+  }
+
+  /// Expects that [num.isNegative] is false.
+  void isNotNegative() {
+    context.expect(() => ['is not negative'], (actual) {
+      if (!actual.isNegative) return null;
+      return Rejection(which: ['is negative']);
+    });
+  }
+
+  /// Expects that [num.isFinite] is true.
+  void isFinite() {
+    context.expect(() => ['is finite'], (actual) {
+      if (actual.isFinite) return null;
+      return Rejection(which: ['is not finite']);
+    });
+  }
+
+  /// Expects that [num.isFinite] is false.
+  ///
+  /// Satisfied by [double.nan], [double.infinity] and
+  /// [double.negativeInfinity].
+  void isNotFinite() {
+    context.expect(() => ['is not finite'], (actual) {
+      if (!actual.isFinite) return null;
+      return Rejection(which: ['is finite']);
+    });
+  }
+
+  /// Expects that [num.isInfinite] is true.
+  ///
+  /// Satisfied by [double.infinity] and [double.negativeInfinity].
+  void isInfinite() {
+    context.expect(() => ['is infinite'], (actual) {
+      if (actual.isInfinite) return null;
+      return Rejection(which: ['is not infinite']);
+    });
+  }
+
+  /// Expects that [num.isInfinite] is false.
+  ///
+  /// Satisfied by [double.nan] and finite numbers.
+  void isNotInfinite() {
+    context.expect(() => ['is not infinite'], (actual) {
+      if (!actual.isInfinite) return null;
+      return Rejection(which: ['is infinite']);
+    });
+  }
+
+  /// Expects that the difference between this number and [other] is less than
+  /// or equal to [delta].
+  void isCloseTo(num other, num delta) {
+    context.expect(() => ['is within <$delta> of <$other>'], (actual) {
+      final difference = (other - actual).abs();
+      if (difference <= delta) return null;
+      return Rejection(which: ['differs by <$difference>']);
+    });
+  }
+}
diff --git a/pkgs/checks/lib/src/extensions/string.dart b/pkgs/checks/lib/src/extensions/string.dart
new file mode 100644
index 0000000..84b20f6
--- /dev/null
+++ b/pkgs/checks/lib/src/extensions/string.dart
@@ -0,0 +1,229 @@
+// Copyright (c) 2022, 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:math' as math;
+
+import '../../context.dart';
+
+import 'core.dart';
+
+extension StringChecks on Subject<String> {
+  /// Expects that the value contains [pattern] according to [String.contains];
+  void contains(Pattern pattern) {
+    context.expect(() => prefixFirst('contains ', literal(pattern)), (actual) {
+      if (actual.contains(pattern)) return null;
+      return Rejection(
+        which: prefixFirst('Does not contain ', literal(pattern)),
+      );
+    });
+  }
+
+  Subject<int> get length => has((m) => m.length, 'length');
+
+  void isEmpty() {
+    context.expect(() => const ['is empty'], (actual) {
+      if (actual.isEmpty) return null;
+      return Rejection(which: ['is not empty']);
+    });
+  }
+
+  void isNotEmpty() {
+    context.expect(() => const ['is not empty'], (actual) {
+      if (actual.isNotEmpty) return null;
+      return Rejection(which: ['is empty']);
+    });
+  }
+
+  void startsWith(Pattern other) {
+    context.expect(
+      () => prefixFirst('starts with ', literal(other)),
+      (actual) {
+        if (actual.startsWith(other)) return null;
+        return Rejection(
+          which: prefixFirst('does not start with ', literal(other)),
+        );
+      },
+    );
+  }
+
+  void endsWith(String other) {
+    context.expect(
+      () => prefixFirst('ends with ', literal(other)),
+      (actual) {
+        if (actual.endsWith(other)) return null;
+        return Rejection(
+          which: prefixFirst('does not end with ', literal(other)),
+        );
+      },
+    );
+  }
+
+  /// Expects that the string matches the pattern [expected].
+  ///
+  /// Fails if [expected] returns an empty result from calling `allMatches` with
+  /// the value.
+  ///
+  /// ```
+  /// check(actual).matchesPattern('abc');
+  /// check(actual).matchesPattern(RegExp(r'\d'));
+  /// ```
+  void matchesPattern(Pattern expected) {
+    context.expect(() => prefixFirst('matches ', literal(expected)), (actual) {
+      if (expected.allMatches(actual).isNotEmpty) return null;
+      return Rejection(
+          which: prefixFirst('does not match ', literal(expected)));
+    });
+  }
+
+  /// Expects that the `String` contains each of the sub strings in expected
+  /// in the given order, with any content between them.
+  ///
+  /// For example, the following will succeed:
+  ///
+  ///     check('abcdefg').containsInOrder(['a','e']);
+  void containsInOrder(Iterable<String> expected) {
+    context.expect(() => prefixFirst('contains, in order: ', literal(expected)),
+        (actual) {
+      var fromIndex = 0;
+      for (var s in expected) {
+        var index = actual.indexOf(s, fromIndex);
+        if (index < 0) {
+          return Rejection(which: [
+            ...prefixFirst(
+                'does not have a match for the substring ', literal(s)),
+            if (fromIndex != 0)
+              'following the other matches up to character $fromIndex'
+          ]);
+        }
+        fromIndex = index + s.length;
+      }
+      return null;
+    });
+  }
+
+  /// Expects that the `String` contains exactly the same code units as
+  /// [expected].
+  void equals(String expected) {
+    context.expect(() => prefixFirst('equals ', literal(expected)),
+        (actual) => _findDifference(actual, expected));
+  }
+
+  /// Expects that the `String` contains the same characters as [expected] if
+  /// both were lower case.
+  void equalsIgnoringCase(String expected) {
+    context.expect(
+        () => prefixFirst('equals ignoring case ', literal(expected)),
+        (actual) => _findDifference(
+            actual.toLowerCase(), expected.toLowerCase(), actual, expected));
+  }
+
+  /// Expects that the `String` contains the same content as [expected],
+  /// ignoring differences in whitsepace.
+  ///
+  /// All runs of whitespace characters are collapsed to a single space, and
+  /// leading and traiilng whitespace are removed before comparison.
+  ///
+  /// For example the following will succeed:
+  ///
+  ///     check(' hello   world ').equalsIgnoringWhitespace('hello world');
+  ///
+  /// While the following will fail:
+  ///
+  ///     check('helloworld').equalsIgnoringWhitespace('hello world');
+  ///     check('he llo world').equalsIgnoringWhitespace('hello world');
+  void equalsIgnoringWhitespace(String expected) {
+    context.expect(
+        () => prefixFirst('equals ignoring whitespace ', literal(expected)),
+        (actual) {
+      final collapsedActual = _collapseWhitespace(actual);
+      final collapsedExpected = _collapseWhitespace(expected);
+      return _findDifference(collapsedActual, collapsedExpected,
+          collapsedActual, collapsedExpected);
+    });
+  }
+}
+
+Rejection? _findDifference(String actual, String expected,
+    [String? actualDisplay, String? expectedDisplay]) {
+  if (actual == expected) return null;
+  final escapedActual = escape(actual);
+  final escapedExpected = escape(expected);
+  final escapedActualDisplay =
+      actualDisplay != null ? escape(actualDisplay) : escapedActual;
+  final escapedExpectedDisplay =
+      expectedDisplay != null ? escape(expectedDisplay) : escapedExpected;
+  final minLength = math.min(escapedActual.length, escapedExpected.length);
+  var i = 0;
+  for (; i < minLength; i++) {
+    if (escapedActual.codeUnitAt(i) != escapedExpected.codeUnitAt(i)) {
+      break;
+    }
+  }
+  if (i == minLength) {
+    if (escapedExpected.length < escapedActual.length) {
+      if (expected.isEmpty) {
+        return Rejection(which: ['is not the empty string']);
+      }
+      return Rejection(which: [
+        'is too long with unexpected trailing characters:',
+        _trailing(escapedActualDisplay, i)
+      ]);
+    } else {
+      if (actual.isEmpty) {
+        return Rejection(actual: [
+          'an empty string'
+        ], which: [
+          'is missing all expected characters:',
+          _trailing(escapedExpectedDisplay, 0)
+        ]);
+      }
+      return Rejection(which: [
+        'is too short with missing trailing characters:',
+        _trailing(escapedExpectedDisplay, i)
+      ]);
+    }
+  } else {
+    final indentation = ' ' * (i > 10 ? 14 : i);
+    return Rejection(which: [
+      'differs at offset $i:',
+      '${_leading(escapedExpectedDisplay, i)}'
+          '${_trailing(escapedExpectedDisplay, i)}',
+      '${_leading(escapedActualDisplay, i)}'
+          '${_trailing(escapedActualDisplay, i)}',
+      '$indentation^'
+    ]);
+  }
+}
+
+/// The truncated beginning of [s] up to the [end] character.
+String _leading(String s, int end) =>
+    (end > 10) ? '... ${s.substring(end - 10, end)}' : s.substring(0, end);
+
+/// The truncated remainder of [s] starting at the [start] character.
+String _trailing(String s, int start) => (start + 10 > s.length)
+    ? s.substring(start)
+    : '${s.substring(start, start + 10)} ...';
+
+/// Utility function to collapse whitespace runs to single spaces
+/// and strip leading/trailing whitespace.
+String _collapseWhitespace(String string) {
+  var result = StringBuffer();
+  var skipSpace = true;
+  for (var i = 0; i < string.length; i++) {
+    var character = string[i];
+    if (_isWhitespace(character)) {
+      if (!skipSpace) {
+        result.write(' ');
+        skipSpace = true;
+      }
+    } else {
+      result.write(character);
+      skipSpace = false;
+    }
+  }
+  return result.toString().trim();
+}
+
+bool _isWhitespace(String ch) =>
+    ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t';
diff --git a/pkgs/checks/mono_pkg.yaml b/pkgs/checks/mono_pkg.yaml
new file mode 100644
index 0000000..b447edb
--- /dev/null
+++ b/pkgs/checks/mono_pkg.yaml
@@ -0,0 +1,15 @@
+# See https://pub.dev/packages/mono_repo
+
+stages:
+- analyze_and_format:
+  - group:
+    - format
+    - analyze: --fatal-infos
+    sdk: dev
+  - group:
+    - analyze
+    sdk: pubspec
+- unit_test:
+  - group:
+    - command: dart test
+    sdk: [dev, pubspec]
diff --git a/pkgs/checks/pubspec.yaml b/pkgs/checks/pubspec.yaml
new file mode 100644
index 0000000..8c59ba4
--- /dev/null
+++ b/pkgs/checks/pubspec.yaml
@@ -0,0 +1,18 @@
+name: checks
+version: 0.3.1-wip
+description: >-
+  A framework for checking values against expectations and building custom
+  expectations.
+repository: https://github.com/dart-lang/test/tree/master/pkgs/checks
+resolution: workspace
+
+environment:
+  sdk: ^3.5.0
+
+dependencies:
+  async: ^2.8.0
+  meta: ^1.9.0
+  test_api: ">=0.5.0 <0.8.0"
+
+dev_dependencies:
+  test: ^1.21.3
diff --git a/pkgs/checks/test/context_test.dart b/pkgs/checks/test/context_test.dart
new file mode 100644
index 0000000..820ff88
--- /dev/null
+++ b/pkgs/checks/test/context_test.dart
@@ -0,0 +1,172 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:async/async.dart' hide Result;
+import 'package:checks/checks.dart';
+import 'package:checks/context.dart';
+import 'package:test/scaffolding.dart';
+import 'package:test_api/hooks.dart';
+import 'package:test_api/hooks_testing.dart';
+
+void main() {
+  group('Context', () {
+    test('expectAsync holds test open', () async {
+      late void Function() callback;
+      final monitor = TestCaseMonitor.start(() {
+        check(null).context.expectAsync(() => [''], (actual) async {
+          final completer = Completer<void>();
+          callback = completer.complete;
+          await completer.future;
+          return null;
+        });
+      });
+      await pumpEventQueue();
+      check(monitor).state.equals(State.running);
+      callback();
+      await monitor.onDone;
+      check(monitor).didPass();
+    });
+
+    test('expectAsync does not hold test open past exception', () async {
+      late void Function() callback;
+      final monitor = TestCaseMonitor.start(() {
+        check(null).context.expectAsync(() => [''], (actual) async {
+          final completer = Completer<void>();
+          callback = completer.complete;
+          await completer.future;
+          throw 'oh no!';
+        });
+      });
+      await pumpEventQueue();
+      check(monitor).state.equals(State.running);
+      callback();
+      await monitor.onDone;
+      check(monitor)
+        ..state.equals(State.failed)
+        ..errors.single.has((e) => e.error, 'error').equals('oh no!');
+    });
+
+    test('nestAsync holds test open', () async {
+      late void Function() callback;
+      final monitor = TestCaseMonitor.start(() {
+        check(null).context.nestAsync(() => [''], (actual) async {
+          final completer = Completer<void>();
+          callback = completer.complete;
+          await completer.future;
+          return Extracted.value(null);
+        }, null);
+      });
+      await pumpEventQueue();
+      check(monitor).state.equals(State.running);
+      callback();
+      await monitor.onDone;
+      check(monitor).didPass();
+    });
+
+    test('nestAsync holds test open past async condition', () async {
+      late void Function() callback;
+      final monitor = TestCaseMonitor.start(() {
+        check(null).context.nestAsync(() => [''], (actual) async {
+          return Extracted.value(null);
+        }, (it) async {
+          final completer = Completer<void>();
+          callback = completer.complete;
+          await completer.future;
+        });
+      });
+      await pumpEventQueue();
+      check(monitor).state.equals(State.running);
+      callback();
+      await monitor.onDone;
+      check(monitor).didPass();
+    });
+
+    test('nestAsync does not hold test open past exception', () async {
+      late void Function() callback;
+      final monitor = TestCaseMonitor.start(() {
+        check(null).context.nestAsync<Object?>(() => [''], (actual) async {
+          final completer = Completer<void>();
+          callback = completer.complete;
+          await completer.future;
+          throw 'oh no!';
+        }, null);
+      });
+      await pumpEventQueue();
+      check(monitor).state.equals(State.running);
+      callback();
+      await monitor.onDone;
+      check(monitor)
+        ..state.equals(State.failed)
+        ..errors.single.has((e) => e.error, 'error').equals('oh no!');
+    });
+
+    test('expectUnawaited can fail the test after it completes', () async {
+      late void Function() callback;
+      final monitor = await TestCaseMonitor.run(() {
+        check(null).context.expectUnawaited(() => [''], (actual, reject) {
+          final completer = Completer<void>()
+            ..future.then((_) {
+              reject(Rejection(which: ['foo']));
+            });
+          callback = completer.complete;
+        });
+      });
+      check(monitor).state.equals(State.passed);
+      callback();
+      await pumpEventQueue();
+      check(monitor)
+        ..state.equals(State.failed)
+        ..errors.unorderedMatches([
+          (it) => it
+              .has((e) => e.error, 'error')
+              .isA<TestFailure>()
+              .has((f) => f.message, 'message')
+              .isNotNull()
+              .endsWith('Which: foo'),
+          (it) => it
+              .has((e) => e.error, 'error')
+              .isA<String>()
+              .startsWith('This test failed after it had already completed.')
+        ]);
+    });
+  });
+
+  group('SkipExtension', () {
+    test('marks the test as skipped', () async {
+      final monitor = await TestCaseMonitor.run(() {
+        check(null).skip('skip').isNotNull();
+      });
+      check(monitor).state.equals(State.skipped);
+    });
+  });
+}
+
+extension _MonitorChecks on Subject<TestCaseMonitor> {
+  Subject<State> get state => has((m) => m.state, 'state');
+  Subject<Iterable<AsyncError>> get errors => has((m) => m.errors, 'errors');
+  Subject<StreamQueue<AsyncError>> get onError =>
+      has((m) => m.onError, 'onError').withQueue;
+
+  /// Expects that the monitored test is completed as success with no errors.
+  ///
+  /// Sets up an unawaited expectation that the test does not emit errors in the
+  /// future in addition to checking there have been no errors yet.
+  void didPass() {
+    errors.isEmpty();
+    state.equals(State.passed);
+    onError.context.expectUnawaited(() => ['emits no further errors'],
+        (actual, reject) async {
+      await for (var error in actual.rest) {
+        reject(Rejection(which: [
+          ...prefixFirst('threw late error', literal(error.error)),
+          ...const LineSplitter().convert(
+              TestHandle.current.formatStackTrace(error.stackTrace).toString())
+        ]));
+      }
+    });
+  }
+}
diff --git a/pkgs/checks/test/describe_test.dart b/pkgs/checks/test/describe_test.dart
new file mode 100644
index 0000000..d117a7b
--- /dev/null
+++ b/pkgs/checks/test/describe_test.dart
@@ -0,0 +1,24 @@
+// Copyright (c) 2023, 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:checks/checks.dart';
+import 'package:checks/context.dart';
+import 'package:test/scaffolding.dart';
+
+void main() {
+  group('describe', () {
+    test('succeeds for empty conditions', () {
+      check(describe((_) {})).isEmpty();
+    });
+    test('includes condition clauses', () {
+      check(describe((it) => it.equals(1))).deepEquals(['  equals <1>']);
+    });
+    test('includes nested clauses', () {
+      check(describe<String>((it) => it.length.equals(1))).deepEquals([
+        '  has length that:',
+        '    equals <1>',
+      ]);
+    });
+  });
+}
diff --git a/pkgs/checks/test/extensions/async_test.dart b/pkgs/checks/test/extensions/async_test.dart
new file mode 100644
index 0000000..5bfe550
--- /dev/null
+++ b/pkgs/checks/test/extensions/async_test.dart
@@ -0,0 +1,531 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+import 'package:checks/checks.dart';
+import 'package:checks/context.dart';
+import 'package:test/scaffolding.dart';
+import 'package:test_api/hooks.dart';
+
+import '../test_shared.dart';
+
+void main() {
+  group('FutureChecks', () {
+    group('completes', () {
+      test('succeeds for a future that completes to a value', () async {
+        await check(_futureSuccess()).completes((it) => it.equals(42));
+      });
+      test('rejects futures which complete as errors', () async {
+        await check(_futureFail()).isRejectedByAsync(
+          (it) => it.completes((it) => it.equals(1)),
+          actual: ['a future that completes as an error'],
+          which: ['threw <UnimplementedError> at:', 'fake trace'],
+        );
+      });
+      test('can be described', () async {
+        await check((Subject<Future> it) => it.completes())
+            .hasAsyncDescriptionWhich(
+                (it) => it.deepEquals(['  completes to a value']));
+        await check((Subject<Future> it) => it.completes((it) => it.equals(42)))
+            .hasAsyncDescriptionWhich((it) => it.deepEquals([
+                  '  completes to a value that:',
+                  '    equals <42>',
+                ]));
+      });
+    });
+
+    group('throws', () {
+      test(
+          'succeeds for a future that compeletes to an error of the expected type',
+          () async {
+        await check(_futureFail()).throws<UnimplementedError>(
+            (it) => it.has((p0) => p0.message, 'message').isNull());
+      });
+      test('fails for futures that complete to a value', () async {
+        await check(_futureSuccess()).isRejectedByAsync(
+          (it) => it.throws(),
+          actual: ['completed to <42>'],
+          which: ['did not throw'],
+        );
+      });
+      test('failes for futures that complete to an error of the wrong type',
+          () async {
+        await check(_futureFail()).isRejectedByAsync(
+          (it) => it.throws<StateError>(),
+          actual: ['completed to error <UnimplementedError>'],
+          which: [
+            'threw an exception that is not a StateError at:',
+            'fake trace'
+          ],
+        );
+      });
+      test('can be described', () async {
+        await check((Subject<Future<void>> it) => it.throws())
+            .hasAsyncDescriptionWhich(
+                (it) => it.deepEquals(['  completes to an error']));
+        await check((Subject<Future<void>> it) => it.throws<StateError>())
+            .hasAsyncDescriptionWhich((it) =>
+                it.deepEquals(['  completes to an error of type StateError']));
+      });
+    });
+
+    group('doesNotComplete', () {
+      test('succeeds for a Future that never completes', () async {
+        check(Completer<void>().future).doesNotComplete();
+      });
+      test('fails for a Future that completes as a value', () async {
+        Object? testFailure;
+        runZonedGuarded(() {
+          final completer = Completer<String>();
+          check(completer.future).doesNotComplete();
+          completer.complete('value');
+        }, (e, st) {
+          testFailure = e;
+        });
+        await pumpEventQueue();
+        check(testFailure)
+            .isA<TestFailure>()
+            .has((f) => f.message, 'message')
+            .isNotNull()
+            .equals('''
+Expected: a Future<String> that:
+  does not complete
+Actual: a future that completed to 'value\'''');
+      });
+      test('fails for a Future that completes as an error', () async {
+        Object? testFailure;
+        runZonedGuarded(() {
+          final completer = Completer<String>();
+          check(completer.future).doesNotComplete();
+          completer.completeError('error', StackTrace.fromString('fake trace'));
+        }, (e, st) {
+          testFailure = e;
+        });
+        await pumpEventQueue();
+        check(testFailure)
+            .isA<TestFailure>()
+            .has((f) => f.message, 'message')
+            .isNotNull()
+            .equals('''
+Expected: a Future<String> that:
+  does not complete
+Actual: a future that completed as an error:
+Which: threw 'error'
+fake trace''');
+      });
+      test('can be described', () async {
+        await check((Subject<Future<void>> it) => it.doesNotComplete())
+            .hasAsyncDescriptionWhich(
+                (it) => it.deepEquals(['  does not complete']));
+      });
+    });
+  });
+
+  group('StreamChecks', () {
+    group('emits', () {
+      test('succeeds for a stream that emits a value', () async {
+        await check(_countingStream(5)).emits((it) => it.equals(0));
+      });
+      test('fails for a stream that closes without emitting', () async {
+        await check(_countingStream(0)).isRejectedByAsync(
+          (it) => it.emits(),
+          actual: ['a stream'],
+          which: ['closed without emitting enough values'],
+        );
+      });
+      test('fails for a stream that emits an error', () async {
+        await check(_countingStream(1, errorAt: 0)).isRejectedByAsync(
+          (it) => it.emits(),
+          actual: ['a stream with error <UnimplementedError: Error at 1>'],
+          which: ['emitted an error instead of a value at:', 'fake trace'],
+        );
+      });
+      test('can be described', () async {
+        await check((Subject<StreamQueue<void>> it) => it.emits())
+            .hasAsyncDescriptionWhich(
+                (it) => it.deepEquals(['  emits a value']));
+        await check((Subject<StreamQueue<int>> it) =>
+                it.emits((it) => it.equals(42)))
+            .hasAsyncDescriptionWhich((it) => it.deepEquals([
+                  '  emits a value that:',
+                  '    equals <42>',
+                ]));
+      });
+      test('does not consume error', () async {
+        final queue = _countingStream(1, errorAt: 0);
+        await softCheckAsync<StreamQueue<int>>(queue, (it) => it.emits());
+        await check(queue).emitsError();
+      });
+    });
+
+    group('emitsError', () {
+      test('succeeds for a stream that emits an error', () async {
+        await check(_countingStream(1, errorAt: 0))
+            .emitsError<UnimplementedError>();
+      });
+      test('fails for a stream that closes without emitting an error',
+          () async {
+        await check(_countingStream(0)).isRejectedByAsync(
+          (it) => it.emitsError(),
+          actual: ['a stream'],
+          which: ['closed without emitting an expected error'],
+        );
+      });
+      test('fails for a stream that emits value', () async {
+        await check(_countingStream(1)).isRejectedByAsync(
+          (it) => it.emitsError(),
+          actual: ['a stream emitting value <0>'],
+          which: ['closed without emitting an error'],
+        );
+      });
+      test('fails for a stream that emits an error of the incorrect type',
+          () async {
+        await check(_countingStream(1, errorAt: 0)).isRejectedByAsync(
+          (it) => it.emitsError<StateError>(),
+          actual: ['a stream with error <UnimplementedError: Error at 1>'],
+          which: ['emitted an error which is not StateError at:', 'fake trace'],
+        );
+      });
+      test('can be described', () async {
+        await check((Subject<StreamQueue<void>> it) => it.emitsError())
+            .hasAsyncDescriptionWhich(
+                (it) => it.deepEquals(['  emits an error']));
+        await check(
+                (Subject<StreamQueue<void>> it) => it.emitsError<StateError>())
+            .hasAsyncDescriptionWhich(
+                (it) => it.deepEquals(['  emits an error of type StateError']));
+        await check((Subject<StreamQueue<void>> it) => it
+              ..emitsError<StateError>(
+                  (it) => it.has((e) => e.message, 'message').equals('foo')))
+            .hasAsyncDescriptionWhich((it) => it.deepEquals([
+                  '  emits an error of type StateError that:',
+                  '    has message that:',
+                  '      equals \'foo\''
+                ]));
+      });
+      test('uses a transaction', () async {
+        final queue = _countingStream(1);
+        await softCheckAsync<StreamQueue<int>>(queue, (it) => it.emitsError());
+        await check(queue).emits((it) => it.equals(0));
+      });
+    });
+
+    group('emitsThrough', () {
+      test('succeeds for a stream that eventuall emits a matching value',
+          () async {
+        await check(_countingStream(5)).emitsThrough((it) => it.equals(4));
+      });
+      test('fails for a stream that closes without emitting a matching value',
+          () async {
+        await check(_countingStream(4)).isRejectedByAsync(
+          (it) => it.emitsThrough((it) => it.equals(5)),
+          actual: ['a stream'],
+          which: ['ended after emitting 4 elements with none matching'],
+        );
+      });
+      test('can be described', () async {
+        await check((Subject<StreamQueue<int>> it) =>
+                it.emitsThrough((it) => it.equals(42)))
+            .hasAsyncDescriptionWhich((it) => it.deepEquals([
+                  '  emits any values then emits a value that:',
+                  '    equals <42>'
+                ]));
+      });
+      test('uses a transaction', () async {
+        final queue = _countingStream(1);
+        await softCheckAsync(
+            queue,
+            (Subject<StreamQueue<int>> it) =>
+                it.emitsThrough((it) => it.equals(42)));
+        await check(queue).emits((it) => it.equals(0));
+      });
+      test('consumes events', () async {
+        final queue = _countingStream(3);
+        await check(queue).emitsThrough((it) => it.equals(1));
+        await check(queue).emits((it) => it.equals(2));
+      });
+    });
+
+    group('emitsInOrder', () {
+      test('succeeds for happy case', () async {
+        await check(_countingStream(2)).inOrder([
+          (it) => it.emits((it) => it.equals(0)),
+          (it) => it.emits((it) => it.equals(1)),
+          (it) => it.isDone(),
+        ]);
+      });
+      test('reports which condition failed', () async {
+        await check(_countingStream(1)).isRejectedByAsync(
+          (it) => it.inOrder([(it) => it.emits(), (it) => it.emits()]),
+          actual: ['a stream'],
+          which: [
+            'satisfied 1 conditions then',
+            'failed to satisfy the condition at index 1',
+            'because it closed without emitting enough values'
+          ],
+        );
+      });
+      test('nestes the report for deep failures', () async {
+        await check(_countingStream(2)).isRejectedByAsync(
+          (it) => it.inOrder(
+              [(it) => it.emits(), (it) => it.emits((it) => it.equals(2))]),
+          actual: ['a stream'],
+          which: [
+            'satisfied 1 conditions then',
+            'failed to satisfy the condition at index 1',
+            'because it:',
+            '  emits a value that:',
+            '  Actual: <1>',
+            '  Which: are not equal',
+          ],
+        );
+      });
+      test('gets described with the number of conditions', () async {
+        await check(
+                (Subject<StreamQueue<int>> it) => it.inOrder([(_) {}, (_) {}]))
+            .hasAsyncDescriptionWhich(
+                (it) => it.deepEquals(['  satisfies 2 conditions in order']));
+      });
+      test('uses a transaction', () async {
+        final queue = _countingStream(3);
+        await softCheckAsync<StreamQueue<int>>(
+            queue,
+            (it) => it.inOrder([
+                  (it) => it.emits((it) => it.equals(0)),
+                  (it) => it.emits((it) => it.equals(1)),
+                  (it) => it.emits((it) => it.equals(42)),
+                ]));
+        await check(queue).inOrder([
+          (it) => it.emits((it) => it.equals(0)),
+          (it) => it.emits((it) => it.equals(1)),
+          (it) => it.emits((it) => it.equals(2)),
+          (it) => it.isDone(),
+        ]);
+      });
+      test('consumes events', () async {
+        final queue = _countingStream(3);
+        await check(queue).inOrder([(it) => it.emits(), (it) => it.emits()]);
+        await check(queue).emits((it) => it.equals(2));
+      });
+    });
+
+    group('neverEmits', () {
+      test(
+          'succeeds for a stream that closes without emitting a matching value',
+          () async {
+        await check(_countingStream(5)).neverEmits((it) => it.equals(5));
+      });
+      test('fails for a stream that emits a matching value', () async {
+        await check(_countingStream(6)).isRejectedByAsync(
+          (it) => it.neverEmits((it) => it.equals(5)),
+          actual: ['a stream'],
+          which: ['emitted <5>', 'following 5 other items'],
+        );
+      });
+      test('can be described', () async {
+        await check((Subject<StreamQueue<int>> it) =>
+                it.neverEmits((it) => it.equals(42)))
+            .hasAsyncDescriptionWhich((it) => it.deepEquals([
+                  '  never emits a value that:',
+                  '    equals <42>',
+                ]));
+      });
+      test('uses a transaction', () async {
+        final queue = _countingStream(2);
+        await softCheckAsync<StreamQueue<int>>(
+            queue, (it) => it.neverEmits((it) => it.equals(1)));
+        await check(queue).inOrder([
+          (it) => it.emits((it) => it.equals(0)),
+          (it) => it.emits((it) => it.equals(1)),
+          (it) => it.isDone(),
+        ]);
+      });
+    });
+
+    group('mayEmit', () {
+      test('succeeds for a stream that emits a matching value', () async {
+        await check(_countingStream(1)).mayEmit((it) => it.equals(0));
+      });
+      test('succeeds for a stream that emits an error', () async {
+        await check(_countingStream(1, errorAt: 0))
+            .mayEmit((it) => it.equals(0));
+      });
+      test('succeeds for a stream that closes', () async {
+        await check(_countingStream(0)).mayEmit((it) => it.equals(42));
+      });
+      test('consumes a matching event', () async {
+        final queue = _countingStream(2);
+        await softCheckAsync<StreamQueue<int>>(
+            queue, (it) => it.mayEmit((it) => it.equals(0)));
+        await check(queue).emits((it) => it.equals(1));
+      });
+      test('does not consume a non-matching event', () async {
+        final queue = _countingStream(2);
+        await softCheckAsync<StreamQueue<int>>(
+            queue, (it) => it.mayEmit((it) => it.equals(1)));
+        await check(queue).emits((it) => it.equals(0));
+      });
+      test('does not consume an error', () async {
+        final queue = _countingStream(1, errorAt: 0);
+        await softCheckAsync<StreamQueue<int>>(
+            queue, (it) => it.mayEmit((it) => it.equals(0)));
+        await check(queue).emitsError<UnimplementedError>(
+            (it) => it.has((e) => e.message, 'message').equals('Error at 1'));
+      });
+    });
+
+    group('mayEmitMultiple', () {
+      test('succeeds for a stream that emits a matching value', () async {
+        await check(_countingStream(1)).mayEmitMultiple((it) => it.equals(0));
+      });
+      test('succeeds for a stream that emits an error', () async {
+        await check(_countingStream(1, errorAt: 0))
+            .mayEmitMultiple((it) => it.equals(0));
+      });
+      test('succeeds for a stream that closes', () async {
+        await check(_countingStream(0)).mayEmitMultiple((it) => it.equals(42));
+      });
+      test('consumes matching events', () async {
+        final queue = _countingStream(3);
+        await softCheckAsync<StreamQueue<int>>(
+            queue, (it) => it.mayEmitMultiple((it) => it.isLessThan(2)));
+        await check(queue).emits((it) => it.equals(2));
+      });
+      test('consumes no events if no events match', () async {
+        final queue = _countingStream(2);
+        await softCheckAsync<StreamQueue<int>>(
+            queue, (it) => it.mayEmitMultiple((it) => it.isLessThan(0)));
+        await check(queue).emits((it) => it.equals(0));
+      });
+      test('does not consume an error', () async {
+        final queue = _countingStream(1, errorAt: 0);
+        await softCheckAsync<StreamQueue<int>>(
+            queue, (it) => it.mayEmitMultiple((it) => it.equals(0)));
+        await check(queue).emitsError<UnimplementedError>(
+            (it) => it.has((e) => e.message, 'message').equals('Error at 1'));
+      });
+    });
+
+    group('isDone', () {
+      test('succeeds for an empty stream', () async {
+        await check(_countingStream(0)).isDone();
+      });
+      test('fails for a stream that emits a value', () async {
+        await check(_countingStream(1)).isRejectedByAsync((it) => it.isDone(),
+            actual: ['a stream'], which: ['emitted an unexpected value: <0>']);
+      });
+      test('fails for a stream that emits an error', () async {
+        final controller = StreamController<void>();
+        controller.addError('sad', StackTrace.fromString('fake trace'));
+        await check(StreamQueue(controller.stream)).isRejectedByAsync(
+            (it) => it.isDone(),
+            actual: ['a stream'],
+            which: ['emitted an unexpected error: \'sad\'', 'fake trace']);
+      });
+      test('uses a transaction', () async {
+        final queue = _countingStream(1);
+        await softCheckAsync<StreamQueue<int>>(queue, (it) => it.isDone());
+        await check(queue).emits((it) => it.equals(0));
+      });
+      test('can be described', () async {
+        await check((Subject<StreamQueue<int>> it) => it.isDone())
+            .hasAsyncDescriptionWhich((it) => it.deepEquals(['  is done']));
+      });
+    });
+
+    group('emitsAnyOf', () {
+      test('succeeds for a stream that matches one condition', () async {
+        await check(_countingStream(1)).anyOf([
+          (it) => it.emits((it) => it.equals(42)),
+          (it) => it.emits((it) => it.equals(0))
+        ]);
+      });
+      test('fails for a stream that matches no conditions', () async {
+        await check(_countingStream(0)).isRejectedByAsync(
+            (it) => it.anyOf([
+                  (it) => it.emits(),
+                  (it) => it.emitsThrough((it) => it.equals(1)),
+                ]),
+            actual: [
+              'a stream'
+            ],
+            which: [
+              'failed to satisfy any condition',
+              'failed the condition at index 0 because it:',
+              '  closed without emitting enough values',
+              'failed the condition at index 1 because it:',
+              '  ended after emitting 0 elements with none matching',
+            ]);
+      });
+      test('includes nested details for nested failures', () async {
+        await check(_countingStream(1)).isRejectedByAsync(
+            (it) => it.anyOf([
+                  (it) => it.emits((it) => it.equals(42)),
+                  (it) => it.emitsThrough((it) => it.equals(10)),
+                ]),
+            actual: [
+              'a stream'
+            ],
+            which: [
+              'failed to satisfy any condition',
+              'failed the condition at index 0 because it:',
+              '  emits a value that:',
+              '  Actual: <0>',
+              '  Which: are not equal',
+              'failed the condition at index 1 because it:',
+              '  ended after emitting 1 elements with none matching',
+            ]);
+      });
+      test('gets described with the number of conditions', () async {
+        await check((Subject<StreamQueue<int>> it) =>
+                it..anyOf([(it) => it.emits(), (it) => it.emits()]))
+            .hasAsyncDescriptionWhich(
+                (it) => it.deepEquals(['  satisfies any of 2 conditions']));
+      });
+      test('uses a transaction', () async {
+        final queue = _countingStream(1);
+        await softCheckAsync<StreamQueue<int>>(
+            queue,
+            (it) => it.anyOf([
+                  (it) => it.emits((it) => it.equals(10)),
+                  (it) => it.emitsThrough((it) => it.equals(42)),
+                ]));
+        await check(queue).emits((it) => it.equals(0));
+      });
+      test('consumes events', () async {
+        final queue = _countingStream(3);
+        await check(queue).anyOf([
+          (it) => it.emits((it) => it.equals(1)),
+          (it) => it.emitsThrough((it) => it.equals(1))
+        ]);
+        await check(queue).emits((it) => it.equals(2));
+      });
+    });
+  });
+
+  group('StreamQueueWrap', () {
+    test('can wrap streams in a queue', () async {
+      await check(Stream.value(1)).withQueue.emits();
+    });
+  });
+}
+
+Future<int> _futureSuccess() => Future.microtask(() => 42);
+
+Future<int> _futureFail() =>
+    Future.error(UnimplementedError(), StackTrace.fromString('fake trace'));
+
+StreamQueue<int> _countingStream(int count, {int? errorAt}) => StreamQueue(
+      Stream.fromIterable(
+        Iterable<int>.generate(count, (index) {
+          if (index == errorAt) {
+            Error.throwWithStackTrace(UnimplementedError('Error at $count'),
+                StackTrace.fromString('fake trace'));
+          }
+          return index;
+        }),
+      ),
+    );
diff --git a/pkgs/checks/test/extensions/collection_equality_test.dart b/pkgs/checks/test/extensions/collection_equality_test.dart
new file mode 100644
index 0000000..28a10dd
--- /dev/null
+++ b/pkgs/checks/test/extensions/collection_equality_test.dart
@@ -0,0 +1,197 @@
+// Copyright (c) 2023, 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:checks/checks.dart';
+import 'package:checks/src/collection_equality.dart';
+import 'package:test/scaffolding.dart';
+
+void main() {
+  group('deepCollectionEquals', () {
+    test('allows nested collections with equal elements', () {
+      check(deepCollectionEquals([
+        'a',
+        {'b': 1},
+        {'c', 'd'},
+        [
+          ['e']
+        ],
+      ], [
+        'a',
+        {'b': 1},
+        {'c', 'd'},
+        [
+          ['e']
+        ],
+      ])).isNull();
+    });
+
+    test('allows collections inside sets', () {
+      check(deepCollectionEquals({
+        {'a': 1}
+      }, {
+        {'a': 1}
+      })).isNull();
+    });
+
+    test('allows collections as Map keys', () {
+      check(deepCollectionEquals([
+        {
+          {'a': 1}: {'b': 2}
+        }
+      ], [
+        {
+          {'a': 1}: {'b': 2}
+        }
+      ])).isNull();
+    });
+
+    test('allows conditions in place of elements in lists', () {
+      check(deepCollectionEquals([
+        'a',
+        'b'
+      ], [
+        (Subject<dynamic> it) => it.isA<String>().which((it) => it
+          ..startsWith('a')
+          ..length.isLessThan(2)),
+        (Subject<dynamic> it) => it.isA<String>().startsWith('b')
+      ])).isNull();
+    });
+
+    test('allows conditions in place of values in maps', () {
+      check(deepCollectionEquals([
+        {'a': 'b'}
+      ], [
+        {'a': (Subject<dynamic> it) => it.isA<String>().startsWith('b')}
+      ])).isNull();
+    });
+
+    test('allows conditions in place of elements in sets', () {
+      check(deepCollectionEquals({
+        'b',
+        'a'
+      }, {
+        'a',
+        (Subject<dynamic> it) => it.isA<String>().startsWith('b')
+      })).isNull();
+    });
+
+    test('allows conditions in place of keys in maps', () {
+      check(deepCollectionEquals({
+        'a': 'b'
+      }, {
+        (Subject<dynamic> it) => it.isA<String>().startsWith('a'): 'b'
+      })).isNull();
+    });
+
+    test('reports non-Set elements', () {
+      check(deepCollectionEquals([
+        ['a']
+      ], [
+        {'a'}
+      ])).isNotNull().deepEquals(['at [<0>] is not a Set']);
+    });
+
+    test('reports long iterables', () {
+      check(deepCollectionEquals([0], [])).isNotNull().deepEquals([
+        'has more elements than expected',
+        'expected an iterable with 0 element(s)'
+      ]);
+    });
+
+    test('reports short iterables', () {
+      check(deepCollectionEquals([], [0])).isNotNull().deepEquals([
+        'has too few elements',
+        'expected an iterable with at least 1 element(s)'
+      ]);
+    });
+
+    test('reports unequal elements in iterables', () {
+      check(deepCollectionEquals([0], [1]))
+          .isNotNull()
+          .deepEquals(['at [<0>] is <0>', 'which does not equal <1>']);
+    });
+
+    test('reports unmet conditions in iterables', () {
+      check(deepCollectionEquals(
+              [0], [(Subject<dynamic> it) => it.isA<int>().isGreaterThan(0)]))
+          .isNotNull()
+          .deepEquals([
+        'has an element at [<0>] that:',
+        '  Actual: <0>',
+        '  which is not greater than <0>'
+      ]);
+    });
+
+    test('reports unmet conditions in map values', () {
+      check(deepCollectionEquals({
+        'a': 'b'
+      }, {
+        'a': (Subject<dynamic> it) => it.isA<String>().startsWith('a')
+      })).isNotNull().deepEquals([
+        "has an element at ['a'] that:",
+        "  Actual: 'b'",
+        "  which does not start with 'a'",
+      ]);
+    });
+
+    test('reports unmet conditions in map keys', () {
+      check(deepCollectionEquals({
+        'b': 'a'
+      }, {
+        (Subject<dynamic> it) => it.isA<String>().startsWith('a'): 'a'
+      })).isNotNull().deepEquals([
+        'has no entry to match <A value that:',
+        '  is a String',
+        "  starts with 'a'>: 'a'",
+      ]);
+    });
+
+    test('maintains paths through maps when the keys are all values', () {
+      check(deepCollectionEquals({
+        'a': [
+          {'b': 'c'}
+        ]
+      }, {
+        'a': [
+          {'b': 'd'}
+        ]
+      })).isNotNull().deepEquals([
+        "at ['a'][<0>]['b'] is 'c'",
+        "which does not equal 'd'",
+      ]);
+    });
+
+    test('reports recursive lists', () {
+      var l = <Object>[];
+      l.add(l);
+      check(deepCollectionEquals(l, l))
+          .isNotNull()
+          .deepEquals(['exceeds the depth limit of 1000']);
+    });
+
+    test('reports recursive sets', () {
+      var s = <Object>{};
+      s.add(s);
+      check(deepCollectionEquals(s, s))
+          .isNotNull()
+          .deepEquals(['exceeds the depth limit of 1000']);
+    });
+
+    test('reports maps with recursive keys', () {
+      var m = <Object, Object>{};
+      m[m] = 0;
+      check(deepCollectionEquals(m, m))
+          .isNotNull()
+          .deepEquals(['exceeds the depth limit of 1000']);
+    });
+
+    test('reports maps with recursive values', () {
+      var m = <Object, Object>{};
+      m[0] = m;
+      check(deepCollectionEquals(m, m))
+          .isNotNull()
+          .deepEquals(['exceeds the depth limit of 1000']);
+    });
+  });
+}
diff --git a/pkgs/checks/test/extensions/core_test.dart b/pkgs/checks/test/extensions/core_test.dart
new file mode 100644
index 0000000..2133c36
--- /dev/null
+++ b/pkgs/checks/test/extensions/core_test.dart
@@ -0,0 +1,163 @@
+// Copyright (c) 2022, 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:checks/checks.dart';
+import 'package:test/scaffolding.dart';
+
+import '../test_shared.dart';
+
+void main() {
+  group('TypeChecks', () {
+    test('isA', () {
+      check(1).isA<int>();
+
+      check(1).isRejectedBy((it) => it.isA<String>(), which: ['Is a int']);
+    });
+  });
+  group('HasField', () {
+    test('has', () {
+      check(1).has((v) => v.isOdd, 'isOdd').isTrue();
+
+      check(null).isRejectedBy(
+          (it) => it.has((v) {
+                Error.throwWithStackTrace(
+                    UnimplementedError(), StackTrace.fromString('fake trace'));
+              }, 'foo').isNotNull(),
+          which: [
+            'threw while trying to read foo: <UnimplementedError>',
+            'fake trace'
+          ]);
+    });
+
+    test('which', () {
+      check(true).which((it) => it.isTrue());
+    });
+
+    test('not', () {
+      check(false).not((it) => it.isTrue());
+      check(true).isRejectedBy((it) => it.not((it) => it.isTrue()), which: [
+        'is a value that: ',
+        '    is true',
+      ]);
+    });
+
+    group('anyOf', () {
+      test('succeeds for happy case', () {
+        check(-10)
+            .anyOf([(it) => it.isGreaterThan(1), (it) => it.isLessThan(-1)]);
+      });
+      test('rejects values that do not satisfy any condition', () {
+        check(0).isRejectedBy(
+            (it) => it.anyOf(
+                [(it) => it.isGreaterThan(1), (it) => it.isLessThan(-1)]),
+            which: ['did not match any condition']);
+      });
+    });
+  });
+
+  group('BoolChecks', () {
+    test('isTrue', () {
+      check(true).isTrue();
+
+      check(false).isRejectedBy((it) => it.isTrue());
+    });
+
+    test('isFalse', () {
+      check(false).isFalse();
+
+      check(true).isRejectedBy((it) => it.isFalse());
+    });
+  });
+
+  group('EqualityChecks', () {
+    test('equals', () {
+      check(1).equals(1);
+
+      check(1).isRejectedBy((it) => it.equals(2), which: ['are not equal']);
+    });
+    test('identical', () {
+      check(1).identicalTo(1);
+
+      check(1)
+          .isRejectedBy((it) => it.identicalTo(2), which: ['is not identical']);
+    });
+  });
+  group('NullabilityChecks', () {
+    test('isNotNull', () {
+      check(1).isNotNull();
+
+      check(null).isRejectedBy((it) => it.isNotNull());
+    });
+    test('isNull', () {
+      check(null).isNull();
+
+      check(1).isRejectedBy((it) => it.isNull());
+    });
+  });
+
+  group('ComparableChecks on Duration', () {
+    group('isGreaterThan', () {
+      test('succeeds for greater', () {
+        check(const Duration(seconds: 10))
+            .isGreaterThan(const Duration(seconds: 1));
+      });
+      test('fails for equal', () {
+        check(const Duration(seconds: 10)).isRejectedBy(
+            (it) => it.isGreaterThan(const Duration(seconds: 10)),
+            which: ['is not greater than <0:00:10.000000>']);
+      });
+      test('fails for less', () {
+        check(const Duration(seconds: 10)).isRejectedBy(
+            (it) => it.isGreaterThan(const Duration(seconds: 50)),
+            which: ['is not greater than <0:00:50.000000>']);
+      });
+    });
+    group('isGreaterOrEqual', () {
+      test('succeeds for greater', () {
+        check(const Duration(seconds: 10))
+            .isGreaterOrEqual(const Duration(seconds: 1));
+      });
+      test('succeeds for equal', () {
+        check(const Duration(seconds: 10))
+            .isGreaterOrEqual(const Duration(seconds: 10));
+      });
+      test('fails for less', () {
+        check(const Duration(seconds: 10)).isRejectedBy(
+            (it) => it.isGreaterOrEqual(const Duration(seconds: 50)),
+            which: ['is not greater than or equal to <0:00:50.000000>']);
+      });
+    });
+    group('isLessThan', () {
+      test('succeeds for less', () {
+        check(const Duration(seconds: 1))
+            .isLessThan(const Duration(seconds: 10));
+      });
+      test('fails for equal', () {
+        check(const Duration(seconds: 10)).isRejectedBy(
+            (it) => it.isLessThan(const Duration(seconds: 10)),
+            which: ['is not less than <0:00:10.000000>']);
+      });
+      test('fails for greater', () {
+        check(const Duration(seconds: 50)).isRejectedBy(
+            (it) => it.isLessThan(const Duration(seconds: 10)),
+            which: ['is not less than <0:00:10.000000>']);
+      });
+    });
+    group('isLessOrEqual', () {
+      test('succeeds for less', () {
+        check(const Duration(seconds: 10))
+            .isLessOrEqual(const Duration(seconds: 50));
+      });
+      test('succeeds for equal', () {
+        check(const Duration(seconds: 10))
+            .isLessOrEqual(const Duration(seconds: 10));
+      });
+      test('fails for greater', () {
+        check(const Duration(seconds: 10)).isRejectedBy(
+            (it) => it.isLessOrEqual(const Duration(seconds: 1)),
+            which: ['is not less than or equal to <0:00:01.000000>']);
+      });
+    });
+  });
+}
diff --git a/pkgs/checks/test/extensions/function_test.dart b/pkgs/checks/test/extensions/function_test.dart
new file mode 100644
index 0000000..034d8fe
--- /dev/null
+++ b/pkgs/checks/test/extensions/function_test.dart
@@ -0,0 +1,44 @@
+// Copyright (c) 2022, 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:checks/checks.dart';
+import 'package:test/scaffolding.dart';
+
+import '../test_shared.dart';
+
+void main() {
+  group('ThrowsChecks', () {
+    group('throws', () {
+      test('succeeds for happy case', () {
+        check(() => throw StateError('oops!')).throws<StateError>();
+      });
+      test('fails for functions that return normally', () {
+        check(() {}).isRejectedBy((it) => it.throws<StateError>(),
+            actual: ['a function that returned <null>'],
+            which: ['did not throw']);
+      });
+      test('fails for functions that throw the wrong type', () {
+        check(() => throw StateError('oops!')).isRejectedBy(
+          (it) => it.throws<ArgumentError>(),
+          actual: ['a function that threw error <Bad state: oops!>'],
+          which: ['did not throw an ArgumentError'],
+        );
+      });
+    });
+
+    group('returnsNormally', () {
+      test('succeeds for happy case', () {
+        check(() => 1).returnsNormally().equals(1);
+      });
+      test('fails for functions that throw', () {
+        check(() {
+          Error.throwWithStackTrace(
+              StateError('oops!'), StackTrace.fromString('fake trace'));
+        }).isRejectedBy((it) => it.returnsNormally(),
+            actual: ['a function that throws'],
+            which: ['threw <Bad state: oops!>', 'fake trace']);
+      });
+    });
+  });
+}
diff --git a/pkgs/checks/test/extensions/iterable_test.dart b/pkgs/checks/test/extensions/iterable_test.dart
new file mode 100644
index 0000000..dafd84d
--- /dev/null
+++ b/pkgs/checks/test/extensions/iterable_test.dart
@@ -0,0 +1,275 @@
+// Copyright (c) 2022, 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:checks/checks.dart';
+import 'package:test/scaffolding.dart';
+
+import '../test_shared.dart';
+
+Iterable<int> get _testIterable => Iterable.generate(2, (i) => i);
+
+void main() {
+  test('length', () {
+    check(_testIterable).length.equals(2);
+  });
+
+  group('first', () {
+    test('succeeds for happy case', () {
+      check(_testIterable).first.equals(0);
+    });
+    test('rejects empty iterable', () {
+      check(<Object>[])
+          .isRejectedBy((it) => it.first.equals(0), which: ['has no elements']);
+    });
+  });
+
+  group('last', () {
+    test('succeeds for happy case', () {
+      check(_testIterable).last.equals(1);
+    });
+    test('rejects empty iterable', () {
+      check(<Object>[])
+          .isRejectedBy((it) => it.last.equals(0), which: ['has no elements']);
+    });
+  });
+
+  group('single', () {
+    test('succeeds for happy case', () {
+      check([42]).single.equals(42);
+    });
+    test('rejects empty iterable', () {
+      check(<Object>[]).isRejectedBy((it) => it.single.equals(0),
+          which: ['has no elements']);
+    });
+    test('rejects iterable with too many elements', () {
+      check(_testIterable).isRejectedBy((it) => it.single.equals(0),
+          which: ['has more than one element']);
+    });
+  });
+
+  test('isEmpty', () {
+    check(<Object>[]).isEmpty();
+    check(_testIterable)
+        .isRejectedBy((it) => it.isEmpty(), which: ['is not empty']);
+  });
+
+  test('isNotEmpty', () {
+    check(_testIterable).isNotEmpty();
+    check(const Iterable<int>.empty())
+        .isRejectedBy((it) => it.isNotEmpty(), which: ['is empty']);
+  });
+
+  test('contains', () {
+    check(_testIterable).contains(0);
+    check(_testIterable)
+        .isRejectedBy((it) => it.contains(2), which: ['does not contain <2>']);
+  });
+  test('any', () {
+    check(_testIterable).any((it) => it.equals(1));
+    check(_testIterable).isRejectedBy((it) => it.any((it) => it.equals(2)),
+        which: ['Contains no matching element']);
+  });
+
+  group('containsInOrder', () {
+    test('succeeds for happy case', () {
+      check([0, 1, 0, 2, 0, 3]).containsInOrder([1, 2, 3]);
+    });
+    test('can use Condition<dynamic>', () {
+      check([0, 1]).containsInOrder(
+          [(Subject<dynamic> it) => it.isA<int>().isGreaterThan(0)]);
+    });
+    test('can use Condition<T>', () {
+      check([0, 1]).containsInOrder([(Subject<int> it) => it.isGreaterThan(0)]);
+    });
+    test('fails for not found elements by equality', () async {
+      check([0]).isRejectedBy((it) => it.containsInOrder([1]), which: [
+        'did not have an element matching the expectation at index 0 <1>'
+      ]);
+    });
+    test('fails for not found elements by condition', () async {
+      check([0]).isRejectedBy(
+          (it) => it.containsInOrder(
+              [(Subject<dynamic> it) => it.isA<int>().isGreaterThan(0)]),
+          which: [
+            'did not have an element matching the expectation at index 0 '
+                '<A value that:',
+            '  is a int',
+            '  is greater than <0>>'
+          ]);
+    });
+    test('can be described', () {
+      check((Subject<Iterable> it) => it.containsInOrder([1, 2, 3]))
+          .description
+          .deepEquals(['  contains, in order: [1, 2, 3]']);
+      check((Subject<Iterable> it) =>
+              it.containsInOrder([1, (Subject<dynamic> it) => it.equals(2)]))
+          .description
+          .deepEquals([
+        '  contains, in order: [1,',
+        '  <A value that:',
+        '    equals <2>>]'
+      ]);
+    });
+  });
+
+  group('containsMatchingInOrder', () {
+    test('succeeds for happy case', () {
+      check([0, 1, 0, 2, 0, 3]).containsMatchingInOrder([
+        (it) => it.isLessThan(2),
+        (it) => it.isLessThan(3),
+        (it) => it.isLessThan(4),
+      ]);
+    });
+    test('fails for not found elements', () async {
+      check([0]).isRejectedBy(
+          (it) => it.containsMatchingInOrder([(it) => it.isGreaterThan(0)]),
+          which: [
+            'did not have an element matching the expectation at index 0 '
+                '<A value that:',
+            '  is greater than <0>>'
+          ]);
+    });
+    test('can be described', () {
+      check((Subject<Iterable<int>> it) => it.containsMatchingInOrder([
+            (it) => it.isLessThan(2),
+            (it) => it.isLessThan(3),
+            (it) => it.isLessThan(4),
+          ])).description.deepEquals([
+        '  contains, in order: [<A value that:',
+        '    is less than <2>>,',
+        '  <A value that:',
+        '    is less than <3>>,',
+        '  <A value that:',
+        '    is less than <4>>]',
+      ]);
+      check((Subject<Iterable<int>> it) => it.containsMatchingInOrder(
+              [(it) => it.equals(1), (it) => it.equals(2)]))
+          .description
+          .deepEquals([
+        '  contains, in order: [<A value that:',
+        '    equals <1>>,',
+        '  <A value that:',
+        '    equals <2>>]'
+      ]);
+    });
+  });
+
+  group('containsEqualInOrder', () {
+    test('succeeds for happy case', () {
+      check([0, 1, 0, 2, 0, 3]).containsEqualInOrder([1, 2, 3]);
+    });
+    test('fails for not found elements', () async {
+      check([0]).isRejectedBy((it) => it.containsEqualInOrder([1]), which: [
+        'did not have an element equal to the expectation at index 0 <1>'
+      ]);
+    });
+    test('can be described', () {
+      check((Subject<Iterable<int>> it) => it.containsEqualInOrder([1, 2, 3]))
+          .description
+          .deepEquals(['  contains, in order: [1, 2, 3]']);
+      check((Subject<Iterable<int>> it) => it.containsEqualInOrder([1, 2]))
+          .description
+          .deepEquals([
+        '  contains, in order: [1, 2]',
+      ]);
+    });
+  });
+  group('every', () {
+    test('succeeds for the happy path', () {
+      check(_testIterable).every((it) => it.isGreaterOrEqual(-1));
+    });
+
+    test('includes details of first failing element', () async {
+      check(_testIterable)
+          .isRejectedBy((it) => it.every((it) => it.isLessThan(0)), which: [
+        'has an element at index 0 that:',
+        '  Actual: <0>',
+        '  Which: is not less than <0>',
+      ]);
+    });
+  });
+
+  group('unorderedEquals', () {
+    test('success for happy case', () {
+      check(_testIterable).unorderedEquals(_testIterable.toList().reversed);
+    });
+
+    test('reports unmatched elements', () {
+      check(_testIterable).isRejectedBy(
+          (it) => it.unorderedEquals(_testIterable.followedBy([42, 100])),
+          which: [
+            'has no element equal to the expected element at index 2: <42>',
+            'or 1 other elements'
+          ]);
+    });
+
+    test('reports unexpected elements', () {
+      check(_testIterable.followedBy([42, 100]))
+          .isRejectedBy((it) => it.unorderedEquals(_testIterable), which: [
+        'has an unexpected element at index 2: <42>',
+        'and 1 other unexpected elements'
+      ]);
+    });
+  });
+
+  group('unorderedMatches', () {
+    test('success for happy case', () {
+      check(_testIterable).unorderedMatches(
+          _testIterable.toList().reversed.map((i) => (it) => it.equals(i)));
+    });
+
+    test('reports unmatched elements', () {
+      check(_testIterable).isRejectedBy(
+          (it) => it.unorderedMatches(_testIterable
+              .followedBy([42, 100]).map((i) => (it) => it.equals(i))),
+          which: [
+            'has no element matching the condition at index 2:',
+            '  equals <42>',
+            'or 1 other conditions'
+          ]);
+    });
+
+    test('reports unexpected elements', () {
+      check(_testIterable.followedBy([42, 100])).isRejectedBy(
+          (it) => it
+              .unorderedMatches(_testIterable.map((i) => (it) => it.equals(i))),
+          which: [
+            'has an unmatched element at index 2: <42>',
+            'and 1 other unmatched elements'
+          ]);
+    });
+  });
+
+  group('pairwiseMatches', () {
+    test('succeeds for the happy path', () {
+      check(_testIterable).pairwiseMatches([1, 2],
+          (expected) => (it) => it.isLessThan(expected), 'is less than');
+    });
+    test('fails for mismatched element', () async {
+      check(_testIterable).isRejectedBy(
+          (it) => it.pairwiseMatches([1, 1],
+              (expected) => (it) => it.isLessThan(expected), 'is less than'),
+          which: [
+            'does not have an element at index 1 that:',
+            '  is less than <1>',
+            'Actual element at index 1: <1>',
+            'Which: is not less than <1>'
+          ]);
+    });
+    test('fails for too few elements', () {
+      check(_testIterable).isRejectedBy(
+          (it) => it.pairwiseMatches([1, 2, 3],
+              (expected) => (it) => it.isLessThan(expected), 'is less than'),
+          which: [
+            'has too few elements, there is no element to match at index 2'
+          ]);
+    });
+    test('fails for too many elements', () {
+      check(_testIterable).isRejectedBy(
+          (it) => it.pairwiseMatches([1],
+              (expected) => (it) => it.isLessThan(expected), 'is less than'),
+          which: ['has too many elements, expected exactly 1']);
+    });
+  });
+}
diff --git a/pkgs/checks/test/extensions/map_test.dart b/pkgs/checks/test/extensions/map_test.dart
new file mode 100644
index 0000000..cb3e3e4
--- /dev/null
+++ b/pkgs/checks/test/extensions/map_test.dart
@@ -0,0 +1,119 @@
+// Copyright (c) 2022, 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:checks/checks.dart';
+import 'package:test/scaffolding.dart';
+
+import '../test_shared.dart';
+
+const _testMap = {
+  'a': 1,
+  'b': 2,
+};
+
+void main() {
+  test('length', () {
+    check(_testMap).length.equals(2);
+  });
+  test('entries', () {
+    check(_testMap).entries.any(
+          (it) => it
+            ..has((p0) => p0.key, 'key').equals('a')
+            ..has((p0) => p0.value, 'value').equals(1),
+        );
+  });
+  test('keys', () {
+    check(_testMap).keys.contains('a');
+  });
+  test('values', () {
+    check(_testMap).values.contains(1);
+  });
+
+  group('operator []', () {
+    test('succeeds for a key that exists', () {
+      check(_testMap)['a'].equals(1);
+    });
+    test('fails for a missing key', () {
+      check(_testMap).isRejectedBy((it) => it['z'],
+          which: ["does not contain the key 'z'"]);
+    });
+    test('can be described', () {
+      check((Subject<Map<String, Object>> it) => it['some\nlong\nkey'])
+          .description
+          .deepEquals([
+        "  contains a value for 'some",
+        '  long',
+        "  key'",
+      ]);
+      check((Subject<Map<String, Object>> it) =>
+          it['some\nlong\nkey'].equals(1)).description.deepEquals([
+        "  contains a value for 'some",
+        '  long',
+        "  key' that:",
+        '    equals <1>',
+      ]);
+    });
+  });
+  test('isEmpty', () {
+    check(<String, int>{}).isEmpty();
+    check(_testMap).isRejectedBy((it) => it.isEmpty(), which: ['is not empty']);
+  });
+  test('isNotEmpty', () {
+    check(_testMap).isNotEmpty();
+    check(<Object, Object>{})
+        .isRejectedBy((it) => it.isNotEmpty(), which: ['is not empty']);
+  });
+  group('containsKey', () {
+    test('succeeds for a key that exists', () {
+      check(_testMap).containsKey('a');
+    });
+    test('fails for a missing key', () {
+      check(_testMap).isRejectedBy(
+        (it) => it.containsKey('c'),
+        which: ["does not contain key 'c'"],
+      );
+    });
+    test('can be described', () {
+      check((Subject<Map<String, Object>> it) =>
+          it.containsKey('some\nlong\nkey')).description.deepEquals([
+        "  contains key 'some",
+        '  long',
+        "  key'",
+      ]);
+    });
+  });
+  test('containsKeyThat', () {
+    check(_testMap).containsKeyThat((it) => it.equals('a'));
+    check(_testMap).isRejectedBy(
+      (it) => it.containsKeyThat((it) => it.equals('c')),
+      which: ['Contains no matching key'],
+    );
+  });
+  group('containsValue', () {
+    test('succeeds for happy case', () {
+      check(_testMap).containsValue(1);
+    });
+    test('fails for missing value', () {
+      check(_testMap).isRejectedBy(
+        (it) => it.containsValue(3),
+        which: ['does not contain value <3>'],
+      );
+    });
+    test('can be described', () {
+      check((Subject<Map<String, String>> it) =>
+          it.containsValue('some\nlong\nkey')).description.deepEquals([
+        "  contains value 'some",
+        '  long',
+        "  key'",
+      ]);
+    });
+  });
+  test('containsValueThat', () {
+    check(_testMap).containsValueThat((it) => it.equals(1));
+    check(_testMap).isRejectedBy(
+      (it) => it.containsValueThat((it) => it.equals(3)),
+      which: ['Contains no matching value'],
+    );
+  });
+}
diff --git a/pkgs/checks/test/extensions/math_test.dart b/pkgs/checks/test/extensions/math_test.dart
new file mode 100644
index 0000000..0f346d3
--- /dev/null
+++ b/pkgs/checks/test/extensions/math_test.dart
@@ -0,0 +1,149 @@
+// Copyright (c) 2022, 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:checks/checks.dart';
+import 'package:test/scaffolding.dart';
+
+import '../test_shared.dart';
+
+void main() {
+  group('num checks', () {
+    group('isNaN', () {
+      test('succeeds for happy case', () {
+        check(double.nan).isNaN();
+      });
+      test('fails for ints', () {
+        check(42).isRejectedBy((it) => it.isNaN(), which: ['is a number']);
+      });
+      test('fails for numeric doubles', () {
+        check(42.1).isRejectedBy((it) => it.isNaN(), which: ['is a number']);
+      });
+    });
+
+    group('isNotNan', () {
+      test('succeeds for ints', () {
+        check(42).isNotNaN();
+      });
+      test('succeeds numeric doubles', () {
+        check(42.1).isNotNaN();
+      });
+      test('fails for NaN', () {
+        check(double.nan).isRejectedBy((it) => it.isNotNaN(),
+            which: ['is not a number (NaN)']);
+      });
+    });
+    group('isNegative', () {
+      test('succeeds for negative ints', () {
+        check(-1).isNegative();
+      });
+      test('succeeds for -0.0', () {
+        check(-0.0).isNegative();
+      });
+      test('fails for zero', () {
+        check(0)
+            .isRejectedBy((it) => it.isNegative(), which: ['is not negative']);
+      });
+    });
+    group('isNotNegative', () {
+      test('succeeds for positive ints', () {
+        check(1).isNotNegative();
+      });
+      test('succeeds for 0', () {
+        check(0).isNotNegative();
+      });
+      test('fails for -0.0', () {
+        check(-0.0)
+            .isRejectedBy((it) => it.isNotNegative(), which: ['is negative']);
+      });
+      test('fails for negative numbers', () {
+        check(-1)
+            .isRejectedBy((it) => it.isNotNegative(), which: ['is negative']);
+      });
+    });
+
+    group('isFinite', () {
+      test('succeeds for finite numbers', () {
+        check(1).isFinite();
+      });
+      test('fails for NaN', () {
+        check(double.nan)
+            .isRejectedBy((it) => it.isFinite(), which: ['is not finite']);
+      });
+      test('fails for infinity', () {
+        check(double.infinity)
+            .isRejectedBy((it) => it.isFinite(), which: ['is not finite']);
+      });
+      test('fails for negative infinity', () {
+        check(double.negativeInfinity)
+            .isRejectedBy((it) => it.isFinite(), which: ['is not finite']);
+      });
+    });
+    group('isNotFinite', () {
+      test('succeeds for infinity', () {
+        check(double.infinity).isNotFinite();
+      });
+      test('succeeds for negative infinity', () {
+        check(double.negativeInfinity).isNotFinite();
+      });
+      test('succeeds for NaN', () {
+        check(double.nan).isNotFinite();
+      });
+      test('fails for finite numbers', () {
+        check(1).isRejectedBy((it) => it.isNotFinite(), which: ['is finite']);
+      });
+    });
+    group('isInfinite', () {
+      test('succeeds for infinity', () {
+        check(double.infinity).isInfinite();
+      });
+      test('succeeds for negative infinity', () {
+        check(double.negativeInfinity).isInfinite();
+      });
+      test('fails for NaN', () {
+        check(double.nan)
+            .isRejectedBy((it) => it.isInfinite(), which: ['is not infinite']);
+      });
+      test('fails for finite numbers', () {
+        check(1)
+            .isRejectedBy((it) => it.isInfinite(), which: ['is not infinite']);
+      });
+    });
+
+    group('isNotInfinite', () {
+      test('succeeds for finite numbers', () {
+        check(1).isNotInfinite();
+      });
+      test('succeeds for NaN', () {
+        check(double.nan).isNotInfinite();
+      });
+      test('fails for infinity', () {
+        check(double.infinity)
+            .isRejectedBy((it) => it.isNotInfinite(), which: ['is infinite']);
+      });
+      test('fails for negative infinity', () {
+        check(double.negativeInfinity)
+            .isRejectedBy((it) => it.isNotInfinite(), which: ['is infinite']);
+      });
+    });
+    group('closeTo', () {
+      test('succeeds for equal numbers', () {
+        check(1).isCloseTo(1, 1);
+      });
+      test('succeeds at less than delta away', () {
+        check(1).isCloseTo(2, 2);
+      });
+      test('succeeds at exactly delta away', () {
+        check(1).isCloseTo(2, 1);
+      });
+      test('fails for low values', () {
+        check(1).isRejectedBy((it) => it.isCloseTo(3, 1),
+            which: ['differs by <2>']);
+      });
+      test('fails for high values', () {
+        check(5).isRejectedBy((it) => it.isCloseTo(3, 1),
+            which: ['differs by <2>']);
+      });
+    });
+  });
+}
diff --git a/pkgs/checks/test/extensions/string_test.dart b/pkgs/checks/test/extensions/string_test.dart
new file mode 100644
index 0000000..2b651f1
--- /dev/null
+++ b/pkgs/checks/test/extensions/string_test.dart
@@ -0,0 +1,200 @@
+// Copyright (c) 2022, 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:checks/checks.dart';
+import 'package:test/scaffolding.dart';
+
+import '../test_shared.dart';
+
+void main() {
+  group('StringChecks', () {
+    test('contains', () {
+      check('bob').contains('bo');
+      check('bob').isRejectedBy((it) => it.contains('kayleb'),
+          which: ["Does not contain 'kayleb'"]);
+    });
+    test('length', () {
+      check('bob').length.equals(3);
+    });
+    test('isEmpty', () {
+      check('').isEmpty();
+      check('bob').isRejectedBy((it) => it.isEmpty(), which: ['is not empty']);
+    });
+    test('isNotEmpty', () {
+      check('bob').isNotEmpty();
+      check('').isRejectedBy((it) => it.isNotEmpty(), which: ['is empty']);
+    });
+    test('startsWith', () {
+      check('bob').startsWith('bo');
+      check('bob').isRejectedBy((it) => it.startsWith('kayleb'),
+          which: ["does not start with 'kayleb'"]);
+    });
+    test('endsWith', () {
+      check('bob').endsWith('ob');
+      check('bob').isRejectedBy((it) => it.endsWith('kayleb'),
+          which: ["does not end with 'kayleb'"]);
+    });
+
+    group('matches', () {
+      test('succeeds for strings that match a regex', () {
+        check('123').matchesPattern(RegExp(r'\d\d\d'));
+      });
+      test('succeeds for strings that match a string pattern', () {
+        check(r'\d').matchesPattern(r'\d');
+      });
+      test('fails for non-matching regex', () {
+        check('abc').isRejectedBy((it) => it.matchesPattern(RegExp(r'\d\d\d')),
+            which: [r'does not match <RegExp: pattern=\d\d\d flags=>']);
+      });
+      test('fails for non-matching string pattern', () {
+        // A string is _not_ converted to a regex, string patterns must match
+        // directly.
+        check('123').isRejectedBy((it) => it.matchesPattern(r'\d\d\d'),
+            which: [r"does not match '\\d\\d\\d'"]);
+      });
+      test('can be described', () {
+        check((Subject<String> it) => it.matchesPattern(RegExp(r'\d\d\d')))
+            .description
+            .deepEquals([r'  matches <RegExp: pattern=\d\d\d flags=>']);
+        check((Subject<String> it) => it.matchesPattern('abc'))
+            .description
+            .deepEquals([r"  matches 'abc'"]);
+      });
+    });
+
+    group('containsInOrder', () {
+      test('happy case', () {
+        check('foo bar baz').containsInOrder(['foo', 'baz']);
+      });
+      test('reports when first substring is missing', () {
+        check('baz').isRejectedBy((it) => it.containsInOrder(['foo', 'baz']),
+            which: ['does not have a match for the substring \'foo\'']);
+      });
+      test('reports when substring is missing following a match', () {
+        check('foo bar')
+            .isRejectedBy((it) => it.containsInOrder(['foo', 'baz']), which: [
+          'does not have a match for the substring \'baz\'',
+          'following the other matches up to character 3'
+        ]);
+      });
+    });
+
+    group('equals', () {
+      test('succeeeds for happy case', () {
+        check('foo').equals('foo');
+      });
+      test('succeeeds for equal empty strings', () {
+        check('').equals('');
+      });
+      test('reports extra characters for long string', () {
+        check('foobar').isRejectedBy((it) => it.equals('foo'),
+            which: ['is too long with unexpected trailing characters:', 'bar']);
+      });
+      test('reports extra characters for long string against empty', () {
+        check('foo').isRejectedBy((it) => it.equals(''),
+            which: ['is not the empty string']);
+      });
+      test('reports truncated extra characters for very long string', () {
+        check('foobar baz more stuff').isRejectedBy((it) => it.equals('foo'),
+            which: [
+              'is too long with unexpected trailing characters:',
+              'bar baz mo ...'
+            ]);
+      });
+      test('reports missing characters for short string', () {
+        check('foo').isRejectedBy((it) => it.equals('foobar'),
+            which: ['is too short with missing trailing characters:', 'bar']);
+      });
+      test('reports missing characters for empty string', () {
+        check('').isRejectedBy((it) => it.equals('foo bar baz'),
+            actual: ['an empty string'],
+            which: ['is missing all expected characters:', 'foo bar ba ...']);
+      });
+      test('reports truncated missing characters for very short string', () {
+        check('foo').isRejectedBy((it) => it.equals('foobar baz more stuff'),
+            which: [
+              'is too short with missing trailing characters:',
+              'bar baz mo ...'
+            ]);
+      });
+      test('reports index of different character', () {
+        check('hit').isRejectedBy((it) => it.equals('hat'), which: [
+          'differs at offset 1:',
+          'hat',
+          'hit',
+          ' ^',
+        ]);
+      });
+      test('reports truncated index of different character in large string',
+          () {
+        check('blah blah blah hit blah blah blah').isRejectedBy(
+            (it) => it.equals('blah blah blah hat blah blah blah'),
+            which: [
+              'differs at offset 16:',
+              '... lah blah hat blah bl ...',
+              '... lah blah hit blah bl ...',
+              '              ^',
+            ]);
+      });
+    });
+
+    group('equalsIgnoringCase', () {
+      test('succeeeds for happy case', () {
+        check('FOO').equalsIgnoringCase('foo');
+        check('foo').equalsIgnoringCase('FOO');
+      });
+      test('reports original extra characters for long string', () {
+        check('FOOBAR').isRejectedBy((it) => it.equalsIgnoringCase('foo'),
+            which: ['is too long with unexpected trailing characters:', 'BAR']);
+      });
+      test('reports original missing characters for short string', () {
+        check('FOO').isRejectedBy((it) => it.equalsIgnoringCase('fooBAR'),
+            which: ['is too short with missing trailing characters:', 'BAR']);
+      });
+      test('reports index of different character with original characters', () {
+        check('HiT').isRejectedBy((it) => it.equalsIgnoringCase('hAt'), which: [
+          'differs at offset 1:',
+          'hAt',
+          'HiT',
+          ' ^',
+        ]);
+      });
+    });
+
+    group('equalsIgnoringWhitespace', () {
+      test('allows differing internal whitespace', () {
+        check('foo \t\n bar').equalsIgnoringWhitespace('foo bar');
+      });
+      test('allows extra leading/trailing whitespace', () {
+        check(' foo ').equalsIgnoringWhitespace('foo');
+      });
+      test('allows missing leading/trailing whitespace', () {
+        check('foo').equalsIgnoringWhitespace(' foo ');
+      });
+      test('reports original extra characters for long string', () {
+        check('foo \t bar \n baz').isRejectedBy(
+            (it) => it.equalsIgnoringWhitespace('foo bar'),
+            which: [
+              'is too long with unexpected trailing characters:',
+              ' baz'
+            ]);
+      });
+      test('reports original missing characters for short string', () {
+        check('foo  bar').isRejectedBy(
+            (it) => it.equalsIgnoringWhitespace('foo bar baz'),
+            which: ['is too short with missing trailing characters:', ' baz']);
+      });
+      test('reports index of different character with original characters', () {
+        check('x  hit  x').isRejectedBy(
+            (it) => it.equalsIgnoringWhitespace('x hat x'),
+            which: [
+              'differs at offset 3:',
+              'x hat x',
+              'x hit x',
+              '   ^',
+            ]);
+      });
+    });
+  });
+}
diff --git a/pkgs/checks/test/failure_message_test.dart b/pkgs/checks/test/failure_message_test.dart
new file mode 100644
index 0000000..ee5401c
--- /dev/null
+++ b/pkgs/checks/test/failure_message_test.dart
@@ -0,0 +1,53 @@
+import 'package:checks/checks.dart';
+import 'package:test/scaffolding.dart';
+import 'package:test_api/hooks.dart' show TestFailure;
+
+void main() {
+  group('failures', () {
+    test('includes expected, actual, and which', () {
+      check(() {
+        check(1).isGreaterThan(2);
+      }).throwsFailure().equals('''
+Expected: a int that:
+  is greater than <2>
+Actual: <1>
+Which: is not greater than <2>''');
+    });
+
+    test('includes matching portions of actual', () {
+      check(() {
+        check(<dynamic>[]).length.equals(1);
+      }).throwsFailure().equals('''
+Expected: a List<dynamic> that:
+  has length that:
+    equals <1>
+Actual: a List<dynamic> that:
+  has length that:
+  Actual: <0>
+  Which: are not equal''');
+    });
+
+    test('include a reason when provided', () {
+      check(() {
+        check(because: 'Some reason', 1).isGreaterThan(2);
+      }).throwsFailure().endsWith('Reason: Some reason');
+    });
+
+    test('retain type label following isNotNull', () {
+      check(() {
+        check<int?>(1).isNotNull().isGreaterThan(2);
+      }).throwsFailure().startsWith('Expected: a int? that:\n');
+    });
+
+    test('retain reason following isNotNull', () {
+      check(() {
+        check<int?>(because: 'Some reason', 1).isNotNull().isGreaterThan(2);
+      }).throwsFailure().endsWith('Reason: Some reason');
+    });
+  });
+}
+
+extension on Subject<void Function()> {
+  Subject<String> throwsFailure() =>
+      throws<TestFailure>().has((f) => f.message, 'message').isNotNull();
+}
diff --git a/pkgs/checks/test/soft_check_test.dart b/pkgs/checks/test/soft_check_test.dart
new file mode 100644
index 0000000..d919b0d
--- /dev/null
+++ b/pkgs/checks/test/soft_check_test.dart
@@ -0,0 +1,30 @@
+// Copyright (c) 2023, 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:checks/checks.dart';
+import 'package:test/scaffolding.dart';
+
+import 'test_shared.dart';
+
+void main() {
+  group('softCheck', () {
+    test('returns the first failure', () {
+      check(0).isRejectedBy(
+          (it) => it
+            ..isGreaterThan(1)
+            ..isGreaterThan(2),
+          which: ['is not greater than <1>']);
+    });
+  });
+  group('softCheckAsync', () {
+    test('returns the first failure', () async {
+      await check(Future.value(0)).isRejectedByAsync(
+          (it) => it
+            ..completes((it) => it.isGreaterThan(1))
+            ..completes((it) => it.isGreaterThan(2)),
+          actual: ['<0>'],
+          which: ['is not greater than <1>']);
+    });
+  });
+}
diff --git a/pkgs/checks/test/test_shared.dart b/pkgs/checks/test/test_shared.dart
new file mode 100644
index 0000000..87401ca
--- /dev/null
+++ b/pkgs/checks/test/test_shared.dart
@@ -0,0 +1,92 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:checks/checks.dart';
+import 'package:checks/context.dart';
+
+extension RejectionChecks<T> on Subject<T> {
+  void isRejectedBy(Condition<T> condition,
+      {Iterable<String>? actual, Iterable<String>? which}) {
+    late T actualValue;
+    var didRunCallback = false;
+    final rejection = context.nest<Rejection>(
+        () => ['does not meet a condition with a Rejection'], (value) {
+      actualValue = value;
+      didRunCallback = true;
+      final failure = softCheck(value, condition);
+      if (failure == null) {
+        return Extracted.rejection(which: [
+          'was accepted by the condition checking:',
+          ...describe(condition)
+        ]);
+      }
+      return Extracted.value(failure.rejection);
+    });
+    if (didRunCallback) {
+      rejection
+          .has((r) => r.actual, 'actual')
+          .deepEquals(actual ?? literal(actualValue));
+    } else {
+      rejection
+          .has((r) => r.actual, 'actual')
+          .context
+          .expect(() => ['is left default'], (_) => null);
+    }
+    if (which == null) {
+      rejection.has((r) => r.which, 'which').isNull();
+    } else {
+      rejection.has((r) => r.which, 'which').isNotNull().deepEquals(which);
+    }
+  }
+
+  Future<void> isRejectedByAsync(Condition<T> condition,
+      {Iterable<String>? actual, Iterable<String>? which}) async {
+    late T actualValue;
+    var didRunCallback = false;
+    await context.nestAsync<Rejection>(
+        () => ['does not meet an async condition with a Rejection'],
+        (value) async {
+      actualValue = value;
+      didRunCallback = true;
+      final failure = await softCheckAsync(value, condition);
+      if (failure == null) {
+        return Extracted.rejection(which: [
+          'was accepted by the condition checking:',
+          ...await describeAsync(condition)
+        ]);
+      }
+      return Extracted.value(failure.rejection);
+    }, (rejection) {
+      if (didRunCallback) {
+        rejection
+            .has((r) => r.actual, 'actual')
+            .deepEquals(actual ?? literal(actualValue));
+      } else {
+        rejection
+            .has((r) => r.actual, 'actual')
+            .context
+            .expect(() => ['is left default'], (_) => null);
+      }
+      if (which == null) {
+        rejection.has((r) => r.which, 'which').isNull();
+      } else {
+        rejection.has((r) => r.which, 'which').isNotNull().deepEquals(which);
+      }
+    });
+  }
+}
+
+extension ConditionChecks<T> on Subject<Condition<T>> {
+  Subject<Iterable<String>> get description =>
+      has((c) => describe<T>(c), 'description');
+  Future<void> hasAsyncDescriptionWhich(
+          Condition<Iterable<String>> descriptionCondition) =>
+      context.nestAsync(
+          () => ['has description'],
+          (condition) async =>
+              Extracted.value(await describeAsync<T>(condition)),
+          descriptionCondition);
+}
diff --git a/pkgs/fake_async/.gitignore b/pkgs/fake_async/.gitignore
new file mode 100644
index 0000000..63fe85d
--- /dev/null
+++ b/pkgs/fake_async/.gitignore
@@ -0,0 +1,5 @@
+.packages
+.pub/
+.dart_tool/
+build/
+pubspec.lock
diff --git a/pkgs/fake_async/AUTHORS b/pkgs/fake_async/AUTHORS
new file mode 100644
index 0000000..e8063a8
--- /dev/null
+++ b/pkgs/fake_async/AUTHORS
@@ -0,0 +1,6 @@
+# Below is a list of people and organizations that have contributed
+# to the project. Names should be added to the list like so:
+#
+#   Name/Organization <email address>
+
+Google Inc.
diff --git a/pkgs/fake_async/CHANGELOG.md b/pkgs/fake_async/CHANGELOG.md
new file mode 100644
index 0000000..0f964ee
--- /dev/null
+++ b/pkgs/fake_async/CHANGELOG.md
@@ -0,0 +1,102 @@
+## 1.3.2
+
+* Require Dart 3.3
+* Fix bug where a `flushTimers` or `elapse` call from within
+  the callback of a periodic timer would immediately invoke
+  the same timer.
+* Move to `dart-lang/test` monorepo.
+* Require Dart 3.5.
+
+## 1.3.1
+
+* Populate the pubspec `repository` field.
+
+## 1.3.0
+
+* `FakeTimer.tick` will return a value instead of throwing.
+* `FakeAsync.includeTimerStackTrace` allows controlling whether timers created
+   with a FakeAsync will include a creation Stack Trace.
+
+## 1.2.0
+
+* Stable release for null safety.
+
+## 1.2.0-nullsafety.3
+
+* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release
+  guidelines.
+
+## 1.2.0-nullsafety.2
+
+* Allow prerelease versions of the 2.12 sdk.
+
+## 1.2.0-nullsafety.1
+
+* Allow 2.10 stable and 2.11.0 dev SDK versions.
+
+## 1.2.0-nullsafety
+
+Pre-release for the null safety migration of this package.
+
+Note that `1.2.0` may not be the final stable null safety release version,
+we reserve the right to release it as a `2.0.0` breaking change.
+
+This release will be pinned to only allow pre-release sdk versions starting
+from `2.10.0-0`.
+
+## 1.1.0
+
+* Exposed the `FakeTimer` class as a public class.
+* Added `FakeAsync.pendingTimers` which gives access to all pending timers at
+  the time of the call.
+
+## 1.0.2
+
+* Update min SDK to 2.2.0
+
+## 1.0.1
+
+* Update to lowercase Dart core library constants.
+* Fix use of deprecated `isInstanceOf` matcher.
+
+## 1.0.0
+
+This release contains the `FakeAsync` class that was defined in [`quiver`][].
+It's backwards-compatible with both the `quiver` version *and* the old version
+of the `fake_async` package.
+
+[`quiver`]: https://pub.dev/packages/quiver
+
+### New Features
+
+* A top-level `fakeAsync()` function was added that encapsulates
+  `new FakeAsync().run(...)`.
+
+### New Features Relative to `quiver`
+
+* `FakeAsync.elapsed` returns the total amount of fake time elapsed since the
+  `FakeAsync` instance was created.
+
+* `new FakeAsync()` now takes an `initialTime` argument that sets the default
+  time for clocks created with `FakeAsync.getClock()`, and for the `clock`
+  package's top-level `clock` variable.
+
+### New Features Relative to `fake_async` 0.1
+
+* `FakeAsync.periodicTimerCount`, `FakeAsync.nonPeriodicTimerCount`, and
+  `FakeAsync.microtaskCount` provide visibility into the events scheduled within
+  `FakeAsync.run()`.
+
+* `FakeAsync.getClock()` provides access to fully-featured `Clock` objects based
+  on `FakeAsync`'s elapsed time.
+
+* `FakeAsync.flushMicrotasks()` empties the microtask queue without elapsing any
+  time or running any timers.
+
+* `FakeAsync.flushTimers()` runs all microtasks and timers until there are no
+  more scheduled.
+
+## 0.1.2
+
+* Integrate with the clock package.
+
diff --git a/pkgs/fake_async/LICENSE b/pkgs/fake_async/LICENSE
new file mode 100644
index 0000000..7a4a3ea
--- /dev/null
+++ b/pkgs/fake_async/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
\ No newline at end of file
diff --git a/pkgs/fake_async/README.md b/pkgs/fake_async/README.md
new file mode 100644
index 0000000..c7bb40b
--- /dev/null
+++ b/pkgs/fake_async/README.md
@@ -0,0 +1,51 @@
+[![pub package](https://img.shields.io/pub/v/fake_async.svg)](https://pub.dev/packages/fake_async)
+[![package publisher](https://img.shields.io/pub/publisher/fake_async.svg)](https://pub.dev/packages/fake_async/publisher)
+
+This package provides a [`FakeAsync`][] class, which makes it easy to
+deterministically test code that uses asynchronous features like `Future`s,
+`Stream`s, `Timer`s, and microtasks. It creates an environment in which the user
+can explicitly control Dart's notion of the "current time". When the time is
+advanced, `FakeAsync` fires all asynchronous events that are scheduled for that
+time period without actually needing the test to wait for real time to elapse.
+
+[`FakeAsync`]: https://www.dartdocs.org/documentation/fake_async/latest/fake_async/FakeAsync-class.html
+
+For example:
+
+```dart
+import 'dart:async';
+
+import 'package:fake_async/fake_async.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test("Future.timeout() throws an error once the timeout is up", () {
+    // Any code run within [fakeAsync] is run within the context of the
+    // [FakeAsync] object passed to the callback.
+    fakeAsync((async) {
+      // All asynchronous features that rely on timing are automatically
+      // controlled by [fakeAsync].
+      expect(Completer().future.timeout(Duration(seconds: 5)),
+          throwsA(isA<TimeoutException>()));
+
+      // This will cause the timeout above to fire immediately, without waiting
+      // 5 seconds of real time.
+      async.elapse(Duration(seconds: 5));
+    });
+  });
+}
+```
+
+## Integration With `clock`
+
+`FakeAsync` can't control the time reported by [`DateTime.now()`][] or by
+the [`Stopwatch`][] class, since they're not part of `dart:async`. However, if
+you create them using the [`clock`][] package's [`clock.now()`][] or
+[`clock.stopwatch()`][] functions, `FakeAsync` will automatically override
+them to use the same notion of time as `dart:async` classes.
+
+[`DateTime.now()`]: https://api.dart.dev/stable/dart-core/DateTime/DateTime.now.html
+[`Stopwatch`]: https://api.dart.dev/stable/dart-core/Stopwatch-class.html
+[`clock`]: https://pub.dev/packages/clock
+[`clock.now()`]: https://pub.dev/documentation/clock/latest/clock/Clock/now.html
+[`clock.stopwatch()`]: https://pub.dev/documentation/clock/latest/clock/Clock/stopwatch.html
diff --git a/pkgs/fake_async/analysis_options.yaml b/pkgs/fake_async/analysis_options.yaml
new file mode 100644
index 0000000..4e1b8e8
--- /dev/null
+++ b/pkgs/fake_async/analysis_options.yaml
@@ -0,0 +1,21 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
+analyzer:
+  language:
+    strict-casts: true
+
+linter:
+  rules:
+    - avoid_bool_literals_in_conditional_expressions
+    - avoid_classes_with_only_static_members
+    - avoid_returning_this
+    - avoid_unused_constructor_parameters
+    - cancel_subscriptions
+    - cascade_invocations
+    - comment_references
+    - join_return_with_assignment
+    - literal_only_boolean_expressions
+    - no_adjacent_strings_in_list
+    - prefer_const_constructors
+    - prefer_final_locals
+    - test_types_in_equals
+    - unnecessary_await_in_return
diff --git a/pkgs/fake_async/lib/fake_async.dart b/pkgs/fake_async/lib/fake_async.dart
new file mode 100644
index 0000000..b4eea85
--- /dev/null
+++ b/pkgs/fake_async/lib/fake_async.dart
@@ -0,0 +1,332 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async';
+import 'dart:collection';
+
+import 'package:clock/clock.dart';
+import 'package:collection/collection.dart';
+
+/// The type of a microtask callback.
+typedef _Microtask = void Function();
+
+/// Runs [callback] in a [Zone] where all asynchrony is controlled by an
+/// instance of [FakeAsync].
+///
+/// All [Future]s, [Stream]s, [Timer]s, microtasks, and other time-based
+/// asynchronous features used within [callback] are controlled by calls to
+/// [FakeAsync.elapse] rather than the passing of real time.
+///
+/// The [`clock`][] property will be set to a clock that reports the fake
+/// elapsed time. By default, it starts at the time [fakeAsync] was created
+/// (according to [`clock.now()`][]), but this can be controlled by passing
+/// [initialTime].
+///
+/// [`clock`]: https://www.dartdocs.org/documentation/clock/latest/clock/clock.html
+/// [`clock.now()`]: https://www.dartdocs.org/documentation/clock/latest/clock/Clock/now.html
+///
+/// Returns the result of [callback].
+T fakeAsync<T>(T Function(FakeAsync async) callback, {DateTime? initialTime}) =>
+    FakeAsync(initialTime: initialTime).run(callback);
+
+/// A class that mocks out the passage of time within a [Zone].
+///
+/// Test code can be passed as a callback to [run], which causes it to be run in
+/// a [Zone] which fakes timer and microtask creation, such that they are run
+/// during calls to [elapse] which simulates the asynchronous passage of time.
+///
+/// The synchronous passage of time (as from blocking or expensive calls) can
+/// also be simulated using [elapseBlocking].
+class FakeAsync {
+  /// The value of [clock] within [run].
+  late final Clock _clock;
+
+  /// The amount of fake time that's elapsed since this [FakeAsync] was
+  /// created.
+  Duration get elapsed => _elapsed;
+  var _elapsed = Duration.zero;
+
+  /// Whether Timers created by this FakeAsync will include a creation stack
+  /// trace in [FakeAsync.pendingTimersDebugString].
+  final bool includeTimerStackTrace;
+
+  /// The fake time at which the current call to [elapse] will finish running.
+  ///
+  /// This is `null` if there's no current call to [elapse].
+  Duration? _elapsingTo;
+
+  /// Tasks that are scheduled to run when fake time progresses.
+  final _microtasks = Queue<_Microtask>();
+
+  /// All timers created within [run].
+  final _timers = <FakeTimer>{};
+
+  /// All the current pending timers.
+  List<FakeTimer> get pendingTimers => _timers.toList(growable: false);
+
+  /// The debug strings for all the current pending timers.
+  List<String> get pendingTimersDebugString =>
+      pendingTimers.map((timer) => timer.debugString).toList(growable: false);
+
+  /// The number of active periodic timers created within a call to [run] or
+  /// [fakeAsync].
+  int get periodicTimerCount =>
+      _timers.where((timer) => timer.isPeriodic).length;
+
+  /// The number of active non-periodic timers created within a call to [run] or
+  /// [fakeAsync].
+  int get nonPeriodicTimerCount =>
+      _timers.where((timer) => !timer.isPeriodic).length;
+
+  /// The number of pending microtasks scheduled within a call to [run] or
+  /// [fakeAsync].
+  int get microtaskCount => _microtasks.length;
+
+  /// Creates a [FakeAsync].
+  ///
+  /// Within [run], the [`clock`][] property will start at [initialTime] and
+  /// move forward as fake time elapses.
+  ///
+  /// [`clock`]: https://www.dartdocs.org/documentation/clock/latest/clock/clock.html
+  ///
+  /// Note: it's usually more convenient to use [fakeAsync] rather than creating
+  /// a [FakeAsync] object and calling [run] manually.
+  FakeAsync({DateTime? initialTime, this.includeTimerStackTrace = true}) {
+    final nonNullInitialTime = initialTime ?? clock.now();
+    _clock = Clock(() => nonNullInitialTime.add(elapsed));
+  }
+
+  /// Returns a fake [Clock] whose time can is elapsed by calls to [elapse] and
+  /// [elapseBlocking].
+  ///
+  /// The returned clock starts at [initialTime] plus the fake time that's
+  /// already been elapsed. Further calls to [elapse] and [elapseBlocking] will
+  /// advance the clock as well.
+  ///
+  /// Note that it's usually easier to use the top-level [`clock`][] property.
+  /// Only call this function if you want a different [initialTime] than the
+  /// default.
+  ///
+  /// [`clock`]: https://www.dartdocs.org/documentation/clock/latest/clock/clock.html
+  Clock getClock(DateTime initialTime) =>
+      Clock(() => initialTime.add(_elapsed));
+
+  /// Simulates the asynchronous passage of time.
+  ///
+  /// Throws an [ArgumentError] if [duration] is negative. Throws a [StateError]
+  /// if a previous call to [elapse] has not yet completed.
+  ///
+  /// Any timers created within [run] or [fakeAsync] will fire if their time is
+  /// within [duration]. The microtask queue is processed before and after each
+  /// timer fires.
+  void elapse(Duration duration) {
+    if (duration.inMicroseconds < 0) {
+      throw ArgumentError.value(duration, 'duration', 'may not be negative');
+    } else if (_elapsingTo != null) {
+      throw StateError('Cannot elapse until previous elapse is complete.');
+    }
+
+    _elapsingTo = _elapsed + duration;
+    _fireTimersWhile((next) => next._nextCall <= _elapsingTo!);
+    _elapseTo(_elapsingTo!);
+    _elapsingTo = null;
+  }
+
+  /// Simulates the synchronous passage of time, resulting from blocking or
+  /// expensive calls.
+  ///
+  /// Neither timers nor microtasks are run during this call, but if this is
+  /// called within [elapse] they may fire afterwards.
+  ///
+  /// Throws an [ArgumentError] if [duration] is negative.
+  void elapseBlocking(Duration duration) {
+    if (duration.inMicroseconds < 0) {
+      throw ArgumentError('Cannot call elapse with negative duration');
+    }
+
+    _elapsed += duration;
+    final elapsingTo = _elapsingTo;
+    if (elapsingTo != null && _elapsed > elapsingTo) _elapsingTo = _elapsed;
+  }
+
+  /// Runs [callback] in a [Zone] where all asynchrony is controlled by `this`.
+  ///
+  /// All [Future]s, [Stream]s, [Timer]s, microtasks, and other time-based
+  /// asynchronous features used within [callback] are controlled by calls to
+  /// [elapse] rather than the passing of real time.
+  ///
+  /// The [`clock`][] property will be set to a clock that reports the fake
+  /// elapsed time. By default, it starts at the time the [FakeAsync] was
+  /// created (according to [`clock.now()`][]), but this can be controlled by
+  /// passing `initialTime` to [FakeAsync.new].
+  ///
+  /// [`clock`]: https://www.dartdocs.org/documentation/clock/latest/clock/clock.html
+  /// [`clock.now()`]: https://www.dartdocs.org/documentation/clock/latest/clock/Clock/now.html
+  ///
+  /// Calls [callback] with `this` as argument and returns its result.
+  ///
+  /// Note: it's usually more convenient to use [fakeAsync] rather than creating
+  /// a [FakeAsync] object and calling [run] manually.
+  T run<T>(T Function(FakeAsync self) callback) =>
+      runZoned(() => withClock(_clock, () => callback(this)),
+          zoneSpecification: ZoneSpecification(
+              createTimer: (_, __, ___, duration, callback) =>
+                  _createTimer(duration, callback, false),
+              createPeriodicTimer: (_, __, ___, duration, callback) =>
+                  _createTimer(duration, callback, true),
+              scheduleMicrotask: (_, __, ___, microtask) =>
+                  _microtasks.add(microtask)));
+
+  /// Runs all pending microtasks scheduled within a call to [run] or
+  /// [fakeAsync] until there are no more microtasks scheduled.
+  ///
+  /// Does not run timers.
+  void flushMicrotasks() {
+    while (_microtasks.isNotEmpty) {
+      _microtasks.removeFirst()();
+    }
+  }
+
+  /// Elapses time until there are no more active timers.
+  ///
+  /// If `flushPeriodicTimers` is `true` (the default), this will repeatedly run
+  /// periodic timers until they're explicitly canceled. Otherwise, this will
+  /// stop when the only active timers are periodic.
+  ///
+  /// The [timeout] controls how much fake time may elapse before a [StateError]
+  /// is thrown. This ensures that a periodic timer doesn't cause this method to
+  /// deadlock. It defaults to one hour.
+  void flushTimers(
+      {Duration timeout = const Duration(hours: 1),
+      bool flushPeriodicTimers = true}) {
+    final absoluteTimeout = _elapsed + timeout;
+    _fireTimersWhile((timer) {
+      if (timer._nextCall > absoluteTimeout) {
+        // TODO(nweiz): Make this a [TimeoutException].
+        throw StateError('Exceeded timeout $timeout while flushing timers');
+      }
+
+      if (flushPeriodicTimers) return _timers.isNotEmpty;
+
+      // Continue firing timers until the only ones left are periodic *and*
+      // every periodic timer has had a change to run against the final
+      // value of [_elapsed].
+      return _timers
+          .any((timer) => !timer.isPeriodic || timer._nextCall <= _elapsed);
+    });
+  }
+
+  /// Invoke the callback for each timer until [predicate] returns `false` for
+  /// the next timer that would be fired.
+  ///
+  /// Microtasks are flushed before and after each timer is fired. Before each
+  /// timer fires, [_elapsed] is updated to the appropriate duration.
+  void _fireTimersWhile(bool Function(FakeTimer timer) predicate) {
+    flushMicrotasks();
+    for (;;) {
+      if (_timers.isEmpty) break;
+
+      final timer = minBy(_timers, (FakeTimer timer) => timer._nextCall)!;
+      if (!predicate(timer)) break;
+
+      _elapseTo(timer._nextCall);
+      timer._fire();
+      flushMicrotasks();
+    }
+  }
+
+  /// Creates a new timer controlled by `this` that fires [callback] after
+  /// [duration] (or every [duration] if [periodic] is `true`).
+  Timer _createTimer(Duration duration, Function callback, bool periodic) {
+    final timer = FakeTimer._(duration, callback, periodic, this,
+        includeStackTrace: includeTimerStackTrace);
+    _timers.add(timer);
+    return timer;
+  }
+
+  /// Sets [_elapsed] to [to] if [to] is longer than [_elapsed].
+  void _elapseTo(Duration to) {
+    if (to > _elapsed) _elapsed = to;
+  }
+}
+
+/// An implementation of [Timer] that's controlled by a [FakeAsync].
+class FakeTimer implements Timer {
+  /// If this is periodic, the time that should elapse between firings of this
+  /// timer.
+  ///
+  /// This is not used by non-periodic timers.
+  final Duration duration;
+
+  /// The callback to invoke when the timer fires.
+  ///
+  /// For periodic timers, this is a `void Function(Timer)`. For non-periodic
+  /// timers, it's a `void Function()`.
+  final Function _callback;
+
+  /// Whether this is a periodic timer.
+  final bool isPeriodic;
+
+  /// The [FakeAsync] instance that controls this timer.
+  final FakeAsync _async;
+
+  /// The value of [FakeAsync._elapsed] at (or after) which this timer should be
+  /// fired.
+  late Duration _nextCall;
+
+  /// The current stack trace when this timer was created.
+  ///
+  /// If [FakeAsync.includeTimerStackTrace] is set to false then accessing
+  /// this field will throw a [TypeError].
+  StackTrace get creationStackTrace => _creationStackTrace!;
+  final StackTrace? _creationStackTrace;
+
+  var _tick = 0;
+
+  @override
+  int get tick => _tick;
+
+  /// Returns debugging information to try to identify the source of the
+  /// [Timer].
+  String get debugString => 'Timer (duration: $duration, periodic: $isPeriodic)'
+      '${_creationStackTrace != null ? ', created:\n$creationStackTrace' : ''}';
+
+  FakeTimer._(Duration duration, this._callback, this.isPeriodic, this._async,
+      {bool includeStackTrace = true})
+      : duration = duration < Duration.zero ? Duration.zero : duration,
+        _creationStackTrace = includeStackTrace ? StackTrace.current : null {
+    _nextCall = _async._elapsed + this.duration;
+  }
+
+  @override
+  bool get isActive => _async._timers.contains(this);
+
+  @override
+  void cancel() => _async._timers.remove(this);
+
+  /// Fires this timer's callback and updates its state as necessary.
+  void _fire() {
+    assert(isActive);
+    _tick++;
+    if (isPeriodic) {
+      _nextCall += duration;
+      // ignore: avoid_dynamic_calls
+      _callback(this);
+    } else {
+      cancel();
+      // ignore: avoid_dynamic_calls
+      _callback();
+    }
+  }
+}
diff --git a/pkgs/fake_async/mono_pkg.yaml b/pkgs/fake_async/mono_pkg.yaml
new file mode 100644
index 0000000..b447edb
--- /dev/null
+++ b/pkgs/fake_async/mono_pkg.yaml
@@ -0,0 +1,15 @@
+# See https://pub.dev/packages/mono_repo
+
+stages:
+- analyze_and_format:
+  - group:
+    - format
+    - analyze: --fatal-infos
+    sdk: dev
+  - group:
+    - analyze
+    sdk: pubspec
+- unit_test:
+  - group:
+    - command: dart test
+    sdk: [dev, pubspec]
diff --git a/pkgs/fake_async/pubspec.yaml b/pkgs/fake_async/pubspec.yaml
new file mode 100644
index 0000000..f345860
--- /dev/null
+++ b/pkgs/fake_async/pubspec.yaml
@@ -0,0 +1,17 @@
+name: fake_async
+version: 1.3.2
+description: >-
+  Fake asynchronous events such as timers and microtasks for deterministic
+  testing.
+repository: https://github.com/dart-lang/test/tree/master/pkgs/fake_async
+environment:
+  sdk: ^3.3.0
+
+dependencies:
+  clock: ^1.1.0
+  collection: ^1.15.0
+
+dev_dependencies:
+  async: ^2.5.0
+  dart_flutter_team_lints: ^2.0.0
+  test: ^1.16.0
diff --git a/pkgs/fake_async/test/fake_async_test.dart b/pkgs/fake_async/test/fake_async_test.dart
new file mode 100644
index 0000000..463eecd
--- /dev/null
+++ b/pkgs/fake_async/test/fake_async_test.dart
@@ -0,0 +1,663 @@
+// Copyright 2014 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async';
+
+import 'package:clock/clock.dart';
+import 'package:fake_async/fake_async.dart';
+import 'package:test/test.dart';
+
+void main() {
+  final initialTime = DateTime(2000);
+  final elapseBy = const Duration(days: 1);
+
+  test('should set initial time', () {
+    expect(FakeAsync().getClock(initialTime).now(), initialTime);
+  });
+
+  group('elapseBlocking', () {
+    test('should elapse time without calling timers', () {
+      Timer(elapseBy ~/ 2, neverCalled);
+      FakeAsync().elapseBlocking(elapseBy);
+    });
+
+    test('should elapse time by the specified amount', () {
+      final async = FakeAsync()..elapseBlocking(elapseBy);
+      expect(async.elapsed, elapseBy);
+    });
+
+    test('should throw when called with a negative duration', () {
+      expect(() => FakeAsync().elapseBlocking(const Duration(days: -1)),
+          throwsArgumentError);
+    });
+  });
+
+  group('elapse', () {
+    test('should elapse time by the specified amount', () {
+      FakeAsync().run((async) {
+        async.elapse(elapseBy);
+        expect(async.elapsed, elapseBy);
+      });
+    });
+
+    test('should throw ArgumentError when called with a negative duration', () {
+      expect(() => FakeAsync().elapse(const Duration(days: -1)),
+          throwsArgumentError);
+    });
+
+    test('should throw when called before previous call is complete', () {
+      FakeAsync().run((async) {
+        Timer(elapseBy ~/ 2, expectAsync0(() {
+          expect(() => async.elapse(elapseBy), throwsStateError);
+        }));
+        async.elapse(elapseBy);
+      });
+    });
+
+    group('when creating timers', () {
+      test('should call timers expiring before or at end time', () {
+        FakeAsync().run((async) {
+          Timer(elapseBy ~/ 2, expectAsync0(() {}));
+          Timer(elapseBy, expectAsync0(() {}));
+          async.elapse(elapseBy);
+        });
+      });
+
+      test('should call timers expiring due to elapseBlocking', () {
+        FakeAsync().run((async) {
+          Timer(elapseBy, () => async.elapseBlocking(elapseBy));
+          Timer(elapseBy * 2, expectAsync0(() {}));
+          async.elapse(elapseBy);
+          expect(async.elapsed, elapseBy * 2);
+        });
+      });
+
+      test('should call timers at their scheduled time', () {
+        FakeAsync().run((async) {
+          Timer(elapseBy ~/ 2, expectAsync0(() {
+            expect(async.elapsed, elapseBy ~/ 2);
+          }));
+
+          final periodicCalledAt = <Duration>[];
+          Timer.periodic(
+              elapseBy ~/ 2, (_) => periodicCalledAt.add(async.elapsed));
+
+          async.elapse(elapseBy);
+          expect(periodicCalledAt, [elapseBy ~/ 2, elapseBy]);
+        });
+      });
+
+      test('should not call timers expiring after end time', () {
+        FakeAsync().run((async) {
+          Timer(elapseBy * 2, neverCalled);
+          async.elapse(elapseBy);
+        });
+      });
+
+      test('should not call canceled timers', () {
+        FakeAsync().run((async) {
+          Timer(elapseBy ~/ 2, neverCalled).cancel();
+          async.elapse(elapseBy);
+        });
+      });
+
+      test('should call periodic timers each time the duration elapses', () {
+        FakeAsync().run((async) {
+          Timer.periodic(elapseBy ~/ 10, expectAsync1((_) {}, count: 10));
+          async.elapse(elapseBy);
+        });
+      });
+
+      test('should call timers occurring at the same time in FIFO order', () {
+        FakeAsync().run((async) {
+          final log = <String>[];
+          Timer(elapseBy ~/ 2, () => log.add('1'));
+          Timer(elapseBy ~/ 2, () => log.add('2'));
+          async.elapse(elapseBy);
+          expect(log, ['1', '2']);
+        });
+      });
+
+      test('should maintain FIFO order even with periodic timers', () {
+        FakeAsync().run((async) {
+          final log = <String>[];
+          Timer.periodic(elapseBy ~/ 2, (_) => log.add('periodic 1'));
+          Timer(elapseBy ~/ 2, () => log.add('delayed 1'));
+          Timer(elapseBy, () => log.add('delayed 2'));
+          Timer.periodic(elapseBy, (_) => log.add('periodic 2'));
+
+          async.elapse(elapseBy);
+          expect(log, [
+            'periodic 1',
+            'delayed 1',
+            'periodic 1',
+            'delayed 2',
+            'periodic 2'
+          ]);
+        });
+      });
+
+      test('should process microtasks surrounding each timer', () {
+        FakeAsync().run((async) {
+          var microtaskCalls = 0;
+          var timerCalls = 0;
+          void scheduleMicrotasks() {
+            for (var i = 0; i < 5; i++) {
+              scheduleMicrotask(() => microtaskCalls++);
+            }
+          }
+
+          scheduleMicrotasks();
+          Timer.periodic(elapseBy ~/ 5, (_) {
+            timerCalls++;
+            expect(microtaskCalls, 5 * timerCalls);
+            scheduleMicrotasks();
+          });
+          async.elapse(elapseBy);
+          expect(timerCalls, 5);
+          expect(microtaskCalls, 5 * (timerCalls + 1));
+        });
+      });
+
+      test('should pass the periodic timer itself to callbacks', () {
+        FakeAsync().run((async) {
+          late Timer constructed;
+          constructed = Timer.periodic(elapseBy, expectAsync1((passed) {
+            expect(passed, same(constructed));
+          }));
+          async.elapse(elapseBy);
+        });
+      });
+
+      test('should call microtasks before advancing time', () {
+        FakeAsync().run((async) {
+          scheduleMicrotask(expectAsync0(() {
+            expect(async.elapsed, Duration.zero);
+          }));
+          async.elapse(const Duration(minutes: 1));
+        });
+      });
+
+      test('should add event before advancing time', () {
+        FakeAsync().run((async) {
+          final controller = StreamController<void>();
+          expect(controller.stream.first.then((_) {
+            expect(async.elapsed, Duration.zero);
+          }), completes);
+          controller.add(null);
+          async.elapse(const Duration(minutes: 1));
+        });
+      });
+
+      test('should increase negative duration timers to zero duration', () {
+        FakeAsync().run((async) {
+          final negativeDuration = const Duration(days: -1);
+          Timer(negativeDuration, expectAsync0(() {
+            expect(async.elapsed, Duration.zero);
+          }));
+          async.elapse(const Duration(minutes: 1));
+        });
+      });
+
+      test('should not be additive with elapseBlocking', () {
+        FakeAsync().run((async) {
+          Timer(Duration.zero, () => async.elapseBlocking(elapseBy * 5));
+          async.elapse(elapseBy);
+          expect(async.elapsed, elapseBy * 5);
+        });
+      });
+
+      group('isActive', () {
+        test('should be false after timer is run', () {
+          FakeAsync().run((async) {
+            final timer = Timer(elapseBy ~/ 2, () {});
+            async.elapse(elapseBy);
+            expect(timer.isActive, isFalse);
+          });
+        });
+
+        test('should be true after periodic timer is run', () {
+          FakeAsync().run((async) {
+            final timer = Timer.periodic(elapseBy ~/ 2, (_) {});
+            async.elapse(elapseBy);
+            expect(timer.isActive, isTrue);
+          });
+        });
+
+        test('should be false after timer is canceled', () {
+          FakeAsync().run((async) {
+            final timer = Timer(elapseBy ~/ 2, () {})..cancel();
+            expect(timer.isActive, isFalse);
+          });
+        });
+      });
+
+      test('should work with new Future()', () {
+        FakeAsync().run((async) {
+          Future(expectAsync0(() {}));
+          async.elapse(Duration.zero);
+        });
+      });
+
+      test('should work with Future.delayed', () {
+        FakeAsync().run((async) {
+          Future.delayed(elapseBy, expectAsync0(() {}));
+          async.elapse(elapseBy);
+        });
+      });
+
+      test('should work with Future.timeout', () {
+        FakeAsync().run((async) {
+          final completer = Completer<void>();
+          expect(completer.future.timeout(elapseBy ~/ 2),
+              throwsA(const TypeMatcher<TimeoutException>()));
+          async.elapse(elapseBy);
+          completer.complete();
+        });
+      });
+
+      // TODO: Pausing and resuming the timeout Stream doesn't work since
+      // it uses `new Stopwatch()`.
+      //
+      // See https://code.google.com/p/dart/issues/detail?id=18149
+      test('should work with Stream.periodic', () {
+        FakeAsync().run((async) {
+          expect(Stream.periodic(const Duration(minutes: 1), (i) => i),
+              emitsInOrder([0, 1, 2]));
+          async.elapse(const Duration(minutes: 3));
+        });
+      });
+
+      test('should work with Stream.timeout', () {
+        FakeAsync().run((async) {
+          final controller = StreamController<int>();
+          final timed = controller.stream.timeout(const Duration(minutes: 2));
+
+          final events = <int>[];
+          final errors = <Object>[];
+          timed.listen(events.add, onError: errors.add);
+
+          controller.add(0);
+          async.elapse(const Duration(minutes: 1));
+          expect(events, [0]);
+
+          async.elapse(const Duration(minutes: 1));
+          expect(errors, hasLength(1));
+          expect(errors.first, const TypeMatcher<TimeoutException>());
+        });
+      });
+    });
+  });
+
+  group('flushMicrotasks', () {
+    test('should flush a microtask', () {
+      FakeAsync().run((async) {
+        Future.microtask(expectAsync0(() {}));
+        async.flushMicrotasks();
+      });
+    });
+
+    test('should flush microtasks scheduled by microtasks in order', () {
+      FakeAsync().run((async) {
+        final log = <int>[];
+        scheduleMicrotask(() {
+          log.add(1);
+          scheduleMicrotask(() => log.add(3));
+        });
+        scheduleMicrotask(() => log.add(2));
+
+        async.flushMicrotasks();
+        expect(log, [1, 2, 3]);
+      });
+    });
+
+    test('should not run timers', () {
+      FakeAsync().run((async) {
+        final log = <int>[];
+        scheduleMicrotask(() => log.add(1));
+        Timer.run(() => log.add(2));
+        Timer.periodic(const Duration(seconds: 1), (_) => log.add(2));
+
+        async.flushMicrotasks();
+        expect(log, [1]);
+      });
+    });
+  });
+
+  group('flushTimers', () {
+    test('should flush timers in FIFO order', () {
+      FakeAsync().run((async) {
+        final log = <int>[];
+        Timer.run(() {
+          log.add(1);
+          Timer(elapseBy, () => log.add(3));
+        });
+        Timer.run(() => log.add(2));
+
+        async.flushTimers(timeout: elapseBy * 2);
+        expect(log, [1, 2, 3]);
+        expect(async.elapsed, elapseBy);
+      });
+    });
+
+    test(
+        'should run collateral periodic timers with non-periodic first if '
+        'scheduled first', () {
+      FakeAsync().run((async) {
+        final log = <String>[];
+        Timer(const Duration(seconds: 2), () => log.add('delayed'));
+        Timer.periodic(const Duration(seconds: 1), (_) => log.add('periodic'));
+
+        async.flushTimers(flushPeriodicTimers: false);
+        expect(log, ['periodic', 'delayed', 'periodic']);
+      });
+    });
+
+    test(
+        'should run collateral periodic timers with periodic first '
+        'if scheduled first', () {
+      FakeAsync().run((async) {
+        final log = <String>[];
+        Timer.periodic(const Duration(seconds: 1), (_) => log.add('periodic'));
+        Timer(const Duration(seconds: 2), () => log.add('delayed'));
+
+        async.flushTimers(flushPeriodicTimers: false);
+        expect(log, ['periodic', 'periodic', 'delayed']);
+      });
+    });
+
+    test('should time out', () {
+      FakeAsync().run((async) {
+        // Schedule 3 timers. All but the last one should fire.
+        for (var delay in [30, 60, 90]) {
+          Timer(Duration(minutes: delay),
+              expectAsync0(() {}, count: delay == 90 ? 0 : 1));
+        }
+
+        expect(() => async.flushTimers(), throwsStateError);
+      });
+    });
+
+    test('should time out a chain of timers', () {
+      FakeAsync().run((async) {
+        var count = 0;
+        void createTimer() {
+          Timer(const Duration(minutes: 30), () {
+            count++;
+            createTimer();
+          });
+        }
+
+        createTimer();
+        expect(() => async.flushTimers(timeout: const Duration(hours: 2)),
+            throwsStateError);
+        expect(count, 4);
+      });
+    });
+
+    test('should time out periodic timers', () {
+      FakeAsync().run((async) {
+        Timer.periodic(
+            const Duration(minutes: 30), expectAsync1((_) {}, count: 2));
+        expect(() => async.flushTimers(timeout: const Duration(hours: 1)),
+            throwsStateError);
+      });
+    });
+
+    test('should flush periodic timers', () {
+      FakeAsync().run((async) {
+        var count = 0;
+        Timer.periodic(const Duration(minutes: 30), (timer) {
+          if (count == 3) timer.cancel();
+          count++;
+        });
+        async.flushTimers(timeout: const Duration(hours: 20));
+        expect(count, 4);
+      });
+    });
+
+    test('should compute absolute timeout as elapsed + timeout', () {
+      FakeAsync().run((async) {
+        var count = 0;
+        void createTimer() {
+          Timer(const Duration(minutes: 30), () {
+            count++;
+            if (count < 4) createTimer();
+          });
+        }
+
+        createTimer();
+        async
+          ..elapse(const Duration(hours: 1))
+          ..flushTimers(timeout: const Duration(hours: 1));
+        expect(count, 4);
+      });
+    });
+  });
+
+  group('stats', () {
+    test('should report the number of pending microtasks', () {
+      FakeAsync().run((async) {
+        expect(async.microtaskCount, 0);
+        scheduleMicrotask(() {});
+        expect(async.microtaskCount, 1);
+        scheduleMicrotask(() {});
+        expect(async.microtaskCount, 2);
+        async.flushMicrotasks();
+        expect(async.microtaskCount, 0);
+      });
+    });
+
+    test('it should report the number of pending periodic timers', () {
+      FakeAsync().run((async) {
+        expect(async.periodicTimerCount, 0);
+        final timer = Timer.periodic(const Duration(minutes: 30), (_) {});
+        expect(async.periodicTimerCount, 1);
+        Timer.periodic(const Duration(minutes: 20), (_) {});
+        expect(async.periodicTimerCount, 2);
+        async.elapse(const Duration(minutes: 20));
+        expect(async.periodicTimerCount, 2);
+        timer.cancel();
+        expect(async.periodicTimerCount, 1);
+      });
+    });
+
+    test('it should report the number of pending non periodic timers', () {
+      FakeAsync().run((async) {
+        expect(async.nonPeriodicTimerCount, 0);
+        final timer = Timer(const Duration(minutes: 30), () {});
+        expect(async.nonPeriodicTimerCount, 1);
+        Timer(const Duration(minutes: 20), () {});
+        expect(async.nonPeriodicTimerCount, 2);
+        async.elapse(const Duration(minutes: 25));
+        expect(async.nonPeriodicTimerCount, 1);
+        timer.cancel();
+        expect(async.nonPeriodicTimerCount, 0);
+      });
+    });
+
+    test('should report debugging information of pending timers', () {
+      FakeAsync().run((fakeAsync) {
+        expect(fakeAsync.pendingTimers, isEmpty);
+        final nonPeriodic =
+            Timer(const Duration(seconds: 1), () {}) as FakeTimer;
+        final periodic =
+            Timer.periodic(const Duration(seconds: 2), (Timer timer) {})
+                as FakeTimer;
+        final debugInfo = fakeAsync.pendingTimers;
+        expect(debugInfo.length, 2);
+        expect(
+          debugInfo,
+          containsAll([
+            nonPeriodic,
+            periodic,
+          ]),
+        );
+
+        const thisFileName = 'fake_async_test.dart';
+        expect(nonPeriodic.debugString, contains(':01.0'));
+        expect(nonPeriodic.debugString, contains('periodic: false'));
+        expect(nonPeriodic.debugString, contains(thisFileName));
+        expect(periodic.debugString, contains(':02.0'));
+        expect(periodic.debugString, contains('periodic: true'));
+        expect(periodic.debugString, contains(thisFileName));
+      });
+    });
+
+    test(
+        'should report debugging information of pending timers excluding '
+        'stack traces', () {
+      FakeAsync(includeTimerStackTrace: false).run((fakeAsync) {
+        expect(fakeAsync.pendingTimers, isEmpty);
+        final nonPeriodic =
+            Timer(const Duration(seconds: 1), () {}) as FakeTimer;
+        final periodic =
+            Timer.periodic(const Duration(seconds: 2), (Timer timer) {})
+                as FakeTimer;
+        final debugInfo = fakeAsync.pendingTimers;
+        expect(debugInfo.length, 2);
+        expect(
+          debugInfo,
+          containsAll([
+            nonPeriodic,
+            periodic,
+          ]),
+        );
+
+        const thisFileName = 'fake_async_test.dart';
+        expect(nonPeriodic.debugString, contains(':01.0'));
+        expect(nonPeriodic.debugString, contains('periodic: false'));
+        expect(nonPeriodic.debugString, isNot(contains(thisFileName)));
+        expect(periodic.debugString, contains(':02.0'));
+        expect(periodic.debugString, contains('periodic: true'));
+        expect(periodic.debugString, isNot(contains(thisFileName)));
+      });
+    });
+  });
+
+  group('timers', () {
+    test("should become inactive as soon as they're invoked", () {
+      return FakeAsync().run((async) {
+        late Timer timer;
+        timer = Timer(elapseBy, expectAsync0(() {
+          expect(timer.isActive, isFalse);
+        }));
+
+        expect(timer.isActive, isTrue);
+        async.elapse(elapseBy);
+        expect(timer.isActive, isFalse);
+      });
+    });
+
+    test('should increment tick in a non-periodic timer', () {
+      return FakeAsync().run((async) {
+        late Timer timer;
+        timer = Timer(elapseBy, expectAsync0(() {
+          expect(timer.tick, 1);
+        }));
+
+        expect(timer.tick, 0);
+        async.elapse(elapseBy);
+      });
+    });
+
+    test('should increment tick in a periodic timer', () {
+      return FakeAsync().run((async) {
+        final ticks = <int>[];
+        Timer.periodic(
+            elapseBy,
+            expectAsync1((timer) {
+              ticks.add(timer.tick);
+            }, count: 2));
+        async
+          ..elapse(elapseBy)
+          ..elapse(elapseBy);
+        expect(ticks, [1, 2]);
+      });
+    });
+
+    test('should update periodic timer state before invoking callback', () {
+      // Regression test for: https://github.com/dart-lang/fake_async/issues/88
+      FakeAsync().run((async) {
+        final log = <String>[];
+        Timer.periodic(const Duration(seconds: 2), (timer) {
+          log.add('periodic ${timer.tick}');
+          async.elapse(Duration.zero);
+        });
+        Timer(const Duration(seconds: 3), () {
+          log.add('single');
+        });
+
+        async.flushTimers(flushPeriodicTimers: false);
+        expect(log, ['periodic 1', 'single']);
+      });
+    });
+  });
+
+  group('clock', () {
+    test('updates following elapse()', () {
+      FakeAsync().run((async) {
+        final before = clock.now();
+        async.elapse(elapseBy);
+        expect(clock.now(), before.add(elapseBy));
+      });
+    });
+
+    test('updates following elapseBlocking()', () {
+      FakeAsync().run((async) {
+        final before = clock.now();
+        async.elapseBlocking(elapseBy);
+        expect(clock.now(), before.add(elapseBy));
+      });
+    });
+
+    group('starts at', () {
+      test('the time at which the FakeAsync was created', () {
+        final start = DateTime.now();
+        FakeAsync().run((async) {
+          expect(clock.now(), _closeToTime(start));
+          async.elapse(elapseBy);
+          expect(clock.now(), _closeToTime(start.add(elapseBy)));
+        });
+      });
+
+      test('the value of clock.now()', () {
+        final start = DateTime(1990, 8, 11);
+        withClock(Clock.fixed(start), () {
+          FakeAsync().run((async) {
+            expect(clock.now(), start);
+            async.elapse(elapseBy);
+            expect(clock.now(), start.add(elapseBy));
+          });
+        });
+      });
+
+      test('an explicit value', () {
+        final start = DateTime(1990, 8, 11);
+        FakeAsync(initialTime: start).run((async) {
+          expect(clock.now(), start);
+          async.elapse(elapseBy);
+          expect(clock.now(), start.add(elapseBy));
+        });
+      });
+    });
+  });
+}
+
+/// Returns a matcher that asserts that a [DateTime] is within 100ms of
+/// [expected].
+Matcher _closeToTime(DateTime expected) => predicate(
+    (actual) =>
+        expected.difference(actual as DateTime).inMilliseconds.abs() < 100,
+    'is close to $expected');
diff --git a/pkgs/matcher/.gitignore b/pkgs/matcher/.gitignore
new file mode 100644
index 0000000..ab3cb76
--- /dev/null
+++ b/pkgs/matcher/.gitignore
@@ -0,0 +1,16 @@
+# Don’t commit the following directories created by pub.
+.buildlog
+.dart_tool/
+.pub/
+build/
+packages
+.packages
+
+# Or the files created by dart2js.
+*.dart.js
+*.js_
+*.js.deps
+*.js.map
+
+# Include when developing application packages.
+pubspec.lock
diff --git a/pkgs/matcher/CHANGELOG.md b/pkgs/matcher/CHANGELOG.md
new file mode 100644
index 0000000..0522c4c
--- /dev/null
+++ b/pkgs/matcher/CHANGELOG.md
@@ -0,0 +1,289 @@
+## 0.12.18-wip
+
+* Remove some dynamic invocations.
+
+## 0.12.17
+
+* Require Dart 3.4
+* Move to `dart-lang/test` monorepo.
+
+## 0.12.16+1
+
+* Require Dart 3.0
+* Support latest version of `package:test_api`.
+
+## 0.12.16
+
+* Expand bounds on `test_api` dependency to allow the next breaking release
+  which will remove the cyclic dependency on this package.
+
+## 0.12.15
+
+* Add `package:matcher/expect.dart` library. Copies the implementation of
+  `expect` and the asynchronous matchers from `package:test`.
+
+## 0.12.14
+
+* Add `containsOnce` matcher.
+* Deprecate `isCyclicInitializationError` and `NullThrownError`. These errors
+  will be removed from the SDK. Update them to catch more general errors.
+
+## 0.12.13
+
+* Require Dart 2.17 or greater.
+* Make `isCastError` no longer depend on the deprecated `CastError` type.
+* Annotate `TypeMatcher.having` with `useResult`.
+
+## 0.12.12
+
+* Add a best practices section to readme.
+* Populate the pubspec `repository` field.
+
+## 0.12.11
+
+* Change many argument types from `dynamic` to `Object?`.
+* Fix `stringContainsInOrder` to account for repetitions and empty strings.
+  * **Note**: This may break some existing tests, as the behavior does change.
+
+## 0.12.10
+
+* Stable release for null safety.
+
+## 0.12.10-nullsafety.3
+
+* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release
+  guidelines.
+
+## 0.12.10-nullsafety.2
+
+- Allow prerelease versions of the 2.12 sdk.
+
+## 0.12.10-nullsafety.1
+
+- Allow 2.10 stable and 2.11.0 dev SDK versions.
+
+## 0.12.10-nullsafety
+
+- Migrate to NNBD.
+  - Apis have been updated to express intent of the existing code and how it
+    handled nulls.
+
+## 0.12.9
+
+- Improve mismatch descriptions for deep matches.  Previously, if the user tried
+  to do a deep match where the expectation included a complex matcher (such as a
+  "having" matcher), the failure message would just say "failed to match ...";
+  it wouldn't call on the expectation's matcher to explain why the match failed.
+
+## 0.12.8
+
+- Add a mismatch description to `TypeMatcher`.
+
+## 0.12.7
+
+- Deprecate the `mirror_matchers.dart` library.
+
+## 0.12.6
+
+- Update minimum Dart SDK to `2.2.0`.
+- Consistently point to `isA` as a replacement for `instanceOf`.
+- Pretty print with private type names.
+
+## 0.12.5
+
+- Add `isA()` to create `TypeMatcher` instances in a more fluent way.
+- **Potentially breaking bug fix**. Ordering matchers no longer treat objects
+  with a partial ordering (such as NaN for double values) as if they had a
+  complete ordering. For instance `greaterThan` now compares with the `>`
+  operator rather not `<` and not `=`. This could cause tests which relied on
+  this bug to start failing.
+
+## 0.12.4
+
+- Add isCastError.
+
+## 0.12.3+1
+
+- Set max SDK version to <3.0.0, and adjusted other dependencies.
+
+## 0.12.3
+
+- Many improvements to `TypeMatcher`
+  - Can now be used directly as `const TypeMatcher<MyType>()`.
+  - Added a type parameter to specify the target `Type`.
+    - Made the `name` constructor parameter optional and marked it deprecated.
+      It's redundant to the type parameter.
+  - Migrated all `isType` matchers to `TypeMatcher`.
+  - Added a `having` function that allows chained validations of specific
+    features of the target type.
+
+    ```dart
+    /// Validates that the object is a [RangeError] with a message containing
+    /// the string 'details' and `start` and `end` properties that are `null`.
+    final _rangeMatcher = isRangeError
+       .having((e) => e.message, 'message', contains('details'))
+       .having((e) => e.start, 'start', isNull)
+       .having((e) => e.end, 'end', isNull);
+    ```
+
+- Deprecated the `isInstanceOf` class. Use `TypeMatcher` instead.
+
+- Improved the output of `Matcher` instances that fail due to type errors.
+
+## 0.12.2+1
+
+- Updated SDK version to 2.0.0-dev.17.0
+
+## 0.12.2
+
+* Fixed `unorderedMatches` in cases where the matchers may match more than one
+  element and order of the elements doesn't line up with the order of the
+  matchers.
+
+* Add containsAll matcher for Iterables. This Matcher checks that all
+  values/matchers in an expected iterable are satisfied by an element in the
+  value without allowing the same value to satisfy multiple matchers.
+
+## 0.12.1+4
+
+* Fixed SDK constraint to allow edge builds.
+
+## 0.12.1+3
+
+* Make `predicate` and `pairwiseCompare` generic methods to allow typed
+ functions to be passed to them as arguments.
+
+* Make internal implementations take better advantage of type promotion to avoid
+  dynamic call overhead.
+
+## 0.12.1+2
+
+* Fixed small documentation issues.
+
+* Fixed small issue in `StringEqualsMatcher`.
+
+* Update to support future Dart language changes.
+
+## 0.12.1+1
+
+* Produce a better error message when a `CustomMatcher`'s feature throws.
+
+## 0.12.1
+
+* Add containsAllInOrder matcher for Iterables
+
+## 0.12.0+2
+
+* Fix all strong-mode warnings.
+
+## 0.12.0+1
+
+* Fix test files to use `test` instead of `unittest` pkg.
+
+## 0.12.0
+
+* Moved a number of members to the
+  [`unittest`](https://pub.dev/packages/unittest) package.
+  * `TestFailure`, `ErrorFormatter`, `expect`, `fail`, and 'wrapAsync'.
+  * `completes`, `completion`, `throws`, and `throwsA` Matchers.
+  * The `Throws` class.
+  * All of the `throws...Error` Matchers.
+
+* Removed `FailureHandler`, `DefaultFailureHandler`,
+  `configureExpectFailureHandler`, and `getOrCreateExpectFailureHandler`.
+  Now that `expect` is in the `unittest` package, these are no longer needed.
+
+* Removed the `name` parameter for `isInstanceOf`. This was previously
+  deprecated, and is no longer necessary since all language implementations now
+  support converting the type parameter to a string directly.
+
+## 0.11.4+6
+
+* Fix a bug introduced in 0.11.4+5 in which operator matchers broke when taking
+  lists of matchers.
+
+## 0.11.4+5
+
+* Fix all strong-mode warnings.
+
+## 0.11.4+4
+
+* Deprecate the name parameter to `isInstanceOf`. All language implementations
+  now support converting the type parameter to a string directly.
+
+## 0.11.4+3
+
+* Fix the examples for `equalsIgnoringWhitespace`.
+
+## 0.11.4+2
+
+* Improve the formatting of strings that contain unprintable ASCII characters.
+
+## 0.11.4+1
+
+* Correctly match and print `String`s containing characters that must be
+  represented as escape sequences.
+
+## 0.11.4
+
+* Remove the type checks in the `isEmpty` and `isNotEmpty` matchers and simply
+  access the `isEmpty` respectively `isNotEmpty` fields. This allows them to
+  work with custom collections. See [Issue
+  21792](https://code.google.com/p/dart/issues/detail?id=21792) and [Issue
+  21562](https://code.google.com/p/dart/issues/detail?id=21562)
+
+## 0.11.3+1
+
+* Fix the `prints` matcher test on dart2js.
+
+## 0.11.3
+
+* Add a `prints` matcher that matches output a callback emits via `print`.
+
+## 0.11.2
+
+* Add an `isNotEmpty` matcher.
+
+## 0.11.1+1
+
+* Refactored libraries and tests.
+
+* Fixed spelling mistake.
+
+## 0.11.1
+
+* Added `isNaN` and `isNotNaN` matchers.
+
+## 0.11.0
+
+* Removed deprecated matchers.
+
+## 0.10.1+1
+
+* Get the tests passing when run on dart2js in minified mode.
+
+## 0.10.1
+
+* Compare sets order-independently when using `equals()`.
+
+## 0.10.0+3
+
+* Removed `@deprecated` annotation on matchers due to
+[Issue 19173](https://code.google.com/p/dart/issues/detail?id=19173)
+
+## 0.10.0+2
+
+* Added types to a number of constants.
+
+## 0.10.0+1
+
+* Matchers related to bad language use have been removed. These represent code
+structure that should rarely or never be validated in tests.
+    * `isAbstractClassInstantiationError`
+    * `throwsAbstractClassInstantiationError`
+    * `isFallThroughError`
+    * `throwsFallThroughError`
+
+* Added types to a number of method arguments.
+
+* The structure of the library and test code has been updated.
diff --git a/pkgs/matcher/LICENSE b/pkgs/matcher/LICENSE
new file mode 100644
index 0000000..000cd7b
--- /dev/null
+++ b/pkgs/matcher/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014, the Dart project authors. 
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google LLC nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/matcher/README.md b/pkgs/matcher/README.md
new file mode 100644
index 0000000..efcae13
--- /dev/null
+++ b/pkgs/matcher/README.md
@@ -0,0 +1,265 @@
+[![pub package](https://img.shields.io/pub/v/matcher.svg)](https://pub.dev/packages/matcher)
+[![package publisher](https://img.shields.io/pub/publisher/matcher.svg)](https://pub.dev/packages/matcher/publisher)
+
+Support for specifying test expectations, such as for unit tests.
+
+The matcher library provides a third-generation assertion mechanism, drawing
+inspiration from [Hamcrest](https://code.google.com/p/hamcrest/).
+
+For more information on testing, see
+[Unit Testing with Dart](https://github.com/dart-lang/test/blob/master/pkgs/test/README.md#writing-tests).
+
+## Using matcher
+
+Expectations start with a call to [`expect()`] or [`expectAsync()`].
+
+[`expect()`]: https://pub.dev/documentation/matcher/latest/expect/expect.html
+[`expectAsync()`]: https://pub.dev/documentation/matcher/latest/expect/expectAsync.html
+
+Any matchers package can be used with `expect()` to do
+complex validations:
+
+[`matcher`]: https://pub.dev/documentation/matcher/latest/matcher/matcher-library.html
+
+```dart
+import 'package:test/test.dart';
+
+void main() {
+  test('.split() splits the string on the delimiter', () {
+    expect('foo,bar,baz', allOf([
+      contains('foo'),
+      isNot(startsWith('bar')),
+      endsWith('baz')
+    ]));
+  });
+}
+```
+
+If a non-matcher value is passed, it will be wrapped with [`equals()`].
+
+[`equals()`]: https://pub.dev/documentation/matcher/latest/expect/equals.html
+
+## Exception matchers
+
+You can also test exceptions with the [`throwsA()`] function or a matcher such
+as [`throwsFormatException`]:
+
+[`throwsA()`]: https://pub.dev/documentation/matcher/latest/expect/throwsA.html
+[`throwsFormatException`]: https://pub.dev/documentation/matcher/latest/expect/throwsFormatException-constant.html
+
+```dart
+import 'package:test/test.dart';
+
+void main() {
+  test('.parse() fails on invalid input', () {
+    expect(() => int.parse('X'), throwsFormatException);
+  });
+}
+```
+
+### Future Matchers
+
+There are a number of useful functions and matchers for more advanced
+asynchrony. The [`completion()`] matcher can be used to test `Futures`; it
+ensures that the test doesn't finish until the `Future` completes, and runs a
+matcher against that `Future`'s value.
+
+[`completion()`]: https://pub.dev/documentation/matcher/latest/expect/completion.html
+
+```dart
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test('Future.value() returns the value', () {
+    expect(Future.value(10), completion(equals(10)));
+  });
+}
+```
+
+The [`throwsA()`] matcher and the various [`throwsExceptionType`] matchers work
+with both synchronous callbacks and asynchronous `Future`s. They ensure that a
+particular type of exception is thrown:
+
+[`throwsExceptionType`]: https://pub.dev/documentation/matcher/latest/expect/throwsException-constant.html
+
+```dart
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test('Future.error() throws the error', () {
+    expect(Future.error('oh no'), throwsA(equals('oh no')));
+    expect(Future.error(StateError('bad state')), throwsStateError);
+  });
+}
+```
+
+The [`expectAsync()`] function wraps another function and has two jobs. First,
+it asserts that the wrapped function is called a certain number of times, and
+will cause the test to fail if it's called too often; second, it keeps the test
+from finishing until the function is called the requisite number of times.
+
+```dart
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test('Stream.fromIterable() emits the values in the iterable', () {
+    var stream = Stream.fromIterable([1, 2, 3]);
+
+    stream.listen(expectAsync1((number) {
+      expect(number, inInclusiveRange(1, 3));
+    }, count: 3));
+  });
+}
+```
+
+[`expectAsync()`]: https://pub.dev/documentation/matcher/latest/expect/expectAsync.html
+
+### Stream Matchers
+
+The `test` package provides a suite of powerful matchers for dealing with
+[asynchronous streams][Stream]. They're expressive and composable, and make it
+easy to write complex expectations about the values emitted by a stream. For
+example:
+
+[Stream]: https://api.dart.dev/stable/dart-async/Stream-class.html
+
+```dart
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test('process emits status messages', () {
+    // Dummy data to mimic something that might be emitted by a process.
+    var stdoutLines = Stream.fromIterable([
+      'Ready.',
+      'Loading took 150ms.',
+      'Succeeded!'
+    ]);
+
+    expect(stdoutLines, emitsInOrder([
+      // Values match individual events.
+      'Ready.',
+
+      // Matchers also run against individual events.
+      startsWith('Loading took'),
+
+      // Stream matchers can be nested. This asserts that one of two events are
+      // emitted after the "Loading took" line.
+      emitsAnyOf(['Succeeded!', 'Failed!']),
+
+      // By default, more events are allowed after the matcher finishes
+      // matching. This asserts instead that the stream emits a done event and
+      // nothing else.
+      emitsDone
+    ]));
+  });
+}
+```
+
+A stream matcher can also match the [`async`] package's [`StreamQueue`] class,
+which allows events to be requested from a stream rather than pushed to the
+consumer. The matcher will consume the matched events, but leave the rest of the
+queue alone so that it can still be used by the test, unlike a normal `Stream`
+which can only have one subscriber. For example:
+
+[`async`]: https://pub.dev/packages/async
+[`StreamQueue`]: https://pub.dev/documentation/async/latest/async/StreamQueue-class.html
+
+```dart
+import 'dart:async';
+
+import 'package:async/async.dart';
+import 'package:test/test.dart';
+
+void main() {
+  test('process emits a WebSocket URL', () async {
+    // Wrap the Stream in a StreamQueue so that we can request events.
+    var stdout = StreamQueue(Stream.fromIterable([
+      'WebSocket URL:',
+      'ws://localhost:1234/',
+      'Waiting for connection...'
+    ]));
+
+    // Ignore lines from the process until it's about to emit the URL.
+    await expectLater(stdout, emitsThrough('WebSocket URL:'));
+
+    // Parse the next line as a URL.
+    var url = Uri.parse(await stdout.next);
+    expect(url.host, equals('localhost'));
+
+    // You can match against the same StreamQueue multiple times.
+    await expectLater(stdout, emits('Waiting for connection...'));
+  });
+}
+```
+
+The following built-in stream matchers are available:
+
+*   [`emits()`] matches a single data event.
+*   [`emitsError()`] matches a single error event.
+*   [`emitsDone`] matches a single done event.
+*   [`mayEmit()`] consumes events if they match an inner matcher, without
+    requiring them to match.
+*   [`mayEmitMultiple()`] works like `mayEmit()`, but it matches events against
+    the matcher as many times as possible.
+*   [`emitsAnyOf()`] consumes events matching one (or more) of several possible
+    matchers.
+*   [`emitsInOrder()`] consumes events matching multiple matchers in a row.
+*   [`emitsInAnyOrder()`] works like `emitsInOrder()`, but it allows the
+    matchers to match in any order.
+*   [`neverEmits()`] matches a stream that finishes *without* matching an inner
+    matcher.
+
+You can also define your own custom stream matchers with [`StreamMatcher()`].
+
+[`emits()`]: https://pub.dev/documentation/matcher/latest/expect/emits.html
+[`emitsError()`]: https://pub.dev/documentation/matcher/latest/expect/emitsError.html
+[`emitsDone`]: https://pub.dev/documentation/matcher/latest/expect/emitsDone.html
+[`mayEmit()`]: https://pub.dev/documentation/matcher/latest/expect/mayEmit.html
+[`mayEmitMultiple()`]: https://pub.dev/documentation/matcher/latest/expect/mayEmitMultiple.html
+[`emitsAnyOf()`]: https://pub.dev/documentation/matcher/latest/expect/emitsAnyOf.html
+[`emitsInOrder()`]: https://pub.dev/documentation/matcher/latest/expect/emitsInOrder.html
+[`emitsInAnyOrder()`]: https://pub.dev/documentation/matcher/latest/expect/emitsInAnyOrder.html
+[`neverEmits()`]: https://pub.dev/documentation/matcher/latest/expect/neverEmits.html
+[`StreamMatcher()`]: https://pub.dev/documentation/matcher/latest/expect/StreamMatcher-class.html
+
+## Best Practices
+
+### Prefer semantically meaningful matchers to comparing derived values
+
+Matchers which have knowledge of the semantics that are tested are able to emit
+more meaningful messages which don't require reading test source to understand
+why the test failed. For instance compare the failures between
+`expect(someList.length, 1)`, and `expect(someList, hasLength(1))`:
+
+```
+// expect(someList.length, 1);
+  Expected: <1>
+    Actual: <2>
+```
+
+```
+// expect(someList, hasLength(1));
+  Expected: an object with length of <1>
+    Actual: ['expected value', 'unexpected value']
+     Which: has length of <2>
+
+```
+
+### Prefer TypeMatcher to predicate if the match can fail in multiple ways
+
+The `predicate` utility is a convenient shortcut for testing an arbitrary
+(synchronous) property of a value, but it discards context and failures are
+opaque. Different failure modes cannot be distinguished in the output which is
+determined by a single "description" argument. Using `isA<SomeType>()` and the
+`TypeMatcher.having` API to extract and test derived properties in a structured
+way brings the context of that structure through to failure messages, so
+failures for different reasons will have distinguishable and actionable failure
+messages.
diff --git a/pkgs/matcher/analysis_options.yaml b/pkgs/matcher/analysis_options.yaml
new file mode 100644
index 0000000..d183f7b
--- /dev/null
+++ b/pkgs/matcher/analysis_options.yaml
@@ -0,0 +1,30 @@
+include: package:lints/recommended.yaml
+
+linter:
+  rules:
+    - always_declare_return_types
+    - avoid_dynamic_calls
+    - avoid_private_typedef_functions
+    - avoid_unused_constructor_parameters
+    - cancel_subscriptions
+    - comment_references
+    - directives_ordering
+    - lines_longer_than_80_chars
+    - literal_only_boolean_expressions
+    - missing_whitespace_between_adjacent_strings
+    - no_adjacent_strings_in_list
+    - no_runtimeType_toString
+    - omit_local_variable_types
+    - only_throw_errors
+    - prefer_const_constructors
+    - prefer_relative_imports
+    - prefer_single_quotes
+    - test_types_in_equals
+    - throw_in_finally
+    - type_annotate_public_apis
+    - unawaited_futures
+    - unnecessary_await_in_return
+    - unnecessary_lambdas
+    - unnecessary_parenthesis
+    - unnecessary_statements
+    - use_super_parameters
diff --git a/pkgs/matcher/lib/expect.dart b/pkgs/matcher/lib/expect.dart
new file mode 100644
index 0000000..c842d30
--- /dev/null
+++ b/pkgs/matcher/lib/expect.dart
@@ -0,0 +1,64 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ignore_for_file: deprecated_member_use_from_same_package
+
+export 'matcher.dart';
+
+export 'src/expect/expect.dart' show ErrorFormatter, expect, expectLater, fail;
+export 'src/expect/expect_async.dart'
+    show
+        Func0,
+        Func1,
+        Func2,
+        Func3,
+        Func4,
+        Func5,
+        Func6,
+        expectAsync,
+        expectAsync0,
+        expectAsync1,
+        expectAsync2,
+        expectAsync3,
+        expectAsync4,
+        expectAsync5,
+        expectAsync6,
+        expectAsyncUntil0,
+        expectAsyncUntil1,
+        expectAsyncUntil2,
+        expectAsyncUntil3,
+        expectAsyncUntil4,
+        expectAsyncUntil5,
+        expectAsyncUntil6;
+export 'src/expect/future_matchers.dart'
+    show completes, completion, doesNotComplete;
+export 'src/expect/never_called.dart' show neverCalled;
+export 'src/expect/prints_matcher.dart' show prints;
+export 'src/expect/stream_matcher.dart' show StreamMatcher;
+export 'src/expect/stream_matchers.dart'
+    show
+        emitsDone,
+        emits,
+        emitsError,
+        mayEmit,
+        emitsAnyOf,
+        emitsInOrder,
+        emitsInAnyOrder,
+        emitsThrough,
+        mayEmitMultiple,
+        neverEmits;
+export 'src/expect/throws_matcher.dart' show Throws, throws, throwsA;
+export 'src/expect/throws_matchers.dart'
+    show
+        throwsArgumentError,
+        throwsConcurrentModificationError,
+        throwsCyclicInitializationError,
+        throwsException,
+        throwsFormatException,
+        throwsNoSuchMethodError,
+        throwsNullThrownError,
+        throwsRangeError,
+        throwsStateError,
+        throwsUnimplementedError,
+        throwsUnsupportedError;
diff --git a/pkgs/matcher/lib/matcher.dart b/pkgs/matcher/lib/matcher.dart
new file mode 100644
index 0000000..236d6f4
--- /dev/null
+++ b/pkgs/matcher/lib/matcher.dart
@@ -0,0 +1,21 @@
+// Copyright (c) 2014, 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.
+
+/// Support for specifying test expectations, such as for unit tests.
+library;
+
+export 'src/core_matchers.dart';
+export 'src/custom_matcher.dart';
+export 'src/description.dart';
+export 'src/equals_matcher.dart';
+export 'src/error_matchers.dart';
+export 'src/interfaces.dart';
+export 'src/iterable_matchers.dart';
+export 'src/map_matchers.dart';
+export 'src/numeric_matchers.dart';
+export 'src/operator_matchers.dart';
+export 'src/order_matchers.dart';
+export 'src/string_matchers.dart';
+export 'src/type_matcher.dart';
+export 'src/util.dart';
diff --git a/pkgs/matcher/lib/mirror_matchers.dart b/pkgs/matcher/lib/mirror_matchers.dart
new file mode 100644
index 0000000..5b2f4b6
--- /dev/null
+++ b/pkgs/matcher/lib/mirror_matchers.dart
@@ -0,0 +1,85 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@Deprecated('Check properties on known types')
+library;
+
+/// The mirror matchers library provides some additional matchers that
+/// make use of `dart:mirrors`.
+import 'dart:mirrors';
+
+import 'matcher.dart';
+
+/// Returns a matcher that checks if a class instance has a property
+/// with name [name], and optionally, if that property in turn satisfies
+/// a [matcher].
+Matcher hasProperty(String name, [Object? matcher]) =>
+    _HasProperty(name, matcher == null ? null : wrapMatcher(matcher));
+
+class _HasProperty extends Matcher {
+  final String _name;
+  final Matcher? _matcher;
+
+  const _HasProperty(this._name, [this._matcher]);
+
+  @override
+  bool matches(Object? item, Map matchState) {
+    var mirror = reflect(item);
+    var classMirror = mirror.type;
+    var symbol = Symbol(_name);
+    var candidate = classMirror.declarations[symbol];
+    if (candidate == null) {
+      addStateInfo(matchState, {'reason': 'has no property named "$_name"'});
+      return false;
+    }
+    var isInstanceField = candidate is VariableMirror && !candidate.isStatic;
+    var isInstanceGetter =
+        candidate is MethodMirror && candidate.isGetter && !candidate.isStatic;
+    if (!(isInstanceField || isInstanceGetter)) {
+      addStateInfo(matchState, {
+        'reason':
+            'has a member named "$_name", but it is not an instance property'
+      });
+      return false;
+    }
+    var matcher = _matcher;
+    if (matcher == null) return true;
+    var result = mirror.getField(symbol);
+    var resultMatches = matcher.matches(result.reflectee, matchState);
+    if (!resultMatches) {
+      addStateInfo(matchState, {'value': result.reflectee});
+    }
+    return resultMatches;
+  }
+
+  @override
+  Description describe(Description description) {
+    description.add('has property "$_name"');
+    if (_matcher != null) {
+      description.add(' which matches ').addDescriptionOf(_matcher);
+    }
+    return description;
+  }
+
+  @override
+  Description describeMismatch(Object? item, Description mismatchDescription,
+      Map matchState, bool verbose) {
+    var reason = matchState['reason'];
+    if (reason != null) {
+      mismatchDescription.add(reason as String);
+    } else {
+      mismatchDescription
+          .add('has property "$_name" with value ')
+          .addDescriptionOf(matchState['value']);
+      var innerDescription = StringDescription();
+      matchState['state'] ??= {};
+      _matcher?.describeMismatch(matchState['value'], innerDescription,
+          matchState['state'] as Map, verbose);
+      if (innerDescription.length > 0) {
+        mismatchDescription.add(' which ').add(innerDescription.toString());
+      }
+    }
+    return mismatchDescription;
+  }
+}
diff --git a/pkgs/matcher/lib/src/core_matchers.dart b/pkgs/matcher/lib/src/core_matchers.dart
new file mode 100644
index 0000000..afb835b
--- /dev/null
+++ b/pkgs/matcher/lib/src/core_matchers.dart
@@ -0,0 +1,327 @@
+// Copyright (c) 2012, 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 'feature_matcher.dart';
+import 'interfaces.dart';
+import 'type_matcher.dart';
+import 'util.dart';
+
+/// Returns a matcher that matches the isEmpty property.
+const Matcher isEmpty = _Empty();
+
+class _Empty extends Matcher {
+  const _Empty();
+
+  @override
+  bool matches(Object? item, Map matchState) => (item as dynamic).isEmpty;
+
+  @override
+  Description describe(Description description) => description.add('empty');
+}
+
+/// Returns a matcher that matches the isNotEmpty property.
+const Matcher isNotEmpty = _NotEmpty();
+
+class _NotEmpty extends Matcher {
+  const _NotEmpty();
+
+  @override
+  bool matches(Object? item, Map matchState) => (item as dynamic).isNotEmpty;
+
+  @override
+  Description describe(Description description) => description.add('non-empty');
+}
+
+/// A matcher that matches any null value.
+const Matcher isNull = _IsNull();
+
+/// A matcher that matches any non-null value.
+const Matcher isNotNull = _IsNotNull();
+
+class _IsNull extends Matcher {
+  const _IsNull();
+  @override
+  bool matches(Object? item, Map matchState) => item == null;
+  @override
+  Description describe(Description description) => description.add('null');
+}
+
+class _IsNotNull extends Matcher {
+  const _IsNotNull();
+  @override
+  bool matches(Object? item, Map matchState) => item != null;
+  @override
+  Description describe(Description description) => description.add('not null');
+}
+
+/// A matcher that matches the Boolean value true.
+const Matcher isTrue = _IsTrue();
+
+/// A matcher that matches anything except the Boolean value true.
+const Matcher isFalse = _IsFalse();
+
+class _IsTrue extends Matcher {
+  const _IsTrue();
+  @override
+  bool matches(Object? item, Map matchState) => item == true;
+  @override
+  Description describe(Description description) => description.add('true');
+}
+
+class _IsFalse extends Matcher {
+  const _IsFalse();
+  @override
+  bool matches(Object? item, Map matchState) => item == false;
+  @override
+  Description describe(Description description) => description.add('false');
+}
+
+/// A matcher that matches the numeric value NaN.
+const Matcher isNaN = _IsNaN();
+
+/// A matcher that matches any non-NaN value.
+const Matcher isNotNaN = _IsNotNaN();
+
+class _IsNaN extends FeatureMatcher<num> {
+  const _IsNaN();
+  @override
+  bool typedMatches(num item, Map matchState) =>
+      double.nan.compareTo(item) == 0;
+  @override
+  Description describe(Description description) => description.add('NaN');
+}
+
+class _IsNotNaN extends FeatureMatcher<num> {
+  const _IsNotNaN();
+  @override
+  bool typedMatches(num item, Map matchState) =>
+      double.nan.compareTo(item) != 0;
+  @override
+  Description describe(Description description) => description.add('not NaN');
+}
+
+/// Returns a matches that matches if the value is the same instance
+/// as [expected], using [identical].
+Matcher same(Object? expected) => _IsSameAs(expected);
+
+class _IsSameAs extends Matcher {
+  final Object? _expected;
+  const _IsSameAs(this._expected);
+  @override
+  bool matches(Object? item, Map matchState) => identical(item, _expected);
+  // If all types were hashable we could show a hash here.
+  @override
+  Description describe(Description description) =>
+      description.add('same instance as ').addDescriptionOf(_expected);
+}
+
+/// A matcher that matches any value.
+const Matcher anything = _IsAnything();
+
+class _IsAnything extends Matcher {
+  const _IsAnything();
+  @override
+  bool matches(Object? item, Map matchState) => true;
+  @override
+  Description describe(Description description) => description.add('anything');
+}
+
+/// **DEPRECATED** Use [isA] instead.
+///
+/// A matcher that matches if an object is an instance of [T] (or a subtype).
+@Deprecated('Use `isA<MyType>()` instead.')
+// ignore: camel_case_types
+class isInstanceOf<T> extends TypeMatcher<T> {
+  const isInstanceOf();
+}
+
+/// A matcher that matches a function call against no exception.
+///
+/// The function will be called once. Any exceptions will be silently swallowed.
+/// The value passed to expect() should be a reference to the function.
+/// Note that the function cannot take arguments; to handle this
+/// a wrapper will have to be created.
+const Matcher returnsNormally = _ReturnsNormally();
+
+class _ReturnsNormally extends FeatureMatcher<Function> {
+  const _ReturnsNormally();
+
+  @override
+  bool typedMatches(Function f, Map matchState) {
+    try {
+      // ignore: unnecessary_cast
+      (f as Function)();
+      return true;
+    } catch (e, s) {
+      addStateInfo(matchState, {'exception': e, 'stack': s});
+      return false;
+    }
+  }
+
+  @override
+  Description describe(Description description) =>
+      description.add('return normally');
+
+  @override
+  Description describeTypedMismatch(Function item,
+      Description mismatchDescription, Map matchState, bool verbose) {
+    mismatchDescription.add('threw ').addDescriptionOf(matchState['exception']);
+    if (verbose) {
+      mismatchDescription.add(' at ').add(matchState['stack'].toString());
+    }
+    return mismatchDescription;
+  }
+}
+
+/// A matcher for [Map].
+const isMap = TypeMatcher<Map>();
+
+/// A matcher for [List].
+const isList = TypeMatcher<List>();
+
+/// Returns a matcher that matches if an object has a length property
+/// that matches [matcher].
+Matcher hasLength(Object? matcher) => _HasLength(wrapMatcher(matcher));
+
+class _HasLength extends Matcher {
+  final Matcher _matcher;
+  const _HasLength(this._matcher);
+
+  @override
+  bool matches(Object? item, Map matchState) {
+    try {
+      final length = (item as dynamic).length;
+      return _matcher.matches(length, matchState);
+    } catch (e) {
+      return false;
+    }
+  }
+
+  @override
+  Description describe(Description description) =>
+      description.add('an object with length of ').addDescriptionOf(_matcher);
+
+  @override
+  Description describeMismatch(Object? item, Description mismatchDescription,
+      Map matchState, bool verbose) {
+    try {
+      final length = (item as dynamic).length;
+      return mismatchDescription.add('has length of ').addDescriptionOf(length);
+    } catch (e) {
+      return mismatchDescription.add('has no length property');
+    }
+  }
+}
+
+/// Returns a matcher that matches if the match argument contains the expected
+/// value.
+///
+/// For [String]s this means substring matching;
+/// for [Map]s it means the map has the key, and for [Iterable]s
+/// it means the iterable has a matching element. In the case of iterables,
+/// [expected] can itself be a matcher.
+Matcher contains(Object? expected) => _Contains(expected);
+
+class _Contains extends Matcher {
+  final Object? _expected;
+
+  const _Contains(this._expected);
+
+  @override
+  bool matches(Object? item, Map matchState) {
+    var expected = _expected;
+    if (item is String) {
+      return expected is Pattern && item.contains(expected);
+    } else if (item is Iterable) {
+      if (expected is Matcher) {
+        return item.any((e) => expected.matches(e, matchState));
+      } else {
+        return item.contains(_expected);
+      }
+    } else if (item is Map) {
+      return item.containsKey(_expected);
+    }
+    return false;
+  }
+
+  @override
+  Description describe(Description description) =>
+      description.add('contains ').addDescriptionOf(_expected);
+
+  @override
+  Description describeMismatch(Object? item, Description mismatchDescription,
+      Map matchState, bool verbose) {
+    if (item is String || item is Iterable || item is Map) {
+      super.describeMismatch(item, mismatchDescription, matchState, verbose);
+      mismatchDescription.add('does not contain ').addDescriptionOf(_expected);
+      return mismatchDescription;
+    } else {
+      return mismatchDescription.add('is not a string, map or iterable');
+    }
+  }
+}
+
+/// Returns a matcher that matches if the match argument is in
+/// the expected value. This is the converse of [contains].
+Matcher isIn(Object? expected) {
+  if (expected is Iterable) {
+    return _In(expected, expected.contains);
+  } else if (expected is String) {
+    return _In<Pattern>(expected, expected.contains);
+  } else if (expected is Map) {
+    return _In(expected, expected.containsKey);
+  }
+
+  throw ArgumentError.value(
+      expected, 'expected', 'Only Iterable, Map, and String are supported.');
+}
+
+class _In<T> extends FeatureMatcher<T> {
+  final Object _source;
+  final bool Function(T) _containsFunction;
+
+  const _In(this._source, this._containsFunction);
+
+  @override
+  bool typedMatches(T item, Map matchState) => _containsFunction(item);
+
+  @override
+  Description describe(Description description) =>
+      description.add('is in ').addDescriptionOf(_source);
+}
+
+/// Returns a matcher that uses an arbitrary function that returns whether the
+/// value is considered a match.
+///
+/// For example:
+///
+///     expect(actual, predicate<num>((v) => (v % 2) == 0, 'is even'));
+///
+/// Use this method when a value is checked for one conceptual property
+/// described by [description].
+///
+/// If the value can be rejected for more than one reason prefer using [isA] and
+/// the [TypeMatcher.having] API to build up a matcher with output that can
+/// distinquish between them.
+///
+/// Using an explicit generict argument allows a passed function literal to have
+/// an inferred argument type of [T], and values of the wrong type will be
+/// rejected with an informative message.
+Matcher predicate<T>(bool Function(T) f,
+        [String description = 'satisfies function']) =>
+    _Predicate(f, description);
+
+class _Predicate<T> extends FeatureMatcher<T> {
+  final bool Function(T) _matcher;
+  final String _description;
+
+  _Predicate(this._matcher, this._description);
+
+  @override
+  bool typedMatches(T item, Map matchState) => _matcher(item);
+
+  @override
+  Description describe(Description description) =>
+      description.add(_description);
+}
diff --git a/pkgs/matcher/lib/src/custom_matcher.dart b/pkgs/matcher/lib/src/custom_matcher.dart
new file mode 100644
index 0000000..b0f2d6b
--- /dev/null
+++ b/pkgs/matcher/lib/src/custom_matcher.dart
@@ -0,0 +1,100 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:stack_trace/stack_trace.dart';
+
+import 'description.dart';
+import 'interfaces.dart';
+import 'util.dart';
+
+/// A base class for [Matcher] instances that match based on some feature of the
+/// value under test.
+///
+/// Derived classes should call the base constructor with a feature name and
+/// description, and an instance matcher, and should implement the
+/// [featureValueOf] abstract method.
+///
+/// The feature description will typically describe the item and the feature,
+/// while the feature name will just name the feature. For example, we may
+/// have a Widget class where each Widget has a price; we could make a
+/// [CustomMatcher] that can make assertions about prices with:
+///
+/// ```dart
+/// class HasPrice extends CustomMatcher {
+///   HasPrice(matcher) : super("Widget with price that is", "price", matcher);
+///   featureValueOf(actual) => (actual as Widget).price;
+/// }
+/// ```
+///
+/// and then use this for example like:
+///
+/// ```dart
+/// expect(inventoryItem, HasPrice(greaterThan(0)));
+/// ```
+class CustomMatcher extends Matcher {
+  final String _featureDescription;
+  final String _featureName;
+  final Matcher _matcher;
+
+  CustomMatcher(
+      this._featureDescription, this._featureName, Object? valueOrMatcher)
+      : _matcher = wrapMatcher(valueOrMatcher);
+
+  /// Override this to extract the interesting feature.
+  Object? featureValueOf(dynamic actual) => actual;
+
+  @override
+  bool matches(Object? item, Map matchState) {
+    try {
+      var f = featureValueOf(item);
+      if (_matcher.matches(f, matchState)) return true;
+      addStateInfo(matchState, {'custom.feature': f});
+    } catch (exception, stack) {
+      addStateInfo(matchState, {
+        'custom.exception': exception.toString(),
+        'custom.stack': Chain.forTrace(stack)
+            .foldFrames(
+                (frame) =>
+                    frame.package == 'test' ||
+                    frame.package == 'stream_channel' ||
+                    frame.package == 'matcher',
+                terse: true)
+            .toString()
+      });
+    }
+    return false;
+  }
+
+  @override
+  Description describe(Description description) =>
+      description.add(_featureDescription).add(' ').addDescriptionOf(_matcher);
+
+  @override
+  Description describeMismatch(Object? item, Description mismatchDescription,
+      Map matchState, bool verbose) {
+    if (matchState['custom.exception'] != null) {
+      mismatchDescription
+          .add('threw ')
+          .addDescriptionOf(matchState['custom.exception'])
+          .add('\n')
+          .add(matchState['custom.stack'].toString());
+      return mismatchDescription;
+    }
+
+    mismatchDescription
+        .add('has ')
+        .add(_featureName)
+        .add(' with value ')
+        .addDescriptionOf(matchState['custom.feature']);
+    var innerDescription = StringDescription();
+
+    _matcher.describeMismatch(matchState['custom.feature'], innerDescription,
+        matchState['state'] as Map, verbose);
+
+    if (innerDescription.length > 0) {
+      mismatchDescription.add(' which ').add(innerDescription.toString());
+    }
+    return mismatchDescription;
+  }
+}
diff --git a/pkgs/matcher/lib/src/description.dart b/pkgs/matcher/lib/src/description.dart
new file mode 100644
index 0000000..090aada
--- /dev/null
+++ b/pkgs/matcher/lib/src/description.dart
@@ -0,0 +1,72 @@
+// Copyright (c) 2012, 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 'interfaces.dart';
+import 'pretty_print.dart';
+
+/// The default implementation of [Description]. This should rarely need
+/// substitution, although conceivably it is a place where other languages
+/// could be supported.
+class StringDescription implements Description {
+  final StringBuffer _out = StringBuffer();
+
+  /// Initialize the description with initial contents [init].
+  StringDescription([String init = '']) {
+    _out.write(init);
+  }
+
+  @override
+  int get length => _out.length;
+
+  /// Get the description as a string.
+  @override
+  String toString() => _out.toString();
+
+  /// Append [text] to the description.
+  @override
+  Description add(String text) {
+    _out.write(text);
+    return this;
+  }
+
+  /// Change the value of the description.
+  @override
+  Description replace(String text) {
+    _out.clear();
+    return add(text);
+  }
+
+  /// Appends a description of [value]. If it is an IMatcher use its
+  /// describe method; if it is a string use its literal value after
+  /// escaping any embedded control characters; otherwise use its
+  /// toString() value and wrap it in angular "quotes".
+  @override
+  Description addDescriptionOf(Object? value) {
+    if (value is Matcher) {
+      value.describe(this);
+    } else {
+      add(prettyPrint(value, maxLineLength: 80, maxItems: 25));
+    }
+    return this;
+  }
+
+  /// Append an [Iterable] [list] of objects to the description, using the
+  /// specified [separator] and framing the list with [start]
+  /// and [end].
+  @override
+  Description addAll(
+      String start, String separator, String end, Iterable list) {
+    var separate = false;
+    add(start);
+    for (var item in list) {
+      if (separate) {
+        add(separator);
+      }
+      addDescriptionOf(item);
+      separate = true;
+    }
+    add(end);
+    return this;
+  }
+}
diff --git a/pkgs/matcher/lib/src/equals_matcher.dart b/pkgs/matcher/lib/src/equals_matcher.dart
new file mode 100644
index 0000000..5c4f4c5
--- /dev/null
+++ b/pkgs/matcher/lib/src/equals_matcher.dart
@@ -0,0 +1,327 @@
+// 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.
+
+import 'feature_matcher.dart';
+import 'interfaces.dart';
+import 'util.dart';
+
+/// Returns a matcher that matches if the value is structurally equal to
+/// [expected].
+///
+/// If [expected] is a [Matcher], then it matches using that. Otherwise it tests
+/// for equality using `==` on the expected value.
+///
+/// For [Iterable]s and [Map]s, this will recursively match the elements. To
+/// handle cyclic structures a recursion depth [limit] can be provided. The
+/// default limit is 100. [Set]s will be compared order-independently.
+Matcher equals(Object? expected, [int limit = 100]) => expected is String
+    ? _StringEqualsMatcher(expected)
+    : _DeepMatcher(expected, limit);
+
+typedef _RecursiveMatcher = _Mismatch? Function(Object?, Object?, String, int);
+
+/// A special equality matcher for strings.
+class _StringEqualsMatcher extends FeatureMatcher<String> {
+  final String _value;
+
+  _StringEqualsMatcher(this._value);
+
+  @override
+  bool typedMatches(String item, Map matchState) => _value == item;
+
+  @override
+  Description describe(Description description) =>
+      description.addDescriptionOf(_value);
+
+  @override
+  Description describeTypedMismatch(String item,
+      Description mismatchDescription, Map matchState, bool verbose) {
+    var buff = StringBuffer();
+    buff.write('is different.');
+    var escapedItem = escape(item);
+    var escapedValue = escape(_value);
+    var minLength = escapedItem.length < escapedValue.length
+        ? escapedItem.length
+        : escapedValue.length;
+    var start = 0;
+    for (; start < minLength; start++) {
+      if (escapedValue.codeUnitAt(start) != escapedItem.codeUnitAt(start)) {
+        break;
+      }
+    }
+    if (start == minLength) {
+      if (escapedValue.length < escapedItem.length) {
+        buff.write(' Both strings start the same, but the actual value also'
+            ' has the following trailing characters: ');
+        _writeTrailing(buff, escapedItem, escapedValue.length);
+      } else {
+        buff.write(' Both strings start the same, but the actual value is'
+            ' missing the following trailing characters: ');
+        _writeTrailing(buff, escapedValue, escapedItem.length);
+      }
+    } else {
+      buff.write('\nExpected: ');
+      _writeLeading(buff, escapedValue, start);
+      _writeTrailing(buff, escapedValue, start);
+      buff.write('\n  Actual: ');
+      _writeLeading(buff, escapedItem, start);
+      _writeTrailing(buff, escapedItem, start);
+      buff.write('\n          ');
+      for (var i = start > 10 ? 14 : start; i > 0; i--) {
+        buff.write(' ');
+      }
+      buff.write('^\n Differ at offset $start');
+    }
+
+    return mismatchDescription.add(buff.toString());
+  }
+
+  static void _writeLeading(StringBuffer buff, String s, int start) {
+    if (start > 10) {
+      buff.write('... ');
+      buff.write(s.substring(start - 10, start));
+    } else {
+      buff.write(s.substring(0, start));
+    }
+  }
+
+  static void _writeTrailing(StringBuffer buff, String s, int start) {
+    if (start + 10 > s.length) {
+      buff.write(s.substring(start));
+    } else {
+      buff.write(s.substring(start, start + 10));
+      buff.write(' ...');
+    }
+  }
+}
+
+class _DeepMatcher extends Matcher {
+  final Object? _expected;
+  final int _limit;
+
+  _DeepMatcher(this._expected, [int limit = 1000]) : _limit = limit;
+
+  _Mismatch? _compareIterables(Iterable expected, Object? actual,
+      _RecursiveMatcher matcher, int depth, String location) {
+    if (actual is Iterable) {
+      var expectedIterator = expected.iterator;
+      var actualIterator = actual.iterator;
+      for (var index = 0;; index++) {
+        // Advance in lockstep.
+        var expectedNext = expectedIterator.moveNext();
+        var actualNext = actualIterator.moveNext();
+
+        // If we reached the end of both, we succeeded.
+        if (!expectedNext && !actualNext) return null;
+
+        // Fail if their lengths are different.
+        var newLocation = '$location[$index]';
+        if (!expectedNext) {
+          return _Mismatch.simple(newLocation, actual, 'longer than expected');
+        }
+        if (!actualNext) {
+          return _Mismatch.simple(newLocation, actual, 'shorter than expected');
+        }
+
+        // Match the elements.
+        var rp = matcher(expectedIterator.current, actualIterator.current,
+            newLocation, depth);
+        if (rp != null) return rp;
+      }
+    } else {
+      return _Mismatch.simple(location, actual, 'is not Iterable');
+    }
+  }
+
+  _Mismatch? _compareSets(Set expected, Object? actual,
+      _RecursiveMatcher matcher, int depth, String location) {
+    if (actual is Iterable) {
+      var other = actual.toSet();
+
+      for (var expectedElement in expected) {
+        if (other.every((actualElement) =>
+            matcher(expectedElement, actualElement, location, depth) != null)) {
+          return _Mismatch(
+              location,
+              actual,
+              (description, verbose) => description
+                  .add('does not contain ')
+                  .addDescriptionOf(expectedElement));
+        }
+      }
+
+      if (other.length > expected.length) {
+        return _Mismatch.simple(location, actual, 'larger than expected');
+      } else if (other.length < expected.length) {
+        return _Mismatch.simple(location, actual, 'smaller than expected');
+      } else {
+        return null;
+      }
+    } else {
+      return _Mismatch.simple(location, actual, 'is not Iterable');
+    }
+  }
+
+  _Mismatch? _recursiveMatch(
+      Object? expected, Object? actual, String location, int depth) {
+    // If the expected value is a matcher, try to match it.
+    if (expected is Matcher) {
+      var matchState = {};
+      if (expected.matches(actual, matchState)) return null;
+      return _Mismatch(location, actual, (description, verbose) {
+        var oldLength = description.length;
+        expected.describeMismatch(actual, description, matchState, verbose);
+        if (depth > 0 && description.length == oldLength) {
+          description.add('does not match ');
+          expected.describe(description);
+        }
+      });
+    } else {
+      // Otherwise, test for equality.
+      try {
+        if (expected == actual) return null;
+      } catch (e) {
+        // TODO(gram): Add a test for this case.
+        return _Mismatch(
+            location,
+            actual,
+            (description, verbose) =>
+                description.add('== threw ').addDescriptionOf(e));
+      }
+    }
+
+    if (depth > _limit) {
+      return _Mismatch.simple(
+          location, actual, 'recursion depth limit exceeded');
+    }
+
+    // If _limit is 1 we can only recurse one level into object.
+    if (depth == 0 || _limit > 1) {
+      if (expected is Set) {
+        return _compareSets(
+            expected, actual, _recursiveMatch, depth + 1, location);
+      } else if (expected is Iterable) {
+        return _compareIterables(
+            expected, actual, _recursiveMatch, depth + 1, location);
+      } else if (expected is Map) {
+        if (actual is! Map) {
+          return _Mismatch.simple(location, actual, 'expected a map');
+        }
+        var err = (expected.length == actual.length)
+            ? ''
+            : 'has different length and ';
+        for (var key in expected.keys) {
+          if (!actual.containsKey(key)) {
+            return _Mismatch(
+                location,
+                actual,
+                (description, verbose) => description
+                    .add('${err}is missing map key ')
+                    .addDescriptionOf(key));
+          }
+        }
+
+        for (var key in actual.keys) {
+          if (!expected.containsKey(key)) {
+            return _Mismatch(
+                location,
+                actual,
+                (description, verbose) => description
+                    .add('${err}has extra map key ')
+                    .addDescriptionOf(key));
+          }
+        }
+
+        for (var key in expected.keys) {
+          var rp = _recursiveMatch(
+              expected[key], actual[key], "$location['$key']", depth + 1);
+          if (rp != null) return rp;
+        }
+
+        return null;
+      }
+    }
+
+    // If we have recursed, show the expected value too; if not, expect() will
+    // show it for us.
+    if (depth > 0) {
+      return _Mismatch(location, actual,
+          (description, verbose) => description.addDescriptionOf(expected),
+          instead: true);
+    } else {
+      return _Mismatch(location, actual, null);
+    }
+  }
+
+  @override
+  bool matches(Object? actual, Map matchState) {
+    var mismatch = _recursiveMatch(_expected, actual, '', 0);
+    if (mismatch == null) return true;
+    addStateInfo(matchState, {'mismatch': mismatch});
+    return false;
+  }
+
+  @override
+  Description describe(Description description) =>
+      description.addDescriptionOf(_expected);
+
+  @override
+  Description describeMismatch(Object? item, Description mismatchDescription,
+      Map matchState, bool verbose) {
+    var mismatch = matchState['mismatch'] as _Mismatch;
+    var describeProblem = mismatch.describeProblem;
+    if (mismatch.location.isNotEmpty) {
+      mismatchDescription
+          .add('at location ')
+          .add(mismatch.location)
+          .add(' is ')
+          .addDescriptionOf(mismatch.actual);
+      if (describeProblem != null) {
+        mismatchDescription
+            .add(' ${mismatch.instead ? 'instead of' : 'which'} ');
+        describeProblem(mismatchDescription, verbose);
+      }
+    } else {
+      // If we didn't get a good reason, that would normally be a
+      // simple 'is <value>' message. We only add that if the mismatch
+      // description is non empty (so we are supplementing the mismatch
+      // description).
+      if (describeProblem == null) {
+        if (mismatchDescription.length > 0) {
+          mismatchDescription.add('is ').addDescriptionOf(item);
+        }
+      } else {
+        describeProblem(mismatchDescription, verbose);
+      }
+    }
+    return mismatchDescription;
+  }
+}
+
+class _Mismatch {
+  /// A human-readable description of the location within the collection where
+  /// the mismatch occurred.
+  final String location;
+
+  /// The actual value found at [location].
+  final Object? actual;
+
+  /// Callback that can create a detailed description of the problem.
+  final void Function(Description, bool verbose)? describeProblem;
+
+  /// If `true`, [describeProblem] describes the expected value, so when the
+  /// final mismatch description is pieced together, it will be preceded by
+  /// `instead of` (e.g. `at location [2] is <3> instead of <4>`).  If `false`,
+  /// [describeProblem] is a problem description from a sub-matcher, so when the
+  /// final mismatch description is pieced together, it will be preceded by
+  /// `which` (e.g. `at location [2] is <foo> which has length of 3`).
+  final bool instead;
+
+  _Mismatch(this.location, this.actual, this.describeProblem,
+      {this.instead = false});
+
+  _Mismatch.simple(this.location, this.actual, String problem)
+      : describeProblem = ((description, verbose) => description.add(problem)),
+        instead = false;
+}
diff --git a/pkgs/matcher/lib/src/error_matchers.dart b/pkgs/matcher/lib/src/error_matchers.dart
new file mode 100644
index 0000000..5b6238d
--- /dev/null
+++ b/pkgs/matcher/lib/src/error_matchers.dart
@@ -0,0 +1,48 @@
+// Copyright (c) 2012, 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 'type_matcher.dart';
+
+/// A matcher for [ArgumentError].
+const isArgumentError = TypeMatcher<ArgumentError>();
+
+/// A matcher for [TypeError].
+@Deprecated('CastError has been deprecated in favor of TypeError. ')
+const isCastError = TypeMatcher<TypeError>();
+
+/// A matcher for [ConcurrentModificationError].
+const isConcurrentModificationError =
+    TypeMatcher<ConcurrentModificationError>();
+
+/// A matcher for [Error].
+@Deprecated(
+    'CyclicInitializationError is deprecated and will be removed in Dart 3. '
+    'Use `isA<Error>()` instead.')
+const isCyclicInitializationError = TypeMatcher<Error>();
+
+/// A matcher for [Exception].
+const isException = TypeMatcher<Exception>();
+
+/// A matcher for [FormatException].
+const isFormatException = TypeMatcher<FormatException>();
+
+/// A matcher for [NoSuchMethodError].
+const isNoSuchMethodError = TypeMatcher<NoSuchMethodError>();
+
+/// A matcher for [TypeError].
+@Deprecated('NullThrownError is deprecated and will be removed in Dart 3. '
+    'Use `isA<TypeError>()` instead.')
+const isNullThrownError = TypeMatcher<TypeError>();
+
+/// A matcher for [RangeError].
+const isRangeError = TypeMatcher<RangeError>();
+
+/// A matcher for [StateError].
+const isStateError = TypeMatcher<StateError>();
+
+/// A matcher for [UnimplementedError].
+const isUnimplementedError = TypeMatcher<UnimplementedError>();
+
+/// A matcher for [UnsupportedError].
+const isUnsupportedError = TypeMatcher<UnsupportedError>();
diff --git a/pkgs/matcher/lib/src/expect/async_matcher.dart b/pkgs/matcher/lib/src/expect/async_matcher.dart
new file mode 100644
index 0000000..854151d
--- /dev/null
+++ b/pkgs/matcher/lib/src/expect/async_matcher.dart
@@ -0,0 +1,68 @@
+// 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.
+
+// ignore_for_file: deprecated_member_use_from_same_package
+
+import 'package:test_api/hooks.dart';
+
+import '../description.dart';
+import '../equals_matcher.dart';
+import '../interfaces.dart';
+import '../operator_matchers.dart';
+import '../type_matcher.dart';
+import 'expect.dart';
+
+/// A matcher that does asynchronous computation.
+///
+/// Rather than implementing [matches], subclasses implement [matchAsync].
+/// [AsyncMatcher.matches] ensures that the test doesn't complete until the
+/// returned future completes, and [expect] returns a future that completes when
+/// the returned future completes so that tests can wait for it.
+abstract class AsyncMatcher extends Matcher {
+  const AsyncMatcher();
+
+  /// Returns `null` if this matches [item], or a [String] description of the
+  /// failure if it doesn't match.
+  ///
+  /// This can return a [Future] or a synchronous value. If it returns a
+  /// [Future], neither [expect] nor the test will complete until that [Future]
+  /// completes.
+  ///
+  /// If this returns a [String] synchronously, [expect] will synchronously
+  /// throw a [TestFailure] and [matches] will synchronously return `false`.
+  dynamic /*FutureOr<String>*/ matchAsync(dynamic item);
+
+  @override
+  bool matches(dynamic item, Map matchState) {
+    final result = matchAsync(item);
+    expect(
+        result,
+        anyOf([
+          equals(null),
+          const TypeMatcher<Future>(),
+          const TypeMatcher<String>()
+        ]),
+        reason: 'matchAsync() may only return a String, a Future, or null.');
+
+    if (result is Future) {
+      final outstandingWork = TestHandle.current.markPending();
+      result.then((realResult) {
+        if (realResult != null) {
+          fail(formatFailure(this, item, realResult as String));
+        }
+        outstandingWork.complete();
+      });
+    } else if (result is String) {
+      matchState[this] = result;
+      return false;
+    }
+
+    return true;
+  }
+
+  @override
+  Description describeMismatch(dynamic item, Description mismatchDescription,
+          Map matchState, bool verbose) =>
+      StringDescription(matchState[this] as String);
+}
diff --git a/pkgs/matcher/lib/src/expect/expect.dart b/pkgs/matcher/lib/src/expect/expect.dart
new file mode 100644
index 0000000..8dd8cae
--- /dev/null
+++ b/pkgs/matcher/lib/src/expect/expect.dart
@@ -0,0 +1,161 @@
+// 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.
+
+// ignore_for_file: deprecated_member_use_from_same_package
+
+import 'package:test_api/hooks.dart';
+
+import '../description.dart';
+import '../equals_matcher.dart';
+import '../interfaces.dart';
+import '../operator_matchers.dart';
+import '../type_matcher.dart';
+import '../util.dart';
+import 'async_matcher.dart';
+import 'future_matchers.dart';
+import 'prints_matcher.dart';
+import 'throws_matcher.dart';
+import 'util/pretty_print.dart';
+
+/// The type used for functions that can be used to build up error reports
+/// upon failures in [expect].
+@Deprecated('Will be removed in 0.13.0.')
+typedef ErrorFormatter = String Function(Object? actual, Matcher matcher,
+    String? reason, Map matchState, bool verbose);
+
+/// Assert that [actual] matches [matcher].
+///
+/// This is the main assertion function. [reason] is optional and is typically
+/// not supplied, as a reason is generated from [matcher]; if [reason]
+/// is included it is appended to the reason generated by the matcher.
+///
+/// [matcher] can be a value in which case it will be wrapped in an
+/// [equals] matcher.
+///
+/// If the assertion fails a [TestFailure] is thrown.
+///
+/// If [skip] is a String or `true`, the assertion is skipped. The arguments are
+/// still evaluated, but [actual] is not verified to match [matcher]. If
+/// [actual] is a [Future], the test won't complete until the future emits a
+/// value.
+///
+/// If [skip] is a string, it should explain why the assertion is skipped; this
+/// reason will be printed when running the test.
+///
+/// Certain matchers, like [completion] and [throwsA], either match or fail
+/// asynchronously. When you use [expect] with these matchers, it ensures that
+/// the test doesn't complete until the matcher has either matched or failed. If
+/// you want to wait for the matcher to complete before continuing the test, you
+/// can call [expectLater] instead and `await` the result.
+void expect(dynamic actual, dynamic matcher,
+    {String? reason,
+    Object? /* String|bool */ skip,
+    @Deprecated('Will be removed in 0.13.0.') bool verbose = false,
+    @Deprecated('Will be removed in 0.13.0.') ErrorFormatter? formatter}) {
+  _expect(actual, matcher,
+      reason: reason, skip: skip, verbose: verbose, formatter: formatter);
+}
+
+/// Just like [expect], but returns a [Future] that completes when the matcher
+/// has finished matching.
+///
+/// For the [completes] and [completion] matchers, as well as [throwsA] and
+/// related matchers when they're matched against a [Future], the returned
+/// future completes when the matched future completes. For the [prints]
+/// matcher, it completes when the future returned by the callback completes.
+/// Otherwise, it completes immediately.
+///
+/// If the matcher fails asynchronously, that failure is piped to the returned
+/// future where it can be handled by user code.
+Future expectLater(dynamic actual, dynamic matcher,
+        {String? reason, Object? /* String|bool */ skip}) =>
+    _expect(actual, matcher, reason: reason, skip: skip);
+
+/// The implementation of [expect] and [expectLater].
+Future _expect(Object? actual, Object? matcher,
+    {String? reason, skip, bool verbose = false, ErrorFormatter? formatter}) {
+  final test = TestHandle.current;
+  formatter ??= (actual, matcher, reason, matchState, verbose) {
+    var mismatchDescription = StringDescription();
+    matcher.describeMismatch(actual, mismatchDescription, matchState, verbose);
+
+    return formatFailure(matcher, actual, mismatchDescription.toString(),
+        reason: reason);
+  };
+
+  if (skip != null && skip is! bool && skip is! String) {
+    throw ArgumentError.value(skip, 'skip', 'must be a bool or a String');
+  }
+
+  matcher = wrapMatcher(matcher);
+  if (skip != null && skip != false) {
+    String message;
+    if (skip is String) {
+      message = 'Skip expect: $skip';
+    } else if (reason != null) {
+      message = 'Skip expect ($reason).';
+    } else {
+      var description = StringDescription().addDescriptionOf(matcher);
+      message = 'Skip expect ($description).';
+    }
+
+    test.markSkipped(message);
+    return Future.sync(() {});
+  }
+
+  if (matcher is AsyncMatcher) {
+    // Avoid async/await so that expect() throws synchronously when possible.
+    var result = matcher.matchAsync(actual);
+    expect(
+        result,
+        anyOf([
+          equals(null),
+          const TypeMatcher<Future>(),
+          const TypeMatcher<String>()
+        ]),
+        reason: 'matchAsync() may only return a String, a Future, or null.');
+
+    if (result is String) {
+      fail(formatFailure(matcher, actual, result, reason: reason));
+    } else if (result is Future) {
+      final outstandingWork = test.markPending();
+      return result.then((realResult) {
+        if (realResult == null) return;
+        fail(formatFailure(matcher as Matcher, actual, realResult as String,
+            reason: reason));
+      }).whenComplete(
+          // Always remove this, in case the failure is caught and handled
+          // gracefully.
+          outstandingWork.complete);
+    }
+
+    return Future.sync(() {});
+  }
+
+  var matchState = {};
+  try {
+    if ((matcher as Matcher).matches(actual, matchState)) {
+      return Future.sync(() {});
+    }
+  } catch (e, trace) {
+    reason ??= '$e at $trace';
+  }
+  fail(formatter(actual, matcher as Matcher, reason, matchState, verbose));
+}
+
+/// Convenience method for throwing a new [TestFailure] with the provided
+/// [message].
+Never fail(String message) => throw TestFailure(message);
+
+// The default error formatter.
+@Deprecated('Will be removed in 0.13.0.')
+String formatFailure(Matcher expected, Object? actual, String which,
+    {String? reason}) {
+  var buffer = StringBuffer();
+  buffer.writeln(indent(prettyPrint(expected), first: 'Expected: '));
+  buffer.writeln(indent(prettyPrint(actual), first: '  Actual: '));
+  if (which.isNotEmpty) buffer.writeln(indent(which, first: '   Which: '));
+  if (reason != null) buffer.writeln(reason);
+  return buffer.toString();
+}
diff --git a/pkgs/matcher/lib/src/expect/expect_async.dart b/pkgs/matcher/lib/src/expect/expect_async.dart
new file mode 100644
index 0000000..88cf6f2
--- /dev/null
+++ b/pkgs/matcher/lib/src/expect/expect_async.dart
@@ -0,0 +1,586 @@
+// 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.
+
+import 'package:test_api/hooks.dart';
+
+import 'util/placeholder.dart';
+
+// Function types returned by expectAsync# methods.
+
+typedef Func0<T> = T Function();
+typedef Func1<T, A> = T Function([A a]);
+typedef Func2<T, A, B> = T Function([A a, B b]);
+typedef Func3<T, A, B, C> = T Function([A a, B b, C c]);
+typedef Func4<T, A, B, C, D> = T Function([A a, B b, C c, D d]);
+typedef Func5<T, A, B, C, D, E> = T Function([A a, B b, C c, D d, E e]);
+typedef Func6<T, A, B, C, D, E, F> = T Function([A a, B b, C c, D d, E e, F f]);
+
+/// A wrapper for a function that ensures that it's called the appropriate
+/// number of times.
+///
+/// The containing test won't be considered to have completed successfully until
+/// this function has been called the appropriate number of times.
+///
+/// The wrapper function is accessible via [func]. It supports up to six
+/// optional and/or required positional arguments, but no named arguments.
+class _ExpectedFunction<T> {
+  /// The wrapped callback.
+  final Function _callback;
+
+  /// The minimum number of calls that are expected to be made to the function.
+  ///
+  /// If fewer calls than this are made, the test will fail.
+  final int _minExpectedCalls;
+
+  /// The maximum number of calls that are expected to be made to the function.
+  ///
+  /// If more calls than this are made, the test will fail.
+  final int _maxExpectedCalls;
+
+  /// A callback that should return whether the function is not expected to have
+  /// any more calls.
+  ///
+  /// This will be called after every time the function is run. The test case
+  /// won't be allowed to terminate until it returns `true`.
+  ///
+  /// This may be `null`. If so, the function is considered to be done after
+  /// it's been run once.
+  final bool Function()? _isDone;
+
+  /// A descriptive name for the function.
+  final String _id;
+
+  /// An optional description of why the function is expected to be called.
+  ///
+  /// If not passed, this will be an empty string.
+  final String _reason;
+
+  /// The number of times the function has been called.
+  int _actualCalls = 0;
+
+  /// The test in which this function was wrapped.
+  late final TestHandle _test;
+
+  /// Whether this function has been called the requisite number of times.
+  late bool _complete;
+
+  OutstandingWork? _outstandingWork;
+
+  /// Wraps [callback] in a function that asserts that it's called at least
+  /// [minExpected] times and no more than [maxExpected] times.
+  ///
+  /// If passed, [id] is used as a descriptive name fo the function and [reason]
+  /// 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})
+      : _callback = callback,
+        _minExpectedCalls = minExpected,
+        _maxExpectedCalls =
+            (maxExpected == 0 && minExpected > 0) ? minExpected : maxExpected,
+        _isDone = isDone,
+        _reason = reason == null ? '' : '\n$reason',
+        _id = _makeCallbackId(id, callback) {
+    try {
+      _test = TestHandle.current;
+    } on OutsideTestException {
+      throw StateError('`expectAsync` must be called within a test.');
+    }
+
+    if (maxExpected > 0 && minExpected > maxExpected) {
+      throw ArgumentError('max ($maxExpected) may not be less than count '
+          '($minExpected).');
+    }
+
+    if (isDone != null || minExpected > 0) {
+      _outstandingWork = _test.markPending();
+      _complete = false;
+    } else {
+      _complete = true;
+    }
+  }
+
+  /// Tries to find a reasonable name for [callback].
+  ///
+  /// 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) {
+    if (id != null) return '$id ';
+
+    // If the callback is not an anonymous closure, try to get the
+    // name.
+    var toString = callback.toString();
+    var prefix = "Function '";
+    var start = toString.indexOf(prefix);
+    if (start == -1) return '';
+
+    start += prefix.length;
+    var end = toString.indexOf("'", start);
+    if (end == -1) return '';
+    return '${toString.substring(start, end)} ';
+  }
+
+  /// Returns a function that has the same number of positional arguments as the
+  /// wrapped function (up to a total of 6).
+  Function get func {
+    if (_callback is Function(Never, Never, Never, Never, Never, Never)) {
+      return max6;
+    }
+    if (_callback is Function(Never, Never, Never, Never, Never)) return max5;
+    if (_callback is Function(Never, Never, Never, Never)) return max4;
+    if (_callback is Function(Never, Never, Never)) return max3;
+    if (_callback is Function(Never, Never)) return max2;
+    if (_callback is Function(Never)) return max1;
+    if (_callback is Function()) return max0;
+
+    _outstandingWork?.complete();
+    throw ArgumentError(
+        'The wrapped function has more than 6 required arguments');
+  }
+
+  // This indirection is critical. It ensures the returned function has an
+  // argument count of zero.
+  T max0() => max6();
+
+  T max1([Object? a0 = placeholder]) => max6(a0);
+
+  T max2([Object? a0 = placeholder, Object? a1 = placeholder]) => max6(a0, a1);
+
+  T max3(
+          [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]) =>
+      max6(a0, a1, a2, a3);
+
+  T max5(
+          [Object? a0 = placeholder,
+          Object? a1 = placeholder,
+          Object? a2 = placeholder,
+          Object? a3 = placeholder,
+          Object? a4 = placeholder]) =>
+      max6(a0, a1, a2, a3, a4);
+
+  T max6(
+          [Object? a0 = placeholder,
+          Object? a1 = placeholder,
+          Object? a2 = placeholder,
+          Object? a3 = placeholder,
+          Object? a4 = placeholder,
+          Object? a5 = placeholder]) =>
+      _run([a0, a1, a2, a3, a4, a5].where((a) => a != placeholder));
+
+  /// Runs the wrapped function with [args] and returns its return value.
+  T _run(Iterable args) {
+    // Note that in the old test, this returned `null` if it encountered an
+    // error, where now it just re-throws that error because Zone machinery will
+    // pass it to the invoker anyway.
+    try {
+      _actualCalls++;
+      if (_test.shouldBeDone) {
+        throw TestFailure(
+            'Callback ${_id}called ($_actualCalls) after test case '
+            '${_test.name} had already completed.$_reason');
+      } else if (_maxExpectedCalls >= 0 && _actualCalls > _maxExpectedCalls) {
+        throw TestFailure('Callback ${_id}called more times than expected '
+            '($_maxExpectedCalls).$_reason');
+      }
+
+      return Function.apply(_callback, args.toList()) as T;
+    } finally {
+      _afterRun();
+    }
+  }
+
+  /// After each time the function is run, check to see if it's complete.
+  void _afterRun() {
+    if (_complete) return;
+    if (_minExpectedCalls > 0 && _actualCalls < _minExpectedCalls) return;
+    if (_isDone != null && !_isDone()) return;
+
+    // Mark this callback as complete and remove it from the test case's
+    // outstanding callback count; if that hits zero the test is done.
+    _complete = true;
+    _outstandingWork?.complete();
+  }
+}
+
+/// This function is deprecated because it doesn't work well with strong mode.
+/// Use [expectAsync0], [expectAsync1],
+/// [expectAsync2], [expectAsync3], [expectAsync4], [expectAsync5], or
+/// [expectAsync6] instead.
+@Deprecated('Will be removed in 0.13.0')
+Function expectAsync(Function callback,
+        {int count = 1, int max = 0, String? id, String? reason}) =>
+    _ExpectedFunction(callback, count, max, id: id, reason: reason).func;
+
+/// Informs the framework that the given [callback] of arity 0 is expected to be
+/// called [count] number of times (by default 1).
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// The test framework will wait for the callback to run the [count] times
+/// before it considers the current test to be complete.
+///
+/// [max] can be used to specify an upper bound on the number of calls; if this
+/// is exceeded the test will fail. If [max] is `0` (the default), the callback
+/// is expected to be called exactly [count] times. If [max] is `-1`, the
+/// callback is allowed to be called any number of times greater than [count].
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with zero arguments. See also
+/// [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}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max0;
+
+/// Informs the framework that the given [callback] of arity 1 is expected to be
+/// called [count] number of times (by default 1).
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// The test framework will wait for the callback to run the [count] times
+/// before it considers the current test to be complete.
+///
+/// [max] can be used to specify an upper bound on the number of calls; if this
+/// is exceeded the test will fail. If [max] is `0` (the default), the callback
+/// is expected to be called exactly [count] times. If [max] is `-1`, the
+/// callback is allowed to be called any number of times greater than [count].
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with one argument. See also
+/// [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}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max1;
+
+/// Informs the framework that the given [callback] of arity 2 is expected to be
+/// called [count] number of times (by default 1).
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// The test framework will wait for the callback to run the [count] times
+/// before it considers the current test to be complete.
+///
+/// [max] can be used to specify an upper bound on the number of calls; if this
+/// is exceeded the test will fail. If [max] is `0` (the default), the callback
+/// is expected to be called exactly [count] times. If [max] is `-1`, the
+/// callback is allowed to be called any number of times greater than [count].
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with two arguments. See also
+/// [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}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max2;
+
+/// Informs the framework that the given [callback] of arity 3 is expected to be
+/// called [count] number of times (by default 1).
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// The test framework will wait for the callback to run the [count] times
+/// before it considers the current test to be complete.
+///
+/// [max] can be used to specify an upper bound on the number of calls; if this
+/// is exceeded the test will fail. If [max] is `0` (the default), the callback
+/// is expected to be called exactly [count] times. If [max] is `-1`, the
+/// callback is allowed to be called any number of times greater than [count].
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with three arguments. See also
+/// [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}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max3;
+
+/// Informs the framework that the given [callback] of arity 4 is expected to be
+/// called [count] number of times (by default 1).
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// The test framework will wait for the callback to run the [count] times
+/// before it considers the current test to be complete.
+///
+/// [max] can be used to specify an upper bound on the number of calls; if this
+/// is exceeded the test will fail. If [max] is `0` (the default), the callback
+/// is expected to be called exactly [count] times. If [max] is `-1`, the
+/// callback is allowed to be called any number of times greater than [count].
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with four arguments. See also
+/// [expectAsync0], [expectAsync1], [expectAsync2], [expectAsync3],
+/// [expectAsync5], and [expectAsync6] for callbacks with different arity.
+Func4<T, A, B, C, D> expectAsync4<T, A, B, C, D>(
+        T Function(A, B, C, D) callback,
+        {int count = 1,
+        int max = 0,
+        String? id,
+        String? reason}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max4;
+
+/// Informs the framework that the given [callback] of arity 5 is expected to be
+/// called [count] number of times (by default 1).
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// The test framework will wait for the callback to run the [count] times
+/// before it considers the current test to be complete.
+///
+/// [max] can be used to specify an upper bound on the number of calls; if this
+/// is exceeded the test will fail. If [max] is `0` (the default), the callback
+/// is expected to be called exactly [count] times. If [max] is `-1`, the
+/// callback is allowed to be called any number of times greater than [count].
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with five arguments. See also
+/// [expectAsync0], [expectAsync1], [expectAsync2], [expectAsync3],
+/// [expectAsync4], and [expectAsync6] for callbacks with different arity.
+Func5<T, A, B, C, D, E> expectAsync5<T, A, B, C, D, E>(
+        T Function(A, B, C, D, E) callback,
+        {int count = 1,
+        int max = 0,
+        String? id,
+        String? reason}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max5;
+
+/// Informs the framework that the given [callback] of arity 6 is expected to be
+/// called [count] number of times (by default 1).
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// The test framework will wait for the callback to run the [count] times
+/// before it considers the current test to be complete.
+///
+/// [max] can be used to specify an upper bound on the number of calls; if this
+/// is exceeded the test will fail. If [max] is `0` (the default), the callback
+/// is expected to be called exactly [count] times. If [max] is `-1`, the
+/// callback is allowed to be called any number of times greater than [count].
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with six arguments. See also
+/// [expectAsync0], [expectAsync1], [expectAsync2], [expectAsync3],
+/// [expectAsync4], and [expectAsync5] for callbacks with different arity.
+Func6<T, A, B, C, D, E, F> expectAsync6<T, A, B, C, D, E, F>(
+        T Function(A, B, C, D, E, F) callback,
+        {int count = 1,
+        int max = 0,
+        String? id,
+        String? reason}) =>
+    _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max6;
+
+/// This function is deprecated because it doesn't work well with strong mode.
+/// Use [expectAsyncUntil0], [expectAsyncUntil1],
+/// [expectAsyncUntil2], [expectAsyncUntil3], [expectAsyncUntil4],
+/// [expectAsyncUntil5], or [expectAsyncUntil6] instead.
+@Deprecated('Will be removed in 0.13.0')
+Function expectAsyncUntil(Function callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction(callback, 0, -1, id: id, reason: reason, isDone: isDone)
+        .func;
+
+/// Informs the framework that the given [callback] of arity 0 is expected to be
+/// called until [isDone] returns true.
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// [isDone] is called after each time the function is run. Only when it returns
+/// true will the callback be considered complete.
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with zero arguments. See also
+/// [expectAsyncUntil1], [expectAsyncUntil2], [expectAsyncUntil3],
+/// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for
+/// callbacks with different arity.
+Func0<T> expectAsyncUntil0<T>(T Function() callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max0;
+
+/// Informs the framework that the given [callback] of arity 1 is expected to be
+/// called until [isDone] returns true.
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// [isDone] is called after each time the function is run. Only when it returns
+/// true will the callback be considered complete.
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with one argument. See also
+/// [expectAsyncUntil0], [expectAsyncUntil2], [expectAsyncUntil3],
+/// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for
+/// callbacks with different arity.
+Func1<T, A> expectAsyncUntil1<T, A>(
+        T Function(A) callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max1;
+
+/// Informs the framework that the given [callback] of arity 2 is expected to be
+/// called until [isDone] returns true.
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// [isDone] is called after each time the function is run. Only when it returns
+/// true will the callback be considered complete.
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with two arguments. See also
+/// [expectAsyncUntil0], [expectAsyncUntil1], [expectAsyncUntil3],
+/// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for
+/// callbacks with different arity.
+Func2<T, A, B> expectAsyncUntil2<T, A, B>(
+        T Function(A, B) callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max2;
+
+/// Informs the framework that the given [callback] of arity 3 is expected to be
+/// called until [isDone] returns true.
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// [isDone] is called after each time the function is run. Only when it returns
+/// true will the callback be considered complete.
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with three arguments. See also
+/// [expectAsyncUntil0], [expectAsyncUntil1], [expectAsyncUntil2],
+/// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for
+/// callbacks with different arity.
+Func3<T, A, B, C> expectAsyncUntil3<T, A, B, C>(
+        T Function(A, B, C) callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max3;
+
+/// Informs the framework that the given [callback] of arity 4 is expected to be
+/// called until [isDone] returns true.
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// [isDone] is called after each time the function is run. Only when it returns
+/// true will the callback be considered complete.
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with four arguments. See also
+/// [expectAsyncUntil0], [expectAsyncUntil1], [expectAsyncUntil2],
+/// [expectAsyncUntil3], [expectAsyncUntil5], and [expectAsyncUntil6] for
+/// callbacks with different arity.
+Func4<T, A, B, C, D> expectAsyncUntil4<T, A, B, C, D>(
+        T Function(A, B, C, D) callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max4;
+
+/// Informs the framework that the given [callback] of arity 5 is expected to be
+/// called until [isDone] returns true.
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// [isDone] is called after each time the function is run. Only when it returns
+/// true will the callback be considered complete.
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with five arguments. See also
+/// [expectAsyncUntil0], [expectAsyncUntil1], [expectAsyncUntil2],
+/// [expectAsyncUntil3], [expectAsyncUntil4], and [expectAsyncUntil6] for
+/// callbacks with different arity.
+Func5<T, A, B, C, D, E> expectAsyncUntil5<T, A, B, C, D, E>(
+        T Function(A, B, C, D, E) callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max5;
+
+/// Informs the framework that the given [callback] of arity 6 is expected to be
+/// called until [isDone] returns true.
+///
+/// Returns a wrapped function that should be used as a replacement of the
+/// original callback.
+///
+/// [isDone] is called after each time the function is run. Only when it returns
+/// true will the callback be considered complete.
+///
+/// Both [id] and [reason] are optional and provide extra information about the
+/// callback when debugging. [id] should be the name of the callback, while
+/// [reason] should be the reason the callback is expected to be called.
+///
+/// This method takes callbacks with six arguments. See also
+/// [expectAsyncUntil0], [expectAsyncUntil1], [expectAsyncUntil2],
+/// [expectAsyncUntil3], [expectAsyncUntil4], and [expectAsyncUntil5] for
+/// callbacks with different arity.
+Func6<T, A, B, C, D, E, F> expectAsyncUntil6<T, A, B, C, D, E, F>(
+        T Function(A, B, C, D, E, F) callback, bool Function() isDone,
+        {String? id, String? reason}) =>
+    _ExpectedFunction<T>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
+        .max6;
diff --git a/pkgs/matcher/lib/src/expect/future_matchers.dart b/pkgs/matcher/lib/src/expect/future_matchers.dart
new file mode 100644
index 0000000..407b9b8
--- /dev/null
+++ b/pkgs/matcher/lib/src/expect/future_matchers.dart
@@ -0,0 +1,119 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ignore_for_file: deprecated_member_use_from_same_package
+
+import 'package:test_api/hooks.dart' show pumpEventQueue;
+
+import '../description.dart';
+import '../interfaces.dart';
+import '../util.dart';
+import 'async_matcher.dart';
+import 'expect.dart';
+import 'throws_matcher.dart';
+import 'util/pretty_print.dart';
+
+/// Matches a [Future] that completes successfully with any value.
+///
+/// This creates an asynchronous expectation. The call to [expect] will return
+/// immediately and execution will continue. To wait for the future to
+/// complete and the expectation to run use [expectLater] and wait on the
+/// returned future.
+///
+/// To test that a Future completes with an exception, you can use [throws] and
+/// [throwsA].
+final Matcher completes = const _Completes(null);
+
+/// Matches a [Future] that completes successfully with a value that matches
+/// [matcher].
+///
+/// This creates an asynchronous expectation. The call to [expect] will return
+/// immediately and execution will continue. Later, when the future completes,
+/// the expectation against [matcher] will run. To wait for the future to
+/// complete and the expectation to run use [expectLater] and wait on the
+/// returned future.
+///
+/// To test that a Future completes with an exception, you can use [throws] and
+/// [throwsA].
+Matcher completion(Object? matcher,
+        [@Deprecated('this parameter is ignored') String? description]) =>
+    _Completes(wrapMatcher(matcher));
+
+class _Completes extends AsyncMatcher {
+  final Matcher? _matcher;
+
+  const _Completes(this._matcher);
+
+  // Avoid async/await so we synchronously start listening to [item].
+  @override
+  dynamic /*FutureOr<String>*/ matchAsync(Object? item) {
+    if (item is! Future) return 'was not a Future';
+
+    return item.then((value) async {
+      if (_matcher == null) return null;
+
+      String? result;
+      if (_matcher is AsyncMatcher) {
+        result = await _matcher.matchAsync(value) as String?;
+        if (result == null) return null;
+      } else {
+        var matchState = {};
+        if (_matcher.matches(value, matchState)) return null;
+        result = _matcher
+            .describeMismatch(value, StringDescription(), matchState, false)
+            .toString();
+      }
+
+      var buffer = StringBuffer();
+      buffer.writeln(indent(prettyPrint(value), first: 'emitted '));
+      if (result.isNotEmpty) buffer.writeln(indent(result, first: '  which '));
+      return buffer.toString().trimRight();
+    });
+  }
+
+  @override
+  Description describe(Description description) {
+    if (_matcher == null) {
+      description.add('completes successfully');
+    } else {
+      description.add('completes to a value that ').addDescriptionOf(_matcher);
+    }
+    return description;
+  }
+}
+
+/// Matches a [Future] that does not complete.
+///
+/// Note that this creates an asynchronous expectation. The call to
+/// `expect()` that includes this will return immediately and execution will
+/// continue.
+final Matcher doesNotComplete = const _DoesNotComplete();
+
+class _DoesNotComplete extends Matcher {
+  const _DoesNotComplete();
+
+  @override
+  Description describe(Description description) {
+    description.add('does not complete');
+    return description;
+  }
+
+  @override
+  bool matches(Object? item, Map matchState) {
+    if (item is! Future) return false;
+    item.then((value) {
+      fail('Future was not expected to complete but completed with a value of '
+          '$value');
+    });
+    expect(pumpEventQueue(), completes);
+    return true;
+  }
+
+  @override
+  Description describeMismatch(
+      Object? item, Description description, Map matchState, bool verbose) {
+    if (item is! Future) return description.add('$item is not a Future');
+    return description;
+  }
+}
diff --git a/pkgs/matcher/lib/src/expect/never_called.dart b/pkgs/matcher/lib/src/expect/never_called.dart
new file mode 100644
index 0000000..20b5299
--- /dev/null
+++ b/pkgs/matcher/lib/src/expect/never_called.dart
@@ -0,0 +1,68 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test_api/hooks.dart';
+
+import 'expect.dart';
+import 'future_matchers.dart';
+import 'util/placeholder.dart';
+import 'util/pretty_print.dart';
+
+/// Returns a function that causes the test to fail if it's called.
+///
+/// This can safely be passed in place of any callback that takes ten or fewer
+/// positional parameters. For example:
+///
+/// ```dart
+/// // Asserts that the stream never emits an event.
+/// stream.listen(neverCalled);
+/// ```
+///
+/// This also ensures that the test doesn't complete until a call to
+/// [pumpEventQueue] finishes, so that the callback has a chance to be called.
+Null Function(
+    [Object?,
+    Object?,
+    Object?,
+    Object?,
+    Object?,
+    Object?,
+    Object?,
+    Object?,
+    Object?,
+    Object?]) get neverCalled {
+  // Make sure the test stays alive long enough to call the function if it's
+  // going to.
+  expect(pumpEventQueue(), completes);
+
+  var zone = Zone.current;
+  return (
+      [a1 = placeholder,
+      a2 = placeholder,
+      a3 = placeholder,
+      a4 = placeholder,
+      a5 = placeholder,
+      a6 = placeholder,
+      a7 = placeholder,
+      a8 = placeholder,
+      a9 = placeholder,
+      a10 = placeholder]) {
+    var arguments = [a1, a2, a3, a4, a5, a6, a7, a8, a9, a10]
+        .where((argument) => argument != placeholder)
+        .toList();
+
+    var argsText = arguments.isEmpty
+        ? ' no arguments.'
+        : ':\n${bullet(arguments.map(prettyPrint))}';
+    zone.handleUncaughtError(
+        TestFailure(
+            'Callback should never have been called, but it was called with'
+            '$argsText'),
+        zone.run(Chain.current));
+    return null;
+  };
+}
diff --git a/pkgs/matcher/lib/src/expect/prints_matcher.dart b/pkgs/matcher/lib/src/expect/prints_matcher.dart
new file mode 100644
index 0000000..57ae95e
--- /dev/null
+++ b/pkgs/matcher/lib/src/expect/prints_matcher.dart
@@ -0,0 +1,72 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import '../description.dart';
+import '../interfaces.dart';
+import '../util.dart';
+import 'async_matcher.dart';
+import 'expect.dart';
+import 'util/pretty_print.dart';
+
+/// Matches a [Function] that prints text that matches [matcher].
+///
+/// [matcher] may be a String or a [Matcher].
+///
+/// If the function this runs against returns a [Future], all text printed by
+/// the function (using [Zone] scoping) until that Future completes is matched.
+///
+/// This only tracks text printed using the [print] function.
+///
+/// This returns an [AsyncMatcher], so [expect] won't complete until the matched
+/// function does.
+Matcher prints(Object? matcher) => _Prints(wrapMatcher(matcher));
+
+class _Prints extends AsyncMatcher {
+  final Matcher _matcher;
+
+  _Prints(this._matcher);
+
+  // Avoid async/await so we synchronously fail if the function is
+  // synchronous.
+  @override
+  dynamic /*FutureOr<String>*/ matchAsync(Object? item) {
+    if (item is! Function()) return 'was not a unary Function';
+
+    var buffer = StringBuffer();
+    var result = runZoned(item,
+        zoneSpecification: ZoneSpecification(print: (_, __, ____, line) {
+      buffer.writeln(line);
+    }));
+
+    return result is Future
+        ? result.then((_) => _check(buffer.toString()))
+        : _check(buffer.toString());
+  }
+
+  @override
+  Description describe(Description description) =>
+      description.add('prints ').addDescriptionOf(_matcher);
+
+  /// Verifies that [actual] matches [_matcher] and returns a [String]
+  /// description of the failure if it doesn't.
+  String? _check(String actual) {
+    var matchState = {};
+    if (_matcher.matches(actual, matchState)) return null;
+
+    var result = _matcher
+        .describeMismatch(actual, StringDescription(), matchState, false)
+        .toString();
+
+    var buffer = StringBuffer();
+    if (actual.isEmpty) {
+      buffer.writeln('printed nothing');
+    } else {
+      buffer.writeln(indent(prettyPrint(actual), first: 'printed '));
+    }
+    if (result.isNotEmpty) buffer.writeln(indent(result, first: '  which '));
+    return buffer.toString().trimRight();
+  }
+}
diff --git a/pkgs/matcher/lib/src/expect/stream_matcher.dart b/pkgs/matcher/lib/src/expect/stream_matcher.dart
new file mode 100644
index 0000000..0c1d852
--- /dev/null
+++ b/pkgs/matcher/lib/src/expect/stream_matcher.dart
@@ -0,0 +1,196 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:async/async.dart';
+import 'package:test_api/hooks.dart';
+
+import '../interfaces.dart';
+import 'async_matcher.dart';
+import 'expect.dart';
+import 'util/pretty_print.dart';
+
+/// A matcher that matches events from [Stream]s or [StreamQueue]s.
+///
+/// Stream matchers are designed to make it straightforward to create complex
+/// expectations for streams, and to interleave expectations with the rest of a
+/// test. They can be used on a [Stream] to match all events it emits:
+///
+/// ```dart
+/// expect(stream, emitsInOrder([
+///   // Values match individual events.
+///   "Ready.",
+///
+///   // Matchers also run against individual events.
+///   startsWith("Loading took"),
+///
+///   // Stream matchers can be nested. This asserts that one of two events are
+///   // emitted after the "Loading took" line.
+///   emitsAnyOf(["Succeeded!", "Failed!"]),
+///
+///   // By default, more events are allowed after the matcher finishes
+///   // matching. This asserts instead that the stream emits a done event and
+///   // nothing else.
+///   emitsDone
+/// ]));
+/// ```
+///
+/// It can also match a [StreamQueue], in which case it consumes the matched
+/// events. The call to [expect] returns a [Future] that completes when the
+/// matcher is done matching. You can `await` this to consume different events
+/// at different times:
+///
+/// ```dart
+/// var stdout = StreamQueue(stdoutLineStream);
+///
+/// // Ignore lines from the process until it's about to emit the URL.
+/// await expectLater(stdout, emitsThrough('WebSocket URL:'));
+///
+/// // Parse the next line as a URL.
+/// var url = Uri.parse(await stdout.next);
+/// expect(url.host, equals('localhost'));
+///
+/// // You can match against the same StreamQueue multiple times.
+/// await expectLater(stdout, emits('Waiting for connection...'));
+/// ```
+///
+/// Users can call [StreamMatcher] to create custom matchers.
+abstract class StreamMatcher extends Matcher {
+  /// The description of this matcher.
+  ///
+  /// This is in the subjunctive mood, which means it can be used after the word
+  /// "should". For example, it might be "emit the right events".
+  String get description;
+
+  /// Creates a new [StreamMatcher] described by [description] that matches
+  /// events with [matchQueue].
+  ///
+  /// The [matchQueue] callback is used to implement [StreamMatcher.matchQueue],
+  /// and should follow all the guarantees of that method. In particular:
+  ///
+  /// * If it matches successfully, it should return `null` and possibly consume
+  ///   events.
+  /// * If it fails to match, consume no events and return a description of the
+  ///   failure.
+  /// * The description should be in past tense.
+  /// * The description should be grammatically valid when used after "the
+  ///   stream"—"emitted the wrong events", for example.
+  ///
+  /// The [matchQueue] callback may return the empty string to indicate a
+  /// failure if it has no information to add beyond the description of the
+  /// failure and the events actually emitted by the stream.
+  ///
+  /// 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;
+
+  /// Tries to match events emitted by [queue].
+  ///
+  /// If this matches successfully, it consumes the matching events from [queue]
+  /// and returns `null`.
+  ///
+  /// If this fails to match, it doesn't consume any events and returns a
+  /// description of the failure. This description is in the past tense, and
+  /// could grammatically be used after "the stream". For example, it might
+  /// return "emitted the wrong events".
+  ///
+  /// The description string may also be empty, which indicates that the
+  /// matcher's description and the events actually emitted by the stream are
+  /// enough to understand the failure.
+  ///
+  /// If the queue emits an error, that error is re-thrown unless otherwise
+  /// indicated by the matcher.
+  Future<String?> matchQueue(StreamQueue queue);
+}
+
+/// A concrete implementation of [StreamMatcher].
+///
+/// This is separate from the original type to hide the private [AsyncMatcher]
+/// interface.
+class _StreamMatcher extends AsyncMatcher implements StreamMatcher {
+  @override
+  final String description;
+
+  /// The callback used to implement [matchQueue].
+  final Future<String?> Function(StreamQueue) _matchQueue;
+
+  _StreamMatcher(this._matchQueue, this.description);
+
+  @override
+  Future<String?> matchQueue(StreamQueue queue) => _matchQueue(queue);
+
+  @override
+  dynamic /*FutureOr<String>*/ matchAsync(Object? item) {
+    StreamQueue queue;
+    var shouldCancelQueue = false;
+    if (item is StreamQueue) {
+      queue = item;
+    } else if (item is Stream) {
+      queue = StreamQueue(item);
+      shouldCancelQueue = true;
+    } else {
+      return 'was not a Stream or a StreamQueue';
+    }
+
+    // Avoid async/await in the outer method so that we synchronously error out
+    // for an invalid argument type.
+    var transaction = queue.startTransaction();
+    var copy = transaction.newQueue();
+    return matchQueue(copy).then((result) async {
+      // Accept the transaction if the result is null, indicating that the match
+      // succeeded.
+      if (result == null) {
+        transaction.commit(copy);
+        return null;
+      }
+
+      // 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 subscription = Result.captureStreamTransformer
+          .bind(replay.rest.cast())
+          .listen(events.add, onDone: () => events.add(null));
+
+      // Wait on a timer tick so all buffered events are emitted.
+      await Future.delayed(Duration.zero);
+      _unawaited(subscription.cancel());
+
+      var eventsString = events.map((event) {
+        if (event == null) {
+          return 'x Stream closed.';
+        } else if (event.isValue) {
+          return addBullet(event.asValue!.value.toString());
+        } else {
+          var error = event.asError!;
+          var chain = TestHandle.current.formatStackTrace(error.stackTrace);
+          var text = '${error.error}\n$chain';
+          return indent(text, first: '! ');
+        }
+      }).join('\n');
+      if (eventsString.isEmpty) eventsString = 'no events';
+
+      transaction.reject();
+
+      var buffer = StringBuffer();
+      buffer.writeln(indent(eventsString, first: 'emitted '));
+      if (result.isNotEmpty) buffer.writeln(indent(result, first: '  which '));
+      return buffer.toString().trimRight();
+    }, onError: (Object error) {
+      transaction.reject();
+      // ignore: only_throw_errors
+      throw error;
+    }).then((result) {
+      if (shouldCancelQueue) queue.cancel();
+      return result;
+    });
+  }
+
+  @override
+  Description describe(Description description) =>
+      description.add('should ').add(this.description);
+}
+
+void _unawaited(Future<void> f) {}
diff --git a/pkgs/matcher/lib/src/expect/stream_matchers.dart b/pkgs/matcher/lib/src/expect/stream_matchers.dart
new file mode 100644
index 0000000..02efff3
--- /dev/null
+++ b/pkgs/matcher/lib/src/expect/stream_matchers.dart
@@ -0,0 +1,377 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:async/async.dart';
+
+import '../description.dart';
+import '../interfaces.dart';
+import '../util.dart';
+import 'async_matcher.dart';
+import 'stream_matcher.dart';
+import 'throws_matcher.dart';
+import 'util/pretty_print.dart';
+
+/// Returns a [StreamMatcher] that asserts that the stream emits a "done" event.
+final emitsDone = StreamMatcher(
+    (queue) async => (await queue.hasNext) ? '' : null, 'be done');
+
+/// Returns a [StreamMatcher] for [matcher].
+///
+/// If [matcher] is already a [StreamMatcher], it's returned as-is. If it's any
+/// other [Matcher], this matches a single event that matches that matcher. If
+/// it's any other Object, this matches a single event that's equal to that
+/// object.
+///
+/// This functions like [wrapMatcher] for [StreamMatcher]s: it can convert any
+/// matcher-like value into a proper [StreamMatcher].
+StreamMatcher emits(Object? matcher) {
+  if (matcher is StreamMatcher) return matcher;
+  var wrapped = wrapMatcher(matcher);
+
+  var matcherDescription = wrapped.describe(StringDescription());
+
+  return StreamMatcher((queue) async {
+    if (!await queue.hasNext) return '';
+
+    var matchState = {};
+    var actual = await queue.next;
+    if (wrapped.matches(actual, matchState)) return null;
+
+    var mismatchDescription = StringDescription();
+    wrapped.describeMismatch(actual, mismatchDescription, matchState, false);
+
+    if (mismatchDescription.length == 0) return '';
+    return 'emitted an event that $mismatchDescription';
+  },
+      // TODO(nweiz): add "should" once matcher#42 is fixed.
+      'emit an event that $matcherDescription');
+}
+
+/// Returns a [StreamMatcher] that matches a single error event that matches
+/// [matcher].
+StreamMatcher emitsError(Object? matcher) {
+  var wrapped = wrapMatcher(matcher);
+  var matcherDescription = wrapped.describe(StringDescription());
+  var throwsMatcher = throwsA(wrapped) as AsyncMatcher;
+
+  return StreamMatcher(
+      (queue) => throwsMatcher.matchAsync(queue.next) as Future<String?>,
+      // TODO(nweiz): add "should" once matcher#42 is fixed.
+      'emit an error that $matcherDescription');
+}
+
+/// Returns a [StreamMatcher] that allows (but doesn't require) [matcher] to
+/// match the stream.
+///
+/// This matcher always succeeds; if [matcher] doesn't match, this just consumes
+/// no events.
+StreamMatcher mayEmit(Object? matcher) {
+  var streamMatcher = emits(matcher);
+  return StreamMatcher((queue) async {
+    await queue.withTransaction(
+        (copy) async => (await streamMatcher.matchQueue(copy)) == null);
+    return null;
+  }, 'maybe ${streamMatcher.description}');
+}
+
+/// Returns a [StreamMatcher] that matches the stream if at least one of
+/// [matchers] matches.
+///
+/// If multiple matchers match the stream, this chooses the matcher that
+/// consumes as many events as possible.
+///
+/// If any matchers match the stream, no errors from other matchers are thrown.
+/// If no matchers match and multiple matchers threw errors, the first error is
+/// re-thrown.
+StreamMatcher emitsAnyOf(Iterable matchers) {
+  var streamMatchers = matchers.map(emits).toList();
+  if (streamMatchers.isEmpty) {
+    throw ArgumentError('matcher may not be empty');
+  }
+
+  if (streamMatchers.length == 1) return streamMatchers.first;
+  var description = 'do one of the following:\n'
+      '${bullet(streamMatchers.map((matcher) => matcher.description))}';
+
+  return StreamMatcher((queue) async {
+    var transaction = queue.startTransaction();
+
+    // 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?>.filled(matchers.length, null);
+
+    // The first error thrown. If no matchers match and this exists, we rethrow
+    // it.
+    Object? firstError;
+    StackTrace? firstStackTrace;
+
+    var futures = <Future>[];
+    StreamQueue? consumedMost;
+    for (var i = 0; i < matchers.length; i++) {
+      futures.add(() async {
+        var copy = transaction.newQueue();
+
+        String? result;
+        try {
+          result = await streamMatchers[i].matchQueue(copy);
+        } catch (error, stackTrace) {
+          if (firstError == null) {
+            firstError = error;
+            firstStackTrace = stackTrace;
+          }
+          return;
+        }
+
+        if (result != null) {
+          failures[i] = result;
+        } else if (consumedMost == null ||
+            consumedMost!.eventsDispatched < copy.eventsDispatched) {
+          consumedMost = copy;
+        }
+      }());
+    }
+
+    await Future.wait(futures);
+
+    if (consumedMost == null) {
+      transaction.reject();
+      if (firstError != null) {
+        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) {
+          message += message.contains('\n') ? '\n' : ' ';
+          message += 'because it ${failures[i]}';
+        }
+
+        failureMessages.add(message);
+      }
+
+      return 'failed all options:\n${bullet(failureMessages)}';
+    } else {
+      transaction.commit(consumedMost!);
+      return null;
+    }
+  }, description);
+}
+
+/// Returns a [StreamMatcher] that matches the stream if each matcher in
+/// [matchers] matches, one after another.
+///
+/// If any matcher fails to match, this fails and consumes no events.
+StreamMatcher emitsInOrder(Iterable matchers) {
+  var streamMatchers = matchers.map(emits).toList();
+  if (streamMatchers.length == 1) return streamMatchers.first;
+
+  var description = 'do the following in order:\n'
+      '${bullet(streamMatchers.map((matcher) => matcher.description))}';
+
+  return StreamMatcher((queue) async {
+    for (var i = 0; i < streamMatchers.length; i++) {
+      var matcher = streamMatchers[i];
+      var result = await matcher.matchQueue(queue);
+      if (result == null) continue;
+
+      var newResult = "didn't ${matcher.description}";
+      if (result.isNotEmpty) {
+        newResult += newResult.contains('\n') ? '\n' : ' ';
+        newResult += 'because it $result';
+      }
+      return newResult;
+    }
+    return null;
+  }, description);
+}
+
+/// Returns a [StreamMatcher] that matches any number of events followed by
+/// events that match [matcher].
+///
+/// This consumes all events matched by [matcher], as well as all events before.
+/// If the stream emits a done event without matching [matcher], this fails and
+/// consumes no events.
+StreamMatcher emitsThrough(Object? matcher) {
+  var streamMatcher = emits(matcher);
+  return StreamMatcher((queue) async {
+    var failures = <String>[];
+
+    Future<bool> tryHere() => queue.withTransaction((copy) async {
+          var result = await streamMatcher.matchQueue(copy);
+          if (result == null) return true;
+          failures.add(result);
+          return false;
+        });
+
+    while (await queue.hasNext) {
+      if (await tryHere()) return null;
+      await queue.next;
+    }
+
+    // Try after the queue is done in case the matcher can match an empty
+    // stream.
+    if (await tryHere()) return null;
+
+    var result = 'never did ${streamMatcher.description}';
+
+    var failureMessages =
+        bullet(failures.where((failure) => failure.isNotEmpty));
+    if (failureMessages.isNotEmpty) {
+      result += result.contains('\n') ? '\n' : ' ';
+      result += 'because it:\n$failureMessages';
+    }
+
+    return result;
+  }, 'eventually ${streamMatcher.description}');
+}
+
+/// Returns a [StreamMatcher] that matches any number of events that match
+/// [matcher].
+///
+/// This consumes events until [matcher] no longer matches. It always succeeds;
+/// if [matcher] doesn't match, this just consumes no events. It never rethrows
+/// errors.
+StreamMatcher mayEmitMultiple(Object? matcher) {
+  var streamMatcher = emits(matcher);
+
+  var description = streamMatcher.description;
+  description += description.contains('\n') ? '\n' : ' ';
+  description += 'zero or more times';
+
+  return StreamMatcher((queue) async {
+    while (await _tryMatch(queue, streamMatcher)) {
+      // Do nothing; the matcher presumably already consumed events.
+    }
+    return null;
+  }, description);
+}
+
+/// Returns a [StreamMatcher] that matches a stream that never matches
+/// [matcher].
+///
+/// This doesn't complete until the stream emits a done event. It never consumes
+/// any events. It never re-throws errors.
+StreamMatcher neverEmits(Object? matcher) {
+  var streamMatcher = emits(matcher);
+  return StreamMatcher((queue) async {
+    var events = 0;
+    var matched = false;
+    await queue.withTransaction((copy) async {
+      while (await copy.hasNext) {
+        matched = await _tryMatch(copy, streamMatcher);
+        if (matched) return false;
+
+        events++;
+
+        try {
+          await copy.next;
+        } catch (_) {
+          // Ignore errors events.
+        }
+      }
+
+      matched = await _tryMatch(copy, streamMatcher);
+      return false;
+    });
+
+    if (!matched) return null;
+    return "after $events ${pluralize('event', events)} did "
+        '${streamMatcher.description}';
+  }, 'never ${streamMatcher.description}');
+}
+
+/// Returns whether [matcher] matches [queue] at its current position.
+///
+/// This treats errors as failures to match.
+Future<bool> _tryMatch(StreamQueue queue, StreamMatcher matcher) {
+  return queue.withTransaction((copy) async {
+    try {
+      return (await matcher.matchQueue(copy)) == null;
+    } catch (_) {
+      return false;
+    }
+  });
+}
+
+/// Returns a [StreamMatcher] that matches the stream if each matcher in
+/// [matchers] matches, in any order.
+///
+/// If any matcher fails to match, this fails and consumes no events. If the
+/// matchers match in multiple different possible orders, this chooses the order
+/// that consumes as many events as possible.
+///
+/// If any sequence of matchers matches the stream, no errors from other
+/// sequences are thrown. If no sequences match and multiple sequences throw
+/// errors, the first error is re-thrown.
+///
+/// Note that checking every ordering of [matchers] is O(n!) in the worst case,
+/// so this should only be called when there are very few [matchers].
+StreamMatcher emitsInAnyOrder(Iterable matchers) {
+  var streamMatchers = matchers.map(emits).toSet();
+  if (streamMatchers.length == 1) return streamMatchers.first;
+  var description = 'do the following in any order:\n'
+      '${bullet(streamMatchers.map((matcher) => matcher.description))}';
+
+  return StreamMatcher(
+      (queue) async => await _tryInAnyOrder(queue, streamMatchers) ? null : '',
+      description);
+}
+
+/// Returns whether [queue] matches [matchers] in any order.
+Future<bool> _tryInAnyOrder(
+    StreamQueue queue, Set<StreamMatcher> matchers) async {
+  if (matchers.length == 1) {
+    return await matchers.first.matchQueue(queue) == null;
+  }
+
+  var transaction = queue.startTransaction();
+  StreamQueue? consumedMost;
+
+  // The first error thrown. If no matchers match and this exists, we rethrow
+  // it.
+  Object? firstError;
+  StackTrace? firstStackTrace;
+
+  await Future.wait(matchers.map((matcher) async {
+    var copy = transaction.newQueue();
+    try {
+      if (await matcher.matchQueue(copy) != null) return;
+    } catch (error, stackTrace) {
+      if (firstError == null) {
+        firstError = error;
+        firstStackTrace = stackTrace;
+      }
+      return;
+    }
+
+    var rest = Set<StreamMatcher>.from(matchers);
+    rest.remove(matcher);
+
+    try {
+      if (!await _tryInAnyOrder(copy, rest)) return;
+    } catch (error, stackTrace) {
+      if (firstError == null) {
+        firstError = error;
+        firstStackTrace = stackTrace;
+      }
+      return;
+    }
+
+    if (consumedMost == null ||
+        consumedMost!.eventsDispatched < copy.eventsDispatched) {
+      consumedMost = copy;
+    }
+  }));
+
+  if (consumedMost == null) {
+    transaction.reject();
+    if (firstError != null) await Future.error(firstError!, firstStackTrace);
+    return false;
+  } else {
+    transaction.commit(consumedMost!);
+    return true;
+  }
+}
diff --git a/pkgs/matcher/lib/src/expect/throws_matcher.dart b/pkgs/matcher/lib/src/expect/throws_matcher.dart
new file mode 100644
index 0000000..37676ef
--- /dev/null
+++ b/pkgs/matcher/lib/src/expect/throws_matcher.dart
@@ -0,0 +1,139 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ignore_for_file: deprecated_member_use_from_same_package
+
+import 'package:test_api/hooks.dart';
+
+import '../description.dart';
+import '../interfaces.dart';
+import '../util.dart';
+import 'async_matcher.dart';
+import 'util/pretty_print.dart';
+
+/// This function is deprecated.
+///
+/// Use [throwsA] instead. We strongly recommend that you add assertions about
+/// at least the type of the error, but you can write `throwsA(anything)` to
+/// mimic the behavior of this matcher.
+@Deprecated('Will be removed in 0.13.0')
+const Matcher throws = Throws();
+
+/// This can be used to match three kinds of objects:
+///
+/// * A [Function] that throws an exception when called. The function cannot
+///   take any arguments. If you want to test that a function expecting
+///   arguments throws, wrap it in another zero-argument function that calls
+///   the one you want to test.
+///
+/// * A [Future] that completes with an exception. Note that this creates an
+///   asynchronous expectation. The call to `expect()` that includes this will
+///   return immediately and execution will continue. Later, when the future
+///   completes, the actual expectation will run.
+///
+/// * A [Function] that returns a [Future] that completes with an exception.
+///
+/// In all three cases, when an exception is thrown, this will test that the
+/// exception object matches [matcher]. If [matcher] is not an instance of
+/// [Matcher], it will implicitly be treated as `equals(matcher)`.
+///
+/// Examples:
+/// ```dart
+/// void functionThatThrows() => throw SomeException();
+///
+/// void functionWithArgument(bool shouldThrow) {
+///   if (shouldThrow) {
+///     throw SomeException();
+///   }
+/// }
+///
+/// Future<void> asyncFunctionThatThrows() async => throw SomeException();
+///
+/// expect(functionThatThrows, throwsA(isA<SomeException>()));
+///
+/// expect(() => functionWithArgument(true), throwsA(isA<SomeException>()));
+///
+/// var future = asyncFunctionThatThrows();
+/// await expectLater(future, throwsA(isA<SomeException>()));
+///
+/// await expectLater(
+///     asyncFunctionThatThrows, throwsA(isA<SomeException>()));
+/// ```
+Matcher throwsA(Object? matcher) => Throws(wrapMatcher(matcher));
+
+/// Use the [throwsA] function instead.
+@Deprecated('Will be removed in 0.13.0')
+class Throws extends AsyncMatcher {
+  final Matcher? _matcher;
+
+  const Throws([Matcher? matcher]) : _matcher = matcher;
+
+  // Avoid async/await so we synchronously fail if we match a synchronous
+  // function.
+  @override
+  dynamic /*FutureOr<String>*/ matchAsync(Object? item) {
+    if (item is! Function && item is! Future) {
+      return 'was not a Function or Future';
+    }
+
+    if (item is Future) {
+      return _matchFuture(item, 'emitted ');
+    }
+
+    try {
+      var value = (item as Function)();
+      if (value is Future) {
+        return _matchFuture(value, 'returned a Future that emitted ');
+      }
+
+      return indent(prettyPrint(value), first: 'returned ');
+    } catch (error, trace) {
+      return _check(error, trace);
+    }
+  }
+
+  /// 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) {
+      return description.add('throws');
+    } else {
+      return description.add('throws ').addDescriptionOf(_matcher);
+    }
+  }
+
+  /// Verifies that [error] matches [_matcher] and returns a [String]
+  /// description of the failure if it doesn't.
+  String? _check(error, StackTrace? trace) {
+    if (_matcher == null) return null;
+
+    var matchState = {};
+    if (_matcher.matches(error, matchState)) return null;
+
+    var result = _matcher
+        .describeMismatch(error, StringDescription(), matchState, false)
+        .toString();
+
+    var buffer = StringBuffer();
+    buffer.writeln(indent(prettyPrint(error), first: 'threw '));
+    if (trace != null) {
+      buffer.writeln(indent(
+          TestHandle.current.formatStackTrace(trace).toString(),
+          first: 'stack '));
+    }
+    if (result.isNotEmpty) buffer.writeln(indent(result, first: 'which '));
+    return buffer.toString().trimRight();
+  }
+}
diff --git a/pkgs/matcher/lib/src/expect/throws_matchers.dart b/pkgs/matcher/lib/src/expect/throws_matchers.dart
new file mode 100644
index 0000000..67d35b7
--- /dev/null
+++ b/pkgs/matcher/lib/src/expect/throws_matchers.dart
@@ -0,0 +1,72 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ignore_for_file: deprecated_member_use_from_same_package
+
+import '../error_matchers.dart';
+import '../interfaces.dart';
+import '../type_matcher.dart';
+import 'throws_matcher.dart';
+
+/// A matcher for functions that throw ArgumentError.
+///
+/// See [throwsA] for objects that this can be matched against.
+const Matcher throwsArgumentError = Throws(isArgumentError);
+
+/// A matcher for functions that throw ConcurrentModificationError.
+///
+/// See [throwsA] for objects that this can be matched against.
+const Matcher throwsConcurrentModificationError =
+    Throws(isConcurrentModificationError);
+
+/// A matcher for functions that throw CyclicInitializationError.
+///
+/// See [throwsA] for objects that this can be matched against.
+@Deprecated('throwsCyclicInitializationError has been deprecated, because '
+    'the type will longer exists in Dart 3.0. It will now catch any kind of '
+    'error, not only CyclicInitializationError.')
+const Matcher throwsCyclicInitializationError = Throws(TypeMatcher<Error>());
+
+/// A matcher for functions that throw Exception.
+///
+/// See [throwsA] for objects that this can be matched against.
+const Matcher throwsException = Throws(isException);
+
+/// A matcher for functions that throw FormatException.
+///
+/// See [throwsA] for objects that this can be matched against.
+const Matcher throwsFormatException = Throws(isFormatException);
+
+/// A matcher for functions that throw NoSuchMethodError.
+///
+/// See [throwsA] for objects that this can be matched against.
+const Matcher throwsNoSuchMethodError = Throws(isNoSuchMethodError);
+
+/// A matcher for functions that throw NullThrownError.
+///
+/// See [throwsA] for objects that this can be matched against.
+@Deprecated('throwsNullThrownError has been deprecated, because '
+    'NullThrownError has been replaced with TypeError. '
+    'Use `throwsA(isA<TypeError>())` instead.')
+const Matcher throwsNullThrownError = Throws(TypeMatcher<TypeError>());
+
+/// A matcher for functions that throw RangeError.
+///
+/// See [throwsA] for objects that this can be matched against.
+const Matcher throwsRangeError = Throws(isRangeError);
+
+/// A matcher for functions that throw StateError.
+///
+/// See [throwsA] for objects that this can be matched against.
+const Matcher throwsStateError = Throws(isStateError);
+
+/// A matcher for functions that throw Exception.
+///
+/// See [throwsA] for objects that this can be matched against.
+const Matcher throwsUnimplementedError = Throws(isUnimplementedError);
+
+/// A matcher for functions that throw UnsupportedError.
+///
+/// See [throwsA] for objects that this can be matched against.
+const Matcher throwsUnsupportedError = Throws(isUnsupportedError);
diff --git a/pkgs/matcher/lib/src/expect/util/placeholder.dart b/pkgs/matcher/lib/src/expect/util/placeholder.dart
new file mode 100644
index 0000000..ee2dc70
--- /dev/null
+++ b/pkgs/matcher/lib/src/expect/util/placeholder.dart
@@ -0,0 +1,16 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// A class that's used as a default argument to detect whether an argument was
+/// passed.
+///
+/// We use a custom class for this rather than just `const Object()` so that
+/// callers can't accidentally pass the placeholder value.
+class _Placeholder {
+  const _Placeholder();
+}
+
+/// A placeholder to use as a default argument value to detect whether an
+/// argument was passed.
+const placeholder = _Placeholder();
diff --git a/pkgs/matcher/lib/src/expect/util/pretty_print.dart b/pkgs/matcher/lib/src/expect/util/pretty_print.dart
new file mode 100644
index 0000000..de635ba
--- /dev/null
+++ b/pkgs/matcher/lib/src/expect/util/pretty_print.dart
@@ -0,0 +1,48 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:term_glyph/term_glyph.dart' as glyph;
+
+import '../../description.dart';
+
+/// Indent each line in [text] by [first] spaces.
+///
+/// [first] is used in place of the first line's indentation.
+String indent(String text, {required String first}) {
+  final prefix = ' ' * first.length;
+  var lines = text.split('\n');
+  if (lines.length == 1) return '$first$text';
+
+  var buffer = StringBuffer('$first${lines.first}\n');
+
+  // Write out all but the first and last lines with [prefix].
+  for (var line in lines.skip(1).take(lines.length - 2)) {
+    buffer.writeln('$prefix$line');
+  }
+  buffer.write('$prefix${lines.last}');
+  return buffer.toString();
+}
+
+/// Returns a pretty-printed representation of [value].
+///
+/// The matcher package doesn't expose its pretty-print function directly, but
+/// we can use it through StringDescription.
+String prettyPrint(Object? value) =>
+    StringDescription().addDescriptionOf(value).toString();
+
+/// Indents [text], and adds a bullet at the beginning.
+String addBullet(String text) => indent(text, first: '${glyph.bullet} ');
+
+/// Converts [strings] to a bulleted list.
+String bullet(Iterable<String> strings) => strings.map(addBullet).join('\n');
+
+/// Returns [name] if [number] is 1, or the plural of [name] otherwise.
+///
+/// By default, this just adds "s" to the end of [name] to get the plural. If
+/// [plural] is passed, that's used instead.
+String pluralize(String name, int number, {String? plural}) {
+  if (number == 1) return name;
+  if (plural != null) return plural;
+  return '${name}s';
+}
diff --git a/pkgs/matcher/lib/src/feature_matcher.dart b/pkgs/matcher/lib/src/feature_matcher.dart
new file mode 100644
index 0000000..1dbe56c
--- /dev/null
+++ b/pkgs/matcher/lib/src/feature_matcher.dart
@@ -0,0 +1,34 @@
+// 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.
+
+import 'interfaces.dart';
+import 'type_matcher.dart';
+
+/// A package-private [TypeMatcher] implementation that makes it easy for
+/// subclasses to validate aspects of specific types while providing consistent
+/// type checking.
+abstract class FeatureMatcher<T> extends TypeMatcher<T> {
+  const FeatureMatcher();
+
+  @override
+  bool matches(dynamic item, Map matchState) =>
+      super.matches(item, matchState) && typedMatches(item as T, matchState);
+
+  bool typedMatches(T item, Map matchState);
+
+  @override
+  Description describeMismatch(Object? item, Description mismatchDescription,
+      Map matchState, bool verbose) {
+    if (item is T) {
+      return describeTypedMismatch(
+          item, mismatchDescription, matchState, verbose);
+    }
+
+    return super.describe(mismatchDescription.add('not an '));
+  }
+
+  Description describeTypedMismatch(T item, Description mismatchDescription,
+          Map matchState, bool verbose) =>
+      mismatchDescription;
+}
diff --git a/pkgs/matcher/lib/src/having_matcher.dart b/pkgs/matcher/lib/src/having_matcher.dart
new file mode 100644
index 0000000..2d2dc3d
--- /dev/null
+++ b/pkgs/matcher/lib/src/having_matcher.dart
@@ -0,0 +1,75 @@
+// 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.
+
+import 'custom_matcher.dart';
+import 'interfaces.dart';
+import 'type_matcher.dart';
+import 'util.dart';
+
+/// A package-private [TypeMatcher] implementation that handles is returned
+/// by calls to [TypeMatcher.having].
+class HavingMatcher<T> implements TypeMatcher<T> {
+  final TypeMatcher<T> _parent;
+  final List<_FunctionMatcher<T>> _functionMatchers;
+
+  HavingMatcher(this._parent, String description, Object? Function(T) feature,
+      dynamic matcher)
+      : _functionMatchers = [
+          _FunctionMatcher<T>(description, feature, matcher)
+        ];
+
+  HavingMatcher._fromExisting(
+      this._parent,
+      String description,
+      Object? Function(T) feature,
+      dynamic matcher,
+      Iterable<_FunctionMatcher<T>>? existing)
+      : _functionMatchers = [
+          ...?existing,
+          _FunctionMatcher<T>(description, feature, matcher)
+        ];
+
+  @override
+  TypeMatcher<T> having(
+          Object? Function(T) feature, String description, dynamic matcher) =>
+      HavingMatcher._fromExisting(
+          _parent, description, feature, matcher, _functionMatchers);
+
+  @override
+  bool matches(dynamic item, Map matchState) {
+    for (var matcher in <Matcher>[_parent].followedBy(_functionMatchers)) {
+      if (!matcher.matches(item, matchState)) {
+        addStateInfo(matchState, {'matcher': matcher});
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @override
+  Description describeMismatch(Object? item, Description mismatchDescription,
+      Map matchState, bool verbose) {
+    var matcher = matchState['matcher'] as Matcher;
+    matcher.describeMismatch(
+        item, mismatchDescription, matchState['state'] as Map, verbose);
+    return mismatchDescription;
+  }
+
+  @override
+  Description describe(Description description) => description
+      .add('')
+      .addDescriptionOf(_parent)
+      .add(' with ')
+      .addAll('', ' and ', '', _functionMatchers);
+}
+
+class _FunctionMatcher<T> extends CustomMatcher {
+  final Object? Function(T value) _feature;
+
+  _FunctionMatcher(String name, this._feature, Object? matcher)
+      : super('`$name`:', '`$name`', matcher);
+
+  @override
+  Object? featureValueOf(covariant T actual) => _feature(actual);
+}
diff --git a/pkgs/matcher/lib/src/interfaces.dart b/pkgs/matcher/lib/src/interfaces.dart
new file mode 100644
index 0000000..24527f7
--- /dev/null
+++ b/pkgs/matcher/lib/src/interfaces.dart
@@ -0,0 +1,60 @@
+// Copyright (c) 2012, 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.
+
+/// Matchers build up their error messages by appending to Description objects.
+///
+/// This interface is implemented by StringDescription.
+///
+/// This interface is unlikely to need other implementations, but could be
+/// useful to replace in some cases - e.g. language conversion.
+abstract class Description {
+  int get length;
+
+  /// Change the value of the description.
+  Description replace(String text);
+
+  /// This is used to add arbitrary text to the description.
+  Description add(String text);
+
+  /// This is used to add a meaningful description of a value.
+  Description addDescriptionOf(Object? value);
+
+  /// This is used to add a description of an [Iterable] [list],
+  /// with appropriate [start] and [end] markers and inter-element [separator].
+  Description addAll(String start, String separator, String end, Iterable list);
+}
+
+/// The base class for all matchers.
+///
+/// [matches] and [describe] must be implemented by subclasses.
+///
+/// Subclasses can override [describeMismatch] if a more specific description is
+/// required when the matcher fails.
+abstract class Matcher {
+  const Matcher();
+
+  /// Does the matching of the actual vs expected values.
+  ///
+  /// [item] is the actual value. [matchState] can be supplied
+  /// and may be used to add details about the mismatch that are too
+  /// costly to determine in [describeMismatch].
+  bool matches(dynamic item, Map matchState);
+
+  /// Builds a textual description of the matcher.
+  Description describe(Description description);
+
+  /// Builds a textual description of a specific mismatch.
+  ///
+  /// [item] is the value that was tested by [matches]; [matchState] is
+  /// the [Map] that was passed to and supplemented by [matches]
+  /// with additional information about the mismatch, and [mismatchDescription]
+  /// is the [Description] that is being built to describe the mismatch.
+  ///
+  /// A few matchers make use of the [verbose] flag to provide detailed
+  /// information that is not typically included but can be of help in
+  /// diagnosing failures, such as stack traces.
+  Description describeMismatch(dynamic item, Description mismatchDescription,
+          Map matchState, bool verbose) =>
+      mismatchDescription;
+}
diff --git a/pkgs/matcher/lib/src/iterable_matchers.dart b/pkgs/matcher/lib/src/iterable_matchers.dart
new file mode 100644
index 0000000..abc9688
--- /dev/null
+++ b/pkgs/matcher/lib/src/iterable_matchers.dart
@@ -0,0 +1,416 @@
+// Copyright (c) 2012, 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 'description.dart';
+import 'equals_matcher.dart';
+import 'feature_matcher.dart';
+import 'interfaces.dart';
+import 'util.dart';
+
+/// Returns a matcher which matches [Iterable]s in which all elements
+/// match the given [valueOrMatcher].
+Matcher everyElement(Object? valueOrMatcher) =>
+    _EveryElement(wrapMatcher(valueOrMatcher));
+
+class _EveryElement extends _IterableMatcher {
+  final Matcher _matcher;
+
+  _EveryElement(this._matcher);
+
+  @override
+  bool typedMatches(Iterable item, Map matchState) {
+    var i = 0;
+    for (var element in item) {
+      if (!_matcher.matches(element, matchState)) {
+        addStateInfo(matchState, {'index': i, 'element': element});
+        return false;
+      }
+      ++i;
+    }
+    return true;
+  }
+
+  @override
+  Description describe(Description description) =>
+      description.add('every element(').addDescriptionOf(_matcher).add(')');
+
+  @override
+  Description describeTypedMismatch(dynamic item,
+      Description mismatchDescription, Map matchState, bool verbose) {
+    if (matchState['index'] != null) {
+      var index = matchState['index'];
+      var element = matchState['element'];
+      mismatchDescription
+          .add('has value ')
+          .addDescriptionOf(element)
+          .add(' which ');
+      var subDescription = StringDescription();
+      _matcher.describeMismatch(
+          element, subDescription, matchState['state'] as Map, verbose);
+      if (subDescription.length > 0) {
+        mismatchDescription.add(subDescription.toString());
+      } else {
+        mismatchDescription.add("doesn't match ");
+        _matcher.describe(mismatchDescription);
+      }
+      mismatchDescription.add(' at index $index');
+      return mismatchDescription;
+    }
+    return super
+        .describeMismatch(item, mismatchDescription, matchState, verbose);
+  }
+}
+
+/// Returns a matcher which matches [Iterable]s in which at least one
+/// element matches the given [valueOrMatcher].
+Matcher anyElement(Object? valueOrMatcher) =>
+    _AnyElement(wrapMatcher(valueOrMatcher));
+
+class _AnyElement extends _IterableMatcher {
+  final Matcher _matcher;
+
+  _AnyElement(this._matcher);
+
+  @override
+  bool typedMatches(Iterable item, Map matchState) =>
+      item.any((e) => _matcher.matches(e, matchState));
+
+  @override
+  Description describe(Description description) =>
+      description.add('some element ').addDescriptionOf(_matcher);
+}
+
+/// Returns a matcher which matches [Iterable]s that have the same
+/// length and the same elements as [expected], in the same order.
+///
+/// This is equivalent to [equals] but does not recurse.
+Matcher orderedEquals(Iterable expected) => _OrderedEquals(expected);
+
+class _OrderedEquals extends _IterableMatcher {
+  final Iterable _expected;
+  final Matcher _matcher;
+
+  _OrderedEquals(this._expected) : _matcher = equals(_expected, 1);
+
+  @override
+  bool typedMatches(Iterable item, Map matchState) =>
+      _matcher.matches(item, matchState);
+
+  @override
+  Description describe(Description description) =>
+      description.add('equals ').addDescriptionOf(_expected).add(' ordered');
+
+  @override
+  Description describeTypedMismatch(Iterable item,
+      Description mismatchDescription, Map matchState, bool verbose) {
+    return _matcher.describeMismatch(
+        item, mismatchDescription, matchState, verbose);
+  }
+}
+
+/// Returns a matcher which matches [Iterable]s that have the same length and
+/// the same elements as [expected], but not necessarily in the same order.
+///
+/// Note that this is worst case O(n^2) runtime and memory usage so it should
+/// only be used on small iterables.
+Matcher unorderedEquals(Iterable expected) => _UnorderedEquals(expected);
+
+class _UnorderedEquals extends _UnorderedMatches {
+  final List _expectedValues;
+
+  _UnorderedEquals(Iterable expected)
+      : _expectedValues = expected.toList(),
+        super(expected.map(equals));
+
+  @override
+  Description describe(Description description) => description
+      .add('equals ')
+      .addDescriptionOf(_expectedValues)
+      .add(' unordered');
+}
+
+/// Iterable matchers match against [Iterable]s. We add this intermediate
+/// class to give better mismatch error messages than the base Matcher class.
+abstract class _IterableMatcher extends FeatureMatcher<Iterable> {
+  const _IterableMatcher();
+}
+
+/// Returns a matcher which matches [Iterable]s whose elements match the
+/// matchers in [expected], but not necessarily in the same order.
+///
+/// Note that this is worst case O(n^2) runtime and memory usage so it should
+/// only be used on small iterables.
+Matcher unorderedMatches(Iterable expected) => _UnorderedMatches(expected);
+
+class _UnorderedMatches extends _IterableMatcher {
+  final List<Matcher> _expected;
+  final bool _allowUnmatchedValues;
+
+  _UnorderedMatches(Iterable expected, {bool allowUnmatchedValues = false})
+      : _expected = expected.map(wrapMatcher).toList(),
+        _allowUnmatchedValues = allowUnmatchedValues;
+
+  String? _test(List values) {
+    // Check the lengths are the same.
+    if (_expected.length > values.length) {
+      return 'has too few elements (${values.length} < ${_expected.length})';
+    } else if (!_allowUnmatchedValues && _expected.length < values.length) {
+      return 'has too many elements (${values.length} > ${_expected.length})';
+    }
+
+    var edges = List.generate(values.length, (_) => <int>[], growable: false);
+    for (var v = 0; v < values.length; v++) {
+      for (var m = 0; m < _expected.length; m++) {
+        if (_expected[m].matches(values[v], {})) {
+          edges[v].add(m);
+        }
+      }
+    }
+    // The index into `values` matched with each matcher or `null` if no value
+    // has been matched yet.
+    var matched = List<int?>.filled(_expected.length, null);
+    for (var valueIndex = 0; valueIndex < values.length; valueIndex++) {
+      _findPairing(edges, valueIndex, matched);
+    }
+    for (var matcherIndex = 0;
+        matcherIndex < _expected.length;
+        matcherIndex++) {
+      if (matched[matcherIndex] == null) {
+        final description = StringDescription()
+            .add('has no match for ')
+            .addDescriptionOf(_expected[matcherIndex])
+            .add(' at index $matcherIndex');
+        final remainingUnmatched =
+            matched.sublist(matcherIndex + 1).where((m) => m == null).length;
+        return remainingUnmatched == 0
+            ? description.toString()
+            : description
+                .add(' along with $remainingUnmatched other unmatched')
+                .toString();
+      }
+    }
+    return null;
+  }
+
+  @override
+  bool typedMatches(Iterable item, Map mismatchState) =>
+      _test(item.toList()) == null;
+
+  @override
+  Description describe(Description description) => description
+      .add('matches ')
+      .addAll('[', ', ', ']', _expected)
+      .add(' unordered');
+
+  @override
+  Description describeTypedMismatch(Iterable item,
+          Description mismatchDescription, Map matchState, bool verbose) =>
+      mismatchDescription.add(_test(item.toList())!);
+
+  /// Returns `true` if the value at [valueIndex] can be paired with some
+  /// unmatched matcher and updates the state of [matched].
+  ///
+  /// If there is a conflict where multiple values may match the same matcher
+  /// recursively looks for a new place to match the old value.
+  bool _findPairing(
+          List<List<int>> edges, int valueIndex, List<int?> matched) =>
+      _findPairingInner(edges, valueIndex, matched, <int>{});
+
+  /// Implementation of [_findPairing], tracks [reserved] which are the
+  /// matchers that have been used _during_ this search.
+  bool _findPairingInner(List<List<int>> edges, int valueIndex,
+      List<int?> matched, Set<int> reserved) {
+    final possiblePairings =
+        edges[valueIndex].where((m) => !reserved.contains(m));
+    for (final matcherIndex in possiblePairings) {
+      reserved.add(matcherIndex);
+      final previouslyMatched = matched[matcherIndex];
+      if (previouslyMatched == null ||
+          // If the matcher isn't already free, check whether the existing value
+          // occupying the matcher can be bumped to another one.
+          _findPairingInner(edges, matched[matcherIndex]!, matched, reserved)) {
+        matched[matcherIndex] = valueIndex;
+        return true;
+      }
+    }
+    return false;
+  }
+}
+
+/// A pairwise matcher for [Iterable]s.
+///
+/// The [comparator] function, taking an expected and an actual argument, and
+/// returning whether they match, will be applied to each pair in order.
+/// [description] should be a meaningful name for the comparator.
+Matcher pairwiseCompare<S, T>(Iterable<S> expected,
+        bool Function(S, T) comparator, String description) =>
+    _PairwiseCompare(expected, comparator, description);
+
+typedef _Comparator<S, T> = bool Function(S a, T b);
+
+class _PairwiseCompare<S, T> extends _IterableMatcher {
+  final Iterable<S> _expected;
+  final _Comparator<S, T> _comparator;
+  final String _description;
+
+  _PairwiseCompare(this._expected, this._comparator, this._description);
+
+  @override
+  bool typedMatches(Iterable item, Map matchState) {
+    if (item.length != _expected.length) return false;
+    var iterator = item.iterator;
+    var i = 0;
+    for (var e in _expected) {
+      iterator.moveNext();
+      if (!_comparator(e, iterator.current as T)) {
+        addStateInfo(matchState,
+            {'index': i, 'expected': e, 'actual': iterator.current});
+        return false;
+      }
+      i++;
+    }
+    return true;
+  }
+
+  @override
+  Description describe(Description description) =>
+      description.add('pairwise $_description ').addDescriptionOf(_expected);
+
+  @override
+  Description describeTypedMismatch(Iterable item,
+      Description mismatchDescription, Map matchState, bool verbose) {
+    if (item.length != _expected.length) {
+      return mismatchDescription
+          .add('has length ${item.length} instead of ${_expected.length}');
+    } else {
+      return mismatchDescription
+          .add('has ')
+          .addDescriptionOf(matchState['actual'])
+          .add(' which is not $_description ')
+          .addDescriptionOf(matchState['expected'])
+          .add(' at index ${matchState["index"]}');
+    }
+  }
+}
+
+/// Matches [Iterable]s which contain an element matching every value in
+/// [expected] in any order, and may contain additional values.
+///
+/// For example: `[0, 1, 0, 2, 0]` matches `containsAll([1, 2])` and
+/// `containsAll([2, 1])` but not `containsAll([1, 2, 3])`.
+///
+/// Will only match values which implement [Iterable].
+///
+/// Each element in the value will only be considered a match for a single
+/// matcher in [expected] even if it could satisfy more than one. For instance
+/// `containsAll([greaterThan(1), greaterThan(2)])` will not be satisfied by
+/// `[3]`. To check that all matchers are satisfied within an iterable and allow
+/// the same element to satisfy multiple matchers use
+/// `allOf(matchers.map(contains))`.
+///
+/// Note that this is worst case O(n^2) runtime and memory usage so it should
+/// only be used on small iterables.
+Matcher containsAll(Iterable expected) => _ContainsAll(expected);
+
+class _ContainsAll extends _UnorderedMatches {
+  final Iterable _unwrappedExpected;
+
+  _ContainsAll(Iterable expected)
+      : _unwrappedExpected = expected,
+        super(expected.map(wrapMatcher), allowUnmatchedValues: true);
+  @override
+  Description describe(Description description) =>
+      description.add('contains all of ').addDescriptionOf(_unwrappedExpected);
+}
+
+/// Matches [Iterable]s which contain an element matching every value in
+/// [expected] in the same order, but may contain additional values interleaved
+/// throughout.
+///
+/// For example: `[0, 1, 0, 2, 0]` matches `containsAllInOrder([1, 2])` but not
+/// `containsAllInOrder([2, 1])` or `containsAllInOrder([1, 2, 3])`.
+///
+/// Will only match values which implement [Iterable].
+Matcher containsAllInOrder(Iterable expected) => _ContainsAllInOrder(expected);
+
+class _ContainsAllInOrder extends _IterableMatcher {
+  final Iterable _expected;
+
+  _ContainsAllInOrder(this._expected);
+
+  String? _test(Iterable item, Map matchState) {
+    var matchers = _expected.map(wrapMatcher).toList();
+    var matcherIndex = 0;
+    for (var value in item) {
+      if (matchers[matcherIndex].matches(value, matchState)) matcherIndex++;
+      if (matcherIndex == matchers.length) return null;
+    }
+    return StringDescription()
+        .add('did not find a value matching ')
+        .addDescriptionOf(matchers[matcherIndex])
+        .add(' following expected prior values')
+        .toString();
+  }
+
+  @override
+  bool typedMatches(Iterable item, Map matchState) =>
+      _test(item, matchState) == null;
+
+  @override
+  Description describe(Description description) => description
+      .add('contains in order(')
+      .addDescriptionOf(_expected)
+      .add(')');
+
+  @override
+  Description describeTypedMismatch(Iterable item,
+          Description mismatchDescription, Map matchState, bool verbose) =>
+      mismatchDescription.add(_test(item, matchState)!);
+}
+
+/// Matches [Iterable]s where exactly one element matches the expected
+/// value, and all other elements don't match.
+Matcher containsOnce(Object? expected) => _ContainsOnce(expected);
+
+class _ContainsOnce extends _IterableMatcher {
+  final Object? _expected;
+
+  _ContainsOnce(this._expected);
+
+  String? _test(Iterable item, Map matchState) {
+    var matcher = wrapMatcher(_expected);
+    var matches = [
+      for (var value in item)
+        if (matcher.matches(value, matchState)) value,
+    ];
+    if (matches.length == 1) {
+      return null;
+    }
+    if (matches.isEmpty) {
+      return StringDescription()
+          .add('did not find a value matching ')
+          .addDescriptionOf(matcher)
+          .toString();
+    }
+    return StringDescription()
+        .add('expected only one value matching ')
+        .addDescriptionOf(matcher)
+        .add(' but found multiple: ')
+        .addAll('', ', ', '', matches)
+        .toString();
+  }
+
+  @override
+  bool typedMatches(Iterable item, Map matchState) =>
+      _test(item, matchState) == null;
+
+  @override
+  Description describe(Description description) =>
+      description.add('contains once(').addDescriptionOf(_expected).add(')');
+
+  @override
+  Description describeTypedMismatch(Iterable item,
+          Description mismatchDescription, Map matchState, bool verbose) =>
+      mismatchDescription.add(_test(item, matchState)!);
+}
diff --git a/pkgs/matcher/lib/src/map_matchers.dart b/pkgs/matcher/lib/src/map_matchers.dart
new file mode 100644
index 0000000..4476d06
--- /dev/null
+++ b/pkgs/matcher/lib/src/map_matchers.dart
@@ -0,0 +1,69 @@
+// Copyright (c) 2012, 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 'interfaces.dart';
+import 'util.dart';
+
+/// Returns a matcher which matches maps containing the given [value].
+Matcher containsValue(Object? value) => _ContainsValue(value);
+
+class _ContainsValue extends Matcher {
+  final Object? _value;
+
+  const _ContainsValue(this._value);
+
+  @override
+  bool matches(Object? item, Map matchState) =>
+      // ignore: avoid_dynamic_calls
+      (item as dynamic).containsValue(_value);
+  @override
+  Description describe(Description description) =>
+      description.add('contains value ').addDescriptionOf(_value);
+}
+
+/// Returns a matcher which matches maps containing the key-value pair
+/// with [key] => [valueOrMatcher].
+Matcher containsPair(Object? key, Object? valueOrMatcher) =>
+    _ContainsMapping(key, wrapMatcher(valueOrMatcher));
+
+class _ContainsMapping extends Matcher {
+  final Object? _key;
+  final Matcher _valueMatcher;
+
+  const _ContainsMapping(this._key, this._valueMatcher);
+
+  @override
+  bool matches(Object? item, Map matchState) =>
+      // ignore: avoid_dynamic_calls
+      (item as dynamic).containsKey(_key) &&
+      _valueMatcher.matches((item as dynamic)[_key], matchState);
+
+  @override
+  Description describe(Description description) {
+    return description
+        .add('contains pair ')
+        .addDescriptionOf(_key)
+        .add(' => ')
+        .addDescriptionOf(_valueMatcher);
+  }
+
+  @override
+  Description describeMismatch(Object? item, Description mismatchDescription,
+      Map matchState, bool verbose) {
+    // ignore: avoid_dynamic_calls
+    if (!((item as dynamic).containsKey(_key) as bool)) {
+      return mismatchDescription
+          .add(" doesn't contain key ")
+          .addDescriptionOf(_key);
+    } else {
+      mismatchDescription
+          .add(' contains key ')
+          .addDescriptionOf(_key)
+          .add(' but with value ');
+      _valueMatcher.describeMismatch(
+          (item as dynamic)[_key], mismatchDescription, matchState, verbose);
+      return mismatchDescription;
+    }
+  }
+}
diff --git a/pkgs/matcher/lib/src/numeric_matchers.dart b/pkgs/matcher/lib/src/numeric_matchers.dart
new file mode 100644
index 0000000..a798160
--- /dev/null
+++ b/pkgs/matcher/lib/src/numeric_matchers.dart
@@ -0,0 +1,89 @@
+// Copyright (c) 2012, 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 'feature_matcher.dart';
+import 'interfaces.dart';
+
+/// Returns a matcher which matches if the match argument is within [delta]
+/// of some [value].
+///
+/// In other words, this matches if the match argument is greater than
+/// than or equal [value]-[delta] and less than or equal to [value]+[delta].
+Matcher closeTo(num value, num delta) => _IsCloseTo(value, delta);
+
+class _IsCloseTo extends FeatureMatcher<num> {
+  final num _value, _delta;
+
+  const _IsCloseTo(this._value, this._delta);
+
+  @override
+  bool typedMatches(num item, Map matchState) {
+    var diff = item - _value;
+    if (diff < 0) diff = -diff;
+    return diff <= _delta;
+  }
+
+  @override
+  Description describe(Description description) => description
+      .add('a numeric value within ')
+      .addDescriptionOf(_delta)
+      .add(' of ')
+      .addDescriptionOf(_value);
+
+  @override
+  Description describeTypedMismatch(
+      num item, Description mismatchDescription, Map matchState, bool verbose) {
+    var diff = item - _value;
+    if (diff < 0) diff = -diff;
+    return mismatchDescription.add(' differs by ').addDescriptionOf(diff);
+  }
+}
+
+/// Returns a matcher which matches if the match argument is greater
+/// than or equal to [low] and less than or equal to [high].
+Matcher inInclusiveRange(num low, num high) => _InRange(low, high, true, true);
+
+/// Returns a matcher which matches if the match argument is greater
+/// than [low] and less than [high].
+Matcher inExclusiveRange(num low, num high) =>
+    _InRange(low, high, false, false);
+
+/// Returns a matcher which matches if the match argument is greater
+/// than [low] and less than or equal to [high].
+Matcher inOpenClosedRange(num low, num high) =>
+    _InRange(low, high, false, true);
+
+/// Returns a matcher which matches if the match argument is greater
+/// than or equal to a [low] and less than [high].
+Matcher inClosedOpenRange(num low, num high) =>
+    _InRange(low, high, true, false);
+
+class _InRange extends FeatureMatcher<num> {
+  final num _low, _high;
+  final bool _lowMatchValue, _highMatchValue;
+
+  const _InRange(
+      this._low, this._high, this._lowMatchValue, this._highMatchValue);
+
+  @override
+  bool typedMatches(num value, Map matchState) {
+    if (value < _low || value > _high) {
+      return false;
+    }
+    if (value == _low) {
+      return _lowMatchValue;
+    }
+    if (value == _high) {
+      return _highMatchValue;
+    }
+    // Value may still be outside if range if it can't be compared.
+    return value > _low && value < _high;
+  }
+
+  @override
+  Description describe(Description description) =>
+      description.add('be in range from '
+          "$_low (${_lowMatchValue ? 'inclusive' : 'exclusive'}) to "
+          "$_high (${_highMatchValue ? 'inclusive' : 'exclusive'})");
+}
diff --git a/pkgs/matcher/lib/src/operator_matchers.dart b/pkgs/matcher/lib/src/operator_matchers.dart
new file mode 100644
index 0000000..15e50ff
--- /dev/null
+++ b/pkgs/matcher/lib/src/operator_matchers.dart
@@ -0,0 +1,131 @@
+// Copyright (c) 2012, 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 'interfaces.dart';
+import 'util.dart';
+
+/// Returns a matcher that inverts [valueOrMatcher] to its logical negation.
+Matcher isNot(Object? valueOrMatcher) => _IsNot(wrapMatcher(valueOrMatcher));
+
+class _IsNot extends Matcher {
+  final Matcher _matcher;
+
+  const _IsNot(this._matcher);
+
+  @override
+  bool matches(dynamic item, Map matchState) =>
+      !_matcher.matches(item, matchState);
+
+  @override
+  Description describe(Description description) =>
+      description.add('not ').addDescriptionOf(_matcher);
+}
+
+/// This returns a matcher that matches if all of the matchers passed as
+/// arguments (up to 7) match.
+///
+/// Instead of passing the matchers separately they can be passed as a single
+/// List argument. Any argument that is not a matcher is implicitly wrapped in a
+/// Matcher to check for equality.
+Matcher allOf(Object? arg0,
+    [Object? arg1,
+    Object? arg2,
+    Object? arg3,
+    Object? arg4,
+    Object? arg5,
+    Object? arg6]) {
+  return _AllOf(_wrapArgs(arg0, arg1, arg2, arg3, arg4, arg5, arg6));
+}
+
+class _AllOf extends Matcher {
+  final List<Matcher> _matchers;
+
+  const _AllOf(this._matchers);
+
+  @override
+  bool matches(dynamic item, Map matchState) {
+    for (var matcher in _matchers) {
+      if (!matcher.matches(item, matchState)) {
+        addStateInfo(matchState, {'matcher': matcher});
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @override
+  Description describeMismatch(dynamic item, Description mismatchDescription,
+      Map matchState, bool verbose) {
+    var matcher = matchState['matcher'] as Matcher;
+    matcher.describeMismatch(
+        item, mismatchDescription, matchState['state'], verbose);
+    return mismatchDescription;
+  }
+
+  @override
+  Description describe(Description description) =>
+      description.addAll('(', ' and ', ')', _matchers);
+}
+
+/// Matches if any of the given matchers evaluate to true.
+///
+/// The arguments can be a set of matchers as separate parameters
+/// (up to 7), or a List of matchers.
+///
+/// The matchers are evaluated from left to right using short-circuit
+/// evaluation, so evaluation stops as soon as a matcher returns true.
+///
+/// Any argument that is not a matcher is implicitly wrapped in a
+/// Matcher to check for equality.
+Matcher anyOf(Object? arg0,
+    [Object? arg1,
+    Object? arg2,
+    Object? arg3,
+    Object? arg4,
+    Object? arg5,
+    Object? arg6]) {
+  return _AnyOf(_wrapArgs(arg0, arg1, arg2, arg3, arg4, arg5, arg6));
+}
+
+class _AnyOf extends Matcher {
+  final List<Matcher> _matchers;
+
+  const _AnyOf(this._matchers);
+
+  @override
+  bool matches(dynamic item, Map matchState) {
+    for (var matcher in _matchers) {
+      if (matcher.matches(item, matchState)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @override
+  Description describe(Description description) =>
+      description.addAll('(', ' or ', ')', _matchers);
+}
+
+List<Matcher> _wrapArgs(Object? arg0, Object? arg1, Object? arg2, Object? arg3,
+    Object? arg4, Object? arg5, Object? arg6) {
+  Iterable args;
+  if (arg0 is List) {
+    if (arg1 != null ||
+        arg2 != null ||
+        arg3 != null ||
+        arg4 != null ||
+        arg5 != null ||
+        arg6 != null) {
+      throw ArgumentError('If arg0 is a List, all other arguments must be'
+          ' null.');
+    }
+
+    args = arg0;
+  } else {
+    args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6].where((e) => e != null);
+  }
+
+  return args.map(wrapMatcher).toList();
+}
diff --git a/pkgs/matcher/lib/src/order_matchers.dart b/pkgs/matcher/lib/src/order_matchers.dart
new file mode 100644
index 0000000..6fe7c76
--- /dev/null
+++ b/pkgs/matcher/lib/src/order_matchers.dart
@@ -0,0 +1,108 @@
+// 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.
+
+import 'interfaces.dart';
+
+/// Returns a matcher which matches if the match argument is greater
+/// than the given [value].
+Matcher greaterThan(Object value) =>
+    _OrderingMatcher(value, false, false, true, 'a value greater than');
+
+/// Returns a matcher which matches if the match argument is greater
+/// than or equal to the given [value].
+Matcher greaterThanOrEqualTo(Object value) => _OrderingMatcher(
+    value, true, false, true, 'a value greater than or equal to');
+
+/// Returns a matcher which matches if the match argument is less
+/// than the given [value].
+Matcher lessThan(Object value) =>
+    _OrderingMatcher(value, false, true, false, 'a value less than');
+
+/// Returns a matcher which matches if the match argument is less
+/// than or equal to the given [value].
+Matcher lessThanOrEqualTo(Object value) =>
+    _OrderingMatcher(value, true, true, false, 'a value less than or equal to');
+
+/// A matcher which matches if the match argument is zero.
+const Matcher isZero =
+    _OrderingMatcher(0, true, false, false, 'a value equal to');
+
+/// A matcher which matches if the match argument is non-zero.
+const Matcher isNonZero =
+    _OrderingMatcher(0, false, true, true, 'a value not equal to');
+
+/// A matcher which matches if the match argument is positive.
+const Matcher isPositive =
+    _OrderingMatcher(0, false, false, true, 'a positive value', false);
+
+/// A matcher which matches if the match argument is zero or negative.
+const Matcher isNonPositive =
+    _OrderingMatcher(0, true, true, false, 'a non-positive value', false);
+
+/// A matcher which matches if the match argument is negative.
+const Matcher isNegative =
+    _OrderingMatcher(0, false, true, false, 'a negative value', false);
+
+/// A matcher which matches if the match argument is zero or positive.
+const Matcher isNonNegative =
+    _OrderingMatcher(0, true, false, true, 'a non-negative value', false);
+
+// TODO(kevmoo) Note that matchers that use _OrderingComparison only use
+// `==` and `<` operators to evaluate the match. Or change the matcher.
+class _OrderingMatcher extends Matcher {
+  /// Expected value.
+  final Object _value;
+
+  /// What to return if actual == expected
+  final bool _equalValue;
+
+  /// What to return if actual < expected
+  final bool _lessThanValue;
+
+  /// What to return if actual > expected
+  final bool _greaterThanValue;
+
+  /// Textual name of the inequality
+  final String _comparisonDescription;
+
+  /// Whether to include the expected value in the description
+  final bool _valueInDescription;
+
+  const _OrderingMatcher(this._value, this._equalValue, this._lessThanValue,
+      this._greaterThanValue, this._comparisonDescription,
+      [bool valueInDescription = true])
+      : _valueInDescription = valueInDescription;
+
+  @override
+  bool matches(Object? item, Map matchState) {
+    if (item == _value) {
+      return _equalValue;
+    } else if ((item as dynamic) < _value) {
+      return _lessThanValue;
+    } else if ((item as dynamic) > _value) {
+      return _greaterThanValue;
+    } else {
+      return false;
+    }
+  }
+
+  @override
+  Description describe(Description description) {
+    if (_valueInDescription) {
+      return description
+          .add(_comparisonDescription)
+          .add(' ')
+          .addDescriptionOf(_value);
+    } else {
+      return description.add(_comparisonDescription);
+    }
+  }
+
+  @override
+  Description describeMismatch(dynamic item, Description mismatchDescription,
+      Map matchState, bool verbose) {
+    mismatchDescription.add('is not ');
+    return describe(mismatchDescription);
+  }
+}
diff --git a/pkgs/matcher/lib/src/pretty_print.dart b/pkgs/matcher/lib/src/pretty_print.dart
new file mode 100644
index 0000000..d9eaaec
--- /dev/null
+++ b/pkgs/matcher/lib/src/pretty_print.dart
@@ -0,0 +1,133 @@
+// Copyright (c) 2013, 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 'description.dart';
+import 'interfaces.dart';
+import 'util.dart';
+
+/// Returns a pretty-printed representation of [object].
+///
+/// If [maxLineLength] is passed, this will attempt to ensure that each line is
+/// no longer than [maxLineLength] characters long. This isn't guaranteed, since
+/// individual objects may have string representations that are too long, but
+/// most lines will be less than [maxLineLength] long.
+///
+/// If [maxItems] is passed, [Iterable]s and [Map]s will only print their first
+/// [maxItems] members or key/value pairs, respectively.
+String prettyPrint(Object? object, {int? maxLineLength, int? maxItems}) {
+  String prettyPrintImpl(
+      Object? object, int indent, Set<Object?> seen, bool top) {
+    // If the object is a matcher, use its description.
+    if (object is Matcher) {
+      var description = StringDescription();
+      object.describe(description);
+      return '<$description>';
+    }
+
+    // Avoid looping infinitely on recursively-nested data structures.
+    if (seen.contains(object)) return '(recursive)';
+    seen = seen.union({object});
+    String pp(Object? child) => prettyPrintImpl(child, indent + 2, seen, false);
+
+    if (object is Iterable) {
+      // Print the type name for non-List iterables.
+      var type = object is List ? '' : '${_typeName(object)}:';
+
+      // Truncate the list of strings if it's longer than [maxItems].
+      var strings = object.map(pp).toList();
+      if (maxItems != null && strings.length > maxItems) {
+        strings.replaceRange(maxItems - 1, strings.length, ['...']);
+      }
+
+      // If the printed string is short and doesn't contain a newline, print it
+      // as a single line.
+      var singleLine = "$type[${strings.join(', ')}]";
+      if ((maxLineLength == null ||
+              singleLine.length + indent <= maxLineLength) &&
+          !singleLine.contains('\n')) {
+        return singleLine;
+      }
+
+      // Otherwise, print each member on its own line.
+      return '$type[\n${strings.map((string) {
+        return _indent(indent + 2) + string;
+      }).join(',\n')}\n${_indent(indent)}]';
+    } else if (object is Map) {
+      // Convert the contents of the map to string representations.
+      var strings = object.keys.map((key) {
+        return '${pp(key)}: ${pp(object[key])}';
+      }).toList();
+
+      // Truncate the list of strings if it's longer than [maxItems].
+      if (maxItems != null && strings.length > maxItems) {
+        strings.replaceRange(maxItems - 1, strings.length, ['...']);
+      }
+
+      // If the printed string is short and doesn't contain a newline, print it
+      // as a single line.
+      var singleLine = '{${strings.join(", ")}}';
+      if ((maxLineLength == null ||
+              singleLine.length + indent <= maxLineLength) &&
+          !singleLine.contains('\n')) {
+        return singleLine;
+      }
+
+      // Otherwise, print each key/value pair on its own line.
+      return '{\n${strings.map((string) {
+        return _indent(indent + 2) + string;
+      }).join(',\n')}\n${_indent(indent)}}';
+    } else if (object is String) {
+      // Escape strings and print each line on its own line.
+      var value = object
+          .split('\n')
+          .map(_escapeString)
+          .join("\\n'\n${_indent(indent + 2)}'");
+      return "'$value'";
+    } else {
+      var value = object.toString().replaceAll('\n', '${_indent(indent)}\n');
+      var defaultToString = value.startsWith('Instance of ');
+
+      // If this is the top-level call to [prettyPrint], wrap the value on angle
+      // brackets to set it apart visually.
+      if (top) value = '<$value>';
+
+      // Print the type of objects with custom [toString] methods. Primitive
+      // objects and objects that don't implement a custom [toString] don't need
+      // to have their types printed.
+      if (object is num ||
+          object is bool ||
+          object is Function ||
+          object is RegExp ||
+          object is MapEntry ||
+          object is Expando ||
+          object == null ||
+          defaultToString) {
+        return value;
+      } else {
+        return '${_typeName(object)}:$value';
+      }
+    }
+  }
+
+  return prettyPrintImpl(object, 0, <Object?>{}, true);
+}
+
+String _indent(int length) => List.filled(length, ' ').join('');
+
+/// Returns the name of the type of [x] with fallbacks for core types with
+/// private implementations.
+String _typeName(Object x) {
+  if (x is Type) return 'Type';
+  if (x is Uri) return 'Uri';
+  if (x is Set) return 'Set';
+  if (x is BigInt) return 'BigInt';
+  return '${x.runtimeType}';
+}
+
+/// Returns [source] with any control characters replaced by their escape
+/// sequences.
+///
+/// This doesn't add quotes to the string, but it does escape single quote
+/// characters so that single quotes can be applied externally.
+String _escapeString(String source) => escape(source).replaceAll("'", r"\'");
diff --git a/pkgs/matcher/lib/src/string_matchers.dart b/pkgs/matcher/lib/src/string_matchers.dart
new file mode 100644
index 0000000..b819fa5
--- /dev/null
+++ b/pkgs/matcher/lib/src/string_matchers.dart
@@ -0,0 +1,183 @@
+// Copyright (c) 2012, 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 'feature_matcher.dart';
+import 'interfaces.dart';
+
+/// Returns a matcher which matches if the match argument is a string and
+/// is equal to [value] when compared case-insensitively.
+Matcher equalsIgnoringCase(String value) => _IsEqualIgnoringCase(value);
+
+class _IsEqualIgnoringCase extends FeatureMatcher<String> {
+  final String _value;
+  final String _matchValue;
+
+  _IsEqualIgnoringCase(String value)
+      : _value = value,
+        _matchValue = value.toLowerCase();
+
+  @override
+  bool typedMatches(String item, Map matchState) =>
+      _matchValue == item.toLowerCase();
+
+  @override
+  Description describe(Description description) =>
+      description.addDescriptionOf(_value).add(' ignoring case');
+}
+
+/// Returns a matcher which matches if the match argument is a string and
+/// is equal to [value], ignoring whitespace.
+///
+/// In this matcher, "ignoring whitespace" means comparing with all runs of
+/// whitespace collapsed to single space characters and leading and trailing
+/// whitespace removed.
+///
+/// For example, the following will all match successfully:
+///
+///     expect("hello   world", equalsIgnoringWhitespace("hello world"));
+///     expect("  hello world", equalsIgnoringWhitespace("hello world"));
+///     expect("hello world  ", equalsIgnoringWhitespace("hello world"));
+///
+/// The following will not match:
+///
+///     expect("helloworld", equalsIgnoringWhitespace("hello world"));
+///     expect("he llo world", equalsIgnoringWhitespace("hello world"));
+Matcher equalsIgnoringWhitespace(String value) =>
+    _IsEqualIgnoringWhitespace(value);
+
+class _IsEqualIgnoringWhitespace extends FeatureMatcher<String> {
+  final String _matchValue;
+
+  _IsEqualIgnoringWhitespace(String value)
+      : _matchValue = collapseWhitespace(value);
+
+  @override
+  bool typedMatches(String item, Map matchState) =>
+      _matchValue == collapseWhitespace(item);
+
+  @override
+  Description describe(Description description) =>
+      description.addDescriptionOf(_matchValue).add(' ignoring whitespace');
+
+  @override
+  Description describeTypedMismatch(dynamic item,
+      Description mismatchDescription, Map matchState, bool verbose) {
+    return mismatchDescription
+        .add('is ')
+        .addDescriptionOf(collapseWhitespace(item))
+        .add(' with whitespace compressed');
+  }
+}
+
+/// Returns a matcher that matches if the match argument is a string and
+/// starts with [prefixString].
+Matcher startsWith(String prefixString) => _StringStartsWith(prefixString);
+
+class _StringStartsWith extends FeatureMatcher<String> {
+  final String _prefix;
+
+  const _StringStartsWith(this._prefix);
+
+  @override
+  bool typedMatches(String item, Map matchState) => item.startsWith(_prefix);
+
+  @override
+  Description describe(Description description) =>
+      description.add('a string starting with ').addDescriptionOf(_prefix);
+}
+
+/// Returns a matcher that matches if the match argument is a string and
+/// ends with [suffixString].
+Matcher endsWith(String suffixString) => _StringEndsWith(suffixString);
+
+class _StringEndsWith extends FeatureMatcher<String> {
+  final String _suffix;
+
+  const _StringEndsWith(this._suffix);
+
+  @override
+  bool typedMatches(String item, Map matchState) => item.endsWith(_suffix);
+
+  @override
+  Description describe(Description description) =>
+      description.add('a string ending with ').addDescriptionOf(_suffix);
+}
+
+/// Returns a matcher that matches if the match argument is a string and
+/// contains a given list of [substrings] in relative order.
+///
+/// For example, `stringContainsInOrder(["a", "e", "i", "o", "u"])` will match
+/// "abcdefghijklmnopqrstuvwxyz".
+
+Matcher stringContainsInOrder(List<String> substrings) =>
+    _StringContainsInOrder(substrings);
+
+class _StringContainsInOrder extends FeatureMatcher<String> {
+  final List<String> _substrings;
+
+  const _StringContainsInOrder(this._substrings);
+
+  @override
+  bool typedMatches(String item, Map matchState) {
+    var fromIndex = 0;
+    for (var s in _substrings) {
+      var index = item.indexOf(s, fromIndex);
+      if (index < 0) return false;
+      fromIndex = index + s.length;
+    }
+    return true;
+  }
+
+  @override
+  Description describe(Description description) => description.addAll(
+      'a string containing ', ', ', ' in order', _substrings);
+}
+
+/// Returns a matcher that matches if the match argument is a string and
+/// matches the regular expression given by [re].
+///
+/// [re] can be a [RegExp] instance or a [String]; in the latter case it will be
+/// used to create a RegExp instance.
+Matcher matches(Pattern re) => _MatchesRegExp(re);
+
+class _MatchesRegExp extends FeatureMatcher<String> {
+  final RegExp _regexp;
+
+  _MatchesRegExp(Pattern re)
+      : _regexp = (re is String)
+            ? RegExp(re)
+            : (re is RegExp)
+                ? re
+                : throw ArgumentError('matches requires a regexp or string');
+
+  @override
+  bool typedMatches(dynamic item, Map matchState) => _regexp.hasMatch(item);
+
+  @override
+  Description describe(Description description) =>
+      description.add("match '${_regexp.pattern}'");
+}
+
+/// Utility function to collapse whitespace runs to single spaces
+/// and strip leading/trailing whitespace.
+String collapseWhitespace(String string) {
+  var result = StringBuffer();
+  var skipSpace = true;
+  for (var i = 0; i < string.length; i++) {
+    var character = string[i];
+    if (_isWhitespace(character)) {
+      if (!skipSpace) {
+        result.write(' ');
+        skipSpace = true;
+      }
+    } else {
+      result.write(character);
+      skipSpace = false;
+    }
+  }
+  return result.toString().trim();
+}
+
+bool _isWhitespace(String ch) =>
+    ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t';
diff --git a/pkgs/matcher/lib/src/type_matcher.dart b/pkgs/matcher/lib/src/type_matcher.dart
new file mode 100644
index 0000000..9d32b9f
--- /dev/null
+++ b/pkgs/matcher/lib/src/type_matcher.dart
@@ -0,0 +1,115 @@
+// 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.
+
+import 'package:meta/meta.dart';
+
+import 'having_matcher.dart';
+import 'interfaces.dart';
+
+/// Returns a matcher that matches objects with type [T].
+///
+/// ```dart
+/// expect(shouldBeDuration, isA<Duration>());
+/// ```
+///
+/// Expectations can be chained on top of the type using the
+/// [TypeMatcher.having] method to add additional constraints.
+TypeMatcher<T> isA<T>() => TypeMatcher<T>();
+
+/// A [Matcher] subclass that supports validating the [Type] of the target
+/// object.
+///
+/// ```dart
+/// expect(shouldBeDuration, TypeMatcher<Duration>());
+/// ```
+///
+/// If you want to further validate attributes of the specified [Type], use the
+/// [having] function.
+///
+/// ```dart
+/// void shouldThrowRangeError(int value) {
+///   throw RangeError.range(value, 10, 20);
+/// }
+///
+/// expect(
+///     () => shouldThrowRangeError(5),
+///     throwsA(const TypeMatcher<RangeError>()
+///         .having((e) => e.start, 'start', greaterThanOrEqualTo(10))
+///         .having((e) => e.end, 'end', lessThanOrEqualTo(20))));
+/// ```
+///
+/// Notice that you can chain multiple calls to [having] to verify multiple
+/// aspects of an object.
+///
+/// Note: All of the top-level `isType` matchers exposed by this package are
+/// instances of [TypeMatcher], so you can use the [having] function without
+/// creating your own instance.
+///
+/// ```dart
+/// expect(
+///     () => shouldThrowRangeError(5),
+///     throwsA(isRangeError
+///         .having((e) => e.start, 'start', greaterThanOrEqualTo(10))
+///         .having((e) => e.end, 'end', lessThanOrEqualTo(20))));
+/// ```
+class TypeMatcher<T> extends Matcher {
+  final String? _name;
+
+  /// Create a matcher matches instances of type [T].
+  ///
+  /// For a fluent API to create TypeMatchers see [isA].
+  const TypeMatcher(
+      [@Deprecated('Provide a type argument to TypeMatcher and omit the name. '
+          'This argument will be removed in the next release.')
+      String? name])
+      : _name =
+            // ignore: deprecated_member_use_from_same_package
+            name;
+
+  /// Returns a new [TypeMatcher] that validates the existing type as well as
+  /// a specific [feature] of the object with the provided [matcher].
+  ///
+  /// Provides a human-readable [description] of the [feature] to make debugging
+  /// failures easier.
+  ///
+  /// ```dart
+  /// /// Validates that the object is a [RangeError] with a message containing
+  /// /// the string 'details' and `start` and `end` properties that are `null`.
+  /// final _rangeMatcher = isRangeError
+  ///    .having((e) => e.message, 'message', contains('details'))
+  ///    .having((e) => e.start, 'start', isNull)
+  ///    .having((e) => e.end, 'end', isNull);
+  /// ```
+  @useResult
+  TypeMatcher<T> having(
+          Object? Function(T) feature, String description, dynamic matcher) =>
+      HavingMatcher(this, description, feature, matcher);
+
+  @override
+  Description describe(Description description) {
+    var name = _name ?? _stripDynamic(T);
+    return description.add("<Instance of '$name'>");
+  }
+
+  @override
+  bool matches(Object? item, Map matchState) => item is T;
+
+  @override
+  Description describeMismatch(dynamic item, Description mismatchDescription,
+      Map matchState, bool verbose) {
+    var name = _name ?? _stripDynamic(T);
+    return mismatchDescription.add("is not an instance of '$name'");
+  }
+}
+
+final _dart2DynamicArgs = RegExp('<dynamic(, dynamic)*>');
+
+/// With this expression `{}.runtimeType.toString`,
+/// Dart 1: "`<Instance of Map>`"
+/// Dart 2: "`<Instance of Map<dynamic, dynamic>>`"
+///
+/// This functions returns the Dart 1 output, when Dart 2 runtime semantics
+/// are enabled.
+String _stripDynamic(Type type) =>
+    type.toString().replaceAll(_dart2DynamicArgs, '');
diff --git a/pkgs/matcher/lib/src/util.dart b/pkgs/matcher/lib/src/util.dart
new file mode 100644
index 0000000..af0ba2c
--- /dev/null
+++ b/pkgs/matcher/lib/src/util.dart
@@ -0,0 +1,70 @@
+// Copyright (c) 2014, 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 'core_matchers.dart';
+import 'equals_matcher.dart';
+import 'interfaces.dart';
+
+/// A [Map] between whitespace characters and their escape sequences.
+const _escapeMap = {
+  '\n': r'\n',
+  '\r': r'\r',
+  '\f': r'\f',
+  '\b': r'\b',
+  '\t': r'\t',
+  '\v': r'\v',
+  '\x7F': r'\x7F', // delete
+};
+
+/// A [RegExp] that matches whitespace characters that should be escaped.
+final _escapeRegExp = RegExp(
+    '[\\x00-\\x07\\x0E-\\x1F${_escapeMap.keys.map(_getHexLiteral).join()}]');
+
+/// Useful utility for nesting match states.
+void addStateInfo(Map matchState, Map values) {
+  var innerState = Map.of(matchState);
+  matchState.clear();
+  matchState['state'] = innerState;
+  matchState.addAll(values);
+}
+
+/// Takes an argument and returns an equivalent [Matcher].
+///
+/// If the argument is already a matcher this does nothing,
+/// else if the argument is a function, it generates a predicate
+/// function matcher, else it generates an equals matcher.
+Matcher wrapMatcher(Object? valueOrMatcher) {
+  if (valueOrMatcher is Matcher) {
+    return valueOrMatcher;
+  } else if (valueOrMatcher is bool Function(Object?)) {
+    // already a predicate that can handle anything
+    return predicate(valueOrMatcher);
+  } else if (valueOrMatcher is bool Function(Never)) {
+    // unary predicate, but expects a specific type
+    // so wrap it.
+    // ignore: unnecessary_lambdas
+    return predicate((a) => (valueOrMatcher as dynamic)(a));
+  } else {
+    return equals(valueOrMatcher);
+  }
+}
+
+/// Returns [str] with all whitespace characters represented as their escape
+/// sequences.
+///
+/// Backslash characters are escaped as `\\`
+String escape(String str) {
+  str = str.replaceAll('\\', r'\\');
+  return str.replaceAllMapped(_escapeRegExp, (match) {
+    var mapped = _escapeMap[match[0]];
+    if (mapped != null) return mapped;
+    return _getHexLiteral(match[0]!);
+  });
+}
+
+/// Given single-character string, return the hex-escaped equivalent.
+String _getHexLiteral(String input) {
+  var rune = input.runes.single;
+  return r'\x' + rune.toRadixString(16).toUpperCase().padLeft(2, '0');
+}
diff --git a/pkgs/matcher/mono_pkg.yaml b/pkgs/matcher/mono_pkg.yaml
new file mode 100644
index 0000000..9ef7927
--- /dev/null
+++ b/pkgs/matcher/mono_pkg.yaml
@@ -0,0 +1,17 @@
+# See https://pub.dev/packages/mono_repo
+
+sdk:
+- dev
+
+stages:
+- analyze_and_format:
+  - group:
+    - format
+    - analyze: --fatal-infos
+  - group:
+    - analyze
+    sdk: pubspec
+- unit_test:
+  - group:
+    - command: dart test
+    sdk: [dev, pubspec]
diff --git a/pkgs/matcher/pubspec.yaml b/pkgs/matcher/pubspec.yaml
new file mode 100644
index 0000000..237e559
--- /dev/null
+++ b/pkgs/matcher/pubspec.yaml
@@ -0,0 +1,25 @@
+name: matcher
+version: 0.12.18-wip
+description: >-
+  Support for specifying test expectations via an extensible Matcher class.
+  Also includes a number of built-in Matcher implementations for common cases.
+repository: https://github.com/dart-lang/test/tree/master/pkgs/matcher
+
+environment:
+  sdk: ^3.4.0
+
+dependencies:
+  async: ^2.10.0
+  meta: ^1.8.0
+  stack_trace: ^1.10.0
+  term_glyph: ^1.2.0
+  test_api: ">=0.5.0 <0.8.0"
+
+dev_dependencies:
+  fake_async: ^1.3.0
+  lints: ^3.0.0
+  test: ^1.23.0
+
+dependency_overrides:
+  test: 1.25.0
+  test_api: 0.7.3
diff --git a/pkgs/matcher/test/core_matchers_test.dart b/pkgs/matcher/test/core_matchers_test.dart
new file mode 100644
index 0000000..04fc8b3
--- /dev/null
+++ b/pkgs/matcher/test/core_matchers_test.dart
@@ -0,0 +1,247 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:matcher/matcher.dart';
+import 'package:test/test.dart' show test, group;
+
+import 'test_utils.dart';
+
+void main() {
+  test('isTrue', () {
+    shouldPass(true, isTrue);
+    shouldFail(false, isTrue, 'Expected: true Actual: <false>');
+  });
+
+  test('isFalse', () {
+    shouldPass(false, isFalse);
+    shouldFail(10, isFalse, 'Expected: false Actual: <10>');
+    shouldFail(true, isFalse, 'Expected: false Actual: <true>');
+  });
+
+  test('isNull', () {
+    shouldPass(null, isNull);
+    shouldFail(false, isNull, 'Expected: null Actual: <false>');
+  });
+
+  test('isNotNull', () {
+    shouldPass(false, isNotNull);
+    shouldFail(null, isNotNull, 'Expected: not null Actual: <null>');
+  });
+
+  test('isNaN', () {
+    shouldPass(double.nan, isNaN);
+    shouldFail(3.1, isNaN, 'Expected: NaN Actual: <3.1>');
+    shouldFail('not a num', isNaN, endsWith('not an <Instance of \'num\'>'));
+  });
+
+  test('isNotNaN', () {
+    shouldPass(3.1, isNotNaN);
+    shouldFail(double.nan, isNotNaN, 'Expected: not NaN Actual: <NaN>');
+    shouldFail('not a num', isNotNaN, endsWith('not an <Instance of \'num\'>'));
+  });
+
+  test('same', () {
+    var a = {};
+    var b = {};
+    shouldPass(a, same(a));
+    shouldFail(b, same(a), 'Expected: same instance as {} Actual: {}');
+  });
+
+  test('equals', () {
+    var a = {};
+    var b = {};
+    shouldPass(a, equals(a));
+    shouldPass(a, equals(b));
+  });
+
+  test('equals with null', () {
+    Object? a; // null
+    var b = {};
+    shouldPass(a, equals(a));
+    shouldFail(
+        a, equals(b), 'Expected: {} Actual: <null> Which: expected a map');
+    shouldFail(b, equals(a), 'Expected: <null> Actual: {}');
+  });
+
+  test('equals with a set', () {
+    var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
+    var set1 = numbers.toSet();
+    numbers.shuffle();
+    var set2 = numbers.toSet();
+
+    shouldPass(set2, equals(set1));
+    shouldPass(numbers, equals(set1));
+    shouldFail(
+        [1, 2, 3, 4, 5, 6, 7, 8, 9],
+        equals(set1),
+        matches(r'Expected: .*:\[1, 2, 3, 4, 5, 6, 7, 8, 9, 10\]'
+            r'  Actual: \[1, 2, 3, 4, 5, 6, 7, 8, 9\]'
+            r'   Which: does not contain <10>'));
+    shouldFail(
+        [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
+        equals(set1),
+        matches(r'Expected: .*:\[1, 2, 3, 4, 5, 6, 7, 8, 9, 10\]'
+            r'  Actual: \[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11\]'
+            r'   Which: larger than expected'));
+  });
+
+  test('anything', () {
+    var a = {};
+    shouldPass(0, anything);
+    shouldPass(null, anything);
+    shouldPass(a, anything);
+    shouldFail(a, isNot(anything), 'Expected: not anything Actual: {}');
+  });
+
+  test('returnsNormally', () {
+    shouldPass(doesNotThrow, returnsNormally);
+    shouldFail(
+        doesThrow,
+        returnsNormally,
+        matches(r'Expected: return normally'
+            r'  Actual: <Closure.*>'
+            r'   Which: threw StateError:<Bad state: X>'));
+    shouldFail('not a function', returnsNormally,
+        contains('not an <Instance of \'Function\'>'));
+  });
+
+  test('hasLength', () {
+    var a = {};
+    var b = [];
+    shouldPass(a, hasLength(0));
+    shouldPass(b, hasLength(0));
+    shouldPass('a', hasLength(1));
+    shouldFail(
+        0,
+        hasLength(0),
+        'Expected: an object with length of <0> '
+        'Actual: <0> '
+        'Which: has no length property');
+
+    b.add(0);
+    shouldPass(b, hasLength(1));
+    shouldFail(
+        b,
+        hasLength(2),
+        'Expected: an object with length of <2> '
+        'Actual: [0] '
+        'Which: has length of <1>');
+
+    b.add(0);
+    shouldFail(
+        b,
+        hasLength(1),
+        'Expected: an object with length of <1> '
+        'Actual: [0, 0] '
+        'Which: has length of <2>');
+    shouldPass(b, hasLength(2));
+  });
+
+  test('scalar type mismatch', () {
+    shouldFail(
+        'error',
+        equals(5.1),
+        'Expected: <5.1> '
+            "Actual: 'error'");
+  });
+
+  test('nested type mismatch', () {
+    shouldFail(
+        ['error'],
+        equals([5.1]),
+        'Expected: [5.1] '
+        "Actual: ['error'] "
+        "Which: at location [0] is 'error' instead of <5.1>");
+  });
+
+  test('doubly-nested type mismatch', () {
+    shouldFail(
+        [
+          ['error']
+        ],
+        equals([
+          [5.1]
+        ]),
+        'Expected: [[5.1]] '
+        "Actual: [['error']] "
+        "Which: at location [0][0] is 'error' instead of <5.1>");
+  });
+
+  test('doubly nested inequality', () {
+    var actual1 = [
+      ['foo', 'bar'],
+      ['foo'],
+      3,
+      []
+    ];
+    var expected1 = [
+      ['foo', 'bar'],
+      ['foo'],
+      4,
+      []
+    ];
+    var reason1 = "Expected: [['foo', 'bar'], ['foo'], 4, []] "
+        "Actual: [['foo', 'bar'], ['foo'], 3, []] "
+        'Which: at location [2] is <3> instead of <4>';
+
+    var actual2 = [
+      ['foo', 'barry'],
+      ['foo'],
+      4,
+      []
+    ];
+    var expected2 = [
+      ['foo', 'bar'],
+      ['foo'],
+      4,
+      []
+    ];
+    var reason2 = "Expected: [['foo', 'bar'], ['foo'], 4, []] "
+        "Actual: [['foo', 'barry'], ['foo'], 4, []] "
+        "Which: at location [0][1] is 'barry' instead of 'bar'";
+
+    var actual3 = [
+      ['foo', 'bar'],
+      ['foo'],
+      4,
+      {'foo': 'bar'}
+    ];
+    var expected3 = [
+      ['foo', 'bar'],
+      ['foo'],
+      4,
+      {'foo': 'barry'}
+    ];
+    var reason3 = "Expected: [['foo', 'bar'], ['foo'], 4, {'foo': 'barry'}] "
+        "Actual: [['foo', 'bar'], ['foo'], 4, {'foo': 'bar'}] "
+        "Which: at location [3]['foo'] is 'bar' instead of 'barry'";
+
+    shouldFail(actual1, equals(expected1), reason1);
+    shouldFail(actual2, equals(expected2), reason2);
+    shouldFail(actual3, equals(expected3), reason3);
+  });
+
+  group('Predicate Matchers', () {
+    test('isInstanceOf', () {
+      shouldFail(0, predicate((x) => x is String, 'an instance of String'),
+          'Expected: an instance of String Actual: <0>');
+      shouldPass('cow', predicate((x) => x is String, 'an instance of String'));
+
+      shouldFail(0, predicate((bool x) => x, 'bool value is true'),
+          endsWith("not an <Instance of 'bool'>"));
+    });
+  });
+
+  group('wrapMatcher', () {
+    test('wraps a predicate which allows a nullable argument', () {
+      final matcher = wrapMatcher((_) => true);
+      shouldPass(null, matcher);
+    });
+
+    test('wraps a predicate which has a typed argument check', () {
+      final matcher = wrapMatcher((int _) => true);
+      shouldPass(1, matcher);
+    });
+  });
+}
diff --git a/pkgs/matcher/test/custom_matcher_test.dart b/pkgs/matcher/test/custom_matcher_test.dart
new file mode 100644
index 0000000..d0a17c9
--- /dev/null
+++ b/pkgs/matcher/test/custom_matcher_test.dart
@@ -0,0 +1,48 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:matcher/matcher.dart';
+import 'package:test/test.dart' show test;
+
+import 'test_utils.dart';
+
+class _BadCustomMatcher extends CustomMatcher {
+  _BadCustomMatcher() : super('feature', 'description', {1: 'a'});
+  @override
+  Object? featureValueOf(dynamic actual) => throw Exception('bang');
+}
+
+class _HasPrice extends CustomMatcher {
+  _HasPrice(Object? matcher)
+      : super('Widget with a price that is', 'price', matcher);
+  @override
+  Object? featureValueOf(Object? actual) => (actual as Widget).price;
+}
+
+void main() {
+  test('Feature Matcher', () {
+    var w = Widget();
+    w.price = 10;
+    shouldPass(w, _HasPrice(10));
+    shouldPass(w, _HasPrice(greaterThan(0)));
+    shouldFail(
+        w,
+        _HasPrice(greaterThan(10)),
+        'Expected: Widget with a price that is a value greater than <10> '
+        "Actual: <Instance of 'Widget'> "
+        'Which: has price with value <10> which is not '
+        'a value greater than <10>');
+  });
+
+  test('Custom Matcher Exception', () {
+    shouldFail(
+        'a',
+        _BadCustomMatcher(),
+        allOf([
+          contains("Expected: feature {1: 'a'} "),
+          contains("Actual: 'a' "),
+          contains("Which: threw 'Exception: bang' "),
+        ]));
+  });
+}
diff --git a/pkgs/matcher/test/escape_test.dart b/pkgs/matcher/test/escape_test.dart
new file mode 100644
index 0000000..c898054
--- /dev/null
+++ b/pkgs/matcher/test/escape_test.dart
@@ -0,0 +1,63 @@
+// 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.
+
+// ignore_for_file: missing_whitespace_between_adjacent_strings
+
+import 'package:test/test.dart';
+
+void main() {
+  group('escaping should work with', () {
+    _testEscaping('no escaped chars', 'Hello, world!', 'Hello, world!');
+    _testEscaping('newline', '\n', r'\n');
+    _testEscaping('carriage return', '\r', r'\r');
+    _testEscaping('form feed', '\f', r'\f');
+    _testEscaping('backspace', '\b', r'\b');
+    _testEscaping('tab', '\t', r'\t');
+    _testEscaping('vertical tab', '\v', r'\v');
+    _testEscaping('null byte', '\x00', r'\x00');
+    _testEscaping('ASCII control character', '\x11', r'\x11');
+    _testEscaping('delete', '\x7F', r'\x7F');
+    _testEscaping('escape combos', r'\n', r'\\n');
+    _testEscaping(
+        'All characters',
+        'A new line\nA charriage return\rA form feed\fA backspace\b'
+            'A tab\tA vertical tab\vA slash\\A null byte\x00A control char\x1D'
+            'A delete\x7F',
+        r'A new line\nA charriage return\rA form feed\fA backspace\b'
+            r'A tab\tA vertical tab\vA slash\\A null byte\x00A control char\x1D'
+            r'A delete\x7F');
+  });
+
+  group('unequal strings remain unequal when escaped', () {
+    _testUnequalStrings('with a newline', '\n', r'\n');
+    _testUnequalStrings('with slash literals', '\\', r'\\');
+  });
+}
+
+/// Creates a [test] with name [name] that verifies [source] escapes to value
+/// [target].
+void _testEscaping(String name, String source, String target) {
+  test(name, () {
+    var escaped = escape(source);
+    expect(escaped == target, isTrue,
+        reason: 'Expected escaped value: $target\n'
+            '  Actual escaped value: $escaped');
+  });
+}
+
+/// Creates a [test] with name [name] that ensures two different [String] values
+/// [s1] and [s2] remain unequal when escaped.
+void _testUnequalStrings(String name, String s1, String s2) {
+  test(name, () {
+    // Explicitly not using the equals matcher
+    expect(s1 != s2, isTrue, reason: 'The source values should be unequal');
+
+    var escapedS1 = escape(s1);
+    var escapedS2 = escape(s2);
+
+    // Explicitly not using the equals matcher
+    expect(escapedS1 != escapedS2, isTrue,
+        reason: 'Unequal strings, when escaped, should remain unequal.');
+  });
+}
diff --git a/pkgs/matcher/test/expect_async_test.dart b/pkgs/matcher/test/expect_async_test.dart
new file mode 100644
index 0000000..7619893
--- /dev/null
+++ b/pkgs/matcher/test/expect_async_test.dart
@@ -0,0 +1,393 @@
+// 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.
+
+// ignore_for_file: only_throw_errors
+
+import 'dart:async';
+
+import 'package:fake_async/fake_async.dart';
+import 'package:test/test.dart';
+import 'package:test_api/hooks_testing.dart';
+
+import 'utils_new.dart';
+
+void main() {
+  group('supports a function with this many arguments:', () {
+    test('0', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        expectAsync0(() {
+          callbackRun = true;
+        })();
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test('1', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        expectAsync1((int arg) {
+          expect(arg, equals(1));
+          callbackRun = true;
+        })(1);
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test('2', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        expectAsync2((arg1, arg2) {
+          expect(arg1, equals(1));
+          expect(arg2, equals(2));
+          callbackRun = true;
+        })(1, 2);
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test('3', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        expectAsync3((arg1, arg2, arg3) {
+          expect(arg1, equals(1));
+          expect(arg2, equals(2));
+          expect(arg3, equals(3));
+          callbackRun = true;
+        })(1, 2, 3);
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test('4', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        expectAsync4((arg1, arg2, arg3, arg4) {
+          expect(arg1, equals(1));
+          expect(arg2, equals(2));
+          expect(arg3, equals(3));
+          expect(arg4, equals(4));
+          callbackRun = true;
+        })(1, 2, 3, 4);
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test('5', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        expectAsync5((arg1, arg2, arg3, arg4, arg5) {
+          expect(arg1, equals(1));
+          expect(arg2, equals(2));
+          expect(arg3, equals(3));
+          expect(arg4, equals(4));
+          expect(arg5, equals(5));
+          callbackRun = true;
+        })(1, 2, 3, 4, 5);
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test('6', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        expectAsync6((arg1, arg2, arg3, arg4, arg5, arg6) {
+          expect(arg1, equals(1));
+          expect(arg2, equals(2));
+          expect(arg3, equals(3));
+          expect(arg4, equals(4));
+          expect(arg5, equals(5));
+          expect(arg6, equals(6));
+          callbackRun = true;
+        })(1, 2, 3, 4, 5, 6);
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+  });
+
+  group('with optional arguments', () {
+    test('allows them to be passed', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        expectAsync1(([arg = 1]) {
+          expect(arg, equals(2));
+          callbackRun = true;
+        })(2);
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test('allows them not to be passed', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        expectAsync1(([arg = 1]) {
+          expect(arg, equals(1));
+          callbackRun = true;
+        })();
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+  });
+
+  group('by default', () {
+    test("won't allow the test to complete until it's called", () async {
+      late void Function() callback;
+      final monitor = TestCaseMonitor.start(() {
+        callback = expectAsync0(() {});
+      });
+
+      await pumpEventQueue();
+      expect(monitor.state, equals(State.running));
+      callback();
+      await monitor.onDone;
+
+      expectTestPassed(monitor);
+    });
+
+    test('may only be called once', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        var callback = expectAsync0(() {});
+        callback();
+        callback();
+      });
+
+      expectTestFailed(
+          monitor, 'Callback called more times than expected (1).');
+    });
+  });
+
+  group('with count', () {
+    test(
+        "won't allow the test to complete until it's called at least that "
+        'many times', () async {
+      late void Function() callback;
+      final monitor = TestCaseMonitor.start(() {
+        callback = expectAsync0(() {}, count: 3);
+      });
+
+      await pumpEventQueue();
+      expect(monitor.state, equals(State.running));
+      callback();
+
+      await pumpEventQueue();
+      expect(monitor.state, equals(State.running));
+      callback();
+
+      await pumpEventQueue();
+      expect(monitor.state, equals(State.running));
+      callback();
+
+      await monitor.onDone;
+
+      expectTestPassed(monitor);
+    });
+
+    test("will throw an error if it's called more than that many times",
+        () async {
+      var monitor = await TestCaseMonitor.run(() {
+        var callback = expectAsync0(() {}, count: 3);
+        callback();
+        callback();
+        callback();
+        callback();
+      });
+
+      expectTestFailed(
+          monitor, 'Callback called more times than expected (3).');
+    });
+
+    group('0,', () {
+      test("won't block the test's completion", () {
+        expectAsync0(() {}, count: 0);
+      });
+
+      test("will throw an error if it's ever called", () async {
+        var monitor = await TestCaseMonitor.run(() {
+          expectAsync0(() {}, count: 0)();
+        });
+
+        expectTestFailed(
+            monitor, 'Callback called more times than expected (0).');
+      });
+    });
+  });
+
+  group('with max', () {
+    test('will allow the callback to be called that many times', () {
+      var callback = expectAsync0(() {}, max: 3);
+      callback();
+      callback();
+      callback();
+    });
+
+    test('will allow the callback to be called fewer than that many times', () {
+      var callback = expectAsync0(() {}, max: 3);
+      callback();
+    });
+
+    test("will throw an error if it's called more than that many times",
+        () async {
+      var monitor = await TestCaseMonitor.run(() {
+        var callback = expectAsync0(() {}, max: 3);
+        callback();
+        callback();
+        callback();
+        callback();
+      });
+
+      expectTestFailed(
+          monitor, 'Callback called more times than expected (3).');
+    });
+
+    test('-1, will allow the callback to be called any number of times', () {
+      var callback = expectAsync0(() {}, max: -1);
+      for (var i = 0; i < 20; i++) {
+        callback();
+      }
+    });
+  });
+
+  test('will throw an error if max is less than count', () {
+    expect(() => expectAsync0(() {}, max: 1, count: 2), throwsArgumentError);
+  });
+
+  group('expectAsyncUntil()', () {
+    test("won't allow the test to complete until isDone returns true",
+        () async {
+      late TestCaseMonitor monitor;
+      late Future future;
+      monitor = TestCaseMonitor.start(() {
+        var done = false;
+        var callback = expectAsyncUntil0(() {}, () => done);
+
+        future = () async {
+          await pumpEventQueue();
+          expect(monitor.state, equals(State.running));
+          callback();
+          await pumpEventQueue();
+          expect(monitor.state, equals(State.running));
+          done = true;
+          callback();
+        }();
+      });
+      await monitor.onDone;
+
+      expectTestPassed(monitor);
+      // Ensure that the outer test doesn't complete until the inner future
+      // completes.
+      await future;
+    });
+
+    test("doesn't call isDone until after the callback is called", () {
+      var callbackRun = false;
+      expectAsyncUntil0(() => callbackRun = true, () {
+        expect(callbackRun, isTrue);
+        return true;
+      })();
+    });
+  });
+
+  test('allows errors', () async {
+    var monitor = await TestCaseMonitor.run(() {
+      expect(expectAsync0(() => throw 'oh no'), throwsA('oh no'));
+    });
+
+    expectTestPassed(monitor);
+  });
+
+  test('may be called in a non-test zone', () async {
+    var monitor = await TestCaseMonitor.run(() {
+      var callback = expectAsync0(() {});
+      Zone.root.run(callback);
+    });
+    expectTestPassed(monitor);
+  });
+
+  test('may be called in a FakeAsync zone that does not run further', () async {
+    var monitor = await TestCaseMonitor.run(() {
+      FakeAsync().run((_) {
+        var callback = expectAsync0(() {});
+        callback();
+      });
+    });
+    expectTestPassed(monitor);
+  });
+
+  group('old-style expectAsync()', () {
+    test('works with no arguments', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        // ignore: deprecated_member_use_from_same_package, avoid_dynamic_calls
+        expectAsync(() {
+          callbackRun = true;
+        })();
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test('works with dynamic arguments', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        // ignore: deprecated_member_use_from_same_package, avoid_dynamic_calls
+        expectAsync((arg1, arg2) {
+          callbackRun = true;
+        })(1, 2);
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test('works with non-nullable arguments', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        // ignore: deprecated_member_use_from_same_package, avoid_dynamic_calls
+        expectAsync((int arg1, int arg2) {
+          callbackRun = true;
+        })(1, 2);
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test('works with 6 arguments', () async {
+      var callbackRun = false;
+      var monitor = await TestCaseMonitor.run(() {
+        // ignore: deprecated_member_use_from_same_package, avoid_dynamic_calls
+        expectAsync((arg1, arg2, arg3, arg4, arg5, arg6) {
+          callbackRun = true;
+        })(1, 2, 3, 4, 5, 6);
+      });
+
+      expectTestPassed(monitor);
+      expect(callbackRun, isTrue);
+    });
+
+    test("doesn't support a function with 7 arguments", () {
+      // ignore: deprecated_member_use_from_same_package
+      expect(() => expectAsync((a, b, c, d, e, f, g) {}), throwsArgumentError);
+    });
+  });
+}
diff --git a/pkgs/matcher/test/expect_test.dart b/pkgs/matcher/test/expect_test.dart
new file mode 100644
index 0000000..e2ef497
--- /dev/null
+++ b/pkgs/matcher/test/expect_test.dart
@@ -0,0 +1,36 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test/test.dart';
+
+import 'utils_new.dart';
+
+void main() {
+  group('returned Future from expectLater()', () {
+    test('completes immediately for a sync matcher', () {
+      expect(expectLater(true, isTrue), completes);
+    });
+
+    test('contains the expect failure', () {
+      expect(expectLater(Future.value(true), completion(isFalse)),
+          throwsA(isTestFailure(anything)));
+    });
+
+    test('contains an async error', () {
+      expect(expectLater(Future.error('oh no'), completion(isFalse)),
+          throwsA('oh no'));
+    });
+  });
+
+  group('an async matcher that fails synchronously', () {
+    test('throws synchronously', () {
+      expect(() => expect(() {}, throwsA(anything)),
+          throwsA(isTestFailure(anything)));
+    });
+
+    test('can be used with synchronous operators', () {
+      expect(() {}, isNot(throwsA(anything)));
+    });
+  });
+}
diff --git a/pkgs/matcher/test/having_test.dart b/pkgs/matcher/test/having_test.dart
new file mode 100644
index 0000000..ddada77
--- /dev/null
+++ b/pkgs/matcher/test/having_test.dart
@@ -0,0 +1,129 @@
+// 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.
+
+// ignore_for_file: lines_longer_than_80_chars
+
+import 'package:matcher/matcher.dart';
+import 'package:test/test.dart' show test, expect, throwsA, group;
+
+import 'test_utils.dart';
+
+void main() {
+  test('success', () {
+    shouldPass(RangeError('details'), _rangeMatcher);
+  });
+
+  test('failure', () {
+    shouldFail(
+      CustomRangeError.range(-1, 1, 10),
+      _rangeMatcher,
+      "Expected: <Instance of 'RangeError'> with "
+      "`message`: contains 'details' and `start`: null and `end`: null "
+      'Actual: CustomRangeError:<RangeError: Invalid value: details> '
+      "Which: has `message` with value 'Invalid value' "
+      "which does not contain 'details'",
+    );
+  });
+
+  // This code is used in the [TypeMatcher] doc comments.
+  test('integration and example', () {
+    void shouldThrowRangeError(int value) {
+      throw RangeError.range(value, 10, 20);
+    }
+
+    expect(
+        () => shouldThrowRangeError(5),
+        throwsA(const TypeMatcher<RangeError>()
+            .having((e) => e.start, 'start', greaterThanOrEqualTo(10))
+            .having((e) => e.end, 'end', lessThanOrEqualTo(20))));
+
+    expect(
+        () => shouldThrowRangeError(5),
+        throwsA(isRangeError
+            .having((e) => e.start, 'start', greaterThanOrEqualTo(10))
+            .having((e) => e.end, 'end', lessThanOrEqualTo(20))));
+  });
+
+  test('having inside deep matcher', () {
+    shouldFail(
+        [RangeError.range(-1, 1, 10)],
+        equals([_rangeMatcher]),
+        anyOf([
+          equalsIgnoringWhitespace(
+              "Expected: [ <<Instance of 'RangeError'> with "
+              "`message`: contains 'details' and `start`: null and `end`: null> ] "
+              'Actual: [RangeError:RangeError: '
+              'Invalid value: Not in inclusive range 1..10: -1] '
+              'Which: at location [0] is RangeError:<RangeError: '
+              'Invalid value: Not in inclusive range 1..10: -1> '
+              "which has `message` with value 'Invalid value' "
+              "which does not contain 'details'"),
+          equalsIgnoringWhitespace(// Older SDKs
+              "Expected: [ <<Instance of 'RangeError'> with "
+              "`message`: contains 'details' and `start`: null and `end`: null> ] "
+              'Actual: [RangeError:RangeError: '
+              'Invalid value: Not in range 1..10, inclusive: -1] '
+              'Which: at location [0] is RangeError:<RangeError: '
+              'Invalid value: Not in range 1..10, inclusive: -1> '
+              "which has `message` with value 'Invalid value' "
+              "which does not contain 'details'")
+        ]));
+  });
+
+  group('CustomMatcher copy', () {
+    test('Feature Matcher', () {
+      var w = Widget();
+      w.price = 10;
+      shouldPass(w, _hasPrice(10));
+      shouldPass(w, _hasPrice(greaterThan(0)));
+      shouldFail(
+          w,
+          _hasPrice(greaterThan(10)),
+          "Expected: <Instance of 'Widget'> with `price`: a value greater than <10> "
+          "Actual: <Instance of 'Widget'> "
+          'Which: has `price` with value <10> which is not '
+          'a value greater than <10>');
+    });
+
+    test('Custom Matcher Exception', () {
+      shouldFail(
+          'a',
+          _badCustomMatcher(),
+          allOf([
+            contains(
+                "Expected: <Instance of 'Widget'> with `feature`: {1: 'a'} "),
+            contains("Actual: 'a'"),
+          ]));
+      shouldFail(
+          Widget(),
+          _badCustomMatcher(),
+          allOf([
+            contains(
+                "Expected: <Instance of 'Widget'> with `feature`: {1: 'a'} "),
+            contains("Actual: <Instance of 'Widget'> "),
+            contains("Which: threw 'Exception: bang' "),
+          ]));
+    });
+  });
+}
+
+final _rangeMatcher = isRangeError
+    .having((e) => e.message, 'message', contains('details'))
+    .having((e) => e.start, 'start', isNull)
+    .having((e) => e.end, 'end', isNull);
+
+Matcher _hasPrice(Object matcher) =>
+    const TypeMatcher<Widget>().having((e) => e.price, 'price', matcher);
+
+Matcher _badCustomMatcher() => const TypeMatcher<Widget>()
+    .having((e) => throw Exception('bang'), 'feature', {1: 'a'});
+
+class CustomRangeError extends RangeError {
+  CustomRangeError.range(
+      super.invalidValue, int super.minValue, int super.maxValue)
+      : super.range();
+
+  @override
+  String toString() => 'RangeError: Invalid value: details';
+}
diff --git a/pkgs/matcher/test/iterable_matchers_test.dart b/pkgs/matcher/test/iterable_matchers_test.dart
new file mode 100644
index 0000000..7607d18
--- /dev/null
+++ b/pkgs/matcher/test/iterable_matchers_test.dart
@@ -0,0 +1,395 @@
+// Copyright (c) 2012, 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 'test_utils.dart';
+
+void main() {
+  test('isEmpty', () {
+    shouldPass([], isEmpty);
+    shouldFail([1], isEmpty, 'Expected: empty Actual: [1]');
+  });
+
+  test('isNotEmpty', () {
+    shouldFail([], isNotEmpty, 'Expected: non-empty Actual: []');
+    shouldPass([1], isNotEmpty);
+  });
+
+  test('contains', () {
+    var d = [1, 2];
+    shouldPass(d, contains(1));
+    shouldFail(
+        d,
+        contains(0),
+        'Expected: contains <0> '
+        'Actual: [1, 2] '
+        'Which: does not contain <0>');
+
+    shouldFail(
+        'String',
+        contains(42),
+        "Expected: contains <42> Actual: 'String' "
+            'Which: does not contain <42>');
+  });
+
+  test('equals with matcher element', () {
+    var d = ['foo', 'bar'];
+    shouldPass(d, equals(['foo', startsWith('ba')]));
+    shouldFail(
+        d,
+        equals(['foo', endsWith('ba')]),
+        "Expected: ['foo', <a string ending with 'ba'>] "
+        "Actual: ['foo', 'bar'] "
+        "Which: at location [1] is 'bar' which "
+        "does not match a string ending with 'ba'");
+  });
+
+  test('isIn', () {
+    // Iterable
+    shouldPass(1, isIn([1, 2]));
+    shouldFail(0, isIn([1, 2]), 'Expected: is in [1, 2] Actual: <0>');
+
+    // Map
+    shouldPass(1, isIn({1: null}));
+    shouldFail(0, isIn({1: null}), 'Expected: is in {1: null} Actual: <0>');
+
+    // String
+    shouldPass('42', isIn('1421'));
+    shouldFail('42', isIn('41'), "Expected: is in '41' Actual: '42'");
+    shouldFail(
+        0, isIn('a string'), endsWith('not an <Instance of \'Pattern\'>'));
+
+    // Invalid arg
+    expect(() => isIn(42), throwsArgumentError);
+  });
+
+  test('everyElement', () {
+    var d = [1, 2];
+    var e = [1, 1, 1];
+    shouldFail(
+        d,
+        everyElement(1),
+        'Expected: every element(<1>) '
+        'Actual: [1, 2] '
+        "Which: has value <2> which doesn't match <1> at index 1");
+    shouldPass(e, everyElement(1));
+    shouldFail('not iterable', everyElement(1),
+        endsWith('not an <Instance of \'Iterable\'>'));
+  });
+
+  test('nested everyElement', () {
+    var d = [
+      ['foo', 'bar'],
+      ['foo'],
+      []
+    ];
+    var e = [
+      ['foo', 'bar'],
+      ['foo'],
+      3,
+      []
+    ];
+    shouldPass(d, everyElement(anyOf(isEmpty, contains('foo'))));
+    shouldFail(
+        d,
+        everyElement(everyElement(equals('foo'))),
+        "Expected: every element(every element('foo')) "
+        "Actual: [['foo', 'bar'], ['foo'], []] "
+        "Which: has value ['foo', 'bar'] which has value 'bar' "
+        'which is different. Expected: foo Actual: bar ^ '
+        'Differ at offset 0 at index 1 at index 0');
+    shouldFail(
+        d,
+        everyElement(allOf(hasLength(greaterThan(0)), contains('foo'))),
+        'Expected: every element((an object with length of a value '
+        "greater than <0> and contains 'foo')) "
+        "Actual: [['foo', 'bar'], ['foo'], []] "
+        'Which: has value [] which has length of <0> at index 2');
+    shouldFail(
+        d,
+        everyElement(allOf(contains('foo'), hasLength(greaterThan(0)))),
+        "Expected: every element((contains 'foo' and "
+        'an object with length of a value greater than <0>)) '
+        "Actual: [['foo', 'bar'], ['foo'], []] "
+        "Which: has value [] which does not contain 'foo' at index 2");
+    shouldFail(
+        e,
+        everyElement(allOf(contains('foo'), hasLength(greaterThan(0)))),
+        "Expected: every element((contains 'foo' and an object with "
+        'length of a value greater than <0>)) '
+        "Actual: [['foo', 'bar'], ['foo'], 3, []] "
+        'Which: has value <3> which is not a string, map or iterable '
+        'at index 2');
+  });
+
+  test('anyElement', () {
+    var d = [1, 2];
+    var e = [1, 1, 1];
+    shouldPass(d, anyElement(2));
+    shouldFail(
+        e, anyElement(2), 'Expected: some element <2> Actual: [1, 1, 1]');
+    shouldFail('not an iterable', anyElement(2),
+        endsWith('not an <Instance of \'Iterable\'>'));
+  });
+
+  test('orderedEquals', () {
+    shouldPass([null], orderedEquals([null]));
+    var d = [1, 2];
+    shouldPass(d, orderedEquals([1, 2]));
+    shouldFail(
+        d,
+        orderedEquals([2, 1]),
+        'Expected: equals [2, 1] ordered '
+        'Actual: [1, 2] '
+        'Which: at location [0] is <1> instead of <2>');
+    shouldFail('not an iterable', orderedEquals([1]),
+        endsWith('not an <Instance of \'Iterable\'>'));
+  });
+
+  test('unorderedEquals', () {
+    var d = [1, 2];
+    shouldPass(d, unorderedEquals([2, 1]));
+    shouldFail(
+        d,
+        unorderedEquals([1]),
+        'Expected: equals [1] unordered '
+        'Actual: [1, 2] '
+        'Which: has too many elements (2 > 1)');
+    shouldFail(
+        d,
+        unorderedEquals([3, 2, 1]),
+        'Expected: equals [3, 2, 1] unordered '
+        'Actual: [1, 2] '
+        'Which: has too few elements (2 < 3)');
+    shouldFail(
+        d,
+        unorderedEquals([3, 1]),
+        'Expected: equals [3, 1] unordered '
+        'Actual: [1, 2] '
+        'Which: has no match for <3> at index 0');
+    shouldFail(
+        d,
+        unorderedEquals([3, 4]),
+        'Expected: equals [3, 4] unordered '
+        'Actual: [1, 2] '
+        'Which: has no match for <3> at index 0'
+        ' along with 1 other unmatched');
+    shouldFail('not an iterable', unorderedEquals([1]),
+        endsWith('not an <Instance of \'Iterable\'>'));
+  });
+
+  test('unorderedMatches', () {
+    var d = [1, 2];
+    shouldPass(d, unorderedMatches([2, 1]));
+    shouldPass(d, unorderedMatches([greaterThan(1), greaterThan(0)]));
+    shouldPass(d, unorderedMatches([greaterThan(0), greaterThan(1)]));
+    shouldPass([2, 1], unorderedMatches([greaterThan(1), greaterThan(0)]));
+
+    shouldPass([2, 1], unorderedMatches([greaterThan(0), greaterThan(1)]));
+    // Excersize the case where pairings should get "bumped" multiple times
+    shouldPass(
+        [0, 1, 2, 3, 5, 6],
+        unorderedMatches([
+          greaterThan(1), // 6
+          equals(2), // 2
+          allOf([lessThan(3), isNot(0)]), // 1
+          equals(0), // 0
+          predicate((int v) => v % 2 == 1), // 3
+          equals(5), // 5
+        ]));
+    shouldFail(
+        d,
+        unorderedMatches([greaterThan(0)]),
+        'Expected: matches [a value greater than <0>] unordered '
+        'Actual: [1, 2] '
+        'Which: has too many elements (2 > 1)');
+    shouldFail(
+        d,
+        unorderedMatches([3, 2, 1]),
+        'Expected: matches [<3>, <2>, <1>] unordered '
+        'Actual: [1, 2] '
+        'Which: has too few elements (2 < 3)');
+    shouldFail(
+        d,
+        unorderedMatches([3, 1]),
+        'Expected: matches [<3>, <1>] unordered '
+        'Actual: [1, 2] '
+        'Which: has no match for <3> at index 0');
+    shouldFail(
+        d,
+        unorderedMatches([greaterThan(3), greaterThan(0)]),
+        'Expected: matches [a value greater than <3>, a value greater than '
+        '<0>] unordered '
+        'Actual: [1, 2] '
+        'Which: has no match for a value greater than <3> at index 0');
+    shouldFail('not an iterable', unorderedMatches([greaterThan(1)]),
+        endsWith('not an <Instance of \'Iterable\'>'));
+  });
+
+  test('containsAll', () {
+    var d = [0, 1, 2];
+    shouldPass(d, containsAll([1, 2]));
+    shouldPass(d, containsAll([2, 1]));
+    shouldPass(d, containsAll([greaterThan(0), greaterThan(1)]));
+    shouldPass([2, 1], containsAll([greaterThan(0), greaterThan(1)]));
+    shouldFail(
+        d,
+        containsAll([1, 2, 3]),
+        'Expected: contains all of [1, 2, 3] '
+        'Actual: [0, 1, 2] '
+        'Which: has no match for <3> at index 2');
+    shouldFail(
+        1,
+        containsAll([1]),
+        'Expected: contains all of [1] '
+        'Actual: <1> '
+        "Which: not an <Instance of 'Iterable'>");
+    shouldFail(
+        [-1, 2],
+        containsAll([greaterThan(0), greaterThan(1)]),
+        'Expected: contains all of [<a value greater than <0>>, '
+        '<a value greater than <1>>] '
+        'Actual: [-1, 2] '
+        'Which: has no match for a value greater than <1> at index 1');
+    shouldFail('not an iterable', containsAll([1, 2, 3]),
+        endsWith('not an <Instance of \'Iterable\'>'));
+  });
+
+  test('containsAllInOrder', () {
+    var d = [0, 1, 0, 2];
+    shouldPass(d, containsAllInOrder([1, 2]));
+    shouldPass(d, containsAllInOrder([greaterThan(0), greaterThan(1)]));
+    shouldFail(
+        d,
+        containsAllInOrder([2, 1]),
+        'Expected: contains in order([2, 1]) '
+        'Actual: [0, 1, 0, 2] '
+        'Which: did not find a value matching <1> following expected prior '
+        'values');
+    shouldFail(
+        d,
+        containsAllInOrder([greaterThan(1), greaterThan(0)]),
+        'Expected: contains in order([<a value greater than <1>>, '
+        '<a value greater than <0>>]) '
+        'Actual: [0, 1, 0, 2] '
+        'Which: did not find a value matching a value greater than <0> '
+        'following expected prior values');
+    shouldFail(
+        d,
+        containsAllInOrder([1, 2, 3]),
+        'Expected: contains in order([1, 2, 3]) '
+        'Actual: [0, 1, 0, 2] '
+        'Which: did not find a value matching <3> following expected prior '
+        'values');
+    shouldFail(
+        1,
+        containsAllInOrder([1]),
+        'Expected: contains in order([1]) '
+        'Actual: <1> '
+        "Which: not an <Instance of 'Iterable'>");
+  });
+
+  test('containsOnce', () {
+    shouldPass([1, 2, 3, 4], containsOnce(2));
+    shouldPass([1, 2, 11, 3], containsOnce(greaterThan(10)));
+    shouldFail(
+        [1, 2, 3, 4],
+        containsOnce(10),
+        'Expected: contains once(<10>) '
+        'Actual: [1, 2, 3, 4] '
+        'Which: did not find a value matching <10>');
+    shouldFail(
+        [1, 2, 3, 4],
+        containsOnce(greaterThan(10)),
+        'Expected: contains once(a value greater than <10>) '
+        'Actual: [1, 2, 3, 4] '
+        'Which: did not find a value matching a value greater than <10>');
+    shouldFail(
+        [1, 2, 1, 2],
+        containsOnce(2),
+        'Expected: contains once(<2>) '
+        'Actual: [1, 2, 1, 2] '
+        'Which: expected only one value matching <2> '
+        'but found multiple: <2>, <2>');
+    shouldFail(
+        [1, 2, 10, 20],
+        containsOnce(greaterThan(5)),
+        'Expected: contains once(a value greater than <5>) '
+        'Actual: [1, 2, 10, 20] '
+        'Which: expected only one value matching a value greater than <5> '
+        'but found multiple: <10>, <20>');
+  });
+
+  test('pairwise compare', () {
+    var c = [1, 2];
+    var d = [1, 2, 3];
+    var e = [1, 4, 9];
+    shouldFail(
+        'x',
+        pairwiseCompare(e, (int e, int a) => a <= e, 'less than or equal'),
+        'Expected: pairwise less than or equal [1, 4, 9] '
+            "Actual: 'x' "
+            "Which: not an <Instance of 'Iterable'>");
+    shouldFail(
+        c,
+        pairwiseCompare(e, (int e, int a) => a <= e, 'less than or equal'),
+        'Expected: pairwise less than or equal [1, 4, 9] '
+        'Actual: [1, 2] '
+        'Which: has length 2 instead of 3');
+    shouldPass(
+        d, pairwiseCompare(e, (int e, int a) => a <= e, 'less than or equal'));
+    shouldFail(
+        d,
+        pairwiseCompare(e, (int e, int a) => a < e, 'less than'),
+        'Expected: pairwise less than [1, 4, 9] '
+        'Actual: [1, 2, 3] '
+        'Which: has <1> which is not less than <1> at index 0');
+    shouldPass(
+        d, pairwiseCompare(e, (int e, int a) => a * a == e, 'square root of'));
+    shouldFail(
+        d,
+        pairwiseCompare(e, (int e, int a) => a + a == e, 'double'),
+        'Expected: pairwise double [1, 4, 9] '
+        'Actual: [1, 2, 3] '
+        'Which: has <1> which is not double <1> at index 0');
+    shouldFail(
+        'not an iterable',
+        pairwiseCompare(e, (int e, int a) => a + a == e, 'double'),
+        endsWith('not an <Instance of \'Iterable\'>'));
+  });
+
+  test('isEmpty', () {
+    var d = SimpleIterable(0);
+    var e = SimpleIterable(1);
+    shouldPass(d, isEmpty);
+    shouldFail(
+        e,
+        isEmpty,
+        'Expected: empty '
+        'Actual: SimpleIterable:[1]');
+  });
+
+  test('isNotEmpty', () {
+    var d = SimpleIterable(0);
+    var e = SimpleIterable(1);
+    shouldPass(e, isNotEmpty);
+    shouldFail(
+        d,
+        isNotEmpty,
+        'Expected: non-empty '
+        'Actual: SimpleIterable:[]');
+  });
+
+  test('contains', () {
+    var d = SimpleIterable(3);
+    shouldPass(d, contains(2));
+    shouldFail(
+        d,
+        contains(5),
+        'Expected: contains <5> '
+        'Actual: SimpleIterable:[3, 2, 1] '
+        'Which: does not contain <5>');
+  });
+}
diff --git a/pkgs/matcher/test/map_matchers_test.dart b/pkgs/matcher/test/map_matchers_test.dart
new file mode 100644
index 0000000..4c699ab
--- /dev/null
+++ b/pkgs/matcher/test/map_matchers_test.dart
@@ -0,0 +1,100 @@
+import 'package:matcher/matcher.dart'
+    show contains, containsValue, containsPair;
+import 'package:test/test.dart' show test;
+
+import 'test_utils.dart';
+
+void main() {
+  test('contains', () {
+    shouldPass({'a': 1}, contains('a'));
+    shouldPass({null: 1}, contains(null));
+    shouldFail(
+      {'a': 1},
+      contains(2),
+      'Expected: contains <2> '
+      'Actual: {\'a\': 1} '
+      'Which: does not contain <2>',
+    );
+    shouldFail(
+      {'a': 1},
+      contains(null),
+      'Expected: contains <null> '
+      'Actual: {\'a\': 1} '
+      'Which: does not contain <null>',
+    );
+  });
+
+  test('containsValue', () {
+    shouldPass({'a': 1, 'null': null}, containsValue(1));
+    shouldPass({'a': 1, 'null': null}, containsValue(null));
+    shouldFail(
+      {'a': 1, 'null': null},
+      containsValue(2),
+      'Expected: contains value <2> '
+      "Actual: {'a': 1, 'null': null}",
+    );
+  });
+
+  test('containsPair', () {
+    shouldPass({'a': 1, 'null': null}, containsPair('a', 1));
+    shouldPass({'a': 1, 'null': null}, containsPair('null', null));
+    shouldPass({null: null}, containsPair(null, null));
+    shouldFail(
+      {'a': 1, 'null': null},
+      containsPair('a', 2),
+      "Expected: contains pair 'a' => <2> "
+      "Actual: {'a': 1, 'null': null} "
+      "Which:  contains key 'a' but with value is <1>",
+    );
+    shouldFail(
+      {'a': 1, 'null': null},
+      containsPair('b', 1),
+      "Expected: contains pair 'b' => <1> "
+      "Actual: {'a': 1, 'null': null} "
+      "Which:  doesn't contain key 'b'",
+    );
+    shouldFail(
+      {'a': 1, 'null': null},
+      containsPair('null', 2),
+      "Expected: contains pair 'null' => <2> "
+      "Actual: {'a': 1, 'null': null} "
+      "Which:  contains key 'null' but with value is <null>",
+    );
+    shouldFail(
+      {'a': 1, 'null': null},
+      containsPair('2', null),
+      "Expected: contains pair '2' => <null> "
+      "Actual: {'a': 1, 'null': null} "
+      "Which:  doesn't contain key '2'",
+    );
+    shouldFail(
+      {'a': 1, 'null': null},
+      containsPair('2', 'b'),
+      "Expected: contains pair '2' => 'b' "
+      "Actual: {'a': 1, 'null': null} "
+      "Which:  doesn't contain key '2'",
+    );
+    shouldFail(
+      {null: null},
+      containsPair('not null', null),
+      "Expected: contains pair 'not null' => <null> "
+      'Actual: {null: null} '
+      "Which:  doesn't contain key 'not null'",
+    );
+    shouldFail(
+      {null: null},
+      containsPair(null, 'not null'),
+      'Expected: contains pair <null> => \'not null\' '
+      'Actual: {null: null} '
+      'Which: contains key <null> but with value not an '
+      '<Instance of \'String\'>',
+    );
+    shouldFail(
+      {null: null},
+      containsPair('not null', 'not null'),
+      'Expected: contains pair \'not null\' => \'not null\' '
+      'Actual: {null: null} '
+      'Which:  doesn\'t contain key \'not null\' ',
+    );
+  });
+}
diff --git a/pkgs/matcher/test/matcher/completion_test.dart b/pkgs/matcher/test/matcher/completion_test.dart
new file mode 100644
index 0000000..9259cd1
--- /dev/null
+++ b/pkgs/matcher/test/matcher/completion_test.dart
@@ -0,0 +1,192 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:test_api/hooks_testing.dart';
+
+import '../utils_new.dart';
+
+void main() {
+  group('[doesNotComplete]', () {
+    test('fails when provided a non future', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        expect(10, doesNotComplete);
+      });
+
+      expectTestFailed(monitor, contains('10 is not a Future'));
+    });
+
+    test('succeeds when a future does not complete', () {
+      var completer = Completer();
+      expect(completer.future, doesNotComplete);
+    });
+
+    test('fails when a future does complete', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        var completer = Completer();
+        completer.complete(null);
+        expect(completer.future, doesNotComplete);
+      });
+
+      expectTestFailed(
+          monitor,
+          'Future was not expected to complete but completed with a value of'
+          ' null');
+    });
+
+    test('fails when a future completes after the expect', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        var completer = Completer();
+        expect(completer.future, doesNotComplete);
+        completer.complete(null);
+      });
+
+      expectTestFailed(
+          monitor,
+          'Future was not expected to complete but completed with a value of'
+          ' null');
+    });
+
+    test('fails when a future eventually completes', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        var completer = Completer();
+        expect(completer.future, doesNotComplete);
+        Future(() async {
+          await pumpEventQueue(times: 10);
+        }).then(completer.complete);
+      });
+
+      expectTestFailed(
+          monitor,
+          'Future was not expected to complete but completed with a value of'
+          ' null');
+    });
+  });
+  group('[completes]', () {
+    test('blocks the test until the Future completes', () async {
+      final completer = Completer<void>();
+      final monitor = TestCaseMonitor.start(() {
+        expect(completer.future, completes);
+      });
+      await pumpEventQueue();
+      expect(monitor.state, State.running);
+      completer.complete();
+      await monitor.onDone;
+      expectTestPassed(monitor);
+    });
+
+    test('with an error', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        expect(Future.error('X'), completes);
+      });
+
+      expect(monitor.state, equals(State.failed));
+      expect(monitor.errors, [isAsyncError(equals('X'))]);
+    });
+
+    test('with a failure', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        expect(Future.error(TestFailure('oh no')), completes);
+      });
+
+      expectTestFailed(monitor, 'oh no');
+    });
+
+    test('with a non-future', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        expect(10, completes);
+      });
+
+      expectTestFailed(
+          monitor,
+          'Expected: completes successfully\n'
+          '  Actual: <10>\n'
+          '   Which: was not a Future\n');
+    });
+
+    test('with a successful future', () {
+      expect(Future.value('1'), completes);
+    });
+  });
+
+  group('[completion]', () {
+    test('blocks the test until the Future completes', () async {
+      final completer = Completer<Object?>();
+      final monitor = TestCaseMonitor.start(() {
+        expect(completer.future, completion(isNull));
+      });
+      await pumpEventQueue();
+      expect(monitor.state, State.running);
+      completer.complete(null);
+      await monitor.onDone;
+      expectTestPassed(monitor);
+    });
+
+    test('with an error', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        expect(Future.error('X'), completion(isNull));
+      });
+
+      expect(monitor.state, equals(State.failed));
+      expect(monitor.errors, [isAsyncError(equals('X'))]);
+    });
+
+    test('with a failure', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        expect(Future.error(TestFailure('oh no')), completion(isNull));
+      });
+
+      expectTestFailed(monitor, 'oh no');
+    });
+
+    test('with a non-future', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        expect(10, completion(equals(10)));
+      });
+
+      expectTestFailed(
+          monitor,
+          'Expected: completes to a value that <10>\n'
+          '  Actual: <10>\n'
+          '   Which: was not a Future\n');
+    });
+
+    test('with an incorrect value', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        expect(Future.value('a'), completion(equals('b')));
+      });
+
+      expectTestFailed(
+          monitor,
+          allOf([
+            startsWith("Expected: completes to a value that 'b'\n"
+                '  Actual: <'),
+            endsWith('>\n'
+                "   Which: emitted 'a'\n"
+                '            which is different.\n'
+                '                  Expected: b\n'
+                '                    Actual: a\n'
+                '                            ^\n'
+                '                   Differ at offset 0\n')
+          ]));
+    });
+
+    test("blocks expectLater's Future", () async {
+      var completer = Completer();
+      var fired = false;
+      unawaited(expectLater(completer.future, completion(equals(1))).then((_) {
+        fired = true;
+      }));
+
+      await pumpEventQueue();
+      expect(fired, isFalse);
+
+      completer.complete(1);
+      await pumpEventQueue();
+      expect(fired, isTrue);
+    });
+  });
+}
diff --git a/pkgs/matcher/test/matcher/prints_test.dart b/pkgs/matcher/test/matcher/prints_test.dart
new file mode 100644
index 0000000..cbdb12a
--- /dev/null
+++ b/pkgs/matcher/test/matcher/prints_test.dart
@@ -0,0 +1,208 @@
+// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:test_api/hooks_testing.dart';
+
+import '../utils_new.dart';
+
+void main() {
+  group('synchronous', () {
+    test('passes with an expected print', () {
+      expect(() => print('Hello, world!'), prints('Hello, world!\n'));
+    });
+
+    test('combines multiple prints', () {
+      expect(() {
+        print('Hello');
+        print('World!');
+      }, prints('Hello\nWorld!\n'));
+    });
+
+    test('works with a Matcher', () {
+      expect(() => print('Hello, world!'), prints(contains('Hello')));
+    });
+
+    test('describes a failure nicely', () async {
+      void local() => print('Hello, world!');
+      var monitor = await TestCaseMonitor.run(() {
+        expect(local, prints('Goodbye, world!\n'));
+      });
+
+      expectTestFailed(
+          monitor,
+          allOf([
+            startsWith("Expected: prints 'Goodbye, world!\\n'\n"
+                "            ''\n"
+                '  Actual: <'),
+            endsWith('>\n'
+                "   Which: printed 'Hello, world!\\n'\n"
+                "                    ''\n"
+                '            which is different.\n'
+                '                  Expected: Goodbye, w ...\n'
+                '                    Actual: Hello, wor ...\n'
+                '                            ^\n'
+                '                   Differ at offset 0\n')
+          ]));
+    });
+
+    test('describes a failure with a non-descriptive Matcher nicely', () async {
+      void local() => print('Hello, world!');
+      var monitor = await TestCaseMonitor.run(() {
+        expect(local, prints(contains('Goodbye')));
+      });
+
+      expectTestFailed(
+          monitor,
+          allOf([
+            startsWith("Expected: prints contains 'Goodbye'\n"
+                '  Actual: <'),
+            endsWith('>\n'
+                "   Which: printed 'Hello, world!\\n'\n"
+                "                    ''\n"
+                '            which does not contain \'Goodbye\'\n')
+          ]));
+    });
+
+    test('describes a failure with no text nicely', () async {
+      void local() {}
+      var monitor = await TestCaseMonitor.run(() {
+        expect(local, prints(contains('Goodbye')));
+      });
+
+      expectTestFailed(
+          monitor,
+          allOf([
+            startsWith("Expected: prints contains 'Goodbye'\n"
+                '  Actual: <'),
+            endsWith('>\n'
+                '   Which: printed nothing\n'
+                '            which does not contain \'Goodbye\'\n')
+          ]));
+    });
+
+    test('with a non-function', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        expect(10, prints(contains('Goodbye')));
+      });
+
+      expectTestFailed(
+          monitor,
+          "Expected: prints contains 'Goodbye'\n"
+          '  Actual: <10>\n'
+          '   Which: was not a unary Function\n');
+    });
+  });
+
+  group('asynchronous', () {
+    test('passes with an expected print', () {
+      expect(() => Future(() => print('Hello, world!')),
+          prints('Hello, world!\n'));
+    });
+
+    test('combines multiple prints', () {
+      expect(
+          () => Future(() {
+                print('Hello');
+                print('World!');
+              }),
+          prints('Hello\nWorld!\n'));
+    });
+
+    test('works with a Matcher', () {
+      expect(() => Future(() => print('Hello, world!')),
+          prints(contains('Hello')));
+    });
+
+    test('describes a failure nicely', () async {
+      void local() => Future(() => print('Hello, world!'));
+      var monitor = await TestCaseMonitor.run(() {
+        expect(local, prints('Goodbye, world!\n'));
+      });
+
+      expectTestFailed(
+          monitor,
+          allOf([
+            startsWith("Expected: prints 'Goodbye, world!\\n'\n"
+                "            ''\n"
+                '  Actual: <'),
+            contains('>\n'
+                "   Which: printed 'Hello, world!\\n'\n"
+                "                    ''\n"
+                '            which is different.\n'
+                '                  Expected: Goodbye, w ...\n'
+                '                    Actual: Hello, wor ...\n'
+                '                            ^\n'
+                '                   Differ at offset 0')
+          ]));
+    });
+
+    test('describes a failure with a non-descriptive Matcher nicely', () async {
+      void local() => Future(() => print('Hello, world!'));
+      var monitor = await TestCaseMonitor.run(() {
+        expect(local, prints(contains('Goodbye')));
+      });
+
+      expectTestFailed(
+          monitor,
+          allOf([
+            startsWith("Expected: prints contains 'Goodbye'\n"
+                '  Actual: <'),
+            contains('>\n'
+                "   Which: printed 'Hello, world!\\n'\n"
+                "                    ''")
+          ]));
+    });
+
+    test('describes a failure with no text nicely', () async {
+      void local() => Future.value();
+      var monitor = await TestCaseMonitor.run(() {
+        expect(local, prints(contains('Goodbye')));
+      });
+
+      expectTestFailed(
+          monitor,
+          allOf([
+            startsWith("Expected: prints contains 'Goodbye'\n"
+                '  Actual: <'),
+            contains('>\n'
+                '   Which: printed nothing')
+          ]));
+    });
+
+    test("won't let the test end until the Future completes", () async {
+      final completer = Completer<void>();
+      final monitor = TestCaseMonitor.start(() {
+        expect(() => completer.future, prints(isEmpty));
+      });
+      await pumpEventQueue();
+      expect(monitor.state, State.running);
+      completer.complete();
+      await monitor.onDone;
+      expectTestPassed(monitor);
+    });
+
+    test("blocks expectLater's Future", () async {
+      var completer = Completer();
+      var fired = false;
+
+      unawaited(expectLater(() {
+        scheduleMicrotask(() => print('hello!'));
+        return completer.future;
+      }, prints('hello!\n'))
+          .then((_) {
+        fired = true;
+      }));
+
+      await pumpEventQueue();
+      expect(fired, isFalse);
+
+      completer.complete();
+      await pumpEventQueue();
+      expect(fired, isTrue);
+    });
+  });
+}
diff --git a/pkgs/matcher/test/matcher/throws_test.dart b/pkgs/matcher/test/matcher/throws_test.dart
new file mode 100644
index 0000000..25b93a9
--- /dev/null
+++ b/pkgs/matcher/test/matcher/throws_test.dart
@@ -0,0 +1,282 @@
+// 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.
+
+// ignore_for_file: only_throw_errors
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:test_api/hooks_testing.dart';
+
+import '../utils_new.dart';
+
+void main() {
+  group('synchronous', () {
+    group('[throws]', () {
+      test('with a function that throws an error', () {
+        // ignore: deprecated_member_use_from_same_package
+        expect(() => throw 'oh no', throws);
+      });
+
+      test("with a function that doesn't throw", () async {
+        void local() {}
+        var monitor = await TestCaseMonitor.run(() {
+          // ignore: deprecated_member_use_from_same_package
+          expect(local, throws);
+        });
+
+        expectTestFailed(
+            monitor,
+            allOf([
+              startsWith('Expected: throws\n'
+                  '  Actual: <'),
+              endsWith('>\n'
+                  '   Which: returned <null>\n')
+            ]));
+      });
+
+      test('with a non-function', () async {
+        var monitor = await TestCaseMonitor.run(() {
+          // ignore: deprecated_member_use_from_same_package
+          expect(10, throws);
+        });
+
+        expectTestFailed(
+            monitor,
+            'Expected: throws\n'
+            '  Actual: <10>\n'
+            '   Which: was not a Function or Future\n');
+      });
+    });
+
+    group('[throwsA]', () {
+      test('with a function that throws an identical error', () {
+        expect(() => throw 'oh no', throwsA('oh no'));
+      });
+
+      test('with a function that throws a matching error', () {
+        expect(() => throw const FormatException('bad'),
+            throwsA(isFormatException));
+      });
+
+      test("with a function that doesn't throw", () async {
+        void local() {}
+        var monitor = await TestCaseMonitor.run(() {
+          expect(local, throwsA('oh no'));
+        });
+
+        expectTestFailed(
+            monitor,
+            allOf([
+              startsWith("Expected: throws 'oh no'\n"
+                  '  Actual: <'),
+              endsWith('>\n'
+                  '   Which: returned <null>\n')
+            ]));
+      });
+
+      test('with a non-function', () async {
+        var monitor = await TestCaseMonitor.run(() {
+          expect(10, throwsA('oh no'));
+        });
+
+        expectTestFailed(
+            monitor,
+            "Expected: throws 'oh no'\n"
+            '  Actual: <10>\n'
+            '   Which: was not a Function or Future\n');
+      });
+
+      test('with a function that throws the wrong error', () async {
+        var monitor = await TestCaseMonitor.run(() {
+          expect(() => throw 'aw dang', throwsA('oh no'));
+        });
+
+        expectTestFailed(
+            monitor,
+            allOf([
+              startsWith("Expected: throws 'oh no'\n"
+                  '  Actual: <'),
+              contains('>\n'
+                  "   Which: threw 'aw dang'\n"
+                  '          stack'),
+              endsWith('          which is different.\n'
+                  '                Expected: oh no\n'
+                  '                  Actual: aw dang\n'
+                  '                          ^\n'
+                  '                 Differ at offset 0\n')
+            ]));
+      });
+    });
+  });
+
+  group('asynchronous', () {
+    group('[throws]', () {
+      test('with a Future that throws an error', () {
+        // ignore: deprecated_member_use_from_same_package
+        expect(Future.error('oh no'), throws);
+      });
+
+      test("with a Future that doesn't throw", () async {
+        var monitor = await TestCaseMonitor.run(() {
+          // ignore: deprecated_member_use_from_same_package
+          expect(Future.value(), throws);
+        });
+
+        expectTestFailed(
+            monitor,
+            allOf([
+              startsWith('Expected: throws\n'
+                  '  Actual: <'),
+              endsWith('>\n'
+                  '   Which: emitted <null>\n')
+            ]));
+      });
+
+      test('with a closure that returns a Future that throws an error', () {
+        // ignore: deprecated_member_use_from_same_package
+        expect(() => Future.error('oh no'), throws);
+      });
+
+      test("with a closure that returns a Future that doesn't throw", () async {
+        var monitor = await TestCaseMonitor.run(() {
+          // ignore: deprecated_member_use_from_same_package
+          expect(Future.value, throws);
+        });
+
+        expectTestFailed(
+            monitor,
+            allOf([
+              startsWith('Expected: throws\n'
+                  '  Actual: <'),
+              endsWith('>\n'
+                  '   Which: returned a Future that emitted <null>\n')
+            ]));
+      });
+
+      test("won't let the test end until the Future completes", () async {
+        late void Function() callback;
+        final monitor = TestCaseMonitor.start(() {
+          final completer = Completer<void>();
+          // ignore: deprecated_member_use_from_same_package
+          expect(completer.future, throws);
+          callback = () => completer.completeError('oh no');
+        });
+        await pumpEventQueue();
+        expect(monitor.state, State.running);
+        callback();
+        await monitor.onDone;
+        expectTestPassed(monitor);
+      });
+    });
+
+    group('[throwsA]', () {
+      test('with a Future that throws an identical error', () {
+        expect(Future.error('oh no'), throwsA('oh no'));
+      });
+
+      test('with a Future that throws a matching error', () {
+        expect(Future.error(const FormatException('bad')),
+            throwsA(isFormatException));
+      });
+
+      test("with a Future that doesn't throw", () async {
+        var monitor = await TestCaseMonitor.run(() {
+          expect(Future.value(), throwsA('oh no'));
+        });
+
+        expectTestFailed(
+            monitor,
+            allOf([
+              startsWith("Expected: throws 'oh no'\n"
+                  '  Actual: <'),
+              endsWith('>\n'
+                  '   Which: emitted <null>\n')
+            ]));
+      });
+
+      test('with a Future that throws the wrong error', () async {
+        var monitor = await TestCaseMonitor.run(() {
+          expect(Future.error('aw dang'), throwsA('oh no'));
+        });
+
+        expectTestFailed(
+            monitor,
+            allOf([
+              startsWith("Expected: throws 'oh no'\n"
+                  '  Actual: <'),
+              contains('>\n'
+                  "   Which: threw 'aw dang'\n")
+            ]));
+      });
+
+      test('with a closure that returns a Future that throws a matching error',
+          () {
+        expect(() => Future.error(const FormatException('bad')),
+            throwsA(isFormatException));
+      });
+
+      test("with a closure that returns a Future that doesn't throw", () async {
+        var monitor = await TestCaseMonitor.run(() {
+          expect(Future.value, throwsA('oh no'));
+        });
+
+        expectTestFailed(
+            monitor,
+            allOf([
+              startsWith("Expected: throws 'oh no'\n"
+                  '  Actual: <'),
+              endsWith('>\n'
+                  '   Which: returned a Future that emitted <null>\n')
+            ]));
+      });
+
+      test('with closure that returns a Future that throws the wrong error',
+          () async {
+        var monitor = await TestCaseMonitor.run(() {
+          expect(() => Future.error('aw dang'), throwsA('oh no'));
+        });
+
+        expectTestFailed(
+            monitor,
+            allOf([
+              startsWith("Expected: throws 'oh no'\n"
+                  '  Actual: <'),
+              contains('>\n'
+                  "   Which: threw 'aw dang'\n")
+            ]));
+      });
+
+      test("won't let the test end until the Future completes", () async {
+        late void Function() callback;
+        final monitor = TestCaseMonitor.start(() {
+          final completer = Completer<void>();
+          expect(completer.future, throwsA('oh no'));
+          callback = () => completer.completeError('oh no');
+        });
+        await pumpEventQueue();
+        expect(monitor.state, State.running);
+        callback();
+        await monitor.onDone;
+
+        expectTestPassed(monitor);
+      });
+
+      test("blocks expectLater's Future", () async {
+        var completer = Completer();
+        var fired = false;
+        unawaited(expectLater(completer.future, throwsArgumentError).then((_) {
+          fired = true;
+        }));
+
+        await pumpEventQueue();
+        expect(fired, isFalse);
+
+        completer.completeError(ArgumentError('oh no'));
+        await pumpEventQueue();
+        expect(fired, isTrue);
+      });
+    });
+  });
+}
diff --git a/pkgs/matcher/test/matcher/throws_type_test.dart b/pkgs/matcher/test/matcher/throws_type_test.dart
new file mode 100644
index 0000000..557a73a
--- /dev/null
+++ b/pkgs/matcher/test/matcher/throws_type_test.dart
@@ -0,0 +1,177 @@
+// 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.
+
+// ignore_for_file: only_throw_errors
+
+import 'package:test/test.dart';
+import 'package:test_api/hooks_testing.dart';
+
+import '../utils_new.dart';
+
+void main() {
+  group('[throwsArgumentError]', () {
+    test('passes when a ArgumentError is thrown', () {
+      expect(() => throw ArgumentError(''), throwsArgumentError);
+    });
+
+    test('fails when a non-ArgumentError is thrown', () async {
+      var liveTest = await TestCaseMonitor.run(() {
+        expect(() => throw Exception(), throwsArgumentError);
+      });
+
+      expectTestFailed(liveTest,
+          startsWith("Expected: throws <Instance of 'ArgumentError'>"));
+    });
+  });
+
+  group('[throwsConcurrentModificationError]', () {
+    test('passes when a ConcurrentModificationError is thrown', () {
+      expect(() => throw ConcurrentModificationError(''),
+          throwsConcurrentModificationError);
+    });
+
+    test('fails when a non-ConcurrentModificationError is thrown', () async {
+      var liveTest = await TestCaseMonitor.run(() {
+        expect(() => throw Exception(), throwsConcurrentModificationError);
+      });
+
+      expectTestFailed(
+          liveTest,
+          startsWith(
+              "Expected: throws <Instance of 'ConcurrentModificationError'>"));
+    });
+  });
+
+  group('[throwsCyclicInitializationError]', () {
+    test('passes when a CyclicInitializationError is thrown', () {
+      expect(
+          () => _CyclicInitializationFailure().x,
+          // ignore: deprecated_member_use_from_same_package
+          throwsCyclicInitializationError);
+    });
+
+    test('fails when a non-CyclicInitializationError is thrown', () async {
+      var liveTest = await TestCaseMonitor.run(() {
+        // ignore: deprecated_member_use_from_same_package
+        expect(() => throw Exception(), throwsCyclicInitializationError);
+      });
+
+      expectTestFailed(
+          liveTest, startsWith("Expected: throws <Instance of 'Error'>"));
+    });
+  });
+
+  group('[throwsException]', () {
+    test('passes when a Exception is thrown', () {
+      expect(() => throw Exception(''), throwsException);
+    });
+
+    test('fails when a non-Exception is thrown', () async {
+      var liveTest = await TestCaseMonitor.run(() {
+        expect(() => throw 'oh no', throwsException);
+      });
+
+      expectTestFailed(
+          liveTest, startsWith("Expected: throws <Instance of 'Exception'>"));
+    });
+  });
+
+  group('[throwsFormatException]', () {
+    test('passes when a FormatException is thrown', () {
+      expect(() => throw const FormatException(''), throwsFormatException);
+    });
+
+    test('fails when a non-FormatException is thrown', () async {
+      var liveTest = await TestCaseMonitor.run(() {
+        expect(() => throw Exception(), throwsFormatException);
+      });
+
+      expectTestFailed(liveTest,
+          startsWith("Expected: throws <Instance of 'FormatException'>"));
+    });
+  });
+
+  group('[throwsNoSuchMethodError]', () {
+    test('passes when a NoSuchMethodError is thrown', () {
+      expect(() {
+        // ignore: avoid_dynamic_calls
+        (1 as dynamic).notAMethodOnInt();
+      }, throwsNoSuchMethodError);
+    });
+
+    test('fails when a non-NoSuchMethodError is thrown', () async {
+      var liveTest = await TestCaseMonitor.run(() {
+        expect(() => throw Exception(), throwsNoSuchMethodError);
+      });
+
+      expectTestFailed(liveTest,
+          startsWith("Expected: throws <Instance of 'NoSuchMethodError'>"));
+    });
+  });
+
+  group('[throwsRangeError]', () {
+    test('passes when a RangeError is thrown', () {
+      expect(() => throw RangeError(''), throwsRangeError);
+    });
+
+    test('fails when a non-RangeError is thrown', () async {
+      var liveTest = await TestCaseMonitor.run(() {
+        expect(() => throw Exception(), throwsRangeError);
+      });
+
+      expectTestFailed(
+          liveTest, startsWith("Expected: throws <Instance of 'RangeError'>"));
+    });
+  });
+
+  group('[throwsStateError]', () {
+    test('passes when a StateError is thrown', () {
+      expect(() => throw StateError(''), throwsStateError);
+    });
+
+    test('fails when a non-StateError is thrown', () async {
+      var liveTest = await TestCaseMonitor.run(() {
+        expect(() => throw Exception(), throwsStateError);
+      });
+
+      expectTestFailed(
+          liveTest, startsWith("Expected: throws <Instance of 'StateError'>"));
+    });
+  });
+
+  group('[throwsUnimplementedError]', () {
+    test('passes when a UnimplementedError is thrown', () {
+      expect(() => throw UnimplementedError(''), throwsUnimplementedError);
+    });
+
+    test('fails when a non-UnimplementedError is thrown', () async {
+      var liveTest = await TestCaseMonitor.run(() {
+        expect(() => throw Exception(), throwsUnimplementedError);
+      });
+
+      expectTestFailed(liveTest,
+          startsWith("Expected: throws <Instance of 'UnimplementedError'>"));
+    });
+  });
+
+  group('[throwsUnsupportedError]', () {
+    test('passes when a UnsupportedError is thrown', () {
+      expect(() => throw UnsupportedError(''), throwsUnsupportedError);
+    });
+
+    test('fails when a non-UnsupportedError is thrown', () async {
+      var liveTest = await TestCaseMonitor.run(() {
+        expect(() => throw Exception(), throwsUnsupportedError);
+      });
+
+      expectTestFailed(liveTest,
+          startsWith("Expected: throws <Instance of 'UnsupportedError'>"));
+    });
+  });
+}
+
+class _CyclicInitializationFailure {
+  late int x = y;
+  late int y = x;
+}
diff --git a/pkgs/matcher/test/mirror_matchers_test.dart b/pkgs/matcher/test/mirror_matchers_test.dart
new file mode 100644
index 0000000..06f0df4
--- /dev/null
+++ b/pkgs/matcher/test/mirror_matchers_test.dart
@@ -0,0 +1,56 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ignore_for_file: deprecated_member_use_from_same_package
+
+@TestOn('vm')
+library;
+
+import 'package:matcher/mirror_matchers.dart';
+import 'package:test/test.dart';
+
+import 'test_utils.dart';
+
+class C {
+  int instanceField = 1;
+  int get instanceGetter => 2;
+  static int staticField = 3;
+  static int get staticGetter => 4;
+}
+
+void main() {
+  test('hasProperty', () {
+    var foo = [3];
+    shouldPass(foo, hasProperty('length', 1));
+    shouldFail(
+        foo,
+        hasProperty('foo'),
+        'Expected: has property "foo" '
+        'Actual: [3] '
+        'Which: has no property named "foo"');
+    shouldFail(
+        foo,
+        hasProperty('length', 2),
+        'Expected: has property "length" which matches <2> '
+        'Actual: [3] '
+        'Which: has property "length" with value <1>');
+    var c = C();
+    shouldPass(c, hasProperty('instanceField', 1));
+    shouldPass(c, hasProperty('instanceGetter', 2));
+    shouldFail(
+        c,
+        hasProperty('staticField'),
+        'Expected: has property "staticField" '
+        'Actual: <Instance of \'C\'> '
+        'Which: has a member named "staticField",'
+        ' but it is not an instance property');
+    shouldFail(
+        c,
+        hasProperty('staticGetter'),
+        'Expected: has property "staticGetter" '
+        'Actual: <Instance of \'C\'> '
+        'Which: has a member named "staticGetter",'
+        ' but it is not an instance property');
+  });
+}
diff --git a/pkgs/matcher/test/never_called_test.dart b/pkgs/matcher/test/never_called_test.dart
new file mode 100644
index 0000000..4c83e39
--- /dev/null
+++ b/pkgs/matcher/test/never_called_test.dart
@@ -0,0 +1,75 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:term_glyph/term_glyph.dart' as glyph;
+import 'package:test/test.dart';
+import 'package:test_api/hooks_testing.dart';
+
+import 'utils_new.dart';
+
+void main() {
+  setUpAll(() {
+    glyph.ascii = true;
+  });
+
+  test("doesn't throw if it isn't called", () async {
+    var monitor = await TestCaseMonitor.run(() {
+      const Stream.empty().listen(neverCalled);
+    });
+
+    expectTestPassed(monitor);
+  });
+
+  group("if it's called", () {
+    test('throws', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        neverCalled();
+      });
+
+      expectTestFailed(
+          monitor,
+          'Callback should never have been called, but it was called with no '
+          'arguments.');
+    });
+
+    test('pretty-prints arguments', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        neverCalled(1, 'foo\nbar');
+      });
+
+      expectTestFailed(
+          monitor,
+          'Callback should never have been called, but it was called with:\n'
+          '* <1>\n'
+          "* 'foo\\n'\n"
+          "    'bar'");
+    });
+
+    test('keeps the test alive', () async {
+      var monitor = await TestCaseMonitor.run(() {
+        pumpEventQueue(times: 10).then(neverCalled);
+      });
+
+      expectTestFailed(
+          monitor,
+          'Callback should never have been called, but it was called with:\n'
+          '* <null>');
+    });
+
+    test("can't be caught", () async {
+      var monitor = await TestCaseMonitor.run(() {
+        try {
+          neverCalled();
+        } catch (_) {
+          // Do nothing.
+        }
+      });
+
+      expectTestFailed(
+          monitor,
+          'Callback should never have been called, but it was called with '
+          'no arguments.');
+    });
+  });
+}
diff --git a/pkgs/matcher/test/numeric_matchers_test.dart b/pkgs/matcher/test/numeric_matchers_test.dart
new file mode 100644
index 0000000..3919588
--- /dev/null
+++ b/pkgs/matcher/test/numeric_matchers_test.dart
@@ -0,0 +1,98 @@
+// Copyright (c) 2012, 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 'test_utils.dart';
+
+void main() {
+  test('closeTo', () {
+    shouldPass(0, closeTo(0, 1));
+    shouldPass(-1, closeTo(0, 1));
+    shouldPass(1, closeTo(0, 1));
+    shouldFail(
+        1.001,
+        closeTo(0, 1),
+        'Expected: a numeric value within <1> of <0> '
+        'Actual: <1.001> '
+        'Which: differs by <1.001>');
+    shouldFail(
+        -1.001,
+        closeTo(0, 1),
+        'Expected: a numeric value within <1> of <0> '
+        'Actual: <-1.001> '
+        'Which: differs by <1.001>');
+    shouldFail(
+        'not a num', closeTo(0, 1), endsWith('not an <Instance of \'num\'>'));
+  });
+
+  test('inInclusiveRange', () {
+    shouldFail(
+        -1,
+        inInclusiveRange(0, 2),
+        'Expected: be in range from 0 (inclusive) to 2 (inclusive) '
+        'Actual: <-1>');
+    shouldPass(0, inInclusiveRange(0, 2));
+    shouldPass(1, inInclusiveRange(0, 2));
+    shouldPass(2, inInclusiveRange(0, 2));
+    shouldFail(
+        3,
+        inInclusiveRange(0, 2),
+        'Expected: be in range from 0 (inclusive) to 2 (inclusive) '
+        'Actual: <3>');
+    shouldFail('not a num', inInclusiveRange(0, 1),
+        endsWith('not an <Instance of \'num\'>'));
+  });
+
+  test('inExclusiveRange', () {
+    shouldFail(
+        0,
+        inExclusiveRange(0, 2),
+        'Expected: be in range from 0 (exclusive) to 2 (exclusive) '
+        'Actual: <0>');
+    shouldPass(1, inExclusiveRange(0, 2));
+    shouldFail(
+        2,
+        inExclusiveRange(0, 2),
+        'Expected: be in range from 0 (exclusive) to 2 (exclusive) '
+        'Actual: <2>');
+    shouldFail('not a num', inExclusiveRange(0, 1),
+        endsWith('not an <Instance of \'num\'>'));
+  });
+
+  test('inOpenClosedRange', () {
+    shouldFail(
+        0,
+        inOpenClosedRange(0, 2),
+        'Expected: be in range from 0 (exclusive) to 2 (inclusive) '
+        'Actual: <0>');
+    shouldPass(1, inOpenClosedRange(0, 2));
+    shouldPass(2, inOpenClosedRange(0, 2));
+    shouldFail('not a num', inOpenClosedRange(0, 1),
+        endsWith('not an <Instance of \'num\'>'));
+  });
+
+  test('inClosedOpenRange', () {
+    shouldPass(0, inClosedOpenRange(0, 2));
+    shouldPass(1, inClosedOpenRange(0, 2));
+    shouldFail(
+        2,
+        inClosedOpenRange(0, 2),
+        'Expected: be in range from 0 (inclusive) to 2 (exclusive) '
+        'Actual: <2>');
+    shouldFail('not a num', inClosedOpenRange(0, 1),
+        endsWith('not an <Instance of \'num\'>'));
+  });
+
+  group('NaN', () {
+    test('inInclusiveRange', () {
+      shouldFail(
+          double.nan,
+          inExclusiveRange(double.negativeInfinity, double.infinity),
+          'Expected: be in range from '
+          '-Infinity (exclusive) to Infinity (exclusive) '
+          'Actual: <NaN>');
+    });
+  });
+}
diff --git a/pkgs/matcher/test/operator_matchers_test.dart b/pkgs/matcher/test/operator_matchers_test.dart
new file mode 100644
index 0000000..f4b6d3a
--- /dev/null
+++ b/pkgs/matcher/test/operator_matchers_test.dart
@@ -0,0 +1,63 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:matcher/matcher.dart';
+import 'package:test/test.dart' show test, expect, throwsArgumentError;
+
+import 'test_utils.dart';
+
+void main() {
+  test('anyOf', () {
+    // with a list
+    shouldFail(
+        0, anyOf([equals(1), equals(2)]), 'Expected: (<1> or <2>) Actual: <0>');
+    shouldPass(1, anyOf([equals(1), equals(2)]));
+
+    // with individual items
+    shouldFail(
+        0, anyOf(equals(1), equals(2)), 'Expected: (<1> or <2>) Actual: <0>');
+    shouldPass(1, anyOf(equals(1), equals(2)));
+  });
+
+  test('allOf', () {
+    // with a list
+    shouldPass(1, allOf([lessThan(10), greaterThan(0)]));
+    shouldFail(
+        -1,
+        allOf([lessThan(10), greaterThan(0)]),
+        'Expected: (a value less than <10> and a value greater than <0>) '
+        'Actual: <-1> '
+        'Which: is not a value greater than <0>');
+
+    // with individual items
+    shouldPass(1, allOf(lessThan(10), greaterThan(0)));
+    shouldFail(
+        -1,
+        allOf(lessThan(10), greaterThan(0)),
+        'Expected: (a value less than <10> and a value greater than <0>) '
+        'Actual: <-1> '
+        'Which: is not a value greater than <0>');
+
+    // with maximum items
+    shouldPass(
+        1,
+        allOf(lessThan(10), lessThan(9), lessThan(8), lessThan(7), lessThan(6),
+            lessThan(5), lessThan(4)));
+    shouldFail(
+        4,
+        allOf(lessThan(10), lessThan(9), lessThan(8), lessThan(7), lessThan(6),
+            lessThan(5), lessThan(4)),
+        'Expected: (a value less than <10> and a value less than <9> and a '
+        'value less than <8> and a value less than <7> and a value less than '
+        '<6> and a value less than <5> and a value less than <4>) '
+        'Actual: <4> '
+        'Which: is not a value less than <4>');
+  });
+
+  test('If the first argument is a List, the rest must be null', () {
+    expect(() => allOf([], 5), throwsArgumentError);
+    expect(
+        () => anyOf([], null, null, null, null, null, 42), throwsArgumentError);
+  });
+}
diff --git a/pkgs/matcher/test/order_matchers_test.dart b/pkgs/matcher/test/order_matchers_test.dart
new file mode 100644
index 0000000..8a7c3df
--- /dev/null
+++ b/pkgs/matcher/test/order_matchers_test.dart
@@ -0,0 +1,149 @@
+// Copyright (c) 2012, 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 'test_utils.dart';
+
+void main() {
+  test('greaterThan', () {
+    shouldPass(10, greaterThan(9));
+    shouldFail(
+        9,
+        greaterThan(10),
+        'Expected: a value greater than <10> '
+        'Actual: <9> '
+        'Which: is not a value greater than <10>');
+  });
+
+  test('greaterThanOrEqualTo', () {
+    shouldPass(10, greaterThanOrEqualTo(10));
+    shouldFail(
+        9,
+        greaterThanOrEqualTo(10),
+        'Expected: a value greater than or equal to <10> '
+        'Actual: <9> '
+        'Which: is not a value greater than or equal to <10>');
+  });
+
+  test('lessThan', () {
+    shouldFail(
+        10,
+        lessThan(9),
+        'Expected: a value less than <9> '
+        'Actual: <10> '
+        'Which: is not a value less than <9>');
+    shouldPass(9, lessThan(10));
+  });
+
+  test('lessThanOrEqualTo', () {
+    shouldPass(10, lessThanOrEqualTo(10));
+    shouldFail(
+        11,
+        lessThanOrEqualTo(10),
+        'Expected: a value less than or equal to <10> '
+        'Actual: <11> '
+        'Which: is not a value less than or equal to <10>');
+  });
+
+  test('isZero', () {
+    shouldPass(0, isZero);
+    shouldFail(
+        1,
+        isZero,
+        'Expected: a value equal to <0> '
+        'Actual: <1> '
+        'Which: is not a value equal to <0>');
+  });
+
+  test('isNonZero', () {
+    shouldFail(
+        0,
+        isNonZero,
+        'Expected: a value not equal to <0> '
+        'Actual: <0> '
+        'Which: is not a value not equal to <0>');
+    shouldPass(1, isNonZero);
+  });
+
+  test('isPositive', () {
+    shouldFail(
+        -1,
+        isPositive,
+        'Expected: a positive value '
+        'Actual: <-1> '
+        'Which: is not a positive value');
+    shouldFail(
+        0,
+        isPositive,
+        'Expected: a positive value '
+        'Actual: <0> '
+        'Which: is not a positive value');
+    shouldPass(1, isPositive);
+  });
+
+  test('isNegative', () {
+    shouldPass(-1, isNegative);
+    shouldFail(
+        0,
+        isNegative,
+        'Expected: a negative value '
+        'Actual: <0> '
+        'Which: is not a negative value');
+  });
+
+  test('isNonPositive', () {
+    shouldPass(-1, isNonPositive);
+    shouldPass(0, isNonPositive);
+    shouldFail(
+        1,
+        isNonPositive,
+        'Expected: a non-positive value '
+        'Actual: <1> '
+        'Which: is not a non-positive value');
+  });
+
+  test('isNonNegative', () {
+    shouldPass(1, isNonNegative);
+    shouldPass(0, isNonNegative);
+    shouldFail(
+        -1,
+        isNonNegative,
+        'Expected: a non-negative value '
+        'Actual: <-1> '
+        'Which: is not a non-negative value');
+  });
+
+  group('NaN', () {
+    test('greaterThan', () {
+      shouldFail(
+          double.nan,
+          greaterThan(10),
+          'Expected: a value greater than <10> '
+          'Actual: <NaN> '
+          'Which: is not a value greater than <10>');
+      shouldFail(
+          10,
+          greaterThan(double.nan),
+          'Expected: a value greater than <NaN> '
+          'Actual: <10> '
+          'Which: is not a value greater than <NaN>');
+    });
+
+    test('lessThanOrEqualTo', () {
+      shouldFail(
+          double.nan,
+          lessThanOrEqualTo(10),
+          'Expected: a value less than or equal to <10> '
+          'Actual: <NaN> '
+          'Which: is not a value less than or equal to <10>');
+      shouldFail(
+          10,
+          lessThanOrEqualTo(double.nan),
+          'Expected: a value less than or equal to <NaN> '
+          'Actual: <10> '
+          'Which: is not a value less than or equal to <NaN>');
+    });
+  });
+}
diff --git a/pkgs/matcher/test/pretty_print_test.dart b/pkgs/matcher/test/pretty_print_test.dart
new file mode 100644
index 0000000..184704b
--- /dev/null
+++ b/pkgs/matcher/test/pretty_print_test.dart
@@ -0,0 +1,263 @@
+// Copyright (c) 2013, 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:collection';
+
+import 'package:matcher/matcher.dart';
+import 'package:matcher/src/pretty_print.dart';
+import 'package:test/test.dart' show group, test, expect;
+
+class DefaultToString {}
+
+class CustomToString {
+  @override
+  String toString() => 'string representation';
+}
+
+class _PrivateName {
+  @override
+  String toString() => 'string representation';
+}
+
+class _PrivateNameIterable extends IterableMixin {
+  @override
+  Iterator get iterator => [1, 2, 3].iterator;
+}
+
+void main() {
+  test('with primitive objects', () {
+    expect(prettyPrint(12), equals('<12>'));
+    expect(prettyPrint(12.13), equals('<12.13>'));
+    expect(prettyPrint(true), equals('<true>'));
+    expect(prettyPrint(null), equals('<null>'));
+    expect(prettyPrint(() => 12), matches(r'<Closure.*>'));
+  });
+
+  group('with a string', () {
+    test('containing simple characters', () {
+      expect(prettyPrint('foo'), equals("'foo'"));
+    });
+
+    test('containing newlines', () {
+      expect(
+          prettyPrint('foo\nbar\nbaz'),
+          equals("'foo\\n'\n"
+              "  'bar\\n'\n"
+              "  'baz'"));
+    });
+
+    test('containing escapable characters', () {
+      expect(prettyPrint("foo\rbar\tbaz'qux\v"),
+          equals(r"'foo\rbar\tbaz\'qux\v'"));
+    });
+  });
+
+  group('with an iterable', () {
+    test('containing primitive objects', () {
+      expect(prettyPrint([1, true, 'foo']), equals("[1, true, 'foo']"));
+    });
+
+    test('containing a multiline string', () {
+      expect(
+          prettyPrint(['foo', 'bar\nbaz\nbip', 'qux']),
+          equals('[\n'
+              "  'foo',\n"
+              "  'bar\\n'\n"
+              "    'baz\\n'\n"
+              "    'bip',\n"
+              "  'qux'\n"
+              ']'));
+    });
+
+    test('containing a matcher', () {
+      expect(prettyPrint(['foo', endsWith('qux')]),
+          equals("['foo', <a string ending with 'qux'>]"));
+    });
+
+    test("that's under maxLineLength", () {
+      expect(prettyPrint([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxLineLength: 30),
+          equals('[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]'));
+    });
+
+    test("that's over maxLineLength", () {
+      expect(
+          prettyPrint([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxLineLength: 29),
+          equals('[\n'
+              '  0,\n'
+              '  1,\n'
+              '  2,\n'
+              '  3,\n'
+              '  4,\n'
+              '  5,\n'
+              '  6,\n'
+              '  7,\n'
+              '  8,\n'
+              '  9\n'
+              ']'));
+    });
+
+    test('factors indentation into maxLineLength', () {
+      expect(
+          prettyPrint([
+            'foo\nbar',
+            [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
+          ], maxLineLength: 30),
+          equals('[\n'
+              "  'foo\\n'\n"
+              "    'bar',\n"
+              '  [\n'
+              '    0,\n'
+              '    1,\n'
+              '    2,\n'
+              '    3,\n'
+              '    4,\n'
+              '    5,\n'
+              '    6,\n'
+              '    7,\n'
+              '    8,\n'
+              '    9\n'
+              '  ]\n'
+              ']'));
+    });
+
+    test("that's under maxItems", () {
+      expect(prettyPrint([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxItems: 10),
+          equals('[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]'));
+    });
+
+    test("that's over maxItems", () {
+      expect(prettyPrint([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxItems: 9),
+          equals('[0, 1, 2, 3, 4, 5, 6, 7, ...]'));
+    });
+
+    test("that's recursive", () {
+      var list = <dynamic>[1, 2, 3];
+      list.add(list);
+      expect(prettyPrint(list), equals('[1, 2, 3, (recursive)]'));
+    });
+  });
+
+  group('with a map', () {
+    test('containing primitive objects', () {
+      expect(prettyPrint({'foo': 1, 'bar': true}),
+          equals("{'foo': 1, 'bar': true}"));
+    });
+
+    test('containing a multiline string key', () {
+      expect(
+          prettyPrint({'foo\nbar': 1, 'bar': true}),
+          equals('{\n'
+              "  'foo\\n'\n"
+              "    'bar': 1,\n"
+              "  'bar': true\n"
+              '}'));
+    });
+
+    test('containing a multiline string value', () {
+      expect(
+          prettyPrint({'foo': 'bar\nbaz', 'qux': true}),
+          equals('{\n'
+              "  'foo': 'bar\\n'\n"
+              "    'baz',\n"
+              "  'qux': true\n"
+              '}'));
+    });
+
+    test('containing a multiline string key/value pair', () {
+      expect(
+          prettyPrint({'foo\nbar': 'baz\nqux'}),
+          equals('{\n'
+              "  'foo\\n'\n"
+              "    'bar': 'baz\\n'\n"
+              "    'qux'\n"
+              '}'));
+    });
+
+    test('containing a matcher key', () {
+      expect(prettyPrint({endsWith('bar'): 'qux'}),
+          equals("{<a string ending with 'bar'>: 'qux'}"));
+    });
+
+    test('containing a matcher value', () {
+      expect(prettyPrint({'foo': endsWith('qux')}),
+          equals("{'foo': <a string ending with 'qux'>}"));
+    });
+
+    test("that's under maxLineLength", () {
+      expect(prettyPrint({'0': 1, '2': 3, '4': 5, '6': 7}, maxLineLength: 32),
+          equals("{'0': 1, '2': 3, '4': 5, '6': 7}"));
+    });
+
+    test("that's over maxLineLength", () {
+      expect(
+          prettyPrint({'0': 1, '2': 3, '4': 5, '6': 7}, maxLineLength: 31),
+          equals('{\n'
+              "  '0': 1,\n"
+              "  '2': 3,\n"
+              "  '4': 5,\n"
+              "  '6': 7\n"
+              '}'));
+    });
+
+    test('factors indentation into maxLineLength', () {
+      expect(
+          prettyPrint([
+            'foo\nbar',
+            {'0': 1, '2': 3, '4': 5, '6': 7}
+          ], maxLineLength: 32),
+          equals('[\n'
+              "  'foo\\n'\n"
+              "    'bar',\n"
+              '  {\n'
+              "    '0': 1,\n"
+              "    '2': 3,\n"
+              "    '4': 5,\n"
+              "    '6': 7\n"
+              '  }\n'
+              ']'));
+    });
+
+    test("that's under maxItems", () {
+      expect(prettyPrint({'0': 1, '2': 3, '4': 5, '6': 7}, maxItems: 4),
+          equals("{'0': 1, '2': 3, '4': 5, '6': 7}"));
+    });
+
+    test("that's over maxItems", () {
+      expect(prettyPrint({'0': 1, '2': 3, '4': 5, '6': 7}, maxItems: 3),
+          equals("{'0': 1, '2': 3, ...}"));
+    });
+  });
+  group('with an object', () {
+    test('with a default [toString]', () {
+      expect(prettyPrint(DefaultToString()),
+          equals("<Instance of 'DefaultToString'>"));
+    });
+
+    test('with a custom [toString]', () {
+      expect(prettyPrint(CustomToString()),
+          equals('CustomToString:<string representation>'));
+    });
+
+    test('with a custom [toString] and a private name', () {
+      expect(prettyPrint(_PrivateName()),
+          equals('_PrivateName:<string representation>'));
+    });
+  });
+
+  group('with an iterable', () {
+    test("that's not a list", () {
+      expect(prettyPrint([1, 2, 3, 4].map((n) => n * 2)),
+          equals('MappedListIterable<int, int>:[2, 4, 6, 8]'));
+    });
+
+    test("that's not a list and has a private name", () {
+      expect(prettyPrint(_PrivateNameIterable()),
+          equals('_PrivateNameIterable:[1, 2, 3]'));
+    });
+  });
+
+  test('Type', () {
+    expect(prettyPrint(''.runtimeType), 'Type:<String>');
+  });
+}
diff --git a/pkgs/matcher/test/stream_matcher_test.dart b/pkgs/matcher/test/stream_matcher_test.dart
new file mode 100644
index 0000000..c4af666
--- /dev/null
+++ b/pkgs/matcher/test/stream_matcher_test.dart
@@ -0,0 +1,358 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+import 'package:term_glyph/term_glyph.dart' as glyph;
+import 'package:test/test.dart';
+
+import 'utils_new.dart';
+
+void main() {
+  setUpAll(() {
+    glyph.ascii = true;
+  });
+
+  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]));
+    errorStream = Stream.fromFuture(Future.error('oh no!', StackTrace.current));
+    errorQueue = StreamQueue(
+        Stream.fromFuture(Future.error('oh no!', StackTrace.current)));
+  });
+
+  group('emits()', () {
+    test('matches the first event of a Stream', () {
+      expect(stream, emits(1));
+    });
+
+    test('rejects the first event of a Stream', () {
+      expect(
+          expectLater(stream, emits(2)),
+          throwsTestFailure(allOf([
+            startsWith('Expected: should emit an event that <2>\n'),
+            endsWith('   Which: emitted * 1\n'
+                '                  * 2\n'
+                '                  * 3\n'
+                '                  * 4\n'
+                '                  * 5\n'
+                '                  x Stream closed.\n')
+          ])));
+    });
+
+    test('matches and consumes the next event of a StreamQueue', () {
+      expect(queue, emits(1));
+      expect(queue.next, completion(equals(2)));
+      expect(queue, emits(3));
+      expect(queue.next, completion(equals(4)));
+    });
+
+    test('rejects and does not consume the first event of a StreamQueue', () {
+      expect(
+          expectLater(queue, emits(2)),
+          throwsTestFailure(allOf([
+            startsWith('Expected: should emit an event that <2>\n'),
+            endsWith('   Which: emitted * 1\n'
+                '                  * 2\n'
+                '                  * 3\n'
+                '                  * 4\n'
+                '                  * 5\n'
+                '                  x Stream closed.\n')
+          ])));
+
+      expect(queue, emits(1));
+    });
+
+    test('rejects an empty stream', () {
+      expect(
+          expectLater(const Stream.empty(), emits(1)),
+          throwsTestFailure(allOf([
+            startsWith('Expected: should emit an event that <1>\n'),
+            endsWith('   Which: emitted x Stream closed.\n')
+          ])));
+    });
+
+    test('forwards a stream error', () {
+      expect(expectLater(errorStream, emits(1)), throwsA('oh no!'));
+    });
+
+    test('wraps a normal matcher', () {
+      expect(queue, emits(lessThan(5)));
+      expect(expectLater(queue, emits(greaterThan(5))),
+          throwsTestFailure(anything));
+    });
+
+    test('returns a StreamMatcher as-is', () {
+      expect(queue, emits(emitsThrough(4)));
+      expect(queue, emits(5));
+    });
+  });
+
+  group('emitsDone', () {
+    test('succeeds for an empty stream', () {
+      expect(const Stream.empty(), emitsDone);
+    });
+
+    test('fails for a stream with events', () {
+      expect(
+          expectLater(stream, emitsDone),
+          throwsTestFailure(allOf([
+            startsWith('Expected: should be done\n'),
+            endsWith('   Which: emitted * 1\n'
+                '                  * 2\n'
+                '                  * 3\n'
+                '                  * 4\n'
+                '                  * 5\n'
+                '                  x Stream closed.\n')
+          ])));
+    });
+  });
+
+  group('emitsError()', () {
+    test('consumes a matching error', () {
+      expect(errorQueue, emitsError('oh no!'));
+      expect(errorQueue.hasNext, completion(isFalse));
+    });
+
+    test('fails for a non-matching error', () {
+      expect(
+          expectLater(errorStream, emitsError('oh heck')),
+          throwsTestFailure(allOf([
+            startsWith("Expected: should emit an error that 'oh heck'\n"),
+            contains('   Which: emitted ! oh no!\n'),
+            contains('                  x Stream closed.\n'
+                "            which threw 'oh no!'\n"
+                '                  stack '),
+            endsWith('                  which is different.\n'
+                '                        Expected: oh heck\n'
+                '                          Actual: oh no!\n'
+                '                                     ^\n'
+                '                         Differ at offset 3\n')
+          ])));
+    });
+
+    test('fails for a stream with events', () {
+      expect(
+          expectLater(stream, emitsDone),
+          throwsTestFailure(allOf([
+            startsWith('Expected: should be done\n'),
+            endsWith('   Which: emitted * 1\n'
+                '                  * 2\n'
+                '                  * 3\n'
+                '                  * 4\n'
+                '                  * 5\n'
+                '                  x Stream closed.\n')
+          ])));
+    });
+  });
+
+  group('mayEmit()', () {
+    test('consumes a matching event', () {
+      expect(queue, mayEmit(1));
+      expect(queue, emits(2));
+    });
+
+    test('allows a non-matching event', () {
+      expect(queue, mayEmit('fish'));
+      expect(queue, emits(1));
+    });
+  });
+
+  group('emitsAnyOf()', () {
+    test('consumes an event that matches a matcher', () {
+      expect(queue, emitsAnyOf([2, 1, 3]));
+      expect(queue, emits(2));
+    });
+
+    test('consumes as many events as possible', () {
+      expect(
+          queue,
+          emitsAnyOf([
+            1,
+            emitsInOrder([1, 2]),
+            emitsInOrder([1, 2, 3])
+          ]));
+
+      expect(queue, emits(4));
+    });
+
+    test('fails if no matchers match', () {
+      expect(
+          expectLater(stream, emitsAnyOf([2, 3, 4])),
+          throwsTestFailure(allOf([
+            startsWith('Expected: should do one of the following:\n'
+                '          * emit an event that <2>\n'
+                '          * emit an event that <3>\n'
+                '          * emit an event that <4>\n'),
+            endsWith('   Which: emitted * 1\n'
+                '                  * 2\n'
+                '                  * 3\n'
+                '                  * 4\n'
+                '                  * 5\n'
+                '                  x Stream closed.\n'
+                '            which failed all options:\n'
+                '                  * failed to emit an event that <2>\n'
+                '                  * failed to emit an event that <3>\n'
+                '                  * failed to emit an event that <4>\n')
+          ])));
+    });
+
+    test('allows an error if any matcher matches', () {
+      expect(errorStream, emitsAnyOf([1, 2, emitsError('oh no!')]));
+    });
+
+    test('rethrows an error if no matcher matches', () {
+      expect(
+          expectLater(errorStream, emitsAnyOf([1, 2, 3])), throwsA('oh no!'));
+    });
+  });
+
+  group('emitsInOrder()', () {
+    test('consumes matching events', () {
+      expect(queue, emitsInOrder([1, 2, emitsThrough(4)]));
+      expect(queue, emits(5));
+    });
+
+    test("fails if the matchers don't match in order", () {
+      expect(
+          expectLater(queue, emitsInOrder([1, 3, 2])),
+          throwsTestFailure(allOf([
+            startsWith('Expected: should do the following in order:\n'
+                '          * emit an event that <1>\n'
+                '          * emit an event that <3>\n'
+                '          * emit an event that <2>\n'),
+            endsWith('   Which: emitted * 1\n'
+                '                  * 2\n'
+                '                  * 3\n'
+                '                  * 4\n'
+                '                  * 5\n'
+                '                  x Stream closed.\n'
+                "            which didn't emit an event that <3>\n")
+          ])));
+    });
+  });
+
+  group('emitsThrough()', () {
+    test('consumes events including those matching the matcher', () {
+      expect(queue, emitsThrough(emitsInOrder([3, 4])));
+      expect(queue, emits(5));
+    });
+
+    test('consumes the entire queue with emitsDone', () {
+      expect(queue, emitsThrough(emitsDone));
+      expect(queue.hasNext, completion(isFalse));
+    });
+
+    test('fails if the queue never matches the matcher', () {
+      expect(
+          expectLater(queue, emitsThrough(6)),
+          throwsTestFailure(allOf([
+            startsWith('Expected: should eventually emit an event that <6>\n'),
+            endsWith('   Which: emitted * 1\n'
+                '                  * 2\n'
+                '                  * 3\n'
+                '                  * 4\n'
+                '                  * 5\n'
+                '                  x Stream closed.\n'
+                '            which never did emit an event that <6>\n')
+          ])));
+    });
+  });
+
+  group('mayEmitMultiple()', () {
+    test('consumes multiple instances of the given matcher', () {
+      expect(queue, mayEmitMultiple(lessThan(3)));
+      expect(queue, emits(3));
+    });
+
+    test('consumes zero instances of the given matcher', () {
+      expect(queue, mayEmitMultiple(6));
+      expect(queue, emits(1));
+    });
+
+    test("doesn't rethrow errors", () {
+      expect(errorQueue, mayEmitMultiple(1));
+      expect(errorQueue, emitsError('oh no!'));
+    });
+  });
+
+  group('neverEmits()', () {
+    test('succeeds if the event never matches', () {
+      expect(queue, neverEmits(6));
+      expect(queue, emits(1));
+    });
+
+    test('fails if the event matches', () {
+      expect(
+          expectLater(stream, neverEmits(4)),
+          throwsTestFailure(allOf([
+            startsWith('Expected: should never emit an event that <4>\n'),
+            endsWith('   Which: emitted * 1\n'
+                '                  * 2\n'
+                '                  * 3\n'
+                '                  * 4\n'
+                '                  * 5\n'
+                '                  x Stream closed.\n'
+                '            which after 3 events did emit an event that <4>\n')
+          ])));
+    });
+
+    test('fails if emitsDone matches', () {
+      expect(expectLater(stream, neverEmits(emitsDone)),
+          throwsTestFailure(anything));
+    });
+
+    test("doesn't rethrow errors", () {
+      expect(errorQueue, neverEmits(6));
+      expect(errorQueue, emitsError('oh no!'));
+    });
+  });
+
+  group('emitsInAnyOrder()', () {
+    test('consumes events that match in any order', () {
+      expect(queue, emitsInAnyOrder([3, 1, 2]));
+      expect(queue, emits(4));
+    });
+
+    test("fails if the events don't match in any order", () {
+      expect(
+          expectLater(stream, emitsInAnyOrder([4, 1, 2])),
+          throwsTestFailure(allOf([
+            startsWith('Expected: should do the following in any order:\n'
+                '          * emit an event that <4>\n'
+                '          * emit an event that <1>\n'
+                '          * emit an event that <2>\n'),
+            endsWith('   Which: emitted * 1\n'
+                '                  * 2\n'
+                '                  * 3\n'
+                '                  * 4\n'
+                '                  * 5\n'
+                '                  x Stream closed.\n')
+          ])));
+    });
+
+    test("doesn't rethrow if some ordering matches", () {
+      expect(errorQueue, emitsInAnyOrder([emitsDone, emitsError('oh no!')]));
+    });
+
+    test('rethrows if no ordering matches', () {
+      expect(
+          expectLater(errorQueue, emitsInAnyOrder([1, emitsError('oh no!')])),
+          throwsA('oh no!'));
+    });
+  });
+
+  test('A custom StreamController doesn\'t hang on close', () async {
+    var controller = StreamController<void>();
+    var done = expectLater(controller.stream, emits(null));
+    controller.add(null);
+    await done;
+    await controller.close();
+  });
+}
diff --git a/pkgs/matcher/test/string_matchers_test.dart b/pkgs/matcher/test/string_matchers_test.dart
new file mode 100644
index 0000000..be9e768
--- /dev/null
+++ b/pkgs/matcher/test/string_matchers_test.dart
@@ -0,0 +1,151 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:matcher/matcher.dart';
+import 'package:test/test.dart' show test, expect;
+
+import 'test_utils.dart';
+
+void main() {
+  test('Reports mismatches in whitespace and escape sequences', () {
+    shouldFail('before\nafter', equals('before\\nafter'),
+        contains('Differ at offset 7'));
+  });
+
+  test('Retains outer matcher mismatch text', () {
+    shouldFail(
+        {'word': 'thing'},
+        containsPair('word', equals('notthing')),
+        allOf([
+          contains("contains key 'word' but with value is different"),
+          contains('Differ at offset 0')
+        ]));
+  });
+
+  test('collapseWhitespace', () {
+    var source = '\t\r\n hello\t\r\n world\r\t \n';
+    expect(collapseWhitespace(source), 'hello world');
+  });
+
+  test('isEmpty', () {
+    shouldPass('', isEmpty);
+    shouldFail(null, isEmpty, startsWith('Expected: empty  Actual: <null>'));
+    shouldFail(0, isEmpty, startsWith('Expected: empty  Actual: <0>'));
+    shouldFail('a', isEmpty, startsWith("Expected: empty  Actual: 'a'"));
+  });
+
+  // Regression test for: https://code.google.com/p/dart/issues/detail?id=21562
+  test('isNot(isEmpty)', () {
+    shouldPass('a', isNot(isEmpty));
+    shouldFail('', isNot(isEmpty), 'Expected: not empty Actual: \'\'');
+    shouldFail(null, isNot(isEmpty),
+        startsWith('Expected: not empty  Actual: <null>'));
+  });
+
+  test('isNotEmpty', () {
+    shouldFail('', isNotEmpty, startsWith("Expected: non-empty  Actual: ''"));
+    shouldFail(
+        null, isNotEmpty, startsWith('Expected: non-empty  Actual: <null>'));
+    shouldFail(0, isNotEmpty, startsWith('Expected: non-empty  Actual: <0>'));
+    shouldPass('a', isNotEmpty);
+  });
+
+  test('equalsIgnoringCase', () {
+    shouldPass('hello', equalsIgnoringCase('HELLO'));
+    shouldFail('hi', equalsIgnoringCase('HELLO'),
+        "Expected: 'HELLO' ignoring case Actual: 'hi'");
+    shouldFail(42, equalsIgnoringCase('HELLO'),
+        endsWith('not an <Instance of \'String\'>'));
+  });
+
+  test('equalsIgnoringWhitespace', () {
+    shouldPass(' hello   world  ', equalsIgnoringWhitespace('hello world'));
+    shouldFail(
+        ' helloworld  ',
+        equalsIgnoringWhitespace('hello world'),
+        "Expected: 'hello world' ignoring whitespace "
+            "Actual: ' helloworld ' "
+            "Which: is 'helloworld' with whitespace compressed");
+    shouldFail(42, equalsIgnoringWhitespace('HELLO'),
+        endsWith('not an <Instance of \'String\'>'));
+  });
+
+  test('startsWith', () {
+    shouldPass('hello', startsWith(''));
+    shouldPass('hello', startsWith('hell'));
+    shouldPass('hello', startsWith('hello'));
+    shouldFail(
+        'hello',
+        startsWith('hello '),
+        "Expected: a string starting with 'hello ' "
+            "Actual: 'hello'");
+    shouldFail(
+        42, startsWith('hello '), endsWith('not an <Instance of \'String\'>'));
+  });
+
+  test('endsWith', () {
+    shouldPass('hello', endsWith(''));
+    shouldPass('hello', endsWith('lo'));
+    shouldPass('hello', endsWith('hello'));
+    shouldFail(
+        'hello',
+        endsWith(' hello'),
+        "Expected: a string ending with ' hello' "
+            "Actual: 'hello'");
+    shouldFail(
+        42, startsWith('hello '), endsWith('not an <Instance of \'String\'>'));
+  });
+
+  test('contains', () {
+    shouldPass('hello', contains(''));
+    shouldPass('hello', contains('h'));
+    shouldPass('hello', contains('o'));
+    shouldPass('hello', contains('hell'));
+    shouldPass('hello', contains('hello'));
+    shouldFail('hello', contains(' '),
+        "Expected: contains ' ' Actual: 'hello' Which: does not contain ' '");
+  });
+
+  test('stringContainsInOrder', () {
+    shouldPass('goodbye cruel world', stringContainsInOrder(['']));
+    shouldPass('goodbye cruel world', stringContainsInOrder(['goodbye']));
+    shouldPass('goodbye cruel world', stringContainsInOrder(['cruel']));
+    shouldPass('goodbye cruel world', stringContainsInOrder(['world']));
+    shouldPass(
+        'goodbye cruel world', stringContainsInOrder(['good', 'bye', 'world']));
+    shouldPass(
+        'goodbye cruel world', stringContainsInOrder(['goodbye', 'cruel']));
+    shouldPass(
+        'goodbye cruel world', stringContainsInOrder(['cruel', 'world']));
+    shouldPass('goodbye cruel world',
+        stringContainsInOrder(['goodbye', 'cruel', 'world']));
+    shouldPass(
+        'foo', stringContainsInOrder(['f', '', '', '', 'o', '', '', 'o']));
+
+    shouldFail(
+        'abc',
+        stringContainsInOrder(['ab', 'bc']),
+        "Expected: a string containing 'ab', 'bc' in order "
+            "Actual: 'abc'");
+    shouldFail(
+        'hello',
+        stringContainsInOrder(['hello', 'hello']),
+        "Expected: a string containing 'hello', 'hello' in order "
+            "Actual: 'hello'");
+    shouldFail(
+        'goodbye cruel world',
+        stringContainsInOrder(['goo', 'cruel', 'bye']),
+        "Expected: a string containing 'goo', 'cruel', 'bye' in order "
+            "Actual: 'goodbye cruel world'");
+  });
+
+  test('matches', () {
+    shouldPass('c0d', matches('[a-z][0-9][a-z]'));
+    shouldPass('c0d', matches(RegExp('[a-z][0-9][a-z]')));
+    shouldFail('cOd', matches('[a-z][0-9][a-z]'),
+        "Expected: match '[a-z][0-9][a-z]' Actual: 'cOd'");
+    shouldFail(42, matches('[a-z][0-9][a-z]'),
+        endsWith('not an <Instance of \'String\'>'));
+  });
+}
diff --git a/pkgs/matcher/test/test_utils.dart b/pkgs/matcher/test/test_utils.dart
new file mode 100644
index 0000000..67b61a1
--- /dev/null
+++ b/pkgs/matcher/test/test_utils.dart
@@ -0,0 +1,67 @@
+// Copyright (c) 2012, 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';
+
+void shouldFail(Object? value, Matcher matcher, Object? expected) {
+  var failed = false;
+  try {
+    expect(value, matcher);
+  } on TestFailure catch (err) {
+    failed = true;
+
+    var errorString = err.message;
+
+    if (expected is String) {
+      expect(errorString, equalsIgnoringWhitespace(expected));
+    } else {
+      expect(errorString?.replaceAll('\n', ''), expected);
+    }
+  }
+
+  expect(failed, isTrue, reason: 'Expected to fail.');
+}
+
+void shouldPass(Object? value, Matcher matcher) {
+  expect(value, matcher);
+}
+
+void doesNotThrow() {}
+void doesThrow() {
+  throw StateError('X');
+}
+
+class Widget {
+  int? price;
+}
+
+class SimpleIterable extends Iterable<int> {
+  final int count;
+
+  SimpleIterable(this.count);
+
+  @override
+  Iterator<int> get iterator => _SimpleIterator(count);
+}
+
+class _SimpleIterator implements Iterator<int> {
+  int _count;
+  int _current;
+
+  _SimpleIterator(this._count) : _current = -1;
+
+  @override
+  bool moveNext() {
+    if (_count > 0) {
+      _current = _count;
+      _count--;
+      return true;
+    }
+    _current = -1;
+    return false;
+  }
+
+  @override
+  int get current => _current;
+}
diff --git a/pkgs/matcher/test/type_matcher_test.dart b/pkgs/matcher/test/type_matcher_test.dart
new file mode 100644
index 0000000..99d4459
--- /dev/null
+++ b/pkgs/matcher/test/type_matcher_test.dart
@@ -0,0 +1,66 @@
+// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ignore_for_file: deprecated_member_use_from_same_package
+import 'package:matcher/matcher.dart';
+import 'package:test/test.dart' show test, group;
+
+import 'test_utils.dart';
+
+void main() {
+  _test(isMap, {}, name: 'Map');
+  _test(isList, [], name: 'List');
+  _test(isArgumentError, ArgumentError());
+  _test(isCastError, TypeError());
+  _test<Exception>(isException, const FormatException());
+  _test(isFormatException, const FormatException());
+  _test(isStateError, StateError('oops'));
+  _test(isRangeError, RangeError('oops'));
+  _test(isUnimplementedError, UnimplementedError('oops'));
+  _test(isUnsupportedError, UnsupportedError('oops'));
+  _test(isConcurrentModificationError, ConcurrentModificationError());
+  _test(isCyclicInitializationError, Error());
+  _test<NoSuchMethodError?>(isNoSuchMethodError, null,
+      name: 'NoSuchMethodError');
+  _test(isNullThrownError, TypeError());
+
+  group('custom `TypeMatcher`', () {
+    _test(const isInstanceOf<String>(), 'hello');
+    _test(const _StringMatcher(), 'hello');
+    _test(const TypeMatcher<String>(), 'hello');
+    _test(isA<String>(), 'hello');
+  });
+}
+
+void _test<T>(Matcher typeMatcher, T matchingInstance, {String? name}) {
+  name ??= T.toString();
+  group('for `$name`', () {
+    if (matchingInstance != null) {
+      test('succeeds', () {
+        shouldPass(matchingInstance, typeMatcher);
+      });
+    }
+
+    test('fails', () {
+      shouldFail(
+        const _TestType(),
+        typeMatcher,
+        "Expected: <Instance of '$name'> Actual: <Instance of '_TestType'>"
+        " Which: is not an instance of '$name'",
+      );
+    });
+  });
+}
+
+// Validate that existing implementations continue to work.
+class _StringMatcher extends TypeMatcher {
+  const _StringMatcher() : super('String');
+
+  @override
+  bool matches(dynamic item, Map matchState) => item is String;
+}
+
+class _TestType {
+  const _TestType();
+}
diff --git a/pkgs/matcher/test/utils_new.dart b/pkgs/matcher/test/utils_new.dart
new file mode 100644
index 0000000..7d85a02
--- /dev/null
+++ b/pkgs/matcher/test/utils_new.dart
@@ -0,0 +1,49 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:matcher/expect.dart';
+import 'package:test_api/hooks_testing.dart';
+
+/// Asserts that [monitor] has completed and passed.
+///
+/// If the test had any errors, they're surfaced nicely into the outer test.
+void expectTestPassed(TestCaseMonitor monitor) {
+  // Since the test is expected to pass, we forward any current or future errors
+  // to the running test, because they're definitely unexpected and it is most
+  // useful for the error to point directly to the throw point.
+  for (var error in monitor.errors) {
+    Zone.current.handleUncaughtError(error.error, error.stackTrace);
+  }
+  monitor.onError.listen((error) {
+    Zone.current.handleUncaughtError(error.error, error.stackTrace);
+  });
+
+  expect(monitor.state, State.passed);
+}
+
+/// Asserts that [monitor] failed with a single [TestFailure] whose message
+/// matches [message].
+void expectTestFailed(TestCaseMonitor monitor, Object? message) {
+  expect(monitor.state, State.failed);
+  expect(monitor.errors, [isAsyncError(isTestFailure(message))]);
+}
+
+/// Returns a matcher that matches a [AsyncError] with an `error` field matching
+/// [errorMatcher].
+Matcher isAsyncError(Matcher errorMatcher) =>
+    isA<AsyncError>().having((e) => e.error, 'error', errorMatcher);
+
+/// Returns a matcher that matches a [TestFailure] with the given [message].
+///
+/// [message] can be a string or a [Matcher].
+Matcher isTestFailure(Object? message) => const TypeMatcher<TestFailure>()
+    .having((e) => e.message, 'message', message);
+
+/// Returns a matcher that matches a callback or Future that throws a
+/// [TestFailure] with the given [message].
+///
+/// [message] can be a string or a [Matcher].
+Matcher throwsTestFailure(Object? message) => throwsA(isTestFailure(message));
diff --git a/pkgs/test/.test_config b/pkgs/test/.test_config
new file mode 100644
index 0000000..412fc5c
--- /dev/null
+++ b/pkgs/test/.test_config
@@ -0,0 +1,3 @@
+{
+  "test_package": true
+}
\ No newline at end of file
diff --git a/pkgs/test/CHANGELOG.md b/pkgs/test/CHANGELOG.md
new file mode 100644
index 0000000..afe8dec
--- /dev/null
+++ b/pkgs/test/CHANGELOG.md
@@ -0,0 +1,1836 @@
+## 1.25.12
+
+* Fix hang when running multiple precompiled browser tests.
+
+## 1.25.11
+
+* Update to be forward compatible with `package:shelf_web_socket` version `3.x`.
+
+## 1.25.10
+
+* Update the `package:vm_service` constraint to allow version `15.x`.
+
+## 1.25.9
+
+* Allow `analyzer: '>=6.0.0 <8.0.0'`
+* Fix dart2wasm tests on windows.
+* Increase SDK constraint to ^3.5.0.
+* Support running Node.js tests compiled with dart2wasm.
+* Allow `firefox` or `firefox-bin` executable name on macOS.
+
+## 1.25.8
+
+* Increase SDK constraint to ^3.4.0.
+
+## 1.25.7
+
+* Enable asserts for `dart2wasm` tests.
+
+## 1.25.6
+
+* Point API doc links to `package:test` canonical libraries.
+* Fix testing with `dart2wasm` - use `dart compile wasm` instead of depending on
+  SDK internals
+* Update min SDK constraint to 3.2.0.
+
+## 1.25.5
+
+* Update the `package:web_socket_channel` version constraint to allow `3.x`.
+* Update the `package:shelf_web_socket` version constraint to allow `2.x`.
+
+## 1.25.4
+
+* Add `@doNotSubmit` to more declarations of the `solo` parameter.
+
+## 1.25.3
+
+* Remove outdated StreamMatcher link from README table of contents.
+* Document the silent reporter in CLI help output.
+* Support enabling experiments with the dart2wasm compiler.
+* Added [`@doNotSubmit`](https://pub.dev/documentation/meta/latest/meta/doNotSubmit-constant.html) to `test(solo: ...)` and `group(solo: ...)`. In
+  practice, this means that code that was relying on ignoring deprecation
+  warnings and using `solo` or `group` with a `skip` parameter will now fail if
+  `dart analyze --fatal-infos` (or similar) is enabled.
+
+## 1.25.2
+
+* Fix a bug running browser tests with paths containing windows directory
+  separator follow by a character which is an invalid Dart string escape
+  sequence.
+
+## 1.25.1
+
+* Fix a bug where in precompiled mode, html files for tests were no longer
+  created.
+* Support the latest version of `package:js`.
+* Document the silent reporter in CLI help output.
+
+## 1.25.0
+
+* Handle paths with leading `/` when spawning test isolates.
+* Add support for the `dart2wasm` compiler in chrome and firefox.
+* **BREAKING**: Remove the `experimental-chrome-wasm` platform, you can now use
+  `-p chrome -c dart2wasm` instead.
+  * Note that this has always been advertised as a change that would happen in a
+    future non-breaking release.
+* **BREAKING**:Dropped support for `--pub-serve` which has long not been tested
+  or supported.
+  * We do not anticipate much if any actual breakage or existing usage of this
+    feature, which is why we are making this change in a non-breaking release.
+  * If you do require this feature, file an issue and we can look at adding it
+    back.
+* **BREAKING**: Fully remove support for Internet Explorer.
+* Fix running of tests defined under `lib/` with relative imports to other
+  libraries in the package.
+
+## 1.24.9
+
+* Update the vm_service constraint to allow version `13.x`.
+
+## 1.24.8
+
+* Remove spurious deprecation during autocomplete for `setUp` and `tearDown`.
+
+## 1.24.7
+
+* Simplify the initialization of the per-suite message channel within browser
+  tests. See https://github.com/dart-lang/test/issues/2065
+* Add a timeout to browser test suite loads.
+* Fix running of browser tests that use deferred loaded libraries.
+
+## 1.24.6
+
+* Fix communication failures between minified test apps and the non-minified
+  host app.
+* Add support for discontinuing after the first failing test with `--fail-fast`.
+
+## 1.24.5
+
+* Change `compiling <path>` to `loading <path>` message in all cases. Surface
+  the "loading" messages in the situations where previously only the
+  "compiling" message would be used.
+* Support browser tests where the frame creates the message channel.
+
+## 1.24.4
+
+* Drop support for null unsafe Dart, bump SDK constraint to `3.0.0`.
+* Make some annotation classes `final`: `OnPlatform`, `Retry`, `Skip`, `Tags`,
+  `TestOn`, `Timeout`.
+* Fix the `root_` fields in the JSON reporter when running a test on Windows
+  with an absolute path.
+* Add support for `SAFARI_EXECUTABLE`, `FIREFOX_EXECUTABLE` and
+  `MS_EDGE_EXECUTABLE` for custom browser installations.
+* Allow the latest analyzer (6.x.x).
+* Add `MOZ_AUTOMATION=1` environmental variable to Firefox runner, to make
+  launcher process on Windows wait for browser exit.
+
+## 1.24.3
+
+* Fix compatibility with wasm number semantics.
+
+## 1.24.2
+
+* Copy an existing nonce from a script on the test HTML page to the script
+  created by the test runner host javascript. This only impacts environments
+  testing with custom HTML that includes a nonce.
+* Support the Microsoft Edge browser (use the `edge` platform in your test
+  configuration file or `-p edge` on the command line).
+
+## 1.24.1
+
+* Handle a missing `'compiler'` value when running a test compiled against a
+  newer `test_api` than the runner back end is using. The expectation was that
+  the json protocol is only used across packages compatible with the same major
+  version of the `test_api` package, but `flutter test` does not check the
+  version of packages in the pub solve for user test code.
+
+## 1.24.0
+
+* Support the `--compiler` flag, which can be used to configure which compiler
+  to use.
+  * To specify a compiler by platform, the argument supports platform selectors
+    through this syntax `[<platform>:]<compiler>`. For example the command line
+    argument `--compiler vm:source` would run all vm tests from source instead
+    of compiling to kernel first.
+  * If no given compiler is compatible for a platform, it will use its default
+    compiler instead.
+* Add support for running tests as native executables (vm platform only).
+  * You can run tests this way with `--compiler exe`.
+* Support compiler identifiers in platform selectors.
+* List the supported compilers for each platform in the usage text.
+* Update all reporters to print the compiler along with the platform name
+  when configured to print the platform. Extend the logic for printing platofrm
+  information to do so if any compilers are explicitly configured.
+* Deprecate `--use-data-isolate-strategy`. It is now an alias for `-c vm:source`
+  which is roughly equivalent. If this is breaking for you please file an issue.
+
+## 1.23.1
+
+* Fix running paths by absolute path (with drive letter) on windows.
+
+## 1.23.0
+
+* Avoid empty expandable groups for tests without extra output in Github
+  reporter.
+* Add support for CHROME_EXECUTABLE environment variable. This overrides any
+  config file settings.
+* Support running tests by absolute file uri.
+
+## 1.22.2
+
+* Don't run `tearDown` until the test body and outstanding work is complete,
+  even if the test has already failed.
+* Change URL secrets for browser tests to always be alphanumeric characters.
+
+## 1.22.1
+
+* Add documentation for the `--ignore-timeouts` argument.
+* Merge command lines args repeating the same test path to run the suite one
+  time with all the test cases across the different arguments.
+* Fix VM tests which run after some test has changed the working directory.
+  There are still issues with browser tests after changing directory.
+* Deprecate `throwsNullThrownError`, use `throwsA(isA<TypeError>())` instead. The
+  implementation has been changed to ease migrations.
+* Deprecate `throwsCyclicInitializationError` and replace the implementation
+  with `Throws(TypeMatcher<Error>())`. The specific exception no longer exists
+  and there is no guarantee about what type of error will be thrown.
+
+## 1.22.0
+
+* Fix an issue with the github reporter where tests that fail asynchronously
+  after they've completed would show up as succeeded tests.
+* Add the `experimental-chrome-wasm` platform. This is very unstable and will
+  eventually be deleted, to be replaced by a `--compiler` flag. See
+  https://github.com/dart-lang/test/issues/1776 for more information on future
+  plans.
+
+## 1.21.7
+
+* Support `package:matcher` version `0.12.13`.
+
+## 1.21.6
+
+* Require Dart >= 2.18.0
+* Fix the coverage usage example in the README.md
+* Support the latest `package:test_api` and `package:test_core`.
+
+## 1.21.5
+
+* Fix `printOnFailure` output to be associated with the correct test.
+* Migrate all dom interactions to static interop.
+
+## 1.21.4
+
+* Make the labels for test loading more readable in the compact and expanded
+  reporters, use gray instead of black.
+* Print a command to re-run the failed test after each failure in the compact
+  reporter.
+* Fix the package config path used when running pre-compiled vm tests.
+
+## 1.21.3
+
+* Support the latest `package:test_api` and `package:test_core`.
+
+## 1.21.2
+
+* Add `Target` to restrict `TestOn` annotation to library level.
+* Update the github reporter to output the platform in the test names when
+  multiple platforms are used.
+* Fix `spawnHybridUri` support for `package:` uris.
+
+## 1.21.1
+
+* Fix a bug loading JS sources with non-utf8 content while parsing coverage
+  information from chrome.
+
+## 1.21.0
+
+* Allow analyzer version `4.x`.
+* Add a `github` reporter option for use with GitHub Actions.
+* Make the `github` test reporter the default when we detect we're running on
+  GitHub Actions.
+
+## 1.20.2
+
+* Drop `dart2js-path` command line argument.
+* Allow loading tests under a path with the directory named `packages`.
+* Add retry for launching browsers. Reduce timeout back to 30 seconds.
+
+## 1.20.1
+
+* Allow the latest `vm_service` package.
+
+## 1.20.0
+
+* Update `analyzer` constraint to `>=2.0.0 <4.0.0`.
+* Add an `--ignore-timeouts` command line flag, which disables all timeouts
+  for all tests. This can be useful when debugging, so tests don't time out
+  during debug sessions.
+* Create a trusted types policy when available for assigning the script URL for
+  web tests.
+
+## 1.19.5
+
+* Try to get more logging from `chrome` on windows to diagnose intermittent
+  failures.
+
+## 1.19.4
+
+* Wait for paused VM platform isolates before shutdown.
+* `TestFailure` implements `Exception` for compatibility with
+  `only_throw_exceptions`.
+
+## 1.19.3
+
+* Remove duplicate logging of suggestion to enable the `chain-stack-traces`
+  flag, a single log will now appear at the end.
+
+## 1.19.2
+
+* Republish with missing JS file for browser tests.
+
+## 1.19.1
+
+* Fix parsing of file paths into a URI on windows.
+
+## 1.19.0
+
+* Support query parameters `name`, `full-name`, `line`, and `col` on test paths,
+  which will apply the filters to only those test suites.
+  * All specified filters must match for a test to run.
+  * Global filters (ie: `--name`) are also still respected and must match.
+  * The `line` and `col` will match if any frame from the test trace matches
+    (the test trace is the current stack trace where `test` is invoked).
+* Give a better exception when using `markTestSkipped` outside of a test.
+
+## 1.18.2
+
+* Publish with the `host.dart.js` file.
+
+## 1.18.1
+
+* Add defaulting for older test backends that don't pass a configuration for
+  the `allow_duplicate_test_names` parameter to the remote listener.
+
+## 1.18.0
+
+* Add configuration to disallow duplicate test and group names. See the
+  [docs][allow_duplicate_test_names] for more information.
+* Remove dependency on pedantic.
+
+[allow_duplicate_test_names]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#allow_duplicate_test_names
+
+## 1.17.12
+
+* Support the latest `test_core`.
+* Re-use the cached dill file from previous runs on subsequent runs.
+
+## 1.17.11
+
+* Use the latest `package:matcher`.
+  * Change many argument types from `dynamic` to `Object?`.
+  * Fix `stringContainsInOrder` to account for repetitions and empty strings.
+    * **Note**: This may break some existing tests, as the behavior does change.
+
+## 1.17.10
+
+* Report incomplete tests as errors in the JSON reporter when the run is
+  canceled early.
+* Update `analyzer` constraint to `>=1.0.0 <3.0.0`.
+
+## 1.17.9
+
+* Fix a bug where a tag level configuration would cause test suites with that
+  tag to ignore the `--test-randomize-ordering-seed` argument.
+
+## 1.17.8
+
+* Update json reporter docs with updated nullability annotations and
+  descriptions.
+* Add `time` field to the json reporters `allSuites` event type so that all
+  event types can be unified.
+
+## 1.17.7
+
+* Support the latest `test_core`.
+
+## 1.17.6
+
+* Give a better error when `printOnFailure` is called from outside a test
+  zone.
+
+## 1.17.5
+
+* Support the latest vm_service release (`7.0.0`).
+
+## 1.17.4
+
+* Fix race condition between compilation of vm tests and the running of
+  isolates.
+
+## 1.17.3
+
+* Forward experiment args from the runner executable to the compiler with the
+  new vm test loading strategy.
+
+## 1.17.2
+
+* Fix a windows issue with the new loading strategy.
+
+## 1.17.1
+
+* Fix an issue where you couldn't have tests compiled in both sound and
+  unsound null safety modes.
+
+## 1.17.0
+
+* Change the default way VM tests are launched and ran to greatly speed up
+  loading performance.
+  * You can force the old strategy with `--use-data-isolate-strategy` flag if
+    you run into issues, but please also file a bug.
+* Disable stack trace chaining by default. It can be re-enabled by explicitly
+  passing the `--chain-stack-traces` flag.
+* Remove `phantomjs` support completely, it was previously broken.
+* Fix `expectAsync` function type checks.
+* Add libraries `scaffolding.dart`, and `expect.dart` to allow importing a
+  subset of the normal surface area.
+
+## 1.16.8
+
+* Fix an issue where coverage collection could hang on Chrome.
+* ~~Disable stack trace chaining by default. It can be re-enabled by explicitly
+  passing the `--chain-stack-traces` flag.~~
+
+## 1.16.7
+
+* Update `spawnHybridCode` to default to the current packages language version.
+* Update `test_core` and `test_api` deps.
+
+## 1.16.6
+
+* Complete the migration to null safety.
+
+## 1.16.5
+
+* Expand several deps to allow the latest versions.
+
+## 1.16.4
+
+* Update `test_core` dependency to `0.3.14`.
+
+## 1.16.3
+
+* Update `web_socket_channel` dependency to support latest.
+
+## 1.16.2
+
+* Update `test_core` dependency to `0.3.13`.
+
+## 1.16.1
+
+* Allow the latest analyzer `1.0.0`.
+
+## 1.16.0
+
+* Stable null safety release.
+
+## 1.16.0-nullsafety.19
+
+* Use the `test_api` for stable null safety.
+
+## 1.16.0-nullsafety.18
+
+* Expand upper bound constraints for some null safe migrated packages.
+
+## 1.16.0-nullsafety.17
+
+* Support the latest shelf release (`1.x.x`).
+
+## 1.16.0-nullsafety.16
+
+* Support the latest vm_service release (`6.x.x`).
+
+## 1.16.0-nullsafety.15
+
+* Support the latest coverage release (`0.15.x`).
+
+## 1.16.0-nullsafety.14
+
+* Allow the latest args release (`2.x`).
+
+## 1.16.0-nullsafety.13
+
+* Allow the latest glob release (`2.x`).
+
+## 1.16.0-nullsafety.12
+
+* Fix `spawnHybridUri` on windows.
+* Fix failures running tests on the `node` platform.
+* Allow `package:yaml` version `3.x.x`.
+
+## 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.7 (Backport)
+
+* Fix `spawnHybridUri` on windows.
+
+## 1.15.6 (Backport)
+
+* Support `package:analyzer` version `0.41.x`.
+
+## 1.15.5 (Backport)
+
+* Fix `spawnHybridUri` to respect language versioning of the spawned uri.
+
+## 1.15.4
+
+* Allow analyzer 0.40.x.
+
+## 1.15.3
+
+* Update to `matcher` version `0.12.9` which improves the mismatch description
+  for deep collection equality matchers and TypeMatcher.
+
+## 1.15.2
+
+* Use the latest `test_core` which resolves an issue with the latest
+  `package:meta`.
+
+## 1.15.1
+
+* Avoid a confusing stack trace when there is a problem loading a platform when
+  using the JSON reporter and enabling debugging.
+* Restore behavior of listening for both `IPv6` and `IPv4` sockets for the node
+  platform.
+
+## 1.15.0
+
+* Update bootstrapping logic to ensure the bootstrap library has
+  the same language version as the test.
+* The Node platform will now communicate over only IPv6 if it is available.
+
+## 1.14.7
+
+* Support the latest `package:coverage`.
+
+
+## 1.14.6
+
+* Update `test_core` to `0.3.6`.
+
+## 1.14.5
+
+* Add additional information to an exception when we end up with a null
+  `RunnerSuite`.
+
+## 1.14.4
+
+* Use non-headless Chrome when provided the flag `--pause-after-load`.
+
+## 1.14.3
+
+* 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.
+* Update `test_core` to `0.3.4`.
+
+## 1.14.2
+
+* Update `test_core` to `0.3.3`.
+
+## 1.14.1
+
+* Allow the latest shelf_packages_handler.
+
+## 1.14.0
+
+* Drop the `package_resolver` dependency for the `package_config` dependency
+  which is lower level.
+
+## 1.13.0
+
+* Enable asserts in code running through `spawnHybrid` APIs.
+* Exit with a non-zero code if no tests were ran, whether due to skips or having
+  no tests defined.
+* Fix the stack trace labels in SDK code for `dart2js` compiled tests.
+* Cancel any StreamQueue that is created as a part of a stream matcher once it
+  is done matching.
+  * This fixes a bug where using a matcher on a custom stream controller and
+    then awaiting the `close()` method on that controller would hang.
+* Avoid causing the test runner to hang if there is a timeout during a
+  `tearDown` callback following a failing test case.
+
+## 1.12.0
+
+* Bump minimum SDK to `2.4.0` for safer usage of for-loop elements.
+* Deprecate `PhantomJS` and provide warning when used. Support for `PhantomJS`
+  will be removed in version `2.0.0`.
+* Support coverage collection for the Chrome platform. See `README.md` for usage
+  details.
+
+## 1.11.1
+
+* Allow `test_api` `0.2.13` to work around a bug in the SDK version `2.3.0`.
+
+## 1.11.0
+
+* Add `file_reporters` configuration option and `--file-reporter` CLI option to
+  allow specifying a separate reporter that writes to a file instead of stdout.
+
+## 1.10.0
+
+* Add `customHtmlTemplateFile` configuration option to allow sharing an
+  html template between tests
+* Depend on the latest `package:test_core`.
+* Depend on the latest `package:test_api`.
+
+## 1.9.4
+
+* Extend the timeout for synthetic tests, e.g. `tearDownAll`.
+* Depend on the latest `package:test_core`.
+* Depend on the latest `package:test_api`.
+
+## 1.9.3
+
+* Depend on the latest `package:test_core`.
+* Support the latest `package:analyzer`.
+* Update to latest `package:matcher`. Improves output for instances of private
+  classes.
+
+## 1.9.2
+
+* Depend on the latest `package:test_api` and `package:test_core`.
+* While using `solo` tests that are not run will now be reported as skipped.
+
+## 1.9.1
+
+* Depend on latest `test_core`.
+
+## 1.9.0
+
+* Implement code coverage collection for VM based tests
+
+## 1.8.0
+
+* Expose the previously hidden sharding arguments
+  * `--total-shards` specifies how many shards the suite should
+    be split into
+  * `--shard-index` specifies which shard should be run
+
+## 1.7.0
+
+* Add a `--debug` flag for running the VM/Chrome in debug mode.
+
+## 1.6.11
+
+* Depend on the latest `test_core` and `test_api`.
+
+## 1.6.10
+
+* Depend on the latest `test_core`.
+
+## 1.6.9
+
+* Add `--disable-dev-shm-usage` to the default Chrome flags.
+
+## 1.6.8
+
+* Depend on the latest `test_core` and `test_api`.
+
+## 1.6.7
+
+* Allow `analyzer` version `0.38.x`.
+
+## 1.6.6
+
+* Pass `--server-mode` to dart2js instead of `--categories=Server` to fix a
+  warning about the flag deprecation.
+* Drop dependency on `pub_semver`.
+* Fix issue with the latest `Utf8Decoder` and the `node` platform.
+
+## 1.6.5
+
+* Depend on the latest `test_core`.
+* Depend on the latest `package:analyzer`.
+
+## 1.6.4
+
+* Don't swallow exceptions from callbacks in `expectAsync*`.
+* Internal cleanup - fix lints.
+
+## 1.6.3
+
+* Depend on latest `package:test_core`.
+  * This fixes an issue where non-completed tests were considered passing.
+
+## 1.6.2
+
+* Avoid `dart:isolate` imports on code loaded in tests.
+
+## 1.6.1
+
+* Allow `stream_channel` version `2.0.0`.
+
+## 1.6.0
+
+* Allow `analyzer` version `0.36.x`.
+* Matcher changes:
+  * Add `isA()` to create `TypeMatcher` instances in a more fluent way.
+  * Add `isCastError`.
+  * **Potentially breaking bug fix**. Ordering matchers no longer treat objects
+    with a partial ordering (such as NaN for double values) as if they had a
+    complete ordering. For instance `greaterThan` now compares with the `>`
+    operator rather not `<` and not `=`. This could cause tests which relied on
+    this bug to start failing.
+
+## 1.5.3
+
+* Allow `analyzer` version `0.35.x`.
+
+## 1.5.2
+
+* Require Dart SDK `>=2.1.0`.
+* Depend on latest `test_core` and `test_api`.
+
+## 1.5.1
+
+* Depend on latest `test_core` and `test_api`.
+
+## 1.5.0
+
+* Depend on `package:test_core` for core functionality.
+
+## 1.4.0
+
+* Depend on `package:test_api` for core functionality.
+
+## 1.3.4
+
+* Allow remote_listener to be closed and sent an event on close.
+
+## 1.3.3
+
+* Add conditional imports so that `dart:io` is not imported from the main
+  `test.dart` entrypoint unless it is available.
+* Fix an issue with dartdevc in precompiled mode and the json reporter.
+* Fix an issue parsing test metadata annotations without explicit `const`.
+
+## 1.3.2
+
+* Widen the constraints on the analyzer package.
+
+## 1.3.1
+
+* Handle parsing annotations which omit `const` on collection literals.
+* Fix an issue where `root_line`, `root_column`, and `root_url` in the
+  JSON reported may not be populated correctly on Windows.
+* Removed requirement for the test/pub_serve transformer in --pub-serve mode.
+
+## 1.3.0
+
+* When using `--precompiled`, the test runner now allows symlinks to reach
+  outside the precompiled directory. This allows more efficient creation of
+  precompiled directories (using symlinks instead of copies).
+* Updated max sdk range to `<3.0.0`.
+
+## 1.2.0
+
+* Added support for using precompiled kernel files when running vm tests.
+  * When using the `--precompiled` flag we will now first check for a
+    `<original-test-path>.vm_test.vm.app.dill` file, and if present load that
+    directly in the isolate. Otherwise the `<original-test-path>.vm_test.dart`
+    file will be used.
+
+## 1.1.0
+
+* Added a new `pid` field to the StartEvent in the json runner containing the
+  pid of the VM process running the tests.
+
+## 1.0.0
+
+* No change from `0.12.42`. We are simply signalling to users that this is a
+  well supported package and is the preferred way to write Dart tests.
+
+## 0.12.42
+
+* Add support for `solo` test and group. When the argument is `true` only tests
+  and groups marked as solo will be run. It is still recommended that users
+  instead filter their tests by using the runner argument `-n`.
+
+* Updated exported `package:matcher` to `0.12.3` which includes these updates:
+
+  - Many improvements to `TypeMatcher`
+    - Can now be used directly as `const TypeMatcher<MyType>()`.
+    - Added a type parameter to specify the target `Type`.
+      - Made the `name` constructor parameter optional and marked it deprecated.
+        It's redundant to the type parameter.
+    - Migrated all `isType` matchers to `TypeMatcher`.
+    - Added a `having` function that allows chained validations of specific
+      features of the target type.
+
+      ```dart
+      /// Validates that the object is a [RangeError] with a message containing
+      /// the string 'details' and `start` and `end` properties that are `null`.
+      final _rangeMatcher = isRangeError
+         .having((e) => e.message, 'message', contains('details'))
+         .having((e) => e.start, 'start', isNull)
+         .having((e) => e.end, 'end', isNull);
+      ```
+
+  - Deprecated the `isInstanceOf` class. Use `TypeMatcher` instead.
+
+  - Improved the output of `Matcher` instances that fail due to type errors.
+
+## 0.12.41
+
+* Add support for debugging VM tests.
+* Tweak default reporter and color logic again so that they are always enabled
+  on all non-windows platforms.
+
+## 0.12.40
+
+* Added some new optional fields to the json reporter, `root_line`,
+  `root_column`, and `root_url`. These will be present if `url` is not the same
+  as the suite url, and will represent the location in the original test suite
+  from which the call to `test` originated.
+
+## 0.12.39
+
+* Change the default reporter and color defaults to be based on
+  `stdout.supportsAnsiEscapes` instead of based on platform (previously both
+  were disabled on windows).
+
+## 0.12.38+3
+
+* Fix Dart 2 runtime errors around communicating with browsers.
+
+
+## 0.12.38+2
+
+* Fix more Dart 2 runtime type errors.
+
+## 0.12.38+1
+
+* Fix several Dart 2 runtime type errors.
+
+## 0.12.38
+
+* Give `neverCalled` a type that works in Dart 2 semantics.
+* Support `package:analyzer` `0.32.0`.
+
+## 0.12.37
+
+* Removed the transformer, and the `pub_serve.dart` entrypoint. This is not
+  being treated as a breaking change because the minimum sdk constraint now
+  points to an sdk which does not support pub serve or barback any more anyways.
+* Drop the dependency on `barback`.
+
+## 0.12.36
+
+* Expose the test bootstrapping methods, so that build systems can precompile
+  tests without relying on internal apis.
+
+## 0.12.35
+
+* Dropped support for Dart 1. Going forward only Dart 2 will be supported.
+  * If you experience blocking issues and are still on the Dart 1 sdk, we will
+    consider bug fixes on a per-case basis based on severity and impact.
+  * Drop support for `dartium` and `content-shell` platforms since those are
+    removed from the Dart 2 SDK.
+* Fixed an issue `--precompiled` node tests in subdirectories.
+* Fixed some dart2 issues with node test bootstrapping code so that dartdevc
+  tests can run.
+* Fixed default custom html handler so it correctly includes the
+  packages/test/dart.js file. This allows you to get proper errors instead of
+  timeouts if there are load exceptions in the browser.
+* Upgrade to package:matcher 0.12.2
+
+## 0.12.34
+
+* Requires at least Dart 1.24.0.
+* The `--precompiled` flag is now supported for the vm platform and the node
+  platform.
+* On browser platforms the `--precompiled` flag now serves all sources directly
+  from the precompiled directory, and will never attempt to do its own
+  compilation.
+
+## 0.12.33
+
+* Pass `--categories=Server` to `dart2js` when compiling tests for Node.js. This
+  tells it that `dart:html` is unavailable.
+
+* Don't crash when attempting to format stack traces when running via
+  `dart path/to/test.dart`.
+
+## 0.12.32+2
+
+* Work around an SDK bug that caused timeouts in asynchronous code.
+
+## 0.12.32+1
+
+* Fix a bug that broke content shell on Dart 1.24.
+
+## 0.12.32
+
+* Add an `include` configuration field which specifies the path to another
+  configuration file whose configuration should be used.
+
+* Add a `google` platform selector variable that's only true on Google's
+  internal infrastructure.
+
+## 0.12.31
+
+* Add a `headless` configuration option for Chrome.
+
+* Re-enable headless mode for Chrome by default.
+
+* Don't hang when a Node.js test fails to compile.
+
+## 0.12.30+4
+
+* Stop running Chrome in headless mode temporarily to work around a browser bug.
+
+## 0.12.30+3
+
+* Fix a memory leak when loading browser tests.
+
+## 0.12.30+2
+
+* Avoid loading test suites whose tags are excluded by `--excluded-tags`.
+
+## 0.12.30+1
+
+* Internal changes.
+
+## 0.12.30
+
+* Platform selectors for operating systems now work for Node.js tests
+  ([#742][]).
+
+* `fail()` is now typed to return `Null`, so it can be used in the same places
+  as a raw `throw`.
+
+* Run Chrome in headless mode unless debugging is enabled.
+
+[#742]: https://github.com/dart-lang/test/issues/742
+
+## 0.12.29+1
+
+* Fix strong mode runtime cast failures.
+
+## 0.12.29
+
+* Node.js tests can now import modules from a top-level `node_modules`
+  directory, if one exists.
+
+* Raw `console.log()` calls no longer crash Node.js tests.
+
+* When a browser crashes, include its standard output in the error message.
+
+## 0.12.28+1
+
+* Add a `pumpEventQueue()` function to make it easy to wait until all
+  asynchronous tasks are complete.
+
+* Add a `neverCalled` getter that returns a function that causes the test to
+  fail if it's ever called.
+
+## 0.12.27+1
+
+* Increase the timeout for loading tests to 12 minutes.
+
+## 0.12.27
+
+* When `addTearDown()` is called within a call to `setUpAll()`, it runs its
+  callback after *all* tests instead of running it after the `setUpAll()`
+  callback.
+
+* When running in an interactive terminal, the test runner now prints status
+  lines as wide as the terminal and no wider.
+
+## 0.12.26+1
+
+* Fix lower bound on package `stack_trace`. Now 1.6.0.
+* Manually close browser process streams to prevent test hangs.
+
+## 0.12.26
+
+* The `spawnHybridUri()` function now allows root-relative URLs, which are
+  interpreted as relative to the root of the package.
+
+## 0.12.25
+
+* Add a `override_platforms` configuration field which allows test platforms'
+  settings (such as browsers' executables) to be overridden by the user.
+
+* Add a `define_platforms` configuration field which makes it possible to define
+  new platforms that use the same logic as existing ones but have different
+  settings.
+
+## 0.12.24+8
+
+* `spawnHybridUri()` now interprets relative URIs correctly in browser tests.
+
+## 0.12.24+7
+
+* Declare support for `async` 2.0.0.
+
+## 0.12.24+6
+
+* Small refactoring to make the package compatible with strong-mode compliant Zone API.
+  No user-visible change.
+
+## 0.12.24+5
+
+* Expose a way for tests to forward a `loadException` to the server.
+
+## 0.12.24+4
+
+* Drain browser process `stdout` and `stdin`. This resolves test flakiness, especially in Travis
+  with the `Precise` image.
+
+## 0.12.24+3
+
+* Extend `deserializeTimeout`.
+
+## 0.12.24+2
+
+* Only force exit if `FORCE_TEST_EXIT` is set in the environment.
+
+## 0.12.24+1
+
+* Widen version constraint on `analyzer`.
+
+## 0.12.24
+
+* Add a `node` platform for compiling tests to JavaScript and running them on
+  Node.js.
+
+## 0.12.23+1
+
+* Remove unused imports.
+
+## 0.12.23
+
+* Add a `fold_stack_frames` field for `dart_test.yaml`. This will
+  allow users to customize which packages' frames are folded.
+
+## 0.12.22+2
+
+* Properly allocate ports when debugging Chrome and Dartium in an IPv6-only
+  environment.
+
+## 0.12.22+1
+
+* Support `args` 1.0.0.
+
+* Run tear-down callbacks in the same error zone as the test function. This
+  makes it possible to safely share `Future`s and `Stream`s between tests and
+  their tear-downs.
+
+## 0.12.22
+
+* Add a `retry` option to `test()` and `group()` functions, as well
+  as `@Retry()`  annotation for test files and a `retry`
+  configuration field for `dart_test.yaml`.  A test with reties
+  enabled will be re-run if it fails for a reason other than a
+  `TestFailure`.
+
+* Add a `--no-retry` runner flag that disables retries of failing tests.
+
+* Fix a "concurrent modification during iteration" error when calling
+  `addTearDown()` from within a tear down.
+
+## 0.12.21
+
+* Add a `doesNotComplete` matcher that asserts that a Future never completes.
+
+* `throwsA()` and all related matchers will now match functions that return
+  `Future`s that emit exceptions.
+
+* Respect `onPlatform` for groups.
+
+* Only print browser load errors once per browser.
+
+* Gracefully time out when attempting to deserialize a test suite.
+
+## 0.12.20+13
+
+* Upgrade to package:matcher 0.12.1
+
+## 0.12.20+12
+
+* Now support `v0.30.0` of `pkg/analyzer`
+
+* The test executable now does a "hard exit" when complete to ensure lingering
+  isolates or async code don't block completion. This may affect users trying
+  to use the Dart service protocol or observatory.
+
+## 0.12.20+11
+
+* Refactor bootstrapping to simplify the test/pub_serve transformer.
+
+## 0.12.20+10
+
+* Refactor for internal tools.
+
+## 0.12.20+9
+
+* Introduce new flag `--chain-stack-traces` to conditionally chain stack traces.
+
+## 0.12.20+8
+
+* Fixed more blockers for compiling with `dev_compiler`.
+* Dartfmt the entire repo.
+
+* **Note:** 0.12.20+5-0.12.20+7 were tagged but not officially published.
+
+## 0.12.20+4
+
+* Fixed strong-mode errors and other blockers for compiling with `dev_compiler`.
+
+## 0.12.20+3
+
+* `--pause-after-load` no longer deadlocks with recent versions of Chrome.
+
+* Fix Dartified stack traces for JS-compiled tests run through `pub serve`.
+
+## 0.12.20+2
+
+* Print "[E]" after test failures to make them easier to identify visually and
+  via automated search.
+
+## 0.12.20+1
+
+* Tighten the dependency on `stream_channel` to reflect the APIs being used.
+
+* Use a 1024 x 768 iframe for browser tests.
+
+## 0.12.20
+
+* **Breaking change:** The `expect()` method no longer returns a `Future`, since
+  this broke backwards-compatibility in cases where a void function was
+  returning an `expect()` (such as `void foo() => expect(...)`). Instead, a new
+  `expectLater()` function has been added that return a `Future` that completes
+  when the matcher has finished running.
+
+* The `verbose` parameter to `expect()` and the `formatFailure()` function are
+  deprecated.
+
+## 0.12.19+1
+
+* Make sure asynchronous matchers that can fail synchronously, such as
+  `throws*()` and `prints()`, can be used with synchronous matcher operators
+  like `isNot()`.
+
+## 0.12.19
+
+* Added the `StreamMatcher` class, as well as several built-in stream matchers:
+  `emits()`, `emitsError()`, `emitsDone, mayEmit()`, `mayEmitMultiple()`,
+  `emitsAnyOf()`, `emitsInOrder()`, `emitsInAnyOrder()`, and `neverEmits()`.
+
+* `expect()` now returns a Future for the asynchronous matchers `completes`,
+  `completion()`, `throws*()`, and `prints()`.
+
+* Add a `printOnFailure()` method for providing debugging information that's
+  only printed when a test fails.
+
+* Automatically configure the [`term_glyph`][term_glyph] package to use ASCII
+  glyphs when the test runner is running on Windows.
+
+[term_glyph]: https://pub.dev/packages/term_glyph
+
+* Deprecate the `throws` matcher in favor of `throwsA()`.
+
+* Deprecate the `Throws` class. These matchers should only be constructed via
+  `throwsA()`.
+
+## 0.12.18+1
+
+* Fix the deprecated `expectAsync()` function. The deprecation caused it to
+  fail to support functions that take arguments.
+
+## 0.12.18
+
+* Add an `addTearDown()` function, which allows tests to register additional
+  tear-down callbacks as they're running.
+
+* Add the `spawnHybridUri()` and `spawnHybridCode()` functions, which allow
+  browser tests to run code on the VM.
+
+* Fix the new `expectAsync` functions so that they don't produce analysis errors
+  when passed callbacks with optional arguments.
+
+## 0.12.17+3
+
+* Internal changes only.
+
+## 0.12.17+2
+
+* Fix Dartium debugging on Windows.
+
+## 0.12.17+1
+
+* Fix a bug where tags couldn't be marked as skipped.
+
+## 0.12.17
+
+* Deprecate `expectAsync` and `expectAsyncUntil`, since they currently can't be
+  made to work cleanly in strong mode. They are replaced with separate methods
+  for each number of callback arguments:
+  * `expectAsync0`, `expectAsync1`, ... `expectAsync6`, and
+  * `expectAsyncUntil0`, `expectAsyncUntil1`, ... `expectAsyncUntil6`.
+
+## 0.12.16
+
+* Allow tools to interact with browser debuggers using the JSON reporter.
+
+## 0.12.15+12
+
+* Fix a race condition that could cause the runner to stall for up to three
+  seconds after completing.
+
+## 0.12.15+11
+
+* Make test iframes visible when debugging.
+
+## 0.12.15+10
+
+* Throw a better error if a group body is asynchronous.
+
+## 0.12.15+9
+
+* Widen version constraint on `analyzer`.
+
+## 0.12.15+8
+
+* Make test suites with thousands of tests load much faster on the VM (and
+  possibly other platforms).
+
+## 0.12.15+7
+
+* Fix a bug where tags would be dropped when `on_platform` was defined in a
+  config file.
+
+## 0.12.15+6
+
+* Fix a broken link in the `--help` documentation.
+
+## 0.12.15+5
+
+* Internal-only change.
+
+## 0.12.15+4
+
+* Widen version constraint on `analyzer`.
+
+## 0.12.15+3
+
+* Move `nestingMiddleware` to `lib/src/util/path_handler.dart` to enable a
+  cleaner separation between test-runner files and test writing files.
+
+## 0.12.15+2
+
+* Support running without a `packages/` directory.
+
+## 0.12.15+1
+
+* Declare support for version 1.19 of the Dart SDK.
+
+## 0.12.15
+
+* Add a `skip` parameter to `expect()`. Marking a single expect as skipped will
+  cause the test itself to be marked as skipped.
+
+* Add a `--run-skipped` parameter and `run_skipped` configuration field that
+  cause tests to be run even if they're marked as skipped.
+
+## 0.12.14+1
+
+* Narrow the constraint on `yaml`.
+
+## 0.12.14
+
+* Add test and group location information to the JSON reporter.
+
+## 0.12.13+5
+
+* Declare support for version 1.18 of the Dart SDK.
+
+* Use the latest `collection` package.
+
+## 0.12.13+4
+
+* Compatibility with an upcoming release of the `collection` package.
+
+## 0.12.13+3
+
+* Internal changes only.
+
+## 0.12.13+2
+
+* Fix all strong-mode errors and warnings.
+
+## 0.12.13+1
+
+* Declare support for version 1.17 of the Dart SDK.
+
+## 0.12.13
+
+* Add support for a global configuration file. On Windows, this file defaults to
+  `%LOCALAPPDATA%\DartTest.yaml`. On Unix, it defaults to `~/.dart_test.yaml`.
+  It can also be explicitly set using the `DART_TEST_CONFIG` environment
+  variable. See [the configuration documentation][global config] for details.
+
+* The `--name` and `--plain-name` arguments may be passed more than once, and
+  may be passed together. A test must match all name constraints in order to be
+  run.
+
+* Add `names` and `plain_names` fields to the package configuration file. These
+  allow presets to control which tests are run based on their names.
+
+* Add `include_tags` and `exclude_tags` fields to the package configuration
+  file. These allow presets to control which tests are run based on their tags.
+
+* Add a `pause_after_load` field to the package configuration file. This allows
+  presets to enable debugging mode.
+
+[global config]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#global-configuration
+
+## 0.12.12
+
+* Add support for [test presets][]. These are defined using the `presets` field
+  in the package configuration file. They can be selected by passing `--preset`
+  or `-P`, or by using the `add_presets` field in the package configuration
+  file.
+
+* Add an `on_os` field to the package configuration file that allows users to
+  select different configuration for different operating systems.
+
+* Add an `on_platform` field to the package configuration file that allows users
+  to configure all tests differently depending on which platform they run on.
+
+* Add an `ios` platform selector variable. This variable will only be true when
+  the `test` executable itself is running on iOS, not when it's running browser
+  tests on an iOS browser.
+
+[test presets]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/package_config.md#configuration-presets
+
+## 0.12.11+2
+
+* Update to `shelf_web_socket` 0.2.0.
+
+## 0.12.11+1
+
+* Purely internal change.
+
+## 0.12.11
+
+* Add a `tags` field to the package configuration file that allows users to
+  provide configuration for specific tags.
+
+* The `--tags` and `--exclude-tags` command-line flags now allow
+  [boolean selector syntax][]. For example, you can now pass `--tags "(chrome ||
+  firefox) && !slow"` to select quick Chrome or Firefox tests.
+
+[boolean selector syntax]: https://github.com/dart-lang/boolean_selector/blob/master/README.md
+
+## 0.12.10+2
+
+* Re-add help output separators.
+
+* Tighten the constraint on `args`.
+
+## 0.12.10+1
+
+* Temporarily remove separators from the help output. Version 0.12.8 was
+  erroneously released without an appropriate `args` constraint for the features
+  it used; this version will help ensure that users who can't use `args` 0.13.1
+  will get a working version of `test`.
+
+## 0.12.10
+
+* Add support for a package-level configuration file called `dart_test.yaml`.
+
+## 0.12.9
+
+* Add `SuiteEvent` to the JSON reporter, which reports data about the suites in
+  which tests are run.
+
+* Add `AllSuitesEvent` to the JSON reporter, which reports the total number of
+  suites that will be run.
+
+* Add `Group.testCount` to the JSON reporter, which reports the total number of
+  tests in each group.
+
+## 0.12.8
+
+* Organize the `--help` output into sections.
+
+* Add a `--timeout` flag.
+
+## 0.12.7
+
+* Add the ability to re-run tests while debugging. When the browser is paused at
+  a breakpoint, the test runner will open an interactive console on the command
+  line that can be used to restart the test.
+
+* Add support for passing any object as a description to `test()` and `group()`.
+  These objects will be converted to strings.
+
+* Add the ability to tag tests. Tests with specific tags may be run by passing
+  the `--tags` command-line argument, or excluded by passing the
+  `--exclude-tags` parameter.
+
+  This feature is not yet complete. For now, tags are only intended to be added
+  temporarily to enable use-cases like [focusing][] on a specific test or group.
+  Further development can be followed on [the issue tracker][issue 16].
+
+* Wait for a test's tear-down logic to run, even if it times out.
+
+[focusing]: https://jasmine.github.io/2.1/focused_specs.html
+[issue 16]: https://github.com/dart-lang/test/issues/16
+
+## 0.12.6+2
+
+* Declare compatibility with `http_parser` 2.0.0.
+
+## 0.12.6+1
+
+* Declare compatibility with `http_multi_server` 2.0.0.
+
+## 0.12.6
+
+* Add a machine-readable JSON reporter. For details, see
+  [the protocol documentation][json-protocol].
+
+* Skipped groups now properly print skip messages.
+
+[json-protocol]: https://github.com/dart-lang/test/blob/master/pkgs/test/json_reporter.md
+
+## 0.12.5+2
+
+* Declare compatibility with Dart 1.14 and 1.15.
+
+## 0.12.5+1
+
+* Fixed a deadlock bug when using `setUpAll()` and `tearDownAll()`.
+
+## 0.12.5
+
+* Add `setUpAll()` and `tearDownAll()` methods that run callbacks before and
+  after all tests in a group or suite. **Note that these methods are for special
+  cases and should be avoided**—they make it very easy to accidentally introduce
+  dependencies between tests. Use `setUp()` and `tearDown()` instead if
+  possible.
+
+* Allow `setUp()` and `tearDown()` to be called multiple times within the same
+  group.
+
+* When a `tearDown()` callback runs after a signal has been caught, it can now
+  schedule out-of-band asynchronous callbacks normally rather than having them
+  throw exceptions.
+
+* Don't show package warnings when compiling tests with dart2js. This was
+  accidentally enabled in 0.12.2, but was never intended.
+
+## 0.12.4+9
+
+* If a `tearDown()` callback throws an error, outer `tearDown()` callbacks are
+  still executed.
+
+## 0.12.4+8
+
+* Don't compile tests to JavaScript when running via `pub serve` on Dartium or
+  content shell.
+
+## 0.12.4+7
+
+* Support `http_parser` 1.0.0.
+
+## 0.12.4+6
+
+* Fix a broken link in the README.
+
+## 0.12.4+5
+
+* Internal changes only.
+
+## 0.12.4+4
+
+* Widen the Dart SDK constraint to include `1.13.0`.
+
+## 0.12.4+3
+
+* Make source maps work properly in the browser when not using `--pub-serve`.
+
+## 0.12.4+2
+
+* Fix a memory leak when running many browser tests where old test suites failed
+  to be unloaded when they were supposed to.
+
+## 0.12.4+1
+
+* Require Dart SDK >= `1.11.0` and `shelf` >= `0.6.0`, allowing `test` to remove
+  various hacks and workarounds.
+
+## 0.12.4
+
+* Add a `--pause-after-load` flag that pauses the test runner after each suite
+  is loaded so that breakpoints and other debugging annotations can be added.
+  Currently this is only supported on browsers.
+
+* Add a `Timeout.none` value indicating that a test should never time out.
+
+* The `dart-vm` platform selector variable is now `true` for Dartium and content
+  shell.
+
+* The compact reporter no longer prints status lines that only update the clock
+  if they would get in the way of messages or errors from a test.
+
+* The expanded reporter no longer double-prints the descriptions of skipped
+  tests.
+
+## 0.12.3+9
+
+* Widen the constraint on `analyzer` to include `0.26.0`.
+
+## 0.12.3+8
+
+* Fix an uncaught error that could crop up when killing the test runner process
+  at the wrong time.
+
+## 0.12.3+7
+
+* Add a missing dependency on the `collection` package.
+
+## 0.12.3+6
+
+**This version was unpublished due to [issue 287][].**
+
+* Properly report load errors caused by failing to start browsers.
+
+* Substantially increase browser timeouts. These timeouts are the cause of a lot
+  of flakiness, and now that they don't block test running there's less harm in
+  making them longer.
+
+## 0.12.3+5
+
+**This version was unpublished due to [issue 287][].**
+
+* Fix a crash when skipping tests because their platforms don't match.
+
+## 0.12.3+4
+
+**This version was unpublished due to [issue 287][].**
+
+* The compact reporter will update the timer every second, rather than only
+  updating it occasionally.
+
+* The compact reporter will now print the full, untruncated test name before any
+  errors or prints emitted by a test.
+
+* The expanded reporter will now *always* print the full, untruncated test name.
+
+## 0.12.3+3
+
+**This version was unpublished due to [issue 287][].**
+
+* Limit the number of test suites loaded at once. This helps ensure that the
+  test runner won't run out of memory when running many test suites that each
+  load a large amount of code.
+
+## 0.12.3+2
+
+**This version was unpublished due to [issue 287][].**
+
+[issue 287]: https://github.com/dart-lang/test/issues/287
+
+* Improve the display of syntax errors in VM tests.
+
+* Work around a [Firefox bug][]. Computed styles now work in tests on Firefox.
+
+[Firefox bug]: https://bugzilla.mozilla.org/show_bug.cgi?id=548397
+
+* Fix a bug where VM tests would be loaded from the wrong URLs on Windows (or in
+  special circumstances on other operating systems).
+
+## 0.12.3+1
+
+* Fix a bug that caused the test runner to crash on Windows because symlink
+  resolution failed.
+
+## 0.12.3
+
+* If a future matched against the `completes` or `completion()` matcher throws
+  an error, that error is printed directly rather than being wrapped in a
+  string. This allows such errors to be captured using the Zone API and improves
+  formatting.
+
+* Improve support for Polymer tests. This fixes a flaky time-out error and adds
+  support for Dartifying JavaScript stack traces when running Polymer tests via
+  `pub serve`.
+
+* In order to be more extensible, all exception handling within tests now uses
+  the Zone API.
+
+* Add a heartbeat to reset a test's timeout whenever the test interacts with the
+  test infrastructure.
+
+* `expect()`, `expectAsync()`, and `expectAsyncUntil()` throw more useful errors
+  if called outside a test body.
+
+## 0.12.2
+
+* Convert JavaScript stack traces into Dart stack traces using source maps. This
+  can be disabled with the new `--js-trace` flag.
+
+* Improve the browser test suite timeout logic to avoid timeouts when running
+  many browser suites at once.
+
+## 0.12.1
+
+* Add a `--verbose-trace` flag to include core library frames in stack traces.
+
+## 0.12.0
+
+### Test Runner
+
+`0.12.0` adds support for a test runner, which can be run via
+`pub run test:test` (or `pub run test` in Dart 1.10). By default it runs all
+files recursively in the `test/` directory that end in `_test.dart` and aren't
+in a `packages/` directory.
+
+The test runner supports running tests on the Dart VM and many different
+browsers. Test files can use the `@TestOn` annotation to declare which platforms
+they support. For more information on this and many more new features, see [the
+README](README).
+
+[README]: https://github.com/dart-lang/test/blob/master/README.md
+
+### Removed and Changed APIs
+
+As part of moving to a runner-based model, most test configuration is moving out
+of the test file and into the runner. As such, many ancillary APIs have been
+removed. These APIs include `skip_` and `solo_` functions, `Configuration` and
+all its subclasses, `TestCase`, `TestFunction`, `testConfiguration`,
+`formatStacks`, `filterStacks`, `groupSep`, `logMessage`, `testCases`,
+`BREATH_INTERVAL`, `currentTestCase`, `PASS`, `FAIL`, `ERROR`, `filterTests`,
+`runTests`, `ensureInitialized`, `setSoloTest`, `enableTest`, `disableTest`, and
+`withTestEnvironment`.
+
+`FailureHandler`, `DefaultFailureHandler`, `configureExpectFailureHandler`, and
+`getOrCreateExpectFailureHandler` which used to be exported from the `matcher`
+package have also been removed. They existed to enable integration between
+`test` and `matcher` that has been streamlined.
+
+A number of APIs from `matcher` have been into `test`, including: `completes`,
+`completion`, `ErrorFormatter`, `expect`,`fail`, `prints`, `TestFailure`,
+`Throws`, and all of the `throws` methods. Some of these have changed slightly:
+
+* `expect` no longer has a named `failureHandler` argument.
+
+* `expect` added an optional `formatter` argument.
+
+* `completion` argument `id` renamed to `description`.
+
+## 0.11.6+4
+
+* Fix some strong mode warnings we missed in the `vm_config.dart` and
+  `html_config.dart` libraries.
+
+## 0.11.6+3
+
+* Fix a bug introduced in 0.11.6+2 in which operator matchers broke when taking
+  lists of matchers.
+
+## 0.11.6+2
+
+* Fix all strong mode warnings.
+
+## 0.11.6+1
+
+* Give tests more time to start running.
+
+## 0.11.6
+
+* Merge in the last `0.11.x` release of `matcher` to allow projects to use both
+  `test` and `unittest` without conflicts.
+
+* Fix running individual tests with `HtmlIndividualConfiguration` when the test
+  name contains URI-escaped values and is provided with the `group` query
+  parameter.
+
+## 0.11.5+1
+
+* Internal code cleanups and documentation improvements.
+
+## 0.11.5
+
+* Bumped the version constraint for `matcher`.
+
+## 0.11.4
+
+* Bump the version constraint for `matcher`.
+
+## 0.11.3
+
+* Narrow the constraint on matcher to ensure that new features are reflected in
+  unittest's version.
+
+## 0.11.2
+
+* Prints a warning instead of throwing an error when setting the test
+  configuration after it has already been set. The first configuration is always
+  used.
+
+## 0.11.1+1
+
+* Fix bug in withTestEnvironment where test cases were not reinitialized if
+  called multiple times.
+
+## 0.11.1
+
+* Add `reason` named argument to `expectAsync` and `expectAsyncUntil`, which has
+  the same definition as `expect`'s `reason` argument.
+* Added support for private test environments.
+
+## 0.11.0+6
+
+* Refactored package tests.
+
+## 0.11.0+5
+
+* Release test functions after each test is run.
+
+## 0.11.0+4
+
+* Fix for [20153](https://code.google.com/p/dart/issues/detail?id=20153)
+
+## 0.11.0+3
+
+* Updated maximum `matcher` version.
+
+## 0.11.0+2
+
+* Removed unused files from tests and standardized remaining test file names.
+
+## 0.11.0+1
+
+* Widen the version constraint for `stack_trace`.
+
+## 0.11.0
+
+* Deprecated methods have been removed:
+  * `expectAsync0`, `expectAsync1`, and `expectAsync2` - use `expectAsync`
+    instead
+  * `expectAsyncUntil0`, `expectAsyncUntil1`, and `expectAsyncUntil2` - use
+    `expectAsyncUntil` instead
+  * `guardAsync` - no longer needed
+  * `protectAsync0`, `protectAsync1`, and `protectAsync2` - no longer needed
+* `matcher.dart` and `mirror_matchers.dart` have been removed. They are now in
+  the `matcher` package.
+* `mock.dart` has been removed. It is now in the `mock` package.
+
+## 0.10.1+2
+
+* Fixed deprecation message for `mock`.
+
+## 0.10.1+1
+
+* Fixed CHANGELOG
+* Moved to triple-slash for all doc comments.
+
+## 0.10.1
+
+* **DEPRECATED**
+  * `matcher.dart` and `mirror_matchers.dart` are now in the `matcher`
+    package.
+  * `mock.dart` is now in the `mock` package.
+* `equals` now allows a nested matcher as an expected list element or map value
+  when doing deep matching.
+* `expectAsync` and `expectAsyncUntil` now support up to 6 positional arguments
+  and correctly handle functions with optional positional arguments with default
+  values.
+
+## 0.10.0
+
+* Each test is run in a separate `Zone`. This ensures that any exceptions that
+  occur is async operations are reported back to the source test case.
+* **DEPRECATED** `guardAsync`, `protectAsync0`, `protectAsync1`,
+  and `protectAsync2`
+  * Running each test in a `Zone` addresses the need for these methods.
+* **NEW!** `expectAsync` replaces the now deprecated `expectAsync0`,
+  `expectAsync1` and `expectAsync2`
+* **NEW!** `expectAsyncUntil` replaces the now deprecated `expectAsyncUntil0`,
+  `expectAsyncUntil1` and `expectAsyncUntil2`
+* `TestCase`:
+  * Removed properties: `setUp`, `tearDown`, `testFunction`
+  * `enabled` is now get-only
+  * Removed methods: `pass`, `fail`, `error`
+* `interactive_html_config.dart` has been removed.
+* `runTests`, `tearDown`, `setUp`, `test`, `group`, `solo_test`, and
+  `solo_group` now throw a `StateError` if called while tests are running.
+* `rerunTests` has been removed.
diff --git a/pkgs/test/LICENSE b/pkgs/test/LICENSE
new file mode 100644
index 0000000..000cd7b
--- /dev/null
+++ b/pkgs/test/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2014, the Dart project authors. 
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google LLC nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/test/README.md b/pkgs/test/README.md
new file mode 100644
index 0000000..36103d9
--- /dev/null
+++ b/pkgs/test/README.md
@@ -0,0 +1,874 @@
+[![pub package](https://img.shields.io/pub/v/test.svg)](https://pub.dev/packages/test)
+[![package publisher](https://img.shields.io/pub/publisher/test.svg)](https://pub.dev/packages/test/publisher)
+
+`test` provides a standard way of writing and running tests in Dart.
+
+## Using package:test
+
+* [Writing Tests](#writing-tests)
+* [Running Tests](#running-tests)
+  * [Sharding Tests](#sharding-tests)
+  * [Test Concurrency](#test-concurrency)
+  * [Shuffling Tests](#shuffling-tests)
+  * [Selecting a Test Reporter](#selecting-a-test-reporter)
+  * [Collecting Code Coverage](#collecting-code-coverage)
+  * [Restricting Tests to Certain Platforms](#restricting-tests-to-certain-platforms)
+  * [Platform Selectors](#platform-selectors)
+  * [Compiler Selectors](#compiler-selectors)
+  * [Running Tests on Node.js](#running-tests-on-nodejs)
+* [Asynchronous Tests](#asynchronous-tests)
+* [Running Tests With Custom HTML](#running-tests-with-custom-html)
+  * [Providing a custom HTML template](#providing-a-custom-html-template)
+* [Configuring Tests](#configuring-tests)
+  * [Skipping Tests](#skipping-tests)
+  * [Timeouts](#timeouts)
+  * [Platform-Specific Configuration](#platform-specific-configuration)
+  * [Whole-Package Configuration](#whole-package-configuration)
+* [Tagging Tests](#tagging-tests)
+* [Debugging](#debugging)
+* [Browser/VM Hybrid Tests](#browservm-hybrid-tests)
+* [Support for Other Packages](#support-for-other-packages)
+  * [`build_runner`](#build_runner)
+  * [`term_glyph`](#term_glyph)
+* [Further Reading](#further-reading)
+
+## Writing Tests
+
+Tests are specified using the top-level [`test()`] function.
+Test asserts can be made using [`expect` from `package:matcher`][expect]
+
+[`test()`]: https://pub.dev/documentation/test/latest/test/test.html
+
+[expect]: https://pub.dev/documentation/matcher/latest/expect/expect.html
+
+```dart
+import 'package:test/test.dart';
+
+void main() {
+  test('String.split() splits the string on the delimiter', () {
+    var string = 'foo,bar,baz';
+    expect(string.split(','), equals(['foo', 'bar', 'baz']));
+  });
+
+  test('String.trim() removes surrounding whitespace', () {
+    var string = '  foo ';
+    expect(string.trim(), equals('foo'));
+  });
+}
+```
+
+Tests can be grouped together using the [`group()`] function. Each group's
+description is added to the beginning of its test's descriptions.
+
+[`group()`]: https://pub.dev/documentation/test/latest/test/group.html
+
+```dart
+import 'package:test/test.dart';
+
+void main() {
+  group('String', () {
+    test('.split() splits the string on the delimiter', () {
+      var string = 'foo,bar,baz';
+      expect(string.split(','), equals(['foo', 'bar', 'baz']));
+    });
+
+    test('.trim() removes surrounding whitespace', () {
+      var string = '  foo ';
+      expect(string.trim(), equals('foo'));
+    });
+  });
+
+  group('int', () {
+    test('.remainder() returns the remainder of division', () {
+      expect(11.remainder(3), equals(2));
+    });
+
+    test('.toRadixString() returns a hex string', () {
+      expect(11.toRadixString(16), equals('b'));
+    });
+  });
+}
+```
+
+You can use the [`setUp()`] and [`tearDown()`] functions to share code between
+tests. The `setUp()` callback will run before every test in a group or test
+suite, and `tearDown()` will run after. `tearDown()` will run even if a test
+fails, to ensure that it has a chance to clean up after itself.
+
+```dart
+import 'package:test/test.dart';
+
+void main() {
+  late HttpServer server;
+  late Uri url;
+  setUp(() async {
+    server = await HttpServer.bind('localhost', 0);
+    url = Uri.parse('http://${server.address.host}:${server.port}');
+  });
+
+  tearDown(() async {
+    await server.close(force: true);
+    server = null;
+    url = null;
+  });
+
+  // ...
+}
+```
+
+[`setUp()`]: https://pub.dev/documentation/test/latest/test/setUp.html
+
+[`tearDown()`]: https://pub.dev/documentation/test/latest/test/tearDown.html
+
+## Running Tests
+
+A single test file can be run just using `dart test path/to/test.dart` (as of
+Dart 2.10 - prior sdk versions must use `pub run test` instead of `dart test`).
+
+![Single file being run via "dart test"](https://raw.githubusercontent.com/dart-lang/test/master/pkgs/test/image/test1.gif)
+
+Many tests can be run at a time using `dart test path/to/dir`.
+
+![Directory being run via "dart test".](https://raw.githubusercontent.com/dart-lang/test/master/pkgs/test/image/test2.gif)
+
+It's also possible to run a test on the Dart VM only by invoking it using `dart
+path/to/test.dart`, but this doesn't load the full test runner and will be
+missing some features.
+
+The test runner accepts one or more path arguments. If there are no path
+arguments the runner defaults to running tests under the `test/` directory. When
+running for `test/` or any other directory, the runner will recursively search
+the directory for files that match the test name pattern `*_test.dart`. The
+pattern can be overridden in `dart_test.yaml`. When a path argument is a file
+instead of a directory it will be run as a test, regardless of the file name.
+Arguments which use shell globbing should avoid including non-test files in the
+path argument. Tests which are run by other test runners may use a different
+default path, such as `integration_test/`. Directories other than `test/` are
+ignored by `dart test` unless passed explicitly as a path to run.
+
+You can select specific tests cases to run by name using `dart test -n "test
+name"`. The string is interpreted as a regular expression, and only tests whose
+description (including any group descriptions) match that regular expression
+will be run. You can also use the `-N` flag to run tests whose names contain a
+plain-text string.
+
+By default, tests are run in the Dart VM, but you can run them in the browser as
+well by passing `dart test -p chrome path/to/test.dart`. `test` will take
+care of starting the browser and loading the tests, and all the results will be
+reported on the command line just like for VM tests. In fact, you can even run
+tests on both platforms with a single command: `dart test -p "chrome,vm"
+path/to/test.dart`.
+
+By default each platform has a default compiler, but some of them support
+more than one compiler. You can choose which compiler to use by passing
+`dart test -c source`, which would run all VM tests from source instead of
+compiling them to kernel. This also supports targeting a specific platform
+using normal platform selectors, like this `dart test -c vm:source`.
+
+### Test Path Queries
+
+Some query parameters are supported on test paths, which allow you to filter the
+tests that will run within just those paths. These filters are merged with any
+global options that are passed, and all filters must match for a test to be ran.
+
+- **name**: Works the same as `--name` (simple contains check).
+  - This is the only option that supports more than one entry.
+- **full-name**: Requires an exact match for the name of the test.
+- **line**: Matches any test that originates from this line in the test suite.
+- **col**: Matches any test that originates from this column in the test suite.
+
+**Example Usage**: `dart test "path/to/test.dart?line=10&col=2"`
+
+#### Line/Col Matching Semantics
+
+The `line` and `col` filters match against the current stack trace taken from
+the invocation to the `test` function, and are considered a match if
+**any frame** in the trace meets **all** of the following criteria:
+
+* The URI of the frame matches the root test suite uri.
+  * This means it will not match lines from imported libraries.
+* If both `line` and `col` are passed, both must match **the same frame**.
+* The specific `line` and `col` to be matched are defined by the tools creating
+  the stack trace. This generally means they are 1 based and not 0 based, but
+  this package is not in control of the exact semantics and they may vary based
+  on platform implementations.
+
+### Sharding Tests
+
+Tests can also be sharded with the `--total-shards` and `--shard-index` arguments,
+allowing you to split up your test suites and run them separately. For example,
+if you wanted to run 3 shards of your test suite, you could run them as follows:
+
+```bash
+dart test --total-shards 3 --shard-index 0 path/to/test.dart
+dart test --total-shards 3 --shard-index 1 path/to/test.dart
+dart test --total-shards 3 --shard-index 2 path/to/test.dart
+```
+Sharding: This refers to the process of splitting up a large test suite into
+smaller subsets (called shards) that can be run independently. Sharding is
+particularly useful for distributed testing, where multiple machines are used
+to run tests simultaneously. By dividing the test suite into smaller subsets,
+you can run tests in parallel across multiple machines, which can significantly
+reduce the overall testing time.
+
+### Test concurrency
+
+Test suites run concurrently by default, using half of the host's CPU cores. Use
+`--concurrency` to control the number of test suites that runs concurrently,
+meaning that multiple tests in independent suites or platforms can run at the
+same time. For example, if you wanted to run the tests on 4 threads, you could
+run the tests as follows:
+
+```bash
+dart test --concurrency=4
+```
+This can speed up the overall testing process, especially if you have a large
+number of test suites.
+
+### Shuffling Tests
+
+Test order can be shuffled with the `--test-randomize-ordering-seed` argument.
+This allows you to shuffle your tests with a specific seed (deterministic) or
+a random seed for each run. For example, consider the following test runs:
+
+```bash
+dart test --test-randomize-ordering-seed=12345
+dart test --test-randomize-ordering-seed=random
+```
+
+Setting `--test-randomize-ordering-seed=0` will have the same effect as not
+specifying it at all, meaning the test order will remain as-is.
+
+### Selecting a Test Reporter
+
+You can adjust the output format of test results using the `--reporter=<option>`
+command line flag. The default format is the `compact` output format - a single
+line, continuously updated as tests are run. When running on the GitHub Actions CI
+however (detected via checking the `GITHUB_ACTIONS` environment variable for `true`),
+the default changes to the `github` output format - a reporter customized
+for that CI/CD system.
+
+The available options for the `--reporter` flag are:
+
+- `compact`: a single, continuously updated line
+- `expanded`: a separate line for each update
+- `github`: a custom reporter for GitHub Actions
+- `json`: a machine-readable format; see https://dart.dev/go/test-docs/json_reporter.md
+
+### Collecting Code Coverage
+
+To collect code coverage, you can run tests with the `--coverage <directory>`
+argument. The directory specified can be an absolute or relative path.
+If a directory does not exist at the path specified, a directory will be
+created. If a directory does exist, files may be overwritten with the latest
+coverage data, if they conflict.
+
+This option will enable code coverage collection on a suite-by-suite basis,
+and the resulting coverage files will be outputted in the directory specified.
+The files can then be formatted using the `package:coverage`
+`format_coverage` executable.
+
+Coverage gathering is currently only implemented for tests run on the Dart VM or
+Chrome.
+
+Here's an example of how to run tests and format the collected coverage to LCOV:
+
+```shell
+## Run Dart tests and output them at directory `./coverage`:
+dart run test --coverage=./coverage
+
+## Activate package `coverage` (if needed):
+dart pub global activate coverage
+
+## Format collected coverage to LCOV (only for directory "lib")
+dart pub global run coverage:format_coverage --packages=.dart_tool/package_config.json --report-on=lib --lcov -o ./coverage/lcov.info -i ./coverage
+
+## Generate LCOV report:
+genhtml -o ./coverage/report ./coverage/lcov.info
+
+## Open the HTML coverage report:
+open ./coverage/report/index.html
+```
+
+* *LCOV is a GNU tool which provides information about what parts of a program are
+  actually executed (i.e. "covered") while running a particular test case.*
+* The binary `genhtml` is one of the LCOV tools.
+* See the LCOV project for more: https://github.com/linux-test-project/lcov
+* See the Homebrew LCOV formula: https://formulae.brew.sh/formula/lcov
+
+### Restricting Tests to Certain Platforms
+
+Some test files only make sense to run on particular platforms. They may use
+`dart:html` or `dart:io`, they might test Windows' particular filesystem
+behavior, or they might use a feature that's only available in Chrome. The
+[`@TestOn`] annotation makes it easy to declare exactly which platforms a test
+file should run on. Just put it at the top of your file, before any `library` or
+`import` declarations:
+
+```dart
+@TestOn('vm')
+
+import 'dart:io';
+
+import 'package:test/test.dart';
+
+void main() {
+  // ...
+}
+```
+
+[`@TestOn`]: https://pub.dev/documentation/test/latest/test/TestOn-class.html
+
+The string you pass to `@TestOn` is what's called a "platform selector", and it
+specifies exactly which platforms a test can run on. It can be as simple as the
+name of a platform, or a more complex Dart-like boolean expression involving
+these platform names.
+
+You can also declare that your entire package only works on certain platforms by
+adding a [`test_on` field] to your package config file.
+
+[`test_on` field]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#test_on
+
+### Platform Selectors
+
+Platform selectors use the [boolean selector syntax] defined in the
+[`boolean_selector`] package, which is a subset of Dart's expression syntax that
+only supports boolean operations. The following identifiers are defined:
+
+[boolean selector syntax]: https://github.com/dart-lang/boolean_selector/blob/master/README.md
+
+[`boolean_selector`]: https://pub.dev/packages/boolean_selector
+
+* `vm`: Whether the test is running on the command-line Dart VM.
+
+* `chrome`: Whether the test is running on Google Chrome.
+
+* `firefox`: Whether the test is running on Mozilla Firefox.
+
+* `safari`: Whether the test is running on Apple Safari.
+
+* `edge`: Whether the test is running on Microsoft Edge browser.
+
+* `node`: Whether the test is running on Node.js.
+
+* `dart-vm`: Whether the test is running on the Dart VM in any context. It's
+  identical to `!js`.
+
+* `browser`: Whether the test is running in any browser.
+
+* `js`: Whether the test has been compiled to JS. This is identical to
+  `!dart-vm`.
+
+* `blink`: Whether the test is running in a browser that uses the Blink
+  rendering engine.
+
+* `windows`: Whether the test is running on Windows. This can only be `true` if
+  either `vm` or `node` is true.
+
+* `mac-os`: Whether the test is running on MacOS. This can only be `true` if
+  either `vm` or `node` is true.
+
+* `linux`: Whether the test is running on Linux. This can only be `true` if
+  either `vm` or `node` is true.
+
+* `android`: Whether the test is running on Android. If `vm` is false, this will
+  be `false` as well, which means that this *won't* be true if the test is
+  running on an Android browser.
+
+* `ios`: Whether the test is running on iOS. If `vm` is false, this will be
+  `false` as well, which means that this *won't* be true if the test is running
+  on an iOS browser.
+
+* `posix`: Whether the test is running on a POSIX operating system. This is
+  equivalent to `!windows`.
+
+* `dart2js`: Whether the test has been compiled with Dart2Js.
+
+* `dart2wasm`: Whether the test has been compiled with Dart2Wasm.
+
+* `kernel`: Whether the test has been compiled to kernel.
+
+* `source`: Whether the test has been run with no compiler (from source).
+
+For example, if you wanted to run a test on every browser but Chrome, you would
+write `@TestOn('browser && !chrome')`.
+
+### Running Tests on Node.js
+
+The test runner also supports compiling tests to JavaScript and running them on
+[Node.js] by passing `--platform node`. Note that Node has access to *neither*
+`dart:html` nor `dart:io`, so any platform-specific APIs will have to be invoked
+using the [`js`] package. However, it may be useful when testing APIs that are
+meant to be used by JavaScript code.
+
+[Node.js]: https://nodejs.org/en/
+
+[`js`]: https://pub.dev/packages/js
+
+The test runner looks for an executable named `node` (on Mac OS or Linux) or
+`node.exe` (on Windows) on your system path. When compiling Node.js tests, it
+passes `-Dnode=true`, so tests can determine whether they're running on Node
+using [`const bool.fromEnvironment('node')`][bool.fromEnvironment]. It also sets
+`--server-mode`, which will tell the compiler that `dart:html` is not available.
+
+[bool.fromEnvironment]: https://api.dart.dev/stable/dart-core/bool/bool.fromEnvironment.html
+
+If a top-level `node_modules` directory exists, tests running on Node.js can
+import modules from it.
+
+## Asynchronous Tests
+
+Tests written with `async`/`await` will work automatically. The test runner
+won't consider the test finished until the returned `Future` completes.
+
+```dart
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test('Future.value() returns the value', () async {
+    var value = await Future.value(10);
+    expect(value, equals(10));
+  });
+}
+```
+
+### Uncaught Async Errors
+
+Any uncaught asynchronous error throws within the zone that a test is running in
+will cause the test to be considered a failure. This can cause a test which was
+previously considered complete and passing to change into a failure if the
+uncaught async error is raised late. If all test cases within the suite have
+completed this may cause some errors to be missed, or to surface in only some
+runs.
+
+Avoid uncaught async errors by ensuring that all futures have an error handler
+[before they complete as an error][early-handler].
+
+[early-handler]:https://dart.dev/guides/libraries/futures-error-handling#potential-problem-failing-to-register-error-handlers-early
+
+## Running Tests With Custom HTML
+
+By default, the test runner will generate its own empty HTML file for browser
+tests. However, tests that need custom HTML can create their own files. These
+files have three requirements:
+
+* They must have the same name as the test, with `.dart` replaced by `.html`. You can also
+  provide a configuration path to an HTML file if you want it to be reused across all tests.
+  See [Providing a custom HTML template](#providing-a-custom-html-template) below.
+
+* They must contain a `link` tag with `rel="x-dart-test"` and an `href`
+  attribute pointing to the test script.
+
+* They must contain `<script src="packages/test/dart.js"></script>`.
+
+For example, if you had a test called `custom_html_test.dart`, you might write
+the following HTML file:
+
+```html
+<!doctype html>
+<!-- custom_html_test.html -->
+<html>
+  <head>
+    <title>Custom HTML Test</title>
+    <link rel="x-dart-test" href="custom_html_test.dart">
+    <script src="packages/test/dart.js"></script>
+  </head>
+  <body>
+    // ...
+  </body>
+</html>
+```
+
+### 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.
+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.
+
+You can also optionally use any number of `{{testName}}` placeholders which will be replaced by the test filename.
+
+The template can't be named like any test file, as that would clash with using the
+custom HTML mechanics. In such a case, an error will be thrown.
+
+For example:
+
+```yaml
+custom_html_template_path: html_template.html.tpl
+```
+
+```html
+<!doctype html>
+<!-- html_template.html.tpl -->
+<html>
+  <head>
+    <title>{{testName}} Test</title>
+    {{testScript}}
+    <script src="packages/test/dart.js"></script>
+  </head>
+  <body>
+    // ...
+  </body>
+</html>
+```
+
+## Configuring Tests
+
+### Skipping Tests
+
+If a test, group, or entire suite isn't working yet, and you just want it to stop
+complaining, you can mark it as "skipped". The test or tests won't be run, and,
+if you supply a reason why, that reason will be printed. In general, skipping
+tests indicates that they should run but is temporarily not working. If they're
+fundamentally incompatible with a platform, [`@TestOn`/`testOn`][TestOn]
+should be used instead.
+
+[TestOn]: #restricting-tests-to-certain-platforms
+
+To skip a test suite, put a `@Skip` annotation at the top of the file:
+
+```dart
+@Skip('currently failing (see issue 1234)')
+
+import 'package:test/test.dart';
+
+void main() {
+  // ...
+}
+```
+
+The string you pass should describe why the test is skipped. You don't have to
+include it, but it's a good idea to document why the test isn't running.
+
+Groups and individual tests can be skipped by passing the `skip` parameter. This
+can be either `true` or a String describing why the test is skipped. For
+example:
+
+```dart
+import 'package:test/test.dart';
+
+void main() {
+  group('complicated algorithm tests', () {
+    // ...
+  }, skip: "the algorithm isn't quite right");
+
+  test('error-checking test', () {
+    // ...
+  }, skip: 'TODO: add error-checking.');
+}
+```
+
+### Timeouts
+
+By default, tests will time out after 30 seconds of inactivity. The timeout
+applies to deadlocks or cases where the test stops making progress, it does not
+ensure that an overall test case or test suite completes within any set time.
+
+Timeouts can be configured on a per-test, -group, or -suite basis. To change the
+timeout for a test suite, put a `@Timeout` annotation at the top of the file:
+
+```dart
+@Timeout(Duration(seconds: 45))
+
+import 'package:test/test.dart';
+
+void main() {
+  // ...
+}
+```
+
+In addition to setting an absolute timeout, you can set the timeout relative to
+the default using `@Timeout.factor`. For example, `@Timeout.factor(1.5)` will
+set the timeout to one and a half times as long as the default—45 seconds.
+
+Timeouts can be set for tests and groups using the `timeout` parameter. This
+parameter takes a `Timeout` object just like the annotation. For example:
+
+```dart
+import 'package:test/test.dart';
+
+void main() {
+  group('slow tests', () {
+    // ...
+
+    test('even slower test', () {
+      // ...
+    }, timeout: Timeout.factor(2));
+  }, timeout: Timeout(Duration(minutes: 1)));
+}
+```
+
+Nested timeouts apply in order from outermost to innermost. That means that
+"even slower test" will take two minutes to time out, since it multiplies the
+group's timeout by 2.
+
+### Platform-Specific Configuration
+
+Sometimes a test may need to be configured differently for different platforms.
+Windows might run your code slower than other platforms, or your DOM
+manipulation might not work right on Safari yet. For these cases, you can use
+the `@OnPlatform` annotation and the `onPlatform` named parameter to `test()`
+and `group()`. For example:
+
+```dart
+@OnPlatform({
+  // Give Windows some extra wiggle-room before timing out.
+  'windows': Timeout.factor(2)
+})
+
+import 'package:test/test.dart';
+
+void main() {
+  test('do a thing', () {
+    // ...
+  }, onPlatform: {
+    'safari': Skip('Safari is currently broken (see #1234)')
+  });
+}
+```
+
+Both the annotation and the parameter take a map. The map's keys are [platform
+selectors](#platform-selectors) which describe the platforms for which the
+specialized configuration applies. Its values are instances of some of the same
+annotation classes that can be used for a suite: `Skip` and `Timeout`. A value
+can also be a list of these values.
+
+If multiple platforms match, the configuration is applied in order from first to
+last, just as they would in nested groups. This means that for configuration
+like duration-based timeouts, the last matching value wins.
+
+You can also set up global platform-specific configuration using the
+[package configuration file][configuring platforms].
+
+[configuring platforms]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#configuring-platforms
+
+### Tagging Tests
+
+Tags are short strings that you can associate with tests, groups, and suites.
+They don't have any built-in meaning, but they're very useful nonetheless: you
+can associate your own custom configuration with them, or you can use them to
+easily filter tests so you only run the ones you need to.
+
+Tags are defined using the `@Tags` annotation for suites and the `tags` named
+parameter to `test()` and `group()`. For example:
+
+```dart
+@Tags(['browser'])
+
+import 'package:test/test.dart';
+
+void main() {
+  test('successfully launches Chrome', () {
+    // ...
+  }, tags: 'chrome');
+
+  test('launches two browsers at once', () {
+    // ...
+  }, tags: ['chrome', 'firefox']);
+}
+```
+
+If the test runner encounters a tag that wasn't declared in the
+[package configuration file][configuring tags], it'll print a warning, so be
+sure to include all your tags there. You can also use the file to provide
+default configuration for tags, like giving all `browser` tests twice as much
+time before they time out.
+
+[configuring tags]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#configuring-tags
+
+Tests can be filtered based on their tags by passing command line flags. The
+`--tags` or `-t` flag will cause the test runner to only run tests with the
+given tags, and the `--exclude-tags` or `-x` flag will cause it to only run
+tests *without* the given tags. These flags also support
+[boolean selector syntax]. For example, you can pass `--tags "(chrome ||
+firefox) && !slow"` to select quick Chrome or Firefox tests.
+
+Note that tags must be valid Dart identifiers, although they may also contain
+hyphens.
+
+### Whole-Package Configuration
+
+For configuration that applies across multiple files, or even the entire
+package, `test` supports a configuration file called `dart_test.yaml`. At its
+simplest, this file can contain the same sort of configuration that can be
+passed as command-line arguments:
+
+```yaml
+# This package's tests are very slow. Double the default timeout.
+timeout: 2x
+
+# This is a browser-only package, so test on chrome by default.
+platforms: [chrome]
+```
+
+The configuration file sets new defaults. These defaults can still be overridden
+by command-line arguments, just like the built-in defaults. In the example
+above, you could pass `--platform firefox` to run on Firefox.
+
+A configuration file can do much more than just set global defaults. See
+[the full documentation][package config] for more details.
+
+[package config]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md
+
+### Compiler flags
+
+The test runner does not support general purpose flags to control compilation
+such as `-D` defines or flags like `--no-sound-null-safety`. In most cases it is
+preferable to avoid writing tests that depend on the fine-grained compiler
+configuration. For instance to choose between sound and unsound null safety,
+prefer to choose a language version for each test which has the desired behavior
+by default - choose a language version below `2.12` to disable sound null
+safety, and a language version above `2.12` to enable sound null safety. When
+fine-grained configuration is unavoidable, the approach varies by platform.
+
+Compilation for browser and node tests can be configured by passing arguments to
+`dart compile js` with `--dart2js-args` options.
+
+Fine-grained compilation configuration is not supported for the VM. Any
+configuration which impacts runtime behavior for the entire VM, such as `-D`
+defines (when used for non-const values) and runtime behavior experiments, will
+influence both the test runner and the isolates spawned to run test suites.
+Experiments which are breaking may cause incompatibilities with the test runner.
+These may be specified with a `DART_VM_OPTIONS` environment variable when
+running with `pub run test`, or by passing them to the `dart` command before the
+`test` subcommand when using `dart test`.
+
+## Debugging
+
+Tests can be debugged interactively using platforms' built-in development tools.
+Tests running on browsers can use those browsers' development consoles to inspect
+the document, set breakpoints, and step through code. Those running on the Dart
+VM use [Dart DevTools][devtools].
+
+[devtools]: https://dart.dev/tools/dart-devtools
+
+The first step when debugging is to pass the `--pause-after-load` flag to the
+test runner. This pauses the browser after each test suite has loaded, so that
+you have time to open the development tools and set breakpoints. For the Dart VM
+it will print the remote debugger URL.
+
+Once you've set breakpoints, either click the big arrow in the middle of the web
+page or press Enter in your terminal to start the tests running. When you hit a
+breakpoint, the runner will open its own debugging console in the terminal that
+controls how tests are run. You can type "restart" there to re-run your test as
+many times as you need to figure out what's going on.
+
+Normally, browser tests are run in hidden iframes. However, when debugging, the
+iframe for the current test suite is expanded to fill the browser window so you
+can see and interact with any HTML it renders. Note that the Dart animation may
+still be visible behind the iframe; to hide it, just add a `background-color` to
+the page's HTML.
+
+## Browser/VM Hybrid Tests
+
+Code that's written for the browser often needs to talk to some kind of server.
+Maybe you're testing the HTML served by your app, or maybe you're writing a
+library that communicates over WebSockets. We call tests that run code on both
+the browser and the VM **hybrid tests**.
+
+Hybrid tests use one of two functions: [`spawnHybridCode()`] and
+[`spawnHybridUri()`]. Both of these spawn Dart VM
+[isolates][dart:isolate] that can import `dart:io` and other VM-only libraries.
+The only difference is where the code from the isolate comes from:
+`spawnHybridCode()` takes a chunk of actual Dart code, whereas
+`spawnHybridUri()` takes a URL. They both return a [`StreamChannel`] that
+communicates with the hybrid isolate. For example:
+
+[`spawnHybridCode()`]: https://pub.dev/documentation/test/latest/test/spawnHybridCode.html
+
+[`spawnHybridUri()`]: https://pub.dev/documentation/test/latest/test/spawnHybridUri.html
+
+[dart:isolate]: https://api.dart.dev/stable/dart-isolate/dart-isolate-library.html
+
+[`StreamChannel`]: https://pub.dev/documentation/stream_channel/latest/stream_channel/StreamChannel-class.html
+
+```dart
+// ## test/web_socket_server.dart
+
+// The library loaded by spawnHybridUri() can import any packages that your
+// package depends on, including those that only work on the VM.
+import 'package:shelf/shelf_io.dart' as io;
+import 'package:shelf_web_socket/shelf_web_socket.dart';
+import 'package:stream_channel/stream_channel.dart';
+
+// Once the hybrid isolate starts, it will call the special function
+// hybridMain() with a StreamChannel that's connected to the channel
+// returned spawnHybridCode().
+hybridMain(StreamChannel channel) async {
+  // Start a WebSocket server that just sends "hello!" to its clients.
+  var server = await io.serve(webSocketHandler((webSocket, _) {
+    webSocket.sink.add('hello!');
+  }), 'localhost', 0);
+
+  // Send the port number of the WebSocket server to the browser test, so
+  // it knows what to connect to.
+  channel.sink.add(server.port);
+}
+
+
+// ## test/web_socket_test.dart
+
+@TestOn('browser')
+
+import 'dart:html';
+
+import 'package:test/test.dart';
+
+void main() {
+  test('connects to a server-side WebSocket', () async {
+    // Each spawnHybrid function returns a StreamChannel that communicates with
+    // the hybrid isolate. You can close this channel to kill the isolate.
+    var channel = spawnHybridUri('web_socket_server.dart');
+
+    // Get the port for the WebSocket server from the hybrid isolate.
+    var port = await channel.stream.first;
+
+    var socket = WebSocket('ws://localhost:$port');
+    var message = await socket.onMessage.first;
+    expect(message.data, equals('hello!'));
+  });
+}
+```
+
+![A diagram showing a test in a browser communicating with a Dart VM isolate outside the browser.](https://raw.githubusercontent.com/dart-lang/test/master/pkgs/test/image/hybrid.png)
+
+**Note**: If you write hybrid tests, be sure to add a dependency on the
+`stream_channel` package, since you're using its API!
+
+## Support for Other Packages
+
+### `build_runner`
+
+If you are using `package:build_runner` to build your package, then you will
+need a dependency on `build_test` in your `dev_dependencies`, and then you can
+use the `pub run build_runner test` command to run tests.
+
+To supply arguments to `package:test`, you need to separate them from your build
+args with a `--` argument. For example, running all web tests in release mode
+would look like this `pub run build_runner test --release -- -p vm`.
+
+### `term_glyph`
+
+The [`term_glyph`] package provides getters for Unicode glyphs with
+ASCII alternatives. `test` ensures that it's configured to produce ASCII when
+the user is running on Windows, where Unicode isn't supported. This ensures that
+testing libraries can use Unicode on POSIX operating systems without breaking
+Windows users.
+
+[`term_glyph`]: https://pub.dev/packages/term_glyph
+
+## Further Reading
+
+Check out the [API docs] for detailed information about all the functions
+available to tests.
+
+[API docs]: https://pub.dev/documentation/test/latest/
+
+The test runner also supports a machine-readable JSON-based reporter. This
+reporter allows the test runner to be wrapped and its progress presented in
+custom ways (for example, in an IDE). See [the protocol documentation][json] for
+more details.
+
+[json]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/json_reporter.md
diff --git a/pkgs/test/bin/test.dart b/pkgs/test/bin/test.dart
new file mode 100644
index 0000000..0b40282
--- /dev/null
+++ b/pkgs/test/bin/test.dart
@@ -0,0 +1,5 @@
+// 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.
+
+export 'package:test/src/executable.dart';
diff --git a/pkgs/test/dart_test.yaml b/pkgs/test/dart_test.yaml
new file mode 100644
index 0000000..49af036
--- /dev/null
+++ b/pkgs/test/dart_test.yaml
@@ -0,0 +1,55 @@
+# Disallow duplicate test names in this package
+allow_duplicate_test_names: false
+
+# Fold frames from helper packages we use in our tests, but not from test
+# itself.
+fold_stack_frames:
+  except:
+  - shelf_test_handler
+  - stream_channel
+  - test_descriptor
+  - test_process
+
+presets:
+  # "-P terse-trace" folds frames from test's implementation to make the output
+  # less verbose when
+  terse-trace:
+    fold_stack_frames:
+      except: [test]
+
+tags:
+  browser:
+    timeout: 2x
+
+    # Browsers can sometimes randomly time out while starting, especially on
+    # Travis which is pretty slow. Don't retry locally because it makes
+    # debugging more annoying.
+    presets: {travis: {retry: 3}}
+
+  dart2js:
+    add_tags: [browser]
+    timeout: 2x
+
+  firefox:
+    add_tags: [dart2js]
+    test_on: linux
+  chrome: {add_tags: [dart2js]}
+
+  safari:
+    add_tags: [dart2js]
+    test_on: mac-os
+
+  edge:
+    add_tags: [dart2js]
+    test_on: windows
+
+  # Tests that run pub. These tests may need to be excluded when there are local
+  # dependency_overrides.
+  pub:
+    timeout: 2x
+
+  # Tests that use Node.js. These tests may need to be excluded on systems that
+  # don't have Node installed.
+  node:
+    timeout: 2x
+    test_on: linux
diff --git a/pkgs/test/doc/architecture.md b/pkgs/test/doc/architecture.md
new file mode 100644
index 0000000..c378e0c
--- /dev/null
+++ b/pkgs/test/doc/architecture.md
@@ -0,0 +1,236 @@
+# Test Package Architecture
+
+* [Code Organization](#code-organization)
+  * [Frontend](#frontend)
+  * [Backend](#backend)
+  * [Runner](#runner)
+* [Lifecycle of a Test Run](#lifecycle-of-a-test-run)
+  * [Loading a Suite on the VM](#loading-a-suite-on-the-vm)
+  * [Loading a Suite in the Browser](#loading-a-suite-in-the-browser)
+
+## Code Organization
+
+From a user's perspective, the test package provides two main pieces of
+functionality: an API for defining tests, and a command-line tool to run those
+tests. The structure of the package reflects this division. The code is divided
+into three main sections: the frontend, the backend, and the runner.
+
+### Frontend
+
+The [`lib/src/frontend`][frontend] directory contains APIs that are exposed to
+the user when they import `package:test/test.dart`. This includes core functions
+such as `expect()` and `expectAsync()`, test-specific matchers such as
+`throwsA()` and `prints()`, and annotation classes such as `TestOn` and
+`Timeout`. The functions that define the top-level structure of the test, such
+as `test()` and `group()`, are defined in `lib/test.dart`, but they can be
+thought of as frontend functions as well.
+
+[frontend]: https://github.com/dart-lang/test/tree/master/lib/src/frontend
+
+The frontend communicates with the backend using zone-scoped getters.
+[`Invoker.current`][Invoker] provides access to the current test case to
+matchers like [`completion()`][completion], for example to control when
+it completes. Structural functions use [`Declarer.current`][Declarer] to
+gradually build up an in-memory representation of a test suite. The runner is in
+charge of setting up these variables, but the frontend never communicates with
+the runner directly.
+
+[Invoker]: https://github.com/dart-lang/test/blob/master/lib/src/backend/invoker.dart
+[completion]: https://pub.dev/documentation/matcher/latest/expect/completion.html
+[Declarer]: https://github.com/dart-lang/test/blob/master/lib/src/backend/declarer.dart
+
+### Backend
+
+The [`lib/src/backend`][backend] directory contains classes that represent the
+in-memory structure of a test suite. A [`Suite`][Suite] represents a single test
+file, and class contains a tree of [`Group`][Group]s, each of which contains
+many [`Test`][Test]s. These classes are built using a [`Declarer`][Declarer].
+
+[backend]: https://github.com/dart-lang/test/tree/master/lib/src/backend
+[Suite]: https://github.com/dart-lang/test/blob/master/lib/src/backend/suite.dart
+[Group]: https://github.com/dart-lang/test/blob/master/lib/src/backend/group.dart
+[Test]: https://github.com/dart-lang/test/blob/master/lib/src/backend/test.dart
+
+The backend also contains the [`Invoker`][Invoker], which is responsible for
+actually running an individual test case—including tracking how many outstanding
+asynchronous callbacks are pending, handling exceptions, and timing out the test
+if it takes too long. The `Invoker` provides information about the status of a
+running test as streams and futures on a [`LiveTest`][LiveTest] object.
+
+[LiveTest]: https://github.com/dart-lang/test/blob/master/lib/src/backend/live_test.dart
+
+The backend provides a bridge between the frontend and the runner. The runner
+sets up the `Declarer` and starts the `Invoker`, which the frontend functions
+then communicate with directly.
+
+### Runner
+
+The [`lib/src/runner`][runner] directory contains the code that's executed when
+`dart test` is invoked. It's in charge of locating test files, loading them,
+executing them, and communicating their results to the user. It's also by far
+the biggest section. For more information on the runner architecture, see
+[Lifecycle of a Test Run](#lifecycle-of-a-test-suite) below.
+
+[runner]: https://github.com/dart-lang/test/tree/master/lib/src/runner
+
+## Lifecycle of a Test Run
+
+To understand generally how the test runner works, let's look at an example run.
+When the user first invokes `dart test`, the command-line arguments and
+[configuration files][] are combined into a single
+[`Configuration`][Configuration] object which is passed into the
+[`Runner`][Runner] class. The `Runner` is mostly just glue: it starts up the
+various components necessary for a test run, and connects them to one another.
+It's also in charge of handling certain `Configuration` flags.
+
+[configuration files]: https://github.com/dart-lang/test/blob/master/doc/configuration.md
+[Configuration]: https://github.com/dart-lang/test/tree/master/lib/src/runner/configuration.dart
+[Runner]: https://github.com/dart-lang/test/tree/master/lib/src/runner.dart
+
+The first thing the runner starts is the [`Engine`][Engine]. The engine iterates
+through a test suite's tests and invokes them in order. It knows how to handle
+set-up and tear-down functions, and how to combine the output of multiple test
+suites running concurrently. It exposes its progress through a collection of
+getters and streams that provide access to individual [`LiveTest`][LiveTest]s.
+
+[Engine]: https://github.com/dart-lang/test/tree/master/lib/src/runner/engine.dart
+
+The runner then passes the `Engine` to a [`Reporter`][Reporter], which listens
+to the `Engine`'s streams and exposes the information there to the user, usually
+by printing human-readable text. [`CompactReporter`][CompactReporter] is the
+default on Posix platforms, but others may be selected based on the
+`Configuration`. Nearly everything the user sees comes through the reporter.
+
+[Reporter]: https://github.com/dart-lang/test/tree/master/lib/src/runner/reporter.dart
+[CompactReporter]: https://github.com/dart-lang/test/tree/master/lib/src/runner/reporter/compact.dart
+
+The `Engine` and `Reporter` can't do much of anything, though, without any test
+suites to run. The next step is to load those suites. The [`Loader`][Loader] is
+in charge of this part. It takes in file or directory paths and finds all the
+test files they contain—by default any files matching `*_test.dart`. It then
+proceeds to load each file on all the platforms specified in the `Configuration`
+that's also supported by the test suite.
+
+[Loader]: https://github.com/dart-lang/test/tree/master/lib/src/runner/loader.dart
+
+The specifics of loading suites differs based on whether the platform is a
+browser or the Dart VM. I'll cover each platform below, but for now let's stick
+to what they have in common. Every platform will emit a
+[`LoadSuite`][LoadSuite], which is a synthetic [`Suite`][Suite] containing a
+single test that, when invoked, produces the actual `Suite` defined in the test
+file.
+
+[LoadSuite]: https://github.com/dart-lang/test/tree/master/lib/src/runner/load_suite.dart
+
+Wrapping the loading process in a synthetic `Suite` gives us the very useful
+invariant that *all test errors occur within a `Suite`*. Loading can fail in all
+sorts of ways—the code might not compile, the `main()` method might throw, the
+browser might not be installed, and so on. Locating those errors within a
+`Suite` means that the `Engine` and `Reporter`, which already know how to deal
+with test errors, can deal with load errors in exactly the same way. It makes
+the load process a little more complex, but it makes everything else a lot
+cleaner.
+
+Once a `Suite` has been loaded, the runner does a little post-processing to make
+sure the `Configuration` is handled properly. It filters out tests whose tags
+don't match the `--tags` flag, or whose names don't match the `--name` flag.
+Then it passes the resulting `Suite`s on to the `Engine` and they begin to run.
+
+### Loading a Suite on the VM
+
+Let's start with looking at how suites are loaded on the Dart VM, since the
+process is substantially simpler than loading them on a browser. This loading is
+handled by the [`VMPlatform`][VMPlatform], which extends the
+[`PlatformPlugin`][PlatformPlugin] class. [Eventually][issue 49], we plan to
+support a user-accessible platform plugin API, so we model platforms as plugins
+to prepare for that.
+
+[VMPlatform]: https://github.com/dart-lang/test/tree/master/lib/src/runner/vm/platform.dart
+[PlatformPlugin]: https://github.com/dart-lang/test/tree/master/lib/src/runner/plugin/platform.dart
+[issue 49]: https://github.com/dart-lang/test/issues/49
+
+In its simplest form, a `PlatformPlugin`'s responsibility is just to create a
+[`StreamChannel`][StreamChannel] that connects the test runner to a remote
+isolate—everything else is handled by helper functions. The `VMPlatform` uses
+[`Isolate`][Isolate]s to dynamically load its test suites, and then communicates
+with them using an [`IsolateChannel`][IsolateChannel]. It passes in a `data:`
+URI containing Dart code that imports the user's code, and runs that code in the
+context of the [`serializeSuite()`][remote platform helpers] helper, and the
+`PlatformPlugin` superclass deserializes it on the other side using
+[`deserializeSuite()`][platform helpers].
+
+[StreamChannel]: https://pub.dev/packages/stream_channel
+[Isolate]: https://api.dart.dev/stable/dart-isolate/Isolate-class.html
+[IsolateChannel]: https://pub.dev/documentation/stream_channel/latest/stream_channel/IsolateChannel-class.html
+[remote platform helpers]: https://github.com/dart-lang/test/tree/master/lib/src/runner/plugin/remote_platform_helpers.dart
+[platform helpers]: https://github.com/dart-lang/test/tree/master/lib/src/runner/plugin/platform_helpers.dart
+
+When a test suite is serialized and deserialized, it's not just converted to and
+from some static representation like JSON. The [`Engine`][Engine] needs
+fine-grained control over the remote suite, and the [`Reporter`][Reporter] needs
+fine-grained access to the [`LiveTest`][LiveTest]s it emits. To make this work,
+the helper functions use the [`MultiChannel`][MultiChannel] class to tunnel
+streams for each test through the main `IsolateChannel`. Each test has its own
+virtual channel that gets a message when the test runner calls
+[`Test.load()`][Test], and that sends messages back to indicate the progress of
+the test.
+
+Information about these virtual channels, as well as test names and metadata,
+are bundled up into a JSON object and sent over the `IsolateChannel` to be
+deserialized. The deserialization process then converts them into
+[`RunnerTest`][RunnerTest]s within a [`RunnerSuite`][RunnerSuite], which the
+`Engine` can then run just like normal `Test`s in a normal [`Suite`][Suite].
+
+[MultiChannel]: https://pub.dev/documentation/stream_channel/latest/stream_channel/MultiChannel-class.html
+[RunnerTest]: https://github.com/dart-lang/test/tree/master/lib/src/runner/runner_test.dart
+[RunnerSuite]: https://github.com/dart-lang/test/tree/master/lib/src/runner/runner_suite.dart
+
+### Loading a Suite in the Browser
+
+The [`BrowserPlatform`][BrowserPlatform] class also extends
+[`PlatformPlugin`][PlatformPlugin], but rather than just emitting a
+[`StreamChannel`][StreamChannel] and letting the plugin helpers do the rest, it
+takes more control over the loading process. It emits its own
+[`RunnerSuite`][RunnerSuite], which allows it to expose its own
+[`Environment`][Environment] to enable debugging.
+
+[BrowserPlatform]: https://github.com/dart-lang/test/tree/master/lib/src/runner/browser/platform.dart
+[Environment]: https://github.com/dart-lang/test/tree/master/lib/src/runner/environment.dart
+
+Whereas the [`VMPlatform`][VMPlatform] loads each separate suite in isolation,
+the `BrowserPlatform` shares a substantial amount of resources between suites.
+All suites load their code from a single HTTP server, which is managed by the
+platform. This server provides access to compiled JavaScript for other browsers,
+and to HTML files that bootstrap the tests.
+
+In addition to sharing a server, when multiple suites are loaded for the same
+browser, they all share a tab within that browser. Each separate browser is
+controlled by its own [`BrowserManager`][BrowserManager], which uses
+`WebSocket`s to communicate with Dart code running in the main frame—also known
+as [the host][host].
+
+[BrowserManager]: https://github.com/dart-lang/test/tree/master/lib/src/runner/browser/browser_manager.dart
+[host]: https://github.com/dart-lang/test/tree/master/lib/src/runner/browser/static/host.dart
+
+Each browser is spawned with a tab pointing to
+`packages/test/src/runner/browser/static/index.html`, the host page. The host's
+code then opens a `WebSocket` connection to a dynamically-generated URL. This
+URL tells the `BrowserPlatform` which `BrowserManager` to send the `WebSocket`
+to.
+
+To load a suite for this browser, the `BrowserPlatform` passes the URL for that
+suite's HTML file to the `BrowserManager`, which in turn sends it down to the
+host page. The host opens this HTML in an iframe, opens a
+[`StreamChannel`][StreamChannel] with this iframe using
+[`Window.postMessage()`][Window.postMessage]. It then tunnels this channel
+through the `WebSocket` connection, again using [`MultiChannel`][MultiChannel],
+so that the `BrowserManager` has a direct line to the iframe where the tests are
+defined.
+
+[Window.postMessage]: https://api.dart.dev/stable/dart-html/Window/postMessage.html
+
+From this point forward the process is similar to `VMPlatform`. The iframe
+serializes its test suite using [`serializeSuite()`][remote platform helpers],
+and the `BrowserManager` deserializes it using
+[`deserializeSuite()`][platform helpers]. It's then forwarded to the `Loader`
+via the `BrowserPlatform`.
diff --git a/pkgs/test/doc/configuration.md b/pkgs/test/doc/configuration.md
new file mode 100644
index 0000000..6541b97
--- /dev/null
+++ b/pkgs/test/doc/configuration.md
@@ -0,0 +1,890 @@
+Each package may include a configuration file that applies to the package as a
+whole. This file can be used to provide custom defaults for various options, to
+define configuration for multiple files, and more.
+
+The file is named `dart_test.yaml` and lives at the root of the package, next to
+the package's pubspec. Like the pubspec, it's a [YAML][] file. Here's an
+example:
+
+[YAML]: http://yaml.org/
+
+```yaml
+# This package's tests are very slow. Double the default timeout.
+timeout: 2x
+
+# This is a browser-only package, so test on chrome by default.
+platforms: [chrome]
+
+tags:
+  # Integration tests are even slower, so increase the timeout again.
+  integration: {timeout: 2x}
+
+  # Sanity tests are quick and verify that nothing is obviously wrong. Declaring
+  # the tag allows us to tag tests with it without getting warnings.
+  sanity:
+```
+
+* [Test Configuration](#test-configuration)
+  * [`timeout`](#timeout)
+  * [`ignore-timeouts`](#ignore-timeouts)
+  * [`verbose_trace`](#verbose_trace)
+  * [`chain_stack_traces`](#chain_stack_traces)
+  * [`js_trace`](#js_trace)
+  * [`skip`](#skip)
+  * [`retry`](#retry)
+  * [`test_on`](#test_on)
+  * [`allow_test_randomization`](#allow_test_randomization)
+  * [`allow_duplicate_test_names`](#allow_duplicate_test_names)
+* [Runner Configuration](#runner-configuration)
+  * [`include`](#include)
+  * [`paths`](#paths)
+  * [`filename`](#filename)
+  * [`names`](#names)
+  * [`plain_names`](#plain_names)
+  * [`include_tags`](#include_tags)
+  * [`exclude_tags`](#exclude_tags)
+  * [`platforms`](#platforms)
+  * [`concurrency`](#concurrency)
+  * [`pause_after_load`](#pause_after_load)
+  * [`run_skipped`](#run_skipped)
+  * [`reporter`](#reporter)
+  * [`file_reporters`](#file_reporters)
+  * [`fold_stack_frames`](#fold_stack_frames)
+  * [`custom_html_template_path`](#custom_html_template_path)
+* [Configuring Tags](#configuring-tags)
+  * [`tags`](#tags)
+  * [`add_tags`](#add_tags)
+* [Configuring Platforms](#configuring-platforms)
+  * [`on_os`](#on_os)
+  * [`on_platform`](#on_platform)
+  * [`override_platforms`](#override_platforms)
+  * [`define_platforms`](#define_platforms)
+  * [Browser and Node.js Settings](#browser-and-nodejs-settings)
+    * [`arguments`](#arguments)
+    * [`executable`](#executable)
+    * [`headless`](#headless)
+* [Configuration Presets](#configuration-presets)
+  * [`presets`](#presets)
+  * [`add_presets`](#add_presets)
+* [Global Configuration](#global-configuration)
+
+## Test Configuration
+
+There are two major categories of configuration field: "test" and "runner". Test
+configuration controls how individual tests run, while
+[runner configuration](#runner-configuration) controls the test runner as a
+whole. Both types of fields may be used at the top level of a configuration
+file. However, because different tests can have different test configuration,
+only test configuration fields may be used to [configure tags](#tags) or
+[platforms](#on_platform).
+
+### `timeout`
+
+This field indicates how much time the test runner should allow a test to remain
+inactive before it considers that test to have failed. It has three possible
+formats:
+
+* The string "none" indicates that tests should never time out.
+
+* A number followed by a unit abbreviation indicates an exact time. For example,
+  "1m" means a timeout of one minute, and "30s" means a timeout of thirty
+  seconds. Multiple numbers can be combined, as in "1m 30s".
+
+* A number followed by "x" indicates a multiple. This is applied to the default
+  value of 30s.
+
+```yaml
+timeout: 1m
+```
+
+###  `ignore-timeouts`
+
+This field disables all timeouts for all tests. This can be useful when debugging, so tests don't time out during debug sessions. It defaults to `false`.
+
+```yaml
+ignore-timeouts: true
+```
+
+### `verbose_trace`
+
+This boolean field controls whether or not stack traces caused by errors are
+trimmed to remove internal stack frames. This includes frames from the Dart core
+libraries, the [`stack_trace`][stack_trace] package, and the `test` package
+itself. It defaults to `false`.
+
+[stack_trace]: https://pub.dev/packages/stack_trace
+
+```yaml
+verbose_trace: true
+```
+
+### `chain_stack_traces`
+
+This boolean field controls whether or not stack traces are chained.
+Disabling [`stack trace chaining`][stack trace chaining] will improve
+performance for heavily asynchronous code at the cost of debuggability.
+
+[stack trace chaining]: https://github.com/dart-lang/stack_trace/blob/master/README.md#stack-chains
+
+```yaml
+chain_stack_traces: false
+```
+
+### `js_trace`
+
+This boolean field controls whether or not stack traces caused by errors that
+occur while running Dart compiled to JS are converted back to Dart style. This
+conversion uses the source map generated by `dart2js` to approximate the
+original Dart line, column, and in some cases member name for each stack frame.
+It defaults to `false`.
+
+```yaml
+js_trace: true
+```
+
+### `skip`
+
+This field controls whether or not tests are skipped. It's usually applied to
+[specific tags](#configuring-tags) rather than used at the top level. Like the
+`skip` parameter for [`test()`][test], it can either be a boolean indicating
+whether the tests are skipped or a string indicating the reason they're skipped.
+
+[test]: https://pub.dev/documentation/test/latest/test/test.html
+
+```yaml
+tags:
+  chrome:
+    skip: "Our Chrome launcher is busted. See issue 1234."
+```
+
+This field is not supported in the
+[global configuration file](#global-configuration).
+
+### `retry`
+
+This int field controls how many times a test is retried upon failure.
+
+```yaml
+tags:
+  chrome:
+    retry: 3 # Retry chrome failures 3 times.
+```
+
+This field is not supported in the
+[global configuration file](#global-configuration).
+
+### `test_on`
+
+This field declares which platforms a test supports. It takes a
+[platform selector][platform selectors] and only allows tests to run on
+platforms that match the selector. It's often used with
+[specific tags](#configuring-tags) to ensure that certain features will only be
+tested on supported platforms.
+
+[platform selectors]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors
+
+```yaml
+tags:
+  # Test on browsers other than firefox
+  some_feature: {test_on: "browser && !firefox"}
+```
+
+The field can also be used at the top level of the configuration file to
+indicate that the entire package only supports a particular platform. If someone
+tries to run the tests on an unsupported platform, the runner will print a
+warning and skip that platform.
+
+```yaml
+# This package uses dart:io.
+test_on: vm
+```
+
+This field is not supported in the
+[global configuration file](#global-configuration).
+
+### `allow_test_randomization`
+
+This can be used to disable test randomization for certain tests, regardless
+of the `--test-randomize-ordering-seed` configuration.
+
+This is typically useful when a subset of your tests are order dependent, but
+you want to run the other ones with randomized ordering.
+
+```yaml
+tags:
+  doNotRandomize:
+    allow_test_randomization: false
+```
+
+### `allow_duplicate_test_names`
+
+This can be used to allow multiple tests in the same suite to have the same
+name. This is disabled by default because it is usually an indication of a
+mistake, and it causes problems with IDE integrations which run tests by name.
+
+It can be disabled for all tests:
+
+```yaml
+allow_duplicate_test_names: true
+```
+
+Or for tagged tests only (useful if migrating to enable this):
+
+```yaml
+tags:
+  allowDuplicates:
+    allow_duplicate_test_names: true
+```
+
+It cannot be globally enabled or configured on the command line - this would
+make it so that tests might pass on one users machine but not anothers which
+should be avoided.
+
+## Runner Configuration
+
+Unlike [test configuration](#test-configuration), runner configuration affects
+the test runner as a whole rather than individual tests. It can only be used at
+the top level of the configuration file.
+
+### `include`
+
+This field loads another configuration file. It's useful for repositories that
+contain multiple packages and want to share configuration among them. It takes a
+(usually relative) `file:` URL.
+
+If you have a repository with the following structure:
+
+```
+repo/
+  dart_test_base.yaml
+  package/
+    test/
+    dart_test.yaml
+    pubspec.yaml
+```
+
+```yaml
+# repo/dart_test_base.yaml
+filename: "test_*.dart"
+```
+
+```yaml
+# repo/package/dart_test.yaml
+include: ../dart_test_base.yaml
+```
+
+...tests in the `package` directory will use configuration from both
+`dart_test_base.yaml` and `dart_test.yaml`.
+
+The local configuration file's fields take precedence over those from an
+included file, so it's possible to override a base configuration.
+
+### `paths`
+
+This field indicates the default paths that the test runner should run. These
+paths are usually directories, although single filenames may be used as well.
+Paths must be relative, and they must be in URL format so that they're
+compatible across operating systems. This defaults to `[test]`.
+
+```yaml
+paths: [dart/test]
+
+paths:
+- test/instantaneous
+- test/fast
+- test/middling
+```
+
+This field is not supported in the
+[global configuration file](#global-configuration).
+
+### `filename`
+
+This field indicates the filename pattern that the test runner uses to find test
+files in directories. All files in directories passed on the command line (or in
+directories in [`paths`](#paths), if none are passed) whose basenames match this
+pattern will be loaded and run as tests.
+
+This supports the full [glob syntax][]. However, since it's only compared
+against a path's basename, path separators aren't especially useful. It defaults
+to `"*_test.dart"`.
+
+```yaml
+filename: "test_*.dart"
+```
+
+[glob syntax]: https://github.com/dart-lang/glob#syntax
+
+This field is not supported in the
+[global configuration file](#global-configuration).
+
+### `names`
+
+This field causes the runner to only run tests whose names match the given
+regular expressions. A test's name must match *all* regular expressions in
+`names`, as well as containing all strings in [`plain_names`](#plain_names), in
+order to be run.
+
+This is usually used in a [preset](#configuration-presets) to make it possible
+to quickly select a given set of tests.
+
+```yaml
+presets:
+  # Pass "-P chrome" to run only Chrome tests.
+  chrome:
+    names:
+    - "^browser:"
+    - "[Cc]hrome"
+```
+
+This field is not supported in the
+[global configuration file](#global-configuration).
+
+### `plain_names`
+
+This field causes the runner to only run tests whose names contain the given
+strings. A test's name must contain *all* strings in `plain_names`, as well as
+matching all regular expressions in [`names`](#names), in order to be run.
+
+This is usually used in a [preset](#configuration-presets) to make it possible
+to quickly select a given set of tests.
+
+```yaml
+presets:
+  # Pass "-P feature" to run only tests with "feature name" in the name.
+  feature:
+    plain_names:
+    - "feature name"
+```
+
+This field is not supported in the
+[global configuration file](#global-configuration).
+
+### `include_tags`
+
+This field causes the runner to only run tests whose tags match the given
+[boolean selector][]. If both `include_tags` and [`exclude_tags`](#exclude_tags)
+are used, the exclusions take precedence.
+
+[boolean selector]: https://github.com/dart-lang/boolean_selector/blob/master/README.md
+
+This is usually used in a [preset](#configuration-preset) to make it possible to
+quickly select a set of tests.
+
+```yaml
+presets:
+  # Pass "-P windowless" to run tests that don't open browser windows.
+  windowless:
+    include_tags: !browser || content-shell
+```
+
+This field is not supported in the
+[global configuration file](#global-configuration).
+
+### `exclude_tags`
+
+This field causes the runner not to run tests whose tags match the given
+[boolean selector][]. If both [`include_tags`](#include_tags) and `exclude_tags`
+are used, the exclusions take precedence.
+
+This is usually used in a [preset](#configuration-preset) to make it possible to
+quickly select a set of tests.
+
+```yaml
+presets:
+  # Pass "-P windowless" to run tests that don't open browser windows.
+  windowless:
+    exclude_tags: browser && !content-shell
+```
+
+This field is not supported in the
+[global configuration file](#global-configuration).
+
+### `platforms`
+
+This field indicates which platforms tests should run on by default. It allows
+the same platform identifiers that can be passed to `--platform`. If multiple
+platforms are included, the test runner will default to running tests on all of
+them. This defaults to `[vm]`.
+
+```yaml
+platforms: [chrome]
+
+platforms:
+- chrome
+- firefox
+```
+
+### `compilers`
+
+This field indicates which compilers tests should be compiled with by default.
+It allows the same compiler selectors that can be passed to `--compiler`. If
+a given platform has no supported compiler configured, it will use its default.
+
+```yaml
+compilers: [source]
+
+compilers:
+- source
+```
+
+### `concurrency`
+
+This field indicates the default number of test suites to run in parallel. More
+parallelism can improve the overall speed of running tests up to a point, but
+eventually it just adds more memory overhead without any performance gain. This
+defaults to approximately half the number of processors on the current machine.
+If it's set to 1, only one test suite will run at a time.
+
+```yaml
+concurrency: 3
+```
+
+### `pause_after_load`
+
+This field indicates that the test runner should pause for debugging after each
+test suite is loaded but before its tests are executed. If it's set,
+[`concurrency`](#concurrency) is automatically set to 1 and
+[`timeout`](#timeout) is automatically set to `none`.
+
+This is usually used in a [preset](#configuration-presets).
+
+```yaml
+presets:
+  # Pass "-P debug" to enable debugging configuration
+  debug:
+    pause_after_load: true
+    exclude_tags: undebuggable
+    reporter: expanded
+```
+
+### `run_skipped`
+
+This field indicates that the test runner should run tests even if they're
+marked as skipped.
+
+This is usually used in a [preset](#configuration-presets).
+
+```yaml
+presets:
+  # Pass "-P all" to run all tests
+  debug:
+    run_skipped: true
+    paths: ["test/", "extra_test/"]
+```
+
+### `reporter`
+
+This field indicates the default reporter to use. It may be set to "compact",
+"expanded", or "json" (although why anyone would want to default to JSON is
+beyond me). It defaults to "expanded" on Windows and "compact" everywhere else.
+
+```yaml
+reporter: expanded
+```
+
+This field is not supported in the
+[global configuration file](#global-configuration).
+
+### `file_reporters`
+
+This field specifies additional reporters to use that will write their output to
+a file rather than stdout. It should be a map of reporter names to filepaths.
+
+```yaml
+file_reporters:
+  json: reports/tests.json
+```
+
+This field is not supported in the
+[global configuration file](#global-configuration).
+
+### `fold_stack_frames`
+
+This field controls which packages' stack frames will be folded away
+when displaying stack traces. Packages contained in the `except`
+option will be folded. If `only` is provided, all packages not
+contained in this list will be folded. By default,
+frames from the `test` package and the `stream_channel`
+package are folded.
+
+```yaml
+fold_stack_frames:
+  except:
+  - test
+  - stream_channel
+```
+
+Sample stack trace, note the absence of `package:test`
+and `package:stream_channel`:
+```
+test/sample_test.dart 7:5   main.<fn>
+===== asynchronous gap ===========================
+dart:async                  _Completer.completeError
+test/sample_test.dart 8:3   main.<fn>
+===== asynchronous gap ===========================
+dart:async                  _asyncThenWrapperHelper
+test/sample_test.dart 5:27  main.<fn>
+```
+
+### `custom_html_template_path`
+
+This field specifies the path of the HTML template file to be used for tests run in an HTML environment.
+Any HTML file that is named the same as the test and in the same directory will take precedence over the template.
+For more information about the usage of this option see [Providing a custom HTML template](https://github.com/dart-lang/test/blob/master/README.md#providing-a-custom-html-template)
+
+## Configuring Tags
+
+### `tags`
+
+The `tag` field can be used to apply [test configuration](#test-configuration)
+to all tests [with a given tag][tagging tests] or set of tags. It takes a map
+from tag selectors to configuration maps. These configuration maps are just like
+the top level of the configuration file, except that they may not contain
+[runner configuration](#runner-configuration).
+
+[tagging tests]: https://github.com/dart-lang/test/blob/master/README.md#tagging-tests
+
+```yaml
+tags:
+  # Integration tests need more time to run.
+  integration:
+    timeout: 1m
+```
+
+Tags may also have no configuration associated with them. The test runner prints
+a warning whenever it encounters a tag it doesn't recognize, and this the best
+way to tell it that a tag exists.
+
+```yaml
+# We occasionally want to use --tags or --exclude-tags on these tags.
+tags:
+  # A test that spawns a browser.
+  browser:
+
+  # A test that needs Ruby installed.
+  ruby:
+```
+
+You can also use [boolean selector syntax][] to define configuration that
+involves multiple tags. For example:
+
+[boolean selector syntax]: https://github.com/dart-lang/boolean_selector/blob/master/README.md
+
+```yaml
+tags:
+  # Tests that invoke sub-processes tend to be a little slower.
+  ruby || python:
+    timeout: 1.5x
+```
+
+Tag configuration is applied at whatever level the tag appears—so if a group is
+tagged as `integration`, its timeout will take precedence over the suite's
+timeout but not any tests'. If the group itself had a timeout declared, the
+group's explicit timeout would take precedence over the tag.
+
+If multiple tags appear at the same level, and they have conflicting
+configurations, the test runner *does not guarantee* what order they'll be
+resolved in. In practice, conflicting configuration is pretty unlikely and it's
+easy to just explicitly specify what you want on the test itself.
+
+This field counts as [test configuration](#test-configuration). It is not
+supported in the [global configuration file](#global-configuration).
+
+### `add_tags`
+
+This field adds additional tags. It's technically
+[test configuration](#test-configuration), but it's usually used in more
+specific contexts. For example, when included in a tag's configuration, it can
+be used to enable tag inheritance, where adding one tag implicitly adds another
+as well. It takes a list of tag name strings.
+
+```yaml
+tags:
+  # Any test that spawns a browser.
+  browser:
+    timeout: 2x
+
+  # Tests that spawn specific browsers. These automatically get the browser tag
+  # as well.
+  chrome: {add_tags: [browser]}
+  firefox: {add_tags: [browser]}
+  safari: {add_tags: [browser]}
+  edge: {add_tags: [browser]}
+```
+
+This field is not supported in the
+[global configuration file](#global-configuration).
+
+## Configuring Platforms
+
+There are two different kinds of platform configuration.
+[Operating system configuration](#on_os) cares about the operating system on
+which test runner is running. It sets global configuration for the runner on
+particular OSes. [Test platform configuration](#on_platform), on the other hand,
+cares about the platform the *test* is running on (like the
+[`@OnPlatform` annotation][@OnPlatform]). It sets configuration for particular
+tests that are running on matching platforms.
+
+[@OnPlatform]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-specific-configuration
+
+### `on_os`
+
+This field applies configuration when specific operating systems are being used.
+It takes a map from operating system identifiers (the same ones that are used in
+[platform selectors][]) to configuration maps that are applied on those
+operating systems. These configuration maps are just like the top level of the
+configuration file, and allow any fields that may be used in the context where
+`on_os` was used.
+
+```yaml
+on_os:
+  windows:
+    # Both of these are the defaults anyway, but let's be explicit about it.
+    color: false
+    runner: expanded
+
+    # My Windows machine is real slow.
+    timeout: 2x
+
+  # My Linux machine has SO MUCH RAM.
+  linux:
+    concurrency: 500
+```
+
+This field counts as [test configuration](#test-configuration). If it's used in
+a context that only allows test configuration, it may only contain test
+configuration.
+
+### `on_platform`
+
+This field applies configuration to tests that are run on specific platforms. It
+takes a map from [platform selectors][] to configuration maps that are applied
+to tests run on those platforms. These configuration maps are just like the top
+level of the configuration file, except that they may not contain
+[runner configuration](#runner-configuration).
+
+```yaml
+# Our code is kind of slow on Blink and WebKit.
+on_platform:
+  chrome || safari: {timeout: 2x}
+```
+
+**Note**: operating system names that appear in `on_platform` refer to tests
+that are run on the Dart VM under that operating system. To configure all tests
+when running on a particular operating system, use [`on_os`](#on_os) instead.
+
+This field counts as [test configuration](#test-configuration).
+
+### `override_platforms`
+
+This field allows you to customize the settings for built-in test platforms. It
+takes a map from platform identifiers to settings for those platforms. For example:
+
+```yaml
+override_platforms:
+  chrome:
+    # The settings to override for this platform.
+    settings:
+      executable: chromium
+```
+
+This tells the test runner to use the `chromium` executable for Chrome tests. It
+calls that executable with the same logic and flags it normally uses for Chrome.
+
+Each platform can define exactly which settings it supports. All browsers and
+Node.js support [the same settings](#browser-and-node-js-settings), but the VM
+doesn't support any settings and so can't be overridden.
+
+### `define_platforms`
+
+You can define new platforms in terms of old ones using the `define_platforms`
+field. This lets you define variants of existing platforms without overriding
+the old ones. This field takes a map from the new platform identifiers to
+definitions for those platforms. For example:
+
+```yaml
+define_platforms:
+  # This identifier is used to select the platform with the --platform flag.
+  chromium:
+    # A human-friendly name for the platform.
+    name: Chromium
+
+    # The identifier for the platform that this is based on.
+    extends: chrome
+
+    # Settings for the new child platform.
+    settings:
+      executable: chromium
+```
+
+Once this is defined, you can run `dart test -p chromium` and it will run
+those tests in the Chromium browser, using the same logic it normally uses for
+Chrome. You can even use `chromium` in platform selectors; for example, you
+might pass `testOn: "chromium"` to declare that a test is Chromium-specific.
+User-defined platforms also count as their parents, so Chromium will run tests
+that say `testOn: "chrome"` as well.
+
+Each platform can define exactly which settings it supports. All browsers and
+Node.js support [the same settings](#browser-and-node-js-settings), but the VM
+doesn't support any settings and so can't be extended.
+
+This field is not supported in the
+[global configuration file](#global-configuration).
+
+### Browser and Node.js Settings
+
+All built-in browser platforms, as well as the built-in Node.js platform,
+provide the same settings that can be set using
+[`define_platforms`](#define_platforms), which control how their executables are
+invoked.
+
+#### `arguments`
+
+The `arguments` field provides extra arguments to the executable. It takes a
+string, and parses it in the same way as the POSIX shell:
+
+```yaml
+override_platforms:
+  firefox:
+    settings:
+      arguments: -headless
+```
+
+#### `executable`
+
+The `executable` field tells the test runner where to look for the executable to
+use to start the subprocess. It has three sub-keys, one for each supported
+operating system, which each take a path or an executable name:
+
+```yaml
+define_platforms:
+  chromium:
+    name: Chromium
+    extends: chrome
+
+    settings:
+      executable:
+        linux: chromium
+        mac_os: /Applications/Chromium.app/Contents/MacOS/Chromium
+        windows: Chromium\Application\chrome.exe
+```
+
+Executables can be defined in three ways:
+
+* As a plain basename, with no path separators. These executables are passed
+  directly to the OS, which looks them up using the `PATH` environment variable.
+
+* As an absolute path, which is used as-is.
+
+* **Only on Windows**, as a relative path. The test runner will look up this
+  path relative to the `LOCALAPPATA`, `PROGRAMFILES`, and `PROGRAMFILES(X86)`
+  environment variables, in that order.
+
+If a platform is omitted, it defaults to using the built-in executable location.
+
+As a shorthand, you can also define the same executable for all operating
+systems:
+
+```yaml
+define_platforms:
+  chromium:
+    name: Chromium
+    extends: chrome
+
+    settings:
+      executable: chromium
+```
+
+#### `headless`
+
+The `headless` field says whether or not to run the browser in headless mode.
+It defaults to `true`. It's currently only supported for Chrome:
+
+```yaml
+override_platforms:
+  chrome:
+    settings:
+      headless: false
+```
+
+Note that headless mode is always disabled when debugging.
+
+## Configuration Presets
+
+*Presets* are collections of configuration that can be explicitly selected on
+the command-line. They're useful for quickly selecting options that are
+frequently used together, for providing special configuration for continuous
+integration systems, and for defining more complex logic than can be expressed
+directly using command-line arguments.
+
+Presets can be selected on the command line using the `--preset` or `-P` flag.
+Any number of presets can be selected this way; if they conflict, the last one
+selected wins. Only presets that are defined in the configuration file may be
+selected.
+
+### `presets`
+
+This field defines which presets are available to select. It takes a map from
+preset names to configuration maps that are applied when those presets are
+selected. These configuration maps are just like the top level of the
+configuration file, and allow any fields that may be used in the context where
+`presets` was used.
+
+```yaml
+presets:
+  # Use this when you need completely un-munged stack traces.
+  debug:
+    verbose_trace: false
+    js_trace: true
+
+  # Shortcut for running only browser tests.
+  browser:
+    paths:
+    - test/runner/browser
+```
+
+The `presets` field counts as [test configuration](#test-configuration). It can
+be useful to use it in combination with other fields for advanced preset
+behavior.
+
+```yaml
+tags:
+  chrome:
+    skip: "Our Chrome launcher is busted. See issue 1234."
+
+    # Pass -P force to verify that the launcher is still busted.
+    presets: {force: {skip: false}}
+```
+
+### `add_presets`
+
+This field selects additional presets. It's technically
+[runner configuration](#runner-configuration), but it's usually used in more
+specific contexts. For example, when included in a preset's configuration, it
+can be used to enable preset inheritance, where selecting one preset implicitly
+selects another as well. It takes a list of preset name strings.
+
+```yaml
+presets:
+  # Shortcut for running only browser tests.
+  browser:
+    paths: [test/runner/browser]
+
+  # Shortcut for running only Chrome tests.
+  chrome:
+    filename: "chrome_*_test.dart"
+    add_presets: [browser]
+```
+
+## Global Configuration
+
+The test runner also supports a global configuration file. On Windows, this
+file's local defaults to `%LOCALAPPDATA%\DartTest.yaml`. On Unix, it defaults to
+`~/.dart_test.yaml`. It can also be explicitly set using the `DART_TEST_CONFIG`
+environment variable.
+
+The global configuration file supports a subset of the fields supported by the
+package-specific configuration file. In general, it doesn't support fields that
+are closely tied to the structure of an individual package. Fields that are not
+supported in the global configuration file say so in their documentation.
diff --git a/pkgs/test/doc/json_reporter.md b/pkgs/test/doc/json_reporter.md
new file mode 100644
index 0000000..be18ddb
--- /dev/null
+++ b/pkgs/test/doc/json_reporter.md
@@ -0,0 +1,504 @@
+JSON Reporter Protocol
+======================
+
+The test runner supports a JSON reporter which provides a machine-readable
+representation of the test runner's progress. This reporter is intended for use
+by IDEs and other tools to present a custom view of the test runner's operation
+without needing to parse output intended for humans.
+
+Note that the test runner is highly asynchronous, and users of this protocol
+shouldn't make assumptions about the ordering of events beyond what's explicitly
+specified in this document. It's possible for events from multiple tests to be
+intertwined, for a single test to emit an error after it completed successfully,
+and so on.
+
+## Usage
+
+Pass the `--reporter json` command-line flag to the test runner to activate the
+JSON reporter.
+
+    dart test --reporter json <path-to-test-file>
+
+You may also use the `--file-reporter` option to enable the JSON reporter output
+to a file, in addition to another reporter writing to stdout.
+
+    dart test --file-reporter json:reports/tests.json <path-to-test-file>
+
+The JSON stream will be emitted via standard output. It will be a stream of JSON
+objects, separated by newlines.
+
+See `json_reporter.schema.json` for a formal description of the protocol schema.
+See `test/runner/json_reporter_test.dart` for some sample output.
+
+## Compatibility
+
+The protocol emitted by the JSON reporter is considered part of the public API
+of the `test` package, and is subject to its [semantic versioning][semver]
+restrictions. In particular:
+
+[semver]: https://dart.dev/tools/pub/versioning#semantic-versions
+
+* No new feature will be added to the protocol without increasing the test
+  package's minor version number.
+
+* No breaking change will be made to the protocol without increasing the test
+  package's major version number.
+
+The following changes are not considered breaking. This is not necessarily a
+comprehensive list.
+
+* Adding a new attribute to an existing object.
+
+* Adding a new type of any object with a `type` parameter.
+
+* Adding new test state values.
+
+## Reading this Document
+
+Each major type of JSON object used by the protocol is described by a class.
+Classes have names which are referred to in this document, but are not used as
+part of the protocol. Classes have typed attributes, which refer to the types
+and names of attributes in the JSON objects. If an attribute's type is another
+class, that refers to a nested object. The special type `List<...>` indicates a
+JSON list of the given type.
+
+Classes can "extend" one another, meaning that the subclass has all the
+attributes of the superclass. Concrete subclasses can be distinguished by the
+specific value of their `type` attribute. Classes may be abstract, indicating
+that only their subclasses will ever be used.
+
+## Events
+
+### Event
+
+```
+abstract class Event {
+  // The type of the event.
+  //
+  // This is always one of the subclass types listed below.
+  String type;
+
+  // The time (in milliseconds) that has elapsed since the test runner started.
+  int time;
+}
+```
+
+This is the root class of the protocol. All root-level objects emitted by the
+JSON reporter will be subclasses of `Event`.
+
+### StartEvent
+
+```
+class StartEvent extends Event {
+  String type = "start";
+
+  // The version of the JSON reporter protocol being used.
+  //
+  // This is a semantic version, but it reflects only the version of the
+  // protocol—it's not identical to the version of the test runner itself.
+  String protocolVersion;
+
+  // The version of the test runner being used.
+  //
+  // This is null if for some reason the version couldn't be loaded.
+  String? runnerVersion;
+
+  // The pid of the VM process running the tests.
+  int pid;
+}
+```
+
+A single start event is emitted before any other events. It indicates that the
+test runner has started running.
+
+### AllSuitesEvent
+
+```
+class AllSuitesEvent extends Event {
+  String type = "allSuites";
+
+  /// The total number of suites that will be loaded.
+  int count;
+}
+```
+
+A single suite count event is emitted once the test runner knows the total
+number of suites that will be loaded over the course of the test run. Because
+this is determined asynchronously, its position relative to other events (except
+`StartEvent`) is not guaranteed.
+
+### SuiteEvent
+
+```
+class SuiteEvent extends Event {
+  String type = "suite";
+
+  /// Metadata about the suite.
+  Suite suite;
+}
+```
+
+A suite event is emitted before any `GroupEvent`s for groups in a given test
+suite. This is the only event that contains the full metadata about a suite;
+future events will refer to the suite by its opaque ID.
+
+### DebugEvent
+
+```
+class DebugEvent extends Event {
+  String type = "debug";
+
+  /// The suite for which debug information is reported.
+  int suiteID;
+
+  /// The HTTP URL for the Dart Observatory, or `null` if the Observatory isn't
+  /// available for this suite.
+  String? observatory;
+
+  /// The HTTP URL for the remote debugger for this suite's host page, or `null`
+  /// if no remote debugger is available for this suite.
+  String? remoteDebugger;
+}
+```
+
+A debug event is emitted after (although not necessarily directly after) a
+`SuiteEvent`, and includes information about how to debug that suite. It's only
+emitted if the `--debug` flag is passed to the test runner.
+
+Note that the `remoteDebugger` URL refers to a remote debugger whose protocol
+may differ based on the browser the suite is running on. You can tell which
+protocol is in use by the `Suite.platform` field for the suite with the given
+ID. Since the same browser instance is used for multiple suites, different
+suites may have the same `host` URL, although only one suite at a time will be
+active when `--pause-after-load` is passed.
+
+### GroupEvent
+
+```
+class GroupEvent extends Event {
+  String type = "group";
+
+  /// Metadata about the group.
+  Group group;
+}
+```
+
+A group event is emitted before any `TestStartEvent`s for tests in a given
+group. This is the only event that contains the full metadata about a group;
+future events will refer to the group by its opaque ID.
+
+This includes the implicit group at the root of each suite, which has a `null`
+name. However, it does *not* include implicit groups for the virtual suites
+generated to represent loading test files.
+
+If the group is skipped, a single `TestStartEvent` will be emitted for a test
+within the group, followed by a `TestDoneEvent` marked as skipped. The
+`group.metadata` field should *not* be used for determining whether a group is
+skipped.
+
+### TestStartEvent
+
+```
+class TestStartEvent extends Event {
+  String type = "testStart";
+
+  // Metadata about the test that started.
+  Test test;
+}
+```
+
+An event emitted when a test begins running. This is the only event that
+contains the full metadata about a test; future events will refer to the test by
+its opaque ID.
+
+If the test is skipped, its `TestDoneEvent` will have `skipped` set to `true`.
+The `test.metadata` should *not* be used for determining whether a test is
+skipped.
+
+### MessageEvent
+
+```
+class MessageEvent extends Event {
+  String type = "print";
+
+  // The ID of the test that printed a message.
+  int testID;
+
+  // The type of message being printed.
+  String messageType;
+
+  // The message that was printed.
+  String message;
+}
+```
+
+A `MessageEvent` indicates that a test emitted a message that should be
+displayed to the user. The `messageType` field indicates the precise type of
+this message. Different message types should be visually distinguishable.
+
+A message of type "print" comes from a user explicitly calling `print()`.
+
+A message of type "skip" comes from a test, or a section of a test, being
+skipped. A skip message shouldn't be considered the authoritative source that a
+test was skipped; the `TestDoneEvent.skipped` field should be used instead.
+
+### ErrorEvent
+
+```
+class ErrorEvent extends Event {
+  String type = "error";
+
+  // The ID of the test that experienced the error.
+  int testID;
+
+  // The result of calling toString() on the error object.
+  String error;
+
+  // The error's stack trace, in the stack_trace package format.
+  String stackTrace;
+
+  // Whether the error was a TestFailure.
+  bool isFailure;
+}
+```
+
+A `ErrorEvent` indicates that a test encountered an uncaught error. Note
+that this may happen even after the test has completed, in which case it should
+be considered to have failed.
+
+If a test is asynchronous, it may encounter multiple errors, which will result
+in multiple `ErrorEvent`s.
+
+### TestDoneEvent
+
+```
+class TestDoneEvent extends Event {
+  String type = "testDone";
+
+  // The ID of the test that completed.
+  int testID;
+
+  // The result of the test.
+  String result;
+
+  // Whether the test's result should be hidden.
+  bool hidden;
+
+  // Whether the test (or some part of it) was skipped.
+  bool skipped;
+}
+```
+
+An event emitted when a test completes. The `result` attribute indicates the
+result of the test:
+
+* `"success"` if the test had no errors.
+
+* `"failure"` if the test had a `TestFailure` but no other errors.
+
+* `"error"` if the test had an error other than a `TestFailure`.
+
+If the test encountered an error, the `TestDoneEvent` will be emitted after the
+corresponding `ErrorEvent`.
+
+The `hidden` attribute indicates that the test's result should be hidden and not
+counted towards the total number of tests run for the suite. This is true for
+virtual tests created for loading test suites, `setUpAll()`, and
+`tearDownAll()`. Only successful tests will be hidden.
+
+Note that it's possible for a test to encounter an error after completing. In
+that case, it should be considered to have failed, but no additional
+`TestDoneEvent` will be emitted. If a previously-hidden test encounters an
+error after completing, it should be made visible.
+
+### DoneEvent
+
+```
+class DoneEvent extends Event {
+  String type = "done";
+
+  // Whether all tests succeeded (or were skipped).
+  //
+  // Will be `null` if the test runner was close before all tests completed
+  // running.
+  bool? success;
+}
+```
+
+An event indicating the result of the entire test run. This will be the final
+event emitted by the reporter.
+
+## Other Classes
+
+### Test
+
+```
+class Test {
+  // An opaque ID for the test.
+  int id;
+
+  // The name of the test, including prefixes from any containing groups.
+  String name;
+
+  // The ID of the suite containing this test.
+  int suiteID;
+
+  // The IDs of groups containing this test, in order from outermost to
+  // innermost.
+  List<int> groupIDs;
+
+  // The (1-based) line on which the test was defined, or `null`.
+  int? line;
+
+  // The (1-based) column on which the test was defined, or `null`.
+  int? column;
+
+  // The URL for the file in which the test was defined, or `null`.
+  String? url;
+
+  // The (1-based) line in the original test suite from which the test
+  // originated.
+  //
+  // Will only be present if `root_url` is different from `url`.
+  int? root_line;
+
+  // The (1-based) line on in the original test suite from which the test
+  // originated.
+  //
+  // Will only be present if `root_url` is different from `url`.
+  int? root_column;
+
+  // The URL for the original test suite in which the test was defined.
+  //
+  // Will only be present if different from `url`.
+  String? root_url;
+
+  // This field is deprecated and should not be used.
+  Metadata metadata;
+}
+```
+
+A single test case. The test's ID is unique in the context of this test run.
+It's used elsewhere in the protocol to refer to this test without including its
+full representation.
+
+Most tests will have at least one group ID, representing the implicit root
+group. However, some may not; these should be treated as having no group
+metadata.
+
+The `line`, `column`, and `url` fields indicate the location the `test()`
+function was called to create this test. They're treated as a unit: they'll
+either all be `null` or they'll all be non-`null`. The URL is always absolute,
+and may be a `package:` URL.
+
+### Suite
+
+```
+class Suite {
+  // An opaque ID for the group.
+  int id;
+
+  // The platform on which the suite is running.
+  String platform;
+
+  // The path to the suite's file, or `null` if that path is unknown.
+  String? path;
+}
+```
+
+A test suite corresponding to a loaded test file. The suite's ID is unique in
+the context of this test run. It's used elsewhere in the protocol to refer to
+this suite without including its full representation.
+
+A suite's platform is one of the platforms that can be passed to the
+`--platform` option, or `null` if there is no platform (for example if the file
+doesn't exist at all). Its path is either absolute or relative to the root of
+the current package.
+
+### Group
+
+```
+class Group {
+  // An opaque ID for the group.
+  int id;
+
+  // The name of the group, including prefixes from any containing groups.
+  String name;
+
+  // The ID of the suite containing this group.
+  int suiteID;
+
+  // The ID of the group's parent group, unless it's the root group.
+  int? parentID;
+
+  // The number of tests (recursively) within this group.
+  int testCount;
+
+  // The (1-based) line on which the group was defined, or `null`.
+  int? line;
+
+  // The (1-based) column on which the group was defined, or `null`.
+  int? column;
+
+  // The URL for the file in which the group was defined, or `null`.
+  String? url;
+
+  // This field is deprecated and should not be used.
+  Metadata metadata;
+}
+```
+
+A group containing test cases. The group's ID is unique in the context of this
+test run. It's used elsewhere in the protocol to refer to this group without
+including its full representation.
+
+The implicit group at the root of each test suite has `null` `name` and
+`parentID` attributes.
+
+The `line`, `column`, and `url` fields indicate the location the `group()`
+function was called to create this group. They're treated as a unit: they'll
+either all be `null` or they'll all be non-`null`. The URL is always absolute,
+and may be a `package:` URL.
+
+### Metadata
+
+```
+class Metadata {
+  bool skip;
+
+  // The reason the tests was skipped, or `null` if it wasn't skipped.
+  String? skipReason;
+}
+```
+
+The metadata class is deprecated and should not be used.
+
+## Remote Debugger APIs
+
+When running browser tests with `--pause-after-load`, the test package embeds a
+few APIs in the JavaScript context of the host page. These allow tools to
+control the debugging process in the same way a user might do from the command
+line. They can be accessed by connecting to the remote debugger using the
+[`DebugEvent.remoteDebugger`](#DebugEvent) URL.
+
+All APIs are defined as methods on the top-level `dartTest` object. The
+following methods are available:
+
+### `resume()`
+
+Calling `resume()` when the test runner is paused causes it to resume running
+tests. If the test runner is not paused, it won't do anything. When
+`--pause-after-load` is passed, the test runner will pause after loading each
+suite but before any tests are run.
+
+This gives external tools a chance to use the remote debugger protocol to set
+breakpoints before tests have begun executing. They can start the test runner
+with `--pause-after-load`, connect to the remote debugger using the
+[`DebugEvent.remoteDebugger`](#DebugEvent) URL, set breakpoints, then call
+`dartTest.resume()` in the host frame when they're finished.
+
+### `restartCurrent()`
+
+Calling `restartCurrent()` when the test runner is running a test causes it to
+re-run that test once it completes its current run. It's intended to be called
+when the browser is paused, as at a breakpoint.
diff --git a/pkgs/test/doc/json_reporter.schema.json b/pkgs/test/doc/json_reporter.schema.json
new file mode 100644
index 0000000..25417e2
--- /dev/null
+++ b/pkgs/test/doc/json_reporter.schema.json
@@ -0,0 +1,220 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "description": "An event emitted by the test package's JSON reporter",
+
+  "definitions": {
+    "GroupEntry": {
+      "required": [
+        "id",
+        "suiteID",
+        "metadata",
+        "line",
+        "column",
+        "url"
+      ],
+      "properties": {
+        "id": {"type": "integer", "minimum": 0},
+        "suiteID": {"type": "integer", "minimum": 0},
+        "metadata": {"$ref": "#/definitions/Metadata"}
+      },
+      "oneOf": [
+        {
+          "properties": {
+            "line": {"type": "integer", "minimum": 1},
+            "column": {"type": "integer", "minimum": 1},
+            "url": {"type": "string", "format": "uri"}
+          }
+        },
+        {
+          "properties": {
+            "line": {"type": "null"},
+            "column": {"type": "null"},
+            "url": {"type": "null"}
+          }
+        }
+      ]
+    },
+
+    "Test": {
+      "allOf": [{"$ref": "#/definitions/GroupEntry"}],
+      "required": [
+        "name",
+        "groupIDs"
+      ],
+      "properties": {
+        "name": {"type": "string"},
+        "groupIDs": {
+          "type": "array",
+          "items": {"type": "integer", "minimum": 0}
+        }
+      }
+    },
+
+    "Suite": {
+      "required": ["id", "platform", "path"],
+      "properties": {
+        "id": {"type": "integer", "minimum": 0},
+        "platform": {
+          "oneOf": [{"type": "string"}, {"type": "null"}]
+        },
+        "path": {"type": "string"}
+      }
+    },
+
+    "Group": {
+      "allOf": [{"$ref": "#/definitions/GroupEntry"}],
+      "required": ["name"],
+      "properties": {
+        "parentID": {
+          "oneOf": [{"type": "integer", "minimum": 0}, {"type": "null"}]
+        },
+        "name": {
+          "oneOf": [{"type": "string"}, {"type": "null"}]
+        },
+        "testCount": {"type": "integer", "minimum": 0}
+      }
+    },
+
+    "Metadata": {
+      "required": ["skip", "skipReason"],
+      "properties": {
+        "skip": {"type": "boolean"},
+        "skipReason": {
+          "oneOf": [{"type": "string"}, {"type": "null"}]
+        }
+      }
+    }
+  },
+
+  "required": ["type", "time"],
+  "properties": {
+    "time": {"type": "integer", "minimum": 0},
+    "type": {"type": "string"}
+  },
+
+  "oneOf": [
+    {
+      "title": "StartEvent",
+      "required": ["protocolVersion", "runnerVersion"],
+      "properties": {
+        "type": {"enum": ["start"]},
+        "pid": {"type": "integer", "minimum": 0},
+        "protocolVersion": {"type": "string", "pattern": "^0\\.1\\."},
+        "runnerVersion": {
+          "oneOf": [{"type": "string"}, {"type": "null"}]
+        }
+      }
+    },
+
+    {
+      "title": "TestStartEvent",
+      "required": ["test"],
+      "properties": {
+        "type": {"enum": ["testStart"]},
+        "test": {"$ref": "#/definitions/Test"}
+      }
+    },
+
+    {
+      "title": "AllSuitesEvent",
+      "required": ["count"],
+      "properties": {
+        "type": {"enum": ["allSuites"]},
+        "count": {"type": "integer", "minimum": 0}
+      }
+    },
+
+    {
+      "title": "SuiteEvent",
+      "required": ["suite"],
+      "properties": {
+        "type": {"enum": ["suite"]},
+        "suite": {"$ref": "#/definitions/Suite"}
+      }
+    },
+
+    {
+      "title": "DebugEvent",
+      "required": ["suiteID"],
+      "properties": {
+        "type": {"enum": ["debug"]},
+        "suiteID": {"type": "integer", "minimum": 0},
+        "observatory": {
+          "oneOf": [{"type": "string", "format": "uri"}, {"type": "null"}]
+        },
+        "remoteDebugger": {
+          "oneOf": [{"type": "string", "format": "uri"}, {"type": "null"}]
+        }
+      }
+    },
+
+    {
+      "title": "GroupEvent",
+      "required": ["group"],
+      "properties": {
+        "type": {"enum": ["group"]},
+        "group": {"$ref": "#/definitions/Group"}
+      }
+    },
+
+    {
+      "title": "MessageEvent",
+      "required": ["testID", "message", "type"],
+      "properties": {
+        "type": {"enum": ["print"]},
+        "testID": {"type": "integer", "minimum": 0},
+        "message": {"type": "string"},
+        "messageType": {"type": "string", "enum": ["print", "skip"]}
+      }
+    },
+
+    {
+      "title": "ErrorEvent",
+      "required": ["testID", "error", "stackTrace", "isFailure"],
+      "properties": {
+        "type": {"enum": ["error"]},
+        "testID": {"type": "integer", "minimum": 0},
+        "error": {"type": "string"},
+        "stackTrace": {"type": "string"},
+        "isFailure": {"type": "boolean"}
+      }
+    },
+
+    {
+      "title": "TestDoneEvent",
+      "required": ["testID", "result", "hidden", "skipped"],
+      "properties": {
+        "type": {"enum": ["testDone"]},
+        "testID": {"type": "integer", "minimum": 0},
+        "result": {"type": "string", "enum": ["success", "failure", "error"]},
+        "hidden": {"type": "boolean"},
+        "skipped": {"type": "boolean"}
+      }
+    },
+
+    {
+      "title": "DoneEvent",
+      "required": ["success"],
+      "properties": {
+        "type": {"enum": ["done"]},
+        "success": {"type": "boolean"}
+      }
+    },
+
+    {
+      "title": "FutureEvent",
+      "description":
+        "A placeholder event to allow new events to be added in the future.",
+      "properties": {
+        "type": {
+          "not": {
+            "enum": [
+              "start", "testStart", "allSuites", "suite", "group", "print",
+              "error", "testDone", "done", "debug"
+            ]
+          }
+        }
+      }
+    }
+  ]
+}
diff --git a/pkgs/test/doc/package_config.md b/pkgs/test/doc/package_config.md
new file mode 120000
index 0000000..422034a
--- /dev/null
+++ b/pkgs/test/doc/package_config.md
@@ -0,0 +1 @@
+configuration.md
\ No newline at end of file
diff --git a/pkgs/test/image/hybrid.png b/pkgs/test/image/hybrid.png
new file mode 100644
index 0000000..554a28c
--- /dev/null
+++ b/pkgs/test/image/hybrid.png
Binary files differ
diff --git a/pkgs/test/image/test1.gif b/pkgs/test/image/test1.gif
new file mode 100644
index 0000000..289e3ee
--- /dev/null
+++ b/pkgs/test/image/test1.gif
Binary files differ
diff --git a/pkgs/test/image/test2.gif b/pkgs/test/image/test2.gif
new file mode 100644
index 0000000..7bae0fc
--- /dev/null
+++ b/pkgs/test/image/test2.gif
Binary files differ
diff --git a/pkgs/test/lib/bootstrap/browser.dart b/pkgs/test/lib/bootstrap/browser.dart
new file mode 100644
index 0000000..b70cf11
--- /dev/null
+++ b/pkgs/test/lib/bootstrap/browser.dart
@@ -0,0 +1,5 @@
+// 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.
+
+export '../src/bootstrap/browser.dart';
diff --git a/pkgs/test/lib/bootstrap/node.dart b/pkgs/test/lib/bootstrap/node.dart
new file mode 100644
index 0000000..0214d6b
--- /dev/null
+++ b/pkgs/test/lib/bootstrap/node.dart
@@ -0,0 +1,5 @@
+// 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.
+
+export '../src/bootstrap/node.dart';
diff --git a/pkgs/test/lib/bootstrap/vm.dart b/pkgs/test/lib/bootstrap/vm.dart
new file mode 100644
index 0000000..8115736
--- /dev/null
+++ b/pkgs/test/lib/bootstrap/vm.dart
@@ -0,0 +1,5 @@
+// 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.
+
+export 'package:test_core/src/bootstrap/vm.dart';
diff --git a/pkgs/test/lib/dart.js b/pkgs/test/lib/dart.js
new file mode 100644
index 0000000..3840ba7
--- /dev/null
+++ b/pkgs/test/lib/dart.js
@@ -0,0 +1,80 @@
+// 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.
+
+// This script runs in HTML files and loads the corresponding test scripts for
+// a JS browser. It's used by "pub serve" and user-authored HTML files;
+window.onload = function() {
+
+// Sends an error message to the server indicating that the script failed to
+// load.
+//
+// This mimics a MultiChannel-formatted message.
+var sendLoadException = function(message) {
+  window.parent.postMessage({
+    "data": [0, {"type": "loadException", "message": message}],
+    "exception": true,
+  }, window.location.origin);
+}
+
+// Listen for dartLoadException events and forward to the server.
+window.addEventListener('dartLoadException', function(e) {
+  sendLoadException(e.detail);
+});
+
+// The basename of the current page.
+var name = window.location.href.replace(/.*\//, '').replace(/#.*/, '');
+
+// Find <link rel="x-dart-test">.
+var links = document.getElementsByTagName("link");
+var testLinks = [];
+var length = links.length;
+for (var i = 0; i < length; ++i) {
+  if (links[i].rel == "x-dart-test") testLinks.push(links[i]);
+}
+
+if (testLinks.length != 1) {
+  sendLoadException(
+      'Expected exactly 1 <link rel="x-dart-test"> in ' + name + ', found ' +
+          testLinks.length + '.');
+  return;
+}
+
+var link = testLinks[0];
+
+if (link.href == '') {
+  sendLoadException(
+      'Expected <link rel="x-dart-test"> in ' + name + ' to have an "href" ' +
+          'attribute.');
+  return;
+}
+
+var script = document.createElement('script');
+
+if (typeof trustedTypes !== 'undefined') {
+  const sanitizer = trustedTypes.createPolicy('dart#test', {
+    createScriptURL: (input) => input + '.browser_test.dart.js'
+  });
+  script.src = sanitizer.createScriptURL(link.href);
+} else {
+  script.src = link.href + '.browser_test.dart.js';
+}
+
+script.onerror = function(event) {
+  var message = "Failed to load script at " + script.src +
+      (event.message ? ": " + event.message : ".");
+  sendLoadException(message);
+};
+
+Array.from(document.querySelectorAll('script')).some(currentScript => {
+  if (currentScript.nonce) {
+    script.nonce = currentScript.nonce;
+    return true;
+  }
+});
+
+var parent = link.parentNode;
+document.currentScript = script;
+parent.replaceChild(script, link);
+
+};
diff --git a/pkgs/test/lib/expect.dart b/pkgs/test/lib/expect.dart
new file mode 100644
index 0000000..5e62622
--- /dev/null
+++ b/pkgs/test/lib/expect.dart
@@ -0,0 +1,5 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+export 'package:matcher/expect.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/scaffolding.dart b/pkgs/test/lib/scaffolding.dart
new file mode 100644
index 0000000..06e9029
--- /dev/null
+++ b/pkgs/test/lib/scaffolding.dart
@@ -0,0 +1,5 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+export 'package:test_core/src/scaffolding.dart';
diff --git a/pkgs/test/lib/src/bootstrap/browser.dart b/pkgs/test/lib/src/bootstrap/browser.dart
new file mode 100644
index 0000000..1a66e2b
--- /dev/null
+++ b/pkgs/test/lib/src/bootstrap/browser.dart
@@ -0,0 +1,21 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test_core/src/runner/plugin/remote_platform_helpers.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/stack_trace_mapper.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,
+    {StreamChannel<Object?>? testChannel}) {
+  var channel = serializeSuite(getMain, hidePrints: false,
+      beforeLoad: (suiteChannel) async {
+    var serialized = await suiteChannel('test.browser.mapper').stream.first;
+    if (serialized is! Map) return;
+    setStackTraceMapper(JSStackTraceMapper.deserialize(serialized)!);
+  });
+  (testChannel ?? postMessageChannel()).pipe(channel);
+}
diff --git a/pkgs/test/lib/src/bootstrap/node.dart b/pkgs/test/lib/src/bootstrap/node.dart
new file mode 100644
index 0000000..21089a4
--- /dev/null
+++ b/pkgs/test/lib/src/bootstrap/node.dart
@@ -0,0 +1,18 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test_core/src/runner/plugin/remote_platform_helpers.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports
+
+import '../runner/node/socket_channel.dart';
+
+/// Bootstraps a browser test to communicate with the test runner.
+void internalBootstrapNodeTest(Function Function() getMain) {
+  var channel = serializeSuite(getMain, beforeLoad: (suiteChannel) async {
+    var serialized = await suiteChannel('test.node.mapper').stream.first;
+    if (serialized is! Map) return;
+    setStackTraceMapper(JSStackTraceMapper.deserialize(serialized)!);
+  });
+  socketChannel().then((socket) => socket.pipe(channel));
+}
diff --git a/pkgs/test/lib/src/executable.dart b/pkgs/test/lib/src/executable.dart
new file mode 100644
index 0000000..5780694
--- /dev/null
+++ b/pkgs/test/lib/src/executable.dart
@@ -0,0 +1,23 @@
+// 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.
+
+import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
+// ignore: implementation_imports
+import 'package:test_core/src/executable.dart' as executable;
+import 'package:test_core/src/runner/hack_register_platform.dart'; // ignore: implementation_imports
+
+import 'runner/browser/platform.dart';
+import 'runner/node/platform.dart';
+
+Future<void> main(List<String> args) async {
+  registerPlatformPlugin([Runtime.nodeJS], NodePlatform.new);
+  registerPlatformPlugin([
+    Runtime.chrome,
+    Runtime.edge,
+    Runtime.firefox,
+    Runtime.safari,
+  ], BrowserPlatform.start);
+
+  await executable.main(args);
+}
diff --git a/pkgs/test/lib/src/runner/browser/browser.dart b/pkgs/test/lib/src/runner/browser/browser.dart
new file mode 100644
index 0000000..b025ea2
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/browser.dart
@@ -0,0 +1,138 @@
+// 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.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:test_core/src/runner/application_exception.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/errors.dart'; // ignore: implementation_imports
+
+/// An interface for running browser instances.
+///
+/// This is intentionally coarse-grained: browsers are controlled primary from
+/// inside a single tab. Thus this interface only provides support for closing
+/// the browser and seeing if it closes itself.
+///
+/// Any errors starting or running the browser process are reported through
+/// [onExit].
+abstract class Browser {
+  String get name;
+
+  /// The remote debugger URL for this browser.
+  ///
+  /// This will  complete to `null` for browsers that don't support remote
+  /// debugging, or if the remote debugging URL can't be found.
+  Future<Uri?> get remoteDebuggerUrl async => null;
+
+  /// The underlying process.
+  ///
+  /// This will fire once the process has started successfully.
+  Future<Process> get _process => _processCompleter.future;
+  final _processCompleter = Completer<Process>();
+
+  /// Whether [close] has been called.
+  var _closed = false;
+
+  /// A future that completes when the browser exits.
+  ///
+  /// If there's a problem starting or running the browser, this will complete
+  /// with an error.
+  Future<void> get onExit => _onExitCompleter.future;
+  final _onExitCompleter = Completer<void>();
+
+  /// Standard IO streams for the underlying browser process.
+  final _ioSubscriptions = <StreamSubscription<String>>[];
+
+  final output = <String>[];
+
+  /// Creates a new browser.
+  ///
+  /// This is intended to be called by subclasses. They pass in [startBrowser],
+  /// which asynchronously returns the browser process. Any errors in
+  /// [startBrowser] (even those raised asynchronously after it returns) are
+  /// piped to [onExit] and will cause the browser to be killed.
+  Browser(Future<Process> Function() startBrowser) {
+    // 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.
+    runZonedGuarded(() async {
+      var process = await startBrowser();
+      _processCompleter.complete(process);
+
+      void drainOutput(Stream<List<int>> stream) {
+        try {
+          _ioSubscriptions.add(stream
+              .transform(utf8.decoder)
+              .transform(const LineSplitter())
+              .listen(output.add, cancelOnError: true));
+        } on StateError catch (_) {}
+      }
+
+      // If we don't drain the stdout and stderr the process can hang.
+      drainOutput(process.stdout);
+      drainOutput(process.stderr);
+
+      var exitCode = await process.exitCode;
+
+      // This hack dodges an otherwise intractable race condition. When the user
+      // presses Control-C, the signal is sent to the browser and the test
+      // runner at the same time. It's possible for the browser to exit before
+      // the [Browser.close] is called, which would trigger the error below.
+      //
+      // A negative exit code signals that the process exited due to a signal.
+      // However, it's possible that this signal didn't come from the user's
+      // Control-C, in which case we do want to throw the error. The only way to
+      // resolve the ambiguity is to wait a brief amount of time and see if this
+      // browser is actually closed.
+      if (!_closed && exitCode < 0) {
+        await Future<void>.delayed(const Duration(milliseconds: 200));
+      }
+
+      if (!_closed && exitCode != 0) {
+        var outputString = output.join('\n');
+        var message = '$name failed with exit code $exitCode.';
+        if (outputString.isNotEmpty) {
+          message += '\nStandard output:\n$outputString';
+        }
+
+        throw ApplicationException(message);
+      }
+
+      _onExitCompleter.complete();
+    }, (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());
+
+      if (_onExitCompleter.isCompleted) return;
+      _onExitCompleter.completeError(
+          ApplicationException(
+              'Failed to run $name: ${getErrorMessage(error)}.'),
+          stackTrace);
+    });
+  }
+
+  /// Kills the browser process.
+  ///
+  /// Returns the same [Future] as [onExit], except that it won't emit
+  /// exceptions.
+  Future<void> close() async {
+    _closed = true;
+
+    // If we don't manually close the stream the test runner can hang.
+    // For example this happens with Chrome Headless.
+    // See SDK issue: https://github.com/dart-lang/sdk/issues/31264
+    for (var stream in _ioSubscriptions) {
+      unawaited(stream.cancel());
+    }
+
+    (await _process).kill();
+
+    // Swallow exceptions. The user should explicitly use [onExit] for these.
+    return onExit.onError((_, __) {});
+  }
+}
diff --git a/pkgs/test/lib/src/runner/browser/browser_manager.dart b/pkgs/test/lib/src/runner/browser/browser_manager.dart
new file mode 100644
index 0000000..409fbb4
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/browser_manager.dart
@@ -0,0 +1,359 @@
+// 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.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:async/async.dart';
+import 'package:pool/pool.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test_api/backend.dart' show Compiler, Runtime, StackTraceMapper;
+import 'package:test_core/src/runner/application_exception.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/environment.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/load_exception.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/plugin/platform_helpers.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
+import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports
+import 'package:web_socket_channel/web_socket_channel.dart';
+
+import '../executable_settings.dart';
+import 'browser.dart';
+import 'chrome.dart';
+import 'firefox.dart';
+import 'microsoft_edge.dart';
+import 'safari.dart';
+
+/// A class that manages the connection to a single running browser.
+///
+/// This is in charge of telling the browser which test suites to load and
+/// converting its responses into [Suite] objects.
+class BrowserManager {
+  /// The browser instance that this is connected to via [_channel].
+  final Browser _browser;
+
+  // TODO(nweiz): Consider removing the duplication between this and
+  // [_browser.name].
+  /// The [Runtime] for [_browser].
+  final Runtime _runtime;
+
+  /// The channel used to communicate with the browser.
+  ///
+  /// This is connected to a page running `static/host.dart`.
+  late final MultiChannel<Object> _channel;
+
+  /// A pool that ensures that limits the number of initial connections the
+  /// manager will wait for at once.
+  ///
+  /// This isn't the *total* number of connections; any number of iframes may be
+  /// loaded in the same browser. However, the browser can only load so many at
+  /// once, and we want a timeout in case they fail so we only wait for so many
+  /// at once.
+  final _pool = Pool(8);
+
+  /// The ID of the next suite to be loaded.
+  ///
+  /// This is used to ensure that the suites can be referred to consistently
+  /// across the client and server.
+  int _suiteID = 0;
+
+  /// Whether the channel to the browser has closed.
+  bool _closed = false;
+
+  /// The completer for [_BrowserEnvironment.displayPause].
+  ///
+  /// This will be `null` as long as the browser isn't displaying a pause
+  /// screen.
+  CancelableCompleter<void>? _pauseCompleter;
+
+  /// The controller for [_BrowserEnvironment.onRestart].
+  final _onRestartController = StreamController<void>.broadcast();
+
+  /// The environment to attach to each suite.
+  late final Future<_BrowserEnvironment> _environment;
+
+  /// Controllers for every suite in this browser.
+  ///
+  /// These are used to mark suites as debugging or not based on the browser's
+  /// pings.
+  final _controllers = <RunnerSuiteController>{};
+
+  // A timer that's reset whenever we receive a message from the browser.
+  //
+  // Because the browser stops running code when the user is actively debugging,
+  // this lets us detect whether they're debugging reasonably accurately.
+  late final RestartableTimer _timer;
+
+  /// Starts the browser identified by [runtime] and has it connect to [url].
+  ///
+  /// [url] should serve a page that establishes a WebSocket connection with
+  /// this process. That connection, once established, should be emitted via
+  /// [future]. If [debug] is true, starts the browser in debug mode, with its
+  /// debugger interfaces on and detected.
+  ///
+  /// The [settings] indicate how to invoke this browser's executable.
+  ///
+  /// Returns the browser manager, or throws an [ApplicationException] if a
+  /// connection fails to be established.
+  static Future<BrowserManager> start(
+          Runtime runtime,
+          Uri url,
+          Future<WebSocketChannel> future,
+          ExecutableSettings settings,
+          Configuration configuration) =>
+      _start(runtime, url, future, settings, configuration, 1);
+
+  static const _maxRetries = 3;
+  static Future<BrowserManager> _start(
+      Runtime runtime,
+      Uri url,
+      Future<WebSocketChannel> future,
+      ExecutableSettings settings,
+      Configuration configuration,
+      int attempt) {
+    var browser = _newBrowser(url, runtime, settings, configuration);
+
+    var completer = Completer<BrowserManager>();
+
+    // TODO(nweiz): Gracefully handle the browser being killed before the
+    // tests complete.
+    browser.onExit
+        .then<void>((_) => throw ApplicationException(
+            '${runtime.name} exited before connecting.'))
+        .onError<Object>((error, stackTrace) {
+      if (!completer.isCompleted) {
+        completer.completeError(error, stackTrace);
+      }
+    });
+
+    future.then((webSocket) {
+      if (completer.isCompleted) return;
+      completer.complete(BrowserManager._(browser, runtime, webSocket));
+    }).onError((Object error, StackTrace stackTrace) {
+      browser.close();
+      if (completer.isCompleted) return;
+      completer.completeError(error, stackTrace);
+    });
+
+    return completer.future.timeout(const Duration(seconds: 30), onTimeout: () {
+      browser.close();
+      if (attempt >= _maxRetries) {
+        throw ApplicationException(
+            'Timed out waiting for ${runtime.name} to connect.\n'
+            'Browser output: ${browser.output.join('\n')}');
+      }
+      return _start(runtime, url, future, settings, configuration, ++attempt);
+    });
+  }
+
+  /// Starts the browser identified by [browser] using [settings] and has it load [url].
+  ///
+  /// If [debug] is true, starts the browser in debug mode.
+  static Browser _newBrowser(Uri url, Runtime browser,
+          ExecutableSettings settings, Configuration configuration) =>
+      switch (browser.root) {
+        Runtime.chrome => Chrome(url, configuration, settings: settings),
+        Runtime.firefox => Firefox(url, settings: settings),
+        Runtime.safari => Safari(url, settings: settings),
+        Runtime.edge => MicrosoftEdge(url, configuration, settings: settings),
+        _ => throw ArgumentError('$browser is not a browser.'),
+      };
+
+  /// Creates a new BrowserManager that communicates with [browser] over
+  /// [webSocket].
+  BrowserManager._(this._browser, this._runtime, WebSocketChannel webSocket) {
+    // The duration should be short enough that the debugging console is open as
+    // soon as the user is done setting breakpoints, but long enough that a test
+    // doing a lot of synchronous work doesn't trigger a false positive.
+    //
+    // Start this canceled because we don't want it to start ticking until we
+    // get some response from the iframe.
+    _timer = RestartableTimer(const Duration(seconds: 3), () {
+      for (var controller in _controllers) {
+        controller.setDebugging(true);
+      }
+    })
+      ..cancel();
+
+    // Whenever we get a message, no matter which child channel it's for, we the
+    // know browser is still running code which means the user isn't debugging.
+    _channel = MultiChannel(
+        webSocket.cast<String>().transform(jsonDocument).changeStream((stream) {
+      return stream.map((message) {
+        if (!_closed) _timer.reset();
+        for (var controller in _controllers) {
+          controller.setDebugging(false);
+        }
+
+        return message;
+      });
+    }));
+
+    _environment = _loadBrowserEnvironment();
+    _channel.stream.listen(
+        (message) => _onMessage(message as Map<Object, Object?>),
+        onDone: close);
+  }
+
+  /// Loads [_BrowserEnvironment].
+  Future<_BrowserEnvironment> _loadBrowserEnvironment() async =>
+      _BrowserEnvironment(
+          this, await _browser.remoteDebuggerUrl, _onRestartController.stream);
+
+  /// Tells the browser the load a test suite from the URL [url].
+  ///
+  /// [url] should be an HTML page with a reference to the JS-compiled test
+  /// suite. [path] is the path of the original test suite file, which is used
+  /// for reporting. [suiteConfig] is the configuration for the test suite.
+  ///
+  /// If [mapper] is passed, it's used to map stack traces for errors coming
+  /// from this test suite.
+  Future<RunnerSuite> load(String path, Uri url, SuiteConfiguration suiteConfig,
+      Map<String, Object?> message, Compiler compiler,
+      {StackTraceMapper? mapper, Duration? timeout}) async {
+    url = url.replace(
+        fragment: Uri.encodeFull(jsonEncode({
+      'metadata': suiteConfig.metadata.serialize(),
+      'browser': _runtime.identifier,
+      'compiler': compiler.serialize(),
+    })));
+
+    var suiteID = _suiteID++;
+    RunnerSuiteController? controller;
+    void closeIframe() {
+      if (_closed) return;
+      if (controller != null) _controllers.remove(controller);
+      _channel.sink.add({'command': 'closeSuite', 'id': suiteID});
+    }
+
+    // The virtual channel will be closed when the suite is closed, in which
+    // case we should unload the iframe.
+    var virtualChannel = _channel.virtualChannel();
+    var suiteChannelID = virtualChannel.id;
+    var suiteChannel = virtualChannel
+        .transformStream(StreamTransformer.fromHandlers(handleDone: (sink) {
+      closeIframe();
+      sink.close();
+    }));
+
+    var suite = _pool.withResource<RunnerSuite>(() async {
+      _channel.sink.add({
+        'command': 'loadSuite',
+        'url': url.toString(),
+        'id': suiteID,
+        'channel': suiteChannelID
+      });
+
+      try {
+        controller = deserializeSuite(
+            path,
+            currentPlatform(_runtime, compiler),
+            suiteConfig,
+            await _environment,
+            suiteChannel.cast(),
+            message, gatherCoverage: () async {
+          var browser = _browser;
+          if (browser is Chrome) return browser.gatherCoverage();
+          return {};
+        });
+
+        controller!
+            .channel('test.browser.mapper')
+            .sink
+            .add(mapper?.serialize());
+
+        _controllers.add(controller!);
+        return await controller!.suite;
+      } catch (_) {
+        closeIframe();
+        rethrow;
+      }
+    });
+    if (timeout != null) {
+      suite = suite.timeout(timeout, onTimeout: () {
+        throw LoadException(
+            path,
+            'Timed out waiting for browser to load test suite. '
+            'Browser output: ${_browser.output.join('\n')}');
+      });
+    }
+    return suite;
+  }
+
+  /// An implementation of [Environment.displayPause].
+  CancelableOperation<void> _displayPause() {
+    if (_pauseCompleter != null) return _pauseCompleter!.operation;
+
+    final pauseCompleter = _pauseCompleter = CancelableCompleter(onCancel: () {
+      _channel.sink.add({'command': 'resume'});
+      _pauseCompleter = null;
+    });
+
+    pauseCompleter.operation.value.whenComplete(() {
+      _pauseCompleter = null;
+    });
+
+    _channel.sink.add({'command': 'displayPause'});
+
+    return pauseCompleter.operation;
+  }
+
+  /// The callback for handling messages received from the host page.
+  void _onMessage(Map<Object, Object?> message) {
+    switch (message['command'] as String) {
+      case 'ping':
+        break;
+
+      case 'restart':
+        _onRestartController.add(null);
+        break;
+
+      case 'resume':
+        _pauseCompleter?.complete();
+        break;
+
+      default:
+        // Unreachable.
+        assert(false);
+        break;
+    }
+  }
+
+  /// Closes the manager and releases any resources it owns, including closing
+  /// the browser.
+  Future<void> close() => _closeMemoizer.runOnce(() {
+        _closed = true;
+        _timer.cancel();
+        _pauseCompleter?.complete();
+        _pauseCompleter = null;
+        _controllers.clear();
+        return _browser.close();
+      });
+  final _closeMemoizer = AsyncMemoizer<void>();
+}
+
+/// An implementation of [Environment] for the browser.
+///
+/// All methods forward directly to [BrowserManager].
+class _BrowserEnvironment implements Environment {
+  final BrowserManager _manager;
+
+  @override
+  final supportsDebugging = true;
+
+  @override
+  Null get observatoryUrl => null;
+
+  @override
+  final Uri? remoteDebuggerUrl;
+
+  @override
+  final Stream<void> onRestart;
+
+  _BrowserEnvironment(this._manager, this.remoteDebuggerUrl, this.onRestart);
+
+  @override
+  CancelableOperation<void> displayPause() => _manager._displayPause();
+}
diff --git a/pkgs/test/lib/src/runner/browser/chrome.dart b/pkgs/test/lib/src/runner/browser/chrome.dart
new file mode 100644
index 0000000..e7dff75
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/chrome.dart
@@ -0,0 +1,194 @@
+// 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.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:collection/collection.dart';
+import 'package:coverage/coverage.dart';
+import 'package:path/path.dart' as p;
+import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports
+import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
+
+import '../executable_settings.dart';
+import 'browser.dart';
+import 'chromium.dart';
+import 'default_settings.dart';
+
+/// A class for running an instance of Chrome.
+///
+/// Most of the communication with the browser is expected to happen via HTTP,
+/// so this exposes a bare-bones API. The browser starts as soon as the class is
+/// constructed, and is killed when [close] is called.
+///
+/// Any errors starting or running the process are reported through [onExit].
+class Chrome extends Browser {
+  @override
+  final name = 'Chrome';
+
+  @override
+  final Future<Uri?> remoteDebuggerUrl;
+
+  final Future<WipConnection> _tabConnection;
+  final Map<String, String> _idToUrl;
+
+  /// Starts a new instance of Chrome open to the given [url], which may be a
+  /// [Uri] or a [String].
+  factory Chrome(Uri url, Configuration configuration,
+      {ExecutableSettings? settings}) {
+    settings ??= defaultSettings[Runtime.chrome]!;
+    var remoteDebuggerCompleter = Completer<Uri?>.sync();
+    var connectionCompleter = Completer<WipConnection>();
+    var idToUrl = <String, String>{};
+    return Chrome._(() async {
+      Future<Process> tryPort([int? port]) async {
+        var process = await ChromiumBasedBrowser.chrome.spawn(
+          url,
+          configuration,
+          settings: settings,
+          additionalArgs: [
+            if (port != null)
+              // Chrome doesn't provide any way of ensuring that this port was
+              // successfully bound. It produces an error if the binding fails,
+              // but without a reliable and fast way to tell if it succeeded
+              // that doesn't provide us much. It's very unlikely that this port
+              // will fail, though.
+              '--remote-debugging-port=$port',
+          ],
+        );
+
+        if (port != null) {
+          remoteDebuggerCompleter.complete(
+              getRemoteDebuggerUrl(Uri.parse('http://localhost:$port')));
+
+          connectionCompleter.complete(_connect(process, port, idToUrl, url));
+        } else {
+          remoteDebuggerCompleter.complete(null);
+        }
+
+        return process;
+      }
+
+      if (!configuration.debug) return tryPort();
+      return getUnusedPort<Process>(tryPort);
+    }, remoteDebuggerCompleter.future, connectionCompleter.future, idToUrl);
+  }
+
+  /// Returns a Dart based hit-map containing coverage report, suitable for use
+  /// with `package:coverage`.
+  Future<Map<String, dynamic>> gatherCoverage() async {
+    var tabConnection = await _tabConnection;
+    var response = await tabConnection.debugger.connection
+        .sendCommand('Profiler.takePreciseCoverage', {});
+    var result =
+        (response.result!['result'] as List).cast<Map<String, dynamic>>();
+    var httpClient = HttpClient();
+    var coverage = await parseChromeCoverage(
+      result,
+      (scriptId) => _sourceProvider(scriptId, httpClient),
+      (scriptId) => _sourceMapProvider(scriptId, httpClient),
+      _sourceUriProvider,
+    );
+    httpClient.close(force: true);
+    return coverage;
+  }
+
+  Chrome._(super.startBrowser, this.remoteDebuggerUrl, this._tabConnection,
+      this._idToUrl);
+
+  Future<Uri?> _sourceUriProvider(String sourceUrl, String scriptId) async {
+    var script = _idToUrl[scriptId];
+    if (script == null) return null;
+    var sourceUri = Uri.parse(sourceUrl);
+    if (sourceUri.scheme == 'file') return sourceUri;
+    // If the provided sourceUrl is relative, determine the package path.
+    var uri = Uri.parse(script);
+    var path = p.join(
+        p.joinAll(uri.pathSegments.sublist(1, uri.pathSegments.length - 1)),
+        sourceUrl);
+    return path.contains('/packages/')
+        ? Uri(scheme: 'package', path: path.split('/packages/').last)
+        : null;
+  }
+
+  Future<String?> _sourceMapProvider(
+      String scriptId, HttpClient httpClient) async {
+    var script = _idToUrl[scriptId];
+    if (script == null) return null;
+    return await httpClient.getString('$script.map');
+  }
+
+  Future<String?> _sourceProvider(
+      String scriptId, HttpClient httpClient) async {
+    var script = _idToUrl[scriptId];
+    if (script == null) return null;
+    return await httpClient.getString(script);
+  }
+}
+
+Future<WipConnection> _connect(
+    Process process, int port, Map<String, String> idToUrl, Uri url) async {
+  // Wait for Chrome to be in a ready state.
+  await process.stderr
+      .transform(utf8.decoder)
+      .transform(const LineSplitter())
+      .firstWhere((line) => line.startsWith('DevTools listening'));
+
+  var chromeConnection = ChromeConnection('localhost', port);
+  ChromeTab? tab;
+  var attempt = 0;
+  while (tab == null) {
+    attempt++;
+    var tabs = await chromeConnection.getTabs();
+    tab = tabs.firstWhereOrNull((tab) => tab.url == url.toString());
+    if (tab == null) {
+      await Future<void>.delayed(const Duration(milliseconds: 100));
+      if (attempt > 5) {
+        throw StateError('Could not connect to test tab with url: $url');
+      }
+    }
+  }
+  var tabConnection = await tab.connect();
+
+  // Enable debugging.
+  await tabConnection.debugger.enable();
+
+  // Coverage reports are in terms of scriptIds so keep note of URLs.
+  tabConnection.debugger.onScriptParsed.listen((data) {
+    var script = data.script;
+    if (script.url.isNotEmpty) idToUrl[script.scriptId] = script.url;
+  });
+
+  // Enable coverage collection.
+  await tabConnection.debugger.connection.sendCommand('Profiler.enable', {});
+  await tabConnection.debugger.connection.sendCommand(
+      'Profiler.startPreciseCoverage', {'detailed': true, 'callCount': false});
+
+  return tabConnection;
+}
+
+extension on HttpClient {
+  Encoding determineEncoding(HttpHeaders headers) {
+    final contentType = headers.contentType?.charset;
+
+    /// Using the `charset` property of the `contentType` if available.
+    /// If it's unavailable or if the encoding name is unknown, [latin1] is used by default,
+    /// as per [RFC 2616][].
+    ///
+    /// [RFC 2616]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html
+    return Encoding.getByName(contentType) ?? latin1;
+  }
+
+  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];
+    final encoding = determineEncoding(response.headers);
+    return encoding.decode(bytes);
+  }
+}
diff --git a/pkgs/test/lib/src/runner/browser/chromium.dart b/pkgs/test/lib/src/runner/browser/chromium.dart
new file mode 100644
index 0000000..b0243ba
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/chromium.dart
@@ -0,0 +1,63 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports
+
+import '../executable_settings.dart';
+import 'default_settings.dart';
+
+enum ChromiumBasedBrowser {
+  chrome(Runtime.chrome),
+  microsoftEdge(Runtime.edge);
+
+  final Runtime runtime;
+
+  const ChromiumBasedBrowser(this.runtime);
+
+  Future<Process> spawn(
+    Uri url,
+    Configuration configuration, {
+    ExecutableSettings? settings,
+    List<String> additionalArgs = const [],
+  }) async {
+    settings ??= defaultSettings[runtime];
+
+    var dir = createTempDir();
+    var args = [
+      '--user-data-dir=$dir',
+      url.toString(),
+      '--enable-logging=stderr',
+      '--v=0',
+      '--disable-extensions',
+      '--disable-popup-blocking',
+      '--bwsi',
+      '--no-first-run',
+      '--no-default-browser-check',
+      '--disable-default-apps',
+      '--disable-translate',
+      '--disable-dev-shm-usage',
+      if (settings!.headless && !configuration.pauseAfterLoad) ...[
+        '--headless',
+        '--disable-gpu',
+      ],
+      if (!configuration.debug)
+        // We don't actually connect to the remote debugger, but Chrome will
+        // close as soon as the page is loaded if we don't turn it on.
+        '--remote-debugging-port=0',
+      ...settings.arguments,
+      ...additionalArgs,
+    ];
+
+    var process = await Process.start(settings.executable, args);
+
+    unawaited(process.exitCode.then((_) => Directory(dir).deleteWithRetry()));
+
+    return process;
+  }
+}
diff --git a/pkgs/test/lib/src/runner/browser/compilers/compiler_support.dart b/pkgs/test/lib/src/runner/browser/compilers/compiler_support.dart
new file mode 100644
index 0000000..4fa1b88
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/compilers/compiler_support.dart
@@ -0,0 +1,105 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:test_api/backend.dart' show StackTraceMapper, SuitePlatform;
+import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
+import 'package:web_socket_channel/web_socket_channel.dart'; // ignore: implementation_imports
+
+/// The shared interface for all compiler support libraries.
+abstract class CompilerSupport {
+  /// The global test runner configuration.
+  final Configuration config;
+
+  /// The default template path.
+  final String defaultTemplatePath;
+
+  CompilerSupport(this.config, this.defaultTemplatePath);
+
+  /// The URL at which this compiler serves its tests.
+  ///
+  /// Each compiler serves its tests under a different directory.
+  Uri get serverUrl;
+
+  /// Compiles [dartPath] using [suiteConfig] for [platform].
+  ///
+  /// [dartPath] is the path to the original `.dart` test suite, relative to the
+  /// package root.
+  Future<void> compileSuite(
+      String dartPath, SuiteConfiguration suiteConfig, SuitePlatform platform);
+
+  /// Retrieves a stack trace mapper for [dartPath] if available.
+  ///
+  /// [dartPath] is the path to the original `.dart` test suite, relative to the
+  /// package root.
+  StackTraceMapper? stackTraceMapperForPath(String dartPath);
+
+  /// Returns the eventual URI for the web socket, as well as the channel itself
+  /// once the connection is established.
+  (Uri uri, Future<WebSocketChannel> socket) get webSocket;
+
+  /// Closes down anything necessary for this implementation.
+  Future<void> close();
+
+  /// A handler that serves html wrapper files used to bootstrap tests.
+  shelf.Response htmlWrapperHandler(shelf.Request request);
+}
+
+mixin JsHtmlWrapper on CompilerSupport {
+  @override
+  shelf.Response htmlWrapperHandler(shelf.Request request) {
+    var path = p.fromUri(request.url);
+
+    if (path.endsWith('.html')) {
+      var test = p.setExtension(path, '.dart');
+      var scriptBase = htmlEscape.convert(p.basename(test));
+      var link = '<link rel="x-dart-test" href="$scriptBase">';
+      var testName = htmlEscape.convert(test);
+      var template = config.customHtmlTemplatePath ?? defaultTemplatePath;
+      var contents = File(template).readAsStringSync();
+      var processedContents = contents
+          // Checked during loading phase that there is only one {{testScript}} placeholder.
+          .replaceFirst('{{testScript}}', link)
+          .replaceAll('{{testName}}', testName);
+      return shelf.Response.ok(processedContents,
+          headers: {'Content-Type': 'text/html'});
+    }
+
+    return shelf.Response.notFound('Not found.');
+  }
+}
+
+mixin WasmHtmlWrapper on CompilerSupport {
+  @override
+  shelf.Response htmlWrapperHandler(shelf.Request request) {
+    var path = p.fromUri(request.url);
+
+    if (path.endsWith('.html')) {
+      var test = '${p.withoutExtension(path)}.dart';
+      var scriptBase = htmlEscape.convert(p.basename(test));
+      var link = '<link rel="x-dart-test" href="$scriptBase">';
+      var testName = htmlEscape.convert(test);
+      var template = config.customHtmlTemplatePath ?? defaultTemplatePath;
+      var contents = File(template).readAsStringSync();
+      var jsRuntime = p.basename('$test.browser_test.dart.mjs');
+      var wasmData = '<data id="WasmBootstrapInfo" '
+          'data-wasmurl="${p.basename('$test.browser_test.dart.wasm')}" '
+          'data-jsruntimeurl="$jsRuntime"></data>';
+      var processedContents = contents
+          // Checked during loading phase that there is only one {{testScript}} placeholder.
+          .replaceFirst('{{testScript}}', '$link\n$wasmData')
+          .replaceAll('{{testName}}', testName);
+      return shelf.Response.ok(processedContents,
+          headers: {'Content-Type': 'text/html'});
+    }
+
+    return shelf.Response.notFound('Not found.');
+  }
+}
diff --git a/pkgs/test/lib/src/runner/browser/compilers/dart2js.dart b/pkgs/test/lib/src/runner/browser/compilers/dart2js.dart
new file mode 100644
index 0000000..3cd6745
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/compilers/dart2js.dart
@@ -0,0 +1,186 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:http_multi_server/http_multi_server.dart';
+import 'package:path/path.dart' as p;
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:shelf/shelf_io.dart' as shelf_io;
+import 'package:shelf_packages_handler/shelf_packages_handler.dart';
+import 'package:shelf_static/shelf_static.dart';
+import 'package:shelf_web_socket/shelf_web_socket.dart';
+import 'package:test_api/backend.dart' show StackTraceMapper, SuitePlatform;
+import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/dart2js_compiler_pool.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/package_version.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/package_config.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports
+import 'package:web_socket_channel/web_socket_channel.dart';
+
+import '../../../util/math.dart';
+import '../../../util/one_off_handler.dart';
+import '../../../util/package_map.dart';
+import '../../../util/path_handler.dart';
+import 'compiler_support.dart';
+
+/// Support for Dart2Js compiled tests.
+class Dart2JsSupport extends CompilerSupport with JsHtmlWrapper {
+  /// Whether [close] has been called.
+  bool _closed = false;
+
+  /// The temporary directory in which compiled JS is emitted.
+  final _compiledDir = createTempDir();
+
+  /// A map from test suite paths to Futures that will complete once those
+  /// suites are finished compiling.
+  ///
+  /// This is used to make sure that a given test suite is only compiled once
+  /// per run, rather than once per browser per run.
+  final _compileFutures = <String, Future<void>>{};
+
+  /// The [Dart2JsCompilerPool] managing active instances of `dart2js`.
+  final _compilerPool = Dart2JsCompilerPool();
+
+  /// Mappers for Dartifying stack traces, indexed by test path.
+  final _mappers = <String, StackTraceMapper>{};
+
+  /// A [PathHandler] used to serve test specific artifacts.
+  final _pathHandler = PathHandler();
+
+  /// The root directory served statically by this server.
+  final String _root;
+
+  /// Each compiler serves its tests under a different randomly-generated
+  /// secret URI to ensure that other users on the same system can't snoop
+  /// on data being served through this server, as well as distinguish tests
+  /// from different compilers from each other.
+  final String _secret = randomUrlSecret();
+
+  /// The underlying server.
+  final shelf.Server _server;
+
+  /// A [OneOffHandler] for servicing WebSocket connections for
+  /// [BrowserManager]s.
+  ///
+  /// This is one-off because each [BrowserManager] can only connect to a single
+  /// WebSocket.
+  final _webSocketHandler = OneOffHandler();
+
+  @override
+  Uri get serverUrl => _server.url.resolve('$_secret/');
+
+  Dart2JsSupport._(super.config, super.defaultTemplatePath, this._server,
+      this._root, String faviconPath) {
+    var cascade = shelf.Cascade()
+        .add(_webSocketHandler.handler)
+        .add(packagesDirHandler())
+        .add(_pathHandler.handler)
+        .add(createStaticHandler(_root))
+        .add(htmlWrapperHandler);
+
+    var pipeline = const shelf.Pipeline()
+        .addMiddleware(PathHandler.nestedIn(_secret))
+        .addHandler(cascade.handler);
+
+    _server.mount(shelf.Cascade()
+        .add(createFileHandler(faviconPath))
+        .add(pipeline)
+        .handler);
+  }
+
+  static Future<Dart2JsSupport> start({
+    required Configuration config,
+    required String defaultTemplatePath,
+    required String root,
+    required String faviconPath,
+  }) async {
+    var server = shelf_io.IOServer(await HttpMultiServer.loopback(0));
+    return Dart2JsSupport._(
+        config, defaultTemplatePath, server, root, faviconPath);
+  }
+
+  @override
+  Future<void> compileSuite(
+      String dartPath, SuiteConfiguration suiteConfig, SuitePlatform platform) {
+    return _compileFutures.putIfAbsent(dartPath, () async {
+      var dir = Directory(_compiledDir).createTempSync('test_').path;
+      var jsPath = p.join(dir, '${p.basename(dartPath)}.browser_test.dart.js');
+      var bootstrapContent = '''
+        ${suiteConfig.metadata.languageVersionComment ?? await rootPackageLanguageVersionComment}
+        import 'package:test/src/bootstrap/browser.dart';
+        import 'package:test/src/runner/browser/dom.dart' as dom;
+
+        import '${await absoluteUri(dartPath)}' as test;
+
+        void main() {
+          dom.window.console.log(r'Startup for test path $dartPath');
+          internalBootstrapBrowserTest(() => test.main);
+        }
+      ''';
+
+      await _compilerPool.compile(bootstrapContent, jsPath, suiteConfig);
+      if (_closed) return;
+
+      var bootstrapUrl = '${p.toUri(p.relative(dartPath, from: _root)).path}'
+          '.browser_test.dart';
+      _pathHandler.add(bootstrapUrl, (request) {
+        return shelf.Response.ok(bootstrapContent,
+            headers: {'Content-Type': 'application/dart'});
+      });
+
+      var jsUrl = '${p.toUri(p.relative(dartPath, from: _root)).path}'
+          '.browser_test.dart.js';
+      _pathHandler.add(jsUrl, (request) {
+        return shelf.Response.ok(File(jsPath).readAsStringSync(),
+            headers: {'Content-Type': 'application/javascript'});
+      });
+
+      var mapUrl = '${p.toUri(p.relative(dartPath, from: _root)).path}'
+          '.browser_test.dart.js.map';
+      _pathHandler.add(mapUrl, (request) {
+        return shelf.Response.ok(File('$jsPath.map').readAsStringSync(),
+            headers: {'Content-Type': 'application/json'});
+      });
+
+      if (suiteConfig.jsTrace) return;
+      var mapPath = '$jsPath.map';
+      _mappers[dartPath] = JSStackTraceMapper(File(mapPath).readAsStringSync(),
+          mapUrl: p.toUri(mapPath),
+          sdkRoot: Uri.parse('org-dartlang-sdk:///sdk'),
+          packageMap: (await currentPackageConfig).toPackageMap());
+    });
+  }
+
+  @override
+  Future<void> close() async {
+    if (_closed) return;
+    _closed = true;
+    await Future.wait([
+      Directory(_compiledDir).deleteWithRetry(),
+      _compilerPool.close(),
+      _server.close(),
+    ]);
+  }
+
+  @override
+  StackTraceMapper? stackTraceMapperForPath(String dartPath) =>
+      _mappers[dartPath];
+
+  @override
+  (Uri, Future<WebSocketChannel>) get webSocket {
+    var completer = Completer<WebSocketChannel>.sync();
+    // Note: the WebSocketChannel type below is needed for compatibility with
+    // package:shelf_web_socket v2.
+    var path =
+        _webSocketHandler.create(webSocketHandler((WebSocketChannel ws, _) {
+      completer.complete(ws);
+    }));
+    var webSocketUrl = serverUrl.replace(scheme: 'ws').resolve(path);
+    return (webSocketUrl, completer.future);
+  }
+}
diff --git a/pkgs/test/lib/src/runner/browser/compilers/dart2wasm.dart b/pkgs/test/lib/src/runner/browser/compilers/dart2wasm.dart
new file mode 100644
index 0000000..31ad661
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/compilers/dart2wasm.dart
@@ -0,0 +1,192 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:http_multi_server/http_multi_server.dart';
+import 'package:path/path.dart' as p;
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:shelf/shelf_io.dart' as shelf_io;
+import 'package:shelf_packages_handler/shelf_packages_handler.dart';
+import 'package:shelf_static/shelf_static.dart';
+import 'package:shelf_web_socket/shelf_web_socket.dart';
+import 'package:test_api/backend.dart' show StackTraceMapper, SuitePlatform;
+import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/package_version.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/wasm_compiler_pool.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/package_config.dart'; // ignore: implementation_imports
+import 'package:web_socket_channel/web_socket_channel.dart';
+
+import '../../../util/math.dart';
+import '../../../util/one_off_handler.dart';
+import '../../../util/path_handler.dart';
+import '../browser_manager.dart';
+import 'compiler_support.dart';
+
+/// Support for Dart2Wasm compiled tests.
+class Dart2WasmSupport extends CompilerSupport with WasmHtmlWrapper {
+  /// Whether [close] has been called.
+  bool _closed = false;
+
+  /// The temporary directory in which compiled JS is emitted.
+  final _compiledDir = createTempDir();
+
+  /// A map from test suite paths to Futures that will complete once those
+  /// suites are finished compiling.
+  ///
+  /// This is used to make sure that a given test suite is only compiled once
+  /// per run, rather than once per browser per run.
+  final _compileFutures = <String, Future<void>>{};
+
+  /// The [WasmCompilerPool] managing active instances of `dart2wasm`.
+  final _compilerPool = WasmCompilerPool();
+
+  /// The `package:test` side wrapper for the Dart2Wasm runtime.
+  final String _jsRuntimeWrapper;
+
+  /// Mappers for Dartifying stack traces, indexed by test path.
+  final _mappers = <String, StackTraceMapper>{};
+
+  /// A [PathHandler] used to serve test specific artifacts.
+  final _pathHandler = PathHandler();
+
+  /// The root directory served statically by this server.
+  final String _root;
+
+  /// Each compiler serves its tests under a different randomly-generated
+  /// secret URI to ensure that other users on the same system can't snoop
+  /// on data being served through this server, as well as distinguish tests
+  /// from different compilers from each other.
+  final String _secret = randomUrlSecret();
+
+  /// The underlying server.
+  final shelf.Server _server;
+
+  /// A [OneOffHandler] for servicing WebSocket connections for
+  /// [BrowserManager]s.
+  ///
+  /// This is one-off because each [BrowserManager] can only connect to a single
+  /// WebSocket.
+  final _webSocketHandler = OneOffHandler();
+
+  @override
+  Uri get serverUrl => _server.url.resolve('$_secret/');
+
+  Dart2WasmSupport._(super.config, super.defaultTemplatePath,
+      this._jsRuntimeWrapper, this._server, this._root, String faviconPath) {
+    var cascade = shelf.Cascade()
+        .add(_webSocketHandler.handler)
+        .add(packagesDirHandler())
+        .add(_pathHandler.handler)
+        .add(createStaticHandler(_root))
+        .add(htmlWrapperHandler);
+
+    var pipeline = const shelf.Pipeline()
+        .addMiddleware(PathHandler.nestedIn(_secret))
+        .addHandler(cascade.handler);
+
+    _server.mount(shelf.Cascade()
+        .add(createFileHandler(faviconPath))
+        .add(pipeline)
+        .handler);
+  }
+
+  static Future<Dart2WasmSupport> start({
+    required Configuration config,
+    required String defaultTemplatePath,
+    required String jsRuntimeWrapper,
+    required String root,
+    required String faviconPath,
+  }) async {
+    var server = shelf_io.IOServer(await HttpMultiServer.loopback(0));
+    return Dart2WasmSupport._(config, defaultTemplatePath, jsRuntimeWrapper,
+        server, root, faviconPath);
+  }
+
+  @override
+  Future<void> compileSuite(
+      String dartPath, SuiteConfiguration suiteConfig, SuitePlatform platform) {
+    return _compileFutures.putIfAbsent(dartPath, () async {
+      var dir = Directory(_compiledDir).createTempSync('test_').path;
+
+      var baseCompiledPath =
+          p.join(dir, '${p.basename(dartPath)}.browser_test.dart');
+      var baseUrl =
+          '${p.toUri(p.relative(dartPath, from: _root)).path}.browser_test.dart';
+      var wasmUrl = '$baseUrl.wasm';
+      var jsRuntimeWrapperUrl = '$baseUrl.js';
+      var jsRuntimeUrl = '$baseUrl.mjs';
+      var htmlUrl = '$baseUrl.html';
+
+      var bootstrapContent = '''
+        ${suiteConfig.metadata.languageVersionComment ?? await rootPackageLanguageVersionComment}
+        import 'package:test/src/bootstrap/browser.dart';
+
+        import '${await absoluteUri(dartPath)}' as test;
+
+        void main() {
+          internalBootstrapBrowserTest(() => test.main);
+        }
+      ''';
+
+      await _compilerPool.compile(
+          bootstrapContent, baseCompiledPath, suiteConfig);
+      if (_closed) return;
+
+      var wasmPath = '$baseCompiledPath.wasm';
+      _pathHandler.add(wasmUrl, (request) {
+        return shelf.Response.ok(File(wasmPath).readAsBytesSync(),
+            headers: {'Content-Type': 'application/wasm'});
+      });
+
+      _pathHandler.add(jsRuntimeWrapperUrl, (request) {
+        return shelf.Response.ok(File(_jsRuntimeWrapper).readAsBytesSync(),
+            headers: {'Content-Type': 'application/javascript'});
+      });
+
+      var jsRuntimePath = '$baseCompiledPath.mjs';
+      _pathHandler.add(jsRuntimeUrl, (request) {
+        return shelf.Response.ok(File(jsRuntimePath).readAsBytesSync(),
+            headers: {'Content-Type': 'application/javascript'});
+      });
+
+      var htmlPath = '$baseCompiledPath.html';
+      _pathHandler.add(htmlUrl, (request) {
+        return shelf.Response.ok(File(htmlPath).readAsBytesSync(),
+            headers: {'Content-Type': 'text/html'});
+      });
+    });
+  }
+
+  @override
+  Future<void> close() async {
+    if (_closed) return;
+    _closed = true;
+    await Future.wait([
+      Directory(_compiledDir).deleteWithRetry(),
+      _compilerPool.close(),
+      _server.close(),
+    ]);
+  }
+
+  @override
+  StackTraceMapper? stackTraceMapperForPath(String dartPath) =>
+      _mappers[dartPath];
+
+  @override
+  (Uri, Future<WebSocketChannel>) get webSocket {
+    var completer = Completer<WebSocketChannel>.sync();
+    // Note: the WebSocketChannel type below is needed for compatibility with
+    // package:shelf_web_socket v2.
+    var path =
+        _webSocketHandler.create(webSocketHandler((WebSocketChannel ws, _) {
+      completer.complete(ws);
+    }));
+    var webSocketUrl = serverUrl.replace(scheme: 'ws').resolve(path);
+    return (webSocketUrl, completer.future);
+  }
+}
diff --git a/pkgs/test/lib/src/runner/browser/compilers/precompiled.dart b/pkgs/test/lib/src/runner/browser/compilers/precompiled.dart
new file mode 100644
index 0000000..9cf44aa
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/compilers/precompiled.dart
@@ -0,0 +1,149 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:http_multi_server/http_multi_server.dart';
+import 'package:path/path.dart' as p;
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:shelf/shelf_io.dart' as shelf_io;
+import 'package:shelf_packages_handler/shelf_packages_handler.dart';
+import 'package:shelf_static/shelf_static.dart';
+import 'package:shelf_web_socket/shelf_web_socket.dart';
+import 'package:test_api/backend.dart'
+    show Compiler, StackTraceMapper, SuitePlatform;
+import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/package_config.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports
+import 'package:web_socket_channel/web_socket_channel.dart';
+
+import '../../../util/math.dart';
+import '../../../util/one_off_handler.dart';
+import '../../../util/package_map.dart';
+import '../../../util/path_handler.dart';
+import 'compiler_support.dart';
+
+class JsPrecompiledSupport = PrecompiledSupport with JsHtmlWrapper;
+class WasmPrecompiledSupport = PrecompiledSupport with WasmHtmlWrapper;
+
+/// Support for precompiled test files.
+abstract class PrecompiledSupport extends CompilerSupport {
+  /// Whether [close] has been called.
+  bool _closed = false;
+
+  /// Mappers for Dartifying stack traces, indexed by test path.
+  final _mappers = <String, StackTraceMapper>{};
+
+  /// The root directory served statically by the server.
+  final String _root;
+
+  /// Each compiler serves its tests under a different randomly-generated
+  /// secret URI to ensure that other users on the same system can't snoop
+  /// on data being served through this server, as well as distinguish tests
+  /// from different compilers from each other.
+  final String _secret = randomUrlSecret();
+
+  /// The underlying server.
+  final shelf.Server _server;
+
+  /// A [OneOffHandler] for servicing WebSocket connections for
+  /// [BrowserManager]s.
+  ///
+  /// This is one-off because each [BrowserManager] can only connect to a single
+  /// WebSocket.
+  final _webSocketHandler = OneOffHandler();
+
+  /// The URL at which this compiler serves its tests.
+  ///
+  /// Each compiler serves its tests under a different directory.
+  @override
+  Uri get serverUrl => _server.url.resolve('$_secret/');
+
+  PrecompiledSupport._(super.config, super.defaultTemplatePath, this._server,
+      this._root, String faviconPath) {
+    var cascade = shelf.Cascade()
+        .add(_webSocketHandler.handler)
+        .add(createStaticHandler(_root, serveFilesOutsidePath: true))
+        // TODO: This packages dir handler should not be necessary?
+        .add(packagesDirHandler())
+        // Even for precompiled tests, we will auto-create a bootstrap html file
+        // if none was present.
+        .add(htmlWrapperHandler);
+
+    var pipeline = const shelf.Pipeline()
+        .addMiddleware(PathHandler.nestedIn(_secret))
+        .addHandler(cascade.handler);
+
+    _server.mount(shelf.Cascade()
+        .add(createFileHandler(faviconPath))
+        .add(pipeline)
+        .handler);
+  }
+
+  static Future<PrecompiledSupport> start({
+    required Compiler compiler,
+    required Configuration config,
+    required String defaultTemplatePath,
+    required String root,
+    required String faviconPath,
+  }) async {
+    var server = shelf_io.IOServer(await HttpMultiServer.loopback(0));
+
+    return switch (compiler) {
+      Compiler.dart2js => JsPrecompiledSupport._(
+          config, defaultTemplatePath, server, root, faviconPath),
+      Compiler.dart2wasm => WasmPrecompiledSupport._(
+          config, defaultTemplatePath, server, root, faviconPath),
+      Compiler.exe ||
+      Compiler.kernel ||
+      Compiler.source =>
+        throw UnsupportedError(
+            'The browser platform does not support $compiler'),
+    };
+  }
+
+  /// Compiles [dartPath] using [suiteConfig] for [platform].
+  @override
+  Future<void> compileSuite(String dartPath, SuiteConfiguration suiteConfig,
+      SuitePlatform platform) 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());
+    }
+  }
+
+  /// Retrieves a stack trace mapper for [path] if available.
+  @override
+  StackTraceMapper? stackTraceMapperForPath(String dartPath) =>
+      _mappers[dartPath];
+
+  /// Closes down anything necessary for this implementation.
+  @override
+  Future<void> close() async {
+    if (_closed) return;
+    _closed = true;
+    await _server.close();
+  }
+
+  @override
+  (Uri, Future<WebSocketChannel>) get webSocket {
+    var completer = Completer<WebSocketChannel>.sync();
+    // Note: the WebSocketChannel type below is needed for compatibility with
+    // package:shelf_web_socket v2.
+    var path =
+        _webSocketHandler.create(webSocketHandler((WebSocketChannel ws, _) {
+      completer.complete(ws);
+    }));
+    var webSocketUrl = serverUrl.replace(scheme: 'ws').resolve(path);
+    return (webSocketUrl, completer.future);
+  }
+}
diff --git a/pkgs/test/lib/src/runner/browser/default_settings.dart b/pkgs/test/lib/src/runner/browser/default_settings.dart
new file mode 100644
index 0000000..312fc11
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/default_settings.dart
@@ -0,0 +1,37 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:collection';
+
+import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
+import '../executable_settings.dart';
+
+/// Default settings for starting browser executables.
+final defaultSettings = UnmodifiableMapView({
+  Runtime.chrome: ExecutableSettings(
+      linuxExecutable: 'google-chrome',
+      macOSExecutable:
+          '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
+      windowsExecutable: r'Google\Chrome\Application\chrome.exe',
+      environmentOverride: 'CHROME_EXECUTABLE'),
+  Runtime.edge: ExecutableSettings(
+    linuxExecutable: 'microsoft-edge-stable',
+    windowsExecutable: r'Microsoft\Edge\Application\msedge.exe',
+    macOSExecutable:
+        '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
+    environmentOverride: 'MS_EDGE_EXECUTABLE',
+  ),
+  Runtime.firefox: ExecutableSettings(
+      linuxExecutable: 'firefox',
+      macOSExecutables: [
+        '/Applications/Firefox.app/Contents/MacOS/firefox-bin',
+        '/Applications/Firefox.app/Contents/MacOS/firefox',
+        'firefox',
+      ],
+      windowsExecutable: r'Mozilla Firefox\firefox.exe',
+      environmentOverride: 'FIREFOX_EXECUTABLE'),
+  Runtime.safari: ExecutableSettings(
+      macOSExecutable: '/Applications/Safari.app/Contents/MacOS/Safari',
+      environmentOverride: 'SAFARI_EXECUTABLE'),
+});
diff --git a/pkgs/test/lib/src/runner/browser/dom.dart b/pkgs/test/lib/src/runner/browser/dom.dart
new file mode 100644
index 0000000..d0fba48
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/dom.dart
@@ -0,0 +1,280 @@
+// Copyright (c) 2022, 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:js_util' as js_util;
+
+import 'package:js/js.dart';
+
+@JS()
+@staticInterop
+class Window extends EventTarget {}
+
+extension WindowExtension on Window {
+  @pragma('dart2js:as:trust')
+  Window get parent => js_util.getProperty<dynamic>(this, 'parent') as Window;
+  external Location get location;
+  Console get console => js_util.getProperty(this, 'console') as Console;
+  CSSStyleDeclaration? getComputedStyle(Element elt, [String? pseudoElt]) =>
+      js_util.callMethod(this, 'getComputedStyle', <Object>[
+        elt,
+        if (pseudoElt != null) pseudoElt
+      ]) as CSSStyleDeclaration?;
+  external Navigator get navigator;
+  void postMessage(Object message, String targetOrigin,
+          [List<MessagePort>? messagePorts]) =>
+      js_util.callMethod(this, 'postMessage', <Object?>[
+        js_util.jsify(message),
+        targetOrigin,
+        if (messagePorts != null) js_util.jsify(messagePorts)
+      ]);
+}
+
+@JS('window')
+external Window get window;
+
+@JS()
+@staticInterop
+class Console {}
+
+extension ConsoleExtension on Console {
+  external void log(Object? object);
+  external void warn(Object? object);
+}
+
+@JS()
+@staticInterop
+class Document extends Node {}
+
+extension DocumentExtension on Document {
+  external Element? querySelector(String selectors);
+  Element createElement(String name, [Object? options]) => js_util.callMethod(
+          this, 'createElement', <Object>[name, if (options != null) options])
+      as Element;
+}
+
+@JS()
+@staticInterop
+class HTMLDocument extends Document {}
+
+extension HTMLDocumentExtension on HTMLDocument {
+  external HTMLBodyElement? get body;
+  external String? get title;
+}
+
+@JS('document')
+external HTMLDocument get document;
+
+@JS()
+@staticInterop
+class Navigator {}
+
+extension NavigatorExtension on Navigator {
+  external String get userAgent;
+}
+
+@JS()
+@staticInterop
+class Element extends Node {}
+
+extension DomElementExtension on Element {
+  external DomTokenList get classList;
+}
+
+@JS()
+@staticInterop
+class HTMLElement extends Element {}
+
+@JS()
+@staticInterop
+class HTMLBodyElement extends HTMLElement {}
+
+@JS()
+@staticInterop
+class Node extends EventTarget {}
+
+extension NodeExtension on Node {
+  external Node appendChild(Node node);
+  void remove() {
+    if (parentNode != null) {
+      final parent = parentNode!;
+      parent.removeChild(this);
+    }
+  }
+
+  external Node removeChild(Node child);
+  external Node? get parentNode;
+}
+
+@JS()
+@staticInterop
+class EventTarget {}
+
+extension EventTargetExtension on EventTarget {
+  void addEventListener(String type, EventListener? listener,
+      [bool? useCapture]) {
+    if (listener != null) {
+      js_util.callMethod<void>(this, 'addEventListener',
+          <Object>[type, listener, if (useCapture != null) useCapture]);
+    }
+  }
+
+  void removeEventListener(String type, EventListener? listener,
+      [bool? useCapture]) {
+    if (listener != null) {
+      js_util.callMethod<void>(this, 'removeEventListener',
+          <Object>[type, listener, if (useCapture != null) useCapture]);
+    }
+  }
+}
+
+typedef EventListener = void Function(Event event);
+
+@JS()
+@staticInterop
+class Event {}
+
+extension EventExtension on Event {
+  external void stopPropagation();
+}
+
+@JS()
+@staticInterop
+class MessageEvent extends Event {}
+
+extension MessageEventExtension on MessageEvent {
+  dynamic get data => js_util.dartify(js_util.getProperty(this, 'data'));
+  external String get origin;
+  List<MessagePort> get ports =>
+      js_util.getProperty<List>(this, 'ports').cast<MessagePort>();
+
+  /// The source may be a `WindowProxy`, a `MessagePort`, or a `ServiceWorker`.
+  ///
+  /// When a message is sent from an iframe through `window.parent.postMessage`
+  /// the source will be a `WindowProxy` which has the same methods as [Window].
+  @pragma('dart2js:as:trust')
+  MessageEventSource get source =>
+      js_util.getProperty<dynamic>(this, 'source') as MessageEventSource;
+}
+
+@JS()
+@staticInterop
+class MessageEventSource {}
+
+extension MessageEventSourceExtension on MessageEventSource {
+  @pragma('dart2js:as:trust')
+  MessageEventSourceLocation? get location =>
+      js_util.getProperty<dynamic>(this, 'location')
+          as MessageEventSourceLocation;
+}
+
+@JS()
+@staticInterop
+class MessageEventSourceLocation {}
+
+extension MessageEventSourceLocationExtension on MessageEventSourceLocation {
+  external String? get href;
+}
+
+@JS()
+@staticInterop
+class Location {}
+
+extension LocationExtension on Location {
+  external String get href;
+  external String get origin;
+}
+
+@JS()
+@staticInterop
+class MessagePort extends EventTarget {}
+
+extension MessagePortExtension on MessagePort {
+  void postMessage(Object? message) => js_util.callMethod(this, 'postMessage',
+      <Object>[if (message != null) js_util.jsify(message) as Object]);
+  external void start();
+}
+
+@JS()
+@staticInterop
+class CSSStyleDeclaration {}
+
+@JS()
+@staticInterop
+class HTMLScriptElement extends HTMLElement {}
+
+extension HTMLScriptElementExtension on HTMLScriptElement {
+  external set src(String value);
+}
+
+HTMLScriptElement createHTMLScriptElement() =>
+    document.createElement('script') as HTMLScriptElement;
+
+@JS()
+@staticInterop
+class DomTokenList {}
+
+extension DomTokenListExtension on DomTokenList {
+  external void add(String value);
+  external void remove(String value);
+  external bool contains(String token);
+}
+
+@JS()
+@staticInterop
+class HTMLIFrameElement extends HTMLElement {}
+
+extension HTMLIFrameElementExtension on HTMLIFrameElement {
+  external String? get src;
+  external set src(String? value);
+  external Window get contentWindow;
+}
+
+HTMLIFrameElement createHTMLIFrameElement() =>
+    document.createElement('iframe') as HTMLIFrameElement;
+
+@JS()
+@staticInterop
+class WebSocket extends EventTarget {}
+
+extension WebSocketExtension on WebSocket {
+  external void send(Object? data);
+}
+
+WebSocket createWebSocket(String url) =>
+    _callConstructor('WebSocket', <Object>[url])! as WebSocket;
+
+@JS()
+@staticInterop
+class MessageChannel {}
+
+extension MessageChannelExtension on MessageChannel {
+  external MessagePort get port1;
+  external MessagePort get port2;
+}
+
+MessageChannel createMessageChannel() =>
+    _callConstructor('MessageChannel', <Object>[])! as MessageChannel;
+
+Object? _findConstructor(String constructorName) =>
+    js_util.getProperty(window, constructorName);
+
+Object? _callConstructor(String constructorName, List<Object?> args) {
+  final constructor = _findConstructor(constructorName);
+  if (constructor == null) {
+    return null;
+  }
+  return js_util.callConstructor(constructor, args);
+}
+
+class Subscription {
+  final String type;
+  final EventTarget target;
+  final EventListener listener;
+
+  Subscription(this.target, this.type, this.listener) {
+    target.addEventListener(type, listener);
+  }
+
+  void cancel() => target.removeEventListener(type, listener);
+}
diff --git a/pkgs/test/lib/src/runner/browser/firefox.dart b/pkgs/test/lib/src/runner/browser/firefox.dart
new file mode 100644
index 0000000..503f103
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/firefox.dart
@@ -0,0 +1,59 @@
+// 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.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports
+
+import '../executable_settings.dart';
+import 'browser.dart';
+import 'default_settings.dart';
+
+final _preferences = '''
+user_pref("browser.shell.checkDefaultBrowser", false);
+user_pref("dom.disable_open_during_load", false);
+user_pref("dom.max_script_run_time", 0);
+''';
+
+/// A class for running an instance of Firefox.
+///
+/// Most of the communication with the browser is expected to happen via HTTP,
+/// so this exposes a bare-bones API. The browser starts as soon as the class is
+/// constructed, and is killed when [close] is called.
+///
+/// Any errors starting or running the process are reported through [onExit].
+class Firefox extends Browser {
+  @override
+  final name = 'Firefox';
+
+  Firefox(Uri url, {ExecutableSettings? settings})
+      : super(() =>
+            _startBrowser(url, settings ?? defaultSettings[Runtime.firefox]!));
+
+  /// Starts a new instance of Firefox open to the given [url], which may be a
+  /// [Uri] or a [String].
+  static Future<Process> _startBrowser(
+      Uri url, ExecutableSettings settings) async {
+    var dir = createTempDir();
+    File(p.join(dir, 'prefs.js')).writeAsStringSync(_preferences);
+
+    var process = await Process.start(settings.executable, [
+      '--profile',
+      dir,
+      url.toString(),
+      '--no-remote',
+      ...settings.arguments,
+    ], environment: {
+      'MOZ_CRASHREPORTER_DISABLE': '1',
+      'MOZ_AUTOMATION': '1',
+    });
+
+    unawaited(process.exitCode.then((_) => Directory(dir).deleteWithRetry()));
+
+    return process;
+  }
+}
diff --git a/pkgs/test/lib/src/runner/browser/microsoft_edge.dart b/pkgs/test/lib/src/runner/browser/microsoft_edge.dart
new file mode 100644
index 0000000..08fa8e2
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/microsoft_edge.dart
@@ -0,0 +1,23 @@
+// Copyright (c) 2023, 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_core/src/runner/configuration.dart'; // ignore: implementation_imports
+
+import '../executable_settings.dart';
+import 'browser.dart';
+import 'chromium.dart';
+
+/// A class for running an instance of Microsoft Edge, a Chromium-based browser.
+class MicrosoftEdge extends Browser {
+  @override
+  String get name => 'Edge';
+
+  MicrosoftEdge(Uri url, Configuration configuration,
+      {ExecutableSettings? settings})
+      : super(() => ChromiumBasedBrowser.microsoftEdge.spawn(
+              url,
+              configuration,
+              settings: settings,
+            ));
+}
diff --git a/pkgs/test/lib/src/runner/browser/platform.dart b/pkgs/test/lib/src/runner/browser/platform.dart
new file mode 100644
index 0000000..da03a3e
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/platform.dart
@@ -0,0 +1,269 @@
+// 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.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:async/async.dart';
+import 'package:path/path.dart' as p;
+import 'package:test_api/backend.dart' show Compiler, Runtime, SuitePlatform;
+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/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
+import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/package_config.dart'; // ignore: implementation_imports
+import 'package:yaml/yaml.dart';
+
+import '../executable_settings.dart';
+import 'browser_manager.dart';
+import 'compilers/compiler_support.dart';
+import 'compilers/dart2js.dart';
+import 'compilers/dart2wasm.dart';
+import 'compilers/precompiled.dart';
+import 'default_settings.dart';
+
+class BrowserPlatform extends PlatformPlugin
+    implements CustomizablePlatform<ExecutableSettings> {
+  /// Starts the server.
+  ///
+  /// [root] is the root directory that the server should serve. It defaults to
+  /// the working directory.
+  static Future<BrowserPlatform> start({String? root}) async {
+    var packageConfig = await currentPackageConfig;
+    return BrowserPlatform._(
+        Configuration.current,
+        p.fromUri(packageConfig.resolve(
+            Uri.parse('package:test/src/runner/browser/static/favicon.ico'))),
+        p.fromUri(packageConfig.resolve(Uri.parse(
+            'package:test/src/runner/browser/static/default.html.tpl'))),
+        p.fromUri(packageConfig.resolve(Uri.parse(
+            'package:test/src/runner/browser/static/run_wasm_chrome.js'))),
+        root: root);
+  }
+
+  /// The test runner configuration.
+  final Configuration _config;
+
+  /// The cached [CompilerSupport] for each compiler.
+  final _compilerSupport = <Compiler, Future<CompilerSupport>>{};
+
+  /// The `package:test` side wrapper for the Dart2Wasm runtime.
+  final String _jsRuntimeWrapper;
+
+  /// The URL for this server and [compiler] combination.
+  ///
+  /// Each compiler serves its tests under a different randomly-generated
+  /// secret URI to ensure that other users on the same system can't snoop
+  /// on data being served through this server, as well as distinguish tests
+  /// from different compilers from each other.
+  Future<CompilerSupport> compilerSupport(Compiler compiler) =>
+      _compilerSupport.putIfAbsent(compiler, () {
+        if (_config.suiteDefaults.precompiledPath != null) {
+          return PrecompiledSupport.start(
+              compiler: compiler,
+              config: _config,
+              defaultTemplatePath: _defaultTemplatePath,
+              root: _config.suiteDefaults.precompiledPath!,
+              faviconPath: _faviconPath);
+        }
+        return switch (compiler) {
+          Compiler.dart2js => Dart2JsSupport.start(
+              config: _config,
+              defaultTemplatePath: _defaultTemplatePath,
+              root: _root,
+              faviconPath: _faviconPath),
+          Compiler.dart2wasm => Dart2WasmSupport.start(
+              config: _config,
+              defaultTemplatePath: _defaultTemplatePath,
+              jsRuntimeWrapper: _jsRuntimeWrapper,
+              root: _root,
+              faviconPath: _faviconPath),
+          _ => throw StateError('Unexpected compiler $compiler'),
+        };
+      });
+
+  /// The root directory served statically by this server.
+  final String _root;
+
+  /// Whether [close] has been called.
+  bool get _closed => _closeMemo.hasRun;
+
+  /// A map from browser identifiers to futures that will complete to the
+  /// [BrowserManager]s for those browsers, or `null` if they failed to load.
+  ///
+  /// This should only be accessed through [_browserManagerFor].
+  final _browserManagers = <(Runtime, Compiler), Future<BrowserManager?>>{};
+
+  /// Settings for invoking each browser.
+  ///
+  /// This starts out with the default settings, which may be overridden by user settings.
+  final _browserSettings =
+      Map<Runtime, ExecutableSettings>.from(defaultSettings);
+
+  /// The default template for html tests.
+  final String _defaultTemplatePath;
+
+  final String _faviconPath;
+
+  BrowserPlatform._(Configuration config, this._faviconPath,
+      this._defaultTemplatePath, this._jsRuntimeWrapper,
+      {String? root})
+      : _config = config,
+        _root = root ?? p.current;
+
+  @override
+  ExecutableSettings parsePlatformSettings(YamlMap settings) =>
+      ExecutableSettings.parse(settings);
+
+  @override
+  ExecutableSettings mergePlatformSettings(
+          ExecutableSettings settings1, ExecutableSettings settings2) =>
+      settings1.merge(settings2);
+
+  @override
+  void customizePlatform(Runtime runtime, ExecutableSettings settings) {
+    var oldSettings =
+        _browserSettings[runtime] ?? _browserSettings[runtime.root];
+    if (oldSettings != null) settings = oldSettings.merge(settings);
+    _browserSettings[runtime] = settings;
+  }
+
+  /// Loads the test suite at [path] on the platform [platform].
+  ///
+  /// This will start a browser to load the suite if one isn't already running.
+  /// Throws an [ArgumentError] if `platform.platform` isn't a browser.
+  @override
+  Future<RunnerSuite?> load(String path, SuitePlatform platform,
+      SuiteConfiguration suiteConfig, Map<String, Object?> message) async {
+    var browser = platform.runtime;
+    assert(suiteConfig.runtimes.contains(browser.identifier));
+
+    if (!browser.isBrowser) {
+      throw ArgumentError('$browser is not a browser.');
+    }
+
+    var compiler = platform.compiler;
+    var support = await compilerSupport(compiler);
+
+    var htmlPathFromTestPath = '${p.withoutExtension(path)}.html';
+    if (File(htmlPathFromTestPath).existsSync()) {
+      if (_config.customHtmlTemplatePath != null &&
+          p.basename(htmlPathFromTestPath) ==
+              p.basename(_config.customHtmlTemplatePath!)) {
+        throw LoadException(
+            path,
+            'template file "${p.basename(_config.customHtmlTemplatePath!)}" cannot be named '
+            'like the test file.');
+      }
+      _checkHtmlCorrectness(htmlPathFromTestPath, path);
+    } else if (_config.customHtmlTemplatePath != null) {
+      var htmlTemplatePath = _config.customHtmlTemplatePath!;
+      if (!File(htmlTemplatePath).existsSync()) {
+        throw LoadException(
+            path, '"$htmlTemplatePath" does not exist or is not readable');
+      }
+
+      final templateFileContents = File(htmlTemplatePath).readAsStringSync();
+      if ('{{testScript}}'.allMatches(templateFileContents).length != 1) {
+        throw LoadException(path,
+            '"$htmlTemplatePath" must contain exactly one {{testScript}} placeholder');
+      }
+      _checkHtmlCorrectness(htmlTemplatePath, path);
+    }
+
+    if (_closed) return null;
+    await support.compileSuite(path, suiteConfig, platform);
+
+    var suiteUrl = support.serverUrl.resolveUri(
+        p.toUri('${p.withoutExtension(p.relative(path, from: _root))}.html'));
+
+    if (_closed) return null;
+
+    var browserManager = await _browserManagerFor(browser, compiler);
+    if (_closed || browserManager == null) return null;
+
+    var timeout = const Duration(seconds: 30);
+    if (suiteConfig.metadata.timeout.apply(timeout) case final suiteTimeout?
+        when suiteTimeout > timeout) {
+      timeout = suiteTimeout;
+    }
+    var suite = await browserManager.load(
+        path, suiteUrl, suiteConfig, message, platform.compiler,
+        mapper: (await compilerSupport(compiler)).stackTraceMapperForPath(path),
+        timeout: timeout);
+    if (_closed) return null;
+    return suite;
+  }
+
+  void _checkHtmlCorrectness(String htmlPath, String path) {
+    if (!File(htmlPath).readAsStringSync().contains('packages/test/dart.js')) {
+      throw LoadException(
+          path,
+          '"$htmlPath" must contain <script src="packages/test/dart.js">'
+          '</script>.');
+    }
+  }
+
+  /// Returns the [BrowserManager] for [runtime], which should be a browser.
+  ///
+  /// If no browser manager is running yet, starts one.
+  Future<BrowserManager?> _browserManagerFor(
+      Runtime browser, Compiler compiler) {
+    var managerFuture = _browserManagers[(browser, compiler)];
+    if (managerFuture != null) return managerFuture;
+
+    var future = _createBrowserManager(browser, compiler);
+    // Store null values for browsers that error out so we know not to load them
+    // again.
+    _browserManagers[(browser, compiler)] =
+        future.then<BrowserManager?>((value) => value).onError((_, __) => null);
+
+    return future;
+  }
+
+  Future<BrowserManager> _createBrowserManager(
+      Runtime browser, Compiler compiler) async {
+    var support = await compilerSupport(compiler);
+    var (webSocketUrl, socketFuture) = support.webSocket;
+    var hostUrl = support.serverUrl
+        .resolve('packages/test/src/runner/browser/static/index.html')
+        .replace(queryParameters: {
+      'managerUrl': webSocketUrl.toString(),
+      'debug': _config.debug.toString()
+    });
+
+    return BrowserManager.start(
+        browser, hostUrl, socketFuture, _browserSettings[browser]!, _config);
+  }
+
+  /// Close all the browsers that the server currently has open.
+  ///
+  /// Note that this doesn't close the server itself. Browser tests can still be
+  /// loaded, they'll just spawn new browsers.
+  @override
+  Future<List<void>> closeEphemeral() {
+    var managers = _browserManagers.values.toList();
+    _browserManagers.clear();
+    return Future.wait(managers.map((manager) async {
+      var result = await manager;
+      if (result == null) return;
+      await result.close();
+    }));
+  }
+
+  /// Closes the server and releases all its resources.
+  ///
+  /// Returns a [Future] that completes once the server is closed and its
+  /// resources have been fully released.
+  @override
+  Future<void> close() async => _closeMemo.runOnce(() => Future.wait([
+        for (var browser in _browserManagers.values)
+          browser.then((b) => b?.close()),
+        for (var support in _compilerSupport.values)
+          support.then((s) => s.close()),
+      ]));
+  final _closeMemo = AsyncMemoizer<void>();
+}
diff --git a/pkgs/test/lib/src/runner/browser/post_message_channel.dart b/pkgs/test/lib/src/runner/browser/post_message_channel.dart
new file mode 100644
index 0000000..60a5a6b
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/post_message_channel.dart
@@ -0,0 +1,31 @@
+// 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.
+
+import 'dart:js_util';
+
+import 'package:stream_channel/stream_channel.dart';
+
+import 'dom.dart' as dom;
+
+/// Constructs a [StreamChannel] wrapping a new [MessageChannel] communicating
+/// with the host page.
+///
+/// Sends a [MessagePort] to the host page for the channel.
+StreamChannel<Object?> postMessageChannel() {
+  dom.window.console.log('Suite starting, sending channel to host');
+  var controller = StreamChannelController<Object?>(sync: true);
+  var channel = dom.createMessageChannel();
+  dom.window.parent
+      .postMessage('port', dom.window.location.origin, [channel.port2]);
+  var portSubscription = dom.Subscription(channel.port1, 'message',
+      allowInterop((dom.Event event) {
+    controller.local.sink.add((event as dom.MessageEvent).data);
+  }));
+  channel.port1.start();
+
+  controller.local.stream
+      .listen(channel.port1.postMessage, onDone: portSubscription.cancel);
+
+  return controller.foreign;
+}
diff --git a/pkgs/test/lib/src/runner/browser/safari.dart b/pkgs/test/lib/src/runner/browser/safari.dart
new file mode 100644
index 0000000..4e97205
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/safari.dart
@@ -0,0 +1,48 @@
+// 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.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports
+
+import '../executable_settings.dart';
+import 'browser.dart';
+import 'default_settings.dart';
+
+/// A class for running an instance of Safari.
+///
+/// Any errors starting or running the process are reported through [onExit].
+class Safari extends Browser {
+  @override
+  final name = 'Safari';
+
+  Safari(Uri url, {ExecutableSettings? settings})
+      : super(() =>
+            _startBrowser(url, settings ?? defaultSettings[Runtime.safari]!));
+
+  /// Starts a new instance of Safari open to the given [url], which may be a
+  /// [Uri] or a [String].
+  static Future<Process> _startBrowser(
+      Uri url, ExecutableSettings settings) async {
+    var dir = createTempDir();
+
+    // Safari will only open files (not general URLs) via the command-line
+    // API, so we create a dummy file to redirect it to the page we actually
+    // want it to load.
+    var redirect = p.join(dir, 'redirect.html');
+    File(redirect).writeAsStringSync(
+        '<script>location = ${jsonEncode(url.toString())}</script>');
+
+    var process = await Process.start(
+        settings.executable, settings.arguments.toList()..add(redirect));
+
+    unawaited(process.exitCode.then((_) => Directory(dir).deleteWithRetry()));
+
+    return process;
+  }
+}
diff --git a/pkgs/test/lib/src/runner/browser/static/default.html.tpl b/pkgs/test/lib/src/runner/browser/static/default.html.tpl
new file mode 100644
index 0000000..a92f529
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/static/default.html.tpl
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>{{testName}} Test</title>
+    {{testScript}}
+    <script src="packages/test/dart.js"></script>
+  </head>
+</html>
diff --git a/pkgs/test/lib/src/runner/browser/static/favicon.ico b/pkgs/test/lib/src/runner/browser/static/favicon.ico
new file mode 100644
index 0000000..7ba349b
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/static/favicon.ico
Binary files differ
diff --git a/pkgs/test/lib/src/runner/browser/static/host.css b/pkgs/test/lib/src/runner/browser/static/host.css
new file mode 100644
index 0000000..b8e0352
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/static/host.css
@@ -0,0 +1,316 @@
+/* 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. */
+
+iframe {
+  /* We would use display: none here, but then Firefox fails to properly compute
+   * styles. See #274 */
+  visibility: hidden;
+  width: 1024px;
+  height: 768px;
+}
+
+#play {
+  display: none;
+  cursor: pointer;
+}
+
+#dark {
+  display: none;
+}
+
+.paused #play {
+  display: block;
+  z-index: 1;
+}
+
+.paused #dark {
+  display: block;
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: 0;
+  right: 0;
+  background-color: rgba(0, 0, 0, 0.5);
+}
+
+.paused #right-flank, .paused #right-ear, .paused #right-paw,
+.paused #left-flank,  .paused #left-ear,  .paused #left-paw {
+  -webkit-animation-play-state: paused;
+          animation-play-state: paused;
+}
+
+.debug body {
+  margin: 0;
+  padding: 0;
+}
+
+.debug iframe {
+  visibility: visible;
+  border: none;
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+}
+
+/* Compiled output from
+ * http://codepen.io/mknadler/pen/11b75cb014a3c382f54abf527655af21. */
+
+svg {
+  position: absolute;
+  margin: auto;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  top: 0;
+}
+
+#right-flank {
+  fill: #0074C1;
+  stroke-color: #0074C1;
+  -webkit-animation: right-flank 8s ease infinite alternate;
+          animation: right-flank 8s ease infinite alternate;
+}
+
+#right-ear {
+  fill: #00B5AB;
+  stroke-color: #00B5AB;
+  -webkit-animation: right-ear 8s ease-in infinite alternate;
+          animation: right-ear 8s ease-in infinite alternate;
+}
+
+#right-paw {
+  fill: #00A6E4;
+  stroke-color: #00A6E4;
+  -webkit-animation: right-paw 8s ease-out infinite alternate;
+          animation: right-paw 8s ease-out infinite alternate;
+}
+
+#left-flank {
+  fill: #00B5AB;
+  stroke-color: #00B5AB;
+  -webkit-animation: left-flank 8s ease-in-out infinite alternate;
+          animation: left-flank 8s ease-in-out infinite alternate;
+}
+
+#left-ear {
+  fill: #0074C1;
+  stroke-color: #0074C1;
+  -webkit-animation: left-ear 8s linear infinite alternate;
+          animation: left-ear 8s linear infinite alternate;
+}
+
+#left-paw {
+  fill: #41C1BC;
+  stroke-color: #41C1BC;
+  -webkit-animation: left-paw 8s ease infinite alternate;
+          animation: left-paw 8s ease infinite alternate;
+}
+
+@-webkit-keyframes left-ear {
+  20% {
+    -webkit-transform: translate(250px, 150px) rotateY(180deg) scale(0.6);
+            transform: translate(250px, 150px) rotateY(180deg) scale(0.6);
+    fill: #00A6E4;
+  }
+  50% {
+    -webkit-transform: translate(100px, 75px) rotateY(80deg) scale(1.1);
+            transform: translate(100px, 75px) rotateY(80deg) scale(1.1);
+    fill: #41C1BC;
+  }
+  80% {
+    -webkit-transform: translate(0px, 0px) scale(1);
+            transform: translate(0px, 0px) scale(1);
+    fill: #0074C1;
+  }
+}
+
+@keyframes left-ear {
+  20% {
+    -webkit-transform: translate(250px, 150px) rotateY(180deg) scale(0.6);
+            transform: translate(250px, 150px) rotateY(180deg) scale(0.6);
+    fill: #00A6E4;
+  }
+  50% {
+    -webkit-transform: translate(100px, 75px) rotateY(80deg) scale(1.1);
+            transform: translate(100px, 75px) rotateY(80deg) scale(1.1);
+    fill: #41C1BC;
+  }
+  80% {
+    -webkit-transform: translate(0px, 0px) scale(1);
+            transform: translate(0px, 0px) scale(1);
+    fill: #0074C1;
+  }
+}
+@-webkit-keyframes right-ear {
+  20% {
+    -webkit-transform: translate(200px, 250px) rotateX(180deg) scale(0.6);
+            transform: translate(200px, 250px) rotateX(180deg) scale(0.6);
+    fill: #41C1BC;
+  }
+  50% {
+    -webkit-transform: translate(75px, 100px) rotateX(80deg) scale(1.1);
+            transform: translate(75px, 100px) rotateX(80deg) scale(1.1);
+    fill: #00A6E4;
+  }
+  80% {
+    -webkit-transform: translate(0px, 0px) scale(1);
+            transform: translate(0px, 0px) scale(1);
+    fill: #00B5AB;
+  }
+}
+@keyframes right-ear {
+  20% {
+    -webkit-transform: translate(200px, 250px) rotateX(180deg) scale(0.6);
+            transform: translate(200px, 250px) rotateX(180deg) scale(0.6);
+    fill: #41C1BC;
+  }
+  50% {
+    -webkit-transform: translate(75px, 100px) rotateX(80deg) scale(1.1);
+            transform: translate(75px, 100px) rotateX(80deg) scale(1.1);
+    fill: #00A6E4;
+  }
+  80% {
+    -webkit-transform: translate(0px, 0px) scale(1);
+            transform: translate(0px, 0px) scale(1);
+    fill: #00B5AB;
+  }
+}
+@-webkit-keyframes left-paw {
+  20% {
+    -webkit-transform: translate(200px, 200px) rotate3d(-1, 0, 0.5, 60deg) scale(0.6);
+            transform: translate(200px, 200px) rotate3d(-1, 0, 0.5, 60deg) scale(0.6);
+    fill: #00B5AB;
+  }
+  50% {
+    -webkit-transform: translate(150px, 250px) rotate3d(-1, 0, 0.5, 90deg) scale(0.6);
+            transform: translate(150px, 250px) rotate3d(-1, 0, 0.5, 90deg) scale(0.6);
+    fill: #00B5AB;
+  }
+  80% {
+    -webkit-transform: translate(0px, 0px) scale(1);
+            transform: translate(0px, 0px) scale(1);
+    fill: #41C1BC;
+  }
+}
+@keyframes left-paw {
+  20% {
+    -webkit-transform: translate(200px, 200px) rotate3d(-1, 0, 0.5, 60deg) scale(0.6);
+            transform: translate(200px, 200px) rotate3d(-1, 0, 0.5, 60deg) scale(0.6);
+    fill: #00B5AB;
+  }
+  50% {
+    -webkit-transform: translate(150px, 250px) rotate3d(-1, 0, 0.5, 90deg) scale(0.6);
+            transform: translate(150px, 250px) rotate3d(-1, 0, 0.5, 90deg) scale(0.6);
+    fill: #00B5AB;
+  }
+  80% {
+    -webkit-transform: translate(0px, 0px) scale(1);
+            transform: translate(0px, 0px) scale(1);
+    fill: #41C1BC;
+  }
+}
+@-webkit-keyframes right-paw {
+  20% {
+    -webkit-transform: translate(200px, 200px) rotate3d(-1, 0, 0.5, -60deg) scale(0.6);
+            transform: translate(200px, 200px) rotate3d(-1, 0, 0.5, -60deg) scale(0.6);
+    fill: #41C1BC;
+  }
+  50% {
+    -webkit-transform: translate(100px, 250px) rotate3d(-1, 0, 0.5, -90deg) scale(0.6);
+            transform: translate(100px, 250px) rotate3d(-1, 0, 0.5, -90deg) scale(0.6);
+    fill: #41C1BC;
+  }
+  80% {
+    -webkit-transform: translate(0px, 0px) scale(1);
+            transform: translate(0px, 0px) scale(1);
+    fill: #00A6E4;
+  }
+}
+@keyframes right-paw {
+  20% {
+    -webkit-transform: translate(200px, 200px) rotate3d(-1, 0, 0.5, -60deg) scale(0.6);
+            transform: translate(200px, 200px) rotate3d(-1, 0, 0.5, -60deg) scale(0.6);
+    fill: #41C1BC;
+  }
+  50% {
+    -webkit-transform: translate(100px, 250px) rotate3d(-1, 0, 0.5, -90deg) scale(0.6);
+            transform: translate(100px, 250px) rotate3d(-1, 0, 0.5, -90deg) scale(0.6);
+    fill: #41C1BC;
+  }
+  80% {
+    -webkit-transform: translate(0px, 0px) scale(1);
+            transform: translate(0px, 0px) scale(1);
+    fill: #00A6E4;
+  }
+}
+@-webkit-keyframes left-flank {
+  20% {
+    -webkit-transform: translate(0px, 100px) scale(0.6);
+            transform: translate(0px, 100px) scale(0.6);
+    fill: #00A6E4;
+  }
+  50% {
+    -webkit-transform: translate(0px, 100px) scale(0.4);
+            transform: translate(0px, 100px) scale(0.4);
+    fill: #00A6E4;
+  }
+  80% {
+    -webkit-transform: translate(0px, 0px) scale(1);
+            transform: translate(0px, 0px) scale(1);
+    fill: #00B5AB;
+  }
+}
+@keyframes left-flank {
+  20% {
+    -webkit-transform: translate(0px, 100px) scale(0.6);
+            transform: translate(0px, 100px) scale(0.6);
+    fill: #00A6E4;
+  }
+  50% {
+    -webkit-transform: translate(0px, 100px) scale(0.4);
+            transform: translate(0px, 100px) scale(0.4);
+    fill: #00A6E4;
+  }
+  80% {
+    -webkit-transform: translate(0px, 0px) scale(1);
+            transform: translate(0px, 0px) scale(1);
+    fill: #00B5AB;
+  }
+}
+@-webkit-keyframes right-flank {
+  20% {
+    -webkit-transform: translate(100px, -25px) scale(0.6);
+            transform: translate(100px, -25px) scale(0.6);
+    fill: #41C1BC;
+  }
+  50% {
+    -webkit-transform: translate(110px, 0px) scale(0.4);
+            transform: translate(110px, 0px) scale(0.4);
+    fill: #00A6E4;
+  }
+  80% {
+    -webkit-transform: translate(0px, 0px) scale(1);
+            transform: translate(0px, 0px) scale(1);
+    fill: #0074C1;
+  }
+}
+@keyframes right-flank {
+  20% {
+    -webkit-transform: translate(100px, -25px) scale(0.6);
+            transform: translate(100px, -25px) scale(0.6);
+    fill: #41C1BC;
+  }
+  50% {
+    -webkit-transform: translate(110px, 0px) scale(0.4);
+            transform: translate(110px, 0px) scale(0.4);
+    fill: #00A6E4;
+  }
+  80% {
+    -webkit-transform: translate(0px, 0px) scale(1);
+            transform: translate(0px, 0px) scale(1);
+    fill: #0074C1;
+  }
+}
diff --git a/pkgs/test/lib/src/runner/browser/static/host.dart.js b/pkgs/test/lib/src/runner/browser/static/host.dart.js
new file mode 100644
index 0000000..fd3ef38
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/static/host.dart.js
@@ -0,0 +1,18333 @@
+// Generated by dart2js (NullSafetyMode.sound, csp, intern-composite-values), the Dart to JavaScript compiler version: 3.2.0-228.0.dev.
+// The code supports the following hooks:
+// dartPrint(message):
+//    if this function is defined it is called instead of the Dart [print]
+//    method.
+//
+// dartMainRunner(main, args):
+//    if this function is defined, the Dart [main] method will not be invoked
+//    directly. Instead, a closure that will invoke [main], and its arguments
+//    [args] is passed to [dartMainRunner].
+//
+// dartDeferredLibraryLoader(uri, successCallback, errorCallback, loadId, loadPriority):
+//    if this function is defined, it will be called when a deferred library
+//    is loaded. It should load and eval the javascript of `uri`, and call
+//    successCallback. If it fails to do so, it should call errorCallback with
+//    an error. The loadId argument is the deferred import that resulted in
+//    this uri being loaded. The loadPriority argument is the priority the
+//    library should be loaded with as specified in the code via the
+//    load-priority annotation (0: normal, 1: high).
+//
+// dartCallInstrumentation(id, qualifiedName):
+//    if this function is defined, it will be called at each entry of a
+//    method or constructor. Used only when compiling programs with
+//    --experiment-call-instrumentation.
+(function dartProgram() {
+  function copyProperties(from, to) {
+    var keys = Object.keys(from);
+    for (var i = 0; i < keys.length; i++) {
+      var key = keys[i];
+      to[key] = from[key];
+    }
+  }
+  function mixinPropertiesHard(from, to) {
+    var keys = Object.keys(from);
+    for (var i = 0; i < keys.length; i++) {
+      var key = keys[i];
+      if (!to.hasOwnProperty(key))
+        to[key] = from[key];
+    }
+  }
+  function mixinPropertiesEasy(from, to) {
+    Object.assign(to, from);
+  }
+  var supportsDirectProtoAccess = function() {
+    var cls = function() {
+    };
+    cls.prototype = {p: {}};
+    var object = new cls();
+    if (!(Object.getPrototypeOf(object) && Object.getPrototypeOf(object).p === cls.prototype.p))
+      return false;
+    try {
+      if (typeof navigator != "undefined" && typeof navigator.userAgent == "string" && navigator.userAgent.indexOf("Chrome/") >= 0)
+        return true;
+      if (typeof version == "function" && version.length == 0) {
+        var v = version();
+        if (/^\d+\.\d+\.\d+\.\d+$/.test(v))
+          return true;
+      }
+    } catch (_) {
+    }
+    return false;
+  }();
+  function inherit(cls, sup) {
+    cls.prototype.constructor = cls;
+    cls.prototype["$is" + cls.name] = cls;
+    if (sup != null) {
+      if (supportsDirectProtoAccess) {
+        Object.setPrototypeOf(cls.prototype, sup.prototype);
+        return;
+      }
+      var clsPrototype = Object.create(sup.prototype);
+      copyProperties(cls.prototype, clsPrototype);
+      cls.prototype = clsPrototype;
+    }
+  }
+  function inheritMany(sup, classes) {
+    for (var i = 0; i < classes.length; i++)
+      inherit(classes[i], sup);
+  }
+  function mixinEasy(cls, mixin) {
+    mixinPropertiesEasy(mixin.prototype, cls.prototype);
+    cls.prototype.constructor = cls;
+  }
+  function mixinHard(cls, mixin) {
+    mixinPropertiesHard(mixin.prototype, cls.prototype);
+    cls.prototype.constructor = cls;
+  }
+  function lazyOld(holder, name, getterName, initializer) {
+    var uninitializedSentinel = holder;
+    holder[name] = uninitializedSentinel;
+    holder[getterName] = function() {
+      holder[getterName] = function() {
+        A.throwCyclicInit(name);
+      };
+      var result;
+      var sentinelInProgress = initializer;
+      try {
+        if (holder[name] === uninitializedSentinel) {
+          result = holder[name] = sentinelInProgress;
+          result = holder[name] = initializer();
+        } else
+          result = holder[name];
+      } finally {
+        if (result === sentinelInProgress)
+          holder[name] = null;
+        holder[getterName] = function() {
+          return this[name];
+        };
+      }
+      return result;
+    };
+  }
+  function lazy(holder, name, getterName, initializer) {
+    var uninitializedSentinel = holder;
+    holder[name] = uninitializedSentinel;
+    holder[getterName] = function() {
+      if (holder[name] === uninitializedSentinel)
+        holder[name] = initializer();
+      holder[getterName] = function() {
+        return this[name];
+      };
+      return holder[name];
+    };
+  }
+  function lazyFinal(holder, name, getterName, initializer) {
+    var uninitializedSentinel = holder;
+    holder[name] = uninitializedSentinel;
+    holder[getterName] = function() {
+      if (holder[name] === uninitializedSentinel) {
+        var value = initializer();
+        if (holder[name] !== uninitializedSentinel)
+          A.throwLateFieldADI(name);
+        holder[name] = value;
+      }
+      var finalValue = holder[name];
+      holder[getterName] = function() {
+        return finalValue;
+      };
+      return finalValue;
+    };
+  }
+  function makeConstList(list) {
+    list.immutable$list = Array;
+    list.fixed$length = Array;
+    return list;
+  }
+  function convertToFastObject(properties) {
+    function t() {
+    }
+    t.prototype = properties;
+    new t();
+    return properties;
+  }
+  function convertAllToFastObject(arrayOfObjects) {
+    for (var i = 0; i < arrayOfObjects.length; ++i)
+      convertToFastObject(arrayOfObjects[i]);
+  }
+  var functionCounter = 0;
+  function instanceTearOffGetter(isIntercepted, parameters) {
+    var cache = null;
+    return isIntercepted ? function(receiver) {
+      if (cache === null)
+        cache = A.closureFromTearOff(parameters);
+      return new cache(receiver, this);
+    } : function() {
+      if (cache === null)
+        cache = A.closureFromTearOff(parameters);
+      return new cache(this, null);
+    };
+  }
+  function staticTearOffGetter(parameters) {
+    var cache = null;
+    return function() {
+      if (cache === null)
+        cache = A.closureFromTearOff(parameters).prototype;
+      return cache;
+    };
+  }
+  var typesOffset = 0;
+  function tearOffParameters(container, isStatic, isIntercepted, requiredParameterCount, optionalParameterDefaultValues, callNames, funsOrNames, funType, applyIndex, needsDirectAccess) {
+    if (typeof funType == "number")
+      funType += typesOffset;
+    return {co: container, iS: isStatic, iI: isIntercepted, rC: requiredParameterCount, dV: optionalParameterDefaultValues, cs: callNames, fs: funsOrNames, fT: funType, aI: applyIndex || 0, nDA: needsDirectAccess};
+  }
+  function installStaticTearOff(holder, getterName, requiredParameterCount, optionalParameterDefaultValues, callNames, funsOrNames, funType, applyIndex) {
+    var parameters = tearOffParameters(holder, true, false, requiredParameterCount, optionalParameterDefaultValues, callNames, funsOrNames, funType, applyIndex, false);
+    var getterFunction = staticTearOffGetter(parameters);
+    holder[getterName] = getterFunction;
+  }
+  function installInstanceTearOff(prototype, getterName, isIntercepted, requiredParameterCount, optionalParameterDefaultValues, callNames, funsOrNames, funType, applyIndex, needsDirectAccess) {
+    isIntercepted = !!isIntercepted;
+    var parameters = tearOffParameters(prototype, false, isIntercepted, requiredParameterCount, optionalParameterDefaultValues, callNames, funsOrNames, funType, applyIndex, !!needsDirectAccess);
+    var getterFunction = instanceTearOffGetter(isIntercepted, parameters);
+    prototype[getterName] = getterFunction;
+  }
+  function setOrUpdateInterceptorsByTag(newTags) {
+    var tags = init.interceptorsByTag;
+    if (!tags) {
+      init.interceptorsByTag = newTags;
+      return;
+    }
+    copyProperties(newTags, tags);
+  }
+  function setOrUpdateLeafTags(newTags) {
+    var tags = init.leafTags;
+    if (!tags) {
+      init.leafTags = newTags;
+      return;
+    }
+    copyProperties(newTags, tags);
+  }
+  function updateTypes(newTypes) {
+    var types = init.types;
+    var length = types.length;
+    types.push.apply(types, newTypes);
+    return length;
+  }
+  function updateHolder(holder, newHolder) {
+    copyProperties(newHolder, holder);
+    return holder;
+  }
+  var hunkHelpers = function() {
+    var mkInstance = function(isIntercepted, requiredParameterCount, optionalParameterDefaultValues, callNames, applyIndex) {
+        return function(container, getterName, name, funType) {
+          return installInstanceTearOff(container, getterName, isIntercepted, requiredParameterCount, optionalParameterDefaultValues, callNames, [name], funType, applyIndex, false);
+        };
+      },
+      mkStatic = function(requiredParameterCount, optionalParameterDefaultValues, callNames, applyIndex) {
+        return function(container, getterName, name, funType) {
+          return installStaticTearOff(container, getterName, requiredParameterCount, optionalParameterDefaultValues, callNames, [name], funType, applyIndex);
+        };
+      };
+    return {inherit: inherit, inheritMany: inheritMany, mixin: mixinEasy, mixinHard: mixinHard, installStaticTearOff: installStaticTearOff, installInstanceTearOff: installInstanceTearOff, _instance_0u: mkInstance(0, 0, null, ["call$0"], 0), _instance_1u: mkInstance(0, 1, null, ["call$1"], 0), _instance_2u: mkInstance(0, 2, null, ["call$2"], 0), _instance_0i: mkInstance(1, 0, null, ["call$0"], 0), _instance_1i: mkInstance(1, 1, null, ["call$1"], 0), _instance_2i: mkInstance(1, 2, null, ["call$2"], 0), _static_0: mkStatic(0, null, ["call$0"], 0), _static_1: mkStatic(1, null, ["call$1"], 0), _static_2: mkStatic(2, null, ["call$2"], 0), makeConstList: makeConstList, lazy: lazy, lazyFinal: lazyFinal, lazyOld: lazyOld, updateHolder: updateHolder, convertToFastObject: convertToFastObject, updateTypes: updateTypes, setOrUpdateInterceptorsByTag: setOrUpdateInterceptorsByTag, setOrUpdateLeafTags: setOrUpdateLeafTags};
+  }();
+  function initializeDeferredHunk(hunk) {
+    typesOffset = init.types.length;
+    hunk(hunkHelpers, init, holders, $);
+  }
+  var J = {
+    makeDispatchRecord(interceptor, proto, extension, indexability) {
+      return {i: interceptor, p: proto, e: extension, x: indexability};
+    },
+    getNativeInterceptor(object) {
+      var proto, objectProto, $constructor, interceptor, t1,
+        record = object[init.dispatchPropertyName];
+      if (record == null)
+        if ($.initNativeDispatchFlag == null) {
+          A.initNativeDispatch();
+          record = object[init.dispatchPropertyName];
+        }
+      if (record != null) {
+        proto = record.p;
+        if (false === proto)
+          return record.i;
+        if (true === proto)
+          return object;
+        objectProto = Object.getPrototypeOf(object);
+        if (proto === objectProto)
+          return record.i;
+        if (record.e === objectProto)
+          throw A.wrapException(A.UnimplementedError$("Return interceptor for " + A.S(proto(object, record))));
+      }
+      $constructor = object.constructor;
+      if ($constructor == null)
+        interceptor = null;
+      else {
+        t1 = $._JS_INTEROP_INTERCEPTOR_TAG;
+        if (t1 == null)
+          t1 = $._JS_INTEROP_INTERCEPTOR_TAG = init.getIsolateTag("_$dart_js");
+        interceptor = $constructor[t1];
+      }
+      if (interceptor != null)
+        return interceptor;
+      interceptor = A.lookupAndCacheInterceptor(object);
+      if (interceptor != null)
+        return interceptor;
+      if (typeof object == "function")
+        return B.JavaScriptFunction_methods;
+      proto = Object.getPrototypeOf(object);
+      if (proto == null)
+        return B.PlainJavaScriptObject_methods;
+      if (proto === Object.prototype)
+        return B.PlainJavaScriptObject_methods;
+      if (typeof $constructor == "function") {
+        t1 = $._JS_INTEROP_INTERCEPTOR_TAG;
+        if (t1 == null)
+          t1 = $._JS_INTEROP_INTERCEPTOR_TAG = init.getIsolateTag("_$dart_js");
+        Object.defineProperty($constructor, t1, {value: B.UnknownJavaScriptObject_methods, enumerable: false, writable: true, configurable: true});
+        return B.UnknownJavaScriptObject_methods;
+      }
+      return B.UnknownJavaScriptObject_methods;
+    },
+    JSArray_JSArray$fixed($length, $E) {
+      if ($length < 0 || $length > 4294967295)
+        throw A.wrapException(A.RangeError$range($length, 0, 4294967295, "length", null));
+      return J.JSArray_JSArray$markFixed(new Array($length), $E);
+    },
+    JSArray_JSArray$growable($length, $E) {
+      if ($length < 0)
+        throw A.wrapException(A.ArgumentError$("Length must be a non-negative integer: " + $length, null));
+      return A._setArrayType(new Array($length), $E._eval$1("JSArray<0>"));
+    },
+    JSArray_JSArray$markFixed(allocation, $E) {
+      return J.JSArray_markFixedList(A._setArrayType(allocation, $E._eval$1("JSArray<0>")), $E);
+    },
+    JSArray_markFixedList(list, $T) {
+      list.fixed$length = Array;
+      return list;
+    },
+    JSArray_markUnmodifiableList(list) {
+      list.fixed$length = Array;
+      list.immutable$list = Array;
+      return list;
+    },
+    JSString__isWhitespace(codeUnit) {
+      if (codeUnit < 256)
+        switch (codeUnit) {
+          case 9:
+          case 10:
+          case 11:
+          case 12:
+          case 13:
+          case 32:
+          case 133:
+          case 160:
+            return true;
+          default:
+            return false;
+        }
+      switch (codeUnit) {
+        case 5760:
+        case 8192:
+        case 8193:
+        case 8194:
+        case 8195:
+        case 8196:
+        case 8197:
+        case 8198:
+        case 8199:
+        case 8200:
+        case 8201:
+        case 8202:
+        case 8232:
+        case 8233:
+        case 8239:
+        case 8287:
+        case 12288:
+        case 65279:
+          return true;
+        default:
+          return false;
+      }
+    },
+    JSString__skipLeadingWhitespace(string, index) {
+      var t1, codeUnit;
+      for (t1 = string.length; index < t1;) {
+        codeUnit = string.charCodeAt(index);
+        if (codeUnit !== 32 && codeUnit !== 13 && !J.JSString__isWhitespace(codeUnit))
+          break;
+        ++index;
+      }
+      return index;
+    },
+    JSString__skipTrailingWhitespace(string, index) {
+      var t1, index0, codeUnit;
+      for (t1 = string.length; index > 0; index = index0) {
+        index0 = index - 1;
+        if (!(index0 < t1))
+          return A.ioore(string, index0);
+        codeUnit = string.charCodeAt(index0);
+        if (codeUnit !== 32 && codeUnit !== 13 && !J.JSString__isWhitespace(codeUnit))
+          break;
+      }
+      return index;
+    },
+    getInterceptor$(receiver) {
+      if (typeof receiver == "number") {
+        if (Math.floor(receiver) == receiver)
+          return J.JSInt.prototype;
+        return J.JSNumNotInt.prototype;
+      }
+      if (typeof receiver == "string")
+        return J.JSString.prototype;
+      if (receiver == null)
+        return J.JSNull.prototype;
+      if (typeof receiver == "boolean")
+        return J.JSBool.prototype;
+      if (Array.isArray(receiver))
+        return J.JSArray.prototype;
+      if (typeof receiver != "object") {
+        if (typeof receiver == "function")
+          return J.JavaScriptFunction.prototype;
+        if (typeof receiver == "symbol")
+          return J.JavaScriptSymbol.prototype;
+        if (typeof receiver == "bigint")
+          return J.JavaScriptBigInt.prototype;
+        return receiver;
+      }
+      if (receiver instanceof A.Object)
+        return receiver;
+      return J.getNativeInterceptor(receiver);
+    },
+    getInterceptor$asx(receiver) {
+      if (typeof receiver == "string")
+        return J.JSString.prototype;
+      if (receiver == null)
+        return receiver;
+      if (Array.isArray(receiver))
+        return J.JSArray.prototype;
+      if (typeof receiver != "object") {
+        if (typeof receiver == "function")
+          return J.JavaScriptFunction.prototype;
+        if (typeof receiver == "symbol")
+          return J.JavaScriptSymbol.prototype;
+        if (typeof receiver == "bigint")
+          return J.JavaScriptBigInt.prototype;
+        return receiver;
+      }
+      if (receiver instanceof A.Object)
+        return receiver;
+      return J.getNativeInterceptor(receiver);
+    },
+    getInterceptor$ax(receiver) {
+      if (receiver == null)
+        return receiver;
+      if (Array.isArray(receiver))
+        return J.JSArray.prototype;
+      if (typeof receiver != "object") {
+        if (typeof receiver == "function")
+          return J.JavaScriptFunction.prototype;
+        if (typeof receiver == "symbol")
+          return J.JavaScriptSymbol.prototype;
+        if (typeof receiver == "bigint")
+          return J.JavaScriptBigInt.prototype;
+        return receiver;
+      }
+      if (receiver instanceof A.Object)
+        return receiver;
+      return J.getNativeInterceptor(receiver);
+    },
+    getInterceptor$n(receiver) {
+      if (typeof receiver == "number")
+        return J.JSNumber.prototype;
+      if (receiver == null)
+        return receiver;
+      if (!(receiver instanceof A.Object))
+        return J.UnknownJavaScriptObject.prototype;
+      return receiver;
+    },
+    getInterceptor$s(receiver) {
+      if (typeof receiver == "string")
+        return J.JSString.prototype;
+      if (receiver == null)
+        return receiver;
+      if (!(receiver instanceof A.Object))
+        return J.UnknownJavaScriptObject.prototype;
+      return receiver;
+    },
+    getInterceptor$x(receiver) {
+      if (receiver == null)
+        return receiver;
+      if (typeof receiver != "object") {
+        if (typeof receiver == "function")
+          return J.JavaScriptFunction.prototype;
+        if (typeof receiver == "symbol")
+          return J.JavaScriptSymbol.prototype;
+        if (typeof receiver == "bigint")
+          return J.JavaScriptBigInt.prototype;
+        return receiver;
+      }
+      if (receiver instanceof A.Object)
+        return receiver;
+      return J.getNativeInterceptor(receiver);
+    },
+    getInterceptor$z(receiver) {
+      if (receiver == null)
+        return receiver;
+      if (!(receiver instanceof A.Object))
+        return J.UnknownJavaScriptObject.prototype;
+      return receiver;
+    },
+    get$hashCode$(receiver) {
+      return J.getInterceptor$(receiver).get$hashCode(receiver);
+    },
+    get$isEmpty$asx(receiver) {
+      return J.getInterceptor$asx(receiver).get$isEmpty(receiver);
+    },
+    get$isNotEmpty$asx(receiver) {
+      return J.getInterceptor$asx(receiver).get$isNotEmpty(receiver);
+    },
+    get$iterator$ax(receiver) {
+      return J.getInterceptor$ax(receiver).get$iterator(receiver);
+    },
+    get$keys$x(receiver) {
+      return J.getInterceptor$x(receiver).get$keys(receiver);
+    },
+    get$length$asx(receiver) {
+      return J.getInterceptor$asx(receiver).get$length(receiver);
+    },
+    get$parent$z(receiver) {
+      return J.getInterceptor$z(receiver).get$parent(receiver);
+    },
+    get$runtimeType$(receiver) {
+      return J.getInterceptor$(receiver).get$runtimeType(receiver);
+    },
+    $eq$(receiver, a0) {
+      if (receiver == null)
+        return a0 == null;
+      if (typeof receiver != "object")
+        return a0 != null && receiver === a0;
+      return J.getInterceptor$(receiver).$eq(receiver, a0);
+    },
+    $index$asx(receiver, a0) {
+      if (typeof a0 === "number")
+        if (Array.isArray(receiver) || typeof receiver == "string" || A.isJsIndexable(receiver, receiver[init.dispatchPropertyName]))
+          if (a0 >>> 0 === a0 && a0 < receiver.length)
+            return receiver[a0];
+      return J.getInterceptor$asx(receiver).$index(receiver, a0);
+    },
+    $indexSet$ax(receiver, a0, a1) {
+      return J.getInterceptor$ax(receiver).$indexSet(receiver, a0, a1);
+    },
+    allMatches$1$s(receiver, a0) {
+      return J.getInterceptor$s(receiver).allMatches$1(receiver, a0);
+    },
+    allMatches$2$s(receiver, a0, a1) {
+      return J.getInterceptor$s(receiver).allMatches$2(receiver, a0, a1);
+    },
+    cancel$0$z(receiver) {
+      return J.getInterceptor$z(receiver).cancel$0(receiver);
+    },
+    cast$1$0$ax(receiver, $T1) {
+      return J.getInterceptor$ax(receiver).cast$1$0(receiver, $T1);
+    },
+    codeUnitAt$1$s(receiver, a0) {
+      return J.getInterceptor$s(receiver).codeUnitAt$1(receiver, a0);
+    },
+    contains$1$asx(receiver, a0) {
+      return J.getInterceptor$asx(receiver).contains$1(receiver, a0);
+    },
+    containsKey$1$x(receiver, a0) {
+      return J.getInterceptor$x(receiver).containsKey$1(receiver, a0);
+    },
+    elementAt$1$ax(receiver, a0) {
+      return J.getInterceptor$ax(receiver).elementAt$1(receiver, a0);
+    },
+    endsWith$1$s(receiver, a0) {
+      return J.getInterceptor$s(receiver).endsWith$1(receiver, a0);
+    },
+    forEach$1$x(receiver, a0) {
+      return J.getInterceptor$x(receiver).forEach$1(receiver, a0);
+    },
+    map$1$1$ax(receiver, a0, $T1) {
+      return J.getInterceptor$ax(receiver).map$1$1(receiver, a0, $T1);
+    },
+    matchAsPrefix$2$s(receiver, a0, a1) {
+      return J.getInterceptor$s(receiver).matchAsPrefix$2(receiver, a0, a1);
+    },
+    noSuchMethod$1$(receiver, a0) {
+      return J.getInterceptor$(receiver).noSuchMethod$1(receiver, a0);
+    },
+    skip$1$ax(receiver, a0) {
+      return J.getInterceptor$ax(receiver).skip$1(receiver, a0);
+    },
+    toInt$0$n(receiver) {
+      return J.getInterceptor$n(receiver).toInt$0(receiver);
+    },
+    toList$0$ax(receiver) {
+      return J.getInterceptor$ax(receiver).toList$0(receiver);
+    },
+    toString$0$(receiver) {
+      return J.getInterceptor$(receiver).toString$0(receiver);
+    },
+    Interceptor: function Interceptor() {
+    },
+    JSBool: function JSBool() {
+    },
+    JSNull: function JSNull() {
+    },
+    JavaScriptObject: function JavaScriptObject() {
+    },
+    LegacyJavaScriptObject: function LegacyJavaScriptObject() {
+    },
+    PlainJavaScriptObject: function PlainJavaScriptObject() {
+    },
+    UnknownJavaScriptObject: function UnknownJavaScriptObject() {
+    },
+    JavaScriptFunction: function JavaScriptFunction() {
+    },
+    JavaScriptBigInt: function JavaScriptBigInt() {
+    },
+    JavaScriptSymbol: function JavaScriptSymbol() {
+    },
+    JSArray: function JSArray(t0) {
+      this.$ti = t0;
+    },
+    JSUnmodifiableArray: function JSUnmodifiableArray(t0) {
+      this.$ti = t0;
+    },
+    ArrayIterator: function ArrayIterator(t0, t1, t2) {
+      var _ = this;
+      _._iterable = t0;
+      _.__interceptors$_length = t1;
+      _._index = 0;
+      _._current = null;
+      _.$ti = t2;
+    },
+    JSNumber: function JSNumber() {
+    },
+    JSInt: function JSInt() {
+    },
+    JSNumNotInt: function JSNumNotInt() {
+    },
+    JSString: function JSString() {
+    }
+  },
+  A = {JS_CONST: function JS_CONST() {
+    },
+    CastIterable_CastIterable(source, $S, $T) {
+      if ($S._eval$1("EfficientLengthIterable<0>")._is(source))
+        return new A._EfficientLengthCastIterable(source, $S._eval$1("@<0>")._bind$1($T)._eval$1("_EfficientLengthCastIterable<1,2>"));
+      return new A.CastIterable(source, $S._eval$1("@<0>")._bind$1($T)._eval$1("CastIterable<1,2>"));
+    },
+    hexDigitValue(char) {
+      var letter,
+        digit = char ^ 48;
+      if (digit <= 9)
+        return digit;
+      letter = char | 32;
+      if (97 <= letter && letter <= 102)
+        return letter - 87;
+      return -1;
+    },
+    SystemHash_combine(hash, value) {
+      hash = hash + value & 536870911;
+      hash = hash + ((hash & 524287) << 10) & 536870911;
+      return hash ^ hash >>> 6;
+    },
+    SystemHash_finish(hash) {
+      hash = hash + ((hash & 67108863) << 3) & 536870911;
+      hash ^= hash >>> 11;
+      return hash + ((hash & 16383) << 15) & 536870911;
+    },
+    checkNotNullable(value, $name, $T) {
+      return value;
+    },
+    isToStringVisiting(object) {
+      var t1, i;
+      for (t1 = $.toStringVisiting.length, i = 0; i < t1; ++i)
+        if (object === $.toStringVisiting[i])
+          return true;
+      return false;
+    },
+    SubListIterable$(_iterable, _start, _endOrLength, $E) {
+      A.RangeError_checkNotNegative(_start, "start");
+      if (_endOrLength != null) {
+        A.RangeError_checkNotNegative(_endOrLength, "end");
+        if (_start > _endOrLength)
+          A.throwExpression(A.RangeError$range(_start, 0, _endOrLength, "start", null));
+      }
+      return new A.SubListIterable(_iterable, _start, _endOrLength, $E._eval$1("SubListIterable<0>"));
+    },
+    MappedIterable_MappedIterable(iterable, $function, $S, $T) {
+      if (type$.EfficientLengthIterable_dynamic._is(iterable))
+        return new A.EfficientLengthMappedIterable(iterable, $function, $S._eval$1("@<0>")._bind$1($T)._eval$1("EfficientLengthMappedIterable<1,2>"));
+      return new A.MappedIterable(iterable, $function, $S._eval$1("@<0>")._bind$1($T)._eval$1("MappedIterable<1,2>"));
+    },
+    TakeIterable_TakeIterable(iterable, takeCount, $E) {
+      var _s9_ = "takeCount";
+      A.ArgumentError_checkNotNull(takeCount, _s9_, type$.int);
+      A.RangeError_checkNotNegative(takeCount, _s9_);
+      if (type$.EfficientLengthIterable_dynamic._is(iterable))
+        return new A.EfficientLengthTakeIterable(iterable, takeCount, $E._eval$1("EfficientLengthTakeIterable<0>"));
+      return new A.TakeIterable(iterable, takeCount, $E._eval$1("TakeIterable<0>"));
+    },
+    SkipIterable_SkipIterable(iterable, count, $E) {
+      var _s5_ = "count";
+      if (type$.EfficientLengthIterable_dynamic._is(iterable)) {
+        A.ArgumentError_checkNotNull(count, _s5_, type$.int);
+        A.RangeError_checkNotNegative(count, _s5_);
+        return new A.EfficientLengthSkipIterable(iterable, count, $E._eval$1("EfficientLengthSkipIterable<0>"));
+      }
+      A.ArgumentError_checkNotNull(count, _s5_, type$.int);
+      A.RangeError_checkNotNegative(count, _s5_);
+      return new A.SkipIterable(iterable, count, $E._eval$1("SkipIterable<0>"));
+    },
+    IterableElementError_noElement() {
+      return new A.StateError("No element");
+    },
+    IterableElementError_tooFew() {
+      return new A.StateError("Too few elements");
+    },
+    CastStream: function CastStream(t0, t1) {
+      this._source = t0;
+      this.$ti = t1;
+    },
+    CastStreamSubscription: function CastStreamSubscription(t0, t1, t2) {
+      var _ = this;
+      _._source = t0;
+      _.__internal$_zone = t1;
+      _._handleError = _._handleData = null;
+      _.$ti = t2;
+    },
+    _CastIterableBase: function _CastIterableBase() {
+    },
+    CastIterator: function CastIterator(t0, t1) {
+      this._source = t0;
+      this.$ti = t1;
+    },
+    CastIterable: function CastIterable(t0, t1) {
+      this._source = t0;
+      this.$ti = t1;
+    },
+    _EfficientLengthCastIterable: function _EfficientLengthCastIterable(t0, t1) {
+      this._source = t0;
+      this.$ti = t1;
+    },
+    _CastListBase: function _CastListBase() {
+    },
+    CastList: function CastList(t0, t1) {
+      this._source = t0;
+      this.$ti = t1;
+    },
+    LateError: function LateError(t0) {
+      this._message = t0;
+    },
+    CodeUnits: function CodeUnits(t0) {
+      this._string = t0;
+    },
+    nullFuture_closure: function nullFuture_closure() {
+    },
+    SentinelValue: function SentinelValue() {
+    },
+    EfficientLengthIterable: function EfficientLengthIterable() {
+    },
+    ListIterable: function ListIterable() {
+    },
+    SubListIterable: function SubListIterable(t0, t1, t2, t3) {
+      var _ = this;
+      _.__internal$_iterable = t0;
+      _._start = t1;
+      _._endOrLength = t2;
+      _.$ti = t3;
+    },
+    ListIterator: function ListIterator(t0, t1, t2) {
+      var _ = this;
+      _.__internal$_iterable = t0;
+      _.__internal$_length = t1;
+      _.__internal$_index = 0;
+      _.__internal$_current = null;
+      _.$ti = t2;
+    },
+    MappedIterable: function MappedIterable(t0, t1, t2) {
+      this.__internal$_iterable = t0;
+      this._f = t1;
+      this.$ti = t2;
+    },
+    EfficientLengthMappedIterable: function EfficientLengthMappedIterable(t0, t1, t2) {
+      this.__internal$_iterable = t0;
+      this._f = t1;
+      this.$ti = t2;
+    },
+    MappedIterator: function MappedIterator(t0, t1, t2) {
+      var _ = this;
+      _.__internal$_current = null;
+      _._iterator = t0;
+      _._f = t1;
+      _.$ti = t2;
+    },
+    MappedListIterable: function MappedListIterable(t0, t1, t2) {
+      this._source = t0;
+      this._f = t1;
+      this.$ti = t2;
+    },
+    WhereIterable: function WhereIterable(t0, t1, t2) {
+      this.__internal$_iterable = t0;
+      this._f = t1;
+      this.$ti = t2;
+    },
+    WhereIterator: function WhereIterator(t0, t1, t2) {
+      this._iterator = t0;
+      this._f = t1;
+      this.$ti = t2;
+    },
+    ExpandIterable: function ExpandIterable(t0, t1, t2) {
+      this.__internal$_iterable = t0;
+      this._f = t1;
+      this.$ti = t2;
+    },
+    ExpandIterator: function ExpandIterator(t0, t1, t2, t3) {
+      var _ = this;
+      _._iterator = t0;
+      _._f = t1;
+      _._currentExpansion = t2;
+      _.__internal$_current = null;
+      _.$ti = t3;
+    },
+    TakeIterable: function TakeIterable(t0, t1, t2) {
+      this.__internal$_iterable = t0;
+      this._takeCount = t1;
+      this.$ti = t2;
+    },
+    EfficientLengthTakeIterable: function EfficientLengthTakeIterable(t0, t1, t2) {
+      this.__internal$_iterable = t0;
+      this._takeCount = t1;
+      this.$ti = t2;
+    },
+    TakeIterator: function TakeIterator(t0, t1, t2) {
+      this._iterator = t0;
+      this._remaining = t1;
+      this.$ti = t2;
+    },
+    SkipIterable: function SkipIterable(t0, t1, t2) {
+      this.__internal$_iterable = t0;
+      this._skipCount = t1;
+      this.$ti = t2;
+    },
+    EfficientLengthSkipIterable: function EfficientLengthSkipIterable(t0, t1, t2) {
+      this.__internal$_iterable = t0;
+      this._skipCount = t1;
+      this.$ti = t2;
+    },
+    SkipIterator: function SkipIterator(t0, t1, t2) {
+      this._iterator = t0;
+      this._skipCount = t1;
+      this.$ti = t2;
+    },
+    SkipWhileIterable: function SkipWhileIterable(t0, t1, t2) {
+      this.__internal$_iterable = t0;
+      this._f = t1;
+      this.$ti = t2;
+    },
+    SkipWhileIterator: function SkipWhileIterator(t0, t1, t2) {
+      var _ = this;
+      _._iterator = t0;
+      _._f = t1;
+      _._hasSkipped = false;
+      _.$ti = t2;
+    },
+    EmptyIterable: function EmptyIterable(t0) {
+      this.$ti = t0;
+    },
+    EmptyIterator: function EmptyIterator(t0) {
+      this.$ti = t0;
+    },
+    WhereTypeIterable: function WhereTypeIterable(t0, t1) {
+      this._source = t0;
+      this.$ti = t1;
+    },
+    WhereTypeIterator: function WhereTypeIterator(t0, t1) {
+      this._source = t0;
+      this.$ti = t1;
+    },
+    FixedLengthListMixin: function FixedLengthListMixin() {
+    },
+    UnmodifiableListMixin: function UnmodifiableListMixin() {
+    },
+    UnmodifiableListBase: function UnmodifiableListBase() {
+    },
+    ReversedListIterable: function ReversedListIterable(t0, t1) {
+      this._source = t0;
+      this.$ti = t1;
+    },
+    Symbol: function Symbol(t0) {
+      this._name = t0;
+    },
+    __CastListBase__CastIterableBase_ListMixin: function __CastListBase__CastIterableBase_ListMixin() {
+    },
+    ConstantMap__throwUnmodifiable() {
+      throw A.wrapException(A.UnsupportedError$("Cannot modify unmodifiable Map"));
+    },
+    unminifyOrTag(rawClassName) {
+      var preserved = init.mangledGlobalNames[rawClassName];
+      if (preserved != null)
+        return preserved;
+      return rawClassName;
+    },
+    isJsIndexable(object, record) {
+      var result;
+      if (record != null) {
+        result = record.x;
+        if (result != null)
+          return result;
+      }
+      return type$.JavaScriptIndexingBehavior_dynamic._is(object);
+    },
+    S(value) {
+      var result;
+      if (typeof value == "string")
+        return value;
+      if (typeof value == "number") {
+        if (value !== 0)
+          return "" + value;
+      } else if (true === value)
+        return "true";
+      else if (false === value)
+        return "false";
+      else if (value == null)
+        return "null";
+      result = J.toString$0$(value);
+      return result;
+    },
+    Primitives_objectHashCode(object) {
+      var hash,
+        property = $.Primitives__identityHashCodeProperty;
+      if (property == null)
+        property = $.Primitives__identityHashCodeProperty = Symbol("identityHashCode");
+      hash = object[property];
+      if (hash == null) {
+        hash = Math.random() * 0x3fffffff | 0;
+        object[property] = hash;
+      }
+      return hash;
+    },
+    Primitives_parseInt(source, radix) {
+      var decimalMatch, maxCharCode, digitsPart, t1, i, _null = null,
+        match = /^\s*[+-]?((0x[a-f0-9]+)|(\d+)|([a-z0-9]+))\s*$/i.exec(source);
+      if (match == null)
+        return _null;
+      if (3 >= match.length)
+        return A.ioore(match, 3);
+      decimalMatch = match[3];
+      if (radix == null) {
+        if (decimalMatch != null)
+          return parseInt(source, 10);
+        if (match[2] != null)
+          return parseInt(source, 16);
+        return _null;
+      }
+      if (radix < 2 || radix > 36)
+        throw A.wrapException(A.RangeError$range(radix, 2, 36, "radix", _null));
+      if (radix === 10 && decimalMatch != null)
+        return parseInt(source, 10);
+      if (radix < 10 || decimalMatch == null) {
+        maxCharCode = radix <= 10 ? 47 + radix : 86 + radix;
+        digitsPart = match[1];
+        for (t1 = digitsPart.length, i = 0; i < t1; ++i)
+          if ((digitsPart.charCodeAt(i) | 32) > maxCharCode)
+            return _null;
+      }
+      return parseInt(source, radix);
+    },
+    Primitives_objectTypeName(object) {
+      return A.Primitives__objectTypeNameNewRti(object);
+    },
+    Primitives__objectTypeNameNewRti(object) {
+      var interceptor, dispatchName, $constructor, constructorName;
+      if (object instanceof A.Object)
+        return A._rtiToString(A.instanceType(object), null);
+      interceptor = J.getInterceptor$(object);
+      if (interceptor === B.Interceptor_methods || interceptor === B.JavaScriptObject_methods || type$.UnknownJavaScriptObject._is(object)) {
+        dispatchName = B.C_JS_CONST(object);
+        if (dispatchName !== "Object" && dispatchName !== "")
+          return dispatchName;
+        $constructor = object.constructor;
+        if (typeof $constructor == "function") {
+          constructorName = $constructor.name;
+          if (typeof constructorName == "string" && constructorName !== "Object" && constructorName !== "")
+            return constructorName;
+        }
+      }
+      return A._rtiToString(A.instanceType(object), null);
+    },
+    Primitives_safeToString(object) {
+      if (typeof object == "number" || A._isBool(object))
+        return J.toString$0$(object);
+      if (typeof object == "string")
+        return JSON.stringify(object);
+      if (object instanceof A.Closure)
+        return object.toString$0(0);
+      return "Instance of '" + A.Primitives_objectTypeName(object) + "'";
+    },
+    Primitives_currentUri() {
+      if (!!self.location)
+        return self.location.href;
+      return null;
+    },
+    Primitives__fromCharCodeApply(array) {
+      var result, i, i0, chunkEnd,
+        end = array.length;
+      if (end <= 500)
+        return String.fromCharCode.apply(null, array);
+      for (result = "", i = 0; i < end; i = i0) {
+        i0 = i + 500;
+        chunkEnd = i0 < end ? i0 : end;
+        result += String.fromCharCode.apply(null, array.slice(i, chunkEnd));
+      }
+      return result;
+    },
+    Primitives_stringFromCodePoints(codePoints) {
+      var t1, _i, i,
+        a = A._setArrayType([], type$.JSArray_int);
+      for (t1 = codePoints.length, _i = 0; _i < codePoints.length; codePoints.length === t1 || (0, A.throwConcurrentModificationError)(codePoints), ++_i) {
+        i = codePoints[_i];
+        if (!A._isInt(i))
+          throw A.wrapException(A.argumentErrorValue(i));
+        if (i <= 65535)
+          B.JSArray_methods.add$1(a, i);
+        else if (i <= 1114111) {
+          B.JSArray_methods.add$1(a, 55296 + (B.JSInt_methods._shrOtherPositive$1(i - 65536, 10) & 1023));
+          B.JSArray_methods.add$1(a, 56320 + (i & 1023));
+        } else
+          throw A.wrapException(A.argumentErrorValue(i));
+      }
+      return A.Primitives__fromCharCodeApply(a);
+    },
+    Primitives_stringFromCharCodes(charCodes) {
+      var t1, _i, i;
+      for (t1 = charCodes.length, _i = 0; _i < t1; ++_i) {
+        i = charCodes[_i];
+        if (!A._isInt(i))
+          throw A.wrapException(A.argumentErrorValue(i));
+        if (i < 0)
+          throw A.wrapException(A.argumentErrorValue(i));
+        if (i > 65535)
+          return A.Primitives_stringFromCodePoints(charCodes);
+      }
+      return A.Primitives__fromCharCodeApply(charCodes);
+    },
+    Primitives_stringFromNativeUint8List(charCodes, start, end) {
+      var i, result, i0, chunkEnd;
+      if (end <= 500 && start === 0 && end === charCodes.length)
+        return String.fromCharCode.apply(null, charCodes);
+      for (i = start, result = ""; i < end; i = i0) {
+        i0 = i + 500;
+        chunkEnd = i0 < end ? i0 : end;
+        result += String.fromCharCode.apply(null, charCodes.subarray(i, chunkEnd));
+      }
+      return result;
+    },
+    Primitives_stringFromCharCode(charCode) {
+      var bits;
+      if (0 <= charCode) {
+        if (charCode <= 65535)
+          return String.fromCharCode(charCode);
+        if (charCode <= 1114111) {
+          bits = charCode - 65536;
+          return String.fromCharCode((B.JSInt_methods._shrOtherPositive$1(bits, 10) | 55296) >>> 0, bits & 1023 | 56320);
+        }
+      }
+      throw A.wrapException(A.RangeError$range(charCode, 0, 1114111, null, null));
+    },
+    Primitives_lazyAsJsDate(receiver) {
+      if (receiver.date === void 0)
+        receiver.date = new Date(receiver._core$_value);
+      return receiver.date;
+    },
+    Primitives_getYear(receiver) {
+      var t1 = A.Primitives_lazyAsJsDate(receiver).getUTCFullYear() + 0;
+      return t1;
+    },
+    Primitives_getMonth(receiver) {
+      var t1 = A.Primitives_lazyAsJsDate(receiver).getUTCMonth() + 1;
+      return t1;
+    },
+    Primitives_getDay(receiver) {
+      var t1 = A.Primitives_lazyAsJsDate(receiver).getUTCDate() + 0;
+      return t1;
+    },
+    Primitives_getHours(receiver) {
+      var t1 = A.Primitives_lazyAsJsDate(receiver).getUTCHours() + 0;
+      return t1;
+    },
+    Primitives_getMinutes(receiver) {
+      var t1 = A.Primitives_lazyAsJsDate(receiver).getUTCMinutes() + 0;
+      return t1;
+    },
+    Primitives_getSeconds(receiver) {
+      var t1 = A.Primitives_lazyAsJsDate(receiver).getUTCSeconds() + 0;
+      return t1;
+    },
+    Primitives_getMilliseconds(receiver) {
+      var t1 = A.Primitives_lazyAsJsDate(receiver).getUTCMilliseconds() + 0;
+      return t1;
+    },
+    Primitives_functionNoSuchMethod($function, positionalArguments, namedArguments) {
+      var $arguments, namedArgumentList, t1 = {};
+      t1.argumentCount = 0;
+      $arguments = [];
+      namedArgumentList = [];
+      t1.argumentCount = positionalArguments.length;
+      B.JSArray_methods.addAll$1($arguments, positionalArguments);
+      t1.names = "";
+      if (namedArguments != null && namedArguments._length !== 0)
+        namedArguments.forEach$1(0, new A.Primitives_functionNoSuchMethod_closure(t1, namedArgumentList, $arguments));
+      return J.noSuchMethod$1$($function, new A.JSInvocationMirror(B.Symbol_call, 0, $arguments, namedArgumentList, 0));
+    },
+    Primitives_applyFunction($function, positionalArguments, namedArguments) {
+      var t1, argumentCount, jsStub;
+      if (Array.isArray(positionalArguments))
+        t1 = namedArguments == null || namedArguments._length === 0;
+      else
+        t1 = false;
+      if (t1) {
+        argumentCount = positionalArguments.length;
+        if (argumentCount === 0) {
+          if (!!$function.call$0)
+            return $function.call$0();
+        } else if (argumentCount === 1) {
+          if (!!$function.call$1)
+            return $function.call$1(positionalArguments[0]);
+        } else if (argumentCount === 2) {
+          if (!!$function.call$2)
+            return $function.call$2(positionalArguments[0], positionalArguments[1]);
+        } else if (argumentCount === 3) {
+          if (!!$function.call$3)
+            return $function.call$3(positionalArguments[0], positionalArguments[1], positionalArguments[2]);
+        } else if (argumentCount === 4) {
+          if (!!$function.call$4)
+            return $function.call$4(positionalArguments[0], positionalArguments[1], positionalArguments[2], positionalArguments[3]);
+        } else if (argumentCount === 5)
+          if (!!$function.call$5)
+            return $function.call$5(positionalArguments[0], positionalArguments[1], positionalArguments[2], positionalArguments[3], positionalArguments[4]);
+        jsStub = $function["call" + "$" + argumentCount];
+        if (jsStub != null)
+          return jsStub.apply($function, positionalArguments);
+      }
+      return A.Primitives__generalApplyFunction($function, positionalArguments, namedArguments);
+    },
+    Primitives__generalApplyFunction($function, positionalArguments, namedArguments) {
+      var defaultValuesClosure, t1, defaultValues, interceptor, jsFunction, maxArguments, missingDefaults, keys, _i, defaultValue, used, key,
+        $arguments = Array.isArray(positionalArguments) ? positionalArguments : A.List_List$of(positionalArguments, true, type$.dynamic),
+        argumentCount = $arguments.length,
+        requiredParameterCount = $function.$requiredArgCount;
+      if (argumentCount < requiredParameterCount)
+        return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments);
+      defaultValuesClosure = $function.$defaultValues;
+      t1 = defaultValuesClosure == null;
+      defaultValues = !t1 ? defaultValuesClosure() : null;
+      interceptor = J.getInterceptor$($function);
+      jsFunction = interceptor["call*"];
+      if (typeof jsFunction == "string")
+        jsFunction = interceptor[jsFunction];
+      if (t1) {
+        if (namedArguments != null && namedArguments._length !== 0)
+          return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments);
+        if (argumentCount === requiredParameterCount)
+          return jsFunction.apply($function, $arguments);
+        return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments);
+      }
+      if (Array.isArray(defaultValues)) {
+        if (namedArguments != null && namedArguments._length !== 0)
+          return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments);
+        maxArguments = requiredParameterCount + defaultValues.length;
+        if (argumentCount > maxArguments)
+          return A.Primitives_functionNoSuchMethod($function, $arguments, null);
+        if (argumentCount < maxArguments) {
+          missingDefaults = defaultValues.slice(argumentCount - requiredParameterCount);
+          if ($arguments === positionalArguments)
+            $arguments = A.List_List$of($arguments, true, type$.dynamic);
+          B.JSArray_methods.addAll$1($arguments, missingDefaults);
+        }
+        return jsFunction.apply($function, $arguments);
+      } else {
+        if (argumentCount > requiredParameterCount)
+          return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments);
+        if ($arguments === positionalArguments)
+          $arguments = A.List_List$of($arguments, true, type$.dynamic);
+        keys = Object.keys(defaultValues);
+        if (namedArguments == null)
+          for (t1 = keys.length, _i = 0; _i < keys.length; keys.length === t1 || (0, A.throwConcurrentModificationError)(keys), ++_i) {
+            defaultValue = defaultValues[A._asString(keys[_i])];
+            if (B.C__Required === defaultValue)
+              return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments);
+            B.JSArray_methods.add$1($arguments, defaultValue);
+          }
+        else {
+          for (t1 = keys.length, used = 0, _i = 0; _i < keys.length; keys.length === t1 || (0, A.throwConcurrentModificationError)(keys), ++_i) {
+            key = A._asString(keys[_i]);
+            if (namedArguments.containsKey$1(0, key)) {
+              ++used;
+              B.JSArray_methods.add$1($arguments, namedArguments.$index(0, key));
+            } else {
+              defaultValue = defaultValues[key];
+              if (B.C__Required === defaultValue)
+                return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments);
+              B.JSArray_methods.add$1($arguments, defaultValue);
+            }
+          }
+          if (used !== namedArguments._length)
+            return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments);
+        }
+        return jsFunction.apply($function, $arguments);
+      }
+    },
+    iae(argument) {
+      throw A.wrapException(A.argumentErrorValue(argument));
+    },
+    ioore(receiver, index) {
+      if (receiver == null)
+        J.get$length$asx(receiver);
+      throw A.wrapException(A.diagnoseIndexError(receiver, index));
+    },
+    diagnoseIndexError(indexable, index) {
+      var $length, _s5_ = "index";
+      if (!A._isInt(index))
+        return new A.ArgumentError(true, index, _s5_, null);
+      $length = A._asInt(J.get$length$asx(indexable));
+      if (index < 0 || index >= $length)
+        return A.IndexError$withLength(index, $length, indexable, _s5_);
+      return A.RangeError$value(index, _s5_);
+    },
+    diagnoseRangeError(start, end, $length) {
+      if (start > $length)
+        return A.RangeError$range(start, 0, $length, "start", null);
+      if (end != null)
+        if (end < start || end > $length)
+          return A.RangeError$range(end, start, $length, "end", null);
+      return new A.ArgumentError(true, end, "end", null);
+    },
+    argumentErrorValue(object) {
+      return new A.ArgumentError(true, object, null, null);
+    },
+    wrapException(ex) {
+      return A.initializeExceptionWrapper(new Error(), ex);
+    },
+    initializeExceptionWrapper(wrapper, ex) {
+      var t1;
+      if (ex == null)
+        ex = new A.TypeError();
+      wrapper.dartException = ex;
+      t1 = A.toStringWrapper;
+      if ("defineProperty" in Object) {
+        Object.defineProperty(wrapper, "message", {get: t1});
+        wrapper.name = "";
+      } else
+        wrapper.toString = t1;
+      return wrapper;
+    },
+    toStringWrapper() {
+      return J.toString$0$(this.dartException);
+    },
+    throwExpression(ex) {
+      throw A.wrapException(ex);
+    },
+    throwExpressionWithWrapper(ex, wrapper) {
+      throw A.initializeExceptionWrapper(wrapper, ex);
+    },
+    throwConcurrentModificationError(collection) {
+      throw A.wrapException(A.ConcurrentModificationError$(collection));
+    },
+    TypeErrorDecoder_extractPattern(message) {
+      var match, $arguments, argumentsExpr, expr, method, receiver;
+      message = A.quoteStringForRegExp(message.replace(String({}), "$receiver$"));
+      match = message.match(/\\\$[a-zA-Z]+\\\$/g);
+      if (match == null)
+        match = A._setArrayType([], type$.JSArray_String);
+      $arguments = match.indexOf("\\$arguments\\$");
+      argumentsExpr = match.indexOf("\\$argumentsExpr\\$");
+      expr = match.indexOf("\\$expr\\$");
+      method = match.indexOf("\\$method\\$");
+      receiver = match.indexOf("\\$receiver\\$");
+      return new A.TypeErrorDecoder(message.replace(new RegExp("\\\\\\$arguments\\\\\\$", "g"), "((?:x|[^x])*)").replace(new RegExp("\\\\\\$argumentsExpr\\\\\\$", "g"), "((?:x|[^x])*)").replace(new RegExp("\\\\\\$expr\\\\\\$", "g"), "((?:x|[^x])*)").replace(new RegExp("\\\\\\$method\\\\\\$", "g"), "((?:x|[^x])*)").replace(new RegExp("\\\\\\$receiver\\\\\\$", "g"), "((?:x|[^x])*)"), $arguments, argumentsExpr, expr, method, receiver);
+    },
+    TypeErrorDecoder_provokeCallErrorOn(expression) {
+      return function($expr$) {
+        var $argumentsExpr$ = "$arguments$";
+        try {
+          $expr$.$method$($argumentsExpr$);
+        } catch (e) {
+          return e.message;
+        }
+      }(expression);
+    },
+    TypeErrorDecoder_provokePropertyErrorOn(expression) {
+      return function($expr$) {
+        try {
+          $expr$.$method$;
+        } catch (e) {
+          return e.message;
+        }
+      }(expression);
+    },
+    JsNoSuchMethodError$(_message, match) {
+      var t1 = match == null,
+        t2 = t1 ? null : match.method;
+      return new A.JsNoSuchMethodError(_message, t2, t1 ? null : match.receiver);
+    },
+    unwrapException(ex) {
+      if (ex == null)
+        return new A.NullThrownFromJavaScriptException(ex);
+      if (typeof ex !== "object")
+        return ex;
+      if ("dartException" in ex)
+        return A.saveStackTrace(ex, ex.dartException);
+      return A._unwrapNonDartException(ex);
+    },
+    saveStackTrace(ex, error) {
+      if (type$.Error._is(error))
+        if (error.$thrownJsError == null)
+          error.$thrownJsError = ex;
+      return error;
+    },
+    _unwrapNonDartException(ex) {
+      var message, number, ieErrorCode, nsme, notClosure, nullCall, nullLiteralCall, undefCall, undefLiteralCall, nullProperty, undefProperty, undefLiteralProperty, match;
+      if (!("message" in ex))
+        return ex;
+      message = ex.message;
+      if ("number" in ex && typeof ex.number == "number") {
+        number = ex.number;
+        ieErrorCode = number & 65535;
+        if ((B.JSInt_methods._shrOtherPositive$1(number, 16) & 8191) === 10)
+          switch (ieErrorCode) {
+            case 438:
+              return A.saveStackTrace(ex, A.JsNoSuchMethodError$(A.S(message) + " (Error " + ieErrorCode + ")", null));
+            case 445:
+            case 5007:
+              A.S(message);
+              return A.saveStackTrace(ex, new A.NullError());
+          }
+      }
+      if (ex instanceof TypeError) {
+        nsme = $.$get$TypeErrorDecoder_noSuchMethodPattern();
+        notClosure = $.$get$TypeErrorDecoder_notClosurePattern();
+        nullCall = $.$get$TypeErrorDecoder_nullCallPattern();
+        nullLiteralCall = $.$get$TypeErrorDecoder_nullLiteralCallPattern();
+        undefCall = $.$get$TypeErrorDecoder_undefinedCallPattern();
+        undefLiteralCall = $.$get$TypeErrorDecoder_undefinedLiteralCallPattern();
+        nullProperty = $.$get$TypeErrorDecoder_nullPropertyPattern();
+        $.$get$TypeErrorDecoder_nullLiteralPropertyPattern();
+        undefProperty = $.$get$TypeErrorDecoder_undefinedPropertyPattern();
+        undefLiteralProperty = $.$get$TypeErrorDecoder_undefinedLiteralPropertyPattern();
+        match = nsme.matchTypeError$1(message);
+        if (match != null)
+          return A.saveStackTrace(ex, A.JsNoSuchMethodError$(A._asString(message), match));
+        else {
+          match = notClosure.matchTypeError$1(message);
+          if (match != null) {
+            match.method = "call";
+            return A.saveStackTrace(ex, A.JsNoSuchMethodError$(A._asString(message), match));
+          } else if (nullCall.matchTypeError$1(message) != null || nullLiteralCall.matchTypeError$1(message) != null || undefCall.matchTypeError$1(message) != null || undefLiteralCall.matchTypeError$1(message) != null || nullProperty.matchTypeError$1(message) != null || nullLiteralCall.matchTypeError$1(message) != null || undefProperty.matchTypeError$1(message) != null || undefLiteralProperty.matchTypeError$1(message) != null) {
+            A._asString(message);
+            return A.saveStackTrace(ex, new A.NullError());
+          }
+        }
+        return A.saveStackTrace(ex, new A.UnknownJsTypeError(typeof message == "string" ? message : ""));
+      }
+      if (ex instanceof RangeError) {
+        if (typeof message == "string" && message.indexOf("call stack") !== -1)
+          return new A.StackOverflowError();
+        message = function(ex) {
+          try {
+            return String(ex);
+          } catch (e) {
+          }
+          return null;
+        }(ex);
+        return A.saveStackTrace(ex, new A.ArgumentError(false, null, null, typeof message == "string" ? message.replace(/^RangeError:\s*/, "") : message));
+      }
+      if (typeof InternalError == "function" && ex instanceof InternalError)
+        if (typeof message == "string" && message === "too much recursion")
+          return new A.StackOverflowError();
+      return ex;
+    },
+    getTraceFromException(exception) {
+      var trace;
+      if (exception == null)
+        return new A._StackTrace(exception);
+      trace = exception.$cachedTrace;
+      if (trace != null)
+        return trace;
+      trace = new A._StackTrace(exception);
+      if (typeof exception === "object")
+        exception.$cachedTrace = trace;
+      return trace;
+    },
+    objectHashCode(object) {
+      if (object == null)
+        return J.get$hashCode$(object);
+      if (typeof object == "object")
+        return A.Primitives_objectHashCode(object);
+      return J.get$hashCode$(object);
+    },
+    fillLiteralMap(keyValuePairs, result) {
+      var index, index0, index1,
+        $length = keyValuePairs.length;
+      for (index = 0; index < $length; index = index1) {
+        index0 = index + 1;
+        index1 = index0 + 1;
+        result.$indexSet(0, keyValuePairs[index], keyValuePairs[index0]);
+      }
+      return result;
+    },
+    _invokeClosure(closure, numberOfArguments, arg1, arg2, arg3, arg4) {
+      type$.Function._as(closure);
+      switch (A._asInt(numberOfArguments)) {
+        case 0:
+          return closure.call$0();
+        case 1:
+          return closure.call$1(arg1);
+        case 2:
+          return closure.call$2(arg1, arg2);
+        case 3:
+          return closure.call$3(arg1, arg2, arg3);
+        case 4:
+          return closure.call$4(arg1, arg2, arg3, arg4);
+      }
+      throw A.wrapException(new A._Exception("Unsupported number of arguments for wrapped closure"));
+    },
+    convertDartClosureToJS(closure, arity) {
+      var $function = closure.$identity;
+      if (!!$function)
+        return $function;
+      $function = A.convertDartClosureToJSUncached(closure, arity);
+      closure.$identity = $function;
+      return $function;
+    },
+    convertDartClosureToJSUncached(closure, arity) {
+      var entry;
+      switch (arity) {
+        case 0:
+          entry = closure.call$0;
+          break;
+        case 1:
+          entry = closure.call$1;
+          break;
+        case 2:
+          entry = closure.call$2;
+          break;
+        case 3:
+          entry = closure.call$3;
+          break;
+        case 4:
+          entry = closure.call$4;
+          break;
+        default:
+          entry = null;
+      }
+      if (entry != null)
+        return entry.bind(closure);
+      return function(closure, arity, invoke) {
+        return function(a1, a2, a3, a4) {
+          return invoke(closure, arity, a1, a2, a3, a4);
+        };
+      }(closure, arity, A._invokeClosure);
+    },
+    Closure_fromTearOff(parameters) {
+      var $prototype, $constructor, t2, trampoline, applyTrampoline, i, stub, stub0, stubName, stubCallName,
+        container = parameters.co,
+        isStatic = parameters.iS,
+        isIntercepted = parameters.iI,
+        needsDirectAccess = parameters.nDA,
+        applyTrampolineIndex = parameters.aI,
+        funsOrNames = parameters.fs,
+        callNames = parameters.cs,
+        $name = funsOrNames[0],
+        callName = callNames[0],
+        $function = container[$name],
+        t1 = parameters.fT;
+      t1.toString;
+      $prototype = isStatic ? Object.create(new A.StaticClosure().constructor.prototype) : Object.create(new A.BoundClosure(null, null).constructor.prototype);
+      $prototype.$initialize = $prototype.constructor;
+      if (isStatic)
+        $constructor = function static_tear_off() {
+          this.$initialize();
+        };
+      else
+        $constructor = function tear_off(a, b) {
+          this.$initialize(a, b);
+        };
+      $prototype.constructor = $constructor;
+      $constructor.prototype = $prototype;
+      $prototype.$_name = $name;
+      $prototype.$_target = $function;
+      t2 = !isStatic;
+      if (t2)
+        trampoline = A.Closure_forwardCallTo($name, $function, isIntercepted, needsDirectAccess);
+      else {
+        $prototype.$static_name = $name;
+        trampoline = $function;
+      }
+      $prototype.$signature = A.Closure__computeSignatureFunctionNewRti(t1, isStatic, isIntercepted);
+      $prototype[callName] = trampoline;
+      for (applyTrampoline = trampoline, i = 1; i < funsOrNames.length; ++i) {
+        stub = funsOrNames[i];
+        if (typeof stub == "string") {
+          stub0 = container[stub];
+          stubName = stub;
+          stub = stub0;
+        } else
+          stubName = "";
+        stubCallName = callNames[i];
+        if (stubCallName != null) {
+          if (t2)
+            stub = A.Closure_forwardCallTo(stubName, stub, isIntercepted, needsDirectAccess);
+          $prototype[stubCallName] = stub;
+        }
+        if (i === applyTrampolineIndex)
+          applyTrampoline = stub;
+      }
+      $prototype["call*"] = applyTrampoline;
+      $prototype.$requiredArgCount = parameters.rC;
+      $prototype.$defaultValues = parameters.dV;
+      return $constructor;
+    },
+    Closure__computeSignatureFunctionNewRti(functionType, isStatic, isIntercepted) {
+      if (typeof functionType == "number")
+        return functionType;
+      if (typeof functionType == "string") {
+        if (isStatic)
+          throw A.wrapException("Cannot compute signature for static tearoff.");
+        return function(recipe, evalOnReceiver) {
+          return function() {
+            return evalOnReceiver(this, recipe);
+          };
+        }(functionType, A.BoundClosure_evalRecipe);
+      }
+      throw A.wrapException("Error in functionType of tearoff");
+    },
+    Closure_cspForwardCall(arity, needsDirectAccess, stubName, $function) {
+      var getReceiver = A.BoundClosure_receiverOf;
+      switch (needsDirectAccess ? -1 : arity) {
+        case 0:
+          return function(entry, receiverOf) {
+            return function() {
+              return receiverOf(this)[entry]();
+            };
+          }(stubName, getReceiver);
+        case 1:
+          return function(entry, receiverOf) {
+            return function(a) {
+              return receiverOf(this)[entry](a);
+            };
+          }(stubName, getReceiver);
+        case 2:
+          return function(entry, receiverOf) {
+            return function(a, b) {
+              return receiverOf(this)[entry](a, b);
+            };
+          }(stubName, getReceiver);
+        case 3:
+          return function(entry, receiverOf) {
+            return function(a, b, c) {
+              return receiverOf(this)[entry](a, b, c);
+            };
+          }(stubName, getReceiver);
+        case 4:
+          return function(entry, receiverOf) {
+            return function(a, b, c, d) {
+              return receiverOf(this)[entry](a, b, c, d);
+            };
+          }(stubName, getReceiver);
+        case 5:
+          return function(entry, receiverOf) {
+            return function(a, b, c, d, e) {
+              return receiverOf(this)[entry](a, b, c, d, e);
+            };
+          }(stubName, getReceiver);
+        default:
+          return function(f, receiverOf) {
+            return function() {
+              return f.apply(receiverOf(this), arguments);
+            };
+          }($function, getReceiver);
+      }
+    },
+    Closure_forwardCallTo(stubName, $function, isIntercepted, needsDirectAccess) {
+      var arity, t1;
+      if (isIntercepted)
+        return A.Closure_forwardInterceptedCallTo(stubName, $function, needsDirectAccess);
+      arity = $function.length;
+      t1 = A.Closure_cspForwardCall(arity, needsDirectAccess, stubName, $function);
+      return t1;
+    },
+    Closure_cspForwardInterceptedCall(arity, needsDirectAccess, stubName, $function) {
+      var getReceiver = A.BoundClosure_receiverOf,
+        getInterceptor = A.BoundClosure_interceptorOf;
+      switch (needsDirectAccess ? -1 : arity) {
+        case 0:
+          throw A.wrapException(new A.RuntimeError("Intercepted function with no arguments."));
+        case 1:
+          return function(entry, interceptorOf, receiverOf) {
+            return function() {
+              return interceptorOf(this)[entry](receiverOf(this));
+            };
+          }(stubName, getInterceptor, getReceiver);
+        case 2:
+          return function(entry, interceptorOf, receiverOf) {
+            return function(a) {
+              return interceptorOf(this)[entry](receiverOf(this), a);
+            };
+          }(stubName, getInterceptor, getReceiver);
+        case 3:
+          return function(entry, interceptorOf, receiverOf) {
+            return function(a, b) {
+              return interceptorOf(this)[entry](receiverOf(this), a, b);
+            };
+          }(stubName, getInterceptor, getReceiver);
+        case 4:
+          return function(entry, interceptorOf, receiverOf) {
+            return function(a, b, c) {
+              return interceptorOf(this)[entry](receiverOf(this), a, b, c);
+            };
+          }(stubName, getInterceptor, getReceiver);
+        case 5:
+          return function(entry, interceptorOf, receiverOf) {
+            return function(a, b, c, d) {
+              return interceptorOf(this)[entry](receiverOf(this), a, b, c, d);
+            };
+          }(stubName, getInterceptor, getReceiver);
+        case 6:
+          return function(entry, interceptorOf, receiverOf) {
+            return function(a, b, c, d, e) {
+              return interceptorOf(this)[entry](receiverOf(this), a, b, c, d, e);
+            };
+          }(stubName, getInterceptor, getReceiver);
+        default:
+          return function(f, interceptorOf, receiverOf) {
+            return function() {
+              var a = [receiverOf(this)];
+              Array.prototype.push.apply(a, arguments);
+              return f.apply(interceptorOf(this), a);
+            };
+          }($function, getInterceptor, getReceiver);
+      }
+    },
+    Closure_forwardInterceptedCallTo(stubName, $function, needsDirectAccess) {
+      var arity, t1;
+      if ($.BoundClosure__interceptorFieldNameCache == null)
+        $.BoundClosure__interceptorFieldNameCache = A.BoundClosure__computeFieldNamed("interceptor");
+      if ($.BoundClosure__receiverFieldNameCache == null)
+        $.BoundClosure__receiverFieldNameCache = A.BoundClosure__computeFieldNamed("receiver");
+      arity = $function.length;
+      t1 = A.Closure_cspForwardInterceptedCall(arity, needsDirectAccess, stubName, $function);
+      return t1;
+    },
+    closureFromTearOff(parameters) {
+      return A.Closure_fromTearOff(parameters);
+    },
+    BoundClosure_evalRecipe(closure, recipe) {
+      return A._Universe_evalInEnvironment(init.typeUniverse, A.instanceType(closure._receiver), recipe);
+    },
+    BoundClosure_receiverOf(closure) {
+      return closure._receiver;
+    },
+    BoundClosure_interceptorOf(closure) {
+      return closure._interceptor;
+    },
+    BoundClosure__computeFieldNamed(fieldName) {
+      var t1, i, $name,
+        template = new A.BoundClosure("receiver", "interceptor"),
+        names = J.JSArray_markFixedList(Object.getOwnPropertyNames(template), type$.nullable_Object);
+      for (t1 = names.length, i = 0; i < t1; ++i) {
+        $name = names[i];
+        if (template[$name] === fieldName)
+          return $name;
+      }
+      throw A.wrapException(A.ArgumentError$("Field name " + fieldName + " not found.", null));
+    },
+    boolConversionCheck(value) {
+      if (value == null)
+        A.assertThrow("boolean expression must not be null");
+      return value;
+    },
+    assertThrow(message) {
+      throw A.wrapException(new A._AssertionError(message));
+    },
+    throwCyclicInit(staticName) {
+      throw A.wrapException(new A._CyclicInitializationError(staticName));
+    },
+    getIsolateAffinityTag($name) {
+      return init.getIsolateTag($name);
+    },
+    defineProperty(obj, property, value) {
+      Object.defineProperty(obj, property, {value: value, enumerable: false, writable: true, configurable: true});
+    },
+    lookupAndCacheInterceptor(obj) {
+      var interceptor, interceptorClass, altTag, mark, t1,
+        tag = A._asString($.getTagFunction.call$1(obj)),
+        record = $.dispatchRecordsForInstanceTags[tag];
+      if (record != null) {
+        Object.defineProperty(obj, init.dispatchPropertyName, {value: record, enumerable: false, writable: true, configurable: true});
+        return record.i;
+      }
+      interceptor = $.interceptorsForUncacheableTags[tag];
+      if (interceptor != null)
+        return interceptor;
+      interceptorClass = init.interceptorsByTag[tag];
+      if (interceptorClass == null) {
+        altTag = A._asStringQ($.alternateTagFunction.call$2(obj, tag));
+        if (altTag != null) {
+          record = $.dispatchRecordsForInstanceTags[altTag];
+          if (record != null) {
+            Object.defineProperty(obj, init.dispatchPropertyName, {value: record, enumerable: false, writable: true, configurable: true});
+            return record.i;
+          }
+          interceptor = $.interceptorsForUncacheableTags[altTag];
+          if (interceptor != null)
+            return interceptor;
+          interceptorClass = init.interceptorsByTag[altTag];
+          tag = altTag;
+        }
+      }
+      if (interceptorClass == null)
+        return null;
+      interceptor = interceptorClass.prototype;
+      mark = tag[0];
+      if (mark === "!") {
+        record = A.makeLeafDispatchRecord(interceptor);
+        $.dispatchRecordsForInstanceTags[tag] = record;
+        Object.defineProperty(obj, init.dispatchPropertyName, {value: record, enumerable: false, writable: true, configurable: true});
+        return record.i;
+      }
+      if (mark === "~") {
+        $.interceptorsForUncacheableTags[tag] = interceptor;
+        return interceptor;
+      }
+      if (mark === "-") {
+        t1 = A.makeLeafDispatchRecord(interceptor);
+        Object.defineProperty(Object.getPrototypeOf(obj), init.dispatchPropertyName, {value: t1, enumerable: false, writable: true, configurable: true});
+        return t1.i;
+      }
+      if (mark === "+")
+        return A.patchInteriorProto(obj, interceptor);
+      if (mark === "*")
+        throw A.wrapException(A.UnimplementedError$(tag));
+      if (init.leafTags[tag] === true) {
+        t1 = A.makeLeafDispatchRecord(interceptor);
+        Object.defineProperty(Object.getPrototypeOf(obj), init.dispatchPropertyName, {value: t1, enumerable: false, writable: true, configurable: true});
+        return t1.i;
+      } else
+        return A.patchInteriorProto(obj, interceptor);
+    },
+    patchInteriorProto(obj, interceptor) {
+      var proto = Object.getPrototypeOf(obj);
+      Object.defineProperty(proto, init.dispatchPropertyName, {value: J.makeDispatchRecord(interceptor, proto, null, null), enumerable: false, writable: true, configurable: true});
+      return interceptor;
+    },
+    makeLeafDispatchRecord(interceptor) {
+      return J.makeDispatchRecord(interceptor, false, null, !!interceptor.$isJavaScriptIndexingBehavior);
+    },
+    makeDefaultDispatchRecord(tag, interceptorClass, proto) {
+      var interceptor = interceptorClass.prototype;
+      if (init.leafTags[tag] === true)
+        return A.makeLeafDispatchRecord(interceptor);
+      else
+        return J.makeDispatchRecord(interceptor, proto, null, null);
+    },
+    initNativeDispatch() {
+      if (true === $.initNativeDispatchFlag)
+        return;
+      $.initNativeDispatchFlag = true;
+      A.initNativeDispatchContinue();
+    },
+    initNativeDispatchContinue() {
+      var map, tags, fun, i, tag, proto, record, interceptorClass;
+      $.dispatchRecordsForInstanceTags = Object.create(null);
+      $.interceptorsForUncacheableTags = Object.create(null);
+      A.initHooks();
+      map = init.interceptorsByTag;
+      tags = Object.getOwnPropertyNames(map);
+      if (typeof window != "undefined") {
+        window;
+        fun = function() {
+        };
+        for (i = 0; i < tags.length; ++i) {
+          tag = tags[i];
+          proto = $.prototypeForTagFunction.call$1(tag);
+          if (proto != null) {
+            record = A.makeDefaultDispatchRecord(tag, map[tag], proto);
+            if (record != null) {
+              Object.defineProperty(proto, init.dispatchPropertyName, {value: record, enumerable: false, writable: true, configurable: true});
+              fun.prototype = proto;
+            }
+          }
+        }
+      }
+      for (i = 0; i < tags.length; ++i) {
+        tag = tags[i];
+        if (/^[A-Za-z_]/.test(tag)) {
+          interceptorClass = map[tag];
+          map["!" + tag] = interceptorClass;
+          map["~" + tag] = interceptorClass;
+          map["-" + tag] = interceptorClass;
+          map["+" + tag] = interceptorClass;
+          map["*" + tag] = interceptorClass;
+        }
+      }
+    },
+    initHooks() {
+      var transformers, i, transformer, getTag, getUnknownTag, prototypeForTag,
+        hooks = B.C_JS_CONST0();
+      hooks = A.applyHooksTransformer(B.C_JS_CONST1, A.applyHooksTransformer(B.C_JS_CONST2, A.applyHooksTransformer(B.C_JS_CONST3, A.applyHooksTransformer(B.C_JS_CONST3, A.applyHooksTransformer(B.C_JS_CONST4, A.applyHooksTransformer(B.C_JS_CONST5, A.applyHooksTransformer(B.C_JS_CONST6(B.C_JS_CONST), hooks)))))));
+      if (typeof dartNativeDispatchHooksTransformer != "undefined") {
+        transformers = dartNativeDispatchHooksTransformer;
+        if (typeof transformers == "function")
+          transformers = [transformers];
+        if (Array.isArray(transformers))
+          for (i = 0; i < transformers.length; ++i) {
+            transformer = transformers[i];
+            if (typeof transformer == "function")
+              hooks = transformer(hooks) || hooks;
+          }
+      }
+      getTag = hooks.getTag;
+      getUnknownTag = hooks.getUnknownTag;
+      prototypeForTag = hooks.prototypeForTag;
+      $.getTagFunction = new A.initHooks_closure(getTag);
+      $.alternateTagFunction = new A.initHooks_closure0(getUnknownTag);
+      $.prototypeForTagFunction = new A.initHooks_closure1(prototypeForTag);
+    },
+    applyHooksTransformer(transformer, hooks) {
+      return transformer(hooks) || hooks;
+    },
+    createRecordTypePredicate(shape, fieldRtis) {
+      var $length = fieldRtis.length,
+        $function = init.rttc["" + $length + ";" + shape];
+      if ($function == null)
+        return null;
+      if ($length === 0)
+        return $function;
+      if ($length === $function.length)
+        return $function.apply(null, fieldRtis);
+      return $function(fieldRtis);
+    },
+    JSSyntaxRegExp_makeNative(source, multiLine, caseSensitive, unicode, dotAll, global) {
+      var m = multiLine ? "m" : "",
+        i = caseSensitive ? "" : "i",
+        u = unicode ? "u" : "",
+        s = dotAll ? "s" : "",
+        g = global ? "g" : "",
+        regexp = function(source, modifiers) {
+          try {
+            return new RegExp(source, modifiers);
+          } catch (e) {
+            return e;
+          }
+        }(source, m + i + u + s + g);
+      if (regexp instanceof RegExp)
+        return regexp;
+      throw A.wrapException(A.FormatException$("Illegal RegExp pattern (" + String(regexp) + ")", source, null));
+    },
+    stringContainsUnchecked(receiver, other, startIndex) {
+      var t1;
+      if (typeof other == "string")
+        return receiver.indexOf(other, startIndex) >= 0;
+      else if (other instanceof A.JSSyntaxRegExp) {
+        t1 = B.JSString_methods.substring$1(receiver, startIndex);
+        return other._nativeRegExp.test(t1);
+      } else {
+        t1 = J.allMatches$1$s(other, B.JSString_methods.substring$1(receiver, startIndex));
+        return !t1.get$isEmpty(t1);
+      }
+    },
+    escapeReplacement(replacement) {
+      if (replacement.indexOf("$", 0) >= 0)
+        return replacement.replace(/\$/g, "$$$$");
+      return replacement;
+    },
+    stringReplaceFirstRE(receiver, regexp, replacement, startIndex) {
+      var match = regexp._execGlobal$2(receiver, startIndex);
+      if (match == null)
+        return receiver;
+      return A.stringReplaceRangeUnchecked(receiver, match._match.index, match.get$end(match), replacement);
+    },
+    quoteStringForRegExp(string) {
+      if (/[[\]{}()*+?.\\^$|]/.test(string))
+        return string.replace(/[[\]{}()*+?.\\^$|]/g, "\\$&");
+      return string;
+    },
+    stringReplaceAllUnchecked(receiver, pattern, replacement) {
+      var nativeRegexp;
+      if (typeof pattern == "string")
+        return A.stringReplaceAllUncheckedString(receiver, pattern, replacement);
+      if (pattern instanceof A.JSSyntaxRegExp) {
+        nativeRegexp = pattern.get$_nativeGlobalVersion();
+        nativeRegexp.lastIndex = 0;
+        return receiver.replace(nativeRegexp, A.escapeReplacement(replacement));
+      }
+      return A.stringReplaceAllGeneral(receiver, pattern, replacement);
+    },
+    stringReplaceAllGeneral(receiver, pattern, replacement) {
+      var t1, startIndex, t2, match;
+      for (t1 = J.allMatches$1$s(pattern, receiver), t1 = t1.get$iterator(t1), startIndex = 0, t2 = ""; t1.moveNext$0();) {
+        match = t1.get$current(t1);
+        t2 = t2 + receiver.substring(startIndex, match.get$start(match)) + replacement;
+        startIndex = match.get$end(match);
+      }
+      t1 = t2 + receiver.substring(startIndex);
+      return t1.charCodeAt(0) == 0 ? t1 : t1;
+    },
+    stringReplaceAllUncheckedString(receiver, pattern, replacement) {
+      var $length, t1, i;
+      if (pattern === "") {
+        if (receiver === "")
+          return replacement;
+        $length = receiver.length;
+        t1 = "" + replacement;
+        for (i = 0; i < $length; ++i)
+          t1 = t1 + receiver[i] + replacement;
+        return t1.charCodeAt(0) == 0 ? t1 : t1;
+      }
+      if (receiver.indexOf(pattern, 0) < 0)
+        return receiver;
+      if (receiver.length < 500 || replacement.indexOf("$", 0) >= 0)
+        return receiver.split(pattern).join(replacement);
+      return receiver.replace(new RegExp(A.quoteStringForRegExp(pattern), "g"), A.escapeReplacement(replacement));
+    },
+    stringReplaceFirstUnchecked(receiver, pattern, replacement, startIndex) {
+      var index, t1, matches, match;
+      if (typeof pattern == "string") {
+        index = receiver.indexOf(pattern, startIndex);
+        if (index < 0)
+          return receiver;
+        return A.stringReplaceRangeUnchecked(receiver, index, index + pattern.length, replacement);
+      }
+      if (pattern instanceof A.JSSyntaxRegExp)
+        return startIndex === 0 ? receiver.replace(pattern._nativeRegExp, A.escapeReplacement(replacement)) : A.stringReplaceFirstRE(receiver, pattern, replacement, startIndex);
+      t1 = J.allMatches$2$s(pattern, receiver, startIndex);
+      matches = t1.get$iterator(t1);
+      if (!matches.moveNext$0())
+        return receiver;
+      match = matches.get$current(matches);
+      return B.JSString_methods.replaceRange$3(receiver, match.get$start(match), match.get$end(match), replacement);
+    },
+    stringReplaceRangeUnchecked(receiver, start, end, replacement) {
+      return receiver.substring(0, start) + replacement + receiver.substring(end);
+    },
+    ConstantMapView: function ConstantMapView(t0, t1) {
+      this._collection$_map = t0;
+      this.$ti = t1;
+    },
+    ConstantMap: function ConstantMap() {
+    },
+    ConstantStringMap: function ConstantStringMap(t0, t1, t2) {
+      this._jsIndex = t0;
+      this._values = t1;
+      this.$ti = t2;
+    },
+    _KeysOrValues: function _KeysOrValues(t0, t1) {
+      this._elements = t0;
+      this.$ti = t1;
+    },
+    _KeysOrValuesOrElementsIterator: function _KeysOrValuesOrElementsIterator(t0, t1, t2) {
+      var _ = this;
+      _._elements = t0;
+      _._length = t1;
+      _.__js_helper$_index = 0;
+      _.__js_helper$_current = null;
+      _.$ti = t2;
+    },
+    Instantiation: function Instantiation() {
+    },
+    Instantiation1: function Instantiation1(t0, t1) {
+      this._genericClosure = t0;
+      this.$ti = t1;
+    },
+    JSInvocationMirror: function JSInvocationMirror(t0, t1, t2, t3, t4) {
+      var _ = this;
+      _._memberName = t0;
+      _.__js_helper$_kind = t1;
+      _._arguments = t2;
+      _._namedArgumentNames = t3;
+      _._typeArgumentCount = t4;
+    },
+    Primitives_functionNoSuchMethod_closure: function Primitives_functionNoSuchMethod_closure(t0, t1, t2) {
+      this._box_0 = t0;
+      this.namedArgumentList = t1;
+      this.$arguments = t2;
+    },
+    TypeErrorDecoder: function TypeErrorDecoder(t0, t1, t2, t3, t4, t5) {
+      var _ = this;
+      _._pattern = t0;
+      _._arguments = t1;
+      _._argumentsExpr = t2;
+      _._expr = t3;
+      _._method = t4;
+      _._receiver = t5;
+    },
+    NullError: function NullError() {
+    },
+    JsNoSuchMethodError: function JsNoSuchMethodError(t0, t1, t2) {
+      this.__js_helper$_message = t0;
+      this._method = t1;
+      this._receiver = t2;
+    },
+    UnknownJsTypeError: function UnknownJsTypeError(t0) {
+      this.__js_helper$_message = t0;
+    },
+    NullThrownFromJavaScriptException: function NullThrownFromJavaScriptException(t0) {
+      this._irritant = t0;
+    },
+    _StackTrace: function _StackTrace(t0) {
+      this._exception = t0;
+      this._trace = null;
+    },
+    Closure: function Closure() {
+    },
+    Closure0Args: function Closure0Args() {
+    },
+    Closure2Args: function Closure2Args() {
+    },
+    TearOffClosure: function TearOffClosure() {
+    },
+    StaticClosure: function StaticClosure() {
+    },
+    BoundClosure: function BoundClosure(t0, t1) {
+      this._receiver = t0;
+      this._interceptor = t1;
+    },
+    _CyclicInitializationError: function _CyclicInitializationError(t0) {
+      this.variableName = t0;
+    },
+    RuntimeError: function RuntimeError(t0) {
+      this.message = t0;
+    },
+    _AssertionError: function _AssertionError(t0) {
+      this.message = t0;
+    },
+    _Required: function _Required() {
+    },
+    JsLinkedHashMap: function JsLinkedHashMap(t0) {
+      var _ = this;
+      _._length = 0;
+      _._last = _._first = _.__js_helper$_rest = _._nums = _._strings = null;
+      _._modifications = 0;
+      _.$ti = t0;
+    },
+    JsLinkedHashMap_values_closure: function JsLinkedHashMap_values_closure(t0) {
+      this.$this = t0;
+    },
+    LinkedHashMapCell: function LinkedHashMapCell(t0, t1) {
+      var _ = this;
+      _.hashMapCellKey = t0;
+      _.hashMapCellValue = t1;
+      _._previous = _._next = null;
+    },
+    LinkedHashMapKeyIterable: function LinkedHashMapKeyIterable(t0, t1) {
+      this._map = t0;
+      this.$ti = t1;
+    },
+    LinkedHashMapKeyIterator: function LinkedHashMapKeyIterator(t0, t1, t2) {
+      var _ = this;
+      _._map = t0;
+      _._modifications = t1;
+      _.__js_helper$_current = _._cell = null;
+      _.$ti = t2;
+    },
+    initHooks_closure: function initHooks_closure(t0) {
+      this.getTag = t0;
+    },
+    initHooks_closure0: function initHooks_closure0(t0) {
+      this.getUnknownTag = t0;
+    },
+    initHooks_closure1: function initHooks_closure1(t0) {
+      this.prototypeForTag = t0;
+    },
+    JSSyntaxRegExp: function JSSyntaxRegExp(t0, t1) {
+      var _ = this;
+      _.pattern = t0;
+      _._nativeRegExp = t1;
+      _._nativeAnchoredRegExp = _._nativeGlobalRegExp = null;
+    },
+    _MatchImplementation: function _MatchImplementation(t0) {
+      this._match = t0;
+    },
+    _AllMatchesIterable: function _AllMatchesIterable(t0, t1, t2) {
+      this._re = t0;
+      this.__js_helper$_string = t1;
+      this.__js_helper$_start = t2;
+    },
+    _AllMatchesIterator: function _AllMatchesIterator(t0, t1, t2) {
+      var _ = this;
+      _._regExp = t0;
+      _.__js_helper$_string = t1;
+      _._nextIndex = t2;
+      _.__js_helper$_current = null;
+    },
+    StringMatch: function StringMatch(t0, t1) {
+      this.start = t0;
+      this.pattern = t1;
+    },
+    _StringAllMatchesIterable: function _StringAllMatchesIterable(t0, t1, t2) {
+      this._input = t0;
+      this._pattern = t1;
+      this.__js_helper$_index = t2;
+    },
+    _StringAllMatchesIterator: function _StringAllMatchesIterator(t0, t1, t2) {
+      var _ = this;
+      _._input = t0;
+      _._pattern = t1;
+      _.__js_helper$_index = t2;
+      _.__js_helper$_current = null;
+    },
+    throwLateFieldNI(fieldName) {
+      A.throwExpressionWithWrapper(new A.LateError("Field '" + fieldName + "' has not been initialized."), new Error());
+    },
+    throwLateFieldAI(fieldName) {
+      A.throwExpressionWithWrapper(new A.LateError("Field '" + fieldName + "' has already been initialized."), new Error());
+    },
+    throwLateFieldADI(fieldName) {
+      A.throwExpressionWithWrapper(new A.LateError("Field '" + fieldName + string$.x27_has_), new Error());
+    },
+    _Cell$named(_name) {
+      var t1 = new A._Cell(_name);
+      return t1._value = t1;
+    },
+    _InitializedCell$named(_name, _initializer) {
+      var t1 = new A._InitializedCell(_name, _initializer);
+      return t1._value = t1;
+    },
+    _Cell: function _Cell(t0) {
+      this.__late_helper$_name = t0;
+      this._value = null;
+    },
+    _InitializedCell: function _InitializedCell(t0, t1) {
+      this.__late_helper$_name = t0;
+      this._value = null;
+      this._initializer = t1;
+    },
+    _ensureNativeList(list) {
+      return list;
+    },
+    NativeInt8List__create1(arg) {
+      return new Int8Array(arg);
+    },
+    _checkValidIndex(index, list, $length) {
+      if (index >>> 0 !== index || index >= $length)
+        throw A.wrapException(A.diagnoseIndexError(list, index));
+    },
+    _checkValidRange(start, end, $length) {
+      var t1;
+      if (!(start >>> 0 !== start))
+        t1 = end >>> 0 !== end || start > end || end > $length;
+      else
+        t1 = true;
+      if (t1)
+        throw A.wrapException(A.diagnoseRangeError(start, end, $length));
+      return end;
+    },
+    NativeByteBuffer: function NativeByteBuffer() {
+    },
+    NativeTypedData: function NativeTypedData() {
+    },
+    NativeByteData: function NativeByteData() {
+    },
+    NativeTypedArray: function NativeTypedArray() {
+    },
+    NativeTypedArrayOfDouble: function NativeTypedArrayOfDouble() {
+    },
+    NativeTypedArrayOfInt: function NativeTypedArrayOfInt() {
+    },
+    NativeFloat32List: function NativeFloat32List() {
+    },
+    NativeFloat64List: function NativeFloat64List() {
+    },
+    NativeInt16List: function NativeInt16List() {
+    },
+    NativeInt32List: function NativeInt32List() {
+    },
+    NativeInt8List: function NativeInt8List() {
+    },
+    NativeUint16List: function NativeUint16List() {
+    },
+    NativeUint32List: function NativeUint32List() {
+    },
+    NativeUint8ClampedList: function NativeUint8ClampedList() {
+    },
+    NativeUint8List: function NativeUint8List() {
+    },
+    _NativeTypedArrayOfDouble_NativeTypedArray_ListMixin: function _NativeTypedArrayOfDouble_NativeTypedArray_ListMixin() {
+    },
+    _NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin: function _NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin() {
+    },
+    _NativeTypedArrayOfInt_NativeTypedArray_ListMixin: function _NativeTypedArrayOfInt_NativeTypedArray_ListMixin() {
+    },
+    _NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin: function _NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin() {
+    },
+    Rti__getQuestionFromStar(universe, rti) {
+      var question = rti._precomputed1;
+      return question == null ? rti._precomputed1 = A._Universe__lookupQuestionRti(universe, rti._primary, true) : question;
+    },
+    Rti__getFutureFromFutureOr(universe, rti) {
+      var future = rti._precomputed1;
+      return future == null ? rti._precomputed1 = A._Universe__lookupInterfaceRti(universe, "Future", [rti._primary]) : future;
+    },
+    Rti__getIsSubtypeCache(rti) {
+      var probe = rti._isSubtypeCache;
+      if (probe != null)
+        return probe;
+      return rti._isSubtypeCache = new Map();
+    },
+    Rti__isUnionOfFunctionType(rti) {
+      var kind = rti._kind;
+      if (kind === 6 || kind === 7 || kind === 8)
+        return A.Rti__isUnionOfFunctionType(rti._primary);
+      return kind === 12 || kind === 13;
+    },
+    Rti__getCanonicalRecipe(rti) {
+      return rti._canonicalRecipe;
+    },
+    findType(recipe) {
+      return A._Universe_eval(init.typeUniverse, recipe, false);
+    },
+    instantiatedGenericFunctionType(genericFunctionRti, instantiationRti) {
+      var t1, cache, key, probe, rti;
+      if (genericFunctionRti == null)
+        return null;
+      t1 = instantiationRti._rest;
+      cache = genericFunctionRti._bindCache;
+      if (cache == null)
+        cache = genericFunctionRti._bindCache = new Map();
+      key = instantiationRti._canonicalRecipe;
+      probe = cache.get(key);
+      if (probe != null)
+        return probe;
+      rti = A._substitute(init.typeUniverse, genericFunctionRti._primary, t1, 0);
+      cache.set(key, rti);
+      return rti;
+    },
+    _substitute(universe, rti, typeArguments, depth) {
+      var baseType, substitutedBaseType, interfaceTypeArguments, substitutedInterfaceTypeArguments, base, substitutedBase, $arguments, substitutedArguments, returnType, substitutedReturnType, functionParameters, substitutedFunctionParameters, bounds, substitutedBounds, index, argument,
+        kind = rti._kind;
+      switch (kind) {
+        case 5:
+        case 1:
+        case 2:
+        case 3:
+        case 4:
+          return rti;
+        case 6:
+          baseType = rti._primary;
+          substitutedBaseType = A._substitute(universe, baseType, typeArguments, depth);
+          if (substitutedBaseType === baseType)
+            return rti;
+          return A._Universe__lookupStarRti(universe, substitutedBaseType, true);
+        case 7:
+          baseType = rti._primary;
+          substitutedBaseType = A._substitute(universe, baseType, typeArguments, depth);
+          if (substitutedBaseType === baseType)
+            return rti;
+          return A._Universe__lookupQuestionRti(universe, substitutedBaseType, true);
+        case 8:
+          baseType = rti._primary;
+          substitutedBaseType = A._substitute(universe, baseType, typeArguments, depth);
+          if (substitutedBaseType === baseType)
+            return rti;
+          return A._Universe__lookupFutureOrRti(universe, substitutedBaseType, true);
+        case 9:
+          interfaceTypeArguments = rti._rest;
+          substitutedInterfaceTypeArguments = A._substituteArray(universe, interfaceTypeArguments, typeArguments, depth);
+          if (substitutedInterfaceTypeArguments === interfaceTypeArguments)
+            return rti;
+          return A._Universe__lookupInterfaceRti(universe, rti._primary, substitutedInterfaceTypeArguments);
+        case 10:
+          base = rti._primary;
+          substitutedBase = A._substitute(universe, base, typeArguments, depth);
+          $arguments = rti._rest;
+          substitutedArguments = A._substituteArray(universe, $arguments, typeArguments, depth);
+          if (substitutedBase === base && substitutedArguments === $arguments)
+            return rti;
+          return A._Universe__lookupBindingRti(universe, substitutedBase, substitutedArguments);
+        case 12:
+          returnType = rti._primary;
+          substitutedReturnType = A._substitute(universe, returnType, typeArguments, depth);
+          functionParameters = rti._rest;
+          substitutedFunctionParameters = A._substituteFunctionParameters(universe, functionParameters, typeArguments, depth);
+          if (substitutedReturnType === returnType && substitutedFunctionParameters === functionParameters)
+            return rti;
+          return A._Universe__lookupFunctionRti(universe, substitutedReturnType, substitutedFunctionParameters);
+        case 13:
+          bounds = rti._rest;
+          depth += bounds.length;
+          substitutedBounds = A._substituteArray(universe, bounds, typeArguments, depth);
+          base = rti._primary;
+          substitutedBase = A._substitute(universe, base, typeArguments, depth);
+          if (substitutedBounds === bounds && substitutedBase === base)
+            return rti;
+          return A._Universe__lookupGenericFunctionRti(universe, substitutedBase, substitutedBounds, true);
+        case 14:
+          index = rti._primary;
+          if (index < depth)
+            return rti;
+          argument = typeArguments[index - depth];
+          if (argument == null)
+            return rti;
+          return argument;
+        default:
+          throw A.wrapException(A.AssertionError$("Attempted to substitute unexpected RTI kind " + kind));
+      }
+    },
+    _substituteArray(universe, rtiArray, typeArguments, depth) {
+      var changed, i, rti, substitutedRti,
+        $length = rtiArray.length,
+        result = A._Utils_newArrayOrEmpty($length);
+      for (changed = false, i = 0; i < $length; ++i) {
+        rti = rtiArray[i];
+        substitutedRti = A._substitute(universe, rti, typeArguments, depth);
+        if (substitutedRti !== rti)
+          changed = true;
+        result[i] = substitutedRti;
+      }
+      return changed ? result : rtiArray;
+    },
+    _substituteNamed(universe, namedArray, typeArguments, depth) {
+      var changed, i, t1, t2, rti, substitutedRti,
+        $length = namedArray.length,
+        result = A._Utils_newArrayOrEmpty($length);
+      for (changed = false, i = 0; i < $length; i += 3) {
+        t1 = namedArray[i];
+        t2 = namedArray[i + 1];
+        rti = namedArray[i + 2];
+        substitutedRti = A._substitute(universe, rti, typeArguments, depth);
+        if (substitutedRti !== rti)
+          changed = true;
+        result.splice(i, 3, t1, t2, substitutedRti);
+      }
+      return changed ? result : namedArray;
+    },
+    _substituteFunctionParameters(universe, functionParameters, typeArguments, depth) {
+      var result,
+        requiredPositional = functionParameters._requiredPositional,
+        substitutedRequiredPositional = A._substituteArray(universe, requiredPositional, typeArguments, depth),
+        optionalPositional = functionParameters._optionalPositional,
+        substitutedOptionalPositional = A._substituteArray(universe, optionalPositional, typeArguments, depth),
+        named = functionParameters._named,
+        substitutedNamed = A._substituteNamed(universe, named, typeArguments, depth);
+      if (substitutedRequiredPositional === requiredPositional && substitutedOptionalPositional === optionalPositional && substitutedNamed === named)
+        return functionParameters;
+      result = new A._FunctionParameters();
+      result._requiredPositional = substitutedRequiredPositional;
+      result._optionalPositional = substitutedOptionalPositional;
+      result._named = substitutedNamed;
+      return result;
+    },
+    _setArrayType(target, rti) {
+      target[init.arrayRti] = rti;
+      return target;
+    },
+    closureFunctionType(closure) {
+      var t1,
+        signature = closure.$signature;
+      if (signature != null) {
+        if (typeof signature == "number")
+          return A.getTypeFromTypesTable(signature);
+        t1 = closure.$signature();
+        return t1;
+      }
+      return null;
+    },
+    instanceOrFunctionType(object, testRti) {
+      var rti;
+      if (A.Rti__isUnionOfFunctionType(testRti))
+        if (object instanceof A.Closure) {
+          rti = A.closureFunctionType(object);
+          if (rti != null)
+            return rti;
+        }
+      return A.instanceType(object);
+    },
+    instanceType(object) {
+      if (object instanceof A.Object)
+        return A._instanceType(object);
+      if (Array.isArray(object))
+        return A._arrayInstanceType(object);
+      return A._instanceTypeFromConstructor(J.getInterceptor$(object));
+    },
+    _arrayInstanceType(object) {
+      var rti = object[init.arrayRti],
+        defaultRti = type$.JSArray_dynamic;
+      if (rti == null)
+        return defaultRti;
+      if (rti.constructor !== defaultRti.constructor)
+        return defaultRti;
+      return rti;
+    },
+    _instanceType(object) {
+      var rti = object.$ti;
+      return rti != null ? rti : A._instanceTypeFromConstructor(object);
+    },
+    _instanceTypeFromConstructor(instance) {
+      var $constructor = instance.constructor,
+        probe = $constructor.$ccache;
+      if (probe != null)
+        return probe;
+      return A._instanceTypeFromConstructorMiss(instance, $constructor);
+    },
+    _instanceTypeFromConstructorMiss(instance, $constructor) {
+      var effectiveConstructor = instance instanceof A.Closure ? Object.getPrototypeOf(Object.getPrototypeOf(instance)).constructor : $constructor,
+        rti = A._Universe_findErasedType(init.typeUniverse, effectiveConstructor.name);
+      $constructor.$ccache = rti;
+      return rti;
+    },
+    getTypeFromTypesTable(index) {
+      var rti,
+        table = init.types,
+        type = table[index];
+      if (typeof type == "string") {
+        rti = A._Universe_eval(init.typeUniverse, type, false);
+        table[index] = rti;
+        return rti;
+      }
+      return type;
+    },
+    getRuntimeTypeOfDartObject(object) {
+      return A.createRuntimeType(A._instanceType(object));
+    },
+    getRuntimeTypeOfClosure(closure) {
+      var rti = A.closureFunctionType(closure);
+      return A.createRuntimeType(rti == null ? A.instanceType(closure) : rti);
+    },
+    _structuralTypeOf(object) {
+      var functionRti = object instanceof A.Closure ? A.closureFunctionType(object) : null;
+      if (functionRti != null)
+        return functionRti;
+      if (type$.TrustedGetRuntimeType._is(object))
+        return J.get$runtimeType$(object)._rti;
+      if (Array.isArray(object))
+        return A._arrayInstanceType(object);
+      return A.instanceType(object);
+    },
+    createRuntimeType(rti) {
+      var t1 = rti._cachedRuntimeType;
+      return t1 == null ? rti._cachedRuntimeType = A._createRuntimeType(rti) : t1;
+    },
+    _createRuntimeType(rti) {
+      var starErasedRti, t1,
+        s = rti._canonicalRecipe,
+        starErasedRecipe = s.replace(/\*/g, "");
+      if (starErasedRecipe === s)
+        return rti._cachedRuntimeType = new A._Type(rti);
+      starErasedRti = A._Universe_eval(init.typeUniverse, starErasedRecipe, true);
+      t1 = starErasedRti._cachedRuntimeType;
+      return t1 == null ? starErasedRti._cachedRuntimeType = A._createRuntimeType(starErasedRti) : t1;
+    },
+    typeLiteral(recipe) {
+      return A.createRuntimeType(A._Universe_eval(init.typeUniverse, recipe, false));
+    },
+    _installSpecializedIsTest(object) {
+      var t1, unstarred, unstarredKind, isFn, $name, predicate, testRti = this;
+      if (testRti === type$.Object)
+        return A._finishIsFn(testRti, object, A._isObject);
+      if (!A.isStrongTopType(testRti))
+        if (!(testRti === type$.legacy_Object))
+          t1 = false;
+        else
+          t1 = true;
+      else
+        t1 = true;
+      if (t1)
+        return A._finishIsFn(testRti, object, A._isTop);
+      t1 = testRti._kind;
+      if (t1 === 7)
+        return A._finishIsFn(testRti, object, A._generalNullableIsTestImplementation);
+      if (t1 === 1)
+        return A._finishIsFn(testRti, object, A._isNever);
+      unstarred = t1 === 6 ? testRti._primary : testRti;
+      unstarredKind = unstarred._kind;
+      if (unstarredKind === 8)
+        return A._finishIsFn(testRti, object, A._isFutureOr);
+      if (unstarred === type$.int)
+        isFn = A._isInt;
+      else if (unstarred === type$.double || unstarred === type$.num)
+        isFn = A._isNum;
+      else if (unstarred === type$.String)
+        isFn = A._isString;
+      else
+        isFn = unstarred === type$.bool ? A._isBool : null;
+      if (isFn != null)
+        return A._finishIsFn(testRti, object, isFn);
+      if (unstarredKind === 9) {
+        $name = unstarred._primary;
+        if (unstarred._rest.every(A.isTopType)) {
+          testRti._specializedTestResource = "$is" + $name;
+          if ($name === "List")
+            return A._finishIsFn(testRti, object, A._isListTestViaProperty);
+          return A._finishIsFn(testRti, object, A._isTestViaProperty);
+        }
+      } else if (unstarredKind === 11) {
+        predicate = A.createRecordTypePredicate(unstarred._primary, unstarred._rest);
+        return A._finishIsFn(testRti, object, predicate == null ? A._isNever : predicate);
+      }
+      return A._finishIsFn(testRti, object, A._generalIsTestImplementation);
+    },
+    _finishIsFn(testRti, object, isFn) {
+      testRti._is = isFn;
+      return testRti._is(object);
+    },
+    _installSpecializedAsCheck(object) {
+      var t1, testRti = this,
+        asFn = A._generalAsCheckImplementation;
+      if (!A.isStrongTopType(testRti))
+        if (!(testRti === type$.legacy_Object))
+          t1 = false;
+        else
+          t1 = true;
+      else
+        t1 = true;
+      if (t1)
+        asFn = A._asTop;
+      else if (testRti === type$.Object)
+        asFn = A._asObject;
+      else {
+        t1 = A.isNullable(testRti);
+        if (t1)
+          asFn = A._generalNullableAsCheckImplementation;
+      }
+      testRti._as = asFn;
+      return testRti._as(object);
+    },
+    _nullIs(testRti) {
+      var t1,
+        kind = testRti._kind;
+      if (!A.isStrongTopType(testRti))
+        if (!(testRti === type$.legacy_Object))
+          if (!(testRti === type$.legacy_Never))
+            if (kind !== 7)
+              if (!(kind === 6 && A._nullIs(testRti._primary)))
+                t1 = kind === 8 && A._nullIs(testRti._primary) || testRti === type$.Null || testRti === type$.JSNull;
+              else
+                t1 = true;
+            else
+              t1 = true;
+          else
+            t1 = true;
+        else
+          t1 = true;
+      else
+        t1 = true;
+      return t1;
+    },
+    _generalIsTestImplementation(object) {
+      var testRti = this;
+      if (object == null)
+        return A._nullIs(testRti);
+      return A.isSubtype(init.typeUniverse, A.instanceOrFunctionType(object, testRti), testRti);
+    },
+    _generalNullableIsTestImplementation(object) {
+      if (object == null)
+        return true;
+      return this._primary._is(object);
+    },
+    _isTestViaProperty(object) {
+      var tag, testRti = this;
+      if (object == null)
+        return A._nullIs(testRti);
+      tag = testRti._specializedTestResource;
+      if (object instanceof A.Object)
+        return !!object[tag];
+      return !!J.getInterceptor$(object)[tag];
+    },
+    _isListTestViaProperty(object) {
+      var tag, testRti = this;
+      if (object == null)
+        return A._nullIs(testRti);
+      if (typeof object != "object")
+        return false;
+      if (Array.isArray(object))
+        return true;
+      tag = testRti._specializedTestResource;
+      if (object instanceof A.Object)
+        return !!object[tag];
+      return !!J.getInterceptor$(object)[tag];
+    },
+    _generalAsCheckImplementation(object) {
+      var t1, testRti = this;
+      if (object == null) {
+        t1 = A.isNullable(testRti);
+        if (t1)
+          return object;
+      } else if (testRti._is(object))
+        return object;
+      A._failedAsCheck(object, testRti);
+    },
+    _generalNullableAsCheckImplementation(object) {
+      var testRti = this;
+      if (object == null)
+        return object;
+      else if (testRti._is(object))
+        return object;
+      A._failedAsCheck(object, testRti);
+    },
+    _failedAsCheck(object, testRti) {
+      throw A.wrapException(A._TypeError$fromMessage(A._Error_compose(object, A._rtiToString(testRti, null))));
+    },
+    checkTypeBound(type, bound, variable, methodName) {
+      if (A.isSubtype(init.typeUniverse, type, bound))
+        return type;
+      throw A.wrapException(A._TypeError$fromMessage("The type argument '" + A._rtiToString(type, null) + "' is not a subtype of the type variable bound '" + A._rtiToString(bound, null) + "' of type variable '" + variable + "' in '" + methodName + "'."));
+    },
+    _Error_compose(object, checkedTypeDescription) {
+      return A.Error_safeToString(object) + ": type '" + A._rtiToString(A._structuralTypeOf(object), null) + "' is not a subtype of type '" + checkedTypeDescription + "'";
+    },
+    _TypeError$fromMessage(message) {
+      return new A._TypeError("TypeError: " + message);
+    },
+    _TypeError__TypeError$forType(object, type) {
+      return new A._TypeError("TypeError: " + A._Error_compose(object, type));
+    },
+    _isFutureOr(object) {
+      var testRti = this,
+        unstarred = testRti._kind === 6 ? testRti._primary : testRti;
+      return unstarred._primary._is(object) || A.Rti__getFutureFromFutureOr(init.typeUniverse, unstarred)._is(object);
+    },
+    _isObject(object) {
+      return object != null;
+    },
+    _asObject(object) {
+      if (object != null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "Object"));
+    },
+    _isTop(object) {
+      return true;
+    },
+    _asTop(object) {
+      return object;
+    },
+    _isNever(object) {
+      return false;
+    },
+    _isBool(object) {
+      return true === object || false === object;
+    },
+    _asBool(object) {
+      if (true === object)
+        return true;
+      if (false === object)
+        return false;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "bool"));
+    },
+    _asBoolS(object) {
+      if (true === object)
+        return true;
+      if (false === object)
+        return false;
+      if (object == null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "bool"));
+    },
+    _asBoolQ(object) {
+      if (true === object)
+        return true;
+      if (false === object)
+        return false;
+      if (object == null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "bool?"));
+    },
+    _asDouble(object) {
+      if (typeof object == "number")
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "double"));
+    },
+    _asDoubleS(object) {
+      if (typeof object == "number")
+        return object;
+      if (object == null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "double"));
+    },
+    _asDoubleQ(object) {
+      if (typeof object == "number")
+        return object;
+      if (object == null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "double?"));
+    },
+    _isInt(object) {
+      return typeof object == "number" && Math.floor(object) === object;
+    },
+    _asInt(object) {
+      if (typeof object == "number" && Math.floor(object) === object)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "int"));
+    },
+    _asIntS(object) {
+      if (typeof object == "number" && Math.floor(object) === object)
+        return object;
+      if (object == null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "int"));
+    },
+    _asIntQ(object) {
+      if (typeof object == "number" && Math.floor(object) === object)
+        return object;
+      if (object == null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "int?"));
+    },
+    _isNum(object) {
+      return typeof object == "number";
+    },
+    _asNum(object) {
+      if (typeof object == "number")
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "num"));
+    },
+    _asNumS(object) {
+      if (typeof object == "number")
+        return object;
+      if (object == null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "num"));
+    },
+    _asNumQ(object) {
+      if (typeof object == "number")
+        return object;
+      if (object == null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "num?"));
+    },
+    _isString(object) {
+      return typeof object == "string";
+    },
+    _asString(object) {
+      if (typeof object == "string")
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "String"));
+    },
+    _asStringS(object) {
+      if (typeof object == "string")
+        return object;
+      if (object == null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "String"));
+    },
+    _asStringQ(object) {
+      if (typeof object == "string")
+        return object;
+      if (object == null)
+        return object;
+      throw A.wrapException(A._TypeError__TypeError$forType(object, "String?"));
+    },
+    _rtiArrayToString(array, genericContext) {
+      var s, sep, i;
+      for (s = "", sep = "", i = 0; i < array.length; ++i, sep = ", ")
+        s += sep + A._rtiToString(array[i], genericContext);
+      return s;
+    },
+    _recordRtiToString(recordType, genericContext) {
+      var fieldCount, names, namesIndex, s, comma, i,
+        partialShape = recordType._primary,
+        fields = recordType._rest;
+      if ("" === partialShape)
+        return "(" + A._rtiArrayToString(fields, genericContext) + ")";
+      fieldCount = fields.length;
+      names = partialShape.split(",");
+      namesIndex = names.length - fieldCount;
+      for (s = "(", comma = "", i = 0; i < fieldCount; ++i, comma = ", ") {
+        s += comma;
+        if (namesIndex === 0)
+          s += "{";
+        s += A._rtiToString(fields[i], genericContext);
+        if (namesIndex >= 0)
+          s += " " + names[namesIndex];
+        ++namesIndex;
+      }
+      return s + "})";
+    },
+    _functionRtiToString(functionType, genericContext, bounds) {
+      var boundsLength, outerContextLength, offset, i, t1, t2, typeParametersText, typeSep, t3, t4, boundRti, kind, parameters, requiredPositional, requiredPositionalLength, optionalPositional, optionalPositionalLength, named, namedLength, returnTypeText, argumentsText, sep, _s2_ = ", ";
+      if (bounds != null) {
+        boundsLength = bounds.length;
+        if (genericContext == null) {
+          genericContext = A._setArrayType([], type$.JSArray_String);
+          outerContextLength = null;
+        } else
+          outerContextLength = genericContext.length;
+        offset = genericContext.length;
+        for (i = boundsLength; i > 0; --i)
+          B.JSArray_methods.add$1(genericContext, "T" + (offset + i));
+        for (t1 = type$.nullable_Object, t2 = type$.legacy_Object, typeParametersText = "<", typeSep = "", i = 0; i < boundsLength; ++i, typeSep = _s2_) {
+          t3 = genericContext.length;
+          t4 = t3 - 1 - i;
+          if (!(t4 >= 0))
+            return A.ioore(genericContext, t4);
+          typeParametersText = B.JSString_methods.$add(typeParametersText + typeSep, genericContext[t4]);
+          boundRti = bounds[i];
+          kind = boundRti._kind;
+          if (!(kind === 2 || kind === 3 || kind === 4 || kind === 5 || boundRti === t1))
+            if (!(boundRti === t2))
+              t3 = false;
+            else
+              t3 = true;
+          else
+            t3 = true;
+          if (!t3)
+            typeParametersText += " extends " + A._rtiToString(boundRti, genericContext);
+        }
+        typeParametersText += ">";
+      } else {
+        typeParametersText = "";
+        outerContextLength = null;
+      }
+      t1 = functionType._primary;
+      parameters = functionType._rest;
+      requiredPositional = parameters._requiredPositional;
+      requiredPositionalLength = requiredPositional.length;
+      optionalPositional = parameters._optionalPositional;
+      optionalPositionalLength = optionalPositional.length;
+      named = parameters._named;
+      namedLength = named.length;
+      returnTypeText = A._rtiToString(t1, genericContext);
+      for (argumentsText = "", sep = "", i = 0; i < requiredPositionalLength; ++i, sep = _s2_)
+        argumentsText += sep + A._rtiToString(requiredPositional[i], genericContext);
+      if (optionalPositionalLength > 0) {
+        argumentsText += sep + "[";
+        for (sep = "", i = 0; i < optionalPositionalLength; ++i, sep = _s2_)
+          argumentsText += sep + A._rtiToString(optionalPositional[i], genericContext);
+        argumentsText += "]";
+      }
+      if (namedLength > 0) {
+        argumentsText += sep + "{";
+        for (sep = "", i = 0; i < namedLength; i += 3, sep = _s2_) {
+          argumentsText += sep;
+          if (named[i + 1])
+            argumentsText += "required ";
+          argumentsText += A._rtiToString(named[i + 2], genericContext) + " " + named[i];
+        }
+        argumentsText += "}";
+      }
+      if (outerContextLength != null) {
+        genericContext.toString;
+        genericContext.length = outerContextLength;
+      }
+      return typeParametersText + "(" + argumentsText + ") => " + returnTypeText;
+    },
+    _rtiToString(rti, genericContext) {
+      var s, questionArgument, argumentKind, $name, $arguments, t1, t2,
+        kind = rti._kind;
+      if (kind === 5)
+        return "erased";
+      if (kind === 2)
+        return "dynamic";
+      if (kind === 3)
+        return "void";
+      if (kind === 1)
+        return "Never";
+      if (kind === 4)
+        return "any";
+      if (kind === 6) {
+        s = A._rtiToString(rti._primary, genericContext);
+        return s;
+      }
+      if (kind === 7) {
+        questionArgument = rti._primary;
+        s = A._rtiToString(questionArgument, genericContext);
+        argumentKind = questionArgument._kind;
+        return (argumentKind === 12 || argumentKind === 13 ? "(" + s + ")" : s) + "?";
+      }
+      if (kind === 8)
+        return "FutureOr<" + A._rtiToString(rti._primary, genericContext) + ">";
+      if (kind === 9) {
+        $name = A._unminifyOrTag(rti._primary);
+        $arguments = rti._rest;
+        return $arguments.length > 0 ? $name + ("<" + A._rtiArrayToString($arguments, genericContext) + ">") : $name;
+      }
+      if (kind === 11)
+        return A._recordRtiToString(rti, genericContext);
+      if (kind === 12)
+        return A._functionRtiToString(rti, genericContext, null);
+      if (kind === 13)
+        return A._functionRtiToString(rti._primary, genericContext, rti._rest);
+      if (kind === 14) {
+        t1 = rti._primary;
+        t2 = genericContext.length;
+        t1 = t2 - 1 - t1;
+        if (!(t1 >= 0 && t1 < t2))
+          return A.ioore(genericContext, t1);
+        return genericContext[t1];
+      }
+      return "?";
+    },
+    _unminifyOrTag(rawClassName) {
+      var preserved = init.mangledGlobalNames[rawClassName];
+      if (preserved != null)
+        return preserved;
+      return rawClassName;
+    },
+    _Universe_findRule(universe, targetType) {
+      var rule = universe.tR[targetType];
+      for (; typeof rule == "string";)
+        rule = universe.tR[rule];
+      return rule;
+    },
+    _Universe_findErasedType(universe, cls) {
+      var $length, erased, $arguments, i, $interface,
+        t1 = universe.eT,
+        probe = t1[cls];
+      if (probe == null)
+        return A._Universe_eval(universe, cls, false);
+      else if (typeof probe == "number") {
+        $length = probe;
+        erased = A._Universe__lookupTerminalRti(universe, 5, "#");
+        $arguments = A._Utils_newArrayOrEmpty($length);
+        for (i = 0; i < $length; ++i)
+          $arguments[i] = erased;
+        $interface = A._Universe__lookupInterfaceRti(universe, cls, $arguments);
+        t1[cls] = $interface;
+        return $interface;
+      } else
+        return probe;
+    },
+    _Universe_addRules(universe, rules) {
+      return A._Utils_objectAssign(universe.tR, rules);
+    },
+    _Universe_addErasedTypes(universe, types) {
+      return A._Utils_objectAssign(universe.eT, types);
+    },
+    _Universe_eval(universe, recipe, normalize) {
+      var rti,
+        t1 = universe.eC,
+        probe = t1.get(recipe);
+      if (probe != null)
+        return probe;
+      rti = A._Parser_parse(A._Parser_create(universe, null, recipe, normalize));
+      t1.set(recipe, rti);
+      return rti;
+    },
+    _Universe_evalInEnvironment(universe, environment, recipe) {
+      var probe, rti,
+        cache = environment._evalCache;
+      if (cache == null)
+        cache = environment._evalCache = new Map();
+      probe = cache.get(recipe);
+      if (probe != null)
+        return probe;
+      rti = A._Parser_parse(A._Parser_create(universe, environment, recipe, true));
+      cache.set(recipe, rti);
+      return rti;
+    },
+    _Universe_bind(universe, environment, argumentsRti) {
+      var argumentsRecipe, probe, rti,
+        cache = environment._bindCache;
+      if (cache == null)
+        cache = environment._bindCache = new Map();
+      argumentsRecipe = argumentsRti._canonicalRecipe;
+      probe = cache.get(argumentsRecipe);
+      if (probe != null)
+        return probe;
+      rti = A._Universe__lookupBindingRti(universe, environment, argumentsRti._kind === 10 ? argumentsRti._rest : [argumentsRti]);
+      cache.set(argumentsRecipe, rti);
+      return rti;
+    },
+    _Universe__installTypeTests(universe, rti) {
+      rti._as = A._installSpecializedAsCheck;
+      rti._is = A._installSpecializedIsTest;
+      return rti;
+    },
+    _Universe__lookupTerminalRti(universe, kind, key) {
+      var rti, t1,
+        probe = universe.eC.get(key);
+      if (probe != null)
+        return probe;
+      rti = new A.Rti(null, null);
+      rti._kind = kind;
+      rti._canonicalRecipe = key;
+      t1 = A._Universe__installTypeTests(universe, rti);
+      universe.eC.set(key, t1);
+      return t1;
+    },
+    _Universe__lookupStarRti(universe, baseType, normalize) {
+      var t1,
+        key = baseType._canonicalRecipe + "*",
+        probe = universe.eC.get(key);
+      if (probe != null)
+        return probe;
+      t1 = A._Universe__createStarRti(universe, baseType, key, normalize);
+      universe.eC.set(key, t1);
+      return t1;
+    },
+    _Universe__createStarRti(universe, baseType, key, normalize) {
+      var baseKind, t1, rti;
+      if (normalize) {
+        baseKind = baseType._kind;
+        if (!A.isStrongTopType(baseType))
+          t1 = baseType === type$.Null || baseType === type$.JSNull || baseKind === 7 || baseKind === 6;
+        else
+          t1 = true;
+        if (t1)
+          return baseType;
+      }
+      rti = new A.Rti(null, null);
+      rti._kind = 6;
+      rti._primary = baseType;
+      rti._canonicalRecipe = key;
+      return A._Universe__installTypeTests(universe, rti);
+    },
+    _Universe__lookupQuestionRti(universe, baseType, normalize) {
+      var t1,
+        key = baseType._canonicalRecipe + "?",
+        probe = universe.eC.get(key);
+      if (probe != null)
+        return probe;
+      t1 = A._Universe__createQuestionRti(universe, baseType, key, normalize);
+      universe.eC.set(key, t1);
+      return t1;
+    },
+    _Universe__createQuestionRti(universe, baseType, key, normalize) {
+      var baseKind, t1, starArgument, rti;
+      if (normalize) {
+        baseKind = baseType._kind;
+        if (!A.isStrongTopType(baseType))
+          if (!(baseType === type$.Null || baseType === type$.JSNull))
+            if (baseKind !== 7)
+              t1 = baseKind === 8 && A.isNullable(baseType._primary);
+            else
+              t1 = true;
+          else
+            t1 = true;
+        else
+          t1 = true;
+        if (t1)
+          return baseType;
+        else if (baseKind === 1 || baseType === type$.legacy_Never)
+          return type$.Null;
+        else if (baseKind === 6) {
+          starArgument = baseType._primary;
+          if (starArgument._kind === 8 && A.isNullable(starArgument._primary))
+            return starArgument;
+          else
+            return A.Rti__getQuestionFromStar(universe, baseType);
+        }
+      }
+      rti = new A.Rti(null, null);
+      rti._kind = 7;
+      rti._primary = baseType;
+      rti._canonicalRecipe = key;
+      return A._Universe__installTypeTests(universe, rti);
+    },
+    _Universe__lookupFutureOrRti(universe, baseType, normalize) {
+      var t1,
+        key = baseType._canonicalRecipe + "/",
+        probe = universe.eC.get(key);
+      if (probe != null)
+        return probe;
+      t1 = A._Universe__createFutureOrRti(universe, baseType, key, normalize);
+      universe.eC.set(key, t1);
+      return t1;
+    },
+    _Universe__createFutureOrRti(universe, baseType, key, normalize) {
+      var t1, t2, rti;
+      if (normalize) {
+        t1 = baseType._kind;
+        if (!A.isStrongTopType(baseType))
+          if (!(baseType === type$.legacy_Object))
+            t2 = false;
+          else
+            t2 = true;
+        else
+          t2 = true;
+        if (t2 || baseType === type$.Object)
+          return baseType;
+        else if (t1 === 1)
+          return A._Universe__lookupInterfaceRti(universe, "Future", [baseType]);
+        else if (baseType === type$.Null || baseType === type$.JSNull)
+          return type$.nullable_Future_Null;
+      }
+      rti = new A.Rti(null, null);
+      rti._kind = 8;
+      rti._primary = baseType;
+      rti._canonicalRecipe = key;
+      return A._Universe__installTypeTests(universe, rti);
+    },
+    _Universe__lookupGenericFunctionParameterRti(universe, index) {
+      var rti, t1,
+        key = "" + index + "^",
+        probe = universe.eC.get(key);
+      if (probe != null)
+        return probe;
+      rti = new A.Rti(null, null);
+      rti._kind = 14;
+      rti._primary = index;
+      rti._canonicalRecipe = key;
+      t1 = A._Universe__installTypeTests(universe, rti);
+      universe.eC.set(key, t1);
+      return t1;
+    },
+    _Universe__canonicalRecipeJoin($arguments) {
+      var s, sep, i,
+        $length = $arguments.length;
+      for (s = "", sep = "", i = 0; i < $length; ++i, sep = ",")
+        s += sep + $arguments[i]._canonicalRecipe;
+      return s;
+    },
+    _Universe__canonicalRecipeJoinNamed($arguments) {
+      var s, sep, i, t1, nameSep,
+        $length = $arguments.length;
+      for (s = "", sep = "", i = 0; i < $length; i += 3, sep = ",") {
+        t1 = $arguments[i];
+        nameSep = $arguments[i + 1] ? "!" : ":";
+        s += sep + t1 + nameSep + $arguments[i + 2]._canonicalRecipe;
+      }
+      return s;
+    },
+    _Universe__lookupInterfaceRti(universe, $name, $arguments) {
+      var probe, rti, t1,
+        s = $name;
+      if ($arguments.length > 0)
+        s += "<" + A._Universe__canonicalRecipeJoin($arguments) + ">";
+      probe = universe.eC.get(s);
+      if (probe != null)
+        return probe;
+      rti = new A.Rti(null, null);
+      rti._kind = 9;
+      rti._primary = $name;
+      rti._rest = $arguments;
+      if ($arguments.length > 0)
+        rti._precomputed1 = $arguments[0];
+      rti._canonicalRecipe = s;
+      t1 = A._Universe__installTypeTests(universe, rti);
+      universe.eC.set(s, t1);
+      return t1;
+    },
+    _Universe__lookupBindingRti(universe, base, $arguments) {
+      var newBase, newArguments, key, probe, rti, t1;
+      if (base._kind === 10) {
+        newBase = base._primary;
+        newArguments = base._rest.concat($arguments);
+      } else {
+        newArguments = $arguments;
+        newBase = base;
+      }
+      key = newBase._canonicalRecipe + (";<" + A._Universe__canonicalRecipeJoin(newArguments) + ">");
+      probe = universe.eC.get(key);
+      if (probe != null)
+        return probe;
+      rti = new A.Rti(null, null);
+      rti._kind = 10;
+      rti._primary = newBase;
+      rti._rest = newArguments;
+      rti._canonicalRecipe = key;
+      t1 = A._Universe__installTypeTests(universe, rti);
+      universe.eC.set(key, t1);
+      return t1;
+    },
+    _Universe__lookupRecordRti(universe, partialShapeTag, fields) {
+      var rti, t1,
+        key = "+" + (partialShapeTag + "(" + A._Universe__canonicalRecipeJoin(fields) + ")"),
+        probe = universe.eC.get(key);
+      if (probe != null)
+        return probe;
+      rti = new A.Rti(null, null);
+      rti._kind = 11;
+      rti._primary = partialShapeTag;
+      rti._rest = fields;
+      rti._canonicalRecipe = key;
+      t1 = A._Universe__installTypeTests(universe, rti);
+      universe.eC.set(key, t1);
+      return t1;
+    },
+    _Universe__lookupFunctionRti(universe, returnType, parameters) {
+      var sep, key, probe, rti, t1,
+        s = returnType._canonicalRecipe,
+        requiredPositional = parameters._requiredPositional,
+        requiredPositionalLength = requiredPositional.length,
+        optionalPositional = parameters._optionalPositional,
+        optionalPositionalLength = optionalPositional.length,
+        named = parameters._named,
+        namedLength = named.length,
+        recipe = "(" + A._Universe__canonicalRecipeJoin(requiredPositional);
+      if (optionalPositionalLength > 0) {
+        sep = requiredPositionalLength > 0 ? "," : "";
+        recipe += sep + "[" + A._Universe__canonicalRecipeJoin(optionalPositional) + "]";
+      }
+      if (namedLength > 0) {
+        sep = requiredPositionalLength > 0 ? "," : "";
+        recipe += sep + "{" + A._Universe__canonicalRecipeJoinNamed(named) + "}";
+      }
+      key = s + (recipe + ")");
+      probe = universe.eC.get(key);
+      if (probe != null)
+        return probe;
+      rti = new A.Rti(null, null);
+      rti._kind = 12;
+      rti._primary = returnType;
+      rti._rest = parameters;
+      rti._canonicalRecipe = key;
+      t1 = A._Universe__installTypeTests(universe, rti);
+      universe.eC.set(key, t1);
+      return t1;
+    },
+    _Universe__lookupGenericFunctionRti(universe, baseFunctionType, bounds, normalize) {
+      var t1,
+        key = baseFunctionType._canonicalRecipe + ("<" + A._Universe__canonicalRecipeJoin(bounds) + ">"),
+        probe = universe.eC.get(key);
+      if (probe != null)
+        return probe;
+      t1 = A._Universe__createGenericFunctionRti(universe, baseFunctionType, bounds, key, normalize);
+      universe.eC.set(key, t1);
+      return t1;
+    },
+    _Universe__createGenericFunctionRti(universe, baseFunctionType, bounds, key, normalize) {
+      var $length, typeArguments, count, i, bound, substitutedBase, substitutedBounds, rti;
+      if (normalize) {
+        $length = bounds.length;
+        typeArguments = A._Utils_newArrayOrEmpty($length);
+        for (count = 0, i = 0; i < $length; ++i) {
+          bound = bounds[i];
+          if (bound._kind === 1) {
+            typeArguments[i] = bound;
+            ++count;
+          }
+        }
+        if (count > 0) {
+          substitutedBase = A._substitute(universe, baseFunctionType, typeArguments, 0);
+          substitutedBounds = A._substituteArray(universe, bounds, typeArguments, 0);
+          return A._Universe__lookupGenericFunctionRti(universe, substitutedBase, substitutedBounds, bounds !== substitutedBounds);
+        }
+      }
+      rti = new A.Rti(null, null);
+      rti._kind = 13;
+      rti._primary = baseFunctionType;
+      rti._rest = bounds;
+      rti._canonicalRecipe = key;
+      return A._Universe__installTypeTests(universe, rti);
+    },
+    _Parser_create(universe, environment, recipe, normalize) {
+      return {u: universe, e: environment, r: recipe, s: [], p: 0, n: normalize};
+    },
+    _Parser_parse(parser) {
+      var t2, i, ch, t3, array, end, item,
+        source = parser.r,
+        t1 = parser.s;
+      for (t2 = source.length, i = 0; i < t2;) {
+        ch = source.charCodeAt(i);
+        if (ch >= 48 && ch <= 57)
+          i = A._Parser_handleDigit(i + 1, ch, source, t1);
+        else if ((((ch | 32) >>> 0) - 97 & 65535) < 26 || ch === 95 || ch === 36 || ch === 124)
+          i = A._Parser_handleIdentifier(parser, i, source, t1, false);
+        else if (ch === 46)
+          i = A._Parser_handleIdentifier(parser, i, source, t1, true);
+        else {
+          ++i;
+          switch (ch) {
+            case 44:
+              break;
+            case 58:
+              t1.push(false);
+              break;
+            case 33:
+              t1.push(true);
+              break;
+            case 59:
+              t1.push(A._Parser_toType(parser.u, parser.e, t1.pop()));
+              break;
+            case 94:
+              t1.push(A._Universe__lookupGenericFunctionParameterRti(parser.u, t1.pop()));
+              break;
+            case 35:
+              t1.push(A._Universe__lookupTerminalRti(parser.u, 5, "#"));
+              break;
+            case 64:
+              t1.push(A._Universe__lookupTerminalRti(parser.u, 2, "@"));
+              break;
+            case 126:
+              t1.push(A._Universe__lookupTerminalRti(parser.u, 3, "~"));
+              break;
+            case 60:
+              t1.push(parser.p);
+              parser.p = t1.length;
+              break;
+            case 62:
+              A._Parser_handleTypeArguments(parser, t1);
+              break;
+            case 38:
+              A._Parser_handleExtendedOperations(parser, t1);
+              break;
+            case 42:
+              t3 = parser.u;
+              t1.push(A._Universe__lookupStarRti(t3, A._Parser_toType(t3, parser.e, t1.pop()), parser.n));
+              break;
+            case 63:
+              t3 = parser.u;
+              t1.push(A._Universe__lookupQuestionRti(t3, A._Parser_toType(t3, parser.e, t1.pop()), parser.n));
+              break;
+            case 47:
+              t3 = parser.u;
+              t1.push(A._Universe__lookupFutureOrRti(t3, A._Parser_toType(t3, parser.e, t1.pop()), parser.n));
+              break;
+            case 40:
+              t1.push(-3);
+              t1.push(parser.p);
+              parser.p = t1.length;
+              break;
+            case 41:
+              A._Parser_handleArguments(parser, t1);
+              break;
+            case 91:
+              t1.push(parser.p);
+              parser.p = t1.length;
+              break;
+            case 93:
+              array = t1.splice(parser.p);
+              A._Parser_toTypes(parser.u, parser.e, array);
+              parser.p = t1.pop();
+              t1.push(array);
+              t1.push(-1);
+              break;
+            case 123:
+              t1.push(parser.p);
+              parser.p = t1.length;
+              break;
+            case 125:
+              array = t1.splice(parser.p);
+              A._Parser_toTypesNamed(parser.u, parser.e, array);
+              parser.p = t1.pop();
+              t1.push(array);
+              t1.push(-2);
+              break;
+            case 43:
+              end = source.indexOf("(", i);
+              t1.push(source.substring(i, end));
+              t1.push(-4);
+              t1.push(parser.p);
+              parser.p = t1.length;
+              i = end + 1;
+              break;
+            default:
+              throw "Bad character " + ch;
+          }
+        }
+      }
+      item = t1.pop();
+      return A._Parser_toType(parser.u, parser.e, item);
+    },
+    _Parser_handleDigit(i, digit, source, stack) {
+      var t1, ch,
+        value = digit - 48;
+      for (t1 = source.length; i < t1; ++i) {
+        ch = source.charCodeAt(i);
+        if (!(ch >= 48 && ch <= 57))
+          break;
+        value = value * 10 + (ch - 48);
+      }
+      stack.push(value);
+      return i;
+    },
+    _Parser_handleIdentifier(parser, start, source, stack, hasPeriod) {
+      var t1, ch, t2, string, environment, recipe,
+        i = start + 1;
+      for (t1 = source.length; i < t1; ++i) {
+        ch = source.charCodeAt(i);
+        if (ch === 46) {
+          if (hasPeriod)
+            break;
+          hasPeriod = true;
+        } else {
+          if (!((((ch | 32) >>> 0) - 97 & 65535) < 26 || ch === 95 || ch === 36 || ch === 124))
+            t2 = ch >= 48 && ch <= 57;
+          else
+            t2 = true;
+          if (!t2)
+            break;
+        }
+      }
+      string = source.substring(start, i);
+      if (hasPeriod) {
+        t1 = parser.u;
+        environment = parser.e;
+        if (environment._kind === 10)
+          environment = environment._primary;
+        recipe = A._Universe_findRule(t1, environment._primary)[string];
+        if (recipe == null)
+          A.throwExpression('No "' + string + '" in "' + A.Rti__getCanonicalRecipe(environment) + '"');
+        stack.push(A._Universe_evalInEnvironment(t1, environment, recipe));
+      } else
+        stack.push(string);
+      return i;
+    },
+    _Parser_handleTypeArguments(parser, stack) {
+      var base,
+        t1 = parser.u,
+        $arguments = A._Parser_collectArray(parser, stack),
+        head = stack.pop();
+      if (typeof head == "string")
+        stack.push(A._Universe__lookupInterfaceRti(t1, head, $arguments));
+      else {
+        base = A._Parser_toType(t1, parser.e, head);
+        switch (base._kind) {
+          case 12:
+            stack.push(A._Universe__lookupGenericFunctionRti(t1, base, $arguments, parser.n));
+            break;
+          default:
+            stack.push(A._Universe__lookupBindingRti(t1, base, $arguments));
+            break;
+        }
+      }
+    },
+    _Parser_handleArguments(parser, stack) {
+      var optionalPositional, named, requiredPositional, returnType, parameters, _null = null,
+        t1 = parser.u,
+        head = stack.pop();
+      if (typeof head == "number")
+        switch (head) {
+          case -1:
+            optionalPositional = stack.pop();
+            named = _null;
+            break;
+          case -2:
+            named = stack.pop();
+            optionalPositional = _null;
+            break;
+          default:
+            stack.push(head);
+            named = _null;
+            optionalPositional = named;
+            break;
+        }
+      else {
+        stack.push(head);
+        named = _null;
+        optionalPositional = named;
+      }
+      requiredPositional = A._Parser_collectArray(parser, stack);
+      head = stack.pop();
+      switch (head) {
+        case -3:
+          head = stack.pop();
+          if (optionalPositional == null)
+            optionalPositional = t1.sEA;
+          if (named == null)
+            named = t1.sEA;
+          returnType = A._Parser_toType(t1, parser.e, head);
+          parameters = new A._FunctionParameters();
+          parameters._requiredPositional = requiredPositional;
+          parameters._optionalPositional = optionalPositional;
+          parameters._named = named;
+          stack.push(A._Universe__lookupFunctionRti(t1, returnType, parameters));
+          return;
+        case -4:
+          stack.push(A._Universe__lookupRecordRti(t1, stack.pop(), requiredPositional));
+          return;
+        default:
+          throw A.wrapException(A.AssertionError$("Unexpected state under `()`: " + A.S(head)));
+      }
+    },
+    _Parser_handleExtendedOperations(parser, stack) {
+      var $top = stack.pop();
+      if (0 === $top) {
+        stack.push(A._Universe__lookupTerminalRti(parser.u, 1, "0&"));
+        return;
+      }
+      if (1 === $top) {
+        stack.push(A._Universe__lookupTerminalRti(parser.u, 4, "1&"));
+        return;
+      }
+      throw A.wrapException(A.AssertionError$("Unexpected extended operation " + A.S($top)));
+    },
+    _Parser_collectArray(parser, stack) {
+      var array = stack.splice(parser.p);
+      A._Parser_toTypes(parser.u, parser.e, array);
+      parser.p = stack.pop();
+      return array;
+    },
+    _Parser_toType(universe, environment, item) {
+      if (typeof item == "string")
+        return A._Universe__lookupInterfaceRti(universe, item, universe.sEA);
+      else if (typeof item == "number") {
+        environment.toString;
+        return A._Parser_indexToType(universe, environment, item);
+      } else
+        return item;
+    },
+    _Parser_toTypes(universe, environment, items) {
+      var i,
+        $length = items.length;
+      for (i = 0; i < $length; ++i)
+        items[i] = A._Parser_toType(universe, environment, items[i]);
+    },
+    _Parser_toTypesNamed(universe, environment, items) {
+      var i,
+        $length = items.length;
+      for (i = 2; i < $length; i += 3)
+        items[i] = A._Parser_toType(universe, environment, items[i]);
+    },
+    _Parser_indexToType(universe, environment, index) {
+      var typeArguments, len,
+        kind = environment._kind;
+      if (kind === 10) {
+        if (index === 0)
+          return environment._primary;
+        typeArguments = environment._rest;
+        len = typeArguments.length;
+        if (index <= len)
+          return typeArguments[index - 1];
+        index -= len;
+        environment = environment._primary;
+        kind = environment._kind;
+      } else if (index === 0)
+        return environment;
+      if (kind !== 9)
+        throw A.wrapException(A.AssertionError$("Indexed base must be an interface type"));
+      typeArguments = environment._rest;
+      if (index <= typeArguments.length)
+        return typeArguments[index - 1];
+      throw A.wrapException(A.AssertionError$("Bad index " + index + " for " + environment.toString$0(0)));
+    },
+    isSubtype(universe, s, t) {
+      var result,
+        sCache = A.Rti__getIsSubtypeCache(s),
+        probe = sCache.get(t);
+      if (probe != null)
+        return probe;
+      result = A._isSubtype(universe, s, null, t, null);
+      sCache.set(t, result);
+      return result;
+    },
+    _isSubtype(universe, s, sEnv, t, tEnv) {
+      var t1, sKind, leftTypeVariable, tKind, t2, sBounds, tBounds, sLength, i, sBound, tBound;
+      if (s === t)
+        return true;
+      if (!A.isStrongTopType(t))
+        if (!(t === type$.legacy_Object))
+          t1 = false;
+        else
+          t1 = true;
+      else
+        t1 = true;
+      if (t1)
+        return true;
+      sKind = s._kind;
+      if (sKind === 4)
+        return true;
+      if (A.isStrongTopType(s))
+        return false;
+      if (s._kind !== 1)
+        t1 = false;
+      else
+        t1 = true;
+      if (t1)
+        return true;
+      leftTypeVariable = sKind === 14;
+      if (leftTypeVariable)
+        if (A._isSubtype(universe, sEnv[s._primary], sEnv, t, tEnv))
+          return true;
+      tKind = t._kind;
+      t1 = s === type$.Null || s === type$.JSNull;
+      if (t1) {
+        if (tKind === 8)
+          return A._isSubtype(universe, s, sEnv, t._primary, tEnv);
+        return t === type$.Null || t === type$.JSNull || tKind === 7 || tKind === 6;
+      }
+      if (t === type$.Object) {
+        if (sKind === 8)
+          return A._isSubtype(universe, s._primary, sEnv, t, tEnv);
+        if (sKind === 6)
+          return A._isSubtype(universe, s._primary, sEnv, t, tEnv);
+        return sKind !== 7;
+      }
+      if (sKind === 6)
+        return A._isSubtype(universe, s._primary, sEnv, t, tEnv);
+      if (tKind === 6) {
+        t1 = A.Rti__getQuestionFromStar(universe, t);
+        return A._isSubtype(universe, s, sEnv, t1, tEnv);
+      }
+      if (sKind === 8) {
+        if (!A._isSubtype(universe, s._primary, sEnv, t, tEnv))
+          return false;
+        return A._isSubtype(universe, A.Rti__getFutureFromFutureOr(universe, s), sEnv, t, tEnv);
+      }
+      if (sKind === 7) {
+        t1 = A._isSubtype(universe, type$.Null, sEnv, t, tEnv);
+        return t1 && A._isSubtype(universe, s._primary, sEnv, t, tEnv);
+      }
+      if (tKind === 8) {
+        if (A._isSubtype(universe, s, sEnv, t._primary, tEnv))
+          return true;
+        return A._isSubtype(universe, s, sEnv, A.Rti__getFutureFromFutureOr(universe, t), tEnv);
+      }
+      if (tKind === 7) {
+        t1 = A._isSubtype(universe, s, sEnv, type$.Null, tEnv);
+        return t1 || A._isSubtype(universe, s, sEnv, t._primary, tEnv);
+      }
+      if (leftTypeVariable)
+        return false;
+      t1 = sKind !== 12;
+      if ((!t1 || sKind === 13) && t === type$.Function)
+        return true;
+      t2 = sKind === 11;
+      if (t2 && t === type$.Record)
+        return true;
+      if (tKind === 13) {
+        if (s === type$.JavaScriptFunction)
+          return true;
+        if (sKind !== 13)
+          return false;
+        sBounds = s._rest;
+        tBounds = t._rest;
+        sLength = sBounds.length;
+        if (sLength !== tBounds.length)
+          return false;
+        sEnv = sEnv == null ? sBounds : sBounds.concat(sEnv);
+        tEnv = tEnv == null ? tBounds : tBounds.concat(tEnv);
+        for (i = 0; i < sLength; ++i) {
+          sBound = sBounds[i];
+          tBound = tBounds[i];
+          if (!A._isSubtype(universe, sBound, sEnv, tBound, tEnv) || !A._isSubtype(universe, tBound, tEnv, sBound, sEnv))
+            return false;
+        }
+        return A._isFunctionSubtype(universe, s._primary, sEnv, t._primary, tEnv);
+      }
+      if (tKind === 12) {
+        if (s === type$.JavaScriptFunction)
+          return true;
+        if (t1)
+          return false;
+        return A._isFunctionSubtype(universe, s, sEnv, t, tEnv);
+      }
+      if (sKind === 9) {
+        if (tKind !== 9)
+          return false;
+        return A._isInterfaceSubtype(universe, s, sEnv, t, tEnv);
+      }
+      if (t2 && tKind === 11)
+        return A._isRecordSubtype(universe, s, sEnv, t, tEnv);
+      return false;
+    },
+    _isFunctionSubtype(universe, s, sEnv, t, tEnv) {
+      var sParameters, tParameters, sRequiredPositional, tRequiredPositional, sRequiredPositionalLength, tRequiredPositionalLength, requiredPositionalDelta, sOptionalPositional, tOptionalPositional, sOptionalPositionalLength, tOptionalPositionalLength, i, t1, sNamed, tNamed, sNamedLength, tNamedLength, sIndex, tIndex, tName, sName, sIsRequired;
+      if (!A._isSubtype(universe, s._primary, sEnv, t._primary, tEnv))
+        return false;
+      sParameters = s._rest;
+      tParameters = t._rest;
+      sRequiredPositional = sParameters._requiredPositional;
+      tRequiredPositional = tParameters._requiredPositional;
+      sRequiredPositionalLength = sRequiredPositional.length;
+      tRequiredPositionalLength = tRequiredPositional.length;
+      if (sRequiredPositionalLength > tRequiredPositionalLength)
+        return false;
+      requiredPositionalDelta = tRequiredPositionalLength - sRequiredPositionalLength;
+      sOptionalPositional = sParameters._optionalPositional;
+      tOptionalPositional = tParameters._optionalPositional;
+      sOptionalPositionalLength = sOptionalPositional.length;
+      tOptionalPositionalLength = tOptionalPositional.length;
+      if (sRequiredPositionalLength + sOptionalPositionalLength < tRequiredPositionalLength + tOptionalPositionalLength)
+        return false;
+      for (i = 0; i < sRequiredPositionalLength; ++i) {
+        t1 = sRequiredPositional[i];
+        if (!A._isSubtype(universe, tRequiredPositional[i], tEnv, t1, sEnv))
+          return false;
+      }
+      for (i = 0; i < requiredPositionalDelta; ++i) {
+        t1 = sOptionalPositional[i];
+        if (!A._isSubtype(universe, tRequiredPositional[sRequiredPositionalLength + i], tEnv, t1, sEnv))
+          return false;
+      }
+      for (i = 0; i < tOptionalPositionalLength; ++i) {
+        t1 = sOptionalPositional[requiredPositionalDelta + i];
+        if (!A._isSubtype(universe, tOptionalPositional[i], tEnv, t1, sEnv))
+          return false;
+      }
+      sNamed = sParameters._named;
+      tNamed = tParameters._named;
+      sNamedLength = sNamed.length;
+      tNamedLength = tNamed.length;
+      for (sIndex = 0, tIndex = 0; tIndex < tNamedLength; tIndex += 3) {
+        tName = tNamed[tIndex];
+        for (; true;) {
+          if (sIndex >= sNamedLength)
+            return false;
+          sName = sNamed[sIndex];
+          sIndex += 3;
+          if (tName < sName)
+            return false;
+          sIsRequired = sNamed[sIndex - 2];
+          if (sName < tName) {
+            if (sIsRequired)
+              return false;
+            continue;
+          }
+          t1 = tNamed[tIndex + 1];
+          if (sIsRequired && !t1)
+            return false;
+          t1 = sNamed[sIndex - 1];
+          if (!A._isSubtype(universe, tNamed[tIndex + 2], tEnv, t1, sEnv))
+            return false;
+          break;
+        }
+      }
+      for (; sIndex < sNamedLength;) {
+        if (sNamed[sIndex + 1])
+          return false;
+        sIndex += 3;
+      }
+      return true;
+    },
+    _isInterfaceSubtype(universe, s, sEnv, t, tEnv) {
+      var rule, recipes, $length, supertypeArgs, i, t1, t2,
+        sName = s._primary,
+        tName = t._primary;
+      for (; sName !== tName;) {
+        rule = universe.tR[sName];
+        if (rule == null)
+          return false;
+        if (typeof rule == "string") {
+          sName = rule;
+          continue;
+        }
+        recipes = rule[tName];
+        if (recipes == null)
+          return false;
+        $length = recipes.length;
+        supertypeArgs = $length > 0 ? new Array($length) : init.typeUniverse.sEA;
+        for (i = 0; i < $length; ++i)
+          supertypeArgs[i] = A._Universe_evalInEnvironment(universe, s, recipes[i]);
+        return A._areArgumentsSubtypes(universe, supertypeArgs, null, sEnv, t._rest, tEnv);
+      }
+      t1 = s._rest;
+      t2 = t._rest;
+      return A._areArgumentsSubtypes(universe, t1, null, sEnv, t2, tEnv);
+    },
+    _areArgumentsSubtypes(universe, sArgs, sVariances, sEnv, tArgs, tEnv) {
+      var i, t1, t2,
+        $length = sArgs.length;
+      for (i = 0; i < $length; ++i) {
+        t1 = sArgs[i];
+        t2 = tArgs[i];
+        if (!A._isSubtype(universe, t1, sEnv, t2, tEnv))
+          return false;
+      }
+      return true;
+    },
+    _isRecordSubtype(universe, s, sEnv, t, tEnv) {
+      var i,
+        sFields = s._rest,
+        tFields = t._rest,
+        sCount = sFields.length;
+      if (sCount !== tFields.length)
+        return false;
+      if (s._primary !== t._primary)
+        return false;
+      for (i = 0; i < sCount; ++i)
+        if (!A._isSubtype(universe, sFields[i], sEnv, tFields[i], tEnv))
+          return false;
+      return true;
+    },
+    isNullable(t) {
+      var t1,
+        kind = t._kind;
+      if (!(t === type$.Null || t === type$.JSNull))
+        if (!A.isStrongTopType(t))
+          if (kind !== 7)
+            if (!(kind === 6 && A.isNullable(t._primary)))
+              t1 = kind === 8 && A.isNullable(t._primary);
+            else
+              t1 = true;
+          else
+            t1 = true;
+        else
+          t1 = true;
+      else
+        t1 = true;
+      return t1;
+    },
+    isTopType(t) {
+      var t1;
+      if (!A.isStrongTopType(t))
+        if (!(t === type$.legacy_Object))
+          t1 = false;
+        else
+          t1 = true;
+      else
+        t1 = true;
+      return t1;
+    },
+    isStrongTopType(t) {
+      var kind = t._kind;
+      return kind === 2 || kind === 3 || kind === 4 || kind === 5 || t === type$.nullable_Object;
+    },
+    _Utils_objectAssign(o, other) {
+      var i, key,
+        keys = Object.keys(other),
+        $length = keys.length;
+      for (i = 0; i < $length; ++i) {
+        key = keys[i];
+        o[key] = other[key];
+      }
+    },
+    _Utils_newArrayOrEmpty($length) {
+      return $length > 0 ? new Array($length) : init.typeUniverse.sEA;
+    },
+    Rti: function Rti(t0, t1) {
+      var _ = this;
+      _._as = t0;
+      _._is = t1;
+      _._cachedRuntimeType = _._specializedTestResource = _._unsoundIsSubtypeCache = _._isSubtypeCache = _._precomputed1 = null;
+      _._kind = 0;
+      _._canonicalRecipe = _._bindCache = _._evalCache = _._rest = _._primary = null;
+    },
+    _FunctionParameters: function _FunctionParameters() {
+      this._named = this._optionalPositional = this._requiredPositional = null;
+    },
+    _Type: function _Type(t0) {
+      this._rti = t0;
+    },
+    _Error: function _Error() {
+    },
+    _TypeError: function _TypeError(t0) {
+      this.__rti$_message = t0;
+    },
+    _AsyncRun__initializeScheduleImmediate() {
+      var div, span, t1 = {};
+      if (self.scheduleImmediate != null)
+        return A.async__AsyncRun__scheduleImmediateJsOverride$closure();
+      if (self.MutationObserver != null && self.document != null) {
+        div = self.document.createElement("div");
+        span = self.document.createElement("span");
+        t1.storedCallback = null;
+        new self.MutationObserver(A.convertDartClosureToJS(new A._AsyncRun__initializeScheduleImmediate_internalCallback(t1), 1)).observe(div, {childList: true});
+        return new A._AsyncRun__initializeScheduleImmediate_closure(t1, div, span);
+      } else if (self.setImmediate != null)
+        return A.async__AsyncRun__scheduleImmediateWithSetImmediate$closure();
+      return A.async__AsyncRun__scheduleImmediateWithTimer$closure();
+    },
+    _AsyncRun__scheduleImmediateJsOverride(callback) {
+      self.scheduleImmediate(A.convertDartClosureToJS(new A._AsyncRun__scheduleImmediateJsOverride_internalCallback(type$.void_Function._as(callback)), 0));
+    },
+    _AsyncRun__scheduleImmediateWithSetImmediate(callback) {
+      self.setImmediate(A.convertDartClosureToJS(new A._AsyncRun__scheduleImmediateWithSetImmediate_internalCallback(type$.void_Function._as(callback)), 0));
+    },
+    _AsyncRun__scheduleImmediateWithTimer(callback) {
+      A.Timer__createTimer(B.Duration_0, type$.void_Function._as(callback));
+    },
+    Timer__createTimer(duration, callback) {
+      var milliseconds = B.JSInt_methods._tdivFast$1(duration._duration, 1000);
+      return A._TimerImpl$(milliseconds, callback);
+    },
+    Timer__createPeriodicTimer(duration, callback) {
+      var milliseconds = B.JSInt_methods._tdivFast$1(duration._duration, 1000);
+      return A._TimerImpl$periodic(milliseconds, callback);
+    },
+    _TimerImpl$(milliseconds, callback) {
+      var t1 = new A._TimerImpl();
+      t1._TimerImpl$2(milliseconds, callback);
+      return t1;
+    },
+    _TimerImpl$periodic(milliseconds, callback) {
+      var t1 = new A._TimerImpl();
+      t1._TimerImpl$periodic$2(milliseconds, callback);
+      return t1;
+    },
+    AsyncError$(error, stackTrace) {
+      var t1 = A.checkNotNullable(error, "error", type$.Object);
+      return new A.AsyncError(t1, stackTrace == null ? A.AsyncError_defaultStackTrace(error) : stackTrace);
+    },
+    AsyncError_defaultStackTrace(error) {
+      var stackTrace;
+      if (type$.Error._is(error)) {
+        stackTrace = error.get$stackTrace();
+        if (stackTrace != null)
+          return stackTrace;
+      }
+      return B._StringStackTrace_3uE;
+    },
+    Future_Future$value(value, $T) {
+      var t1;
+      $T._as(value);
+      t1 = new A._Future($.Zone__current, $T._eval$1("_Future<0>"));
+      t1._asyncComplete$1(value);
+      return t1;
+    },
+    _Future__chainCoreFutureSync(source, target) {
+      var t1, t2, listeners;
+      for (t1 = type$._Future_dynamic; t2 = source._async$_state, (t2 & 4) !== 0;)
+        source = t1._as(source._resultOrListeners);
+      if ((t2 & 24) !== 0) {
+        listeners = target._removeListeners$0();
+        target._cloneResult$1(source);
+        A._Future__propagateToListeners(target, listeners);
+      } else {
+        listeners = type$.nullable__FutureListener_dynamic_dynamic._as(target._resultOrListeners);
+        target._setChained$1(source);
+        source._prependListeners$1(listeners);
+      }
+    },
+    _Future__chainCoreFutureAsync(source, target) {
+      var t2, t3, listeners, _box_0 = {},
+        t1 = _box_0.source = source;
+      for (t2 = type$._Future_dynamic; t3 = t1._async$_state, (t3 & 4) !== 0; t1 = source) {
+        source = t2._as(t1._resultOrListeners);
+        _box_0.source = source;
+      }
+      if ((t3 & 24) === 0) {
+        listeners = type$.nullable__FutureListener_dynamic_dynamic._as(target._resultOrListeners);
+        target._setChained$1(t1);
+        _box_0.source._prependListeners$1(listeners);
+        return;
+      }
+      if ((t3 & 16) === 0 && target._resultOrListeners == null) {
+        target._cloneResult$1(t1);
+        return;
+      }
+      target._async$_state ^= 2;
+      target._zone.scheduleMicrotask$1(new A._Future__chainCoreFutureAsync_closure(_box_0, target));
+    },
+    _Future__propagateToListeners(source, listeners) {
+      var t2, t3, t4, _box_0, t5, t6, hasError, asyncError, nextListener, nextListener0, sourceResult, t7, zone, oldZone, result, current, _box_1 = {},
+        t1 = _box_1.source = source;
+      for (t2 = type$.AsyncError, t3 = type$.nullable__FutureListener_dynamic_dynamic, t4 = type$.Future_dynamic; true;) {
+        _box_0 = {};
+        t5 = t1._async$_state;
+        t6 = (t5 & 16) === 0;
+        hasError = !t6;
+        if (listeners == null) {
+          if (hasError && (t5 & 1) === 0) {
+            asyncError = t2._as(t1._resultOrListeners);
+            t1._zone.handleUncaughtError$2(asyncError.error, asyncError.stackTrace);
+          }
+          return;
+        }
+        _box_0.listener = listeners;
+        nextListener = listeners._nextListener;
+        for (t1 = listeners; nextListener != null; t1 = nextListener, nextListener = nextListener0) {
+          t1._nextListener = null;
+          A._Future__propagateToListeners(_box_1.source, t1);
+          _box_0.listener = nextListener;
+          nextListener0 = nextListener._nextListener;
+        }
+        t5 = _box_1.source;
+        sourceResult = t5._resultOrListeners;
+        _box_0.listenerHasError = hasError;
+        _box_0.listenerValueOrError = sourceResult;
+        if (t6) {
+          t7 = t1.state;
+          t7 = (t7 & 1) !== 0 || (t7 & 15) === 8;
+        } else
+          t7 = true;
+        if (t7) {
+          zone = t1.result._zone;
+          if (hasError) {
+            t1 = t5._zone;
+            t1 = !(t1 === zone || t1.get$errorZone() === zone.get$errorZone());
+          } else
+            t1 = false;
+          if (t1) {
+            t1 = _box_1.source;
+            asyncError = t2._as(t1._resultOrListeners);
+            t1._zone.handleUncaughtError$2(asyncError.error, asyncError.stackTrace);
+            return;
+          }
+          oldZone = $.Zone__current;
+          if (oldZone !== zone)
+            $.Zone__current = zone;
+          else
+            oldZone = null;
+          t1 = _box_0.listener.state;
+          if ((t1 & 15) === 8)
+            new A._Future__propagateToListeners_handleWhenCompleteCallback(_box_0, _box_1, hasError).call$0();
+          else if (t6) {
+            if ((t1 & 1) !== 0)
+              new A._Future__propagateToListeners_handleValueCallback(_box_0, sourceResult).call$0();
+          } else if ((t1 & 2) !== 0)
+            new A._Future__propagateToListeners_handleError(_box_1, _box_0).call$0();
+          if (oldZone != null)
+            $.Zone__current = oldZone;
+          t1 = _box_0.listenerValueOrError;
+          if (t1 instanceof A._Future) {
+            t5 = _box_0.listener.$ti;
+            t5 = t5._eval$1("Future<2>")._is(t1) || !t5._rest[1]._is(t1);
+          } else
+            t5 = false;
+          if (t5) {
+            t4._as(t1);
+            result = _box_0.listener.result;
+            if ((t1._async$_state & 24) !== 0) {
+              current = t3._as(result._resultOrListeners);
+              result._resultOrListeners = null;
+              listeners = result._reverseListeners$1(current);
+              result._async$_state = t1._async$_state & 30 | result._async$_state & 1;
+              result._resultOrListeners = t1._resultOrListeners;
+              _box_1.source = t1;
+              continue;
+            } else
+              A._Future__chainCoreFutureSync(t1, result);
+            return;
+          }
+        }
+        result = _box_0.listener.result;
+        current = t3._as(result._resultOrListeners);
+        result._resultOrListeners = null;
+        listeners = result._reverseListeners$1(current);
+        t1 = _box_0.listenerHasError;
+        t5 = _box_0.listenerValueOrError;
+        if (!t1) {
+          result.$ti._precomputed1._as(t5);
+          result._async$_state = 8;
+          result._resultOrListeners = t5;
+        } else {
+          t2._as(t5);
+          result._async$_state = result._async$_state & 1 | 16;
+          result._resultOrListeners = t5;
+        }
+        _box_1.source = result;
+        t1 = result;
+      }
+    },
+    _registerErrorHandler(errorHandler, zone) {
+      if (type$.dynamic_Function_Object_StackTrace._is(errorHandler))
+        return zone.registerBinaryCallback$3$1(errorHandler, type$.dynamic, type$.Object, type$.StackTrace);
+      if (type$.dynamic_Function_Object._is(errorHandler))
+        return zone.registerUnaryCallback$2$1(errorHandler, type$.dynamic, type$.Object);
+      throw A.wrapException(A.ArgumentError$value(errorHandler, "onError", string$.Error_));
+    },
+    _microtaskLoop() {
+      var entry, next;
+      for (entry = $._nextCallback; entry != null; entry = $._nextCallback) {
+        $._lastPriorityCallback = null;
+        next = entry.next;
+        $._nextCallback = next;
+        if (next == null)
+          $._lastCallback = null;
+        entry.callback.call$0();
+      }
+    },
+    _startMicrotaskLoop() {
+      $._isInCallbackLoop = true;
+      try {
+        A._microtaskLoop();
+      } finally {
+        $._lastPriorityCallback = null;
+        $._isInCallbackLoop = false;
+        if ($._nextCallback != null)
+          $.$get$_AsyncRun__scheduleImmediateClosure().call$1(A.async___startMicrotaskLoop$closure());
+      }
+    },
+    _scheduleAsyncCallback(callback) {
+      var newEntry = new A._AsyncCallbackEntry(callback),
+        lastCallback = $._lastCallback;
+      if (lastCallback == null) {
+        $._nextCallback = $._lastCallback = newEntry;
+        if (!$._isInCallbackLoop)
+          $.$get$_AsyncRun__scheduleImmediateClosure().call$1(A.async___startMicrotaskLoop$closure());
+      } else
+        $._lastCallback = lastCallback.next = newEntry;
+    },
+    _schedulePriorityAsyncCallback(callback) {
+      var entry, lastPriorityCallback, next,
+        t1 = $._nextCallback;
+      if (t1 == null) {
+        A._scheduleAsyncCallback(callback);
+        $._lastPriorityCallback = $._lastCallback;
+        return;
+      }
+      entry = new A._AsyncCallbackEntry(callback);
+      lastPriorityCallback = $._lastPriorityCallback;
+      if (lastPriorityCallback == null) {
+        entry.next = t1;
+        $._nextCallback = $._lastPriorityCallback = entry;
+      } else {
+        next = lastPriorityCallback.next;
+        entry.next = next;
+        $._lastPriorityCallback = lastPriorityCallback.next = entry;
+        if (next == null)
+          $._lastCallback = entry;
+      }
+    },
+    scheduleMicrotask(callback) {
+      var t1, _null = null,
+        currentZone = $.Zone__current;
+      if (B.C__RootZone === currentZone) {
+        A._rootScheduleMicrotask(_null, _null, B.C__RootZone, callback);
+        return;
+      }
+      if (B.C__RootZone === currentZone.get$_scheduleMicrotask().zone)
+        t1 = B.C__RootZone.get$errorZone() === currentZone.get$errorZone();
+      else
+        t1 = false;
+      if (t1) {
+        A._rootScheduleMicrotask(_null, _null, currentZone, currentZone.registerCallback$1$1(callback, type$.void));
+        return;
+      }
+      t1 = $.Zone__current;
+      t1.scheduleMicrotask$1(t1.bindCallbackGuarded$1(callback));
+    },
+    StreamController_StreamController(onCancel, onListen, sync, $T) {
+      return new A._SyncStreamController(onListen, null, null, onCancel, $T._eval$1("_SyncStreamController<0>"));
+    },
+    _runGuarded(notificationHandler) {
+      var e, s, exception;
+      if (notificationHandler == null)
+        return;
+      try {
+        notificationHandler.call$0();
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        $.Zone__current.handleUncaughtError$2(e, s);
+      }
+    },
+    _BufferingStreamSubscription__registerDataHandler(zone, handleData, $T) {
+      var t1 = handleData == null ? A.async___nullDataHandler$closure() : handleData;
+      return zone.registerUnaryCallback$2$1(t1, type$.void, $T);
+    },
+    _BufferingStreamSubscription__registerErrorHandler(zone, handleError) {
+      if (handleError == null)
+        handleError = A.async___nullErrorHandler$closure();
+      if (type$.void_Function_Object_StackTrace._is(handleError))
+        return zone.registerBinaryCallback$3$1(handleError, type$.dynamic, type$.Object, type$.StackTrace);
+      if (type$.void_Function_Object._is(handleError))
+        return zone.registerUnaryCallback$2$1(handleError, type$.dynamic, type$.Object);
+      throw A.wrapException(A.ArgumentError$(string$.handle, null));
+    },
+    _nullDataHandler(value) {
+    },
+    _nullErrorHandler(error, stackTrace) {
+      type$.Object._as(error);
+      type$.StackTrace._as(stackTrace);
+      $.Zone__current.handleUncaughtError$2(error, stackTrace);
+    },
+    _nullDoneHandler() {
+    },
+    Timer_Timer$periodic(duration, callback) {
+      var boundCallback,
+        t1 = $.Zone__current;
+      if (t1 === B.C__RootZone)
+        return t1.createPeriodicTimer$2(duration, callback);
+      boundCallback = t1.bindUnaryCallbackGuarded$1$1(callback, type$.Timer);
+      return $.Zone__current.createPeriodicTimer$2(duration, boundCallback);
+    },
+    ZoneSpecification_ZoneSpecification$from(other, handleUncaughtError) {
+      var t1 = handleUncaughtError == null ? other.handleUncaughtError : handleUncaughtError;
+      return new A._ZoneSpecification(t1, other.run, other.runUnary, other.runBinary, other.registerCallback, other.registerUnaryCallback, other.registerBinaryCallback, other.errorCallback, other.scheduleMicrotask, other.createTimer, other.createPeriodicTimer, other.print, other.fork);
+    },
+    _rootHandleUncaughtError($self, $parent, zone, error, stackTrace) {
+      A._rootHandleError(error, type$.StackTrace._as(stackTrace));
+    },
+    _rootHandleError(error, stackTrace) {
+      A._schedulePriorityAsyncCallback(new A._rootHandleError_closure(error, stackTrace));
+    },
+    _rootRun($self, $parent, zone, f, $R) {
+      var old, t1;
+      type$.nullable_Zone._as($self);
+      type$.nullable_ZoneDelegate._as($parent);
+      type$.Zone._as(zone);
+      $R._eval$1("0()")._as(f);
+      t1 = $.Zone__current;
+      if (t1 === zone)
+        return f.call$0();
+      $.Zone__current = zone;
+      old = t1;
+      try {
+        t1 = f.call$0();
+        return t1;
+      } finally {
+        $.Zone__current = old;
+      }
+    },
+    _rootRunUnary($self, $parent, zone, f, arg, $R, $T) {
+      var old, t1;
+      type$.nullable_Zone._as($self);
+      type$.nullable_ZoneDelegate._as($parent);
+      type$.Zone._as(zone);
+      $R._eval$1("@<0>")._bind$1($T)._eval$1("1(2)")._as(f);
+      $T._as(arg);
+      t1 = $.Zone__current;
+      if (t1 === zone)
+        return f.call$1(arg);
+      $.Zone__current = zone;
+      old = t1;
+      try {
+        t1 = f.call$1(arg);
+        return t1;
+      } finally {
+        $.Zone__current = old;
+      }
+    },
+    _rootRunBinary($self, $parent, zone, f, arg1, arg2, $R, T1, T2) {
+      var old, t1;
+      type$.nullable_Zone._as($self);
+      type$.nullable_ZoneDelegate._as($parent);
+      type$.Zone._as(zone);
+      $R._eval$1("@<0>")._bind$1(T1)._bind$1(T2)._eval$1("1(2,3)")._as(f);
+      T1._as(arg1);
+      T2._as(arg2);
+      t1 = $.Zone__current;
+      if (t1 === zone)
+        return f.call$2(arg1, arg2);
+      $.Zone__current = zone;
+      old = t1;
+      try {
+        t1 = f.call$2(arg1, arg2);
+        return t1;
+      } finally {
+        $.Zone__current = old;
+      }
+    },
+    _rootRegisterCallback($self, $parent, zone, f, $R) {
+      return $R._eval$1("0()")._as(f);
+    },
+    _rootRegisterUnaryCallback($self, $parent, zone, f, $R, $T) {
+      return $R._eval$1("@<0>")._bind$1($T)._eval$1("1(2)")._as(f);
+    },
+    _rootRegisterBinaryCallback($self, $parent, zone, f, $R, T1, T2) {
+      return $R._eval$1("@<0>")._bind$1(T1)._bind$1(T2)._eval$1("1(2,3)")._as(f);
+    },
+    _rootErrorCallback($self, $parent, zone, error, stackTrace) {
+      type$.nullable_StackTrace._as(stackTrace);
+      return null;
+    },
+    _rootScheduleMicrotask($self, $parent, zone, f) {
+      var t1, t2;
+      type$.void_Function._as(f);
+      if (B.C__RootZone !== zone) {
+        t1 = B.C__RootZone.get$errorZone();
+        t2 = zone.get$errorZone();
+        f = t1 !== t2 ? zone.bindCallbackGuarded$1(f) : zone.bindCallback$1$1(f, type$.void);
+      }
+      A._scheduleAsyncCallback(f);
+    },
+    _rootCreateTimer($self, $parent, zone, duration, callback) {
+      type$.Duration._as(duration);
+      type$.void_Function._as(callback);
+      return A.Timer__createTimer(duration, B.C__RootZone !== zone ? zone.bindCallback$1$1(callback, type$.void) : callback);
+    },
+    _rootCreatePeriodicTimer($self, $parent, zone, duration, callback) {
+      type$.Duration._as(duration);
+      type$.void_Function_Timer._as(callback);
+      return A.Timer__createPeriodicTimer(duration, B.C__RootZone !== zone ? zone.bindUnaryCallback$2$1(callback, type$.void, type$.Timer) : callback);
+    },
+    _rootPrint($self, $parent, zone, line) {
+      A.printString(A.S(A._asString(line)));
+    },
+    _rootFork($self, $parent, zone, specification, zoneValues) {
+      var valueMap, t1, handleUncaughtError;
+      type$.nullable_ZoneSpecification._as(specification);
+      type$.nullable_Map_of_nullable_Object_and_nullable_Object._as(zoneValues);
+      valueMap = zone.get$_async$_map();
+      t1 = new A._CustomZone(zone.get$_run(), zone.get$_runUnary(), zone.get$_runBinary(), zone.get$_registerCallback(), zone.get$_registerUnaryCallback(), zone.get$_registerBinaryCallback(), zone.get$_errorCallback(), zone.get$_scheduleMicrotask(), zone.get$_createTimer(), zone.get$_createPeriodicTimer(), zone.get$_print(), zone.get$_fork(), zone.get$_handleUncaughtError(), zone, valueMap);
+      handleUncaughtError = specification.handleUncaughtError;
+      if (handleUncaughtError != null)
+        t1.set$_handleUncaughtError(new A._ZoneFunction(t1, handleUncaughtError, type$._ZoneFunction_of_void_Function_Zone_ZoneDelegate_Zone_Object_StackTrace));
+      return t1;
+    },
+    runZonedGuarded(body, onError, $R) {
+      var error, stackTrace, parentZone, errorHandler, t1, exception, _null = null, zoneSpecification = null, zoneValues = null;
+      A.checkNotNullable(body, "body", $R._eval$1("0()"));
+      A.checkNotNullable(onError, "onError", type$.void_Function_Object_StackTrace);
+      parentZone = $.Zone__current;
+      errorHandler = new A.runZonedGuarded_closure(parentZone, onError);
+      if (zoneSpecification == null)
+        zoneSpecification = new A._ZoneSpecification(errorHandler, _null, _null, _null, _null, _null, _null, _null, _null, _null, _null, _null, _null);
+      else
+        zoneSpecification = A.ZoneSpecification_ZoneSpecification$from(zoneSpecification, errorHandler);
+      try {
+        t1 = parentZone.fork$2$specification$zoneValues(zoneSpecification, zoneValues).run$1$1(body, $R);
+        return t1;
+      } catch (exception) {
+        error = A.unwrapException(exception);
+        stackTrace = A.getTraceFromException(exception);
+        onError.call$2(error, stackTrace);
+      }
+      return _null;
+    },
+    _AsyncRun__initializeScheduleImmediate_internalCallback: function _AsyncRun__initializeScheduleImmediate_internalCallback(t0) {
+      this._box_0 = t0;
+    },
+    _AsyncRun__initializeScheduleImmediate_closure: function _AsyncRun__initializeScheduleImmediate_closure(t0, t1, t2) {
+      this._box_0 = t0;
+      this.div = t1;
+      this.span = t2;
+    },
+    _AsyncRun__scheduleImmediateJsOverride_internalCallback: function _AsyncRun__scheduleImmediateJsOverride_internalCallback(t0) {
+      this.callback = t0;
+    },
+    _AsyncRun__scheduleImmediateWithSetImmediate_internalCallback: function _AsyncRun__scheduleImmediateWithSetImmediate_internalCallback(t0) {
+      this.callback = t0;
+    },
+    _TimerImpl: function _TimerImpl() {
+      this._tick = 0;
+    },
+    _TimerImpl_internalCallback: function _TimerImpl_internalCallback(t0, t1) {
+      this.$this = t0;
+      this.callback = t1;
+    },
+    _TimerImpl$periodic_closure: function _TimerImpl$periodic_closure(t0, t1, t2, t3) {
+      var _ = this;
+      _.$this = t0;
+      _.milliseconds = t1;
+      _.start = t2;
+      _.callback = t3;
+    },
+    AsyncError: function AsyncError(t0, t1) {
+      this.error = t0;
+      this.stackTrace = t1;
+    },
+    _Completer: function _Completer() {
+    },
+    _AsyncCompleter: function _AsyncCompleter(t0, t1) {
+      this.future = t0;
+      this.$ti = t1;
+    },
+    _SyncCompleter: function _SyncCompleter(t0, t1) {
+      this.future = t0;
+      this.$ti = t1;
+    },
+    _FutureListener: function _FutureListener(t0, t1, t2, t3, t4) {
+      var _ = this;
+      _._nextListener = null;
+      _.result = t0;
+      _.state = t1;
+      _.callback = t2;
+      _.errorCallback = t3;
+      _.$ti = t4;
+    },
+    _Future: function _Future(t0, t1) {
+      var _ = this;
+      _._async$_state = 0;
+      _._zone = t0;
+      _._resultOrListeners = null;
+      _.$ti = t1;
+    },
+    _Future__addListener_closure: function _Future__addListener_closure(t0, t1) {
+      this.$this = t0;
+      this.listener = t1;
+    },
+    _Future__prependListeners_closure: function _Future__prependListeners_closure(t0, t1) {
+      this._box_0 = t0;
+      this.$this = t1;
+    },
+    _Future__chainForeignFuture_closure: function _Future__chainForeignFuture_closure(t0) {
+      this.$this = t0;
+    },
+    _Future__chainForeignFuture_closure0: function _Future__chainForeignFuture_closure0(t0) {
+      this.$this = t0;
+    },
+    _Future__chainForeignFuture_closure1: function _Future__chainForeignFuture_closure1(t0, t1, t2) {
+      this.$this = t0;
+      this.e = t1;
+      this.s = t2;
+    },
+    _Future__chainCoreFutureAsync_closure: function _Future__chainCoreFutureAsync_closure(t0, t1) {
+      this._box_0 = t0;
+      this.target = t1;
+    },
+    _Future__asyncCompleteWithValue_closure: function _Future__asyncCompleteWithValue_closure(t0, t1) {
+      this.$this = t0;
+      this.value = t1;
+    },
+    _Future__asyncCompleteError_closure: function _Future__asyncCompleteError_closure(t0, t1, t2) {
+      this.$this = t0;
+      this.error = t1;
+      this.stackTrace = t2;
+    },
+    _Future__propagateToListeners_handleWhenCompleteCallback: function _Future__propagateToListeners_handleWhenCompleteCallback(t0, t1, t2) {
+      this._box_0 = t0;
+      this._box_1 = t1;
+      this.hasError = t2;
+    },
+    _Future__propagateToListeners_handleWhenCompleteCallback_closure: function _Future__propagateToListeners_handleWhenCompleteCallback_closure(t0) {
+      this.originalSource = t0;
+    },
+    _Future__propagateToListeners_handleValueCallback: function _Future__propagateToListeners_handleValueCallback(t0, t1) {
+      this._box_0 = t0;
+      this.sourceResult = t1;
+    },
+    _Future__propagateToListeners_handleError: function _Future__propagateToListeners_handleError(t0, t1) {
+      this._box_1 = t0;
+      this._box_0 = t1;
+    },
+    _AsyncCallbackEntry: function _AsyncCallbackEntry(t0) {
+      this.callback = t0;
+      this.next = null;
+    },
+    Stream: function Stream() {
+    },
+    Stream_pipe_closure: function Stream_pipe_closure(t0) {
+      this.streamConsumer = t0;
+    },
+    Stream_length_closure: function Stream_length_closure(t0, t1) {
+      this._box_0 = t0;
+      this.$this = t1;
+    },
+    Stream_length_closure0: function Stream_length_closure0(t0, t1) {
+      this._box_0 = t0;
+      this.future = t1;
+    },
+    _StreamController: function _StreamController() {
+    },
+    _StreamController__subscribe_closure: function _StreamController__subscribe_closure(t0) {
+      this.$this = t0;
+    },
+    _StreamController__recordCancel_complete: function _StreamController__recordCancel_complete(t0) {
+      this.$this = t0;
+    },
+    _SyncStreamControllerDispatch: function _SyncStreamControllerDispatch() {
+    },
+    _SyncStreamController: function _SyncStreamController(t0, t1, t2, t3, t4) {
+      var _ = this;
+      _._varData = null;
+      _._async$_state = 0;
+      _._doneFuture = null;
+      _.onListen = t0;
+      _.onPause = t1;
+      _.onResume = t2;
+      _.onCancel = t3;
+      _.$ti = t4;
+    },
+    _ControllerStream: function _ControllerStream(t0, t1) {
+      this._controller = t0;
+      this.$ti = t1;
+    },
+    _ControllerSubscription: function _ControllerSubscription(t0, t1, t2, t3, t4, t5, t6) {
+      var _ = this;
+      _._controller = t0;
+      _._onData = t1;
+      _._onError = t2;
+      _._onDone = t3;
+      _._zone = t4;
+      _._async$_state = t5;
+      _._pending = _._cancelFuture = null;
+      _.$ti = t6;
+    },
+    _StreamSinkWrapper: function _StreamSinkWrapper(t0, t1) {
+      this._target = t0;
+      this.$ti = t1;
+    },
+    _AddStreamState_cancel_closure: function _AddStreamState_cancel_closure(t0) {
+      this.$this = t0;
+    },
+    _BufferingStreamSubscription: function _BufferingStreamSubscription() {
+    },
+    _BufferingStreamSubscription__sendError_sendError: function _BufferingStreamSubscription__sendError_sendError(t0, t1, t2) {
+      this.$this = t0;
+      this.error = t1;
+      this.stackTrace = t2;
+    },
+    _BufferingStreamSubscription__sendDone_sendDone: function _BufferingStreamSubscription__sendDone_sendDone(t0) {
+      this.$this = t0;
+    },
+    _StreamImpl: function _StreamImpl() {
+    },
+    _DelayedEvent: function _DelayedEvent() {
+    },
+    _DelayedData: function _DelayedData(t0, t1) {
+      this.value = t0;
+      this.next = null;
+      this.$ti = t1;
+    },
+    _DelayedError: function _DelayedError(t0, t1) {
+      this.error = t0;
+      this.stackTrace = t1;
+      this.next = null;
+    },
+    _DelayedDone: function _DelayedDone() {
+    },
+    _PendingEvents: function _PendingEvents(t0) {
+      var _ = this;
+      _._async$_state = 0;
+      _.lastPendingEvent = _.firstPendingEvent = null;
+      _.$ti = t0;
+    },
+    _PendingEvents_schedule_closure: function _PendingEvents_schedule_closure(t0, t1) {
+      this.$this = t0;
+      this.dispatch = t1;
+    },
+    _DoneStreamSubscription: function _DoneStreamSubscription(t0, t1) {
+      var _ = this;
+      _._async$_state = 1;
+      _._zone = t0;
+      _._onDone = null;
+      _.$ti = t1;
+    },
+    _EmptyStream: function _EmptyStream(t0) {
+      this.$ti = t0;
+    },
+    _ZoneFunction: function _ZoneFunction(t0, t1, t2) {
+      this.zone = t0;
+      this.$function = t1;
+      this.$ti = t2;
+    },
+    _ZoneSpecification: function _ZoneSpecification(t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12) {
+      var _ = this;
+      _.handleUncaughtError = t0;
+      _.run = t1;
+      _.runUnary = t2;
+      _.runBinary = t3;
+      _.registerCallback = t4;
+      _.registerUnaryCallback = t5;
+      _.registerBinaryCallback = t6;
+      _.errorCallback = t7;
+      _.scheduleMicrotask = t8;
+      _.createTimer = t9;
+      _.createPeriodicTimer = t10;
+      _.print = t11;
+      _.fork = t12;
+    },
+    _ZoneDelegate: function _ZoneDelegate(t0) {
+      this._delegationTarget = t0;
+    },
+    _Zone: function _Zone() {
+    },
+    _CustomZone: function _CustomZone(t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14) {
+      var _ = this;
+      _._run = t0;
+      _._runUnary = t1;
+      _._runBinary = t2;
+      _._registerCallback = t3;
+      _._registerUnaryCallback = t4;
+      _._registerBinaryCallback = t5;
+      _._errorCallback = t6;
+      _._scheduleMicrotask = t7;
+      _._createTimer = t8;
+      _._createPeriodicTimer = t9;
+      _._print = t10;
+      _._fork = t11;
+      _._handleUncaughtError = t12;
+      _._delegateCache = null;
+      _.parent = t13;
+      _._async$_map = t14;
+    },
+    _CustomZone_bindCallback_closure: function _CustomZone_bindCallback_closure(t0, t1, t2) {
+      this.$this = t0;
+      this.registered = t1;
+      this.R = t2;
+    },
+    _CustomZone_bindUnaryCallback_closure: function _CustomZone_bindUnaryCallback_closure(t0, t1, t2, t3) {
+      var _ = this;
+      _.$this = t0;
+      _.registered = t1;
+      _.T = t2;
+      _.R = t3;
+    },
+    _CustomZone_bindCallbackGuarded_closure: function _CustomZone_bindCallbackGuarded_closure(t0, t1) {
+      this.$this = t0;
+      this.registered = t1;
+    },
+    _CustomZone_bindUnaryCallbackGuarded_closure: function _CustomZone_bindUnaryCallbackGuarded_closure(t0, t1, t2) {
+      this.$this = t0;
+      this.registered = t1;
+      this.T = t2;
+    },
+    _rootHandleError_closure: function _rootHandleError_closure(t0, t1) {
+      this.error = t0;
+      this.stackTrace = t1;
+    },
+    _RootZone: function _RootZone() {
+    },
+    _RootZone_bindCallback_closure: function _RootZone_bindCallback_closure(t0, t1, t2) {
+      this.$this = t0;
+      this.f = t1;
+      this.R = t2;
+    },
+    _RootZone_bindUnaryCallback_closure: function _RootZone_bindUnaryCallback_closure(t0, t1, t2, t3) {
+      var _ = this;
+      _.$this = t0;
+      _.f = t1;
+      _.T = t2;
+      _.R = t3;
+    },
+    _RootZone_bindCallbackGuarded_closure: function _RootZone_bindCallbackGuarded_closure(t0, t1) {
+      this.$this = t0;
+      this.f = t1;
+    },
+    _RootZone_bindUnaryCallbackGuarded_closure: function _RootZone_bindUnaryCallbackGuarded_closure(t0, t1, t2) {
+      this.$this = t0;
+      this.f = t1;
+      this.T = t2;
+    },
+    runZonedGuarded_closure: function runZonedGuarded_closure(t0, t1) {
+      this.parentZone = t0;
+      this.onError = t1;
+    },
+    HashMap_HashMap($K, $V) {
+      return new A._HashMap($K._eval$1("@<0>")._bind$1($V)._eval$1("_HashMap<1,2>"));
+    },
+    _HashMap__getTableEntry(table, key) {
+      var entry = table[key];
+      return entry === table ? null : entry;
+    },
+    _HashMap__setTableEntry(table, key, value) {
+      if (value == null)
+        table[key] = table;
+      else
+        table[key] = value;
+    },
+    _HashMap__newHashTable() {
+      var table = Object.create(null);
+      A._HashMap__setTableEntry(table, "<non-identifier-key>", table);
+      delete table["<non-identifier-key>"];
+      return table;
+    },
+    LinkedHashMap_LinkedHashMap$_literal(keyValuePairs, $K, $V) {
+      return $K._eval$1("@<0>")._bind$1($V)._eval$1("LinkedHashMap<1,2>")._as(A.fillLiteralMap(keyValuePairs, new A.JsLinkedHashMap($K._eval$1("@<0>")._bind$1($V)._eval$1("JsLinkedHashMap<1,2>"))));
+    },
+    LinkedHashMap_LinkedHashMap$_empty($K, $V) {
+      return new A.JsLinkedHashMap($K._eval$1("@<0>")._bind$1($V)._eval$1("JsLinkedHashMap<1,2>"));
+    },
+    LinkedHashSet_LinkedHashSet$_empty($E) {
+      return new A._LinkedHashSet($E._eval$1("_LinkedHashSet<0>"));
+    },
+    _LinkedHashSet__newHashTable() {
+      var table = Object.create(null);
+      table["<non-identifier-key>"] = table;
+      delete table["<non-identifier-key>"];
+      return table;
+    },
+    _LinkedHashSetIterator$(_set, _modifications, $E) {
+      var t1 = new A._LinkedHashSetIterator(_set, _modifications, $E._eval$1("_LinkedHashSetIterator<0>"));
+      t1._collection$_cell = _set._collection$_first;
+      return t1;
+    },
+    MapBase_mapToString(m) {
+      var result, t1 = {};
+      if (A.isToStringVisiting(m))
+        return "{...}";
+      result = new A.StringBuffer("");
+      try {
+        B.JSArray_methods.add$1($.toStringVisiting, m);
+        result._contents += "{";
+        t1.first = true;
+        J.forEach$1$x(m, new A.MapBase_mapToString_closure(t1, result));
+        result._contents += "}";
+      } finally {
+        if (0 >= $.toStringVisiting.length)
+          return A.ioore($.toStringVisiting, -1);
+        $.toStringVisiting.pop();
+      }
+      t1 = result._contents;
+      return t1.charCodeAt(0) == 0 ? t1 : t1;
+    },
+    _HashMap: function _HashMap(t0) {
+      var _ = this;
+      _._collection$_length = 0;
+      _._keys = _._collection$_rest = _._collection$_nums = _._collection$_strings = null;
+      _.$ti = t0;
+    },
+    _IdentityHashMap: function _IdentityHashMap(t0) {
+      var _ = this;
+      _._collection$_length = 0;
+      _._keys = _._collection$_rest = _._collection$_nums = _._collection$_strings = null;
+      _.$ti = t0;
+    },
+    _HashMapKeyIterable: function _HashMapKeyIterable(t0, t1) {
+      this._collection$_map = t0;
+      this.$ti = t1;
+    },
+    _HashMapKeyIterator: function _HashMapKeyIterator(t0, t1, t2) {
+      var _ = this;
+      _._collection$_map = t0;
+      _._keys = t1;
+      _._offset = 0;
+      _._collection$_current = null;
+      _.$ti = t2;
+    },
+    _LinkedHashSet: function _LinkedHashSet(t0) {
+      var _ = this;
+      _._collection$_length = 0;
+      _._collection$_last = _._collection$_first = _._collection$_rest = _._collection$_nums = _._collection$_strings = null;
+      _._collection$_modifications = 0;
+      _.$ti = t0;
+    },
+    _LinkedHashSetCell: function _LinkedHashSetCell(t0) {
+      this._element = t0;
+      this._collection$_previous = this._collection$_next = null;
+    },
+    _LinkedHashSetIterator: function _LinkedHashSetIterator(t0, t1, t2) {
+      var _ = this;
+      _._set = t0;
+      _._collection$_modifications = t1;
+      _._collection$_current = _._collection$_cell = null;
+      _.$ti = t2;
+    },
+    ListBase: function ListBase() {
+    },
+    MapBase: function MapBase() {
+    },
+    MapBase_mapToString_closure: function MapBase_mapToString_closure(t0, t1) {
+      this._box_0 = t0;
+      this.result = t1;
+    },
+    _UnmodifiableMapMixin: function _UnmodifiableMapMixin() {
+    },
+    MapView: function MapView() {
+    },
+    UnmodifiableMapView: function UnmodifiableMapView(t0, t1) {
+      this._collection$_map = t0;
+      this.$ti = t1;
+    },
+    SetBase: function SetBase() {
+    },
+    _SetBase: function _SetBase() {
+    },
+    _UnmodifiableMapView_MapView__UnmodifiableMapMixin: function _UnmodifiableMapView_MapView__UnmodifiableMapMixin() {
+    },
+    _parseJson(source, reviver) {
+      var e, exception, t1, parsed = null;
+      try {
+        parsed = JSON.parse(source);
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        t1 = A.FormatException$(String(e), null, null);
+        throw A.wrapException(t1);
+      }
+      t1 = A._convertJsonToDartLazy(parsed);
+      return t1;
+    },
+    _convertJsonToDartLazy(object) {
+      var i;
+      if (object == null)
+        return null;
+      if (typeof object != "object")
+        return object;
+      if (Object.getPrototypeOf(object) !== Array.prototype)
+        return new A._JsonMap(object, Object.create(null));
+      for (i = 0; i < object.length; ++i)
+        object[i] = A._convertJsonToDartLazy(object[i]);
+      return object;
+    },
+    Utf8Decoder__convertIntercepted(allowMalformed, codeUnits, start, end) {
+      var casted, result;
+      if (codeUnits instanceof Uint8Array) {
+        casted = codeUnits;
+        end = casted.length;
+        if (end - start < 15)
+          return null;
+        result = A.Utf8Decoder__convertInterceptedUint8List(allowMalformed, casted, start, end);
+        if (result != null && allowMalformed)
+          if (result.indexOf("\ufffd") >= 0)
+            return null;
+        return result;
+      }
+      return null;
+    },
+    Utf8Decoder__convertInterceptedUint8List(allowMalformed, codeUnits, start, end) {
+      var decoder = allowMalformed ? $.$get$Utf8Decoder__decoderNonfatal() : $.$get$Utf8Decoder__decoder();
+      if (decoder == null)
+        return null;
+      if (0 === start && end === codeUnits.length)
+        return A.Utf8Decoder__useTextDecoder(decoder, codeUnits);
+      return A.Utf8Decoder__useTextDecoder(decoder, codeUnits.subarray(start, A.RangeError_checkValidRange(start, end, codeUnits.length)));
+    },
+    Utf8Decoder__useTextDecoder(decoder, codeUnits) {
+      var t1, exception;
+      try {
+        t1 = decoder.decode(codeUnits);
+        return t1;
+      } catch (exception) {
+      }
+      return null;
+    },
+    Base64Codec__checkPadding(source, sourceIndex, sourceEnd, firstPadding, paddingCount, $length) {
+      if (B.JSInt_methods.$mod($length, 4) !== 0)
+        throw A.wrapException(A.FormatException$("Invalid base64 padding, padded length must be multiple of four, is " + $length, source, sourceEnd));
+      if (firstPadding + paddingCount !== $length)
+        throw A.wrapException(A.FormatException$("Invalid base64 padding, '=' not at the end", source, sourceIndex));
+      if (paddingCount > 2)
+        throw A.wrapException(A.FormatException$("Invalid base64 padding, more than two '=' characters", source, sourceIndex));
+    },
+    JsonUnsupportedObjectError$(unsupportedObject, cause, partialResult) {
+      return new A.JsonUnsupportedObjectError(unsupportedObject, cause);
+    },
+    _defaultToEncodable(object) {
+      return object.toJson$0();
+    },
+    _JsonStringStringifier$(_sink, _toEncodable) {
+      return new A._JsonStringStringifier(_sink, [], A.convert___defaultToEncodable$closure());
+    },
+    _JsonStringStringifier_stringify(object, toEncodable, indent) {
+      var t1,
+        output = new A.StringBuffer("");
+      A._JsonStringStringifier_printOn(object, output, toEncodable, indent);
+      t1 = output._contents;
+      return t1.charCodeAt(0) == 0 ? t1 : t1;
+    },
+    _JsonStringStringifier_printOn(object, output, toEncodable, indent) {
+      var stringifier = A._JsonStringStringifier$(output, toEncodable);
+      stringifier.writeObject$1(object);
+    },
+    _Utf8Decoder_errorDescription(state) {
+      switch (state) {
+        case 65:
+          return "Missing extension byte";
+        case 67:
+          return "Unexpected extension byte";
+        case 69:
+          return "Invalid UTF-8 byte";
+        case 71:
+          return "Overlong encoding";
+        case 73:
+          return "Out of unicode range";
+        case 75:
+          return "Encoded surrogate";
+        case 77:
+          return "Unfinished UTF-8 octet sequence";
+        default:
+          return "";
+      }
+    },
+    _Utf8Decoder__makeUint8List(codeUnits, start, end) {
+      var t1, i, b,
+        $length = end - start,
+        bytes = new Uint8Array($length);
+      for (t1 = J.getInterceptor$asx(codeUnits), i = 0; i < $length; ++i) {
+        b = t1.$index(codeUnits, start + i);
+        if ((b & 4294967040) >>> 0 !== 0)
+          b = 255;
+        if (!(i < $length))
+          return A.ioore(bytes, i);
+        bytes[i] = b;
+      }
+      return bytes;
+    },
+    _JsonMap: function _JsonMap(t0, t1) {
+      this._original = t0;
+      this._processed = t1;
+      this._data = null;
+    },
+    _JsonMapKeyIterable: function _JsonMapKeyIterable(t0) {
+      this._convert$_parent = t0;
+    },
+    Utf8Decoder__decoder_closure: function Utf8Decoder__decoder_closure() {
+    },
+    Utf8Decoder__decoderNonfatal_closure: function Utf8Decoder__decoderNonfatal_closure() {
+    },
+    AsciiCodec: function AsciiCodec() {
+    },
+    _UnicodeSubsetEncoder: function _UnicodeSubsetEncoder() {
+    },
+    AsciiEncoder: function AsciiEncoder(t0) {
+      this._subsetMask = t0;
+    },
+    Base64Codec: function Base64Codec() {
+    },
+    Base64Encoder: function Base64Encoder() {
+    },
+    Codec: function Codec() {
+    },
+    _FusedCodec: function _FusedCodec(t0, t1, t2) {
+      this._convert$_first = t0;
+      this._second = t1;
+      this.$ti = t2;
+    },
+    Converter: function Converter() {
+    },
+    Encoding: function Encoding() {
+    },
+    JsonUnsupportedObjectError: function JsonUnsupportedObjectError(t0, t1) {
+      this.unsupportedObject = t0;
+      this.cause = t1;
+    },
+    JsonCyclicError: function JsonCyclicError(t0, t1) {
+      this.unsupportedObject = t0;
+      this.cause = t1;
+    },
+    JsonCodec: function JsonCodec() {
+    },
+    JsonEncoder: function JsonEncoder(t0) {
+      this._toEncodable = t0;
+    },
+    JsonDecoder: function JsonDecoder(t0) {
+      this._reviver = t0;
+    },
+    _JsonStringifier: function _JsonStringifier() {
+    },
+    _JsonStringifier_writeMap_closure: function _JsonStringifier_writeMap_closure(t0, t1) {
+      this._box_0 = t0;
+      this.keyValueList = t1;
+    },
+    _JsonStringStringifier: function _JsonStringStringifier(t0, t1, t2) {
+      this._sink = t0;
+      this._seen = t1;
+      this._toEncodable = t2;
+    },
+    Utf8Codec: function Utf8Codec() {
+    },
+    Utf8Encoder: function Utf8Encoder() {
+    },
+    _Utf8Encoder: function _Utf8Encoder(t0) {
+      this._bufferIndex = this._carry = 0;
+      this._buffer = t0;
+    },
+    Utf8Decoder: function Utf8Decoder(t0) {
+      this._allowMalformed = t0;
+    },
+    _Utf8Decoder: function _Utf8Decoder(t0) {
+      this.allowMalformed = t0;
+      this._state = 16;
+      this._charOrIndex = 0;
+    },
+    int_parse(source, radix) {
+      var value = A.Primitives_parseInt(source, radix);
+      if (value != null)
+        return value;
+      throw A.wrapException(A.FormatException$(source, null, null));
+    },
+    Error__throw(error, stackTrace) {
+      error = A.wrapException(error);
+      if (error == null)
+        error = type$.Object._as(error);
+      error.stack = stackTrace.toString$0(0);
+      throw error;
+      throw A.wrapException("unreachable");
+    },
+    List_List$filled($length, fill, growable, $E) {
+      var i,
+        result = growable ? J.JSArray_JSArray$growable($length, $E) : J.JSArray_JSArray$fixed($length, $E);
+      if ($length !== 0 && fill != null)
+        for (i = 0; i < result.length; ++i)
+          result[i] = fill;
+      return result;
+    },
+    List_List$from(elements, growable, $E) {
+      var t1,
+        list = A._setArrayType([], $E._eval$1("JSArray<0>"));
+      for (t1 = J.get$iterator$ax(elements); t1.moveNext$0();)
+        B.JSArray_methods.add$1(list, $E._as(t1.get$current(t1)));
+      if (growable)
+        return list;
+      return J.JSArray_markFixedList(list, $E);
+    },
+    List_List$of(elements, growable, $E) {
+      var t1;
+      if (growable)
+        return A.List_List$_of(elements, $E);
+      t1 = J.JSArray_markFixedList(A.List_List$_of(elements, $E), $E);
+      return t1;
+    },
+    List_List$_of(elements, $E) {
+      var list, t1;
+      if (Array.isArray(elements))
+        return A._setArrayType(elements.slice(0), $E._eval$1("JSArray<0>"));
+      list = A._setArrayType([], $E._eval$1("JSArray<0>"));
+      for (t1 = J.get$iterator$ax(elements); t1.moveNext$0();)
+        B.JSArray_methods.add$1(list, t1.get$current(t1));
+      return list;
+    },
+    List_List$unmodifiable(elements, $E) {
+      return J.JSArray_markUnmodifiableList(A.List_List$from(elements, false, $E));
+    },
+    String_String$fromCharCodes(charCodes, start, end) {
+      var array, len;
+      if (Array.isArray(charCodes)) {
+        array = charCodes;
+        len = array.length;
+        end = A.RangeError_checkValidRange(start, end, len);
+        return A.Primitives_stringFromCharCodes(start > 0 || end < len ? array.slice(start, end) : array);
+      }
+      if (type$.NativeUint8List._is(charCodes))
+        return A.Primitives_stringFromNativeUint8List(charCodes, start, A.RangeError_checkValidRange(start, end, charCodes.length));
+      return A.String__stringFromIterable(charCodes, start, end);
+    },
+    String_String$fromCharCode(charCode) {
+      return A.Primitives_stringFromCharCode(charCode);
+    },
+    String__stringFromIterable(charCodes, start, end) {
+      var t1, it, i, list, _null = null;
+      if (start < 0)
+        throw A.wrapException(A.RangeError$range(start, 0, J.get$length$asx(charCodes), _null, _null));
+      t1 = end == null;
+      if (!t1 && end < start)
+        throw A.wrapException(A.RangeError$range(end, start, J.get$length$asx(charCodes), _null, _null));
+      it = J.get$iterator$ax(charCodes);
+      for (i = 0; i < start; ++i)
+        if (!it.moveNext$0())
+          throw A.wrapException(A.RangeError$range(start, 0, i, _null, _null));
+      list = [];
+      if (t1)
+        for (; it.moveNext$0();)
+          list.push(it.get$current(it));
+      else
+        for (i = start; i < end; ++i) {
+          if (!it.moveNext$0())
+            throw A.wrapException(A.RangeError$range(end, start, i, _null, _null));
+          list.push(it.get$current(it));
+        }
+      return A.Primitives_stringFromCharCodes(list);
+    },
+    RegExp_RegExp(source, multiLine) {
+      return new A.JSSyntaxRegExp(source, A.JSSyntaxRegExp_makeNative(source, multiLine, true, false, false, false));
+    },
+    StringBuffer__writeAll(string, objects, separator) {
+      var iterator = J.get$iterator$ax(objects);
+      if (!iterator.moveNext$0())
+        return string;
+      if (separator.length === 0) {
+        do
+          string += A.S(iterator.get$current(iterator));
+        while (iterator.moveNext$0());
+      } else {
+        string += A.S(iterator.get$current(iterator));
+        for (; iterator.moveNext$0();)
+          string = string + separator + A.S(iterator.get$current(iterator));
+      }
+      return string;
+    },
+    NoSuchMethodError_NoSuchMethodError$withInvocation(receiver, invocation) {
+      return new A.NoSuchMethodError(receiver, invocation.get$memberName(), invocation.get$positionalArguments(), invocation.get$namedArguments());
+    },
+    Uri_base() {
+      var cachedUri, uri,
+        current = A.Primitives_currentUri();
+      if (current == null)
+        throw A.wrapException(A.UnsupportedError$("'Uri.base' is not supported"));
+      cachedUri = $.Uri__cachedBaseUri;
+      if (cachedUri != null && current === $.Uri__cachedBaseString)
+        return cachedUri;
+      uri = A.Uri_parse(current);
+      $.Uri__cachedBaseUri = uri;
+      $.Uri__cachedBaseString = current;
+      return uri;
+    },
+    _Uri__uriEncode(canonicalTable, text, encoding, spaceToPlus) {
+      var t1, bytes, i, t2, byte, t3,
+        _s16_ = "0123456789ABCDEF";
+      if (encoding === B.C_Utf8Codec) {
+        t1 = $.$get$_Uri__needsNoEncoding();
+        t1 = t1._nativeRegExp.test(text);
+      } else
+        t1 = false;
+      if (t1)
+        return text;
+      bytes = B.C_Utf8Encoder.convert$1(text);
+      for (t1 = bytes.length, i = 0, t2 = ""; i < t1; ++i) {
+        byte = bytes[i];
+        if (byte < 128) {
+          t3 = byte >>> 4;
+          if (!(t3 < 8))
+            return A.ioore(canonicalTable, t3);
+          t3 = (canonicalTable[t3] & 1 << (byte & 15)) !== 0;
+        } else
+          t3 = false;
+        if (t3)
+          t2 += A.Primitives_stringFromCharCode(byte);
+        else
+          t2 = spaceToPlus && byte === 32 ? t2 + "+" : t2 + "%" + _s16_[byte >>> 4 & 15] + _s16_[byte & 15];
+      }
+      return t2.charCodeAt(0) == 0 ? t2 : t2;
+    },
+    DateTime__fourDigits(n) {
+      var absN = Math.abs(n),
+        sign = n < 0 ? "-" : "";
+      if (absN >= 1000)
+        return "" + n;
+      if (absN >= 100)
+        return sign + "0" + absN;
+      if (absN >= 10)
+        return sign + "00" + absN;
+      return sign + "000" + absN;
+    },
+    DateTime__threeDigits(n) {
+      if (n >= 100)
+        return "" + n;
+      if (n >= 10)
+        return "0" + n;
+      return "00" + n;
+    },
+    DateTime__twoDigits(n) {
+      if (n >= 10)
+        return "" + n;
+      return "0" + n;
+    },
+    Error_safeToString(object) {
+      if (typeof object == "number" || A._isBool(object) || object == null)
+        return J.toString$0$(object);
+      if (typeof object == "string")
+        return JSON.stringify(object);
+      return A.Primitives_safeToString(object);
+    },
+    Error_throwWithStackTrace(error, stackTrace) {
+      A.checkNotNullable(error, "error", type$.Object);
+      A.checkNotNullable(stackTrace, "stackTrace", type$.StackTrace);
+      A.Error__throw(error, stackTrace);
+    },
+    AssertionError$(message) {
+      return new A.AssertionError(message);
+    },
+    ArgumentError$(message, $name) {
+      return new A.ArgumentError(false, null, $name, message);
+    },
+    ArgumentError$value(value, $name, message) {
+      return new A.ArgumentError(true, value, $name, message);
+    },
+    ArgumentError_checkNotNull(argument, $name, $T) {
+      return argument;
+    },
+    RangeError$value(value, $name) {
+      return new A.RangeError(null, null, true, value, $name, "Value not in range");
+    },
+    RangeError$range(invalidValue, minValue, maxValue, $name, message) {
+      return new A.RangeError(minValue, maxValue, true, invalidValue, $name, "Invalid value");
+    },
+    RangeError_checkValueInInterval(value, minValue, maxValue, $name) {
+      if (value < minValue || value > maxValue)
+        throw A.wrapException(A.RangeError$range(value, minValue, maxValue, $name, null));
+      return value;
+    },
+    RangeError_checkValidRange(start, end, $length) {
+      if (0 > start || start > $length)
+        throw A.wrapException(A.RangeError$range(start, 0, $length, "start", null));
+      if (end != null) {
+        if (start > end || end > $length)
+          throw A.wrapException(A.RangeError$range(end, start, $length, "end", null));
+        return end;
+      }
+      return $length;
+    },
+    RangeError_checkNotNegative(value, $name) {
+      if (value < 0)
+        throw A.wrapException(A.RangeError$range(value, 0, null, $name, null));
+      return value;
+    },
+    IndexError$withLength(invalidValue, $length, indexable, $name) {
+      return new A.IndexError($length, true, invalidValue, $name, "Index out of range");
+    },
+    UnsupportedError$(message) {
+      return new A.UnsupportedError(message);
+    },
+    UnimplementedError$(message) {
+      return new A.UnimplementedError(message);
+    },
+    StateError$(message) {
+      return new A.StateError(message);
+    },
+    ConcurrentModificationError$(modifiedObject) {
+      return new A.ConcurrentModificationError(modifiedObject);
+    },
+    FormatException$(message, source, offset) {
+      return new A.FormatException(message, source, offset);
+    },
+    Iterable_iterableToShortString(iterable, leftDelimiter, rightDelimiter) {
+      var parts, t1;
+      if (A.isToStringVisiting(iterable)) {
+        if (leftDelimiter === "(" && rightDelimiter === ")")
+          return "(...)";
+        return leftDelimiter + "..." + rightDelimiter;
+      }
+      parts = A._setArrayType([], type$.JSArray_String);
+      B.JSArray_methods.add$1($.toStringVisiting, iterable);
+      try {
+        A._iterablePartsToStrings(iterable, parts);
+      } finally {
+        if (0 >= $.toStringVisiting.length)
+          return A.ioore($.toStringVisiting, -1);
+        $.toStringVisiting.pop();
+      }
+      t1 = A.StringBuffer__writeAll(leftDelimiter, type$.Iterable_dynamic._as(parts), ", ") + rightDelimiter;
+      return t1.charCodeAt(0) == 0 ? t1 : t1;
+    },
+    Iterable_iterableToFullString(iterable, leftDelimiter, rightDelimiter) {
+      var buffer, t1;
+      if (A.isToStringVisiting(iterable))
+        return leftDelimiter + "..." + rightDelimiter;
+      buffer = new A.StringBuffer(leftDelimiter);
+      B.JSArray_methods.add$1($.toStringVisiting, iterable);
+      try {
+        t1 = buffer;
+        t1._contents = A.StringBuffer__writeAll(t1._contents, iterable, ", ");
+      } finally {
+        if (0 >= $.toStringVisiting.length)
+          return A.ioore($.toStringVisiting, -1);
+        $.toStringVisiting.pop();
+      }
+      buffer._contents += rightDelimiter;
+      t1 = buffer._contents;
+      return t1.charCodeAt(0) == 0 ? t1 : t1;
+    },
+    _iterablePartsToStrings(iterable, parts) {
+      var next, ultimateString, penultimateString, penultimate, ultimate, ultimate0, elision,
+        it = iterable.get$iterator(iterable),
+        $length = 0, count = 0;
+      while (true) {
+        if (!($length < 80 || count < 3))
+          break;
+        if (!it.moveNext$0())
+          return;
+        next = A.S(it.get$current(it));
+        B.JSArray_methods.add$1(parts, next);
+        $length += next.length + 2;
+        ++count;
+      }
+      if (!it.moveNext$0()) {
+        if (count <= 5)
+          return;
+        if (0 >= parts.length)
+          return A.ioore(parts, -1);
+        ultimateString = parts.pop();
+        if (0 >= parts.length)
+          return A.ioore(parts, -1);
+        penultimateString = parts.pop();
+      } else {
+        penultimate = it.get$current(it);
+        ++count;
+        if (!it.moveNext$0()) {
+          if (count <= 4) {
+            B.JSArray_methods.add$1(parts, A.S(penultimate));
+            return;
+          }
+          ultimateString = A.S(penultimate);
+          if (0 >= parts.length)
+            return A.ioore(parts, -1);
+          penultimateString = parts.pop();
+          $length += ultimateString.length + 2;
+        } else {
+          ultimate = it.get$current(it);
+          ++count;
+          for (; it.moveNext$0(); penultimate = ultimate, ultimate = ultimate0) {
+            ultimate0 = it.get$current(it);
+            ++count;
+            if (count > 100) {
+              while (true) {
+                if (!($length > 75 && count > 3))
+                  break;
+                if (0 >= parts.length)
+                  return A.ioore(parts, -1);
+                $length -= parts.pop().length + 2;
+                --count;
+              }
+              B.JSArray_methods.add$1(parts, "...");
+              return;
+            }
+          }
+          penultimateString = A.S(penultimate);
+          ultimateString = A.S(ultimate);
+          $length += ultimateString.length + penultimateString.length + 4;
+        }
+      }
+      if (count > parts.length + 2) {
+        $length += 5;
+        elision = "...";
+      } else
+        elision = null;
+      while (true) {
+        if (!($length > 80 && parts.length > 3))
+          break;
+        if (0 >= parts.length)
+          return A.ioore(parts, -1);
+        $length -= parts.pop().length + 2;
+        if (elision == null) {
+          $length += 5;
+          elision = "...";
+        }
+      }
+      if (elision != null)
+        B.JSArray_methods.add$1(parts, elision);
+      B.JSArray_methods.add$1(parts, penultimateString);
+      B.JSArray_methods.add$1(parts, ultimateString);
+    },
+    Object_hash(object1, object2, object3, object4) {
+      var t1;
+      if (B.C_SentinelValue === object3) {
+        t1 = J.get$hashCode$(object1);
+        object2 = J.get$hashCode$(object2);
+        return A.SystemHash_finish(A.SystemHash_combine(A.SystemHash_combine($.$get$_hashSeed(), t1), object2));
+      }
+      if (B.C_SentinelValue === object4) {
+        t1 = J.get$hashCode$(object1);
+        object2 = J.get$hashCode$(object2);
+        object3 = J.get$hashCode$(object3);
+        return A.SystemHash_finish(A.SystemHash_combine(A.SystemHash_combine(A.SystemHash_combine($.$get$_hashSeed(), t1), object2), object3));
+      }
+      t1 = J.get$hashCode$(object1);
+      object2 = J.get$hashCode$(object2);
+      object3 = J.get$hashCode$(object3);
+      object4 = J.get$hashCode$(object4);
+      object4 = A.SystemHash_finish(A.SystemHash_combine(A.SystemHash_combine(A.SystemHash_combine(A.SystemHash_combine($.$get$_hashSeed(), t1), object2), object3), object4));
+      return object4;
+    },
+    Uri_Uri$dataFromString($content) {
+      var t1, _null = null,
+        buffer = new A.StringBuffer(""),
+        indices = A._setArrayType([-1], type$.JSArray_int);
+      A.UriData__writeUri(_null, _null, _null, buffer, indices);
+      B.JSArray_methods.add$1(indices, buffer._contents.length);
+      buffer._contents += ",";
+      A.UriData__uriEncodeBytes(B.List_oFp, B.C_AsciiCodec.encode$1($content), buffer);
+      t1 = buffer._contents;
+      return new A.UriData(t1.charCodeAt(0) == 0 ? t1 : t1, indices, _null).get$uri();
+    },
+    Uri_parse(uri) {
+      var delta, indices, schemeEnd, hostStart, portStart, pathStart, queryStart, fragmentStart, isSimple, scheme, t1, t2, schemeAuth, queryStart0, pathStart0, userInfoStart, userInfo, host, portNumber, port, path, query, _null = null,
+        end = uri.length;
+      if (end >= 5) {
+        if (4 >= end)
+          return A.ioore(uri, 4);
+        delta = ((uri.charCodeAt(4) ^ 58) * 3 | uri.charCodeAt(0) ^ 100 | uri.charCodeAt(1) ^ 97 | uri.charCodeAt(2) ^ 116 | uri.charCodeAt(3) ^ 97) >>> 0;
+        if (delta === 0)
+          return A.UriData__parse(end < end ? B.JSString_methods.substring$2(uri, 0, end) : uri, 5, _null).get$uri();
+        else if (delta === 32)
+          return A.UriData__parse(B.JSString_methods.substring$2(uri, 5, end), 0, _null).get$uri();
+      }
+      indices = A.List_List$filled(8, 0, false, type$.int);
+      B.JSArray_methods.$indexSet(indices, 0, 0);
+      B.JSArray_methods.$indexSet(indices, 1, -1);
+      B.JSArray_methods.$indexSet(indices, 2, -1);
+      B.JSArray_methods.$indexSet(indices, 7, -1);
+      B.JSArray_methods.$indexSet(indices, 3, 0);
+      B.JSArray_methods.$indexSet(indices, 4, 0);
+      B.JSArray_methods.$indexSet(indices, 5, end);
+      B.JSArray_methods.$indexSet(indices, 6, end);
+      if (A._scan(uri, 0, end, 0, indices) >= 14)
+        B.JSArray_methods.$indexSet(indices, 7, end);
+      schemeEnd = indices[1];
+      if (schemeEnd >= 0)
+        if (A._scan(uri, 0, schemeEnd, 20, indices) === 20)
+          indices[7] = schemeEnd;
+      hostStart = indices[2] + 1;
+      portStart = indices[3];
+      pathStart = indices[4];
+      queryStart = indices[5];
+      fragmentStart = indices[6];
+      if (fragmentStart < queryStart)
+        queryStart = fragmentStart;
+      if (pathStart < hostStart)
+        pathStart = queryStart;
+      else if (pathStart <= schemeEnd)
+        pathStart = schemeEnd + 1;
+      if (portStart < hostStart)
+        portStart = pathStart;
+      isSimple = indices[7] < 0;
+      if (isSimple)
+        if (hostStart > schemeEnd + 3) {
+          scheme = _null;
+          isSimple = false;
+        } else {
+          t1 = portStart > 0;
+          if (t1 && portStart + 1 === pathStart) {
+            scheme = _null;
+            isSimple = false;
+          } else {
+            if (!B.JSString_methods.startsWith$2(uri, "\\", pathStart))
+              if (hostStart > 0)
+                t2 = B.JSString_methods.startsWith$2(uri, "\\", hostStart - 1) || B.JSString_methods.startsWith$2(uri, "\\", hostStart - 2);
+              else
+                t2 = false;
+            else
+              t2 = true;
+            if (t2) {
+              scheme = _null;
+              isSimple = false;
+            } else {
+              if (!(queryStart < end && queryStart === pathStart + 2 && B.JSString_methods.startsWith$2(uri, "..", pathStart)))
+                t2 = queryStart > pathStart + 2 && B.JSString_methods.startsWith$2(uri, "/..", queryStart - 3);
+              else
+                t2 = true;
+              if (t2) {
+                scheme = _null;
+                isSimple = false;
+              } else {
+                if (schemeEnd === 4)
+                  if (B.JSString_methods.startsWith$2(uri, "file", 0)) {
+                    if (hostStart <= 0) {
+                      if (!B.JSString_methods.startsWith$2(uri, "/", pathStart)) {
+                        schemeAuth = "file:///";
+                        delta = 3;
+                      } else {
+                        schemeAuth = "file://";
+                        delta = 2;
+                      }
+                      uri = schemeAuth + B.JSString_methods.substring$2(uri, pathStart, end);
+                      schemeEnd -= 0;
+                      t1 = delta - 0;
+                      queryStart += t1;
+                      fragmentStart += t1;
+                      end = uri.length;
+                      hostStart = 7;
+                      portStart = 7;
+                      pathStart = 7;
+                    } else if (pathStart === queryStart) {
+                      ++fragmentStart;
+                      queryStart0 = queryStart + 1;
+                      uri = B.JSString_methods.replaceRange$3(uri, pathStart, queryStart, "/");
+                      ++end;
+                      queryStart = queryStart0;
+                    }
+                    scheme = "file";
+                  } else if (B.JSString_methods.startsWith$2(uri, "http", 0)) {
+                    if (t1 && portStart + 3 === pathStart && B.JSString_methods.startsWith$2(uri, "80", portStart + 1)) {
+                      fragmentStart -= 3;
+                      pathStart0 = pathStart - 3;
+                      queryStart -= 3;
+                      uri = B.JSString_methods.replaceRange$3(uri, portStart, pathStart, "");
+                      end -= 3;
+                      pathStart = pathStart0;
+                    }
+                    scheme = "http";
+                  } else
+                    scheme = _null;
+                else if (schemeEnd === 5 && B.JSString_methods.startsWith$2(uri, "https", 0)) {
+                  if (t1 && portStart + 4 === pathStart && B.JSString_methods.startsWith$2(uri, "443", portStart + 1)) {
+                    fragmentStart -= 4;
+                    pathStart0 = pathStart - 4;
+                    queryStart -= 4;
+                    uri = B.JSString_methods.replaceRange$3(uri, portStart, pathStart, "");
+                    end -= 3;
+                    pathStart = pathStart0;
+                  }
+                  scheme = "https";
+                } else
+                  scheme = _null;
+                isSimple = true;
+              }
+            }
+          }
+        }
+      else
+        scheme = _null;
+      if (isSimple) {
+        if (end < uri.length) {
+          uri = B.JSString_methods.substring$2(uri, 0, end);
+          schemeEnd -= 0;
+          hostStart -= 0;
+          portStart -= 0;
+          pathStart -= 0;
+          queryStart -= 0;
+          fragmentStart -= 0;
+        }
+        return new A._SimpleUri(uri, schemeEnd, hostStart, portStart, pathStart, queryStart, fragmentStart, scheme);
+      }
+      if (scheme == null)
+        if (schemeEnd > 0)
+          scheme = A._Uri__makeScheme(uri, 0, schemeEnd);
+        else {
+          if (schemeEnd === 0)
+            A._Uri__fail(uri, 0, "Invalid empty scheme");
+          scheme = "";
+        }
+      if (hostStart > 0) {
+        userInfoStart = schemeEnd + 3;
+        userInfo = userInfoStart < hostStart ? A._Uri__makeUserInfo(uri, userInfoStart, hostStart - 1) : "";
+        host = A._Uri__makeHost(uri, hostStart, portStart, false);
+        t1 = portStart + 1;
+        if (t1 < pathStart) {
+          portNumber = A.Primitives_parseInt(B.JSString_methods.substring$2(uri, t1, pathStart), _null);
+          port = A._Uri__makePort(portNumber == null ? A.throwExpression(A.FormatException$("Invalid port", uri, t1)) : portNumber, scheme);
+        } else
+          port = _null;
+      } else {
+        port = _null;
+        host = port;
+        userInfo = "";
+      }
+      path = A._Uri__makePath(uri, pathStart, queryStart, _null, scheme, host != null);
+      query = queryStart < fragmentStart ? A._Uri__makeQuery(uri, queryStart + 1, fragmentStart, _null) : _null;
+      return A._Uri$_internal(scheme, userInfo, host, port, path, query, fragmentStart < end ? A._Uri__makeFragment(uri, fragmentStart + 1, end) : _null);
+    },
+    Uri_decodeComponent(encodedComponent) {
+      A._asString(encodedComponent);
+      return A._Uri__uriDecode(encodedComponent, 0, encodedComponent.length, B.C_Utf8Codec, false);
+    },
+    Uri_splitQueryString(query) {
+      var t1 = type$.String;
+      return B.JSArray_methods.fold$1$2(A._setArrayType(query.split("&"), type$.JSArray_String), A.LinkedHashMap_LinkedHashMap$_empty(t1, t1), new A.Uri_splitQueryString_closure(B.C_Utf8Codec), type$.Map_String_String);
+    },
+    Uri__parseIPv4Address(host, start, end) {
+      var t1, i, partStart, partIndex, char, part, partIndex0,
+        _s43_ = "IPv4 address should contain exactly 4 parts",
+        _s37_ = "each part must be in the range 0..255",
+        error = new A.Uri__parseIPv4Address_error(host),
+        result = new Uint8Array(4);
+      for (t1 = host.length, i = start, partStart = i, partIndex = 0; i < end; ++i) {
+        if (!(i >= 0 && i < t1))
+          return A.ioore(host, i);
+        char = host.charCodeAt(i);
+        if (char !== 46) {
+          if ((char ^ 48) > 9)
+            error.call$2("invalid character", i);
+        } else {
+          if (partIndex === 3)
+            error.call$2(_s43_, i);
+          part = A.int_parse(B.JSString_methods.substring$2(host, partStart, i), null);
+          if (part > 255)
+            error.call$2(_s37_, partStart);
+          partIndex0 = partIndex + 1;
+          if (!(partIndex < 4))
+            return A.ioore(result, partIndex);
+          result[partIndex] = part;
+          partStart = i + 1;
+          partIndex = partIndex0;
+        }
+      }
+      if (partIndex !== 3)
+        error.call$2(_s43_, end);
+      part = A.int_parse(B.JSString_methods.substring$2(host, partStart, end), null);
+      if (part > 255)
+        error.call$2(_s37_, partStart);
+      if (!(partIndex < 4))
+        return A.ioore(result, partIndex);
+      result[partIndex] = part;
+      return result;
+    },
+    Uri_parseIPv6Address(host, start, end) {
+      var parts, i, partStart, wildcardSeen, seenDot, char, atEnd, last, bytes, wildCardLength, index, value, j, t2, _null = null,
+        error = new A.Uri_parseIPv6Address_error(host),
+        parseHex = new A.Uri_parseIPv6Address_parseHex(error, host),
+        t1 = host.length;
+      if (t1 < 2)
+        error.call$2("address is too short", _null);
+      parts = A._setArrayType([], type$.JSArray_int);
+      for (i = start, partStart = i, wildcardSeen = false, seenDot = false; i < end; ++i) {
+        if (!(i >= 0 && i < t1))
+          return A.ioore(host, i);
+        char = host.charCodeAt(i);
+        if (char === 58) {
+          if (i === start) {
+            ++i;
+            if (!(i < t1))
+              return A.ioore(host, i);
+            if (host.charCodeAt(i) !== 58)
+              error.call$2("invalid start colon.", i);
+            partStart = i;
+          }
+          if (i === partStart) {
+            if (wildcardSeen)
+              error.call$2("only one wildcard `::` is allowed", i);
+            B.JSArray_methods.add$1(parts, -1);
+            wildcardSeen = true;
+          } else
+            B.JSArray_methods.add$1(parts, parseHex.call$2(partStart, i));
+          partStart = i + 1;
+        } else if (char === 46)
+          seenDot = true;
+      }
+      if (parts.length === 0)
+        error.call$2("too few parts", _null);
+      atEnd = partStart === end;
+      t1 = B.JSArray_methods.get$last(parts);
+      if (atEnd && t1 !== -1)
+        error.call$2("expected a part after last `:`", end);
+      if (!atEnd)
+        if (!seenDot)
+          B.JSArray_methods.add$1(parts, parseHex.call$2(partStart, end));
+        else {
+          last = A.Uri__parseIPv4Address(host, partStart, end);
+          B.JSArray_methods.add$1(parts, (last[0] << 8 | last[1]) >>> 0);
+          B.JSArray_methods.add$1(parts, (last[2] << 8 | last[3]) >>> 0);
+        }
+      if (wildcardSeen) {
+        if (parts.length > 7)
+          error.call$2("an address with a wildcard must have less than 7 parts", _null);
+      } else if (parts.length !== 8)
+        error.call$2("an address without a wildcard must contain exactly 8 parts", _null);
+      bytes = new Uint8Array(16);
+      for (t1 = parts.length, wildCardLength = 9 - t1, i = 0, index = 0; i < t1; ++i) {
+        value = parts[i];
+        if (value === -1)
+          for (j = 0; j < wildCardLength; ++j) {
+            if (!(index >= 0 && index < 16))
+              return A.ioore(bytes, index);
+            bytes[index] = 0;
+            t2 = index + 1;
+            if (!(t2 < 16))
+              return A.ioore(bytes, t2);
+            bytes[t2] = 0;
+            index += 2;
+          }
+        else {
+          t2 = B.JSInt_methods._shrOtherPositive$1(value, 8);
+          if (!(index >= 0 && index < 16))
+            return A.ioore(bytes, index);
+          bytes[index] = t2;
+          t2 = index + 1;
+          if (!(t2 < 16))
+            return A.ioore(bytes, t2);
+          bytes[t2] = value & 255;
+          index += 2;
+        }
+      }
+      return bytes;
+    },
+    _Uri$_internal(scheme, _userInfo, _host, _port, path, _query, _fragment) {
+      return new A._Uri(scheme, _userInfo, _host, _port, path, _query, _fragment);
+    },
+    _Uri__Uri(host, path, pathSegments, scheme) {
+      var userInfo, query, fragment, port, isFile, t1, hasAuthority, t2, _null = null;
+      scheme = scheme == null ? "" : A._Uri__makeScheme(scheme, 0, scheme.length);
+      userInfo = A._Uri__makeUserInfo(_null, 0, 0);
+      host = A._Uri__makeHost(host, 0, host == null ? 0 : host.length, false);
+      query = A._Uri__makeQuery(_null, 0, 0, _null);
+      fragment = A._Uri__makeFragment(_null, 0, 0);
+      port = A._Uri__makePort(_null, scheme);
+      isFile = scheme === "file";
+      if (host == null)
+        t1 = userInfo.length !== 0 || port != null || isFile;
+      else
+        t1 = false;
+      if (t1)
+        host = "";
+      t1 = host == null;
+      hasAuthority = !t1;
+      path = A._Uri__makePath(path, 0, path == null ? 0 : path.length, pathSegments, scheme, hasAuthority);
+      t2 = scheme.length === 0;
+      if (t2 && t1 && !B.JSString_methods.startsWith$1(path, "/"))
+        path = A._Uri__normalizeRelativePath(path, !t2 || hasAuthority);
+      else
+        path = A._Uri__removeDotSegments(path);
+      return A._Uri$_internal(scheme, userInfo, t1 && B.JSString_methods.startsWith$1(path, "//") ? "" : host, port, path, query, fragment);
+    },
+    _Uri__defaultPort(scheme) {
+      if (scheme === "http")
+        return 80;
+      if (scheme === "https")
+        return 443;
+      return 0;
+    },
+    _Uri__fail(uri, index, message) {
+      throw A.wrapException(A.FormatException$(message, uri, index));
+    },
+    _Uri__Uri$file(path, windows) {
+      return windows ? A._Uri__makeWindowsFileUrl(path, false) : A._Uri__makeFileUri(path, false);
+    },
+    _Uri__checkNonWindowsPathReservedCharacters(segments, argumentError) {
+      var t1, _i, segment;
+      for (t1 = segments.length, _i = 0; _i < t1; ++_i) {
+        segment = segments[_i];
+        if (J.contains$1$asx(segment, "/")) {
+          t1 = A.UnsupportedError$("Illegal path character " + A.S(segment));
+          throw A.wrapException(t1);
+        }
+      }
+    },
+    _Uri__checkWindowsPathReservedCharacters(segments, argumentError, firstSegment) {
+      var t1, t2, t3;
+      for (t1 = A.SubListIterable$(segments, firstSegment, null, A._arrayInstanceType(segments)._precomputed1), t2 = t1.$ti, t1 = new A.ListIterator(t1, t1.get$length(t1), t2._eval$1("ListIterator<ListIterable.E>")), t2 = t2._eval$1("ListIterable.E"); t1.moveNext$0();) {
+        t3 = t1.__internal$_current;
+        if (t3 == null)
+          t3 = t2._as(t3);
+        if (B.JSString_methods.contains$1(t3, A.RegExp_RegExp('["*/:<>?\\\\|]', false)))
+          if (argumentError)
+            throw A.wrapException(A.ArgumentError$("Illegal character in path", null));
+          else
+            throw A.wrapException(A.UnsupportedError$("Illegal character in path: " + t3));
+      }
+    },
+    _Uri__checkWindowsDriveLetter(charCode, argumentError) {
+      var t1,
+        _s21_ = "Illegal drive letter ";
+      if (!(65 <= charCode && charCode <= 90))
+        t1 = 97 <= charCode && charCode <= 122;
+      else
+        t1 = true;
+      if (t1)
+        return;
+      if (argumentError)
+        throw A.wrapException(A.ArgumentError$(_s21_ + A.String_String$fromCharCode(charCode), null));
+      else
+        throw A.wrapException(A.UnsupportedError$(_s21_ + A.String_String$fromCharCode(charCode)));
+    },
+    _Uri__makeFileUri(path, slashTerminated) {
+      var _null = null,
+        segments = A._setArrayType(path.split("/"), type$.JSArray_String);
+      if (B.JSString_methods.startsWith$1(path, "/"))
+        return A._Uri__Uri(_null, _null, segments, "file");
+      else
+        return A._Uri__Uri(_null, _null, segments, _null);
+    },
+    _Uri__makeWindowsFileUrl(path, slashTerminated) {
+      var t1, pathSegments, pathStart, hostPart, _s1_ = "\\", _null = null, _s4_ = "file";
+      if (B.JSString_methods.startsWith$1(path, "\\\\?\\"))
+        if (B.JSString_methods.startsWith$2(path, "UNC\\", 4))
+          path = B.JSString_methods.replaceRange$3(path, 0, 7, _s1_);
+        else {
+          path = B.JSString_methods.substring$1(path, 4);
+          t1 = path.length;
+          if (t1 >= 3) {
+            if (1 >= t1)
+              return A.ioore(path, 1);
+            if (path.charCodeAt(1) === 58) {
+              if (2 >= t1)
+                return A.ioore(path, 2);
+              t1 = path.charCodeAt(2) !== 92;
+            } else
+              t1 = true;
+          } else
+            t1 = true;
+          if (t1)
+            throw A.wrapException(A.ArgumentError$value(path, "path", "Windows paths with \\\\?\\ prefix must be absolute"));
+        }
+      else
+        path = A.stringReplaceAllUnchecked(path, "/", _s1_);
+      t1 = path.length;
+      if (t1 > 1 && path.charCodeAt(1) === 58) {
+        if (0 >= t1)
+          return A.ioore(path, 0);
+        A._Uri__checkWindowsDriveLetter(path.charCodeAt(0), true);
+        if (t1 !== 2) {
+          if (2 >= t1)
+            return A.ioore(path, 2);
+          t1 = path.charCodeAt(2) !== 92;
+        } else
+          t1 = true;
+        if (t1)
+          throw A.wrapException(A.ArgumentError$value(path, "path", "Windows paths with drive letter must be absolute"));
+        pathSegments = A._setArrayType(path.split(_s1_), type$.JSArray_String);
+        A._Uri__checkWindowsPathReservedCharacters(pathSegments, true, 1);
+        return A._Uri__Uri(_null, _null, pathSegments, _s4_);
+      }
+      if (B.JSString_methods.startsWith$1(path, _s1_))
+        if (B.JSString_methods.startsWith$2(path, _s1_, 1)) {
+          pathStart = B.JSString_methods.indexOf$2(path, _s1_, 2);
+          t1 = pathStart < 0;
+          hostPart = t1 ? B.JSString_methods.substring$1(path, 2) : B.JSString_methods.substring$2(path, 2, pathStart);
+          pathSegments = A._setArrayType((t1 ? "" : B.JSString_methods.substring$1(path, pathStart + 1)).split(_s1_), type$.JSArray_String);
+          A._Uri__checkWindowsPathReservedCharacters(pathSegments, true, 0);
+          return A._Uri__Uri(hostPart, _null, pathSegments, _s4_);
+        } else {
+          pathSegments = A._setArrayType(path.split(_s1_), type$.JSArray_String);
+          A._Uri__checkWindowsPathReservedCharacters(pathSegments, true, 0);
+          return A._Uri__Uri(_null, _null, pathSegments, _s4_);
+        }
+      else {
+        pathSegments = A._setArrayType(path.split(_s1_), type$.JSArray_String);
+        A._Uri__checkWindowsPathReservedCharacters(pathSegments, true, 0);
+        return A._Uri__Uri(_null, _null, pathSegments, _null);
+      }
+    },
+    _Uri__makePort(port, scheme) {
+      if (port != null && port === A._Uri__defaultPort(scheme))
+        return null;
+      return port;
+    },
+    _Uri__makeHost(host, start, end, strictIPv6) {
+      var t1, t2, index, zoneIDstart, zoneID, i;
+      if (host == null)
+        return null;
+      if (start === end)
+        return "";
+      t1 = host.length;
+      if (!(start >= 0 && start < t1))
+        return A.ioore(host, start);
+      if (host.charCodeAt(start) === 91) {
+        t2 = end - 1;
+        if (!(t2 >= 0 && t2 < t1))
+          return A.ioore(host, t2);
+        if (host.charCodeAt(t2) !== 93)
+          A._Uri__fail(host, start, "Missing end `]` to match `[` in host");
+        t1 = start + 1;
+        index = A._Uri__checkZoneID(host, t1, t2);
+        if (index < t2) {
+          zoneIDstart = index + 1;
+          zoneID = A._Uri__normalizeZoneID(host, B.JSString_methods.startsWith$2(host, "25", zoneIDstart) ? index + 3 : zoneIDstart, t2, "%25");
+        } else
+          zoneID = "";
+        A.Uri_parseIPv6Address(host, t1, index);
+        return B.JSString_methods.substring$2(host, start, index).toLowerCase() + zoneID + "]";
+      }
+      for (i = start; i < end; ++i) {
+        if (!(i < t1))
+          return A.ioore(host, i);
+        if (host.charCodeAt(i) === 58) {
+          index = B.JSString_methods.indexOf$2(host, "%", start);
+          index = index >= start && index < end ? index : end;
+          if (index < end) {
+            zoneIDstart = index + 1;
+            zoneID = A._Uri__normalizeZoneID(host, B.JSString_methods.startsWith$2(host, "25", zoneIDstart) ? index + 3 : zoneIDstart, end, "%25");
+          } else
+            zoneID = "";
+          A.Uri_parseIPv6Address(host, start, index);
+          return "[" + B.JSString_methods.substring$2(host, start, index) + zoneID + "]";
+        }
+      }
+      return A._Uri__normalizeRegName(host, start, end);
+    },
+    _Uri__checkZoneID(host, start, end) {
+      var index = B.JSString_methods.indexOf$2(host, "%", start);
+      return index >= start && index < end ? index : end;
+    },
+    _Uri__normalizeZoneID(host, start, end, prefix) {
+      var t1, index, sectionStart, isNormalized, char, replacement, t2, t3, tail, sourceLength, slice,
+        buffer = prefix !== "" ? new A.StringBuffer(prefix) : null;
+      for (t1 = host.length, index = start, sectionStart = index, isNormalized = true; index < end;) {
+        if (!(index >= 0 && index < t1))
+          return A.ioore(host, index);
+        char = host.charCodeAt(index);
+        if (char === 37) {
+          replacement = A._Uri__normalizeEscape(host, index, true);
+          t2 = replacement == null;
+          if (t2 && isNormalized) {
+            index += 3;
+            continue;
+          }
+          if (buffer == null)
+            buffer = new A.StringBuffer("");
+          t3 = buffer._contents += B.JSString_methods.substring$2(host, sectionStart, index);
+          if (t2)
+            replacement = B.JSString_methods.substring$2(host, index, index + 3);
+          else if (replacement === "%")
+            A._Uri__fail(host, index, "ZoneID should not contain % anymore");
+          buffer._contents = t3 + replacement;
+          index += 3;
+          sectionStart = index;
+          isNormalized = true;
+        } else {
+          if (char < 127) {
+            t2 = char >>> 4;
+            if (!(t2 < 8))
+              return A.ioore(B.List_M1A, t2);
+            t2 = (B.List_M1A[t2] & 1 << (char & 15)) !== 0;
+          } else
+            t2 = false;
+          if (t2) {
+            if (isNormalized && 65 <= char && 90 >= char) {
+              if (buffer == null)
+                buffer = new A.StringBuffer("");
+              if (sectionStart < index) {
+                buffer._contents += B.JSString_methods.substring$2(host, sectionStart, index);
+                sectionStart = index;
+              }
+              isNormalized = false;
+            }
+            ++index;
+          } else {
+            if ((char & 64512) === 55296 && index + 1 < end) {
+              t2 = index + 1;
+              if (!(t2 < t1))
+                return A.ioore(host, t2);
+              tail = host.charCodeAt(t2);
+              if ((tail & 64512) === 56320) {
+                char = (char & 1023) << 10 | tail & 1023 | 65536;
+                sourceLength = 2;
+              } else
+                sourceLength = 1;
+            } else
+              sourceLength = 1;
+            slice = B.JSString_methods.substring$2(host, sectionStart, index);
+            if (buffer == null) {
+              buffer = new A.StringBuffer("");
+              t2 = buffer;
+            } else
+              t2 = buffer;
+            t2._contents += slice;
+            t2._contents += A._Uri__escapeChar(char);
+            index += sourceLength;
+            sectionStart = index;
+          }
+        }
+      }
+      if (buffer == null)
+        return B.JSString_methods.substring$2(host, start, end);
+      if (sectionStart < end)
+        buffer._contents += B.JSString_methods.substring$2(host, sectionStart, end);
+      t1 = buffer._contents;
+      return t1.charCodeAt(0) == 0 ? t1 : t1;
+    },
+    _Uri__normalizeRegName(host, start, end) {
+      var t1, index, sectionStart, buffer, isNormalized, char, replacement, t2, slice, t3, sourceLength, tail;
+      for (t1 = host.length, index = start, sectionStart = index, buffer = null, isNormalized = true; index < end;) {
+        if (!(index >= 0 && index < t1))
+          return A.ioore(host, index);
+        char = host.charCodeAt(index);
+        if (char === 37) {
+          replacement = A._Uri__normalizeEscape(host, index, true);
+          t2 = replacement == null;
+          if (t2 && isNormalized) {
+            index += 3;
+            continue;
+          }
+          if (buffer == null)
+            buffer = new A.StringBuffer("");
+          slice = B.JSString_methods.substring$2(host, sectionStart, index);
+          t3 = buffer._contents += !isNormalized ? slice.toLowerCase() : slice;
+          if (t2) {
+            replacement = B.JSString_methods.substring$2(host, index, index + 3);
+            sourceLength = 3;
+          } else if (replacement === "%") {
+            replacement = "%25";
+            sourceLength = 1;
+          } else
+            sourceLength = 3;
+          buffer._contents = t3 + replacement;
+          index += sourceLength;
+          sectionStart = index;
+          isNormalized = true;
+        } else {
+          if (char < 127) {
+            t2 = char >>> 4;
+            if (!(t2 < 8))
+              return A.ioore(B.List_ejq, t2);
+            t2 = (B.List_ejq[t2] & 1 << (char & 15)) !== 0;
+          } else
+            t2 = false;
+          if (t2) {
+            if (isNormalized && 65 <= char && 90 >= char) {
+              if (buffer == null)
+                buffer = new A.StringBuffer("");
+              if (sectionStart < index) {
+                buffer._contents += B.JSString_methods.substring$2(host, sectionStart, index);
+                sectionStart = index;
+              }
+              isNormalized = false;
+            }
+            ++index;
+          } else {
+            if (char <= 93) {
+              t2 = char >>> 4;
+              if (!(t2 < 8))
+                return A.ioore(B.List_YmH, t2);
+              t2 = (B.List_YmH[t2] & 1 << (char & 15)) !== 0;
+            } else
+              t2 = false;
+            if (t2)
+              A._Uri__fail(host, index, "Invalid character");
+            else {
+              if ((char & 64512) === 55296 && index + 1 < end) {
+                t2 = index + 1;
+                if (!(t2 < t1))
+                  return A.ioore(host, t2);
+                tail = host.charCodeAt(t2);
+                if ((tail & 64512) === 56320) {
+                  char = (char & 1023) << 10 | tail & 1023 | 65536;
+                  sourceLength = 2;
+                } else
+                  sourceLength = 1;
+              } else
+                sourceLength = 1;
+              slice = B.JSString_methods.substring$2(host, sectionStart, index);
+              if (!isNormalized)
+                slice = slice.toLowerCase();
+              if (buffer == null) {
+                buffer = new A.StringBuffer("");
+                t2 = buffer;
+              } else
+                t2 = buffer;
+              t2._contents += slice;
+              t2._contents += A._Uri__escapeChar(char);
+              index += sourceLength;
+              sectionStart = index;
+            }
+          }
+        }
+      }
+      if (buffer == null)
+        return B.JSString_methods.substring$2(host, start, end);
+      if (sectionStart < end) {
+        slice = B.JSString_methods.substring$2(host, sectionStart, end);
+        buffer._contents += !isNormalized ? slice.toLowerCase() : slice;
+      }
+      t1 = buffer._contents;
+      return t1.charCodeAt(0) == 0 ? t1 : t1;
+    },
+    _Uri__makeScheme(scheme, start, end) {
+      var t1, i, containsUpperCase, codeUnit, t2;
+      if (start === end)
+        return "";
+      t1 = scheme.length;
+      if (!(start < t1))
+        return A.ioore(scheme, start);
+      if (!A._Uri__isAlphabeticCharacter(scheme.charCodeAt(start)))
+        A._Uri__fail(scheme, start, "Scheme not starting with alphabetic character");
+      for (i = start, containsUpperCase = false; i < end; ++i) {
+        if (!(i < t1))
+          return A.ioore(scheme, i);
+        codeUnit = scheme.charCodeAt(i);
+        if (codeUnit < 128) {
+          t2 = codeUnit >>> 4;
+          if (!(t2 < 8))
+            return A.ioore(B.List_MMm, t2);
+          t2 = (B.List_MMm[t2] & 1 << (codeUnit & 15)) !== 0;
+        } else
+          t2 = false;
+        if (!t2)
+          A._Uri__fail(scheme, i, "Illegal scheme character");
+        if (65 <= codeUnit && codeUnit <= 90)
+          containsUpperCase = true;
+      }
+      scheme = B.JSString_methods.substring$2(scheme, start, end);
+      return A._Uri__canonicalizeScheme(containsUpperCase ? scheme.toLowerCase() : scheme);
+    },
+    _Uri__canonicalizeScheme(scheme) {
+      if (scheme === "http")
+        return "http";
+      if (scheme === "file")
+        return "file";
+      if (scheme === "https")
+        return "https";
+      if (scheme === "package")
+        return "package";
+      return scheme;
+    },
+    _Uri__makeUserInfo(userInfo, start, end) {
+      if (userInfo == null)
+        return "";
+      return A._Uri__normalizeOrSubstring(userInfo, start, end, B.List_OL3, false, false);
+    },
+    _Uri__makePath(path, start, end, pathSegments, scheme, hasAuthority) {
+      var t1, result,
+        isFile = scheme === "file",
+        ensureLeadingSlash = isFile || hasAuthority;
+      if (path == null) {
+        if (pathSegments == null)
+          return isFile ? "/" : "";
+        t1 = A._arrayInstanceType(pathSegments);
+        result = new A.MappedListIterable(pathSegments, t1._eval$1("String(1)")._as(new A._Uri__makePath_closure()), t1._eval$1("MappedListIterable<1,String>")).join$1(0, "/");
+      } else if (pathSegments != null)
+        throw A.wrapException(A.ArgumentError$("Both path and pathSegments specified", null));
+      else
+        result = A._Uri__normalizeOrSubstring(path, start, end, B.List_XRg, true, true);
+      if (result.length === 0) {
+        if (isFile)
+          return "/";
+      } else if (ensureLeadingSlash && !B.JSString_methods.startsWith$1(result, "/"))
+        result = "/" + result;
+      return A._Uri__normalizePath(result, scheme, hasAuthority);
+    },
+    _Uri__normalizePath(path, scheme, hasAuthority) {
+      var t1 = scheme.length === 0;
+      if (t1 && !hasAuthority && !B.JSString_methods.startsWith$1(path, "/") && !B.JSString_methods.startsWith$1(path, "\\"))
+        return A._Uri__normalizeRelativePath(path, !t1 || hasAuthority);
+      return A._Uri__removeDotSegments(path);
+    },
+    _Uri__makeQuery(query, start, end, queryParameters) {
+      if (query != null)
+        return A._Uri__normalizeOrSubstring(query, start, end, B.List_oFp, true, false);
+      return null;
+    },
+    _Uri__makeFragment(fragment, start, end) {
+      if (fragment == null)
+        return null;
+      return A._Uri__normalizeOrSubstring(fragment, start, end, B.List_oFp, true, false);
+    },
+    _Uri__normalizeEscape(source, index, lowerCase) {
+      var t3, firstDigit, secondDigit, firstDigitValue, secondDigitValue, value,
+        t1 = index + 2,
+        t2 = source.length;
+      if (t1 >= t2)
+        return "%";
+      t3 = index + 1;
+      if (!(t3 >= 0 && t3 < t2))
+        return A.ioore(source, t3);
+      firstDigit = source.charCodeAt(t3);
+      if (!(t1 >= 0))
+        return A.ioore(source, t1);
+      secondDigit = source.charCodeAt(t1);
+      firstDigitValue = A.hexDigitValue(firstDigit);
+      secondDigitValue = A.hexDigitValue(secondDigit);
+      if (firstDigitValue < 0 || secondDigitValue < 0)
+        return "%";
+      value = firstDigitValue * 16 + secondDigitValue;
+      if (value < 127) {
+        t1 = B.JSInt_methods._shrOtherPositive$1(value, 4);
+        if (!(t1 < 8))
+          return A.ioore(B.List_M1A, t1);
+        t1 = (B.List_M1A[t1] & 1 << (value & 15)) !== 0;
+      } else
+        t1 = false;
+      if (t1)
+        return A.Primitives_stringFromCharCode(lowerCase && 65 <= value && 90 >= value ? (value | 32) >>> 0 : value);
+      if (firstDigit >= 97 || secondDigit >= 97)
+        return B.JSString_methods.substring$2(source, index, index + 3).toUpperCase();
+      return null;
+    },
+    _Uri__escapeChar(char) {
+      var codeUnits, t1, flag, encodedBytes, index, byte, t2, t3,
+        _s16_ = "0123456789ABCDEF";
+      if (char < 128) {
+        codeUnits = new Uint8Array(3);
+        codeUnits[0] = 37;
+        t1 = char >>> 4;
+        if (!(t1 < 16))
+          return A.ioore(_s16_, t1);
+        codeUnits[1] = _s16_.charCodeAt(t1);
+        codeUnits[2] = _s16_.charCodeAt(char & 15);
+      } else {
+        if (char > 2047)
+          if (char > 65535) {
+            flag = 240;
+            encodedBytes = 4;
+          } else {
+            flag = 224;
+            encodedBytes = 3;
+          }
+        else {
+          flag = 192;
+          encodedBytes = 2;
+        }
+        t1 = 3 * encodedBytes;
+        codeUnits = new Uint8Array(t1);
+        for (index = 0; --encodedBytes, encodedBytes >= 0; flag = 128) {
+          byte = B.JSInt_methods._shrReceiverPositive$1(char, 6 * encodedBytes) & 63 | flag;
+          if (!(index < t1))
+            return A.ioore(codeUnits, index);
+          codeUnits[index] = 37;
+          t2 = index + 1;
+          t3 = byte >>> 4;
+          if (!(t3 < 16))
+            return A.ioore(_s16_, t3);
+          if (!(t2 < t1))
+            return A.ioore(codeUnits, t2);
+          codeUnits[t2] = _s16_.charCodeAt(t3);
+          t3 = index + 2;
+          if (!(t3 < t1))
+            return A.ioore(codeUnits, t3);
+          codeUnits[t3] = _s16_.charCodeAt(byte & 15);
+          index += 3;
+        }
+      }
+      return A.String_String$fromCharCodes(codeUnits, 0, null);
+    },
+    _Uri__normalizeOrSubstring(component, start, end, charTable, escapeDelimiters, replaceBackslash) {
+      var t1 = A._Uri__normalize(component, start, end, charTable, escapeDelimiters, replaceBackslash);
+      return t1 == null ? B.JSString_methods.substring$2(component, start, end) : t1;
+    },
+    _Uri__normalize(component, start, end, charTable, escapeDelimiters, replaceBackslash) {
+      var t1, t2, index, sectionStart, buffer, char, t3, replacement, sourceLength, tail, t4, _null = null;
+      for (t1 = !escapeDelimiters, t2 = component.length, index = start, sectionStart = index, buffer = _null; index < end;) {
+        if (!(index >= 0 && index < t2))
+          return A.ioore(component, index);
+        char = component.charCodeAt(index);
+        if (char < 127) {
+          t3 = char >>> 4;
+          if (!(t3 < 8))
+            return A.ioore(charTable, t3);
+          t3 = (charTable[t3] & 1 << (char & 15)) !== 0;
+        } else
+          t3 = false;
+        if (t3)
+          ++index;
+        else {
+          if (char === 37) {
+            replacement = A._Uri__normalizeEscape(component, index, false);
+            if (replacement == null) {
+              index += 3;
+              continue;
+            }
+            if ("%" === replacement) {
+              replacement = "%25";
+              sourceLength = 1;
+            } else
+              sourceLength = 3;
+          } else if (char === 92 && replaceBackslash) {
+            replacement = "/";
+            sourceLength = 1;
+          } else {
+            if (t1)
+              if (char <= 93) {
+                t3 = char >>> 4;
+                if (!(t3 < 8))
+                  return A.ioore(B.List_YmH, t3);
+                t3 = (B.List_YmH[t3] & 1 << (char & 15)) !== 0;
+              } else
+                t3 = false;
+            else
+              t3 = false;
+            if (t3) {
+              A._Uri__fail(component, index, "Invalid character");
+              sourceLength = _null;
+              replacement = sourceLength;
+            } else {
+              if ((char & 64512) === 55296) {
+                t3 = index + 1;
+                if (t3 < end) {
+                  if (!(t3 < t2))
+                    return A.ioore(component, t3);
+                  tail = component.charCodeAt(t3);
+                  if ((tail & 64512) === 56320) {
+                    char = (char & 1023) << 10 | tail & 1023 | 65536;
+                    sourceLength = 2;
+                  } else
+                    sourceLength = 1;
+                } else
+                  sourceLength = 1;
+              } else
+                sourceLength = 1;
+              replacement = A._Uri__escapeChar(char);
+            }
+          }
+          if (buffer == null) {
+            buffer = new A.StringBuffer("");
+            t3 = buffer;
+          } else
+            t3 = buffer;
+          t4 = t3._contents += B.JSString_methods.substring$2(component, sectionStart, index);
+          t3._contents = t4 + A.S(replacement);
+          if (typeof sourceLength !== "number")
+            return A.iae(sourceLength);
+          index += sourceLength;
+          sectionStart = index;
+        }
+      }
+      if (buffer == null)
+        return _null;
+      if (sectionStart < end)
+        buffer._contents += B.JSString_methods.substring$2(component, sectionStart, end);
+      t1 = buffer._contents;
+      return t1.charCodeAt(0) == 0 ? t1 : t1;
+    },
+    _Uri__mayContainDotSegments(path) {
+      if (B.JSString_methods.startsWith$1(path, "."))
+        return true;
+      return B.JSString_methods.indexOf$1(path, "/.") !== -1;
+    },
+    _Uri__removeDotSegments(path) {
+      var output, t1, t2, appendSlash, _i, segment, t3;
+      if (!A._Uri__mayContainDotSegments(path))
+        return path;
+      output = A._setArrayType([], type$.JSArray_String);
+      for (t1 = path.split("/"), t2 = t1.length, appendSlash = false, _i = 0; _i < t2; ++_i) {
+        segment = t1[_i];
+        if (J.$eq$(segment, "..")) {
+          t3 = output.length;
+          if (t3 !== 0) {
+            if (0 >= t3)
+              return A.ioore(output, -1);
+            output.pop();
+            if (output.length === 0)
+              B.JSArray_methods.add$1(output, "");
+          }
+          appendSlash = true;
+        } else if ("." === segment)
+          appendSlash = true;
+        else {
+          B.JSArray_methods.add$1(output, segment);
+          appendSlash = false;
+        }
+      }
+      if (appendSlash)
+        B.JSArray_methods.add$1(output, "");
+      return B.JSArray_methods.join$1(output, "/");
+    },
+    _Uri__normalizeRelativePath(path, allowScheme) {
+      var output, t1, t2, appendSlash, _i, segment;
+      if (!A._Uri__mayContainDotSegments(path))
+        return !allowScheme ? A._Uri__escapeScheme(path) : path;
+      output = A._setArrayType([], type$.JSArray_String);
+      for (t1 = path.split("/"), t2 = t1.length, appendSlash = false, _i = 0; _i < t2; ++_i) {
+        segment = t1[_i];
+        if (".." === segment)
+          if (output.length !== 0 && B.JSArray_methods.get$last(output) !== "..") {
+            if (0 >= output.length)
+              return A.ioore(output, -1);
+            output.pop();
+            appendSlash = true;
+          } else {
+            B.JSArray_methods.add$1(output, "..");
+            appendSlash = false;
+          }
+        else if ("." === segment)
+          appendSlash = true;
+        else {
+          B.JSArray_methods.add$1(output, segment);
+          appendSlash = false;
+        }
+      }
+      t1 = output.length;
+      if (t1 !== 0)
+        if (t1 === 1) {
+          if (0 >= t1)
+            return A.ioore(output, 0);
+          t1 = output[0].length === 0;
+        } else
+          t1 = false;
+      else
+        t1 = true;
+      if (t1)
+        return "./";
+      if (appendSlash || B.JSArray_methods.get$last(output) === "..")
+        B.JSArray_methods.add$1(output, "");
+      if (!allowScheme) {
+        if (0 >= output.length)
+          return A.ioore(output, 0);
+        B.JSArray_methods.$indexSet(output, 0, A._Uri__escapeScheme(output[0]));
+      }
+      return B.JSArray_methods.join$1(output, "/");
+    },
+    _Uri__escapeScheme(path) {
+      var i, char, t2,
+        t1 = path.length;
+      if (t1 >= 2 && A._Uri__isAlphabeticCharacter(path.charCodeAt(0)))
+        for (i = 1; i < t1; ++i) {
+          char = path.charCodeAt(i);
+          if (char === 58)
+            return B.JSString_methods.substring$2(path, 0, i) + "%3A" + B.JSString_methods.substring$1(path, i + 1);
+          if (char <= 127) {
+            t2 = char >>> 4;
+            if (!(t2 < 8))
+              return A.ioore(B.List_MMm, t2);
+            t2 = (B.List_MMm[t2] & 1 << (char & 15)) === 0;
+          } else
+            t2 = true;
+          if (t2)
+            break;
+        }
+      return path;
+    },
+    _Uri__packageNameEnd(uri, path) {
+      if (uri.isScheme$1("package") && uri._host == null)
+        return A._skipPackageNameChars(path, 0, path.length);
+      return -1;
+    },
+    _Uri__toWindowsFilePath(uri) {
+      var hasDriveLetter, t2, host,
+        segments = uri.get$pathSegments(),
+        t1 = segments.length;
+      if (t1 > 0 && J.get$length$asx(segments[0]) === 2 && J.codeUnitAt$1$s(segments[0], 1) === 58) {
+        if (0 >= t1)
+          return A.ioore(segments, 0);
+        A._Uri__checkWindowsDriveLetter(J.codeUnitAt$1$s(segments[0], 0), false);
+        A._Uri__checkWindowsPathReservedCharacters(segments, false, 1);
+        hasDriveLetter = true;
+      } else {
+        A._Uri__checkWindowsPathReservedCharacters(segments, false, 0);
+        hasDriveLetter = false;
+      }
+      t2 = uri.get$hasAbsolutePath() && !hasDriveLetter ? "" + "\\" : "";
+      if (uri.get$hasAuthority()) {
+        host = uri.get$host(uri);
+        if (host.length !== 0)
+          t2 = t2 + "\\" + host + "\\";
+      }
+      t2 = A.StringBuffer__writeAll(t2, segments, "\\");
+      t1 = hasDriveLetter && t1 === 1 ? t2 + "\\" : t2;
+      return t1.charCodeAt(0) == 0 ? t1 : t1;
+    },
+    _Uri__hexCharPairToByte(s, pos) {
+      var t1, byte, i, t2, charCode;
+      for (t1 = s.length, byte = 0, i = 0; i < 2; ++i) {
+        t2 = pos + i;
+        if (!(t2 < t1))
+          return A.ioore(s, t2);
+        charCode = s.charCodeAt(t2);
+        if (48 <= charCode && charCode <= 57)
+          byte = byte * 16 + charCode - 48;
+        else {
+          charCode |= 32;
+          if (97 <= charCode && charCode <= 102)
+            byte = byte * 16 + charCode - 87;
+          else
+            throw A.wrapException(A.ArgumentError$("Invalid URL encoding", null));
+        }
+      }
+      return byte;
+    },
+    _Uri__uriDecode(text, start, end, encoding, plusToSpace) {
+      var simple, codeUnit, t2, bytes,
+        t1 = text.length,
+        i = start;
+      while (true) {
+        if (!(i < end)) {
+          simple = true;
+          break;
+        }
+        if (!(i < t1))
+          return A.ioore(text, i);
+        codeUnit = text.charCodeAt(i);
+        if (codeUnit <= 127)
+          if (codeUnit !== 37)
+            t2 = plusToSpace && codeUnit === 43;
+          else
+            t2 = true;
+        else
+          t2 = true;
+        if (t2) {
+          simple = false;
+          break;
+        }
+        ++i;
+      }
+      if (simple) {
+        if (B.C_Utf8Codec !== encoding)
+          t1 = false;
+        else
+          t1 = true;
+        if (t1)
+          return B.JSString_methods.substring$2(text, start, end);
+        else
+          bytes = new A.CodeUnits(B.JSString_methods.substring$2(text, start, end));
+      } else {
+        bytes = A._setArrayType([], type$.JSArray_int);
+        for (i = start; i < end; ++i) {
+          if (!(i < t1))
+            return A.ioore(text, i);
+          codeUnit = text.charCodeAt(i);
+          if (codeUnit > 127)
+            throw A.wrapException(A.ArgumentError$("Illegal percent encoding in URI", null));
+          if (codeUnit === 37) {
+            if (i + 3 > t1)
+              throw A.wrapException(A.ArgumentError$("Truncated URI", null));
+            B.JSArray_methods.add$1(bytes, A._Uri__hexCharPairToByte(text, i + 1));
+            i += 2;
+          } else if (plusToSpace && codeUnit === 43)
+            B.JSArray_methods.add$1(bytes, 32);
+          else
+            B.JSArray_methods.add$1(bytes, codeUnit);
+        }
+      }
+      type$.List_int._as(bytes);
+      return B.Utf8Decoder_false.convert$1(bytes);
+    },
+    _Uri__isAlphabeticCharacter(codeUnit) {
+      var lowerCase = codeUnit | 32;
+      return 97 <= lowerCase && lowerCase <= 122;
+    },
+    UriData__writeUri(mimeType, charsetName, parameters, buffer, indices) {
+      var slashIndex, t1;
+      if (true)
+        buffer._contents = buffer._contents;
+      else {
+        slashIndex = A.UriData__validateMimeType("");
+        if (slashIndex < 0)
+          throw A.wrapException(A.ArgumentError$value("", "mimeType", "Invalid MIME type"));
+        t1 = buffer._contents += A._Uri__uriEncode(B.List_yzX, B.JSString_methods.substring$2("", 0, slashIndex), B.C_Utf8Codec, false);
+        buffer._contents = t1 + "/";
+        buffer._contents += A._Uri__uriEncode(B.List_yzX, B.JSString_methods.substring$1("", slashIndex + 1), B.C_Utf8Codec, false);
+      }
+    },
+    UriData__validateMimeType(mimeType) {
+      var t1, slashIndex, i;
+      for (t1 = mimeType.length, slashIndex = -1, i = 0; i < t1; ++i) {
+        if (mimeType.charCodeAt(i) !== 47)
+          continue;
+        if (slashIndex < 0) {
+          slashIndex = i;
+          continue;
+        }
+        return -1;
+      }
+      return slashIndex;
+    },
+    UriData__parse(text, start, sourceUri) {
+      var t1, i, slashIndex, char, equalsIndex, lastSeparator, t2, data,
+        _s17_ = "Invalid MIME type",
+        indices = A._setArrayType([start - 1], type$.JSArray_int);
+      for (t1 = text.length, i = start, slashIndex = -1, char = null; i < t1; ++i) {
+        char = text.charCodeAt(i);
+        if (char === 44 || char === 59)
+          break;
+        if (char === 47) {
+          if (slashIndex < 0) {
+            slashIndex = i;
+            continue;
+          }
+          throw A.wrapException(A.FormatException$(_s17_, text, i));
+        }
+      }
+      if (slashIndex < 0 && i > start)
+        throw A.wrapException(A.FormatException$(_s17_, text, i));
+      for (; char !== 44;) {
+        B.JSArray_methods.add$1(indices, i);
+        ++i;
+        for (equalsIndex = -1; i < t1; ++i) {
+          if (!(i >= 0))
+            return A.ioore(text, i);
+          char = text.charCodeAt(i);
+          if (char === 61) {
+            if (equalsIndex < 0)
+              equalsIndex = i;
+          } else if (char === 59 || char === 44)
+            break;
+        }
+        if (equalsIndex >= 0)
+          B.JSArray_methods.add$1(indices, equalsIndex);
+        else {
+          lastSeparator = B.JSArray_methods.get$last(indices);
+          if (char !== 44 || i !== lastSeparator + 7 || !B.JSString_methods.startsWith$2(text, "base64", lastSeparator + 1))
+            throw A.wrapException(A.FormatException$("Expecting '='", text, i));
+          break;
+        }
+      }
+      B.JSArray_methods.add$1(indices, i);
+      t2 = i + 1;
+      if ((indices.length & 1) === 1)
+        text = B.C_Base64Codec.normalize$3(0, text, t2, t1);
+      else {
+        data = A._Uri__normalize(text, t2, t1, B.List_oFp, true, false);
+        if (data != null)
+          text = B.JSString_methods.replaceRange$3(text, t2, t1, data);
+      }
+      return new A.UriData(text, indices, sourceUri);
+    },
+    UriData__uriEncodeBytes(canonicalTable, bytes, buffer) {
+      var t1, byteOr, i, byte, t2,
+        _s16_ = "0123456789ABCDEF";
+      for (t1 = bytes.length, byteOr = 0, i = 0; i < t1; ++i) {
+        byte = bytes[i];
+        byteOr |= byte;
+        if (byte < 128) {
+          t2 = byte >>> 4;
+          if (!(t2 < 8))
+            return A.ioore(canonicalTable, t2);
+          t2 = (canonicalTable[t2] & 1 << (byte & 15)) !== 0;
+        } else
+          t2 = false;
+        if (t2)
+          buffer._contents += A.Primitives_stringFromCharCode(byte);
+        else {
+          buffer._contents += A.Primitives_stringFromCharCode(37);
+          t2 = byte >>> 4;
+          if (!(t2 < 16))
+            return A.ioore(_s16_, t2);
+          buffer._contents += A.Primitives_stringFromCharCode(_s16_.charCodeAt(t2));
+          buffer._contents += A.Primitives_stringFromCharCode(_s16_.charCodeAt(byte & 15));
+        }
+      }
+      if ((byteOr & 4294967040) !== 0)
+        for (i = 0; i < t1; ++i) {
+          byte = bytes[i];
+          if (byte > 255)
+            throw A.wrapException(A.ArgumentError$value(byte, "non-byte value", null));
+        }
+    },
+    _createTables() {
+      var _i, t1, t2, t3, b,
+        _s77_ = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~!$&'()*+,;=",
+        _s1_ = ".", _s1_0 = ":", _s1_1 = "/", _s1_2 = "\\", _s1_3 = "?", _s1_4 = "#", _s2_ = "/\\",
+        tables = A._setArrayType(new Array(22), type$.JSArray_Uint8List);
+      for (_i = 0; _i < 22; ++_i)
+        tables[_i] = new Uint8Array(96);
+      t1 = new A._createTables_build(tables);
+      t2 = new A._createTables_setChars();
+      t3 = new A._createTables_setRange();
+      b = t1.call$2(0, 225);
+      t2.call$3(b, _s77_, 1);
+      t2.call$3(b, _s1_, 14);
+      t2.call$3(b, _s1_0, 34);
+      t2.call$3(b, _s1_1, 3);
+      t2.call$3(b, _s1_2, 227);
+      t2.call$3(b, _s1_3, 172);
+      t2.call$3(b, _s1_4, 205);
+      b = t1.call$2(14, 225);
+      t2.call$3(b, _s77_, 1);
+      t2.call$3(b, _s1_, 15);
+      t2.call$3(b, _s1_0, 34);
+      t2.call$3(b, _s2_, 234);
+      t2.call$3(b, _s1_3, 172);
+      t2.call$3(b, _s1_4, 205);
+      b = t1.call$2(15, 225);
+      t2.call$3(b, _s77_, 1);
+      t2.call$3(b, "%", 225);
+      t2.call$3(b, _s1_0, 34);
+      t2.call$3(b, _s1_1, 9);
+      t2.call$3(b, _s1_2, 233);
+      t2.call$3(b, _s1_3, 172);
+      t2.call$3(b, _s1_4, 205);
+      b = t1.call$2(1, 225);
+      t2.call$3(b, _s77_, 1);
+      t2.call$3(b, _s1_0, 34);
+      t2.call$3(b, _s1_1, 10);
+      t2.call$3(b, _s1_2, 234);
+      t2.call$3(b, _s1_3, 172);
+      t2.call$3(b, _s1_4, 205);
+      b = t1.call$2(2, 235);
+      t2.call$3(b, _s77_, 139);
+      t2.call$3(b, _s1_1, 131);
+      t2.call$3(b, _s1_2, 131);
+      t2.call$3(b, _s1_, 146);
+      t2.call$3(b, _s1_3, 172);
+      t2.call$3(b, _s1_4, 205);
+      b = t1.call$2(3, 235);
+      t2.call$3(b, _s77_, 11);
+      t2.call$3(b, _s1_1, 68);
+      t2.call$3(b, _s1_2, 68);
+      t2.call$3(b, _s1_, 18);
+      t2.call$3(b, _s1_3, 172);
+      t2.call$3(b, _s1_4, 205);
+      b = t1.call$2(4, 229);
+      t2.call$3(b, _s77_, 5);
+      t3.call$3(b, "AZ", 229);
+      t2.call$3(b, _s1_0, 102);
+      t2.call$3(b, "@", 68);
+      t2.call$3(b, "[", 232);
+      t2.call$3(b, _s1_1, 138);
+      t2.call$3(b, _s1_2, 138);
+      t2.call$3(b, _s1_3, 172);
+      t2.call$3(b, _s1_4, 205);
+      b = t1.call$2(5, 229);
+      t2.call$3(b, _s77_, 5);
+      t3.call$3(b, "AZ", 229);
+      t2.call$3(b, _s1_0, 102);
+      t2.call$3(b, "@", 68);
+      t2.call$3(b, _s1_1, 138);
+      t2.call$3(b, _s1_2, 138);
+      t2.call$3(b, _s1_3, 172);
+      t2.call$3(b, _s1_4, 205);
+      b = t1.call$2(6, 231);
+      t3.call$3(b, "19", 7);
+      t2.call$3(b, "@", 68);
+      t2.call$3(b, _s1_1, 138);
+      t2.call$3(b, _s1_2, 138);
+      t2.call$3(b, _s1_3, 172);
+      t2.call$3(b, _s1_4, 205);
+      b = t1.call$2(7, 231);
+      t3.call$3(b, "09", 7);
+      t2.call$3(b, "@", 68);
+      t2.call$3(b, _s1_1, 138);
+      t2.call$3(b, _s1_2, 138);
+      t2.call$3(b, _s1_3, 172);
+      t2.call$3(b, _s1_4, 205);
+      t2.call$3(t1.call$2(8, 8), "]", 5);
+      b = t1.call$2(9, 235);
+      t2.call$3(b, _s77_, 11);
+      t2.call$3(b, _s1_, 16);
+      t2.call$3(b, _s2_, 234);
+      t2.call$3(b, _s1_3, 172);
+      t2.call$3(b, _s1_4, 205);
+      b = t1.call$2(16, 235);
+      t2.call$3(b, _s77_, 11);
+      t2.call$3(b, _s1_, 17);
+      t2.call$3(b, _s2_, 234);
+      t2.call$3(b, _s1_3, 172);
+      t2.call$3(b, _s1_4, 205);
+      b = t1.call$2(17, 235);
+      t2.call$3(b, _s77_, 11);
+      t2.call$3(b, _s1_1, 9);
+      t2.call$3(b, _s1_2, 233);
+      t2.call$3(b, _s1_3, 172);
+      t2.call$3(b, _s1_4, 205);
+      b = t1.call$2(10, 235);
+      t2.call$3(b, _s77_, 11);
+      t2.call$3(b, _s1_, 18);
+      t2.call$3(b, _s1_1, 10);
+      t2.call$3(b, _s1_2, 234);
+      t2.call$3(b, _s1_3, 172);
+      t2.call$3(b, _s1_4, 205);
+      b = t1.call$2(18, 235);
+      t2.call$3(b, _s77_, 11);
+      t2.call$3(b, _s1_, 19);
+      t2.call$3(b, _s2_, 234);
+      t2.call$3(b, _s1_3, 172);
+      t2.call$3(b, _s1_4, 205);
+      b = t1.call$2(19, 235);
+      t2.call$3(b, _s77_, 11);
+      t2.call$3(b, _s2_, 234);
+      t2.call$3(b, _s1_3, 172);
+      t2.call$3(b, _s1_4, 205);
+      b = t1.call$2(11, 235);
+      t2.call$3(b, _s77_, 11);
+      t2.call$3(b, _s1_1, 10);
+      t2.call$3(b, _s1_2, 234);
+      t2.call$3(b, _s1_3, 172);
+      t2.call$3(b, _s1_4, 205);
+      b = t1.call$2(12, 236);
+      t2.call$3(b, _s77_, 12);
+      t2.call$3(b, _s1_3, 12);
+      t2.call$3(b, _s1_4, 205);
+      b = t1.call$2(13, 237);
+      t2.call$3(b, _s77_, 13);
+      t2.call$3(b, _s1_3, 13);
+      t3.call$3(t1.call$2(20, 245), "az", 21);
+      b = t1.call$2(21, 245);
+      t3.call$3(b, "az", 21);
+      t3.call$3(b, "09", 21);
+      t2.call$3(b, "+-.", 21);
+      return tables;
+    },
+    _scan(uri, start, end, state, indices) {
+      var t1, i, table, char, transition,
+        tables = $.$get$_scannerTables();
+      for (t1 = uri.length, i = start; i < end; ++i) {
+        if (!(state >= 0 && state < tables.length))
+          return A.ioore(tables, state);
+        table = tables[state];
+        if (!(i < t1))
+          return A.ioore(uri, i);
+        char = uri.charCodeAt(i) ^ 96;
+        transition = table[char > 95 ? 31 : char];
+        state = transition & 31;
+        B.JSArray_methods.$indexSet(indices, transition >>> 5, i);
+      }
+      return state;
+    },
+    _SimpleUri__packageNameEnd(uri) {
+      if (uri._schemeEnd === 7 && B.JSString_methods.startsWith$1(uri._uri, "package") && uri._hostStart <= 0)
+        return A._skipPackageNameChars(uri._uri, uri._pathStart, uri._queryStart);
+      return -1;
+    },
+    _skipPackageNameChars(source, start, end) {
+      var t1, i, dots, char;
+      for (t1 = source.length, i = start, dots = 0; i < end; ++i) {
+        if (!(i >= 0 && i < t1))
+          return A.ioore(source, i);
+        char = source.charCodeAt(i);
+        if (char === 47)
+          return dots !== 0 ? i : -1;
+        if (char === 37 || char === 58)
+          return -1;
+        dots |= char ^ 46;
+      }
+      return -1;
+    },
+    _caseInsensitiveCompareStart(prefix, string, start) {
+      var t1, t2, result, i, t3, stringChar, delta, lowerChar;
+      for (t1 = prefix.length, t2 = string.length, result = 0, i = 0; i < t1; ++i) {
+        t3 = start + i;
+        if (!(t3 < t2))
+          return A.ioore(string, t3);
+        stringChar = string.charCodeAt(t3);
+        delta = prefix.charCodeAt(i) ^ stringChar;
+        if (delta !== 0) {
+          if (delta === 32) {
+            lowerChar = stringChar | delta;
+            if (97 <= lowerChar && lowerChar <= 122) {
+              result = 32;
+              continue;
+            }
+          }
+          return -1;
+        }
+      }
+      return result;
+    },
+    NoSuchMethodError_toString_closure: function NoSuchMethodError_toString_closure(t0, t1) {
+      this._box_0 = t0;
+      this.sb = t1;
+    },
+    DateTime: function DateTime(t0, t1) {
+      this._core$_value = t0;
+      this.isUtc = t1;
+    },
+    Duration: function Duration(t0) {
+      this._duration = t0;
+    },
+    Error: function Error() {
+    },
+    AssertionError: function AssertionError(t0) {
+      this.message = t0;
+    },
+    TypeError: function TypeError() {
+    },
+    ArgumentError: function ArgumentError(t0, t1, t2, t3) {
+      var _ = this;
+      _._hasValue = t0;
+      _.invalidValue = t1;
+      _.name = t2;
+      _.message = t3;
+    },
+    RangeError: function RangeError(t0, t1, t2, t3, t4, t5) {
+      var _ = this;
+      _.start = t0;
+      _.end = t1;
+      _._hasValue = t2;
+      _.invalidValue = t3;
+      _.name = t4;
+      _.message = t5;
+    },
+    IndexError: function IndexError(t0, t1, t2, t3, t4) {
+      var _ = this;
+      _.length = t0;
+      _._hasValue = t1;
+      _.invalidValue = t2;
+      _.name = t3;
+      _.message = t4;
+    },
+    NoSuchMethodError: function NoSuchMethodError(t0, t1, t2, t3) {
+      var _ = this;
+      _._core$_receiver = t0;
+      _._core$_memberName = t1;
+      _._core$_arguments = t2;
+      _._namedArguments = t3;
+    },
+    UnsupportedError: function UnsupportedError(t0) {
+      this.message = t0;
+    },
+    UnimplementedError: function UnimplementedError(t0) {
+      this.message = t0;
+    },
+    StateError: function StateError(t0) {
+      this.message = t0;
+    },
+    ConcurrentModificationError: function ConcurrentModificationError(t0) {
+      this.modifiedObject = t0;
+    },
+    OutOfMemoryError: function OutOfMemoryError() {
+    },
+    StackOverflowError: function StackOverflowError() {
+    },
+    _Exception: function _Exception(t0) {
+      this.message = t0;
+    },
+    FormatException: function FormatException(t0, t1, t2) {
+      this.message = t0;
+      this.source = t1;
+      this.offset = t2;
+    },
+    Iterable: function Iterable() {
+    },
+    Null: function Null() {
+    },
+    Object: function Object() {
+    },
+    _StringStackTrace: function _StringStackTrace(t0) {
+      this._stackTrace = t0;
+    },
+    StringBuffer: function StringBuffer(t0) {
+      this._contents = t0;
+    },
+    Uri_splitQueryString_closure: function Uri_splitQueryString_closure(t0) {
+      this.encoding = t0;
+    },
+    Uri__parseIPv4Address_error: function Uri__parseIPv4Address_error(t0) {
+      this.host = t0;
+    },
+    Uri_parseIPv6Address_error: function Uri_parseIPv6Address_error(t0) {
+      this.host = t0;
+    },
+    Uri_parseIPv6Address_parseHex: function Uri_parseIPv6Address_parseHex(t0, t1) {
+      this.error = t0;
+      this.host = t1;
+    },
+    _Uri: function _Uri(t0, t1, t2, t3, t4, t5, t6) {
+      var _ = this;
+      _.scheme = t0;
+      _._userInfo = t1;
+      _._host = t2;
+      _._port = t3;
+      _.path = t4;
+      _._query = t5;
+      _._fragment = t6;
+      _.___Uri_queryParameters_FI = _.___Uri_hashCode_FI = _.___Uri_pathSegments_FI = _.___Uri__text_FI = $;
+    },
+    _Uri__makePath_closure: function _Uri__makePath_closure() {
+    },
+    UriData: function UriData(t0, t1, t2) {
+      this._text = t0;
+      this._separatorIndices = t1;
+      this._uriCache = t2;
+    },
+    _createTables_build: function _createTables_build(t0) {
+      this.tables = t0;
+    },
+    _createTables_setChars: function _createTables_setChars() {
+    },
+    _createTables_setRange: function _createTables_setRange() {
+    },
+    _SimpleUri: function _SimpleUri(t0, t1, t2, t3, t4, t5, t6, t7) {
+      var _ = this;
+      _._uri = t0;
+      _._schemeEnd = t1;
+      _._hostStart = t2;
+      _._portStart = t3;
+      _._pathStart = t4;
+      _._queryStart = t5;
+      _._fragmentStart = t6;
+      _._schemeCache = t7;
+      _._hashCodeCache = null;
+    },
+    _DataUri: function _DataUri(t0, t1, t2, t3, t4, t5, t6) {
+      var _ = this;
+      _.scheme = t0;
+      _._userInfo = t1;
+      _._host = t2;
+      _._port = t3;
+      _.path = t4;
+      _._query = t5;
+      _._fragment = t6;
+      _.___Uri_queryParameters_FI = _.___Uri_hashCode_FI = _.___Uri_pathSegments_FI = _.___Uri__text_FI = $;
+    },
+    HtmlElement: function HtmlElement() {
+    },
+    AccessibleNodeList: function AccessibleNodeList() {
+    },
+    AnchorElement: function AnchorElement() {
+    },
+    AreaElement: function AreaElement() {
+    },
+    Blob: function Blob() {
+    },
+    CharacterData: function CharacterData() {
+    },
+    CssPerspective: function CssPerspective() {
+    },
+    CssRule: function CssRule() {
+    },
+    CssStyleDeclaration: function CssStyleDeclaration() {
+    },
+    CssStyleDeclarationBase: function CssStyleDeclarationBase() {
+    },
+    CssStyleValue: function CssStyleValue() {
+    },
+    CssTransformComponent: function CssTransformComponent() {
+    },
+    CssTransformValue: function CssTransformValue() {
+    },
+    CssUnparsedValue: function CssUnparsedValue() {
+    },
+    DataTransferItemList: function DataTransferItemList() {
+    },
+    DomException: function DomException() {
+    },
+    DomRectList: function DomRectList() {
+    },
+    DomRectReadOnly: function DomRectReadOnly() {
+    },
+    DomStringList: function DomStringList() {
+    },
+    DomTokenList: function DomTokenList() {
+    },
+    Element: function Element() {
+    },
+    EventTarget: function EventTarget() {
+    },
+    File: function File() {
+    },
+    FileList: function FileList() {
+    },
+    FileWriter: function FileWriter() {
+    },
+    FormElement: function FormElement() {
+    },
+    Gamepad: function Gamepad() {
+    },
+    History: function History() {
+    },
+    HtmlCollection: function HtmlCollection() {
+    },
+    Location: function Location() {
+    },
+    MediaList: function MediaList() {
+    },
+    MidiInputMap: function MidiInputMap() {
+    },
+    MidiInputMap_keys_closure: function MidiInputMap_keys_closure(t0) {
+      this.keys = t0;
+    },
+    MidiOutputMap: function MidiOutputMap() {
+    },
+    MidiOutputMap_keys_closure: function MidiOutputMap_keys_closure(t0) {
+      this.keys = t0;
+    },
+    MimeType: function MimeType() {
+    },
+    MimeTypeArray: function MimeTypeArray() {
+    },
+    Node: function Node() {
+    },
+    NodeList: function NodeList() {
+    },
+    Plugin: function Plugin() {
+    },
+    PluginArray: function PluginArray() {
+    },
+    RtcStatsReport: function RtcStatsReport() {
+    },
+    RtcStatsReport_keys_closure: function RtcStatsReport_keys_closure(t0) {
+      this.keys = t0;
+    },
+    SelectElement: function SelectElement() {
+    },
+    SourceBuffer: function SourceBuffer() {
+    },
+    SourceBufferList: function SourceBufferList() {
+    },
+    SpeechGrammar: function SpeechGrammar() {
+    },
+    SpeechGrammarList: function SpeechGrammarList() {
+    },
+    SpeechRecognitionResult: function SpeechRecognitionResult() {
+    },
+    Storage: function Storage() {
+    },
+    Storage_keys_closure: function Storage_keys_closure(t0) {
+      this.keys = t0;
+    },
+    StyleSheet: function StyleSheet() {
+    },
+    TextTrack: function TextTrack() {
+    },
+    TextTrackCue: function TextTrackCue() {
+    },
+    TextTrackCueList: function TextTrackCueList() {
+    },
+    TextTrackList: function TextTrackList() {
+    },
+    TimeRanges: function TimeRanges() {
+    },
+    Touch: function Touch() {
+    },
+    TouchList: function TouchList() {
+    },
+    TrackDefaultList: function TrackDefaultList() {
+    },
+    Url: function Url() {
+    },
+    VideoTrackList: function VideoTrackList() {
+    },
+    _CssRuleList: function _CssRuleList() {
+    },
+    _DomRect: function _DomRect() {
+    },
+    _GamepadList: function _GamepadList() {
+    },
+    _NamedNodeMap: function _NamedNodeMap() {
+    },
+    _SpeechRecognitionResultList: function _SpeechRecognitionResultList() {
+    },
+    _StyleSheetList: function _StyleSheetList() {
+    },
+    ImmutableListMixin: function ImmutableListMixin() {
+    },
+    FixedSizeListIterator: function FixedSizeListIterator(t0, t1, t2) {
+      var _ = this;
+      _._array = t0;
+      _._html$_length = t1;
+      _._position = -1;
+      _._html$_current = null;
+      _.$ti = t2;
+    },
+    _CssStyleDeclaration_JavaScriptObject_CssStyleDeclarationBase: function _CssStyleDeclaration_JavaScriptObject_CssStyleDeclarationBase() {
+    },
+    _DomRectList_JavaScriptObject_ListMixin: function _DomRectList_JavaScriptObject_ListMixin() {
+    },
+    _DomRectList_JavaScriptObject_ListMixin_ImmutableListMixin: function _DomRectList_JavaScriptObject_ListMixin_ImmutableListMixin() {
+    },
+    _DomStringList_JavaScriptObject_ListMixin: function _DomStringList_JavaScriptObject_ListMixin() {
+    },
+    _DomStringList_JavaScriptObject_ListMixin_ImmutableListMixin: function _DomStringList_JavaScriptObject_ListMixin_ImmutableListMixin() {
+    },
+    _FileList_JavaScriptObject_ListMixin: function _FileList_JavaScriptObject_ListMixin() {
+    },
+    _FileList_JavaScriptObject_ListMixin_ImmutableListMixin: function _FileList_JavaScriptObject_ListMixin_ImmutableListMixin() {
+    },
+    _HtmlCollection_JavaScriptObject_ListMixin: function _HtmlCollection_JavaScriptObject_ListMixin() {
+    },
+    _HtmlCollection_JavaScriptObject_ListMixin_ImmutableListMixin: function _HtmlCollection_JavaScriptObject_ListMixin_ImmutableListMixin() {
+    },
+    _MidiInputMap_JavaScriptObject_MapMixin: function _MidiInputMap_JavaScriptObject_MapMixin() {
+    },
+    _MidiOutputMap_JavaScriptObject_MapMixin: function _MidiOutputMap_JavaScriptObject_MapMixin() {
+    },
+    _MimeTypeArray_JavaScriptObject_ListMixin: function _MimeTypeArray_JavaScriptObject_ListMixin() {
+    },
+    _MimeTypeArray_JavaScriptObject_ListMixin_ImmutableListMixin: function _MimeTypeArray_JavaScriptObject_ListMixin_ImmutableListMixin() {
+    },
+    _NodeList_JavaScriptObject_ListMixin: function _NodeList_JavaScriptObject_ListMixin() {
+    },
+    _NodeList_JavaScriptObject_ListMixin_ImmutableListMixin: function _NodeList_JavaScriptObject_ListMixin_ImmutableListMixin() {
+    },
+    _PluginArray_JavaScriptObject_ListMixin: function _PluginArray_JavaScriptObject_ListMixin() {
+    },
+    _PluginArray_JavaScriptObject_ListMixin_ImmutableListMixin: function _PluginArray_JavaScriptObject_ListMixin_ImmutableListMixin() {
+    },
+    _RtcStatsReport_JavaScriptObject_MapMixin: function _RtcStatsReport_JavaScriptObject_MapMixin() {
+    },
+    _SourceBufferList_EventTarget_ListMixin: function _SourceBufferList_EventTarget_ListMixin() {
+    },
+    _SourceBufferList_EventTarget_ListMixin_ImmutableListMixin: function _SourceBufferList_EventTarget_ListMixin_ImmutableListMixin() {
+    },
+    _SpeechGrammarList_JavaScriptObject_ListMixin: function _SpeechGrammarList_JavaScriptObject_ListMixin() {
+    },
+    _SpeechGrammarList_JavaScriptObject_ListMixin_ImmutableListMixin: function _SpeechGrammarList_JavaScriptObject_ListMixin_ImmutableListMixin() {
+    },
+    _Storage_JavaScriptObject_MapMixin: function _Storage_JavaScriptObject_MapMixin() {
+    },
+    _TextTrackCueList_JavaScriptObject_ListMixin: function _TextTrackCueList_JavaScriptObject_ListMixin() {
+    },
+    _TextTrackCueList_JavaScriptObject_ListMixin_ImmutableListMixin: function _TextTrackCueList_JavaScriptObject_ListMixin_ImmutableListMixin() {
+    },
+    _TextTrackList_EventTarget_ListMixin: function _TextTrackList_EventTarget_ListMixin() {
+    },
+    _TextTrackList_EventTarget_ListMixin_ImmutableListMixin: function _TextTrackList_EventTarget_ListMixin_ImmutableListMixin() {
+    },
+    _TouchList_JavaScriptObject_ListMixin: function _TouchList_JavaScriptObject_ListMixin() {
+    },
+    _TouchList_JavaScriptObject_ListMixin_ImmutableListMixin: function _TouchList_JavaScriptObject_ListMixin_ImmutableListMixin() {
+    },
+    __CssRuleList_JavaScriptObject_ListMixin: function __CssRuleList_JavaScriptObject_ListMixin() {
+    },
+    __CssRuleList_JavaScriptObject_ListMixin_ImmutableListMixin: function __CssRuleList_JavaScriptObject_ListMixin_ImmutableListMixin() {
+    },
+    __GamepadList_JavaScriptObject_ListMixin: function __GamepadList_JavaScriptObject_ListMixin() {
+    },
+    __GamepadList_JavaScriptObject_ListMixin_ImmutableListMixin: function __GamepadList_JavaScriptObject_ListMixin_ImmutableListMixin() {
+    },
+    __NamedNodeMap_JavaScriptObject_ListMixin: function __NamedNodeMap_JavaScriptObject_ListMixin() {
+    },
+    __NamedNodeMap_JavaScriptObject_ListMixin_ImmutableListMixin: function __NamedNodeMap_JavaScriptObject_ListMixin_ImmutableListMixin() {
+    },
+    __SpeechRecognitionResultList_JavaScriptObject_ListMixin: function __SpeechRecognitionResultList_JavaScriptObject_ListMixin() {
+    },
+    __SpeechRecognitionResultList_JavaScriptObject_ListMixin_ImmutableListMixin: function __SpeechRecognitionResultList_JavaScriptObject_ListMixin_ImmutableListMixin() {
+    },
+    __StyleSheetList_JavaScriptObject_ListMixin: function __StyleSheetList_JavaScriptObject_ListMixin() {
+    },
+    __StyleSheetList_JavaScriptObject_ListMixin_ImmutableListMixin: function __StyleSheetList_JavaScriptObject_ListMixin_ImmutableListMixin() {
+    },
+    _convertDartFunctionFast(f) {
+      var ret,
+        existing = f.$dart_jsFunction;
+      if (existing != null)
+        return existing;
+      ret = function(_call, f) {
+        return function() {
+          return _call(f, Array.prototype.slice.apply(arguments));
+        };
+      }(A._callDartFunctionFast, f);
+      ret[$.$get$DART_CLOSURE_PROPERTY_NAME()] = f;
+      f.$dart_jsFunction = ret;
+      return ret;
+    },
+    _callDartFunctionFast(callback, $arguments) {
+      type$.List_dynamic._as($arguments);
+      type$.Function._as(callback);
+      return A.Primitives_applyFunction(callback, $arguments, null);
+    },
+    allowInterop(f, $F) {
+      if (typeof f == "function")
+        return f;
+      else
+        return $F._as(A._convertDartFunctionFast(f));
+    },
+    _noJsifyRequired(o) {
+      return o == null || A._isBool(o) || typeof o == "number" || typeof o == "string" || type$.Int8List._is(o) || type$.Uint8List._is(o) || type$.Uint8ClampedList._is(o) || type$.Int16List._is(o) || type$.Uint16List._is(o) || type$.Int32List._is(o) || type$.Uint32List._is(o) || type$.Float32List._is(o) || type$.Float64List._is(o) || type$.ByteBuffer._is(o) || type$.ByteData._is(o);
+    },
+    jsify(object) {
+      if (A._noJsifyRequired(object))
+        return object;
+      return new A.jsify__convert(new A._IdentityHashMap(type$._IdentityHashMap_of_nullable_Object_and_nullable_Object)).call$1(object);
+    },
+    getProperty(o, $name, $T) {
+      return $T._as(o[$name]);
+    },
+    callMethod(o, method, args, $T) {
+      return $T._as(o[method].apply(o, args));
+    },
+    callConstructor(constr, $arguments, $T) {
+      var args, factoryFunction;
+      if ($arguments instanceof Array)
+        switch ($arguments.length) {
+          case 0:
+            return $T._as(new constr());
+          case 1:
+            return $T._as(new constr($arguments[0]));
+          case 2:
+            return $T._as(new constr($arguments[0], $arguments[1]));
+          case 3:
+            return $T._as(new constr($arguments[0], $arguments[1], $arguments[2]));
+          case 4:
+            return $T._as(new constr($arguments[0], $arguments[1], $arguments[2], $arguments[3]));
+        }
+      args = [null];
+      B.JSArray_methods.addAll$1(args, $arguments);
+      factoryFunction = constr.bind.apply(constr, args);
+      String(factoryFunction);
+      return $T._as(new factoryFunction());
+    },
+    promiseToFuture(jsPromise, $T) {
+      var t1 = new A._Future($.Zone__current, $T._eval$1("_Future<0>")),
+        completer = new A._AsyncCompleter(t1, $T._eval$1("_AsyncCompleter<0>"));
+      jsPromise.then(A.convertDartClosureToJS(new A.promiseToFuture_closure(completer, $T), 1), A.convertDartClosureToJS(new A.promiseToFuture_closure0(completer), 1));
+      return t1;
+    },
+    _noDartifyRequired(o) {
+      return o == null || typeof o === "boolean" || typeof o === "number" || typeof o === "string" || o instanceof Int8Array || o instanceof Uint8Array || o instanceof Uint8ClampedArray || o instanceof Int16Array || o instanceof Uint16Array || o instanceof Int32Array || o instanceof Uint32Array || o instanceof Float32Array || o instanceof Float64Array || o instanceof ArrayBuffer || o instanceof DataView;
+    },
+    dartify(o) {
+      if (A._noDartifyRequired(o))
+        return o;
+      return new A.dartify_convert(new A._IdentityHashMap(type$._IdentityHashMap_of_nullable_Object_and_nullable_Object)).call$1(o);
+    },
+    jsify__convert: function jsify__convert(t0) {
+      this._convertedObjects = t0;
+    },
+    promiseToFuture_closure: function promiseToFuture_closure(t0, t1) {
+      this.completer = t0;
+      this.T = t1;
+    },
+    promiseToFuture_closure0: function promiseToFuture_closure0(t0) {
+      this.completer = t0;
+    },
+    dartify_convert: function dartify_convert(t0) {
+      this._convertedObjects = t0;
+    },
+    NullRejectionException: function NullRejectionException(t0) {
+      this.isUndefined = t0;
+    },
+    Length: function Length() {
+    },
+    LengthList: function LengthList() {
+    },
+    Number: function Number() {
+    },
+    NumberList: function NumberList() {
+    },
+    PointList: function PointList() {
+    },
+    StringList: function StringList() {
+    },
+    Transform: function Transform() {
+    },
+    TransformList: function TransformList() {
+    },
+    _LengthList_JavaScriptObject_ListMixin: function _LengthList_JavaScriptObject_ListMixin() {
+    },
+    _LengthList_JavaScriptObject_ListMixin_ImmutableListMixin: function _LengthList_JavaScriptObject_ListMixin_ImmutableListMixin() {
+    },
+    _NumberList_JavaScriptObject_ListMixin: function _NumberList_JavaScriptObject_ListMixin() {
+    },
+    _NumberList_JavaScriptObject_ListMixin_ImmutableListMixin: function _NumberList_JavaScriptObject_ListMixin_ImmutableListMixin() {
+    },
+    _StringList_JavaScriptObject_ListMixin: function _StringList_JavaScriptObject_ListMixin() {
+    },
+    _StringList_JavaScriptObject_ListMixin_ImmutableListMixin: function _StringList_JavaScriptObject_ListMixin_ImmutableListMixin() {
+    },
+    _TransformList_JavaScriptObject_ListMixin: function _TransformList_JavaScriptObject_ListMixin() {
+    },
+    _TransformList_JavaScriptObject_ListMixin_ImmutableListMixin: function _TransformList_JavaScriptObject_ListMixin_ImmutableListMixin() {
+    },
+    AudioBuffer: function AudioBuffer() {
+    },
+    AudioParamMap: function AudioParamMap() {
+    },
+    AudioParamMap_keys_closure: function AudioParamMap_keys_closure(t0) {
+      this.keys = t0;
+    },
+    AudioTrackList: function AudioTrackList() {
+    },
+    BaseAudioContext: function BaseAudioContext() {
+    },
+    OfflineAudioContext: function OfflineAudioContext() {
+    },
+    _AudioParamMap_JavaScriptObject_MapMixin: function _AudioParamMap_JavaScriptObject_MapMixin() {
+    },
+    NullStreamSink: function NullStreamSink(t0, t1) {
+      var _ = this;
+      _.done = t0;
+      _._addingStream = _._null_stream_sink$_closed = false;
+      _.$ti = t1;
+    },
+    NullStreamSink_addStream_closure: function NullStreamSink_addStream_closure(t0) {
+      this.$this = t0;
+    },
+    Context_Context(style) {
+      return new A.Context(style, ".");
+    },
+    _parseUri(uri) {
+      return uri;
+    },
+    _validateArgList(method, args) {
+      var numArgs, i, numArgs0, message, t1, t2, t3, t4;
+      for (numArgs = args.length, i = 1; i < numArgs; ++i) {
+        if (args[i] == null || args[i - 1] != null)
+          continue;
+        for (; numArgs >= 1; numArgs = numArgs0) {
+          numArgs0 = numArgs - 1;
+          if (args[numArgs0] != null)
+            break;
+        }
+        message = new A.StringBuffer("");
+        t1 = "" + (method + "(");
+        message._contents = t1;
+        t2 = A._arrayInstanceType(args);
+        t3 = t2._eval$1("SubListIterable<1>");
+        t4 = new A.SubListIterable(args, 0, numArgs, t3);
+        t4.SubListIterable$3(args, 0, numArgs, t2._precomputed1);
+        t3 = t1 + new A.MappedListIterable(t4, t3._eval$1("String(ListIterable.E)")._as(new A._validateArgList_closure()), t3._eval$1("MappedListIterable<ListIterable.E,String>")).join$1(0, ", ");
+        message._contents = t3;
+        message._contents = t3 + ("): part " + (i - 1) + " was null, but part " + i + " was not.");
+        throw A.wrapException(A.ArgumentError$(message.toString$0(0), null));
+      }
+    },
+    Context: function Context(t0, t1) {
+      this.style = t0;
+      this._context$_current = t1;
+    },
+    Context_joinAll_closure: function Context_joinAll_closure() {
+    },
+    Context_split_closure: function Context_split_closure() {
+    },
+    _validateArgList_closure: function _validateArgList_closure() {
+    },
+    InternalStyle: function InternalStyle() {
+    },
+    ParsedPath_ParsedPath$parse(path, style) {
+      var t1, parts, separators, t2, start, i,
+        root = style.getRoot$1(path);
+      style.isRootRelative$1(path);
+      if (root != null)
+        path = B.JSString_methods.substring$1(path, root.length);
+      t1 = type$.JSArray_String;
+      parts = A._setArrayType([], t1);
+      separators = A._setArrayType([], t1);
+      t1 = path.length;
+      if (t1 !== 0) {
+        if (0 >= t1)
+          return A.ioore(path, 0);
+        t2 = style.isSeparator$1(path.charCodeAt(0));
+      } else
+        t2 = false;
+      if (t2) {
+        if (0 >= t1)
+          return A.ioore(path, 0);
+        B.JSArray_methods.add$1(separators, path[0]);
+        start = 1;
+      } else {
+        B.JSArray_methods.add$1(separators, "");
+        start = 0;
+      }
+      for (i = start; i < t1; ++i)
+        if (style.isSeparator$1(path.charCodeAt(i))) {
+          B.JSArray_methods.add$1(parts, B.JSString_methods.substring$2(path, start, i));
+          B.JSArray_methods.add$1(separators, path[i]);
+          start = i + 1;
+        }
+      if (start < t1) {
+        B.JSArray_methods.add$1(parts, B.JSString_methods.substring$1(path, start));
+        B.JSArray_methods.add$1(separators, "");
+      }
+      return new A.ParsedPath(style, root, parts, separators);
+    },
+    ParsedPath: function ParsedPath(t0, t1, t2, t3) {
+      var _ = this;
+      _.style = t0;
+      _.root = t1;
+      _.parts = t2;
+      _.separators = t3;
+    },
+    PathException$(message) {
+      return new A.PathException(message);
+    },
+    PathException: function PathException(t0) {
+      this.message = t0;
+    },
+    Style__getPlatformStyle() {
+      if (A.Uri_base().get$scheme() !== "file")
+        return $.$get$Style_url();
+      var t1 = A.Uri_base();
+      if (!B.JSString_methods.endsWith$1(t1.get$path(t1), "/"))
+        return $.$get$Style_url();
+      if (A._Uri__Uri(null, "a/b", null, null).toFilePath$0() === "a\\b")
+        return $.$get$Style_windows();
+      return $.$get$Style_posix();
+    },
+    Style: function Style() {
+    },
+    PosixStyle: function PosixStyle(t0, t1, t2) {
+      this.separatorPattern = t0;
+      this.needsSeparatorPattern = t1;
+      this.rootPattern = t2;
+    },
+    UrlStyle: function UrlStyle(t0, t1, t2, t3) {
+      var _ = this;
+      _.separatorPattern = t0;
+      _.needsSeparatorPattern = t1;
+      _.rootPattern = t2;
+      _.relativeRootPattern = t3;
+    },
+    WindowsStyle: function WindowsStyle(t0, t1, t2, t3) {
+      var _ = this;
+      _.separatorPattern = t0;
+      _.needsSeparatorPattern = t1;
+      _.rootPattern = t2;
+      _.relativeRootPattern = t3;
+    },
+    WindowsStyle_absolutePathToUri_closure: function WindowsStyle_absolutePathToUri_closure() {
+    },
+    Chain_Chain$parse(chain) {
+      var t1, t2,
+        _s51_ = string$.x3d_____;
+      if (chain.length === 0)
+        return new A.Chain(A.List_List$unmodifiable(A._setArrayType([], type$.JSArray_Trace), type$.Trace));
+      t1 = $.$get$vmChainGap();
+      if (B.JSString_methods.contains$1(chain, t1)) {
+        t1 = B.JSString_methods.split$1(chain, t1);
+        t2 = A._arrayInstanceType(t1);
+        return new A.Chain(A.List_List$unmodifiable(new A.MappedIterable(new A.WhereIterable(t1, t2._eval$1("bool(1)")._as(new A.Chain_Chain$parse_closure()), t2._eval$1("WhereIterable<1>")), t2._eval$1("Trace(1)")._as(A.trace_Trace___parseVM_tearOff$closure()), t2._eval$1("MappedIterable<1,Trace>")), type$.Trace));
+      }
+      if (!B.JSString_methods.contains$1(chain, _s51_))
+        return new A.Chain(A.List_List$unmodifiable(A._setArrayType([A.Trace_Trace$parse(chain)], type$.JSArray_Trace), type$.Trace));
+      return new A.Chain(A.List_List$unmodifiable(new A.MappedListIterable(A._setArrayType(chain.split(_s51_), type$.JSArray_String), type$.Trace_Function_String._as(A.trace_Trace___parseFriendly_tearOff$closure()), type$.MappedListIterable_String_Trace), type$.Trace));
+    },
+    Chain: function Chain(t0) {
+      this.traces = t0;
+    },
+    Chain_Chain$parse_closure: function Chain_Chain$parse_closure() {
+    },
+    Chain_toTrace_closure: function Chain_toTrace_closure() {
+    },
+    Chain_toString_closure0: function Chain_toString_closure0() {
+    },
+    Chain_toString__closure0: function Chain_toString__closure0() {
+    },
+    Chain_toString_closure: function Chain_toString_closure(t0) {
+      this.longest = t0;
+    },
+    Chain_toString__closure: function Chain_toString__closure(t0) {
+      this.longest = t0;
+    },
+    Frame___parseVM_tearOff(frame) {
+      return A.Frame_Frame$parseVM(A._asString(frame));
+    },
+    Frame_Frame$parseVM(frame) {
+      return A.Frame__catchFormatException(frame, new A.Frame_Frame$parseVM_closure(frame));
+    },
+    Frame___parseV8_tearOff(frame) {
+      return A.Frame_Frame$parseV8(A._asString(frame));
+    },
+    Frame_Frame$parseV8(frame) {
+      return A.Frame__catchFormatException(frame, new A.Frame_Frame$parseV8_closure(frame));
+    },
+    Frame_Frame$_parseFirefoxEval(frame) {
+      return A.Frame__catchFormatException(frame, new A.Frame_Frame$_parseFirefoxEval_closure(frame));
+    },
+    Frame___parseFirefox_tearOff(frame) {
+      return A.Frame_Frame$parseFirefox(A._asString(frame));
+    },
+    Frame_Frame$parseFirefox(frame) {
+      return A.Frame__catchFormatException(frame, new A.Frame_Frame$parseFirefox_closure(frame));
+    },
+    Frame___parseFriendly_tearOff(frame) {
+      return A.Frame_Frame$parseFriendly(A._asString(frame));
+    },
+    Frame_Frame$parseFriendly(frame) {
+      return A.Frame__catchFormatException(frame, new A.Frame_Frame$parseFriendly_closure(frame));
+    },
+    Frame__uriOrPathToUri(uriOrPath) {
+      if (B.JSString_methods.contains$1(uriOrPath, $.$get$Frame__uriRegExp()))
+        return A.Uri_parse(uriOrPath);
+      else if (B.JSString_methods.contains$1(uriOrPath, $.$get$Frame__windowsRegExp()))
+        return A._Uri__Uri$file(uriOrPath, true);
+      else if (B.JSString_methods.startsWith$1(uriOrPath, "/"))
+        return A._Uri__Uri$file(uriOrPath, false);
+      if (B.JSString_methods.contains$1(uriOrPath, "\\"))
+        return $.$get$windows().toUri$1(uriOrPath);
+      return A.Uri_parse(uriOrPath);
+    },
+    Frame__catchFormatException(text, body) {
+      var t1, exception;
+      try {
+        t1 = body.call$0();
+        return t1;
+      } catch (exception) {
+        if (A.unwrapException(exception) instanceof A.FormatException)
+          return new A.UnparsedFrame(A._Uri__Uri(null, "unparsed", null, null), text);
+        else
+          throw exception;
+      }
+    },
+    Frame: function Frame(t0, t1, t2, t3) {
+      var _ = this;
+      _.uri = t0;
+      _.line = t1;
+      _.column = t2;
+      _.member = t3;
+    },
+    Frame_Frame$parseVM_closure: function Frame_Frame$parseVM_closure(t0) {
+      this.frame = t0;
+    },
+    Frame_Frame$parseV8_closure: function Frame_Frame$parseV8_closure(t0) {
+      this.frame = t0;
+    },
+    Frame_Frame$parseV8_closure_parseLocation: function Frame_Frame$parseV8_closure_parseLocation(t0) {
+      this.frame = t0;
+    },
+    Frame_Frame$_parseFirefoxEval_closure: function Frame_Frame$_parseFirefoxEval_closure(t0) {
+      this.frame = t0;
+    },
+    Frame_Frame$parseFirefox_closure: function Frame_Frame$parseFirefox_closure(t0) {
+      this.frame = t0;
+    },
+    Frame_Frame$parseFriendly_closure: function Frame_Frame$parseFriendly_closure(t0) {
+      this.frame = t0;
+    },
+    LazyTrace: function LazyTrace(t0) {
+      this._thunk = t0;
+      this.__LazyTrace__trace_FI = $;
+    },
+    LazyTrace_terse_closure: function LazyTrace_terse_closure(t0) {
+      this.$this = t0;
+    },
+    Trace_Trace$from(trace) {
+      if (type$.Trace._is(trace))
+        return trace;
+      if (trace instanceof A.Chain)
+        return trace.toTrace$0();
+      return new A.LazyTrace(new A.Trace_Trace$from_closure(trace));
+    },
+    Trace_Trace$parse(trace) {
+      var error, t1, exception;
+      try {
+        if (trace.length === 0) {
+          t1 = A.Trace$(A._setArrayType([], type$.JSArray_Frame), null);
+          return t1;
+        }
+        if (B.JSString_methods.contains$1(trace, $.$get$_v8Trace())) {
+          t1 = A.Trace$parseV8(trace);
+          return t1;
+        }
+        if (B.JSString_methods.contains$1(trace, "\tat ")) {
+          t1 = A.Trace$parseJSCore(trace);
+          return t1;
+        }
+        if (B.JSString_methods.contains$1(trace, $.$get$_firefoxSafariTrace()) || B.JSString_methods.contains$1(trace, $.$get$_firefoxEvalTrace())) {
+          t1 = A.Trace$parseFirefox(trace);
+          return t1;
+        }
+        if (B.JSString_methods.contains$1(trace, string$.x3d_____)) {
+          t1 = A.Chain_Chain$parse(trace).toTrace$0();
+          return t1;
+        }
+        if (B.JSString_methods.contains$1(trace, $.$get$_friendlyTrace())) {
+          t1 = A.Trace$parseFriendly(trace);
+          return t1;
+        }
+        t1 = A.Trace$parseVM(trace);
+        return t1;
+      } catch (exception) {
+        t1 = A.unwrapException(exception);
+        if (t1 instanceof A.FormatException) {
+          error = t1;
+          throw A.wrapException(A.FormatException$(error.message + "\nStack trace:\n" + trace, null, null));
+        } else
+          throw exception;
+      }
+    },
+    Trace___parseVM_tearOff(trace) {
+      return A.Trace$parseVM(A._asString(trace));
+    },
+    Trace$parseVM(trace) {
+      var t1 = A.List_List$unmodifiable(A.Trace__parseVM(trace), type$.Frame);
+      return new A.Trace(t1, new A._StringStackTrace(trace));
+    },
+    Trace__parseVM(trace) {
+      var $frames,
+        t1 = B.JSString_methods.trim$0(trace),
+        t2 = $.$get$vmChainGap(),
+        t3 = type$.WhereIterable_String,
+        lines = new A.WhereIterable(A._setArrayType(A.stringReplaceAllUnchecked(t1, t2, "").split("\n"), type$.JSArray_String), type$.bool_Function_String._as(new A.Trace__parseVM_closure()), t3);
+      if (!lines.get$iterator(lines).moveNext$0())
+        return A._setArrayType([], type$.JSArray_Frame);
+      t1 = A.TakeIterable_TakeIterable(lines, lines.get$length(lines) - 1, t3._eval$1("Iterable.E"));
+      t2 = A._instanceType(t1);
+      t2 = A.MappedIterable_MappedIterable(t1, t2._eval$1("Frame(Iterable.E)")._as(A.frame_Frame___parseVM_tearOff$closure()), t2._eval$1("Iterable.E"), type$.Frame);
+      $frames = A.List_List$of(t2, true, A._instanceType(t2)._eval$1("Iterable.E"));
+      if (!J.endsWith$1$s(lines.get$last(lines), ".da"))
+        B.JSArray_methods.add$1($frames, A.Frame_Frame$parseVM(lines.get$last(lines)));
+      return $frames;
+    },
+    Trace$parseV8(trace) {
+      var t2, t3,
+        t1 = A.SubListIterable$(A._setArrayType(trace.split("\n"), type$.JSArray_String), 1, null, type$.String);
+      t1 = t1.super$Iterable$skipWhile(0, t1.$ti._eval$1("bool(ListIterable.E)")._as(new A.Trace$parseV8_closure()));
+      t2 = type$.Frame;
+      t3 = t1.$ti;
+      t2 = A.List_List$unmodifiable(A.MappedIterable_MappedIterable(t1, t3._eval$1("Frame(Iterable.E)")._as(A.frame_Frame___parseV8_tearOff$closure()), t3._eval$1("Iterable.E"), t2), t2);
+      return new A.Trace(t2, new A._StringStackTrace(trace));
+    },
+    Trace$parseJSCore(trace) {
+      var t1 = A.List_List$unmodifiable(new A.MappedIterable(new A.WhereIterable(A._setArrayType(trace.split("\n"), type$.JSArray_String), type$.bool_Function_String._as(new A.Trace$parseJSCore_closure()), type$.WhereIterable_String), type$.Frame_Function_String._as(A.frame_Frame___parseV8_tearOff$closure()), type$.MappedIterable_String_Frame), type$.Frame);
+      return new A.Trace(t1, new A._StringStackTrace(trace));
+    },
+    Trace$parseFirefox(trace) {
+      var t1 = A.List_List$unmodifiable(new A.MappedIterable(new A.WhereIterable(A._setArrayType(B.JSString_methods.trim$0(trace).split("\n"), type$.JSArray_String), type$.bool_Function_String._as(new A.Trace$parseFirefox_closure()), type$.WhereIterable_String), type$.Frame_Function_String._as(A.frame_Frame___parseFirefox_tearOff$closure()), type$.MappedIterable_String_Frame), type$.Frame);
+      return new A.Trace(t1, new A._StringStackTrace(trace));
+    },
+    Trace___parseFriendly_tearOff(trace) {
+      return A.Trace$parseFriendly(A._asString(trace));
+    },
+    Trace$parseFriendly(trace) {
+      var t1 = trace.length === 0 ? A._setArrayType([], type$.JSArray_Frame) : new A.MappedIterable(new A.WhereIterable(A._setArrayType(B.JSString_methods.trim$0(trace).split("\n"), type$.JSArray_String), type$.bool_Function_String._as(new A.Trace$parseFriendly_closure()), type$.WhereIterable_String), type$.Frame_Function_String._as(A.frame_Frame___parseFriendly_tearOff$closure()), type$.MappedIterable_String_Frame);
+      t1 = A.List_List$unmodifiable(t1, type$.Frame);
+      return new A.Trace(t1, new A._StringStackTrace(trace));
+    },
+    Trace$($frames, original) {
+      var t1 = A.List_List$unmodifiable($frames, type$.Frame);
+      return new A.Trace(t1, new A._StringStackTrace(original == null ? "" : original));
+    },
+    Trace: function Trace(t0, t1) {
+      this.frames = t0;
+      this.original = t1;
+    },
+    Trace_Trace$from_closure: function Trace_Trace$from_closure(t0) {
+      this.trace = t0;
+    },
+    Trace__parseVM_closure: function Trace__parseVM_closure() {
+    },
+    Trace$parseV8_closure: function Trace$parseV8_closure() {
+    },
+    Trace$parseJSCore_closure: function Trace$parseJSCore_closure() {
+    },
+    Trace$parseFirefox_closure: function Trace$parseFirefox_closure() {
+    },
+    Trace$parseFriendly_closure: function Trace$parseFriendly_closure() {
+    },
+    Trace_terse_closure: function Trace_terse_closure() {
+    },
+    Trace_foldFrames_closure: function Trace_foldFrames_closure(t0) {
+      this.oldPredicate = t0;
+    },
+    Trace_foldFrames_closure0: function Trace_foldFrames_closure0(t0) {
+      this._box_0 = t0;
+    },
+    Trace_toString_closure0: function Trace_toString_closure0() {
+    },
+    Trace_toString_closure: function Trace_toString_closure(t0) {
+      this.longest = t0;
+    },
+    UnparsedFrame: function UnparsedFrame(t0, t1) {
+      this.uri = t0;
+      this.member = t1;
+    },
+    GuaranteeChannel$(innerStream, innerSink, allowSinkErrors, $T) {
+      var t2, t1 = {};
+      t1.innerStream = innerStream;
+      t2 = new A.GuaranteeChannel($T._eval$1("GuaranteeChannel<0>"));
+      t2.GuaranteeChannel$3$allowSinkErrors(innerSink, true, t1, $T);
+      return t2;
+    },
+    GuaranteeChannel: function GuaranteeChannel(t0) {
+      var _ = this;
+      _.__GuaranteeChannel__streamController_F = _.__GuaranteeChannel__sink_F = $;
+      _._subscription = null;
+      _._disconnected = false;
+      _.$ti = t0;
+    },
+    GuaranteeChannel_closure: function GuaranteeChannel_closure(t0, t1, t2) {
+      this._box_0 = t0;
+      this.$this = t1;
+      this.T = t2;
+    },
+    GuaranteeChannel__closure: function GuaranteeChannel__closure(t0) {
+      this.$this = t0;
+    },
+    _GuaranteeSink: function _GuaranteeSink(t0, t1, t2, t3, t4) {
+      var _ = this;
+      _._inner = t0;
+      _._channel = t1;
+      _._doneCompleter = t2;
+      _._closed = _._disconnected = false;
+      _._addStreamCompleter = _._addStreamSubscription = null;
+      _._allowErrors = t3;
+      _.$ti = t4;
+    },
+    _GuaranteeSink_addStream_closure: function _GuaranteeSink_addStream_closure(t0) {
+      this.$this = t0;
+    },
+    _MultiChannel$(inner, $T) {
+      var t1 = type$.int;
+      t1 = new A._MultiChannel(inner, A.StreamChannelController$(true, $T), A.LinkedHashMap_LinkedHashMap$_empty(t1, $T._eval$1("StreamChannelController<0>")), A.LinkedHashSet_LinkedHashSet$_empty(t1), A.LinkedHashSet_LinkedHashSet$_empty(t1), $T._eval$1("_MultiChannel<0>"));
+      t1._MultiChannel$1(inner, $T);
+      return t1;
+    },
+    _MultiChannel: function _MultiChannel(t0, t1, t2, t3, t4, t5) {
+      var _ = this;
+      _._multi_channel$_inner = t0;
+      _._innerStreamSubscription = null;
+      _._mainController = t1;
+      _._controllers = t2;
+      _._pendingIds = t3;
+      _._closedIds = t4;
+      _._nextId = 1;
+      _.$ti = t5;
+    },
+    _MultiChannel_closure: function _MultiChannel_closure(t0, t1) {
+      this.$this = t0;
+      this.T = t1;
+    },
+    _MultiChannel_closure0: function _MultiChannel_closure0(t0) {
+      this.$this = t0;
+    },
+    _MultiChannel_closure1: function _MultiChannel_closure1(t0, t1) {
+      this.$this = t0;
+      this.T = t1;
+    },
+    _MultiChannel__closure: function _MultiChannel__closure(t0, t1, t2) {
+      this.$this = t0;
+      this.id = t1;
+      this.T = t2;
+    },
+    _MultiChannel_virtualChannel_closure: function _MultiChannel_virtualChannel_closure(t0, t1) {
+      this._box_0 = t0;
+      this.$this = t1;
+    },
+    _MultiChannel_virtualChannel_closure0: function _MultiChannel_virtualChannel_closure0(t0, t1) {
+      this._box_0 = t0;
+      this.$this = t1;
+    },
+    VirtualChannel: function VirtualChannel(t0, t1, t2, t3) {
+      var _ = this;
+      _._parent = t0;
+      _.stream = t1;
+      _.sink = t2;
+      _.$ti = t3;
+    },
+    StreamChannelController$(sync, $T) {
+      var _null = null,
+        t1 = new A.StreamChannelController($T._eval$1("StreamChannelController<0>")),
+        localToForeignController = A.StreamController_StreamController(_null, _null, true, $T),
+        foreignToLocalController = A.StreamController_StreamController(_null, _null, true, $T),
+        t2 = A._instanceType(foreignToLocalController),
+        t3 = A._instanceType(localToForeignController),
+        t4 = $T._eval$1("StreamChannel<0>");
+      t1.set$__StreamChannelController__local_F(t4._as(A.GuaranteeChannel$(new A._ControllerStream(foreignToLocalController, t2._eval$1("_ControllerStream<1>")), new A._StreamSinkWrapper(localToForeignController, t3._eval$1("_StreamSinkWrapper<1>")), true, $T)));
+      t2 = t4._as(A.GuaranteeChannel$(new A._ControllerStream(localToForeignController, t3._eval$1("_ControllerStream<1>")), new A._StreamSinkWrapper(foreignToLocalController, t2._eval$1("_StreamSinkWrapper<1>")), true, $T));
+      t1.__StreamChannelController__foreign_F !== $ && A.throwLateFieldAI("_foreign");
+      t1.set$__StreamChannelController__foreign_F(t2);
+      return t1;
+    },
+    StreamChannelController: function StreamChannelController(t0) {
+      this.__StreamChannelController__foreign_F = this.__StreamChannelController__local_F = $;
+      this.$ti = t0;
+    },
+    StreamChannelMixin: function StreamChannelMixin() {
+    },
+    EventTargetExtension_addEventListener(_this, type, listener) {
+      var t1 = A._setArrayType([type, listener], type$.JSArray_Object);
+      A.callMethod(_this, "addEventListener", t1, type$.dynamic);
+    },
+    EventTargetExtension_removeEventListener(_this, type, listener) {
+      var t1 = A._setArrayType([type, listener], type$.JSArray_Object);
+      A.callMethod(_this, "removeEventListener", t1, type$.dynamic);
+    },
+    MessagePortExtension_get_postMessage(_this) {
+      return new A.MessagePortExtension_get_postMessage_closure(_this);
+    },
+    _callConstructor(constructorName, args) {
+      var $constructor = self.window[constructorName];
+      if ($constructor == null)
+        return null;
+      return A.callConstructor($constructor, args, type$.nullable_Object);
+    },
+    Subscription$(target, type, listener) {
+      A.EventTargetExtension_addEventListener(target, type, listener);
+      return new A.Subscription(type, target, listener);
+    },
+    MessagePortExtension_get_postMessage_closure: function MessagePortExtension_get_postMessage_closure(t0) {
+      this._this = t0;
+    },
+    Subscription: function Subscription(t0, t1, t2) {
+      this.type = t0;
+      this.target = t1;
+      this.listener = t2;
+    },
+    main() {
+      var t1 = type$.JavaScriptObject;
+      t1._as(self.window.console).log("Dart test runner browser host running");
+      if (J.$eq$($.$get$_currentUrl().get$queryParameters().$index(0, "debug"), "true"))
+        t1._as(type$.nullable_JavaScriptObject._as(self.document.body).classList).add("debug");
+      A.runZonedGuarded(new A.main_closure(), new A.main_closure0(), type$.Null);
+    },
+    _connectToServer() {
+      var t2, controller, t3,
+        t1 = $.$get$_currentUrl().get$queryParameters().$index(0, "managerUrl");
+      t1.toString;
+      t1 = A._callConstructor("WebSocket", A._setArrayType([t1], type$.JSArray_Object));
+      t1.toString;
+      type$.JavaScriptObject._as(t1);
+      t2 = type$.dynamic;
+      controller = A.StreamChannelController$(true, t2);
+      A.EventTargetExtension_addEventListener(t1, "message", A.allowInterop(new A._connectToServer_closure(controller), type$.void_Function_JavaScriptObject));
+      t3 = controller.__StreamChannelController__local_F;
+      t3 === $ && A.throwLateFieldNI("_local");
+      t3 = t3.__GuaranteeChannel__streamController_F;
+      t3 === $ && A.throwLateFieldNI("_streamController");
+      new A._ControllerStream(t3, A._instanceType(t3)._eval$1("_ControllerStream<1>")).listen$1(new A._connectToServer_closure0(t1));
+      t1 = controller.__StreamChannelController__foreign_F;
+      t1 === $ && A.throwLateFieldNI("_foreign");
+      return A._MultiChannel$(t1, t2);
+    },
+    _connectToIframe(url, id) {
+      var t2, t3, t4, iframe, controller, windowSubscription,
+        suiteUrl = A.Uri_parse(url).removeFragment$0(),
+        t1 = type$.JavaScriptObject;
+      t1._as(self.window.console).log("Starting suite " + suiteUrl.toString$0(0));
+      t2 = self.document;
+      t3 = A._setArrayType(["iframe"], type$.JSArray_Object);
+      t4 = type$.dynamic;
+      iframe = t1._as(A.callMethod(t2, "createElement", t3, t4));
+      $._iframes.$indexSet(0, id, iframe);
+      controller = A.StreamChannelController$(true, t4);
+      windowSubscription = A._Cell$named("windowSubscription");
+      windowSubscription._value = A.Subscription$(self.window, "message", A.allowInterop(new A._connectToIframe_closure(iframe, windowSubscription, suiteUrl, id, controller), type$.void_Function_JavaScriptObject));
+      iframe.src = url;
+      t1._as(type$.nullable_JavaScriptObject._as(self.document.body).appendChild(iframe));
+      t1 = controller.__StreamChannelController__foreign_F;
+      t1 === $ && A.throwLateFieldNI("_foreign");
+      return t1;
+    },
+    main_closure: function main_closure() {
+    },
+    main__closure: function main__closure(t0) {
+      this.serverChannel = t0;
+    },
+    main___closure: function main___closure(t0) {
+      this._0_0 = t0;
+    },
+    main___closure0: function main___closure0(t0) {
+      this._0_0 = t0;
+    },
+    main___closure1: function main___closure1(t0) {
+      this._0_0 = t0;
+    },
+    main___closure2: function main___closure2(t0) {
+      this._0_0 = t0;
+    },
+    main___closure3: function main___closure3(t0) {
+      this._0_0 = t0;
+    },
+    main___closure4: function main___closure4(t0) {
+      this._0_0 = t0;
+    },
+    main__closure0: function main__closure0(t0) {
+      this.serverChannel = t0;
+    },
+    main__closure1: function main__closure1(t0) {
+      this.serverChannel = t0;
+    },
+    main__closure2: function main__closure2(t0) {
+      this.serverChannel = t0;
+    },
+    main__closure3: function main__closure3(t0) {
+      this.serverChannel = t0;
+    },
+    main_closure0: function main_closure0() {
+    },
+    _connectToServer_closure: function _connectToServer_closure(t0) {
+      this.controller = t0;
+    },
+    _connectToServer_closure0: function _connectToServer_closure0(t0) {
+      this.webSocket = t0;
+    },
+    _connectToIframe_closure: function _connectToIframe_closure(t0, t1, t2, t3, t4) {
+      var _ = this;
+      _.iframe = t0;
+      _.windowSubscription = t1;
+      _.suiteUrl = t2;
+      _.id = t3;
+      _.controller = t4;
+    },
+    _connectToIframe__closure1: function _connectToIframe__closure1(t0) {
+      this.controller = t0;
+    },
+    _connectToIframe__closure: function _connectToIframe__closure(t0) {
+      this._0_0 = t0;
+    },
+    _connectToIframe__closure0: function _connectToIframe__closure0(t0) {
+      this._0_0 = t0;
+    },
+    max(a, b, $T) {
+      A.checkTypeBound($T, type$.num, "T", "max");
+      return Math.max($T._as(a), $T._as(b));
+    },
+    printString(string) {
+      if (typeof dartPrint == "function") {
+        dartPrint(string);
+        return;
+      }
+      if (typeof console == "object" && typeof console.log != "undefined") {
+        console.log(string);
+        return;
+      }
+      if (typeof print == "function") {
+        print(string);
+        return;
+      }
+      throw "Unable to print message: " + String(string);
+    },
+    _convertNativeToDart_Value(value) {
+      var proto, t1, values, i;
+      if (value == null)
+        return value;
+      if (typeof value == "string" || typeof value == "number" || A._isBool(value))
+        return value;
+      proto = Object.getPrototypeOf(value);
+      t1 = proto === Object.prototype;
+      t1.toString;
+      if (!t1) {
+        t1 = proto === null;
+        t1.toString;
+      } else
+        t1 = true;
+      if (t1)
+        return A.convertNativeToDart_Dictionary(value);
+      t1 = Array.isArray(value);
+      t1.toString;
+      if (t1) {
+        values = [];
+        i = 0;
+        while (true) {
+          t1 = value.length;
+          t1.toString;
+          if (!(i < t1))
+            break;
+          values.push(A._convertNativeToDart_Value(value[i]));
+          ++i;
+        }
+        return values;
+      }
+      return value;
+    },
+    convertNativeToDart_Dictionary(object) {
+      var dict, keys, t1, _i, key, t2;
+      if (object == null)
+        return null;
+      dict = A.LinkedHashMap_LinkedHashMap$_empty(type$.String, type$.dynamic);
+      keys = Object.getOwnPropertyNames(object);
+      for (t1 = keys.length, _i = 0; _i < keys.length; keys.length === t1 || (0, A.throwConcurrentModificationError)(keys), ++_i) {
+        key = keys[_i];
+        t2 = key;
+        t2.toString;
+        dict.$indexSet(0, t2, A._convertNativeToDart_Value(object[key]));
+      }
+      return dict;
+    },
+    current() {
+      var exception, t1, path, lastIndex, uri = null;
+      try {
+        uri = A.Uri_base();
+      } catch (exception) {
+        if (type$.Exception._is(A.unwrapException(exception))) {
+          t1 = $._current;
+          if (t1 != null)
+            return t1;
+          throw exception;
+        } else
+          throw exception;
+      }
+      if (J.$eq$(uri, $._currentUriBase)) {
+        t1 = $._current;
+        t1.toString;
+        return t1;
+      }
+      $._currentUriBase = uri;
+      if ($.$get$Style_platform() === $.$get$Style_url())
+        t1 = $._current = uri.resolve$1(".").toString$0(0);
+      else {
+        path = uri.toFilePath$0();
+        lastIndex = path.length - 1;
+        t1 = $._current = lastIndex === 0 ? path : B.JSString_methods.substring$2(path, 0, lastIndex);
+      }
+      return t1;
+    },
+    isAlphabetic(char) {
+      var t1;
+      if (!(char >= 65 && char <= 90))
+        t1 = char >= 97 && char <= 122;
+      else
+        t1 = true;
+      return t1;
+    },
+    isDriveLetter(path, index) {
+      var t3,
+        t1 = path.length,
+        t2 = index + 2;
+      if (t1 < t2)
+        return false;
+      if (!(index >= 0 && index < t1))
+        return A.ioore(path, index);
+      if (!A.isAlphabetic(path.charCodeAt(index)))
+        return false;
+      t3 = index + 1;
+      if (!(t3 < t1))
+        return A.ioore(path, t3);
+      if (path.charCodeAt(t3) !== 58)
+        return false;
+      if (t1 === t2)
+        return true;
+      if (!(t2 >= 0 && t2 < t1))
+        return A.ioore(path, t2);
+      return path.charCodeAt(t2) === 47;
+    }
+  },
+  B = {};
+  var holders = [A, J, B];
+  var $ = {};
+  A.JS_CONST.prototype = {};
+  J.Interceptor.prototype = {
+    $eq(receiver, other) {
+      return receiver === other;
+    },
+    get$hashCode(receiver) {
+      return A.Primitives_objectHashCode(receiver);
+    },
+    toString$0(receiver) {
+      return "Instance of '" + A.Primitives_objectTypeName(receiver) + "'";
+    },
+    noSuchMethod$1(receiver, invocation) {
+      throw A.wrapException(A.NoSuchMethodError_NoSuchMethodError$withInvocation(receiver, type$.Invocation._as(invocation)));
+    },
+    get$runtimeType(receiver) {
+      return A.createRuntimeType(A._instanceTypeFromConstructor(this));
+    }
+  };
+  J.JSBool.prototype = {
+    toString$0(receiver) {
+      return String(receiver);
+    },
+    get$hashCode(receiver) {
+      return receiver ? 519018 : 218159;
+    },
+    get$runtimeType(receiver) {
+      return A.createRuntimeType(type$.bool);
+    },
+    $isTrustedGetRuntimeType: 1,
+    $isbool: 1
+  };
+  J.JSNull.prototype = {
+    $eq(receiver, other) {
+      return null == other;
+    },
+    toString$0(receiver) {
+      return "null";
+    },
+    get$hashCode(receiver) {
+      return 0;
+    },
+    $isTrustedGetRuntimeType: 1,
+    $isNull: 1
+  };
+  J.JavaScriptObject.prototype = {};
+  J.LegacyJavaScriptObject.prototype = {
+    get$hashCode(receiver) {
+      return 0;
+    },
+    toString$0(receiver) {
+      return String(receiver);
+    }
+  };
+  J.PlainJavaScriptObject.prototype = {};
+  J.UnknownJavaScriptObject.prototype = {};
+  J.JavaScriptFunction.prototype = {
+    toString$0(receiver) {
+      var dartClosure = receiver[$.$get$DART_CLOSURE_PROPERTY_NAME()];
+      if (dartClosure == null)
+        return this.super$LegacyJavaScriptObject$toString(receiver);
+      return "JavaScript function for " + J.toString$0$(dartClosure);
+    },
+    $isFunction: 1
+  };
+  J.JavaScriptBigInt.prototype = {
+    get$hashCode(receiver) {
+      return 0;
+    },
+    toString$0(receiver) {
+      return String(receiver);
+    }
+  };
+  J.JavaScriptSymbol.prototype = {
+    get$hashCode(receiver) {
+      return 0;
+    },
+    toString$0(receiver) {
+      return String(receiver);
+    }
+  };
+  J.JSArray.prototype = {
+    cast$1$0(receiver, $R) {
+      return new A.CastList(receiver, A._arrayInstanceType(receiver)._eval$1("@<1>")._bind$1($R)._eval$1("CastList<1,2>"));
+    },
+    add$1(receiver, value) {
+      A._arrayInstanceType(receiver)._precomputed1._as(value);
+      if (!!receiver.fixed$length)
+        A.throwExpression(A.UnsupportedError$("add"));
+      receiver.push(value);
+    },
+    removeAt$1(receiver, index) {
+      var t1;
+      if (!!receiver.fixed$length)
+        A.throwExpression(A.UnsupportedError$("removeAt"));
+      t1 = receiver.length;
+      if (index >= t1)
+        throw A.wrapException(A.RangeError$value(index, null));
+      return receiver.splice(index, 1)[0];
+    },
+    insert$2(receiver, index, value) {
+      var t1;
+      A._arrayInstanceType(receiver)._precomputed1._as(value);
+      if (!!receiver.fixed$length)
+        A.throwExpression(A.UnsupportedError$("insert"));
+      t1 = receiver.length;
+      if (index > t1)
+        throw A.wrapException(A.RangeError$value(index, null));
+      receiver.splice(index, 0, value);
+    },
+    insertAll$2(receiver, index, iterable) {
+      var insertionLength, end;
+      A._arrayInstanceType(receiver)._eval$1("Iterable<1>")._as(iterable);
+      if (!!receiver.fixed$length)
+        A.throwExpression(A.UnsupportedError$("insertAll"));
+      A.RangeError_checkValueInInterval(index, 0, receiver.length, "index");
+      if (!type$.EfficientLengthIterable_dynamic._is(iterable))
+        iterable = J.toList$0$ax(iterable);
+      insertionLength = J.get$length$asx(iterable);
+      receiver.length = receiver.length + insertionLength;
+      end = index + insertionLength;
+      this.setRange$4(receiver, end, receiver.length, receiver, index);
+      this.setRange$3(receiver, index, end, iterable);
+    },
+    removeLast$0(receiver) {
+      if (!!receiver.fixed$length)
+        A.throwExpression(A.UnsupportedError$("removeLast"));
+      if (receiver.length === 0)
+        throw A.wrapException(A.diagnoseIndexError(receiver, -1));
+      return receiver.pop();
+    },
+    addAll$1(receiver, collection) {
+      var t1;
+      A._arrayInstanceType(receiver)._eval$1("Iterable<1>")._as(collection);
+      if (!!receiver.fixed$length)
+        A.throwExpression(A.UnsupportedError$("addAll"));
+      if (Array.isArray(collection)) {
+        this._addAllFromArray$1(receiver, collection);
+        return;
+      }
+      for (t1 = J.get$iterator$ax(collection); t1.moveNext$0();)
+        receiver.push(t1.get$current(t1));
+    },
+    _addAllFromArray$1(receiver, array) {
+      var len, i;
+      type$.JSArray_dynamic._as(array);
+      len = array.length;
+      if (len === 0)
+        return;
+      if (receiver === array)
+        throw A.wrapException(A.ConcurrentModificationError$(receiver));
+      for (i = 0; i < len; ++i)
+        receiver.push(array[i]);
+    },
+    clear$0(receiver) {
+      if (!!receiver.fixed$length)
+        A.throwExpression(A.UnsupportedError$("clear"));
+      receiver.length = 0;
+    },
+    map$1$1(receiver, f, $T) {
+      var t1 = A._arrayInstanceType(receiver);
+      return new A.MappedListIterable(receiver, t1._bind$1($T)._eval$1("1(2)")._as(f), t1._eval$1("@<1>")._bind$1($T)._eval$1("MappedListIterable<1,2>"));
+    },
+    join$1(receiver, separator) {
+      var i,
+        list = A.List_List$filled(receiver.length, "", false, type$.String);
+      for (i = 0; i < receiver.length; ++i)
+        this.$indexSet(list, i, A.S(receiver[i]));
+      return list.join(separator);
+    },
+    join$0($receiver) {
+      return this.join$1($receiver, "");
+    },
+    skip$1(receiver, n) {
+      return A.SubListIterable$(receiver, n, null, A._arrayInstanceType(receiver)._precomputed1);
+    },
+    fold$1$2(receiver, initialValue, combine, $T) {
+      var $length, value, i;
+      $T._as(initialValue);
+      A._arrayInstanceType(receiver)._bind$1($T)._eval$1("1(1,2)")._as(combine);
+      $length = receiver.length;
+      for (value = initialValue, i = 0; i < $length; ++i) {
+        value = combine.call$2(value, receiver[i]);
+        if (receiver.length !== $length)
+          throw A.wrapException(A.ConcurrentModificationError$(receiver));
+      }
+      return value;
+    },
+    elementAt$1(receiver, index) {
+      if (!(index >= 0 && index < receiver.length))
+        return A.ioore(receiver, index);
+      return receiver[index];
+    },
+    get$first(receiver) {
+      if (receiver.length > 0)
+        return receiver[0];
+      throw A.wrapException(A.IterableElementError_noElement());
+    },
+    get$last(receiver) {
+      var t1 = receiver.length;
+      if (t1 > 0)
+        return receiver[t1 - 1];
+      throw A.wrapException(A.IterableElementError_noElement());
+    },
+    setRange$4(receiver, start, end, iterable, skipCount) {
+      var $length, otherList, otherStart, t1, i;
+      A._arrayInstanceType(receiver)._eval$1("Iterable<1>")._as(iterable);
+      if (!!receiver.immutable$list)
+        A.throwExpression(A.UnsupportedError$("setRange"));
+      A.RangeError_checkValidRange(start, end, receiver.length);
+      $length = end - start;
+      if ($length === 0)
+        return;
+      A.RangeError_checkNotNegative(skipCount, "skipCount");
+      if (type$.List_dynamic._is(iterable)) {
+        otherList = iterable;
+        otherStart = skipCount;
+      } else {
+        otherList = J.skip$1$ax(iterable, skipCount).toList$1$growable(0, false);
+        otherStart = 0;
+      }
+      t1 = J.getInterceptor$asx(otherList);
+      if (otherStart + $length > t1.get$length(otherList))
+        throw A.wrapException(A.IterableElementError_tooFew());
+      if (otherStart < start)
+        for (i = $length - 1; i >= 0; --i)
+          receiver[start + i] = t1.$index(otherList, otherStart + i);
+      else
+        for (i = 0; i < $length; ++i)
+          receiver[start + i] = t1.$index(otherList, otherStart + i);
+    },
+    setRange$3($receiver, start, end, iterable) {
+      return this.setRange$4($receiver, start, end, iterable, 0);
+    },
+    contains$1(receiver, other) {
+      var i;
+      for (i = 0; i < receiver.length; ++i)
+        if (J.$eq$(receiver[i], other))
+          return true;
+      return false;
+    },
+    get$isEmpty(receiver) {
+      return receiver.length === 0;
+    },
+    get$isNotEmpty(receiver) {
+      return receiver.length !== 0;
+    },
+    toString$0(receiver) {
+      return A.Iterable_iterableToFullString(receiver, "[", "]");
+    },
+    toList$1$growable(receiver, growable) {
+      var t1 = A._setArrayType(receiver.slice(0), A._arrayInstanceType(receiver));
+      return t1;
+    },
+    toList$0($receiver) {
+      return this.toList$1$growable($receiver, true);
+    },
+    get$iterator(receiver) {
+      return new J.ArrayIterator(receiver, receiver.length, A._arrayInstanceType(receiver)._eval$1("ArrayIterator<1>"));
+    },
+    get$hashCode(receiver) {
+      return A.Primitives_objectHashCode(receiver);
+    },
+    get$length(receiver) {
+      return receiver.length;
+    },
+    $index(receiver, index) {
+      if (!(index >= 0 && index < receiver.length))
+        throw A.wrapException(A.diagnoseIndexError(receiver, index));
+      return receiver[index];
+    },
+    $indexSet(receiver, index, value) {
+      A._arrayInstanceType(receiver)._precomputed1._as(value);
+      if (!!receiver.immutable$list)
+        A.throwExpression(A.UnsupportedError$("indexed set"));
+      if (!(index >= 0 && index < receiver.length))
+        throw A.wrapException(A.diagnoseIndexError(receiver, index));
+      receiver[index] = value;
+    },
+    $isEfficientLengthIterable: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  J.JSUnmodifiableArray.prototype = {};
+  J.ArrayIterator.prototype = {
+    get$current(_) {
+      var t1 = this._current;
+      return t1 == null ? this.$ti._precomputed1._as(t1) : t1;
+    },
+    moveNext$0() {
+      var t2, _this = this,
+        t1 = _this._iterable,
+        $length = t1.length;
+      if (_this.__interceptors$_length !== $length) {
+        t1 = A.throwConcurrentModificationError(t1);
+        throw A.wrapException(t1);
+      }
+      t2 = _this._index;
+      if (t2 >= $length) {
+        _this.set$_current(null);
+        return false;
+      }
+      _this.set$_current(t1[t2]);
+      ++_this._index;
+      return true;
+    },
+    set$_current(_current) {
+      this._current = this.$ti._eval$1("1?")._as(_current);
+    },
+    $isIterator: 1
+  };
+  J.JSNumber.prototype = {
+    toInt$0(receiver) {
+      var t1;
+      if (receiver >= -2147483648 && receiver <= 2147483647)
+        return receiver | 0;
+      if (isFinite(receiver)) {
+        t1 = receiver < 0 ? Math.ceil(receiver) : Math.floor(receiver);
+        return t1 + 0;
+      }
+      throw A.wrapException(A.UnsupportedError$("" + receiver + ".toInt()"));
+    },
+    toString$0(receiver) {
+      if (receiver === 0 && 1 / receiver < 0)
+        return "-0.0";
+      else
+        return "" + receiver;
+    },
+    get$hashCode(receiver) {
+      var absolute, floorLog2, factor, scaled,
+        intValue = receiver | 0;
+      if (receiver === intValue)
+        return intValue & 536870911;
+      absolute = Math.abs(receiver);
+      floorLog2 = Math.log(absolute) / 0.6931471805599453 | 0;
+      factor = Math.pow(2, floorLog2);
+      scaled = absolute < 1 ? absolute / factor : factor / absolute;
+      return ((scaled * 9007199254740992 | 0) + (scaled * 3542243181176521 | 0)) * 599197 + floorLog2 * 1259 & 536870911;
+    },
+    $mod(receiver, other) {
+      var result = receiver % other;
+      if (result === 0)
+        return 0;
+      if (result > 0)
+        return result;
+      return result + other;
+    },
+    $tdiv(receiver, other) {
+      if ((receiver | 0) === receiver)
+        if (other >= 1 || false)
+          return receiver / other | 0;
+      return this._tdivSlow$1(receiver, other);
+    },
+    _tdivFast$1(receiver, other) {
+      return (receiver | 0) === receiver ? receiver / other | 0 : this._tdivSlow$1(receiver, other);
+    },
+    _tdivSlow$1(receiver, other) {
+      var quotient = receiver / other;
+      if (quotient >= -2147483648 && quotient <= 2147483647)
+        return quotient | 0;
+      if (quotient > 0) {
+        if (quotient !== 1 / 0)
+          return Math.floor(quotient);
+      } else if (quotient > -1 / 0)
+        return Math.ceil(quotient);
+      throw A.wrapException(A.UnsupportedError$("Result of truncating division is " + A.S(quotient) + ": " + A.S(receiver) + " ~/ " + other));
+    },
+    _shrOtherPositive$1(receiver, other) {
+      var t1;
+      if (receiver > 0)
+        t1 = this._shrBothPositive$1(receiver, other);
+      else {
+        t1 = other > 31 ? 31 : other;
+        t1 = receiver >> t1 >>> 0;
+      }
+      return t1;
+    },
+    _shrReceiverPositive$1(receiver, other) {
+      if (0 > other)
+        throw A.wrapException(A.argumentErrorValue(other));
+      return this._shrBothPositive$1(receiver, other);
+    },
+    _shrBothPositive$1(receiver, other) {
+      return other > 31 ? 0 : receiver >>> other;
+    },
+    get$runtimeType(receiver) {
+      return A.createRuntimeType(type$.num);
+    },
+    $isdouble: 1,
+    $isnum: 1
+  };
+  J.JSInt.prototype = {
+    get$runtimeType(receiver) {
+      return A.createRuntimeType(type$.int);
+    },
+    $isTrustedGetRuntimeType: 1,
+    $isint: 1
+  };
+  J.JSNumNotInt.prototype = {
+    get$runtimeType(receiver) {
+      return A.createRuntimeType(type$.double);
+    },
+    $isTrustedGetRuntimeType: 1
+  };
+  J.JSString.prototype = {
+    codeUnitAt$1(receiver, index) {
+      if (index < 0)
+        throw A.wrapException(A.diagnoseIndexError(receiver, index));
+      if (index >= receiver.length)
+        A.throwExpression(A.diagnoseIndexError(receiver, index));
+      return receiver.charCodeAt(index);
+    },
+    allMatches$2(receiver, string, start) {
+      var t1 = string.length;
+      if (start > t1)
+        throw A.wrapException(A.RangeError$range(start, 0, t1, null, null));
+      return new A._StringAllMatchesIterable(string, receiver, start);
+    },
+    allMatches$1($receiver, string) {
+      return this.allMatches$2($receiver, string, 0);
+    },
+    matchAsPrefix$2(receiver, string, start) {
+      var t1, t2, i, t3, _null = null;
+      if (start < 0 || start > string.length)
+        throw A.wrapException(A.RangeError$range(start, 0, string.length, _null, _null));
+      t1 = receiver.length;
+      t2 = string.length;
+      if (start + t1 > t2)
+        return _null;
+      for (i = 0; i < t1; ++i) {
+        t3 = start + i;
+        if (!(t3 >= 0 && t3 < t2))
+          return A.ioore(string, t3);
+        if (string.charCodeAt(t3) !== receiver.charCodeAt(i))
+          return _null;
+      }
+      return new A.StringMatch(start, receiver);
+    },
+    $add(receiver, other) {
+      return receiver + other;
+    },
+    endsWith$1(receiver, other) {
+      var otherLength = other.length,
+        t1 = receiver.length;
+      if (otherLength > t1)
+        return false;
+      return other === this.substring$1(receiver, t1 - otherLength);
+    },
+    replaceFirst$2(receiver, from, to) {
+      A.RangeError_checkValueInInterval(0, 0, receiver.length, "startIndex");
+      return A.stringReplaceFirstUnchecked(receiver, from, to, 0);
+    },
+    split$1(receiver, pattern) {
+      if (typeof pattern == "string")
+        return A._setArrayType(receiver.split(pattern), type$.JSArray_String);
+      else if (pattern instanceof A.JSSyntaxRegExp && pattern.get$_nativeAnchoredVersion().exec("").length - 2 === 0)
+        return A._setArrayType(receiver.split(pattern._nativeRegExp), type$.JSArray_String);
+      else
+        return this._defaultSplit$1(receiver, pattern);
+    },
+    replaceRange$3(receiver, start, end, replacement) {
+      var e = A.RangeError_checkValidRange(start, end, receiver.length);
+      return A.stringReplaceRangeUnchecked(receiver, start, e, replacement);
+    },
+    _defaultSplit$1(receiver, pattern) {
+      var t1, start, $length, match, matchStart, matchEnd,
+        result = A._setArrayType([], type$.JSArray_String);
+      for (t1 = J.allMatches$1$s(pattern, receiver), t1 = t1.get$iterator(t1), start = 0, $length = 1; t1.moveNext$0();) {
+        match = t1.get$current(t1);
+        matchStart = match.get$start(match);
+        matchEnd = match.get$end(match);
+        $length = matchEnd - matchStart;
+        if ($length === 0 && start === matchStart)
+          continue;
+        B.JSArray_methods.add$1(result, this.substring$2(receiver, start, matchStart));
+        start = matchEnd;
+      }
+      if (start < receiver.length || $length > 0)
+        B.JSArray_methods.add$1(result, this.substring$1(receiver, start));
+      return result;
+    },
+    startsWith$2(receiver, pattern, index) {
+      var endIndex;
+      if (index < 0 || index > receiver.length)
+        throw A.wrapException(A.RangeError$range(index, 0, receiver.length, null, null));
+      if (typeof pattern == "string") {
+        endIndex = index + pattern.length;
+        if (endIndex > receiver.length)
+          return false;
+        return pattern === receiver.substring(index, endIndex);
+      }
+      return J.matchAsPrefix$2$s(pattern, receiver, index) != null;
+    },
+    startsWith$1($receiver, pattern) {
+      return this.startsWith$2($receiver, pattern, 0);
+    },
+    substring$2(receiver, start, end) {
+      return receiver.substring(start, A.RangeError_checkValidRange(start, end, receiver.length));
+    },
+    substring$1($receiver, start) {
+      return this.substring$2($receiver, start, null);
+    },
+    trim$0(receiver) {
+      var startIndex, t1, endIndex0,
+        result = receiver.trim(),
+        endIndex = result.length;
+      if (endIndex === 0)
+        return result;
+      if (0 >= endIndex)
+        return A.ioore(result, 0);
+      if (result.charCodeAt(0) === 133) {
+        startIndex = J.JSString__skipLeadingWhitespace(result, 1);
+        if (startIndex === endIndex)
+          return "";
+      } else
+        startIndex = 0;
+      t1 = endIndex - 1;
+      if (!(t1 >= 0))
+        return A.ioore(result, t1);
+      endIndex0 = result.charCodeAt(t1) === 133 ? J.JSString__skipTrailingWhitespace(result, t1) : endIndex;
+      if (startIndex === 0 && endIndex0 === endIndex)
+        return result;
+      return result.substring(startIndex, endIndex0);
+    },
+    $mul(receiver, times) {
+      var s, result;
+      if (0 >= times)
+        return "";
+      if (times === 1 || receiver.length === 0)
+        return receiver;
+      if (times !== times >>> 0)
+        throw A.wrapException(B.C_OutOfMemoryError);
+      for (s = receiver, result = ""; true;) {
+        if ((times & 1) === 1)
+          result = s + result;
+        times = times >>> 1;
+        if (times === 0)
+          break;
+        s += s;
+      }
+      return result;
+    },
+    padLeft$2(receiver, width, padding) {
+      var delta = width - receiver.length;
+      if (delta <= 0)
+        return receiver;
+      return this.$mul(padding, delta) + receiver;
+    },
+    padRight$1(receiver, width) {
+      var delta = width - receiver.length;
+      if (delta <= 0)
+        return receiver;
+      return receiver + this.$mul(" ", delta);
+    },
+    indexOf$2(receiver, pattern, start) {
+      var t1;
+      if (start < 0 || start > receiver.length)
+        throw A.wrapException(A.RangeError$range(start, 0, receiver.length, null, null));
+      t1 = receiver.indexOf(pattern, start);
+      return t1;
+    },
+    indexOf$1($receiver, pattern) {
+      return this.indexOf$2($receiver, pattern, 0);
+    },
+    lastIndexOf$2(receiver, pattern, start) {
+      var t1, t2;
+      if (start == null)
+        start = receiver.length;
+      else if (start < 0 || start > receiver.length)
+        throw A.wrapException(A.RangeError$range(start, 0, receiver.length, null, null));
+      t1 = pattern.length;
+      t2 = receiver.length;
+      if (start + t1 > t2)
+        start = t2 - t1;
+      return receiver.lastIndexOf(pattern, start);
+    },
+    lastIndexOf$1($receiver, pattern) {
+      return this.lastIndexOf$2($receiver, pattern, null);
+    },
+    contains$1(receiver, other) {
+      return A.stringContainsUnchecked(receiver, other, 0);
+    },
+    toString$0(receiver) {
+      return receiver;
+    },
+    get$hashCode(receiver) {
+      var t1, hash, i;
+      for (t1 = receiver.length, hash = 0, i = 0; i < t1; ++i) {
+        hash = hash + receiver.charCodeAt(i) & 536870911;
+        hash = hash + ((hash & 524287) << 10) & 536870911;
+        hash ^= hash >> 6;
+      }
+      hash = hash + ((hash & 67108863) << 3) & 536870911;
+      hash ^= hash >> 11;
+      return hash + ((hash & 16383) << 15) & 536870911;
+    },
+    get$runtimeType(receiver) {
+      return A.createRuntimeType(type$.String);
+    },
+    get$length(receiver) {
+      return receiver.length;
+    },
+    $isTrustedGetRuntimeType: 1,
+    $isPattern: 1,
+    $isString: 1
+  };
+  A.CastStream.prototype = {
+    listen$4$cancelOnError$onDone$onError(onData, cancelOnError, onDone, onError) {
+      var t2,
+        t1 = this.$ti;
+      t1._eval$1("~(2)?")._as(onData);
+      t2 = this._source.listen$3$cancelOnError$onDone(null, cancelOnError, type$.nullable_void_Function._as(onDone));
+      t1 = new A.CastStreamSubscription(t2, $.Zone__current, t1._eval$1("@<1>")._bind$1(t1._rest[1])._eval$1("CastStreamSubscription<1,2>"));
+      t2.onData$1(t1.get$__internal$_onData());
+      t1.onData$1(onData);
+      t1.onError$1(0, onError);
+      return t1;
+    },
+    listen$1(onData) {
+      return this.listen$4$cancelOnError$onDone$onError(onData, null, null, null);
+    },
+    listen$3$onDone$onError(onData, onDone, onError) {
+      return this.listen$4$cancelOnError$onDone$onError(onData, null, onDone, onError);
+    },
+    listen$3$cancelOnError$onDone(onData, cancelOnError, onDone) {
+      return this.listen$4$cancelOnError$onDone$onError(onData, cancelOnError, onDone, null);
+    }
+  };
+  A.CastStreamSubscription.prototype = {
+    cancel$0(_) {
+      return this._source.cancel$0(0);
+    },
+    onData$1(handleData) {
+      var t1 = this.$ti;
+      t1._eval$1("~(2)?")._as(handleData);
+      this.set$_handleData(handleData == null ? null : this.__internal$_zone.registerUnaryCallback$2$1(handleData, type$.dynamic, t1._rest[1]));
+    },
+    onError$1(_, handleError) {
+      var _this = this;
+      _this._source.onError$1(0, handleError);
+      if (handleError == null)
+        _this._handleError = null;
+      else if (type$.void_Function_Object_StackTrace._is(handleError))
+        _this._handleError = _this.__internal$_zone.registerBinaryCallback$3$1(handleError, type$.dynamic, type$.Object, type$.StackTrace);
+      else if (type$.void_Function_Object._is(handleError))
+        _this._handleError = _this.__internal$_zone.registerUnaryCallback$2$1(handleError, type$.dynamic, type$.Object);
+      else
+        throw A.wrapException(A.ArgumentError$(string$.handle, null));
+    },
+    __internal$_onData$1(data) {
+      var targetData, error, stack, handleError, t2, exception, _this = this,
+        t1 = _this.$ti;
+      t1._precomputed1._as(data);
+      t2 = _this._handleData;
+      if (t2 == null)
+        return;
+      targetData = null;
+      try {
+        targetData = t1._rest[1]._as(data);
+      } catch (exception) {
+        error = A.unwrapException(exception);
+        stack = A.getTraceFromException(exception);
+        handleError = _this._handleError;
+        if (handleError == null)
+          _this.__internal$_zone.handleUncaughtError$2(error, stack);
+        else {
+          t1 = type$.Object;
+          t2 = _this.__internal$_zone;
+          if (type$.void_Function_Object_StackTrace._is(handleError))
+            t2.runBinaryGuarded$2$3(handleError, error, stack, t1, type$.StackTrace);
+          else
+            t2.runUnaryGuarded$1$2(type$.void_Function_Object._as(handleError), error, t1);
+        }
+        return;
+      }
+      _this.__internal$_zone.runUnaryGuarded$1$2(t2, targetData, t1._rest[1]);
+    },
+    set$_handleData(_handleData) {
+      this._handleData = this.$ti._eval$1("~(2)?")._as(_handleData);
+    },
+    $isStreamSubscription: 1
+  };
+  A._CastIterableBase.prototype = {
+    get$iterator(_) {
+      var t1 = A._instanceType(this);
+      return new A.CastIterator(J.get$iterator$ax(this.get$_source()), t1._eval$1("@<1>")._bind$1(t1._rest[1])._eval$1("CastIterator<1,2>"));
+    },
+    get$length(_) {
+      return J.get$length$asx(this.get$_source());
+    },
+    get$isEmpty(_) {
+      return J.get$isEmpty$asx(this.get$_source());
+    },
+    get$isNotEmpty(_) {
+      return J.get$isNotEmpty$asx(this.get$_source());
+    },
+    skip$1(_, count) {
+      var t1 = A._instanceType(this);
+      return A.CastIterable_CastIterable(J.skip$1$ax(this.get$_source(), count), t1._precomputed1, t1._rest[1]);
+    },
+    elementAt$1(_, index) {
+      return A._instanceType(this)._rest[1]._as(J.elementAt$1$ax(this.get$_source(), index));
+    },
+    toString$0(_) {
+      return J.toString$0$(this.get$_source());
+    }
+  };
+  A.CastIterator.prototype = {
+    moveNext$0() {
+      return this._source.moveNext$0();
+    },
+    get$current(_) {
+      var t1 = this._source;
+      return this.$ti._rest[1]._as(t1.get$current(t1));
+    },
+    $isIterator: 1
+  };
+  A.CastIterable.prototype = {
+    get$_source() {
+      return this._source;
+    }
+  };
+  A._EfficientLengthCastIterable.prototype = {$isEfficientLengthIterable: 1};
+  A._CastListBase.prototype = {
+    $index(_, index) {
+      return this.$ti._rest[1]._as(J.$index$asx(this._source, index));
+    },
+    $indexSet(_, index, value) {
+      var t1 = this.$ti;
+      J.$indexSet$ax(this._source, index, t1._precomputed1._as(t1._rest[1]._as(value)));
+    },
+    $isEfficientLengthIterable: 1,
+    $isList: 1
+  };
+  A.CastList.prototype = {
+    cast$1$0(_, $R) {
+      return new A.CastList(this._source, this.$ti._eval$1("@<1>")._bind$1($R)._eval$1("CastList<1,2>"));
+    },
+    get$_source() {
+      return this._source;
+    }
+  };
+  A.LateError.prototype = {
+    toString$0(_) {
+      return "LateInitializationError: " + this._message;
+    }
+  };
+  A.CodeUnits.prototype = {
+    get$length(_) {
+      return this._string.length;
+    },
+    $index(_, i) {
+      var t1 = this._string;
+      if (!(i >= 0 && i < t1.length))
+        return A.ioore(t1, i);
+      return t1.charCodeAt(i);
+    }
+  };
+  A.nullFuture_closure.prototype = {
+    call$0() {
+      return A.Future_Future$value(null, type$.Null);
+    },
+    $signature: 60
+  };
+  A.SentinelValue.prototype = {};
+  A.EfficientLengthIterable.prototype = {};
+  A.ListIterable.prototype = {
+    get$iterator(_) {
+      var _this = this;
+      return new A.ListIterator(_this, _this.get$length(_this), A._instanceType(_this)._eval$1("ListIterator<ListIterable.E>"));
+    },
+    get$isEmpty(_) {
+      return this.get$length(this) === 0;
+    },
+    join$1(_, separator) {
+      var first, t1, i, _this = this,
+        $length = _this.get$length(_this);
+      if (separator.length !== 0) {
+        if ($length === 0)
+          return "";
+        first = A.S(_this.elementAt$1(0, 0));
+        if ($length !== _this.get$length(_this))
+          throw A.wrapException(A.ConcurrentModificationError$(_this));
+        for (t1 = first, i = 1; i < $length; ++i) {
+          t1 = t1 + separator + A.S(_this.elementAt$1(0, i));
+          if ($length !== _this.get$length(_this))
+            throw A.wrapException(A.ConcurrentModificationError$(_this));
+        }
+        return t1.charCodeAt(0) == 0 ? t1 : t1;
+      } else {
+        for (i = 0, t1 = ""; i < $length; ++i) {
+          t1 += A.S(_this.elementAt$1(0, i));
+          if ($length !== _this.get$length(_this))
+            throw A.wrapException(A.ConcurrentModificationError$(_this));
+        }
+        return t1.charCodeAt(0) == 0 ? t1 : t1;
+      }
+    },
+    join$0($receiver) {
+      return this.join$1($receiver, "");
+    },
+    map$1$1(_, toElement, $T) {
+      var t1 = A._instanceType(this);
+      return new A.MappedListIterable(this, t1._bind$1($T)._eval$1("1(ListIterable.E)")._as(toElement), t1._eval$1("@<ListIterable.E>")._bind$1($T)._eval$1("MappedListIterable<1,2>"));
+    },
+    fold$1$2(_, initialValue, combine, $T) {
+      var $length, value, i, _this = this;
+      $T._as(initialValue);
+      A._instanceType(_this)._bind$1($T)._eval$1("1(1,ListIterable.E)")._as(combine);
+      $length = _this.get$length(_this);
+      for (value = initialValue, i = 0; i < $length; ++i) {
+        value = combine.call$2(value, _this.elementAt$1(0, i));
+        if ($length !== _this.get$length(_this))
+          throw A.wrapException(A.ConcurrentModificationError$(_this));
+      }
+      return value;
+    },
+    skip$1(_, count) {
+      return A.SubListIterable$(this, count, null, A._instanceType(this)._eval$1("ListIterable.E"));
+    }
+  };
+  A.SubListIterable.prototype = {
+    SubListIterable$3(_iterable, _start, _endOrLength, $E) {
+      var endOrLength,
+        t1 = this._start;
+      A.RangeError_checkNotNegative(t1, "start");
+      endOrLength = this._endOrLength;
+      if (endOrLength != null) {
+        A.RangeError_checkNotNegative(endOrLength, "end");
+        if (t1 > endOrLength)
+          throw A.wrapException(A.RangeError$range(t1, 0, endOrLength, "start", null));
+      }
+    },
+    get$_endIndex() {
+      var $length = J.get$length$asx(this.__internal$_iterable),
+        endOrLength = this._endOrLength;
+      if (endOrLength == null || endOrLength > $length)
+        return $length;
+      return endOrLength;
+    },
+    get$_startIndex() {
+      var $length = J.get$length$asx(this.__internal$_iterable),
+        t1 = this._start;
+      if (t1 > $length)
+        return $length;
+      return t1;
+    },
+    get$length(_) {
+      var endOrLength,
+        $length = J.get$length$asx(this.__internal$_iterable),
+        t1 = this._start;
+      if (t1 >= $length)
+        return 0;
+      endOrLength = this._endOrLength;
+      if (endOrLength == null || endOrLength >= $length)
+        return $length - t1;
+      if (typeof endOrLength !== "number")
+        return endOrLength.$sub();
+      return endOrLength - t1;
+    },
+    elementAt$1(_, index) {
+      var _this = this,
+        realIndex = _this.get$_startIndex() + index;
+      if (index < 0 || realIndex >= _this.get$_endIndex())
+        throw A.wrapException(A.IndexError$withLength(index, _this.get$length(_this), _this, "index"));
+      return J.elementAt$1$ax(_this.__internal$_iterable, realIndex);
+    },
+    skip$1(_, count) {
+      var newStart, endOrLength, _this = this;
+      A.RangeError_checkNotNegative(count, "count");
+      newStart = _this._start + count;
+      endOrLength = _this._endOrLength;
+      if (endOrLength != null && newStart >= endOrLength)
+        return new A.EmptyIterable(_this.$ti._eval$1("EmptyIterable<1>"));
+      return A.SubListIterable$(_this.__internal$_iterable, newStart, endOrLength, _this.$ti._precomputed1);
+    },
+    toList$1$growable(_, growable) {
+      var $length, result, i, _this = this,
+        start = _this._start,
+        t1 = _this.__internal$_iterable,
+        t2 = J.getInterceptor$asx(t1),
+        end = t2.get$length(t1),
+        endOrLength = _this._endOrLength;
+      if (endOrLength != null && endOrLength < end)
+        end = endOrLength;
+      $length = end - start;
+      if ($length <= 0) {
+        t1 = J.JSArray_JSArray$fixed(0, _this.$ti._precomputed1);
+        return t1;
+      }
+      result = A.List_List$filled($length, t2.elementAt$1(t1, start), false, _this.$ti._precomputed1);
+      for (i = 1; i < $length; ++i) {
+        B.JSArray_methods.$indexSet(result, i, t2.elementAt$1(t1, start + i));
+        if (t2.get$length(t1) < end)
+          throw A.wrapException(A.ConcurrentModificationError$(_this));
+      }
+      return result;
+    }
+  };
+  A.ListIterator.prototype = {
+    get$current(_) {
+      var t1 = this.__internal$_current;
+      return t1 == null ? this.$ti._precomputed1._as(t1) : t1;
+    },
+    moveNext$0() {
+      var t3, _this = this,
+        t1 = _this.__internal$_iterable,
+        t2 = J.getInterceptor$asx(t1),
+        $length = t2.get$length(t1);
+      if (_this.__internal$_length !== $length)
+        throw A.wrapException(A.ConcurrentModificationError$(t1));
+      t3 = _this.__internal$_index;
+      if (t3 >= $length) {
+        _this.set$__internal$_current(null);
+        return false;
+      }
+      _this.set$__internal$_current(t2.elementAt$1(t1, t3));
+      ++_this.__internal$_index;
+      return true;
+    },
+    set$__internal$_current(_current) {
+      this.__internal$_current = this.$ti._eval$1("1?")._as(_current);
+    },
+    $isIterator: 1
+  };
+  A.MappedIterable.prototype = {
+    get$iterator(_) {
+      var t1 = A._instanceType(this);
+      return new A.MappedIterator(J.get$iterator$ax(this.__internal$_iterable), this._f, t1._eval$1("@<1>")._bind$1(t1._rest[1])._eval$1("MappedIterator<1,2>"));
+    },
+    get$length(_) {
+      return J.get$length$asx(this.__internal$_iterable);
+    },
+    get$isEmpty(_) {
+      return J.get$isEmpty$asx(this.__internal$_iterable);
+    },
+    elementAt$1(_, index) {
+      return this._f.call$1(J.elementAt$1$ax(this.__internal$_iterable, index));
+    }
+  };
+  A.EfficientLengthMappedIterable.prototype = {$isEfficientLengthIterable: 1};
+  A.MappedIterator.prototype = {
+    moveNext$0() {
+      var _this = this,
+        t1 = _this._iterator;
+      if (t1.moveNext$0()) {
+        _this.set$__internal$_current(_this._f.call$1(t1.get$current(t1)));
+        return true;
+      }
+      _this.set$__internal$_current(null);
+      return false;
+    },
+    get$current(_) {
+      var t1 = this.__internal$_current;
+      return t1 == null ? this.$ti._rest[1]._as(t1) : t1;
+    },
+    set$__internal$_current(_current) {
+      this.__internal$_current = this.$ti._eval$1("2?")._as(_current);
+    },
+    $isIterator: 1
+  };
+  A.MappedListIterable.prototype = {
+    get$length(_) {
+      return J.get$length$asx(this._source);
+    },
+    elementAt$1(_, index) {
+      return this._f.call$1(J.elementAt$1$ax(this._source, index));
+    }
+  };
+  A.WhereIterable.prototype = {
+    get$iterator(_) {
+      return new A.WhereIterator(J.get$iterator$ax(this.__internal$_iterable), this._f, this.$ti._eval$1("WhereIterator<1>"));
+    },
+    map$1$1(_, toElement, $T) {
+      var t1 = this.$ti;
+      return new A.MappedIterable(this, t1._bind$1($T)._eval$1("1(2)")._as(toElement), t1._eval$1("@<1>")._bind$1($T)._eval$1("MappedIterable<1,2>"));
+    }
+  };
+  A.WhereIterator.prototype = {
+    moveNext$0() {
+      var t1, t2;
+      for (t1 = this._iterator, t2 = this._f; t1.moveNext$0();)
+        if (A.boolConversionCheck(t2.call$1(t1.get$current(t1))))
+          return true;
+      return false;
+    },
+    get$current(_) {
+      var t1 = this._iterator;
+      return t1.get$current(t1);
+    },
+    $isIterator: 1
+  };
+  A.ExpandIterable.prototype = {
+    get$iterator(_) {
+      var t1 = this.$ti;
+      return new A.ExpandIterator(J.get$iterator$ax(this.__internal$_iterable), this._f, B.C_EmptyIterator, t1._eval$1("@<1>")._bind$1(t1._rest[1])._eval$1("ExpandIterator<1,2>"));
+    }
+  };
+  A.ExpandIterator.prototype = {
+    get$current(_) {
+      var t1 = this.__internal$_current;
+      return t1 == null ? this.$ti._rest[1]._as(t1) : t1;
+    },
+    moveNext$0() {
+      var t1, t2, _this = this;
+      if (_this._currentExpansion == null)
+        return false;
+      for (t1 = _this._iterator, t2 = _this._f; !_this._currentExpansion.moveNext$0();) {
+        _this.set$__internal$_current(null);
+        if (t1.moveNext$0()) {
+          _this.set$_currentExpansion(null);
+          _this.set$_currentExpansion(J.get$iterator$ax(t2.call$1(t1.get$current(t1))));
+        } else
+          return false;
+      }
+      t1 = _this._currentExpansion;
+      _this.set$__internal$_current(t1.get$current(t1));
+      return true;
+    },
+    set$_currentExpansion(_currentExpansion) {
+      this._currentExpansion = this.$ti._eval$1("Iterator<2>?")._as(_currentExpansion);
+    },
+    set$__internal$_current(_current) {
+      this.__internal$_current = this.$ti._eval$1("2?")._as(_current);
+    },
+    $isIterator: 1
+  };
+  A.TakeIterable.prototype = {
+    get$iterator(_) {
+      return new A.TakeIterator(J.get$iterator$ax(this.__internal$_iterable), this._takeCount, A._instanceType(this)._eval$1("TakeIterator<1>"));
+    }
+  };
+  A.EfficientLengthTakeIterable.prototype = {
+    get$length(_) {
+      var iterableLength = J.get$length$asx(this.__internal$_iterable),
+        t1 = this._takeCount;
+      if (iterableLength > t1)
+        return t1;
+      return iterableLength;
+    },
+    $isEfficientLengthIterable: 1
+  };
+  A.TakeIterator.prototype = {
+    moveNext$0() {
+      if (--this._remaining >= 0)
+        return this._iterator.moveNext$0();
+      this._remaining = -1;
+      return false;
+    },
+    get$current(_) {
+      var t1;
+      if (this._remaining < 0) {
+        this.$ti._precomputed1._as(null);
+        return null;
+      }
+      t1 = this._iterator;
+      return t1.get$current(t1);
+    },
+    $isIterator: 1
+  };
+  A.SkipIterable.prototype = {
+    skip$1(_, count) {
+      A.ArgumentError_checkNotNull(count, "count", type$.int);
+      A.RangeError_checkNotNegative(count, "count");
+      return new A.SkipIterable(this.__internal$_iterable, this._skipCount + count, A._instanceType(this)._eval$1("SkipIterable<1>"));
+    },
+    get$iterator(_) {
+      return new A.SkipIterator(J.get$iterator$ax(this.__internal$_iterable), this._skipCount, A._instanceType(this)._eval$1("SkipIterator<1>"));
+    }
+  };
+  A.EfficientLengthSkipIterable.prototype = {
+    get$length(_) {
+      var $length = J.get$length$asx(this.__internal$_iterable) - this._skipCount;
+      if ($length >= 0)
+        return $length;
+      return 0;
+    },
+    skip$1(_, count) {
+      A.ArgumentError_checkNotNull(count, "count", type$.int);
+      A.RangeError_checkNotNegative(count, "count");
+      return new A.EfficientLengthSkipIterable(this.__internal$_iterable, this._skipCount + count, this.$ti);
+    },
+    $isEfficientLengthIterable: 1
+  };
+  A.SkipIterator.prototype = {
+    moveNext$0() {
+      var t1, i;
+      for (t1 = this._iterator, i = 0; i < this._skipCount; ++i)
+        t1.moveNext$0();
+      this._skipCount = 0;
+      return t1.moveNext$0();
+    },
+    get$current(_) {
+      var t1 = this._iterator;
+      return t1.get$current(t1);
+    },
+    $isIterator: 1
+  };
+  A.SkipWhileIterable.prototype = {
+    get$iterator(_) {
+      return new A.SkipWhileIterator(J.get$iterator$ax(this.__internal$_iterable), this._f, this.$ti._eval$1("SkipWhileIterator<1>"));
+    }
+  };
+  A.SkipWhileIterator.prototype = {
+    moveNext$0() {
+      var t1, t2, _this = this;
+      if (!_this._hasSkipped) {
+        _this._hasSkipped = true;
+        for (t1 = _this._iterator, t2 = _this._f; t1.moveNext$0();)
+          if (!A.boolConversionCheck(t2.call$1(t1.get$current(t1))))
+            return true;
+      }
+      return _this._iterator.moveNext$0();
+    },
+    get$current(_) {
+      var t1 = this._iterator;
+      return t1.get$current(t1);
+    },
+    $isIterator: 1
+  };
+  A.EmptyIterable.prototype = {
+    get$iterator(_) {
+      return B.C_EmptyIterator;
+    },
+    get$isEmpty(_) {
+      return true;
+    },
+    get$length(_) {
+      return 0;
+    },
+    elementAt$1(_, index) {
+      throw A.wrapException(A.RangeError$range(index, 0, 0, "index", null));
+    },
+    map$1$1(_, toElement, $T) {
+      this.$ti._bind$1($T)._eval$1("1(2)")._as(toElement);
+      return new A.EmptyIterable($T._eval$1("EmptyIterable<0>"));
+    },
+    skip$1(_, count) {
+      A.RangeError_checkNotNegative(count, "count");
+      return this;
+    }
+  };
+  A.EmptyIterator.prototype = {
+    moveNext$0() {
+      return false;
+    },
+    get$current(_) {
+      throw A.wrapException(A.IterableElementError_noElement());
+    },
+    $isIterator: 1
+  };
+  A.WhereTypeIterable.prototype = {
+    get$iterator(_) {
+      return new A.WhereTypeIterator(J.get$iterator$ax(this._source), this.$ti._eval$1("WhereTypeIterator<1>"));
+    }
+  };
+  A.WhereTypeIterator.prototype = {
+    moveNext$0() {
+      var t1, t2;
+      for (t1 = this._source, t2 = this.$ti._precomputed1; t1.moveNext$0();)
+        if (t2._is(t1.get$current(t1)))
+          return true;
+      return false;
+    },
+    get$current(_) {
+      var t1 = this._source;
+      return this.$ti._precomputed1._as(t1.get$current(t1));
+    },
+    $isIterator: 1
+  };
+  A.FixedLengthListMixin.prototype = {};
+  A.UnmodifiableListMixin.prototype = {
+    $indexSet(_, index, value) {
+      A._instanceType(this)._eval$1("UnmodifiableListMixin.E")._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot modify an unmodifiable list"));
+    }
+  };
+  A.UnmodifiableListBase.prototype = {};
+  A.ReversedListIterable.prototype = {
+    get$length(_) {
+      return J.get$length$asx(this._source);
+    },
+    elementAt$1(_, index) {
+      var t1 = this._source,
+        t2 = J.getInterceptor$asx(t1);
+      return t2.elementAt$1(t1, t2.get$length(t1) - 1 - index);
+    }
+  };
+  A.Symbol.prototype = {
+    get$hashCode(_) {
+      var hash = this._hashCode;
+      if (hash != null)
+        return hash;
+      hash = 664597 * B.JSString_methods.get$hashCode(this._name) & 536870911;
+      this._hashCode = hash;
+      return hash;
+    },
+    toString$0(_) {
+      return 'Symbol("' + this._name + '")';
+    },
+    $eq(_, other) {
+      if (other == null)
+        return false;
+      return other instanceof A.Symbol && this._name === other._name;
+    },
+    $isSymbol0: 1
+  };
+  A.__CastListBase__CastIterableBase_ListMixin.prototype = {};
+  A.ConstantMapView.prototype = {};
+  A.ConstantMap.prototype = {
+    get$isEmpty(_) {
+      return this.get$length(this) === 0;
+    },
+    toString$0(_) {
+      return A.MapBase_mapToString(this);
+    },
+    $indexSet(_, key, value) {
+      var t1 = A._instanceType(this);
+      t1._precomputed1._as(key);
+      t1._rest[1]._as(value);
+      A.ConstantMap__throwUnmodifiable();
+    },
+    $isMap: 1
+  };
+  A.ConstantStringMap.prototype = {
+    get$length(_) {
+      return this._values.length;
+    },
+    get$__js_helper$_keys() {
+      var keys = this.$keys;
+      if (keys == null) {
+        keys = Object.keys(this._jsIndex);
+        this.$keys = keys;
+      }
+      return keys;
+    },
+    containsKey$1(_, key) {
+      if (typeof key != "string")
+        return false;
+      if ("__proto__" === key)
+        return false;
+      return this._jsIndex.hasOwnProperty(key);
+    },
+    $index(_, key) {
+      if (!this.containsKey$1(0, key))
+        return null;
+      return this._values[this._jsIndex[key]];
+    },
+    forEach$1(_, f) {
+      var keys, values, t1, i;
+      this.$ti._eval$1("~(1,2)")._as(f);
+      keys = this.get$__js_helper$_keys();
+      values = this._values;
+      for (t1 = keys.length, i = 0; i < t1; ++i)
+        f.call$2(keys[i], values[i]);
+    },
+    get$keys(_) {
+      return new A._KeysOrValues(this.get$__js_helper$_keys(), this.$ti._eval$1("_KeysOrValues<1>"));
+    }
+  };
+  A._KeysOrValues.prototype = {
+    get$length(_) {
+      return this._elements.length;
+    },
+    get$isEmpty(_) {
+      return 0 === this._elements.length;
+    },
+    get$isNotEmpty(_) {
+      return 0 !== this._elements.length;
+    },
+    get$iterator(_) {
+      var t1 = this._elements;
+      return new A._KeysOrValuesOrElementsIterator(t1, t1.length, this.$ti._eval$1("_KeysOrValuesOrElementsIterator<1>"));
+    }
+  };
+  A._KeysOrValuesOrElementsIterator.prototype = {
+    get$current(_) {
+      var t1 = this.__js_helper$_current;
+      return t1 == null ? this.$ti._precomputed1._as(t1) : t1;
+    },
+    moveNext$0() {
+      var _this = this,
+        t1 = _this.__js_helper$_index;
+      if (t1 >= _this._length) {
+        _this.set$__js_helper$_current(null);
+        return false;
+      }
+      _this.set$__js_helper$_current(_this._elements[t1]);
+      ++_this.__js_helper$_index;
+      return true;
+    },
+    set$__js_helper$_current(_current) {
+      this.__js_helper$_current = this.$ti._eval$1("1?")._as(_current);
+    },
+    $isIterator: 1
+  };
+  A.Instantiation.prototype = {
+    $eq(_, other) {
+      if (other == null)
+        return false;
+      return other instanceof A.Instantiation1 && this._genericClosure.$eq(0, other._genericClosure) && A.getRuntimeTypeOfClosure(this) === A.getRuntimeTypeOfClosure(other);
+    },
+    get$hashCode(_) {
+      return A.Object_hash(this._genericClosure, A.getRuntimeTypeOfClosure(this), B.C_SentinelValue, B.C_SentinelValue);
+    },
+    toString$0(_) {
+      var t1 = B.JSArray_methods.join$1([A.createRuntimeType(this.$ti._precomputed1)], ", ");
+      return this._genericClosure.toString$0(0) + " with " + ("<" + t1 + ">");
+    }
+  };
+  A.Instantiation1.prototype = {
+    call$2(a0, a1) {
+      return this._genericClosure.call$1$2(a0, a1, this.$ti._rest[0]);
+    },
+    call$4(a0, a1, a2, a3) {
+      return this._genericClosure.call$1$4(a0, a1, a2, a3, this.$ti._rest[0]);
+    },
+    $signature() {
+      return A.instantiatedGenericFunctionType(A.closureFunctionType(this._genericClosure), this.$ti);
+    }
+  };
+  A.JSInvocationMirror.prototype = {
+    get$memberName() {
+      var t1 = this._memberName;
+      return t1;
+    },
+    get$positionalArguments() {
+      var t1, argumentCount, list, index, _this = this;
+      if (_this.__js_helper$_kind === 1)
+        return B.List_empty0;
+      t1 = _this._arguments;
+      argumentCount = t1.length - _this._namedArgumentNames.length - _this._typeArgumentCount;
+      if (argumentCount === 0)
+        return B.List_empty0;
+      list = [];
+      for (index = 0; index < argumentCount; ++index) {
+        if (!(index < t1.length))
+          return A.ioore(t1, index);
+        list.push(t1[index]);
+      }
+      return J.JSArray_markUnmodifiableList(list);
+    },
+    get$namedArguments() {
+      var t1, namedArgumentCount, t2, namedArgumentsStartIndex, map, i, t3, t4, _this = this;
+      if (_this.__js_helper$_kind !== 0)
+        return B.Map_empty0;
+      t1 = _this._namedArgumentNames;
+      namedArgumentCount = t1.length;
+      t2 = _this._arguments;
+      namedArgumentsStartIndex = t2.length - namedArgumentCount - _this._typeArgumentCount;
+      if (namedArgumentCount === 0)
+        return B.Map_empty0;
+      map = new A.JsLinkedHashMap(type$.JsLinkedHashMap_Symbol_dynamic);
+      for (i = 0; i < namedArgumentCount; ++i) {
+        if (!(i < t1.length))
+          return A.ioore(t1, i);
+        t3 = t1[i];
+        t4 = namedArgumentsStartIndex + i;
+        if (!(t4 >= 0 && t4 < t2.length))
+          return A.ioore(t2, t4);
+        map.$indexSet(0, new A.Symbol(t3), t2[t4]);
+      }
+      return new A.ConstantMapView(map, type$.ConstantMapView_Symbol_dynamic);
+    },
+    $isInvocation: 1
+  };
+  A.Primitives_functionNoSuchMethod_closure.prototype = {
+    call$2($name, argument) {
+      var t1;
+      A._asString($name);
+      t1 = this._box_0;
+      t1.names = t1.names + "$" + $name;
+      B.JSArray_methods.add$1(this.namedArgumentList, $name);
+      B.JSArray_methods.add$1(this.$arguments, argument);
+      ++t1.argumentCount;
+    },
+    $signature: 4
+  };
+  A.TypeErrorDecoder.prototype = {
+    matchTypeError$1(message) {
+      var result, t1, _this = this,
+        match = new RegExp(_this._pattern).exec(message);
+      if (match == null)
+        return null;
+      result = Object.create(null);
+      t1 = _this._arguments;
+      if (t1 !== -1)
+        result.arguments = match[t1 + 1];
+      t1 = _this._argumentsExpr;
+      if (t1 !== -1)
+        result.argumentsExpr = match[t1 + 1];
+      t1 = _this._expr;
+      if (t1 !== -1)
+        result.expr = match[t1 + 1];
+      t1 = _this._method;
+      if (t1 !== -1)
+        result.method = match[t1 + 1];
+      t1 = _this._receiver;
+      if (t1 !== -1)
+        result.receiver = match[t1 + 1];
+      return result;
+    }
+  };
+  A.NullError.prototype = {
+    toString$0(_) {
+      return "Null check operator used on a null value";
+    }
+  };
+  A.JsNoSuchMethodError.prototype = {
+    toString$0(_) {
+      var t2, _this = this,
+        _s38_ = "NoSuchMethodError: method not found: '",
+        t1 = _this._method;
+      if (t1 == null)
+        return "NoSuchMethodError: " + _this.__js_helper$_message;
+      t2 = _this._receiver;
+      if (t2 == null)
+        return _s38_ + t1 + "' (" + _this.__js_helper$_message + ")";
+      return _s38_ + t1 + "' on '" + t2 + "' (" + _this.__js_helper$_message + ")";
+    }
+  };
+  A.UnknownJsTypeError.prototype = {
+    toString$0(_) {
+      var t1 = this.__js_helper$_message;
+      return t1.length === 0 ? "Error" : "Error: " + t1;
+    }
+  };
+  A.NullThrownFromJavaScriptException.prototype = {
+    toString$0(_) {
+      return "Throw of null ('" + (this._irritant === null ? "null" : "undefined") + "' from JavaScript)";
+    },
+    $isException: 1
+  };
+  A._StackTrace.prototype = {
+    toString$0(_) {
+      var trace,
+        t1 = this._trace;
+      if (t1 != null)
+        return t1;
+      t1 = this._exception;
+      trace = t1 !== null && typeof t1 === "object" ? t1.stack : null;
+      return this._trace = trace == null ? "" : trace;
+    },
+    $isStackTrace: 1
+  };
+  A.Closure.prototype = {
+    toString$0(_) {
+      var $constructor = this.constructor,
+        $name = $constructor == null ? null : $constructor.name;
+      return "Closure '" + A.unminifyOrTag($name == null ? "unknown" : $name) + "'";
+    },
+    $isFunction: 1,
+    get$$call() {
+      return this;
+    },
+    "call*": "call$1",
+    $requiredArgCount: 1,
+    $defaultValues: null
+  };
+  A.Closure0Args.prototype = {"call*": "call$0", $requiredArgCount: 0};
+  A.Closure2Args.prototype = {"call*": "call$2", $requiredArgCount: 2};
+  A.TearOffClosure.prototype = {};
+  A.StaticClosure.prototype = {
+    toString$0(_) {
+      var $name = this.$static_name;
+      if ($name == null)
+        return "Closure of unknown static method";
+      return "Closure '" + A.unminifyOrTag($name) + "'";
+    }
+  };
+  A.BoundClosure.prototype = {
+    $eq(_, other) {
+      if (other == null)
+        return false;
+      if (this === other)
+        return true;
+      if (!(other instanceof A.BoundClosure))
+        return false;
+      return this.$_target === other.$_target && this._receiver === other._receiver;
+    },
+    get$hashCode(_) {
+      return (A.objectHashCode(this._receiver) ^ A.Primitives_objectHashCode(this.$_target)) >>> 0;
+    },
+    toString$0(_) {
+      return "Closure '" + this.$_name + "' of " + ("Instance of '" + A.Primitives_objectTypeName(this._receiver) + "'");
+    }
+  };
+  A._CyclicInitializationError.prototype = {
+    toString$0(_) {
+      return "Reading static variable '" + this.variableName + "' during its initialization";
+    }
+  };
+  A.RuntimeError.prototype = {
+    toString$0(_) {
+      return "RuntimeError: " + this.message;
+    }
+  };
+  A._AssertionError.prototype = {
+    toString$0(_) {
+      return "Assertion failed: " + A.Error_safeToString(this.message);
+    }
+  };
+  A._Required.prototype = {};
+  A.JsLinkedHashMap.prototype = {
+    get$length(_) {
+      return this._length;
+    },
+    get$isEmpty(_) {
+      return this._length === 0;
+    },
+    get$keys(_) {
+      return new A.LinkedHashMapKeyIterable(this, A._instanceType(this)._eval$1("LinkedHashMapKeyIterable<1>"));
+    },
+    get$values(_) {
+      var t1 = A._instanceType(this);
+      return A.MappedIterable_MappedIterable(new A.LinkedHashMapKeyIterable(this, t1._eval$1("LinkedHashMapKeyIterable<1>")), new A.JsLinkedHashMap_values_closure(this), t1._precomputed1, t1._rest[1]);
+    },
+    containsKey$1(_, key) {
+      var strings, nums;
+      if (typeof key == "string") {
+        strings = this._strings;
+        if (strings == null)
+          return false;
+        return strings[key] != null;
+      } else if (typeof key == "number" && (key & 0x3fffffff) === key) {
+        nums = this._nums;
+        if (nums == null)
+          return false;
+        return nums[key] != null;
+      } else
+        return this.internalContainsKey$1(key);
+    },
+    internalContainsKey$1(key) {
+      var rest = this.__js_helper$_rest;
+      if (rest == null)
+        return false;
+      return this.internalFindBucketIndex$2(rest[this.internalComputeHashCode$1(key)], key) >= 0;
+    },
+    $index(_, key) {
+      var strings, cell, t1, nums, _null = null;
+      if (typeof key == "string") {
+        strings = this._strings;
+        if (strings == null)
+          return _null;
+        cell = strings[key];
+        t1 = cell == null ? _null : cell.hashMapCellValue;
+        return t1;
+      } else if (typeof key == "number" && (key & 0x3fffffff) === key) {
+        nums = this._nums;
+        if (nums == null)
+          return _null;
+        cell = nums[key];
+        t1 = cell == null ? _null : cell.hashMapCellValue;
+        return t1;
+      } else
+        return this.internalGet$1(key);
+    },
+    internalGet$1(key) {
+      var bucket, index,
+        rest = this.__js_helper$_rest;
+      if (rest == null)
+        return null;
+      bucket = rest[this.internalComputeHashCode$1(key)];
+      index = this.internalFindBucketIndex$2(bucket, key);
+      if (index < 0)
+        return null;
+      return bucket[index].hashMapCellValue;
+    },
+    $indexSet(_, key, value) {
+      var strings, nums, rest, hash, bucket, index, _this = this,
+        t1 = A._instanceType(_this);
+      t1._precomputed1._as(key);
+      t1._rest[1]._as(value);
+      if (typeof key == "string") {
+        strings = _this._strings;
+        _this._addHashTableEntry$3(strings == null ? _this._strings = _this._newHashTable$0() : strings, key, value);
+      } else if (typeof key == "number" && (key & 0x3fffffff) === key) {
+        nums = _this._nums;
+        _this._addHashTableEntry$3(nums == null ? _this._nums = _this._newHashTable$0() : nums, key, value);
+      } else {
+        rest = _this.__js_helper$_rest;
+        if (rest == null)
+          rest = _this.__js_helper$_rest = _this._newHashTable$0();
+        hash = _this.internalComputeHashCode$1(key);
+        bucket = rest[hash];
+        if (bucket == null)
+          rest[hash] = [_this._newLinkedCell$2(key, value)];
+        else {
+          index = _this.internalFindBucketIndex$2(bucket, key);
+          if (index >= 0)
+            bucket[index].hashMapCellValue = value;
+          else
+            bucket.push(_this._newLinkedCell$2(key, value));
+        }
+      }
+    },
+    putIfAbsent$2(_, key, ifAbsent) {
+      var t2, value, _this = this,
+        t1 = A._instanceType(_this);
+      t1._precomputed1._as(key);
+      t1._eval$1("2()")._as(ifAbsent);
+      if (_this.containsKey$1(0, key)) {
+        t2 = _this.$index(0, key);
+        return t2 == null ? t1._rest[1]._as(t2) : t2;
+      }
+      value = ifAbsent.call$0();
+      _this.$indexSet(0, key, value);
+      return value;
+    },
+    remove$1(_, key) {
+      var _this = this;
+      if (typeof key == "string")
+        return _this._removeHashTableEntry$2(_this._strings, key);
+      else if (typeof key == "number" && (key & 0x3fffffff) === key)
+        return _this._removeHashTableEntry$2(_this._nums, key);
+      else
+        return _this.internalRemove$1(key);
+    },
+    internalRemove$1(key) {
+      var hash, bucket, index, cell, _this = this,
+        rest = _this.__js_helper$_rest;
+      if (rest == null)
+        return null;
+      hash = _this.internalComputeHashCode$1(key);
+      bucket = rest[hash];
+      index = _this.internalFindBucketIndex$2(bucket, key);
+      if (index < 0)
+        return null;
+      cell = bucket.splice(index, 1)[0];
+      _this._unlinkCell$1(cell);
+      if (bucket.length === 0)
+        delete rest[hash];
+      return cell.hashMapCellValue;
+    },
+    clear$0(_) {
+      var _this = this;
+      if (_this._length > 0) {
+        _this._strings = _this._nums = _this.__js_helper$_rest = _this._first = _this._last = null;
+        _this._length = 0;
+        _this._modified$0();
+      }
+    },
+    forEach$1(_, action) {
+      var cell, modifications, _this = this;
+      A._instanceType(_this)._eval$1("~(1,2)")._as(action);
+      cell = _this._first;
+      modifications = _this._modifications;
+      for (; cell != null;) {
+        action.call$2(cell.hashMapCellKey, cell.hashMapCellValue);
+        if (modifications !== _this._modifications)
+          throw A.wrapException(A.ConcurrentModificationError$(_this));
+        cell = cell._next;
+      }
+    },
+    _addHashTableEntry$3(table, key, value) {
+      var cell,
+        t1 = A._instanceType(this);
+      t1._precomputed1._as(key);
+      t1._rest[1]._as(value);
+      cell = table[key];
+      if (cell == null)
+        table[key] = this._newLinkedCell$2(key, value);
+      else
+        cell.hashMapCellValue = value;
+    },
+    _removeHashTableEntry$2(table, key) {
+      var cell;
+      if (table == null)
+        return null;
+      cell = table[key];
+      if (cell == null)
+        return null;
+      this._unlinkCell$1(cell);
+      delete table[key];
+      return cell.hashMapCellValue;
+    },
+    _modified$0() {
+      this._modifications = this._modifications + 1 & 1073741823;
+    },
+    _newLinkedCell$2(key, value) {
+      var _this = this,
+        t1 = A._instanceType(_this),
+        cell = new A.LinkedHashMapCell(t1._precomputed1._as(key), t1._rest[1]._as(value));
+      if (_this._first == null)
+        _this._first = _this._last = cell;
+      else {
+        t1 = _this._last;
+        t1.toString;
+        cell._previous = t1;
+        _this._last = t1._next = cell;
+      }
+      ++_this._length;
+      _this._modified$0();
+      return cell;
+    },
+    _unlinkCell$1(cell) {
+      var _this = this,
+        previous = cell._previous,
+        next = cell._next;
+      if (previous == null)
+        _this._first = next;
+      else
+        previous._next = next;
+      if (next == null)
+        _this._last = previous;
+      else
+        next._previous = previous;
+      --_this._length;
+      _this._modified$0();
+    },
+    internalComputeHashCode$1(key) {
+      return J.get$hashCode$(key) & 1073741823;
+    },
+    internalFindBucketIndex$2(bucket, key) {
+      var $length, i;
+      if (bucket == null)
+        return -1;
+      $length = bucket.length;
+      for (i = 0; i < $length; ++i)
+        if (J.$eq$(bucket[i].hashMapCellKey, key))
+          return i;
+      return -1;
+    },
+    toString$0(_) {
+      return A.MapBase_mapToString(this);
+    },
+    _newHashTable$0() {
+      var table = Object.create(null);
+      table["<non-identifier-key>"] = table;
+      delete table["<non-identifier-key>"];
+      return table;
+    },
+    $isLinkedHashMap: 1
+  };
+  A.JsLinkedHashMap_values_closure.prototype = {
+    call$1(each) {
+      var t1 = this.$this,
+        t2 = A._instanceType(t1);
+      t1 = t1.$index(0, t2._precomputed1._as(each));
+      return t1 == null ? t2._rest[1]._as(t1) : t1;
+    },
+    $signature() {
+      return A._instanceType(this.$this)._eval$1("2(1)");
+    }
+  };
+  A.LinkedHashMapCell.prototype = {};
+  A.LinkedHashMapKeyIterable.prototype = {
+    get$length(_) {
+      return this._map._length;
+    },
+    get$isEmpty(_) {
+      return this._map._length === 0;
+    },
+    get$iterator(_) {
+      var t1 = this._map,
+        t2 = new A.LinkedHashMapKeyIterator(t1, t1._modifications, this.$ti._eval$1("LinkedHashMapKeyIterator<1>"));
+      t2._cell = t1._first;
+      return t2;
+    },
+    contains$1(_, element) {
+      return this._map.containsKey$1(0, element);
+    }
+  };
+  A.LinkedHashMapKeyIterator.prototype = {
+    get$current(_) {
+      return this.__js_helper$_current;
+    },
+    moveNext$0() {
+      var cell, _this = this,
+        t1 = _this._map;
+      if (_this._modifications !== t1._modifications)
+        throw A.wrapException(A.ConcurrentModificationError$(t1));
+      cell = _this._cell;
+      if (cell == null) {
+        _this.set$__js_helper$_current(null);
+        return false;
+      } else {
+        _this.set$__js_helper$_current(cell.hashMapCellKey);
+        _this._cell = cell._next;
+        return true;
+      }
+    },
+    set$__js_helper$_current(_current) {
+      this.__js_helper$_current = this.$ti._eval$1("1?")._as(_current);
+    },
+    $isIterator: 1
+  };
+  A.initHooks_closure.prototype = {
+    call$1(o) {
+      return this.getTag(o);
+    },
+    $signature: 15
+  };
+  A.initHooks_closure0.prototype = {
+    call$2(o, tag) {
+      return this.getUnknownTag(o, tag);
+    },
+    $signature: 36
+  };
+  A.initHooks_closure1.prototype = {
+    call$1(tag) {
+      return this.prototypeForTag(A._asString(tag));
+    },
+    $signature: 37
+  };
+  A.JSSyntaxRegExp.prototype = {
+    toString$0(_) {
+      return "RegExp/" + this.pattern + "/" + this._nativeRegExp.flags;
+    },
+    get$_nativeGlobalVersion() {
+      var _this = this,
+        t1 = _this._nativeGlobalRegExp;
+      if (t1 != null)
+        return t1;
+      t1 = _this._nativeRegExp;
+      return _this._nativeGlobalRegExp = A.JSSyntaxRegExp_makeNative(_this.pattern, t1.multiline, !t1.ignoreCase, t1.unicode, t1.dotAll, true);
+    },
+    get$_nativeAnchoredVersion() {
+      var _this = this,
+        t1 = _this._nativeAnchoredRegExp;
+      if (t1 != null)
+        return t1;
+      t1 = _this._nativeRegExp;
+      return _this._nativeAnchoredRegExp = A.JSSyntaxRegExp_makeNative(_this.pattern + "|()", t1.multiline, !t1.ignoreCase, t1.unicode, t1.dotAll, true);
+    },
+    firstMatch$1(string) {
+      var m = this._nativeRegExp.exec(string);
+      if (m == null)
+        return null;
+      return new A._MatchImplementation(m);
+    },
+    allMatches$2(_, string, start) {
+      var t1 = string.length;
+      if (start > t1)
+        throw A.wrapException(A.RangeError$range(start, 0, t1, null, null));
+      return new A._AllMatchesIterable(this, string, start);
+    },
+    allMatches$1($receiver, string) {
+      return this.allMatches$2($receiver, string, 0);
+    },
+    _execGlobal$2(string, start) {
+      var match,
+        regexp = this.get$_nativeGlobalVersion();
+      if (regexp == null)
+        regexp = type$.Object._as(regexp);
+      regexp.lastIndex = start;
+      match = regexp.exec(string);
+      if (match == null)
+        return null;
+      return new A._MatchImplementation(match);
+    },
+    _execAnchored$2(string, start) {
+      var match,
+        regexp = this.get$_nativeAnchoredVersion();
+      if (regexp == null)
+        regexp = type$.Object._as(regexp);
+      regexp.lastIndex = start;
+      match = regexp.exec(string);
+      if (match == null)
+        return null;
+      if (0 >= match.length)
+        return A.ioore(match, -1);
+      if (match.pop() != null)
+        return null;
+      return new A._MatchImplementation(match);
+    },
+    matchAsPrefix$2(_, string, start) {
+      if (start < 0 || start > string.length)
+        throw A.wrapException(A.RangeError$range(start, 0, string.length, null, null));
+      return this._execAnchored$2(string, start);
+    },
+    $isPattern: 1,
+    $isRegExp: 1
+  };
+  A._MatchImplementation.prototype = {
+    get$start(_) {
+      return this._match.index;
+    },
+    get$end(_) {
+      var t1 = this._match;
+      return t1.index + t1[0].length;
+    },
+    $isMatch: 1,
+    $isRegExpMatch: 1
+  };
+  A._AllMatchesIterable.prototype = {
+    get$iterator(_) {
+      return new A._AllMatchesIterator(this._re, this.__js_helper$_string, this.__js_helper$_start);
+    }
+  };
+  A._AllMatchesIterator.prototype = {
+    get$current(_) {
+      var t1 = this.__js_helper$_current;
+      return t1 == null ? type$.RegExpMatch._as(t1) : t1;
+    },
+    moveNext$0() {
+      var t1, t2, t3, match, nextIndex, _this = this,
+        string = _this.__js_helper$_string;
+      if (string == null)
+        return false;
+      t1 = _this._nextIndex;
+      t2 = string.length;
+      if (t1 <= t2) {
+        t3 = _this._regExp;
+        match = t3._execGlobal$2(string, t1);
+        if (match != null) {
+          _this.__js_helper$_current = match;
+          nextIndex = match.get$end(match);
+          if (match._match.index === nextIndex) {
+            if (t3._nativeRegExp.unicode) {
+              t1 = _this._nextIndex;
+              t3 = t1 + 1;
+              if (t3 < t2) {
+                if (!(t1 >= 0 && t1 < t2))
+                  return A.ioore(string, t1);
+                t1 = string.charCodeAt(t1);
+                if (t1 >= 55296 && t1 <= 56319) {
+                  if (!(t3 >= 0))
+                    return A.ioore(string, t3);
+                  t1 = string.charCodeAt(t3);
+                  t1 = t1 >= 56320 && t1 <= 57343;
+                } else
+                  t1 = false;
+              } else
+                t1 = false;
+            } else
+              t1 = false;
+            nextIndex = (t1 ? nextIndex + 1 : nextIndex) + 1;
+          }
+          _this._nextIndex = nextIndex;
+          return true;
+        }
+      }
+      _this.__js_helper$_string = _this.__js_helper$_current = null;
+      return false;
+    },
+    $isIterator: 1
+  };
+  A.StringMatch.prototype = {
+    get$end(_) {
+      return this.start + this.pattern.length;
+    },
+    $isMatch: 1,
+    get$start(receiver) {
+      return this.start;
+    }
+  };
+  A._StringAllMatchesIterable.prototype = {
+    get$iterator(_) {
+      return new A._StringAllMatchesIterator(this._input, this._pattern, this.__js_helper$_index);
+    }
+  };
+  A._StringAllMatchesIterator.prototype = {
+    moveNext$0() {
+      var index, end, _this = this,
+        t1 = _this.__js_helper$_index,
+        t2 = _this._pattern,
+        t3 = t2.length,
+        t4 = _this._input,
+        t5 = t4.length;
+      if (t1 + t3 > t5) {
+        _this.__js_helper$_current = null;
+        return false;
+      }
+      index = t4.indexOf(t2, t1);
+      if (index < 0) {
+        _this.__js_helper$_index = t5 + 1;
+        _this.__js_helper$_current = null;
+        return false;
+      }
+      end = index + t3;
+      _this.__js_helper$_current = new A.StringMatch(index, t2);
+      _this.__js_helper$_index = end === _this.__js_helper$_index ? end + 1 : end;
+      return true;
+    },
+    get$current(_) {
+      var t1 = this.__js_helper$_current;
+      t1.toString;
+      return t1;
+    },
+    $isIterator: 1
+  };
+  A._Cell.prototype = {
+    _readLocal$0() {
+      var t1 = this._value;
+      if (t1 === this)
+        throw A.wrapException(new A.LateError("Local '" + this.__late_helper$_name + "' has not been initialized."));
+      return t1;
+    }
+  };
+  A._InitializedCell.prototype = {
+    _readFinal$0() {
+      var result, _this = this,
+        t1 = _this._value;
+      if (t1 === _this) {
+        result = _this._initializer.call$0();
+        if (_this._value !== _this)
+          throw A.wrapException(new A.LateError("Local '" + _this.__late_helper$_name + string$.x27_has_));
+        _this._value = result;
+        t1 = result;
+      }
+      return t1;
+    }
+  };
+  A.NativeByteBuffer.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_ByteBuffer_RkP;
+    },
+    $isTrustedGetRuntimeType: 1,
+    $isByteBuffer: 1
+  };
+  A.NativeTypedData.prototype = {};
+  A.NativeByteData.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_ByteData_zNC;
+    },
+    $isTrustedGetRuntimeType: 1,
+    $isByteData: 1
+  };
+  A.NativeTypedArray.prototype = {
+    get$length(receiver) {
+      return receiver.length;
+    },
+    $isJavaScriptIndexingBehavior: 1
+  };
+  A.NativeTypedArrayOfDouble.prototype = {
+    $index(receiver, index) {
+      A._checkValidIndex(index, receiver, receiver.length);
+      return receiver[index];
+    },
+    $indexSet(receiver, index, value) {
+      A._asDouble(value);
+      A._checkValidIndex(index, receiver, receiver.length);
+      receiver[index] = value;
+    },
+    $isEfficientLengthIterable: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A.NativeTypedArrayOfInt.prototype = {
+    $indexSet(receiver, index, value) {
+      A._asInt(value);
+      A._checkValidIndex(index, receiver, receiver.length);
+      receiver[index] = value;
+    },
+    $isEfficientLengthIterable: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A.NativeFloat32List.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_Float32List_LB7;
+    },
+    $isTrustedGetRuntimeType: 1,
+    $isFloat32List: 1
+  };
+  A.NativeFloat64List.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_Float64List_LB7;
+    },
+    $isTrustedGetRuntimeType: 1,
+    $isFloat64List: 1
+  };
+  A.NativeInt16List.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_Int16List_uXf;
+    },
+    $index(receiver, index) {
+      A._checkValidIndex(index, receiver, receiver.length);
+      return receiver[index];
+    },
+    $isTrustedGetRuntimeType: 1,
+    $isInt16List: 1
+  };
+  A.NativeInt32List.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_Int32List_O50;
+    },
+    $index(receiver, index) {
+      A._checkValidIndex(index, receiver, receiver.length);
+      return receiver[index];
+    },
+    $isTrustedGetRuntimeType: 1,
+    $isInt32List: 1
+  };
+  A.NativeInt8List.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_Int8List_ekJ;
+    },
+    $index(receiver, index) {
+      A._checkValidIndex(index, receiver, receiver.length);
+      return receiver[index];
+    },
+    $isTrustedGetRuntimeType: 1,
+    $isInt8List: 1
+  };
+  A.NativeUint16List.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_Uint16List_2bx;
+    },
+    $index(receiver, index) {
+      A._checkValidIndex(index, receiver, receiver.length);
+      return receiver[index];
+    },
+    $isTrustedGetRuntimeType: 1,
+    $isUint16List: 1
+  };
+  A.NativeUint32List.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_Uint32List_2bx;
+    },
+    $index(receiver, index) {
+      A._checkValidIndex(index, receiver, receiver.length);
+      return receiver[index];
+    },
+    $isTrustedGetRuntimeType: 1,
+    $isUint32List: 1
+  };
+  A.NativeUint8ClampedList.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_Uint8ClampedList_Jik;
+    },
+    get$length(receiver) {
+      return receiver.length;
+    },
+    $index(receiver, index) {
+      A._checkValidIndex(index, receiver, receiver.length);
+      return receiver[index];
+    },
+    $isTrustedGetRuntimeType: 1,
+    $isUint8ClampedList: 1
+  };
+  A.NativeUint8List.prototype = {
+    get$runtimeType(receiver) {
+      return B.Type_Uint8List_WLA;
+    },
+    get$length(receiver) {
+      return receiver.length;
+    },
+    $index(receiver, index) {
+      A._checkValidIndex(index, receiver, receiver.length);
+      return receiver[index];
+    },
+    sublist$2(receiver, start, end) {
+      return new Uint8Array(receiver.subarray(start, A._checkValidRange(start, end, receiver.length)));
+    },
+    $isTrustedGetRuntimeType: 1,
+    $isNativeUint8List: 1,
+    $isUint8List: 1
+  };
+  A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin.prototype = {};
+  A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin.prototype = {};
+  A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin.prototype = {};
+  A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin.prototype = {};
+  A.Rti.prototype = {
+    _eval$1(recipe) {
+      return A._Universe_evalInEnvironment(init.typeUniverse, this, recipe);
+    },
+    _bind$1(typeOrTuple) {
+      return A._Universe_bind(init.typeUniverse, this, typeOrTuple);
+    }
+  };
+  A._FunctionParameters.prototype = {};
+  A._Type.prototype = {
+    toString$0(_) {
+      return A._rtiToString(this._rti, null);
+    }
+  };
+  A._Error.prototype = {
+    toString$0(_) {
+      return this.__rti$_message;
+    }
+  };
+  A._TypeError.prototype = {$isTypeError: 1};
+  A._AsyncRun__initializeScheduleImmediate_internalCallback.prototype = {
+    call$1(_) {
+      var t1 = this._box_0,
+        f = t1.storedCallback;
+      t1.storedCallback = null;
+      f.call$0();
+    },
+    $signature: 10
+  };
+  A._AsyncRun__initializeScheduleImmediate_closure.prototype = {
+    call$1(callback) {
+      var t1, t2;
+      this._box_0.storedCallback = type$.void_Function._as(callback);
+      t1 = this.div;
+      t2 = this.span;
+      t1.firstChild ? t1.removeChild(t2) : t1.appendChild(t2);
+    },
+    $signature: 30
+  };
+  A._AsyncRun__scheduleImmediateJsOverride_internalCallback.prototype = {
+    call$0() {
+      this.callback.call$0();
+    },
+    $signature: 3
+  };
+  A._AsyncRun__scheduleImmediateWithSetImmediate_internalCallback.prototype = {
+    call$0() {
+      this.callback.call$0();
+    },
+    $signature: 3
+  };
+  A._TimerImpl.prototype = {
+    _TimerImpl$2(milliseconds, callback) {
+      if (self.setTimeout != null)
+        self.setTimeout(A.convertDartClosureToJS(new A._TimerImpl_internalCallback(this, callback), 0), milliseconds);
+      else
+        throw A.wrapException(A.UnsupportedError$("`setTimeout()` not found."));
+    },
+    _TimerImpl$periodic$2(milliseconds, callback) {
+      if (self.setTimeout != null)
+        self.setInterval(A.convertDartClosureToJS(new A._TimerImpl$periodic_closure(this, milliseconds, Date.now(), callback), 0), milliseconds);
+      else
+        throw A.wrapException(A.UnsupportedError$("Periodic timer."));
+    },
+    $isTimer: 1
+  };
+  A._TimerImpl_internalCallback.prototype = {
+    call$0() {
+      this.$this._tick = 1;
+      this.callback.call$0();
+    },
+    $signature: 0
+  };
+  A._TimerImpl$periodic_closure.prototype = {
+    call$0() {
+      var duration, _this = this,
+        t1 = _this.$this,
+        tick = t1._tick + 1,
+        t2 = _this.milliseconds;
+      if (t2 > 0) {
+        duration = Date.now() - _this.start;
+        if (duration > (tick + 1) * t2)
+          tick = B.JSInt_methods.$tdiv(duration, t2);
+      }
+      t1._tick = tick;
+      _this.callback.call$1(t1);
+    },
+    $signature: 3
+  };
+  A.AsyncError.prototype = {
+    toString$0(_) {
+      return A.S(this.error);
+    },
+    $isError: 1,
+    get$stackTrace() {
+      return this.stackTrace;
+    }
+  };
+  A._Completer.prototype = {
+    completeError$2(error, stackTrace) {
+      var replacement;
+      A.checkNotNullable(error, "error", type$.Object);
+      if ((this.future._async$_state & 30) !== 0)
+        throw A.wrapException(A.StateError$("Future already completed"));
+      replacement = $.Zone__current.errorCallback$2(error, stackTrace);
+      if (replacement != null) {
+        error = replacement.error;
+        stackTrace = replacement.stackTrace;
+      } else if (stackTrace == null)
+        stackTrace = A.AsyncError_defaultStackTrace(error);
+      this._completeError$2(error, stackTrace);
+    },
+    completeError$1(error) {
+      return this.completeError$2(error, null);
+    },
+    $isCompleter: 1
+  };
+  A._AsyncCompleter.prototype = {
+    complete$1(_, value) {
+      var t2,
+        t1 = this.$ti;
+      t1._eval$1("1/?")._as(value);
+      t2 = this.future;
+      if ((t2._async$_state & 30) !== 0)
+        throw A.wrapException(A.StateError$("Future already completed"));
+      t2._asyncComplete$1(t1._eval$1("1/")._as(value));
+    },
+    complete$0($receiver) {
+      return this.complete$1($receiver, null);
+    },
+    _completeError$2(error, stackTrace) {
+      this.future._asyncCompleteError$2(error, stackTrace);
+    }
+  };
+  A._SyncCompleter.prototype = {
+    complete$1(_, value) {
+      var t2,
+        t1 = this.$ti;
+      t1._eval$1("1/?")._as(value);
+      t2 = this.future;
+      if ((t2._async$_state & 30) !== 0)
+        throw A.wrapException(A.StateError$("Future already completed"));
+      t2._complete$1(t1._eval$1("1/")._as(value));
+    },
+    complete$0($receiver) {
+      return this.complete$1($receiver, null);
+    },
+    _completeError$2(error, stackTrace) {
+      this.future._completeError$2(error, stackTrace);
+    }
+  };
+  A._FutureListener.prototype = {
+    matchesErrorTest$1(asyncError) {
+      if ((this.state & 15) !== 6)
+        return true;
+      return this.result._zone.runUnary$2$2(type$.bool_Function_Object._as(this.callback), asyncError.error, type$.bool, type$.Object);
+    },
+    handleError$1(asyncError) {
+      var exception, _this = this,
+        errorCallback = _this.errorCallback,
+        result = null,
+        t1 = type$.dynamic,
+        t2 = type$.Object,
+        t3 = asyncError.error,
+        t4 = _this.result._zone;
+      if (type$.dynamic_Function_Object_StackTrace._is(errorCallback))
+        result = t4.runBinary$3$3(errorCallback, t3, asyncError.stackTrace, t1, t2, type$.StackTrace);
+      else
+        result = t4.runUnary$2$2(type$.dynamic_Function_Object._as(errorCallback), t3, t1, t2);
+      try {
+        t1 = _this.$ti._eval$1("2/")._as(result);
+        return t1;
+      } catch (exception) {
+        if (type$.TypeError._is(A.unwrapException(exception))) {
+          if ((_this.state & 1) !== 0)
+            throw A.wrapException(A.ArgumentError$("The error handler of Future.then must return a value of the returned future's type", "onError"));
+          throw A.wrapException(A.ArgumentError$("The error handler of Future.catchError must return a value of the future's type", "onError"));
+        } else
+          throw exception;
+      }
+    }
+  };
+  A._Future.prototype = {
+    _setChained$1(source) {
+      this._async$_state = this._async$_state & 1 | 4;
+      this._resultOrListeners = source;
+    },
+    then$1$2$onError(f, onError, $R) {
+      var currentZone, result, t2,
+        t1 = this.$ti;
+      t1._bind$1($R)._eval$1("1/(2)")._as(f);
+      currentZone = $.Zone__current;
+      if (currentZone === B.C__RootZone) {
+        if (onError != null && !type$.dynamic_Function_Object_StackTrace._is(onError) && !type$.dynamic_Function_Object._is(onError))
+          throw A.wrapException(A.ArgumentError$value(onError, "onError", string$.Error_));
+      } else {
+        f = currentZone.registerUnaryCallback$2$1(f, $R._eval$1("0/"), t1._precomputed1);
+        if (onError != null)
+          onError = A._registerErrorHandler(onError, currentZone);
+      }
+      result = new A._Future($.Zone__current, $R._eval$1("_Future<0>"));
+      t2 = onError == null ? 1 : 3;
+      this._addListener$1(new A._FutureListener(result, t2, f, onError, t1._eval$1("@<1>")._bind$1($R)._eval$1("_FutureListener<1,2>")));
+      return result;
+    },
+    then$1$1(f, $R) {
+      return this.then$1$2$onError(f, null, $R);
+    },
+    whenComplete$1(action) {
+      var t1, t2, result;
+      type$.dynamic_Function._as(action);
+      t1 = this.$ti;
+      t2 = $.Zone__current;
+      result = new A._Future(t2, t1);
+      if (t2 !== B.C__RootZone)
+        action = t2.registerCallback$1$1(action, type$.dynamic);
+      this._addListener$1(new A._FutureListener(result, 8, action, null, t1._eval$1("@<1>")._bind$1(t1._precomputed1)._eval$1("_FutureListener<1,2>")));
+      return result;
+    },
+    _setErrorObject$1(error) {
+      this._async$_state = this._async$_state & 1 | 16;
+      this._resultOrListeners = error;
+    },
+    _cloneResult$1(source) {
+      this._async$_state = source._async$_state & 30 | this._async$_state & 1;
+      this._resultOrListeners = source._resultOrListeners;
+    },
+    _addListener$1(listener) {
+      var source, _this = this,
+        t1 = _this._async$_state;
+      if (t1 <= 3) {
+        listener._nextListener = type$.nullable__FutureListener_dynamic_dynamic._as(_this._resultOrListeners);
+        _this._resultOrListeners = listener;
+      } else {
+        if ((t1 & 4) !== 0) {
+          source = type$._Future_dynamic._as(_this._resultOrListeners);
+          if ((source._async$_state & 24) === 0) {
+            source._addListener$1(listener);
+            return;
+          }
+          _this._cloneResult$1(source);
+        }
+        _this._zone.scheduleMicrotask$1(new A._Future__addListener_closure(_this, listener));
+      }
+    },
+    _prependListeners$1(listeners) {
+      var t1, existingListeners, next, cursor, next0, source, _this = this, _box_0 = {};
+      _box_0.listeners = listeners;
+      if (listeners == null)
+        return;
+      t1 = _this._async$_state;
+      if (t1 <= 3) {
+        existingListeners = type$.nullable__FutureListener_dynamic_dynamic._as(_this._resultOrListeners);
+        _this._resultOrListeners = listeners;
+        if (existingListeners != null) {
+          next = listeners._nextListener;
+          for (cursor = listeners; next != null; cursor = next, next = next0)
+            next0 = next._nextListener;
+          cursor._nextListener = existingListeners;
+        }
+      } else {
+        if ((t1 & 4) !== 0) {
+          source = type$._Future_dynamic._as(_this._resultOrListeners);
+          if ((source._async$_state & 24) === 0) {
+            source._prependListeners$1(listeners);
+            return;
+          }
+          _this._cloneResult$1(source);
+        }
+        _box_0.listeners = _this._reverseListeners$1(listeners);
+        _this._zone.scheduleMicrotask$1(new A._Future__prependListeners_closure(_box_0, _this));
+      }
+    },
+    _removeListeners$0() {
+      var current = type$.nullable__FutureListener_dynamic_dynamic._as(this._resultOrListeners);
+      this._resultOrListeners = null;
+      return this._reverseListeners$1(current);
+    },
+    _reverseListeners$1(listeners) {
+      var current, prev, next;
+      for (current = listeners, prev = null; current != null; prev = current, current = next) {
+        next = current._nextListener;
+        current._nextListener = prev;
+      }
+      return prev;
+    },
+    _chainForeignFuture$1(source) {
+      var e, s, exception, _this = this;
+      _this._async$_state ^= 2;
+      try {
+        source.then$1$2$onError(new A._Future__chainForeignFuture_closure(_this), new A._Future__chainForeignFuture_closure0(_this), type$.Null);
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        A.scheduleMicrotask(new A._Future__chainForeignFuture_closure1(_this, e, s));
+      }
+    },
+    _complete$1(value) {
+      var listeners, _this = this,
+        t1 = _this.$ti;
+      t1._eval$1("1/")._as(value);
+      if (t1._eval$1("Future<1>")._is(value))
+        if (t1._is(value))
+          A._Future__chainCoreFutureSync(value, _this);
+        else
+          _this._chainForeignFuture$1(value);
+      else {
+        listeners = _this._removeListeners$0();
+        t1._precomputed1._as(value);
+        _this._async$_state = 8;
+        _this._resultOrListeners = value;
+        A._Future__propagateToListeners(_this, listeners);
+      }
+    },
+    _completeWithValue$1(value) {
+      var listeners, _this = this;
+      _this.$ti._precomputed1._as(value);
+      listeners = _this._removeListeners$0();
+      _this._async$_state = 8;
+      _this._resultOrListeners = value;
+      A._Future__propagateToListeners(_this, listeners);
+    },
+    _completeError$2(error, stackTrace) {
+      var listeners;
+      type$.Object._as(error);
+      type$.StackTrace._as(stackTrace);
+      listeners = this._removeListeners$0();
+      this._setErrorObject$1(A.AsyncError$(error, stackTrace));
+      A._Future__propagateToListeners(this, listeners);
+    },
+    _asyncComplete$1(value) {
+      var t1 = this.$ti;
+      t1._eval$1("1/")._as(value);
+      if (t1._eval$1("Future<1>")._is(value)) {
+        this._chainFuture$1(value);
+        return;
+      }
+      this._asyncCompleteWithValue$1(value);
+    },
+    _asyncCompleteWithValue$1(value) {
+      var _this = this;
+      _this.$ti._precomputed1._as(value);
+      _this._async$_state ^= 2;
+      _this._zone.scheduleMicrotask$1(new A._Future__asyncCompleteWithValue_closure(_this, value));
+    },
+    _chainFuture$1(value) {
+      var t1 = this.$ti;
+      t1._eval$1("Future<1>")._as(value);
+      if (t1._is(value)) {
+        A._Future__chainCoreFutureAsync(value, this);
+        return;
+      }
+      this._chainForeignFuture$1(value);
+    },
+    _asyncCompleteError$2(error, stackTrace) {
+      type$.StackTrace._as(stackTrace);
+      this._async$_state ^= 2;
+      this._zone.scheduleMicrotask$1(new A._Future__asyncCompleteError_closure(this, error, stackTrace));
+    },
+    $isFuture: 1
+  };
+  A._Future__addListener_closure.prototype = {
+    call$0() {
+      A._Future__propagateToListeners(this.$this, this.listener);
+    },
+    $signature: 0
+  };
+  A._Future__prependListeners_closure.prototype = {
+    call$0() {
+      A._Future__propagateToListeners(this.$this, this._box_0.listeners);
+    },
+    $signature: 0
+  };
+  A._Future__chainForeignFuture_closure.prototype = {
+    call$1(value) {
+      var error, stackTrace, exception,
+        t1 = this.$this;
+      t1._async$_state ^= 2;
+      try {
+        t1._completeWithValue$1(t1.$ti._precomputed1._as(value));
+      } catch (exception) {
+        error = A.unwrapException(exception);
+        stackTrace = A.getTraceFromException(exception);
+        t1._completeError$2(error, stackTrace);
+      }
+    },
+    $signature: 10
+  };
+  A._Future__chainForeignFuture_closure0.prototype = {
+    call$2(error, stackTrace) {
+      this.$this._completeError$2(type$.Object._as(error), type$.StackTrace._as(stackTrace));
+    },
+    $signature: 26
+  };
+  A._Future__chainForeignFuture_closure1.prototype = {
+    call$0() {
+      this.$this._completeError$2(this.e, this.s);
+    },
+    $signature: 0
+  };
+  A._Future__chainCoreFutureAsync_closure.prototype = {
+    call$0() {
+      A._Future__chainCoreFutureSync(this._box_0.source, this.target);
+    },
+    $signature: 0
+  };
+  A._Future__asyncCompleteWithValue_closure.prototype = {
+    call$0() {
+      this.$this._completeWithValue$1(this.value);
+    },
+    $signature: 0
+  };
+  A._Future__asyncCompleteError_closure.prototype = {
+    call$0() {
+      this.$this._completeError$2(this.error, this.stackTrace);
+    },
+    $signature: 0
+  };
+  A._Future__propagateToListeners_handleWhenCompleteCallback.prototype = {
+    call$0() {
+      var e, s, t1, exception, t2, originalSource, _this = this, completeResult = null;
+      try {
+        t1 = _this._box_0.listener;
+        completeResult = t1.result._zone.run$1$1(type$.dynamic_Function._as(t1.callback), type$.dynamic);
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        t1 = _this.hasError && type$.AsyncError._as(_this._box_1.source._resultOrListeners).error === e;
+        t2 = _this._box_0;
+        if (t1)
+          t2.listenerValueOrError = type$.AsyncError._as(_this._box_1.source._resultOrListeners);
+        else
+          t2.listenerValueOrError = A.AsyncError$(e, s);
+        t2.listenerHasError = true;
+        return;
+      }
+      if (completeResult instanceof A._Future && (completeResult._async$_state & 24) !== 0) {
+        if ((completeResult._async$_state & 16) !== 0) {
+          t1 = _this._box_0;
+          t1.listenerValueOrError = type$.AsyncError._as(completeResult._resultOrListeners);
+          t1.listenerHasError = true;
+        }
+        return;
+      }
+      if (completeResult instanceof A._Future) {
+        originalSource = _this._box_1.source;
+        t1 = _this._box_0;
+        t1.listenerValueOrError = completeResult.then$1$1(new A._Future__propagateToListeners_handleWhenCompleteCallback_closure(originalSource), type$.dynamic);
+        t1.listenerHasError = false;
+      }
+    },
+    $signature: 0
+  };
+  A._Future__propagateToListeners_handleWhenCompleteCallback_closure.prototype = {
+    call$1(_) {
+      return this.originalSource;
+    },
+    $signature: 28
+  };
+  A._Future__propagateToListeners_handleValueCallback.prototype = {
+    call$0() {
+      var e, s, t1, t2, t3, t4, t5, exception;
+      try {
+        t1 = this._box_0;
+        t2 = t1.listener;
+        t3 = t2.$ti;
+        t4 = t3._precomputed1;
+        t5 = t4._as(this.sourceResult);
+        t1.listenerValueOrError = t2.result._zone.runUnary$2$2(t3._eval$1("2/(1)")._as(t2.callback), t5, t3._eval$1("2/"), t4);
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        t1 = this._box_0;
+        t1.listenerValueOrError = A.AsyncError$(e, s);
+        t1.listenerHasError = true;
+      }
+    },
+    $signature: 0
+  };
+  A._Future__propagateToListeners_handleError.prototype = {
+    call$0() {
+      var asyncError, e, s, t1, exception, t2, _this = this;
+      try {
+        asyncError = type$.AsyncError._as(_this._box_1.source._resultOrListeners);
+        t1 = _this._box_0;
+        if (t1.listener.matchesErrorTest$1(asyncError) && t1.listener.errorCallback != null) {
+          t1.listenerValueOrError = t1.listener.handleError$1(asyncError);
+          t1.listenerHasError = false;
+        }
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        t1 = type$.AsyncError._as(_this._box_1.source._resultOrListeners);
+        t2 = _this._box_0;
+        if (t1.error === e)
+          t2.listenerValueOrError = t1;
+        else
+          t2.listenerValueOrError = A.AsyncError$(e, s);
+        t2.listenerHasError = true;
+      }
+    },
+    $signature: 0
+  };
+  A._AsyncCallbackEntry.prototype = {};
+  A.Stream.prototype = {
+    pipe$1(streamConsumer) {
+      A._instanceType(this)._eval$1("StreamConsumer<Stream.T>")._as(streamConsumer);
+      return streamConsumer.addStream$1(0, this).then$1$1(new A.Stream_pipe_closure(streamConsumer), type$.dynamic);
+    },
+    get$length(_) {
+      var t1 = {},
+        future = new A._Future($.Zone__current, type$._Future_int);
+      t1.count = 0;
+      this.listen$4$cancelOnError$onDone$onError(new A.Stream_length_closure(t1, this), true, new A.Stream_length_closure0(t1, future), future.get$_completeError());
+      return future;
+    }
+  };
+  A.Stream_pipe_closure.prototype = {
+    call$1(_) {
+      return this.streamConsumer.close$0(0);
+    },
+    $signature: 29
+  };
+  A.Stream_length_closure.prototype = {
+    call$1(_) {
+      A._instanceType(this.$this)._eval$1("Stream.T")._as(_);
+      ++this._box_0.count;
+    },
+    $signature() {
+      return A._instanceType(this.$this)._eval$1("~(Stream.T)");
+    }
+  };
+  A.Stream_length_closure0.prototype = {
+    call$0() {
+      this.future._complete$1(this._box_0.count);
+    },
+    $signature: 0
+  };
+  A._StreamController.prototype = {
+    get$_pendingEvents() {
+      var t1, _this = this;
+      if ((_this._async$_state & 8) === 0)
+        return A._instanceType(_this)._eval$1("_PendingEvents<1>?")._as(_this._varData);
+      t1 = A._instanceType(_this);
+      return t1._eval$1("_PendingEvents<1>?")._as(t1._eval$1("_StreamControllerAddStreamState<1>")._as(_this._varData).varData);
+    },
+    _ensurePendingEvents$0() {
+      var events, t1, state, _this = this;
+      if ((_this._async$_state & 8) === 0) {
+        events = _this._varData;
+        if (events == null)
+          events = _this._varData = new A._PendingEvents(A._instanceType(_this)._eval$1("_PendingEvents<1>"));
+        return A._instanceType(_this)._eval$1("_PendingEvents<1>")._as(events);
+      }
+      t1 = A._instanceType(_this);
+      state = t1._eval$1("_StreamControllerAddStreamState<1>")._as(_this._varData);
+      events = state.varData;
+      if (events == null)
+        events = state.varData = new A._PendingEvents(t1._eval$1("_PendingEvents<1>"));
+      return t1._eval$1("_PendingEvents<1>")._as(events);
+    },
+    get$_async$_subscription() {
+      var varData = this._varData;
+      if ((this._async$_state & 8) !== 0)
+        varData = type$._StreamControllerAddStreamState_nullable_Object._as(varData).varData;
+      return A._instanceType(this)._eval$1("_ControllerSubscription<1>")._as(varData);
+    },
+    _badEventState$0() {
+      if ((this._async$_state & 4) !== 0)
+        return new A.StateError("Cannot add event after closing");
+      return new A.StateError("Cannot add event while adding a stream");
+    },
+    _ensureDoneFuture$0() {
+      var t1 = this._doneFuture;
+      if (t1 == null)
+        t1 = this._doneFuture = (this._async$_state & 2) !== 0 ? $.$get$Future__nullFuture() : new A._Future($.Zone__current, type$._Future_void);
+      return t1;
+    },
+    add$1(_, value) {
+      var _this = this;
+      A._instanceType(_this)._precomputed1._as(value);
+      if (_this._async$_state >= 4)
+        throw A.wrapException(_this._badEventState$0());
+      _this._add$1(0, value);
+    },
+    addError$2(error, stackTrace) {
+      var replacement,
+        t1 = type$.Object;
+      t1._as(error);
+      type$.nullable_StackTrace._as(stackTrace);
+      A.checkNotNullable(error, "error", t1);
+      if (this._async$_state >= 4)
+        throw A.wrapException(this._badEventState$0());
+      replacement = $.Zone__current.errorCallback$2(error, stackTrace);
+      if (replacement != null) {
+        error = replacement.error;
+        stackTrace = replacement.stackTrace;
+      } else if (stackTrace == null)
+        stackTrace = A.AsyncError_defaultStackTrace(error);
+      this._async$_addError$2(error, stackTrace);
+    },
+    addError$1(error) {
+      return this.addError$2(error, null);
+    },
+    close$0(_) {
+      var _this = this,
+        t1 = _this._async$_state;
+      if ((t1 & 4) !== 0)
+        return _this._ensureDoneFuture$0();
+      if (t1 >= 4)
+        throw A.wrapException(_this._badEventState$0());
+      t1 = _this._async$_state = t1 | 4;
+      if ((t1 & 1) !== 0)
+        _this._sendDone$0();
+      else if ((t1 & 3) === 0)
+        _this._ensurePendingEvents$0().add$1(0, B.C__DelayedDone);
+      return _this._ensureDoneFuture$0();
+    },
+    _add$1(_, value) {
+      var t2, _this = this,
+        t1 = A._instanceType(_this);
+      t1._precomputed1._as(value);
+      t2 = _this._async$_state;
+      if ((t2 & 1) !== 0)
+        _this._sendData$1(value);
+      else if ((t2 & 3) === 0)
+        _this._ensurePendingEvents$0().add$1(0, new A._DelayedData(value, t1._eval$1("_DelayedData<1>")));
+    },
+    _async$_addError$2(error, stackTrace) {
+      var t1 = this._async$_state;
+      if ((t1 & 1) !== 0)
+        this._sendError$2(error, stackTrace);
+      else if ((t1 & 3) === 0)
+        this._ensurePendingEvents$0().add$1(0, new A._DelayedError(error, stackTrace));
+    },
+    _subscribe$4(onData, onError, onDone, cancelOnError) {
+      var t2, t3, t4, t5, t6, subscription, pendingEvents, addState, _this = this,
+        t1 = A._instanceType(_this);
+      t1._eval$1("~(1)?")._as(onData);
+      type$.nullable_void_Function._as(onDone);
+      if ((_this._async$_state & 3) !== 0)
+        throw A.wrapException(A.StateError$("Stream has already been listened to."));
+      t2 = $.Zone__current;
+      t3 = cancelOnError ? 1 : 0;
+      t4 = A._BufferingStreamSubscription__registerDataHandler(t2, onData, t1._precomputed1);
+      t5 = A._BufferingStreamSubscription__registerErrorHandler(t2, onError);
+      t6 = onDone == null ? A.async___nullDoneHandler$closure() : onDone;
+      subscription = new A._ControllerSubscription(_this, t4, t5, t2.registerCallback$1$1(t6, type$.void), t2, t3, t1._eval$1("_ControllerSubscription<1>"));
+      pendingEvents = _this.get$_pendingEvents();
+      t3 = _this._async$_state |= 1;
+      if ((t3 & 8) !== 0) {
+        addState = t1._eval$1("_StreamControllerAddStreamState<1>")._as(_this._varData);
+        addState.varData = subscription;
+        addState.addSubscription.resume$0(0);
+      } else
+        _this._varData = subscription;
+      subscription._setPendingEvents$1(pendingEvents);
+      subscription._guardCallback$1(new A._StreamController__subscribe_closure(_this));
+      return subscription;
+    },
+    _recordCancel$1(subscription) {
+      var result, onCancel, cancelResult, e, s, exception, result0, _this = this,
+        t1 = A._instanceType(_this);
+      t1._eval$1("StreamSubscription<1>")._as(subscription);
+      result = null;
+      if ((_this._async$_state & 8) !== 0)
+        result = t1._eval$1("_StreamControllerAddStreamState<1>")._as(_this._varData).cancel$0(0);
+      _this._varData = null;
+      _this._async$_state = _this._async$_state & 4294967286 | 2;
+      onCancel = _this.onCancel;
+      if (onCancel != null)
+        if (result == null)
+          try {
+            cancelResult = onCancel.call$0();
+            if (cancelResult instanceof A._Future)
+              result = cancelResult;
+          } catch (exception) {
+            e = A.unwrapException(exception);
+            s = A.getTraceFromException(exception);
+            result0 = new A._Future($.Zone__current, type$._Future_void);
+            result0._asyncCompleteError$2(e, s);
+            result = result0;
+          }
+        else
+          result = result.whenComplete$1(onCancel);
+      t1 = new A._StreamController__recordCancel_complete(_this);
+      if (result != null)
+        result = result.whenComplete$1(t1);
+      else
+        t1.call$0();
+      return result;
+    },
+    $isStreamConsumer: 1,
+    $isStreamSink: 1,
+    $isStreamController: 1,
+    $is_StreamControllerLifecycle: 1,
+    $is_EventDispatch: 1
+  };
+  A._StreamController__subscribe_closure.prototype = {
+    call$0() {
+      A._runGuarded(this.$this.onListen);
+    },
+    $signature: 0
+  };
+  A._StreamController__recordCancel_complete.prototype = {
+    call$0() {
+      var doneFuture = this.$this._doneFuture;
+      if (doneFuture != null && (doneFuture._async$_state & 30) === 0)
+        doneFuture._asyncComplete$1(null);
+    },
+    $signature: 0
+  };
+  A._SyncStreamControllerDispatch.prototype = {
+    _sendData$1(data) {
+      this.$ti._precomputed1._as(data);
+      this.get$_async$_subscription()._add$1(0, data);
+    },
+    _sendError$2(error, stackTrace) {
+      this.get$_async$_subscription()._async$_addError$2(error, stackTrace);
+    },
+    _sendDone$0() {
+      this.get$_async$_subscription()._close$0();
+    }
+  };
+  A._SyncStreamController.prototype = {};
+  A._ControllerStream.prototype = {
+    get$hashCode(_) {
+      return (A.Primitives_objectHashCode(this._controller) ^ 892482866) >>> 0;
+    },
+    $eq(_, other) {
+      if (other == null)
+        return false;
+      if (this === other)
+        return true;
+      return other instanceof A._ControllerStream && other._controller === this._controller;
+    }
+  };
+  A._ControllerSubscription.prototype = {
+    _onCancel$0() {
+      return this._controller._recordCancel$1(this);
+    },
+    _onPause$0() {
+      var t1 = this._controller,
+        t2 = A._instanceType(t1);
+      t2._eval$1("StreamSubscription<1>")._as(this);
+      if ((t1._async$_state & 8) !== 0)
+        t2._eval$1("_StreamControllerAddStreamState<1>")._as(t1._varData).addSubscription.pause$0(0);
+      A._runGuarded(t1.onPause);
+    },
+    _onResume$0() {
+      var t1 = this._controller,
+        t2 = A._instanceType(t1);
+      t2._eval$1("StreamSubscription<1>")._as(this);
+      if ((t1._async$_state & 8) !== 0)
+        t2._eval$1("_StreamControllerAddStreamState<1>")._as(t1._varData).addSubscription.resume$0(0);
+      A._runGuarded(t1.onResume);
+    }
+  };
+  A._StreamSinkWrapper.prototype = {
+    add$1(_, data) {
+      this._target.add$1(0, this.$ti._precomputed1._as(data));
+    },
+    $isStreamConsumer: 1,
+    $isStreamSink: 1
+  };
+  A._AddStreamState_cancel_closure.prototype = {
+    call$0() {
+      this.$this.addStreamFuture._asyncComplete$1(null);
+    },
+    $signature: 3
+  };
+  A._BufferingStreamSubscription.prototype = {
+    _setPendingEvents$1(pendingEvents) {
+      var _this = this;
+      A._instanceType(_this)._eval$1("_PendingEvents<_BufferingStreamSubscription.T>?")._as(pendingEvents);
+      if (pendingEvents == null)
+        return;
+      _this.set$_pending(pendingEvents);
+      if (pendingEvents.lastPendingEvent != null) {
+        _this._async$_state = (_this._async$_state | 64) >>> 0;
+        pendingEvents.schedule$1(_this);
+      }
+    },
+    onData$1(handleData) {
+      var t1 = A._instanceType(this);
+      this.set$_onData(A._BufferingStreamSubscription__registerDataHandler(this._zone, t1._eval$1("~(_BufferingStreamSubscription.T)?")._as(handleData), t1._eval$1("_BufferingStreamSubscription.T")));
+    },
+    onError$1(_, handleError) {
+      this._onError = A._BufferingStreamSubscription__registerErrorHandler(this._zone, handleError);
+    },
+    cancel$0(_) {
+      var _this = this,
+        t1 = (_this._async$_state & 4294967279) >>> 0;
+      _this._async$_state = t1;
+      if ((t1 & 8) === 0)
+        _this._cancel$0();
+      t1 = _this._cancelFuture;
+      return t1 == null ? $.$get$Future__nullFuture() : t1;
+    },
+    _cancel$0() {
+      var t2, _this = this,
+        t1 = _this._async$_state = (_this._async$_state | 8) >>> 0;
+      if ((t1 & 64) !== 0) {
+        t2 = _this._pending;
+        if (t2._async$_state === 1)
+          t2._async$_state = 3;
+      }
+      if ((t1 & 32) === 0)
+        _this.set$_pending(null);
+      _this._cancelFuture = _this._onCancel$0();
+    },
+    _add$1(_, data) {
+      var t2, _this = this,
+        t1 = A._instanceType(_this);
+      t1._eval$1("_BufferingStreamSubscription.T")._as(data);
+      t2 = _this._async$_state;
+      if ((t2 & 8) !== 0)
+        return;
+      if (t2 < 32)
+        _this._sendData$1(data);
+      else
+        _this._addPending$1(new A._DelayedData(data, t1._eval$1("_DelayedData<_BufferingStreamSubscription.T>")));
+    },
+    _async$_addError$2(error, stackTrace) {
+      var t1 = this._async$_state;
+      if ((t1 & 8) !== 0)
+        return;
+      if (t1 < 32)
+        this._sendError$2(error, stackTrace);
+      else
+        this._addPending$1(new A._DelayedError(error, stackTrace));
+    },
+    _close$0() {
+      var _this = this,
+        t1 = _this._async$_state;
+      if ((t1 & 8) !== 0)
+        return;
+      t1 = (t1 | 2) >>> 0;
+      _this._async$_state = t1;
+      if (t1 < 32)
+        _this._sendDone$0();
+      else
+        _this._addPending$1(B.C__DelayedDone);
+    },
+    _onPause$0() {
+    },
+    _onResume$0() {
+    },
+    _onCancel$0() {
+      return null;
+    },
+    _addPending$1($event) {
+      var t1, _this = this,
+        pending = _this._pending;
+      if (pending == null) {
+        pending = new A._PendingEvents(A._instanceType(_this)._eval$1("_PendingEvents<_BufferingStreamSubscription.T>"));
+        _this.set$_pending(pending);
+      }
+      pending.add$1(0, $event);
+      t1 = _this._async$_state;
+      if ((t1 & 64) === 0) {
+        t1 = (t1 | 64) >>> 0;
+        _this._async$_state = t1;
+        if (t1 < 128)
+          pending.schedule$1(_this);
+      }
+    },
+    _sendData$1(data) {
+      var t2, _this = this,
+        t1 = A._instanceType(_this)._eval$1("_BufferingStreamSubscription.T");
+      t1._as(data);
+      t2 = _this._async$_state;
+      _this._async$_state = (t2 | 32) >>> 0;
+      _this._zone.runUnaryGuarded$1$2(_this._onData, data, t1);
+      _this._async$_state = (_this._async$_state & 4294967263) >>> 0;
+      _this._checkState$1((t2 & 4) !== 0);
+    },
+    _sendError$2(error, stackTrace) {
+      var cancelFuture, _this = this,
+        t1 = _this._async$_state,
+        t2 = new A._BufferingStreamSubscription__sendError_sendError(_this, error, stackTrace);
+      if ((t1 & 1) !== 0) {
+        _this._async$_state = (t1 | 16) >>> 0;
+        _this._cancel$0();
+        cancelFuture = _this._cancelFuture;
+        if (cancelFuture != null && cancelFuture !== $.$get$Future__nullFuture())
+          cancelFuture.whenComplete$1(t2);
+        else
+          t2.call$0();
+      } else {
+        t2.call$0();
+        _this._checkState$1((t1 & 4) !== 0);
+      }
+    },
+    _sendDone$0() {
+      var cancelFuture, _this = this,
+        t1 = new A._BufferingStreamSubscription__sendDone_sendDone(_this);
+      _this._cancel$0();
+      _this._async$_state = (_this._async$_state | 16) >>> 0;
+      cancelFuture = _this._cancelFuture;
+      if (cancelFuture != null && cancelFuture !== $.$get$Future__nullFuture())
+        cancelFuture.whenComplete$1(t1);
+      else
+        t1.call$0();
+    },
+    _guardCallback$1(callback) {
+      var t1, _this = this;
+      type$.void_Function._as(callback);
+      t1 = _this._async$_state;
+      _this._async$_state = (t1 | 32) >>> 0;
+      callback.call$0();
+      _this._async$_state = (_this._async$_state & 4294967263) >>> 0;
+      _this._checkState$1((t1 & 4) !== 0);
+    },
+    _checkState$1(wasInputPaused) {
+      var t2, isInputPaused, _this = this,
+        t1 = _this._async$_state;
+      if ((t1 & 64) !== 0 && _this._pending.lastPendingEvent == null) {
+        t1 = _this._async$_state = (t1 & 4294967231) >>> 0;
+        if ((t1 & 4) !== 0)
+          if (t1 < 128) {
+            t2 = _this._pending;
+            t2 = t2 == null ? null : t2.lastPendingEvent == null;
+            t2 = t2 !== false;
+          } else
+            t2 = false;
+        else
+          t2 = false;
+        if (t2) {
+          t1 = (t1 & 4294967291) >>> 0;
+          _this._async$_state = t1;
+        }
+      }
+      for (; true; wasInputPaused = isInputPaused) {
+        if ((t1 & 8) !== 0) {
+          _this.set$_pending(null);
+          return;
+        }
+        isInputPaused = (t1 & 4) !== 0;
+        if (wasInputPaused === isInputPaused)
+          break;
+        _this._async$_state = (t1 ^ 32) >>> 0;
+        if (isInputPaused)
+          _this._onPause$0();
+        else
+          _this._onResume$0();
+        t1 = (_this._async$_state & 4294967263) >>> 0;
+        _this._async$_state = t1;
+      }
+      if ((t1 & 64) !== 0 && t1 < 128)
+        _this._pending.schedule$1(_this);
+    },
+    set$_onData(_onData) {
+      this._onData = A._instanceType(this)._eval$1("~(_BufferingStreamSubscription.T)")._as(_onData);
+    },
+    set$_pending(_pending) {
+      this._pending = A._instanceType(this)._eval$1("_PendingEvents<_BufferingStreamSubscription.T>?")._as(_pending);
+    },
+    $isStreamSubscription: 1,
+    $is_EventDispatch: 1
+  };
+  A._BufferingStreamSubscription__sendError_sendError.prototype = {
+    call$0() {
+      var onError, t3, t4,
+        t1 = this.$this,
+        t2 = t1._async$_state;
+      if ((t2 & 8) !== 0 && (t2 & 16) === 0)
+        return;
+      t1._async$_state = (t2 | 32) >>> 0;
+      onError = t1._onError;
+      t2 = this.error;
+      t3 = type$.Object;
+      t4 = t1._zone;
+      if (type$.void_Function_Object_StackTrace._is(onError))
+        t4.runBinaryGuarded$2$3(onError, t2, this.stackTrace, t3, type$.StackTrace);
+      else
+        t4.runUnaryGuarded$1$2(type$.void_Function_Object._as(onError), t2, t3);
+      t1._async$_state = (t1._async$_state & 4294967263) >>> 0;
+    },
+    $signature: 0
+  };
+  A._BufferingStreamSubscription__sendDone_sendDone.prototype = {
+    call$0() {
+      var t1 = this.$this,
+        t2 = t1._async$_state;
+      if ((t2 & 16) === 0)
+        return;
+      t1._async$_state = (t2 | 42) >>> 0;
+      t1._zone.runGuarded$1(t1._onDone);
+      t1._async$_state = (t1._async$_state & 4294967263) >>> 0;
+    },
+    $signature: 0
+  };
+  A._StreamImpl.prototype = {
+    listen$4$cancelOnError$onDone$onError(onData, cancelOnError, onDone, onError) {
+      var t1 = this.$ti;
+      t1._eval$1("~(1)?")._as(onData);
+      type$.nullable_void_Function._as(onDone);
+      return this._controller._subscribe$4(t1._eval$1("~(1)?")._as(onData), onError, onDone, cancelOnError === true);
+    },
+    listen$1(onData) {
+      return this.listen$4$cancelOnError$onDone$onError(onData, null, null, null);
+    },
+    listen$3$onDone$onError(onData, onDone, onError) {
+      return this.listen$4$cancelOnError$onDone$onError(onData, null, onDone, onError);
+    },
+    listen$2$onDone(onData, onDone) {
+      return this.listen$4$cancelOnError$onDone$onError(onData, null, onDone, null);
+    },
+    listen$3$cancelOnError$onDone(onData, cancelOnError, onDone) {
+      return this.listen$4$cancelOnError$onDone$onError(onData, cancelOnError, onDone, null);
+    }
+  };
+  A._DelayedEvent.prototype = {
+    set$next(_, next) {
+      this.next = type$.nullable__DelayedEvent_dynamic._as(next);
+    },
+    get$next(receiver) {
+      return this.next;
+    }
+  };
+  A._DelayedData.prototype = {
+    perform$1(dispatch) {
+      this.$ti._eval$1("_EventDispatch<1>")._as(dispatch)._sendData$1(this.value);
+    }
+  };
+  A._DelayedError.prototype = {
+    perform$1(dispatch) {
+      dispatch._sendError$2(this.error, this.stackTrace);
+    }
+  };
+  A._DelayedDone.prototype = {
+    perform$1(dispatch) {
+      dispatch._sendDone$0();
+    },
+    get$next(_) {
+      return null;
+    },
+    set$next(_, _0) {
+      throw A.wrapException(A.StateError$("No events after a done."));
+    },
+    $is_DelayedEvent: 1
+  };
+  A._PendingEvents.prototype = {
+    schedule$1(dispatch) {
+      var t1, _this = this;
+      _this.$ti._eval$1("_EventDispatch<1>")._as(dispatch);
+      t1 = _this._async$_state;
+      if (t1 === 1)
+        return;
+      if (t1 >= 1) {
+        _this._async$_state = 1;
+        return;
+      }
+      A.scheduleMicrotask(new A._PendingEvents_schedule_closure(_this, dispatch));
+      _this._async$_state = 1;
+    },
+    add$1(_, $event) {
+      var _this = this,
+        lastEvent = _this.lastPendingEvent;
+      if (lastEvent == null)
+        _this.firstPendingEvent = _this.lastPendingEvent = $event;
+      else {
+        lastEvent.set$next(0, $event);
+        _this.lastPendingEvent = $event;
+      }
+    }
+  };
+  A._PendingEvents_schedule_closure.prototype = {
+    call$0() {
+      var t2, $event, nextEvent,
+        t1 = this.$this,
+        oldState = t1._async$_state;
+      t1._async$_state = 0;
+      if (oldState === 3)
+        return;
+      t2 = t1.$ti._eval$1("_EventDispatch<1>")._as(this.dispatch);
+      $event = t1.firstPendingEvent;
+      nextEvent = $event.get$next($event);
+      t1.firstPendingEvent = nextEvent;
+      if (nextEvent == null)
+        t1.lastPendingEvent = null;
+      $event.perform$1(t2);
+    },
+    $signature: 0
+  };
+  A._DoneStreamSubscription.prototype = {
+    onData$1(handleData) {
+      this.$ti._eval$1("~(1)?")._as(handleData);
+    },
+    onError$1(_, handleError) {
+    },
+    cancel$0(_) {
+      this._async$_state = -1;
+      this.set$_onDone(null);
+      return $.$get$Future__nullFuture();
+    },
+    _onMicrotask$0() {
+      var _0_0, doneHandler, t1, _this = this,
+        unscheduledState = _this._async$_state - 1;
+      if (unscheduledState === 0) {
+        _this._async$_state = -1;
+        _0_0 = _this._onDone;
+        if (_0_0 != null) {
+          doneHandler = _0_0;
+          t1 = true;
+        } else {
+          doneHandler = null;
+          t1 = false;
+        }
+        if (t1) {
+          _this.set$_onDone(null);
+          _this._zone.runGuarded$1(doneHandler);
+        }
+      } else
+        _this._async$_state = unscheduledState;
+    },
+    set$_onDone(_onDone) {
+      this._onDone = type$.nullable_void_Function._as(_onDone);
+    },
+    $isStreamSubscription: 1
+  };
+  A._EmptyStream.prototype = {
+    listen$4$cancelOnError$onDone$onError(onData, cancelOnError, onDone, onError) {
+      var t2,
+        t1 = this.$ti;
+      t1._eval$1("~(1)?")._as(onData);
+      type$.nullable_void_Function._as(onDone);
+      t2 = $.Zone__current;
+      t1 = new A._DoneStreamSubscription(t2, t1._eval$1("_DoneStreamSubscription<1>"));
+      A.scheduleMicrotask(t1.get$_onMicrotask());
+      if (onDone != null)
+        t1.set$_onDone(t2.registerCallback$1$1(onDone, type$.void));
+      return t1;
+    },
+    listen$1(onData) {
+      return this.listen$4$cancelOnError$onDone$onError(onData, null, null, null);
+    },
+    listen$3$onDone$onError(onData, onDone, onError) {
+      return this.listen$4$cancelOnError$onDone$onError(onData, null, onDone, onError);
+    },
+    listen$3$cancelOnError$onDone(onData, cancelOnError, onDone) {
+      return this.listen$4$cancelOnError$onDone$onError(onData, cancelOnError, onDone, null);
+    }
+  };
+  A._ZoneFunction.prototype = {};
+  A._ZoneSpecification.prototype = {$isZoneSpecification: 1};
+  A._ZoneDelegate.prototype = {$isZoneDelegate: 1};
+  A._Zone.prototype = {
+    _processUncaughtError$3(zone, error, stackTrace) {
+      var implZone, handler, parentDelegate, parentZone, currentZone, e, s, implementation, t1, exception;
+      type$.StackTrace._as(stackTrace);
+      implementation = this.get$_handleUncaughtError();
+      implZone = implementation.zone;
+      if (implZone === B.C__RootZone) {
+        A._rootHandleError(error, stackTrace);
+        return;
+      }
+      handler = implementation.$function;
+      parentDelegate = implZone.get$_parentDelegate();
+      t1 = J.get$parent$z(implZone);
+      t1.toString;
+      parentZone = t1;
+      currentZone = $.Zone__current;
+      try {
+        $.Zone__current = parentZone;
+        handler.call$5(implZone, parentDelegate, zone, error, stackTrace);
+        $.Zone__current = currentZone;
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        $.Zone__current = currentZone;
+        t1 = error === e ? stackTrace : s;
+        parentZone._processUncaughtError$3(implZone, e, t1);
+      }
+    },
+    $isZone: 1
+  };
+  A._CustomZone.prototype = {
+    get$_delegate() {
+      var t1 = this._delegateCache;
+      return t1 == null ? this._delegateCache = new A._ZoneDelegate(this) : t1;
+    },
+    get$_parentDelegate() {
+      return this.parent.get$_delegate();
+    },
+    get$errorZone() {
+      return this._handleUncaughtError.zone;
+    },
+    runGuarded$1(f) {
+      var e, s, exception;
+      type$.void_Function._as(f);
+      try {
+        this.run$1$1(f, type$.void);
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        this._processUncaughtError$3(this, type$.Object._as(e), type$.StackTrace._as(s));
+      }
+    },
+    runUnaryGuarded$1$2(f, arg, $T) {
+      var e, s, exception;
+      $T._eval$1("~(0)")._as(f);
+      $T._as(arg);
+      try {
+        this.runUnary$2$2(f, arg, type$.void, $T);
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        this._processUncaughtError$3(this, type$.Object._as(e), type$.StackTrace._as(s));
+      }
+    },
+    runBinaryGuarded$2$3(f, arg1, arg2, T1, T2) {
+      var e, s, exception;
+      T1._eval$1("@<0>")._bind$1(T2)._eval$1("~(1,2)")._as(f);
+      T1._as(arg1);
+      T2._as(arg2);
+      try {
+        this.runBinary$3$3(f, arg1, arg2, type$.void, T1, T2);
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        this._processUncaughtError$3(this, type$.Object._as(e), type$.StackTrace._as(s));
+      }
+    },
+    bindCallback$1$1(f, $R) {
+      return new A._CustomZone_bindCallback_closure(this, this.registerCallback$1$1($R._eval$1("0()")._as(f), $R), $R);
+    },
+    bindUnaryCallback$2$1(f, $R, $T) {
+      return new A._CustomZone_bindUnaryCallback_closure(this, this.registerUnaryCallback$2$1($R._eval$1("@<0>")._bind$1($T)._eval$1("1(2)")._as(f), $R, $T), $T, $R);
+    },
+    bindCallbackGuarded$1(f) {
+      return new A._CustomZone_bindCallbackGuarded_closure(this, this.registerCallback$1$1(type$.void_Function._as(f), type$.void));
+    },
+    bindUnaryCallbackGuarded$1$1(f, $T) {
+      return new A._CustomZone_bindUnaryCallbackGuarded_closure(this, this.registerUnaryCallback$2$1($T._eval$1("~(0)")._as(f), type$.void, $T), $T);
+    },
+    handleUncaughtError$2(error, stackTrace) {
+      this._processUncaughtError$3(this, error, type$.StackTrace._as(stackTrace));
+    },
+    fork$2$specification$zoneValues(specification, zoneValues) {
+      var implementation = this._fork,
+        t1 = implementation.zone;
+      return implementation.$function.call$5(t1, t1.get$_parentDelegate(), this, specification, zoneValues);
+    },
+    run$1$1(f, $R) {
+      var implementation, t1;
+      $R._eval$1("0()")._as(f);
+      implementation = this._run;
+      t1 = implementation.zone;
+      return implementation.$function.call$1$4(t1, t1.get$_parentDelegate(), this, f, $R);
+    },
+    runUnary$2$2(f, arg, $R, $T) {
+      var implementation, t1;
+      $R._eval$1("@<0>")._bind$1($T)._eval$1("1(2)")._as(f);
+      $T._as(arg);
+      implementation = this._runUnary;
+      t1 = implementation.zone;
+      return implementation.$function.call$2$5(t1, t1.get$_parentDelegate(), this, f, arg, $R, $T);
+    },
+    runBinary$3$3(f, arg1, arg2, $R, T1, T2) {
+      var implementation, t1;
+      $R._eval$1("@<0>")._bind$1(T1)._bind$1(T2)._eval$1("1(2,3)")._as(f);
+      T1._as(arg1);
+      T2._as(arg2);
+      implementation = this._runBinary;
+      t1 = implementation.zone;
+      return implementation.$function.call$3$6(t1, t1.get$_parentDelegate(), this, f, arg1, arg2, $R, T1, T2);
+    },
+    registerCallback$1$1(callback, $R) {
+      var implementation, t1;
+      $R._eval$1("0()")._as(callback);
+      implementation = this._registerCallback;
+      t1 = implementation.zone;
+      return implementation.$function.call$1$4(t1, t1.get$_parentDelegate(), this, callback, $R);
+    },
+    registerUnaryCallback$2$1(callback, $R, $T) {
+      var implementation, t1;
+      $R._eval$1("@<0>")._bind$1($T)._eval$1("1(2)")._as(callback);
+      implementation = this._registerUnaryCallback;
+      t1 = implementation.zone;
+      return implementation.$function.call$2$4(t1, t1.get$_parentDelegate(), this, callback, $R, $T);
+    },
+    registerBinaryCallback$3$1(callback, $R, T1, T2) {
+      var implementation, t1;
+      $R._eval$1("@<0>")._bind$1(T1)._bind$1(T2)._eval$1("1(2,3)")._as(callback);
+      implementation = this._registerBinaryCallback;
+      t1 = implementation.zone;
+      return implementation.$function.call$3$4(t1, t1.get$_parentDelegate(), this, callback, $R, T1, T2);
+    },
+    errorCallback$2(error, stackTrace) {
+      var implementation, implementationZone;
+      A.checkNotNullable(error, "error", type$.Object);
+      implementation = this._errorCallback;
+      implementationZone = implementation.zone;
+      if (implementationZone === B.C__RootZone)
+        return null;
+      return implementation.$function.call$5(implementationZone, implementationZone.get$_parentDelegate(), this, error, stackTrace);
+    },
+    scheduleMicrotask$1(f) {
+      var implementation, t1;
+      type$.void_Function._as(f);
+      implementation = this._scheduleMicrotask;
+      t1 = implementation.zone;
+      return implementation.$function.call$4(t1, t1.get$_parentDelegate(), this, f);
+    },
+    createPeriodicTimer$2(duration, f) {
+      var implementation, t1;
+      type$.void_Function_Timer._as(f);
+      implementation = this._createPeriodicTimer;
+      t1 = implementation.zone;
+      return implementation.$function.call$5(t1, t1.get$_parentDelegate(), this, duration, f);
+    },
+    set$_handleUncaughtError(_handleUncaughtError) {
+      this._handleUncaughtError = type$._ZoneFunction_of_void_Function_Zone_ZoneDelegate_Zone_Object_StackTrace._as(_handleUncaughtError);
+    },
+    get$_run() {
+      return this._run;
+    },
+    get$_runUnary() {
+      return this._runUnary;
+    },
+    get$_runBinary() {
+      return this._runBinary;
+    },
+    get$_registerCallback() {
+      return this._registerCallback;
+    },
+    get$_registerUnaryCallback() {
+      return this._registerUnaryCallback;
+    },
+    get$_registerBinaryCallback() {
+      return this._registerBinaryCallback;
+    },
+    get$_errorCallback() {
+      return this._errorCallback;
+    },
+    get$_scheduleMicrotask() {
+      return this._scheduleMicrotask;
+    },
+    get$_createTimer() {
+      return this._createTimer;
+    },
+    get$_createPeriodicTimer() {
+      return this._createPeriodicTimer;
+    },
+    get$_print() {
+      return this._print;
+    },
+    get$_fork() {
+      return this._fork;
+    },
+    get$_handleUncaughtError() {
+      return this._handleUncaughtError;
+    },
+    get$parent(receiver) {
+      return this.parent;
+    },
+    get$_async$_map() {
+      return this._async$_map;
+    }
+  };
+  A._CustomZone_bindCallback_closure.prototype = {
+    call$0() {
+      return this.$this.run$1$1(this.registered, this.R);
+    },
+    $signature() {
+      return this.R._eval$1("0()");
+    }
+  };
+  A._CustomZone_bindUnaryCallback_closure.prototype = {
+    call$1(arg) {
+      var _this = this,
+        t1 = _this.T;
+      return _this.$this.runUnary$2$2(_this.registered, t1._as(arg), _this.R, t1);
+    },
+    $signature() {
+      return this.R._eval$1("@<0>")._bind$1(this.T)._eval$1("1(2)");
+    }
+  };
+  A._CustomZone_bindCallbackGuarded_closure.prototype = {
+    call$0() {
+      return this.$this.runGuarded$1(this.registered);
+    },
+    $signature: 0
+  };
+  A._CustomZone_bindUnaryCallbackGuarded_closure.prototype = {
+    call$1(arg) {
+      var t1 = this.T;
+      return this.$this.runUnaryGuarded$1$2(this.registered, t1._as(arg), t1);
+    },
+    $signature() {
+      return this.T._eval$1("~(0)");
+    }
+  };
+  A._rootHandleError_closure.prototype = {
+    call$0() {
+      A.Error_throwWithStackTrace(this.error, this.stackTrace);
+    },
+    $signature: 0
+  };
+  A._RootZone.prototype = {
+    get$_run() {
+      return B._ZoneFunction__RootZone__rootRun;
+    },
+    get$_runUnary() {
+      return B._ZoneFunction__RootZone__rootRunUnary;
+    },
+    get$_runBinary() {
+      return B._ZoneFunction__RootZone__rootRunBinary;
+    },
+    get$_registerCallback() {
+      return B._ZoneFunction__RootZone__rootRegisterCallback;
+    },
+    get$_registerUnaryCallback() {
+      return B._ZoneFunction_Eeh;
+    },
+    get$_registerBinaryCallback() {
+      return B._ZoneFunction_7G2;
+    },
+    get$_errorCallback() {
+      return B._ZoneFunction__RootZone__rootErrorCallback;
+    },
+    get$_scheduleMicrotask() {
+      return B._ZoneFunction__RootZone__rootScheduleMicrotask;
+    },
+    get$_createTimer() {
+      return B._ZoneFunction__RootZone__rootCreateTimer;
+    },
+    get$_createPeriodicTimer() {
+      return B._ZoneFunction_3bB;
+    },
+    get$_print() {
+      return B._ZoneFunction__RootZone__rootPrint;
+    },
+    get$_fork() {
+      return B._ZoneFunction__RootZone__rootFork;
+    },
+    get$_handleUncaughtError() {
+      return B._ZoneFunction_NMc;
+    },
+    get$parent(_) {
+      return null;
+    },
+    get$_async$_map() {
+      return $.$get$_RootZone__rootMap();
+    },
+    get$_delegate() {
+      var t1 = $._RootZone__rootDelegate;
+      return t1 == null ? $._RootZone__rootDelegate = new A._ZoneDelegate(this) : t1;
+    },
+    get$_parentDelegate() {
+      var t1 = $._RootZone__rootDelegate;
+      return t1 == null ? $._RootZone__rootDelegate = new A._ZoneDelegate(this) : t1;
+    },
+    get$errorZone() {
+      return this;
+    },
+    runGuarded$1(f) {
+      var e, s, exception;
+      type$.void_Function._as(f);
+      try {
+        if (B.C__RootZone === $.Zone__current) {
+          f.call$0();
+          return;
+        }
+        A._rootRun(null, null, this, f, type$.void);
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        A._rootHandleError(type$.Object._as(e), type$.StackTrace._as(s));
+      }
+    },
+    runUnaryGuarded$1$2(f, arg, $T) {
+      var e, s, exception;
+      $T._eval$1("~(0)")._as(f);
+      $T._as(arg);
+      try {
+        if (B.C__RootZone === $.Zone__current) {
+          f.call$1(arg);
+          return;
+        }
+        A._rootRunUnary(null, null, this, f, arg, type$.void, $T);
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        A._rootHandleError(type$.Object._as(e), type$.StackTrace._as(s));
+      }
+    },
+    runBinaryGuarded$2$3(f, arg1, arg2, T1, T2) {
+      var e, s, exception;
+      T1._eval$1("@<0>")._bind$1(T2)._eval$1("~(1,2)")._as(f);
+      T1._as(arg1);
+      T2._as(arg2);
+      try {
+        if (B.C__RootZone === $.Zone__current) {
+          f.call$2(arg1, arg2);
+          return;
+        }
+        A._rootRunBinary(null, null, this, f, arg1, arg2, type$.void, T1, T2);
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        A._rootHandleError(type$.Object._as(e), type$.StackTrace._as(s));
+      }
+    },
+    bindCallback$1$1(f, $R) {
+      return new A._RootZone_bindCallback_closure(this, $R._eval$1("0()")._as(f), $R);
+    },
+    bindUnaryCallback$2$1(f, $R, $T) {
+      return new A._RootZone_bindUnaryCallback_closure(this, $R._eval$1("@<0>")._bind$1($T)._eval$1("1(2)")._as(f), $T, $R);
+    },
+    bindCallbackGuarded$1(f) {
+      return new A._RootZone_bindCallbackGuarded_closure(this, type$.void_Function._as(f));
+    },
+    bindUnaryCallbackGuarded$1$1(f, $T) {
+      return new A._RootZone_bindUnaryCallbackGuarded_closure(this, $T._eval$1("~(0)")._as(f), $T);
+    },
+    handleUncaughtError$2(error, stackTrace) {
+      A._rootHandleError(error, type$.StackTrace._as(stackTrace));
+    },
+    fork$2$specification$zoneValues(specification, zoneValues) {
+      return A._rootFork(null, null, this, specification, zoneValues);
+    },
+    run$1$1(f, $R) {
+      $R._eval$1("0()")._as(f);
+      if ($.Zone__current === B.C__RootZone)
+        return f.call$0();
+      return A._rootRun(null, null, this, f, $R);
+    },
+    runUnary$2$2(f, arg, $R, $T) {
+      $R._eval$1("@<0>")._bind$1($T)._eval$1("1(2)")._as(f);
+      $T._as(arg);
+      if ($.Zone__current === B.C__RootZone)
+        return f.call$1(arg);
+      return A._rootRunUnary(null, null, this, f, arg, $R, $T);
+    },
+    runBinary$3$3(f, arg1, arg2, $R, T1, T2) {
+      $R._eval$1("@<0>")._bind$1(T1)._bind$1(T2)._eval$1("1(2,3)")._as(f);
+      T1._as(arg1);
+      T2._as(arg2);
+      if ($.Zone__current === B.C__RootZone)
+        return f.call$2(arg1, arg2);
+      return A._rootRunBinary(null, null, this, f, arg1, arg2, $R, T1, T2);
+    },
+    registerCallback$1$1(f, $R) {
+      return $R._eval$1("0()")._as(f);
+    },
+    registerUnaryCallback$2$1(f, $R, $T) {
+      return $R._eval$1("@<0>")._bind$1($T)._eval$1("1(2)")._as(f);
+    },
+    registerBinaryCallback$3$1(f, $R, T1, T2) {
+      return $R._eval$1("@<0>")._bind$1(T1)._bind$1(T2)._eval$1("1(2,3)")._as(f);
+    },
+    errorCallback$2(error, stackTrace) {
+      return null;
+    },
+    scheduleMicrotask$1(f) {
+      A._rootScheduleMicrotask(null, null, this, type$.void_Function._as(f));
+    },
+    createPeriodicTimer$2(duration, f) {
+      return A.Timer__createPeriodicTimer(duration, type$.void_Function_Timer._as(f));
+    }
+  };
+  A._RootZone_bindCallback_closure.prototype = {
+    call$0() {
+      return this.$this.run$1$1(this.f, this.R);
+    },
+    $signature() {
+      return this.R._eval$1("0()");
+    }
+  };
+  A._RootZone_bindUnaryCallback_closure.prototype = {
+    call$1(arg) {
+      var _this = this,
+        t1 = _this.T;
+      return _this.$this.runUnary$2$2(_this.f, t1._as(arg), _this.R, t1);
+    },
+    $signature() {
+      return this.R._eval$1("@<0>")._bind$1(this.T)._eval$1("1(2)");
+    }
+  };
+  A._RootZone_bindCallbackGuarded_closure.prototype = {
+    call$0() {
+      return this.$this.runGuarded$1(this.f);
+    },
+    $signature: 0
+  };
+  A._RootZone_bindUnaryCallbackGuarded_closure.prototype = {
+    call$1(arg) {
+      var t1 = this.T;
+      return this.$this.runUnaryGuarded$1$2(this.f, t1._as(arg), t1);
+    },
+    $signature() {
+      return this.T._eval$1("~(0)");
+    }
+  };
+  A.runZonedGuarded_closure.prototype = {
+    call$5($self, $parent, zone, error, stackTrace) {
+      var e, s, exception, t2,
+        t1 = type$.StackTrace;
+      t1._as(stackTrace);
+      try {
+        this.parentZone.runBinary$3$3(this.onError, error, stackTrace, type$.void, type$.Object, t1);
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        s = A.getTraceFromException(exception);
+        t2 = $parent._delegationTarget;
+        if (e === error)
+          t2._processUncaughtError$3(zone, error, stackTrace);
+        else
+          t2._processUncaughtError$3(zone, type$.Object._as(e), t1._as(s));
+      }
+    },
+    $signature: 34
+  };
+  A._HashMap.prototype = {
+    get$length(_) {
+      return this._collection$_length;
+    },
+    get$isEmpty(_) {
+      return this._collection$_length === 0;
+    },
+    get$keys(_) {
+      return new A._HashMapKeyIterable(this, A._instanceType(this)._eval$1("_HashMapKeyIterable<1>"));
+    },
+    containsKey$1(_, key) {
+      var strings, nums;
+      if (typeof key == "string" && key !== "__proto__") {
+        strings = this._collection$_strings;
+        return strings == null ? false : strings[key] != null;
+      } else if (typeof key == "number" && (key & 1073741823) === key) {
+        nums = this._collection$_nums;
+        return nums == null ? false : nums[key] != null;
+      } else
+        return this._containsKey$1(key);
+    },
+    _containsKey$1(key) {
+      var rest = this._collection$_rest;
+      if (rest == null)
+        return false;
+      return this._findBucketIndex$2(this._getBucket$2(rest, key), key) >= 0;
+    },
+    $index(_, key) {
+      var strings, t1, nums;
+      if (typeof key == "string" && key !== "__proto__") {
+        strings = this._collection$_strings;
+        t1 = strings == null ? null : A._HashMap__getTableEntry(strings, key);
+        return t1;
+      } else if (typeof key == "number" && (key & 1073741823) === key) {
+        nums = this._collection$_nums;
+        t1 = nums == null ? null : A._HashMap__getTableEntry(nums, key);
+        return t1;
+      } else
+        return this._get$1(0, key);
+    },
+    _get$1(_, key) {
+      var bucket, index,
+        rest = this._collection$_rest;
+      if (rest == null)
+        return null;
+      bucket = this._getBucket$2(rest, key);
+      index = this._findBucketIndex$2(bucket, key);
+      return index < 0 ? null : bucket[index + 1];
+    },
+    $indexSet(_, key, value) {
+      var strings, nums, _this = this,
+        t1 = A._instanceType(_this);
+      t1._precomputed1._as(key);
+      t1._rest[1]._as(value);
+      if (typeof key == "string" && key !== "__proto__") {
+        strings = _this._collection$_strings;
+        _this._collection$_addHashTableEntry$3(strings == null ? _this._collection$_strings = A._HashMap__newHashTable() : strings, key, value);
+      } else if (typeof key == "number" && (key & 1073741823) === key) {
+        nums = _this._collection$_nums;
+        _this._collection$_addHashTableEntry$3(nums == null ? _this._collection$_nums = A._HashMap__newHashTable() : nums, key, value);
+      } else
+        _this._set$2(key, value);
+    },
+    _set$2(key, value) {
+      var rest, hash, bucket, index, _this = this,
+        t1 = A._instanceType(_this);
+      t1._precomputed1._as(key);
+      t1._rest[1]._as(value);
+      rest = _this._collection$_rest;
+      if (rest == null)
+        rest = _this._collection$_rest = A._HashMap__newHashTable();
+      hash = _this._computeHashCode$1(key);
+      bucket = rest[hash];
+      if (bucket == null) {
+        A._HashMap__setTableEntry(rest, hash, [key, value]);
+        ++_this._collection$_length;
+        _this._keys = null;
+      } else {
+        index = _this._findBucketIndex$2(bucket, key);
+        if (index >= 0)
+          bucket[index + 1] = value;
+        else {
+          bucket.push(key, value);
+          ++_this._collection$_length;
+          _this._keys = null;
+        }
+      }
+    },
+    forEach$1(_, action) {
+      var keys, $length, t2, i, key, t3, _this = this,
+        t1 = A._instanceType(_this);
+      t1._eval$1("~(1,2)")._as(action);
+      keys = _this._computeKeys$0();
+      for ($length = keys.length, t2 = t1._precomputed1, t1 = t1._rest[1], i = 0; i < $length; ++i) {
+        key = keys[i];
+        t2._as(key);
+        t3 = _this.$index(0, key);
+        action.call$2(key, t3 == null ? t1._as(t3) : t3);
+        if (keys !== _this._keys)
+          throw A.wrapException(A.ConcurrentModificationError$(_this));
+      }
+    },
+    _computeKeys$0() {
+      var strings, names, entries, index, i, nums, rest, bucket, $length, i0, _this = this,
+        result = _this._keys;
+      if (result != null)
+        return result;
+      result = A.List_List$filled(_this._collection$_length, null, false, type$.dynamic);
+      strings = _this._collection$_strings;
+      if (strings != null) {
+        names = Object.getOwnPropertyNames(strings);
+        entries = names.length;
+        for (index = 0, i = 0; i < entries; ++i) {
+          result[index] = names[i];
+          ++index;
+        }
+      } else
+        index = 0;
+      nums = _this._collection$_nums;
+      if (nums != null) {
+        names = Object.getOwnPropertyNames(nums);
+        entries = names.length;
+        for (i = 0; i < entries; ++i) {
+          result[index] = +names[i];
+          ++index;
+        }
+      }
+      rest = _this._collection$_rest;
+      if (rest != null) {
+        names = Object.getOwnPropertyNames(rest);
+        entries = names.length;
+        for (i = 0; i < entries; ++i) {
+          bucket = rest[names[i]];
+          $length = bucket.length;
+          for (i0 = 0; i0 < $length; i0 += 2) {
+            result[index] = bucket[i0];
+            ++index;
+          }
+        }
+      }
+      return _this._keys = result;
+    },
+    _collection$_addHashTableEntry$3(table, key, value) {
+      var t1 = A._instanceType(this);
+      t1._precomputed1._as(key);
+      t1._rest[1]._as(value);
+      if (table[key] == null) {
+        ++this._collection$_length;
+        this._keys = null;
+      }
+      A._HashMap__setTableEntry(table, key, value);
+    },
+    _computeHashCode$1(key) {
+      return J.get$hashCode$(key) & 1073741823;
+    },
+    _getBucket$2(table, key) {
+      return table[this._computeHashCode$1(key)];
+    },
+    _findBucketIndex$2(bucket, key) {
+      var $length, i;
+      if (bucket == null)
+        return -1;
+      $length = bucket.length;
+      for (i = 0; i < $length; i += 2)
+        if (J.$eq$(bucket[i], key))
+          return i;
+      return -1;
+    }
+  };
+  A._IdentityHashMap.prototype = {
+    _computeHashCode$1(key) {
+      return A.objectHashCode(key) & 1073741823;
+    },
+    _findBucketIndex$2(bucket, key) {
+      var $length, i, t1;
+      if (bucket == null)
+        return -1;
+      $length = bucket.length;
+      for (i = 0; i < $length; i += 2) {
+        t1 = bucket[i];
+        if (t1 == null ? key == null : t1 === key)
+          return i;
+      }
+      return -1;
+    }
+  };
+  A._HashMapKeyIterable.prototype = {
+    get$length(_) {
+      return this._collection$_map._collection$_length;
+    },
+    get$isEmpty(_) {
+      return this._collection$_map._collection$_length === 0;
+    },
+    get$isNotEmpty(_) {
+      return this._collection$_map._collection$_length !== 0;
+    },
+    get$iterator(_) {
+      var t1 = this._collection$_map;
+      return new A._HashMapKeyIterator(t1, t1._computeKeys$0(), this.$ti._eval$1("_HashMapKeyIterator<1>"));
+    },
+    contains$1(_, element) {
+      return this._collection$_map.containsKey$1(0, element);
+    }
+  };
+  A._HashMapKeyIterator.prototype = {
+    get$current(_) {
+      var t1 = this._collection$_current;
+      return t1 == null ? this.$ti._precomputed1._as(t1) : t1;
+    },
+    moveNext$0() {
+      var _this = this,
+        keys = _this._keys,
+        offset = _this._offset,
+        t1 = _this._collection$_map;
+      if (keys !== t1._keys)
+        throw A.wrapException(A.ConcurrentModificationError$(t1));
+      else if (offset >= keys.length) {
+        _this.set$_collection$_current(null);
+        return false;
+      } else {
+        _this.set$_collection$_current(keys[offset]);
+        _this._offset = offset + 1;
+        return true;
+      }
+    },
+    set$_collection$_current(_current) {
+      this._collection$_current = this.$ti._eval$1("1?")._as(_current);
+    },
+    $isIterator: 1
+  };
+  A._LinkedHashSet.prototype = {
+    get$iterator(_) {
+      var _this = this,
+        t1 = new A._LinkedHashSetIterator(_this, _this._collection$_modifications, _this.$ti._eval$1("_LinkedHashSetIterator<1>"));
+      t1._collection$_cell = _this._collection$_first;
+      return t1;
+    },
+    get$length(_) {
+      return this._collection$_length;
+    },
+    get$isEmpty(_) {
+      return this._collection$_length === 0;
+    },
+    get$isNotEmpty(_) {
+      return this._collection$_length !== 0;
+    },
+    contains$1(_, object) {
+      var nums;
+      if ((object & 1073741823) === object) {
+        nums = this._collection$_nums;
+        if (nums == null)
+          return false;
+        return type$.nullable__LinkedHashSetCell._as(nums[object]) != null;
+      } else
+        return this._contains$1(object);
+    },
+    _contains$1(object) {
+      var rest = this._collection$_rest;
+      if (rest == null)
+        return false;
+      return this._findBucketIndex$2(rest[B.JSInt_methods.get$hashCode(object) & 1073741823], object) >= 0;
+    },
+    add$1(_, element) {
+      var strings, nums, _this = this;
+      _this.$ti._precomputed1._as(element);
+      if (typeof element == "string" && element !== "__proto__") {
+        strings = _this._collection$_strings;
+        return _this._collection$_addHashTableEntry$2(strings == null ? _this._collection$_strings = A._LinkedHashSet__newHashTable() : strings, element);
+      } else if (typeof element == "number" && (element & 1073741823) === element) {
+        nums = _this._collection$_nums;
+        return _this._collection$_addHashTableEntry$2(nums == null ? _this._collection$_nums = A._LinkedHashSet__newHashTable() : nums, element);
+      } else
+        return _this._collection$_add$1(0, element);
+    },
+    _collection$_add$1(_, element) {
+      var rest, hash, bucket, _this = this;
+      _this.$ti._precomputed1._as(element);
+      rest = _this._collection$_rest;
+      if (rest == null)
+        rest = _this._collection$_rest = A._LinkedHashSet__newHashTable();
+      hash = J.get$hashCode$(element) & 1073741823;
+      bucket = rest[hash];
+      if (bucket == null)
+        rest[hash] = [_this._collection$_newLinkedCell$1(element)];
+      else {
+        if (_this._findBucketIndex$2(bucket, element) >= 0)
+          return false;
+        bucket.push(_this._collection$_newLinkedCell$1(element));
+      }
+      return true;
+    },
+    remove$1(_, object) {
+      if ((object & 1073741823) === object)
+        return this._collection$_removeHashTableEntry$2(this._collection$_nums, object);
+      else
+        return this._remove$1(0, object);
+    },
+    _remove$1(_, object) {
+      var hash, bucket, index, cell,
+        rest = this._collection$_rest;
+      if (rest == null)
+        return false;
+      hash = B.JSInt_methods.get$hashCode(object) & 1073741823;
+      bucket = rest[hash];
+      index = this._findBucketIndex$2(bucket, object);
+      if (index < 0)
+        return false;
+      cell = bucket.splice(index, 1)[0];
+      if (0 === bucket.length)
+        delete rest[hash];
+      this._collection$_unlinkCell$1(cell);
+      return true;
+    },
+    _collection$_addHashTableEntry$2(table, element) {
+      this.$ti._precomputed1._as(element);
+      if (type$.nullable__LinkedHashSetCell._as(table[element]) != null)
+        return false;
+      table[element] = this._collection$_newLinkedCell$1(element);
+      return true;
+    },
+    _collection$_removeHashTableEntry$2(table, element) {
+      var cell;
+      if (table == null)
+        return false;
+      cell = type$.nullable__LinkedHashSetCell._as(table[element]);
+      if (cell == null)
+        return false;
+      this._collection$_unlinkCell$1(cell);
+      delete table[element];
+      return true;
+    },
+    _collection$_modified$0() {
+      this._collection$_modifications = this._collection$_modifications + 1 & 1073741823;
+    },
+    _collection$_newLinkedCell$1(element) {
+      var t1, _this = this,
+        cell = new A._LinkedHashSetCell(_this.$ti._precomputed1._as(element));
+      if (_this._collection$_first == null)
+        _this._collection$_first = _this._collection$_last = cell;
+      else {
+        t1 = _this._collection$_last;
+        t1.toString;
+        cell._collection$_previous = t1;
+        _this._collection$_last = t1._collection$_next = cell;
+      }
+      ++_this._collection$_length;
+      _this._collection$_modified$0();
+      return cell;
+    },
+    _collection$_unlinkCell$1(cell) {
+      var _this = this,
+        previous = cell._collection$_previous,
+        next = cell._collection$_next;
+      if (previous == null)
+        _this._collection$_first = next;
+      else
+        previous._collection$_next = next;
+      if (next == null)
+        _this._collection$_last = previous;
+      else
+        next._collection$_previous = previous;
+      --_this._collection$_length;
+      _this._collection$_modified$0();
+    },
+    _findBucketIndex$2(bucket, element) {
+      var $length, i;
+      if (bucket == null)
+        return -1;
+      $length = bucket.length;
+      for (i = 0; i < $length; ++i)
+        if (J.$eq$(bucket[i]._element, element))
+          return i;
+      return -1;
+    }
+  };
+  A._LinkedHashSetCell.prototype = {};
+  A._LinkedHashSetIterator.prototype = {
+    get$current(_) {
+      var t1 = this._collection$_current;
+      return t1 == null ? this.$ti._precomputed1._as(t1) : t1;
+    },
+    moveNext$0() {
+      var _this = this,
+        cell = _this._collection$_cell,
+        t1 = _this._set;
+      if (_this._collection$_modifications !== t1._collection$_modifications)
+        throw A.wrapException(A.ConcurrentModificationError$(t1));
+      else if (cell == null) {
+        _this.set$_collection$_current(null);
+        return false;
+      } else {
+        _this.set$_collection$_current(_this.$ti._eval$1("1?")._as(cell._element));
+        _this._collection$_cell = cell._collection$_next;
+        return true;
+      }
+    },
+    set$_collection$_current(_current) {
+      this._collection$_current = this.$ti._eval$1("1?")._as(_current);
+    },
+    $isIterator: 1
+  };
+  A.ListBase.prototype = {
+    get$iterator(receiver) {
+      return new A.ListIterator(receiver, this.get$length(receiver), A.instanceType(receiver)._eval$1("ListIterator<ListBase.E>"));
+    },
+    elementAt$1(receiver, index) {
+      return this.$index(receiver, index);
+    },
+    get$isEmpty(receiver) {
+      return this.get$length(receiver) === 0;
+    },
+    get$isNotEmpty(receiver) {
+      return !this.get$isEmpty(receiver);
+    },
+    get$first(receiver) {
+      if (this.get$length(receiver) === 0)
+        throw A.wrapException(A.IterableElementError_noElement());
+      return this.$index(receiver, 0);
+    },
+    map$1$1(receiver, f, $T) {
+      var t1 = A.instanceType(receiver);
+      return new A.MappedListIterable(receiver, t1._bind$1($T)._eval$1("1(ListBase.E)")._as(f), t1._eval$1("@<ListBase.E>")._bind$1($T)._eval$1("MappedListIterable<1,2>"));
+    },
+    skip$1(receiver, count) {
+      return A.SubListIterable$(receiver, count, null, A.instanceType(receiver)._eval$1("ListBase.E"));
+    },
+    cast$1$0(receiver, $R) {
+      return new A.CastList(receiver, A.instanceType(receiver)._eval$1("@<ListBase.E>")._bind$1($R)._eval$1("CastList<1,2>"));
+    },
+    fillRange$3(receiver, start, end, fill) {
+      var i;
+      A.instanceType(receiver)._eval$1("ListBase.E?")._as(fill);
+      A.RangeError_checkValidRange(start, end, this.get$length(receiver));
+      for (i = start; i < end; ++i)
+        this.$indexSet(receiver, i, fill);
+    },
+    toString$0(receiver) {
+      return A.Iterable_iterableToFullString(receiver, "[", "]");
+    },
+    $isEfficientLengthIterable: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A.MapBase.prototype = {
+    forEach$1(receiver, action) {
+      var t2, key, t3,
+        t1 = A.instanceType(receiver);
+      t1._eval$1("~(MapBase.K,MapBase.V)")._as(action);
+      for (t2 = J.get$iterator$ax(this.get$keys(receiver)), t1 = t1._eval$1("MapBase.V"); t2.moveNext$0();) {
+        key = t2.get$current(t2);
+        t3 = this.$index(receiver, key);
+        action.call$2(key, t3 == null ? t1._as(t3) : t3);
+      }
+    },
+    containsKey$1(receiver, key) {
+      return J.contains$1$asx(this.get$keys(receiver), key);
+    },
+    get$length(receiver) {
+      return J.get$length$asx(this.get$keys(receiver));
+    },
+    get$isEmpty(receiver) {
+      return J.get$isEmpty$asx(this.get$keys(receiver));
+    },
+    toString$0(receiver) {
+      return A.MapBase_mapToString(receiver);
+    },
+    $isMap: 1
+  };
+  A.MapBase_mapToString_closure.prototype = {
+    call$2(k, v) {
+      var t2,
+        t1 = this._box_0;
+      if (!t1.first)
+        this.result._contents += ", ";
+      t1.first = false;
+      t1 = this.result;
+      t2 = t1._contents += A.S(k);
+      t1._contents = t2 + ": ";
+      t1._contents += A.S(v);
+    },
+    $signature: 16
+  };
+  A._UnmodifiableMapMixin.prototype = {
+    $indexSet(_, key, value) {
+      var t1 = A._instanceType(this);
+      t1._precomputed1._as(key);
+      t1._rest[1]._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot modify unmodifiable map"));
+    }
+  };
+  A.MapView.prototype = {
+    $index(_, key) {
+      return J.$index$asx(this._collection$_map, key);
+    },
+    $indexSet(_, key, value) {
+      var t1 = A._instanceType(this);
+      J.$indexSet$ax(this._collection$_map, t1._precomputed1._as(key), t1._rest[1]._as(value));
+    },
+    containsKey$1(_, key) {
+      return J.containsKey$1$x(this._collection$_map, key);
+    },
+    forEach$1(_, action) {
+      J.forEach$1$x(this._collection$_map, A._instanceType(this)._eval$1("~(1,2)")._as(action));
+    },
+    get$isEmpty(_) {
+      return J.get$isEmpty$asx(this._collection$_map);
+    },
+    get$length(_) {
+      return J.get$length$asx(this._collection$_map);
+    },
+    get$keys(_) {
+      return J.get$keys$x(this._collection$_map);
+    },
+    toString$0(_) {
+      return J.toString$0$(this._collection$_map);
+    },
+    $isMap: 1
+  };
+  A.UnmodifiableMapView.prototype = {};
+  A.SetBase.prototype = {
+    get$isEmpty(_) {
+      return this._collection$_length === 0;
+    },
+    get$isNotEmpty(_) {
+      return this._collection$_length !== 0;
+    },
+    map$1$1(_, f, $T) {
+      var t1 = this.$ti;
+      return new A.EfficientLengthMappedIterable(this, t1._bind$1($T)._eval$1("1(2)")._as(f), t1._eval$1("@<1>")._bind$1($T)._eval$1("EfficientLengthMappedIterable<1,2>"));
+    },
+    toString$0(_) {
+      return A.Iterable_iterableToFullString(this, "{", "}");
+    },
+    skip$1(_, n) {
+      return A.SkipIterable_SkipIterable(this, n, this.$ti._precomputed1);
+    },
+    elementAt$1(_, index) {
+      var iterator, skipCount, t1, _this = this;
+      A.RangeError_checkNotNegative(index, "index");
+      iterator = A._LinkedHashSetIterator$(_this, _this._collection$_modifications, _this.$ti._precomputed1);
+      for (skipCount = index; iterator.moveNext$0();) {
+        if (skipCount === 0) {
+          t1 = iterator._collection$_current;
+          return t1 == null ? iterator.$ti._precomputed1._as(t1) : t1;
+        }
+        --skipCount;
+      }
+      throw A.wrapException(A.IndexError$withLength(index, index - skipCount, _this, "index"));
+    },
+    $isEfficientLengthIterable: 1,
+    $isIterable: 1,
+    $isSet: 1
+  };
+  A._SetBase.prototype = {};
+  A._UnmodifiableMapView_MapView__UnmodifiableMapMixin.prototype = {};
+  A._JsonMap.prototype = {
+    $index(_, key) {
+      var result,
+        t1 = this._processed;
+      if (t1 == null)
+        return this._data.$index(0, key);
+      else if (typeof key != "string")
+        return null;
+      else {
+        result = t1[key];
+        return typeof result == "undefined" ? this._process$1(key) : result;
+      }
+    },
+    get$length(_) {
+      return this._processed == null ? this._data._length : this._convert$_computeKeys$0().length;
+    },
+    get$isEmpty(_) {
+      return this.get$length(this) === 0;
+    },
+    get$keys(_) {
+      var t1;
+      if (this._processed == null) {
+        t1 = this._data;
+        return new A.LinkedHashMapKeyIterable(t1, A._instanceType(t1)._eval$1("LinkedHashMapKeyIterable<1>"));
+      }
+      return new A._JsonMapKeyIterable(this);
+    },
+    $indexSet(_, key, value) {
+      var processed, original, _this = this;
+      if (_this._processed == null)
+        _this._data.$indexSet(0, key, value);
+      else if (_this.containsKey$1(0, key)) {
+        processed = _this._processed;
+        processed[key] = value;
+        original = _this._original;
+        if (original == null ? processed != null : original !== processed)
+          original[key] = null;
+      } else
+        _this._upgrade$0().$indexSet(0, key, value);
+    },
+    containsKey$1(_, key) {
+      if (this._processed == null)
+        return this._data.containsKey$1(0, key);
+      return Object.prototype.hasOwnProperty.call(this._original, key);
+    },
+    forEach$1(_, f) {
+      var keys, i, key, value, _this = this;
+      type$.void_Function_String_dynamic._as(f);
+      if (_this._processed == null)
+        return _this._data.forEach$1(0, f);
+      keys = _this._convert$_computeKeys$0();
+      for (i = 0; i < keys.length; ++i) {
+        key = keys[i];
+        value = _this._processed[key];
+        if (typeof value == "undefined") {
+          value = A._convertJsonToDartLazy(_this._original[key]);
+          _this._processed[key] = value;
+        }
+        f.call$2(key, value);
+        if (keys !== _this._data)
+          throw A.wrapException(A.ConcurrentModificationError$(_this));
+      }
+    },
+    _convert$_computeKeys$0() {
+      var keys = type$.nullable_List_dynamic._as(this._data);
+      if (keys == null)
+        keys = this._data = A._setArrayType(Object.keys(this._original), type$.JSArray_String);
+      return keys;
+    },
+    _upgrade$0() {
+      var result, keys, i, t1, key, _this = this;
+      if (_this._processed == null)
+        return _this._data;
+      result = A.LinkedHashMap_LinkedHashMap$_empty(type$.String, type$.dynamic);
+      keys = _this._convert$_computeKeys$0();
+      for (i = 0; t1 = keys.length, i < t1; ++i) {
+        key = keys[i];
+        result.$indexSet(0, key, _this.$index(0, key));
+      }
+      if (t1 === 0)
+        B.JSArray_methods.add$1(keys, "");
+      else
+        B.JSArray_methods.clear$0(keys);
+      _this._original = _this._processed = null;
+      return _this._data = result;
+    },
+    _process$1(key) {
+      var result;
+      if (!Object.prototype.hasOwnProperty.call(this._original, key))
+        return null;
+      result = A._convertJsonToDartLazy(this._original[key]);
+      return this._processed[key] = result;
+    }
+  };
+  A._JsonMapKeyIterable.prototype = {
+    get$length(_) {
+      var t1 = this._convert$_parent;
+      return t1.get$length(t1);
+    },
+    elementAt$1(_, index) {
+      var t1 = this._convert$_parent;
+      if (t1._processed == null)
+        t1 = t1.get$keys(t1).elementAt$1(0, index);
+      else {
+        t1 = t1._convert$_computeKeys$0();
+        if (!(index >= 0 && index < t1.length))
+          return A.ioore(t1, index);
+        t1 = t1[index];
+      }
+      return t1;
+    },
+    get$iterator(_) {
+      var t1 = this._convert$_parent;
+      if (t1._processed == null) {
+        t1 = t1.get$keys(t1);
+        t1 = t1.get$iterator(t1);
+      } else {
+        t1 = t1._convert$_computeKeys$0();
+        t1 = new J.ArrayIterator(t1, t1.length, A._arrayInstanceType(t1)._eval$1("ArrayIterator<1>"));
+      }
+      return t1;
+    },
+    contains$1(_, key) {
+      return this._convert$_parent.containsKey$1(0, key);
+    }
+  };
+  A.Utf8Decoder__decoder_closure.prototype = {
+    call$0() {
+      var t1, exception;
+      try {
+        t1 = new TextDecoder("utf-8", {fatal: true});
+        return t1;
+      } catch (exception) {
+      }
+      return null;
+    },
+    $signature: 2
+  };
+  A.Utf8Decoder__decoderNonfatal_closure.prototype = {
+    call$0() {
+      var t1, exception;
+      try {
+        t1 = new TextDecoder("utf-8", {fatal: false});
+        return t1;
+      } catch (exception) {
+      }
+      return null;
+    },
+    $signature: 2
+  };
+  A.AsciiCodec.prototype = {
+    encode$1(source) {
+      return B.AsciiEncoder_127.convert$1(source);
+    }
+  };
+  A._UnicodeSubsetEncoder.prototype = {
+    convert$1(string) {
+      var stringLength, $length, result, t1, i, codeUnit;
+      A._asString(string);
+      stringLength = string.length;
+      $length = A.RangeError_checkValidRange(0, null, stringLength) - 0;
+      result = new Uint8Array($length);
+      for (t1 = ~this._subsetMask, i = 0; i < $length; ++i) {
+        if (!(i < stringLength))
+          return A.ioore(string, i);
+        codeUnit = string.charCodeAt(i);
+        if ((codeUnit & t1) !== 0)
+          throw A.wrapException(A.ArgumentError$value(string, "string", "Contains invalid characters."));
+        if (!(i < $length))
+          return A.ioore(result, i);
+        result[i] = codeUnit;
+      }
+      return result;
+    }
+  };
+  A.AsciiEncoder.prototype = {};
+  A.Base64Codec.prototype = {
+    normalize$3(_, source, start, end) {
+      var inverseAlphabet, t2, i, sliceStart, buffer, firstPadding, firstPaddingSourceIndex, paddingCount, i0, char, i1, digit1, t3, digit2, char0, value, endLength, $length,
+        _s64_ = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
+        _s31_ = "Invalid base64 encoding length ",
+        t1 = source.length;
+      end = A.RangeError_checkValidRange(start, end, t1);
+      inverseAlphabet = $.$get$_Base64Decoder__inverseAlphabet();
+      for (t2 = inverseAlphabet.length, i = start, sliceStart = i, buffer = null, firstPadding = -1, firstPaddingSourceIndex = -1, paddingCount = 0; i < end; i = i0) {
+        i0 = i + 1;
+        if (!(i < t1))
+          return A.ioore(source, i);
+        char = source.charCodeAt(i);
+        if (char === 37) {
+          i1 = i0 + 2;
+          if (i1 <= end) {
+            if (!(i0 < t1))
+              return A.ioore(source, i0);
+            digit1 = A.hexDigitValue(source.charCodeAt(i0));
+            t3 = i0 + 1;
+            if (!(t3 < t1))
+              return A.ioore(source, t3);
+            digit2 = A.hexDigitValue(source.charCodeAt(t3));
+            char0 = digit1 * 16 + digit2 - (digit2 & 256);
+            if (char0 === 37)
+              char0 = -1;
+            i0 = i1;
+          } else
+            char0 = -1;
+        } else
+          char0 = char;
+        if (0 <= char0 && char0 <= 127) {
+          if (!(char0 >= 0 && char0 < t2))
+            return A.ioore(inverseAlphabet, char0);
+          value = inverseAlphabet[char0];
+          if (value >= 0) {
+            if (!(value < 64))
+              return A.ioore(_s64_, value);
+            char0 = _s64_.charCodeAt(value);
+            if (char0 === char)
+              continue;
+            char = char0;
+          } else {
+            if (value === -1) {
+              if (firstPadding < 0) {
+                t3 = buffer == null ? null : buffer._contents.length;
+                if (t3 == null)
+                  t3 = 0;
+                firstPadding = t3 + (i - sliceStart);
+                firstPaddingSourceIndex = i;
+              }
+              ++paddingCount;
+              if (char === 61)
+                continue;
+            }
+            char = char0;
+          }
+          if (value !== -2) {
+            if (buffer == null) {
+              buffer = new A.StringBuffer("");
+              t3 = buffer;
+            } else
+              t3 = buffer;
+            t3._contents += B.JSString_methods.substring$2(source, sliceStart, i);
+            t3._contents += A.Primitives_stringFromCharCode(char);
+            sliceStart = i0;
+            continue;
+          }
+        }
+        throw A.wrapException(A.FormatException$("Invalid base64 data", source, i));
+      }
+      if (buffer != null) {
+        t1 = buffer._contents += B.JSString_methods.substring$2(source, sliceStart, end);
+        t2 = t1.length;
+        if (firstPadding >= 0)
+          A.Base64Codec__checkPadding(source, firstPaddingSourceIndex, end, firstPadding, paddingCount, t2);
+        else {
+          endLength = B.JSInt_methods.$mod(t2 - 1, 4) + 1;
+          if (endLength === 1)
+            throw A.wrapException(A.FormatException$(_s31_, source, end));
+          for (; endLength < 4;) {
+            t1 += "=";
+            buffer._contents = t1;
+            ++endLength;
+          }
+        }
+        t1 = buffer._contents;
+        return B.JSString_methods.replaceRange$3(source, start, end, t1.charCodeAt(0) == 0 ? t1 : t1);
+      }
+      $length = end - start;
+      if (firstPadding >= 0)
+        A.Base64Codec__checkPadding(source, firstPaddingSourceIndex, end, firstPadding, paddingCount, $length);
+      else {
+        endLength = B.JSInt_methods.$mod($length, 4);
+        if (endLength === 1)
+          throw A.wrapException(A.FormatException$(_s31_, source, end));
+        if (endLength > 1)
+          source = B.JSString_methods.replaceRange$3(source, end, end, endLength === 2 ? "==" : "=");
+      }
+      return source;
+    }
+  };
+  A.Base64Encoder.prototype = {};
+  A.Codec.prototype = {};
+  A._FusedCodec.prototype = {};
+  A.Converter.prototype = {$isStreamTransformer: 1};
+  A.Encoding.prototype = {};
+  A.JsonUnsupportedObjectError.prototype = {
+    toString$0(_) {
+      var safeString = A.Error_safeToString(this.unsupportedObject);
+      return (this.cause != null ? "Converting object to an encodable object failed:" : "Converting object did not return an encodable object:") + " " + safeString;
+    }
+  };
+  A.JsonCyclicError.prototype = {
+    toString$0(_) {
+      return "Cyclic error in JSON stringify";
+    }
+  };
+  A.JsonCodec.prototype = {
+    decode$2$reviver(_, source, reviver) {
+      var t1 = A._parseJson(source, this.get$decoder()._reviver);
+      return t1;
+    },
+    encode$2$toEncodable(value, toEncodable) {
+      var t1 = A._JsonStringStringifier_stringify(value, this.get$encoder()._toEncodable, null);
+      return t1;
+    },
+    get$encoder() {
+      return B.JsonEncoder_null;
+    },
+    get$decoder() {
+      return B.JsonDecoder_null;
+    }
+  };
+  A.JsonEncoder.prototype = {};
+  A.JsonDecoder.prototype = {};
+  A._JsonStringifier.prototype = {
+    writeStringContent$1(s) {
+      var offset, i, charCode, t1, t2, _this = this,
+        $length = s.length;
+      for (offset = 0, i = 0; i < $length; ++i) {
+        charCode = s.charCodeAt(i);
+        if (charCode > 92) {
+          if (charCode >= 55296) {
+            t1 = charCode & 64512;
+            if (t1 === 55296) {
+              t2 = i + 1;
+              t2 = !(t2 < $length && (s.charCodeAt(t2) & 64512) === 56320);
+            } else
+              t2 = false;
+            if (!t2)
+              if (t1 === 56320) {
+                t1 = i - 1;
+                t1 = !(t1 >= 0 && (s.charCodeAt(t1) & 64512) === 55296);
+              } else
+                t1 = false;
+            else
+              t1 = true;
+            if (t1) {
+              if (i > offset)
+                _this.writeStringSlice$3(s, offset, i);
+              offset = i + 1;
+              _this.writeCharCode$1(92);
+              _this.writeCharCode$1(117);
+              _this.writeCharCode$1(100);
+              t1 = charCode >>> 8 & 15;
+              _this.writeCharCode$1(t1 < 10 ? 48 + t1 : 87 + t1);
+              t1 = charCode >>> 4 & 15;
+              _this.writeCharCode$1(t1 < 10 ? 48 + t1 : 87 + t1);
+              t1 = charCode & 15;
+              _this.writeCharCode$1(t1 < 10 ? 48 + t1 : 87 + t1);
+            }
+          }
+          continue;
+        }
+        if (charCode < 32) {
+          if (i > offset)
+            _this.writeStringSlice$3(s, offset, i);
+          offset = i + 1;
+          _this.writeCharCode$1(92);
+          switch (charCode) {
+            case 8:
+              _this.writeCharCode$1(98);
+              break;
+            case 9:
+              _this.writeCharCode$1(116);
+              break;
+            case 10:
+              _this.writeCharCode$1(110);
+              break;
+            case 12:
+              _this.writeCharCode$1(102);
+              break;
+            case 13:
+              _this.writeCharCode$1(114);
+              break;
+            default:
+              _this.writeCharCode$1(117);
+              _this.writeCharCode$1(48);
+              _this.writeCharCode$1(48);
+              t1 = charCode >>> 4 & 15;
+              _this.writeCharCode$1(t1 < 10 ? 48 + t1 : 87 + t1);
+              t1 = charCode & 15;
+              _this.writeCharCode$1(t1 < 10 ? 48 + t1 : 87 + t1);
+              break;
+          }
+        } else if (charCode === 34 || charCode === 92) {
+          if (i > offset)
+            _this.writeStringSlice$3(s, offset, i);
+          offset = i + 1;
+          _this.writeCharCode$1(92);
+          _this.writeCharCode$1(charCode);
+        }
+      }
+      if (offset === 0)
+        _this.writeString$1(s);
+      else if (offset < $length)
+        _this.writeStringSlice$3(s, offset, $length);
+    },
+    _checkCycle$1(object) {
+      var t1, t2, i, t3;
+      for (t1 = this._seen, t2 = t1.length, i = 0; i < t2; ++i) {
+        t3 = t1[i];
+        if (object == null ? t3 == null : object === t3)
+          throw A.wrapException(new A.JsonCyclicError(object, null));
+      }
+      B.JSArray_methods.add$1(t1, object);
+    },
+    writeObject$1(object) {
+      var customJson, e, t1, exception, _this = this;
+      if (_this.writeJsonValue$1(object))
+        return;
+      _this._checkCycle$1(object);
+      try {
+        customJson = _this._toEncodable.call$1(object);
+        if (!_this.writeJsonValue$1(customJson)) {
+          t1 = A.JsonUnsupportedObjectError$(object, null, _this.get$_partialResult());
+          throw A.wrapException(t1);
+        }
+        t1 = _this._seen;
+        if (0 >= t1.length)
+          return A.ioore(t1, -1);
+        t1.pop();
+      } catch (exception) {
+        e = A.unwrapException(exception);
+        t1 = A.JsonUnsupportedObjectError$(object, e, _this.get$_partialResult());
+        throw A.wrapException(t1);
+      }
+    },
+    writeJsonValue$1(object) {
+      var t1, success, _this = this;
+      if (typeof object == "number") {
+        if (!isFinite(object))
+          return false;
+        _this.writeNumber$1(object);
+        return true;
+      } else if (object === true) {
+        _this.writeString$1("true");
+        return true;
+      } else if (object === false) {
+        _this.writeString$1("false");
+        return true;
+      } else if (object == null) {
+        _this.writeString$1("null");
+        return true;
+      } else if (typeof object == "string") {
+        _this.writeString$1('"');
+        _this.writeStringContent$1(object);
+        _this.writeString$1('"');
+        return true;
+      } else if (type$.List_dynamic._is(object)) {
+        _this._checkCycle$1(object);
+        _this.writeList$1(object);
+        t1 = _this._seen;
+        if (0 >= t1.length)
+          return A.ioore(t1, -1);
+        t1.pop();
+        return true;
+      } else if (type$.Map_dynamic_dynamic._is(object)) {
+        _this._checkCycle$1(object);
+        success = _this.writeMap$1(object);
+        t1 = _this._seen;
+        if (0 >= t1.length)
+          return A.ioore(t1, -1);
+        t1.pop();
+        return success;
+      } else
+        return false;
+    },
+    writeList$1(list) {
+      var t1, i, _this = this;
+      _this.writeString$1("[");
+      t1 = J.getInterceptor$asx(list);
+      if (t1.get$isNotEmpty(list)) {
+        _this.writeObject$1(t1.$index(list, 0));
+        for (i = 1; i < t1.get$length(list); ++i) {
+          _this.writeString$1(",");
+          _this.writeObject$1(t1.$index(list, i));
+        }
+      }
+      _this.writeString$1("]");
+    },
+    writeMap$1(map) {
+      var t2, keyValueList, i, separator, _this = this, _box_0 = {},
+        t1 = J.getInterceptor$asx(map);
+      if (t1.get$isEmpty(map)) {
+        _this.writeString$1("{}");
+        return true;
+      }
+      t2 = t1.get$length(map) * 2;
+      keyValueList = A.List_List$filled(t2, null, false, type$.nullable_Object);
+      i = _box_0.i = 0;
+      _box_0.allStringKeys = true;
+      t1.forEach$1(map, new A._JsonStringifier_writeMap_closure(_box_0, keyValueList));
+      if (!_box_0.allStringKeys)
+        return false;
+      _this.writeString$1("{");
+      for (separator = '"'; i < t2; i += 2, separator = ',"') {
+        _this.writeString$1(separator);
+        _this.writeStringContent$1(A._asString(keyValueList[i]));
+        _this.writeString$1('":');
+        t1 = i + 1;
+        if (!(t1 < t2))
+          return A.ioore(keyValueList, t1);
+        _this.writeObject$1(keyValueList[t1]);
+      }
+      _this.writeString$1("}");
+      return true;
+    }
+  };
+  A._JsonStringifier_writeMap_closure.prototype = {
+    call$2(key, value) {
+      var t1, t2;
+      if (typeof key != "string")
+        this._box_0.allStringKeys = false;
+      t1 = this.keyValueList;
+      t2 = this._box_0;
+      B.JSArray_methods.$indexSet(t1, t2.i++, key);
+      B.JSArray_methods.$indexSet(t1, t2.i++, value);
+    },
+    $signature: 16
+  };
+  A._JsonStringStringifier.prototype = {
+    get$_partialResult() {
+      var t1 = this._sink;
+      return t1 instanceof A.StringBuffer ? t1.toString$0(0) : null;
+    },
+    writeNumber$1(number) {
+      this._sink.write$1(0, B.JSNumber_methods.toString$0(number));
+    },
+    writeString$1(string) {
+      this._sink.write$1(0, string);
+    },
+    writeStringSlice$3(string, start, end) {
+      this._sink.write$1(0, B.JSString_methods.substring$2(string, start, end));
+    },
+    writeCharCode$1(charCode) {
+      this._sink.writeCharCode$1(charCode);
+    }
+  };
+  A.Utf8Codec.prototype = {};
+  A.Utf8Encoder.prototype = {
+    convert$1(string) {
+      var stringLength, end, $length, t1, encoder, t2;
+      A._asString(string);
+      stringLength = string.length;
+      end = A.RangeError_checkValidRange(0, null, stringLength);
+      $length = end - 0;
+      if ($length === 0)
+        return new Uint8Array(0);
+      t1 = new Uint8Array($length * 3);
+      encoder = new A._Utf8Encoder(t1);
+      if (encoder._fillBuffer$3(string, 0, end) !== end) {
+        t2 = end - 1;
+        if (!(t2 >= 0 && t2 < stringLength))
+          return A.ioore(string, t2);
+        encoder._writeReplacementCharacter$0();
+      }
+      return B.NativeUint8List_methods.sublist$2(t1, 0, encoder._bufferIndex);
+    }
+  };
+  A._Utf8Encoder.prototype = {
+    _writeReplacementCharacter$0() {
+      var _this = this,
+        t1 = _this._buffer,
+        t2 = _this._bufferIndex,
+        t3 = _this._bufferIndex = t2 + 1,
+        t4 = t1.length;
+      if (!(t2 < t4))
+        return A.ioore(t1, t2);
+      t1[t2] = 239;
+      t2 = _this._bufferIndex = t3 + 1;
+      if (!(t3 < t4))
+        return A.ioore(t1, t3);
+      t1[t3] = 191;
+      _this._bufferIndex = t2 + 1;
+      if (!(t2 < t4))
+        return A.ioore(t1, t2);
+      t1[t2] = 189;
+    },
+    _writeSurrogate$2(leadingSurrogate, nextCodeUnit) {
+      var rune, t1, t2, t3, t4, _this = this;
+      if ((nextCodeUnit & 64512) === 56320) {
+        rune = 65536 + ((leadingSurrogate & 1023) << 10) | nextCodeUnit & 1023;
+        t1 = _this._buffer;
+        t2 = _this._bufferIndex;
+        t3 = _this._bufferIndex = t2 + 1;
+        t4 = t1.length;
+        if (!(t2 < t4))
+          return A.ioore(t1, t2);
+        t1[t2] = rune >>> 18 | 240;
+        t2 = _this._bufferIndex = t3 + 1;
+        if (!(t3 < t4))
+          return A.ioore(t1, t3);
+        t1[t3] = rune >>> 12 & 63 | 128;
+        t3 = _this._bufferIndex = t2 + 1;
+        if (!(t2 < t4))
+          return A.ioore(t1, t2);
+        t1[t2] = rune >>> 6 & 63 | 128;
+        _this._bufferIndex = t3 + 1;
+        if (!(t3 < t4))
+          return A.ioore(t1, t3);
+        t1[t3] = rune & 63 | 128;
+        return true;
+      } else {
+        _this._writeReplacementCharacter$0();
+        return false;
+      }
+    },
+    _fillBuffer$3(str, start, end) {
+      var t1, t2, t3, stringIndex, codeUnit, t4, t5, _this = this;
+      if (start !== end) {
+        t1 = end - 1;
+        if (!(t1 >= 0 && t1 < str.length))
+          return A.ioore(str, t1);
+        t1 = (str.charCodeAt(t1) & 64512) === 55296;
+      } else
+        t1 = false;
+      if (t1)
+        --end;
+      for (t1 = _this._buffer, t2 = t1.length, t3 = str.length, stringIndex = start; stringIndex < end; ++stringIndex) {
+        if (!(stringIndex < t3))
+          return A.ioore(str, stringIndex);
+        codeUnit = str.charCodeAt(stringIndex);
+        if (codeUnit <= 127) {
+          t4 = _this._bufferIndex;
+          if (t4 >= t2)
+            break;
+          _this._bufferIndex = t4 + 1;
+          t1[t4] = codeUnit;
+        } else {
+          t4 = codeUnit & 64512;
+          if (t4 === 55296) {
+            if (_this._bufferIndex + 4 > t2)
+              break;
+            t4 = stringIndex + 1;
+            if (!(t4 < t3))
+              return A.ioore(str, t4);
+            if (_this._writeSurrogate$2(codeUnit, str.charCodeAt(t4)))
+              stringIndex = t4;
+          } else if (t4 === 56320) {
+            if (_this._bufferIndex + 3 > t2)
+              break;
+            _this._writeReplacementCharacter$0();
+          } else if (codeUnit <= 2047) {
+            t4 = _this._bufferIndex;
+            t5 = t4 + 1;
+            if (t5 >= t2)
+              break;
+            _this._bufferIndex = t5;
+            if (!(t4 < t2))
+              return A.ioore(t1, t4);
+            t1[t4] = codeUnit >>> 6 | 192;
+            _this._bufferIndex = t5 + 1;
+            t1[t5] = codeUnit & 63 | 128;
+          } else {
+            t4 = _this._bufferIndex;
+            if (t4 + 2 >= t2)
+              break;
+            t5 = _this._bufferIndex = t4 + 1;
+            if (!(t4 < t2))
+              return A.ioore(t1, t4);
+            t1[t4] = codeUnit >>> 12 | 224;
+            t4 = _this._bufferIndex = t5 + 1;
+            if (!(t5 < t2))
+              return A.ioore(t1, t5);
+            t1[t5] = codeUnit >>> 6 & 63 | 128;
+            _this._bufferIndex = t4 + 1;
+            if (!(t4 < t2))
+              return A.ioore(t1, t4);
+            t1[t4] = codeUnit & 63 | 128;
+          }
+        }
+      }
+      return stringIndex;
+    }
+  };
+  A.Utf8Decoder.prototype = {
+    convert$1(codeUnits) {
+      var t1, result;
+      type$.List_int._as(codeUnits);
+      t1 = this._allowMalformed;
+      result = A.Utf8Decoder__convertIntercepted(t1, codeUnits, 0, null);
+      if (result != null)
+        return result;
+      return new A._Utf8Decoder(t1).convertGeneral$4(codeUnits, 0, null, true);
+    }
+  };
+  A._Utf8Decoder.prototype = {
+    convertGeneral$4(codeUnits, start, maybeEnd, single) {
+      var end, bytes, errorOffset, result, t1, message, _this = this;
+      type$.List_int._as(codeUnits);
+      end = A.RangeError_checkValidRange(start, maybeEnd, J.get$length$asx(codeUnits));
+      if (start === end)
+        return "";
+      if (type$.Uint8List._is(codeUnits)) {
+        bytes = codeUnits;
+        errorOffset = 0;
+      } else {
+        bytes = A._Utf8Decoder__makeUint8List(codeUnits, start, end);
+        end -= start;
+        errorOffset = start;
+        start = 0;
+      }
+      result = _this._convertRecursive$4(bytes, start, end, single);
+      t1 = _this._state;
+      if ((t1 & 1) !== 0) {
+        message = A._Utf8Decoder_errorDescription(t1);
+        _this._state = 0;
+        throw A.wrapException(A.FormatException$(message, codeUnits, errorOffset + _this._charOrIndex));
+      }
+      return result;
+    },
+    _convertRecursive$4(bytes, start, end, single) {
+      var mid, s1, _this = this;
+      if (end - start > 1000) {
+        mid = B.JSInt_methods._tdivFast$1(start + end, 2);
+        s1 = _this._convertRecursive$4(bytes, start, mid, false);
+        if ((_this._state & 1) !== 0)
+          return s1;
+        return s1 + _this._convertRecursive$4(bytes, mid, end, single);
+      }
+      return _this.decodeGeneral$4(bytes, start, end, single);
+    },
+    decodeGeneral$4(bytes, start, end, single) {
+      var byte, t2, type, t3, i0, markEnd, i1, m, _this = this,
+        _s256_ = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFFFFFFFFFFFFFFFFGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHIHHHJEEBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBKCCCCCCCCCCCCDCLONNNMEEEEEEEEEEE",
+        _s144_ = " \x000:XECCCCCN:lDb \x000:XECCCCCNvlDb \x000:XECCCCCN:lDb AAAAA\x00\x00\x00\x00\x00AAAAA00000AAAAA:::::AAAAAGG000AAAAA00KKKAAAAAG::::AAAAA:IIIIAAAAA000\x800AAAAA\x00\x00\x00\x00 AAAAA",
+        _65533 = 65533,
+        state = _this._state,
+        char = _this._charOrIndex,
+        buffer = new A.StringBuffer(""),
+        i = start + 1,
+        t1 = bytes.length;
+      if (!(start >= 0 && start < t1))
+        return A.ioore(bytes, start);
+      byte = bytes[start];
+      $label0$0:
+        for (t2 = _this.allowMalformed; true;) {
+          for (; true; i = i0) {
+            if (!(byte >= 0 && byte < 256))
+              return A.ioore(_s256_, byte);
+            type = _s256_.charCodeAt(byte) & 31;
+            char = state <= 32 ? byte & 61694 >>> type : (byte & 63 | char << 6) >>> 0;
+            t3 = state + type;
+            if (!(t3 >= 0 && t3 < 144))
+              return A.ioore(_s144_, t3);
+            state = _s144_.charCodeAt(t3);
+            if (state === 0) {
+              buffer._contents += A.Primitives_stringFromCharCode(char);
+              if (i === end)
+                break $label0$0;
+              break;
+            } else if ((state & 1) !== 0) {
+              if (t2)
+                switch (state) {
+                  case 69:
+                  case 67:
+                    buffer._contents += A.Primitives_stringFromCharCode(_65533);
+                    break;
+                  case 65:
+                    buffer._contents += A.Primitives_stringFromCharCode(_65533);
+                    --i;
+                    break;
+                  default:
+                    t3 = buffer._contents += A.Primitives_stringFromCharCode(_65533);
+                    buffer._contents = t3 + A.Primitives_stringFromCharCode(_65533);
+                    break;
+                }
+              else {
+                _this._state = state;
+                _this._charOrIndex = i - 1;
+                return "";
+              }
+              state = 0;
+            }
+            if (i === end)
+              break $label0$0;
+            i0 = i + 1;
+            if (!(i >= 0 && i < t1))
+              return A.ioore(bytes, i);
+            byte = bytes[i];
+          }
+          i0 = i + 1;
+          if (!(i >= 0 && i < t1))
+            return A.ioore(bytes, i);
+          byte = bytes[i];
+          if (byte < 128) {
+            while (true) {
+              if (!(i0 < end)) {
+                markEnd = end;
+                break;
+              }
+              i1 = i0 + 1;
+              if (!(i0 >= 0 && i0 < t1))
+                return A.ioore(bytes, i0);
+              byte = bytes[i0];
+              if (byte >= 128) {
+                markEnd = i1 - 1;
+                i0 = i1;
+                break;
+              }
+              i0 = i1;
+            }
+            if (markEnd - i < 20)
+              for (m = i; m < markEnd; ++m) {
+                if (!(m < t1))
+                  return A.ioore(bytes, m);
+                buffer._contents += A.Primitives_stringFromCharCode(bytes[m]);
+              }
+            else
+              buffer._contents += A.String_String$fromCharCodes(bytes, i, markEnd);
+            if (markEnd === end)
+              break $label0$0;
+            i = i0;
+          } else
+            i = i0;
+        }
+      if (single && state > 32)
+        if (t2)
+          buffer._contents += A.Primitives_stringFromCharCode(_65533);
+        else {
+          _this._state = 77;
+          _this._charOrIndex = end;
+          return "";
+        }
+      _this._state = state;
+      _this._charOrIndex = char;
+      t1 = buffer._contents;
+      return t1.charCodeAt(0) == 0 ? t1 : t1;
+    }
+  };
+  A.NoSuchMethodError_toString_closure.prototype = {
+    call$2(key, value) {
+      var t1, t2, t3;
+      type$.Symbol._as(key);
+      t1 = this.sb;
+      t2 = this._box_0;
+      t3 = t1._contents += t2.comma;
+      t3 += key._name;
+      t1._contents = t3;
+      t1._contents = t3 + ": ";
+      t1._contents += A.Error_safeToString(value);
+      t2.comma = ", ";
+    },
+    $signature: 39
+  };
+  A.DateTime.prototype = {
+    $eq(_, other) {
+      if (other == null)
+        return false;
+      return other instanceof A.DateTime && this._core$_value === other._core$_value && true;
+    },
+    get$hashCode(_) {
+      var t1 = this._core$_value;
+      return (t1 ^ B.JSInt_methods._shrOtherPositive$1(t1, 30)) & 1073741823;
+    },
+    toString$0(_) {
+      var _this = this,
+        y = A.DateTime__fourDigits(A.Primitives_getYear(_this)),
+        m = A.DateTime__twoDigits(A.Primitives_getMonth(_this)),
+        d = A.DateTime__twoDigits(A.Primitives_getDay(_this)),
+        h = A.DateTime__twoDigits(A.Primitives_getHours(_this)),
+        min = A.DateTime__twoDigits(A.Primitives_getMinutes(_this)),
+        sec = A.DateTime__twoDigits(A.Primitives_getSeconds(_this)),
+        ms = A.DateTime__threeDigits(A.Primitives_getMilliseconds(_this));
+      return y + "-" + m + "-" + d + " " + h + ":" + min + ":" + sec + "." + ms + "Z";
+    }
+  };
+  A.Duration.prototype = {
+    $eq(_, other) {
+      if (other == null)
+        return false;
+      return other instanceof A.Duration && this._duration === other._duration;
+    },
+    get$hashCode(_) {
+      return B.JSInt_methods.get$hashCode(this._duration);
+    },
+    toString$0(_) {
+      var minutes, minutesPadding, seconds, secondsPadding,
+        microseconds = this._duration,
+        hours = B.JSInt_methods._tdivFast$1(microseconds, 3600000000);
+      microseconds %= 3600000000;
+      minutes = B.JSInt_methods._tdivFast$1(microseconds, 60000000);
+      microseconds %= 60000000;
+      minutesPadding = minutes < 10 ? "0" : "";
+      seconds = B.JSInt_methods._tdivFast$1(microseconds, 1000000);
+      secondsPadding = seconds < 10 ? "0" : "";
+      return "" + hours + ":" + minutesPadding + minutes + ":" + secondsPadding + seconds + "." + B.JSString_methods.padLeft$2(B.JSInt_methods.toString$0(microseconds % 1000000), 6, "0");
+    }
+  };
+  A.Error.prototype = {
+    get$stackTrace() {
+      return A.getTraceFromException(this.$thrownJsError);
+    }
+  };
+  A.AssertionError.prototype = {
+    toString$0(_) {
+      var t1 = this.message;
+      if (t1 != null)
+        return "Assertion failed: " + A.Error_safeToString(t1);
+      return "Assertion failed";
+    }
+  };
+  A.TypeError.prototype = {};
+  A.ArgumentError.prototype = {
+    get$_errorName() {
+      return "Invalid argument" + (!this._hasValue ? "(s)" : "");
+    },
+    get$_errorExplanation() {
+      return "";
+    },
+    toString$0(_) {
+      var _this = this,
+        $name = _this.name,
+        nameString = $name == null ? "" : " (" + $name + ")",
+        message = _this.message,
+        messageString = message == null ? "" : ": " + A.S(message),
+        prefix = _this.get$_errorName() + nameString + messageString;
+      if (!_this._hasValue)
+        return prefix;
+      return prefix + _this.get$_errorExplanation() + ": " + A.Error_safeToString(_this.get$invalidValue());
+    },
+    get$invalidValue() {
+      return this.invalidValue;
+    }
+  };
+  A.RangeError.prototype = {
+    get$invalidValue() {
+      return A._asNumQ(this.invalidValue);
+    },
+    get$_errorName() {
+      return "RangeError";
+    },
+    get$_errorExplanation() {
+      var explanation,
+        start = this.start,
+        end = this.end;
+      if (start == null)
+        explanation = end != null ? ": Not less than or equal to " + A.S(end) : "";
+      else if (end == null)
+        explanation = ": Not greater than or equal to " + A.S(start);
+      else if (end > start)
+        explanation = ": Not in inclusive range " + A.S(start) + ".." + A.S(end);
+      else
+        explanation = end < start ? ": Valid value range is empty" : ": Only valid value is " + A.S(start);
+      return explanation;
+    }
+  };
+  A.IndexError.prototype = {
+    get$invalidValue() {
+      return A._asInt(this.invalidValue);
+    },
+    get$_errorName() {
+      return "RangeError";
+    },
+    get$_errorExplanation() {
+      if (A._asInt(this.invalidValue) < 0)
+        return ": index must not be negative";
+      var t1 = this.length;
+      if (t1 === 0)
+        return ": no indices are valid";
+      return ": index should be less than " + t1;
+    },
+    get$length(receiver) {
+      return this.length;
+    }
+  };
+  A.NoSuchMethodError.prototype = {
+    toString$0(_) {
+      var $arguments, t1, _i, t2, t3, argument, receiverText, actualParameters, _this = this, _box_0 = {},
+        sb = new A.StringBuffer("");
+      _box_0.comma = "";
+      $arguments = _this._core$_arguments;
+      for (t1 = $arguments.length, _i = 0, t2 = "", t3 = ""; _i < t1; ++_i, t3 = ", ") {
+        argument = $arguments[_i];
+        sb._contents = t2 + t3;
+        t2 = sb._contents += A.Error_safeToString(argument);
+        _box_0.comma = ", ";
+      }
+      _this._namedArguments.forEach$1(0, new A.NoSuchMethodError_toString_closure(_box_0, sb));
+      receiverText = A.Error_safeToString(_this._core$_receiver);
+      actualParameters = sb.toString$0(0);
+      return "NoSuchMethodError: method not found: '" + _this._core$_memberName._name + "'\nReceiver: " + receiverText + "\nArguments: [" + actualParameters + "]";
+    }
+  };
+  A.UnsupportedError.prototype = {
+    toString$0(_) {
+      return "Unsupported operation: " + this.message;
+    }
+  };
+  A.UnimplementedError.prototype = {
+    toString$0(_) {
+      return "UnimplementedError: " + this.message;
+    }
+  };
+  A.StateError.prototype = {
+    toString$0(_) {
+      return "Bad state: " + this.message;
+    }
+  };
+  A.ConcurrentModificationError.prototype = {
+    toString$0(_) {
+      var t1 = this.modifiedObject;
+      if (t1 == null)
+        return "Concurrent modification during iteration.";
+      return "Concurrent modification during iteration: " + A.Error_safeToString(t1) + ".";
+    }
+  };
+  A.OutOfMemoryError.prototype = {
+    toString$0(_) {
+      return "Out of Memory";
+    },
+    get$stackTrace() {
+      return null;
+    },
+    $isError: 1
+  };
+  A.StackOverflowError.prototype = {
+    toString$0(_) {
+      return "Stack Overflow";
+    },
+    get$stackTrace() {
+      return null;
+    },
+    $isError: 1
+  };
+  A._Exception.prototype = {
+    toString$0(_) {
+      return "Exception: " + this.message;
+    },
+    $isException: 1
+  };
+  A.FormatException.prototype = {
+    toString$0(_) {
+      var t1, lineEnd, lineNum, lineStart, previousCharWasCR, i, char, end, start, prefix, postfix,
+        message = this.message,
+        report = "" !== message ? "FormatException: " + message : "FormatException",
+        offset = this.offset,
+        source = this.source;
+      if (typeof source == "string") {
+        if (offset != null)
+          t1 = offset < 0 || offset > source.length;
+        else
+          t1 = false;
+        if (t1)
+          offset = null;
+        if (offset == null) {
+          if (source.length > 78)
+            source = B.JSString_methods.substring$2(source, 0, 75) + "...";
+          return report + "\n" + source;
+        }
+        for (lineEnd = source.length, lineNum = 1, lineStart = 0, previousCharWasCR = false, i = 0; i < offset; ++i) {
+          if (!(i < lineEnd))
+            return A.ioore(source, i);
+          char = source.charCodeAt(i);
+          if (char === 10) {
+            if (lineStart !== i || !previousCharWasCR)
+              ++lineNum;
+            lineStart = i + 1;
+            previousCharWasCR = false;
+          } else if (char === 13) {
+            ++lineNum;
+            lineStart = i + 1;
+            previousCharWasCR = true;
+          }
+        }
+        report = lineNum > 1 ? report + (" (at line " + lineNum + ", character " + (offset - lineStart + 1) + ")\n") : report + (" (at character " + (offset + 1) + ")\n");
+        for (i = offset; i < lineEnd; ++i) {
+          if (!(i >= 0))
+            return A.ioore(source, i);
+          char = source.charCodeAt(i);
+          if (char === 10 || char === 13) {
+            lineEnd = i;
+            break;
+          }
+        }
+        if (lineEnd - lineStart > 78)
+          if (offset - lineStart < 75) {
+            end = lineStart + 75;
+            start = lineStart;
+            prefix = "";
+            postfix = "...";
+          } else {
+            if (lineEnd - offset < 75) {
+              start = lineEnd - 75;
+              end = lineEnd;
+              postfix = "";
+            } else {
+              start = offset - 36;
+              end = offset + 36;
+              postfix = "...";
+            }
+            prefix = "...";
+          }
+        else {
+          end = lineEnd;
+          start = lineStart;
+          prefix = "";
+          postfix = "";
+        }
+        return report + prefix + B.JSString_methods.substring$2(source, start, end) + postfix + "\n" + B.JSString_methods.$mul(" ", offset - start + prefix.length) + "^\n";
+      } else
+        return offset != null ? report + (" (at offset " + A.S(offset) + ")") : report;
+    },
+    $isException: 1
+  };
+  A.Iterable.prototype = {
+    cast$1$0(_, $R) {
+      return A.CastIterable_CastIterable(this, A._instanceType(this)._eval$1("Iterable.E"), $R);
+    },
+    map$1$1(_, toElement, $T) {
+      var t1 = A._instanceType(this);
+      return A.MappedIterable_MappedIterable(this, t1._bind$1($T)._eval$1("1(Iterable.E)")._as(toElement), t1._eval$1("Iterable.E"), $T);
+    },
+    toList$1$growable(_, growable) {
+      return A.List_List$of(this, growable, A._instanceType(this)._eval$1("Iterable.E"));
+    },
+    toList$0($receiver) {
+      return this.toList$1$growable($receiver, true);
+    },
+    get$length(_) {
+      var count,
+        it = this.get$iterator(this);
+      for (count = 0; it.moveNext$0();)
+        ++count;
+      return count;
+    },
+    get$isEmpty(_) {
+      return !this.get$iterator(this).moveNext$0();
+    },
+    get$isNotEmpty(_) {
+      return !this.get$isEmpty(this);
+    },
+    skip$1(_, count) {
+      return A.SkipIterable_SkipIterable(this, count, A._instanceType(this)._eval$1("Iterable.E"));
+    },
+    skipWhile$1(_, test) {
+      var t1 = A._instanceType(this);
+      return new A.SkipWhileIterable(this, t1._eval$1("bool(Iterable.E)")._as(test), t1._eval$1("SkipWhileIterable<Iterable.E>"));
+    },
+    get$first(_) {
+      var it = this.get$iterator(this);
+      if (!it.moveNext$0())
+        throw A.wrapException(A.IterableElementError_noElement());
+      return it.get$current(it);
+    },
+    get$last(_) {
+      var result,
+        it = this.get$iterator(this);
+      if (!it.moveNext$0())
+        throw A.wrapException(A.IterableElementError_noElement());
+      do
+        result = it.get$current(it);
+      while (it.moveNext$0());
+      return result;
+    },
+    elementAt$1(_, index) {
+      var iterator, skipCount;
+      A.RangeError_checkNotNegative(index, "index");
+      iterator = this.get$iterator(this);
+      for (skipCount = index; iterator.moveNext$0();) {
+        if (skipCount === 0)
+          return iterator.get$current(iterator);
+        --skipCount;
+      }
+      throw A.wrapException(A.IndexError$withLength(index, index - skipCount, this, "index"));
+    },
+    toString$0(_) {
+      return A.Iterable_iterableToShortString(this, "(", ")");
+    }
+  };
+  A.Null.prototype = {
+    get$hashCode(_) {
+      return A.Object.prototype.get$hashCode.call(this, this);
+    },
+    toString$0(_) {
+      return "null";
+    }
+  };
+  A.Object.prototype = {$isObject: 1,
+    $eq(_, other) {
+      return this === other;
+    },
+    get$hashCode(_) {
+      return A.Primitives_objectHashCode(this);
+    },
+    toString$0(_) {
+      return "Instance of '" + A.Primitives_objectTypeName(this) + "'";
+    },
+    noSuchMethod$1(_, invocation) {
+      throw A.wrapException(A.NoSuchMethodError_NoSuchMethodError$withInvocation(this, type$.Invocation._as(invocation)));
+    },
+    get$runtimeType(_) {
+      return A.getRuntimeTypeOfDartObject(this);
+    },
+    toString() {
+      return this.toString$0(this);
+    }
+  };
+  A._StringStackTrace.prototype = {
+    toString$0(_) {
+      return this._stackTrace;
+    },
+    $isStackTrace: 1
+  };
+  A.StringBuffer.prototype = {
+    get$length(_) {
+      return this._contents.length;
+    },
+    write$1(_, obj) {
+      this._contents += A.S(obj);
+    },
+    writeCharCode$1(charCode) {
+      this._contents += A.Primitives_stringFromCharCode(charCode);
+    },
+    toString$0(_) {
+      var t1 = this._contents;
+      return t1.charCodeAt(0) == 0 ? t1 : t1;
+    },
+    $isStringSink: 1
+  };
+  A.Uri_splitQueryString_closure.prototype = {
+    call$2(map, element) {
+      var index, key, value, t1;
+      type$.Map_String_String._as(map);
+      A._asString(element);
+      index = B.JSString_methods.indexOf$1(element, "=");
+      if (index === -1) {
+        if (element !== "")
+          J.$indexSet$ax(map, A._Uri__uriDecode(element, 0, element.length, this.encoding, true), "");
+      } else if (index !== 0) {
+        key = B.JSString_methods.substring$2(element, 0, index);
+        value = B.JSString_methods.substring$1(element, index + 1);
+        t1 = this.encoding;
+        J.$indexSet$ax(map, A._Uri__uriDecode(key, 0, key.length, t1, true), A._Uri__uriDecode(value, 0, value.length, t1, true));
+      }
+      return map;
+    },
+    $signature: 40
+  };
+  A.Uri__parseIPv4Address_error.prototype = {
+    call$2(msg, position) {
+      throw A.wrapException(A.FormatException$("Illegal IPv4 address, " + msg, this.host, position));
+    },
+    $signature: 25
+  };
+  A.Uri_parseIPv6Address_error.prototype = {
+    call$2(msg, position) {
+      throw A.wrapException(A.FormatException$("Illegal IPv6 address, " + msg, this.host, position));
+    },
+    $signature: 45
+  };
+  A.Uri_parseIPv6Address_parseHex.prototype = {
+    call$2(start, end) {
+      var value;
+      if (end - start > 4)
+        this.error.call$2("an IPv6 part can only contain a maximum of 4 hex digits", start);
+      value = A.int_parse(B.JSString_methods.substring$2(this.host, start, end), 16);
+      if (value < 0 || value > 65535)
+        this.error.call$2("each part must be in the range of `0x0..0xFFFF`", start);
+      return value;
+    },
+    $signature: 46
+  };
+  A._Uri.prototype = {
+    get$_text() {
+      var t1, t2, t3, t4, _this = this,
+        value = _this.___Uri__text_FI;
+      if (value === $) {
+        t1 = _this.scheme;
+        t2 = t1.length !== 0 ? "" + t1 + ":" : "";
+        t3 = _this._host;
+        t4 = t3 == null;
+        if (!t4 || t1 === "file") {
+          t1 = t2 + "//";
+          t2 = _this._userInfo;
+          if (t2.length !== 0)
+            t1 = t1 + t2 + "@";
+          if (!t4)
+            t1 += t3;
+          t2 = _this._port;
+          if (t2 != null)
+            t1 = t1 + ":" + A.S(t2);
+        } else
+          t1 = t2;
+        t1 += _this.path;
+        t2 = _this._query;
+        if (t2 != null)
+          t1 = t1 + "?" + t2;
+        t2 = _this._fragment;
+        if (t2 != null)
+          t1 = t1 + "#" + t2;
+        value !== $ && A.throwLateFieldADI("_text");
+        value = _this.___Uri__text_FI = t1.charCodeAt(0) == 0 ? t1 : t1;
+      }
+      return value;
+    },
+    get$pathSegments() {
+      var pathToSplit, t1, result, _this = this,
+        value = _this.___Uri_pathSegments_FI;
+      if (value === $) {
+        pathToSplit = _this.path;
+        t1 = pathToSplit.length;
+        if (t1 !== 0) {
+          if (0 >= t1)
+            return A.ioore(pathToSplit, 0);
+          t1 = pathToSplit.charCodeAt(0) === 47;
+        } else
+          t1 = false;
+        if (t1)
+          pathToSplit = B.JSString_methods.substring$1(pathToSplit, 1);
+        result = pathToSplit.length === 0 ? B.List_empty : A.List_List$unmodifiable(new A.MappedListIterable(A._setArrayType(pathToSplit.split("/"), type$.JSArray_String), type$.dynamic_Function_String._as(A.core_Uri_decodeComponent$closure()), type$.MappedListIterable_String_dynamic), type$.String);
+        _this.___Uri_pathSegments_FI !== $ && A.throwLateFieldADI("pathSegments");
+        _this.set$___Uri_pathSegments_FI(result);
+        value = result;
+      }
+      return value;
+    },
+    get$hashCode(_) {
+      var result, _this = this,
+        value = _this.___Uri_hashCode_FI;
+      if (value === $) {
+        result = B.JSString_methods.get$hashCode(_this.get$_text());
+        _this.___Uri_hashCode_FI !== $ && A.throwLateFieldADI("hashCode");
+        _this.___Uri_hashCode_FI = result;
+        value = result;
+      }
+      return value;
+    },
+    get$queryParameters() {
+      var t1, result, _this = this,
+        value = _this.___Uri_queryParameters_FI;
+      if (value === $) {
+        t1 = _this._query;
+        result = new A.UnmodifiableMapView(A.Uri_splitQueryString(t1 == null ? "" : t1), type$.UnmodifiableMapView_String_String);
+        _this.___Uri_queryParameters_FI !== $ && A.throwLateFieldADI("queryParameters");
+        _this.set$___Uri_queryParameters_FI(result);
+        value = result;
+      }
+      return value;
+    },
+    get$userInfo() {
+      return this._userInfo;
+    },
+    get$host(_) {
+      var host = this._host;
+      if (host == null)
+        return "";
+      if (B.JSString_methods.startsWith$1(host, "["))
+        return B.JSString_methods.substring$2(host, 1, host.length - 1);
+      return host;
+    },
+    get$port(_) {
+      var t1 = this._port;
+      return t1 == null ? A._Uri__defaultPort(this.scheme) : t1;
+    },
+    get$query(_) {
+      var t1 = this._query;
+      return t1 == null ? "" : t1;
+    },
+    get$fragment() {
+      var t1 = this._fragment;
+      return t1 == null ? "" : t1;
+    },
+    isScheme$1(scheme) {
+      var thisScheme = this.scheme;
+      if (scheme.length !== thisScheme.length)
+        return false;
+      return A._caseInsensitiveCompareStart(scheme, thisScheme, 0) >= 0;
+    },
+    removeFragment$0() {
+      var _this = this;
+      if (_this._fragment == null)
+        return _this;
+      return A._Uri$_internal(_this.scheme, _this._userInfo, _this._host, _this._port, _this.path, _this._query, null);
+    },
+    _mergePaths$2(base, reference) {
+      var backCount, refStart, baseEnd, t1, newEnd, delta, t2, t3;
+      for (backCount = 0, refStart = 0; B.JSString_methods.startsWith$2(reference, "../", refStart);) {
+        refStart += 3;
+        ++backCount;
+      }
+      baseEnd = B.JSString_methods.lastIndexOf$1(base, "/");
+      t1 = base.length;
+      while (true) {
+        if (!(baseEnd > 0 && backCount > 0))
+          break;
+        newEnd = B.JSString_methods.lastIndexOf$2(base, "/", baseEnd - 1);
+        if (newEnd < 0)
+          break;
+        delta = baseEnd - newEnd;
+        t2 = delta !== 2;
+        if (!t2 || delta === 3) {
+          t3 = newEnd + 1;
+          if (!(t3 < t1))
+            return A.ioore(base, t3);
+          if (base.charCodeAt(t3) === 46)
+            if (t2) {
+              t2 = newEnd + 2;
+              if (!(t2 < t1))
+                return A.ioore(base, t2);
+              t2 = base.charCodeAt(t2) === 46;
+            } else
+              t2 = true;
+          else
+            t2 = false;
+        } else
+          t2 = false;
+        if (t2)
+          break;
+        --backCount;
+        baseEnd = newEnd;
+      }
+      return B.JSString_methods.replaceRange$3(base, baseEnd + 1, null, B.JSString_methods.substring$1(reference, refStart - 3 * backCount));
+    },
+    resolve$1(reference) {
+      return this.resolveUri$1(A.Uri_parse(reference));
+    },
+    resolveUri$1(reference) {
+      var targetScheme, targetUserInfo, targetHost, targetPort, targetPath, targetQuery, packageNameEnd, packageName, mergedPath, t1, _this = this, _null = null;
+      if (reference.get$scheme().length !== 0) {
+        targetScheme = reference.get$scheme();
+        if (reference.get$hasAuthority()) {
+          targetUserInfo = reference.get$userInfo();
+          targetHost = reference.get$host(reference);
+          targetPort = reference.get$hasPort() ? reference.get$port(reference) : _null;
+        } else {
+          targetPort = _null;
+          targetHost = targetPort;
+          targetUserInfo = "";
+        }
+        targetPath = A._Uri__removeDotSegments(reference.get$path(reference));
+        targetQuery = reference.get$hasQuery() ? reference.get$query(reference) : _null;
+      } else {
+        targetScheme = _this.scheme;
+        if (reference.get$hasAuthority()) {
+          targetUserInfo = reference.get$userInfo();
+          targetHost = reference.get$host(reference);
+          targetPort = A._Uri__makePort(reference.get$hasPort() ? reference.get$port(reference) : _null, targetScheme);
+          targetPath = A._Uri__removeDotSegments(reference.get$path(reference));
+          targetQuery = reference.get$hasQuery() ? reference.get$query(reference) : _null;
+        } else {
+          targetUserInfo = _this._userInfo;
+          targetHost = _this._host;
+          targetPort = _this._port;
+          targetPath = _this.path;
+          if (reference.get$path(reference) === "")
+            targetQuery = reference.get$hasQuery() ? reference.get$query(reference) : _this._query;
+          else {
+            packageNameEnd = A._Uri__packageNameEnd(_this, targetPath);
+            if (packageNameEnd > 0) {
+              packageName = B.JSString_methods.substring$2(targetPath, 0, packageNameEnd);
+              targetPath = reference.get$hasAbsolutePath() ? packageName + A._Uri__removeDotSegments(reference.get$path(reference)) : packageName + A._Uri__removeDotSegments(_this._mergePaths$2(B.JSString_methods.substring$1(targetPath, packageName.length), reference.get$path(reference)));
+            } else if (reference.get$hasAbsolutePath())
+              targetPath = A._Uri__removeDotSegments(reference.get$path(reference));
+            else if (targetPath.length === 0)
+              if (targetHost == null)
+                targetPath = targetScheme.length === 0 ? reference.get$path(reference) : A._Uri__removeDotSegments(reference.get$path(reference));
+              else
+                targetPath = A._Uri__removeDotSegments("/" + reference.get$path(reference));
+            else {
+              mergedPath = _this._mergePaths$2(targetPath, reference.get$path(reference));
+              t1 = targetScheme.length === 0;
+              if (!t1 || targetHost != null || B.JSString_methods.startsWith$1(targetPath, "/"))
+                targetPath = A._Uri__removeDotSegments(mergedPath);
+              else
+                targetPath = A._Uri__normalizeRelativePath(mergedPath, !t1 || targetHost != null);
+            }
+            targetQuery = reference.get$hasQuery() ? reference.get$query(reference) : _null;
+          }
+        }
+      }
+      return A._Uri$_internal(targetScheme, targetUserInfo, targetHost, targetPort, targetPath, targetQuery, reference.get$hasFragment() ? reference.get$fragment() : _null);
+    },
+    get$hasAuthority() {
+      return this._host != null;
+    },
+    get$hasPort() {
+      return this._port != null;
+    },
+    get$hasQuery() {
+      return this._query != null;
+    },
+    get$hasFragment() {
+      return this._fragment != null;
+    },
+    get$hasAbsolutePath() {
+      return B.JSString_methods.startsWith$1(this.path, "/");
+    },
+    toFilePath$0() {
+      var pathSegments, _this = this,
+        t1 = _this.scheme;
+      if (t1 !== "" && t1 !== "file")
+        throw A.wrapException(A.UnsupportedError$("Cannot extract a file path from a " + t1 + " URI"));
+      t1 = _this._query;
+      if ((t1 == null ? "" : t1) !== "")
+        throw A.wrapException(A.UnsupportedError$(string$.Cannotfq));
+      t1 = _this._fragment;
+      if ((t1 == null ? "" : t1) !== "")
+        throw A.wrapException(A.UnsupportedError$(string$.Cannotff));
+      t1 = $.$get$_Uri__isWindowsCached();
+      if (t1)
+        t1 = A._Uri__toWindowsFilePath(_this);
+      else {
+        if (_this._host != null && _this.get$host(_this) !== "")
+          A.throwExpression(A.UnsupportedError$(string$.Cannotn));
+        pathSegments = _this.get$pathSegments();
+        A._Uri__checkNonWindowsPathReservedCharacters(pathSegments, false);
+        t1 = A.StringBuffer__writeAll(B.JSString_methods.startsWith$1(_this.path, "/") ? "" + "/" : "", pathSegments, "/");
+        t1 = t1.charCodeAt(0) == 0 ? t1 : t1;
+      }
+      return t1;
+    },
+    toString$0(_) {
+      return this.get$_text();
+    },
+    $eq(_, other) {
+      var t1, t2, _this = this;
+      if (other == null)
+        return false;
+      if (_this === other)
+        return true;
+      if (type$.Uri._is(other))
+        if (_this.scheme === other.get$scheme())
+          if (_this._host != null === other.get$hasAuthority())
+            if (_this._userInfo === other.get$userInfo())
+              if (_this.get$host(_this) === other.get$host(other))
+                if (_this.get$port(_this) === other.get$port(other))
+                  if (_this.path === other.get$path(other)) {
+                    t1 = _this._query;
+                    t2 = t1 == null;
+                    if (!t2 === other.get$hasQuery()) {
+                      if (t2)
+                        t1 = "";
+                      if (t1 === other.get$query(other)) {
+                        t1 = _this._fragment;
+                        t2 = t1 == null;
+                        if (!t2 === other.get$hasFragment()) {
+                          if (t2)
+                            t1 = "";
+                          t1 = t1 === other.get$fragment();
+                        } else
+                          t1 = false;
+                      } else
+                        t1 = false;
+                    } else
+                      t1 = false;
+                  } else
+                    t1 = false;
+                else
+                  t1 = false;
+              else
+                t1 = false;
+            else
+              t1 = false;
+          else
+            t1 = false;
+        else
+          t1 = false;
+      else
+        t1 = false;
+      return t1;
+    },
+    set$___Uri_pathSegments_FI(___Uri_pathSegments_FI) {
+      this.___Uri_pathSegments_FI = type$.List_String._as(___Uri_pathSegments_FI);
+    },
+    set$___Uri_queryParameters_FI(___Uri_queryParameters_FI) {
+      this.___Uri_queryParameters_FI = type$.Map_String_String._as(___Uri_queryParameters_FI);
+    },
+    $isUri: 1,
+    get$scheme() {
+      return this.scheme;
+    },
+    get$path(receiver) {
+      return this.path;
+    }
+  };
+  A._Uri__makePath_closure.prototype = {
+    call$1(s) {
+      return A._Uri__uriEncode(B.List_XRg0, A._asString(s), B.C_Utf8Codec, false);
+    },
+    $signature: 17
+  };
+  A.UriData.prototype = {
+    get$uri() {
+      var t2, queryIndex, end, query, _this = this, _null = null,
+        t1 = _this._uriCache;
+      if (t1 == null) {
+        t1 = _this._separatorIndices;
+        if (0 >= t1.length)
+          return A.ioore(t1, 0);
+        t2 = _this._text;
+        t1 = t1[0] + 1;
+        queryIndex = B.JSString_methods.indexOf$2(t2, "?", t1);
+        end = t2.length;
+        if (queryIndex >= 0) {
+          query = A._Uri__normalizeOrSubstring(t2, queryIndex + 1, end, B.List_oFp, false, false);
+          end = queryIndex;
+        } else
+          query = _null;
+        t1 = _this._uriCache = new A._DataUri("data", "", _null, _null, A._Uri__normalizeOrSubstring(t2, t1, end, B.List_XRg, false, false), query, _null);
+      }
+      return t1;
+    },
+    toString$0(_) {
+      var t2,
+        t1 = this._separatorIndices;
+      if (0 >= t1.length)
+        return A.ioore(t1, 0);
+      t2 = this._text;
+      return t1[0] === -1 ? "data:" + t2 : t2;
+    }
+  };
+  A._createTables_build.prototype = {
+    call$2(state, defaultTransition) {
+      var t1 = this.tables;
+      if (!(state < t1.length))
+        return A.ioore(t1, state);
+      t1 = t1[state];
+      B.NativeUint8List_methods.fillRange$3(t1, 0, 96, defaultTransition);
+      return t1;
+    },
+    $signature: 61
+  };
+  A._createTables_setChars.prototype = {
+    call$3(target, chars, transition) {
+      var t1, i, t2;
+      for (t1 = chars.length, i = 0; i < t1; ++i) {
+        t2 = chars.charCodeAt(i) ^ 96;
+        if (!(t2 < 96))
+          return A.ioore(target, t2);
+        target[t2] = transition;
+      }
+    },
+    $signature: 18
+  };
+  A._createTables_setRange.prototype = {
+    call$3(target, range, transition) {
+      var i, n,
+        t1 = range.length;
+      if (0 >= t1)
+        return A.ioore(range, 0);
+      i = range.charCodeAt(0);
+      if (1 >= t1)
+        return A.ioore(range, 1);
+      n = range.charCodeAt(1);
+      for (; i <= n; ++i) {
+        t1 = (i ^ 96) >>> 0;
+        if (!(t1 < 96))
+          return A.ioore(target, t1);
+        target[t1] = transition;
+      }
+    },
+    $signature: 18
+  };
+  A._SimpleUri.prototype = {
+    get$hasAuthority() {
+      return this._hostStart > 0;
+    },
+    get$hasPort() {
+      return this._hostStart > 0 && this._portStart + 1 < this._pathStart;
+    },
+    get$hasQuery() {
+      return this._queryStart < this._fragmentStart;
+    },
+    get$hasFragment() {
+      return this._fragmentStart < this._uri.length;
+    },
+    get$hasAbsolutePath() {
+      return B.JSString_methods.startsWith$2(this._uri, "/", this._pathStart);
+    },
+    get$scheme() {
+      var t1 = this._schemeCache;
+      return t1 == null ? this._schemeCache = this._computeScheme$0() : t1;
+    },
+    _computeScheme$0() {
+      var t2, _this = this,
+        t1 = _this._schemeEnd;
+      if (t1 <= 0)
+        return "";
+      t2 = t1 === 4;
+      if (t2 && B.JSString_methods.startsWith$1(_this._uri, "http"))
+        return "http";
+      if (t1 === 5 && B.JSString_methods.startsWith$1(_this._uri, "https"))
+        return "https";
+      if (t2 && B.JSString_methods.startsWith$1(_this._uri, "file"))
+        return "file";
+      if (t1 === 7 && B.JSString_methods.startsWith$1(_this._uri, "package"))
+        return "package";
+      return B.JSString_methods.substring$2(_this._uri, 0, t1);
+    },
+    get$userInfo() {
+      var t1 = this._hostStart,
+        t2 = this._schemeEnd + 3;
+      return t1 > t2 ? B.JSString_methods.substring$2(this._uri, t2, t1 - 1) : "";
+    },
+    get$host(_) {
+      var t1 = this._hostStart;
+      return t1 > 0 ? B.JSString_methods.substring$2(this._uri, t1, this._portStart) : "";
+    },
+    get$port(_) {
+      var t1, _this = this;
+      if (_this.get$hasPort())
+        return A.int_parse(B.JSString_methods.substring$2(_this._uri, _this._portStart + 1, _this._pathStart), null);
+      t1 = _this._schemeEnd;
+      if (t1 === 4 && B.JSString_methods.startsWith$1(_this._uri, "http"))
+        return 80;
+      if (t1 === 5 && B.JSString_methods.startsWith$1(_this._uri, "https"))
+        return 443;
+      return 0;
+    },
+    get$path(_) {
+      return B.JSString_methods.substring$2(this._uri, this._pathStart, this._queryStart);
+    },
+    get$query(_) {
+      var t1 = this._queryStart,
+        t2 = this._fragmentStart;
+      return t1 < t2 ? B.JSString_methods.substring$2(this._uri, t1 + 1, t2) : "";
+    },
+    get$fragment() {
+      var t1 = this._fragmentStart,
+        t2 = this._uri;
+      return t1 < t2.length ? B.JSString_methods.substring$1(t2, t1 + 1) : "";
+    },
+    get$pathSegments() {
+      var parts, t2, i,
+        start = this._pathStart,
+        end = this._queryStart,
+        t1 = this._uri;
+      if (B.JSString_methods.startsWith$2(t1, "/", start))
+        ++start;
+      if (start === end)
+        return B.List_empty;
+      parts = A._setArrayType([], type$.JSArray_String);
+      for (t2 = t1.length, i = start; i < end; ++i) {
+        if (!(i >= 0 && i < t2))
+          return A.ioore(t1, i);
+        if (t1.charCodeAt(i) === 47) {
+          B.JSArray_methods.add$1(parts, B.JSString_methods.substring$2(t1, start, i));
+          start = i + 1;
+        }
+      }
+      B.JSArray_methods.add$1(parts, B.JSString_methods.substring$2(t1, start, end));
+      return A.List_List$unmodifiable(parts, type$.String);
+    },
+    get$queryParameters() {
+      var _this = this;
+      if (_this._queryStart >= _this._fragmentStart)
+        return B.Map_empty;
+      return new A.UnmodifiableMapView(A.Uri_splitQueryString(_this.get$query(_this)), type$.UnmodifiableMapView_String_String);
+    },
+    _isPort$1(port) {
+      var portDigitStart = this._portStart + 1;
+      return portDigitStart + port.length === this._pathStart && B.JSString_methods.startsWith$2(this._uri, port, portDigitStart);
+    },
+    removeFragment$0() {
+      var _this = this,
+        t1 = _this._fragmentStart,
+        t2 = _this._uri;
+      if (t1 >= t2.length)
+        return _this;
+      return new A._SimpleUri(B.JSString_methods.substring$2(t2, 0, t1), _this._schemeEnd, _this._hostStart, _this._portStart, _this._pathStart, _this._queryStart, t1, _this._schemeCache);
+    },
+    resolve$1(reference) {
+      return this.resolveUri$1(A.Uri_parse(reference));
+    },
+    resolveUri$1(reference) {
+      if (reference instanceof A._SimpleUri)
+        return this._simpleMerge$2(this, reference);
+      return this._toNonSimple$0().resolveUri$1(reference);
+    },
+    _simpleMerge$2(base, ref) {
+      var t2, t3, t4, isSimple, delta, refStart, basePathStart, packageNameEnd, basePathStart0, baseStart, baseEnd, baseUri, baseStart0, backCount, refStart0, insert,
+        t1 = ref._schemeEnd;
+      if (t1 > 0)
+        return ref;
+      t2 = ref._hostStart;
+      if (t2 > 0) {
+        t3 = base._schemeEnd;
+        if (t3 <= 0)
+          return ref;
+        t4 = t3 === 4;
+        if (t4 && B.JSString_methods.startsWith$1(base._uri, "file"))
+          isSimple = ref._pathStart !== ref._queryStart;
+        else if (t4 && B.JSString_methods.startsWith$1(base._uri, "http"))
+          isSimple = !ref._isPort$1("80");
+        else
+          isSimple = !(t3 === 5 && B.JSString_methods.startsWith$1(base._uri, "https")) || !ref._isPort$1("443");
+        if (isSimple) {
+          delta = t3 + 1;
+          return new A._SimpleUri(B.JSString_methods.substring$2(base._uri, 0, delta) + B.JSString_methods.substring$1(ref._uri, t1 + 1), t3, t2 + delta, ref._portStart + delta, ref._pathStart + delta, ref._queryStart + delta, ref._fragmentStart + delta, base._schemeCache);
+        } else
+          return this._toNonSimple$0().resolveUri$1(ref);
+      }
+      refStart = ref._pathStart;
+      t1 = ref._queryStart;
+      if (refStart === t1) {
+        t2 = ref._fragmentStart;
+        if (t1 < t2) {
+          t3 = base._queryStart;
+          delta = t3 - t1;
+          return new A._SimpleUri(B.JSString_methods.substring$2(base._uri, 0, t3) + B.JSString_methods.substring$1(ref._uri, t1), base._schemeEnd, base._hostStart, base._portStart, base._pathStart, t1 + delta, t2 + delta, base._schemeCache);
+        }
+        t1 = ref._uri;
+        if (t2 < t1.length) {
+          t3 = base._fragmentStart;
+          return new A._SimpleUri(B.JSString_methods.substring$2(base._uri, 0, t3) + B.JSString_methods.substring$1(t1, t2), base._schemeEnd, base._hostStart, base._portStart, base._pathStart, base._queryStart, t2 + (t3 - t2), base._schemeCache);
+        }
+        return base.removeFragment$0();
+      }
+      t2 = ref._uri;
+      if (B.JSString_methods.startsWith$2(t2, "/", refStart)) {
+        basePathStart = base._pathStart;
+        packageNameEnd = A._SimpleUri__packageNameEnd(this);
+        basePathStart0 = packageNameEnd > 0 ? packageNameEnd : basePathStart;
+        delta = basePathStart0 - refStart;
+        return new A._SimpleUri(B.JSString_methods.substring$2(base._uri, 0, basePathStart0) + B.JSString_methods.substring$1(t2, refStart), base._schemeEnd, base._hostStart, base._portStart, basePathStart, t1 + delta, ref._fragmentStart + delta, base._schemeCache);
+      }
+      baseStart = base._pathStart;
+      baseEnd = base._queryStart;
+      if (baseStart === baseEnd && base._hostStart > 0) {
+        for (; B.JSString_methods.startsWith$2(t2, "../", refStart);)
+          refStart += 3;
+        delta = baseStart - refStart + 1;
+        return new A._SimpleUri(B.JSString_methods.substring$2(base._uri, 0, baseStart) + "/" + B.JSString_methods.substring$1(t2, refStart), base._schemeEnd, base._hostStart, base._portStart, baseStart, t1 + delta, ref._fragmentStart + delta, base._schemeCache);
+      }
+      baseUri = base._uri;
+      packageNameEnd = A._SimpleUri__packageNameEnd(this);
+      if (packageNameEnd >= 0)
+        baseStart0 = packageNameEnd;
+      else
+        for (baseStart0 = baseStart; B.JSString_methods.startsWith$2(baseUri, "../", baseStart0);)
+          baseStart0 += 3;
+      backCount = 0;
+      while (true) {
+        refStart0 = refStart + 3;
+        if (!(refStart0 <= t1 && B.JSString_methods.startsWith$2(t2, "../", refStart)))
+          break;
+        ++backCount;
+        refStart = refStart0;
+      }
+      for (t3 = baseUri.length, insert = ""; baseEnd > baseStart0;) {
+        --baseEnd;
+        if (!(baseEnd >= 0 && baseEnd < t3))
+          return A.ioore(baseUri, baseEnd);
+        if (baseUri.charCodeAt(baseEnd) === 47) {
+          if (backCount === 0) {
+            insert = "/";
+            break;
+          }
+          --backCount;
+          insert = "/";
+        }
+      }
+      if (baseEnd === baseStart0 && base._schemeEnd <= 0 && !B.JSString_methods.startsWith$2(baseUri, "/", baseStart)) {
+        refStart -= backCount * 3;
+        insert = "";
+      }
+      delta = baseEnd - refStart + insert.length;
+      return new A._SimpleUri(B.JSString_methods.substring$2(baseUri, 0, baseEnd) + insert + B.JSString_methods.substring$1(t2, refStart), base._schemeEnd, base._hostStart, base._portStart, baseStart, t1 + delta, ref._fragmentStart + delta, base._schemeCache);
+    },
+    toFilePath$0() {
+      var t2, t3, _this = this,
+        t1 = _this._schemeEnd;
+      if (t1 >= 0) {
+        t2 = !(t1 === 4 && B.JSString_methods.startsWith$1(_this._uri, "file"));
+        t1 = t2;
+      } else
+        t1 = false;
+      if (t1)
+        throw A.wrapException(A.UnsupportedError$("Cannot extract a file path from a " + _this.get$scheme() + " URI"));
+      t1 = _this._queryStart;
+      t2 = _this._uri;
+      if (t1 < t2.length) {
+        if (t1 < _this._fragmentStart)
+          throw A.wrapException(A.UnsupportedError$(string$.Cannotfq));
+        throw A.wrapException(A.UnsupportedError$(string$.Cannotff));
+      }
+      t3 = $.$get$_Uri__isWindowsCached();
+      if (t3)
+        t1 = A._Uri__toWindowsFilePath(_this);
+      else {
+        if (_this._hostStart < _this._portStart)
+          A.throwExpression(A.UnsupportedError$(string$.Cannotn));
+        t1 = B.JSString_methods.substring$2(t2, _this._pathStart, t1);
+      }
+      return t1;
+    },
+    get$hashCode(_) {
+      var t1 = this._hashCodeCache;
+      return t1 == null ? this._hashCodeCache = B.JSString_methods.get$hashCode(this._uri) : t1;
+    },
+    $eq(_, other) {
+      if (other == null)
+        return false;
+      if (this === other)
+        return true;
+      return type$.Uri._is(other) && this._uri === other.toString$0(0);
+    },
+    _toNonSimple$0() {
+      var _this = this, _null = null,
+        t1 = _this.get$scheme(),
+        t2 = _this.get$userInfo(),
+        t3 = _this._hostStart > 0 ? _this.get$host(_this) : _null,
+        t4 = _this.get$hasPort() ? _this.get$port(_this) : _null,
+        t5 = _this._uri,
+        t6 = _this._queryStart,
+        t7 = B.JSString_methods.substring$2(t5, _this._pathStart, t6),
+        t8 = _this._fragmentStart;
+      t6 = t6 < t8 ? _this.get$query(_this) : _null;
+      return A._Uri$_internal(t1, t2, t3, t4, t7, t6, t8 < t5.length ? _this.get$fragment() : _null);
+    },
+    toString$0(_) {
+      return this._uri;
+    },
+    $isUri: 1
+  };
+  A._DataUri.prototype = {};
+  A.HtmlElement.prototype = {};
+  A.AccessibleNodeList.prototype = {
+    get$length(receiver) {
+      return receiver.length;
+    }
+  };
+  A.AnchorElement.prototype = {
+    toString$0(receiver) {
+      var t1 = String(receiver);
+      t1.toString;
+      return t1;
+    }
+  };
+  A.AreaElement.prototype = {
+    toString$0(receiver) {
+      var t1 = String(receiver);
+      t1.toString;
+      return t1;
+    }
+  };
+  A.Blob.prototype = {};
+  A.CharacterData.prototype = {
+    get$length(receiver) {
+      return receiver.length;
+    }
+  };
+  A.CssPerspective.prototype = {
+    get$length(receiver) {
+      return receiver.length;
+    }
+  };
+  A.CssRule.prototype = {$isCssRule: 1};
+  A.CssStyleDeclaration.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    }
+  };
+  A.CssStyleDeclarationBase.prototype = {};
+  A.CssStyleValue.prototype = {};
+  A.CssTransformComponent.prototype = {};
+  A.CssTransformValue.prototype = {
+    get$length(receiver) {
+      return receiver.length;
+    }
+  };
+  A.CssUnparsedValue.prototype = {
+    get$length(receiver) {
+      return receiver.length;
+    }
+  };
+  A.DataTransferItemList.prototype = {
+    get$length(receiver) {
+      return receiver.length;
+    }
+  };
+  A.DomException.prototype = {
+    toString$0(receiver) {
+      var t1 = String(receiver);
+      t1.toString;
+      return t1;
+    }
+  };
+  A.DomRectList.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length,
+        t2 = index >>> 0 !== index || index >= t1;
+      t2.toString;
+      if (t2)
+        throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null));
+      t1 = receiver[index];
+      t1.toString;
+      return t1;
+    },
+    $indexSet(receiver, index, value) {
+      type$.Rectangle_num._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      if (!(index >= 0 && index < receiver.length))
+        return A.ioore(receiver, index);
+      return receiver[index];
+    },
+    $isEfficientLengthIterable: 1,
+    $isJavaScriptIndexingBehavior: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A.DomRectReadOnly.prototype = {
+    toString$0(receiver) {
+      var t2,
+        t1 = receiver.left;
+      t1.toString;
+      t2 = receiver.top;
+      t2.toString;
+      return "Rectangle (" + A.S(t1) + ", " + A.S(t2) + ") " + A.S(this.get$width(receiver)) + " x " + A.S(this.get$height(receiver));
+    },
+    $eq(receiver, other) {
+      var t1, t2;
+      if (other == null)
+        return false;
+      if (type$.Rectangle_num._is(other)) {
+        t1 = receiver.left;
+        t1.toString;
+        t2 = other.left;
+        t2.toString;
+        if (t1 === t2) {
+          t1 = receiver.top;
+          t1.toString;
+          t2 = other.top;
+          t2.toString;
+          if (t1 === t2) {
+            t1 = J.getInterceptor$x(other);
+            t1 = this.get$width(receiver) === t1.get$width(other) && this.get$height(receiver) === t1.get$height(other);
+          } else
+            t1 = false;
+        } else
+          t1 = false;
+      } else
+        t1 = false;
+      return t1;
+    },
+    get$hashCode(receiver) {
+      var t2,
+        t1 = receiver.left;
+      t1.toString;
+      t2 = receiver.top;
+      t2.toString;
+      return A.Object_hash(t1, t2, this.get$width(receiver), this.get$height(receiver));
+    },
+    get$_height(receiver) {
+      return receiver.height;
+    },
+    get$height(receiver) {
+      var t1 = this.get$_height(receiver);
+      t1.toString;
+      return t1;
+    },
+    get$_width(receiver) {
+      return receiver.width;
+    },
+    get$width(receiver) {
+      var t1 = this.get$_width(receiver);
+      t1.toString;
+      return t1;
+    },
+    $isRectangle: 1
+  };
+  A.DomStringList.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length,
+        t2 = index >>> 0 !== index || index >= t1;
+      t2.toString;
+      if (t2)
+        throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null));
+      t1 = receiver[index];
+      t1.toString;
+      return t1;
+    },
+    $indexSet(receiver, index, value) {
+      A._asString(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      if (!(index >= 0 && index < receiver.length))
+        return A.ioore(receiver, index);
+      return receiver[index];
+    },
+    $isEfficientLengthIterable: 1,
+    $isJavaScriptIndexingBehavior: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A.DomTokenList.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    }
+  };
+  A.Element.prototype = {
+    toString$0(receiver) {
+      var t1 = receiver.localName;
+      t1.toString;
+      return t1;
+    }
+  };
+  A.EventTarget.prototype = {};
+  A.File.prototype = {$isFile: 1};
+  A.FileList.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length,
+        t2 = index >>> 0 !== index || index >= t1;
+      t2.toString;
+      if (t2)
+        throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null));
+      t1 = receiver[index];
+      t1.toString;
+      return t1;
+    },
+    $indexSet(receiver, index, value) {
+      type$.File._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      if (!(index >= 0 && index < receiver.length))
+        return A.ioore(receiver, index);
+      return receiver[index];
+    },
+    $isEfficientLengthIterable: 1,
+    $isJavaScriptIndexingBehavior: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A.FileWriter.prototype = {
+    get$length(receiver) {
+      return receiver.length;
+    }
+  };
+  A.FormElement.prototype = {
+    get$length(receiver) {
+      return receiver.length;
+    }
+  };
+  A.Gamepad.prototype = {$isGamepad: 1};
+  A.History.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    }
+  };
+  A.HtmlCollection.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length,
+        t2 = index >>> 0 !== index || index >= t1;
+      t2.toString;
+      if (t2)
+        throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null));
+      t1 = receiver[index];
+      t1.toString;
+      return t1;
+    },
+    $indexSet(receiver, index, value) {
+      type$.Node._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      if (!(index >= 0 && index < receiver.length))
+        return A.ioore(receiver, index);
+      return receiver[index];
+    },
+    $isEfficientLengthIterable: 1,
+    $isJavaScriptIndexingBehavior: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A.Location.prototype = {
+    toString$0(receiver) {
+      var t1 = String(receiver);
+      t1.toString;
+      return t1;
+    }
+  };
+  A.MediaList.prototype = {
+    get$length(receiver) {
+      return receiver.length;
+    }
+  };
+  A.MidiInputMap.prototype = {
+    containsKey$1(receiver, key) {
+      return A.convertNativeToDart_Dictionary(receiver.get(key)) != null;
+    },
+    $index(receiver, key) {
+      return A.convertNativeToDart_Dictionary(receiver.get(A._asString(key)));
+    },
+    forEach$1(receiver, f) {
+      var entries, entry, t1;
+      type$.void_Function_String_dynamic._as(f);
+      entries = receiver.entries();
+      for (; true;) {
+        entry = entries.next();
+        t1 = entry.done;
+        t1.toString;
+        if (t1)
+          return;
+        t1 = entry.value[0];
+        t1.toString;
+        f.call$2(t1, A.convertNativeToDart_Dictionary(entry.value[1]));
+      }
+    },
+    get$keys(receiver) {
+      var keys = A._setArrayType([], type$.JSArray_String);
+      this.forEach$1(receiver, new A.MidiInputMap_keys_closure(keys));
+      return keys;
+    },
+    get$length(receiver) {
+      var t1 = receiver.size;
+      t1.toString;
+      return t1;
+    },
+    get$isEmpty(receiver) {
+      var t1 = receiver.size;
+      t1.toString;
+      return t1 === 0;
+    },
+    $indexSet(receiver, key, value) {
+      throw A.wrapException(A.UnsupportedError$("Not supported"));
+    },
+    $isMap: 1
+  };
+  A.MidiInputMap_keys_closure.prototype = {
+    call$2(k, v) {
+      return B.JSArray_methods.add$1(this.keys, k);
+    },
+    $signature: 4
+  };
+  A.MidiOutputMap.prototype = {
+    containsKey$1(receiver, key) {
+      return A.convertNativeToDart_Dictionary(receiver.get(key)) != null;
+    },
+    $index(receiver, key) {
+      return A.convertNativeToDart_Dictionary(receiver.get(A._asString(key)));
+    },
+    forEach$1(receiver, f) {
+      var entries, entry, t1;
+      type$.void_Function_String_dynamic._as(f);
+      entries = receiver.entries();
+      for (; true;) {
+        entry = entries.next();
+        t1 = entry.done;
+        t1.toString;
+        if (t1)
+          return;
+        t1 = entry.value[0];
+        t1.toString;
+        f.call$2(t1, A.convertNativeToDart_Dictionary(entry.value[1]));
+      }
+    },
+    get$keys(receiver) {
+      var keys = A._setArrayType([], type$.JSArray_String);
+      this.forEach$1(receiver, new A.MidiOutputMap_keys_closure(keys));
+      return keys;
+    },
+    get$length(receiver) {
+      var t1 = receiver.size;
+      t1.toString;
+      return t1;
+    },
+    get$isEmpty(receiver) {
+      var t1 = receiver.size;
+      t1.toString;
+      return t1 === 0;
+    },
+    $indexSet(receiver, key, value) {
+      throw A.wrapException(A.UnsupportedError$("Not supported"));
+    },
+    $isMap: 1
+  };
+  A.MidiOutputMap_keys_closure.prototype = {
+    call$2(k, v) {
+      return B.JSArray_methods.add$1(this.keys, k);
+    },
+    $signature: 4
+  };
+  A.MimeType.prototype = {$isMimeType: 1};
+  A.MimeTypeArray.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length,
+        t2 = index >>> 0 !== index || index >= t1;
+      t2.toString;
+      if (t2)
+        throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null));
+      t1 = receiver[index];
+      t1.toString;
+      return t1;
+    },
+    $indexSet(receiver, index, value) {
+      type$.MimeType._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      if (!(index >= 0 && index < receiver.length))
+        return A.ioore(receiver, index);
+      return receiver[index];
+    },
+    $isEfficientLengthIterable: 1,
+    $isJavaScriptIndexingBehavior: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A.Node.prototype = {
+    toString$0(receiver) {
+      var value = receiver.nodeValue;
+      return value == null ? this.super$Interceptor$toString(receiver) : value;
+    },
+    $isNode: 1
+  };
+  A.NodeList.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length,
+        t2 = index >>> 0 !== index || index >= t1;
+      t2.toString;
+      if (t2)
+        throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null));
+      t1 = receiver[index];
+      t1.toString;
+      return t1;
+    },
+    $indexSet(receiver, index, value) {
+      type$.Node._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      if (!(index >= 0 && index < receiver.length))
+        return A.ioore(receiver, index);
+      return receiver[index];
+    },
+    $isEfficientLengthIterable: 1,
+    $isJavaScriptIndexingBehavior: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A.Plugin.prototype = {
+    get$length(receiver) {
+      return receiver.length;
+    },
+    $isPlugin: 1
+  };
+  A.PluginArray.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length,
+        t2 = index >>> 0 !== index || index >= t1;
+      t2.toString;
+      if (t2)
+        throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null));
+      t1 = receiver[index];
+      t1.toString;
+      return t1;
+    },
+    $indexSet(receiver, index, value) {
+      type$.Plugin._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      if (!(index >= 0 && index < receiver.length))
+        return A.ioore(receiver, index);
+      return receiver[index];
+    },
+    $isEfficientLengthIterable: 1,
+    $isJavaScriptIndexingBehavior: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A.RtcStatsReport.prototype = {
+    containsKey$1(receiver, key) {
+      return A.convertNativeToDart_Dictionary(receiver.get(key)) != null;
+    },
+    $index(receiver, key) {
+      return A.convertNativeToDart_Dictionary(receiver.get(A._asString(key)));
+    },
+    forEach$1(receiver, f) {
+      var entries, entry, t1;
+      type$.void_Function_String_dynamic._as(f);
+      entries = receiver.entries();
+      for (; true;) {
+        entry = entries.next();
+        t1 = entry.done;
+        t1.toString;
+        if (t1)
+          return;
+        t1 = entry.value[0];
+        t1.toString;
+        f.call$2(t1, A.convertNativeToDart_Dictionary(entry.value[1]));
+      }
+    },
+    get$keys(receiver) {
+      var keys = A._setArrayType([], type$.JSArray_String);
+      this.forEach$1(receiver, new A.RtcStatsReport_keys_closure(keys));
+      return keys;
+    },
+    get$length(receiver) {
+      var t1 = receiver.size;
+      t1.toString;
+      return t1;
+    },
+    get$isEmpty(receiver) {
+      var t1 = receiver.size;
+      t1.toString;
+      return t1 === 0;
+    },
+    $indexSet(receiver, key, value) {
+      throw A.wrapException(A.UnsupportedError$("Not supported"));
+    },
+    $isMap: 1
+  };
+  A.RtcStatsReport_keys_closure.prototype = {
+    call$2(k, v) {
+      return B.JSArray_methods.add$1(this.keys, k);
+    },
+    $signature: 4
+  };
+  A.SelectElement.prototype = {
+    get$length(receiver) {
+      return receiver.length;
+    }
+  };
+  A.SourceBuffer.prototype = {$isSourceBuffer: 1};
+  A.SourceBufferList.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length,
+        t2 = index >>> 0 !== index || index >= t1;
+      t2.toString;
+      if (t2)
+        throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null));
+      t1 = receiver[index];
+      t1.toString;
+      return t1;
+    },
+    $indexSet(receiver, index, value) {
+      type$.SourceBuffer._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      if (!(index >= 0 && index < receiver.length))
+        return A.ioore(receiver, index);
+      return receiver[index];
+    },
+    $isEfficientLengthIterable: 1,
+    $isJavaScriptIndexingBehavior: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A.SpeechGrammar.prototype = {$isSpeechGrammar: 1};
+  A.SpeechGrammarList.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length,
+        t2 = index >>> 0 !== index || index >= t1;
+      t2.toString;
+      if (t2)
+        throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null));
+      t1 = receiver[index];
+      t1.toString;
+      return t1;
+    },
+    $indexSet(receiver, index, value) {
+      type$.SpeechGrammar._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      if (!(index >= 0 && index < receiver.length))
+        return A.ioore(receiver, index);
+      return receiver[index];
+    },
+    $isEfficientLengthIterable: 1,
+    $isJavaScriptIndexingBehavior: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A.SpeechRecognitionResult.prototype = {
+    get$length(receiver) {
+      return receiver.length;
+    },
+    $isSpeechRecognitionResult: 1
+  };
+  A.Storage.prototype = {
+    containsKey$1(receiver, key) {
+      return receiver.getItem(key) != null;
+    },
+    $index(receiver, key) {
+      return receiver.getItem(A._asString(key));
+    },
+    $indexSet(receiver, key, value) {
+      receiver.setItem(key, value);
+    },
+    forEach$1(receiver, f) {
+      var i, key, t1;
+      type$.void_Function_String_String._as(f);
+      for (i = 0; true; ++i) {
+        key = receiver.key(i);
+        if (key == null)
+          return;
+        t1 = receiver.getItem(key);
+        t1.toString;
+        f.call$2(key, t1);
+      }
+    },
+    get$keys(receiver) {
+      var keys = A._setArrayType([], type$.JSArray_String);
+      this.forEach$1(receiver, new A.Storage_keys_closure(keys));
+      return keys;
+    },
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    get$isEmpty(receiver) {
+      return receiver.key(0) == null;
+    },
+    $isMap: 1
+  };
+  A.Storage_keys_closure.prototype = {
+    call$2(k, v) {
+      return B.JSArray_methods.add$1(this.keys, k);
+    },
+    $signature: 27
+  };
+  A.StyleSheet.prototype = {$isStyleSheet: 1};
+  A.TextTrack.prototype = {$isTextTrack: 1};
+  A.TextTrackCue.prototype = {$isTextTrackCue: 1};
+  A.TextTrackCueList.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length,
+        t2 = index >>> 0 !== index || index >= t1;
+      t2.toString;
+      if (t2)
+        throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null));
+      t1 = receiver[index];
+      t1.toString;
+      return t1;
+    },
+    $indexSet(receiver, index, value) {
+      type$.TextTrackCue._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      if (!(index >= 0 && index < receiver.length))
+        return A.ioore(receiver, index);
+      return receiver[index];
+    },
+    $isEfficientLengthIterable: 1,
+    $isJavaScriptIndexingBehavior: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A.TextTrackList.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length,
+        t2 = index >>> 0 !== index || index >= t1;
+      t2.toString;
+      if (t2)
+        throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null));
+      t1 = receiver[index];
+      t1.toString;
+      return t1;
+    },
+    $indexSet(receiver, index, value) {
+      type$.TextTrack._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      if (!(index >= 0 && index < receiver.length))
+        return A.ioore(receiver, index);
+      return receiver[index];
+    },
+    $isEfficientLengthIterable: 1,
+    $isJavaScriptIndexingBehavior: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A.TimeRanges.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    }
+  };
+  A.Touch.prototype = {$isTouch: 1};
+  A.TouchList.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length,
+        t2 = index >>> 0 !== index || index >= t1;
+      t2.toString;
+      if (t2)
+        throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null));
+      t1 = receiver[index];
+      t1.toString;
+      return t1;
+    },
+    $indexSet(receiver, index, value) {
+      type$.Touch._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      if (!(index >= 0 && index < receiver.length))
+        return A.ioore(receiver, index);
+      return receiver[index];
+    },
+    $isEfficientLengthIterable: 1,
+    $isJavaScriptIndexingBehavior: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A.TrackDefaultList.prototype = {
+    get$length(receiver) {
+      return receiver.length;
+    }
+  };
+  A.Url.prototype = {
+    toString$0(receiver) {
+      var t1 = String(receiver);
+      t1.toString;
+      return t1;
+    }
+  };
+  A.VideoTrackList.prototype = {
+    get$length(receiver) {
+      return receiver.length;
+    }
+  };
+  A._CssRuleList.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length,
+        t2 = index >>> 0 !== index || index >= t1;
+      t2.toString;
+      if (t2)
+        throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null));
+      t1 = receiver[index];
+      t1.toString;
+      return t1;
+    },
+    $indexSet(receiver, index, value) {
+      type$.CssRule._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      if (!(index >= 0 && index < receiver.length))
+        return A.ioore(receiver, index);
+      return receiver[index];
+    },
+    $isEfficientLengthIterable: 1,
+    $isJavaScriptIndexingBehavior: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A._DomRect.prototype = {
+    toString$0(receiver) {
+      var t2, t3, t4,
+        t1 = receiver.left;
+      t1.toString;
+      t2 = receiver.top;
+      t2.toString;
+      t3 = receiver.width;
+      t3.toString;
+      t4 = receiver.height;
+      t4.toString;
+      return "Rectangle (" + A.S(t1) + ", " + A.S(t2) + ") " + A.S(t3) + " x " + A.S(t4);
+    },
+    $eq(receiver, other) {
+      var t1, t2;
+      if (other == null)
+        return false;
+      if (type$.Rectangle_num._is(other)) {
+        t1 = receiver.left;
+        t1.toString;
+        t2 = other.left;
+        t2.toString;
+        if (t1 === t2) {
+          t1 = receiver.top;
+          t1.toString;
+          t2 = other.top;
+          t2.toString;
+          if (t1 === t2) {
+            t1 = receiver.width;
+            t1.toString;
+            t2 = J.getInterceptor$x(other);
+            if (t1 === t2.get$width(other)) {
+              t1 = receiver.height;
+              t1.toString;
+              t2 = t1 === t2.get$height(other);
+              t1 = t2;
+            } else
+              t1 = false;
+          } else
+            t1 = false;
+        } else
+          t1 = false;
+      } else
+        t1 = false;
+      return t1;
+    },
+    get$hashCode(receiver) {
+      var t2, t3, t4,
+        t1 = receiver.left;
+      t1.toString;
+      t2 = receiver.top;
+      t2.toString;
+      t3 = receiver.width;
+      t3.toString;
+      t4 = receiver.height;
+      t4.toString;
+      return A.Object_hash(t1, t2, t3, t4);
+    },
+    get$_height(receiver) {
+      return receiver.height;
+    },
+    get$height(receiver) {
+      var t1 = receiver.height;
+      t1.toString;
+      return t1;
+    },
+    get$_width(receiver) {
+      return receiver.width;
+    },
+    get$width(receiver) {
+      var t1 = receiver.width;
+      t1.toString;
+      return t1;
+    }
+  };
+  A._GamepadList.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length,
+        t2 = index >>> 0 !== index || index >= t1;
+      t2.toString;
+      if (t2)
+        throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null));
+      return receiver[index];
+    },
+    $indexSet(receiver, index, value) {
+      type$.nullable_Gamepad._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      if (!(index >= 0 && index < receiver.length))
+        return A.ioore(receiver, index);
+      return receiver[index];
+    },
+    $isEfficientLengthIterable: 1,
+    $isJavaScriptIndexingBehavior: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A._NamedNodeMap.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length,
+        t2 = index >>> 0 !== index || index >= t1;
+      t2.toString;
+      if (t2)
+        throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null));
+      t1 = receiver[index];
+      t1.toString;
+      return t1;
+    },
+    $indexSet(receiver, index, value) {
+      type$.Node._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      if (!(index >= 0 && index < receiver.length))
+        return A.ioore(receiver, index);
+      return receiver[index];
+    },
+    $isEfficientLengthIterable: 1,
+    $isJavaScriptIndexingBehavior: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A._SpeechRecognitionResultList.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length,
+        t2 = index >>> 0 !== index || index >= t1;
+      t2.toString;
+      if (t2)
+        throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null));
+      t1 = receiver[index];
+      t1.toString;
+      return t1;
+    },
+    $indexSet(receiver, index, value) {
+      type$.SpeechRecognitionResult._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      if (!(index >= 0 && index < receiver.length))
+        return A.ioore(receiver, index);
+      return receiver[index];
+    },
+    $isEfficientLengthIterable: 1,
+    $isJavaScriptIndexingBehavior: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A._StyleSheetList.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length,
+        t2 = index >>> 0 !== index || index >= t1;
+      t2.toString;
+      if (t2)
+        throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null));
+      t1 = receiver[index];
+      t1.toString;
+      return t1;
+    },
+    $indexSet(receiver, index, value) {
+      type$.StyleSheet._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      if (!(index >= 0 && index < receiver.length))
+        return A.ioore(receiver, index);
+      return receiver[index];
+    },
+    $isEfficientLengthIterable: 1,
+    $isJavaScriptIndexingBehavior: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A.ImmutableListMixin.prototype = {
+    get$iterator(receiver) {
+      return new A.FixedSizeListIterator(receiver, this.get$length(receiver), A.instanceType(receiver)._eval$1("FixedSizeListIterator<ImmutableListMixin.E>"));
+    }
+  };
+  A.FixedSizeListIterator.prototype = {
+    moveNext$0() {
+      var _this = this,
+        nextPosition = _this._position + 1,
+        t1 = _this._html$_length;
+      if (nextPosition < t1) {
+        _this.set$_html$_current(J.$index$asx(_this._array, nextPosition));
+        _this._position = nextPosition;
+        return true;
+      }
+      _this.set$_html$_current(null);
+      _this._position = t1;
+      return false;
+    },
+    get$current(_) {
+      var t1 = this._html$_current;
+      return t1 == null ? this.$ti._precomputed1._as(t1) : t1;
+    },
+    set$_html$_current(_current) {
+      this._html$_current = this.$ti._eval$1("1?")._as(_current);
+    },
+    $isIterator: 1
+  };
+  A._CssStyleDeclaration_JavaScriptObject_CssStyleDeclarationBase.prototype = {};
+  A._DomRectList_JavaScriptObject_ListMixin.prototype = {};
+  A._DomRectList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {};
+  A._DomStringList_JavaScriptObject_ListMixin.prototype = {};
+  A._DomStringList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {};
+  A._FileList_JavaScriptObject_ListMixin.prototype = {};
+  A._FileList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {};
+  A._HtmlCollection_JavaScriptObject_ListMixin.prototype = {};
+  A._HtmlCollection_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {};
+  A._MidiInputMap_JavaScriptObject_MapMixin.prototype = {};
+  A._MidiOutputMap_JavaScriptObject_MapMixin.prototype = {};
+  A._MimeTypeArray_JavaScriptObject_ListMixin.prototype = {};
+  A._MimeTypeArray_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {};
+  A._NodeList_JavaScriptObject_ListMixin.prototype = {};
+  A._NodeList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {};
+  A._PluginArray_JavaScriptObject_ListMixin.prototype = {};
+  A._PluginArray_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {};
+  A._RtcStatsReport_JavaScriptObject_MapMixin.prototype = {};
+  A._SourceBufferList_EventTarget_ListMixin.prototype = {};
+  A._SourceBufferList_EventTarget_ListMixin_ImmutableListMixin.prototype = {};
+  A._SpeechGrammarList_JavaScriptObject_ListMixin.prototype = {};
+  A._SpeechGrammarList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {};
+  A._Storage_JavaScriptObject_MapMixin.prototype = {};
+  A._TextTrackCueList_JavaScriptObject_ListMixin.prototype = {};
+  A._TextTrackCueList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {};
+  A._TextTrackList_EventTarget_ListMixin.prototype = {};
+  A._TextTrackList_EventTarget_ListMixin_ImmutableListMixin.prototype = {};
+  A._TouchList_JavaScriptObject_ListMixin.prototype = {};
+  A._TouchList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {};
+  A.__CssRuleList_JavaScriptObject_ListMixin.prototype = {};
+  A.__CssRuleList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {};
+  A.__GamepadList_JavaScriptObject_ListMixin.prototype = {};
+  A.__GamepadList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {};
+  A.__NamedNodeMap_JavaScriptObject_ListMixin.prototype = {};
+  A.__NamedNodeMap_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {};
+  A.__SpeechRecognitionResultList_JavaScriptObject_ListMixin.prototype = {};
+  A.__SpeechRecognitionResultList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {};
+  A.__StyleSheetList_JavaScriptObject_ListMixin.prototype = {};
+  A.__StyleSheetList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {};
+  A.jsify__convert.prototype = {
+    call$1(o) {
+      var t1, convertedMap, t2, key, convertedList;
+      if (A._noJsifyRequired(o))
+        return o;
+      t1 = this._convertedObjects;
+      if (t1.containsKey$1(0, o))
+        return t1.$index(0, o);
+      if (type$.Map_of_nullable_Object_and_nullable_Object._is(o)) {
+        convertedMap = {};
+        t1.$indexSet(0, o, convertedMap);
+        for (t1 = J.getInterceptor$x(o), t2 = J.get$iterator$ax(t1.get$keys(o)); t2.moveNext$0();) {
+          key = t2.get$current(t2);
+          convertedMap[key] = this.call$1(t1.$index(o, key));
+        }
+        return convertedMap;
+      } else if (type$.Iterable_nullable_Object._is(o)) {
+        convertedList = [];
+        t1.$indexSet(0, o, convertedList);
+        B.JSArray_methods.addAll$1(convertedList, J.map$1$1$ax(o, this, type$.dynamic));
+        return convertedList;
+      } else
+        return o;
+    },
+    $signature: 19
+  };
+  A.promiseToFuture_closure.prototype = {
+    call$1(r) {
+      return this.completer.complete$1(0, this.T._eval$1("0/?")._as(r));
+    },
+    $signature: 5
+  };
+  A.promiseToFuture_closure0.prototype = {
+    call$1(e) {
+      if (e == null)
+        return this.completer.completeError$1(new A.NullRejectionException(e === undefined));
+      return this.completer.completeError$1(e);
+    },
+    $signature: 5
+  };
+  A.dartify_convert.prototype = {
+    call$1(o) {
+      var t1, millisSinceEpoch, proto, t2, dartObject, originalKeys, dartKeys, i, jsKey, dartKey, l, $length;
+      if (A._noDartifyRequired(o))
+        return o;
+      t1 = this._convertedObjects;
+      o.toString;
+      if (t1.containsKey$1(0, o))
+        return t1.$index(0, o);
+      if (o instanceof Date) {
+        millisSinceEpoch = o.getTime();
+        if (Math.abs(millisSinceEpoch) <= 864e13)
+          t1 = false;
+        else
+          t1 = true;
+        if (t1)
+          A.throwExpression(A.ArgumentError$("DateTime is outside valid range: " + millisSinceEpoch, null));
+        A.checkNotNullable(true, "isUtc", type$.bool);
+        return new A.DateTime(millisSinceEpoch, true);
+      }
+      if (o instanceof RegExp)
+        throw A.wrapException(A.ArgumentError$("structured clone of RegExp", null));
+      if (typeof Promise != "undefined" && o instanceof Promise)
+        return A.promiseToFuture(o, type$.nullable_Object);
+      proto = Object.getPrototypeOf(o);
+      if (proto === Object.prototype || proto === null) {
+        t2 = type$.nullable_Object;
+        dartObject = A.LinkedHashMap_LinkedHashMap$_empty(t2, t2);
+        t1.$indexSet(0, o, dartObject);
+        originalKeys = Object.keys(o);
+        dartKeys = [];
+        for (t1 = J.getInterceptor$ax(originalKeys), t2 = t1.get$iterator(originalKeys); t2.moveNext$0();)
+          dartKeys.push(A.dartify(t2.get$current(t2)));
+        for (i = 0; i < t1.get$length(originalKeys); ++i) {
+          jsKey = t1.$index(originalKeys, i);
+          if (!(i < dartKeys.length))
+            return A.ioore(dartKeys, i);
+          dartKey = dartKeys[i];
+          if (jsKey != null)
+            dartObject.$indexSet(0, dartKey, this.call$1(o[jsKey]));
+        }
+        return dartObject;
+      }
+      if (o instanceof Array) {
+        l = o;
+        dartObject = [];
+        t1.$indexSet(0, o, dartObject);
+        $length = A._asInt(o.length);
+        for (t1 = J.getInterceptor$asx(l), i = 0; i < $length; ++i)
+          dartObject.push(this.call$1(t1.$index(l, i)));
+        return dartObject;
+      }
+      return o;
+    },
+    $signature: 19
+  };
+  A.NullRejectionException.prototype = {
+    toString$0(_) {
+      return "Promise was rejected with a value of `" + (this.isUndefined ? "undefined" : "null") + "`.";
+    },
+    $isException: 1
+  };
+  A.Length.prototype = {$isLength: 1};
+  A.LengthList.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length;
+      t1.toString;
+      t1 = index >>> 0 !== index || index >= t1;
+      t1.toString;
+      if (t1)
+        throw A.wrapException(A.IndexError$withLength(index, this.get$length(receiver), receiver, null));
+      t1 = receiver.getItem(index);
+      t1.toString;
+      return t1;
+    },
+    $indexSet(receiver, index, value) {
+      type$.Length._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      return this.$index(receiver, index);
+    },
+    $isEfficientLengthIterable: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A.Number.prototype = {$isNumber: 1};
+  A.NumberList.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length;
+      t1.toString;
+      t1 = index >>> 0 !== index || index >= t1;
+      t1.toString;
+      if (t1)
+        throw A.wrapException(A.IndexError$withLength(index, this.get$length(receiver), receiver, null));
+      t1 = receiver.getItem(index);
+      t1.toString;
+      return t1;
+    },
+    $indexSet(receiver, index, value) {
+      type$.Number._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      return this.$index(receiver, index);
+    },
+    $isEfficientLengthIterable: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A.PointList.prototype = {
+    get$length(receiver) {
+      return receiver.length;
+    }
+  };
+  A.StringList.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length;
+      t1.toString;
+      t1 = index >>> 0 !== index || index >= t1;
+      t1.toString;
+      if (t1)
+        throw A.wrapException(A.IndexError$withLength(index, this.get$length(receiver), receiver, null));
+      t1 = receiver.getItem(index);
+      t1.toString;
+      return t1;
+    },
+    $indexSet(receiver, index, value) {
+      A._asString(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      return this.$index(receiver, index);
+    },
+    $isEfficientLengthIterable: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A.Transform.prototype = {$isTransform: 1};
+  A.TransformList.prototype = {
+    get$length(receiver) {
+      var t1 = receiver.length;
+      t1.toString;
+      return t1;
+    },
+    $index(receiver, index) {
+      var t1 = receiver.length;
+      t1.toString;
+      t1 = index >>> 0 !== index || index >= t1;
+      t1.toString;
+      if (t1)
+        throw A.wrapException(A.IndexError$withLength(index, this.get$length(receiver), receiver, null));
+      t1 = receiver.getItem(index);
+      t1.toString;
+      return t1;
+    },
+    $indexSet(receiver, index, value) {
+      type$.Transform._as(value);
+      throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List."));
+    },
+    elementAt$1(receiver, index) {
+      return this.$index(receiver, index);
+    },
+    $isEfficientLengthIterable: 1,
+    $isIterable: 1,
+    $isList: 1
+  };
+  A._LengthList_JavaScriptObject_ListMixin.prototype = {};
+  A._LengthList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {};
+  A._NumberList_JavaScriptObject_ListMixin.prototype = {};
+  A._NumberList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {};
+  A._StringList_JavaScriptObject_ListMixin.prototype = {};
+  A._StringList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {};
+  A._TransformList_JavaScriptObject_ListMixin.prototype = {};
+  A._TransformList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {};
+  A.AudioBuffer.prototype = {
+    get$length(receiver) {
+      return receiver.length;
+    }
+  };
+  A.AudioParamMap.prototype = {
+    containsKey$1(receiver, key) {
+      return A.convertNativeToDart_Dictionary(receiver.get(key)) != null;
+    },
+    $index(receiver, key) {
+      return A.convertNativeToDart_Dictionary(receiver.get(A._asString(key)));
+    },
+    forEach$1(receiver, f) {
+      var entries, entry, t1;
+      type$.void_Function_String_dynamic._as(f);
+      entries = receiver.entries();
+      for (; true;) {
+        entry = entries.next();
+        t1 = entry.done;
+        t1.toString;
+        if (t1)
+          return;
+        t1 = entry.value[0];
+        t1.toString;
+        f.call$2(t1, A.convertNativeToDart_Dictionary(entry.value[1]));
+      }
+    },
+    get$keys(receiver) {
+      var keys = A._setArrayType([], type$.JSArray_String);
+      this.forEach$1(receiver, new A.AudioParamMap_keys_closure(keys));
+      return keys;
+    },
+    get$length(receiver) {
+      var t1 = receiver.size;
+      t1.toString;
+      return t1;
+    },
+    get$isEmpty(receiver) {
+      var t1 = receiver.size;
+      t1.toString;
+      return t1 === 0;
+    },
+    $indexSet(receiver, key, value) {
+      throw A.wrapException(A.UnsupportedError$("Not supported"));
+    },
+    $isMap: 1
+  };
+  A.AudioParamMap_keys_closure.prototype = {
+    call$2(k, v) {
+      return B.JSArray_methods.add$1(this.keys, k);
+    },
+    $signature: 4
+  };
+  A.AudioTrackList.prototype = {
+    get$length(receiver) {
+      return receiver.length;
+    }
+  };
+  A.BaseAudioContext.prototype = {};
+  A.OfflineAudioContext.prototype = {
+    get$length(receiver) {
+      return receiver.length;
+    }
+  };
+  A._AudioParamMap_JavaScriptObject_MapMixin.prototype = {};
+  A.NullStreamSink.prototype = {
+    addStream$1(_, stream) {
+      var _this = this;
+      _this.$ti._eval$1("Stream<1>")._as(stream);
+      _this._checkEventAllowed$0();
+      _this._addingStream = true;
+      return stream.listen$1(null).cancel$0(0).whenComplete$1(new A.NullStreamSink_addStream_closure(_this));
+    },
+    _checkEventAllowed$0() {
+      if (this._null_stream_sink$_closed)
+        throw A.wrapException(A.StateError$("Cannot add to a closed sink."));
+      if (this._addingStream)
+        throw A.wrapException(A.StateError$("Cannot add to a sink while adding a stream."));
+    },
+    close$0(_) {
+      this._null_stream_sink$_closed = true;
+      return this.done;
+    },
+    $isStreamConsumer: 1,
+    $isStreamSink: 1
+  };
+  A.NullStreamSink_addStream_closure.prototype = {
+    call$0() {
+      this.$this._addingStream = false;
+    },
+    $signature: 3
+  };
+  A.Context.prototype = {
+    absolute$15(_, part1, part2, part3, part4, part5, part6, part7, part8, part9, part10, part11, part12, part13, part14, part15) {
+      var t1;
+      A._validateArgList("absolute", A._setArrayType([part1, part2, part3, part4, part5, part6, part7, part8, part9, part10, part11, part12, part13, part14, part15], type$.JSArray_nullable_String));
+      t1 = this.style;
+      t1 = t1.rootLength$1(part1) > 0 && !t1.isRootRelative$1(part1);
+      if (t1)
+        return part1;
+      t1 = this._context$_current;
+      return this.join$16(0, t1 == null ? A.current() : t1, part1, part2, part3, part4, part5, part6, part7, part8, part9, part10, part11, part12, part13, part14, part15);
+    },
+    absolute$1($receiver, part1) {
+      return this.absolute$15($receiver, part1, null, null, null, null, null, null, null, null, null, null, null, null, null, null);
+    },
+    join$16(_, part1, part2, part3, part4, part5, part6, part7, part8, part9, part10, part11, part12, part13, part14, part15, part16) {
+      var parts = A._setArrayType([part1, part2, part3, part4, part5, part6, part7, part8, part9, part10, part11, part12, part13, part14, part15, part16], type$.JSArray_nullable_String);
+      A._validateArgList("join", parts);
+      return this.joinAll$1(new A.WhereTypeIterable(parts, type$.WhereTypeIterable_String));
+    },
+    join$2($receiver, part1, part2) {
+      return this.join$16($receiver, part1, part2, null, null, null, null, null, null, null, null, null, null, null, null, null, null);
+    },
+    joinAll$1(parts) {
+      var t1, t2, t3, needsSeparator, isAbsoluteAndNotRootRelative, t4, t5, parsed, path, t6;
+      type$.Iterable_String._as(parts);
+      for (t1 = parts.$ti, t2 = t1._eval$1("bool(Iterable.E)")._as(new A.Context_joinAll_closure()), t3 = parts.get$iterator(parts), t1 = new A.WhereIterator(t3, t2, t1._eval$1("WhereIterator<Iterable.E>")), t2 = this.style, needsSeparator = false, isAbsoluteAndNotRootRelative = false, t4 = ""; t1.moveNext$0();) {
+        t5 = t3.get$current(t3);
+        if (t2.isRootRelative$1(t5) && isAbsoluteAndNotRootRelative) {
+          parsed = A.ParsedPath_ParsedPath$parse(t5, t2);
+          path = t4.charCodeAt(0) == 0 ? t4 : t4;
+          t4 = B.JSString_methods.substring$2(path, 0, t2.rootLength$2$withDrive(path, true));
+          parsed.root = t4;
+          if (t2.needsSeparator$1(t4))
+            B.JSArray_methods.$indexSet(parsed.separators, 0, t2.get$separator());
+          t4 = "" + parsed.toString$0(0);
+        } else if (t2.rootLength$1(t5) > 0) {
+          isAbsoluteAndNotRootRelative = !t2.isRootRelative$1(t5);
+          t4 = "" + t5;
+        } else {
+          t6 = t5.length;
+          if (t6 !== 0) {
+            if (0 >= t6)
+              return A.ioore(t5, 0);
+            t6 = t2.containsSeparator$1(t5[0]);
+          } else
+            t6 = false;
+          if (!t6)
+            if (needsSeparator)
+              t4 += t2.get$separator();
+          t4 += t5;
+        }
+        needsSeparator = t2.needsSeparator$1(t5);
+      }
+      return t4.charCodeAt(0) == 0 ? t4 : t4;
+    },
+    split$1(_, path) {
+      var parsed = A.ParsedPath_ParsedPath$parse(path, this.style),
+        t1 = parsed.parts,
+        t2 = A._arrayInstanceType(t1),
+        t3 = t2._eval$1("WhereIterable<1>");
+      parsed.set$parts(A.List_List$of(new A.WhereIterable(t1, t2._eval$1("bool(1)")._as(new A.Context_split_closure()), t3), true, t3._eval$1("Iterable.E")));
+      t1 = parsed.root;
+      if (t1 != null)
+        B.JSArray_methods.insert$2(parsed.parts, 0, t1);
+      return parsed.parts;
+    },
+    normalize$1(_, path) {
+      var parsed;
+      if (!this._needsNormalization$1(path))
+        return path;
+      parsed = A.ParsedPath_ParsedPath$parse(path, this.style);
+      parsed.normalize$0(0);
+      return parsed.toString$0(0);
+    },
+    _needsNormalization$1(path) {
+      var t2, i, start, previous, t3, previousPrevious, codeUnit, t4,
+        t1 = this.style,
+        root = t1.rootLength$1(path);
+      if (root !== 0) {
+        if (t1 === $.$get$Style_windows())
+          for (t2 = path.length, i = 0; i < root; ++i) {
+            if (!(i < t2))
+              return A.ioore(path, i);
+            if (path.charCodeAt(i) === 47)
+              return true;
+          }
+        start = root;
+        previous = 47;
+      } else {
+        start = 0;
+        previous = null;
+      }
+      for (t2 = new A.CodeUnits(path)._string, t3 = t2.length, i = start, previousPrevious = null; i < t3; ++i, previousPrevious = previous, previous = codeUnit) {
+        if (!(i >= 0))
+          return A.ioore(t2, i);
+        codeUnit = t2.charCodeAt(i);
+        if (t1.isSeparator$1(codeUnit)) {
+          if (t1 === $.$get$Style_windows() && codeUnit === 47)
+            return true;
+          if (previous != null && t1.isSeparator$1(previous))
+            return true;
+          if (previous === 46)
+            t4 = previousPrevious == null || previousPrevious === 46 || t1.isSeparator$1(previousPrevious);
+          else
+            t4 = false;
+          if (t4)
+            return true;
+        }
+      }
+      if (previous == null)
+        return true;
+      if (t1.isSeparator$1(previous))
+        return true;
+      if (previous === 46)
+        t1 = previousPrevious == null || t1.isSeparator$1(previousPrevious) || previousPrevious === 46;
+      else
+        t1 = false;
+      if (t1)
+        return true;
+      return false;
+    },
+    relative$1(path) {
+      var from, fromParsed, pathParsed, t3, t4, t5, _this = this,
+        _s26_ = 'Unable to find a path to "',
+        t1 = _this.style,
+        t2 = t1.rootLength$1(path);
+      if (t2 <= 0)
+        return _this.normalize$1(0, path);
+      t2 = _this._context$_current;
+      from = t2 == null ? A.current() : t2;
+      if (t1.rootLength$1(from) <= 0 && t1.rootLength$1(path) > 0)
+        return _this.normalize$1(0, path);
+      if (t1.rootLength$1(path) <= 0 || t1.isRootRelative$1(path))
+        path = _this.absolute$1(0, path);
+      if (t1.rootLength$1(path) <= 0 && t1.rootLength$1(from) > 0)
+        throw A.wrapException(A.PathException$(_s26_ + path + '" from "' + from + '".'));
+      fromParsed = A.ParsedPath_ParsedPath$parse(from, t1);
+      fromParsed.normalize$0(0);
+      pathParsed = A.ParsedPath_ParsedPath$parse(path, t1);
+      pathParsed.normalize$0(0);
+      t2 = fromParsed.parts;
+      t3 = t2.length;
+      if (t3 !== 0) {
+        if (0 >= t3)
+          return A.ioore(t2, 0);
+        t2 = J.$eq$(t2[0], ".");
+      } else
+        t2 = false;
+      if (t2)
+        return pathParsed.toString$0(0);
+      t2 = fromParsed.root;
+      t3 = pathParsed.root;
+      if (t2 != t3)
+        t2 = t2 == null || t3 == null || !t1.pathsEqual$2(t2, t3);
+      else
+        t2 = false;
+      if (t2)
+        return pathParsed.toString$0(0);
+      while (true) {
+        t2 = fromParsed.parts;
+        t3 = t2.length;
+        if (t3 !== 0) {
+          t4 = pathParsed.parts;
+          t5 = t4.length;
+          if (t5 !== 0) {
+            if (0 >= t3)
+              return A.ioore(t2, 0);
+            t2 = t2[0];
+            if (0 >= t5)
+              return A.ioore(t4, 0);
+            t4 = t1.pathsEqual$2(t2, t4[0]);
+            t2 = t4;
+          } else
+            t2 = false;
+        } else
+          t2 = false;
+        if (!t2)
+          break;
+        B.JSArray_methods.removeAt$1(fromParsed.parts, 0);
+        B.JSArray_methods.removeAt$1(fromParsed.separators, 1);
+        B.JSArray_methods.removeAt$1(pathParsed.parts, 0);
+        B.JSArray_methods.removeAt$1(pathParsed.separators, 1);
+      }
+      t2 = fromParsed.parts;
+      t3 = t2.length;
+      if (t3 !== 0) {
+        if (0 >= t3)
+          return A.ioore(t2, 0);
+        t2 = J.$eq$(t2[0], "..");
+      } else
+        t2 = false;
+      if (t2)
+        throw A.wrapException(A.PathException$(_s26_ + path + '" from "' + from + '".'));
+      t2 = type$.String;
+      B.JSArray_methods.insertAll$2(pathParsed.parts, 0, A.List_List$filled(fromParsed.parts.length, "..", false, t2));
+      B.JSArray_methods.$indexSet(pathParsed.separators, 0, "");
+      B.JSArray_methods.insertAll$2(pathParsed.separators, 1, A.List_List$filled(fromParsed.parts.length, t1.get$separator(), false, t2));
+      t1 = pathParsed.parts;
+      t2 = t1.length;
+      if (t2 === 0)
+        return ".";
+      if (t2 > 1 && J.$eq$(B.JSArray_methods.get$last(t1), ".")) {
+        B.JSArray_methods.removeLast$0(pathParsed.parts);
+        t1 = pathParsed.separators;
+        if (0 >= t1.length)
+          return A.ioore(t1, -1);
+        t1.pop();
+        if (0 >= t1.length)
+          return A.ioore(t1, -1);
+        t1.pop();
+        B.JSArray_methods.add$1(t1, "");
+      }
+      pathParsed.root = "";
+      pathParsed.removeTrailingSeparators$0();
+      return pathParsed.toString$0(0);
+    },
+    toUri$1(path) {
+      var t2,
+        t1 = this.style;
+      if (t1.rootLength$1(path) <= 0)
+        return t1.relativePathToUri$1(path);
+      else {
+        t2 = this._context$_current;
+        return t1.absolutePathToUri$1(this.join$2(0, t2 == null ? A.current() : t2, path));
+      }
+    },
+    prettyUri$1(uri) {
+      var path, rel, _this = this,
+        typedUri = A._parseUri(uri);
+      if (typedUri.get$scheme() === "file" && _this.style === $.$get$Style_url())
+        return typedUri.toString$0(0);
+      else if (typedUri.get$scheme() !== "file" && typedUri.get$scheme() !== "" && _this.style !== $.$get$Style_url())
+        return typedUri.toString$0(0);
+      path = _this.normalize$1(0, _this.style.pathFromUri$1(A._parseUri(typedUri)));
+      rel = _this.relative$1(path);
+      return _this.split$1(0, rel).length > _this.split$1(0, path).length ? path : rel;
+    }
+  };
+  A.Context_joinAll_closure.prototype = {
+    call$1(part) {
+      return A._asString(part) !== "";
+    },
+    $signature: 1
+  };
+  A.Context_split_closure.prototype = {
+    call$1(part) {
+      return A._asString(part).length !== 0;
+    },
+    $signature: 1
+  };
+  A._validateArgList_closure.prototype = {
+    call$1(arg) {
+      A._asStringQ(arg);
+      return arg == null ? "null" : '"' + arg + '"';
+    },
+    $signature: 31
+  };
+  A.InternalStyle.prototype = {
+    getRoot$1(path) {
+      var t1,
+        $length = this.rootLength$1(path);
+      if ($length > 0)
+        return B.JSString_methods.substring$2(path, 0, $length);
+      if (this.isRootRelative$1(path)) {
+        if (0 >= path.length)
+          return A.ioore(path, 0);
+        t1 = path[0];
+      } else
+        t1 = null;
+      return t1;
+    },
+    relativePathToUri$1(path) {
+      var segments, t2, _null = null,
+        t1 = path.length;
+      if (t1 === 0)
+        return A._Uri__Uri(_null, _null, _null, _null);
+      segments = A.Context_Context(this).split$1(0, path);
+      t2 = t1 - 1;
+      if (!(t2 >= 0))
+        return A.ioore(path, t2);
+      if (this.isSeparator$1(path.charCodeAt(t2)))
+        B.JSArray_methods.add$1(segments, "");
+      return A._Uri__Uri(_null, _null, segments, _null);
+    },
+    pathsEqual$2(path1, path2) {
+      return path1 === path2;
+    }
+  };
+  A.ParsedPath.prototype = {
+    get$hasTrailingSeparator() {
+      var t1 = this.parts;
+      if (t1.length !== 0)
+        t1 = J.$eq$(B.JSArray_methods.get$last(t1), "") || !J.$eq$(B.JSArray_methods.get$last(this.separators), "");
+      else
+        t1 = false;
+      return t1;
+    },
+    removeTrailingSeparators$0() {
+      var t1, t2, _this = this;
+      while (true) {
+        t1 = _this.parts;
+        if (!(t1.length !== 0 && J.$eq$(B.JSArray_methods.get$last(t1), "")))
+          break;
+        B.JSArray_methods.removeLast$0(_this.parts);
+        t1 = _this.separators;
+        if (0 >= t1.length)
+          return A.ioore(t1, -1);
+        t1.pop();
+      }
+      t1 = _this.separators;
+      t2 = t1.length;
+      if (t2 !== 0)
+        B.JSArray_methods.$indexSet(t1, t2 - 1, "");
+    },
+    normalize$0(_) {
+      var t1, t2, leadingDoubles, _i, part, t3, _this = this,
+        newParts = A._setArrayType([], type$.JSArray_String);
+      for (t1 = _this.parts, t2 = t1.length, leadingDoubles = 0, _i = 0; _i < t1.length; t1.length === t2 || (0, A.throwConcurrentModificationError)(t1), ++_i) {
+        part = t1[_i];
+        t3 = J.getInterceptor$(part);
+        if (!(t3.$eq(part, ".") || t3.$eq(part, "")))
+          if (t3.$eq(part, "..")) {
+            t3 = newParts.length;
+            if (t3 !== 0) {
+              if (0 >= t3)
+                return A.ioore(newParts, -1);
+              newParts.pop();
+            } else
+              ++leadingDoubles;
+          } else
+            B.JSArray_methods.add$1(newParts, part);
+      }
+      if (_this.root == null)
+        B.JSArray_methods.insertAll$2(newParts, 0, A.List_List$filled(leadingDoubles, "..", false, type$.String));
+      if (newParts.length === 0 && _this.root == null)
+        B.JSArray_methods.add$1(newParts, ".");
+      _this.set$parts(newParts);
+      t1 = _this.style;
+      _this.set$separators(A.List_List$filled(newParts.length + 1, t1.get$separator(), true, type$.String));
+      t2 = _this.root;
+      if (t2 == null || newParts.length === 0 || !t1.needsSeparator$1(t2))
+        B.JSArray_methods.$indexSet(_this.separators, 0, "");
+      t2 = _this.root;
+      if (t2 != null && t1 === $.$get$Style_windows()) {
+        t2.toString;
+        _this.root = A.stringReplaceAllUnchecked(t2, "/", "\\");
+      }
+      _this.removeTrailingSeparators$0();
+    },
+    toString$0(_) {
+      var i, t2, t3, _this = this,
+        t1 = _this.root;
+      t1 = t1 != null ? "" + t1 : "";
+      for (i = 0; i < _this.parts.length; ++i, t1 = t3) {
+        t2 = _this.separators;
+        if (!(i < t2.length))
+          return A.ioore(t2, i);
+        t2 = A.S(t2[i]);
+        t3 = _this.parts;
+        if (!(i < t3.length))
+          return A.ioore(t3, i);
+        t3 = t1 + t2 + A.S(t3[i]);
+      }
+      t1 += A.S(B.JSArray_methods.get$last(_this.separators));
+      return t1.charCodeAt(0) == 0 ? t1 : t1;
+    },
+    set$parts(parts) {
+      this.parts = type$.List_String._as(parts);
+    },
+    set$separators(separators) {
+      this.separators = type$.List_String._as(separators);
+    }
+  };
+  A.PathException.prototype = {
+    toString$0(_) {
+      return "PathException: " + this.message;
+    },
+    $isException: 1
+  };
+  A.Style.prototype = {
+    toString$0(_) {
+      return this.get$name(this);
+    }
+  };
+  A.PosixStyle.prototype = {
+    containsSeparator$1(path) {
+      return B.JSString_methods.contains$1(path, "/");
+    },
+    isSeparator$1(codeUnit) {
+      return codeUnit === 47;
+    },
+    needsSeparator$1(path) {
+      var t2,
+        t1 = path.length;
+      if (t1 !== 0) {
+        t2 = t1 - 1;
+        if (!(t2 >= 0))
+          return A.ioore(path, t2);
+        t2 = path.charCodeAt(t2) !== 47;
+        t1 = t2;
+      } else
+        t1 = false;
+      return t1;
+    },
+    rootLength$2$withDrive(path, withDrive) {
+      var t1 = path.length;
+      if (t1 !== 0) {
+        if (0 >= t1)
+          return A.ioore(path, 0);
+        t1 = path.charCodeAt(0) === 47;
+      } else
+        t1 = false;
+      if (t1)
+        return 1;
+      return 0;
+    },
+    rootLength$1(path) {
+      return this.rootLength$2$withDrive(path, false);
+    },
+    isRootRelative$1(path) {
+      return false;
+    },
+    pathFromUri$1(uri) {
+      var t1;
+      if (uri.get$scheme() === "" || uri.get$scheme() === "file") {
+        t1 = uri.get$path(uri);
+        return A._Uri__uriDecode(t1, 0, t1.length, B.C_Utf8Codec, false);
+      }
+      throw A.wrapException(A.ArgumentError$("Uri " + uri.toString$0(0) + " must have scheme 'file:'.", null));
+    },
+    absolutePathToUri$1(path) {
+      var parsed = A.ParsedPath_ParsedPath$parse(path, this),
+        t1 = parsed.parts;
+      if (t1.length === 0)
+        B.JSArray_methods.addAll$1(t1, A._setArrayType(["", ""], type$.JSArray_String));
+      else if (parsed.get$hasTrailingSeparator())
+        B.JSArray_methods.add$1(parsed.parts, "");
+      return A._Uri__Uri(null, null, parsed.parts, "file");
+    },
+    get$name() {
+      return "posix";
+    },
+    get$separator() {
+      return "/";
+    }
+  };
+  A.UrlStyle.prototype = {
+    containsSeparator$1(path) {
+      return B.JSString_methods.contains$1(path, "/");
+    },
+    isSeparator$1(codeUnit) {
+      return codeUnit === 47;
+    },
+    needsSeparator$1(path) {
+      var t2,
+        t1 = path.length;
+      if (t1 === 0)
+        return false;
+      t2 = t1 - 1;
+      if (!(t2 >= 0))
+        return A.ioore(path, t2);
+      if (path.charCodeAt(t2) !== 47)
+        return true;
+      return B.JSString_methods.endsWith$1(path, "://") && this.rootLength$1(path) === t1;
+    },
+    rootLength$2$withDrive(path, withDrive) {
+      var i, codeUnit, index, t2,
+        t1 = path.length;
+      if (t1 === 0)
+        return 0;
+      if (0 >= t1)
+        return A.ioore(path, 0);
+      if (path.charCodeAt(0) === 47)
+        return 1;
+      for (i = 0; i < t1; ++i) {
+        codeUnit = path.charCodeAt(i);
+        if (codeUnit === 47)
+          return 0;
+        if (codeUnit === 58) {
+          if (i === 0)
+            return 0;
+          index = B.JSString_methods.indexOf$2(path, "/", B.JSString_methods.startsWith$2(path, "//", i + 1) ? i + 3 : i);
+          if (index <= 0)
+            return t1;
+          if (!withDrive || t1 < index + 3)
+            return index;
+          if (!B.JSString_methods.startsWith$1(path, "file://"))
+            return index;
+          if (!A.isDriveLetter(path, index + 1))
+            return index;
+          t2 = index + 3;
+          return t1 === t2 ? t2 : index + 4;
+        }
+      }
+      return 0;
+    },
+    rootLength$1(path) {
+      return this.rootLength$2$withDrive(path, false);
+    },
+    isRootRelative$1(path) {
+      var t1 = path.length;
+      if (t1 !== 0) {
+        if (0 >= t1)
+          return A.ioore(path, 0);
+        t1 = path.charCodeAt(0) === 47;
+      } else
+        t1 = false;
+      return t1;
+    },
+    pathFromUri$1(uri) {
+      return uri.toString$0(0);
+    },
+    relativePathToUri$1(path) {
+      return A.Uri_parse(path);
+    },
+    absolutePathToUri$1(path) {
+      return A.Uri_parse(path);
+    },
+    get$name() {
+      return "url";
+    },
+    get$separator() {
+      return "/";
+    }
+  };
+  A.WindowsStyle.prototype = {
+    containsSeparator$1(path) {
+      return B.JSString_methods.contains$1(path, "/");
+    },
+    isSeparator$1(codeUnit) {
+      return codeUnit === 47 || codeUnit === 92;
+    },
+    needsSeparator$1(path) {
+      var t2,
+        t1 = path.length;
+      if (t1 === 0)
+        return false;
+      t2 = t1 - 1;
+      if (!(t2 >= 0))
+        return A.ioore(path, t2);
+      t2 = path.charCodeAt(t2);
+      return !(t2 === 47 || t2 === 92);
+    },
+    rootLength$2$withDrive(path, withDrive) {
+      var t2, index,
+        t1 = path.length;
+      if (t1 === 0)
+        return 0;
+      if (0 >= t1)
+        return A.ioore(path, 0);
+      if (path.charCodeAt(0) === 47)
+        return 1;
+      if (path.charCodeAt(0) === 92) {
+        if (t1 >= 2) {
+          if (1 >= t1)
+            return A.ioore(path, 1);
+          t2 = path.charCodeAt(1) !== 92;
+        } else
+          t2 = true;
+        if (t2)
+          return 1;
+        index = B.JSString_methods.indexOf$2(path, "\\", 2);
+        if (index > 0) {
+          index = B.JSString_methods.indexOf$2(path, "\\", index + 1);
+          if (index > 0)
+            return index;
+        }
+        return t1;
+      }
+      if (t1 < 3)
+        return 0;
+      if (!A.isAlphabetic(path.charCodeAt(0)))
+        return 0;
+      if (path.charCodeAt(1) !== 58)
+        return 0;
+      t1 = path.charCodeAt(2);
+      if (!(t1 === 47 || t1 === 92))
+        return 0;
+      return 3;
+    },
+    rootLength$1(path) {
+      return this.rootLength$2$withDrive(path, false);
+    },
+    isRootRelative$1(path) {
+      return this.rootLength$1(path) === 1;
+    },
+    pathFromUri$1(uri) {
+      var path, t1;
+      if (uri.get$scheme() !== "" && uri.get$scheme() !== "file")
+        throw A.wrapException(A.ArgumentError$("Uri " + uri.toString$0(0) + " must have scheme 'file:'.", null));
+      path = uri.get$path(uri);
+      if (uri.get$host(uri) === "") {
+        if (path.length >= 3 && B.JSString_methods.startsWith$1(path, "/") && A.isDriveLetter(path, 1))
+          path = B.JSString_methods.replaceFirst$2(path, "/", "");
+      } else
+        path = "\\\\" + uri.get$host(uri) + path;
+      t1 = A.stringReplaceAllUnchecked(path, "/", "\\");
+      return A._Uri__uriDecode(t1, 0, t1.length, B.C_Utf8Codec, false);
+    },
+    absolutePathToUri$1(path) {
+      var rootParts, t2,
+        parsed = A.ParsedPath_ParsedPath$parse(path, this),
+        t1 = parsed.root;
+      t1.toString;
+      if (B.JSString_methods.startsWith$1(t1, "\\\\")) {
+        rootParts = new A.WhereIterable(A._setArrayType(t1.split("\\"), type$.JSArray_String), type$.bool_Function_String._as(new A.WindowsStyle_absolutePathToUri_closure()), type$.WhereIterable_String);
+        B.JSArray_methods.insert$2(parsed.parts, 0, rootParts.get$last(rootParts));
+        if (parsed.get$hasTrailingSeparator())
+          B.JSArray_methods.add$1(parsed.parts, "");
+        return A._Uri__Uri(rootParts.get$first(rootParts), null, parsed.parts, "file");
+      } else {
+        if (parsed.parts.length === 0 || parsed.get$hasTrailingSeparator())
+          B.JSArray_methods.add$1(parsed.parts, "");
+        t1 = parsed.parts;
+        t2 = parsed.root;
+        t2.toString;
+        t2 = A.stringReplaceAllUnchecked(t2, "/", "");
+        B.JSArray_methods.insert$2(t1, 0, A.stringReplaceAllUnchecked(t2, "\\", ""));
+        return A._Uri__Uri(null, null, parsed.parts, "file");
+      }
+    },
+    codeUnitsEqual$2(codeUnit1, codeUnit2) {
+      var upperCase1;
+      if (codeUnit1 === codeUnit2)
+        return true;
+      if (codeUnit1 === 47)
+        return codeUnit2 === 92;
+      if (codeUnit1 === 92)
+        return codeUnit2 === 47;
+      if ((codeUnit1 ^ codeUnit2) !== 32)
+        return false;
+      upperCase1 = codeUnit1 | 32;
+      return upperCase1 >= 97 && upperCase1 <= 122;
+    },
+    pathsEqual$2(path1, path2) {
+      var t1, t2, i;
+      if (path1 === path2)
+        return true;
+      t1 = path1.length;
+      t2 = path2.length;
+      if (t1 !== t2)
+        return false;
+      for (i = 0; i < t1; ++i) {
+        if (!(i < t2))
+          return A.ioore(path2, i);
+        if (!this.codeUnitsEqual$2(path1.charCodeAt(i), path2.charCodeAt(i)))
+          return false;
+      }
+      return true;
+    },
+    get$name() {
+      return "windows";
+    },
+    get$separator() {
+      return "\\";
+    }
+  };
+  A.WindowsStyle_absolutePathToUri_closure.prototype = {
+    call$1(part) {
+      return A._asString(part) !== "";
+    },
+    $signature: 1
+  };
+  A.Chain.prototype = {
+    toTrace$0() {
+      var t1 = this.traces,
+        t2 = A._arrayInstanceType(t1);
+      return A.Trace$(new A.ExpandIterable(t1, t2._eval$1("Iterable<Frame>(1)")._as(new A.Chain_toTrace_closure()), t2._eval$1("ExpandIterable<1,Frame>")), null);
+    },
+    toString$0(_) {
+      var t1 = this.traces,
+        t2 = A._arrayInstanceType(t1);
+      return new A.MappedListIterable(t1, t2._eval$1("String(1)")._as(new A.Chain_toString_closure(new A.MappedListIterable(t1, t2._eval$1("int(1)")._as(new A.Chain_toString_closure0()), t2._eval$1("MappedListIterable<1,int>")).fold$1$2(0, 0, B.CONSTANT, type$.int))), t2._eval$1("MappedListIterable<1,String>")).join$1(0, string$.x3d_____);
+    },
+    $isStackTrace: 1
+  };
+  A.Chain_Chain$parse_closure.prototype = {
+    call$1(line) {
+      return A._asString(line).length !== 0;
+    },
+    $signature: 1
+  };
+  A.Chain_toTrace_closure.prototype = {
+    call$1(trace) {
+      return type$.Trace._as(trace).get$frames();
+    },
+    $signature: 32
+  };
+  A.Chain_toString_closure0.prototype = {
+    call$1(trace) {
+      var t1 = type$.Trace._as(trace).get$frames(),
+        t2 = A._arrayInstanceType(t1);
+      return new A.MappedListIterable(t1, t2._eval$1("int(1)")._as(new A.Chain_toString__closure0()), t2._eval$1("MappedListIterable<1,int>")).fold$1$2(0, 0, B.CONSTANT, type$.int);
+    },
+    $signature: 33
+  };
+  A.Chain_toString__closure0.prototype = {
+    call$1(frame) {
+      type$.Frame._as(frame);
+      return frame.get$location(frame).length;
+    },
+    $signature: 20
+  };
+  A.Chain_toString_closure.prototype = {
+    call$1(trace) {
+      var t1 = type$.Trace._as(trace).get$frames(),
+        t2 = A._arrayInstanceType(t1);
+      return new A.MappedListIterable(t1, t2._eval$1("String(1)")._as(new A.Chain_toString__closure(this.longest)), t2._eval$1("MappedListIterable<1,String>")).join$0(0);
+    },
+    $signature: 35
+  };
+  A.Chain_toString__closure.prototype = {
+    call$1(frame) {
+      type$.Frame._as(frame);
+      return B.JSString_methods.padRight$1(frame.get$location(frame), this.longest) + "  " + A.S(frame.get$member()) + "\n";
+    },
+    $signature: 21
+  };
+  A.Frame.prototype = {
+    get$isCore() {
+      return this.uri.get$scheme() === "dart";
+    },
+    get$library() {
+      var t1 = this.uri;
+      if (t1.get$scheme() === "data")
+        return "data:...";
+      return $.$get$context().prettyUri$1(t1);
+    },
+    get$$package() {
+      var t1 = this.uri;
+      if (t1.get$scheme() !== "package")
+        return null;
+      return B.JSArray_methods.get$first(t1.get$path(t1).split("/"));
+    },
+    get$location(_) {
+      var t2, _this = this,
+        t1 = _this.line;
+      if (t1 == null)
+        return _this.get$library();
+      t2 = _this.column;
+      if (t2 == null)
+        return _this.get$library() + " " + A.S(t1);
+      return _this.get$library() + " " + A.S(t1) + ":" + A.S(t2);
+    },
+    toString$0(_) {
+      return this.get$location(this) + " in " + A.S(this.member);
+    },
+    get$uri() {
+      return this.uri;
+    },
+    get$line(receiver) {
+      return this.line;
+    },
+    get$column() {
+      return this.column;
+    },
+    get$member() {
+      return this.member;
+    }
+  };
+  A.Frame_Frame$parseVM_closure.prototype = {
+    call$0() {
+      var match, t2, t3, member, uri, lineAndColumn, line, _null = null,
+        t1 = this.frame;
+      if (t1 === "...")
+        return new A.Frame(A._Uri__Uri(_null, _null, _null, _null), _null, _null, "...");
+      match = $.$get$_vmFrame().firstMatch$1(t1);
+      if (match == null)
+        return new A.UnparsedFrame(A._Uri__Uri(_null, "unparsed", _null, _null), t1);
+      t1 = match._match;
+      if (1 >= t1.length)
+        return A.ioore(t1, 1);
+      t2 = t1[1];
+      t2.toString;
+      t3 = $.$get$_asyncBody();
+      t2 = A.stringReplaceAllUnchecked(t2, t3, "<async>");
+      member = A.stringReplaceAllUnchecked(t2, "<anonymous closure>", "<fn>");
+      if (2 >= t1.length)
+        return A.ioore(t1, 2);
+      t2 = t1[2];
+      t3 = t2;
+      t3.toString;
+      if (B.JSString_methods.startsWith$1(t3, "<data:"))
+        uri = A.Uri_Uri$dataFromString("");
+      else {
+        t2 = t2;
+        t2.toString;
+        uri = A.Uri_parse(t2);
+      }
+      if (3 >= t1.length)
+        return A.ioore(t1, 3);
+      lineAndColumn = t1[3].split(":");
+      t1 = lineAndColumn.length;
+      line = t1 > 1 ? A.int_parse(lineAndColumn[1], _null) : _null;
+      return new A.Frame(uri, line, t1 > 2 ? A.int_parse(lineAndColumn[2], _null) : _null, member);
+    },
+    $signature: 6
+  };
+  A.Frame_Frame$parseV8_closure.prototype = {
+    call$0() {
+      var t2, t3, t4, _s4_ = "<fn>",
+        t1 = this.frame,
+        match = $.$get$_v8Frame().firstMatch$1(t1);
+      if (match == null)
+        return new A.UnparsedFrame(A._Uri__Uri(null, "unparsed", null, null), t1);
+      t1 = new A.Frame_Frame$parseV8_closure_parseLocation(t1);
+      t2 = match._match;
+      t3 = t2.length;
+      if (2 >= t3)
+        return A.ioore(t2, 2);
+      t4 = t2[2];
+      if (t4 != null) {
+        t3 = t4;
+        t3.toString;
+        t2 = t2[1];
+        t2.toString;
+        t2 = A.stringReplaceAllUnchecked(t2, "<anonymous>", _s4_);
+        t2 = A.stringReplaceAllUnchecked(t2, "Anonymous function", _s4_);
+        return t1.call$2(t3, A.stringReplaceAllUnchecked(t2, "(anonymous function)", _s4_));
+      } else {
+        if (3 >= t3)
+          return A.ioore(t2, 3);
+        t2 = t2[3];
+        t2.toString;
+        return t1.call$2(t2, _s4_);
+      }
+    },
+    $signature: 6
+  };
+  A.Frame_Frame$parseV8_closure_parseLocation.prototype = {
+    call$2($location, member) {
+      var t2, urlMatch, uri, line, columnMatch, _null = null,
+        t1 = $.$get$_v8EvalLocation(),
+        evalMatch = t1.firstMatch$1($location);
+      for (; evalMatch != null; $location = t2) {
+        t2 = evalMatch._match;
+        if (1 >= t2.length)
+          return A.ioore(t2, 1);
+        t2 = t2[1];
+        t2.toString;
+        evalMatch = t1.firstMatch$1(t2);
+      }
+      if ($location === "native")
+        return new A.Frame(A.Uri_parse("native"), _null, _null, member);
+      urlMatch = $.$get$_v8UrlLocation().firstMatch$1($location);
+      if (urlMatch == null)
+        return new A.UnparsedFrame(A._Uri__Uri(_null, "unparsed", _null, _null), this.frame);
+      t1 = urlMatch._match;
+      if (1 >= t1.length)
+        return A.ioore(t1, 1);
+      t2 = t1[1];
+      t2.toString;
+      uri = A.Frame__uriOrPathToUri(t2);
+      if (2 >= t1.length)
+        return A.ioore(t1, 2);
+      t2 = t1[2];
+      t2.toString;
+      line = A.int_parse(t2, _null);
+      if (3 >= t1.length)
+        return A.ioore(t1, 3);
+      columnMatch = t1[3];
+      return new A.Frame(uri, line, columnMatch != null ? A.int_parse(columnMatch, _null) : _null, member);
+    },
+    $signature: 38
+  };
+  A.Frame_Frame$_parseFirefoxEval_closure.prototype = {
+    call$0() {
+      var t2, member, uri, line, _null = null,
+        t1 = this.frame,
+        match = $.$get$_firefoxEvalLocation().firstMatch$1(t1);
+      if (match == null)
+        return new A.UnparsedFrame(A._Uri__Uri(_null, "unparsed", _null, _null), t1);
+      t1 = match._match;
+      if (1 >= t1.length)
+        return A.ioore(t1, 1);
+      t2 = t1[1];
+      t2.toString;
+      member = A.stringReplaceAllUnchecked(t2, "/<", "");
+      if (2 >= t1.length)
+        return A.ioore(t1, 2);
+      t2 = t1[2];
+      t2.toString;
+      uri = A.Frame__uriOrPathToUri(t2);
+      if (3 >= t1.length)
+        return A.ioore(t1, 3);
+      t1 = t1[3];
+      t1.toString;
+      line = A.int_parse(t1, _null);
+      return new A.Frame(uri, line, _null, member.length === 0 || member === "anonymous" ? "<fn>" : member);
+    },
+    $signature: 6
+  };
+  A.Frame_Frame$parseFirefox_closure.prototype = {
+    call$0() {
+      var t2, t3, t4, uri, member, line, column, _null = null,
+        t1 = this.frame,
+        match = $.$get$_firefoxSafariFrame().firstMatch$1(t1);
+      if (match == null)
+        return new A.UnparsedFrame(A._Uri__Uri(_null, "unparsed", _null, _null), t1);
+      t2 = match._match;
+      if (3 >= t2.length)
+        return A.ioore(t2, 3);
+      t3 = t2[3];
+      t4 = t3;
+      t4.toString;
+      if (B.JSString_methods.contains$1(t4, " line "))
+        return A.Frame_Frame$_parseFirefoxEval(t1);
+      t1 = t3;
+      t1.toString;
+      uri = A.Frame__uriOrPathToUri(t1);
+      t1 = t2.length;
+      if (1 >= t1)
+        return A.ioore(t2, 1);
+      member = t2[1];
+      if (member != null) {
+        if (2 >= t1)
+          return A.ioore(t2, 2);
+        t1 = t2[2];
+        t1.toString;
+        t1 = B.JSString_methods.allMatches$1("/", t1);
+        member += B.JSArray_methods.join$0(A.List_List$filled(t1.get$length(t1), ".<fn>", false, type$.String));
+        if (member === "")
+          member = "<fn>";
+        member = B.JSString_methods.replaceFirst$2(member, $.$get$_initialDot(), "");
+      } else
+        member = "<fn>";
+      if (4 >= t2.length)
+        return A.ioore(t2, 4);
+      t1 = t2[4];
+      if (t1 === "")
+        line = _null;
+      else {
+        t1 = t1;
+        t1.toString;
+        line = A.int_parse(t1, _null);
+      }
+      if (5 >= t2.length)
+        return A.ioore(t2, 5);
+      t1 = t2[5];
+      if (t1 == null || t1 === "")
+        column = _null;
+      else {
+        t1 = t1;
+        t1.toString;
+        column = A.int_parse(t1, _null);
+      }
+      return new A.Frame(uri, line, column, member);
+    },
+    $signature: 6
+  };
+  A.Frame_Frame$parseFriendly_closure.prototype = {
+    call$0() {
+      var t2, uri, line, column, _null = null,
+        t1 = this.frame,
+        match = $.$get$_friendlyFrame().firstMatch$1(t1);
+      if (match == null)
+        throw A.wrapException(A.FormatException$("Couldn't parse package:stack_trace stack trace line '" + t1 + "'.", _null, _null));
+      t1 = match._match;
+      if (1 >= t1.length)
+        return A.ioore(t1, 1);
+      t2 = t1[1];
+      if (t2 === "data:...")
+        uri = A.Uri_Uri$dataFromString("");
+      else {
+        t2 = t2;
+        t2.toString;
+        uri = A.Uri_parse(t2);
+      }
+      if (uri.get$scheme() === "") {
+        t2 = $.$get$context();
+        uri = t2.toUri$1(t2.absolute$15(0, t2.style.pathFromUri$1(A._parseUri(uri)), _null, _null, _null, _null, _null, _null, _null, _null, _null, _null, _null, _null, _null, _null));
+      }
+      if (2 >= t1.length)
+        return A.ioore(t1, 2);
+      t2 = t1[2];
+      if (t2 == null)
+        line = _null;
+      else {
+        t2 = t2;
+        t2.toString;
+        line = A.int_parse(t2, _null);
+      }
+      if (3 >= t1.length)
+        return A.ioore(t1, 3);
+      t2 = t1[3];
+      if (t2 == null)
+        column = _null;
+      else {
+        t2 = t2;
+        t2.toString;
+        column = A.int_parse(t2, _null);
+      }
+      if (4 >= t1.length)
+        return A.ioore(t1, 4);
+      return new A.Frame(uri, line, column, t1[4]);
+    },
+    $signature: 6
+  };
+  A.LazyTrace.prototype = {
+    get$_lazy_trace$_trace() {
+      var result, _this = this,
+        value = _this.__LazyTrace__trace_FI;
+      if (value === $) {
+        result = _this._thunk.call$0();
+        _this.__LazyTrace__trace_FI !== $ && A.throwLateFieldADI("_trace");
+        _this.__LazyTrace__trace_FI = result;
+        value = result;
+      }
+      return value;
+    },
+    get$frames() {
+      return this.get$_lazy_trace$_trace().get$frames();
+    },
+    get$terse() {
+      return new A.LazyTrace(new A.LazyTrace_terse_closure(this));
+    },
+    toString$0(_) {
+      return this.get$_lazy_trace$_trace().toString$0(0);
+    },
+    $isStackTrace: 1,
+    $isTrace: 1
+  };
+  A.LazyTrace_terse_closure.prototype = {
+    call$0() {
+      return this.$this.get$_lazy_trace$_trace().get$terse();
+    },
+    $signature: 22
+  };
+  A.Trace.prototype = {
+    get$terse() {
+      return this.foldFrames$2$terse(new A.Trace_terse_closure(), true);
+    },
+    foldFrames$2$terse(predicate, terse) {
+      var newFrames, t1, t2, t3, _box_0 = {};
+      _box_0.predicate = predicate;
+      type$.bool_Function_Frame._as(predicate);
+      _box_0.predicate = predicate;
+      _box_0.predicate = new A.Trace_foldFrames_closure(predicate);
+      newFrames = A._setArrayType([], type$.JSArray_Frame);
+      for (t1 = this.frames, t2 = A._arrayInstanceType(t1)._eval$1("ReversedListIterable<1>"), t1 = new A.ReversedListIterable(t1, t2), t1 = new A.ListIterator(t1, t1.get$length(t1), t2._eval$1("ListIterator<ListIterable.E>")), t2 = t2._eval$1("ListIterable.E"); t1.moveNext$0();) {
+        t3 = t1.__internal$_current;
+        if (t3 == null)
+          t3 = t2._as(t3);
+        if (t3 instanceof A.UnparsedFrame || !A.boolConversionCheck(_box_0.predicate.call$1(t3)))
+          B.JSArray_methods.add$1(newFrames, t3);
+        else if (newFrames.length === 0 || !A.boolConversionCheck(_box_0.predicate.call$1(B.JSArray_methods.get$last(newFrames))))
+          B.JSArray_methods.add$1(newFrames, new A.Frame(t3.get$uri(), t3.get$line(t3), t3.get$column(), t3.get$member()));
+      }
+      t1 = type$.MappedListIterable_Frame_Frame;
+      newFrames = A.List_List$of(new A.MappedListIterable(newFrames, type$.Frame_Function_Frame._as(new A.Trace_foldFrames_closure0(_box_0)), t1), true, t1._eval$1("ListIterable.E"));
+      if (newFrames.length > 1 && A.boolConversionCheck(_box_0.predicate.call$1(B.JSArray_methods.get$first(newFrames))))
+        B.JSArray_methods.removeAt$1(newFrames, 0);
+      return A.Trace$(new A.ReversedListIterable(newFrames, A._arrayInstanceType(newFrames)._eval$1("ReversedListIterable<1>")), this.original._stackTrace);
+    },
+    toString$0(_) {
+      var t1 = this.frames,
+        t2 = A._arrayInstanceType(t1);
+      return new A.MappedListIterable(t1, t2._eval$1("String(1)")._as(new A.Trace_toString_closure(new A.MappedListIterable(t1, t2._eval$1("int(1)")._as(new A.Trace_toString_closure0()), t2._eval$1("MappedListIterable<1,int>")).fold$1$2(0, 0, B.CONSTANT, type$.int))), t2._eval$1("MappedListIterable<1,String>")).join$0(0);
+    },
+    $isStackTrace: 1,
+    get$frames() {
+      return this.frames;
+    }
+  };
+  A.Trace_Trace$from_closure.prototype = {
+    call$0() {
+      return A.Trace_Trace$parse(this.trace.toString$0(0));
+    },
+    $signature: 22
+  };
+  A.Trace__parseVM_closure.prototype = {
+    call$1(line) {
+      return A._asString(line).length !== 0;
+    },
+    $signature: 1
+  };
+  A.Trace$parseV8_closure.prototype = {
+    call$1(line) {
+      return !B.JSString_methods.startsWith$1(A._asString(line), $.$get$_v8TraceLine());
+    },
+    $signature: 1
+  };
+  A.Trace$parseJSCore_closure.prototype = {
+    call$1(line) {
+      return A._asString(line) !== "\tat ";
+    },
+    $signature: 1
+  };
+  A.Trace$parseFirefox_closure.prototype = {
+    call$1(line) {
+      A._asString(line);
+      return line.length !== 0 && line !== "[native code]";
+    },
+    $signature: 1
+  };
+  A.Trace$parseFriendly_closure.prototype = {
+    call$1(line) {
+      return !B.JSString_methods.startsWith$1(A._asString(line), "=====");
+    },
+    $signature: 1
+  };
+  A.Trace_terse_closure.prototype = {
+    call$1(_) {
+      return false;
+    },
+    $signature: 23
+  };
+  A.Trace_foldFrames_closure.prototype = {
+    call$1(frame) {
+      var t1;
+      if (A.boolConversionCheck(this.oldPredicate.call$1(frame)))
+        return true;
+      if (frame.get$isCore())
+        return true;
+      if (frame.get$$package() === "stack_trace")
+        return true;
+      t1 = frame.get$member();
+      t1.toString;
+      if (!B.JSString_methods.contains$1(t1, "<async>"))
+        return false;
+      return frame.get$line(frame) == null;
+    },
+    $signature: 23
+  };
+  A.Trace_foldFrames_closure0.prototype = {
+    call$1(frame) {
+      var t1, t2;
+      type$.Frame._as(frame);
+      if (frame instanceof A.UnparsedFrame || !A.boolConversionCheck(this._box_0.predicate.call$1(frame)))
+        return frame;
+      t1 = frame.get$library();
+      t2 = $.$get$_terseRegExp();
+      return new A.Frame(A.Uri_parse(A.stringReplaceAllUnchecked(t1, t2, "")), null, null, frame.get$member());
+    },
+    $signature: 62
+  };
+  A.Trace_toString_closure0.prototype = {
+    call$1(frame) {
+      type$.Frame._as(frame);
+      return frame.get$location(frame).length;
+    },
+    $signature: 20
+  };
+  A.Trace_toString_closure.prototype = {
+    call$1(frame) {
+      type$.Frame._as(frame);
+      if (frame instanceof A.UnparsedFrame)
+        return frame.toString$0(0) + "\n";
+      return B.JSString_methods.padRight$1(frame.get$location(frame), this.longest) + "  " + A.S(frame.get$member()) + "\n";
+    },
+    $signature: 21
+  };
+  A.UnparsedFrame.prototype = {
+    toString$0(_) {
+      return this.member;
+    },
+    $isFrame: 1,
+    get$uri() {
+      return this.uri;
+    },
+    get$line() {
+      return null;
+    },
+    get$column() {
+      return null;
+    },
+    get$isCore() {
+      return false;
+    },
+    get$library() {
+      return "unparsed";
+    },
+    get$$package() {
+      return null;
+    },
+    get$location() {
+      return "unparsed";
+    },
+    get$member() {
+      return this.member;
+    }
+  };
+  A.GuaranteeChannel.prototype = {
+    GuaranteeChannel$3$allowSinkErrors(innerSink, allowSinkErrors, _box_0, $T) {
+      var _this = this,
+        t1 = _this.$ti,
+        t2 = t1._eval$1("_GuaranteeSink<1>")._as(new A._GuaranteeSink(innerSink, _this, new A._AsyncCompleter(new A._Future($.Zone__current, type$._Future_dynamic), type$._AsyncCompleter_dynamic), true, $T._eval$1("_GuaranteeSink<0>")));
+      _this.__GuaranteeChannel__sink_F !== $ && A.throwLateFieldAI("_sink");
+      _this.set$__GuaranteeChannel__sink_F(t2);
+      t1 = t1._eval$1("StreamController<1>")._as(A.StreamController_StreamController(null, new A.GuaranteeChannel_closure(_box_0, _this, $T), true, $T));
+      _this.__GuaranteeChannel__streamController_F !== $ && A.throwLateFieldAI("_streamController");
+      _this.set$__GuaranteeChannel__streamController_F(t1);
+    },
+    _onSinkDisconnected$0() {
+      var subscription, t1;
+      this._disconnected = true;
+      subscription = this._subscription;
+      if (subscription != null)
+        subscription.cancel$0(0);
+      t1 = this.__GuaranteeChannel__streamController_F;
+      t1 === $ && A.throwLateFieldNI("_streamController");
+      t1.close$0(0);
+    },
+    set$__GuaranteeChannel__sink_F(__GuaranteeChannel__sink_F) {
+      this.__GuaranteeChannel__sink_F = this.$ti._eval$1("_GuaranteeSink<1>")._as(__GuaranteeChannel__sink_F);
+    },
+    set$__GuaranteeChannel__streamController_F(__GuaranteeChannel__streamController_F) {
+      this.__GuaranteeChannel__streamController_F = this.$ti._eval$1("StreamController<1>")._as(__GuaranteeChannel__streamController_F);
+    },
+    set$_subscription(_subscription) {
+      this._subscription = this.$ti._eval$1("StreamSubscription<1>?")._as(_subscription);
+    }
+  };
+  A.GuaranteeChannel_closure.prototype = {
+    call$0() {
+      var t2, t3,
+        t1 = this.$this;
+      if (t1._disconnected)
+        return;
+      t2 = this._box_0.innerStream;
+      t3 = t1.__GuaranteeChannel__streamController_F;
+      t3 === $ && A.throwLateFieldNI("_streamController");
+      t1.set$_subscription(t2.listen$3$onDone$onError(this.T._eval$1("~(0)")._as(t3.get$add(t3)), new A.GuaranteeChannel__closure(t1), t3.get$addError()));
+    },
+    $signature: 0
+  };
+  A.GuaranteeChannel__closure.prototype = {
+    call$0() {
+      var t1 = this.$this,
+        t2 = t1.__GuaranteeChannel__sink_F;
+      t2 === $ && A.throwLateFieldNI("_sink");
+      t2._onStreamDisconnected$0();
+      t1 = t1.__GuaranteeChannel__streamController_F;
+      t1 === $ && A.throwLateFieldNI("_streamController");
+      t1.close$0(0);
+    },
+    $signature: 0
+  };
+  A._GuaranteeSink.prototype = {
+    add$1(_, data) {
+      var t1, _this = this;
+      _this.$ti._precomputed1._as(data);
+      if (_this._closed)
+        throw A.wrapException(A.StateError$("Cannot add event after closing."));
+      if (_this._addStreamSubscription != null)
+        throw A.wrapException(A.StateError$("Cannot add event while adding stream."));
+      if (_this._disconnected)
+        return;
+      t1 = _this._inner;
+      t1._target.add$1(0, t1.$ti._precomputed1._as(data));
+    },
+    addError$2(error, stackTrace) {
+      var _this = this;
+      type$.Object._as(error);
+      type$.nullable_StackTrace._as(stackTrace);
+      if (_this._closed)
+        throw A.wrapException(A.StateError$("Cannot add event after closing."));
+      if (_this._addStreamSubscription != null)
+        throw A.wrapException(A.StateError$("Cannot add event while adding stream."));
+      if (_this._disconnected)
+        return;
+      _this._addError$2(error, stackTrace);
+    },
+    addError$1(error) {
+      return this.addError$2(error, null);
+    },
+    _addError$2(error, stackTrace) {
+      this._inner._target.addError$2(type$.Object._as(error), type$.nullable_StackTrace._as(stackTrace));
+      return;
+    },
+    _addError$1(error) {
+      return this._addError$2(error, null);
+    },
+    addStream$1(_, stream) {
+      var t2, t3, _this = this,
+        t1 = _this.$ti;
+      t1._eval$1("Stream<1>")._as(stream);
+      if (_this._closed)
+        throw A.wrapException(A.StateError$("Cannot add stream after closing."));
+      if (_this._addStreamSubscription != null)
+        throw A.wrapException(A.StateError$("Cannot add stream while adding stream."));
+      if (_this._disconnected)
+        return A.Future_Future$value(null, type$.void);
+      t2 = _this._addStreamCompleter = new A._SyncCompleter(new A._Future($.Zone__current, type$._Future_dynamic), type$._SyncCompleter_dynamic);
+      t3 = _this._inner;
+      _this.set$_addStreamSubscription(stream.listen$3$onDone$onError(t1._eval$1("~(1)")._as(t3.get$add(t3)), type$.void_Function_$opt_dynamic._as(t2.get$complete(t2)), _this.get$_addError()));
+      return _this._addStreamCompleter.future.then$1$1(new A._GuaranteeSink_addStream_closure(_this), type$.void);
+    },
+    close$0(_) {
+      var _this = this;
+      if (_this._addStreamSubscription != null)
+        throw A.wrapException(A.StateError$("Cannot close sink while adding stream."));
+      if (_this._closed)
+        return _this._doneCompleter.future;
+      _this._closed = true;
+      if (!_this._disconnected) {
+        _this._channel._onSinkDisconnected$0();
+        _this._doneCompleter.complete$1(0, _this._inner._target.close$0(0));
+      }
+      return _this._doneCompleter.future;
+    },
+    _onStreamDisconnected$0() {
+      var t1, t2, _this = this;
+      _this._disconnected = true;
+      t1 = _this._doneCompleter;
+      if ((t1.future._async$_state & 30) === 0)
+        t1.complete$0(0);
+      t1 = _this._addStreamSubscription;
+      if (t1 == null)
+        return;
+      t2 = _this._addStreamCompleter;
+      t2.toString;
+      t2.complete$1(0, t1.cancel$0(0));
+      _this._addStreamCompleter = null;
+      _this.set$_addStreamSubscription(null);
+    },
+    set$_addStreamSubscription(_addStreamSubscription) {
+      this._addStreamSubscription = this.$ti._eval$1("StreamSubscription<1>?")._as(_addStreamSubscription);
+    },
+    $isStreamConsumer: 1,
+    $isStreamSink: 1
+  };
+  A._GuaranteeSink_addStream_closure.prototype = {
+    call$1(_) {
+      var t1 = this.$this;
+      t1._addStreamCompleter = null;
+      t1.set$_addStreamSubscription(null);
+    },
+    $signature: 10
+  };
+  A._MultiChannel.prototype = {
+    _MultiChannel$1(inner, $T) {
+      var t2, t3, _this = this,
+        _s17_ = "_streamController",
+        t1 = _this._mainController;
+      _this._controllers.$indexSet(0, 0, t1);
+      t1 = t1.__StreamChannelController__local_F;
+      t1 === $ && A.throwLateFieldNI("_local");
+      t2 = t1.__GuaranteeChannel__streamController_F;
+      t2 === $ && A.throwLateFieldNI(_s17_);
+      new A._ControllerStream(t2, A._instanceType(t2)._eval$1("_ControllerStream<1>")).listen$2$onDone(new A._MultiChannel_closure(_this, $T), new A._MultiChannel_closure0(_this));
+      t2 = _this._multi_channel$_inner.__GuaranteeChannel__streamController_F;
+      t2 === $ && A.throwLateFieldNI(_s17_);
+      t3 = A._instanceType(t2)._eval$1("_ControllerStream<1>");
+      t1 = t1.__GuaranteeChannel__sink_F;
+      t1 === $ && A.throwLateFieldNI("_sink");
+      _this._innerStreamSubscription = new A.CastStream(new A._ControllerStream(t2, t3), t3._eval$1("CastStream<Stream.T,List<@>>")).listen$3$onDone$onError(new A._MultiChannel_closure1(_this, $T), _this.get$_closeInnerChannel(), t1.get$addError());
+    },
+    virtualChannel$1(id) {
+      var t2, controller, _this = this,
+        _s17_ = "_streamController",
+        _s8_ = "_foreign",
+        t1 = {};
+      t1.outputId = t1.inputId = null;
+      t1.inputId = id;
+      t1.outputId = id + 1;
+      if (_this._multi_channel$_inner == null) {
+        t1 = _this.$ti;
+        t2 = A.Future_Future$value(null, type$.dynamic);
+        return new A.VirtualChannel(_this, new A._EmptyStream(t1._eval$1("_EmptyStream<1>")), new A.NullStreamSink(t2, t1._eval$1("NullStreamSink<1>")), t1._eval$1("VirtualChannel<1>"));
+      }
+      controller = A._Cell$named("controller");
+      if (_this._pendingIds.remove$1(0, id)) {
+        t2 = _this._controllers.$index(0, id);
+        t2.toString;
+        controller._value = t2;
+      } else {
+        t2 = _this._controllers;
+        if (t2.containsKey$1(0, id) || _this._closedIds.contains$1(0, id))
+          throw A.wrapException(A.ArgumentError$("A virtual channel with id " + id + " already exists.", null));
+        else {
+          controller._value = A.StreamChannelController$(true, _this.$ti._precomputed1);
+          t2.$indexSet(0, id, controller._readLocal$0());
+        }
+      }
+      t2 = controller._readLocal$0().__StreamChannelController__local_F;
+      t2 === $ && A.throwLateFieldNI("_local");
+      t2 = t2.__GuaranteeChannel__streamController_F;
+      t2 === $ && A.throwLateFieldNI(_s17_);
+      new A._ControllerStream(t2, A._instanceType(t2)._eval$1("_ControllerStream<1>")).listen$2$onDone(new A._MultiChannel_virtualChannel_closure(t1, _this), new A._MultiChannel_virtualChannel_closure0(t1, _this));
+      t1 = controller._readLocal$0().__StreamChannelController__foreign_F;
+      t1 === $ && A.throwLateFieldNI(_s8_);
+      t1 = t1.__GuaranteeChannel__streamController_F;
+      t1 === $ && A.throwLateFieldNI(_s17_);
+      t2 = controller._readLocal$0().__StreamChannelController__foreign_F;
+      t2 === $ && A.throwLateFieldNI(_s8_);
+      t2 = t2.__GuaranteeChannel__sink_F;
+      t2 === $ && A.throwLateFieldNI("_sink");
+      return new A.VirtualChannel(_this, new A._ControllerStream(t1, A._instanceType(t1)._eval$1("_ControllerStream<1>")), t2, _this.$ti._eval$1("VirtualChannel<1>"));
+    },
+    _closeChannel$2(inputId, outputId) {
+      var t1, t2, _this = this;
+      _this._closedIds.add$1(0, inputId);
+      t1 = _this._controllers;
+      t2 = t1.remove$1(0, inputId);
+      t2.toString;
+      t2 = t2.__StreamChannelController__local_F;
+      t2 === $ && A.throwLateFieldNI("_local");
+      t2 = t2.__GuaranteeChannel__sink_F;
+      t2 === $ && A.throwLateFieldNI("_sink");
+      t2.close$0(0);
+      t2 = _this._multi_channel$_inner;
+      if (t2 == null)
+        return;
+      t2 = t2.__GuaranteeChannel__sink_F;
+      t2 === $ && A.throwLateFieldNI("_sink");
+      t2.add$1(0, A._setArrayType([outputId], type$.JSArray_int));
+      if (t1._length === 0)
+        _this._closeInnerChannel$0();
+    },
+    _closeInnerChannel$0() {
+      var t2, t3, _i, t4, _this = this,
+        t1 = _this._multi_channel$_inner.__GuaranteeChannel__sink_F;
+      t1 === $ && A.throwLateFieldNI("_sink");
+      t1.close$0(0);
+      _this._innerStreamSubscription._source.cancel$0(0);
+      _this._multi_channel$_inner = null;
+      for (t1 = _this._controllers, t2 = t1.get$values(t1), t2 = A.List_List$of(t2, false, A._instanceType(t2)._eval$1("Iterable.E")), t3 = t2.length, _i = 0; _i < t3; ++_i) {
+        t4 = t2[_i].__StreamChannelController__local_F;
+        t4 === $ && A.throwLateFieldNI("_local");
+        t4 = t4.__GuaranteeChannel__sink_F;
+        t4 === $ && A.throwLateFieldNI("_sink");
+        t4.close$0(0);
+      }
+      t1.clear$0(0);
+    },
+    $isMultiChannel: 1
+  };
+  A._MultiChannel_closure.prototype = {
+    call$1(message) {
+      var t1;
+      this.T._as(message);
+      t1 = this.$this._multi_channel$_inner.__GuaranteeChannel__sink_F;
+      t1 === $ && A.throwLateFieldNI("_sink");
+      return t1.add$1(0, [0, message]);
+    },
+    $signature() {
+      return this.T._eval$1("~(0)");
+    }
+  };
+  A._MultiChannel_closure0.prototype = {
+    call$0() {
+      return this.$this._closeChannel$2(0, 0);
+    },
+    $signature: 0
+  };
+  A._MultiChannel_closure1.prototype = {
+    call$1(message) {
+      var t1, id, t2, t3, controller, t4;
+      type$.List_dynamic._as(message);
+      t1 = J.getInterceptor$asx(message);
+      id = B.JSNumber_methods.toInt$0(A._asNum(t1.$index(message, 0)));
+      t2 = this.$this;
+      if (t2._closedIds.contains$1(0, id))
+        return;
+      t3 = this.T;
+      controller = t2._controllers.putIfAbsent$2(0, id, new A._MultiChannel__closure(t2, id, t3));
+      t2 = t1.get$length(message);
+      t4 = controller.__StreamChannelController__local_F;
+      if (t2 > 1) {
+        t4 === $ && A.throwLateFieldNI("_local");
+        t2 = t4.__GuaranteeChannel__sink_F;
+        t2 === $ && A.throwLateFieldNI("_sink");
+        t2.add$1(0, t3._as(t1.$index(message, 1)));
+      } else {
+        t4 === $ && A.throwLateFieldNI("_local");
+        t1 = t4.__GuaranteeChannel__sink_F;
+        t1 === $ && A.throwLateFieldNI("_sink");
+        t1.close$0(0);
+      }
+    },
+    $signature: 42
+  };
+  A._MultiChannel__closure.prototype = {
+    call$0() {
+      this.$this._pendingIds.add$1(0, this.id);
+      return A.StreamChannelController$(true, this.T);
+    },
+    $signature() {
+      return this.T._eval$1("StreamChannelController<0>()");
+    }
+  };
+  A._MultiChannel_virtualChannel_closure.prototype = {
+    call$1(message) {
+      var t1 = this.$this;
+      t1.$ti._precomputed1._as(message);
+      t1 = t1._multi_channel$_inner.__GuaranteeChannel__sink_F;
+      t1 === $ && A.throwLateFieldNI("_sink");
+      return t1.add$1(0, [this._box_0.outputId, message]);
+    },
+    $signature() {
+      return this.$this.$ti._eval$1("~(1)");
+    }
+  };
+  A._MultiChannel_virtualChannel_closure0.prototype = {
+    call$0() {
+      var t1 = this._box_0;
+      return this.$this._closeChannel$2(t1.inputId, t1.outputId);
+    },
+    $signature: 0
+  };
+  A.VirtualChannel.prototype = {$isMultiChannel: 1};
+  A.StreamChannelController.prototype = {
+    set$__StreamChannelController__local_F(__StreamChannelController__local_F) {
+      this.__StreamChannelController__local_F = this.$ti._eval$1("StreamChannel<1>")._as(__StreamChannelController__local_F);
+    },
+    set$__StreamChannelController__foreign_F(__StreamChannelController__foreign_F) {
+      this.__StreamChannelController__foreign_F = this.$ti._eval$1("StreamChannel<1>")._as(__StreamChannelController__foreign_F);
+    }
+  };
+  A.StreamChannelMixin.prototype = {$isStreamChannel: 1};
+  A.MessagePortExtension_get_postMessage_closure.prototype = {
+    call$1(message) {
+      var t2,
+        t1 = A._setArrayType([], type$.JSArray_Object);
+      if (message != null) {
+        t2 = A.jsify(message);
+        t1.push(t2 == null ? type$.Object._as(t2) : t2);
+      }
+      return A.callMethod(this._this, "postMessage", t1, type$.void);
+    },
+    $signature: 7
+  };
+  A.Subscription.prototype = {};
+  A.main_closure.prototype = {
+    call$0() {
+      var play,
+        serverChannel = A._connectToServer(),
+        t1 = serverChannel._mainController.__StreamChannelController__foreign_F;
+      t1 === $ && A.throwLateFieldNI("_foreign");
+      t1 = t1.__GuaranteeChannel__streamController_F;
+      t1 === $ && A.throwLateFieldNI("_streamController");
+      new A._ControllerStream(t1, A._instanceType(t1)._eval$1("_ControllerStream<1>")).listen$1(new A.main__closure(serverChannel));
+      A.Timer_Timer$periodic(new A.Duration(1000000), new A.main__closure0(serverChannel));
+      play = type$.nullable_JavaScriptObject._as(self.document.querySelector("#play"));
+      play.toString;
+      A.EventTargetExtension_addEventListener(play, "click", A.allowInterop(new A.main__closure1(serverChannel), type$.void_Function_JavaScriptObject));
+      t1 = type$.void_Function;
+      self.dartTest = type$.JavaScriptObject._as({resume: A.allowInterop(new A.main__closure2(serverChannel), t1), restartCurrent: A.allowInterop(new A.main__closure3(serverChannel), t1)});
+    },
+    $signature: 3
+  };
+  A.main__closure.prototype = {
+    call$1(message) {
+      var _0_4, _0_3, _0_9, _0_12, _0_15, _0_14, t1, t2, channel, url, id, suiteChannel, _null = null;
+      $label0$0: {
+        _0_4 = A._InitializedCell$named("#0#4", new A.main___closure(message));
+        _0_3 = A._InitializedCell$named("#0#3", new A.main___closure0(message));
+        _0_9 = A._InitializedCell$named("#0#9", new A.main___closure1(message));
+        _0_12 = A._InitializedCell$named("#0#12", new A.main___closure2(message));
+        _0_15 = A._InitializedCell$named("#0#15", new A.main___closure3(message));
+        _0_14 = A._InitializedCell$named("#0#14", new A.main___closure4(message));
+        t1 = type$.Map_dynamic_dynamic._is(message);
+        if (t1) {
+          if (_0_4._readFinal$0() == null)
+            t2 = A.boolConversionCheck(_0_3._readFinal$0());
+          else
+            t2 = true;
+          if (t2)
+            if ("loadSuite" === _0_4._readFinal$0()) {
+              if (_0_9._readFinal$0() == null)
+                t2 = J.containsKey$1$x(message, "channel");
+              else
+                t2 = true;
+              if (t2)
+                if (typeof _0_9._readFinal$0() == "number") {
+                  channel = _0_9._readFinal$0();
+                  if (_0_12._readFinal$0() == null)
+                    t2 = J.containsKey$1$x(message, "url");
+                  else
+                    t2 = true;
+                  if (t2)
+                    if (typeof _0_12._readFinal$0() == "string") {
+                      url = _0_12._readFinal$0();
+                      if (_0_15._readFinal$0() == null)
+                        t2 = A.boolConversionCheck(_0_14._readFinal$0());
+                      else
+                        t2 = true;
+                      if (t2)
+                        if (typeof _0_15._readFinal$0() == "number") {
+                          id = _0_15._readFinal$0();
+                          t2 = true;
+                        } else {
+                          id = _null;
+                          t2 = false;
+                        }
+                      else {
+                        id = _null;
+                        t2 = false;
+                      }
+                    } else {
+                      id = _null;
+                      url = id;
+                      t2 = false;
+                    }
+                  else {
+                    id = _null;
+                    url = id;
+                    t2 = false;
+                  }
+                } else {
+                  id = _null;
+                  url = id;
+                  channel = url;
+                  t2 = false;
+                }
+              else {
+                id = _null;
+                url = id;
+                channel = url;
+                t2 = false;
+              }
+            } else {
+              id = _null;
+              url = id;
+              channel = url;
+              t2 = false;
+            }
+          else {
+            id = _null;
+            url = id;
+            channel = url;
+            t2 = false;
+          }
+        } else {
+          id = _null;
+          url = id;
+          channel = url;
+          t2 = false;
+        }
+        if (t2) {
+          suiteChannel = this.serverChannel.virtualChannel$1(J.toInt$0$n(channel));
+          t1 = suiteChannel.$ti._eval$1("StreamChannel<1>")._as(A._connectToIframe(url, J.toInt$0$n(id)));
+          t2 = t1.__GuaranteeChannel__sink_F;
+          t2 === $ && A.throwLateFieldNI("_sink");
+          suiteChannel.stream.pipe$1(t2);
+          t1 = t1.__GuaranteeChannel__streamController_F;
+          t1 === $ && A.throwLateFieldNI("_streamController");
+          new A._ControllerStream(t1, A._instanceType(t1)._eval$1("_ControllerStream<1>")).pipe$1(suiteChannel.sink);
+          break $label0$0;
+        }
+        if (t1) {
+          if (_0_4._readFinal$0() == null)
+            t2 = A.boolConversionCheck(_0_3._readFinal$0());
+          else
+            t2 = true;
+          t2 = t2 && "displayPause" === _0_4._readFinal$0();
+        } else
+          t2 = false;
+        if (t2) {
+          type$.JavaScriptObject._as(type$.nullable_JavaScriptObject._as(self.document.body).classList).add("paused");
+          break $label0$0;
+        }
+        if (t1) {
+          if (_0_4._readFinal$0() == null)
+            t2 = A.boolConversionCheck(_0_3._readFinal$0());
+          else
+            t2 = true;
+          t2 = t2 && "resume" === _0_4._readFinal$0();
+        } else
+          t2 = false;
+        if (t2) {
+          type$.JavaScriptObject._as(type$.nullable_JavaScriptObject._as(self.document.body).classList).remove("paused");
+          break $label0$0;
+        }
+        if (t1) {
+          if (_0_4._readFinal$0() == null)
+            t1 = A.boolConversionCheck(_0_3._readFinal$0());
+          else
+            t1 = true;
+          if (t1)
+            if ("closeSuite" === _0_4._readFinal$0()) {
+              if (_0_15._readFinal$0() == null)
+                t1 = A.boolConversionCheck(_0_14._readFinal$0());
+              else
+                t1 = true;
+              if (t1) {
+                id = _0_15._readFinal$0();
+                t1 = true;
+              } else {
+                id = _null;
+                t1 = false;
+              }
+            } else {
+              id = _null;
+              t1 = false;
+            }
+          else {
+            id = _null;
+            t1 = false;
+          }
+        } else {
+          id = _null;
+          t1 = false;
+        }
+        if (t1) {
+          t1 = $._iframes.remove$1(0, id);
+          t2 = type$.nullable_JavaScriptObject;
+          if (t2._as(t1.parentNode) != null)
+            type$.JavaScriptObject._as(t2._as(t1.parentNode).removeChild(t1));
+          t1 = $._subscriptions.remove$1(0, id);
+          if (t1 != null)
+            J.cancel$0$z(t1);
+          t1 = $._domSubscriptions.remove$1(0, id);
+          if (t1 != null)
+            A.EventTargetExtension_removeEventListener(t1.target, t1.type, t1.listener);
+          break $label0$0;
+        }
+        type$.JavaScriptObject._as(self.window.console).warn("Unhandled message from test runner: " + A.S(message));
+      }
+    },
+    $signature: 5
+  };
+  A.main___closure.prototype = {
+    call$0() {
+      return J.$index$asx(type$.Map_dynamic_dynamic._as(this._0_0), "command");
+    },
+    $signature: 2
+  };
+  A.main___closure0.prototype = {
+    call$0() {
+      return J.containsKey$1$x(type$.Map_dynamic_dynamic._as(this._0_0), "command");
+    },
+    $signature: 24
+  };
+  A.main___closure1.prototype = {
+    call$0() {
+      return J.$index$asx(type$.Map_dynamic_dynamic._as(this._0_0), "channel");
+    },
+    $signature: 2
+  };
+  A.main___closure2.prototype = {
+    call$0() {
+      return J.$index$asx(type$.Map_dynamic_dynamic._as(this._0_0), "url");
+    },
+    $signature: 2
+  };
+  A.main___closure3.prototype = {
+    call$0() {
+      return J.$index$asx(type$.Map_dynamic_dynamic._as(this._0_0), "id");
+    },
+    $signature: 2
+  };
+  A.main___closure4.prototype = {
+    call$0() {
+      return J.containsKey$1$x(type$.Map_dynamic_dynamic._as(this._0_0), "id");
+    },
+    $signature: 24
+  };
+  A.main__closure0.prototype = {
+    call$1(_) {
+      var t1, t2;
+      type$.Timer._as(_);
+      t1 = this.serverChannel._mainController.__StreamChannelController__foreign_F;
+      t1 === $ && A.throwLateFieldNI("_foreign");
+      t1 = t1.__GuaranteeChannel__sink_F;
+      t1 === $ && A.throwLateFieldNI("_sink");
+      t2 = type$.String;
+      return t1.add$1(0, A.LinkedHashMap_LinkedHashMap$_literal(["command", "ping"], t2, t2));
+    },
+    $signature: 44
+  };
+  A.main__closure1.prototype = {
+    call$1(_) {
+      var t2,
+        t1 = type$.JavaScriptObject;
+      t1._as(_);
+      t2 = type$.nullable_JavaScriptObject;
+      if (!A._asBool(t1._as(t2._as(self.document.body).classList).contains("paused")))
+        return;
+      t1._as(t2._as(self.document.body).classList).remove("paused");
+      t1 = this.serverChannel._mainController.__StreamChannelController__foreign_F;
+      t1 === $ && A.throwLateFieldNI("_foreign");
+      t1 = t1.__GuaranteeChannel__sink_F;
+      t1 === $ && A.throwLateFieldNI("_sink");
+      t2 = type$.String;
+      t1.add$1(0, A.LinkedHashMap_LinkedHashMap$_literal(["command", "resume"], t2, t2));
+    },
+    $signature: 8
+  };
+  A.main__closure2.prototype = {
+    call$0() {
+      var t1 = type$.nullable_JavaScriptObject,
+        t2 = type$.JavaScriptObject;
+      if (!A._asBool(t2._as(t1._as(self.document.body).classList).contains("paused")))
+        return;
+      t2._as(t1._as(self.document.body).classList).remove("paused");
+      t1 = this.serverChannel._mainController.__StreamChannelController__foreign_F;
+      t1 === $ && A.throwLateFieldNI("_foreign");
+      t1 = t1.__GuaranteeChannel__sink_F;
+      t1 === $ && A.throwLateFieldNI("_sink");
+      t2 = type$.String;
+      t1.add$1(0, A.LinkedHashMap_LinkedHashMap$_literal(["command", "resume"], t2, t2));
+    },
+    $signature: 0
+  };
+  A.main__closure3.prototype = {
+    call$0() {
+      var t2,
+        t1 = this.serverChannel._mainController.__StreamChannelController__foreign_F;
+      t1 === $ && A.throwLateFieldNI("_foreign");
+      t1 = t1.__GuaranteeChannel__sink_F;
+      t1 === $ && A.throwLateFieldNI("_sink");
+      t2 = type$.String;
+      t1.add$1(0, A.LinkedHashMap_LinkedHashMap$_literal(["command", "restart"], t2, t2));
+    },
+    $signature: 0
+  };
+  A.main_closure0.prototype = {
+    call$2(error, stackTrace) {
+      type$.Object._as(error);
+      type$.StackTrace._as(stackTrace);
+      type$.JavaScriptObject._as(self.window.console).warn(A.S(error) + "\n" + A.Trace_Trace$from(stackTrace).get$terse().toString$0(0));
+    },
+    $signature: 11
+  };
+  A._connectToServer_closure.prototype = {
+    call$1(message) {
+      var t1;
+      type$.JavaScriptObject._as(message);
+      t1 = this.controller.__StreamChannelController__local_F;
+      t1 === $ && A.throwLateFieldNI("_local");
+      t1 = t1.__GuaranteeChannel__sink_F;
+      t1 === $ && A.throwLateFieldNI("_sink");
+      t1.add$1(0, B.C_JsonCodec.decode$2$reviver(0, A._asString(A.dartify(message.data)), null));
+    },
+    $signature: 8
+  };
+  A._connectToServer_closure0.prototype = {
+    call$1(message) {
+      return this.webSocket.send(B.C_JsonCodec.encode$2$toEncodable(message, null));
+    },
+    $signature: 5
+  };
+  A._connectToIframe_closure.prototype = {
+    call$1($event) {
+      var t2, _0_0, _0_6, _0_11, port, data, _this = this, _null = null,
+        t1 = type$.JavaScriptObject;
+      t1._as($event);
+      if (A._asString($event.origin) !== A._asString(t1._as(self.window.location).origin))
+        return;
+      t2 = $event.source.location;
+      t2 = t2 == null ? _null : A._asStringQ(t2.href);
+      if (t2 != A._asStringQ(_this.iframe.src))
+        return;
+      $event.stopPropagation();
+      t2 = _this.windowSubscription._readLocal$0();
+      A.EventTargetExtension_removeEventListener(t2.target, t2.type, t2.listener);
+      $label0$0: {
+        _0_0 = A.dartify($event.data);
+        _0_6 = A._InitializedCell$named("#0#6", new A._connectToIframe__closure(_0_0));
+        _0_11 = A._InitializedCell$named("#0#11", new A._connectToIframe__closure0(_0_0));
+        if ("port" === _0_0) {
+          t1._as(self.window.console).log("Connecting channel for suite " + _this.suiteUrl.toString$0(0));
+          t1 = J.cast$1$0$ax(type$.List_dynamic._as($event.ports), t1);
+          port = t1.get$first(t1);
+          t1 = _this.id;
+          t2 = _this.controller;
+          $._domSubscriptions.$indexSet(0, t1, A.Subscription$(port, "message", A.allowInterop(new A._connectToIframe__closure1(t2), type$.void_Function_JavaScriptObject)));
+          port.start();
+          t2 = t2.__StreamChannelController__local_F;
+          t2 === $ && A.throwLateFieldNI("_local");
+          t2 = t2.__GuaranteeChannel__streamController_F;
+          t2 === $ && A.throwLateFieldNI("_streamController");
+          $._subscriptions.$indexSet(0, t1, new A._ControllerStream(t2, A._instanceType(t2)._eval$1("_ControllerStream<1>")).listen$1(A.MessagePortExtension_get_postMessage(port)));
+          break $label0$0;
+        }
+        if (type$.Map_dynamic_dynamic._is(_0_0)) {
+          if (_0_6._readFinal$0() == null)
+            t1 = J.containsKey$1$x(_0_0, "exception");
+          else
+            t1 = true;
+          if (t1)
+            if (true === _0_6._readFinal$0()) {
+              if (_0_11._readFinal$0() == null)
+                t1 = J.containsKey$1$x(_0_0, "data");
+              else
+                t1 = true;
+              if (t1) {
+                data = _0_11._readFinal$0();
+                t1 = true;
+              } else {
+                data = _null;
+                t1 = false;
+              }
+            } else {
+              data = _null;
+              t1 = false;
+            }
+          else {
+            data = _null;
+            t1 = false;
+          }
+        } else {
+          data = _null;
+          t1 = false;
+        }
+        if (t1) {
+          t1 = _this.controller.__StreamChannelController__local_F;
+          t1 === $ && A.throwLateFieldNI("_local");
+          t1 = t1.__GuaranteeChannel__sink_F;
+          t1 === $ && A.throwLateFieldNI("_sink");
+          t1.add$1(0, data);
+        }
+      }
+    },
+    $signature: 8
+  };
+  A._connectToIframe__closure1.prototype = {
+    call$1($event) {
+      var t1;
+      type$.JavaScriptObject._as($event);
+      t1 = this.controller.__StreamChannelController__local_F;
+      t1 === $ && A.throwLateFieldNI("_local");
+      t1 = t1.__GuaranteeChannel__sink_F;
+      t1 === $ && A.throwLateFieldNI("_sink");
+      t1.add$1(0, A.dartify($event.data));
+    },
+    $signature: 8
+  };
+  A._connectToIframe__closure.prototype = {
+    call$0() {
+      return J.$index$asx(type$.Map_dynamic_dynamic._as(this._0_0), "exception");
+    },
+    $signature: 2
+  };
+  A._connectToIframe__closure0.prototype = {
+    call$0() {
+      return J.$index$asx(type$.Map_dynamic_dynamic._as(this._0_0), "data");
+    },
+    $signature: 2
+  };
+  (function aliases() {
+    var _ = J.Interceptor.prototype;
+    _.super$Interceptor$toString = _.toString$0;
+    _ = J.LegacyJavaScriptObject.prototype;
+    _.super$LegacyJavaScriptObject$toString = _.toString$0;
+    _ = A.Iterable.prototype;
+    _.super$Iterable$skipWhile = _.skipWhile$1;
+  })();
+  (function installTearOffs() {
+    var _instance_1_u = hunkHelpers._instance_1u,
+      _static_1 = hunkHelpers._static_1,
+      _static_0 = hunkHelpers._static_0,
+      _static_2 = hunkHelpers._static_2,
+      _static = hunkHelpers.installStaticTearOff,
+      _instance = hunkHelpers.installInstanceTearOff,
+      _instance_2_u = hunkHelpers._instance_2u,
+      _instance_1_i = hunkHelpers._instance_1i,
+      _instance_0_u = hunkHelpers._instance_0u;
+    _instance_1_u(A.CastStreamSubscription.prototype, "get$__internal$_onData", "__internal$_onData$1", 7);
+    _static_1(A, "async__AsyncRun__scheduleImmediateJsOverride$closure", "_AsyncRun__scheduleImmediateJsOverride", 13);
+    _static_1(A, "async__AsyncRun__scheduleImmediateWithSetImmediate$closure", "_AsyncRun__scheduleImmediateWithSetImmediate", 13);
+    _static_1(A, "async__AsyncRun__scheduleImmediateWithTimer$closure", "_AsyncRun__scheduleImmediateWithTimer", 13);
+    _static_0(A, "async___startMicrotaskLoop$closure", "_startMicrotaskLoop", 0);
+    _static_1(A, "async___nullDataHandler$closure", "_nullDataHandler", 5);
+    _static_2(A, "async___nullErrorHandler$closure", "_nullErrorHandler", 11);
+    _static_0(A, "async___nullDoneHandler$closure", "_nullDoneHandler", 0);
+    _static(A, "async___rootHandleUncaughtError$closure", 5, null, ["call$5"], ["_rootHandleUncaughtError"], 47, 0);
+    _static(A, "async___rootRun$closure", 4, null, ["call$1$4", "call$4"], ["_rootRun", function($self, $parent, zone, f) {
+      return A._rootRun($self, $parent, zone, f, type$.dynamic);
+    }], 48, 1);
+    _static(A, "async___rootRunUnary$closure", 5, null, ["call$2$5", "call$5"], ["_rootRunUnary", function($self, $parent, zone, f, arg) {
+      return A._rootRunUnary($self, $parent, zone, f, arg, type$.dynamic, type$.dynamic);
+    }], 49, 1);
+    _static(A, "async___rootRunBinary$closure", 6, null, ["call$3$6", "call$6"], ["_rootRunBinary", function($self, $parent, zone, f, arg1, arg2) {
+      return A._rootRunBinary($self, $parent, zone, f, arg1, arg2, type$.dynamic, type$.dynamic, type$.dynamic);
+    }], 50, 1);
+    _static(A, "async___rootRegisterCallback$closure", 4, null, ["call$1$4", "call$4"], ["_rootRegisterCallback", function($self, $parent, zone, f) {
+      return A._rootRegisterCallback($self, $parent, zone, f, type$.dynamic);
+    }], 51, 0);
+    _static(A, "async___rootRegisterUnaryCallback$closure", 4, null, ["call$2$4", "call$4"], ["_rootRegisterUnaryCallback", function($self, $parent, zone, f) {
+      return A._rootRegisterUnaryCallback($self, $parent, zone, f, type$.dynamic, type$.dynamic);
+    }], 52, 0);
+    _static(A, "async___rootRegisterBinaryCallback$closure", 4, null, ["call$3$4", "call$4"], ["_rootRegisterBinaryCallback", function($self, $parent, zone, f) {
+      return A._rootRegisterBinaryCallback($self, $parent, zone, f, type$.dynamic, type$.dynamic, type$.dynamic);
+    }], 53, 0);
+    _static(A, "async___rootErrorCallback$closure", 5, null, ["call$5"], ["_rootErrorCallback"], 54, 0);
+    _static(A, "async___rootScheduleMicrotask$closure", 4, null, ["call$4"], ["_rootScheduleMicrotask"], 55, 0);
+    _static(A, "async___rootCreateTimer$closure", 5, null, ["call$5"], ["_rootCreateTimer"], 56, 0);
+    _static(A, "async___rootCreatePeriodicTimer$closure", 5, null, ["call$5"], ["_rootCreatePeriodicTimer"], 57, 0);
+    _static(A, "async___rootPrint$closure", 4, null, ["call$4"], ["_rootPrint"], 58, 0);
+    _static(A, "async___rootFork$closure", 5, null, ["call$5"], ["_rootFork"], 59, 0);
+    _instance(A._SyncCompleter.prototype, "get$complete", 1, 0, function() {
+      return [null];
+    }, ["call$1", "call$0"], ["complete$1", "complete$0"], 43, 0, 0);
+    _instance_2_u(A._Future.prototype, "get$_completeError", "_completeError$2", 11);
+    var _;
+    _instance_1_i(_ = A._StreamController.prototype, "get$add", "add$1", 7);
+    _instance(_, "get$addError", 0, 1, function() {
+      return [null];
+    }, ["call$2", "call$1"], ["addError$2", "addError$1"], 12, 0, 0);
+    _instance_1_i(A._StreamSinkWrapper.prototype, "get$add", "add$1", 7);
+    _instance_0_u(A._DoneStreamSubscription.prototype, "get$_onMicrotask", "_onMicrotask$0", 0);
+    _static_1(A, "convert___defaultToEncodable$closure", "_defaultToEncodable", 15);
+    _static_1(A, "core_Uri_decodeComponent$closure", "Uri_decodeComponent", 17);
+    _static_1(A, "frame_Frame___parseVM_tearOff$closure", "Frame___parseVM_tearOff", 9);
+    _static_1(A, "frame_Frame___parseV8_tearOff$closure", "Frame___parseV8_tearOff", 9);
+    _static_1(A, "frame_Frame___parseFirefox_tearOff$closure", "Frame___parseFirefox_tearOff", 9);
+    _static_1(A, "frame_Frame___parseFriendly_tearOff$closure", "Frame___parseFriendly_tearOff", 9);
+    _static_1(A, "trace_Trace___parseVM_tearOff$closure", "Trace___parseVM_tearOff", 14);
+    _static_1(A, "trace_Trace___parseFriendly_tearOff$closure", "Trace___parseFriendly_tearOff", 14);
+    _instance(_ = A._GuaranteeSink.prototype, "get$addError", 0, 1, function() {
+      return [null];
+    }, ["call$2", "call$1"], ["addError$2", "addError$1"], 12, 0, 0);
+    _instance(_, "get$_addError", 0, 1, function() {
+      return [null];
+    }, ["call$2", "call$1"], ["_addError$2", "_addError$1"], 12, 0, 0);
+    _instance_0_u(A._MultiChannel.prototype, "get$_closeInnerChannel", "_closeInnerChannel$0", 0);
+    _static(A, "math__max$closure", 2, null, ["call$1$2", "call$2"], ["max", function(a, b) {
+      return A.max(a, b, type$.num);
+    }], 41, 0);
+  })();
+  (function inheritance() {
+    var _mixin = hunkHelpers.mixin,
+      _inherit = hunkHelpers.inherit,
+      _inheritMany = hunkHelpers.inheritMany;
+    _inherit(A.Object, null);
+    _inheritMany(A.Object, [A.JS_CONST, J.Interceptor, J.ArrayIterator, A.Stream, A.CastStreamSubscription, A.Iterable, A.CastIterator, A.Error, A.ListBase, A.Closure, A.SentinelValue, A.ListIterator, A.MappedIterator, A.WhereIterator, A.ExpandIterator, A.TakeIterator, A.SkipIterator, A.SkipWhileIterator, A.EmptyIterator, A.WhereTypeIterator, A.FixedLengthListMixin, A.UnmodifiableListMixin, A.Symbol, A.MapView, A.ConstantMap, A._KeysOrValuesOrElementsIterator, A.JSInvocationMirror, A.TypeErrorDecoder, A.NullThrownFromJavaScriptException, A._StackTrace, A._Required, A.MapBase, A.LinkedHashMapCell, A.LinkedHashMapKeyIterator, A.JSSyntaxRegExp, A._MatchImplementation, A._AllMatchesIterator, A.StringMatch, A._StringAllMatchesIterator, A._Cell, A._InitializedCell, A.Rti, A._FunctionParameters, A._Type, A._TimerImpl, A.AsyncError, A._Completer, A._FutureListener, A._Future, A._AsyncCallbackEntry, A._StreamController, A._SyncStreamControllerDispatch, A._BufferingStreamSubscription, A._StreamSinkWrapper, A._DelayedEvent, A._DelayedDone, A._PendingEvents, A._DoneStreamSubscription, A._ZoneFunction, A._ZoneSpecification, A._ZoneDelegate, A._Zone, A._HashMapKeyIterator, A.SetBase, A._LinkedHashSetCell, A._LinkedHashSetIterator, A._UnmodifiableMapMixin, A.Codec, A.Converter, A._JsonStringifier, A._Utf8Encoder, A._Utf8Decoder, A.DateTime, A.Duration, A.OutOfMemoryError, A.StackOverflowError, A._Exception, A.FormatException, A.Null, A._StringStackTrace, A.StringBuffer, A._Uri, A.UriData, A._SimpleUri, A.CssStyleDeclarationBase, A.ImmutableListMixin, A.FixedSizeListIterator, A.NullRejectionException, A.NullStreamSink, A.Context, A.Style, A.ParsedPath, A.PathException, A.Chain, A.Frame, A.LazyTrace, A.Trace, A.UnparsedFrame, A.StreamChannelMixin, A._GuaranteeSink, A.StreamChannelController, A.Subscription]);
+    _inheritMany(J.Interceptor, [J.JSBool, J.JSNull, J.JavaScriptObject, J.JavaScriptBigInt, J.JavaScriptSymbol, J.JSNumber, J.JSString]);
+    _inheritMany(J.JavaScriptObject, [J.LegacyJavaScriptObject, J.JSArray, A.NativeByteBuffer, A.NativeTypedData, A.EventTarget, A.AccessibleNodeList, A.Blob, A.CssTransformComponent, A.CssRule, A._CssStyleDeclaration_JavaScriptObject_CssStyleDeclarationBase, A.CssStyleValue, A.DataTransferItemList, A.DomException, A._DomRectList_JavaScriptObject_ListMixin, A.DomRectReadOnly, A._DomStringList_JavaScriptObject_ListMixin, A.DomTokenList, A._FileList_JavaScriptObject_ListMixin, A.Gamepad, A.History, A._HtmlCollection_JavaScriptObject_ListMixin, A.Location, A.MediaList, A._MidiInputMap_JavaScriptObject_MapMixin, A._MidiOutputMap_JavaScriptObject_MapMixin, A.MimeType, A._MimeTypeArray_JavaScriptObject_ListMixin, A._NodeList_JavaScriptObject_ListMixin, A.Plugin, A._PluginArray_JavaScriptObject_ListMixin, A._RtcStatsReport_JavaScriptObject_MapMixin, A.SpeechGrammar, A._SpeechGrammarList_JavaScriptObject_ListMixin, A.SpeechRecognitionResult, A._Storage_JavaScriptObject_MapMixin, A.StyleSheet, A._TextTrackCueList_JavaScriptObject_ListMixin, A.TimeRanges, A.Touch, A._TouchList_JavaScriptObject_ListMixin, A.TrackDefaultList, A.Url, A.__CssRuleList_JavaScriptObject_ListMixin, A.__GamepadList_JavaScriptObject_ListMixin, A.__NamedNodeMap_JavaScriptObject_ListMixin, A.__SpeechRecognitionResultList_JavaScriptObject_ListMixin, A.__StyleSheetList_JavaScriptObject_ListMixin, A.Length, A._LengthList_JavaScriptObject_ListMixin, A.Number, A._NumberList_JavaScriptObject_ListMixin, A.PointList, A._StringList_JavaScriptObject_ListMixin, A.Transform, A._TransformList_JavaScriptObject_ListMixin, A.AudioBuffer, A._AudioParamMap_JavaScriptObject_MapMixin]);
+    _inheritMany(J.LegacyJavaScriptObject, [J.PlainJavaScriptObject, J.UnknownJavaScriptObject, J.JavaScriptFunction]);
+    _inherit(J.JSUnmodifiableArray, J.JSArray);
+    _inheritMany(J.JSNumber, [J.JSInt, J.JSNumNotInt]);
+    _inheritMany(A.Stream, [A.CastStream, A._StreamImpl, A._EmptyStream]);
+    _inheritMany(A.Iterable, [A._CastIterableBase, A.EfficientLengthIterable, A.MappedIterable, A.WhereIterable, A.ExpandIterable, A.TakeIterable, A.SkipIterable, A.SkipWhileIterable, A.WhereTypeIterable, A._KeysOrValues, A._AllMatchesIterable, A._StringAllMatchesIterable]);
+    _inheritMany(A._CastIterableBase, [A.CastIterable, A.__CastListBase__CastIterableBase_ListMixin]);
+    _inherit(A._EfficientLengthCastIterable, A.CastIterable);
+    _inherit(A._CastListBase, A.__CastListBase__CastIterableBase_ListMixin);
+    _inherit(A.CastList, A._CastListBase);
+    _inheritMany(A.Error, [A.LateError, A.TypeError, A.JsNoSuchMethodError, A.UnknownJsTypeError, A._CyclicInitializationError, A.RuntimeError, A.AssertionError, A._Error, A.JsonUnsupportedObjectError, A.ArgumentError, A.NoSuchMethodError, A.UnsupportedError, A.UnimplementedError, A.StateError, A.ConcurrentModificationError]);
+    _inherit(A.UnmodifiableListBase, A.ListBase);
+    _inherit(A.CodeUnits, A.UnmodifiableListBase);
+    _inheritMany(A.Closure, [A.Closure0Args, A.Instantiation, A.Closure2Args, A.TearOffClosure, A.JsLinkedHashMap_values_closure, A.initHooks_closure, A.initHooks_closure1, A._AsyncRun__initializeScheduleImmediate_internalCallback, A._AsyncRun__initializeScheduleImmediate_closure, A._Future__chainForeignFuture_closure, A._Future__propagateToListeners_handleWhenCompleteCallback_closure, A.Stream_pipe_closure, A.Stream_length_closure, A._CustomZone_bindUnaryCallback_closure, A._CustomZone_bindUnaryCallbackGuarded_closure, A._RootZone_bindUnaryCallback_closure, A._RootZone_bindUnaryCallbackGuarded_closure, A.runZonedGuarded_closure, A._Uri__makePath_closure, A._createTables_setChars, A._createTables_setRange, A.jsify__convert, A.promiseToFuture_closure, A.promiseToFuture_closure0, A.dartify_convert, A.Context_joinAll_closure, A.Context_split_closure, A._validateArgList_closure, A.WindowsStyle_absolutePathToUri_closure, A.Chain_Chain$parse_closure, A.Chain_toTrace_closure, A.Chain_toString_closure0, A.Chain_toString__closure0, A.Chain_toString_closure, A.Chain_toString__closure, A.Trace__parseVM_closure, A.Trace$parseV8_closure, A.Trace$parseJSCore_closure, A.Trace$parseFirefox_closure, A.Trace$parseFriendly_closure, A.Trace_terse_closure, A.Trace_foldFrames_closure, A.Trace_foldFrames_closure0, A.Trace_toString_closure0, A.Trace_toString_closure, A._GuaranteeSink_addStream_closure, A._MultiChannel_closure, A._MultiChannel_closure1, A._MultiChannel_virtualChannel_closure, A.MessagePortExtension_get_postMessage_closure, A.main__closure, A.main__closure0, A.main__closure1, A._connectToServer_closure, A._connectToServer_closure0, A._connectToIframe_closure, A._connectToIframe__closure1]);
+    _inheritMany(A.Closure0Args, [A.nullFuture_closure, A._AsyncRun__scheduleImmediateJsOverride_internalCallback, A._AsyncRun__scheduleImmediateWithSetImmediate_internalCallback, A._TimerImpl_internalCallback, A._TimerImpl$periodic_closure, A._Future__addListener_closure, A._Future__prependListeners_closure, A._Future__chainForeignFuture_closure1, A._Future__chainCoreFutureAsync_closure, A._Future__asyncCompleteWithValue_closure, A._Future__asyncCompleteError_closure, A._Future__propagateToListeners_handleWhenCompleteCallback, A._Future__propagateToListeners_handleValueCallback, A._Future__propagateToListeners_handleError, A.Stream_length_closure0, A._StreamController__subscribe_closure, A._StreamController__recordCancel_complete, A._AddStreamState_cancel_closure, A._BufferingStreamSubscription__sendError_sendError, A._BufferingStreamSubscription__sendDone_sendDone, A._PendingEvents_schedule_closure, A._CustomZone_bindCallback_closure, A._CustomZone_bindCallbackGuarded_closure, A._rootHandleError_closure, A._RootZone_bindCallback_closure, A._RootZone_bindCallbackGuarded_closure, A.Utf8Decoder__decoder_closure, A.Utf8Decoder__decoderNonfatal_closure, A.NullStreamSink_addStream_closure, A.Frame_Frame$parseVM_closure, A.Frame_Frame$parseV8_closure, A.Frame_Frame$_parseFirefoxEval_closure, A.Frame_Frame$parseFirefox_closure, A.Frame_Frame$parseFriendly_closure, A.LazyTrace_terse_closure, A.Trace_Trace$from_closure, A.GuaranteeChannel_closure, A.GuaranteeChannel__closure, A._MultiChannel_closure0, A._MultiChannel__closure, A._MultiChannel_virtualChannel_closure0, A.main_closure, A.main___closure, A.main___closure0, A.main___closure1, A.main___closure2, A.main___closure3, A.main___closure4, A.main__closure2, A.main__closure3, A._connectToIframe__closure, A._connectToIframe__closure0]);
+    _inheritMany(A.EfficientLengthIterable, [A.ListIterable, A.EmptyIterable, A.LinkedHashMapKeyIterable, A._HashMapKeyIterable]);
+    _inheritMany(A.ListIterable, [A.SubListIterable, A.MappedListIterable, A.ReversedListIterable, A._JsonMapKeyIterable]);
+    _inherit(A.EfficientLengthMappedIterable, A.MappedIterable);
+    _inherit(A.EfficientLengthTakeIterable, A.TakeIterable);
+    _inherit(A.EfficientLengthSkipIterable, A.SkipIterable);
+    _inherit(A._UnmodifiableMapView_MapView__UnmodifiableMapMixin, A.MapView);
+    _inherit(A.UnmodifiableMapView, A._UnmodifiableMapView_MapView__UnmodifiableMapMixin);
+    _inherit(A.ConstantMapView, A.UnmodifiableMapView);
+    _inherit(A.ConstantStringMap, A.ConstantMap);
+    _inherit(A.Instantiation1, A.Instantiation);
+    _inheritMany(A.Closure2Args, [A.Primitives_functionNoSuchMethod_closure, A.initHooks_closure0, A._Future__chainForeignFuture_closure0, A.MapBase_mapToString_closure, A._JsonStringifier_writeMap_closure, A.NoSuchMethodError_toString_closure, A.Uri_splitQueryString_closure, A.Uri__parseIPv4Address_error, A.Uri_parseIPv6Address_error, A.Uri_parseIPv6Address_parseHex, A._createTables_build, A.MidiInputMap_keys_closure, A.MidiOutputMap_keys_closure, A.RtcStatsReport_keys_closure, A.Storage_keys_closure, A.AudioParamMap_keys_closure, A.Frame_Frame$parseV8_closure_parseLocation, A.main_closure0]);
+    _inherit(A.NullError, A.TypeError);
+    _inheritMany(A.TearOffClosure, [A.StaticClosure, A.BoundClosure]);
+    _inherit(A._AssertionError, A.AssertionError);
+    _inheritMany(A.MapBase, [A.JsLinkedHashMap, A._HashMap, A._JsonMap]);
+    _inheritMany(A.NativeTypedData, [A.NativeByteData, A.NativeTypedArray]);
+    _inheritMany(A.NativeTypedArray, [A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin, A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin]);
+    _inherit(A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin, A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin);
+    _inherit(A.NativeTypedArrayOfDouble, A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin);
+    _inherit(A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin, A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin);
+    _inherit(A.NativeTypedArrayOfInt, A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin);
+    _inheritMany(A.NativeTypedArrayOfDouble, [A.NativeFloat32List, A.NativeFloat64List]);
+    _inheritMany(A.NativeTypedArrayOfInt, [A.NativeInt16List, A.NativeInt32List, A.NativeInt8List, A.NativeUint16List, A.NativeUint32List, A.NativeUint8ClampedList, A.NativeUint8List]);
+    _inherit(A._TypeError, A._Error);
+    _inheritMany(A._Completer, [A._AsyncCompleter, A._SyncCompleter]);
+    _inherit(A._SyncStreamController, A._StreamController);
+    _inherit(A._ControllerStream, A._StreamImpl);
+    _inherit(A._ControllerSubscription, A._BufferingStreamSubscription);
+    _inheritMany(A._DelayedEvent, [A._DelayedData, A._DelayedError]);
+    _inheritMany(A._Zone, [A._CustomZone, A._RootZone]);
+    _inherit(A._IdentityHashMap, A._HashMap);
+    _inherit(A._SetBase, A.SetBase);
+    _inherit(A._LinkedHashSet, A._SetBase);
+    _inheritMany(A.Codec, [A.Encoding, A.Base64Codec, A._FusedCodec, A.JsonCodec]);
+    _inheritMany(A.Encoding, [A.AsciiCodec, A.Utf8Codec]);
+    _inheritMany(A.Converter, [A._UnicodeSubsetEncoder, A.Base64Encoder, A.JsonEncoder, A.JsonDecoder, A.Utf8Encoder, A.Utf8Decoder]);
+    _inherit(A.AsciiEncoder, A._UnicodeSubsetEncoder);
+    _inherit(A.JsonCyclicError, A.JsonUnsupportedObjectError);
+    _inherit(A._JsonStringStringifier, A._JsonStringifier);
+    _inheritMany(A.ArgumentError, [A.RangeError, A.IndexError]);
+    _inherit(A._DataUri, A._Uri);
+    _inheritMany(A.EventTarget, [A.Node, A.FileWriter, A.SourceBuffer, A._SourceBufferList_EventTarget_ListMixin, A.TextTrack, A.TextTrackCue, A._TextTrackList_EventTarget_ListMixin, A.VideoTrackList, A.AudioTrackList, A.BaseAudioContext]);
+    _inheritMany(A.Node, [A.Element, A.CharacterData]);
+    _inherit(A.HtmlElement, A.Element);
+    _inheritMany(A.HtmlElement, [A.AnchorElement, A.AreaElement, A.FormElement, A.SelectElement]);
+    _inherit(A.CssPerspective, A.CssTransformComponent);
+    _inherit(A.CssStyleDeclaration, A._CssStyleDeclaration_JavaScriptObject_CssStyleDeclarationBase);
+    _inheritMany(A.CssStyleValue, [A.CssTransformValue, A.CssUnparsedValue]);
+    _inherit(A._DomRectList_JavaScriptObject_ListMixin_ImmutableListMixin, A._DomRectList_JavaScriptObject_ListMixin);
+    _inherit(A.DomRectList, A._DomRectList_JavaScriptObject_ListMixin_ImmutableListMixin);
+    _inherit(A._DomStringList_JavaScriptObject_ListMixin_ImmutableListMixin, A._DomStringList_JavaScriptObject_ListMixin);
+    _inherit(A.DomStringList, A._DomStringList_JavaScriptObject_ListMixin_ImmutableListMixin);
+    _inherit(A.File, A.Blob);
+    _inherit(A._FileList_JavaScriptObject_ListMixin_ImmutableListMixin, A._FileList_JavaScriptObject_ListMixin);
+    _inherit(A.FileList, A._FileList_JavaScriptObject_ListMixin_ImmutableListMixin);
+    _inherit(A._HtmlCollection_JavaScriptObject_ListMixin_ImmutableListMixin, A._HtmlCollection_JavaScriptObject_ListMixin);
+    _inherit(A.HtmlCollection, A._HtmlCollection_JavaScriptObject_ListMixin_ImmutableListMixin);
+    _inherit(A.MidiInputMap, A._MidiInputMap_JavaScriptObject_MapMixin);
+    _inherit(A.MidiOutputMap, A._MidiOutputMap_JavaScriptObject_MapMixin);
+    _inherit(A._MimeTypeArray_JavaScriptObject_ListMixin_ImmutableListMixin, A._MimeTypeArray_JavaScriptObject_ListMixin);
+    _inherit(A.MimeTypeArray, A._MimeTypeArray_JavaScriptObject_ListMixin_ImmutableListMixin);
+    _inherit(A._NodeList_JavaScriptObject_ListMixin_ImmutableListMixin, A._NodeList_JavaScriptObject_ListMixin);
+    _inherit(A.NodeList, A._NodeList_JavaScriptObject_ListMixin_ImmutableListMixin);
+    _inherit(A._PluginArray_JavaScriptObject_ListMixin_ImmutableListMixin, A._PluginArray_JavaScriptObject_ListMixin);
+    _inherit(A.PluginArray, A._PluginArray_JavaScriptObject_ListMixin_ImmutableListMixin);
+    _inherit(A.RtcStatsReport, A._RtcStatsReport_JavaScriptObject_MapMixin);
+    _inherit(A._SourceBufferList_EventTarget_ListMixin_ImmutableListMixin, A._SourceBufferList_EventTarget_ListMixin);
+    _inherit(A.SourceBufferList, A._SourceBufferList_EventTarget_ListMixin_ImmutableListMixin);
+    _inherit(A._SpeechGrammarList_JavaScriptObject_ListMixin_ImmutableListMixin, A._SpeechGrammarList_JavaScriptObject_ListMixin);
+    _inherit(A.SpeechGrammarList, A._SpeechGrammarList_JavaScriptObject_ListMixin_ImmutableListMixin);
+    _inherit(A.Storage, A._Storage_JavaScriptObject_MapMixin);
+    _inherit(A._TextTrackCueList_JavaScriptObject_ListMixin_ImmutableListMixin, A._TextTrackCueList_JavaScriptObject_ListMixin);
+    _inherit(A.TextTrackCueList, A._TextTrackCueList_JavaScriptObject_ListMixin_ImmutableListMixin);
+    _inherit(A._TextTrackList_EventTarget_ListMixin_ImmutableListMixin, A._TextTrackList_EventTarget_ListMixin);
+    _inherit(A.TextTrackList, A._TextTrackList_EventTarget_ListMixin_ImmutableListMixin);
+    _inherit(A._TouchList_JavaScriptObject_ListMixin_ImmutableListMixin, A._TouchList_JavaScriptObject_ListMixin);
+    _inherit(A.TouchList, A._TouchList_JavaScriptObject_ListMixin_ImmutableListMixin);
+    _inherit(A.__CssRuleList_JavaScriptObject_ListMixin_ImmutableListMixin, A.__CssRuleList_JavaScriptObject_ListMixin);
+    _inherit(A._CssRuleList, A.__CssRuleList_JavaScriptObject_ListMixin_ImmutableListMixin);
+    _inherit(A._DomRect, A.DomRectReadOnly);
+    _inherit(A.__GamepadList_JavaScriptObject_ListMixin_ImmutableListMixin, A.__GamepadList_JavaScriptObject_ListMixin);
+    _inherit(A._GamepadList, A.__GamepadList_JavaScriptObject_ListMixin_ImmutableListMixin);
+    _inherit(A.__NamedNodeMap_JavaScriptObject_ListMixin_ImmutableListMixin, A.__NamedNodeMap_JavaScriptObject_ListMixin);
+    _inherit(A._NamedNodeMap, A.__NamedNodeMap_JavaScriptObject_ListMixin_ImmutableListMixin);
+    _inherit(A.__SpeechRecognitionResultList_JavaScriptObject_ListMixin_ImmutableListMixin, A.__SpeechRecognitionResultList_JavaScriptObject_ListMixin);
+    _inherit(A._SpeechRecognitionResultList, A.__SpeechRecognitionResultList_JavaScriptObject_ListMixin_ImmutableListMixin);
+    _inherit(A.__StyleSheetList_JavaScriptObject_ListMixin_ImmutableListMixin, A.__StyleSheetList_JavaScriptObject_ListMixin);
+    _inherit(A._StyleSheetList, A.__StyleSheetList_JavaScriptObject_ListMixin_ImmutableListMixin);
+    _inherit(A._LengthList_JavaScriptObject_ListMixin_ImmutableListMixin, A._LengthList_JavaScriptObject_ListMixin);
+    _inherit(A.LengthList, A._LengthList_JavaScriptObject_ListMixin_ImmutableListMixin);
+    _inherit(A._NumberList_JavaScriptObject_ListMixin_ImmutableListMixin, A._NumberList_JavaScriptObject_ListMixin);
+    _inherit(A.NumberList, A._NumberList_JavaScriptObject_ListMixin_ImmutableListMixin);
+    _inherit(A._StringList_JavaScriptObject_ListMixin_ImmutableListMixin, A._StringList_JavaScriptObject_ListMixin);
+    _inherit(A.StringList, A._StringList_JavaScriptObject_ListMixin_ImmutableListMixin);
+    _inherit(A._TransformList_JavaScriptObject_ListMixin_ImmutableListMixin, A._TransformList_JavaScriptObject_ListMixin);
+    _inherit(A.TransformList, A._TransformList_JavaScriptObject_ListMixin_ImmutableListMixin);
+    _inherit(A.AudioParamMap, A._AudioParamMap_JavaScriptObject_MapMixin);
+    _inherit(A.OfflineAudioContext, A.BaseAudioContext);
+    _inherit(A.InternalStyle, A.Style);
+    _inheritMany(A.InternalStyle, [A.PosixStyle, A.UrlStyle, A.WindowsStyle]);
+    _inheritMany(A.StreamChannelMixin, [A.GuaranteeChannel, A._MultiChannel, A.VirtualChannel]);
+    _mixin(A.UnmodifiableListBase, A.UnmodifiableListMixin);
+    _mixin(A.__CastListBase__CastIterableBase_ListMixin, A.ListBase);
+    _mixin(A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin, A.ListBase);
+    _mixin(A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin, A.FixedLengthListMixin);
+    _mixin(A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin, A.ListBase);
+    _mixin(A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin, A.FixedLengthListMixin);
+    _mixin(A._SyncStreamController, A._SyncStreamControllerDispatch);
+    _mixin(A._UnmodifiableMapView_MapView__UnmodifiableMapMixin, A._UnmodifiableMapMixin);
+    _mixin(A._CssStyleDeclaration_JavaScriptObject_CssStyleDeclarationBase, A.CssStyleDeclarationBase);
+    _mixin(A._DomRectList_JavaScriptObject_ListMixin, A.ListBase);
+    _mixin(A._DomRectList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A._DomStringList_JavaScriptObject_ListMixin, A.ListBase);
+    _mixin(A._DomStringList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A._FileList_JavaScriptObject_ListMixin, A.ListBase);
+    _mixin(A._FileList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A._HtmlCollection_JavaScriptObject_ListMixin, A.ListBase);
+    _mixin(A._HtmlCollection_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A._MidiInputMap_JavaScriptObject_MapMixin, A.MapBase);
+    _mixin(A._MidiOutputMap_JavaScriptObject_MapMixin, A.MapBase);
+    _mixin(A._MimeTypeArray_JavaScriptObject_ListMixin, A.ListBase);
+    _mixin(A._MimeTypeArray_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A._NodeList_JavaScriptObject_ListMixin, A.ListBase);
+    _mixin(A._NodeList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A._PluginArray_JavaScriptObject_ListMixin, A.ListBase);
+    _mixin(A._PluginArray_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A._RtcStatsReport_JavaScriptObject_MapMixin, A.MapBase);
+    _mixin(A._SourceBufferList_EventTarget_ListMixin, A.ListBase);
+    _mixin(A._SourceBufferList_EventTarget_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A._SpeechGrammarList_JavaScriptObject_ListMixin, A.ListBase);
+    _mixin(A._SpeechGrammarList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A._Storage_JavaScriptObject_MapMixin, A.MapBase);
+    _mixin(A._TextTrackCueList_JavaScriptObject_ListMixin, A.ListBase);
+    _mixin(A._TextTrackCueList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A._TextTrackList_EventTarget_ListMixin, A.ListBase);
+    _mixin(A._TextTrackList_EventTarget_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A._TouchList_JavaScriptObject_ListMixin, A.ListBase);
+    _mixin(A._TouchList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A.__CssRuleList_JavaScriptObject_ListMixin, A.ListBase);
+    _mixin(A.__CssRuleList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A.__GamepadList_JavaScriptObject_ListMixin, A.ListBase);
+    _mixin(A.__GamepadList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A.__NamedNodeMap_JavaScriptObject_ListMixin, A.ListBase);
+    _mixin(A.__NamedNodeMap_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A.__SpeechRecognitionResultList_JavaScriptObject_ListMixin, A.ListBase);
+    _mixin(A.__SpeechRecognitionResultList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A.__StyleSheetList_JavaScriptObject_ListMixin, A.ListBase);
+    _mixin(A.__StyleSheetList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A._LengthList_JavaScriptObject_ListMixin, A.ListBase);
+    _mixin(A._LengthList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A._NumberList_JavaScriptObject_ListMixin, A.ListBase);
+    _mixin(A._NumberList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A._StringList_JavaScriptObject_ListMixin, A.ListBase);
+    _mixin(A._StringList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A._TransformList_JavaScriptObject_ListMixin, A.ListBase);
+    _mixin(A._TransformList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin);
+    _mixin(A._AudioParamMap_JavaScriptObject_MapMixin, A.MapBase);
+  })();
+  var init = {
+    typeUniverse: {eC: new Map(), tR: {}, eT: {}, tPV: {}, sEA: []},
+    mangledGlobalNames: {int: "int", double: "double", num: "num", String: "String", bool: "bool", Null: "Null", List: "List"},
+    mangledNames: {},
+    types: ["~()", "bool(String)", "@()", "Null()", "~(String,@)", "~(@)", "Frame()", "~(Object?)", "~(JavaScriptObject)", "Frame(String)", "Null(@)", "~(Object,StackTrace)", "~(Object[StackTrace?])", "~(~())", "Trace(String)", "@(@)", "~(Object?,Object?)", "String(String)", "~(Uint8List,String,int)", "Object?(Object?)", "int(Frame)", "String(Frame)", "Trace()", "bool(Frame)", "bool()", "~(String,int)", "Null(Object,StackTrace)", "~(String,String)", "_Future<@>(@)", "Future<@>(@)", "Null(~())", "String(String?)", "List<Frame>(Trace)", "int(Trace)", "~(Zone,ZoneDelegate,Zone,Object,StackTrace)", "String(Trace)", "@(@,String)", "@(String)", "Frame(String,String)", "~(Symbol0,@)", "Map<String,String>(Map<String,String>,String)", "0^(0^,0^)<num>", "~(List<@>)", "~([Object?])", "~(Timer)", "~(String,int?)", "int(int,int)", "~(Zone?,ZoneDelegate?,Zone,Object,StackTrace)", "0^(Zone?,ZoneDelegate?,Zone,0^())<Object?>", "0^(Zone?,ZoneDelegate?,Zone,0^(1^),1^)<Object?,Object?>", "0^(Zone?,ZoneDelegate?,Zone,0^(1^,2^),1^,2^)<Object?,Object?,Object?>", "0^()(Zone,ZoneDelegate,Zone,0^())<Object?>", "0^(1^)(Zone,ZoneDelegate,Zone,0^(1^))<Object?,Object?>", "0^(1^,2^)(Zone,ZoneDelegate,Zone,0^(1^,2^))<Object?,Object?,Object?>", "AsyncError?(Zone,ZoneDelegate,Zone,Object,StackTrace?)", "~(Zone?,ZoneDelegate?,Zone,~())", "Timer(Zone,ZoneDelegate,Zone,Duration,~())", "Timer(Zone,ZoneDelegate,Zone,Duration,~(Timer))", "~(Zone,ZoneDelegate,Zone,String)", "Zone(Zone?,ZoneDelegate?,Zone,ZoneSpecification?,Map<Object?,Object?>?)", "Future<Null>()", "Uint8List(@,@)", "Frame(Frame)"],
+    interceptorsByTag: null,
+    leafTags: null,
+    arrayRti: Symbol("$ti")
+  };
+  A._Universe_addRules(init.typeUniverse, JSON.parse('{"PlainJavaScriptObject":"LegacyJavaScriptObject","UnknownJavaScriptObject":"LegacyJavaScriptObject","JavaScriptFunction":"LegacyJavaScriptObject","AbortPaymentEvent":"JavaScriptObject","ExtendableEvent":"JavaScriptObject","Event":"JavaScriptObject","AudioContext":"BaseAudioContext","AbsoluteOrientationSensor":"EventTarget","OrientationSensor":"EventTarget","Sensor":"EventTarget","MathMLElement":"Element","AudioElement":"HtmlElement","MediaElement":"HtmlElement","HtmlDocument":"Node","Document":"Node","VttCue":"TextTrackCue","CDataSection":"CharacterData","Text":"CharacterData","HtmlFormControlsCollection":"HtmlCollection","CssCharsetRule":"CssRule","CssMatrixComponent":"CssTransformComponent","CssStyleSheet":"StyleSheet","CssurlImageValue":"CssStyleValue","CssImageValue":"CssStyleValue","CssResourceValue":"CssStyleValue","JSBool":{"bool":[],"TrustedGetRuntimeType":[]},"JSNull":{"Null":[],"TrustedGetRuntimeType":[]},"LegacyJavaScriptObject":{"JavaScriptObject":[]},"JSArray":{"List":["1"],"JavaScriptObject":[],"EfficientLengthIterable":["1"],"Iterable":["1"]},"JSUnmodifiableArray":{"JSArray":["1"],"List":["1"],"JavaScriptObject":[],"EfficientLengthIterable":["1"],"Iterable":["1"]},"ArrayIterator":{"Iterator":["1"]},"JSNumber":{"double":[],"num":[]},"JSInt":{"double":[],"int":[],"num":[],"TrustedGetRuntimeType":[]},"JSNumNotInt":{"double":[],"num":[],"TrustedGetRuntimeType":[]},"JSString":{"String":[],"Pattern":[],"TrustedGetRuntimeType":[]},"CastStream":{"Stream":["2"],"Stream.T":"2"},"CastStreamSubscription":{"StreamSubscription":["2"]},"_CastIterableBase":{"Iterable":["2"]},"CastIterator":{"Iterator":["2"]},"CastIterable":{"_CastIterableBase":["1","2"],"Iterable":["2"],"Iterable.E":"2"},"_EfficientLengthCastIterable":{"CastIterable":["1","2"],"_CastIterableBase":["1","2"],"EfficientLengthIterable":["2"],"Iterable":["2"],"Iterable.E":"2"},"_CastListBase":{"ListBase":["2"],"List":["2"],"_CastIterableBase":["1","2"],"EfficientLengthIterable":["2"],"Iterable":["2"]},"CastList":{"_CastListBase":["1","2"],"ListBase":["2"],"List":["2"],"_CastIterableBase":["1","2"],"EfficientLengthIterable":["2"],"Iterable":["2"],"ListBase.E":"2","Iterable.E":"2"},"LateError":{"Error":[]},"CodeUnits":{"ListBase":["int"],"UnmodifiableListMixin":["int"],"List":["int"],"EfficientLengthIterable":["int"],"Iterable":["int"],"ListBase.E":"int","UnmodifiableListMixin.E":"int"},"EfficientLengthIterable":{"Iterable":["1"]},"ListIterable":{"EfficientLengthIterable":["1"],"Iterable":["1"]},"SubListIterable":{"ListIterable":["1"],"EfficientLengthIterable":["1"],"Iterable":["1"],"ListIterable.E":"1","Iterable.E":"1"},"ListIterator":{"Iterator":["1"]},"MappedIterable":{"Iterable":["2"],"Iterable.E":"2"},"EfficientLengthMappedIterable":{"MappedIterable":["1","2"],"EfficientLengthIterable":["2"],"Iterable":["2"],"Iterable.E":"2"},"MappedIterator":{"Iterator":["2"]},"MappedListIterable":{"ListIterable":["2"],"EfficientLengthIterable":["2"],"Iterable":["2"],"ListIterable.E":"2","Iterable.E":"2"},"WhereIterable":{"Iterable":["1"],"Iterable.E":"1"},"WhereIterator":{"Iterator":["1"]},"ExpandIterable":{"Iterable":["2"],"Iterable.E":"2"},"ExpandIterator":{"Iterator":["2"]},"TakeIterable":{"Iterable":["1"],"Iterable.E":"1"},"EfficientLengthTakeIterable":{"TakeIterable":["1"],"EfficientLengthIterable":["1"],"Iterable":["1"],"Iterable.E":"1"},"TakeIterator":{"Iterator":["1"]},"SkipIterable":{"Iterable":["1"],"Iterable.E":"1"},"EfficientLengthSkipIterable":{"SkipIterable":["1"],"EfficientLengthIterable":["1"],"Iterable":["1"],"Iterable.E":"1"},"SkipIterator":{"Iterator":["1"]},"SkipWhileIterable":{"Iterable":["1"],"Iterable.E":"1"},"SkipWhileIterator":{"Iterator":["1"]},"EmptyIterable":{"EfficientLengthIterable":["1"],"Iterable":["1"],"Iterable.E":"1"},"EmptyIterator":{"Iterator":["1"]},"WhereTypeIterable":{"Iterable":["1"],"Iterable.E":"1"},"WhereTypeIterator":{"Iterator":["1"]},"UnmodifiableListBase":{"ListBase":["1"],"UnmodifiableListMixin":["1"],"List":["1"],"EfficientLengthIterable":["1"],"Iterable":["1"]},"ReversedListIterable":{"ListIterable":["1"],"EfficientLengthIterable":["1"],"Iterable":["1"],"ListIterable.E":"1","Iterable.E":"1"},"Symbol":{"Symbol0":[]},"ConstantMapView":{"UnmodifiableMapView":["1","2"],"_UnmodifiableMapView_MapView__UnmodifiableMapMixin":["1","2"],"MapView":["1","2"],"_UnmodifiableMapMixin":["1","2"],"Map":["1","2"]},"ConstantMap":{"Map":["1","2"]},"ConstantStringMap":{"ConstantMap":["1","2"],"Map":["1","2"]},"_KeysOrValues":{"Iterable":["1"],"Iterable.E":"1"},"_KeysOrValuesOrElementsIterator":{"Iterator":["1"]},"Instantiation":{"Closure":[],"Function":[]},"Instantiation1":{"Closure":[],"Function":[]},"JSInvocationMirror":{"Invocation":[]},"NullError":{"TypeError":[],"Error":[]},"JsNoSuchMethodError":{"Error":[]},"UnknownJsTypeError":{"Error":[]},"NullThrownFromJavaScriptException":{"Exception":[]},"_StackTrace":{"StackTrace":[]},"Closure":{"Function":[]},"Closure0Args":{"Closure":[],"Function":[]},"Closure2Args":{"Closure":[],"Function":[]},"TearOffClosure":{"Closure":[],"Function":[]},"StaticClosure":{"Closure":[],"Function":[]},"BoundClosure":{"Closure":[],"Function":[]},"_CyclicInitializationError":{"Error":[]},"RuntimeError":{"Error":[]},"_AssertionError":{"Error":[]},"JsLinkedHashMap":{"MapBase":["1","2"],"LinkedHashMap":["1","2"],"Map":["1","2"],"MapBase.K":"1","MapBase.V":"2"},"LinkedHashMapKeyIterable":{"EfficientLengthIterable":["1"],"Iterable":["1"],"Iterable.E":"1"},"LinkedHashMapKeyIterator":{"Iterator":["1"]},"JSSyntaxRegExp":{"RegExp":[],"Pattern":[]},"_MatchImplementation":{"RegExpMatch":[],"Match":[]},"_AllMatchesIterable":{"Iterable":["RegExpMatch"],"Iterable.E":"RegExpMatch"},"_AllMatchesIterator":{"Iterator":["RegExpMatch"]},"StringMatch":{"Match":[]},"_StringAllMatchesIterable":{"Iterable":["Match"],"Iterable.E":"Match"},"_StringAllMatchesIterator":{"Iterator":["Match"]},"NativeByteBuffer":{"JavaScriptObject":[],"ByteBuffer":[],"TrustedGetRuntimeType":[]},"NativeTypedData":{"JavaScriptObject":[]},"NativeByteData":{"JavaScriptObject":[],"ByteData":[],"TrustedGetRuntimeType":[]},"NativeTypedArray":{"JavaScriptIndexingBehavior":["1"],"JavaScriptObject":[]},"NativeTypedArrayOfDouble":{"ListBase":["double"],"List":["double"],"JavaScriptIndexingBehavior":["double"],"JavaScriptObject":[],"EfficientLengthIterable":["double"],"Iterable":["double"],"FixedLengthListMixin":["double"]},"NativeTypedArrayOfInt":{"ListBase":["int"],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JavaScriptObject":[],"EfficientLengthIterable":["int"],"Iterable":["int"],"FixedLengthListMixin":["int"]},"NativeFloat32List":{"ListBase":["double"],"Float32List":[],"List":["double"],"JavaScriptIndexingBehavior":["double"],"JavaScriptObject":[],"EfficientLengthIterable":["double"],"Iterable":["double"],"FixedLengthListMixin":["double"],"TrustedGetRuntimeType":[],"ListBase.E":"double"},"NativeFloat64List":{"ListBase":["double"],"Float64List":[],"List":["double"],"JavaScriptIndexingBehavior":["double"],"JavaScriptObject":[],"EfficientLengthIterable":["double"],"Iterable":["double"],"FixedLengthListMixin":["double"],"TrustedGetRuntimeType":[],"ListBase.E":"double"},"NativeInt16List":{"ListBase":["int"],"Int16List":[],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JavaScriptObject":[],"EfficientLengthIterable":["int"],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"NativeInt32List":{"ListBase":["int"],"Int32List":[],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JavaScriptObject":[],"EfficientLengthIterable":["int"],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"NativeInt8List":{"ListBase":["int"],"Int8List":[],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JavaScriptObject":[],"EfficientLengthIterable":["int"],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"NativeUint16List":{"ListBase":["int"],"Uint16List":[],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JavaScriptObject":[],"EfficientLengthIterable":["int"],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"NativeUint32List":{"ListBase":["int"],"Uint32List":[],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JavaScriptObject":[],"EfficientLengthIterable":["int"],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"NativeUint8ClampedList":{"ListBase":["int"],"Uint8ClampedList":[],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JavaScriptObject":[],"EfficientLengthIterable":["int"],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"NativeUint8List":{"ListBase":["int"],"Uint8List":[],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JavaScriptObject":[],"EfficientLengthIterable":["int"],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"_Error":{"Error":[]},"_TypeError":{"TypeError":[],"Error":[]},"AsyncError":{"Error":[]},"_Future":{"Future":["1"]},"_TimerImpl":{"Timer":[]},"_Completer":{"Completer":["1"]},"_AsyncCompleter":{"_Completer":["1"],"Completer":["1"]},"_SyncCompleter":{"_Completer":["1"],"Completer":["1"]},"_StreamController":{"StreamController":["1"],"StreamSink":["1"],"StreamConsumer":["1"],"_StreamControllerLifecycle":["1"],"_EventDispatch":["1"]},"_SyncStreamController":{"_SyncStreamControllerDispatch":["1"],"_StreamController":["1"],"StreamController":["1"],"StreamSink":["1"],"StreamConsumer":["1"],"_StreamControllerLifecycle":["1"],"_EventDispatch":["1"]},"_ControllerStream":{"_StreamImpl":["1"],"Stream":["1"],"Stream.T":"1"},"_ControllerSubscription":{"_BufferingStreamSubscription":["1"],"StreamSubscription":["1"],"_EventDispatch":["1"],"_BufferingStreamSubscription.T":"1"},"_StreamSinkWrapper":{"StreamSink":["1"],"StreamConsumer":["1"]},"_BufferingStreamSubscription":{"StreamSubscription":["1"],"_EventDispatch":["1"],"_BufferingStreamSubscription.T":"1"},"_StreamImpl":{"Stream":["1"]},"_DelayedData":{"_DelayedEvent":["1"]},"_DelayedError":{"_DelayedEvent":["@"]},"_DelayedDone":{"_DelayedEvent":["@"]},"_DoneStreamSubscription":{"StreamSubscription":["1"]},"_EmptyStream":{"Stream":["1"],"Stream.T":"1"},"_ZoneSpecification":{"ZoneSpecification":[]},"_ZoneDelegate":{"ZoneDelegate":[]},"_Zone":{"Zone":[]},"_CustomZone":{"_Zone":[],"Zone":[]},"_RootZone":{"_Zone":[],"Zone":[]},"_HashMap":{"MapBase":["1","2"],"Map":["1","2"],"MapBase.K":"1","MapBase.V":"2"},"_IdentityHashMap":{"_HashMap":["1","2"],"MapBase":["1","2"],"Map":["1","2"],"MapBase.K":"1","MapBase.V":"2"},"_HashMapKeyIterable":{"EfficientLengthIterable":["1"],"Iterable":["1"],"Iterable.E":"1"},"_HashMapKeyIterator":{"Iterator":["1"]},"_LinkedHashSet":{"SetBase":["1"],"Set":["1"],"EfficientLengthIterable":["1"],"Iterable":["1"]},"_LinkedHashSetIterator":{"Iterator":["1"]},"ListBase":{"List":["1"],"EfficientLengthIterable":["1"],"Iterable":["1"]},"MapBase":{"Map":["1","2"]},"MapView":{"Map":["1","2"]},"UnmodifiableMapView":{"_UnmodifiableMapView_MapView__UnmodifiableMapMixin":["1","2"],"MapView":["1","2"],"_UnmodifiableMapMixin":["1","2"],"Map":["1","2"]},"SetBase":{"Set":["1"],"EfficientLengthIterable":["1"],"Iterable":["1"]},"_SetBase":{"SetBase":["1"],"Set":["1"],"EfficientLengthIterable":["1"],"Iterable":["1"]},"_JsonMap":{"MapBase":["String","@"],"Map":["String","@"],"MapBase.K":"String","MapBase.V":"@"},"_JsonMapKeyIterable":{"ListIterable":["String"],"EfficientLengthIterable":["String"],"Iterable":["String"],"ListIterable.E":"String","Iterable.E":"String"},"AsciiCodec":{"Codec":["String","List<int>"]},"_UnicodeSubsetEncoder":{"Converter":["String","List<int>"],"StreamTransformer":["String","List<int>"]},"AsciiEncoder":{"Converter":["String","List<int>"],"StreamTransformer":["String","List<int>"]},"Base64Codec":{"Codec":["List<int>","String"]},"Base64Encoder":{"Converter":["List<int>","String"],"StreamTransformer":["List<int>","String"]},"_FusedCodec":{"Codec":["1","3"]},"Converter":{"StreamTransformer":["1","2"]},"Encoding":{"Codec":["String","List<int>"]},"JsonUnsupportedObjectError":{"Error":[]},"JsonCyclicError":{"Error":[]},"JsonCodec":{"Codec":["Object?","String"]},"JsonEncoder":{"Converter":["Object?","String"],"StreamTransformer":["Object?","String"]},"JsonDecoder":{"Converter":["String","Object?"],"StreamTransformer":["String","Object?"]},"Utf8Codec":{"Codec":["String","List<int>"]},"Utf8Encoder":{"Converter":["String","List<int>"],"StreamTransformer":["String","List<int>"]},"Utf8Decoder":{"Converter":["List<int>","String"],"StreamTransformer":["List<int>","String"]},"double":{"num":[]},"int":{"num":[]},"List":{"EfficientLengthIterable":["1"],"Iterable":["1"]},"RegExpMatch":{"Match":[]},"String":{"Pattern":[]},"AssertionError":{"Error":[]},"TypeError":{"Error":[]},"ArgumentError":{"Error":[]},"RangeError":{"Error":[]},"IndexError":{"Error":[]},"NoSuchMethodError":{"Error":[]},"UnsupportedError":{"Error":[]},"UnimplementedError":{"Error":[]},"StateError":{"Error":[]},"ConcurrentModificationError":{"Error":[]},"OutOfMemoryError":{"Error":[]},"StackOverflowError":{"Error":[]},"_Exception":{"Exception":[]},"FormatException":{"Exception":[]},"_StringStackTrace":{"StackTrace":[]},"StringBuffer":{"StringSink":[]},"_Uri":{"Uri":[]},"_SimpleUri":{"Uri":[]},"_DataUri":{"Uri":[]},"CssRule":{"JavaScriptObject":[]},"File":{"JavaScriptObject":[]},"Gamepad":{"JavaScriptObject":[]},"MimeType":{"JavaScriptObject":[]},"Node":{"JavaScriptObject":[]},"Plugin":{"JavaScriptObject":[]},"SourceBuffer":{"JavaScriptObject":[]},"SpeechGrammar":{"JavaScriptObject":[]},"SpeechRecognitionResult":{"JavaScriptObject":[]},"StyleSheet":{"JavaScriptObject":[]},"TextTrack":{"JavaScriptObject":[]},"TextTrackCue":{"JavaScriptObject":[]},"Touch":{"JavaScriptObject":[]},"HtmlElement":{"Node":[],"JavaScriptObject":[]},"AccessibleNodeList":{"JavaScriptObject":[]},"AnchorElement":{"Node":[],"JavaScriptObject":[]},"AreaElement":{"Node":[],"JavaScriptObject":[]},"Blob":{"JavaScriptObject":[]},"CharacterData":{"Node":[],"JavaScriptObject":[]},"CssPerspective":{"JavaScriptObject":[]},"CssStyleDeclaration":{"JavaScriptObject":[]},"CssStyleValue":{"JavaScriptObject":[]},"CssTransformComponent":{"JavaScriptObject":[]},"CssTransformValue":{"JavaScriptObject":[]},"CssUnparsedValue":{"JavaScriptObject":[]},"DataTransferItemList":{"JavaScriptObject":[]},"DomException":{"JavaScriptObject":[]},"DomRectList":{"ListBase":["Rectangle<num>"],"ImmutableListMixin":["Rectangle<num>"],"List":["Rectangle<num>"],"JavaScriptIndexingBehavior":["Rectangle<num>"],"JavaScriptObject":[],"EfficientLengthIterable":["Rectangle<num>"],"Iterable":["Rectangle<num>"],"ImmutableListMixin.E":"Rectangle<num>","ListBase.E":"Rectangle<num>"},"DomRectReadOnly":{"JavaScriptObject":[],"Rectangle":["num"]},"DomStringList":{"ListBase":["String"],"ImmutableListMixin":["String"],"List":["String"],"JavaScriptIndexingBehavior":["String"],"JavaScriptObject":[],"EfficientLengthIterable":["String"],"Iterable":["String"],"ImmutableListMixin.E":"String","ListBase.E":"String"},"DomTokenList":{"JavaScriptObject":[]},"Element":{"Node":[],"JavaScriptObject":[]},"EventTarget":{"JavaScriptObject":[]},"FileList":{"ListBase":["File"],"ImmutableListMixin":["File"],"List":["File"],"JavaScriptIndexingBehavior":["File"],"JavaScriptObject":[],"EfficientLengthIterable":["File"],"Iterable":["File"],"ImmutableListMixin.E":"File","ListBase.E":"File"},"FileWriter":{"JavaScriptObject":[]},"FormElement":{"Node":[],"JavaScriptObject":[]},"History":{"JavaScriptObject":[]},"HtmlCollection":{"ListBase":["Node"],"ImmutableListMixin":["Node"],"List":["Node"],"JavaScriptIndexingBehavior":["Node"],"JavaScriptObject":[],"EfficientLengthIterable":["Node"],"Iterable":["Node"],"ImmutableListMixin.E":"Node","ListBase.E":"Node"},"Location":{"JavaScriptObject":[]},"MediaList":{"JavaScriptObject":[]},"MidiInputMap":{"JavaScriptObject":[],"MapBase":["String","@"],"Map":["String","@"],"MapBase.K":"String","MapBase.V":"@"},"MidiOutputMap":{"JavaScriptObject":[],"MapBase":["String","@"],"Map":["String","@"],"MapBase.K":"String","MapBase.V":"@"},"MimeTypeArray":{"ListBase":["MimeType"],"ImmutableListMixin":["MimeType"],"List":["MimeType"],"JavaScriptIndexingBehavior":["MimeType"],"JavaScriptObject":[],"EfficientLengthIterable":["MimeType"],"Iterable":["MimeType"],"ImmutableListMixin.E":"MimeType","ListBase.E":"MimeType"},"NodeList":{"ListBase":["Node"],"ImmutableListMixin":["Node"],"List":["Node"],"JavaScriptIndexingBehavior":["Node"],"JavaScriptObject":[],"EfficientLengthIterable":["Node"],"Iterable":["Node"],"ImmutableListMixin.E":"Node","ListBase.E":"Node"},"PluginArray":{"ListBase":["Plugin"],"ImmutableListMixin":["Plugin"],"List":["Plugin"],"JavaScriptIndexingBehavior":["Plugin"],"JavaScriptObject":[],"EfficientLengthIterable":["Plugin"],"Iterable":["Plugin"],"ImmutableListMixin.E":"Plugin","ListBase.E":"Plugin"},"RtcStatsReport":{"JavaScriptObject":[],"MapBase":["String","@"],"Map":["String","@"],"MapBase.K":"String","MapBase.V":"@"},"SelectElement":{"Node":[],"JavaScriptObject":[]},"SourceBufferList":{"ListBase":["SourceBuffer"],"ImmutableListMixin":["SourceBuffer"],"List":["SourceBuffer"],"JavaScriptIndexingBehavior":["SourceBuffer"],"JavaScriptObject":[],"EfficientLengthIterable":["SourceBuffer"],"Iterable":["SourceBuffer"],"ImmutableListMixin.E":"SourceBuffer","ListBase.E":"SourceBuffer"},"SpeechGrammarList":{"ListBase":["SpeechGrammar"],"ImmutableListMixin":["SpeechGrammar"],"List":["SpeechGrammar"],"JavaScriptIndexingBehavior":["SpeechGrammar"],"JavaScriptObject":[],"EfficientLengthIterable":["SpeechGrammar"],"Iterable":["SpeechGrammar"],"ImmutableListMixin.E":"SpeechGrammar","ListBase.E":"SpeechGrammar"},"Storage":{"JavaScriptObject":[],"MapBase":["String","String"],"Map":["String","String"],"MapBase.K":"String","MapBase.V":"String"},"TextTrackCueList":{"ListBase":["TextTrackCue"],"ImmutableListMixin":["TextTrackCue"],"List":["TextTrackCue"],"JavaScriptIndexingBehavior":["TextTrackCue"],"JavaScriptObject":[],"EfficientLengthIterable":["TextTrackCue"],"Iterable":["TextTrackCue"],"ImmutableListMixin.E":"TextTrackCue","ListBase.E":"TextTrackCue"},"TextTrackList":{"ListBase":["TextTrack"],"ImmutableListMixin":["TextTrack"],"List":["TextTrack"],"JavaScriptIndexingBehavior":["TextTrack"],"JavaScriptObject":[],"EfficientLengthIterable":["TextTrack"],"Iterable":["TextTrack"],"ImmutableListMixin.E":"TextTrack","ListBase.E":"TextTrack"},"TimeRanges":{"JavaScriptObject":[]},"TouchList":{"ListBase":["Touch"],"ImmutableListMixin":["Touch"],"List":["Touch"],"JavaScriptIndexingBehavior":["Touch"],"JavaScriptObject":[],"EfficientLengthIterable":["Touch"],"Iterable":["Touch"],"ImmutableListMixin.E":"Touch","ListBase.E":"Touch"},"TrackDefaultList":{"JavaScriptObject":[]},"Url":{"JavaScriptObject":[]},"VideoTrackList":{"JavaScriptObject":[]},"_CssRuleList":{"ListBase":["CssRule"],"ImmutableListMixin":["CssRule"],"List":["CssRule"],"JavaScriptIndexingBehavior":["CssRule"],"JavaScriptObject":[],"EfficientLengthIterable":["CssRule"],"Iterable":["CssRule"],"ImmutableListMixin.E":"CssRule","ListBase.E":"CssRule"},"_DomRect":{"JavaScriptObject":[],"Rectangle":["num"]},"_GamepadList":{"ListBase":["Gamepad?"],"ImmutableListMixin":["Gamepad?"],"List":["Gamepad?"],"JavaScriptIndexingBehavior":["Gamepad?"],"JavaScriptObject":[],"EfficientLengthIterable":["Gamepad?"],"Iterable":["Gamepad?"],"ImmutableListMixin.E":"Gamepad?","ListBase.E":"Gamepad?"},"_NamedNodeMap":{"ListBase":["Node"],"ImmutableListMixin":["Node"],"List":["Node"],"JavaScriptIndexingBehavior":["Node"],"JavaScriptObject":[],"EfficientLengthIterable":["Node"],"Iterable":["Node"],"ImmutableListMixin.E":"Node","ListBase.E":"Node"},"_SpeechRecognitionResultList":{"ListBase":["SpeechRecognitionResult"],"ImmutableListMixin":["SpeechRecognitionResult"],"List":["SpeechRecognitionResult"],"JavaScriptIndexingBehavior":["SpeechRecognitionResult"],"JavaScriptObject":[],"EfficientLengthIterable":["SpeechRecognitionResult"],"Iterable":["SpeechRecognitionResult"],"ImmutableListMixin.E":"SpeechRecognitionResult","ListBase.E":"SpeechRecognitionResult"},"_StyleSheetList":{"ListBase":["StyleSheet"],"ImmutableListMixin":["StyleSheet"],"List":["StyleSheet"],"JavaScriptIndexingBehavior":["StyleSheet"],"JavaScriptObject":[],"EfficientLengthIterable":["StyleSheet"],"Iterable":["StyleSheet"],"ImmutableListMixin.E":"StyleSheet","ListBase.E":"StyleSheet"},"FixedSizeListIterator":{"Iterator":["1"]},"NullRejectionException":{"Exception":[]},"Length":{"JavaScriptObject":[]},"Number":{"JavaScriptObject":[]},"Transform":{"JavaScriptObject":[]},"LengthList":{"ListBase":["Length"],"ImmutableListMixin":["Length"],"List":["Length"],"JavaScriptObject":[],"EfficientLengthIterable":["Length"],"Iterable":["Length"],"ImmutableListMixin.E":"Length","ListBase.E":"Length"},"NumberList":{"ListBase":["Number"],"ImmutableListMixin":["Number"],"List":["Number"],"JavaScriptObject":[],"EfficientLengthIterable":["Number"],"Iterable":["Number"],"ImmutableListMixin.E":"Number","ListBase.E":"Number"},"PointList":{"JavaScriptObject":[]},"StringList":{"ListBase":["String"],"ImmutableListMixin":["String"],"List":["String"],"JavaScriptObject":[],"EfficientLengthIterable":["String"],"Iterable":["String"],"ImmutableListMixin.E":"String","ListBase.E":"String"},"TransformList":{"ListBase":["Transform"],"ImmutableListMixin":["Transform"],"List":["Transform"],"JavaScriptObject":[],"EfficientLengthIterable":["Transform"],"Iterable":["Transform"],"ImmutableListMixin.E":"Transform","ListBase.E":"Transform"},"AudioBuffer":{"JavaScriptObject":[]},"AudioParamMap":{"JavaScriptObject":[],"MapBase":["String","@"],"Map":["String","@"],"MapBase.K":"String","MapBase.V":"@"},"AudioTrackList":{"JavaScriptObject":[]},"BaseAudioContext":{"JavaScriptObject":[]},"OfflineAudioContext":{"JavaScriptObject":[]},"NullStreamSink":{"StreamSink":["1"],"StreamConsumer":["1"]},"PathException":{"Exception":[]},"PosixStyle":{"InternalStyle":[]},"UrlStyle":{"InternalStyle":[]},"WindowsStyle":{"InternalStyle":[]},"Chain":{"StackTrace":[]},"LazyTrace":{"Trace":[],"StackTrace":[]},"Trace":{"StackTrace":[]},"UnparsedFrame":{"Frame":[]},"GuaranteeChannel":{"StreamChannelMixin":["1"],"StreamChannel":["1"]},"_GuaranteeSink":{"StreamSink":["1"],"StreamConsumer":["1"]},"_MultiChannel":{"StreamChannelMixin":["1"],"MultiChannel":["1"],"StreamChannel":["1"]},"VirtualChannel":{"StreamChannelMixin":["1"],"MultiChannel":["1"],"StreamChannel":["1"]},"StreamChannelMixin":{"StreamChannel":["1"]},"Int8List":{"List":["int"],"EfficientLengthIterable":["int"],"Iterable":["int"]},"Uint8List":{"List":["int"],"EfficientLengthIterable":["int"],"Iterable":["int"]},"Uint8ClampedList":{"List":["int"],"EfficientLengthIterable":["int"],"Iterable":["int"]},"Int16List":{"List":["int"],"EfficientLengthIterable":["int"],"Iterable":["int"]},"Uint16List":{"List":["int"],"EfficientLengthIterable":["int"],"Iterable":["int"]},"Int32List":{"List":["int"],"EfficientLengthIterable":["int"],"Iterable":["int"]},"Uint32List":{"List":["int"],"EfficientLengthIterable":["int"],"Iterable":["int"]},"Float32List":{"List":["double"],"EfficientLengthIterable":["double"],"Iterable":["double"]},"Float64List":{"List":["double"],"EfficientLengthIterable":["double"],"Iterable":["double"]}}'));
+  A._Universe_addErasedTypes(init.typeUniverse, JSON.parse('{"UnmodifiableListBase":1,"__CastListBase__CastIterableBase_ListMixin":2,"NativeTypedArray":1,"_DelayedEvent":1,"_SetBase":1}'));
+  var string$ = {
+    x27_has_: "' has been assigned during initialization.",
+    x3d_____: "===== asynchronous gap ===========================\n",
+    Cannotff: "Cannot extract a file path from a URI with a fragment component",
+    Cannotfq: "Cannot extract a file path from a URI with a query component",
+    Cannotn: "Cannot extract a non-Windows file path from a file URI with an authority",
+    Error_: "Error handler must accept one Object or one Object and a StackTrace as arguments, and return a value of the returned future's type",
+    handle: "handleError callback must take either an Object (the error), or both an Object (the error) and a StackTrace."
+  };
+  var type$ = (function rtii() {
+    var findType = A.findType;
+    return {
+      AsyncError: findType("AsyncError"),
+      ByteBuffer: findType("ByteBuffer"),
+      ByteData: findType("ByteData"),
+      ConstantMapView_Symbol_dynamic: findType("ConstantMapView<Symbol0,@>"),
+      CssRule: findType("CssRule"),
+      Duration: findType("Duration"),
+      EfficientLengthIterable_dynamic: findType("EfficientLengthIterable<@>"),
+      Error: findType("Error"),
+      Exception: findType("Exception"),
+      File: findType("File"),
+      Float32List: findType("Float32List"),
+      Float64List: findType("Float64List"),
+      Frame: findType("Frame"),
+      Frame_Function_Frame: findType("Frame(Frame)"),
+      Frame_Function_String: findType("Frame(String)"),
+      Function: findType("Function"),
+      Future_dynamic: findType("Future<@>"),
+      Int16List: findType("Int16List"),
+      Int32List: findType("Int32List"),
+      Int8List: findType("Int8List"),
+      Invocation: findType("Invocation"),
+      Iterable_String: findType("Iterable<String>"),
+      Iterable_dynamic: findType("Iterable<@>"),
+      Iterable_nullable_Object: findType("Iterable<Object?>"),
+      JSArray_Frame: findType("JSArray<Frame>"),
+      JSArray_Object: findType("JSArray<Object>"),
+      JSArray_String: findType("JSArray<String>"),
+      JSArray_Trace: findType("JSArray<Trace>"),
+      JSArray_Uint8List: findType("JSArray<Uint8List>"),
+      JSArray_dynamic: findType("JSArray<@>"),
+      JSArray_int: findType("JSArray<int>"),
+      JSArray_nullable_String: findType("JSArray<String?>"),
+      JSNull: findType("JSNull"),
+      JavaScriptFunction: findType("JavaScriptFunction"),
+      JavaScriptIndexingBehavior_dynamic: findType("JavaScriptIndexingBehavior<@>"),
+      JavaScriptObject: findType("JavaScriptObject"),
+      JsLinkedHashMap_Symbol_dynamic: findType("JsLinkedHashMap<Symbol0,@>"),
+      Length: findType("Length"),
+      List_String: findType("List<String>"),
+      List_dynamic: findType("List<@>"),
+      List_int: findType("List<int>"),
+      Map_String_String: findType("Map<String,String>"),
+      Map_dynamic_dynamic: findType("Map<@,@>"),
+      Map_of_nullable_Object_and_nullable_Object: findType("Map<Object?,Object?>"),
+      MappedIterable_String_Frame: findType("MappedIterable<String,Frame>"),
+      MappedListIterable_Frame_Frame: findType("MappedListIterable<Frame,Frame>"),
+      MappedListIterable_String_Trace: findType("MappedListIterable<String,Trace>"),
+      MappedListIterable_String_dynamic: findType("MappedListIterable<String,@>"),
+      MimeType: findType("MimeType"),
+      NativeUint8List: findType("NativeUint8List"),
+      Node: findType("Node"),
+      Null: findType("Null"),
+      Number: findType("Number"),
+      Object: findType("Object"),
+      Plugin: findType("Plugin"),
+      Record: findType("Record"),
+      Rectangle_num: findType("Rectangle<num>"),
+      RegExpMatch: findType("RegExpMatch"),
+      SourceBuffer: findType("SourceBuffer"),
+      SpeechGrammar: findType("SpeechGrammar"),
+      SpeechRecognitionResult: findType("SpeechRecognitionResult"),
+      StackTrace: findType("StackTrace"),
+      String: findType("String"),
+      StyleSheet: findType("StyleSheet"),
+      Symbol: findType("Symbol0"),
+      TextTrack: findType("TextTrack"),
+      TextTrackCue: findType("TextTrackCue"),
+      Timer: findType("Timer"),
+      Touch: findType("Touch"),
+      Trace: findType("Trace"),
+      Trace_Function_String: findType("Trace(String)"),
+      Transform: findType("Transform"),
+      TrustedGetRuntimeType: findType("TrustedGetRuntimeType"),
+      TypeError: findType("TypeError"),
+      Uint16List: findType("Uint16List"),
+      Uint32List: findType("Uint32List"),
+      Uint8ClampedList: findType("Uint8ClampedList"),
+      Uint8List: findType("Uint8List"),
+      UnknownJavaScriptObject: findType("UnknownJavaScriptObject"),
+      UnmodifiableMapView_String_String: findType("UnmodifiableMapView<String,String>"),
+      Uri: findType("Uri"),
+      WhereIterable_String: findType("WhereIterable<String>"),
+      WhereTypeIterable_String: findType("WhereTypeIterable<String>"),
+      Zone: findType("Zone"),
+      _AsyncCompleter_dynamic: findType("_AsyncCompleter<@>"),
+      _Future_dynamic: findType("_Future<@>"),
+      _Future_int: findType("_Future<int>"),
+      _Future_void: findType("_Future<~>"),
+      _IdentityHashMap_of_nullable_Object_and_nullable_Object: findType("_IdentityHashMap<Object?,Object?>"),
+      _StreamControllerAddStreamState_nullable_Object: findType("_StreamControllerAddStreamState<Object?>"),
+      _SyncCompleter_dynamic: findType("_SyncCompleter<@>"),
+      _ZoneFunction_of_void_Function_Zone_ZoneDelegate_Zone_Object_StackTrace: findType("_ZoneFunction<~(Zone,ZoneDelegate,Zone,Object,StackTrace)>"),
+      bool: findType("bool"),
+      bool_Function_Frame: findType("bool(Frame)"),
+      bool_Function_Object: findType("bool(Object)"),
+      bool_Function_String: findType("bool(String)"),
+      double: findType("double"),
+      dynamic: findType("@"),
+      dynamic_Function: findType("@()"),
+      dynamic_Function_Object: findType("@(Object)"),
+      dynamic_Function_Object_StackTrace: findType("@(Object,StackTrace)"),
+      dynamic_Function_String: findType("@(String)"),
+      int: findType("int"),
+      legacy_Never: findType("0&*"),
+      legacy_Object: findType("Object*"),
+      nullable_Future_Null: findType("Future<Null>?"),
+      nullable_Gamepad: findType("Gamepad?"),
+      nullable_JavaScriptObject: findType("JavaScriptObject?"),
+      nullable_List_dynamic: findType("List<@>?"),
+      nullable_Map_of_nullable_Object_and_nullable_Object: findType("Map<Object?,Object?>?"),
+      nullable_Object: findType("Object?"),
+      nullable_StackTrace: findType("StackTrace?"),
+      nullable_Zone: findType("Zone?"),
+      nullable_ZoneDelegate: findType("ZoneDelegate?"),
+      nullable_ZoneSpecification: findType("ZoneSpecification?"),
+      nullable__DelayedEvent_dynamic: findType("_DelayedEvent<@>?"),
+      nullable__FutureListener_dynamic_dynamic: findType("_FutureListener<@,@>?"),
+      nullable__LinkedHashSetCell: findType("_LinkedHashSetCell?"),
+      nullable_void_Function: findType("~()?"),
+      num: findType("num"),
+      void: findType("~"),
+      void_Function: findType("~()"),
+      void_Function_$opt_dynamic: findType("~([@])"),
+      void_Function_JavaScriptObject: findType("~(JavaScriptObject)"),
+      void_Function_Object: findType("~(Object)"),
+      void_Function_Object_StackTrace: findType("~(Object,StackTrace)"),
+      void_Function_String_String: findType("~(String,String)"),
+      void_Function_String_dynamic: findType("~(String,@)"),
+      void_Function_Timer: findType("~(Timer)")
+    };
+  })();
+  (function constants() {
+    var makeConstList = hunkHelpers.makeConstList;
+    B.Interceptor_methods = J.Interceptor.prototype;
+    B.JSArray_methods = J.JSArray.prototype;
+    B.JSInt_methods = J.JSInt.prototype;
+    B.JSNumber_methods = J.JSNumber.prototype;
+    B.JSString_methods = J.JSString.prototype;
+    B.JavaScriptFunction_methods = J.JavaScriptFunction.prototype;
+    B.JavaScriptObject_methods = J.JavaScriptObject.prototype;
+    B.NativeUint8List_methods = A.NativeUint8List.prototype;
+    B.PlainJavaScriptObject_methods = J.PlainJavaScriptObject.prototype;
+    B.UnknownJavaScriptObject_methods = J.UnknownJavaScriptObject.prototype;
+    B.AsciiEncoder_127 = new A.AsciiEncoder(127);
+    B.CONSTANT = new A.Instantiation1(A.math__max$closure(), A.findType("Instantiation1<int>"));
+    B.C_AsciiCodec = new A.AsciiCodec();
+    B.C_Base64Encoder = new A.Base64Encoder();
+    B.C_Base64Codec = new A.Base64Codec();
+    B.C_EmptyIterator = new A.EmptyIterator(A.findType("EmptyIterator<0&>"));
+    B.C_JS_CONST = function getTagFallback(o) {
+  var s = Object.prototype.toString.call(o);
+  return s.substring(8, s.length - 1);
+};
+    B.C_JS_CONST0 = function() {
+  var toStringFunction = Object.prototype.toString;
+  function getTag(o) {
+    var s = toStringFunction.call(o);
+    return s.substring(8, s.length - 1);
+  }
+  function getUnknownTag(object, tag) {
+    if (/^HTML[A-Z].*Element$/.test(tag)) {
+      var name = toStringFunction.call(object);
+      if (name == "[object Object]") return null;
+      return "HTMLElement";
+    }
+  }
+  function getUnknownTagGenericBrowser(object, tag) {
+    if (self.HTMLElement && object instanceof HTMLElement) return "HTMLElement";
+    return getUnknownTag(object, tag);
+  }
+  function prototypeForTag(tag) {
+    if (typeof window == "undefined") return null;
+    if (typeof window[tag] == "undefined") return null;
+    var constructor = window[tag];
+    if (typeof constructor != "function") return null;
+    return constructor.prototype;
+  }
+  function discriminator(tag) { return null; }
+  var isBrowser = typeof navigator == "object";
+  return {
+    getTag: getTag,
+    getUnknownTag: isBrowser ? getUnknownTagGenericBrowser : getUnknownTag,
+    prototypeForTag: prototypeForTag,
+    discriminator: discriminator };
+};
+    B.C_JS_CONST6 = function(getTagFallback) {
+  return function(hooks) {
+    if (typeof navigator != "object") return hooks;
+    var ua = navigator.userAgent;
+    if (ua.indexOf("DumpRenderTree") >= 0) return hooks;
+    if (ua.indexOf("Chrome") >= 0) {
+      function confirm(p) {
+        return typeof window == "object" && window[p] && window[p].name == p;
+      }
+      if (confirm("Window") && confirm("HTMLElement")) return hooks;
+    }
+    hooks.getTag = getTagFallback;
+  };
+};
+    B.C_JS_CONST1 = function(hooks) {
+  if (typeof dartExperimentalFixupGetTag != "function") return hooks;
+  hooks.getTag = dartExperimentalFixupGetTag(hooks.getTag);
+};
+    B.C_JS_CONST2 = function(hooks) {
+  var getTag = hooks.getTag;
+  var prototypeForTag = hooks.prototypeForTag;
+  function getTagFixed(o) {
+    var tag = getTag(o);
+    if (tag == "Document") {
+      if (!!o.xmlVersion) return "!Document";
+      return "!HTMLDocument";
+    }
+    return tag;
+  }
+  function prototypeForTagFixed(tag) {
+    if (tag == "Document") return null;
+    return prototypeForTag(tag);
+  }
+  hooks.getTag = getTagFixed;
+  hooks.prototypeForTag = prototypeForTagFixed;
+};
+    B.C_JS_CONST5 = function(hooks) {
+  var userAgent = typeof navigator == "object" ? navigator.userAgent : "";
+  if (userAgent.indexOf("Firefox") == -1) return hooks;
+  var getTag = hooks.getTag;
+  var quickMap = {
+    "BeforeUnloadEvent": "Event",
+    "DataTransfer": "Clipboard",
+    "GeoGeolocation": "Geolocation",
+    "Location": "!Location",
+    "WorkerMessageEvent": "MessageEvent",
+    "XMLDocument": "!Document"};
+  function getTagFirefox(o) {
+    var tag = getTag(o);
+    return quickMap[tag] || tag;
+  }
+  hooks.getTag = getTagFirefox;
+};
+    B.C_JS_CONST4 = function(hooks) {
+  var userAgent = typeof navigator == "object" ? navigator.userAgent : "";
+  if (userAgent.indexOf("Trident/") == -1) return hooks;
+  var getTag = hooks.getTag;
+  var quickMap = {
+    "BeforeUnloadEvent": "Event",
+    "DataTransfer": "Clipboard",
+    "HTMLDDElement": "HTMLElement",
+    "HTMLDTElement": "HTMLElement",
+    "HTMLPhraseElement": "HTMLElement",
+    "Position": "Geoposition"
+  };
+  function getTagIE(o) {
+    var tag = getTag(o);
+    var newTag = quickMap[tag];
+    if (newTag) return newTag;
+    if (tag == "Object") {
+      if (window.DataView && (o instanceof window.DataView)) return "DataView";
+    }
+    return tag;
+  }
+  function prototypeForTagIE(tag) {
+    var constructor = window[tag];
+    if (constructor == null) return null;
+    return constructor.prototype;
+  }
+  hooks.getTag = getTagIE;
+  hooks.prototypeForTag = prototypeForTagIE;
+};
+    B.C_JS_CONST3 = function(hooks) { return hooks; }
+;
+    B.C_JsonCodec = new A.JsonCodec();
+    B.C_OutOfMemoryError = new A.OutOfMemoryError();
+    B.C_SentinelValue = new A.SentinelValue();
+    B.C_Utf8Codec = new A.Utf8Codec();
+    B.C_Utf8Encoder = new A.Utf8Encoder();
+    B.C__DelayedDone = new A._DelayedDone();
+    B.C__Required = new A._Required();
+    B.C__RootZone = new A._RootZone();
+    B.Duration_0 = new A.Duration(0);
+    B.JsonDecoder_null = new A.JsonDecoder(null);
+    B.JsonEncoder_null = new A.JsonEncoder(null);
+    B.List_M1A = A._setArrayType(makeConstList([0, 0, 24576, 1023, 65534, 34815, 65534, 18431]), type$.JSArray_int);
+    B.List_MMm = A._setArrayType(makeConstList([0, 0, 26624, 1023, 65534, 2047, 65534, 2047]), type$.JSArray_int);
+    B.List_OL3 = A._setArrayType(makeConstList([0, 0, 32722, 12287, 65534, 34815, 65534, 18431]), type$.JSArray_int);
+    B.List_XRg0 = A._setArrayType(makeConstList([0, 0, 32722, 12287, 65535, 34815, 65534, 18431]), type$.JSArray_int);
+    B.List_XRg = A._setArrayType(makeConstList([0, 0, 65490, 12287, 65535, 34815, 65534, 18431]), type$.JSArray_int);
+    B.List_YmH = A._setArrayType(makeConstList([0, 0, 32776, 33792, 1, 10240, 0, 0]), type$.JSArray_int);
+    B.List_ejq = A._setArrayType(makeConstList([0, 0, 32754, 11263, 65534, 34815, 65534, 18431]), type$.JSArray_int);
+    B.List_empty = A._setArrayType(makeConstList([]), type$.JSArray_String);
+    B.List_empty0 = A._setArrayType(makeConstList([]), type$.JSArray_dynamic);
+    B.List_oFp = A._setArrayType(makeConstList([0, 0, 65490, 45055, 65535, 34815, 65534, 18431]), type$.JSArray_int);
+    B.List_yzX = A._setArrayType(makeConstList([0, 0, 27858, 1023, 65534, 51199, 65535, 32767]), type$.JSArray_int);
+    B.Object_empty = {};
+    B.Map_empty = new A.ConstantStringMap(B.Object_empty, [], A.findType("ConstantStringMap<String,String>"));
+    B.Map_empty0 = new A.ConstantStringMap(B.Object_empty, [], A.findType("ConstantStringMap<Symbol0,@>"));
+    B.Symbol_call = new A.Symbol("call");
+    B.Type_ByteBuffer_RkP = A.typeLiteral("ByteBuffer");
+    B.Type_ByteData_zNC = A.typeLiteral("ByteData");
+    B.Type_Float32List_LB7 = A.typeLiteral("Float32List");
+    B.Type_Float64List_LB7 = A.typeLiteral("Float64List");
+    B.Type_Int16List_uXf = A.typeLiteral("Int16List");
+    B.Type_Int32List_O50 = A.typeLiteral("Int32List");
+    B.Type_Int8List_ekJ = A.typeLiteral("Int8List");
+    B.Type_Object_xQ6 = A.typeLiteral("Object");
+    B.Type_Uint16List_2bx = A.typeLiteral("Uint16List");
+    B.Type_Uint32List_2bx = A.typeLiteral("Uint32List");
+    B.Type_Uint8ClampedList_Jik = A.typeLiteral("Uint8ClampedList");
+    B.Type_Uint8List_WLA = A.typeLiteral("Uint8List");
+    B.Utf8Decoder_false = new A.Utf8Decoder(false);
+    B._StringStackTrace_3uE = new A._StringStackTrace("");
+    B._ZoneFunction_3bB = new A._ZoneFunction(B.C__RootZone, A.async___rootCreatePeriodicTimer$closure(), A.findType("_ZoneFunction<Timer(Zone,ZoneDelegate,Zone,Duration,~(Timer))>"));
+    B._ZoneFunction_7G2 = new A._ZoneFunction(B.C__RootZone, A.async___rootRegisterBinaryCallback$closure(), A.findType("_ZoneFunction<0^(1^,2^)(Zone,ZoneDelegate,Zone,0^(1^,2^))<Object?,Object?,Object?>>"));
+    B._ZoneFunction_Eeh = new A._ZoneFunction(B.C__RootZone, A.async___rootRegisterUnaryCallback$closure(), A.findType("_ZoneFunction<0^(1^)(Zone,ZoneDelegate,Zone,0^(1^))<Object?,Object?>>"));
+    B._ZoneFunction_NMc = new A._ZoneFunction(B.C__RootZone, A.async___rootHandleUncaughtError$closure(), type$._ZoneFunction_of_void_Function_Zone_ZoneDelegate_Zone_Object_StackTrace);
+    B._ZoneFunction__RootZone__rootCreateTimer = new A._ZoneFunction(B.C__RootZone, A.async___rootCreateTimer$closure(), A.findType("_ZoneFunction<Timer(Zone,ZoneDelegate,Zone,Duration,~())>"));
+    B._ZoneFunction__RootZone__rootErrorCallback = new A._ZoneFunction(B.C__RootZone, A.async___rootErrorCallback$closure(), A.findType("_ZoneFunction<AsyncError?(Zone,ZoneDelegate,Zone,Object,StackTrace?)>"));
+    B._ZoneFunction__RootZone__rootFork = new A._ZoneFunction(B.C__RootZone, A.async___rootFork$closure(), A.findType("_ZoneFunction<Zone(Zone,ZoneDelegate,Zone,ZoneSpecification?,Map<Object?,Object?>?)>"));
+    B._ZoneFunction__RootZone__rootPrint = new A._ZoneFunction(B.C__RootZone, A.async___rootPrint$closure(), A.findType("_ZoneFunction<~(Zone,ZoneDelegate,Zone,String)>"));
+    B._ZoneFunction__RootZone__rootRegisterCallback = new A._ZoneFunction(B.C__RootZone, A.async___rootRegisterCallback$closure(), A.findType("_ZoneFunction<0^()(Zone,ZoneDelegate,Zone,0^())<Object?>>"));
+    B._ZoneFunction__RootZone__rootRun = new A._ZoneFunction(B.C__RootZone, A.async___rootRun$closure(), A.findType("_ZoneFunction<0^(Zone,ZoneDelegate,Zone,0^())<Object?>>"));
+    B._ZoneFunction__RootZone__rootRunBinary = new A._ZoneFunction(B.C__RootZone, A.async___rootRunBinary$closure(), A.findType("_ZoneFunction<0^(Zone,ZoneDelegate,Zone,0^(1^,2^),1^,2^)<Object?,Object?,Object?>>"));
+    B._ZoneFunction__RootZone__rootRunUnary = new A._ZoneFunction(B.C__RootZone, A.async___rootRunUnary$closure(), A.findType("_ZoneFunction<0^(Zone,ZoneDelegate,Zone,0^(1^),1^)<Object?,Object?>>"));
+    B._ZoneFunction__RootZone__rootScheduleMicrotask = new A._ZoneFunction(B.C__RootZone, A.async___rootScheduleMicrotask$closure(), A.findType("_ZoneFunction<~(Zone,ZoneDelegate,Zone,~())>"));
+  })();
+  (function staticFields() {
+    $._JS_INTEROP_INTERCEPTOR_TAG = null;
+    $.toStringVisiting = A._setArrayType([], type$.JSArray_Object);
+    $.Primitives__identityHashCodeProperty = null;
+    $.BoundClosure__receiverFieldNameCache = null;
+    $.BoundClosure__interceptorFieldNameCache = null;
+    $.getTagFunction = null;
+    $.alternateTagFunction = null;
+    $.prototypeForTagFunction = null;
+    $.dispatchRecordsForInstanceTags = null;
+    $.interceptorsForUncacheableTags = null;
+    $.initNativeDispatchFlag = null;
+    $._nextCallback = null;
+    $._lastCallback = null;
+    $._lastPriorityCallback = null;
+    $._isInCallbackLoop = false;
+    $.Zone__current = B.C__RootZone;
+    $._RootZone__rootDelegate = null;
+    $.Uri__cachedBaseString = "";
+    $.Uri__cachedBaseUri = null;
+    $._currentUriBase = null;
+    $._current = null;
+    $._iframes = A.LinkedHashMap_LinkedHashMap$_empty(type$.int, type$.JavaScriptObject);
+    $._subscriptions = A.LinkedHashMap_LinkedHashMap$_empty(type$.int, A.findType("StreamSubscription<~>"));
+    $._domSubscriptions = A.LinkedHashMap_LinkedHashMap$_empty(type$.int, A.findType("Subscription"));
+  })();
+  (function lazyInitializers() {
+    var _lazyFinal = hunkHelpers.lazyFinal;
+    _lazyFinal($, "DART_CLOSURE_PROPERTY_NAME", "$get$DART_CLOSURE_PROPERTY_NAME", () => A.getIsolateAffinityTag("_$dart_dartClosure"));
+    _lazyFinal($, "nullFuture", "$get$nullFuture", () => B.C__RootZone.run$1$1(new A.nullFuture_closure(), A.findType("Future<Null>")));
+    _lazyFinal($, "TypeErrorDecoder_noSuchMethodPattern", "$get$TypeErrorDecoder_noSuchMethodPattern", () => A.TypeErrorDecoder_extractPattern(A.TypeErrorDecoder_provokeCallErrorOn({
+      toString: function() {
+        return "$receiver$";
+      }
+    })));
+    _lazyFinal($, "TypeErrorDecoder_notClosurePattern", "$get$TypeErrorDecoder_notClosurePattern", () => A.TypeErrorDecoder_extractPattern(A.TypeErrorDecoder_provokeCallErrorOn({$method$: null,
+      toString: function() {
+        return "$receiver$";
+      }
+    })));
+    _lazyFinal($, "TypeErrorDecoder_nullCallPattern", "$get$TypeErrorDecoder_nullCallPattern", () => A.TypeErrorDecoder_extractPattern(A.TypeErrorDecoder_provokeCallErrorOn(null)));
+    _lazyFinal($, "TypeErrorDecoder_nullLiteralCallPattern", "$get$TypeErrorDecoder_nullLiteralCallPattern", () => A.TypeErrorDecoder_extractPattern(function() {
+      var $argumentsExpr$ = "$arguments$";
+      try {
+        null.$method$($argumentsExpr$);
+      } catch (e) {
+        return e.message;
+      }
+    }()));
+    _lazyFinal($, "TypeErrorDecoder_undefinedCallPattern", "$get$TypeErrorDecoder_undefinedCallPattern", () => A.TypeErrorDecoder_extractPattern(A.TypeErrorDecoder_provokeCallErrorOn(void 0)));
+    _lazyFinal($, "TypeErrorDecoder_undefinedLiteralCallPattern", "$get$TypeErrorDecoder_undefinedLiteralCallPattern", () => A.TypeErrorDecoder_extractPattern(function() {
+      var $argumentsExpr$ = "$arguments$";
+      try {
+        (void 0).$method$($argumentsExpr$);
+      } catch (e) {
+        return e.message;
+      }
+    }()));
+    _lazyFinal($, "TypeErrorDecoder_nullPropertyPattern", "$get$TypeErrorDecoder_nullPropertyPattern", () => A.TypeErrorDecoder_extractPattern(A.TypeErrorDecoder_provokePropertyErrorOn(null)));
+    _lazyFinal($, "TypeErrorDecoder_nullLiteralPropertyPattern", "$get$TypeErrorDecoder_nullLiteralPropertyPattern", () => A.TypeErrorDecoder_extractPattern(function() {
+      try {
+        null.$method$;
+      } catch (e) {
+        return e.message;
+      }
+    }()));
+    _lazyFinal($, "TypeErrorDecoder_undefinedPropertyPattern", "$get$TypeErrorDecoder_undefinedPropertyPattern", () => A.TypeErrorDecoder_extractPattern(A.TypeErrorDecoder_provokePropertyErrorOn(void 0)));
+    _lazyFinal($, "TypeErrorDecoder_undefinedLiteralPropertyPattern", "$get$TypeErrorDecoder_undefinedLiteralPropertyPattern", () => A.TypeErrorDecoder_extractPattern(function() {
+      try {
+        (void 0).$method$;
+      } catch (e) {
+        return e.message;
+      }
+    }()));
+    _lazyFinal($, "_AsyncRun__scheduleImmediateClosure", "$get$_AsyncRun__scheduleImmediateClosure", () => A._AsyncRun__initializeScheduleImmediate());
+    _lazyFinal($, "Future__nullFuture", "$get$Future__nullFuture", () => A.findType("_Future<Null>")._as($.$get$nullFuture()));
+    _lazyFinal($, "_RootZone__rootMap", "$get$_RootZone__rootMap", () => {
+      var t1 = type$.dynamic;
+      return A.HashMap_HashMap(t1, t1);
+    });
+    _lazyFinal($, "Utf8Decoder__decoder", "$get$Utf8Decoder__decoder", () => new A.Utf8Decoder__decoder_closure().call$0());
+    _lazyFinal($, "Utf8Decoder__decoderNonfatal", "$get$Utf8Decoder__decoderNonfatal", () => new A.Utf8Decoder__decoderNonfatal_closure().call$0());
+    _lazyFinal($, "_Base64Decoder__inverseAlphabet", "$get$_Base64Decoder__inverseAlphabet", () => A.NativeInt8List__create1(A._ensureNativeList(A._setArrayType([-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -2, -2, -2, -2, -2, 62, -2, 62, -2, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -2, -2, -2, -1, -2, -2, -2, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -2, -2, -2, -2, 63, -2, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -2, -2, -2, -2, -2], type$.JSArray_int))));
+    _lazyFinal($, "_Uri__isWindowsCached", "$get$_Uri__isWindowsCached", () => typeof process != "undefined" && Object.prototype.toString.call(process) == "[object process]" && process.platform == "win32");
+    _lazyFinal($, "_Uri__needsNoEncoding", "$get$_Uri__needsNoEncoding", () => A.RegExp_RegExp("^[\\-\\.0-9A-Z_a-z~]*$", false));
+    _lazyFinal($, "_hashSeed", "$get$_hashSeed", () => A.objectHashCode(B.Type_Object_xQ6));
+    _lazyFinal($, "_scannerTables", "$get$_scannerTables", () => A._createTables());
+    _lazyFinal($, "windows", "$get$windows", () => A.Context_Context($.$get$Style_windows()));
+    _lazyFinal($, "context", "$get$context", () => new A.Context($.$get$Style_platform(), null));
+    _lazyFinal($, "Style_posix", "$get$Style_posix", () => new A.PosixStyle(A.RegExp_RegExp("/", false), A.RegExp_RegExp("[^/]$", false), A.RegExp_RegExp("^/", false)));
+    _lazyFinal($, "Style_windows", "$get$Style_windows", () => new A.WindowsStyle(A.RegExp_RegExp("[/\\\\]", false), A.RegExp_RegExp("[^/\\\\]$", false), A.RegExp_RegExp("^(\\\\\\\\[^\\\\]+\\\\[^\\\\/]+|[a-zA-Z]:[/\\\\])", false), A.RegExp_RegExp("^[/\\\\](?![/\\\\])", false)));
+    _lazyFinal($, "Style_url", "$get$Style_url", () => new A.UrlStyle(A.RegExp_RegExp("/", false), A.RegExp_RegExp("(^[a-zA-Z][-+.a-zA-Z\\d]*://|[^/])$", false), A.RegExp_RegExp("[a-zA-Z][-+.a-zA-Z\\d]*://[^/]*", false), A.RegExp_RegExp("^/", false)));
+    _lazyFinal($, "Style_platform", "$get$Style_platform", () => A.Style__getPlatformStyle());
+    _lazyFinal($, "_vmFrame", "$get$_vmFrame", () => A.RegExp_RegExp("^#\\d+\\s+(\\S.*) \\((.+?)((?::\\d+){0,2})\\)$", false));
+    _lazyFinal($, "_v8Frame", "$get$_v8Frame", () => A.RegExp_RegExp("^\\s*at (?:(\\S.*?)(?: \\[as [^\\]]+\\])? \\((.*)\\)|(.*))$", false));
+    _lazyFinal($, "_v8UrlLocation", "$get$_v8UrlLocation", () => A.RegExp_RegExp("^(.*?):(\\d+)(?::(\\d+))?$|native$", false));
+    _lazyFinal($, "_v8EvalLocation", "$get$_v8EvalLocation", () => A.RegExp_RegExp("^eval at (?:\\S.*?) \\((.*)\\)(?:, .*?:\\d+:\\d+)?$", false));
+    _lazyFinal($, "_firefoxEvalLocation", "$get$_firefoxEvalLocation", () => A.RegExp_RegExp("(\\S+)@(\\S+) line (\\d+) >.* (Function|eval):\\d+:\\d+", false));
+    _lazyFinal($, "_firefoxSafariFrame", "$get$_firefoxSafariFrame", () => A.RegExp_RegExp("^(?:([^@(/]*)(?:\\(.*\\))?((?:/[^/]*)*)(?:\\(.*\\))?@)?(.*?):(\\d*)(?::(\\d*))?$", false));
+    _lazyFinal($, "_friendlyFrame", "$get$_friendlyFrame", () => A.RegExp_RegExp("^(\\S+)(?: (\\d+)(?::(\\d+))?)?\\s+([^\\d].*)$", false));
+    _lazyFinal($, "_asyncBody", "$get$_asyncBody", () => A.RegExp_RegExp("<(<anonymous closure>|[^>]+)_async_body>", false));
+    _lazyFinal($, "_initialDot", "$get$_initialDot", () => A.RegExp_RegExp("^\\.", false));
+    _lazyFinal($, "Frame__uriRegExp", "$get$Frame__uriRegExp", () => A.RegExp_RegExp("^[a-zA-Z][-+.a-zA-Z\\d]*://", false));
+    _lazyFinal($, "Frame__windowsRegExp", "$get$Frame__windowsRegExp", () => A.RegExp_RegExp("^([a-zA-Z]:[\\\\/]|\\\\\\\\)", false));
+    _lazyFinal($, "_terseRegExp", "$get$_terseRegExp", () => A.RegExp_RegExp("(-patch)?([/\\\\].*)?$", false));
+    _lazyFinal($, "_v8Trace", "$get$_v8Trace", () => A.RegExp_RegExp("\\n    ?at ", false));
+    _lazyFinal($, "_v8TraceLine", "$get$_v8TraceLine", () => A.RegExp_RegExp("    ?at ", false));
+    _lazyFinal($, "_firefoxEvalTrace", "$get$_firefoxEvalTrace", () => A.RegExp_RegExp("@\\S+ line \\d+ >.* (Function|eval):\\d+:\\d+", false));
+    _lazyFinal($, "_firefoxSafariTrace", "$get$_firefoxSafariTrace", () => A.RegExp_RegExp("^(([.0-9A-Za-z_$/<]|\\(.*\\))*@)?[^\\s]*:\\d*$", true));
+    _lazyFinal($, "_friendlyTrace", "$get$_friendlyTrace", () => A.RegExp_RegExp("^[^\\s<][^\\s]*( \\d+(:\\d+)?)?[ \\t]+[^\\s]+$", true));
+    _lazyFinal($, "vmChainGap", "$get$vmChainGap", () => A.RegExp_RegExp("^<asynchronous suspension>\\n?$", true));
+    _lazyFinal($, "_currentUrl", "$get$_currentUrl", () => A.Uri_parse(A.getProperty(A.getProperty(self.window, "location", type$.JavaScriptObject), "href", type$.String)));
+  })();
+  (function nativeSupport() {
+    !function() {
+      var intern = function(s) {
+        var o = {};
+        o[s] = 1;
+        return Object.keys(hunkHelpers.convertToFastObject(o))[0];
+      };
+      init.getIsolateTag = function(name) {
+        return intern("___dart_" + name + init.isolateTag);
+      };
+      var tableProperty = "___dart_isolate_tags_";
+      var usedProperties = Object[tableProperty] || (Object[tableProperty] = Object.create(null));
+      var rootProperty = "_ZxYxX";
+      for (var i = 0;; i++) {
+        var property = intern(rootProperty + "_" + i + "_");
+        if (!(property in usedProperties)) {
+          usedProperties[property] = 1;
+          init.isolateTag = property;
+          break;
+        }
+      }
+      init.dispatchPropertyName = init.getIsolateTag("dispatch_record");
+    }();
+    hunkHelpers.setOrUpdateInterceptorsByTag({WebGL: J.Interceptor, AbortPaymentEvent: J.JavaScriptObject, AnimationEffectReadOnly: J.JavaScriptObject, AnimationEffectTiming: J.JavaScriptObject, AnimationEffectTimingReadOnly: J.JavaScriptObject, AnimationEvent: J.JavaScriptObject, AnimationPlaybackEvent: J.JavaScriptObject, AnimationTimeline: J.JavaScriptObject, AnimationWorkletGlobalScope: J.JavaScriptObject, ApplicationCacheErrorEvent: J.JavaScriptObject, AuthenticatorAssertionResponse: J.JavaScriptObject, AuthenticatorAttestationResponse: J.JavaScriptObject, AuthenticatorResponse: J.JavaScriptObject, BackgroundFetchClickEvent: J.JavaScriptObject, BackgroundFetchEvent: J.JavaScriptObject, BackgroundFetchFailEvent: J.JavaScriptObject, BackgroundFetchFetch: J.JavaScriptObject, BackgroundFetchManager: J.JavaScriptObject, BackgroundFetchSettledFetch: J.JavaScriptObject, BackgroundFetchedEvent: J.JavaScriptObject, BarProp: J.JavaScriptObject, BarcodeDetector: J.JavaScriptObject, BeforeInstallPromptEvent: J.JavaScriptObject, BeforeUnloadEvent: J.JavaScriptObject, BlobEvent: J.JavaScriptObject, BluetoothRemoteGATTDescriptor: J.JavaScriptObject, Body: J.JavaScriptObject, BudgetState: J.JavaScriptObject, CacheStorage: J.JavaScriptObject, CanMakePaymentEvent: J.JavaScriptObject, CanvasGradient: J.JavaScriptObject, CanvasPattern: J.JavaScriptObject, CanvasRenderingContext2D: J.JavaScriptObject, Client: J.JavaScriptObject, Clients: J.JavaScriptObject, ClipboardEvent: J.JavaScriptObject, CloseEvent: J.JavaScriptObject, CompositionEvent: J.JavaScriptObject, CookieStore: J.JavaScriptObject, Coordinates: J.JavaScriptObject, Credential: J.JavaScriptObject, CredentialUserData: J.JavaScriptObject, CredentialsContainer: J.JavaScriptObject, Crypto: J.JavaScriptObject, CryptoKey: J.JavaScriptObject, CSS: J.JavaScriptObject, CSSVariableReferenceValue: J.JavaScriptObject, CustomElementRegistry: J.JavaScriptObject, CustomEvent: J.JavaScriptObject, DataTransfer: J.JavaScriptObject, DataTransferItem: J.JavaScriptObject, DeprecatedStorageInfo: J.JavaScriptObject, DeprecatedStorageQuota: J.JavaScriptObject, DeprecationReport: J.JavaScriptObject, DetectedBarcode: J.JavaScriptObject, DetectedFace: J.JavaScriptObject, DetectedText: J.JavaScriptObject, DeviceAcceleration: J.JavaScriptObject, DeviceMotionEvent: J.JavaScriptObject, DeviceOrientationEvent: J.JavaScriptObject, DeviceRotationRate: J.JavaScriptObject, DirectoryEntry: J.JavaScriptObject, webkitFileSystemDirectoryEntry: J.JavaScriptObject, FileSystemDirectoryEntry: J.JavaScriptObject, DirectoryReader: J.JavaScriptObject, WebKitDirectoryReader: J.JavaScriptObject, webkitFileSystemDirectoryReader: J.JavaScriptObject, FileSystemDirectoryReader: J.JavaScriptObject, DocumentOrShadowRoot: J.JavaScriptObject, DocumentTimeline: J.JavaScriptObject, DOMError: J.JavaScriptObject, DOMImplementation: J.JavaScriptObject, Iterator: J.JavaScriptObject, DOMMatrix: J.JavaScriptObject, DOMMatrixReadOnly: J.JavaScriptObject, DOMParser: J.JavaScriptObject, DOMPoint: J.JavaScriptObject, DOMPointReadOnly: J.JavaScriptObject, DOMQuad: J.JavaScriptObject, DOMStringMap: J.JavaScriptObject, Entry: J.JavaScriptObject, webkitFileSystemEntry: J.JavaScriptObject, FileSystemEntry: J.JavaScriptObject, ErrorEvent: J.JavaScriptObject, Event: J.JavaScriptObject, InputEvent: J.JavaScriptObject, SubmitEvent: J.JavaScriptObject, ExtendableEvent: J.JavaScriptObject, ExtendableMessageEvent: J.JavaScriptObject, External: J.JavaScriptObject, FaceDetector: J.JavaScriptObject, FederatedCredential: J.JavaScriptObject, FetchEvent: J.JavaScriptObject, FileEntry: J.JavaScriptObject, webkitFileSystemFileEntry: J.JavaScriptObject, FileSystemFileEntry: J.JavaScriptObject, DOMFileSystem: J.JavaScriptObject, WebKitFileSystem: J.JavaScriptObject, webkitFileSystem: J.JavaScriptObject, FileSystem: J.JavaScriptObject, FocusEvent: J.JavaScriptObject, FontFace: J.JavaScriptObject, FontFaceSetLoadEvent: J.JavaScriptObject, FontFaceSource: J.JavaScriptObject, ForeignFetchEvent: J.JavaScriptObject, FormData: J.JavaScriptObject, GamepadButton: J.JavaScriptObject, GamepadEvent: J.JavaScriptObject, GamepadPose: J.JavaScriptObject, Geolocation: J.JavaScriptObject, Position: J.JavaScriptObject, GeolocationPosition: J.JavaScriptObject, HashChangeEvent: J.JavaScriptObject, Headers: J.JavaScriptObject, HTMLHyperlinkElementUtils: J.JavaScriptObject, IdleDeadline: J.JavaScriptObject, ImageBitmap: J.JavaScriptObject, ImageBitmapRenderingContext: J.JavaScriptObject, ImageCapture: J.JavaScriptObject, ImageData: J.JavaScriptObject, InputDeviceCapabilities: J.JavaScriptObject, InstallEvent: J.JavaScriptObject, IntersectionObserver: J.JavaScriptObject, IntersectionObserverEntry: J.JavaScriptObject, InterventionReport: J.JavaScriptObject, KeyboardEvent: J.JavaScriptObject, KeyframeEffect: J.JavaScriptObject, KeyframeEffectReadOnly: J.JavaScriptObject, MediaCapabilities: J.JavaScriptObject, MediaCapabilitiesInfo: J.JavaScriptObject, MediaDeviceInfo: J.JavaScriptObject, MediaEncryptedEvent: J.JavaScriptObject, MediaError: J.JavaScriptObject, MediaKeyMessageEvent: J.JavaScriptObject, MediaKeyStatusMap: J.JavaScriptObject, MediaKeySystemAccess: J.JavaScriptObject, MediaKeys: J.JavaScriptObject, MediaKeysPolicy: J.JavaScriptObject, MediaMetadata: J.JavaScriptObject, MediaQueryListEvent: J.JavaScriptObject, MediaSession: J.JavaScriptObject, MediaSettingsRange: J.JavaScriptObject, MediaStreamEvent: J.JavaScriptObject, MediaStreamTrackEvent: J.JavaScriptObject, MemoryInfo: J.JavaScriptObject, MessageChannel: J.JavaScriptObject, MessageEvent: J.JavaScriptObject, Metadata: J.JavaScriptObject, MIDIConnectionEvent: J.JavaScriptObject, MIDIMessageEvent: J.JavaScriptObject, MouseEvent: J.JavaScriptObject, DragEvent: J.JavaScriptObject, MutationEvent: J.JavaScriptObject, MutationObserver: J.JavaScriptObject, WebKitMutationObserver: J.JavaScriptObject, MutationRecord: J.JavaScriptObject, NavigationPreloadManager: J.JavaScriptObject, Navigator: J.JavaScriptObject, NavigatorAutomationInformation: J.JavaScriptObject, NavigatorConcurrentHardware: J.JavaScriptObject, NavigatorCookies: J.JavaScriptObject, NavigatorUserMediaError: J.JavaScriptObject, NodeFilter: J.JavaScriptObject, NodeIterator: J.JavaScriptObject, NonDocumentTypeChildNode: J.JavaScriptObject, NonElementParentNode: J.JavaScriptObject, NoncedElement: J.JavaScriptObject, NotificationEvent: J.JavaScriptObject, OffscreenCanvasRenderingContext2D: J.JavaScriptObject, OverconstrainedError: J.JavaScriptObject, PageTransitionEvent: J.JavaScriptObject, PaintRenderingContext2D: J.JavaScriptObject, PaintSize: J.JavaScriptObject, PaintWorkletGlobalScope: J.JavaScriptObject, PasswordCredential: J.JavaScriptObject, Path2D: J.JavaScriptObject, PaymentAddress: J.JavaScriptObject, PaymentInstruments: J.JavaScriptObject, PaymentManager: J.JavaScriptObject, PaymentRequestEvent: J.JavaScriptObject, PaymentRequestUpdateEvent: J.JavaScriptObject, PaymentResponse: J.JavaScriptObject, PerformanceEntry: J.JavaScriptObject, PerformanceLongTaskTiming: J.JavaScriptObject, PerformanceMark: J.JavaScriptObject, PerformanceMeasure: J.JavaScriptObject, PerformanceNavigation: J.JavaScriptObject, PerformanceNavigationTiming: J.JavaScriptObject, PerformanceObserver: J.JavaScriptObject, PerformanceObserverEntryList: J.JavaScriptObject, PerformancePaintTiming: J.JavaScriptObject, PerformanceResourceTiming: J.JavaScriptObject, PerformanceServerTiming: J.JavaScriptObject, PerformanceTiming: J.JavaScriptObject, Permissions: J.JavaScriptObject, PhotoCapabilities: J.JavaScriptObject, PointerEvent: J.JavaScriptObject, PopStateEvent: J.JavaScriptObject, PositionError: J.JavaScriptObject, GeolocationPositionError: J.JavaScriptObject, Presentation: J.JavaScriptObject, PresentationConnectionAvailableEvent: J.JavaScriptObject, PresentationConnectionCloseEvent: J.JavaScriptObject, PresentationReceiver: J.JavaScriptObject, ProgressEvent: J.JavaScriptObject, PromiseRejectionEvent: J.JavaScriptObject, PublicKeyCredential: J.JavaScriptObject, PushEvent: J.JavaScriptObject, PushManager: J.JavaScriptObject, PushMessageData: J.JavaScriptObject, PushSubscription: J.JavaScriptObject, PushSubscriptionOptions: J.JavaScriptObject, Range: J.JavaScriptObject, RelatedApplication: J.JavaScriptObject, ReportBody: J.JavaScriptObject, ReportingObserver: J.JavaScriptObject, ResizeObserver: J.JavaScriptObject, ResizeObserverEntry: J.JavaScriptObject, RTCCertificate: J.JavaScriptObject, RTCDataChannelEvent: J.JavaScriptObject, RTCDTMFToneChangeEvent: J.JavaScriptObject, RTCIceCandidate: J.JavaScriptObject, mozRTCIceCandidate: J.JavaScriptObject, RTCLegacyStatsReport: J.JavaScriptObject, RTCPeerConnectionIceEvent: J.JavaScriptObject, RTCRtpContributingSource: J.JavaScriptObject, RTCRtpReceiver: J.JavaScriptObject, RTCRtpSender: J.JavaScriptObject, RTCSessionDescription: J.JavaScriptObject, mozRTCSessionDescription: J.JavaScriptObject, RTCStatsResponse: J.JavaScriptObject, RTCTrackEvent: J.JavaScriptObject, Screen: J.JavaScriptObject, ScrollState: J.JavaScriptObject, ScrollTimeline: J.JavaScriptObject, SecurityPolicyViolationEvent: J.JavaScriptObject, Selection: J.JavaScriptObject, SensorErrorEvent: J.JavaScriptObject, SharedArrayBuffer: J.JavaScriptObject, SpeechRecognitionAlternative: J.JavaScriptObject, SpeechRecognitionError: J.JavaScriptObject, SpeechRecognitionEvent: J.JavaScriptObject, SpeechSynthesisEvent: J.JavaScriptObject, SpeechSynthesisVoice: J.JavaScriptObject, StaticRange: J.JavaScriptObject, StorageEvent: J.JavaScriptObject, StorageManager: J.JavaScriptObject, StyleMedia: J.JavaScriptObject, StylePropertyMap: J.JavaScriptObject, StylePropertyMapReadonly: J.JavaScriptObject, SyncEvent: J.JavaScriptObject, SyncManager: J.JavaScriptObject, TaskAttributionTiming: J.JavaScriptObject, TextDetector: J.JavaScriptObject, TextEvent: J.JavaScriptObject, TextMetrics: J.JavaScriptObject, TouchEvent: J.JavaScriptObject, TrackDefault: J.JavaScriptObject, TrackEvent: J.JavaScriptObject, TransitionEvent: J.JavaScriptObject, WebKitTransitionEvent: J.JavaScriptObject, TreeWalker: J.JavaScriptObject, TrustedHTML: J.JavaScriptObject, TrustedScriptURL: J.JavaScriptObject, TrustedURL: J.JavaScriptObject, UIEvent: J.JavaScriptObject, UnderlyingSourceBase: J.JavaScriptObject, URLSearchParams: J.JavaScriptObject, VRCoordinateSystem: J.JavaScriptObject, VRDeviceEvent: J.JavaScriptObject, VRDisplayCapabilities: J.JavaScriptObject, VRDisplayEvent: J.JavaScriptObject, VREyeParameters: J.JavaScriptObject, VRFrameData: J.JavaScriptObject, VRFrameOfReference: J.JavaScriptObject, VRPose: J.JavaScriptObject, VRSessionEvent: J.JavaScriptObject, VRStageBounds: J.JavaScriptObject, VRStageBoundsPoint: J.JavaScriptObject, VRStageParameters: J.JavaScriptObject, ValidityState: J.JavaScriptObject, VideoPlaybackQuality: J.JavaScriptObject, VideoTrack: J.JavaScriptObject, VTTRegion: J.JavaScriptObject, WheelEvent: J.JavaScriptObject, WindowClient: J.JavaScriptObject, WorkletAnimation: J.JavaScriptObject, WorkletGlobalScope: J.JavaScriptObject, XPathEvaluator: J.JavaScriptObject, XPathExpression: J.JavaScriptObject, XPathNSResolver: J.JavaScriptObject, XPathResult: J.JavaScriptObject, XMLSerializer: J.JavaScriptObject, XSLTProcessor: J.JavaScriptObject, Bluetooth: J.JavaScriptObject, BluetoothCharacteristicProperties: J.JavaScriptObject, BluetoothRemoteGATTServer: J.JavaScriptObject, BluetoothRemoteGATTService: J.JavaScriptObject, BluetoothUUID: J.JavaScriptObject, BudgetService: J.JavaScriptObject, Cache: J.JavaScriptObject, DOMFileSystemSync: J.JavaScriptObject, DirectoryEntrySync: J.JavaScriptObject, DirectoryReaderSync: J.JavaScriptObject, EntrySync: J.JavaScriptObject, FileEntrySync: J.JavaScriptObject, FileReaderSync: J.JavaScriptObject, FileWriterSync: J.JavaScriptObject, HTMLAllCollection: J.JavaScriptObject, Mojo: J.JavaScriptObject, MojoHandle: J.JavaScriptObject, MojoInterfaceRequestEvent: J.JavaScriptObject, MojoWatcher: J.JavaScriptObject, NFC: J.JavaScriptObject, PagePopupController: J.JavaScriptObject, Report: J.JavaScriptObject, Request: J.JavaScriptObject, ResourceProgressEvent: J.JavaScriptObject, Response: J.JavaScriptObject, SubtleCrypto: J.JavaScriptObject, USBAlternateInterface: J.JavaScriptObject, USBConfiguration: J.JavaScriptObject, USBConnectionEvent: J.JavaScriptObject, USBDevice: J.JavaScriptObject, USBEndpoint: J.JavaScriptObject, USBInTransferResult: J.JavaScriptObject, USBInterface: J.JavaScriptObject, USBIsochronousInTransferPacket: J.JavaScriptObject, USBIsochronousInTransferResult: J.JavaScriptObject, USBIsochronousOutTransferPacket: J.JavaScriptObject, USBIsochronousOutTransferResult: J.JavaScriptObject, USBOutTransferResult: J.JavaScriptObject, WorkerLocation: J.JavaScriptObject, WorkerNavigator: J.JavaScriptObject, Worklet: J.JavaScriptObject, IDBCursor: J.JavaScriptObject, IDBCursorWithValue: J.JavaScriptObject, IDBFactory: J.JavaScriptObject, IDBIndex: J.JavaScriptObject, IDBKeyRange: J.JavaScriptObject, IDBObjectStore: J.JavaScriptObject, IDBObservation: J.JavaScriptObject, IDBObserver: J.JavaScriptObject, IDBObserverChanges: J.JavaScriptObject, IDBVersionChangeEvent: J.JavaScriptObject, SVGAngle: J.JavaScriptObject, SVGAnimatedAngle: J.JavaScriptObject, SVGAnimatedBoolean: J.JavaScriptObject, SVGAnimatedEnumeration: J.JavaScriptObject, SVGAnimatedInteger: J.JavaScriptObject, SVGAnimatedLength: J.JavaScriptObject, SVGAnimatedLengthList: J.JavaScriptObject, SVGAnimatedNumber: J.JavaScriptObject, SVGAnimatedNumberList: J.JavaScriptObject, SVGAnimatedPreserveAspectRatio: J.JavaScriptObject, SVGAnimatedRect: J.JavaScriptObject, SVGAnimatedString: J.JavaScriptObject, SVGAnimatedTransformList: J.JavaScriptObject, SVGMatrix: J.JavaScriptObject, SVGPoint: J.JavaScriptObject, SVGPreserveAspectRatio: J.JavaScriptObject, SVGRect: J.JavaScriptObject, SVGUnitTypes: J.JavaScriptObject, AudioListener: J.JavaScriptObject, AudioParam: J.JavaScriptObject, AudioProcessingEvent: J.JavaScriptObject, AudioTrack: J.JavaScriptObject, AudioWorkletGlobalScope: J.JavaScriptObject, AudioWorkletProcessor: J.JavaScriptObject, OfflineAudioCompletionEvent: J.JavaScriptObject, PeriodicWave: J.JavaScriptObject, WebGLActiveInfo: J.JavaScriptObject, ANGLEInstancedArrays: J.JavaScriptObject, ANGLE_instanced_arrays: J.JavaScriptObject, WebGLBuffer: J.JavaScriptObject, WebGLCanvas: J.JavaScriptObject, WebGLColorBufferFloat: J.JavaScriptObject, WebGLCompressedTextureASTC: J.JavaScriptObject, WebGLCompressedTextureATC: J.JavaScriptObject, WEBGL_compressed_texture_atc: J.JavaScriptObject, WebGLCompressedTextureETC1: J.JavaScriptObject, WEBGL_compressed_texture_etc1: J.JavaScriptObject, WebGLCompressedTextureETC: J.JavaScriptObject, WebGLCompressedTexturePVRTC: J.JavaScriptObject, WEBGL_compressed_texture_pvrtc: J.JavaScriptObject, WebGLCompressedTextureS3TC: J.JavaScriptObject, WEBGL_compressed_texture_s3tc: J.JavaScriptObject, WebGLCompressedTextureS3TCsRGB: J.JavaScriptObject, WebGLContextEvent: J.JavaScriptObject, WebGLDebugRendererInfo: J.JavaScriptObject, WEBGL_debug_renderer_info: J.JavaScriptObject, WebGLDebugShaders: J.JavaScriptObject, WEBGL_debug_shaders: J.JavaScriptObject, WebGLDepthTexture: J.JavaScriptObject, WEBGL_depth_texture: J.JavaScriptObject, WebGLDrawBuffers: J.JavaScriptObject, WEBGL_draw_buffers: J.JavaScriptObject, EXTsRGB: J.JavaScriptObject, EXT_sRGB: J.JavaScriptObject, EXTBlendMinMax: J.JavaScriptObject, EXT_blend_minmax: J.JavaScriptObject, EXTColorBufferFloat: J.JavaScriptObject, EXTColorBufferHalfFloat: J.JavaScriptObject, EXTDisjointTimerQuery: J.JavaScriptObject, EXTDisjointTimerQueryWebGL2: J.JavaScriptObject, EXTFragDepth: J.JavaScriptObject, EXT_frag_depth: J.JavaScriptObject, EXTShaderTextureLOD: J.JavaScriptObject, EXT_shader_texture_lod: J.JavaScriptObject, EXTTextureFilterAnisotropic: J.JavaScriptObject, EXT_texture_filter_anisotropic: J.JavaScriptObject, WebGLFramebuffer: J.JavaScriptObject, WebGLGetBufferSubDataAsync: J.JavaScriptObject, WebGLLoseContext: J.JavaScriptObject, WebGLExtensionLoseContext: J.JavaScriptObject, WEBGL_lose_context: J.JavaScriptObject, OESElementIndexUint: J.JavaScriptObject, OES_element_index_uint: J.JavaScriptObject, OESStandardDerivatives: J.JavaScriptObject, OES_standard_derivatives: J.JavaScriptObject, OESTextureFloat: J.JavaScriptObject, OES_texture_float: J.JavaScriptObject, OESTextureFloatLinear: J.JavaScriptObject, OES_texture_float_linear: J.JavaScriptObject, OESTextureHalfFloat: J.JavaScriptObject, OES_texture_half_float: J.JavaScriptObject, OESTextureHalfFloatLinear: J.JavaScriptObject, OES_texture_half_float_linear: J.JavaScriptObject, OESVertexArrayObject: J.JavaScriptObject, OES_vertex_array_object: J.JavaScriptObject, WebGLProgram: J.JavaScriptObject, WebGLQuery: J.JavaScriptObject, WebGLRenderbuffer: J.JavaScriptObject, WebGLRenderingContext: J.JavaScriptObject, WebGL2RenderingContext: J.JavaScriptObject, WebGLSampler: J.JavaScriptObject, WebGLShader: J.JavaScriptObject, WebGLShaderPrecisionFormat: J.JavaScriptObject, WebGLSync: J.JavaScriptObject, WebGLTexture: J.JavaScriptObject, WebGLTimerQueryEXT: J.JavaScriptObject, WebGLTransformFeedback: J.JavaScriptObject, WebGLUniformLocation: J.JavaScriptObject, WebGLVertexArrayObject: J.JavaScriptObject, WebGLVertexArrayObjectOES: J.JavaScriptObject, WebGL2RenderingContextBase: J.JavaScriptObject, ArrayBuffer: A.NativeByteBuffer, ArrayBufferView: A.NativeTypedData, DataView: A.NativeByteData, Float32Array: A.NativeFloat32List, Float64Array: A.NativeFloat64List, Int16Array: A.NativeInt16List, Int32Array: A.NativeInt32List, Int8Array: A.NativeInt8List, Uint16Array: A.NativeUint16List, Uint32Array: A.NativeUint32List, Uint8ClampedArray: A.NativeUint8ClampedList, CanvasPixelArray: A.NativeUint8ClampedList, Uint8Array: A.NativeUint8List, HTMLAudioElement: A.HtmlElement, HTMLBRElement: A.HtmlElement, HTMLBaseElement: A.HtmlElement, HTMLBodyElement: A.HtmlElement, HTMLButtonElement: A.HtmlElement, HTMLCanvasElement: A.HtmlElement, HTMLContentElement: A.HtmlElement, HTMLDListElement: A.HtmlElement, HTMLDataElement: A.HtmlElement, HTMLDataListElement: A.HtmlElement, HTMLDetailsElement: A.HtmlElement, HTMLDialogElement: A.HtmlElement, HTMLDivElement: A.HtmlElement, HTMLEmbedElement: A.HtmlElement, HTMLFieldSetElement: A.HtmlElement, HTMLHRElement: A.HtmlElement, HTMLHeadElement: A.HtmlElement, HTMLHeadingElement: A.HtmlElement, HTMLHtmlElement: A.HtmlElement, HTMLIFrameElement: A.HtmlElement, HTMLImageElement: A.HtmlElement, HTMLInputElement: A.HtmlElement, HTMLLIElement: A.HtmlElement, HTMLLabelElement: A.HtmlElement, HTMLLegendElement: A.HtmlElement, HTMLLinkElement: A.HtmlElement, HTMLMapElement: A.HtmlElement, HTMLMediaElement: A.HtmlElement, HTMLMenuElement: A.HtmlElement, HTMLMetaElement: A.HtmlElement, HTMLMeterElement: A.HtmlElement, HTMLModElement: A.HtmlElement, HTMLOListElement: A.HtmlElement, HTMLObjectElement: A.HtmlElement, HTMLOptGroupElement: A.HtmlElement, HTMLOptionElement: A.HtmlElement, HTMLOutputElement: A.HtmlElement, HTMLParagraphElement: A.HtmlElement, HTMLParamElement: A.HtmlElement, HTMLPictureElement: A.HtmlElement, HTMLPreElement: A.HtmlElement, HTMLProgressElement: A.HtmlElement, HTMLQuoteElement: A.HtmlElement, HTMLScriptElement: A.HtmlElement, HTMLShadowElement: A.HtmlElement, HTMLSlotElement: A.HtmlElement, HTMLSourceElement: A.HtmlElement, HTMLSpanElement: A.HtmlElement, HTMLStyleElement: A.HtmlElement, HTMLTableCaptionElement: A.HtmlElement, HTMLTableCellElement: A.HtmlElement, HTMLTableDataCellElement: A.HtmlElement, HTMLTableHeaderCellElement: A.HtmlElement, HTMLTableColElement: A.HtmlElement, HTMLTableElement: A.HtmlElement, HTMLTableRowElement: A.HtmlElement, HTMLTableSectionElement: A.HtmlElement, HTMLTemplateElement: A.HtmlElement, HTMLTextAreaElement: A.HtmlElement, HTMLTimeElement: A.HtmlElement, HTMLTitleElement: A.HtmlElement, HTMLTrackElement: A.HtmlElement, HTMLUListElement: A.HtmlElement, HTMLUnknownElement: A.HtmlElement, HTMLVideoElement: A.HtmlElement, HTMLDirectoryElement: A.HtmlElement, HTMLFontElement: A.HtmlElement, HTMLFrameElement: A.HtmlElement, HTMLFrameSetElement: A.HtmlElement, HTMLMarqueeElement: A.HtmlElement, HTMLElement: A.HtmlElement, AccessibleNodeList: A.AccessibleNodeList, HTMLAnchorElement: A.AnchorElement, HTMLAreaElement: A.AreaElement, Blob: A.Blob, CDATASection: A.CharacterData, CharacterData: A.CharacterData, Comment: A.CharacterData, ProcessingInstruction: A.CharacterData, Text: A.CharacterData, CSSPerspective: A.CssPerspective, CSSCharsetRule: A.CssRule, CSSConditionRule: A.CssRule, CSSFontFaceRule: A.CssRule, CSSGroupingRule: A.CssRule, CSSImportRule: A.CssRule, CSSKeyframeRule: A.CssRule, MozCSSKeyframeRule: A.CssRule, WebKitCSSKeyframeRule: A.CssRule, CSSKeyframesRule: A.CssRule, MozCSSKeyframesRule: A.CssRule, WebKitCSSKeyframesRule: A.CssRule, CSSMediaRule: A.CssRule, CSSNamespaceRule: A.CssRule, CSSPageRule: A.CssRule, CSSRule: A.CssRule, CSSStyleRule: A.CssRule, CSSSupportsRule: A.CssRule, CSSViewportRule: A.CssRule, CSSStyleDeclaration: A.CssStyleDeclaration, MSStyleCSSProperties: A.CssStyleDeclaration, CSS2Properties: A.CssStyleDeclaration, CSSImageValue: A.CssStyleValue, CSSKeywordValue: A.CssStyleValue, CSSNumericValue: A.CssStyleValue, CSSPositionValue: A.CssStyleValue, CSSResourceValue: A.CssStyleValue, CSSUnitValue: A.CssStyleValue, CSSURLImageValue: A.CssStyleValue, CSSStyleValue: A.CssStyleValue, CSSMatrixComponent: A.CssTransformComponent, CSSRotation: A.CssTransformComponent, CSSScale: A.CssTransformComponent, CSSSkew: A.CssTransformComponent, CSSTranslation: A.CssTransformComponent, CSSTransformComponent: A.CssTransformComponent, CSSTransformValue: A.CssTransformValue, CSSUnparsedValue: A.CssUnparsedValue, DataTransferItemList: A.DataTransferItemList, DOMException: A.DomException, ClientRectList: A.DomRectList, DOMRectList: A.DomRectList, DOMRectReadOnly: A.DomRectReadOnly, DOMStringList: A.DomStringList, DOMTokenList: A.DomTokenList, MathMLElement: A.Element, SVGAElement: A.Element, SVGAnimateElement: A.Element, SVGAnimateMotionElement: A.Element, SVGAnimateTransformElement: A.Element, SVGAnimationElement: A.Element, SVGCircleElement: A.Element, SVGClipPathElement: A.Element, SVGDefsElement: A.Element, SVGDescElement: A.Element, SVGDiscardElement: A.Element, SVGEllipseElement: A.Element, SVGFEBlendElement: A.Element, SVGFEColorMatrixElement: A.Element, SVGFEComponentTransferElement: A.Element, SVGFECompositeElement: A.Element, SVGFEConvolveMatrixElement: A.Element, SVGFEDiffuseLightingElement: A.Element, SVGFEDisplacementMapElement: A.Element, SVGFEDistantLightElement: A.Element, SVGFEFloodElement: A.Element, SVGFEFuncAElement: A.Element, SVGFEFuncBElement: A.Element, SVGFEFuncGElement: A.Element, SVGFEFuncRElement: A.Element, SVGFEGaussianBlurElement: A.Element, SVGFEImageElement: A.Element, SVGFEMergeElement: A.Element, SVGFEMergeNodeElement: A.Element, SVGFEMorphologyElement: A.Element, SVGFEOffsetElement: A.Element, SVGFEPointLightElement: A.Element, SVGFESpecularLightingElement: A.Element, SVGFESpotLightElement: A.Element, SVGFETileElement: A.Element, SVGFETurbulenceElement: A.Element, SVGFilterElement: A.Element, SVGForeignObjectElement: A.Element, SVGGElement: A.Element, SVGGeometryElement: A.Element, SVGGraphicsElement: A.Element, SVGImageElement: A.Element, SVGLineElement: A.Element, SVGLinearGradientElement: A.Element, SVGMarkerElement: A.Element, SVGMaskElement: A.Element, SVGMetadataElement: A.Element, SVGPathElement: A.Element, SVGPatternElement: A.Element, SVGPolygonElement: A.Element, SVGPolylineElement: A.Element, SVGRadialGradientElement: A.Element, SVGRectElement: A.Element, SVGScriptElement: A.Element, SVGSetElement: A.Element, SVGStopElement: A.Element, SVGStyleElement: A.Element, SVGElement: A.Element, SVGSVGElement: A.Element, SVGSwitchElement: A.Element, SVGSymbolElement: A.Element, SVGTSpanElement: A.Element, SVGTextContentElement: A.Element, SVGTextElement: A.Element, SVGTextPathElement: A.Element, SVGTextPositioningElement: A.Element, SVGTitleElement: A.Element, SVGUseElement: A.Element, SVGViewElement: A.Element, SVGGradientElement: A.Element, SVGComponentTransferFunctionElement: A.Element, SVGFEDropShadowElement: A.Element, SVGMPathElement: A.Element, Element: A.Element, AbsoluteOrientationSensor: A.EventTarget, Accelerometer: A.EventTarget, AccessibleNode: A.EventTarget, AmbientLightSensor: A.EventTarget, Animation: A.EventTarget, ApplicationCache: A.EventTarget, DOMApplicationCache: A.EventTarget, OfflineResourceList: A.EventTarget, BackgroundFetchRegistration: A.EventTarget, BatteryManager: A.EventTarget, BroadcastChannel: A.EventTarget, CanvasCaptureMediaStreamTrack: A.EventTarget, DedicatedWorkerGlobalScope: A.EventTarget, EventSource: A.EventTarget, FileReader: A.EventTarget, FontFaceSet: A.EventTarget, Gyroscope: A.EventTarget, XMLHttpRequest: A.EventTarget, XMLHttpRequestEventTarget: A.EventTarget, XMLHttpRequestUpload: A.EventTarget, LinearAccelerationSensor: A.EventTarget, Magnetometer: A.EventTarget, MediaDevices: A.EventTarget, MediaKeySession: A.EventTarget, MediaQueryList: A.EventTarget, MediaRecorder: A.EventTarget, MediaSource: A.EventTarget, MediaStream: A.EventTarget, MediaStreamTrack: A.EventTarget, MessagePort: A.EventTarget, MIDIAccess: A.EventTarget, MIDIInput: A.EventTarget, MIDIOutput: A.EventTarget, MIDIPort: A.EventTarget, NetworkInformation: A.EventTarget, Notification: A.EventTarget, OffscreenCanvas: A.EventTarget, OrientationSensor: A.EventTarget, PaymentRequest: A.EventTarget, Performance: A.EventTarget, PermissionStatus: A.EventTarget, PresentationAvailability: A.EventTarget, PresentationConnection: A.EventTarget, PresentationConnectionList: A.EventTarget, PresentationRequest: A.EventTarget, RelativeOrientationSensor: A.EventTarget, RemotePlayback: A.EventTarget, RTCDataChannel: A.EventTarget, DataChannel: A.EventTarget, RTCDTMFSender: A.EventTarget, RTCPeerConnection: A.EventTarget, webkitRTCPeerConnection: A.EventTarget, mozRTCPeerConnection: A.EventTarget, ScreenOrientation: A.EventTarget, Sensor: A.EventTarget, ServiceWorker: A.EventTarget, ServiceWorkerContainer: A.EventTarget, ServiceWorkerGlobalScope: A.EventTarget, ServiceWorkerRegistration: A.EventTarget, SharedWorker: A.EventTarget, SharedWorkerGlobalScope: A.EventTarget, SpeechRecognition: A.EventTarget, webkitSpeechRecognition: A.EventTarget, SpeechSynthesis: A.EventTarget, SpeechSynthesisUtterance: A.EventTarget, VR: A.EventTarget, VRDevice: A.EventTarget, VRDisplay: A.EventTarget, VRSession: A.EventTarget, VisualViewport: A.EventTarget, WebSocket: A.EventTarget, Window: A.EventTarget, DOMWindow: A.EventTarget, Worker: A.EventTarget, WorkerGlobalScope: A.EventTarget, WorkerPerformance: A.EventTarget, BluetoothDevice: A.EventTarget, BluetoothRemoteGATTCharacteristic: A.EventTarget, Clipboard: A.EventTarget, MojoInterfaceInterceptor: A.EventTarget, USB: A.EventTarget, IDBDatabase: A.EventTarget, IDBOpenDBRequest: A.EventTarget, IDBVersionChangeRequest: A.EventTarget, IDBRequest: A.EventTarget, IDBTransaction: A.EventTarget, AnalyserNode: A.EventTarget, RealtimeAnalyserNode: A.EventTarget, AudioBufferSourceNode: A.EventTarget, AudioDestinationNode: A.EventTarget, AudioNode: A.EventTarget, AudioScheduledSourceNode: A.EventTarget, AudioWorkletNode: A.EventTarget, BiquadFilterNode: A.EventTarget, ChannelMergerNode: A.EventTarget, AudioChannelMerger: A.EventTarget, ChannelSplitterNode: A.EventTarget, AudioChannelSplitter: A.EventTarget, ConstantSourceNode: A.EventTarget, ConvolverNode: A.EventTarget, DelayNode: A.EventTarget, DynamicsCompressorNode: A.EventTarget, GainNode: A.EventTarget, AudioGainNode: A.EventTarget, IIRFilterNode: A.EventTarget, MediaElementAudioSourceNode: A.EventTarget, MediaStreamAudioDestinationNode: A.EventTarget, MediaStreamAudioSourceNode: A.EventTarget, OscillatorNode: A.EventTarget, Oscillator: A.EventTarget, PannerNode: A.EventTarget, AudioPannerNode: A.EventTarget, webkitAudioPannerNode: A.EventTarget, ScriptProcessorNode: A.EventTarget, JavaScriptAudioNode: A.EventTarget, StereoPannerNode: A.EventTarget, WaveShaperNode: A.EventTarget, EventTarget: A.EventTarget, File: A.File, FileList: A.FileList, FileWriter: A.FileWriter, HTMLFormElement: A.FormElement, Gamepad: A.Gamepad, History: A.History, HTMLCollection: A.HtmlCollection, HTMLFormControlsCollection: A.HtmlCollection, HTMLOptionsCollection: A.HtmlCollection, Location: A.Location, MediaList: A.MediaList, MIDIInputMap: A.MidiInputMap, MIDIOutputMap: A.MidiOutputMap, MimeType: A.MimeType, MimeTypeArray: A.MimeTypeArray, Document: A.Node, DocumentFragment: A.Node, HTMLDocument: A.Node, ShadowRoot: A.Node, XMLDocument: A.Node, Attr: A.Node, DocumentType: A.Node, Node: A.Node, NodeList: A.NodeList, RadioNodeList: A.NodeList, Plugin: A.Plugin, PluginArray: A.PluginArray, RTCStatsReport: A.RtcStatsReport, HTMLSelectElement: A.SelectElement, SourceBuffer: A.SourceBuffer, SourceBufferList: A.SourceBufferList, SpeechGrammar: A.SpeechGrammar, SpeechGrammarList: A.SpeechGrammarList, SpeechRecognitionResult: A.SpeechRecognitionResult, Storage: A.Storage, CSSStyleSheet: A.StyleSheet, StyleSheet: A.StyleSheet, TextTrack: A.TextTrack, TextTrackCue: A.TextTrackCue, VTTCue: A.TextTrackCue, TextTrackCueList: A.TextTrackCueList, TextTrackList: A.TextTrackList, TimeRanges: A.TimeRanges, Touch: A.Touch, TouchList: A.TouchList, TrackDefaultList: A.TrackDefaultList, URL: A.Url, VideoTrackList: A.VideoTrackList, CSSRuleList: A._CssRuleList, ClientRect: A._DomRect, DOMRect: A._DomRect, GamepadList: A._GamepadList, NamedNodeMap: A._NamedNodeMap, MozNamedAttrMap: A._NamedNodeMap, SpeechRecognitionResultList: A._SpeechRecognitionResultList, StyleSheetList: A._StyleSheetList, SVGLength: A.Length, SVGLengthList: A.LengthList, SVGNumber: A.Number, SVGNumberList: A.NumberList, SVGPointList: A.PointList, SVGStringList: A.StringList, SVGTransform: A.Transform, SVGTransformList: A.TransformList, AudioBuffer: A.AudioBuffer, AudioParamMap: A.AudioParamMap, AudioTrackList: A.AudioTrackList, AudioContext: A.BaseAudioContext, webkitAudioContext: A.BaseAudioContext, BaseAudioContext: A.BaseAudioContext, OfflineAudioContext: A.OfflineAudioContext});
+    hunkHelpers.setOrUpdateLeafTags({WebGL: true, AbortPaymentEvent: true, AnimationEffectReadOnly: true, AnimationEffectTiming: true, AnimationEffectTimingReadOnly: true, AnimationEvent: true, AnimationPlaybackEvent: true, AnimationTimeline: true, AnimationWorkletGlobalScope: true, ApplicationCacheErrorEvent: true, AuthenticatorAssertionResponse: true, AuthenticatorAttestationResponse: true, AuthenticatorResponse: true, BackgroundFetchClickEvent: true, BackgroundFetchEvent: true, BackgroundFetchFailEvent: true, BackgroundFetchFetch: true, BackgroundFetchManager: true, BackgroundFetchSettledFetch: true, BackgroundFetchedEvent: true, BarProp: true, BarcodeDetector: true, BeforeInstallPromptEvent: true, BeforeUnloadEvent: true, BlobEvent: true, BluetoothRemoteGATTDescriptor: true, Body: true, BudgetState: true, CacheStorage: true, CanMakePaymentEvent: true, CanvasGradient: true, CanvasPattern: true, CanvasRenderingContext2D: true, Client: true, Clients: true, ClipboardEvent: true, CloseEvent: true, CompositionEvent: true, CookieStore: true, Coordinates: true, Credential: true, CredentialUserData: true, CredentialsContainer: true, Crypto: true, CryptoKey: true, CSS: true, CSSVariableReferenceValue: true, CustomElementRegistry: true, CustomEvent: true, DataTransfer: true, DataTransferItem: true, DeprecatedStorageInfo: true, DeprecatedStorageQuota: true, DeprecationReport: true, DetectedBarcode: true, DetectedFace: true, DetectedText: true, DeviceAcceleration: true, DeviceMotionEvent: true, DeviceOrientationEvent: true, DeviceRotationRate: true, DirectoryEntry: true, webkitFileSystemDirectoryEntry: true, FileSystemDirectoryEntry: true, DirectoryReader: true, WebKitDirectoryReader: true, webkitFileSystemDirectoryReader: true, FileSystemDirectoryReader: true, DocumentOrShadowRoot: true, DocumentTimeline: true, DOMError: true, DOMImplementation: true, Iterator: true, DOMMatrix: true, DOMMatrixReadOnly: true, DOMParser: true, DOMPoint: true, DOMPointReadOnly: true, DOMQuad: true, DOMStringMap: true, Entry: true, webkitFileSystemEntry: true, FileSystemEntry: true, ErrorEvent: true, Event: true, InputEvent: true, SubmitEvent: true, ExtendableEvent: true, ExtendableMessageEvent: true, External: true, FaceDetector: true, FederatedCredential: true, FetchEvent: true, FileEntry: true, webkitFileSystemFileEntry: true, FileSystemFileEntry: true, DOMFileSystem: true, WebKitFileSystem: true, webkitFileSystem: true, FileSystem: true, FocusEvent: true, FontFace: true, FontFaceSetLoadEvent: true, FontFaceSource: true, ForeignFetchEvent: true, FormData: true, GamepadButton: true, GamepadEvent: true, GamepadPose: true, Geolocation: true, Position: true, GeolocationPosition: true, HashChangeEvent: true, Headers: true, HTMLHyperlinkElementUtils: true, IdleDeadline: true, ImageBitmap: true, ImageBitmapRenderingContext: true, ImageCapture: true, ImageData: true, InputDeviceCapabilities: true, InstallEvent: true, IntersectionObserver: true, IntersectionObserverEntry: true, InterventionReport: true, KeyboardEvent: true, KeyframeEffect: true, KeyframeEffectReadOnly: true, MediaCapabilities: true, MediaCapabilitiesInfo: true, MediaDeviceInfo: true, MediaEncryptedEvent: true, MediaError: true, MediaKeyMessageEvent: true, MediaKeyStatusMap: true, MediaKeySystemAccess: true, MediaKeys: true, MediaKeysPolicy: true, MediaMetadata: true, MediaQueryListEvent: true, MediaSession: true, MediaSettingsRange: true, MediaStreamEvent: true, MediaStreamTrackEvent: true, MemoryInfo: true, MessageChannel: true, MessageEvent: true, Metadata: true, MIDIConnectionEvent: true, MIDIMessageEvent: true, MouseEvent: true, DragEvent: true, MutationEvent: true, MutationObserver: true, WebKitMutationObserver: true, MutationRecord: true, NavigationPreloadManager: true, Navigator: true, NavigatorAutomationInformation: true, NavigatorConcurrentHardware: true, NavigatorCookies: true, NavigatorUserMediaError: true, NodeFilter: true, NodeIterator: true, NonDocumentTypeChildNode: true, NonElementParentNode: true, NoncedElement: true, NotificationEvent: true, OffscreenCanvasRenderingContext2D: true, OverconstrainedError: true, PageTransitionEvent: true, PaintRenderingContext2D: true, PaintSize: true, PaintWorkletGlobalScope: true, PasswordCredential: true, Path2D: true, PaymentAddress: true, PaymentInstruments: true, PaymentManager: true, PaymentRequestEvent: true, PaymentRequestUpdateEvent: true, PaymentResponse: true, PerformanceEntry: true, PerformanceLongTaskTiming: true, PerformanceMark: true, PerformanceMeasure: true, PerformanceNavigation: true, PerformanceNavigationTiming: true, PerformanceObserver: true, PerformanceObserverEntryList: true, PerformancePaintTiming: true, PerformanceResourceTiming: true, PerformanceServerTiming: true, PerformanceTiming: true, Permissions: true, PhotoCapabilities: true, PointerEvent: true, PopStateEvent: true, PositionError: true, GeolocationPositionError: true, Presentation: true, PresentationConnectionAvailableEvent: true, PresentationConnectionCloseEvent: true, PresentationReceiver: true, ProgressEvent: true, PromiseRejectionEvent: true, PublicKeyCredential: true, PushEvent: true, PushManager: true, PushMessageData: true, PushSubscription: true, PushSubscriptionOptions: true, Range: true, RelatedApplication: true, ReportBody: true, ReportingObserver: true, ResizeObserver: true, ResizeObserverEntry: true, RTCCertificate: true, RTCDataChannelEvent: true, RTCDTMFToneChangeEvent: true, RTCIceCandidate: true, mozRTCIceCandidate: true, RTCLegacyStatsReport: true, RTCPeerConnectionIceEvent: true, RTCRtpContributingSource: true, RTCRtpReceiver: true, RTCRtpSender: true, RTCSessionDescription: true, mozRTCSessionDescription: true, RTCStatsResponse: true, RTCTrackEvent: true, Screen: true, ScrollState: true, ScrollTimeline: true, SecurityPolicyViolationEvent: true, Selection: true, SensorErrorEvent: true, SharedArrayBuffer: true, SpeechRecognitionAlternative: true, SpeechRecognitionError: true, SpeechRecognitionEvent: true, SpeechSynthesisEvent: true, SpeechSynthesisVoice: true, StaticRange: true, StorageEvent: true, StorageManager: true, StyleMedia: true, StylePropertyMap: true, StylePropertyMapReadonly: true, SyncEvent: true, SyncManager: true, TaskAttributionTiming: true, TextDetector: true, TextEvent: true, TextMetrics: true, TouchEvent: true, TrackDefault: true, TrackEvent: true, TransitionEvent: true, WebKitTransitionEvent: true, TreeWalker: true, TrustedHTML: true, TrustedScriptURL: true, TrustedURL: true, UIEvent: true, UnderlyingSourceBase: true, URLSearchParams: true, VRCoordinateSystem: true, VRDeviceEvent: true, VRDisplayCapabilities: true, VRDisplayEvent: true, VREyeParameters: true, VRFrameData: true, VRFrameOfReference: true, VRPose: true, VRSessionEvent: true, VRStageBounds: true, VRStageBoundsPoint: true, VRStageParameters: true, ValidityState: true, VideoPlaybackQuality: true, VideoTrack: true, VTTRegion: true, WheelEvent: true, WindowClient: true, WorkletAnimation: true, WorkletGlobalScope: true, XPathEvaluator: true, XPathExpression: true, XPathNSResolver: true, XPathResult: true, XMLSerializer: true, XSLTProcessor: true, Bluetooth: true, BluetoothCharacteristicProperties: true, BluetoothRemoteGATTServer: true, BluetoothRemoteGATTService: true, BluetoothUUID: true, BudgetService: true, Cache: true, DOMFileSystemSync: true, DirectoryEntrySync: true, DirectoryReaderSync: true, EntrySync: true, FileEntrySync: true, FileReaderSync: true, FileWriterSync: true, HTMLAllCollection: true, Mojo: true, MojoHandle: true, MojoInterfaceRequestEvent: true, MojoWatcher: true, NFC: true, PagePopupController: true, Report: true, Request: true, ResourceProgressEvent: true, Response: true, SubtleCrypto: true, USBAlternateInterface: true, USBConfiguration: true, USBConnectionEvent: true, USBDevice: true, USBEndpoint: true, USBInTransferResult: true, USBInterface: true, USBIsochronousInTransferPacket: true, USBIsochronousInTransferResult: true, USBIsochronousOutTransferPacket: true, USBIsochronousOutTransferResult: true, USBOutTransferResult: true, WorkerLocation: true, WorkerNavigator: true, Worklet: true, IDBCursor: true, IDBCursorWithValue: true, IDBFactory: true, IDBIndex: true, IDBKeyRange: true, IDBObjectStore: true, IDBObservation: true, IDBObserver: true, IDBObserverChanges: true, IDBVersionChangeEvent: true, SVGAngle: true, SVGAnimatedAngle: true, SVGAnimatedBoolean: true, SVGAnimatedEnumeration: true, SVGAnimatedInteger: true, SVGAnimatedLength: true, SVGAnimatedLengthList: true, SVGAnimatedNumber: true, SVGAnimatedNumberList: true, SVGAnimatedPreserveAspectRatio: true, SVGAnimatedRect: true, SVGAnimatedString: true, SVGAnimatedTransformList: true, SVGMatrix: true, SVGPoint: true, SVGPreserveAspectRatio: true, SVGRect: true, SVGUnitTypes: true, AudioListener: true, AudioParam: true, AudioProcessingEvent: true, AudioTrack: true, AudioWorkletGlobalScope: true, AudioWorkletProcessor: true, OfflineAudioCompletionEvent: true, PeriodicWave: true, WebGLActiveInfo: true, ANGLEInstancedArrays: true, ANGLE_instanced_arrays: true, WebGLBuffer: true, WebGLCanvas: true, WebGLColorBufferFloat: true, WebGLCompressedTextureASTC: true, WebGLCompressedTextureATC: true, WEBGL_compressed_texture_atc: true, WebGLCompressedTextureETC1: true, WEBGL_compressed_texture_etc1: true, WebGLCompressedTextureETC: true, WebGLCompressedTexturePVRTC: true, WEBGL_compressed_texture_pvrtc: true, WebGLCompressedTextureS3TC: true, WEBGL_compressed_texture_s3tc: true, WebGLCompressedTextureS3TCsRGB: true, WebGLContextEvent: true, WebGLDebugRendererInfo: true, WEBGL_debug_renderer_info: true, WebGLDebugShaders: true, WEBGL_debug_shaders: true, WebGLDepthTexture: true, WEBGL_depth_texture: true, WebGLDrawBuffers: true, WEBGL_draw_buffers: true, EXTsRGB: true, EXT_sRGB: true, EXTBlendMinMax: true, EXT_blend_minmax: true, EXTColorBufferFloat: true, EXTColorBufferHalfFloat: true, EXTDisjointTimerQuery: true, EXTDisjointTimerQueryWebGL2: true, EXTFragDepth: true, EXT_frag_depth: true, EXTShaderTextureLOD: true, EXT_shader_texture_lod: true, EXTTextureFilterAnisotropic: true, EXT_texture_filter_anisotropic: true, WebGLFramebuffer: true, WebGLGetBufferSubDataAsync: true, WebGLLoseContext: true, WebGLExtensionLoseContext: true, WEBGL_lose_context: true, OESElementIndexUint: true, OES_element_index_uint: true, OESStandardDerivatives: true, OES_standard_derivatives: true, OESTextureFloat: true, OES_texture_float: true, OESTextureFloatLinear: true, OES_texture_float_linear: true, OESTextureHalfFloat: true, OES_texture_half_float: true, OESTextureHalfFloatLinear: true, OES_texture_half_float_linear: true, OESVertexArrayObject: true, OES_vertex_array_object: true, WebGLProgram: true, WebGLQuery: true, WebGLRenderbuffer: true, WebGLRenderingContext: true, WebGL2RenderingContext: true, WebGLSampler: true, WebGLShader: true, WebGLShaderPrecisionFormat: true, WebGLSync: true, WebGLTexture: true, WebGLTimerQueryEXT: true, WebGLTransformFeedback: true, WebGLUniformLocation: true, WebGLVertexArrayObject: true, WebGLVertexArrayObjectOES: true, WebGL2RenderingContextBase: true, ArrayBuffer: true, ArrayBufferView: false, DataView: true, Float32Array: true, Float64Array: true, Int16Array: true, Int32Array: true, Int8Array: true, Uint16Array: true, Uint32Array: true, Uint8ClampedArray: true, CanvasPixelArray: true, Uint8Array: false, HTMLAudioElement: true, HTMLBRElement: true, HTMLBaseElement: true, HTMLBodyElement: true, HTMLButtonElement: true, HTMLCanvasElement: true, HTMLContentElement: true, HTMLDListElement: true, HTMLDataElement: true, HTMLDataListElement: true, HTMLDetailsElement: true, HTMLDialogElement: true, HTMLDivElement: true, HTMLEmbedElement: true, HTMLFieldSetElement: true, HTMLHRElement: true, HTMLHeadElement: true, HTMLHeadingElement: true, HTMLHtmlElement: true, HTMLIFrameElement: true, HTMLImageElement: true, HTMLInputElement: true, HTMLLIElement: true, HTMLLabelElement: true, HTMLLegendElement: true, HTMLLinkElement: true, HTMLMapElement: true, HTMLMediaElement: true, HTMLMenuElement: true, HTMLMetaElement: true, HTMLMeterElement: true, HTMLModElement: true, HTMLOListElement: true, HTMLObjectElement: true, HTMLOptGroupElement: true, HTMLOptionElement: true, HTMLOutputElement: true, HTMLParagraphElement: true, HTMLParamElement: true, HTMLPictureElement: true, HTMLPreElement: true, HTMLProgressElement: true, HTMLQuoteElement: true, HTMLScriptElement: true, HTMLShadowElement: true, HTMLSlotElement: true, HTMLSourceElement: true, HTMLSpanElement: true, HTMLStyleElement: true, HTMLTableCaptionElement: true, HTMLTableCellElement: true, HTMLTableDataCellElement: true, HTMLTableHeaderCellElement: true, HTMLTableColElement: true, HTMLTableElement: true, HTMLTableRowElement: true, HTMLTableSectionElement: true, HTMLTemplateElement: true, HTMLTextAreaElement: true, HTMLTimeElement: true, HTMLTitleElement: true, HTMLTrackElement: true, HTMLUListElement: true, HTMLUnknownElement: true, HTMLVideoElement: true, HTMLDirectoryElement: true, HTMLFontElement: true, HTMLFrameElement: true, HTMLFrameSetElement: true, HTMLMarqueeElement: true, HTMLElement: false, AccessibleNodeList: true, HTMLAnchorElement: true, HTMLAreaElement: true, Blob: false, CDATASection: true, CharacterData: true, Comment: true, ProcessingInstruction: true, Text: true, CSSPerspective: true, CSSCharsetRule: true, CSSConditionRule: true, CSSFontFaceRule: true, CSSGroupingRule: true, CSSImportRule: true, CSSKeyframeRule: true, MozCSSKeyframeRule: true, WebKitCSSKeyframeRule: true, CSSKeyframesRule: true, MozCSSKeyframesRule: true, WebKitCSSKeyframesRule: true, CSSMediaRule: true, CSSNamespaceRule: true, CSSPageRule: true, CSSRule: true, CSSStyleRule: true, CSSSupportsRule: true, CSSViewportRule: true, CSSStyleDeclaration: true, MSStyleCSSProperties: true, CSS2Properties: true, CSSImageValue: true, CSSKeywordValue: true, CSSNumericValue: true, CSSPositionValue: true, CSSResourceValue: true, CSSUnitValue: true, CSSURLImageValue: true, CSSStyleValue: false, CSSMatrixComponent: true, CSSRotation: true, CSSScale: true, CSSSkew: true, CSSTranslation: true, CSSTransformComponent: false, CSSTransformValue: true, CSSUnparsedValue: true, DataTransferItemList: true, DOMException: true, ClientRectList: true, DOMRectList: true, DOMRectReadOnly: false, DOMStringList: true, DOMTokenList: true, MathMLElement: true, SVGAElement: true, SVGAnimateElement: true, SVGAnimateMotionElement: true, SVGAnimateTransformElement: true, SVGAnimationElement: true, SVGCircleElement: true, SVGClipPathElement: true, SVGDefsElement: true, SVGDescElement: true, SVGDiscardElement: true, SVGEllipseElement: true, SVGFEBlendElement: true, SVGFEColorMatrixElement: true, SVGFEComponentTransferElement: true, SVGFECompositeElement: true, SVGFEConvolveMatrixElement: true, SVGFEDiffuseLightingElement: true, SVGFEDisplacementMapElement: true, SVGFEDistantLightElement: true, SVGFEFloodElement: true, SVGFEFuncAElement: true, SVGFEFuncBElement: true, SVGFEFuncGElement: true, SVGFEFuncRElement: true, SVGFEGaussianBlurElement: true, SVGFEImageElement: true, SVGFEMergeElement: true, SVGFEMergeNodeElement: true, SVGFEMorphologyElement: true, SVGFEOffsetElement: true, SVGFEPointLightElement: true, SVGFESpecularLightingElement: true, SVGFESpotLightElement: true, SVGFETileElement: true, SVGFETurbulenceElement: true, SVGFilterElement: true, SVGForeignObjectElement: true, SVGGElement: true, SVGGeometryElement: true, SVGGraphicsElement: true, SVGImageElement: true, SVGLineElement: true, SVGLinearGradientElement: true, SVGMarkerElement: true, SVGMaskElement: true, SVGMetadataElement: true, SVGPathElement: true, SVGPatternElement: true, SVGPolygonElement: true, SVGPolylineElement: true, SVGRadialGradientElement: true, SVGRectElement: true, SVGScriptElement: true, SVGSetElement: true, SVGStopElement: true, SVGStyleElement: true, SVGElement: true, SVGSVGElement: true, SVGSwitchElement: true, SVGSymbolElement: true, SVGTSpanElement: true, SVGTextContentElement: true, SVGTextElement: true, SVGTextPathElement: true, SVGTextPositioningElement: true, SVGTitleElement: true, SVGUseElement: true, SVGViewElement: true, SVGGradientElement: true, SVGComponentTransferFunctionElement: true, SVGFEDropShadowElement: true, SVGMPathElement: true, Element: false, AbsoluteOrientationSensor: true, Accelerometer: true, AccessibleNode: true, AmbientLightSensor: true, Animation: true, ApplicationCache: true, DOMApplicationCache: true, OfflineResourceList: true, BackgroundFetchRegistration: true, BatteryManager: true, BroadcastChannel: true, CanvasCaptureMediaStreamTrack: true, DedicatedWorkerGlobalScope: true, EventSource: true, FileReader: true, FontFaceSet: true, Gyroscope: true, XMLHttpRequest: true, XMLHttpRequestEventTarget: true, XMLHttpRequestUpload: true, LinearAccelerationSensor: true, Magnetometer: true, MediaDevices: true, MediaKeySession: true, MediaQueryList: true, MediaRecorder: true, MediaSource: true, MediaStream: true, MediaStreamTrack: true, MessagePort: true, MIDIAccess: true, MIDIInput: true, MIDIOutput: true, MIDIPort: true, NetworkInformation: true, Notification: true, OffscreenCanvas: true, OrientationSensor: true, PaymentRequest: true, Performance: true, PermissionStatus: true, PresentationAvailability: true, PresentationConnection: true, PresentationConnectionList: true, PresentationRequest: true, RelativeOrientationSensor: true, RemotePlayback: true, RTCDataChannel: true, DataChannel: true, RTCDTMFSender: true, RTCPeerConnection: true, webkitRTCPeerConnection: true, mozRTCPeerConnection: true, ScreenOrientation: true, Sensor: true, ServiceWorker: true, ServiceWorkerContainer: true, ServiceWorkerGlobalScope: true, ServiceWorkerRegistration: true, SharedWorker: true, SharedWorkerGlobalScope: true, SpeechRecognition: true, webkitSpeechRecognition: true, SpeechSynthesis: true, SpeechSynthesisUtterance: true, VR: true, VRDevice: true, VRDisplay: true, VRSession: true, VisualViewport: true, WebSocket: true, Window: true, DOMWindow: true, Worker: true, WorkerGlobalScope: true, WorkerPerformance: true, BluetoothDevice: true, BluetoothRemoteGATTCharacteristic: true, Clipboard: true, MojoInterfaceInterceptor: true, USB: true, IDBDatabase: true, IDBOpenDBRequest: true, IDBVersionChangeRequest: true, IDBRequest: true, IDBTransaction: true, AnalyserNode: true, RealtimeAnalyserNode: true, AudioBufferSourceNode: true, AudioDestinationNode: true, AudioNode: true, AudioScheduledSourceNode: true, AudioWorkletNode: true, BiquadFilterNode: true, ChannelMergerNode: true, AudioChannelMerger: true, ChannelSplitterNode: true, AudioChannelSplitter: true, ConstantSourceNode: true, ConvolverNode: true, DelayNode: true, DynamicsCompressorNode: true, GainNode: true, AudioGainNode: true, IIRFilterNode: true, MediaElementAudioSourceNode: true, MediaStreamAudioDestinationNode: true, MediaStreamAudioSourceNode: true, OscillatorNode: true, Oscillator: true, PannerNode: true, AudioPannerNode: true, webkitAudioPannerNode: true, ScriptProcessorNode: true, JavaScriptAudioNode: true, StereoPannerNode: true, WaveShaperNode: true, EventTarget: false, File: true, FileList: true, FileWriter: true, HTMLFormElement: true, Gamepad: true, History: true, HTMLCollection: true, HTMLFormControlsCollection: true, HTMLOptionsCollection: true, Location: true, MediaList: true, MIDIInputMap: true, MIDIOutputMap: true, MimeType: true, MimeTypeArray: true, Document: true, DocumentFragment: true, HTMLDocument: true, ShadowRoot: true, XMLDocument: true, Attr: true, DocumentType: true, Node: false, NodeList: true, RadioNodeList: true, Plugin: true, PluginArray: true, RTCStatsReport: true, HTMLSelectElement: true, SourceBuffer: true, SourceBufferList: true, SpeechGrammar: true, SpeechGrammarList: true, SpeechRecognitionResult: true, Storage: true, CSSStyleSheet: true, StyleSheet: true, TextTrack: true, TextTrackCue: true, VTTCue: true, TextTrackCueList: true, TextTrackList: true, TimeRanges: true, Touch: true, TouchList: true, TrackDefaultList: true, URL: true, VideoTrackList: true, CSSRuleList: true, ClientRect: true, DOMRect: true, GamepadList: true, NamedNodeMap: true, MozNamedAttrMap: true, SpeechRecognitionResultList: true, StyleSheetList: true, SVGLength: true, SVGLengthList: true, SVGNumber: true, SVGNumberList: true, SVGPointList: true, SVGStringList: true, SVGTransform: true, SVGTransformList: true, AudioBuffer: true, AudioParamMap: true, AudioTrackList: true, AudioContext: true, webkitAudioContext: true, BaseAudioContext: false, OfflineAudioContext: true});
+    A.NativeTypedArray.$nativeSuperclassTag = "ArrayBufferView";
+    A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin.$nativeSuperclassTag = "ArrayBufferView";
+    A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin.$nativeSuperclassTag = "ArrayBufferView";
+    A.NativeTypedArrayOfDouble.$nativeSuperclassTag = "ArrayBufferView";
+    A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin.$nativeSuperclassTag = "ArrayBufferView";
+    A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin.$nativeSuperclassTag = "ArrayBufferView";
+    A.NativeTypedArrayOfInt.$nativeSuperclassTag = "ArrayBufferView";
+    A._SourceBufferList_EventTarget_ListMixin.$nativeSuperclassTag = "EventTarget";
+    A._SourceBufferList_EventTarget_ListMixin_ImmutableListMixin.$nativeSuperclassTag = "EventTarget";
+    A._TextTrackList_EventTarget_ListMixin.$nativeSuperclassTag = "EventTarget";
+    A._TextTrackList_EventTarget_ListMixin_ImmutableListMixin.$nativeSuperclassTag = "EventTarget";
+  })();
+  Function.prototype.call$1 = function(a) {
+    return this(a);
+  };
+  Function.prototype.call$2 = function(a, b) {
+    return this(a, b);
+  };
+  Function.prototype.call$0 = function() {
+    return this();
+  };
+  Function.prototype.call$1$1 = function(a) {
+    return this(a);
+  };
+  Function.prototype.call$3$3 = function(a, b, c) {
+    return this(a, b, c);
+  };
+  Function.prototype.call$5 = function(a, b, c, d, e) {
+    return this(a, b, c, d, e);
+  };
+  Function.prototype.call$3 = function(a, b, c) {
+    return this(a, b, c);
+  };
+  Function.prototype.call$3$6 = function(a, b, c, d, e, f) {
+    return this(a, b, c, d, e, f);
+  };
+  Function.prototype.call$1$4 = function(a, b, c, d) {
+    return this(a, b, c, d);
+  };
+  Function.prototype.call$2$1 = function(a) {
+    return this(a);
+  };
+  Function.prototype.call$2$5 = function(a, b, c, d, e) {
+    return this(a, b, c, d, e);
+  };
+  Function.prototype.call$2$4 = function(a, b, c, d) {
+    return this(a, b, c, d);
+  };
+  Function.prototype.call$4 = function(a, b, c, d) {
+    return this(a, b, c, d);
+  };
+  Function.prototype.call$2$2 = function(a, b) {
+    return this(a, b);
+  };
+  Function.prototype.call$3$1 = function(a) {
+    return this(a);
+  };
+  Function.prototype.call$3$4 = function(a, b, c, d) {
+    return this(a, b, c, d);
+  };
+  Function.prototype.call$1$2 = function(a, b) {
+    return this(a, b);
+  };
+  Function.prototype.call$1$0 = function() {
+    return this();
+  };
+  Function.prototype.call$2$3 = function(a, b, c) {
+    return this(a, b, c);
+  };
+  convertAllToFastObject(holders);
+  convertToFastObject($);
+  (function(callback) {
+    if (typeof document === "undefined") {
+      callback(null);
+      return;
+    }
+    if (typeof document.currentScript != "undefined") {
+      callback(document.currentScript);
+      return;
+    }
+    var scripts = document.scripts;
+    function onLoad(event) {
+      for (var i = 0; i < scripts.length; ++i)
+        scripts[i].removeEventListener("load", onLoad, false);
+      callback(event.target);
+    }
+    for (var i = 0; i < scripts.length; ++i)
+      scripts[i].addEventListener("load", onLoad, false);
+  })(function(currentScript) {
+    init.currentScript = currentScript;
+    var callMain = A.main;
+    if (typeof dartMainRunner === "function")
+      dartMainRunner(callMain, []);
+    else
+      callMain([]);
+  });
+})();
+
+//# sourceMappingURL=host.dart.js.map
diff --git a/pkgs/test/lib/src/runner/browser/static/index.html b/pkgs/test/lib/src/runner/browser/static/index.html
new file mode 100644
index 0000000..69617fa
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/static/index.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>test Browser Host</title>
+  <link rel="stylesheet" type="text/css" href="host.css" />
+</head>
+<body>
+  <svg id="dart" version="1.1" x="0px" y="0px" width="400px" height="400px" viewBox="0 0 400 400">
+    <path id="right-flank" fill="#0083C9" d="M249.379,226.486l-6.676,15.572L166.174,166h58.82c0,0,2.807-0.409,3.645,1.966L249.379,226.486z"/>
+    <path id="right-ear"   fill="#00D2B8" d="M201.84,141.906L166.174,166h58.82c0,0,2.168-0.25,2.645,0.566l-2.694-8.848l-15.024-14.68C207.555,140.329,203.578,140.744,201.84,141.906z"/>
+    <path id="left-flank"  fill="#00D2B8" d="M242.616,241.856l-15.022,6.799l-60.493-21.429c-1.035-0.395-1.101-3.696-1.101-3.696v-57.932L242.616,241.856z"/>
+    <path id="left-paw"    fill="#55DECA" d="M167.003,227.098l60.636,21.558l15.064-6.799L237.224,259h-43.856c0,0-14.077-13.929-18.141-17.993C171.162,236.943,169.162,233.989,167.003,227.098z"/>
+    <path id="right-paw"   fill="#00A4E4" d="M227.676,166.365c0.963,1.401,1.361,2.473,1.361,2.473l20.352,57.648l-6.711,15.37L259,236.463v-44.854c0,0-13.678-13.965-17.741-17.882C237.193,169.811,231.466,166.319,227.676,166.365z"/>
+    <path id="left-ear"    fill="#0083C9" d="M166.769,227.098c0,0-0.769-1.104-0.769-4.355v-57.144l-23.115,34.877c-1.626,1.774-1.567,6.538,1.595,9.755l13.636,13.892L166.769,227.098z"/>
+  </svg>
+  <div id="dark"></div>
+  <svg id="play" version="1.1" x="0px" y="0px" width="80px" height="80px" viewBox="0 0 25 25">
+    <defs><filter id="blur"><feGaussianBlur stdDeviation="0.3" id="feGaussianBlur5097" /></filter></defs>
+    <path d="M 3.777014,1.3715789 A 1.1838119,1.1838119 0 0 0 2.693923,2.5488509 V 22.444746 a 1.1838119,1.1838119 0 0 0 1.765908,1.035999 l 17.235259,-9.95972 a 1.1838119,1.1838119 0 0 0 0,-2.071998 L 4.459831,1.5128519 A 1.1838119,1.1838119 0 0 0 3.777014,1.3715789 z" style="opacity:0.5;stroke:#000000;stroke-width:1;filter:url(#blur)" />
+    <path style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.32722104" d="M 3.4770491,1.0714664 A 1.1838119,1.1838119 0 0 0 2.3939589,2.2487382 V 22.144633 a 1.1838119,1.1838119 0 0 0 1.7659079,1.035999 l 17.2352602,-9.95972 a 1.1838119,1.1838119 0 0 0 0,-2.071998 L 4.1598668,1.2127389 A 1.1838119,1.1838119 0 0 0 3.4770491,1.0714664 z" />
+  </svg>
+  <script src="host.dart.js"></script>
+</body>
+</html>
diff --git a/pkgs/test/lib/src/runner/browser/static/run_wasm_chrome.js b/pkgs/test/lib/src/runner/browser/static/run_wasm_chrome.js
new file mode 100644
index 0000000..6e3bda0
--- /dev/null
+++ b/pkgs/test/lib/src/runner/browser/static/run_wasm_chrome.js
@@ -0,0 +1,47 @@
+// Copyright (c) 2022, 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.
+
+// TODO(joshualitt): Investigate making this a module. Currently, Dart2Wasm is
+// broken in D8 with modules because of an issue with async. This may or may not
+// affect chrome.
+(async () => {
+  // Fetch and compile Wasm binary.
+  let data = document.getElementById("WasmBootstrapInfo").dataset;
+
+  // Instantiate the Dart module, importing from the global scope.
+  let dart2wasmJsRuntime = await import("./" + data.jsruntimeurl);
+
+  // Support three versions of dart2wasm:
+  //
+  // (1) Versions before 3.6.0-167.0.dev require the user to compile using the
+  // browser's `WebAssembly` API, the compiled module needs to be instantiated
+  // using the JS runtime.
+  //
+  // (2) Versions starting with 3.6.0-167.0.dev added helpers for compiling and
+  // instantiating.
+  //
+  // (3) Versions starting with 3.6.0-212.0.dev made compilation functions
+  // return a new type that comes with instantiation and invoke methods.
+
+  if (dart2wasmJsRuntime.compileStreaming !== undefined) {
+    // Version (2) or (3).
+    let compiledModule = await dart2wasmJsRuntime.compileStreaming(
+      fetch(data.wasmurl),
+    );
+    if (compiledModule.instantiate !== undefined) {
+      // Version (3).
+      let instantiatedModule = await compiledModule.instantiate();
+      instantiatedModule.invokeMain();
+    } else {
+      // Version (2).
+      let dartInstance = await dart2wasmJsRuntime.instantiate(compiledModule, {});
+      await dart2wasmJsRuntime.invoke(dartInstance);
+    }
+  } else {
+    // Version (1).
+    let modulePromise = WebAssembly.compileStreaming(fetch(data.wasmurl));
+    let dartInstance = await dart2wasmJsRuntime.instantiate(modulePromise, {});
+    await dart2wasmJsRuntime.invoke(dartInstance);
+  }
+})();
diff --git a/pkgs/test/lib/src/runner/executable_settings.dart b/pkgs/test/lib/src/runner/executable_settings.dart
new file mode 100644
index 0000000..904dc51
--- /dev/null
+++ b/pkgs/test/lib/src/runner/executable_settings.dart
@@ -0,0 +1,224 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:io/io.dart';
+import 'package:path/path.dart' as p;
+import 'package:source_span/source_span.dart';
+import 'package:yaml/yaml.dart';
+
+/// User-provided settings for invoking an executable.
+class ExecutableSettings {
+  /// Additional arguments to pass to the executable.
+  final List<String> arguments;
+
+  /// The path to the executable on Linux.
+  ///
+  /// This may be an absolute path or a basename, in which case it will be
+  /// looked up on the system path. It may not be relative.
+  final String? _linuxExecutable;
+
+  /// Potential commands to execute on Mac OS to launch this executable.
+  ///
+  /// The values may be an absolute path, or a basename.
+  /// The chosen command will be the first value from this list which either is
+  /// a full path that exists, or is a basename. When a basename is included it
+  /// should always be at the end of the list because it will always terminate
+  /// the search through the list. Relative paths are not allowed.
+  final List<String>? _macOSExectuables;
+
+  /// The path to the executable on Windows.
+  ///
+  /// This may be an absolute path; a basename, in which case it will be looked
+  /// up on the system path; or a relative path, in which case it will be looked
+  /// up relative to the paths in the `LOCALAPPDATA`, `PROGRAMFILES`, and
+  /// `PROGRAMFILES(X64)` environment variables.
+  final String? _windowsExecutable;
+
+  /// The environment variable, if any, to use as an override for the default
+  /// path.
+  final String? _environmentOverride;
+
+  /// The path to the executable for the current operating system.
+  String get executable {
+    if (_environmentOverride != null) {
+      final envVariable = Platform.environment[_environmentOverride];
+      if (envVariable != null) return envVariable;
+    }
+
+    if (Platform.isMacOS) {
+      if (_macOSExectuables != null) {
+        for (final path in _macOSExectuables) {
+          if (p.basename(path) == path) return path;
+          if (p.isAbsolute(path)) {
+            if (File(path).existsSync()) return path;
+          } else {
+            throw ArgumentError(
+                'Mac OS executable must be a basename or an absolute path.'
+                ' Got relative path: $path');
+          }
+        }
+      }
+      throw ArgumentError('Could not find a command basename or an existing '
+          'path in $_macOSExectuables');
+    }
+    if (!Platform.isWindows) return _linuxExecutable!;
+    final windowsExecutable = _windowsExecutable!;
+    if (p.isAbsolute(windowsExecutable)) return windowsExecutable;
+    if (p.basename(windowsExecutable) == windowsExecutable) {
+      return windowsExecutable;
+    }
+
+    var prefixes = [
+      Platform.environment['LOCALAPPDATA'],
+      Platform.environment['PROGRAMFILES'],
+      Platform.environment['PROGRAMFILES(X86)']
+    ];
+
+    for (var prefix in prefixes) {
+      if (prefix == null) continue;
+
+      var path = p.join(prefix, windowsExecutable);
+      if (File(path).existsSync()) return path;
+    }
+
+    // If we can't find a path that works, return one that doesn't. This will
+    // cause an "executable not found" error to surface.
+    return p.join(
+        prefixes.firstWhere((prefix) => prefix != null, orElse: () => '.')!,
+        _windowsExecutable);
+  }
+
+  /// Whether to invoke the browser in headless mode.
+  ///
+  /// This is currently only supported by Chrome.
+  bool get headless => _headless ?? true;
+  final bool? _headless;
+
+  /// Parses settings from a user-provided YAML mapping.
+  factory ExecutableSettings.parse(YamlMap settings) {
+    List<String>? arguments;
+    var argumentsNode = settings.nodes['arguments'];
+    if (argumentsNode != null) {
+      var value = argumentsNode.value;
+      if (value is String) {
+        try {
+          arguments = shellSplit(value);
+        } on FormatException catch (error) {
+          throw SourceSpanFormatException(error.message, argumentsNode.span);
+        }
+      } else {
+        throw SourceSpanFormatException(
+            'Must be a string.', argumentsNode.span);
+      }
+    }
+
+    String? linuxExecutable;
+    String? macOSExecutable;
+    String? windowsExecutable;
+    var executableNode = settings.nodes['executable'];
+    if (executableNode != null) {
+      var value = executableNode.value;
+      if (value is String) {
+        // Don't check this on Windows because people may want to set relative
+        // paths in their global config.
+        if (!Platform.isWindows) {
+          _assertNotRelative(executableNode as YamlScalar);
+        }
+
+        linuxExecutable = value;
+        macOSExecutable = value;
+        windowsExecutable = value;
+      } else if (executableNode is YamlMap) {
+        linuxExecutable = _getExecutable(executableNode.nodes['linux']);
+        macOSExecutable = _getExecutable(executableNode.nodes['mac_os']);
+        windowsExecutable = _getExecutable(executableNode.nodes['windows'],
+            allowRelative: true);
+      } else {
+        throw SourceSpanFormatException(
+            'Must be a map or a string.', executableNode.span);
+      }
+    }
+
+    var headless = true;
+    var headlessNode = settings.nodes['headless'];
+    if (headlessNode != null) {
+      var value = headlessNode.value;
+      if (value is bool) {
+        headless = value;
+      } else {
+        throw SourceSpanFormatException(
+            'Must be a boolean.', headlessNode.span);
+      }
+    }
+
+    return ExecutableSettings(
+        arguments: arguments,
+        linuxExecutable: linuxExecutable,
+        macOSExecutable: macOSExecutable,
+        windowsExecutable: windowsExecutable,
+        headless: headless);
+  }
+
+  /// Asserts that [executableNode] is a string or `null` and returns it.
+  ///
+  /// If [allowRelative] is `false` (the default), asserts that the value isn't
+  /// a relative path.
+  static String? _getExecutable(YamlNode? executableNode,
+      {bool allowRelative = false}) {
+    if (executableNode == null || executableNode.value == null) return null;
+    if (executableNode.value is! String) {
+      throw SourceSpanFormatException('Must be a string.', executableNode.span);
+    }
+    if (!allowRelative) _assertNotRelative(executableNode as YamlScalar);
+    return executableNode.value as String;
+  }
+
+  /// Throws a [SourceSpanFormatException] if [executableNode]'s value is a
+  /// relative POSIX path that's not just a plain basename.
+  ///
+  /// We loop up basenames on the PATH and we can resolve absolute paths, but we
+  /// have no way of interpreting relative paths.
+  static void _assertNotRelative(YamlScalar executableNode) {
+    var executable = executableNode.value as String;
+    if (!p.posix.isRelative(executable)) return;
+    if (p.posix.basename(executable) == executable) return;
+
+    throw SourceSpanFormatException(
+        'Linux and Mac OS executables may not be relative paths.',
+        executableNode.span);
+  }
+
+  ExecutableSettings(
+      {Iterable<String>? arguments,
+      String? linuxExecutable,
+      String? macOSExecutable,
+      List<String>? macOSExecutables,
+      String? windowsExecutable,
+      String? environmentOverride,
+      bool? headless})
+      : arguments = arguments == null ? const [] : List.unmodifiable(arguments),
+        _linuxExecutable = linuxExecutable,
+        _macOSExectuables =
+            _normalizeMacExecutable(macOSExecutable, macOSExecutables),
+        _windowsExecutable = windowsExecutable,
+        _environmentOverride = environmentOverride,
+        _headless = headless;
+
+  /// Merges [this] with [other], with [other]'s settings taking priority.
+  ExecutableSettings merge(ExecutableSettings other) => ExecutableSettings(
+      arguments: arguments.toList()..addAll(other.arguments),
+      headless: other._headless ?? _headless,
+      linuxExecutable: other._linuxExecutable ?? _linuxExecutable,
+      macOSExecutables: other._macOSExectuables ?? _macOSExectuables,
+      windowsExecutable: other._windowsExecutable ?? _windowsExecutable);
+}
+
+List<String>? _normalizeMacExecutable(
+    String? singleArgument, List<String>? listArgument) {
+  if (listArgument != null) return listArgument;
+  if (singleArgument != null) return [singleArgument];
+  return null;
+}
diff --git a/pkgs/test/lib/src/runner/node/platform.dart b/pkgs/test/lib/src/runner/node/platform.dart
new file mode 100644
index 0000000..16cd9c6
--- /dev/null
+++ b/pkgs/test/lib/src/runner/node/platform.dart
@@ -0,0 +1,380 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:async/async.dart';
+import 'package:node_preamble/preamble.dart' as preamble;
+import 'package:package_config/package_config.dart';
+import 'package:path/path.dart' as p;
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test_api/backend.dart'
+    show Compiler, Runtime, StackTraceMapper, SuitePlatform;
+import 'package:test_core/src/runner/application_exception.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/dart2js_compiler_pool.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/plugin/environment.dart'; // ignore: implementation_imports
+import 'package:test_core/src/runner/plugin/platform_helpers.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
+import 'package:test_core/src/runner/wasm_compiler_pool.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/errors.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/package_config.dart'; // ignore: implementation_imports
+import 'package:test_core/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports
+import 'package:yaml/yaml.dart';
+
+import '../../util/package_map.dart';
+import '../executable_settings.dart';
+
+/// A platform that loads tests in Node.js processes.
+class NodePlatform extends PlatformPlugin
+    implements CustomizablePlatform<ExecutableSettings> {
+  /// The test runner configuration.
+  final Configuration _config;
+
+  /// The [Dart2JsCompilerPool] managing active instances of `dart2js`.
+  final _jsCompilers = Dart2JsCompilerPool(['-Dnode=true', '--server-mode']);
+  final _wasmCompilers = WasmCompilerPool(['-Dnode=true']);
+
+  /// The temporary directory in which compiled JS is emitted.
+  final _compiledDir = createTempDir();
+
+  /// Executable settings for [Runtime.nodeJS] and runtimes that extend
+  /// it.
+  final _settings = {
+    Runtime.nodeJS: ExecutableSettings(
+        linuxExecutable: 'node',
+        macOSExecutable: 'node',
+        windowsExecutable: 'node.exe')
+  };
+
+  NodePlatform() : _config = Configuration.current;
+
+  @override
+  ExecutableSettings parsePlatformSettings(YamlMap settings) =>
+      ExecutableSettings.parse(settings);
+
+  @override
+  ExecutableSettings mergePlatformSettings(
+          ExecutableSettings settings1, ExecutableSettings settings2) =>
+      settings1.merge(settings2);
+
+  @override
+  void customizePlatform(Runtime runtime, ExecutableSettings settings) {
+    var oldSettings = _settings[runtime] ?? _settings[runtime.root];
+    if (oldSettings != null) settings = oldSettings.merge(settings);
+    _settings[runtime] = settings;
+  }
+
+  @override
+  Future<RunnerSuite> load(String path, SuitePlatform platform,
+      SuiteConfiguration suiteConfig, Map<String, Object?> message) async {
+    if (platform.compiler != Compiler.dart2js &&
+        platform.compiler != Compiler.dart2wasm) {
+      throw StateError(
+          'Unsupported compiler for the Node platform ${platform.compiler}.');
+    }
+    var (channel, stackMapper) =
+        await _loadChannel(path, platform, suiteConfig);
+    var controller = deserializeSuite(path, platform, suiteConfig,
+        const PluginEnvironment(), channel, message);
+
+    controller.channel('test.node.mapper').sink.add(stackMapper?.serialize());
+
+    return await controller.suite;
+  }
+
+  /// Loads a [StreamChannel] communicating with the test suite at [path].
+  ///
+  /// Returns that channel along with a [StackTraceMapper] representing the
+  /// source map for the compiled suite.
+  Future<(StreamChannel<Object?>, StackTraceMapper?)> _loadChannel(String path,
+      SuitePlatform platform, SuiteConfiguration suiteConfig) async {
+    final servers = await _loopback();
+
+    try {
+      var (process, stackMapper) =
+          await _spawnProcess(path, platform, suiteConfig, servers.first.port);
+
+      // Forward Node's standard IO to the print handler so it's associated with
+      // the load test.
+      //
+      // TODO(nweiz): Associate this with the current test being run, if any.
+      process.stdout.transform(lineSplitter).listen(print);
+      process.stderr.transform(lineSplitter).listen(print);
+
+      // Wait for the first connection (either over ipv4 or v6). If the proccess
+      // exits before it connects, throw instead of waiting for a connection
+      // indefinitely.
+      var socket = await Future.any([
+        StreamGroup.merge(servers).first,
+        process.exitCode.then((_) => null),
+      ]);
+
+      if (socket == null) {
+        throw LoadException(
+            path, 'Node exited before connecting to the test channel.');
+      }
+
+      var channel = StreamChannel(socket.cast<List<int>>(), socket)
+          .transform(StreamChannelTransformer.fromCodec(utf8))
+          .transform(_chunksToLines)
+          .transform(jsonDocument)
+          .transformStream(StreamTransformer.fromHandlers(handleDone: (sink) {
+        process.kill();
+        sink.close();
+      }));
+
+      return (channel, stackMapper);
+    } finally {
+      unawaited(Future.wait<void>(servers.map((s) =>
+          s.close().then<ServerSocket?>((v) => v).onError((_, __) => null))));
+    }
+  }
+
+  /// Spawns a Node.js process that loads the Dart test suite at [path].
+  ///
+  /// Returns that channel along with a [StackTraceMapper] representing the
+  /// source map for the compiled suite.
+  Future<(Process, StackTraceMapper?)> _spawnProcess(
+      String path,
+      SuitePlatform platform,
+      SuiteConfiguration suiteConfig,
+      int socketPort) async {
+    if (_config.suiteDefaults.precompiledPath != null) {
+      return _spawnPrecompiledProcess(path, platform.runtime, suiteConfig,
+          socketPort, _config.suiteDefaults.precompiledPath!);
+    } else {
+      return switch (platform.compiler) {
+        Compiler.dart2js => _spawnNormalJsProcess(
+            path, platform.runtime, suiteConfig, socketPort),
+        Compiler.dart2wasm => _spawnNormalWasmProcess(
+            path, platform.runtime, suiteConfig, socketPort),
+        _ => throw StateError('Unsupported compiler ${platform.compiler}'),
+      };
+    }
+  }
+
+  Future<String> _entrypointScriptForTest(
+      String testPath, SuiteConfiguration suiteConfig) async {
+    return '''
+        ${suiteConfig.metadata.languageVersionComment ?? await rootPackageLanguageVersionComment}
+        import "package:test/src/bootstrap/node.dart";
+
+        import "${p.toUri(p.absolute(testPath))}" as test;
+
+        void main() {
+          internalBootstrapNodeTest(() => test.main);
+        }
+      ''';
+  }
+
+  /// Compiles [testPath] with dart2js, adds the node preamble, and then spawns
+  /// a Node.js process that loads that Dart test suite.
+  Future<(Process, StackTraceMapper?)> _spawnNormalJsProcess(String testPath,
+      Runtime runtime, SuiteConfiguration suiteConfig, int socketPort) async {
+    var dir = Directory(_compiledDir).createTempSync('test_').path;
+    var jsPath = p.join(dir, '${p.basename(testPath)}.node_test.dart.js');
+    await _jsCompilers.compile(
+      await _entrypointScriptForTest(testPath, suiteConfig),
+      jsPath,
+      suiteConfig,
+    );
+
+    // Add the Node.js preamble to ensure that the dart2js output is
+    // compatible. Use the minified version so the source map remains valid.
+    var jsFile = File(jsPath);
+    await jsFile.writeAsString(
+        preamble.getPreamble(minified: true) + await jsFile.readAsString());
+
+    StackTraceMapper? mapper;
+    if (!suiteConfig.jsTrace) {
+      var mapPath = '$jsPath.map';
+      mapper = JSStackTraceMapper(await File(mapPath).readAsString(),
+          mapUrl: p.toUri(mapPath),
+          sdkRoot: Uri.parse('org-dartlang-sdk:///sdk'),
+          packageMap: (await currentPackageConfig).toPackageMap());
+    }
+
+    return (await _startProcess(runtime, jsPath, socketPort), mapper);
+  }
+
+  /// Compiles [testPath] with dart2wasm, adds a JS entrypoint and then spawns
+  /// a Node.js process loading the compiled test suite.
+  Future<(Process, StackTraceMapper?)> _spawnNormalWasmProcess(String testPath,
+      Runtime runtime, SuiteConfiguration suiteConfig, int socketPort) async {
+    var dir = Directory(_compiledDir).createTempSync('test_').path;
+    // dart2wasm will emit a .wasm file and a .mjs file responsible for loading
+    // that file.
+    var wasmPath = p.join(dir, '${p.basename(testPath)}.node_test.dart.wasm');
+    var loader = '${p.basename(testPath)}.node_test.dart.wasm.mjs';
+
+    // We need to create an additional entrypoint file loading the wasm module.
+    var jsPath = p.join(dir, '${p.basename(testPath)}.node_test.dart.js');
+
+    await _wasmCompilers.compile(
+      await _entrypointScriptForTest(testPath, suiteConfig),
+      wasmPath,
+      suiteConfig,
+    );
+
+    await File(jsPath).writeAsString('''
+const { createReadStream } = require('fs');
+const { once } = require('events');
+const { PassThrough } = require('stream');
+
+const main = async () => {
+  const { instantiate, invoke } = await import("./$loader");
+
+  const wasmContents = createReadStream("$wasmPath.wasm");
+  const stream = new PassThrough();
+  wasmContents.pipe(stream);
+
+  await once(wasmContents, 'open');
+  const response = new Response(
+    stream,
+    {
+      headers: {
+        "Content-Type": "application/wasm"
+      }
+    }
+  );
+  const instancePromise = WebAssembly.compileStreaming(response);
+  const module = await instantiate(instancePromise, {});
+  invoke(module);
+};
+
+main();
+''');
+
+    return (await _startProcess(runtime, jsPath, socketPort), null);
+  }
+
+  /// Spawns a Node.js process that loads the Dart test suite at [testPath]
+  /// under [precompiledPath].
+  Future<(Process, StackTraceMapper?)> _spawnPrecompiledProcess(
+      String testPath,
+      Runtime runtime,
+      SuiteConfiguration suiteConfig,
+      int socketPort,
+      String precompiledPath) async {
+    StackTraceMapper? mapper;
+    var jsPath = p.join(precompiledPath, '$testPath.node_test.dart.js');
+    if (!suiteConfig.jsTrace) {
+      var mapPath = '$jsPath.map';
+      mapper = JSStackTraceMapper(await File(mapPath).readAsString(),
+          mapUrl: p.toUri(mapPath),
+          sdkRoot: Uri.parse('org-dartlang-sdk:///sdk'),
+          packageMap: (await findPackageConfig(Directory(precompiledPath)))!
+              .toPackageMap());
+    }
+
+    return (await _startProcess(runtime, jsPath, socketPort), mapper);
+  }
+
+  /// Starts the Node.js process for [runtime] with [jsPath].
+  Future<Process> _startProcess(
+      Runtime runtime, String jsPath, int socketPort) async {
+    var settings = _settings[runtime]!;
+
+    var nodeModules = p.absolute('node_modules');
+    var nodePath = Platform.environment['NODE_PATH'];
+    nodePath = nodePath == null ? nodeModules : '$nodePath:$nodeModules';
+
+    try {
+      return await Process.start(
+          settings.executable,
+          settings.arguments.toList()
+            ..add(jsPath)
+            ..add(socketPort.toString()),
+          environment: {'NODE_PATH': nodePath});
+    } catch (error, stackTrace) {
+      await Future<Never>.error(
+          ApplicationException(
+              'Failed to run ${runtime.name}: ${getErrorMessage(error)}'),
+          stackTrace);
+    }
+  }
+
+  @override
+  Future<void> close() => _closeMemo.runOnce(() async {
+        await _jsCompilers.close();
+        await _wasmCompilers.close();
+        await Directory(_compiledDir).deleteWithRetry();
+      });
+  final _closeMemo = AsyncMemoizer<void>();
+}
+
+Future<List<ServerSocket>> _loopback({int remainingRetries = 5}) async {
+  if (!await _supportsIPv4) {
+    return [await ServerSocket.bind(InternetAddress.loopbackIPv6, 0)];
+  }
+
+  var v4Server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
+  if (!await _supportsIPv6) return [v4Server];
+
+  try {
+    // Reuse the IPv4 server's port so that if [port] is 0, both servers use
+    // the same ephemeral port.
+    var v6Server =
+        await ServerSocket.bind(InternetAddress.loopbackIPv6, v4Server.port);
+    return [v4Server, v6Server];
+  } on SocketException catch (error) {
+    if (error.osError?.errorCode != _addressInUseErrno) rethrow;
+    if (remainingRetries == 0) rethrow;
+
+    // A port being available on IPv4 doesn't necessarily mean that the same
+    // port is available on IPv6. If it's not (which is rare in practice),
+    // we try again until we find one that's available on both.
+    unawaited(v4Server.close());
+    return await _loopback(remainingRetries: remainingRetries - 1);
+  }
+}
+
+/// Whether this computer supports binding to IPv6 addresses.
+final Future<bool> _supportsIPv6 = () async {
+  try {
+    var socket = await ServerSocket.bind(InternetAddress.loopbackIPv6, 0);
+    unawaited(socket.close());
+    return true;
+  } on SocketException catch (_) {
+    return false;
+  }
+}();
+
+/// Whether this computer supports binding to IPv4 addresses.
+final Future<bool> _supportsIPv4 = () async {
+  try {
+    var socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
+    unawaited(socket.close());
+    return true;
+  } on SocketException catch (_) {
+    return false;
+  }
+}();
+
+/// The error code for an error caused by a port already being in use.
+final int _addressInUseErrno = () {
+  if (Platform.isWindows) return 10048;
+  if (Platform.isMacOS) return 48;
+  assert(Platform.isLinux);
+  return 98;
+}();
+
+/// A [StreamChannelTransformer] that converts a chunked string channel to a
+/// line-by-line channel.
+///
+/// Note that this is only safe for channels whose messages are guaranteed not
+/// to contain newlines.
+final _chunksToLines = StreamChannelTransformer<String, String>(
+    const LineSplitter(),
+    StreamSinkTransformer.fromHandlers(
+        handleData: (data, sink) => sink.add('$data\n')));
diff --git a/pkgs/test/lib/src/runner/node/socket_channel.dart b/pkgs/test/lib/src/runner/node/socket_channel.dart
new file mode 100644
index 0000000..95e81de
--- /dev/null
+++ b/pkgs/test/lib/src/runner/node/socket_channel.dart
@@ -0,0 +1,43 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+import 'dart:async';
+import 'dart:convert';
+import 'dart:js_interop';
+
+import 'package:stream_channel/stream_channel.dart';
+
+@JS('process.argv')
+external JSArray<JSString> get _args;
+
+extension type _Net._(JSObject _) {
+  external _Socket connect(int port);
+}
+
+extension type _Socket._(JSObject _) {
+  external void setEncoding(JSString encoding);
+  external void on(JSString event, JSFunction callback);
+  external void write(JSString data);
+}
+
+/// Returns a [StreamChannel] of JSON-encodable objects that communicates over a
+/// socket whose port is given by `process.argv[2]`.
+Future<StreamChannel<Object?>> socketChannel() async {
+  final net = (await importModule('node:net'.toJS).toDart) as _Net;
+
+  var socket = net.connect(int.parse(_args.toDart[2].toDart));
+  socket.setEncoding('utf8'.toJS);
+
+  var socketSink = StreamController<Object?>(sync: true)
+    ..stream.listen((event) => socket.write('${jsonEncode(event)}\n'.toJS));
+
+  var socketStream = StreamController<String>(sync: true);
+  socket.on(
+    'data'.toJS,
+    ((JSString chunk) => socketStream.add(chunk.toDart)).toJS,
+  );
+
+  return StreamChannel.withCloseGuarantee(
+      socketStream.stream.transform(const LineSplitter()).map(jsonDecode),
+      socketSink);
+}
diff --git a/pkgs/test/lib/src/util/math.dart b/pkgs/test/lib/src/util/math.dart
new file mode 100644
index 0000000..d6bbbc7
--- /dev/null
+++ b/pkgs/test/lib/src/util/math.dart
@@ -0,0 +1,20 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:math' as math;
+
+final _rand = math.Random();
+
+/// Returns a random 32 character alphanumeric string ([a-zA-Z0-9]), which is
+/// suitable as a url secret.
+String randomUrlSecret() {
+  var buffer = StringBuffer();
+  while (buffer.length < 32) {
+    buffer.write(_alphaChars[_rand.nextInt(_alphaChars.length)]);
+  }
+  return buffer.toString();
+}
+
+const _alphaChars =
+    '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
diff --git a/pkgs/test/lib/src/util/one_off_handler.dart b/pkgs/test/lib/src/util/one_off_handler.dart
new file mode 100644
index 0000000..7667df3
--- /dev/null
+++ b/pkgs/test/lib/src/util/one_off_handler.dart
@@ -0,0 +1,46 @@
+// 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.
+
+import 'dart:async';
+import 'package:path/path.dart' as p;
+import 'package:shelf/shelf.dart' as shelf;
+
+/// A Shelf handler that provides support for one-time handlers.
+///
+/// This is useful for handlers that only expect to be hit once before becoming
+/// invalid and don't need to have a persistent URL.
+class OneOffHandler {
+  /// A map from URL paths to handlers.
+  final _handlers = <String, shelf.Handler>{};
+
+  /// The counter of handlers that have been activated.
+  var _counter = 0;
+
+  /// The actual [shelf.Handler] that dispatches requests.
+  shelf.Handler get handler => _onRequest;
+
+  /// Creates a new one-off handler that forwards to [handler].
+  ///
+  /// Returns a string that's the URL path for hitting this handler, relative to
+  /// the URL for the one-off handler itself.
+  ///
+  /// [handler] will be unmounted as soon as it receives a request.
+  String create(shelf.Handler handler) {
+    var path = _counter.toString();
+    _handlers[path] = handler;
+    _counter++;
+    return path;
+  }
+
+  /// Dispatches [request] to the appropriate handler.
+  FutureOr<shelf.Response> _onRequest(shelf.Request request) {
+    var components = p.url.split(request.url.path);
+    if (components.isEmpty) return shelf.Response.notFound(null);
+
+    var path = components.removeAt(0);
+    var handler = _handlers.remove(path);
+    if (handler == null) return shelf.Response.notFound(null);
+    return handler(request.change(path: path));
+  }
+}
diff --git a/pkgs/test/lib/src/util/package_map.dart b/pkgs/test/lib/src/util/package_map.dart
new file mode 100644
index 0000000..6b30afa
--- /dev/null
+++ b/pkgs/test/lib/src/util/package_map.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.
+
+import 'package:package_config/package_config.dart';
+
+/// Adds methods to convert to a package map on [PackageConfig].
+extension PackageMap on PackageConfig {
+  /// A package map exactly matching the current package config
+  Map<String, Uri> toPackageMap() =>
+      {for (var package in packages) package.name: package.packageUriRoot};
+
+  /// A package map with all the current packages but where the uris are all
+  /// of the form 'packages/${package.name}'.
+  Map<String, Uri> toPackagesDirPackageMap() => {
+        for (var package in packages)
+          package.name: Uri.parse('packages/${package.name}')
+      };
+}
diff --git a/pkgs/test/lib/src/util/path_handler.dart b/pkgs/test/lib/src/util/path_handler.dart
new file mode 100644
index 0000000..10a8c4e
--- /dev/null
+++ b/pkgs/test/lib/src/util/path_handler.dart
@@ -0,0 +1,62 @@
+// 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.
+
+import 'dart:async';
+import 'package:path/path.dart' as p;
+import 'package:shelf/shelf.dart' as shelf;
+
+/// A handler that routes to sub-handlers based on exact path prefixes.
+class PathHandler {
+  /// A trie of path components to handlers.
+  final _paths = _Node();
+
+  /// The shelf handler.
+  shelf.Handler get handler => _onRequest;
+
+  /// Returns middleware that nests all requests beneath the URL prefix
+  /// [beneath].
+  static shelf.Middleware nestedIn(String beneath) {
+    return (handler) {
+      var pathHandler = PathHandler()..add(beneath, handler);
+      return pathHandler.handler;
+    };
+  }
+
+  /// Routes requests at or under [path] to [handler].
+  ///
+  /// If [path] is a parent or child directory of another path in this handler,
+  /// the longest matching prefix wins.
+  void add(String path, shelf.Handler handler) {
+    var node = _paths;
+    for (var component in p.url.split(path)) {
+      node = node.children.putIfAbsent(component, _Node.new);
+    }
+    node.handler = handler;
+  }
+
+  FutureOr<shelf.Response> _onRequest(shelf.Request request) {
+    shelf.Handler? handler;
+    int? handlerIndex;
+    _Node? node = _paths;
+    var components = p.url.split(request.url.path);
+    for (var i = 0; i < components.length; i++) {
+      node = node!.children[components[i]];
+      if (node == null) break;
+      if (node.handler == null) continue;
+      handler = node.handler;
+      handlerIndex = i;
+    }
+
+    if (handler == null) return shelf.Response.notFound('Not found.');
+
+    return handler(request.change(
+        path: p.url.joinAll(components.take(handlerIndex! + 1))));
+  }
+}
+
+/// A trie node.
+class _Node {
+  shelf.Handler? handler;
+  final children = <String, _Node>{};
+}
diff --git a/pkgs/test/lib/test.dart b/pkgs/test/lib/test.dart
new file mode 100644
index 0000000..7bcc673
--- /dev/null
+++ b/pkgs/test/lib/test.dart
@@ -0,0 +1,15 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+export 'package:matcher/expect.dart';
+// Deprecated exports not surfaced through focused libraries.
+// ignore: deprecated_member_use
+export 'package:matcher/src/expect/expect.dart' show ErrorFormatter;
+// ignore: deprecated_member_use
+export 'package:matcher/src/expect/expect_async.dart' show expectAsync;
+// ignore: deprecated_member_use
+export 'package:matcher/src/expect/throws_matcher.dart' show Throws, throws;
+// The non-deprecated API (through a deprecated import).
+// ignore: deprecated_member_use
+export 'package:test_core/test_core.dart';
diff --git a/pkgs/test/mono_pkg.yaml b/pkgs/test/mono_pkg.yaml
new file mode 100644
index 0000000..2aaa4f9
--- /dev/null
+++ b/pkgs/test/mono_pkg.yaml
@@ -0,0 +1,51 @@
+# See https://pub.dev/packages/mono_repo
+
+sdk:
+- dev
+- pubspec
+
+stages:
+- analyze_and_format:
+  - group:
+    - format
+    - analyze: --fatal-infos
+    sdk:
+    - dev
+    os:
+    - linux
+- unit_test:
+  - command: xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 0
+  - command: xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 1
+  - command: xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 2
+  - command: xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 3
+  - command: xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 4
+  - command: dart test --preset travis --total-shards 5 --shard-index 0
+    os:
+    - windows
+    - osx
+    sdk:
+    - pubspec
+  - command: dart test --preset travis --total-shards 5 --shard-index 1
+    os:
+    - windows
+    - osx
+    sdk:
+    - pubspec
+  - command: dart test --preset travis --total-shards 5 --shard-index 2
+    os:
+    - windows
+    - osx
+    sdk:
+    - pubspec
+  - command: dart test --preset travis --total-shards 5 --shard-index 3
+    os:
+    - windows
+    - osx
+    sdk:
+    - pubspec
+  - command: dart test --preset travis --total-shards 5 --shard-index 4
+    os:
+    - windows
+    - osx
+    sdk:
+    - pubspec
diff --git a/pkgs/test/pubspec.yaml b/pkgs/test/pubspec.yaml
new file mode 100644
index 0000000..0ab65ad
--- /dev/null
+++ b/pkgs/test/pubspec.yaml
@@ -0,0 +1,53 @@
+name: test
+version: 1.25.12
+description: >-
+  A full featured library for writing and running Dart tests across platforms.
+repository: https://github.com/dart-lang/test/tree/master/pkgs/test
+resolution: workspace
+
+environment:
+  sdk: ^3.5.0
+
+dependencies:
+  analyzer: '>=6.0.0 <8.0.0'
+  async: ^2.5.0
+  boolean_selector: ^2.1.0
+  collection: ^1.15.0
+  coverage: ^1.0.1
+  http_multi_server: ^3.0.0
+  io: ^1.0.0
+  js: '>=0.6.4 <0.8.0'
+
+  # Use a tight version constraint to ensure that a constraint on matcher
+  # properly constrains all features it provides.
+  matcher: '>=0.12.16 <0.12.17'
+
+  node_preamble: ^2.0.0
+  package_config: ^2.0.0
+  path: ^1.8.0
+  pool: ^1.5.0
+  shelf: ^1.0.0
+  shelf_packages_handler: ^3.0.0
+  shelf_static: ^1.0.0
+  shelf_web_socket: '>=1.0.0 <3.0.0'
+  source_span: ^1.8.0
+  stack_trace: ^1.10.0
+  stream_channel: ^2.1.0
+
+  # Use an exact version until the test_api and test_core package are stable.
+  test_api: 0.7.4
+  test_core: 0.6.8
+
+  typed_data: ^1.3.0
+  web_socket_channel: '>=2.0.0 <4.0.0'
+  webkit_inspection_protocol: ^1.0.0
+  yaml: ^3.0.0
+
+dev_dependencies:
+  fake_async: ^1.0.0
+  glob: ^2.0.0
+  test_descriptor: ^2.0.0
+  test_process: ^2.0.0
+
+topics:
+ - testing
diff --git a/pkgs/test/test/common.dart b/pkgs/test/test/common.dart
new file mode 100644
index 0000000..1cad23f
--- /dev/null
+++ b/pkgs/test/test/common.dart
@@ -0,0 +1,6 @@
+// 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';
+
+void myTest(String name, void Function() testFn) => test(name, testFn);
diff --git a/pkgs/test/test/io.dart b/pkgs/test/test/io.dart
new file mode 100644
index 0000000..b13f8bd
--- /dev/null
+++ b/pkgs/test/test/io.dart
@@ -0,0 +1,158 @@
+// 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.
+
+import 'dart:async';
+import 'dart:io';
+import 'dart:isolate';
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+import 'package:test_process/test_process.dart';
+
+/// The path to the root directory of the `test` package.
+final Future<String> packageDir =
+    Isolate.resolvePackageUri(Uri(scheme: 'package', path: 'test/'))
+        .then((uri) {
+  var dir = p.dirname(uri!.path);
+  // If it starts with a `/C:` or other drive letter, remove the leading `/`.
+  if (dir[0] == '/' && dir[2] == ':') 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),
+    Platform.isWindows ? 'pub.bat' : 'pub'));
+
+/// The platform-specific message emitted when a nonexistent file is loaded.
+final String noSuchFileMessage = Platform.isWindows
+    ? 'The system cannot find the file specified.'
+    : 'No such file or directory';
+
+/// An operating system name that's different than the current operating system.
+final otherOS = Platform.isWindows ? 'mac-os' : 'windows';
+
+/// Expects that the entire stdout stream of [test] equals [expected].
+void expectStdoutEquals(TestProcess test, String expected) =>
+    _expectStreamEquals(test.stdoutStream(), expected);
+
+/// Expects that the entire stderr stream of [test] equals [expected].
+void expectStderrEquals(TestProcess test, String expected) =>
+    _expectStreamEquals(test.stderrStream(), expected);
+
+/// Expects that the entirety of the line stream [stream] equals [expected].
+void _expectStreamEquals(Stream<String> stream, String expected) {
+  expect((() async {
+    var lines = await stream.toList();
+    expect(lines.join('\n').trim(), equals(expected.trim()));
+  })(), completes);
+}
+
+/// Returns a [StreamMatcher] that asserts that the stream emits strings
+/// containing each string in [strings] in order.
+///
+/// This expects each string in [strings] to match a different string in the
+/// stream.
+StreamMatcher containsInOrder(Iterable<String> strings) =>
+    emitsInOrder(strings.map((string) => emitsThrough(contains(string))));
+
+/// Lazily compile the test package to kernel and re-use that, initialized with
+/// [precompileTestExecutable].
+String? _testExecutablePath;
+
+/// Must be invoked before any call to [runTests], should be invoked in a top
+/// level `setUpAll` for best caching results.
+Future<void> precompileTestExecutable() async {
+  if (_testExecutablePath != null) {
+    throw StateError('Test executable already precompiled');
+  }
+  var tmpDirectory = await Directory.systemTemp.createTemp('test');
+  var precompiledPath = p.join(tmpDirectory.path, 'test_runner.dill');
+  var result = await Process.run(Platform.executable, [
+    'compile',
+    'kernel',
+    p.url.join(await packageDir, 'bin', 'test.dart'),
+    '-o',
+    precompiledPath,
+  ]);
+  if (result.exitCode != 0) {
+    throw StateError(
+        'Failed to compile test runner:\n${result.stdout}\n${result.stderr}');
+  }
+
+  addTearDown(() async {
+    await tmpDirectory.delete(recursive: true);
+  });
+  _testExecutablePath = precompiledPath;
+}
+
+/// Runs the test executable with the package root set properly.
+///
+/// You must invoke [precompileTestExecutable] before invoking this function.
+Future<TestProcess> runTest(Iterable<String> args,
+    {String? reporter,
+    String? fileReporter,
+    int? concurrency,
+    Map<String, String>? environment,
+    bool forwardStdio = false,
+    String? packageConfig,
+    Iterable<String>? vmArgs}) async {
+  concurrency ??= 1;
+  var testExecutablePath = _testExecutablePath;
+  if (testExecutablePath == null) {
+    throw StateError(
+        'You must call `precompileTestExecutable` before calling `runTest`');
+  }
+
+  var allArgs = [
+    ...?vmArgs,
+    testExecutablePath,
+    '--concurrency=$concurrency',
+    if (reporter != null) '--reporter=$reporter',
+    if (fileReporter != null) '--file-reporter=$fileReporter',
+    ...args,
+  ];
+
+  environment ??= {};
+  environment.putIfAbsent('_DART_TEST_TESTING', () => 'true');
+
+  return await runDart(allArgs,
+      environment: environment,
+      description: 'dart bin/test.dart',
+      forwardStdio: forwardStdio,
+      packageConfig: packageConfig);
+}
+
+/// Runs Dart.
+///
+/// If [packageConfig] is provided then that is passed for the `--packages`
+/// arg, otherwise the current isolate config is passed.
+Future<TestProcess> runDart(Iterable<String> args,
+    {Map<String, String>? environment,
+    String? description,
+    bool forwardStdio = false,
+    String? packageConfig}) async {
+  var allArgs = <String>[
+    ...Platform.executableArguments.where((arg) =>
+        !arg.startsWith('--package-root=') && !arg.startsWith('--packages=')),
+    '--packages=${packageConfig ?? await Isolate.packageConfig}',
+    ...args
+  ];
+
+  return await TestProcess.start(
+      p.absolute(Platform.resolvedExecutable), allArgs,
+      workingDirectory: d.sandbox,
+      environment: environment,
+      description: description,
+      forwardStdio: forwardStdio);
+}
+
+/// Runs Pub.
+Future<TestProcess> runPub(Iterable<String> args,
+    {Map<String, String>? environment}) {
+  return TestProcess.start(_pubPath, args,
+      workingDirectory: d.sandbox,
+      environment: environment,
+      description: 'pub ${args.first}');
+}
diff --git a/pkgs/test/test/runner/browser/chrome_test.dart b/pkgs/test/test/runner/browser/chrome_test.dart
new file mode 100644
index 0000000..ffec227
--- /dev/null
+++ b/pkgs/test/test/runner/browser/chrome_test.dart
@@ -0,0 +1,100 @@
+// 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.
+
+@TestOn('vm')
+@Tags(['chrome'])
+library;
+
+import 'package:test/src/runner/browser/chrome.dart';
+import 'package:test/src/runner/executable_settings.dart';
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../../io.dart';
+import '../../utils.dart';
+import 'code_server.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('starts Chrome with the given URL', () async {
+    var server = await CodeServer.start();
+
+    server.handleJavaScript('''
+var webSocket = new WebSocket(window.location.href.replace("http://", "ws://"));
+webSocket.addEventListener("open", function() {
+  webSocket.send("loaded!");
+});
+''');
+    var webSocket = server.handleWebSocket();
+
+    var chrome = Chrome(server.url, configuration());
+    addTearDown(() => chrome.close());
+
+    expect(await (await webSocket).stream.first, equals('loaded!'));
+  },
+      // It's not clear why, but this test in particular seems to time out
+      // when run in parallel with many other tests.
+      timeout: const Timeout.factor(2));
+
+  test("a process can be killed synchronously after it's started", () async {
+    var server = await CodeServer.start();
+    var chrome = Chrome(server.url, configuration());
+    await chrome.close();
+  });
+
+  test('reports an error in onExit', () {
+    var chrome = Chrome(Uri.https('dart.dev'), configuration(),
+        settings: ExecutableSettings(
+            linuxExecutable: '_does_not_exist',
+            macOSExecutable: '_does_not_exist',
+            windowsExecutable: '_does_not_exist'));
+    expect(
+        chrome.onExit,
+        throwsA(isApplicationException(
+            startsWith('Failed to run Chrome: $noSuchFileMessage'))));
+  });
+
+  test('can run successful tests', () async {
+    await d.file('test.dart', '''
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {});
+}
+''').create();
+
+    var test = await runTest(['-p', 'chrome', 'test.dart']);
+    expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+    await test.shouldExit(0);
+  });
+
+  test('can run failing tests', () async {
+    await d.file('test.dart', '''
+import 'package:test/test.dart';
+
+void main() {
+  test("failure", () => throw TestFailure("oh no"));
+}
+''').create();
+
+    var test = await runTest(['-p', 'chrome', 'test.dart']);
+    expect(test.stdout, emitsThrough(contains('-1: Some tests failed.')));
+    await test.shouldExit(1);
+  });
+
+  test('can override chrome location with CHROME_EXECUTABLE var', () async {
+    await d.file('test.dart', '''
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {});
+}
+''').create();
+    var test = await runTest(['-p', 'chrome', 'test.dart'],
+        environment: {'CHROME_EXECUTABLE': '/some/bad/path'});
+    expect(test.stdout, emitsThrough(contains('Failed to run Chrome:')));
+    await test.shouldExit(1);
+  });
+}
diff --git a/pkgs/test/test/runner/browser/code_server.dart b/pkgs/test/test/runner/browser/code_server.dart
new file mode 100644
index 0000000..1aba5de
--- /dev/null
+++ b/pkgs/test/test/runner/browser/code_server.dart
@@ -0,0 +1,145 @@
+// 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.
+
+import 'dart:async';
+import 'dart:collection';
+
+import 'package:http_multi_server/http_multi_server.dart';
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:shelf/shelf_io.dart' as shelf_io;
+import 'package:shelf_web_socket/shelf_web_socket.dart';
+import 'package:test/test.dart' show TestFailure, printOnFailure;
+import 'package:web_socket_channel/web_socket_channel.dart';
+
+/// A class that serves Dart and/or JS code and receives WebSocket connections.
+class CodeServer {
+  final _Handler _handler;
+
+  /// The URL of the server (including the port).
+  final Uri url;
+
+  static Future<CodeServer> start() async {
+    var server = await HttpMultiServer.loopback(0);
+    var handler = _Handler(Zone.current.handleUncaughtError);
+    shelf_io.serveRequests(server, (request) {
+      if (request.method == 'GET' && request.url.path == 'favicon.ico') {
+        return shelf.Response.notFound(null);
+      } else {
+        return handler(request);
+      }
+    });
+
+    return CodeServer._(handler, Uri.parse('http://localhost:${server.port}'));
+  }
+
+  CodeServer._(this._handler, this.url);
+
+  /// Sets up a handler for the root of the server, "/", that serves a basic
+  /// HTML page with a script tag that will run [javaScript].
+  void handleJavaScript(String javaScript) {
+    _handler.expect('GET', '/', (_) {
+      return shelf.Response.ok('''
+<!doctype html>
+<html>
+<head>
+  <script src="index.js"></script>
+</head>
+</html>
+''', headers: {'content-type': 'text/html'});
+    });
+
+    _handler.expect('GET', '/index.js', (_) {
+      return shelf.Response.ok(javaScript,
+          headers: {'content-type': 'application/javascript'});
+    });
+  }
+
+  /// Handles a WebSocket connection to the root of the server, and returns a
+  /// future that will complete to the WebSocket.
+  Future<WebSocketChannel> handleWebSocket() {
+    var completer = Completer<WebSocketChannel>();
+    // Note: the WebSocketChannel type below is needed for compatibility with
+    // package:shelf_web_socket v2.
+    _handler.expect('GET', '/', webSocketHandler((WebSocketChannel ws, _) {
+      completer.complete(ws);
+    }));
+    return completer.future;
+  }
+}
+
+/// A [shelf.Handler] that handles requests as specified by [expect].
+class _Handler {
+  /// A callback called whenever an unexpected exception is thrown.
+  ///
+  /// This is used over throwing errors directly since the request handler might
+  /// not be running in the same error zone as the test.
+  final void Function(Object, StackTrace) _onError;
+
+  /// The queue of expected requests to this handler.
+  final _expectations = Queue<_Expectation>();
+
+  /// Creates a new handler that handles requests using handlers provided by
+  /// [expect].
+  _Handler(this._onError);
+
+  /// Expects that a single HTTP request with the given [method] and [path] will
+  /// be made to [this].
+  ///
+  /// The [path] should be root-relative; that is, it should start with "/".
+  ///
+  /// When a matching request is made, [handler] is used to handle that request.
+  ///
+  /// If this is called multiple times, the requests are expected to occur in
+  /// the same order.
+  void expect(String method, String path, shelf.Handler handler) {
+    _expectations.add(_Expectation(method, path, handler));
+  }
+
+  /// The implementation of [shelf.Handler].
+  FutureOr<shelf.Response> call(shelf.Request request) async {
+    const description = 'ShelfTesthandler';
+    var requestInfo = '${request.method} /${request.url}';
+    printOnFailure('[$description] $requestInfo');
+
+    try {
+      if (_expectations.isEmpty) {
+        throw TestFailure(
+            '$description received unexpected request $requestInfo.');
+      }
+
+      var expectation = _expectations.removeFirst();
+      if ((expectation.method != null &&
+              expectation.method != request.method) ||
+          (expectation.path != '/${request.url.path}' &&
+              expectation.path != null)) {
+        var message = '$description received unexpected request $requestInfo.';
+        if (expectation.method != null) {
+          message += '\nExpected ${expectation.method} ${expectation.path}.';
+        }
+        throw TestFailure(message);
+      }
+
+      return await expectation.handler(request);
+    } on shelf.HijackException catch (_) {
+      rethrow;
+    } catch (error, stackTrace) {
+      _onError(error, stackTrace);
+      return shelf.Response.internalServerError(body: '$error');
+    }
+  }
+}
+
+/// A single expectation for an HTTP request sent to a [_Handler].
+class _Expectation {
+  /// The expected request method, or [null] if this allows any requests.
+  final String? method;
+
+  /// The expected request path, or [null] if this allows any requests.
+  final String? path;
+
+  /// The handler to use for requests that match this expectation.
+  final shelf.Handler handler;
+
+  _Expectation(this.method, this.path, this.handler);
+}
diff --git a/pkgs/test/test/runner/browser/compact_reporter_test.dart b/pkgs/test/test/runner/browser/compact_reporter_test.dart
new file mode 100644
index 0000000..753ffa1
--- /dev/null
+++ b/pkgs/test/test/runner/browser/compact_reporter_test.dart
@@ -0,0 +1,35 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('prints the platform name when running on multiple platforms', () async {
+    await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {});
+}
+''').create();
+
+    var test = await runTest(
+        ['-p', 'chrome', '-p', 'vm', '-j', '1', 'test.dart'],
+        reporter: 'compact');
+
+    expect(test.stdout, containsInOrder(['[Chrome, Dart2Js]', '[VM, Kernel]']));
+    await test.shouldExit(0);
+  }, tags: 'chrome');
+}
diff --git a/pkgs/test/test/runner/browser/expanded_reporter_test.dart b/pkgs/test/test/runner/browser/expanded_reporter_test.dart
new file mode 100644
index 0000000..875aaba
--- /dev/null
+++ b/pkgs/test/test/runner/browser/expanded_reporter_test.dart
@@ -0,0 +1,36 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('prints the platform name when running on multiple platforms', () async {
+    await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {});
+}
+''').create();
+
+    var test = await runTest([
+      '-r', 'expanded', '-p', 'chrome', '-p', 'vm', '-j', '1', //
+      'test.dart'
+    ]);
+
+    expect(test.stdoutStream(), emitsThrough(contains('[VM, Kernel]')));
+    expect(test.stdout, emitsThrough(contains('[Chrome, Dart2Js]')));
+    await test.shouldExit(0);
+  }, tags: ['chrome']);
+}
diff --git a/pkgs/test/test/runner/browser/firefox_html_test.dart b/pkgs/test/test/runner/browser/firefox_html_test.dart
new file mode 100644
index 0000000..c9a5302
--- /dev/null
+++ b/pkgs/test/test/runner/browser/firefox_html_test.dart
@@ -0,0 +1,18 @@
+// 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.
+
+@TestOn('firefox')
+library;
+
+import 'package:test/src/runner/browser/dom.dart' as dom;
+import 'package:test/test.dart';
+
+void main() {
+  // Regression test for #274. Firefox doesn't compute styles within hidden
+  // iframes (https://bugzilla.mozilla.org/show_bug.cgi?id=548397), so we have
+  // to do some special stuff to make sure tests that care about that work.
+  test('getComputedStyle() works', () {
+    expect(dom.window.getComputedStyle(dom.document.body!), isNotNull);
+  });
+}
diff --git a/pkgs/test/test/runner/browser/firefox_test.dart b/pkgs/test/test/runner/browser/firefox_test.dart
new file mode 100644
index 0000000..864f49c
--- /dev/null
+++ b/pkgs/test/test/runner/browser/firefox_test.dart
@@ -0,0 +1,115 @@
+// 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.
+@TestOn('vm')
+@Tags(['firefox'])
+library;
+
+import 'package:test/src/runner/browser/firefox.dart';
+import 'package:test/src/runner/executable_settings.dart';
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../../io.dart';
+import '../../utils.dart';
+import 'code_server.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('starts Firefox with the given URL', () async {
+    var server = await CodeServer.start();
+
+    server.handleJavaScript('''
+var webSocket = new WebSocket(window.location.href.replace("http://", "ws://"));
+webSocket.addEventListener("open", function() {
+  webSocket.send("loaded!");
+});
+''');
+    var webSocket = server.handleWebSocket();
+
+    var firefox = Firefox(server.url);
+    addTearDown(() => firefox.close());
+
+    expect(await (await webSocket).stream.first, equals('loaded!'));
+  });
+
+  test("a process can be killed synchronously after it's started", () async {
+    var server = await CodeServer.start();
+
+    var firefox = Firefox(server.url);
+    await firefox.close();
+  });
+
+  test('reports an error in onExit', () {
+    var firefox = Firefox(Uri.https('dart.dev'),
+        settings: ExecutableSettings(
+            linuxExecutable: '_does_not_exist',
+            macOSExecutable: '_does_not_exist',
+            windowsExecutable: '_does_not_exist'));
+    expect(
+        firefox.onExit,
+        throwsA(isApplicationException(
+            startsWith('Failed to run Firefox: $noSuchFileMessage'))));
+  });
+
+  test('can run successful tests', () async {
+    await d.file('test.dart', '''
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {});
+}
+''').create();
+
+    var test = await runTest(['-p', 'firefox', 'test.dart']);
+    expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+    await test.shouldExit(0);
+  });
+
+  test('can run failing tests', () async {
+    await d.file('test.dart', '''
+import 'package:test/test.dart';
+
+void main() {
+  test("failure", () => throw TestFailure("oh no"));
+}
+''').create();
+
+    var test = await runTest(['-p', 'firefox', 'test.dart']);
+    expect(test.stdout, emitsThrough(contains('-1: Some tests failed.')));
+    await test.shouldExit(1);
+  });
+
+  test('can override firefox location with FIREFOX_EXECUTABLE var', () async {
+    await d.file('test.dart', '''
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {});
+}
+''').create();
+    var test = await runTest(['-p', 'firefox', 'test.dart'],
+        environment: {'FIREFOX_EXECUTABLE': '/some/bad/path'});
+    expect(test.stdout, emitsThrough(contains('Failed to run Firefox:')));
+    await test.shouldExit(1);
+  });
+
+  test('not impacted by CHROME_EXECUTABLE var', () async {
+    await d.file('test.dart', '''
+import 'dart:html';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {
+    assert(window.navigator.vendor != 'Google Inc.');
+  });
+}
+''').create();
+    var test = await runTest(['-p', 'firefox', 'test.dart'],
+        environment: {'CHROME_EXECUTABLE': '/some/bad/path'});
+    expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+    await test.shouldExit(0);
+  });
+}
diff --git a/pkgs/test/test/runner/browser/loader_test.dart b/pkgs/test/test/runner/browser/loader_test.dart
new file mode 100644
index 0000000..17cdb9a
--- /dev/null
+++ b/pkgs/test/test/runner/browser/loader_test.dart
@@ -0,0 +1,181 @@
+// 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.
+
+@TestOn('vm')
+@Tags(['chrome'])
+library;
+
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:test/src/runner/browser/platform.dart';
+import 'package:test/test.dart';
+import 'package:test_api/src/backend/runtime.dart';
+import 'package:test_api/src/backend/state.dart';
+import 'package:test_api/src/backend/test.dart';
+import 'package:test_core/src/runner/hack_register_platform.dart';
+import 'package:test_core/src/runner/loader.dart';
+import 'package:test_core/src/runner/runner_suite.dart';
+import 'package:test_core/src/runner/runner_test.dart';
+import 'package:test_core/src/runner/runtime_selection.dart';
+import 'package:test_core/src/runner/suite.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../../utils.dart';
+
+late Loader _loader;
+
+/// A configuration that loads suites on Chrome.
+final _chrome =
+    SuiteConfiguration.runtimes([RuntimeSelection(Runtime.chrome.identifier)]);
+
+void main() {
+  setUp(() async {
+    // The default loader doesn't have the platforms registered.
+    registerPlatformPlugin([
+      Runtime.chrome,
+    ], () => BrowserPlatform.start(root: d.sandbox));
+
+    _loader = Loader();
+
+    await d.file('a_test.dart', '''
+      import 'dart:async';
+
+      import 'package:test/test.dart';
+
+      void main() {
+        test("success", () {});
+        test("failure", () => throw TestFailure('oh no'));
+        test("error", () => throw 'oh no');
+      }
+    ''').create();
+  });
+
+  tearDown(() => _loader.close());
+
+  group('.loadFile()', () {
+    late RunnerSuite suite;
+    setUp(() async {
+      var suites = await _loader
+          .loadFile(p.join(d.sandbox, 'a_test.dart'), _chrome)
+          .toList();
+
+      expect(suites, hasLength(1));
+      var loadSuite = suites.first;
+      suite = (await loadSuite.getSuite())!;
+    });
+
+    test('returns a suite with the file path and platform', () {
+      expect(suite.path, equals(p.join(d.sandbox, 'a_test.dart')));
+      expect(suite.platform.runtime, equals(Runtime.chrome));
+      expect(suite.platform.compiler, equals(Runtime.chrome.defaultCompiler));
+    });
+
+    test('returns tests with the correct names', () {
+      expect(suite.group.entries, hasLength(3));
+      expect(suite.group.entries[0].name, equals('success'));
+      expect(suite.group.entries[1].name, equals('failure'));
+      expect(suite.group.entries[2].name, equals('error'));
+    });
+
+    test('can load and run a successful test', () {
+      var liveTest = (suite.group.entries[0] as RunnerTest).load(suite);
+
+      expectStates(liveTest, [
+        const State(Status.running, Result.success),
+        const State(Status.complete, Result.success)
+      ]);
+      expectErrors(liveTest, []);
+
+      return liveTest.run().whenComplete(() => liveTest.close());
+    });
+
+    test('can load and run a failing test', () {
+      var liveTest = (suite.group.entries[1] as RunnerTest).load(suite);
+      expectSingleFailure(liveTest);
+      return liveTest.run().whenComplete(() => liveTest.close());
+    });
+  });
+
+  test('loads tests that are defined asynchronously', () async {
+    File(p.join(d.sandbox, 'a_test.dart')).writeAsStringSync('''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+Future main() {
+  return Future(() {
+    test("success", () {});
+
+    return Future(() {
+      test("failure", () => throw TestFailure('oh no'));
+
+      return Future(() {
+        test("error", () => throw 'oh no');
+      });
+    });
+  });
+}
+''');
+
+    var suites = await _loader
+        .loadFile(p.join(d.sandbox, 'a_test.dart'), _chrome)
+        .toList();
+    expect(suites, hasLength(1));
+    var loadSuite = suites.first;
+    var suite = (await loadSuite.getSuite())!;
+    expect(suite.group.entries, hasLength(3));
+    expect(suite.group.entries[0].name, equals('success'));
+    expect(suite.group.entries[1].name, equals('failure'));
+    expect(suite.group.entries[2].name, equals('error'));
+  });
+
+  test('loads a suite both in the browser and the VM', () async {
+    var path = p.join(d.sandbox, 'a_test.dart');
+
+    var suites = await _loader
+        .loadFile(
+            path,
+            SuiteConfiguration.runtimes([
+              RuntimeSelection(Runtime.vm.identifier),
+              RuntimeSelection(Runtime.chrome.identifier)
+            ]))
+        .asyncMap((loadSuite) => loadSuite.getSuite())
+        .cast<RunnerSuite>()
+        .toList();
+    expect(suites[0].platform.runtime, equals(Runtime.vm));
+    expect(suites[0].platform.compiler, equals(Runtime.vm.defaultCompiler));
+    expect(suites[0].path, equals(path));
+    expect(suites[1].platform.runtime, equals(Runtime.chrome));
+    expect(suites[1].platform.compiler, equals(Runtime.chrome.defaultCompiler));
+    expect(suites[1].path, equals(path));
+
+    for (var suite in suites) {
+      expect(suite.group.entries, hasLength(3));
+      expect(suite.group.entries[0].name, equals('success'));
+      expect(suite.group.entries[1].name, equals('failure'));
+      expect(suite.group.entries[2].name, equals('error'));
+    }
+  });
+
+  test('a print in a loaded file is piped through the LoadSuite', () async {
+    File(p.join(d.sandbox, 'a_test.dart')).writeAsStringSync('''
+void main() {
+  print('print within test');
+}
+''');
+    var suites = await _loader
+        .loadFile(p.join(d.sandbox, 'a_test.dart'), _chrome)
+        .toList();
+    expect(suites, hasLength(1));
+    var loadSuite = suites.first;
+
+    var liveTest = (loadSuite.group.entries.single as Test).load(loadSuite);
+    // Skip the "Compiled" message from dart2js.
+    expect(liveTest.onMessage.skip(1).first.then((message) => message.text),
+        completion(equals('print within test')));
+    await liveTest.run();
+    expectTestPassed(liveTest);
+  });
+}
diff --git a/pkgs/test/test/runner/browser/microsoft_edge_test.dart b/pkgs/test/test/runner/browser/microsoft_edge_test.dart
new file mode 100644
index 0000000..a48bfd2
--- /dev/null
+++ b/pkgs/test/test/runner/browser/microsoft_edge_test.dart
@@ -0,0 +1,91 @@
+// Copyright (c) 2023, 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.
+
+@TestOn('vm')
+@Tags(['edge'])
+library;
+
+import 'package:test/src/runner/browser/microsoft_edge.dart';
+import 'package:test/src/runner/executable_settings.dart';
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../../io.dart';
+import '../../utils.dart';
+import 'code_server.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('starts edge with the given URL', () async {
+    var server = await CodeServer.start();
+
+    server.handleJavaScript('''
+var webSocket = new WebSocket(window.location.href.replace("http://", "ws://"));
+webSocket.addEventListener("open", function() {
+  webSocket.send("loaded!");
+});
+''');
+    var webSocket = server.handleWebSocket();
+
+    var edge = MicrosoftEdge(server.url, configuration());
+    addTearDown(() => edge.close());
+
+    expect(await (await webSocket).stream.first, equals('loaded!'));
+  }, timeout: const Timeout.factor(2));
+
+  test('reports an error in onExit', () {
+    var edge = MicrosoftEdge(Uri.parse('https://dart.dev'), configuration(),
+        settings: ExecutableSettings(
+            linuxExecutable: '_does_not_exist',
+            macOSExecutable: '_does_not_exist',
+            windowsExecutable: '_does_not_exist'));
+    expect(
+        edge.onExit,
+        throwsA(isApplicationException(
+            startsWith('Failed to run Edge: $noSuchFileMessage'))));
+  });
+
+  test('can run successful tests', () async {
+    await d.file('test.dart', '''
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {});
+}
+''').create();
+
+    var test = await runTest(['-p', 'edge', 'test.dart']);
+    expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+    await test.shouldExit(0);
+  });
+
+  test('can run failing tests', () async {
+    await d.file('test.dart', '''
+import 'package:test/test.dart';
+
+void main() {
+  test("failure", () => throw TestFailure("oh no"));
+}
+''').create();
+
+    var test = await runTest(['-p', 'edge', 'test.dart']);
+    expect(test.stdout, emitsThrough(contains('-1: Some tests failed.')));
+    await test.shouldExit(1);
+  });
+
+  test('can override edge location with MS_EDGE_EXECUTABLE var', () async {
+    await d.file('test.dart', '''
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {});
+}
+''').create();
+    var test = await runTest(['-p', 'edge', 'test.dart'],
+        environment: {'MS_EDGE_EXECUTABLE': '/some/bad/path'});
+    expect(test.stdout, emitsThrough(contains('Failed to run Edge:')));
+    await test.shouldExit(1);
+  });
+}
diff --git a/pkgs/test/test/runner/browser/runner_test.dart b/pkgs/test/test/runner/browser/runner_test.dart
new file mode 100644
index 0000000..c55cbb8
--- /dev/null
+++ b/pkgs/test/test/runner/browser/runner_test.dart
@@ -0,0 +1,978 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:convert';
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../../io.dart';
+
+final _success = '''
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {});
+}
+''';
+
+final _failure = '''
+import 'package:test/test.dart';
+
+void main() {
+  test("failure", () => throw TestFailure("oh no"));
+}
+''';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  group('fails gracefully if', () {
+    test('a test file fails to compile', () async {
+      await d.file('test.dart', 'invalid Dart file').create();
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+
+      expect(
+          test.stdout,
+          containsInOrder([
+            'Error: Compilation failed.',
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart": dart2js failed.'
+          ]));
+      await test.shouldExit(1);
+    }, tags: 'chrome');
+
+    test('a test file throws', () async {
+      await d.file('test.dart', "void main() => throw 'oh no';").create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart": oh no'
+          ]));
+      await test.shouldExit(1);
+    }, tags: 'chrome');
+
+    test("a test file doesn't have a main defined", () async {
+      await d.file('test.dart', 'void foo() {}').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart": No top-level main() function defined.'
+          ]));
+      await test.shouldExit(1);
+    }, tags: 'chrome', skip: 'https://github.com/dart-lang/test/issues/894');
+
+    test('a test file has a non-function main', () async {
+      await d.file('test.dart', 'int main;').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart": Top-level main getter is not a function.'
+          ]));
+      await test.shouldExit(1);
+    }, tags: 'chrome', skip: 'https://github.com/dart-lang/test/issues/894');
+
+    test('a test file has a main with arguments', () async {
+      await d.file('test.dart', 'void main(arg) {}').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart": Top-level main() function takes arguments.'
+          ]));
+      await test.shouldExit(1);
+    }, tags: 'chrome');
+
+    test('a custom HTML file has no script tag', () async {
+      await d.file('test.dart', 'void main() {}').create();
+
+      await d.file('test.html', '''
+<html>
+<head>
+  <link rel="x-dart-test" href="test.dart">
+</head>
+</html>
+''').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart": "test.html" must contain '
+                '<script src="packages/test/dart.js"></script>.'
+          ]));
+      await test.shouldExit(1);
+    }, tags: 'chrome');
+
+    test('a custom HTML file has no link', () async {
+      await d.file('test.dart', 'void main() {}').create();
+
+      await d.file('test.html', '''
+<html>
+<head>
+  <script src="packages/test/dart.js"></script>
+</head>
+</html>
+''').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart": Expected exactly 1 '
+                '<link rel="x-dart-test"> in test.html, found 0.'
+          ]));
+      await test.shouldExit(1);
+    }, tags: 'chrome');
+
+    test('a custom HTML file has too many links', () async {
+      await d.file('test.dart', 'void main() {}').create();
+
+      await d.file('test.html', '''
+<html>
+<head>
+  <link rel='x-dart-test' href='test.dart'>
+  <link rel='x-dart-test' href='test.dart'>
+  <script src="packages/test/dart.js"></script>
+</head>
+</html>
+''').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart": Expected exactly 1 '
+                '<link rel="x-dart-test"> in test.html, found 2.'
+          ]));
+      await test.shouldExit(1);
+    }, tags: 'chrome');
+
+    test('a custom HTML file has no href in the link', () async {
+      await d.file('test.dart', 'void main() {}').create();
+
+      await d.file('test.html', '''
+<html>
+<head>
+  <link rel='x-dart-test'>
+  <script src="packages/test/dart.js"></script>
+</head>
+</html>
+''').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart": Expected <link rel="x-dart-test"> in '
+                'test.html to have an "href" attribute.'
+          ]));
+      await test.shouldExit(1);
+    }, tags: 'chrome');
+
+    test('a custom HTML file has an invalid test URL', () async {
+      await d.file('test.dart', 'void main() {}').create();
+
+      await d.file('test.html', '''
+<html>
+<head>
+  <link rel='x-dart-test' href='wrong.dart'>
+  <script src="packages/test/dart.js"></script>
+</head>
+</html>
+''').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart": Failed to load script at '
+          ]));
+      await test.shouldExit(1);
+    }, tags: 'chrome');
+
+    test(
+        'still errors even with a custom HTML template set since it will take precedence',
+        () async {
+      await d.file('test.dart', 'void main() {}').create();
+
+      await d.file('test.html', '''
+<html>
+<head>
+  <link rel="x-dart-test" href="test.dart">
+</head>
+</html>
+''').create();
+
+      await d
+          .file(
+              'global_test.yaml',
+              jsonEncode(
+                  {'custom_html_template_path': 'html_template.html.tpl'}))
+          .create();
+
+      await d.file('html_template.html.tpl', '''
+<html>
+<head>
+  {{testScript}}
+  <script src="packages/test/dart.js"></script>
+</head>
+<body>
+  <div id="foo"></div>
+</body>
+</html>
+''').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart'],
+          environment: {'DART_TEST_CONFIG': 'global_test.yaml'});
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart": "test.html" must contain '
+                '<script src="packages/test/dart.js"></script>.'
+          ]));
+      await test.shouldExit(1);
+    }, tags: 'chrome');
+
+    group('with a custom HTML template', () {
+      setUp(() async {
+        await d.file('test.dart', _success).create();
+        await d
+            .file(
+                'global_test.yaml',
+                jsonEncode(
+                    {'custom_html_template_path': 'html_template.html.tpl'}))
+            .create();
+      });
+
+      test('that does not exist', () async {
+        var test = await runTest(['-p', 'chrome', 'test.dart'],
+            environment: {'DART_TEST_CONFIG': 'global_test.yaml'});
+        expect(
+            test.stdout,
+            containsInOrder([
+              '-1: loading test.dart [E]',
+              'Failed to load "test.dart": "html_template.html.tpl" does not exist or is not readable'
+            ]));
+        await test.shouldExit(1);
+      }, tags: 'chrome');
+
+      test("that doesn't contain the {{testScript}} tag", () async {
+        await d.file('html_template.html.tpl', '''
+<html>
+<head>
+  <script src="packages/test/dart.js"></script>
+</head>
+<body>
+  <div id="foo"></div>
+</body>
+</html>
+''').create();
+
+        var test = await runTest(['-p', 'chrome', 'test.dart'],
+            environment: {'DART_TEST_CONFIG': 'global_test.yaml'});
+        expect(
+            test.stdout,
+            containsInOrder([
+              '-1: loading test.dart [E]',
+              'Failed to load "test.dart": "html_template.html.tpl" must contain exactly one {{testScript}} placeholder'
+            ]));
+        await test.shouldExit(1);
+      }, tags: 'chrome');
+
+      test('that contains more than one {{testScript}} tag', () async {
+        await d.file('html_template.html.tpl', '''
+<html>
+<head>
+  {{testScript}}
+  {{testScript}}
+  <script src="packages/test/dart.js"></script>
+</head>
+<body>
+  <div id="foo"></div>
+</body>
+</html>
+''').create();
+
+        var test = await runTest(['-p', 'chrome', 'test.dart'],
+            environment: {'DART_TEST_CONFIG': 'global_test.yaml'});
+        expect(
+            test.stdout,
+            containsInOrder([
+              '-1: loading test.dart [E]',
+              'Failed to load "test.dart": "html_template.html.tpl" must contain exactly one {{testScript}} placeholder'
+            ]));
+        await test.shouldExit(1);
+      }, tags: 'chrome');
+
+      test('that has no script tag', () async {
+        await d.file('html_template.html.tpl', '''
+<html>
+<head>
+  {{testScript}}
+</head>
+</html>
+''').create();
+
+        var test = await runTest(['-p', 'chrome', 'test.dart'],
+            environment: {'DART_TEST_CONFIG': 'global_test.yaml'});
+        expect(
+            test.stdout,
+            containsInOrder([
+              '-1: loading test.dart [E]',
+              'Failed to load "test.dart": "html_template.html.tpl" must contain '
+                  '<script src="packages/test/dart.js"></script>.'
+            ]));
+        await test.shouldExit(1);
+      }, tags: 'chrome');
+
+      test('that is named like the test file', () async {
+        await d.file('test.html', '''
+<html>
+<head>
+  {{testScript}}
+  <script src="packages/test/dart.js"></script>
+</head>
+</html>
+''').create();
+
+        await d
+            .file('global_test_2.yaml',
+                jsonEncode({'custom_html_template_path': 'test.html'}))
+            .create();
+        var test = await runTest(['-p', 'chrome', 'test.dart'],
+            environment: {'DART_TEST_CONFIG': 'global_test_2.yaml'});
+        expect(
+            test.stdout,
+            containsInOrder([
+              '-1: loading test.dart [E]',
+              'Failed to load "test.dart": template file "test.html" cannot be named '
+                  'like the test file.'
+            ]));
+        await test.shouldExit(1);
+      });
+    });
+  });
+
+  group('runs successful tests', () {
+    test('on a browser and the VM', () async {
+      await d.file('test.dart', _success).create();
+      var test = await runTest(['-p', 'chrome', '-p', 'vm', 'test.dart']);
+
+      expect(test.stdout, emitsThrough(contains('+2: All tests passed!')));
+      await test.shouldExit(0);
+    }, tags: 'chrome');
+
+    test('with setUpAll', () async {
+      await d.file('test.dart', r'''
+          import 'package:test/test.dart';
+
+          void main() {
+            setUpAll(() => print("in setUpAll"));
+
+            test("test", () {});
+          }
+          ''').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('+0: (setUpAll)')));
+      expect(test.stdout, emits('in setUpAll'));
+      await test.shouldExit(0);
+    }, tags: 'chrome');
+
+    test('with tearDownAll', () async {
+      await d.file('test.dart', r'''
+          import 'package:test/test.dart';
+
+          void main() {
+            tearDownAll(() => print("in tearDownAll"));
+
+            test("test", () {});
+          }
+          ''').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: (tearDownAll)')));
+      expect(test.stdout, emits('in tearDownAll'));
+      await test.shouldExit(0);
+    }, tags: 'chrome');
+
+    // Regression test; this broke in 0.12.0-beta.9.
+    test('on a file in a subdirectory', () async {
+      await d.dir('dir', [d.file('test.dart', _success)]).create();
+
+      var test = await runTest(['-p', 'chrome', 'dir/test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    }, tags: 'chrome');
+
+    group('with a custom HTML template file', () {
+      group('without a {{testName}} tag', () {
+        setUp(() async {
+          await d
+              .file(
+                  'global_test.yaml',
+                  jsonEncode(
+                      {'custom_html_template_path': 'html_template.html.tpl'}))
+              .create();
+          await d.file('html_template.html.tpl', '''
+  <html>
+  <head>
+    {{testScript}}
+    <script src="packages/test/dart.js"></script>
+  </head>
+  <body>
+    <div id="foo"></div>
+  </body>
+  </html>
+  ''').create();
+
+          await d.file('test.dart', '''
+  import 'package:test/src/runner/browser/dom.dart' as dom;
+  import 'package:test/test.dart';
+
+  void main() {
+    test("success", () {
+      expect(dom.document.querySelector('#foo'), isNotNull);
+    });
+  }
+  ''').create();
+        });
+
+        test('on Chrome', () async {
+          var test = await runTest(['-p', 'chrome', 'test.dart'],
+              environment: {'DART_TEST_CONFIG': 'global_test.yaml'});
+          expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+          await test.shouldExit(0);
+        }, tags: 'chrome');
+      });
+
+      group('with a {{testName}} tag', () {
+        setUp(() async {
+          await d
+              .file(
+                  'global_test.yaml',
+                  jsonEncode(
+                      {'custom_html_template_path': 'html_template.html.tpl'}))
+              .create();
+          await d.file('html_template.html.tpl', '''
+  <html>
+  <head>
+    <title>{{testName}}</title>
+    {{testScript}}
+    <script src="packages/test/dart.js"></script>
+  </head>
+  <body>
+    <div id="foo"></div>
+  </body>
+  </html>
+  ''').create();
+
+          await d.file('test-with-title.dart', '''
+  import 'package:test/src/runner/browser/dom.dart' as dom;
+  import 'package:test/test.dart';
+
+  void main() {
+    test("success", () {
+      expect(dom.document.querySelector('#foo'), isNotNull);
+    });
+    test("title", () {
+      expect(dom.document.title, 'test-with-title.dart');
+    });
+  }
+  ''').create();
+        });
+
+        test('on Chrome', () async {
+          var test = await runTest(['-p', 'chrome', 'test-with-title.dart'],
+              environment: {'DART_TEST_CONFIG': 'global_test.yaml'});
+          expect(test.stdout, emitsThrough(contains('+2: All tests passed!')));
+          await test.shouldExit(0);
+        }, tags: 'chrome');
+      });
+    });
+
+    group('with a custom HTML file', () {
+      setUp(() async {
+        await d.file('test.dart', '''
+import 'package:test/src/runner/browser/dom.dart' as dom;
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {
+    expect(dom.document.querySelector('#foo'), isNotNull);
+  });
+}
+''').create();
+
+        await d.file('test.html', '''
+<html>
+<head>
+  <link rel='x-dart-test' href='test.dart'>
+  <script src="packages/test/dart.js"></script>
+</head>
+<body>
+  <div id="foo"></div>
+</body>
+</html>
+''').create();
+      });
+
+      test('on Chrome', () async {
+        var test = await runTest(['-p', 'chrome', 'test.dart']);
+        expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+        await test.shouldExit(0);
+      }, tags: 'chrome');
+
+      // Regression test for https://github.com/dart-lang/test/issues/82.
+      test('ignores irrelevant link tags', () async {
+        await d.file('test.html', '''
+<html>
+<head>
+  <link rel='x-dart-test-not'>
+  <link rel='other' href='test.dart'>
+  <link rel='x-dart-test' href='test.dart'>
+  <script src="packages/test/dart.js"></script>
+</head>
+<body>
+  <div id="foo"></div>
+</body>
+</html>
+''').create();
+
+        var test = await runTest(['-p', 'chrome', 'test.dart']);
+        expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+        await test.shouldExit(0);
+      }, tags: 'chrome');
+
+      test('takes precedence over provided HTML template', () async {
+        await d
+            .file(
+                'global_test.yaml',
+                jsonEncode(
+                    {'custom_html_template_path': 'html_template.html.tpl'}))
+            .create();
+        await d.file('html_template.html.tpl', '''
+<html>
+<head>
+  {{testScript}}
+  <script src="packages/test/dart.js"></script>
+</head>
+<body>
+  <div id="not-foo"></div>
+</body>
+</html>
+''').create();
+
+        var test = await runTest(['-p', 'chrome', 'test.dart'],
+            environment: {'DART_TEST_CONFIG': 'global_test.yaml'});
+        expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+        await test.shouldExit(0);
+      }, tags: 'chrome');
+    });
+  });
+
+  group('runs failing tests', () {
+    test('that fail only on the browser', () async {
+      await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+void main() {
+  test("test", () {
+    if (p.style == p.Style.url) throw TestFailure("oh no");
+  });
+}
+''').create();
+
+      var test = await runTest(['-p', 'chrome', '-p', 'vm', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1 -1: Some tests failed.')));
+      await test.shouldExit(1);
+    }, tags: 'chrome');
+
+    test('that fail only on the VM', () async {
+      await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+void main() {
+  test("test", () {
+    if (p.style != p.Style.url) throw TestFailure("oh no");
+  });
+}
+''').create();
+
+      var test = await runTest(['-p', 'chrome', '-p', 'vm', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1 -1: Some tests failed.')));
+      await test.shouldExit(1);
+    }, tags: 'chrome');
+
+    group('with a custom HTML file', () {
+      setUp(() async {
+        await d.file('test.dart', '''
+import 'package:test/src/runner/browser/dom.dart' as dom;
+import 'package:test/test.dart';
+
+void main() {
+  test("failure", () {
+    expect(dom.document.querySelector('#foo'), isNull);
+  });
+}
+''').create();
+
+        await d.file('test.html', '''
+<html>
+<head>
+  <link rel='x-dart-test' href='test.dart'>
+  <script src="packages/test/dart.js"></script>
+</head>
+<body>
+  <div id="foo"></div>
+</body>
+</html>
+''').create();
+      });
+
+      test('on Chrome', () async {
+        var test = await runTest(['-p', 'chrome', 'test.dart']);
+        expect(test.stdout, emitsThrough(contains('-1: Some tests failed.')));
+        await test.shouldExit(1);
+      }, tags: 'chrome');
+    });
+  });
+
+  test('the compiler uses colors if the test runner uses colors', () async {
+    await d.file('test.dart', '{').create();
+
+    var test = await runTest(['--color', '-p', 'chrome', 'test.dart']);
+    expect(test.stdout, emitsThrough(contains('\u001b[31m')));
+    await test.shouldExit(1);
+  }, tags: 'chrome');
+
+  test('forwards prints from the browser test', () async {
+    await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("test", () {
+    print("Hello,");
+    return Future(() => print("world!"));
+  });
+}
+''').create();
+
+    var test = await runTest(['-p', 'chrome', 'test.dart']);
+    expect(test.stdout, emitsInOrder([emitsThrough('Hello,'), 'world!']));
+    await test.shouldExit(0);
+  }, tags: 'chrome');
+
+  test('dartifies stack traces for JS-compiled tests by default', () async {
+    await d.file('test.dart', _failure).create();
+
+    var test = await runTest(['-p', 'chrome', '--verbose-trace', 'test.dart']);
+    expect(test.stdout,
+        containsInOrder([' main.<fn>', 'package:test', 'dart:async/zone.dart']),
+        skip: 'https://github.com/dart-lang/sdk/issues/41949');
+    await test.shouldExit(1);
+  }, tags: 'chrome');
+
+  test("doesn't dartify stack traces for JS-compiled tests with --js-trace",
+      () async {
+    await d.file('test.dart', _failure).create();
+
+    var test = await runTest(
+        ['-p', 'chrome', '--verbose-trace', '--js-trace', 'test.dart']);
+    expect(test.stdoutStream(), neverEmits(endsWith(' main.<fn>')));
+    expect(test.stdoutStream(), neverEmits(contains('package:test')));
+    expect(test.stdoutStream(), neverEmits(contains('dart:async/zone.dart')));
+    expect(test.stdout, emitsThrough(contains('-1: Some tests failed.')));
+    await test.shouldExit(1);
+  }, tags: 'chrome');
+
+  test('respects top-level @Timeout declarations', () async {
+    await d.file('test.dart', '''
+@Timeout(const Duration(seconds: 0))
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("timeout", () => Future.delayed(Duration.zero));
+}
+''').create();
+
+    var test = await runTest(['-p', 'chrome', 'test.dart']);
+    expect(
+        test.stdout,
+        containsInOrder(
+            ['Test timed out after 0 seconds.', '-1: Some tests failed.']));
+    await test.shouldExit(1);
+  }, tags: 'chrome');
+
+  group('with onPlatform', () {
+    test('respects matching Skips', () async {
+      await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("fail", () => throw 'oh no', onPlatform: {"browser": Skip()});
+}
+''').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('+0 ~1: All tests skipped.')));
+      await test.shouldExit(0);
+    }, tags: 'chrome');
+
+    test('ignores non-matching Skips', () async {
+      await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {}, onPlatform: {"vm": Skip()});
+}
+''').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    }, tags: 'chrome');
+
+    test('respects matching Timeouts', () async {
+      await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("fail", () async {
+    await Future.delayed(Duration.zero);
+    throw 'oh no';
+  }, onPlatform: {
+    "browser": Timeout(Duration.zero)
+  });
+}
+''').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder(
+              ['Test timed out after 0 seconds.', '-1: Some tests failed.']));
+      await test.shouldExit(1);
+    }, tags: 'chrome');
+
+    test('ignores non-matching Timeouts', () async {
+      await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {}, onPlatform: {
+    "vm": Timeout(Duration(seconds: 0))
+  });
+}
+''').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    }, tags: 'chrome');
+
+    test('applies matching platforms in order', () async {
+      await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {}, onPlatform: {
+    "browser": Skip("first"),
+    "browser || windows": Skip("second"),
+    "browser || linux": Skip("third"),
+    "browser || mac-os": Skip("fourth"),
+    "browser || android": Skip("fifth")
+  });
+}
+''').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(test.stdoutStream(), neverEmits(contains('Skip: first')));
+      expect(test.stdoutStream(), neverEmits(contains('Skip: second')));
+      expect(test.stdoutStream(), neverEmits(contains('Skip: third')));
+      expect(test.stdoutStream(), neverEmits(contains('Skip: fourth')));
+      expect(test.stdout, emitsThrough(contains('Skip: fifth')));
+      await test.shouldExit(0);
+    }, tags: 'chrome');
+  });
+
+  group('with an @OnPlatform annotation', () {
+    test('respects matching Skips', () async {
+      await d.file('test.dart', '''
+@OnPlatform(const {"browser": const Skip()})
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("fail", () => throw 'oh no');
+}
+''').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('~1: All tests skipped.')));
+      await test.shouldExit(0);
+    }, tags: 'chrome');
+
+    test('ignores non-matching Skips', () async {
+      await d.file('test.dart', '''
+@OnPlatform(const {"vm": const Skip()})
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {});
+}
+''').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    }, tags: 'chrome');
+
+    test('respects matching Timeouts', () async {
+      await d.file('test.dart', '''
+@OnPlatform(const {
+  "browser": const Timeout(const Duration(seconds: 0))
+})
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("fail", () async {
+    await Future.delayed(Duration.zero);
+    throw 'oh no';
+  });
+}
+''').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder(
+              ['Test timed out after 0 seconds.', '-1: Some tests failed.']));
+      await test.shouldExit(1);
+    }, tags: 'chrome');
+
+    test('ignores non-matching Timeouts', () async {
+      await d.file('test.dart', '''
+@OnPlatform(const {
+  "vm": const Timeout(const Duration(seconds: 0))
+})
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {});
+}
+''').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      await test.shouldExit(0);
+    }, tags: 'chrome');
+  });
+
+  group('deferred loading', () {
+    test('can run browser tests with deferred library imports', () async {
+      await d.file('deferred.dart', '''
+int x = 1;
+''').create();
+      await d.file('test.dart', '''
+import 'package:test/test.dart';
+
+import 'deferred.dart' deferred as d;
+
+void main() {
+  test("success", () async {
+    await d.loadLibrary();
+    expect(d.x, 1);
+  });
+}
+''').create();
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    }, tags: 'chrome');
+
+    test('stack trace mapping works for deferred loaded libraries', () async {
+      await d.file('deferred.dart', '''
+int get x {
+  throw 'Oh no!';
+}
+''').create();
+      await d.file('test.dart', '''
+import 'package:test/test.dart';
+
+import 'deferred.dart' deferred as d;
+
+void main() {
+  test("failure", () async {
+    await d.loadLibrary();
+    expect(d.x, 1);
+  });
+}
+''').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            'Oh no!',
+            'deferred.dart',
+            'test.dart',
+          ]));
+      await test.shouldExit(1);
+    }, tags: 'chrome');
+  });
+}
diff --git a/pkgs/test/test/runner/browser/safari_test.dart b/pkgs/test/test/runner/browser/safari_test.dart
new file mode 100644
index 0000000..28f4022
--- /dev/null
+++ b/pkgs/test/test/runner/browser/safari_test.dart
@@ -0,0 +1,100 @@
+// 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.
+
+@TestOn('vm')
+@Tags(['safari'])
+library;
+
+import 'package:test/src/runner/browser/safari.dart';
+import 'package:test/src/runner/executable_settings.dart';
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../../io.dart';
+import '../../utils.dart';
+import 'code_server.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('starts Safari with the given URL',
+      skip: 'https://github.com/dart-lang/test/issues/1253', () async {
+    var server = await CodeServer.start();
+
+    server.handleJavaScript('''
+var webSocket = new WebSocket(window.location.href.replace("http://", "ws://"));
+webSocket.addEventListener("open", function() {
+  webSocket.send("loaded!");
+});
+''');
+    var webSocket = server.handleWebSocket();
+
+    var safari = Safari(server.url);
+    addTearDown(() => safari.close());
+
+    expect(await (await webSocket).stream.first, equals('loaded!'));
+  });
+
+  test("a process can be killed synchronously after it's started", () async {
+    var server = await CodeServer.start();
+
+    var safari = Safari(server.url);
+    await safari.close();
+  });
+
+  test('reports an error in onExit', () {
+    var safari = Safari(Uri.https('dart.dev'),
+        settings: ExecutableSettings(
+            linuxExecutable: '_does_not_exist',
+            macOSExecutable: '_does_not_exist',
+            windowsExecutable: '_does_not_exist'));
+    expect(
+        safari.onExit,
+        throwsA(isApplicationException(
+            startsWith('Failed to run Safari: $noSuchFileMessage'))));
+  });
+
+  test('can run successful tests',
+      skip: 'https://github.com/dart-lang/test/issues/1253', () async {
+    await d.file('test.dart', '''
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {});
+}
+''').create();
+
+    var test = await runTest(['-p', 'safari', 'test.dart']);
+    expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+    await test.shouldExit(0);
+  });
+
+  test('can run failing tests', () async {
+    await d.file('test.dart', '''
+import 'package:test/test.dart';
+
+void main() {
+  test("failure", () => throw TestFailure("oh no"));
+}
+''').create();
+
+    var test = await runTest(['-p', 'safari', 'test.dart']);
+    expect(test.stdout, emitsThrough(contains('-1: Some tests failed.')));
+    await test.shouldExit(1);
+  });
+
+  test('can override safari location with SAFARI_EXECUTABLE var', () async {
+    await d.file('test.dart', '''
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {});
+}
+''').create();
+    var test = await runTest(['-p', 'safari', 'test.dart'],
+        environment: {'SAFARI_EXECUTABLE': '/some/bad/path'});
+    expect(test.stdout, emitsThrough(contains('Failed to run Safari:')));
+    await test.shouldExit(1);
+  });
+}
diff --git a/pkgs/test/test/runner/compact_reporter_test.dart b/pkgs/test/test/runner/compact_reporter_test.dart
new file mode 100644
index 0000000..c935e02
--- /dev/null
+++ b/pkgs/test/test/runner/compact_reporter_test.dart
@@ -0,0 +1,550 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('reports when no tests are run', () async {
+    await d.file('test.dart', 'void main() {}').create();
+
+    var test = await runTest(['test.dart'], reporter: 'compact');
+    expect(test.stdout, emitsThrough(contains('No tests ran.')));
+    await test.shouldExit(79);
+  });
+
+  test('runs several successful tests and reports when each completes', () {
+    return _expectReport('''
+        test('success 1', () {});
+        test('success 2', () {});
+        test('success 3', () {});''', '''
+        +0: loading test.dart
+        +0: success 1
+        +1: success 1
+        +1: success 2
+        +2: success 2
+        +2: success 3
+        +3: success 3
+        +3: All tests passed!''');
+  });
+
+  test('runs several failing tests and reports when each fails', () {
+    return _expectReport('''
+        test('failure 1', () => throw TestFailure('oh no'));
+        test('failure 2', () => throw TestFailure('oh no'));
+        test('failure 3', () => throw TestFailure('oh no'));''', '''
+        +0: loading test.dart
+        +0: failure 1
+        +0 -1: failure 1 [E]
+          oh no
+          test.dart 6:33  main.<fn>
+
+
+        +0 -1: failure 2
+        +0 -2: failure 2 [E]
+          oh no
+          test.dart 7:33  main.<fn>
+
+
+        +0 -2: failure 3
+        +0 -3: failure 3 [E]
+          oh no
+          test.dart 8:33  main.<fn>
+
+
+        +0 -3: Some tests failed.''');
+  });
+
+  test('includes the full stack trace with --verbose-trace', () async {
+    await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("failure", () => throw "oh no");
+}
+''').create();
+
+    var test =
+        await runTest(['--verbose-trace', 'test.dart'], reporter: 'compact');
+    expect(test.stdout, emitsThrough(contains('dart:async')));
+    await test.shouldExit(1);
+  });
+
+  test('runs failing tests along with successful tests', () {
+    return _expectReport('''
+        test('failure 1', () => throw TestFailure('oh no'));
+        test('success 1', () {});
+        test('failure 2', () => throw TestFailure('oh no'));
+        test('success 2', () {});''', '''
+        +0: loading test.dart
+        +0: failure 1
+        +0 -1: failure 1 [E]
+          oh no
+          test.dart 6:33  main.<fn>
+
+
+        +0 -1: success 1
+        +1 -1: success 1
+        +1 -1: failure 2
+        +1 -2: failure 2 [E]
+          oh no
+          test.dart 8:33  main.<fn>
+
+
+        +1 -2: success 2
+        +2 -2: success 2
+        +2 -2: Some tests failed.''');
+  });
+
+  test('gracefully handles multiple test failures in a row', () {
+    return _expectReport('''
+        // This completer ensures that the test isolate isn't killed until all
+        // errors have been thrown.
+        var completer = Completer();
+        test('failures', () {
+          Future.microtask(() => throw 'first error');
+          Future.microtask(() => throw 'second error');
+          Future.microtask(() => throw 'third error');
+          Future.microtask(completer.complete);
+        });
+        test('wait', () => completer.future);''', '''
+        +0: loading test.dart
+        +0: failures
+        +0 -1: failures [E]
+          first error
+          test.dart 10:34  main.<fn>.<fn>
+          ===== asynchronous gap ===========================
+          dart:async       new Future.microtask
+          test.dart 10:18  main.<fn>
+
+          second error
+          test.dart 11:34  main.<fn>.<fn>
+          ===== asynchronous gap ===========================
+          dart:async       new Future.microtask
+          test.dart 11:18  main.<fn>
+
+          third error
+          test.dart 12:34  main.<fn>.<fn>
+          ===== asynchronous gap ===========================
+          dart:async       new Future.microtask
+          test.dart 12:18  main.<fn>
+
+
+        +0 -1: wait
+        +1 -1: wait
+        +1 -1: Some tests failed.''');
+  });
+
+  test('prints the full test name before an error', () {
+    return _expectReport('''
+        test(
+           'really gosh dang long test name. Even longer than that. No, yet '
+               'longer. Even more. We have to get to at least 200 characters. '
+               'I know that seems like a lot, but I believe in you. A little '
+               'more... okay, that should do it.',
+           () => throw TestFailure('oh no'));''', '''
+        +0: loading test.dart
+        +0: really ... than that. No, yet longer. Even more. We have to get to at least 200 characters. I know that seems like a lot, but I believe in you. A little more... okay, that should do it.
+        +0 -1: really gosh dang long test name. Even longer than that. No, yet longer. Even more. We have to get to at least 200 characters. I know that seems like a lot, but I believe in you. A little more... okay, that should do it. [E]
+          oh no
+          test.dart 11:18  main.<fn>
+
+
+        +0 -1: Some tests failed.''');
+  });
+
+  group('print:', () {
+    test('handles multiple prints', () {
+      return _expectReport('''
+        test('test', () {
+          print("one");
+          print("two");
+          print("three");
+          print("four");
+        });''', '''
+        +0: loading test.dart
+        +0: test
+        one
+        two
+        three
+        four
+
+        +1: test
+        +1: All tests passed!''');
+    });
+
+    test('handles a print after the test completes', () {
+      return _expectReport('''
+        // This completer ensures that the test isolate isn't killed until all
+        // prints have happened.
+        var testDone = Completer();
+        var waitStarted = Completer();
+        test('test', () {
+          waitStarted.future.then((_) {
+            Future(() => print("one"));
+            Future(() => print("two"));
+            Future(() => print("three"));
+            Future(() => print("four"));
+            Future(testDone.complete);
+          });
+        });
+
+        test('wait', () {
+          waitStarted.complete();
+          return testDone.future;
+        });''', '''
+        +0: loading test.dart
+        +0: test
+        +1: test
+        +1: wait
+        +1: test
+        one
+        two
+        three
+        four
+
+        +2: wait
+        +2: All tests passed!''');
+    });
+
+    test('interleaves prints and errors', () {
+      return _expectReport('''
+        // This completer ensures that the test isolate isn't killed until all
+        // prints have happened.
+        var completer = Completer();
+        test('test', () {
+          scheduleMicrotask(() {
+            print("three");
+            print("four");
+            throw "second error";
+          });
+
+          scheduleMicrotask(() {
+            print("five");
+            print("six");
+            completer.complete();
+          });
+
+          print("one");
+          print("two");
+          throw "first error";
+        });
+
+        test('wait', () => completer.future);''', '''
+        +0: loading test.dart
+        +0: test
+        one
+        two
+
+        +0 -1: test [E]
+          first error
+          test.dart 24:11  main.<fn>
+
+        three
+        four
+          second error
+          test.dart 13:13  main.<fn>.<fn>
+          ===== asynchronous gap ===========================
+          dart:async       scheduleMicrotask
+          test.dart 10:11  main.<fn>
+
+        five
+        six
+
+        +0 -1: wait
+        +1 -1: wait
+        +1 -1: Some tests failed.''');
+    });
+
+    test('prints the full test name before a print', () {
+      return _expectReport('''
+          test(
+             'really gosh dang long test name. Even longer than that. No, yet '
+                 'longer. Even more. We have to get to at least 200 '
+                 'characters. I know that seems like a lot, but I believe in '
+                 'you. A little more... okay, that should do it.',
+             () => print('hello'));''', '''
+          +0: loading test.dart
+          +0: really ... than that. No, yet longer. Even more. We have to get to at least 200 characters. I know that seems like a lot, but I believe in you. A little more... okay, that should do it.
+          +0: really gosh dang long test name. Even longer than that. No, yet longer. Even more. We have to get to at least 200 characters. I know that seems like a lot, but I believe in you. A little more... okay, that should do it.
+          hello
+
+          +1: really ... than that. No, yet longer. Even more. We have to get to at least 200 characters. I know that seems like a lot, but I believe in you. A little more... okay, that should do it.
+          +1: All tests passed!''');
+    });
+
+    test("doesn't print a clock update between two prints", () {
+      return _expectReport('''
+          test('slow', () async {
+            print('hello');
+            await Future.delayed(Duration(seconds: 3));
+            print('goodbye');
+          });''', '''
+          +0: loading test.dart
+          +0: slow
+          hello
+          goodbye
+
+          +1: slow
+          +1: All tests passed!''');
+    });
+  });
+
+  group('skip:', () {
+    test('displays skipped tests separately', () {
+      return _expectReport('''
+          test('skip 1', () {}, skip: true);
+          test('skip 2', () {}, skip: true);
+          test('skip 3', () {}, skip: true);''', '''
+          +0: loading test.dart
+          +0: skip 1
+          +0 ~1: skip 1
+          +0 ~1: skip 2
+          +0 ~2: skip 2
+          +0 ~2: skip 3
+          +0 ~3: skip 3
+          +0 ~3: All tests skipped.''');
+    });
+
+    test('displays a skipped group', () {
+      return _expectReport('''
+          group('skip', () {
+            test('test 1', () {});
+            test('test 2', () {});
+            test('test 3', () {});
+          }, skip: true);''', '''
+          +0: loading test.dart
+          +0: skip test 1
+          +0 ~1: skip test 1
+          +0 ~1: skip test 2
+          +0 ~2: skip test 2
+          +0 ~2: skip test 3
+          +0 ~3: skip test 3
+          +0 ~3: All tests skipped.''');
+    });
+
+    test('runs skipped tests along with successful tests', () {
+      return _expectReport('''
+          test('skip 1', () {}, skip: true);
+          test('success 1', () {});
+          test('skip 2', () {}, skip: true);
+          test('success 2', () {});''', '''
+          +0: loading test.dart
+          +0: skip 1
+          +0 ~1: skip 1
+          +0 ~1: success 1
+          +1 ~1: success 1
+          +1 ~1: skip 2
+          +1 ~2: skip 2
+          +1 ~2: success 2
+          +2 ~2: success 2
+          +2 ~2: All tests passed!''');
+    });
+
+    test('runs skipped tests along with successful and failing tests', () {
+      return _expectReport('''
+          test('failure 1', () => throw TestFailure('oh no'));
+          test('skip 1', () {}, skip: true);
+          test('success 1', () {});
+          test('failure 2', () => throw TestFailure('oh no'));
+          test('skip 2', () {}, skip: true);
+          test('success 2', () {});''', '''
+          +0: loading test.dart
+          +0: failure 1
+          +0 -1: failure 1 [E]
+            oh no
+            test.dart 6:35  main.<fn>
+
+
+          +0 -1: skip 1
+          +0 ~1 -1: skip 1
+          +0 ~1 -1: success 1
+          +1 ~1 -1: success 1
+          +1 ~1 -1: failure 2
+          +1 ~1 -2: failure 2 [E]
+            oh no
+            test.dart 9:35  main.<fn>
+
+
+          +1 ~1 -2: skip 2
+          +1 ~2 -2: skip 2
+          +1 ~2 -2: success 2
+          +2 ~2 -2: success 2
+          +2 ~2 -2: Some tests failed.''');
+    });
+
+    test('displays the skip reason if available', () {
+      return _expectReport('''
+          test('skip 1', () {}, skip: 'some reason');
+          test('skip 2', () {}, skip: 'or another');''', '''
+          +0: loading test.dart
+          +0: skip 1
+            Skip: some reason
+
+          +0 ~1: skip 1
+          +0 ~1: skip 2
+            Skip: or another
+
+          +0 ~2: skip 2
+          +0 ~2: All tests skipped.''');
+    });
+
+    test('runs skipped tests with --run-skipped', () {
+      return _expectReport('''
+          test('skip 1', () {}, skip: 'some reason');
+          test('skip 2', () {}, skip: 'or another');''', '''
+          +0: loading test.dart
+          +0: skip 1
+          +1: skip 1
+          +1: skip 2
+          +2: skip 2
+          +2: All tests passed!''', args: ['--run-skipped']);
+    });
+  });
+
+  test('Directs users to enable stack trace chaining if disabled', () async {
+    await _expectReport(
+        '''test('failure 1', () => throw TestFailure('oh no'));''', '''
+        +0: loading test.dart
+        +0: failure 1
+        +0 -1: failure 1 [E]
+          oh no
+          test.dart 6:25  main.<fn>
+        +0 -1: Some tests failed.
+        Consider enabling the flag chain-stack-traces to receive more detailed exceptions.
+        For example, 'dart test --chain-stack-traces'.''',
+        chainStackTraces: false);
+  });
+
+  group('gives non-windows users a way to re-run failed tests', () {
+    final executablePath = p.absolute(Platform.resolvedExecutable);
+
+    test('with simple names', () {
+      return _expectReport('''
+        test('failure', () {
+          expect(1, equals(2));
+        });''', '''
+        +0: loading test.dart
+        +0: failure
+        +0 -1: failure [E]
+          Expected: <2>
+            Actual: <1>
+
+        To run this test again: $executablePath test test.dart -p vm --plain-name 'failure'
+
+        +0 -1: Some tests failed.''');
+    });
+
+    test('escapes names containing single quotes', () {
+      return _expectReport('''
+        test("failure with a ' in the name", () {
+          expect(1, equals(2));
+        });''', '''
+        +0: loading test.dart
+        +0: failure with a ' in the name
+        +0 -1: failure with a ' in the name [E]
+          Expected: <2>
+            Actual: <1>
+
+        To run this test again: $executablePath test test.dart -p vm --plain-name 'failure with a '\\'' in the name'
+
+        +0 -1: Some tests failed.''');
+    });
+  }, testOn: '!windows');
+
+  group('gives windows users a way to re-run failed tests', () {
+    final executablePath = p.absolute(Platform.resolvedExecutable);
+
+    test('with simple names', () {
+      return _expectReport('''
+        test('failure', () {
+          expect(1, equals(2));
+        });''', '''
+        +0: loading test.dart
+        +0: failure
+        +0 -1: failure [E]
+          Expected: <2>
+            Actual: <1>
+
+        To run this test again: $executablePath test test.dart -p vm --plain-name "failure"
+
+        +0 -1: Some tests failed.''');
+    });
+
+    test('escapes names containing double quotes', () {
+      return _expectReport('''
+        test('failure with a " in the name', () {
+          expect(1, equals(2));
+        });''', '''
+        +0: loading test.dart
+        +0: failure with a " in the name
+        +0 -1: failure with a " in the name [E]
+          Expected: <2>
+            Actual: <1>
+
+        To run this test again: $executablePath test test.dart -p vm --plain-name "failure with a """ in the name"
+
+        +0 -1: Some tests failed.''');
+    });
+  }, testOn: 'windows');
+}
+
+Future<void> _expectReport(String tests, String expected,
+    {List<String> args = const [], bool chainStackTraces = true}) async {
+  await d.file('test.dart', '''
+    import 'dart:async';
+
+    import 'package:test/test.dart';
+
+    void main() {
+$tests
+    }
+  ''').create();
+
+  var test = await runTest([
+    'test.dart',
+    if (chainStackTraces) '--chain-stack-traces',
+    ...args,
+  ], reporter: 'compact');
+  await test.shouldExit();
+
+  var stdoutLines = await test.stdout.rest.toList();
+
+  // Skip the first CR, remove excess trailing whitespace, and trim off
+  // timestamps.
+  String? lastLine;
+  var actual = stdoutLines.skip(1).map((line) {
+    if (line.startsWith('  ') || line.isEmpty) return line.trimRight();
+
+    var trimmed = line.trim().replaceFirst(RegExp('^[0-9]{2}:[0-9]{2} '), '');
+
+    // Trim identical lines so the test isn't dependent on how fast each test
+    // runs.
+    if (trimmed == lastLine) return null;
+    lastLine = trimmed;
+    return trimmed;
+  }).where((line) => line != null);
+
+  // Un-indent the expected string.
+  var indentation = expected.indexOf(RegExp('[^ ]'));
+  var expectedLines = expected.split('\n').map((line) {
+    if (line.isEmpty) return line;
+    return line.substring(indentation);
+  });
+
+  expect(actual, containsAllInOrder(expectedLines));
+}
diff --git a/pkgs/test/test/runner/compiler_runtime_matrix_test.dart b/pkgs/test/test/runner/compiler_runtime_matrix_test.dart
new file mode 100644
index 0000000..74d8802
--- /dev/null
+++ b/pkgs/test/test/runner/compiler_runtime_matrix_test.dart
@@ -0,0 +1,174 @@
+// Copyright (c) 2023, 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.
+
+@TestOn('vm')
+@Timeout.factor(2)
+library;
+
+import 'dart:io';
+
+import 'package:test/test.dart';
+import 'package:test_api/backend.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../io.dart';
+
+void main() {
+  setUpAll(() async {
+    await precompileTestExecutable();
+  });
+
+  for (var runtime in Runtime.builtIn) {
+    for (var compiler in runtime.supportedCompilers) {
+      // Ignore the platforms we can't run on this OS.
+      if ((runtime == Runtime.edge && !Platform.isWindows) ||
+          (runtime == Runtime.safari && !Platform.isMacOS)) {
+        continue;
+      }
+      String? skipReason;
+      if (runtime == Runtime.safari) {
+        skipReason = 'https://github.com/dart-lang/test/issues/1253';
+      } else if (compiler == Compiler.dart2wasm) {
+        skipReason = 'Wasm tests are experimental and require special setup';
+      } else if ([Runtime.firefox, Runtime.nodeJS].contains(runtime) &&
+          Platform.isWindows) {
+        skipReason = 'https://github.com/dart-lang/test/issues/1942';
+      } else if (runtime == Runtime.firefox && Platform.isMacOS) {
+        skipReason = 'https://github.com/dart-lang/test/pull/2276';
+      }
+      group('--runtime ${runtime.identifier} --compiler ${compiler.identifier}',
+          skip: skipReason, () {
+        final testArgs = [
+          'test.dart',
+          '-p',
+          runtime.identifier,
+          '-c',
+          compiler.identifier
+        ];
+
+        test('can run passing tests', () async {
+          await d.file('test.dart', _goodTest).create();
+          var test = await runTest(testArgs);
+
+          expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+          await test.shouldExit(0);
+        });
+
+        test('fails gracefully for invalid code', () async {
+          await d.file('test.dart', _compileErrorTest).create();
+          var test = await runTest(testArgs);
+
+          expect(
+              test.stdout,
+              containsInOrder([
+                "Error: A value of type 'String' can't be assigned to a variable of type 'int'.",
+                "int x = 'hello';",
+              ]));
+
+          await test.shouldExit(1);
+        });
+
+        test('fails gracefully for test failures', () async {
+          await d.file('test.dart', _failingTest).create();
+          var test = await runTest(testArgs);
+
+          expect(
+              test.stdout,
+              containsInOrder([
+                'Expected: <2>',
+                'Actual: <1>',
+                'test.dart 5',
+                '+0 -1: Some tests failed.',
+              ]));
+
+          await test.shouldExit(1);
+        });
+
+        test('fails gracefully if a test file throws in main', () async {
+          await d.file('test.dart', _throwingTest).create();
+          var test = await runTest(testArgs);
+          expect(
+              test.stdout,
+              containsInOrder([
+                '-1: [${runtime.name}, ${compiler.name}] loading test.dart [E]',
+                'Failed to load "test.dart": oh no'
+              ]));
+          await test.shouldExit(1);
+        });
+
+        test('captures prints', () async {
+          await d.file('test.dart', _testWithPrints).create();
+          var test = await runTest([...testArgs, '-r', 'json']);
+
+          expect(
+              test.stdout,
+              containsInOrder([
+                '"messageType":"print","message":"hello","type":"print"',
+              ]));
+
+          await test.shouldExit(0);
+        });
+
+        if (runtime.isDartVM) {
+          test('forwards stdout/stderr', () async {
+            await d.file('test.dart', _testWithStdOutAndErr).create();
+            var test = await runTest(testArgs, reporter: 'silent');
+
+            expect(test.stdout, emitsThrough('hello'));
+            expect(test.stderr, emits('world'));
+            await test.shouldExit(0);
+          },
+              skip: Platform.isWindows && compiler == Compiler.exe
+                  ? 'https://github.com/dart-lang/test/issues/2150'
+                  : null);
+        }
+      });
+    }
+  }
+}
+
+final _goodTest = '''
+  import 'package:test/test.dart';
+
+  void main() {
+    test("success", () {});
+  }
+''';
+
+final _failingTest = '''
+  import 'package:test/test.dart';
+
+  void main() {
+    test("failure", () {
+      expect(1, 2);
+    });
+  }
+''';
+
+final _compileErrorTest = '''
+int x = 'hello';
+
+void main() {}
+''';
+
+final _throwingTest = "void main() => throw 'oh no';";
+
+final _testWithPrints = '''
+import 'package:test/test.dart';
+
+void main() {
+  print('hello');
+  test('success', () {});
+}''';
+
+final _testWithStdOutAndErr = '''
+import 'dart:io';
+import 'package:test/test.dart';
+
+void main() async {
+  stdout.writeln('hello');
+  await stdout.flush();
+  stderr.writeln('world');
+  test('success', () {});
+}''';
diff --git a/pkgs/test/test/runner/compiler_test.dart b/pkgs/test/test/runner/compiler_test.dart
new file mode 100644
index 0000000..5cf7a47
--- /dev/null
+++ b/pkgs/test/test/runner/compiler_test.dart
@@ -0,0 +1,77 @@
+// Copyright (c) 2023, 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.
+
+@TestOn('vm')
+library;
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../io.dart';
+
+final _test = '''
+  import 'package:test/test.dart';
+
+  void main() {
+    test("success", () {});
+  }
+''';
+
+void main() {
+  setUpAll(() async {
+    await precompileTestExecutable();
+    await d.file('test.dart', _test).create();
+  });
+
+  group('--compiler', () {
+    test(
+        'uses the default compiler if none other is specified for the platform',
+        () async {
+      var test =
+          await runTest(['test.dart', '-p', 'chrome,vm', '-c', 'dart2js']);
+
+      expect(test.stdout, emitsThrough(contains('[Chrome, Dart2Js]')));
+      expect(test.stdout, emitsThrough(contains('[VM, Kernel]')));
+      expect(test.stdout, emitsThrough(contains('+2: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('runs all supported compiler and platform combinations', () async {
+      var test = await runTest(
+          ['test.dart', '-p', 'chrome,vm', '-c', 'dart2js,kernel,source']);
+
+      expect(test.stdout, emitsThrough(contains('[Chrome, Dart2Js]')));
+      expect(test.stdout, emitsThrough(contains('[VM, Kernel]')));
+      expect(test.stdout, emitsThrough(contains('[VM, Source]')));
+      expect(test.stdout, emitsThrough(contains('+3: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('supports platform selectors', () async {
+      var test = await runTest(
+          ['test.dart', '-p', 'vm', '-c', 'vm:source,browser:kernel']);
+
+      expect(test.stdout, emitsThrough(contains('[VM, Source]')));
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test(
+        'will only run a given test once for each compiler, even if there are '
+        'multiple matches', () async {
+      var test =
+          await runTest(['test.dart', '-p', 'vm', '-c', 'vm:source,source']);
+
+      expect(test.stdout, emitsThrough(contains('[VM, Source]')));
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('fails on unknown compilers', () async {
+      var test = await runTest(['test.dart', '-c', 'fake']);
+      expect(test.stderr, emitsThrough(contains('Invalid compiler `fake`')));
+      await test.shouldExit(64);
+    });
+  });
+}
diff --git a/pkgs/test/test/runner/configuration/compiler_test.dart b/pkgs/test/test/runner/configuration/compiler_test.dart
new file mode 100644
index 0000000..d8f8d33
--- /dev/null
+++ b/pkgs/test/test/runner/configuration/compiler_test.dart
@@ -0,0 +1,75 @@
+// Copyright (c) 2023, 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:convert';
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  group('compilers', () {
+    test('uses specified compilers for supporting platforms', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'compilers': ['source']
+              }))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['-p', 'chrome,vm', 'test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '+0: [Chrome, Dart2Js]',
+            '+1: [VM, Source]',
+            '+2: All tests passed!',
+          ]));
+      await test.shouldExit(0);
+    });
+
+    test('supports platform selectors with compilers', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'compilers': ['vm:source', 'browser:kernel']
+              }))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['-p', 'chrome,vm', 'test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '+0: [Chrome, Dart2Js]',
+            '+1: [VM, Source]',
+            '+2: All tests passed!',
+          ]));
+      await test.shouldExit(0);
+    });
+  });
+}
diff --git a/pkgs/test/test/runner/configuration/configuration_test.dart b/pkgs/test/test/runner/configuration/configuration_test.dart
new file mode 100644
index 0000000..03cc728
--- /dev/null
+++ b/pkgs/test/test/runner/configuration/configuration_test.dart
@@ -0,0 +1,326 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'package:boolean_selector/boolean_selector.dart';
+import 'package:test/test.dart';
+import 'package:test_core/src/runner/configuration/reporters.dart';
+import 'package:test_core/src/runner/suite.dart';
+import 'package:test_core/src/util/io.dart';
+
+import '../../utils.dart';
+
+void main() {
+  group('merge', () {
+    group('for most fields', () {
+      test('if neither is defined, preserves the default', () {
+        var merged = configuration().merge(configuration());
+        expect(merged.help, isFalse);
+        expect(merged.version, isFalse);
+        expect(merged.pauseAfterLoad, isFalse);
+        expect(merged.debug, isFalse);
+        expect(merged.color, equals(canUseSpecialChars));
+        expect(merged.configurationPath, equals('dart_test.yaml'));
+        expect(merged.reporter, equals(defaultReporter));
+        expect(merged.fileReporters, isEmpty);
+        expect(merged.shardIndex, isNull);
+        expect(merged.totalShards, isNull);
+        expect(merged.testRandomizeOrderingSeed, isNull);
+        expect(merged.testSelections.keys.single, 'test');
+      });
+
+      test("if only the old configuration's is defined, uses it", () {
+        var merged = configuration(
+            help: true,
+            version: true,
+            pauseAfterLoad: true,
+            debug: true,
+            color: true,
+            configurationPath: 'special_test.yaml',
+            reporter: 'json',
+            fileReporters: {'json': 'out.json'},
+            shardIndex: 3,
+            totalShards: 10,
+            testRandomizeOrderingSeed: 123,
+            testSelections: const {
+              'bar': {TestSelection()}
+            }).merge(configuration());
+
+        expect(merged.help, isTrue);
+        expect(merged.version, isTrue);
+        expect(merged.pauseAfterLoad, isTrue);
+        expect(merged.debug, isTrue);
+        expect(merged.color, isTrue);
+        expect(merged.configurationPath, equals('special_test.yaml'));
+        expect(merged.reporter, equals('json'));
+        expect(merged.fileReporters, equals({'json': 'out.json'}));
+        expect(merged.shardIndex, equals(3));
+        expect(merged.totalShards, equals(10));
+        expect(merged.testRandomizeOrderingSeed, 123);
+        expect(merged.testSelections.keys.single, 'bar');
+      });
+
+      test("if only the new configuration's is defined, uses it", () {
+        var merged = configuration().merge(configuration(
+            help: true,
+            version: true,
+            pauseAfterLoad: true,
+            debug: true,
+            color: true,
+            configurationPath: 'special_test.yaml',
+            reporter: 'json',
+            fileReporters: {'json': 'out.json'},
+            shardIndex: 3,
+            totalShards: 10,
+            testRandomizeOrderingSeed: 123,
+            testSelections: const {
+              'bar': {TestSelection()}
+            }));
+
+        expect(merged.help, isTrue);
+        expect(merged.version, isTrue);
+        expect(merged.pauseAfterLoad, isTrue);
+        expect(merged.debug, isTrue);
+        expect(merged.color, isTrue);
+        expect(merged.configurationPath, equals('special_test.yaml'));
+        expect(merged.reporter, equals('json'));
+        expect(merged.fileReporters, equals({'json': 'out.json'}));
+        expect(merged.shardIndex, equals(3));
+        expect(merged.totalShards, equals(10));
+        expect(merged.testRandomizeOrderingSeed, 123);
+        expect(merged.testSelections.keys.single, 'bar');
+      });
+
+      test(
+          "if the two configurations conflict, uses the new configuration's "
+          'values', () {
+        var older = configuration(
+            help: true,
+            version: false,
+            pauseAfterLoad: true,
+            debug: true,
+            color: false,
+            configurationPath: 'special_test.yaml',
+            reporter: 'json',
+            fileReporters: {'json': 'old.json'},
+            shardIndex: 2,
+            totalShards: 4,
+            testRandomizeOrderingSeed: 0,
+            testSelections: const {
+              'bar': {TestSelection()}
+            });
+        var newer = configuration(
+            help: false,
+            version: true,
+            pauseAfterLoad: false,
+            debug: false,
+            color: true,
+            configurationPath: 'test_special.yaml',
+            reporter: 'compact',
+            fileReporters: {'json': 'new.json'},
+            shardIndex: 3,
+            totalShards: 10,
+            testRandomizeOrderingSeed: 123,
+            testSelections: const {
+              'blech': {TestSelection()}
+            });
+        var merged = older.merge(newer);
+
+        expect(merged.help, isFalse);
+        expect(merged.version, isTrue);
+        expect(merged.pauseAfterLoad, isFalse);
+        expect(merged.debug, isFalse);
+        expect(merged.color, isTrue);
+        expect(merged.configurationPath, equals('test_special.yaml'));
+        expect(merged.reporter, equals('compact'));
+        expect(merged.fileReporters, equals({'json': 'new.json'}));
+        expect(merged.shardIndex, equals(3));
+        expect(merged.totalShards, equals(10));
+        expect(merged.testRandomizeOrderingSeed, 123);
+        expect(merged.testSelections.keys.single, 'blech');
+      });
+    });
+
+    group('for chosenPresets', () {
+      test('if neither is defined, preserves the default', () {
+        var merged = configuration().merge(configuration());
+        expect(merged.chosenPresets, isEmpty);
+      });
+
+      test("if only the old configuration's is defined, uses it", () {
+        var merged = configuration(chosenPresets: ['baz', 'bang'])
+            .merge(configuration());
+        expect(merged.chosenPresets, equals(['baz', 'bang']));
+      });
+
+      test("if only the new configuration's is defined, uses it", () {
+        var merged = configuration()
+            .merge(configuration(chosenPresets: ['baz', 'bang']));
+        expect(merged.chosenPresets, equals(['baz', 'bang']));
+      });
+
+      test('if both are defined, unions them', () {
+        var merged = configuration(chosenPresets: ['baz', 'bang'])
+            .merge(configuration(chosenPresets: ['qux']));
+        expect(merged.chosenPresets, equals(['baz', 'bang', 'qux']));
+      });
+    });
+
+    group('for presets', () {
+      test('merges each nested configuration', () {
+        var merged = configuration(presets: {
+          'bang': configuration(pauseAfterLoad: true),
+          'qux': configuration(color: true)
+        }).merge(configuration(presets: {
+          'qux': configuration(color: false),
+          'zap': configuration(help: true)
+        }));
+
+        expect(merged.presets['bang']!.pauseAfterLoad, isTrue);
+        expect(merged.presets['qux']!.color, isFalse);
+        expect(merged.presets['zap']!.help, isTrue);
+      });
+
+      test('automatically resolves a matching chosen preset', () {
+        var config = configuration(
+            presets: {'foo': configuration(color: true)},
+            chosenPresets: ['foo']);
+        expect(config.presets, isEmpty);
+        expect(config.chosenPresets, equals(['foo']));
+        expect(config.knownPresets, equals(['foo']));
+        expect(config.color, isTrue);
+      });
+
+      test('resolves a chosen presets in order', () {
+        var config = configuration(presets: {
+          'foo': configuration(color: true),
+          'bar': configuration(color: false)
+        }, chosenPresets: [
+          'foo',
+          'bar'
+        ]);
+        expect(config.presets, isEmpty);
+        expect(config.chosenPresets, equals(['foo', 'bar']));
+        expect(config.knownPresets, unorderedEquals(['foo', 'bar']));
+        expect(config.color, isFalse);
+
+        config = configuration(presets: {
+          'foo': configuration(color: true),
+          'bar': configuration(color: false)
+        }, chosenPresets: [
+          'bar',
+          'foo'
+        ]);
+        expect(config.presets, isEmpty);
+        expect(config.chosenPresets, equals(['bar', 'foo']));
+        expect(config.knownPresets, unorderedEquals(['foo', 'bar']));
+        expect(config.color, isTrue);
+      });
+
+      test('ignores inapplicable chosen presets', () {
+        var config = configuration(presets: {}, chosenPresets: ['baz']);
+        expect(config.presets, isEmpty);
+        expect(config.chosenPresets, equals(['baz']));
+        expect(config.knownPresets, equals(isEmpty));
+      });
+
+      test('resolves presets through merging', () {
+        var config = configuration(presets: {'foo': configuration(color: true)})
+            .merge(configuration(chosenPresets: ['foo']));
+
+        expect(config.presets, isEmpty);
+        expect(config.chosenPresets, equals(['foo']));
+        expect(config.knownPresets, equals(['foo']));
+        expect(config.color, isTrue);
+      });
+
+      test('preserves known presets through merging', () {
+        var config = configuration(
+            presets: {'foo': configuration(color: true)},
+            chosenPresets: ['foo']).merge(configuration());
+
+        expect(config.presets, isEmpty);
+        expect(config.chosenPresets, equals(['foo']));
+        expect(config.knownPresets, equals(['foo']));
+        expect(config.color, isTrue);
+      });
+    });
+
+    group('for include and excludeTags', () {
+      test('if neither is defined, preserves the default', () {
+        var merged = configuration().merge(configuration());
+        expect(merged.includeTags, equals(BooleanSelector.all));
+        expect(merged.excludeTags, equals(BooleanSelector.none));
+      });
+
+      test("if only the old configuration's is defined, uses it", () {
+        var merged = configuration(
+                includeTags: BooleanSelector.parse('foo || bar'),
+                excludeTags: BooleanSelector.parse('baz || bang'))
+            .merge(configuration());
+
+        expect(merged.includeTags, equals(BooleanSelector.parse('foo || bar')));
+        expect(
+            merged.excludeTags, equals(BooleanSelector.parse('baz || bang')));
+      });
+
+      test("if only the configuration's is defined, uses it", () {
+        var merged = configuration().merge(configuration(
+            includeTags: BooleanSelector.parse('foo || bar'),
+            excludeTags: BooleanSelector.parse('baz || bang')));
+
+        expect(merged.includeTags, equals(BooleanSelector.parse('foo || bar')));
+        expect(
+            merged.excludeTags, equals(BooleanSelector.parse('baz || bang')));
+      });
+
+      test('if both are defined, unions or intersects them', () {
+        var older = configuration(
+            includeTags: BooleanSelector.parse('foo || bar'),
+            excludeTags: BooleanSelector.parse('baz || bang'));
+        var newer = configuration(
+            includeTags: BooleanSelector.parse('blip'),
+            excludeTags: BooleanSelector.parse('qux'));
+        var merged = older.merge(newer);
+
+        expect(merged.includeTags,
+            equals(BooleanSelector.parse('(foo || bar) && blip')));
+        expect(merged.excludeTags,
+            equals(BooleanSelector.parse('(baz || bang) || qux')));
+      });
+    });
+
+    group('for globalPatterns', () {
+      test('if neither is defined, preserves the default', () {
+        var merged = configuration().merge(configuration());
+        expect(merged.globalPatterns, isEmpty);
+      });
+
+      test("if only the old configuration's is defined, uses it", () {
+        var merged = configuration(globalPatterns: ['beep', 'boop'])
+            .merge(configuration());
+
+        expect(merged.globalPatterns, equals(['beep', 'boop']));
+      });
+
+      test("if only the new configuration's is defined, uses it", () {
+        var merged = configuration()
+            .merge(configuration(globalPatterns: ['beep', 'boop']));
+
+        expect(merged.globalPatterns, equals(['beep', 'boop']));
+      });
+
+      test('if both are defined, unions them', () {
+        var older = configuration(globalPatterns: ['beep', 'boop']);
+        var newer = configuration(globalPatterns: ['bonk']);
+        var merged = older.merge(newer);
+
+        expect(
+            merged.globalPatterns, unorderedEquals(['beep', 'boop', 'bonk']));
+      });
+    });
+  });
+}
diff --git a/pkgs/test/test/runner/configuration/custom_platform_test.dart b/pkgs/test/test/runner/configuration/custom_platform_test.dart
new file mode 100644
index 0000000..ca9ec05
--- /dev/null
+++ b/pkgs/test/test/runner/configuration/custom_platform_test.dart
@@ -0,0 +1,930 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:io';
+
+import 'package:test/src/runner/browser/default_settings.dart';
+import 'package:test/test.dart';
+import 'package:test_api/src/backend/runtime.dart';
+import 'package:test_core/src/util/exit_codes.dart' as exit_codes;
+import 'package:test_descriptor/test_descriptor.dart' as d;
+import 'package:test_process/test_process.dart';
+
+import '../../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  setUp(() async {
+    await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("success", () {});
+        }
+      ''').create();
+  });
+
+  group('override_platforms', () {
+    group('can override a browser', () {
+      test('without any changes', () async {
+        await d.file('dart_test.yaml', '''
+          override_platforms:
+            chrome:
+              settings: {}
+        ''').create();
+
+        var test = await runTest(['-p', 'chrome', 'test.dart']);
+        expect(test.stdout, emitsThrough(contains('All tests passed!')));
+        await test.shouldExit(0);
+      }, tags: 'chrome');
+
+      test("that's user-defined", () async {
+        await d.file('dart_test.yaml', '''
+          define_platforms:
+            chromium:
+              name: Chromium
+              extends: chrome
+              settings: {}
+
+          override_platforms:
+            chromium:
+              settings: {}
+        ''').create();
+
+        var test = await runTest(['-p', 'chromium', 'test.dart']);
+        expect(test.stdout, emitsThrough(contains('All tests passed!')));
+        await test.shouldExit(0);
+      }, tags: 'chrome');
+
+      test('with a basename-only executable', () async {
+        await d.file('dart_test.yaml', '''
+          override_platforms:
+            firefox:
+              settings:
+                executable:
+                  linux: firefox
+                  mac_os: firefox
+                  windows: firefox.exe
+        ''').create();
+
+        var test = await runTest(['-p', 'firefox', 'test.dart']);
+        expect(test.stdout, emitsThrough(contains('All tests passed!')));
+        await test.shouldExit(0);
+      }, tags: 'firefox');
+
+      test('with an absolute-path executable', () async {
+        String path;
+        if (Platform.isLinux) {
+          var process = await TestProcess.start('which', ['google-chrome']);
+          path = await process.stdout.next;
+          await process.shouldExit(0);
+        } else {
+          path = defaultSettings[Runtime.chrome]!.executable;
+        }
+
+        await d.file('dart_test.yaml', '''
+          override_platforms:
+            chrome:
+              settings:
+                executable: $path
+        ''').create();
+
+        var test = await runTest(['-p', 'chrome', 'test.dart']);
+        expect(test.stdout, emitsThrough(contains('All tests passed!')));
+        await test.shouldExit(0);
+      }, tags: 'chrome');
+
+      test('with non-headless mode', () async {
+        await d.file('dart_test.yaml', '''
+          override_platforms:
+            chrome:
+              settings:
+                headless: false
+        ''').create();
+
+        await d.file('test.dart', '''
+          import 'package:test/src/runner/browser/dom.dart' as dom;
+          import 'package:test/test.dart';
+
+          void main() {
+            test("is not headless", () {
+              expect(dom.window.navigator.userAgent,
+                  isNot(contains('HeadlessChrome')));
+            });
+          }
+        ''').create();
+
+        var test = await runTest(['-p', 'chrome', 'test.dart']);
+        expect(test.stdout, emitsThrough(contains('All tests passed!')));
+        await test.shouldExit(0);
+      }, tags: 'chrome');
+    });
+
+    test('can override Node.js without any changes', () async {
+      await d.file('dart_test.yaml', '''
+        override_platforms:
+          node:
+            settings: {}
+      ''').create();
+
+      var test = await runTest(['-p', 'node', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('All tests passed!')));
+      await test.shouldExit(0);
+    }, tags: 'node');
+
+    group('errors', () {
+      test('rejects a non-map value', () async {
+        await d.file('dart_test.yaml', 'override_platforms: 12').create();
+
+        var test = await runTest([]);
+        expect(test.stderr,
+            containsInOrder(['override_platforms must be a map.', '^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects a non-string key', () async {
+        await d
+            .file('dart_test.yaml', 'override_platforms: {12: null}')
+            .create();
+
+        var test = await runTest([]);
+        expect(test.stderr,
+            containsInOrder(['Platform identifier must be a string.', '^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects a non-identifier-like key', () async {
+        await d
+            .file('dart_test.yaml', 'override_platforms: {foo bar: null}')
+            .create();
+
+        var test = await runTest([]);
+        expect(
+            test.stderr,
+            containsInOrder([
+              'Platform identifier must be an (optionally hyphenated) Dart '
+                  'identifier.',
+              '^^^^^^^'
+            ]));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects a non-map definition', () async {
+        await d.file('dart_test.yaml', '''
+          override_platforms:
+            chrome: 12
+        ''').create();
+
+        var test = await runTest([]);
+        expect(test.stderr,
+            containsInOrder(['Platform definition must be a map.', '^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('requires a settings key', () async {
+        await d.file('dart_test.yaml', '''
+          override_platforms:
+            chrome: {}
+        ''').create();
+
+        var test = await runTest([]);
+        expect(test.stderr,
+            containsInOrder(['Missing required field "settings".', '^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('settings must be a map', () async {
+        await d.file('dart_test.yaml', '''
+          override_platforms:
+            chrome:
+              settings: null
+        ''').create();
+
+        var test = await runTest([]);
+        expect(test.stderr, containsInOrder(['Must be a map.', '^^^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('the overridden platform must exist', () async {
+        await d.file('dart_test.yaml', '''
+          override_platforms:
+            chromium:
+              settings: {}
+        ''').create();
+
+        var test = await runTest(['test.dart']);
+        expect(test.stderr,
+            containsInOrder(['Unknown platform "chromium".', '^^^^^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test("uncustomizable platforms can't be overridden", () async {
+        await d.file('dart_test.yaml', '''
+          override_platforms:
+            vm:
+              settings: {}
+        ''').create();
+
+        var test = await runTest(['-p', 'vm', 'test.dart']);
+        expect(test.stdout,
+            containsInOrder(['The "vm" platform can\'t be customized.', '^^']));
+        await test.shouldExit(1);
+      });
+
+      group('when overriding browsers', () {
+        test('executable must be a string or map', () async {
+          await d.file('dart_test.yaml', '''
+            override_platforms:
+              chrome:
+                settings:
+                  executable: 12
+          ''').create();
+
+          var test = await runTest(['-p', 'chrome', 'test.dart']);
+          expect(test.stdout,
+              containsInOrder(['Must be a map or a string.', '^^']));
+          await test.shouldExit(1);
+        });
+
+        test('executable string may not be relative on POSIX', () async {
+          await d.file('dart_test.yaml', '''
+            override_platforms:
+              chrome:
+                settings:
+                  executable: foo/bar
+          ''').create();
+
+          var test = await runTest(['-p', 'chrome', 'test.dart']);
+          expect(
+              test.stdout,
+              containsInOrder([
+                'Linux and Mac OS executables may not be relative paths.',
+                '^^^^^^^'
+              ]));
+          await test.shouldExit(1);
+        },
+            // We allow relative executables for Windows so that Windows users
+            // can set a global executable without having to explicitly write
+            // `windows:`.
+            testOn: '!windows');
+
+        test('Linux executable must be a string', () async {
+          await d.file('dart_test.yaml', '''
+            override_platforms:
+              chrome:
+                settings:
+                  executable:
+                    linux: 12
+          ''').create();
+
+          var test = await runTest(['-p', 'chrome', 'test.dart']);
+          expect(test.stdout, containsInOrder(['Must be a string.', '^^']));
+          await test.shouldExit(1);
+        });
+
+        test('Linux executable may not be relative', () async {
+          await d.file('dart_test.yaml', '''
+            override_platforms:
+              chrome:
+                settings:
+                  executable:
+                    linux: foo/bar
+          ''').create();
+
+          var test = await runTest(['-p', 'chrome', 'test.dart']);
+          expect(
+              test.stdout,
+              containsInOrder([
+                'Linux and Mac OS executables may not be relative paths.',
+                '^^^^^^^'
+              ]));
+          await test.shouldExit(1);
+        });
+
+        test('Mac OS executable must be a string', () async {
+          await d.file('dart_test.yaml', '''
+            override_platforms:
+              chrome:
+                settings:
+                  executable:
+                    mac_os: 12
+          ''').create();
+
+          var test = await runTest(['-p', 'chrome', 'test.dart']);
+          expect(test.stdout, containsInOrder(['Must be a string.', '^^']));
+          await test.shouldExit(1);
+        });
+
+        test('Mac OS executable may not be relative', () async {
+          await d.file('dart_test.yaml', '''
+            override_platforms:
+              chrome:
+                settings:
+                  executable:
+                    mac_os: foo/bar
+          ''').create();
+
+          var test = await runTest(['-p', 'chrome', 'test.dart']);
+          expect(
+              test.stdout,
+              containsInOrder([
+                'Linux and Mac OS executables may not be relative paths.',
+                '^^^^^^^'
+              ]));
+          await test.shouldExit(1);
+        });
+
+        test('Windows executable must be a string', () async {
+          await d.file('dart_test.yaml', '''
+            override_platforms:
+              chrome:
+                settings:
+                  executable:
+                    windows: 12
+          ''').create();
+
+          var test = await runTest(['-p', 'chrome', 'test.dart']);
+          expect(test.stdout, containsInOrder(['Must be a string.', '^^']));
+          await test.shouldExit(1);
+        });
+
+        test('executable must exist', () async {
+          await d.file('dart_test.yaml', '''
+            override_platforms:
+              chrome:
+                settings:
+                  executable: _does_not_exist
+          ''').create();
+
+          var test = await runTest(['-p', 'chrome', 'test.dart']);
+          expect(
+              test.stdout,
+              emitsThrough(
+                  contains('Failed to run Chrome: $noSuchFileMessage')));
+          await test.shouldExit(1);
+        });
+
+        test('executable must exist for Node.js', () async {
+          await d.file('dart_test.yaml', '''
+            override_platforms:
+              node:
+                settings:
+                  executable: _does_not_exist
+          ''').create();
+
+          var test = await runTest(['-p', 'node', 'test.dart']);
+          expect(
+              test.stdout,
+              emitsThrough(
+                  contains('Failed to run Node.js: $noSuchFileMessage')));
+          await test.shouldExit(1);
+        }, tags: 'node');
+
+        test('headless must be a boolean', () async {
+          await d.file('dart_test.yaml', '''
+            override_platforms:
+              chrome:
+                settings:
+                  headless: definitely
+          ''').create();
+
+          var test = await runTest(['-p', 'chrome', 'test.dart']);
+          expect(
+              test.stdout,
+              emitsThrough(
+                  containsInOrder(['Must be a boolean.', '^^^^^^^^^^'])));
+          await test.shouldExit(1);
+        });
+      });
+    });
+  });
+
+  group('define_platforms', () {
+    group('can define a new browser', () {
+      group('without any changes', () {
+        setUp(() async {
+          await d.file('dart_test.yaml', '''
+            define_platforms:
+              chromium:
+                name: Chromium
+                extends: chrome
+                settings: {}
+          ''').create();
+        });
+
+        test('that can be used to run tests', () async {
+          var test = await runTest(['-p', 'chromium', 'test.dart']);
+          expect(test.stdout, emitsThrough(contains('All tests passed!')));
+          await test.shouldExit(0);
+        }, tags: 'chrome');
+
+        test('that can be used in platform selectors', () async {
+          await d.file('test.dart', '''
+            import 'package:test/test.dart';
+
+            void main() {
+              test("success", () {}, testOn: "chromium");
+            }
+          ''').create();
+
+          var test = await runTest(['-p', 'chromium', 'test.dart']);
+          expect(test.stdout, emitsThrough(contains('All tests passed!')));
+          await test.shouldExit(0);
+
+          test = await runTest(['-p', 'chrome', 'test.dart']);
+          expect(test.stdout, emitsThrough(contains('No tests ran.')));
+          await test.shouldExit(79);
+        }, tags: 'chrome');
+
+        test('that counts as its parent', () async {
+          await d.file('test.dart', '''
+            import 'package:test/test.dart';
+
+            void main() {
+              test("success", () {}, testOn: "chrome");
+            }
+          ''').create();
+
+          var test = await runTest(['-p', 'chromium', 'test.dart']);
+          expect(test.stdout, emitsThrough(contains('All tests passed!')));
+          await test.shouldExit(0);
+        }, tags: 'chrome');
+      });
+
+      test('with a basename-only executable', () async {
+        await d.file('dart_test.yaml', '''
+          define_platforms:
+            my-firefox:
+              name: My Firefox
+              extends: firefox
+              settings:
+                executable:
+                  linux: firefox
+                  mac_os: firefox
+                  windows: firefox.exe
+        ''').create();
+
+        var test = await runTest(['-p', 'my-firefox', 'test.dart']);
+        expect(test.stdout, emitsThrough(contains('All tests passed!')));
+        await test.shouldExit(0);
+      }, tags: 'firefox');
+
+      test('with an absolute-path executable', () async {
+        String path;
+        if (Platform.isLinux) {
+          var process = await TestProcess.start('which', ['google-chrome']);
+          path = await process.stdout.next;
+          await process.shouldExit(0);
+        } else {
+          path = defaultSettings[Runtime.chrome]!.executable;
+        }
+
+        await d.file('dart_test.yaml', '''
+          define_platforms:
+            chromium:
+              name: Chromium
+              extends: chrome
+              settings:
+                executable: $path
+        ''').create();
+
+        var test = await runTest(['-p', 'chromium', 'test.dart']);
+        expect(test.stdout, emitsThrough(contains('All tests passed!')));
+        await test.shouldExit(0);
+      }, tags: 'chrome');
+    });
+
+    group('errors', () {
+      test('rejects a non-map value', () async {
+        await d.file('dart_test.yaml', 'define_platforms: 12').create();
+
+        var test = await runTest([]);
+        expect(test.stderr,
+            containsInOrder(['define_platforms must be a map.', '^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects a non-string key', () async {
+        await d.file('dart_test.yaml', 'define_platforms: {12: null}').create();
+
+        var test = await runTest([]);
+        expect(test.stderr,
+            containsInOrder(['Platform identifier must be a string.', '^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects a non-identifier-like key', () async {
+        await d
+            .file('dart_test.yaml', 'define_platforms: {foo bar: null}')
+            .create();
+
+        var test = await runTest([]);
+        expect(
+            test.stderr,
+            containsInOrder([
+              'Platform identifier must be an (optionally hyphenated) Dart '
+                  'identifier.',
+              '^^^^^^^'
+            ]));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects a non-map definition', () async {
+        await d.file('dart_test.yaml', '''
+          define_platforms:
+            chromium: 12
+        ''').create();
+
+        var test = await runTest([]);
+        expect(test.stderr,
+            containsInOrder(['Platform definition must be a map.', '^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('requires a name key', () async {
+        await d.file('dart_test.yaml', '''
+          define_platforms:
+            chromium:
+              extends: chrome
+              settings: {}
+        ''').create();
+
+        var test = await runTest([]);
+        expect(
+            test.stderr,
+            containsInOrder(
+                ['Missing required field "name".', 'extends: chrome']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('name must be a string', () async {
+        await d.file('dart_test.yaml', '''
+          define_platforms:
+            chromium:
+              name: null
+              extends: chrome
+              settings: {}
+        ''').create();
+
+        var test = await runTest([]);
+        expect(test.stderr, containsInOrder(['Must be a string.', '^^^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('requires an extends key', () async {
+        await d.file('dart_test.yaml', '''
+          define_platforms:
+            chromium:
+              name: Chromium
+              settings: {}
+        ''').create();
+
+        var test = await runTest([]);
+        expect(
+            test.stderr,
+            containsInOrder(
+                ['Missing required field "extends".', 'name: Chromium']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('extends must be a string', () async {
+        await d.file('dart_test.yaml', '''
+          define_platforms:
+            chromium:
+              name: Chromium
+              extends: null
+              settings: {}
+        ''').create();
+
+        var test = await runTest([]);
+        expect(test.stderr,
+            containsInOrder(['Platform parent must be a string.', '^^^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('extends must be identifier-like', () async {
+        await d.file('dart_test.yaml', '''
+          define_platforms:
+            chromium:
+              name: Chromium
+              extends: foo bar
+              settings: {}
+        ''').create();
+
+        var test = await runTest([]);
+        expect(
+            test.stderr,
+            containsInOrder([
+              'Platform parent must be an (optionally hyphenated) Dart '
+                  'identifier.',
+              '^^^^^^^'
+            ]));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('requires a settings key', () async {
+        await d.file('dart_test.yaml', '''
+          define_platforms:
+            chromium:
+              name: Chromium
+              extends: chrome
+        ''').create();
+
+        var test = await runTest([]);
+        expect(
+            test.stderr,
+            containsInOrder(
+                ['Missing required field "settings".', 'name: Chromium']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('settings must be a map', () async {
+        await d.file('dart_test.yaml', '''
+          define_platforms:
+            chromium:
+              name: Chromium
+              extends: chrome
+              settings: null
+        ''').create();
+
+        var test = await runTest([]);
+        expect(test.stderr, containsInOrder(['Must be a map.', '^^^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('the new platform may not override an existing platform', () async {
+        await d.file('dart_test.yaml', '''
+          define_platforms:
+            chrome:
+              name: Chromium
+              extends: firefox
+              settings: {}
+        ''').create();
+
+        await d.dir('test').create();
+
+        var test = await runTest([]);
+        expect(
+            test.stderr,
+            containsInOrder([
+              'The platform "chrome" already exists. Use override_platforms to '
+                  'override it.',
+              '^^^^^^'
+            ]));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('the new platform must extend an existing platform', () async {
+        await d.file('dart_test.yaml', '''
+          define_platforms:
+            chromium:
+              name: Chromium
+              extends: foobar
+              settings: {}
+        ''').create();
+
+        await d.dir('test').create();
+
+        var test = await runTest([]);
+        expect(test.stderr, containsInOrder(['Unknown platform.', '^^^^^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test("the new platform can't extend an uncustomizable platform",
+          () async {
+        await d.file('dart_test.yaml', '''
+          define_platforms:
+            myvm:
+              name: My VM
+              extends: vm
+              settings: {}
+        ''').create();
+
+        var test = await runTest(['-p', 'myvm', 'test.dart']);
+        expect(test.stdout,
+            containsInOrder(['The "vm" platform can\'t be customized.', '^^']));
+        await test.shouldExit(1);
+      });
+
+      group('when overriding browsers', () {
+        test('executable must be a string or map', () async {
+          await d.file('dart_test.yaml', '''
+            define_platforms:
+              chromium:
+                name: Chromium
+                extends: chrome
+                settings:
+                  executable: 12
+          ''').create();
+
+          var test = await runTest(['-p', 'chromium', 'test.dart']);
+          expect(test.stdout,
+              containsInOrder(['Must be a map or a string.', '^^']));
+          await test.shouldExit(1);
+        });
+
+        test('executable string may not be relative on POSIX', () async {
+          await d.file('dart_test.yaml', '''
+            define_platforms:
+              chromium:
+                name: Chromium
+                extends: chrome
+                settings:
+                  executable: foo/bar
+          ''').create();
+
+          var test = await runTest(['-p', 'chromium', 'test.dart']);
+          expect(
+              test.stdout,
+              containsInOrder([
+                'Linux and Mac OS executables may not be relative paths.',
+                '^^^^^^^'
+              ]));
+          await test.shouldExit(1);
+        },
+            // We allow relative executables for Windows so that Windows users
+            // can set a global executable without having to explicitly write
+            // `windows:`.
+            testOn: '!windows');
+
+        test('Linux executable must be a string', () async {
+          await d.file('dart_test.yaml', '''
+            define_platforms:
+              chromium:
+                name: Chromium
+                extends: chrome
+                settings:
+                  executable:
+                    linux: 12
+          ''').create();
+
+          var test = await runTest(['-p', 'chromium', 'test.dart']);
+          expect(test.stdout, containsInOrder(['Must be a string.', '^^']));
+          await test.shouldExit(1);
+        });
+
+        test('Linux executable may not be relative', () async {
+          await d.file('dart_test.yaml', '''
+            define_platforms:
+              chromium:
+                name: Chromium
+                extends: chrome
+                settings:
+                  executable:
+                    linux: foo/bar
+          ''').create();
+
+          var test = await runTest(['-p', 'chromium', 'test.dart']);
+          expect(
+              test.stdout,
+              containsInOrder([
+                'Linux and Mac OS executables may not be relative paths.',
+                '^^^^^^^'
+              ]));
+          await test.shouldExit(1);
+        });
+
+        test('Mac OS executable must be a string', () async {
+          await d.file('dart_test.yaml', '''
+            define_platforms:
+              chromium:
+                name: Chromium
+                extends: chrome
+                settings:
+                  executable:
+                    mac_os: 12
+          ''').create();
+
+          var test = await runTest(['-p', 'chromium', 'test.dart']);
+          expect(test.stdout, containsInOrder(['Must be a string.', '^^']));
+          await test.shouldExit(1);
+        });
+
+        test('Mac OS executable may not be relative', () async {
+          await d.file('dart_test.yaml', '''
+            define_platforms:
+              chromium:
+                name: Chromium
+                extends: chrome
+                settings:
+                  executable:
+                    mac_os: foo/bar
+          ''').create();
+
+          var test = await runTest(['-p', 'chromium', 'test.dart']);
+          expect(
+              test.stdout,
+              containsInOrder([
+                'Linux and Mac OS executables may not be relative paths.',
+                '^^^^^^^'
+              ]));
+          await test.shouldExit(1);
+        });
+
+        test('Windows executable must be a string', () async {
+          await d.file('dart_test.yaml', '''
+            define_platforms:
+              chromium:
+                name: Chromium
+                extends: chrome
+                settings:
+                  executable:
+                    windows: 12
+          ''').create();
+
+          var test = await runTest(['-p', 'chromium', 'test.dart']);
+          expect(test.stdout, containsInOrder(['Must be a string.', '^^']));
+          await test.shouldExit(1);
+        });
+
+        test('executable must exist', () async {
+          await d.file('dart_test.yaml', '''
+            define_platforms:
+              chromium:
+                name: Chromium
+                extends: chrome
+                settings:
+                  executable: _does_not_exist
+          ''').create();
+
+          var test = await runTest(['-p', 'chromium', 'test.dart']);
+          expect(
+              test.stdout,
+              emitsThrough(
+                  contains('Failed to run Chrome: $noSuchFileMessage')));
+          await test.shouldExit(1);
+        });
+
+        test('arguments must be a string', () async {
+          await d.file('dart_test.yaml', '''
+            define_platforms:
+              chromium:
+                name: Chromium
+                extends: chrome
+                settings:
+                  arguments: 12
+          ''').create();
+
+          var test = await runTest(['-p', 'chromium', 'test.dart']);
+          expect(test.stdout, containsInOrder(['Must be a string.', '^^']));
+          await test.shouldExit(1);
+        });
+
+        test('arguments must be shell parseable', () async {
+          await d.file('dart_test.yaml', '''
+            define_platforms:
+              chromium:
+                name: Chromium
+                extends: chrome
+                settings:
+                  arguments: --foo 'bar
+          ''').create();
+
+          var test = await runTest(['-p', 'chromium', 'test.dart']);
+          expect(test.stdout,
+              containsInOrder(['Unmatched single quote.', '^^^^^^^^^^']));
+          await test.shouldExit(1);
+        });
+
+        test('headless must be a boolean', () async {
+          await d.file('dart_test.yaml', '''
+            define_platforms:
+              chromium:
+                name: Chromium
+                extends: chrome
+                settings:
+                  headless: definitely
+          ''').create();
+
+          var test = await runTest(['-p', 'chromium', 'test.dart']);
+          expect(
+              test.stdout,
+              emitsThrough(
+                  containsInOrder(['Must be a boolean.', '^^^^^^^^^^'])));
+          await test.shouldExit(1);
+        });
+
+        test('with an argument that causes the browser to quit', () async {
+          await d.file('dart_test.yaml', '''
+            define_platforms:
+              myfox:
+                name: My Firefox
+                extends: firefox
+                settings:
+                  arguments: --version
+          ''').create();
+
+          var test = await runTest(['-p', 'myfox', 'test.dart']);
+          expect(test.stdout,
+              emitsThrough(contains('My Firefox exited before connecting.')));
+          await test.shouldExit(1);
+        }, tags: 'firefox');
+      });
+    });
+  });
+}
diff --git a/pkgs/test/test/runner/configuration/duplicate_names_test.dart b/pkgs/test/test/runner/configuration/duplicate_names_test.dart
new file mode 100644
index 0000000..5a02f52
--- /dev/null
+++ b/pkgs/test/test/runner/configuration/duplicate_names_test.dart
@@ -0,0 +1,82 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('vm')
+library;
+
+import 'dart:convert';
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  group('duplicate names', () {
+    group('can be disabled for', () {
+      for (var function in ['group', 'test']) {
+        test('${function}s', () async {
+          await d
+              .file('dart_test.yaml',
+                  jsonEncode({'allow_duplicate_test_names': false}))
+              .create();
+
+          var testName = 'test';
+          await d.file('test.dart', '''
+          import 'package:test/test.dart';
+
+          void main() {
+            $function("$testName", () {});
+            $function("$testName", () {});
+          }
+        ''').create();
+
+          var test = await runTest([
+            'test.dart',
+            '--configuration',
+            p.join(d.sandbox, 'dart_test.yaml')
+          ]);
+
+          expect(
+              test.stdout,
+              emitsThrough(contains(
+                  'A test with the name "$testName" was already declared.')));
+
+          await test.shouldExit(1);
+        });
+      }
+    });
+    group('are allowed by default for', () {
+      for (var function in ['group', 'test']) {
+        test('${function}s', () async {
+          var testName = 'test';
+          await d.file('test.dart', '''
+          import 'package:test/test.dart';
+
+          void main() {
+            $function("$testName", () {});
+            $function("$testName", () {});
+
+            // Needed so at least one test runs when testing groups.
+            test('a test', () {
+              expect(true, isTrue);
+            });
+          }
+        ''').create();
+
+          var test = await runTest(
+            ['test.dart'],
+          );
+
+          expect(test.stdout, emitsThrough(contains('All tests passed!')));
+
+          await test.shouldExit(0);
+        });
+      }
+    });
+  });
+}
diff --git a/pkgs/test/test/runner/configuration/global_test.dart b/pkgs/test/test/runner/configuration/global_test.dart
new file mode 100644
index 0000000..98e8741
--- /dev/null
+++ b/pkgs/test/test/runner/configuration/global_test.dart
@@ -0,0 +1,122 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:convert';
+
+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;
+
+import '../../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('ignores an empty file', () async {
+    await d.file('global_test.yaml', '').create();
+
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("success", () {});
+      }
+    ''').create();
+
+    var test = await runTest(['test.dart'],
+        environment: {'DART_TEST_CONFIG': 'global_test.yaml'});
+    expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+    await test.shouldExit(0);
+  });
+
+  test('uses supported test configuration', () async {
+    await d
+        .file('global_test.yaml', jsonEncode({'verbose_trace': true}))
+        .create();
+
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("failure", () => throw "oh no");
+      }
+    ''').create();
+
+    var test = await runTest(['test.dart'],
+        environment: {'DART_TEST_CONFIG': 'global_test.yaml'});
+    expect(test.stdout, emitsThrough(contains('dart:async')));
+    await test.shouldExit(1);
+  });
+
+  test('uses supported runner configuration', () async {
+    await d.file('global_test.yaml', jsonEncode({'reporter': 'json'})).create();
+
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("success", () {});
+      }
+    ''').create();
+
+    var test = await runTest(['test.dart'],
+        environment: {'DART_TEST_CONFIG': 'global_test.yaml'});
+    expect(test.stdout, emitsThrough(contains('"testStart"')));
+    await test.shouldExit(0);
+  });
+
+  test('local configuration takes precedence', () async {
+    await d
+        .file('global_test.yaml', jsonEncode({'verbose_trace': true}))
+        .create();
+
+    await d
+        .file('dart_test.yaml', jsonEncode({'verbose_trace': false}))
+        .create();
+
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("failure", () => throw "oh no");
+      }
+    ''').create();
+
+    var test = await runTest(['test.dart'],
+        environment: {'DART_TEST_CONFIG': 'global_test.yaml'});
+    expect(test.stdout, neverEmits(contains('dart:isolate-patch')));
+    await test.shouldExit(1);
+  });
+
+  group('disallows local-only configuration:', () {
+    for (var field in [
+      'skip', 'retry', 'test_on', 'paths', 'filename', 'names', 'tags', //
+      'plain_names', 'include_tags', 'exclude_tags', 'add_tags',
+      'define_platforms', 'allow_duplicate_test_names',
+    ]) {
+      test('for $field', () async {
+        await d.file('global_test.yaml', jsonEncode({field: null})).create();
+
+        await d.file('test.dart', '''
+          import 'package:test/test.dart';
+
+          void main() {
+            test("success", () {});
+          }
+        ''').create();
+
+        var test = await runTest(['test.dart'],
+            environment: {'DART_TEST_CONFIG': 'global_test.yaml'});
+        expect(
+            test.stderr,
+            containsInOrder(
+                ["of global_test.yaml: $field isn't supported here.", '^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+    }
+  });
+}
diff --git a/pkgs/test/test/runner/configuration/include_test.dart b/pkgs/test/test/runner/configuration/include_test.dart
new file mode 100644
index 0000000..5d064cf
--- /dev/null
+++ b/pkgs/test/test/runner/configuration/include_test.dart
@@ -0,0 +1,170 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:test_core/src/runner/configuration.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+void main() {
+  test('should merge with a base configuration', () async {
+    await d.dir('repo', [
+      d.file('dart_test_base.yaml', 'filename: "test_*.dart"'),
+      d.dir('pkg', [
+        d.file('dart_test.yaml', '''
+          include: ../dart_test_base.yaml
+          concurrency: 3
+        '''),
+      ]),
+    ]).create();
+
+    var path = p.join(d.sandbox, 'repo', 'pkg', 'dart_test.yaml');
+    var config = Configuration.load(path);
+    expect(config.filename.pattern, equals('test_*.dart'));
+    expect(config.concurrency, equals(3));
+  });
+
+  test('should merge fields with a base configuration', () async {
+    await d.dir('repo', [
+      d.file('dart_test_base.yaml', '''
+        tags:
+          hello:
+      '''),
+      d.dir('pkg', [
+        d.file('dart_test.yaml', '''
+          include: ../dart_test_base.yaml
+          tags:
+            world:
+        '''),
+      ]),
+    ]).create();
+
+    var path = p.join(d.sandbox, 'repo', 'pkg', 'dart_test.yaml');
+    var config = Configuration.load(path);
+    expect(config.knownTags, unorderedEquals(['hello', 'world']));
+  });
+
+  test('should allow an included file to include a file', () async {
+    await d.dir('repo', [
+      d.file('dart_test_base_base.yaml', '''
+        tags:
+          tag:
+      '''),
+      d.file('dart_test_base.yaml', '''
+        include: dart_test_base_base.yaml
+        filename: "test_*.dart"
+      '''),
+      d.dir('pkg', [
+        d.file('dart_test.yaml', '''
+          include: ../dart_test_base.yaml
+          concurrency: 3
+        '''),
+      ]),
+    ]).create();
+
+    var path = p.join(d.sandbox, 'repo', 'pkg', 'dart_test.yaml');
+    var config = Configuration.load(path);
+    expect(config.knownTags, ['tag']);
+    expect(config.filename.pattern, 'test_*.dart');
+    expect(config.concurrency, 3);
+  });
+
+  test('should not allow an include field in a test config context', () async {
+    await d.dir('repo', [
+      d.dir('pkg', [
+        d.file('dart_test.yaml', r'''
+          tags:
+            foo:
+              include: ../dart_test.yaml
+        '''),
+      ]),
+    ]).create();
+
+    var path = p.join(d.sandbox, 'repo', 'pkg', 'dart_test.yaml');
+    expect(
+        () => Configuration.load(path),
+        throwsA(allOf(
+            isFormatException,
+            predicate((error) =>
+                error.toString().contains("include isn't supported here")))));
+  });
+
+  test('should allow an include field in a runner config context', () async {
+    await d.dir('repo', [
+      d.dir('pkg', [
+        d.file('dart_test.yaml', '''
+          presets:
+            bar:
+              include: other_dart_test.yaml
+              pause_after_load: true
+        '''),
+        d.file('other_dart_test.yaml', 'reporter: expanded'),
+      ]),
+    ]).create();
+
+    var path = p.join(d.sandbox, 'repo', 'pkg', 'dart_test.yaml');
+    var config = Configuration.load(path);
+    var presetBar = config.presets['bar']!;
+    expect(presetBar.pauseAfterLoad, isTrue);
+    expect(presetBar.reporter, 'expanded');
+  });
+
+  test('local configuration should take precedence after merging', () async {
+    await d.dir('repo', [
+      d.dir('pkg', [
+        d.file('dart_test.yaml', '''
+          include: other_dart_test.yaml
+          concurrency: 5
+        '''),
+        d.file('other_dart_test.yaml', 'concurrency: 10'),
+      ]),
+    ]).create();
+
+    var path = p.join(d.sandbox, 'repo', 'pkg', 'dart_test.yaml');
+    var config = Configuration.load(path);
+    expect(config.concurrency, 5);
+  });
+
+  group('gracefully handles', () {
+    test('a non-string include field', () async {
+      await d.dir('repo', [
+        d.dir('pkg', [
+          d.file('dart_test.yaml', 'include: 3'),
+        ]),
+      ]).create();
+
+      var path = p.join(d.sandbox, 'repo', 'pkg', 'dart_test.yaml');
+      expect(() => Configuration.load(path), throwsFormatException);
+    });
+
+    test('a non-existent included file', () async {
+      await d.dir('repo', [
+        d.dir('pkg', [
+          d.file('dart_test.yaml', 'include: other_test.yaml'),
+        ]),
+      ]).create();
+
+      var path = p.join(d.sandbox, 'repo', 'pkg', 'dart_test.yaml');
+      expect(() => Configuration.load(path), throwsFormatException);
+    });
+
+    test('an include field in a test config context', () async {
+      await d.dir('repo', [
+        d.dir('pkg', [
+          d.file('dart_test.yaml', '''
+            tags:
+              foo:
+                include: ../dart_test.yaml
+          '''),
+        ]),
+      ]).create();
+
+      var path = p.join(d.sandbox, 'repo', 'pkg', 'dart_test.yaml');
+      expect(() => Configuration.load(path), throwsFormatException);
+    });
+  });
+}
diff --git a/pkgs/test/test/runner/configuration/platform_test.dart b/pkgs/test/test/runner/configuration/platform_test.dart
new file mode 100644
index 0000000..014c0ed
--- /dev/null
+++ b/pkgs/test/test/runner/configuration/platform_test.dart
@@ -0,0 +1,310 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:convert';
+
+import 'package:test/test.dart';
+import 'package:test_core/src/util/exit_codes.dart' as exit_codes;
+import 'package:test_core/src/util/io.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  group('on_platform', () {
+    test('applies platform-specific configuration to matching tests', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'on_platform': {
+                  'chrome': {'timeout': '0s'}
+                }
+              }))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () => Future.delayed(Duration.zero));
+        }
+      ''').create();
+
+      var test = await runTest(['-p', 'chrome,vm', 'test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder(
+              ['-1: [Chrome, Dart2Js] test [E]', '+1 -1: Some tests failed.']));
+      await test.shouldExit(1);
+    }, tags: ['chrome']);
+
+    test('supports platform selectors', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'on_platform': {
+                  'chrome || vm': {'timeout': '0s'}
+                }
+              }))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () => Future.delayed(Duration.zero));
+        }
+      ''').create();
+
+      var test = await runTest(['-p', 'chrome,vm', 'test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: [Chrome, Dart2Js] test [E]',
+            '-2: [VM, Kernel] test [E]',
+            '-2: Some tests failed.'
+          ]));
+      await test.shouldExit(1);
+    }, tags: ['chrome']);
+
+    group('errors', () {
+      test('rejects an invalid selector type', () async {
+        await d.file('dart_test.yaml', '{"on_platform": {12: null}}').create();
+
+        var test = await runTest([]);
+        expect(test.stderr,
+            containsInOrder(['on_platform key must be a string', '^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects an invalid selector', () async {
+        await d
+            .file(
+                'dart_test.yaml',
+                jsonEncode({
+                  'on_platform': {'foo bar': null}
+                }))
+            .create();
+
+        var test = await runTest([]);
+        expect(
+            test.stderr,
+            containsInOrder([
+              'Invalid on_platform key: Expected end of input.',
+              '^^^^^^^^^'
+            ]));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects a selector with an undefined variable', () async {
+        await d
+            .file(
+                'dart_test.yaml',
+                jsonEncode({
+                  'on_platform': {'foo': null}
+                }))
+            .create();
+
+        await d.dir('test').create();
+
+        var test = await runTest([]);
+        expect(test.stderr, containsInOrder(['Undefined variable.', '^^^^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects an invalid map', () async {
+        await d
+            .file(
+                'dart_test.yaml',
+                jsonEncode({
+                  'on_platform': {'linux': 12}
+                }))
+            .create();
+
+        var test = await runTest([]);
+        expect(test.stderr,
+            containsInOrder(['on_platform value must be a map.', '^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects an invalid configuration', () async {
+        await d
+            .file(
+                'dart_test.yaml',
+                jsonEncode({
+                  'on_platform': {
+                    'linux': {'timeout': '12p'}
+                  }
+                }))
+            .create();
+
+        var test = await runTest([]);
+        expect(test.stderr,
+            containsInOrder(['Invalid timeout: expected unit.', '^^^^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects runner configuration', () async {
+        await d
+            .file(
+                'dart_test.yaml',
+                jsonEncode({
+                  'on_platform': {
+                    'linux': {'filename': '*_blorp'}
+                  }
+                }))
+            .create();
+
+        var test = await runTest([]);
+        expect(test.stderr,
+            containsInOrder(["filename isn't supported here.", '^^^^^^^^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+    });
+  });
+
+  group('on_os', () {
+    test('applies OS-specific configuration on a matching OS', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'on_os': {
+                  currentOS.identifier: {'filename': 'test_*.dart'}
+                }
+              }))
+          .create();
+
+      await d.file('foo_test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("foo_test", () {});
+        }
+      ''').create();
+
+      await d.file('test_foo.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test_foo", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['.']);
+      expect(
+          test.stdout,
+          containsInOrder(
+              ['+0: ./test_foo.dart: test_foo', '+1: All tests passed!']));
+      await test.shouldExit(0);
+    });
+
+    test("doesn't apply OS-specific configuration on a non-matching OS",
+        () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'on_os': {
+                  otherOS: {'filename': 'test_*.dart'}
+                }
+              }))
+          .create();
+
+      await d.file('foo_test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("foo_test", () {});
+        }
+      ''').create();
+
+      await d.file('test_foo.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test_foo", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['.']);
+      expect(
+          test.stdout,
+          containsInOrder(
+              ['+0: ./foo_test.dart: foo_test', '+1: All tests passed!']));
+      await test.shouldExit(0);
+    });
+
+    group('errors', () {
+      test('rejects an invalid OS type', () async {
+        await d.file('dart_test.yaml', '{"on_os": {12: null}}').create();
+
+        var test = await runTest([]);
+        expect(
+            test.stderr, containsInOrder(['on_os key must be a string', '^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects an unknown OS name', () async {
+        await d
+            .file(
+                'dart_test.yaml',
+                jsonEncode({
+                  'on_os': {'foo': null}
+                }))
+            .create();
+
+        var test = await runTest([]);
+        expect(
+            test.stderr,
+            containsInOrder(
+                ['Invalid on_os key: No such operating system.', '^^^^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects an invalid map', () async {
+        await d
+            .file(
+                'dart_test.yaml',
+                jsonEncode({
+                  'on_os': {'linux': 12}
+                }))
+            .create();
+
+        var test = await runTest([]);
+        expect(
+            test.stderr, containsInOrder(['on_os value must be a map.', '^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects an invalid configuration', () async {
+        await d
+            .file(
+                'dart_test.yaml',
+                jsonEncode({
+                  'on_os': {
+                    'linux': {'timeout': '12p'}
+                  }
+                }))
+            .create();
+
+        var test = await runTest([]);
+        expect(test.stderr,
+            containsInOrder(['Invalid timeout: expected unit.', '^^^^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+    });
+  });
+}
diff --git a/pkgs/test/test/runner/configuration/presets_test.dart b/pkgs/test/test/runner/configuration/presets_test.dart
new file mode 100644
index 0000000..193f12e
--- /dev/null
+++ b/pkgs/test/test/runner/configuration/presets_test.dart
@@ -0,0 +1,515 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:convert';
+
+import 'package:test/test.dart';
+import 'package:test_core/src/util/exit_codes.dart' as exit_codes;
+import 'package:test_core/src/util/io.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  group('presets', () {
+    test("don't do anything by default", () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'presets': {
+                  'foo': {'timeout': '0s'}
+                }
+              }))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () => Future.delayed(Duration.zero));
+        }
+      ''').create();
+
+      await (await runTest(['test.dart'])).shouldExit(0);
+    });
+
+    test('can be selected on the command line', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'presets': {
+                  'foo': {'timeout': '0s'}
+                }
+              }))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () => Future.delayed(Duration.zero));
+        }
+      ''').create();
+
+      var test = await runTest(['-P', 'foo', 'test.dart']);
+      expect(test.stdout,
+          containsInOrder(['-1: test [E]', '-1: Some tests failed.']));
+      await test.shouldExit(1);
+    });
+
+    test('multiple presets can be selected', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'presets': {
+                  'foo': {'timeout': '0s'},
+                  'bar': {
+                    'paths': ['test.dart']
+                  }
+                }
+              }))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () => Future.delayed(Duration.zero));
+        }
+      ''').create();
+
+      var test = await runTest(['-P', 'foo,bar']);
+      expect(test.stdout,
+          containsInOrder(['-1: test [E]', '-1: Some tests failed.']));
+      await test.shouldExit(1);
+    });
+
+    test('the latter preset takes precedence', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'presets': {
+                  'foo': {'timeout': '0s'},
+                  'bar': {'timeout': '30s'}
+                }
+              }))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () => Future.delayed(Duration.zero));
+        }
+      ''').create();
+
+      await (await runTest(['-P', 'foo,bar', 'test.dart'])).shouldExit(0);
+
+      var test = await runTest(['-P', 'bar,foo', 'test.dart']);
+      expect(test.stdout,
+          containsInOrder(['-1: test [E]', '-1: Some tests failed.']));
+      await test.shouldExit(1);
+    });
+
+    test('a preset takes precedence over the base configuration', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'presets': {
+                  'foo': {'timeout': '0s'}
+                },
+                'timeout': '30s'
+              }))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () => Future.delayed(Duration.zero));
+        }
+      ''').create();
+
+      var test = await runTest(['-P', 'foo', 'test.dart']);
+      expect(test.stdout,
+          containsInOrder(['-1: test [E]', '-1: Some tests failed.']));
+      await test.shouldExit(1);
+
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'presets': {
+                  'foo': {'timeout': '30s'}
+                },
+                'timeout': '00s'
+              }))
+          .create();
+
+      await (await runTest(['-P', 'foo', 'test.dart'])).shouldExit(0);
+    });
+
+    test('a nested preset is activated', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'tags': {
+                  'foo': {
+                    'presets': {
+                      'bar': {'timeout': '0s'}
+                    },
+                  },
+                }
+              }))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test 1", () => Future.delayed(Duration.zero), tags: "foo");
+          test("test 2", () => Future.delayed(Duration.zero));
+        }
+      ''').create();
+
+      var test = await runTest(['-P', 'bar', 'test.dart']);
+      expect(test.stdout,
+          containsInOrder(['+0 -1: test 1 [E]', '+1 -1: Some tests failed.']));
+      await test.shouldExit(1);
+
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'presets': {
+                  'foo': {'timeout': '30s'}
+                },
+                'timeout': '00s'
+              }))
+          .create();
+
+      await (await runTest(['-P', 'foo', 'test.dart'])).shouldExit(0);
+    });
+  });
+
+  group('add_presets', () {
+    test('selects a preset', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'presets': {
+                  'foo': {'timeout': '0s'}
+                },
+                'add_presets': ['foo']
+              }))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () => Future.delayed(Duration.zero));
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdout,
+          containsInOrder(['-1: test [E]', '-1: Some tests failed.']));
+      await test.shouldExit(1);
+    });
+
+    test('applies presets in selection order', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'presets': {
+                  'foo': {'timeout': '0s'},
+                  'bar': {'timeout': '30s'}
+                },
+                'add_presets': ['foo', 'bar']
+              }))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () => Future.delayed(Duration.zero));
+        }
+      ''').create();
+
+      await (await runTest(['test.dart'])).shouldExit(0);
+
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'presets': {
+                  'foo': {'timeout': '0s'},
+                  'bar': {'timeout': '30s'}
+                },
+                'add_presets': ['bar', 'foo']
+              }))
+          .create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdout,
+          containsInOrder(['-1: test [E]', '-1: Some tests failed.']));
+      await test.shouldExit(1);
+    });
+
+    test('allows preset inheritance via add_presets', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'presets': {
+                  'foo': {
+                    'add_presets': ['bar']
+                  },
+                  'bar': {'timeout': '0s'}
+                }
+              }))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () => Future.delayed(Duration.zero));
+        }
+      ''').create();
+
+      var test = await runTest(['-P', 'foo', 'test.dart']);
+      expect(test.stdout,
+          containsInOrder(['+0 -1: test [E]', '-1: Some tests failed.']));
+      await test.shouldExit(1);
+    });
+
+    test('allows circular preset inheritance via add_presets', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'presets': {
+                  'foo': {
+                    'add_presets': ['bar']
+                  },
+                  'bar': {
+                    'add_presets': ['foo']
+                  }
+                }
+              }))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () {});
+        }
+      ''').create();
+
+      await (await runTest(['-P', 'foo', 'test.dart'])).shouldExit(0);
+    });
+  });
+
+  group('errors', () {
+    group('presets', () {
+      test('rejects an invalid preset type', () async {
+        await d.file('dart_test.yaml', '{"presets": {12: null}}').create();
+
+        var test = await runTest([]);
+        expect(test.stderr,
+            containsInOrder(['presets key must be a string', '^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects an invalid preset name', () async {
+        await d
+            .file(
+                'dart_test.yaml',
+                jsonEncode({
+                  'presets': {'foo bar': null}
+                }))
+            .create();
+
+        var test = await runTest([]);
+        expect(
+            test.stderr,
+            containsInOrder([
+              'presets key must be an (optionally hyphenated) Dart identifier.',
+              '^^^^^^^^^'
+            ]));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects an invalid preset map', () async {
+        await d.file('dart_test.yaml', jsonEncode({'presets': 12})).create();
+
+        var test = await runTest([]);
+        expect(test.stderr, containsInOrder(['presets must be a map', '^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects an invalid preset configuration', () async {
+        await d
+            .file(
+                'dart_test.yaml',
+                jsonEncode({
+                  'presets': {
+                    'foo': {'timeout': '12p'}
+                  }
+                }))
+            .create();
+
+        var test = await runTest([]);
+        expect(test.stderr,
+            containsInOrder(['Invalid timeout: expected unit', '^^^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects runner configuration in a non-runner context', () async {
+        await d
+            .file(
+                'dart_test.yaml',
+                jsonEncode({
+                  'tags': {
+                    'foo': {
+                      'presets': {
+                        'bar': {'filename': '*_blorp.dart'}
+                      }
+                    }
+                  }
+                }))
+            .create();
+
+        var test = await runTest([]);
+        expect(test.stderr,
+            containsInOrder(["filename isn't supported here.", '^^^^^^^^^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('fails if an undefined preset is passed', () async {
+        var test = await runTest(['-P', 'foo']);
+        expect(test.stderr, emitsThrough(contains('Undefined preset "foo".')));
+        await test.shouldExit(exit_codes.usage);
+      });
+
+      test('fails if an undefined preset is added', () async {
+        await d
+            .file(
+                'dart_test.yaml',
+                jsonEncode({
+                  'add_presets': ['foo', 'bar']
+                }))
+            .create();
+
+        var test = await runTest([]);
+        expect(test.stderr,
+            emitsThrough(contains('Undefined presets "foo" and "bar".')));
+        await test.shouldExit(exit_codes.usage);
+      });
+
+      test('fails if an undefined preset is added in a nested context',
+          () async {
+        await d
+            .file(
+                'dart_test.yaml',
+                jsonEncode({
+                  'on_os': {
+                    currentOS.identifier: {
+                      'add_presets': ['bar']
+                    }
+                  }
+                }))
+            .create();
+
+        var test = await runTest([]);
+        expect(test.stderr, emitsThrough(contains('Undefined preset "bar".')));
+        await test.shouldExit(exit_codes.usage);
+      });
+    });
+
+    group('add_presets', () {
+      test('rejects an invalid list type', () async {
+        await d
+            .file('dart_test.yaml', jsonEncode({'add_presets': 'foo'}))
+            .create();
+
+        var test = await runTest(['test.dart']);
+        expect(test.stderr,
+            containsInOrder(['add_presets must be a list', '^^^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects an invalid preset type', () async {
+        await d
+            .file(
+                'dart_test.yaml',
+                jsonEncode({
+                  'add_presets': [12]
+                }))
+            .create();
+
+        var test = await runTest(['test.dart']);
+        expect(test.stderr,
+            containsInOrder(['Preset name must be a string', '^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects an invalid preset name', () async {
+        await d
+            .file(
+                'dart_test.yaml',
+                jsonEncode({
+                  'add_presets': ['foo bar']
+                }))
+            .create();
+
+        var test = await runTest(['test.dart']);
+        expect(
+            test.stderr,
+            containsInOrder([
+              'Preset name must be an (optionally hyphenated) Dart identifier.',
+              '^^^^^^^^^'
+            ]));
+        await test.shouldExit(exit_codes.data);
+      });
+    });
+  });
+}
diff --git a/pkgs/test/test/runner/configuration/randomize_order_test.dart b/pkgs/test/test/runner/configuration/randomize_order_test.dart
new file mode 100644
index 0000000..690f54b
--- /dev/null
+++ b/pkgs/test/test/runner/configuration/randomize_order_test.dart
@@ -0,0 +1,234 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:convert';
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('shuffles test order when passed a seed', () async {
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("test 1", () {});
+        test("test 2", () {});
+        test("test 3", () {});
+        test("test 4", () {});
+      }
+    ''').create();
+
+    // Test with a given seed
+    var test =
+        await runTest(['test.dart', '--test-randomize-ordering-seed=987654']);
+    expect(
+        test.stdout,
+        containsInOrder([
+          '+0: test 4',
+          '+1: test 3',
+          '+2: test 1',
+          '+3: test 2',
+          '+4: All tests passed!'
+        ]));
+    await test.shouldExit(0);
+
+    // Do not shuffle when passed 0
+    test = await runTest(['test.dart', '--test-randomize-ordering-seed=0']);
+    expect(
+        test.stdout,
+        containsInOrder([
+          '+0: test 1',
+          '+1: test 2',
+          '+2: test 3',
+          '+3: test 4',
+          '+4: All tests passed!'
+        ]));
+    await test.shouldExit(0);
+
+    // Do not shuffle when passed nothing
+    test = await runTest(['test.dart']);
+    expect(
+        test.stdout,
+        containsInOrder([
+          '+0: test 1',
+          '+1: test 2',
+          '+2: test 3',
+          '+3: test 4',
+          '+4: All tests passed!'
+        ]));
+    await test.shouldExit(0);
+
+    // Shuffle when passed random
+    test =
+        await runTest(['test.dart', '--test-randomize-ordering-seed=random']);
+    expect(
+        test.stdout,
+        emitsInAnyOrder([
+          contains('Shuffling test order with --test-randomize-ordering-seed'),
+          isNot(contains(
+              'Shuffling test order with --test-randomize-ordering-seed=0'))
+        ]));
+    await test.shouldExit(0);
+
+    // Doesn't log about shuffling with the json reporter
+    test = await runTest(
+        ['test.dart', '--test-randomize-ordering-seed=random', '-r', 'json']);
+    expect(test.stdout, neverEmits(contains('Shuffling test order')));
+    await test.shouldExit(0);
+  });
+
+  test('test shuffling can be disabled in dart_test.yml', () async {
+    await d
+        .file(
+            'dart_test.yaml',
+            jsonEncode({
+              'tags': {
+                'doNotShuffle': {'allow_test_randomization': false}
+              }
+            }))
+        .create();
+
+    await d.file('test.dart', '''
+      @Tags(['doNotShuffle'])
+      import 'package:test/test.dart';
+
+      void main() {
+        test("test 1", () {});
+        test("test 2", () {});
+        test("test 3", () {});
+        test("test 4", () {});
+      }
+    ''').create();
+
+    var test =
+        await runTest(['test.dart', '--test-randomize-ordering-seed=987654']);
+    expect(
+        test.stdout,
+        containsInOrder([
+          '+0: test 1',
+          '+1: test 2',
+          '+2: test 3',
+          '+3: test 4',
+          '+4: All tests passed!'
+        ]));
+    await test.shouldExit(0);
+  });
+
+  test('shuffles each suite with the same seed', () async {
+    await d.file('1_test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("test 1.1", () {});
+        test("test 1.2", () {});
+        test("test 1.3", () {});
+      }
+    ''').create();
+
+    await d.file('2_test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("test 2.1", () {});
+        test("test 2.2", () {});
+        test("test 2.3", () {});
+      }
+    ''').create();
+
+    var test = await runTest(['.', '--test-randomize-ordering-seed=12345']);
+    expect(
+        test.stdout,
+        emitsInAnyOrder([
+          containsInOrder([
+            './1_test.dart: test 1.2',
+            './1_test.dart: test 1.3',
+            './1_test.dart: test 1.1'
+          ]),
+          containsInOrder([
+            './2_test.dart: test 2.2',
+            './2_test.dart: test 2.3',
+            './2_test.dart: test 2.1'
+          ]),
+          contains('+6: All tests passed!')
+        ]));
+    await test.shouldExit(0);
+  });
+
+  test('shuffles groups as well as tests in groups', () async {
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+      group("Group 1", () {
+        test("test 1.1", () {});
+        test("test 1.2", () {});
+        test("test 1.3", () {});
+        test("test 1.4", () {});
+       });
+      group("Group 2", () {
+        test("test 2.1", () {});
+        test("test 2.2", () {});
+        test("test 2.3", () {});
+        test("test 2.4", () {});
+       });
+      }
+    ''').create();
+
+    // Test with a given seed
+    var test =
+        await runTest(['test.dart', '--test-randomize-ordering-seed=123']);
+    expect(
+        test.stdout,
+        containsInOrder([
+          '+0: Group 2 test 2.4',
+          '+1: Group 2 test 2.2',
+          '+2: Group 2 test 2.1',
+          '+3: Group 2 test 2.3',
+          '+4: Group 1 test 1.4',
+          '+5: Group 1 test 1.2',
+          '+6: Group 1 test 1.1',
+          '+7: Group 1 test 1.3',
+          '+8: All tests passed!'
+        ]));
+    await test.shouldExit(0);
+  });
+
+  test('shuffles nested groups', () async {
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+      group("Group 1", () {
+        test("test 1.1", () {});
+        test("test 1.2", () {});
+        group("Group 2", () {
+          test("test 2.3", () {});
+          test("test 2.4", () {});
+        });
+       });
+      }
+    ''').create();
+
+    var test =
+        await runTest(['test.dart', '--test-randomize-ordering-seed=123']);
+    expect(
+        test.stdout,
+        containsInOrder([
+          '+0: Group 1 test 1.1',
+          '+1: Group 1 Group 2 test 2.4',
+          '+2: Group 1 Group 2 test 2.3',
+          '+3: Group 1 test 1.2',
+          '+4: All tests passed!'
+        ]));
+    await test.shouldExit(0);
+  });
+}
diff --git a/pkgs/test/test/runner/configuration/suite_test.dart b/pkgs/test/test/runner/configuration/suite_test.dart
new file mode 100644
index 0000000..1c2fc62
--- /dev/null
+++ b/pkgs/test/test/runner/configuration/suite_test.dart
@@ -0,0 +1,147 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'package:boolean_selector/boolean_selector.dart';
+import 'package:test/test.dart';
+import 'package:test_api/src/backend/platform_selector.dart';
+import 'package:test_api/src/backend/runtime.dart';
+import 'package:test_core/src/runner/compiler_selection.dart';
+import 'package:test_core/src/runner/runtime_selection.dart';
+
+import '../../utils.dart';
+
+void main() {
+  group('merge', () {
+    group('for most fields', () {
+      test('if neither is defined, preserves the default', () {
+        var merged = suiteConfiguration().merge(suiteConfiguration());
+        expect(merged.jsTrace, isFalse);
+        expect(merged.runSkipped, isFalse);
+        expect(merged.precompiledPath, isNull);
+        expect(merged.runtimes, equals([Runtime.vm.identifier]));
+        expect(merged.compilerSelections, isNull);
+      });
+
+      test("if only the old configuration's is defined, uses it", () {
+        var merged = suiteConfiguration(
+                jsTrace: true,
+                runSkipped: true,
+                precompiledPath: '/tmp/js',
+                runtimes: [RuntimeSelection(Runtime.chrome.identifier)],
+                compilerSelections: [CompilerSelection.parse('dart2js')])
+            .merge(suiteConfiguration());
+
+        expect(merged.jsTrace, isTrue);
+        expect(merged.runSkipped, isTrue);
+        expect(merged.precompiledPath, equals('/tmp/js'));
+        expect(merged.runtimes, equals([Runtime.chrome.identifier]));
+        expect(merged.compilerSelections,
+            equals([CompilerSelection.parse('dart2js')]));
+      });
+
+      test("if only the configuration's is defined, uses it", () {
+        var merged = suiteConfiguration().merge(suiteConfiguration(
+            jsTrace: true,
+            runSkipped: true,
+            precompiledPath: '/tmp/js',
+            runtimes: [RuntimeSelection(Runtime.chrome.identifier)],
+            compilerSelections: [CompilerSelection.parse('dart2js')]));
+
+        expect(merged.jsTrace, isTrue);
+        expect(merged.runSkipped, isTrue);
+        expect(merged.precompiledPath, equals('/tmp/js'));
+        expect(merged.runtimes, equals([Runtime.chrome.identifier]));
+        expect(merged.compilerSelections,
+            equals([CompilerSelection.parse('dart2js')]));
+      });
+
+      test(
+          "if the two configurations conflict, uses the configuration's "
+          'values', () {
+        var older = suiteConfiguration(
+            jsTrace: false,
+            runSkipped: true,
+            precompiledPath: '/tmp/js',
+            runtimes: [RuntimeSelection(Runtime.chrome.identifier)],
+            compilerSelections: [CompilerSelection.parse('dart2js')]);
+        var newer = suiteConfiguration(
+            jsTrace: true,
+            runSkipped: false,
+            precompiledPath: '../js',
+            runtimes: [RuntimeSelection(Runtime.firefox.identifier)],
+            compilerSelections: [CompilerSelection.parse('source')]);
+        var merged = older.merge(newer);
+
+        expect(merged.jsTrace, isTrue);
+        expect(merged.runSkipped, isFalse);
+        expect(merged.precompiledPath, equals('../js'));
+        expect(merged.runtimes, equals([Runtime.firefox.identifier]));
+        expect(merged.compilerSelections,
+            equals([CompilerSelection.parse('source')]));
+      });
+    });
+
+    group('for dart2jsArgs', () {
+      test('if neither is defined, preserves the default', () {
+        var merged = suiteConfiguration().merge(suiteConfiguration());
+        expect(merged.dart2jsArgs, isEmpty);
+      });
+
+      test("if only the old configuration's is defined, uses it", () {
+        var merged = suiteConfiguration(dart2jsArgs: ['--foo', '--bar'])
+            .merge(suiteConfiguration());
+        expect(merged.dart2jsArgs, equals(['--foo', '--bar']));
+      });
+
+      test("if only the configuration's is defined, uses it", () {
+        var merged = suiteConfiguration()
+            .merge(suiteConfiguration(dart2jsArgs: ['--foo', '--bar']));
+        expect(merged.dart2jsArgs, equals(['--foo', '--bar']));
+      });
+
+      test('if both are defined, concatenates them', () {
+        var older = suiteConfiguration(dart2jsArgs: ['--foo', '--bar']);
+        var newer = suiteConfiguration(dart2jsArgs: ['--baz']);
+        var merged = older.merge(newer);
+        expect(merged.dart2jsArgs, equals(['--foo', '--bar', '--baz']));
+      });
+    });
+
+    group('for config maps', () {
+      test('merges each nested configuration', () {
+        var merged = suiteConfiguration(tags: {
+          BooleanSelector.parse('foo'):
+              suiteConfiguration(precompiledPath: 'path/'),
+          BooleanSelector.parse('bar'): suiteConfiguration(jsTrace: true)
+        }, onPlatform: {
+          PlatformSelector.parse('vm'):
+              suiteConfiguration(precompiledPath: 'path/'),
+          PlatformSelector.parse('chrome'): suiteConfiguration(jsTrace: true)
+        }).merge(suiteConfiguration(tags: {
+          BooleanSelector.parse('bar'): suiteConfiguration(jsTrace: false),
+          BooleanSelector.parse('baz'): suiteConfiguration(runSkipped: true)
+        }, onPlatform: {
+          PlatformSelector.parse('chrome'): suiteConfiguration(jsTrace: false),
+          PlatformSelector.parse('firefox'):
+              suiteConfiguration(runSkipped: true)
+        }));
+
+        expect(merged.tags[BooleanSelector.parse('foo')]!.precompiledPath,
+            equals('path/'));
+        expect(merged.tags[BooleanSelector.parse('bar')]!.jsTrace, isFalse);
+        expect(merged.tags[BooleanSelector.parse('baz')]!.runSkipped, isTrue);
+
+        expect(merged.onPlatform[PlatformSelector.parse('vm')]!.precompiledPath,
+            'path/');
+        expect(merged.onPlatform[PlatformSelector.parse('chrome')]!.jsTrace,
+            isFalse);
+        expect(merged.onPlatform[PlatformSelector.parse('firefox')]!.runSkipped,
+            isTrue);
+      });
+    });
+  });
+}
diff --git a/pkgs/test/test/runner/configuration/tags_test.dart b/pkgs/test/test/runner/configuration/tags_test.dart
new file mode 100644
index 0000000..bbdb1d1
--- /dev/null
+++ b/pkgs/test/test/runner/configuration/tags_test.dart
@@ -0,0 +1,401 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:convert';
+
+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;
+
+import '../../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('adds the specified tags', () async {
+    await d
+        .file(
+            'dart_test.yaml',
+            jsonEncode({
+              'add_tags': ['foo', 'bar']
+            }))
+        .create();
+
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("test", () {});
+      }
+    ''').create();
+
+    var test = await runTest(['--exclude-tag', 'foo', 'test.dart']);
+    expect(test.stdout, emitsThrough(contains('No tests ran.')));
+    await test.shouldExit(79);
+
+    test = await runTest(['--exclude-tag', 'bar', 'test.dart']);
+    expect(test.stdout, emitsThrough(contains('No tests ran.')));
+    await test.shouldExit(79);
+
+    test = await runTest(['test.dart']);
+    expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+    await test.shouldExit(0);
+  });
+
+  group('tags', () {
+    test("doesn't warn for tags that exist in the configuration", () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'tags': {'foo': null}
+              }))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdout, neverEmits(contains('Warning: Tags were used')));
+      await test.shouldExit(0);
+    });
+
+    test('applies tag-specific configuration only to matching tests', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'tags': {
+                  'foo': {'timeout': '0s'}
+                }
+              }))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test 1", () => Future.delayed(Duration.zero), tags: ['foo']);
+          test("test 2", () => Future.delayed(Duration.zero));
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdout,
+          containsInOrder(['-1: test 1 [E]', '+1 -1: Some tests failed.']));
+      await test.shouldExit(1);
+    });
+
+    test('supports tag selectors', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'tags': {
+                  'foo && bar': {'timeout': '0s'}
+                }
+              }))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test 1", () => Future.delayed(Duration.zero), tags: ['foo']);
+          test("test 2", () => Future.delayed(Duration.zero), tags: ['bar']);
+          test("test 3", () => Future.delayed(Duration.zero),
+              tags: ['foo', 'bar']);
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdout,
+          containsInOrder(['+2 -1: test 3 [E]', '+2 -1: Some tests failed.']));
+      await test.shouldExit(1);
+    });
+
+    test('allows tag inheritance via add_tags', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'tags': {
+                  'foo': null,
+                  'bar': {
+                    'add_tags': ['foo']
+                  }
+                }
+              }))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test 1", () {}, tags: ['bar']);
+          test("test 2", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart', '--tags', 'foo']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    // Regression test for #503.
+    test('skips tests whose tags are marked as skip', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'tags': {
+                  'foo': {'skip': 'some reason'}
+                }
+              }))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test 1", () => throw 'bad', tags: ['foo']);
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stdout, containsInOrder(['some reason', 'All tests skipped.']));
+      await test.shouldExit(0);
+    });
+  });
+
+  group('include_tags and exclude_tags', () {
+    test('only runs tests with the included tags', () async {
+      await d
+          .file('dart_test.yaml', jsonEncode({'include_tags': 'foo && bar'}))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("zip", () {}, tags: "foo");
+          test("zap", () {}, tags: "bar");
+          test("zop", () {}, tags: ["foo", "bar"]);
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stdout, containsInOrder(['+0: zop', '+1: All tests passed!']));
+      await test.shouldExit(0);
+    });
+
+    test("doesn't run tests with the excluded tags", () async {
+      await d
+          .file('dart_test.yaml', jsonEncode({'exclude_tags': 'foo && bar'}))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("zip", () {}, tags: "foo");
+          test("zap", () {}, tags: "bar");
+          test("zop", () {}, tags: ["foo", "bar"]);
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdout,
+          containsInOrder(['+0: zip', '+1: zap', '+2: All tests passed!']));
+      await test.shouldExit(0);
+    });
+  });
+
+  group('errors', () {
+    group('tags', () {
+      test('rejects an invalid tag type', () async {
+        await d.file('dart_test.yaml', '{"tags": {12: null}}').create();
+
+        var test = await runTest([]);
+        expect(
+            test.stderr, containsInOrder(['tags key must be a string', '^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects an invalid tag selector', () async {
+        await d
+            .file(
+                'dart_test.yaml',
+                jsonEncode({
+                  'tags': {'foo bar': null}
+                }))
+            .create();
+
+        var test = await runTest([]);
+        expect(
+            test.stderr,
+            containsInOrder(
+                ['Invalid tags key: Expected end of input.', '^^^^^^^^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects an invalid tag map', () async {
+        await d.file('dart_test.yaml', jsonEncode({'tags': 12})).create();
+
+        var test = await runTest([]);
+        expect(test.stderr, containsInOrder(['tags must be a map', '^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects an invalid tag configuration', () async {
+        await d
+            .file(
+                'dart_test.yaml',
+                jsonEncode({
+                  'tags': {
+                    'foo': {'timeout': '12p'}
+                  }
+                }))
+            .create();
+
+        var test = await runTest([]);
+        expect(test.stderr,
+            containsInOrder(['Invalid timeout: expected unit', '^^^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects runner configuration', () async {
+        await d
+            .file(
+                'dart_test.yaml',
+                jsonEncode({
+                  'tags': {
+                    'foo': {'filename': '*_blorp.dart'}
+                  }
+                }))
+            .create();
+
+        var test = await runTest([]);
+        expect(test.stderr,
+            containsInOrder(["filename isn't supported here.", '^^^^^^^^^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+    });
+
+    group('add_tags', () {
+      test('rejects an invalid list type', () async {
+        await d
+            .file('dart_test.yaml', jsonEncode({'add_tags': 'foo'}))
+            .create();
+
+        var test = await runTest(['test.dart']);
+        expect(
+            test.stderr, containsInOrder(['add_tags must be a list', '^^^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects an invalid tag type', () async {
+        await d
+            .file(
+                'dart_test.yaml',
+                jsonEncode({
+                  'add_tags': [12]
+                }))
+            .create();
+
+        var test = await runTest(['test.dart']);
+        expect(
+            test.stderr, containsInOrder(['Tag name must be a string', '^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects an invalid tag name', () async {
+        await d
+            .file(
+                'dart_test.yaml',
+                jsonEncode({
+                  'add_tags': ['foo bar']
+                }))
+            .create();
+
+        var test = await runTest(['test.dart']);
+        expect(
+            test.stderr,
+            containsInOrder([
+              'Tag name must be an (optionally hyphenated) Dart identifier.',
+              '^^^^^^^^^'
+            ]));
+        await test.shouldExit(exit_codes.data);
+      });
+    });
+
+    group('include_tags', () {
+      test('rejects an invalid type', () async {
+        await d
+            .file('dart_test.yaml', jsonEncode({'include_tags': 12}))
+            .create();
+
+        var test = await runTest(['test.dart']);
+        expect(test.stderr,
+            containsInOrder(['include_tags must be a string', '^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects an invalid selector', () async {
+        await d
+            .file('dart_test.yaml', jsonEncode({'include_tags': 'foo bar'}))
+            .create();
+
+        var test = await runTest([]);
+        expect(
+            test.stderr,
+            containsInOrder(
+                ['Invalid include_tags: Expected end of input.', '^^^^^^^^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+    });
+
+    group('exclude_tags', () {
+      test('rejects an invalid type', () async {
+        await d
+            .file('dart_test.yaml', jsonEncode({'exclude_tags': 12}))
+            .create();
+
+        var test = await runTest(['test.dart']);
+        expect(test.stderr,
+            containsInOrder(['exclude_tags must be a string', '^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+
+      test('rejects an invalid selector', () async {
+        await d
+            .file('dart_test.yaml', jsonEncode({'exclude_tags': 'foo bar'}))
+            .create();
+
+        var test = await runTest([]);
+        expect(
+            test.stderr,
+            containsInOrder(
+                ['Invalid exclude_tags: Expected end of input.', '^^^^^^^^^']));
+        await test.shouldExit(exit_codes.data);
+      });
+    });
+  });
+}
diff --git a/pkgs/test/test/runner/configuration/top_level_error_test.dart b/pkgs/test/test/runner/configuration/top_level_error_test.dart
new file mode 100644
index 0000000..56a717c
--- /dev/null
+++ b/pkgs/test/test/runner/configuration/top_level_error_test.dart
@@ -0,0 +1,417 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:convert';
+
+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;
+
+import '../../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('rejects an invalid fold_stack_frames', () async {
+    await d
+        .file('dart_test.yaml', jsonEncode({'fold_stack_frames': 'flup'}))
+        .create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stderr,
+        containsInOrder(['fold_stack_frames must be a map', '^^^^^^']));
+    await test.shouldExit(exit_codes.data);
+  });
+
+  test('rejects multiple fold_stack_frames keys', () async {
+    await d
+        .file(
+            'dart_test.yaml',
+            jsonEncode({
+              'fold_stack_frames': {
+                'except': ['blah'],
+                'only': ['blah']
+              }
+            }))
+        .create();
+
+    var test = await runTest(['test.dart']);
+    expect(
+        test.stderr,
+        containsInOrder(
+            ['Can only contain one of "only" or "except".', '^^^^^^']));
+    await test.shouldExit(exit_codes.data);
+  });
+
+  test('rejects invalid fold_stack_frames keys', () async {
+    await d
+        .file(
+            'dart_test.yaml',
+            jsonEncode({
+              'fold_stack_frames': {'invalid': 'blah'}
+            }))
+        .create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stderr,
+        containsInOrder(['Must be "only" or "except".', '^^^^^^']));
+    await test.shouldExit(exit_codes.data);
+  });
+
+  test('rejects invalid fold_stack_frames values', () async {
+    await d
+        .file(
+            'dart_test.yaml',
+            jsonEncode({
+              'fold_stack_frames': {'only': 'blah'}
+            }))
+        .create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stderr,
+        containsInOrder(['Folded packages must be strings', '^^^^^^']));
+    await test.shouldExit(exit_codes.data);
+  });
+
+  test('rejects an invalid pause_after_load', () async {
+    await d
+        .file('dart_test.yaml', jsonEncode({'pause_after_load': 'flup'}))
+        .create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stderr,
+        containsInOrder(['pause_after_load must be a boolean', '^^^^^^']));
+    await test.shouldExit(exit_codes.data);
+  });
+
+  test('rejects an invalid verbose_trace', () async {
+    await d
+        .file('dart_test.yaml', jsonEncode({'verbose_trace': 'flup'}))
+        .create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stderr,
+        containsInOrder(['verbose_trace must be a boolean', '^^^^^^']));
+    await test.shouldExit(exit_codes.data);
+  });
+
+  test('rejects an invalid chain_stack_traces', () async {
+    await d
+        .file('dart_test.yaml', jsonEncode({'chain_stack_traces': 'flup'}))
+        .create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stderr,
+        containsInOrder(['chain_stack_traces must be a boolean', '^^^^^^']));
+    await test.shouldExit(exit_codes.data);
+  });
+
+  test('rejects an invalid retry', () async {
+    await d.file('dart_test.yaml', jsonEncode({'retry': 'flup'})).create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stderr,
+        containsInOrder(['retry must be a non-negative int', '^^^^^^']));
+    await test.shouldExit(exit_codes.data);
+  });
+
+  test('rejects an negative retry values', () async {
+    await d.file('dart_test.yaml', jsonEncode({'retry': -1})).create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stderr,
+        containsInOrder(['retry must be a non-negative int', '^^']));
+    await test.shouldExit(exit_codes.data);
+  });
+
+  test('rejects an invalid js_trace', () async {
+    await d.file('dart_test.yaml', jsonEncode({'js_trace': 'flup'})).create();
+
+    var test = await runTest(['test.dart']);
+    expect(
+        test.stderr, containsInOrder(['js_trace must be a boolean', '^^^^^^']));
+    await test.shouldExit(exit_codes.data);
+  });
+
+  group('reporter', () {
+    test('rejects an invalid type', () async {
+      await d.file('dart_test.yaml', jsonEncode({'reporter': 12})).create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stderr, containsInOrder(['reporter must be a string', '^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+
+    test('rejects an invalid name', () async {
+      await d
+          .file('dart_test.yaml', jsonEncode({'reporter': 'non-existent'}))
+          .create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stderr,
+          containsInOrder(
+              ['Unknown reporter "non-existent"', '^^^^^^^^^^^^^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+  });
+
+  group('file_reporters', () {
+    test('rejects an invalid type', () async {
+      await d
+          .file('dart_test.yaml', jsonEncode({'file_reporters': 12}))
+          .create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stderr, containsInOrder(['file_reporters must be a map', '^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+
+    test('rejects an invalid value type', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'file_reporters': {'json': 12}
+              }))
+          .create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stderr,
+          containsInOrder(['file_reporters value must be a string', '^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+
+    test('rejects an invalid name', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'file_reporters': {'non-existent': 'out'}
+              }))
+          .create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stderr,
+          containsInOrder(
+              ['Unknown reporter "non-existent"', '^^^^^^^^^^^^^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+  });
+
+  test('rejects an invalid concurrency', () async {
+    await d.file('dart_test.yaml', jsonEncode({'concurrency': 'foo'})).create();
+
+    var test = await runTest(['test.dart']);
+    expect(
+        test.stderr, containsInOrder(['concurrency must be an int', '^^^^^']));
+    await test.shouldExit(exit_codes.data);
+  });
+
+  group('timeout', () {
+    test('rejects an invalid type', () async {
+      await d.file('dart_test.yaml', jsonEncode({'timeout': 12})).create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stderr, containsInOrder(['timeout must be a string', '^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+
+    test('rejects an invalid format', () async {
+      await d.file('dart_test.yaml', jsonEncode({'timeout': '12p'})).create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stderr,
+          containsInOrder(['Invalid timeout: expected unit', '^^^^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+  });
+
+  group('names', () {
+    test('rejects an invalid list type', () async {
+      await d.file('dart_test.yaml', jsonEncode({'names': 'vm'})).create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stderr, containsInOrder(['names must be a list', '^^^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+
+    test('rejects an invalid member type', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'names': [12]
+              }))
+          .create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stderr, containsInOrder(['Names must be strings', '^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+
+    test('rejects an invalid RegExp', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'names': ['(foo']
+              }))
+          .create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stderr,
+          containsInOrder(['Invalid name: Unterminated group', '^^^^^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+  });
+
+  group('plain_names', () {
+    test('rejects an invalid list type', () async {
+      await d
+          .file('dart_test.yaml', jsonEncode({'plain_names': 'vm'}))
+          .create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stderr, containsInOrder(['plain_names must be a list', '^^^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+
+    test('rejects an invalid member type', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'plain_names': [12]
+              }))
+          .create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stderr, containsInOrder(['Names must be strings', '^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+  });
+
+  group('platforms', () {
+    test('rejects an invalid list type', () async {
+      await d.file('dart_test.yaml', jsonEncode({'platforms': 'vm'})).create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stderr, containsInOrder(['platforms must be a list', '^^^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+
+    test('rejects an invalid member type', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'platforms': [12]
+              }))
+          .create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stderr,
+          containsInOrder(['Platform name must be a string', '^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+
+    test('rejects an invalid member name', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'platforms': ['foo']
+              }))
+          .create();
+
+      await d.dir('test').create();
+
+      var test = await runTest([]);
+      expect(test.stderr, containsInOrder(['Unknown platform "foo"', '^^^^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+  });
+
+  group('paths', () {
+    test('rejects an invalid list type', () async {
+      await d.file('dart_test.yaml', jsonEncode({'paths': 'test'})).create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stderr, containsInOrder(['paths must be a list', '^^^^^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+
+    test('rejects an invalid member type', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'paths': [12]
+              }))
+          .create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stderr, containsInOrder(['Paths must be strings', '^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+
+    test('rejects an absolute path', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'paths': ['/foo']
+              }))
+          .create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stderr, containsInOrder(['Paths must be relative.', '^^^^^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+
+    test('rejects an invalid URI', () async {
+      await d
+          .file(
+              'dart_test.yaml',
+              jsonEncode({
+                'paths': [':invalid']
+              }))
+          .create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stderr,
+          containsInOrder(['Invalid path: Invalid empty scheme', '^^^^^^^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+  });
+
+  group('filename', () {
+    test('rejects an invalid type', () async {
+      await d.file('dart_test.yaml', jsonEncode({'filename': 12})).create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stderr, containsInOrder(['filename must be a string.', '^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+
+    test('rejects an invalid format', () async {
+      await d.file('dart_test.yaml', jsonEncode({'filename': '{foo'})).create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stderr,
+          containsInOrder(['Invalid filename: expected ",".', '^^^^^^']));
+      await test.shouldExit(exit_codes.data);
+    });
+  });
+}
diff --git a/pkgs/test/test/runner/configuration/top_level_test.dart b/pkgs/test/test/runner/configuration/top_level_test.dart
new file mode 100644
index 0000000..9c749a4
--- /dev/null
+++ b/pkgs/test/test/runner/configuration/top_level_test.dart
@@ -0,0 +1,578 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:test/test.dart';
+import 'package:test_core/src/util/io.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('ignores an empty file', () async {
+    await d.file('dart_test.yaml', '').create();
+
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("success", () {});
+      }
+    ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+    await test.shouldExit(0);
+  });
+
+  test('loads configuration from the path passed to --configuration', () async {
+    // Make sure dart_test.yaml is ignored.
+    await d.file('dart_test.yaml', jsonEncode({'run_skipped': true})).create();
+
+    await d.file('special_test.yaml', jsonEncode({'skip': true})).create();
+
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("test", () => throw "oh no");
+      }
+    ''').create();
+
+    var test =
+        await runTest(['--configuration', 'special_test.yaml', 'test.dart']);
+    expect(test.stdout, emitsThrough(contains('All tests skipped.')));
+    await test.shouldExit(0);
+  });
+
+  test('pauses the test runner after a suite loads with pause_after_load: true',
+      () async {
+    await d
+        .file('dart_test.yaml', jsonEncode({'pause_after_load': true}))
+        .create();
+
+    await d.file('test.dart', '''
+import 'package:test/test.dart';
+
+void main() {
+  print('loaded test!');
+
+  test("success", () {});
+}
+''').create();
+
+    var test = await runTest(['-p', 'chrome', 'test.dart']);
+    await expectLater(test.stdout, emitsThrough('loaded test!'));
+    await expectLater(
+        test.stdout,
+        emitsInOrder([
+          '',
+          equalsIgnoringWhitespace('''
+            The test runner is paused. Open the dev console in Chrome and set
+            breakpoints. Once you're finished, return to this terminal and press
+            Enter.
+          ''')
+        ]));
+
+    var nextLineFired = false;
+
+    unawaited(test.stdout.next.then(expectAsync1((line) {
+      expect(line, contains('+0: success'));
+      nextLineFired = true;
+    })));
+
+    // Wait a little bit to be sure that the tests don't start running without
+    // our input.
+    await Future<void>.delayed(const Duration(seconds: 2));
+    expect(nextLineFired, isFalse);
+
+    test.stdin.writeln();
+    await expectLater(
+        test.stdout, emitsThrough(contains('+1: All tests passed!')));
+    await test.shouldExit(0);
+  }, tags: 'chrome', onPlatform: const {
+    'windows': Skip('https://github.com/dart-lang/test/issues/1613')
+  });
+
+  test('runs skipped tests with run_skipped: true', () async {
+    await d.file('dart_test.yaml', jsonEncode({'run_skipped': true})).create();
+
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("skip", () => print("In test!"), skip: true);
+      }
+    ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, emitsThrough(contains('In test!')));
+    expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+    await test.shouldExit(0);
+  });
+
+  test('includes the full stack with verbose_trace: true', () async {
+    await d
+        .file('dart_test.yaml', jsonEncode({'verbose_trace': true}))
+        .create();
+
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("failure", () => throw "oh no");
+      }
+    ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, emitsThrough(contains('dart:async')));
+    await test.shouldExit(1);
+  });
+
+  test('disables stack trace chaining with chain_stack_traces: false',
+      () async {
+    await d
+        .file('dart_test.yaml', jsonEncode({'chain_stack_traces': false}))
+        .create();
+
+    await d.file('test.dart', '''
+         import 'dart:async';
+
+         import 'package:test/test.dart';
+
+          void main() {
+            test("failure", () async{
+              await Future((){});
+              await Future((){});
+              throw "oh no";
+            });
+          }
+    ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(
+        test.stdout,
+        containsInOrder([
+          '+0: failure',
+          '+0 -1: failure [E]',
+          'oh no',
+          'test.dart 9:15  main.<fn>',
+        ]));
+    await test.shouldExit(1);
+  });
+
+  test("doesn't dartify stack traces for JS-compiled tests with js_trace: true",
+      () async {
+    await d.file('dart_test.yaml', jsonEncode({'js_trace': true})).create();
+
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("failure", () => throw "oh no");
+      }
+    ''').create();
+
+    var test = await runTest(['-p', 'chrome', '--verbose-trace', 'test.dart']);
+    expect(test.stdoutStream(), neverEmits(endsWith(' main.<fn>')));
+    expect(test.stdoutStream(), neverEmits(contains('package:test')));
+    expect(test.stdoutStream(), neverEmits(contains('dart:async/zone.dart')));
+    expect(test.stdout, emitsThrough(contains('-1: Some tests failed.')));
+    await test.shouldExit(1);
+  }, tags: 'chrome');
+
+  test('retries tests with retry: 1', () async {
+    await d.file('dart_test.yaml', jsonEncode({'retry': 1})).create();
+
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+      import 'dart:async';
+
+      var attempt = 0;
+      void main() {
+        test("test", () {
+          attempt++;
+          if(attempt <= 1) {
+            throw 'Failure!';
+          }
+        });
+      }
+
+    ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, emitsThrough(contains('+1: All tests passed')));
+    await test.shouldExit(0);
+  });
+
+  test('skips tests with skip: true', () async {
+    await d.file('dart_test.yaml', jsonEncode({'skip': true})).create();
+
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("test", () {});
+      }
+    ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, emitsThrough(contains('All tests skipped.')));
+    await test.shouldExit(0);
+  });
+
+  test('skips tests with skip: reason', () async {
+    await d
+        .file('dart_test.yaml', jsonEncode({'skip': 'Tests are boring.'}))
+        .create();
+
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("test", () {});
+      }
+    ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, emitsThrough(contains('Tests are boring.')));
+    expect(test.stdout, emitsThrough(contains('All tests skipped.')));
+    await test.shouldExit(0);
+  });
+
+  group('test_on', () {
+    test('runs tests on a platform matching platform', () async {
+      await d.file('dart_test.yaml', jsonEncode({'test_on': 'vm'})).create();
+
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdout, emitsThrough(contains('All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('warns about the VM when no OSes are supported', () async {
+      await d
+          .file('dart_test.yaml', jsonEncode({'test_on': 'chrome'}))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stderr,
+          emits(
+              "Warning: this package doesn't support running tests on the Dart "
+              'VM.'));
+      expect(test.stdout, emitsThrough(contains('No tests ran.')));
+      await test.shouldExit(79);
+    });
+
+    test('warns about the OS when some OSes are supported', () async {
+      await d.file('dart_test.yaml', jsonEncode({'test_on': otherOS})).create();
+
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stderr,
+          emits("Warning: this package doesn't support running tests on "
+              '${currentOS.name}.'));
+      expect(test.stdout, emitsThrough(contains('No tests ran.')));
+      await test.shouldExit(79);
+    });
+
+    test('warns about browsers in general when no browsers are supported',
+        () async {
+      await d.file('dart_test.yaml', jsonEncode({'test_on': 'vm'})).create();
+
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['-p', 'chrome', 'test.dart']);
+      expect(
+          test.stderr,
+          emits(
+              "Warning: this package doesn't support running tests on browsers."));
+      expect(test.stdout, emitsThrough(contains('No tests ran.')));
+      await test.shouldExit(79);
+    });
+
+    test(
+        'warns about specific browsers when specific browsers are '
+        'supported', () async {
+      await d
+          .file('dart_test.yaml', jsonEncode({'test_on': 'safari'}))
+          .create();
+
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['-p', 'chrome,firefox', 'test.dart']);
+      expect(
+          test.stderr,
+          emits("Warning: this package doesn't support running tests on Chrome "
+              'or Firefox.'));
+      expect(test.stdout, emitsThrough(contains('No tests ran.')));
+      await test.shouldExit(79);
+    });
+  });
+
+  test('uses the specified reporter', () async {
+    await d.file('dart_test.yaml', jsonEncode({'reporter': 'json'})).create();
+
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("success", () {});
+      }
+    ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, emitsThrough(contains('"testStart"')));
+    await test.shouldExit(0);
+  });
+
+  test('uses the specified concurrency', () async {
+    await d.file('dart_test.yaml', jsonEncode({'concurrency': 2})).create();
+
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("success", () {});
+      }
+    ''').create();
+
+    // We can't reliably test the concurrency, but this at least ensures that
+    // it doesn't fail to parse.
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+    await test.shouldExit(0);
+  });
+
+  test('uses the specified timeout', () async {
+    await d.file('dart_test.yaml', jsonEncode({'timeout': '0s'})).create();
+
+    await d.file('test.dart', '''
+      import 'dart:async';
+
+      import 'package:test/test.dart';
+
+      void main() {
+        test("success", () => Future.delayed(Duration.zero));
+      }
+    ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(
+        test.stdout,
+        containsInOrder(
+            ['Test timed out after 0 seconds.', '-1: Some tests failed.']));
+    await test.shouldExit(1);
+  });
+
+  test('runs on the specified platforms', () async {
+    await d
+        .file(
+            'dart_test.yaml',
+            jsonEncode({
+              'platforms': ['vm', 'chrome']
+            }))
+        .create();
+
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("success", () {});
+      }
+    ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout,
+        containsInOrder(['[VM, Kernel] success', '[Chrome, Dart2Js] success']));
+    await test.shouldExit(0);
+  }, tags: 'chrome');
+
+  test('command line args take precedence', () async {
+    await d.file('dart_test.yaml', jsonEncode({'timeout': '0s'})).create();
+
+    await d.file('test.dart', '''
+      import 'dart:async';
+
+      import 'package:test/test.dart';
+
+      void main() {
+        test("success", () => Future.delayed(Duration.zero));
+      }
+    ''').create();
+
+    var test = await runTest(['--timeout=none', 'test.dart']);
+    expect(test.stdout, emitsThrough(contains('All tests passed!')));
+    await test.shouldExit(0);
+  });
+
+  test('uses the specified regexp names', () async {
+    await d
+        .file(
+            'dart_test.yaml',
+            jsonEncode({
+              'names': ['z[ia]p', 'a']
+            }))
+        .create();
+
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("zip", () {});
+        test("zap", () {});
+        test("zop", () {});
+      }
+    ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, containsInOrder(['+0: zap', '+1: All tests passed!']));
+    await test.shouldExit(0);
+  });
+
+  test('uses the specified plain names', () async {
+    await d
+        .file(
+            'dart_test.yaml',
+            jsonEncode({
+              'names': ['z', 'a']
+            }))
+        .create();
+
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("zip", () {});
+        test("zap", () {});
+        test("zop", () {});
+      }
+    ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, containsInOrder(['+0: zap', '+1: All tests passed!']));
+    await test.shouldExit(0);
+  });
+
+  test('uses the specified paths', () async {
+    await d
+        .file(
+            'dart_test.yaml',
+            jsonEncode({
+              'paths': ['zip', 'zap']
+            }))
+        .create();
+
+    await d.dir('zip', [
+      d.file('zip_test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("success", () {});
+        }
+      ''')
+    ]).create();
+
+    await d.dir('zap', [
+      d.file('zip_test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("success", () {});
+        }
+      ''')
+    ]).create();
+
+    await d.dir('zop', [
+      d.file('zip_test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("failure", () => throw "oh no");
+        }
+      ''')
+    ]).create();
+
+    var test = await runTest([]);
+    expect(test.stdout, emitsThrough(contains('All tests passed!')));
+    await test.shouldExit(0);
+  });
+
+  test('uses the specified filename', () async {
+    await d
+        .file('dart_test.yaml', jsonEncode({'filename': 'test_*.dart'}))
+        .create();
+
+    await d.dir('test', [
+      d.file('test_foo.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("success", () {});
+        }
+      '''),
+      d.file('foo_test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("failure", () => throw "oh no");
+        }
+      '''),
+      d.file('test_foo.bart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("failure", () => throw "oh no");
+        }
+      ''')
+    ]).create();
+
+    var test = await runTest([]);
+    expect(test.stdout, emitsThrough(contains('All tests passed!')));
+    await test.shouldExit(0);
+  });
+}
diff --git a/pkgs/test/test/runner/coverage_test.dart b/pkgs/test/test/runner/coverage_test.dart
new file mode 100644
index 0000000..b149345
--- /dev/null
+++ b/pkgs/test/test/runner/coverage_test.dart
@@ -0,0 +1,138 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+import 'package:test_process/test_process.dart';
+
+import '../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  group('with the --coverage flag,', () {
+    late Directory coverageDirectory;
+
+    Future<void> validateCoverage(TestProcess test, String coveragePath) async {
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+
+      final coverageFile = File(p.join(coverageDirectory.path, coveragePath));
+      final coverage = await coverageFile.readAsString();
+      final jsonCoverage = json.decode(coverage);
+      expect(jsonCoverage['coverage'], isNotEmpty);
+    }
+
+    setUp(() async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test 1", () {
+            expect(true, isTrue);
+          });
+        }
+      ''').create();
+
+      coverageDirectory =
+          await Directory.systemTemp.createTemp('test_coverage');
+    });
+
+    tearDown(() async {
+      await coverageDirectory.delete(recursive: true);
+    });
+
+    test('gathers coverage for VM tests', () async {
+      var test =
+          await runTest(['--coverage', coverageDirectory.path, 'test.dart']);
+      await validateCoverage(test, 'test.dart.vm.json');
+    });
+
+    test('gathers coverage for Chrome tests', () async {
+      var test = await runTest(
+          ['--coverage', coverageDirectory.path, 'test.dart', '-p', 'chrome']);
+      await validateCoverage(test, 'test.dart.chrome.json');
+    });
+
+    test(
+        'gathers coverage for Chrome tests when JS files contain unicode characters',
+        () async {
+      final sourceMapFileContent =
+          '{"version":3,"file":"","sources":[],"names":[],"mappings":""}';
+      final jsContent = '''
+        (function() {
+          '© '
+          window.foo = function foo() {
+            return 'foo';
+          };
+        })({
+
+          '© ': ''
+          });
+      ''';
+      await d.file('file_with_unicode.js', jsContent).create();
+      await d.file('file_with_unicode.js.map', sourceMapFileContent).create();
+
+      await d.file('js_with_unicode_test.dart', '''
+        import 'dart:async';
+
+        import 'package:js/js.dart';
+        import 'package:js/js_util.dart';
+
+        import 'package:test/src/runner/browser/dom.dart' as dom;
+        import 'package:test/test.dart';
+
+        Future<void> loadScript(String src) async {
+          final controller = StreamController();
+          final scriptLoaded = controller.stream.first;
+          final script = dom.createHTMLScriptElement()..src = src;
+          script.addEventListener('load',
+              allowInterop((_) {
+                controller.add('loaded');
+              }));
+          dom.document.body!.appendChild(script);
+          await scriptLoaded.timeout(Duration(seconds: 1));
+        }
+
+        void main() {
+          test("test 1", () async {
+            await loadScript('file_with_unicode.js');
+            expect(getProperty(dom.window, 'foo'), isNotNull);
+            callMethod(dom.window, 'foo', []);
+            expect(true, isTrue);
+          });
+        }
+      ''').create();
+
+      final jsBytes = utf8.encode(jsContent);
+      final jsLatin1 = latin1.decode(jsBytes);
+      final jsUtf8 = utf8.decode(jsBytes);
+      expect(jsLatin1, isNot(jsUtf8),
+          reason: 'test setup: should have decoded differently');
+
+      const functionPattern = 'function foo';
+      expect([jsLatin1, jsUtf8], everyElement(contains(functionPattern)));
+      expect(jsLatin1.indexOf(functionPattern),
+          isNot(jsUtf8.indexOf(functionPattern)),
+          reason:
+              'test setup: decoding should have shifted the position of the function');
+
+      var test = await runTest([
+        '--coverage',
+        coverageDirectory.path,
+        'js_with_unicode_test.dart',
+        '-p',
+        'chrome'
+      ]);
+      await validateCoverage(test, 'js_with_unicode_test.dart.chrome.json');
+    });
+  });
+}
diff --git a/pkgs/test/test/runner/engine_test.dart b/pkgs/test/test/runner/engine_test.dart
new file mode 100644
index 0000000..6b7df23
--- /dev/null
+++ b/pkgs/test/test/runner/engine_test.dart
@@ -0,0 +1,357 @@
+// 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.
+
+import 'dart:async';
+import 'dart:math';
+
+import 'package:test/test.dart';
+import 'package:test_api/src/backend/group.dart';
+import 'package:test_api/src/backend/state.dart';
+import 'package:test_core/src/runner/engine.dart';
+
+import '../utils.dart';
+
+void main() {
+  test('runs each test in each suite in order', () async {
+    var testsRun = 0;
+    var tests = declare(() {
+      for (var i = 0; i < 4; i++) {
+        test(
+            'test ${i + 1}',
+            expectAsync0(() {
+              expect(testsRun, equals(i));
+              testsRun++;
+            }, max: 1));
+      }
+    });
+
+    var engine = Engine.withSuites([
+      runnerSuite(Group.root(tests.take(2))),
+      runnerSuite(Group.root(tests.skip(2)))
+    ]);
+
+    await engine.run();
+    expect(testsRun, equals(4));
+  });
+
+  test('runs tests in a suite added after run() was called', () {
+    var testsRun = 0;
+    var tests = declare(() {
+      for (var i = 0; i < 4; i++) {
+        test(
+            'test ${i + 1}',
+            expectAsync0(() {
+              expect(testsRun, equals(i));
+              testsRun++;
+            }, max: 1));
+      }
+    });
+
+    var engine = Engine();
+    expect(
+        engine.run().then((_) {
+          expect(testsRun, equals(4));
+        }),
+        completes);
+
+    engine.suiteSink.add(runnerSuite(Group.root(tests)));
+    engine.suiteSink.close();
+  });
+
+  test('returns fail if any test does not complete', () async {
+    var completer = Completer<void>();
+    var engine = declareEngine(() {
+      test('completes', () {});
+      test('does not complete', () async {
+        await completer.future;
+      });
+    });
+    expect(engine.run(), completion(isFalse));
+    await pumpEventQueue();
+    unawaited(engine.close());
+    // We need to complete this so the outer test finishes.
+    completer.complete();
+  });
+
+  test(
+      'emits each test before it starts running and after the previous test '
+      'finished', () {
+    var testsRun = 0;
+    var engine = declareEngine(() {
+      for (var i = 0; i < 3; i++) {
+        test('test ${i + 1}', expectAsync0(() => testsRun++, max: 1));
+      }
+    });
+
+    engine.onTestStarted.listen(expectAsync1((liveTest) {
+      // [testsRun] should be one less than the test currently running.
+      expect(liveTest.test.name, equals('test ${testsRun + 1}'));
+
+      // [Engine.onTestStarted] is guaranteed to fire before the first
+      // [LiveTest.onStateChange].
+      expect(liveTest.onStateChange.first,
+          completion(equals(const State(Status.running, Result.success))));
+    }, count: 3, max: 3));
+
+    return engine.run();
+  });
+
+  test('.run() returns true if every test passes', () {
+    var engine = declareEngine(() {
+      for (var i = 0; i < 2; i++) {
+        test('test ${i + 1}', () {});
+      }
+    });
+
+    expect(engine.run(), completion(isTrue));
+  });
+
+  test('.run() returns false if any test fails', () {
+    var engine = declareEngine(() {
+      for (var i = 0; i < 2; i++) {
+        test('test ${i + 1}', () {});
+      }
+      test('failure', () => throw TestFailure('oh no'));
+    });
+
+    expect(engine.run(), completion(isFalse));
+  });
+
+  test('.run() returns false if any test errors', () {
+    var engine = declareEngine(() {
+      for (var i = 0; i < 2; i++) {
+        test('test ${i + 1}', () {});
+      }
+      test('failure', () => throw 'oh no');
+    });
+
+    expect(engine.run(), completion(isFalse));
+  });
+
+  test('.run() does not run more tests after failure for stopOnFirstFailure',
+      () async {
+    var secondTestRan = false;
+    var engine = declareEngine(() {
+      test('failure', () => throw 'oh no');
+      test('subsequent', () {
+        secondTestRan = true;
+      });
+    }, stopOnFirstFailure: true);
+    await expectLater(engine.run(), completion(isFalse));
+    expect(secondTestRan, false);
+  });
+
+  test('.run() may not be called more than once', () {
+    var engine = Engine.withSuites([]);
+    expect(engine.run(), completes);
+    expect(engine.run, throwsStateError);
+  });
+
+  test('runs tearDown after a test times out', () {
+    // Declare this here so the expect is in the context of this test, rather
+    // than the inner test.
+    var secondTestStarted = false;
+    var firstTestFinished = false;
+    var tearDownBody = expectAsync0(() {
+      expect(secondTestStarted, isFalse);
+      expect(firstTestFinished, isFalse);
+    });
+
+    var engine = declareEngine(() {
+      // This ensures that the first test doesn't actually finish until the
+      // second test runs.
+      var firstTestCompleter = Completer<void>();
+
+      group('group', () {
+        tearDown(tearDownBody);
+
+        test('first test', () async {
+          await firstTestCompleter.future;
+          firstTestFinished = true;
+        }, timeout: const Timeout(Duration.zero));
+      });
+
+      test('second test', () {
+        secondTestStarted = true;
+        firstTestCompleter.complete();
+      });
+    });
+
+    expect(engine.run(), completes);
+  });
+
+  group('for a skipped test', () {
+    test("doesn't run the test's body", () async {
+      var bodyRun = false;
+      var engine = declareEngine(() {
+        test('test', () => bodyRun = true, skip: true);
+      });
+
+      await engine.run();
+      expect(bodyRun, isFalse);
+    });
+
+    test("runs the test's body with --run-skipped", () async {
+      var bodyRun = false;
+      var engine = declareEngine(() {
+        test('test', () => bodyRun = true, skip: true);
+      }, runSkipped: true);
+
+      await engine.run();
+      expect(bodyRun, isTrue);
+    });
+
+    test('exposes a LiveTest that emits the correct states', () {
+      var tests = declare(() {
+        test('test', () {}, skip: true);
+      });
+
+      var engine = Engine.withSuites([runnerSuite(Group.root(tests))]);
+
+      engine.onTestStarted.listen(expectAsync1((liveTest) {
+        expect(liveTest, same(engine.liveTests.single));
+        expect(liveTest.test.name, equals(tests.single.name));
+
+        var i = 0;
+        liveTest.onStateChange.listen(expectAsync1((state) {
+          if (i == 0) {
+            expect(state, equals(const State(Status.running, Result.success)));
+          } else if (i == 1) {
+            expect(state, equals(const State(Status.running, Result.skipped)));
+          } else if (i == 2) {
+            expect(state, equals(const State(Status.complete, Result.skipped)));
+          }
+          i++;
+        }, count: 3));
+
+        expect(liveTest.onComplete, completes);
+      }));
+
+      return engine.run();
+    });
+  });
+
+  group('for a skipped group', () {
+    test("doesn't run a test in the group", () async {
+      var bodyRun = false;
+      var engine = declareEngine(() {
+        group('group', () {
+          test('test', () => bodyRun = true);
+        }, skip: true);
+      });
+
+      await engine.run();
+      expect(bodyRun, isFalse);
+    });
+
+    test('runs tests in the group with --run-skipped', () async {
+      var bodyRun = false;
+      var engine = declareEngine(() {
+        group('group', () {
+          test('test', () => bodyRun = true);
+        }, skip: true);
+      }, runSkipped: true);
+
+      await engine.run();
+      expect(bodyRun, isTrue);
+    });
+
+    test('runs tests in the group when they are skip: false', () async {
+      var bodyRun = false;
+      var engine = declareEngine(() {
+        group('group', () {
+          test('test', skip: false, () => bodyRun = true);
+        }, skip: true);
+      });
+
+      await engine.run();
+      expect(bodyRun, isTrue);
+    });
+
+    test('exposes a LiveTest that emits the correct states', () {
+      var entries = declare(() {
+        group('group', () {
+          test('test', () {});
+        }, skip: true);
+      });
+
+      var engine = Engine.withSuites([runnerSuite(Group.root(entries))]);
+
+      engine.onTestStarted.listen(expectAsync1((liveTest) {
+        expect(liveTest, same(engine.liveTests.single));
+        expect(liveTest.test.name, equals('group test'));
+
+        var i = 0;
+        liveTest.onStateChange.listen(expectAsync1((state) {
+          if (i == 0) {
+            expect(state, equals(const State(Status.running, Result.success)));
+          } else if (i == 1) {
+            expect(state, equals(const State(Status.running, Result.skipped)));
+          } else if (i == 2) {
+            expect(state, equals(const State(Status.complete, Result.skipped)));
+          }
+          i++;
+        }, count: 3));
+
+        expect(liveTest.onComplete, completes);
+      }));
+
+      return engine.run();
+    });
+  });
+
+  group('concurrency', () {
+    test('is shared between runner and load suites', () async {
+      for (var concurrency = 1; concurrency < 5; concurrency++) {
+        var testsLoaded = 0;
+        var maxLoadConcurrency = 0;
+        var testsRunning = 0;
+        var maxTestConcurrency = 0;
+        var testCount = concurrency * 2;
+
+        Future<void> updateAndCheckConcurrency(
+            {bool isLoadSuite = false}) async {
+          if (isLoadSuite) {
+            testsLoaded++;
+            maxLoadConcurrency = max(maxLoadConcurrency, testsLoaded);
+            expect(testsLoaded, lessThanOrEqualTo(concurrency));
+          } else {
+            testsRunning++;
+            maxTestConcurrency = max(maxTestConcurrency, testsRunning);
+            expect(testsRunning, lessThanOrEqualTo(concurrency));
+          }
+          // Simulate the test/loading taking some amount of time so that
+          // we actually reach max concurrency.
+          await Future<void>.delayed(const Duration(milliseconds: 100));
+          if (!isLoadSuite) {
+            testsRunning--;
+            testsLoaded--;
+          }
+        }
+
+        var tests = declare(() {
+          for (var i = 0; i < testCount; i++) {
+            test('test ${i + 1}', () async {
+              await updateAndCheckConcurrency();
+            });
+          }
+        });
+        var engine = Engine.withSuites([
+          for (var i = 0; i < testCount; i++)
+            loadSuite('group $i', () async {
+              await updateAndCheckConcurrency(isLoadSuite: true);
+              return runnerSuite(Group.root([tests[i]]));
+            }),
+        ], concurrency: concurrency);
+
+        await engine.run();
+        expect(engine.liveTests.length, testCount);
+
+        // We should reach but not exceed max concurrency
+        expect(maxTestConcurrency, concurrency);
+        expect(maxLoadConcurrency, concurrency);
+      }
+    });
+  });
+}
diff --git a/pkgs/test/test/runner/expanded_reporter_test.dart b/pkgs/test/test/runner/expanded_reporter_test.dart
new file mode 100644
index 0000000..1c11686
--- /dev/null
+++ b/pkgs/test/test/runner/expanded_reporter_test.dart
@@ -0,0 +1,386 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('reports when no tests are run', () async {
+    await d.file('test.dart', 'void main() {}').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, emitsThrough(contains('No tests ran.')));
+    await test.shouldExit(79);
+  });
+
+  test('runs several successful tests and reports when each completes', () {
+    return _expectReport('''
+        test('success 1', () {});
+        test('success 2', () {});
+        test('success 3', () {});''', '''
+        +0: loading test.dart
+        +0: success 1
+        +1: success 2
+        +2: success 3
+        +3: All tests passed!''');
+  });
+
+  test('runs several failing tests and reports when each fails', () {
+    return _expectReport('''
+        test('failure 1', () => throw TestFailure('oh no'));
+        test('failure 2', () => throw TestFailure('oh no'));
+        test('failure 3', () => throw TestFailure('oh no'));''', '''
+        +0: loading test.dart
+        +0: failure 1
+        +0 -1: failure 1 [E]
+          oh no
+          test.dart 6:33  main.<fn>
+
+        +0 -1: failure 2
+        +0 -2: failure 2 [E]
+          oh no
+          test.dart 7:33  main.<fn>
+
+        +0 -2: failure 3
+        +0 -3: failure 3 [E]
+          oh no
+          test.dart 8:33  main.<fn>
+
+        +0 -3: Some tests failed.''');
+  });
+
+  test('includes the full stack trace with --verbose-trace', () async {
+    await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("failure", () => throw "oh no");
+}
+''').create();
+
+    var test = await runTest(['--verbose-trace', 'test.dart']);
+    expect(test.stdout, emitsThrough(contains('dart:async')));
+    await test.shouldExit(1);
+  });
+
+  test('runs failing tests along with successful tests', () {
+    return _expectReport('''
+        test('failure 1', () => throw TestFailure('oh no'));
+        test('success 1', () {});
+        test('failure 2', () => throw TestFailure('oh no'));
+        test('success 2', () {});''', '''
+        +0: loading test.dart
+        +0: failure 1
+        +0 -1: failure 1 [E]
+          oh no
+          test.dart 6:33  main.<fn>
+
+        +0 -1: success 1
+        +1 -1: failure 2
+        +1 -2: failure 2 [E]
+          oh no
+          test.dart 8:33  main.<fn>
+
+        +1 -2: success 2
+        +2 -2: Some tests failed.''');
+  });
+
+  test('always prints the full test name', () {
+    return _expectReport('''
+        test(
+           'really gosh dang long test name. Even longer than that. No, yet '
+               'longer. A little more... okay, that should do it.',
+           () {});''', '''
+        +0: loading test.dart
+        +0: really gosh dang long test name. Even longer than that. No, yet longer. A little more... okay, that should do it.
+        +1: All tests passed!''');
+  });
+
+  test('gracefully handles multiple test failures in a row', () {
+    return _expectReport('''
+        // This completer ensures that the test isolate isn't killed until all
+        // errors have been thrown.
+        var completer = Completer();
+        test('failures', () {
+          Future.microtask(() => throw 'first error');
+          Future.microtask(() => throw 'second error');
+          Future.microtask(() => throw 'third error');
+          Future.microtask(completer.complete);
+        });
+        test('wait', () => completer.future);''', '''
+        +0: loading test.dart
+        +0: failures
+        +0 -1: failures [E]
+          first error
+          test.dart 10:34  main.<fn>.<fn>
+          ===== asynchronous gap ===========================
+          dart:async       new Future.microtask
+          test.dart 10:18  main.<fn>
+
+          second error
+          test.dart 11:34  main.<fn>.<fn>
+          ===== asynchronous gap ===========================
+          dart:async       new Future.microtask
+          test.dart 11:18  main.<fn>
+
+          third error
+          test.dart 12:34  main.<fn>.<fn>
+          ===== asynchronous gap ===========================
+          dart:async       new Future.microtask
+          test.dart 12:18  main.<fn>
+
+        +0 -1: wait
+        +1 -1: Some tests failed.''');
+  });
+
+  group('print:', () {
+    test('handles multiple prints', () {
+      return _expectReport('''
+        test('test', () {
+          print("one");
+          print("two");
+          print("three");
+          print("four");
+        });''', '''
+        +0: loading test.dart
+        +0: test
+        one
+        two
+        three
+        four
+        +1: All tests passed!''');
+    });
+
+    test('handles a print after the test completes', () {
+      return _expectReport('''
+        // This completer ensures that the test isolate isn't killed until all
+        // prints have happened.
+        var testDone = Completer();
+        var waitStarted = Completer();
+        test('test', () async {
+          waitStarted.future.then((_) {
+            Future(() => print("one"));
+            Future(() => print("two"));
+            Future(() => print("three"));
+            Future(() => print("four"));
+            Future(testDone.complete);
+          });
+        });
+
+        test('wait', () {
+          waitStarted.complete();
+          return testDone.future;
+        });''', '''
+        +0: loading test.dart
+        +0: test
+        +1: wait
+        +1: test
+        one
+        two
+        three
+        four
+        +2: All tests passed!''');
+    });
+
+    test('interleaves prints and errors', () {
+      return _expectReport('''
+        // This completer ensures that the test isolate isn't killed until all
+        // prints have happened.
+        var completer = Completer();
+        test('test', () {
+          scheduleMicrotask(() {
+            print("three");
+            print("four");
+            throw "second error";
+          });
+
+          scheduleMicrotask(() {
+            print("five");
+            print("six");
+            completer.complete();
+          });
+
+          print("one");
+          print("two");
+          throw "first error";
+        });
+
+        test('wait', () => completer.future);''', '''
+        +0: loading test.dart
+        +0: test
+        one
+        two
+        +0 -1: test [E]
+          first error
+          test.dart 24:11  main.<fn>
+
+        three
+        four
+          second error
+          test.dart 13:13  main.<fn>.<fn>
+          ===== asynchronous gap ===========================
+          dart:async       scheduleMicrotask
+          test.dart 10:11  main.<fn>
+
+        five
+        six
+        +0 -1: wait
+        +1 -1: Some tests failed.''');
+    });
+  });
+
+  group('skip:', () {
+    test('displays skipped tests separately', () {
+      return _expectReport('''
+          test('skip 1', () {}, skip: true);
+          test('skip 2', () {}, skip: true);
+          test('skip 3', () {}, skip: true);''', '''
+          +0: loading test.dart
+          +0: skip 1
+          +0 ~1: skip 2
+          +0 ~2: skip 3
+          +0 ~3: All tests skipped.''');
+    });
+
+    test('displays a skipped group', () {
+      return _expectReport('''
+          group('skip', () {
+            test('test 1', () {});
+            test('test 2', () {});
+            test('test 3', () {});
+          }, skip: true);''', '''
+          +0: loading test.dart
+          +0: skip test 1
+          +0 ~1: skip test 2
+          +0 ~2: skip test 3
+          +0 ~3: All tests skipped.''');
+    });
+
+    test('runs skipped tests along with successful tests', () {
+      return _expectReport('''
+          test('skip 1', () {}, skip: true);
+          test('success 1', () {});
+          test('skip 2', () {}, skip: true);
+          test('success 2', () {});''', '''
+          +0: loading test.dart
+          +0: skip 1
+          +0 ~1: success 1
+          +1 ~1: skip 2
+          +1 ~2: success 2
+          +2 ~2: All tests passed!''');
+    });
+
+    test('runs skipped tests along with successful and failing tests', () {
+      return _expectReport('''
+          test('failure 1', () => throw TestFailure('oh no'));
+          test('skip 1', () {}, skip: true);
+          test('success 1', () {});
+          test('failure 2', () => throw TestFailure('oh no'));
+          test('skip 2', () {}, skip: true);
+          test('success 2', () {});''', '''
+          +0: loading test.dart
+          +0: failure 1
+          +0 -1: failure 1 [E]
+            oh no
+            test.dart 6:35  main.<fn>
+
+          +0 -1: skip 1
+          +0 ~1 -1: success 1
+          +1 ~1 -1: failure 2
+          +1 ~1 -2: failure 2 [E]
+            oh no
+            test.dart 9:35  main.<fn>
+
+          +1 ~1 -2: skip 2
+          +1 ~2 -2: success 2
+          +2 ~2 -2: Some tests failed.''');
+    });
+
+    test('displays the skip reason if available', () {
+      return _expectReport('''
+          test('skip 1', () {}, skip: 'some reason');
+          test('skip 2', () {}, skip: 'or another');''', '''
+          +0: loading test.dart
+          +0: skip 1
+            Skip: some reason
+          +0 ~1: skip 2
+            Skip: or another
+          +0 ~2: All tests skipped.''');
+    });
+
+    test('runs skipped tests with --run-skipped', () {
+      return _expectReport('''
+          test('skip 1', () {}, skip: 'some reason');
+          test('skip 2', () {}, skip: 'or another');''', '''
+          +0: loading test.dart
+          +0: skip 1
+          +1: skip 2
+          +2: All tests passed!''', args: ['--run-skipped']);
+    });
+  });
+
+  test('Directs users to enable stack trace chaining if disabled', () async {
+    await _expectReport(
+        '''test('failure 1', () => throw TestFailure('oh no'));''', '''
+        +0: loading test.dart
+        +0: failure 1
+        +0 -1: failure 1 [E]
+          oh no
+          test.dart 6:25  main.<fn>
+
+        +0 -1: Some tests failed.
+
+        Consider enabling the flag chain-stack-traces to receive more detailed exceptions.
+        For example, 'dart test --chain-stack-traces'.''',
+        chainStackTraces: false);
+  });
+}
+
+Future<void> _expectReport(String tests, String expected,
+    {List<String> args = const [], bool chainStackTraces = true}) async {
+  await d.file('test.dart', '''
+    import 'dart:async';
+
+    import 'package:test/test.dart';
+
+    void main() {
+$tests
+    }
+  ''').create();
+
+  var test = await runTest([
+    'test.dart',
+    if (chainStackTraces) '--chain-stack-traces',
+    ...args,
+  ]);
+  await test.shouldExit();
+
+  var stdoutLines = await test.stdoutStream().toList();
+
+  // Remove excess trailing whitespace and trim off timestamps.
+  var actual = stdoutLines.map((line) {
+    if (line.startsWith('  ') || line.isEmpty) return line.trimRight();
+    return line.trim().replaceFirst(RegExp('^[0-9]{2}:[0-9]{2} '), '');
+  }).join('\n');
+
+  // Un-indent the expected string.
+  var indentation = expected.indexOf(RegExp('[^ ]'));
+  expected = expected.split('\n').map((line) {
+    if (line.isEmpty) return line;
+    return line.substring(indentation);
+  }).join('\n');
+
+  expect(actual, equals(expected));
+}
diff --git a/pkgs/test/test/runner/failures_only_reporter_test.dart b/pkgs/test/test/runner/failures_only_reporter_test.dart
new file mode 100644
index 0000000..3e2690a
--- /dev/null
+++ b/pkgs/test/test/runner/failures_only_reporter_test.dart
@@ -0,0 +1,257 @@
+// Copyright (c) 2024, 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('reports when no tests are run', () async {
+    await d.file('test.dart', 'void main() {}').create();
+
+    var test = await runTest(['test.dart'], reporter: 'failures-only');
+    expect(test.stdout, emitsThrough(contains('No tests ran.')));
+    await test.shouldExit(79);
+  });
+
+  test('runs several successful tests and reports only at the end', () {
+    return _expectReport('''
+        test('success 1', () {});
+        test('success 2', () {});
+        test('success 3', () {});''', '''
+        +3: All tests passed!''');
+  });
+
+  test('runs several failing tests and reports when each fails', () {
+    return _expectReport('''
+        test('failure 1', () => throw TestFailure('oh no'));
+        test('failure 2', () => throw TestFailure('oh no'));
+        test('failure 3', () => throw TestFailure('oh no'));''', '''
+        +0 -1: failure 1 [E]
+          oh no
+          test.dart 6:33  main.<fn>
+
+        +0 -2: failure 2 [E]
+          oh no
+          test.dart 7:33  main.<fn>
+
+        +0 -3: failure 3 [E]
+          oh no
+          test.dart 8:33  main.<fn>
+
+        +0 -3: Some tests failed.''');
+  });
+
+  test('includes the full stack trace with --verbose-trace', () async {
+    await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("failure", () => throw "oh no");
+}
+''').create();
+
+    var test = await runTest(['--verbose-trace', 'test.dart'],
+        reporter: 'failures-only');
+    expect(test.stdout, emitsThrough(contains('dart:async')));
+    await test.shouldExit(1);
+  });
+
+  test('reports only failing tests amid successful tests', () {
+    return _expectReport('''
+        test('failure 1', () => throw TestFailure('oh no'));
+        test('success 1', () {});
+        test('failure 2', () => throw TestFailure('oh no'));
+        test('success 2', () {});''', '''
+        +0 -1: failure 1 [E]
+          oh no
+          test.dart 6:33  main.<fn>
+
+        +1 -2: failure 2 [E]
+          oh no
+          test.dart 8:33  main.<fn>
+
+        +2 -2: Some tests failed.''');
+  });
+
+  group('print:', () {
+    test('handles multiple prints', () {
+      return _expectReport('''
+        test('test', () {
+          print("one");
+          print("two");
+          print("three");
+          print("four");
+        });''', '''
+        +0: test
+        one
+        two
+        three
+        four
+        +1: All tests passed!''');
+    });
+
+    test('handles a print after the test completes', () {
+      return _expectReport('''
+        // This completer ensures that the test isolate isn't killed until all
+        // prints have happened.
+        var testDone = Completer();
+        var waitStarted = Completer();
+        test('test', () async {
+          waitStarted.future.then((_) {
+            Future(() => print("one"));
+            Future(() => print("two"));
+            Future(() => print("three"));
+            Future(() => print("four"));
+            Future(testDone.complete);
+          });
+        });
+
+        test('wait', () {
+          waitStarted.complete();
+          return testDone.future;
+        });''', '''
+        +1: test
+        one
+        two
+        three
+        four
+        +2: All tests passed!''');
+    });
+
+    test('interleaves prints and errors', () {
+      return _expectReport('''
+        // This completer ensures that the test isolate isn't killed until all
+        // prints have happened.
+        var completer = Completer();
+        test('test', () {
+          scheduleMicrotask(() {
+            print("three");
+            print("four");
+            throw "second error";
+          });
+
+          scheduleMicrotask(() {
+            print("five");
+            print("six");
+            completer.complete();
+          });
+
+          print("one");
+          print("two");
+          throw "first error";
+        });
+
+        test('wait', () => completer.future);''', '''
+        +0: test
+        one
+        two
+        +0 -1: test [E]
+          first error
+          test.dart 24:11  main.<fn>
+
+        three
+        four
+          second error
+          test.dart 13:13  main.<fn>.<fn>
+          ===== asynchronous gap ===========================
+          dart:async       scheduleMicrotask
+          test.dart 10:11  main.<fn>
+
+        five
+        six
+        +1 -1: Some tests failed.''');
+    });
+  });
+
+  group('skip:', () {
+    test('does not emit for skips', () {
+      return _expectReport('''
+          test('skip 1', () {}, skip: true);
+          test('skip 2', () {}, skip: true);
+          test('skip 3', () {}, skip: true);''', '''
+          +0 ~3: All tests skipped.''');
+    });
+
+    test('runs skipped tests along with successful and failing tests', () {
+      return _expectReport('''
+          test('failure 1', () => throw TestFailure('oh no'));
+          test('skip 1', () {}, skip: true);
+          test('success 1', () {});
+          test('failure 2', () => throw TestFailure('oh no'));
+          test('skip 2', () {}, skip: true);
+          test('success 2', () {});''', '''
+          +0 -1: failure 1 [E]
+            oh no
+            test.dart 6:35  main.<fn>
+
+          +1 ~1 -2: failure 2 [E]
+            oh no
+            test.dart 9:35  main.<fn>
+
+          +2 ~2 -2: Some tests failed.''');
+    });
+  });
+
+  test('Directs users to enable stack trace chaining if disabled', () async {
+    await _expectReport(
+        '''test('failure 1', () => throw TestFailure('oh no'));''', '''
+        +0 -1: failure 1 [E]
+          oh no
+          test.dart 6:25  main.<fn>
+
+        +0 -1: Some tests failed.
+
+        Consider enabling the flag chain-stack-traces to receive more detailed exceptions.
+        For example, 'dart test --chain-stack-traces'.''',
+        chainStackTraces: false);
+  });
+}
+
+Future<void> _expectReport(String tests, String expected,
+    {List<String> args = const [], bool chainStackTraces = true}) async {
+  await d.file('test.dart', '''
+    import 'dart:async';
+
+    import 'package:test/test.dart';
+
+    void main() {
+$tests
+    }
+  ''').create();
+
+  var test = await runTest([
+    'test.dart',
+    if (chainStackTraces) '--chain-stack-traces',
+    ...args,
+  ], reporter: 'failures-only');
+  await test.shouldExit();
+
+  var stdoutLines = await test.stdoutStream().toList();
+
+  // Remove excess trailing whitespace.
+  var actual = stdoutLines.map((line) {
+    if (line.startsWith('  ') || line.isEmpty) return line.trimRight();
+    return line.trim();
+  }).join('\n');
+
+  // Un-indent the expected string.
+  var indentation = expected.indexOf(RegExp('[^ ]'));
+  expected = expected.split('\n').map((line) {
+    if (line.isEmpty) return line;
+    return line.substring(indentation);
+  }).join('\n');
+
+  expect(actual, equals(expected));
+}
diff --git a/pkgs/test/test/runner/github_reporter_test.dart b/pkgs/test/test/runner/github_reporter_test.dart
new file mode 100644
index 0000000..db50058
--- /dev/null
+++ b/pkgs/test/test/runner/github_reporter_test.dart
@@ -0,0 +1,377 @@
+// Copyright (c) 2022, 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('reports when no tests are run', () async {
+    await d.file('test.dart', 'void main() {}').create();
+
+    var test = await runTest(['test.dart'], reporter: 'github');
+    expect(test.stdout, emitsThrough(contains('0 tests passed')));
+    await test.shouldExit(79);
+  });
+
+  test('runs several successful tests and reports when each completes', () {
+    return _expectReport('''
+        test('success 1', () {});
+        test('success 2', () {});
+        test('success 3', () {});''', '''
+        ✅ success 1
+        ✅ success 2
+        ✅ success 3
+        🎉 3 tests passed.''');
+  });
+
+  test('includes the platform name when multiple platforms are run', () {
+    return _expectReportLines('''
+        test('success 1', () {});''', [
+      '✅ [VM, Kernel] success 1',
+      '✅ [Chrome, Dart2Js] success 1',
+      '🎉 2 tests passed.',
+    ], args: [
+      '-p',
+      'vm,chrome'
+    ]);
+  });
+
+  test('runs several failing tests and reports when each fails', () {
+    return _expectReport('''
+        test('failure 1', () => throw TestFailure('oh no'));
+        test('failure 2', () => throw TestFailure('oh no'));
+        test('failure 3', () => throw TestFailure('oh no'));''', '''
+        ::group::❌ failure 1 (failed)
+        oh no
+        test.dart 6:33  main.<fn>
+        ::endgroup::
+        ::group::❌ failure 2 (failed)
+        oh no
+        test.dart 7:33  main.<fn>
+        ::endgroup::
+        ::group::❌ failure 3 (failed)
+        oh no
+        test.dart 8:33  main.<fn>
+        ::endgroup::
+        ::error::0 tests passed, 3 failed.''');
+  });
+
+  test('includes the full stack trace with --verbose-trace', () async {
+    await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("failure", () => throw "oh no");
+}
+''').create();
+
+    var test =
+        await runTest(['--verbose-trace', 'test.dart'], reporter: 'github');
+    expect(test.stdout, emitsThrough(contains('dart:async')));
+    await test.shouldExit(1);
+  });
+
+  test('runs failing tests along with successful tests', () {
+    return _expectReport('''
+        test('failure 1', () => throw TestFailure('oh no'));
+        test('success 1', () {});
+        test('failure 2', () => throw TestFailure('oh no'));
+        test('success 2', () {});''', '''
+        ::group::❌ failure 1 (failed)
+        oh no
+        test.dart 6:33  main.<fn>
+        ::endgroup::
+        ✅ success 1
+        ::group::❌ failure 2 (failed)
+        oh no
+        test.dart 8:33  main.<fn>
+        ::endgroup::
+        ✅ success 2
+        ::error::2 tests passed, 2 failed.''');
+  });
+
+  test('always prints the full test name', () {
+    return _expectReport(
+      '''
+        test(
+           'really gosh dang long test name. Even longer than that. No, yet '
+               'longer. A little more... okay, that should do it.',
+           () {});''',
+      '✅ really gosh dang long test name. Even longer than that. No, yet longer. A little more... okay, that should do it.',
+      useContains: true,
+    );
+  });
+
+  test('gracefully handles multiple test failures in a row', () {
+    return _expectReport('''
+        // This completer ensures that the test isolate isn't killed until all
+        // errors have been thrown.
+        var completer = Completer();
+        test('failures', () {
+          Future.microtask(() => throw 'first error');
+          Future.microtask(() => throw 'second error');
+          Future.microtask(() => throw 'third error');
+          Future.microtask(completer.complete);
+        });
+        test('wait', () => completer.future);''', '''
+        ::group::❌ failures (failed)
+        first error
+        test.dart 10:34  main.<fn>.<fn>
+        second error
+        test.dart 11:34  main.<fn>.<fn>
+        third error
+        test.dart 12:34  main.<fn>.<fn>
+        ::endgroup::
+        ✅ wait
+        ::error::1 test passed, 1 failed.''');
+  });
+
+  test('displays failures occuring after a test completes', () {
+    return _expectReport(
+      '''
+      test('fail after completion', () {
+        Future(() {
+          Zone.current.handleUncaughtError('foo', StackTrace.current);
+        });
+      });
+
+      test('second test so that the first failure is reported', () {});''',
+      '''
+        ✅ fail after completion
+        ::group::❌ fail after completion (failed after test completion)
+        foo
+        test.dart 8:62  main.<fn>.<fn>
+        ::endgroup::
+        ::group::❌ fail after completion (failed after test completion)
+        This test failed after it had already completed.
+        Make sure to use a matching library which informs the test runner
+        of pending async work.
+        test.dart 8:62  main.<fn>.<fn>
+        ::endgroup::
+        ✅ second test so that the first failure is reported
+        ::error::1 test passed, 1 failed.''',
+    );
+  });
+
+  group('print:', () {
+    test('handles multiple prints', () {
+      return _expectReport(
+        '''
+        test('test', () {
+          print("one");
+          print("two");
+          print("three");
+          print("four");
+        });''',
+        '''
+        ::group::✅ test
+        one
+        two
+        three
+        four
+        ::endgroup::''',
+        useContains: true,
+      );
+    });
+
+    test('handles a print after the test completes', () {
+      return _expectReport('''
+        // This completer ensures that the test isolate isn't killed until all
+        // prints have happened.
+        var testDone = Completer();
+        var waitStarted = Completer();
+        test('test', () async {
+          waitStarted.future.then((_) {
+            Future(() => print("one"));
+            Future(() => print("two"));
+            Future(() => print("three"));
+            Future(() => print("four"));
+            Future(testDone.complete);
+          });
+        });
+
+        test('wait', () {
+          waitStarted.complete();
+          return testDone.future;
+        });''', '''
+        ✅ test
+        one
+        two
+        three
+        four
+        ✅ wait
+        🎉 2 tests passed.''');
+    });
+  });
+
+  group('skip:', () {
+    test('displays skipped tests', () {
+      return _expectReport('''
+          test('skip 1', () {}, skip: true);
+          test('skip 2', () {}, skip: true);
+          test('skip 3', () {}, skip: true);''', '''
+          ❎ skip 1 (skipped)
+          ❎ skip 2 (skipped)
+          ❎ skip 3 (skipped)
+          🎉 0 tests passed, 3 skipped.''');
+    });
+
+    test('displays a skipped group', () {
+      return _expectReport('''
+          group('skip', () {
+            test('test 1', () {});
+            test('test 2', () {});
+            test('test 3', () {});
+          }, skip: true);''', '''
+          ❎ skip test 1 (skipped)
+          ❎ skip test 2 (skipped)
+          ❎ skip test 3 (skipped)
+          🎉 0 tests passed, 3 skipped.''');
+    });
+
+    test('runs skipped tests along with successful tests', () {
+      return _expectReport('''
+          test('skip 1', () {}, skip: true);
+          test('success 1', () {});
+          test('skip 2', () {}, skip: true);
+          test('success 2', () {});''', '''
+          ❎ skip 1 (skipped)
+          ✅ success 1
+          ❎ skip 2 (skipped)
+          ✅ success 2
+          🎉 2 tests passed, 2 skipped.''');
+    });
+
+    test('runs skipped tests along with successful and failing tests', () {
+      return _expectReport('''
+          test('failure 1', () => throw TestFailure('oh no'));
+          test('skip 1', () {}, skip: true);
+          test('success 1', () {});
+          test('failure 2', () => throw TestFailure('oh no'));
+          test('skip 2', () {}, skip: true);
+          test('success 2', () {});''', '''
+          ::group::❌ failure 1 (failed)
+          oh no
+          test.dart 6:35  main.<fn>
+          ::endgroup::
+          ❎ skip 1 (skipped)
+          ✅ success 1
+          ::group::❌ failure 2 (failed)
+          oh no
+          test.dart 9:35  main.<fn>
+          ::endgroup::
+          ❎ skip 2 (skipped)
+          ✅ success 2
+          ::error::2 tests passed, 2 failed, 2 skipped.''');
+    });
+
+    test('displays the skip reason if available', () {
+      return _expectReport('''
+          test('skip 1', () {}, skip: 'some reason');
+          test('skip 2', () {}, skip: 'or another');''', '''
+          ::group::❎ skip 1 (skipped)
+          Skip: some reason
+          ::endgroup::
+          ::group::❎ skip 2 (skipped)
+          Skip: or another
+          ::endgroup::
+          🎉 0 tests passed, 2 skipped.''');
+    });
+  });
+
+  test('loadSuite, setUpAll, and tearDownAll hidden if no content', () {
+    return _expectReport('''
+          group('one', () {
+            setUpAll(() {/* nothing to do here */});
+            tearDownAll(() {/* nothing to do here */});
+            test('test 1', () {});
+          });''', '''
+          ✅ one test 1
+          🎉 1 test passed.''');
+  });
+
+  test('setUpAll and tearDownAll show when they have content', () {
+    return _expectReport('''
+          group('one', () {
+            setUpAll(() { print('one'); });
+            tearDownAll(() { print('two'); });
+            test('test 1', () {});
+          });''', '''
+          ::group::✅ one (setUpAll)
+          one
+          ::endgroup::
+          ✅ one test 1
+          ::group::✅ one (tearDownAll)
+          two
+          ::endgroup::
+          🎉 1 test passed.''');
+  });
+}
+
+/// Expects exactly [expected] to appear in the test output.
+///
+/// If [useContains] is passed, then the output only must contain [expected].
+Future<void> _expectReport(
+  String tests,
+  String expected, {
+  List<String> args = const [],
+  bool useContains = false,
+}) async {
+  expected = expected.split('\n').map(_unindent).join('\n');
+
+  var actual = (await _reportLines(tests, args)).join('\n');
+
+  expect(actual, useContains ? contains(expected) : equals(expected));
+}
+
+/// Expects all of [expected] lines to appear in the test output, but additional
+/// output is allowed.
+Future<void> _expectReportLines(
+  String tests,
+  List<String> expected, {
+  List<String> args = const [],
+}) async {
+  expected = [for (var line in expected) _unindent(line)];
+  var actual = await _reportLines(tests, args);
+  expect(actual, containsAllInOrder(expected));
+}
+
+/// All the output lines from running [tests].
+Future<List<String>> _reportLines(String tests, List<String> args) async {
+  await d.file('test.dart', '''
+    import 'dart:async';
+
+    import 'package:test/test.dart';
+
+    void main() {
+$tests
+    }
+  ''').create();
+
+  var test = await runTest([
+    'test.dart',
+    ...args,
+  ], reporter: 'github');
+  await test.shouldExit();
+
+  var stdoutLines = await test.stdoutStream().toList();
+  return stdoutLines
+      .map((line) => line.trim())
+      .where((line) => line.isNotEmpty)
+      .toList();
+}
+
+/// Removes all leading space from [line].
+String _unindent(String line) => line.trimLeft();
diff --git a/pkgs/test/test/runner/json_file_reporter_test.dart b/pkgs/test/test/runner/json_file_reporter_test.dart
new file mode 100644
index 0000000..ce3bd42
--- /dev/null
+++ b/pkgs/test/test/runner/json_file_reporter_test.dart
@@ -0,0 +1,155 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:async';
+import 'dart:io';
+
+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;
+
+import '../io.dart';
+import 'json_reporter_utils.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('runs successful tests with a stdout reporter and file reporter', () {
+    return _expectReports('''
+      test('success 1', () {});
+      test('success 2', () {});
+      test('success 3', () {});
+    ''', '''
+      +0: loading test.dart
+      +0: success 1
+      +1: success 2
+      +2: success 3
+      +3: All tests passed!''', [
+      [
+        suiteJson(0),
+        testStartJson(1, 'loading test.dart', groupIDs: []),
+        testDoneJson(1, hidden: true),
+      ],
+      [
+        groupJson(2, testCount: 3),
+        testStartJson(3, 'success 1', line: 6, column: 7),
+        testDoneJson(3),
+        testStartJson(4, 'success 2', line: 7, column: 7),
+        testDoneJson(4),
+        testStartJson(5, 'success 3', line: 8, column: 7),
+        testDoneJson(5),
+      ]
+    ], doneJson());
+  });
+
+  test('runs failing tests with a stdout reporter and file reporter', () {
+    return _expectReports('''
+      test('failure 1', () => throw new TestFailure('oh no'));
+      test('failure 2', () => throw new TestFailure('oh no'));
+      test('failure 3', () => throw new TestFailure('oh no'));
+    ''', '''
+      +0: loading test.dart
+      +0: failure 1
+      +0 -1: failure 1 [E]
+        oh no
+        test.dart 6:31  main.<fn>
+
+      +0 -1: failure 2
+      +0 -2: failure 2 [E]
+        oh no
+        test.dart 7:31  main.<fn>
+
+      +0 -2: failure 3
+      +0 -3: failure 3 [E]
+        oh no
+        test.dart 8:31  main.<fn>
+
+      +0 -3: Some tests failed.''', [
+      [
+        suiteJson(0),
+        testStartJson(1, 'loading test.dart', groupIDs: []),
+        testDoneJson(1, hidden: true),
+      ],
+      [
+        groupJson(2, testCount: 3),
+        testStartJson(3, 'failure 1', line: 6, column: 7),
+        errorJson(3, 'oh no', isFailure: true),
+        testDoneJson(3, result: 'failure'),
+        testStartJson(4, 'failure 2', line: 7, column: 7),
+        errorJson(4, 'oh no', isFailure: true),
+        testDoneJson(4, result: 'failure'),
+        testStartJson(5, 'failure 3', line: 8, column: 7),
+        errorJson(5, 'oh no', isFailure: true),
+        testDoneJson(5, result: 'failure'),
+      ]
+    ], doneJson(success: false));
+  });
+
+  group('reports an error if --file-reporter', () {
+    test('is not in the form <reporter>:<filepath>', () async {
+      var test = await runTest(['--file-reporter=json']);
+      expect(test.stderr,
+          emits(contains('option must be in the form <reporter>:<filepath>')));
+      await test.shouldExit(exit_codes.usage);
+    });
+
+    test('targets a non-existent reporter', () async {
+      var test = await runTest(['--file-reporter=nope:output.txt']);
+      expect(
+          test.stderr, emits(contains('"nope" is not a supported reporter')));
+      await test.shouldExit(exit_codes.usage);
+    });
+  });
+}
+
+Future<void> _expectReports(
+    String tests,
+    String stdoutExpected,
+    List<List<Object /*Map|Matcher*/ >> jsonFileExpected,
+    Map<Object, Object> jsonFileDone,
+    {List<String> args = const []}) async {
+  await d.file('test.dart', '''
+    import 'dart:async';
+
+    import 'package:test/test.dart';
+
+    void main() {
+$tests
+    }
+  ''').create();
+
+  var test = await runTest(['test.dart', '--chain-stack-traces', ...args],
+      // Write to a file within a dir that doesn't yet exist to verify that the
+      // file is created recursively.
+      fileReporter: 'json:reports/tests.json');
+  await test.shouldExit();
+
+  // ---- stdout reporter verification ----
+  var stdoutLines = await test.stdoutStream().toList();
+
+  // Remove excess trailing whitespace and trim off timestamps.
+  var actual = stdoutLines.map((line) {
+    if (line.startsWith('  ') || line.isEmpty) return line.trimRight();
+    return line.trim().replaceFirst(RegExp('^[0-9]{2}:[0-9]{2} '), '');
+  }).join('\n');
+
+  // Un-indent the expected string.
+  var indentation = stdoutExpected.indexOf(RegExp('[^ ]'));
+  stdoutExpected = stdoutExpected.split('\n').map((line) {
+    if (line.isEmpty) return line;
+    return line.substring(indentation);
+  }).join('\n');
+
+  expect(actual, equals(stdoutExpected));
+
+  // ---- file reporter verification ----
+  var fileOutputLines =
+      File(p.join(d.sandbox, 'reports', 'tests.json')).readAsLinesSync();
+  await expectJsonReport(
+      fileOutputLines, test.pid, jsonFileExpected, jsonFileDone);
+}
diff --git a/pkgs/test/test/runner/json_reporter_test.dart b/pkgs/test/test/runner/json_reporter_test.dart
new file mode 100644
index 0000000..a771438
--- /dev/null
+++ b/pkgs/test/test/runner/json_reporter_test.dart
@@ -0,0 +1,655 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:async';
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../io.dart';
+import 'json_reporter_utils.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('runs several successful tests and reports when each completes', () {
+    return _expectReport('''
+      test('success 1', () {});
+      test('success 2', () {});
+      test('success 3', () {});
+    ''', [
+      [
+        suiteJson(0),
+        testStartJson(1, 'loading test.dart', groupIDs: []),
+        testDoneJson(1, hidden: true),
+      ],
+      [
+        groupJson(2, testCount: 3),
+        testStartJson(3, 'success 1', line: 6, column: 7),
+        testDoneJson(3),
+        testStartJson(4, 'success 2', line: 7, column: 7),
+        testDoneJson(4),
+        testStartJson(5, 'success 3', line: 8, column: 7),
+        testDoneJson(5),
+      ]
+    ], doneJson());
+  });
+
+  test('runs several failing tests and reports when each fails', () {
+    return _expectReport('''
+      test('failure 1', () => throw TestFailure('oh no'));
+      test('failure 2', () => throw TestFailure('oh no'));
+      test('failure 3', () => throw TestFailure('oh no'));
+    ''', [
+      [
+        suiteJson(0),
+        testStartJson(1, 'loading test.dart', groupIDs: []),
+        testDoneJson(1, hidden: true),
+      ],
+      [
+        groupJson(2, testCount: 3),
+        testStartJson(3, 'failure 1', line: 6, column: 7),
+        errorJson(3, 'oh no', isFailure: true),
+        testDoneJson(3, result: 'failure'),
+        testStartJson(4, 'failure 2', line: 7, column: 7),
+        errorJson(4, 'oh no', isFailure: true),
+        testDoneJson(4, result: 'failure'),
+        testStartJson(5, 'failure 3', line: 8, column: 7),
+        errorJson(5, 'oh no', isFailure: true),
+        testDoneJson(5, result: 'failure'),
+      ]
+    ], doneJson(success: false));
+  });
+
+  test('includes the full stack trace with --verbose-trace', () async {
+    await d.file('test.dart', '''
+      import 'dart:async';
+
+      import 'package:test/test.dart';
+
+      void main() {
+        test("failure", () => throw "oh no");
+      }
+    ''').create();
+
+    var test =
+        await runTest(['--verbose-trace', 'test.dart'], reporter: 'json');
+    expect(test.stdout, emitsThrough(contains('dart:async')));
+    await test.shouldExit(1);
+  });
+
+  test('runs failing tests along with successful tests', () {
+    return _expectReport('''
+      test('failure 1', () => throw TestFailure('oh no'));
+      test('success 1', () {});
+      test('failure 2', () => throw TestFailure('oh no'));
+      test('success 2', () {});
+    ''', [
+      [
+        suiteJson(0),
+        testStartJson(1, 'loading test.dart', groupIDs: []),
+        testDoneJson(1, hidden: true),
+      ],
+      [
+        groupJson(2, testCount: 4),
+        testStartJson(3, 'failure 1', line: 6, column: 7),
+        errorJson(3, 'oh no', isFailure: true),
+        testDoneJson(3, result: 'failure'),
+        testStartJson(4, 'success 1', line: 7, column: 7),
+        testDoneJson(4),
+        testStartJson(5, 'failure 2', line: 8, column: 7),
+        errorJson(5, 'oh no', isFailure: true),
+        testDoneJson(5, result: 'failure'),
+        testStartJson(6, 'success 2', line: 9, column: 7),
+        testDoneJson(6),
+      ]
+    ], doneJson(success: false));
+  });
+
+  test('gracefully handles multiple test failures in a row', () {
+    return _expectReport('''
+      // This completer ensures that the test isolate isn't killed until all
+      // errors have been thrown.
+      var completer = Completer();
+      test('failures', () {
+        Future.microtask(() => throw 'first error');
+        Future.microtask(() => throw 'second error');
+        Future.microtask(() => throw 'third error');
+        Future.microtask(completer.complete);
+      });
+      test('wait', () => completer.future);
+    ''', [
+      [
+        suiteJson(0),
+        testStartJson(1, 'loading test.dart', groupIDs: []),
+        testDoneJson(1, hidden: true),
+      ],
+      [
+        groupJson(2, testCount: 2),
+        testStartJson(3, 'failures', line: 9, column: 7),
+        errorJson(3, 'first error'),
+        errorJson(3, 'second error'),
+        errorJson(3, 'third error'),
+        testDoneJson(3, result: 'error'),
+        testStartJson(4, 'wait', line: 15, column: 7),
+        testDoneJson(4),
+      ]
+    ], doneJson(success: false));
+  });
+
+  test('gracefully handles a test failing after completion', () {
+    return _expectReport('''
+      // These completers ensure that the first test won't fail until the second
+      // one is running, and that the test isolate isn't killed until all errors
+      // have been thrown.
+      var waitStarted = Completer();
+      var testDone = Completer();
+      test('failure', () {
+        waitStarted.future.then((_) {
+          Future.microtask(testDone.complete);
+          throw 'oh no';
+        });
+      });
+      test('wait', () {
+        waitStarted.complete();
+        return testDone.future;
+      });
+    ''', [
+      [
+        suiteJson(0),
+        testStartJson(1, 'loading test.dart', groupIDs: []),
+        testDoneJson(1, hidden: true),
+      ],
+      [
+        groupJson(2, testCount: 2),
+        testStartJson(3, 'failure', line: 11, column: 7),
+        testDoneJson(3),
+        testStartJson(4, 'wait', line: 17, column: 7),
+        errorJson(3, 'oh no'),
+        errorJson(
+            3,
+            'This test failed after it had already completed.\n'
+            'Make sure to use a matching library which informs the '
+            'test runner\nof pending async work.'),
+        testDoneJson(4),
+      ]
+    ], doneJson(success: false));
+  });
+
+  test('reports each test in its proper groups', () {
+    return _expectReport('''
+      group('group 1', () {
+        group('.2', () {
+          group('.3', () {
+            test('success', () {});
+          });
+        });
+
+        test('success1', () {});
+        test('success2', () {});
+      });
+    ''', [
+      [
+        suiteJson(0),
+        testStartJson(1, 'loading test.dart', groupIDs: []),
+        testDoneJson(1, hidden: true),
+      ],
+      [
+        groupJson(2, testCount: 3),
+        groupJson(3,
+            name: 'group 1', parentID: 2, testCount: 3, line: 6, column: 7),
+        groupJson(4, name: 'group 1 .2', parentID: 3, line: 7, column: 9),
+        groupJson(5, name: 'group 1 .2 .3', parentID: 4, line: 8, column: 11),
+        testStartJson(6, 'group 1 .2 .3 success',
+            groupIDs: [2, 3, 4, 5], line: 9, column: 13),
+        testDoneJson(6),
+        testStartJson(7, 'group 1 success1',
+            groupIDs: [2, 3], line: 13, column: 9),
+        testDoneJson(7),
+        testStartJson(8, 'group 1 success2',
+            groupIDs: [2, 3], line: 14, column: 9),
+        testDoneJson(8),
+      ]
+    ], doneJson());
+  });
+
+  group('print:', () {
+    test('handles multiple prints', () {
+      return _expectReport('''
+        test('test', () {
+          print("one");
+          print("two");
+          print("three");
+          print("four");
+        });
+      ''', [
+        [
+          suiteJson(0),
+          testStartJson(1, 'loading test.dart', groupIDs: []),
+          testDoneJson(1, hidden: true),
+        ],
+        [
+          groupJson(2),
+          testStartJson(3, 'test', line: 6, column: 9),
+          printJson(3, 'one'),
+          printJson(3, 'two'),
+          printJson(3, 'three'),
+          printJson(3, 'four'),
+          testDoneJson(3),
+        ]
+      ], doneJson());
+    });
+
+    test('handles a print after the test completes', () {
+      return _expectReport('''
+        // This completer ensures that the test isolate isn't killed until all
+        // prints have happened.
+        var testDone = Completer();
+        var waitStarted = Completer();
+        test('test', () async {
+          waitStarted.future.then((_) {
+            Future(() => print("one"));
+            Future(() => print("two"));
+            Future(() => print("three"));
+            Future(() => print("four"));
+            Future(testDone.complete);
+          });
+        });
+
+        test('wait', () {
+          waitStarted.complete();
+          return testDone.future;
+        });
+      ''', [
+        [
+          suiteJson(0),
+          testStartJson(1, 'loading test.dart', groupIDs: []),
+          testDoneJson(1, hidden: true),
+        ],
+        [
+          groupJson(2, testCount: 2),
+          testStartJson(3, 'test', line: 10, column: 9),
+          testDoneJson(3),
+          testStartJson(4, 'wait', line: 20, column: 9),
+          printJson(3, 'one'),
+          printJson(3, 'two'),
+          printJson(3, 'three'),
+          printJson(3, 'four'),
+          testDoneJson(4),
+        ]
+      ], doneJson());
+    });
+
+    test('interleaves prints and errors', () {
+      return _expectReport('''
+        // This completer ensures that the test isolate isn't killed until all
+        // prints have happened.
+        var completer = Completer();
+        test('test', () {
+          scheduleMicrotask(() {
+            print("three");
+            print("four");
+            throw "second error";
+          });
+
+          scheduleMicrotask(() {
+            print("five");
+            print("six");
+            completer.complete();
+          });
+
+          print("one");
+          print("two");
+          throw "first error";
+        });
+
+        test('wait', () => completer.future);
+      ''', [
+        [
+          suiteJson(0),
+          testStartJson(1, 'loading test.dart', groupIDs: []),
+          testDoneJson(1, hidden: true),
+        ],
+        [
+          groupJson(2, testCount: 2),
+          testStartJson(3, 'test', line: 9, column: 9),
+          printJson(3, 'one'),
+          printJson(3, 'two'),
+          errorJson(3, 'first error'),
+          printJson(3, 'three'),
+          printJson(3, 'four'),
+          errorJson(3, 'second error'),
+          printJson(3, 'five'),
+          printJson(3, 'six'),
+          testDoneJson(3, result: 'error'),
+          testStartJson(4, 'wait', line: 27, column: 9),
+          testDoneJson(4),
+        ]
+      ], doneJson(success: false));
+    });
+  });
+
+  group('skip:', () {
+    test('reports skipped tests', () {
+      return _expectReport('''
+        test('skip 1', () {}, skip: true);
+        test('skip 2', () {}, skip: true);
+        test('skip 3', () {}, skip: true);
+      ''', [
+        [
+          suiteJson(0),
+          testStartJson(1, 'loading test.dart', groupIDs: []),
+          testDoneJson(1, hidden: true),
+        ],
+        [
+          groupJson(2, testCount: 3),
+          testStartJson(3, 'skip 1', skip: true, line: 6, column: 9),
+          testDoneJson(3, skipped: true),
+          testStartJson(4, 'skip 2', skip: true, line: 7, column: 9),
+          testDoneJson(4, skipped: true),
+          testStartJson(5, 'skip 3', skip: true, line: 8, column: 9),
+          testDoneJson(5, skipped: true),
+        ]
+      ], doneJson());
+    });
+
+    test('reports skipped groups', () {
+      return _expectReport('''
+        group('skip', () {
+          test('success 1', () {});
+          test('success 2', () {});
+          test('success 3', () {});
+        }, skip: true);
+      ''', [
+        [
+          suiteJson(0),
+          testStartJson(1, 'loading test.dart', groupIDs: []),
+          testDoneJson(1, hidden: true),
+        ],
+        [
+          groupJson(2, testCount: 3),
+          groupJson(3,
+              name: 'skip',
+              parentID: 2,
+              skip: true,
+              testCount: 3,
+              line: 6,
+              column: 9),
+          testStartJson(4, 'skip success 1',
+              groupIDs: [2, 3], skip: true, line: 7, column: 11),
+          testDoneJson(4, skipped: true),
+          testStartJson(5, 'skip success 2',
+              groupIDs: [2, 3], skip: true, line: 8, column: 11),
+          testDoneJson(5, skipped: true),
+          testStartJson(6, 'skip success 3',
+              groupIDs: [2, 3], skip: true, line: 9, column: 11),
+          testDoneJson(6, skipped: true),
+        ]
+      ], doneJson());
+    });
+
+    test('reports the skip reason if available', () {
+      return _expectReport('''
+        test('skip 1', () {}, skip: 'some reason');
+        test('skip 2', () {}, skip: 'or another');
+      ''', [
+        [
+          suiteJson(0),
+          testStartJson(1, 'loading test.dart', groupIDs: []),
+          testDoneJson(1, hidden: true),
+        ],
+        [
+          groupJson(2, testCount: 2),
+          testStartJson(3, 'skip 1', skip: 'some reason', line: 6, column: 9),
+          printJson(3, 'Skip: some reason', type: 'skip'),
+          testDoneJson(3, skipped: true),
+          testStartJson(4, 'skip 2', skip: 'or another', line: 7, column: 9),
+          printJson(4, 'Skip: or another', type: 'skip'),
+          testDoneJson(4, skipped: true),
+        ]
+      ], doneJson());
+    });
+
+    test('runs skipped tests with --run-skipped', () {
+      return _expectReport(
+          '''
+        test('skip 1', () {}, skip: 'some reason');
+        test('skip 2', () {}, skip: 'or another');
+      ''',
+          [
+            [
+              suiteJson(0),
+              testStartJson(1, 'loading test.dart', groupIDs: []),
+              testDoneJson(1, hidden: true),
+            ],
+            [
+              groupJson(2, testCount: 2),
+              testStartJson(3, 'skip 1', line: 6, column: 9),
+              testDoneJson(3),
+              testStartJson(4, 'skip 2', line: 7, column: 9),
+              testDoneJson(4),
+            ]
+          ],
+          doneJson(),
+          args: ['--run-skipped']);
+    });
+  });
+
+  group('reports line and column numbers for', () {
+    test('the first call to setUpAll()', () {
+      return _expectReport('''
+        setUpAll(() {});
+        setUpAll(() {});
+        setUpAll(() {});
+        test('success', () {});
+      ''', [
+        [
+          suiteJson(0),
+          testStartJson(1, 'loading test.dart', groupIDs: []),
+          testDoneJson(1, hidden: true),
+        ],
+        [
+          groupJson(2, testCount: 1),
+          testStartJson(3, '(setUpAll)', line: 6, column: 9),
+          testDoneJson(3, hidden: true),
+          testStartJson(4, 'success', line: 9, column: 9),
+          testDoneJson(4),
+          testStartJson(5, '(tearDownAll)'),
+          testDoneJson(5, hidden: true),
+        ]
+      ], doneJson());
+    });
+
+    test('the first call to tearDownAll()', () {
+      return _expectReport('''
+        tearDownAll(() {});
+        tearDownAll(() {});
+        tearDownAll(() {});
+        test('success', () {});
+      ''', [
+        [
+          testStartJson(1, 'loading test.dart', groupIDs: []),
+          testDoneJson(1, hidden: true),
+        ],
+        [
+          suiteJson(0),
+          groupJson(2, testCount: 1),
+          testStartJson(3, 'success', line: 9, column: 9),
+          testDoneJson(3),
+          testStartJson(4, '(tearDownAll)', line: 6, column: 9),
+          testDoneJson(4, hidden: true),
+        ]
+      ], doneJson());
+    });
+
+    test('a test compiled to JS', () {
+      return _expectReport(
+          '''
+        test('success', () {});
+      ''',
+          [
+            [
+              suiteJson(0, platform: 'chrome'),
+              testStartJson(1, 'loading test.dart', groupIDs: []),
+              printJson(
+                  1,
+                  isA<String>().having((s) => s.split('\n'), 'lines',
+                      contains(startsWith('Compiled')))),
+              testDoneJson(1, hidden: true),
+            ],
+            [
+              groupJson(2, testCount: 1),
+              testStartJson(3, 'success', line: 6, column: 9),
+              testDoneJson(3),
+            ]
+          ],
+          doneJson(),
+          args: ['-p', 'chrome']);
+    }, tags: ['chrome'], skip: 'https://github.com/dart-lang/test/issues/872');
+
+    test('the root suite from a relative path', () {
+      return _expectReport(
+          '''
+      customTest('success 1', () {});
+      test('success 2', () {});
+    ''',
+          [
+            [
+              suiteJson(0),
+              testStartJson(1, 'loading test.dart', groupIDs: []),
+              testDoneJson(1, hidden: true),
+            ],
+            [
+              groupJson(2, testCount: 2),
+              testStartJson(3, 'success 1',
+                  line: 3,
+                  column: 60,
+                  url: p.toUri(p.join(d.sandbox, 'common.dart')).toString(),
+                  rootColumn: 7,
+                  rootLine: 7,
+                  rootUrl: p.toUri(p.join(d.sandbox, 'test.dart')).toString()),
+              testDoneJson(3),
+              testStartJson(4, 'success 2', line: 8, column: 7),
+              testDoneJson(4),
+            ]
+          ],
+          doneJson(),
+          externalLibraries: {
+            'common.dart': '''
+import 'package:test/test.dart';
+
+void customTest(String name, dynamic Function() testFn) => test(name, testFn);
+''',
+          });
+    });
+
+    test('the root suite from an absolute path', () {
+      final path = p.prettyUri(p.join(d.sandbox, 'test.dart'));
+      return _expectReport(
+          '''
+      customTest('success 1', () {});
+      test('success 2', () {});
+    ''',
+          useRelativePath: false,
+          [
+            [
+              suiteJson(0, path: equalsIgnoringCase(path)),
+              testStartJson(
+                  1, allOf(startsWith('loading '), endsWith('test.dart')),
+                  groupIDs: []),
+              testDoneJson(1, hidden: true),
+            ],
+            [
+              groupJson(2, testCount: 2),
+              testStartJson(3, 'success 1',
+                  line: 3,
+                  column: 60,
+                  url: p.toUri(p.join(d.sandbox, 'common.dart')).toString(),
+                  rootColumn: 7,
+                  rootLine: 7,
+                  rootUrl: p.toUri(p.join(d.sandbox, 'test.dart')).toString()),
+              testDoneJson(3),
+              testStartJson(4, 'success 2', line: 8, column: 7),
+              testDoneJson(4),
+            ]
+          ],
+          doneJson(),
+          externalLibraries: {
+            'common.dart': '''
+import 'package:test/test.dart';
+
+void customTest(String name, dynamic Function() testFn) => test(name, testFn);
+''',
+          });
+    });
+  });
+
+  test(
+      "doesn't report line and column information for a test compiled to JS "
+      'with --js-trace', () {
+    return _expectReport(
+        '''
+      test('success', () {});
+    ''',
+        [
+          [
+            suiteJson(0, platform: 'chrome'),
+            testStartJson(1, 'loading test.dart', groupIDs: []),
+            printJson(
+                1,
+                isA<String>().having((s) => s.split('\n'), 'lines',
+                    contains(startsWith('Compiled')))),
+            testDoneJson(1, hidden: true),
+          ],
+          [
+            groupJson(2, testCount: 1),
+            testStartJson(3, 'success'),
+            testDoneJson(3),
+          ],
+        ],
+        doneJson(),
+        args: ['-p', 'chrome', '--js-trace']);
+  }, tags: ['chrome']);
+}
+
+/// Asserts that the tests defined by [tests] produce the JSON events in
+/// [expected].
+///
+/// If [externalLibraries] are provided it should be a map of relative file
+/// paths to contents. All libraries will be added as imports to the test, and
+/// files will be created for them.
+Future<void> _expectReport(String tests,
+    List<List<Object /*Map|Matcher*/ >> expected, Map<Object, Object> done,
+    {List<String> args = const [],
+    bool useRelativePath = true,
+    Map<String, String> externalLibraries = const {}}) async {
+  var testContent = StringBuffer('''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+''');
+  for (var entry in externalLibraries.entries) {
+    testContent.writeln("import '${entry.key}';");
+    await d.file(entry.key, entry.value).create();
+  }
+  testContent
+    ..writeln('void main() {')
+    ..writeln(tests)
+    ..writeln('}');
+
+  await d.file('test.dart', testContent.toString()).create();
+  var testPath = useRelativePath ? 'test.dart' : p.join(d.sandbox, 'test.dart');
+
+  var test = await runTest([testPath, '--chain-stack-traces', ...args],
+      reporter: 'json');
+  await test.shouldExit();
+
+  var stdoutLines = await test.stdoutStream().toList();
+  return expectJsonReport(stdoutLines, test.pid, expected, done);
+}
diff --git a/pkgs/test/test/runner/json_reporter_utils.dart b/pkgs/test/test/runner/json_reporter_utils.dart
new file mode 100644
index 0000000..bbdbb78
--- /dev/null
+++ b/pkgs/test/test/runner/json_reporter_utils.dart
@@ -0,0 +1,209 @@
+// 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';
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:test_core/src/runner/version.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+/// Asserts that the outputs from running tests with a JSON reporter match the
+/// given expectations.
+///
+/// Verifies that [outputLines] matches each set of matchers in [expected],
+/// includes the [testPid] from the test process, and ends with [done].
+Future<void> expectJsonReport(
+    List<String> outputLines,
+    int testPid,
+    List<List<Object /*Map|Matcher*/ >> expected,
+    Map<Object, Object> done) async {
+  // Ensure the output is of the same length, including start, done and all
+  // suites messages.
+  expect(outputLines,
+      hasLength(expected.fold<int>(3, (int a, m) => a + m.length)));
+
+  final decoded = [
+    for (final line in outputLines)
+      (jsonDecode(line) as Map)
+        ..remove('time')
+        ..remove('stackTrace')
+  ];
+
+  // Should contain all suites message.
+  expect(decoded, containsAll([_allSuitesJson()]));
+
+  // A single start event is emitted first.
+  final start = {
+    'type': 'start',
+    'protocolVersion': '0.1.1',
+    'runnerVersion': testVersion,
+    'pid': testPid,
+  };
+  expect(decoded.first, equals(start));
+
+  // A single done event is emitted last.
+  expect(decoded.last, equals(done));
+
+  for (var value in expected) {
+    expect(decoded, containsAllInOrder(value));
+  }
+}
+
+/// Returns the event emitted by the JSON reporter providing information about
+/// all suites.
+///
+/// The [count] defaults to 1.
+Map<String, Object> _allSuitesJson({int count = 1}) =>
+    {'type': 'allSuites', 'count': count};
+
+/// Returns the event emitted by the JSON reporter indicating that a suite has
+/// begun running.
+///
+/// The [platform] defaults to `'vm'`.
+/// The [path] defaults to `equals('test.dart')`.
+Map<String, Object> suiteJson(int id,
+        {String platform = 'vm', Matcher? path}) =>
+    {
+      'type': 'suite',
+      'suite': {
+        'id': id,
+        'platform': platform,
+        'path': path ?? 'test.dart',
+      }
+    };
+
+/// Returns the event emitted by the JSON reporter indicating that a group has
+/// begun running.
+///
+/// If [skip] is `true`, the group is expected to be marked as skipped without a
+/// reason. If it's a [String], the group is expected to be marked as skipped
+/// with that reason.
+///
+/// The [testCount] parameter indicates the number of tests in the group. It
+/// defaults to 1.
+Map<String, Object> groupJson(int id,
+    {String? name,
+    int? suiteID,
+    int? parentID,
+    Object? skip,
+    int? testCount,
+    int? line,
+    int? column}) {
+  if ((line == null) != (column == null)) {
+    throw ArgumentError(
+        'line and column must either both be null or both be passed');
+  }
+
+  return {
+    'type': 'group',
+    'group': {
+      'id': id,
+      'name': name ?? '',
+      'suiteID': suiteID ?? 0,
+      'parentID': parentID,
+      'metadata': _metadataJson(skip: skip),
+      'testCount': testCount ?? 1,
+      'line': line,
+      'column': column,
+      'url': line == null
+          ? null
+          : p.toUri(p.join(d.sandbox, 'test.dart')).toString()
+    }
+  };
+}
+
+/// Returns the event emitted by the JSON reporter indicating that a test has
+/// begun running.
+///
+/// If [parentIDs] is passed, it's the IDs of groups containing this test. If
+/// [skip] is `true`, the test is expected to be marked as skipped without a
+/// reason. If it's a [String], the test is expected to be marked as skipped
+/// with that reason.
+Map<String, Object> testStartJson(int id, Object /*String|Matcher*/ name,
+    {int? suiteID,
+    Iterable<int>? groupIDs,
+    int? line,
+    int? column,
+    String? url,
+    Object? skip,
+    int? rootLine,
+    int? rootColumn,
+    String? rootUrl}) {
+  if ((line == null) != (column == null)) {
+    throw ArgumentError(
+        'line and column must either both be null or both be passed');
+  }
+
+  url ??=
+      line == null ? null : p.toUri(p.join(d.sandbox, 'test.dart')).toString();
+  return {
+    'type': 'testStart',
+    'test': {
+      'id': id,
+      'name': name,
+      'suiteID': suiteID ?? 0,
+      'groupIDs': groupIDs ?? [2],
+      'metadata': _metadataJson(skip: skip),
+      'line': line,
+      'column': column,
+      'url': url,
+      if (rootLine != null) 'root_line': rootLine,
+      if (rootColumn != null) 'root_column': rootColumn,
+      if (rootUrl != null) 'root_url': rootUrl,
+    }
+  };
+}
+
+/// Returns the event emitted by the JSON reporter indicating that a test
+/// printed [message].
+Matcher printJson(int id, dynamic /*String|Matcher*/ message,
+        {String type = 'print'}) =>
+    allOf(
+      hasLength(4),
+      containsPair('type', 'print'),
+      containsPair('testID', id),
+      containsPair('message', message),
+      containsPair('messageType', type),
+    );
+
+/// Returns the event emitted by the JSON reporter indicating that a test
+/// emitted [error].
+///
+/// The [isFailure] parameter indicates whether the error was a [TestFailure] or
+/// not.
+Map<String, Object> errorJson(int id, String error, {bool isFailure = false}) =>
+    {'type': 'error', 'testID': id, 'error': error, 'isFailure': isFailure};
+
+/// Returns the event emitted by the JSON reporter indicating that a test
+/// finished.
+///
+/// The [result] parameter indicates the result of the test. It defaults to
+/// `"success"`.
+///
+/// The [hidden] parameter indicates whether the test should not be displayed
+/// after finishing. The [skipped] parameter indicates whether the test was
+/// skipped.
+Map<String, Object> testDoneJson(int id,
+        {String result = 'success',
+        bool hidden = false,
+        bool skipped = false}) =>
+    {
+      'type': 'testDone',
+      'testID': id,
+      'result': result,
+      'hidden': hidden,
+      'skipped': skipped
+    };
+
+/// Returns the event emitted by the JSON reporter indicating that the entire
+/// run finished.
+Map<String, Object> doneJson({bool success = true}) =>
+    {'type': 'done', 'success': success};
+
+/// Returns the serialized metadata corresponding to [skip].
+Map<String, Object?> _metadataJson({Object? skip}) => {
+      'skip': skip == true || skip is String,
+      'skipReason': skip is String ? skip : null
+    };
diff --git a/pkgs/test/test/runner/line_and_col_test.dart b/pkgs/test/test/runner/line_and_col_test.dart
new file mode 100644
index 0000000..c513cef
--- /dev/null
+++ b/pkgs/test/test/runner/line_and_col_test.dart
@@ -0,0 +1,386 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('vm')
+library;
+
+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;
+
+import '../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  group('with test.dart?line=<line> query', () {
+    test('selects test with the matching line', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("a", () {});
+          test("b", () => throw TestFailure("oh no"));
+          test("c", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?line=6']);
+
+      expect(
+        test.stdout,
+        emitsThrough(contains('+1: All tests passed!')),
+      );
+
+      await test.shouldExit(0);
+    });
+
+    test('selects multiple tests on the same line', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("a", () {}); test("b", () {});
+          test("c", () => throw TestFailure("oh no"));
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?line=4']);
+
+      expect(
+        test.stdout,
+        emitsThrough(contains('+2: All tests passed!')),
+      );
+
+      await test.shouldExit(0);
+    });
+
+    test('selects groups with a matching line', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          group("a", () {
+            test("b", () {});
+          });
+          group("b", () {
+            test("b", () => throw TestFailure("oh no"));
+          });
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?line=4']);
+
+      expect(
+        test.stdout,
+        emitsThrough(contains('+1: All tests passed!')),
+      );
+
+      await test.shouldExit(0);
+    });
+
+    test('No matching tests', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("a", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?line=1']);
+
+      expect(
+        test.stderr,
+        emitsThrough(contains('No tests were found.')),
+      );
+
+      await test.shouldExit(exit_codes.noTestsRan);
+    });
+
+    test('allows the line anywhere in the stack trace', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void runTest(String name) {
+          test(name, () {});
+        }
+
+        void main() {
+          runTest("a");
+          test("b", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?line=8']);
+
+      expect(
+        test.stdout,
+        emitsThrough(contains('+1: All tests passed!')),
+      );
+
+      await test.shouldExit(0);
+    });
+  });
+
+  group('with test.dart?col=<col> query', () {
+    test('selects single test with the matching column', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("a", () {});
+            test("b", () => throw TestFailure("oh no"));
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?col=11']);
+
+      expect(
+        test.stdout,
+        emitsThrough(contains('+1: All tests passed!')),
+      );
+
+      await test.shouldExit(0);
+    });
+
+    test('selects multiple tests starting on the same column', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("a", () {});
+          test("b", () {});
+            test("c", () => throw TestFailure("oh no"));
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?col=11']);
+
+      expect(
+        test.stdout,
+        emitsThrough(contains('+2: All tests passed!')),
+      );
+
+      await test.shouldExit(0);
+    });
+
+    test('selects groups with a matching column', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          group("a", () {
+            test("b", () {});
+          });
+            group("b", () {
+              test("b", () => throw TestFailure("oh no"));
+            });
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?col=11']);
+
+      expect(
+        test.stdout,
+        emitsThrough(contains('+1: All tests passed!')),
+      );
+
+      await test.shouldExit(0);
+    });
+
+    test('No matching tests', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("a", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?col=1']);
+
+      expect(
+        test.stderr,
+        emitsThrough(contains('No tests were found.')),
+      );
+
+      await test.shouldExit(exit_codes.noTestsRan);
+    });
+
+    test('allows the col anywhere in the stack trace', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void runTest(String name) {
+          test(name, () {});
+        }
+
+        void main() {
+            runTest("a");
+          test("b", () => throw TestFailure("oh no"));
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?col=13']);
+
+      expect(
+        test.stdout,
+        emitsThrough(contains('+1: All tests passed!')),
+      );
+
+      await test.shouldExit(0);
+    });
+  });
+
+  group('with test.dart?line=<line>&col=<col> query', () {
+    test('selects test with the matching line and col in the same frame',
+        () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          void runTests() {
+          test("a", () {});test("b", () => throw TestFailure("oh no"));
+          }
+          runTests();
+          test("c", () => throw TestFailure("oh no"));
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?line=5&col=11']);
+
+      expect(
+        test.stdout,
+        emitsThrough(contains('+1: All tests passed!')),
+      );
+
+      await test.shouldExit(0);
+    });
+
+    test('selects group with the matching line and col', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          group("a", () {
+            test("b", () {});
+            test("c", () {});
+          });
+          group("d", () {
+            test("e", () => throw TestFailure("oh no"));
+          });
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?line=4&col=11']);
+
+      expect(
+        test.stdout,
+        emitsThrough(contains('+2: All tests passed!')),
+      );
+
+      await test.shouldExit(0);
+    });
+
+    test('no matching tests - col doesnt match', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("a", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?line=4&col=1']);
+
+      expect(
+        test.stderr,
+        emitsThrough(contains('No tests were found.')),
+      );
+
+      await test.shouldExit(exit_codes.noTestsRan);
+    });
+
+    test('no matching tests - line doesnt match', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("a", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?line=1&col=11']);
+
+      expect(
+        test.stderr,
+        emitsThrough(contains('No tests were found.')),
+      );
+
+      await test.shouldExit(exit_codes.noTestsRan);
+    });
+
+    test('supports browser tests', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("a", () {});
+          test("b", () => throw TestFailure("oh no"));
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?line=4&col=11', '-p', 'chrome']);
+
+      expect(
+        test.stdout,
+        emitsThrough(contains('+1: All tests passed!')),
+      );
+
+      await test.shouldExit(0);
+    });
+
+    test('supports node tests', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("a", () {});
+          test("b", () => throw TestFailure("oh no"));
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?line=4&col=11', '-p', 'node']);
+
+      expect(
+        test.stdout,
+        emitsThrough(contains('+1: All tests passed!')),
+      );
+
+      await test.shouldExit(0);
+    }, retry: 3);
+  });
+
+  test('bundles runs by suite, deduplicates tests that match multiple times',
+      () async {
+    await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test('a', () {});
+          test('b', () => throw TestFailure('oh no'));
+        }
+      ''').create();
+
+    var test = await runTest(['test.dart?line=4', 'test.dart?full-name=a']);
+
+    expect(
+      test.stdout,
+      emitsThrough(contains('+1: All tests passed!')),
+    );
+
+    await test.shouldExit(0);
+  });
+}
diff --git a/pkgs/test/test/runner/load_suite_test.dart b/pkgs/test/test/runner/load_suite_test.dart
new file mode 100644
index 0000000..ab5fc79
--- /dev/null
+++ b/pkgs/test/test/runner/load_suite_test.dart
@@ -0,0 +1,178 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:test_api/src/backend/group.dart';
+import 'package:test_api/src/backend/runtime.dart';
+import 'package:test_api/src/backend/state.dart';
+import 'package:test_api/src/backend/test.dart';
+import 'package:test_core/src/runner/load_exception.dart';
+import 'package:test_core/src/runner/load_suite.dart';
+import 'package:test_core/src/runner/runner_suite.dart';
+import 'package:test_core/src/runner/suite.dart';
+
+import '../utils.dart';
+
+void main() {
+  late RunnerSuite innerSuite;
+  setUp(() {
+    innerSuite = runnerSuite(Group.root([]));
+  });
+
+  test('running a load test causes LoadSuite.suite to emit a suite', () async {
+    var suite = LoadSuite('name', SuiteConfiguration.empty, suitePlatform,
+        () => Future.value(innerSuite));
+    expect(suite.group.entries, hasLength(1));
+
+    expect(suite.suite, completion(equals(innerSuite)));
+    var liveTest = (suite.group.entries.single as Test).load(suite);
+    await liveTest.run();
+    expectTestPassed(liveTest);
+  });
+
+  test("running a load suite's body may be synchronous", () async {
+    var suite = LoadSuite(
+        'name', SuiteConfiguration.empty, suitePlatform, () => innerSuite);
+    expect(suite.group.entries, hasLength(1));
+
+    expect(suite.suite, completion(equals(innerSuite)));
+    var liveTest = (suite.group.entries.single as Test).load(suite);
+    await liveTest.run();
+    expectTestPassed(liveTest);
+  });
+
+  test("a load test doesn't complete until the body returns", () async {
+    var completer = Completer<RunnerSuite>();
+    var suite = LoadSuite('name', SuiteConfiguration.empty, suitePlatform,
+        () => completer.future);
+    expect(suite.group.entries, hasLength(1));
+
+    var liveTest = (suite.group.entries.single as Test).load(suite);
+    expect(liveTest.run(), completes);
+    await Future<void>.delayed(Duration.zero);
+    expect(liveTest.state.status, equals(Status.running));
+
+    completer.complete(innerSuite);
+    await Future<void>.delayed(Duration.zero);
+    expectTestPassed(liveTest);
+  });
+
+  test('a load test forwards errors and completes LoadSuite.suite to null',
+      () async {
+    var suite = LoadSuite('name', SuiteConfiguration.empty, suitePlatform, () {
+      return fail('error');
+    });
+    expect(suite.group.entries, hasLength(1));
+
+    expect(suite.suite, completion(isNull));
+
+    var liveTest = (suite.group.entries.single as Test).load(suite);
+    await liveTest.run();
+    expectTestFailed(liveTest, 'error');
+  });
+
+  test("a load test completes early if it's closed", () async {
+    var suite = LoadSuite('name', SuiteConfiguration.empty, suitePlatform,
+        () => Completer<RunnerSuite>().future);
+    expect(suite.group.entries, hasLength(1));
+
+    var liveTest = (suite.group.entries.single as Test).load(suite);
+    expect(liveTest.run(), completes);
+    await Future<void>.delayed(Duration.zero);
+    expect(liveTest.state.status, equals(Status.running));
+
+    expect(liveTest.close(), completes);
+  });
+
+  test('forLoadException() creates a suite that completes to a LoadException',
+      () async {
+    var exception = LoadException('path', 'error');
+    var suite = LoadSuite.forLoadException(exception, SuiteConfiguration.empty);
+    expect(suite.group.entries, hasLength(1));
+
+    expect(suite.suite, completion(isNull));
+
+    var liveTest = (suite.group.entries.single as Test).load(suite);
+    await liveTest.run();
+    expect(liveTest.state.status, equals(Status.complete));
+    expect(liveTest.state.result, equals(Result.error));
+    expect(liveTest.errors, hasLength(1));
+    expect(liveTest.errors.first.error, equals(exception));
+  });
+
+  test('forSuite() creates a load suite that completes to a test suite',
+      () async {
+    var suite = LoadSuite.forSuite(innerSuite);
+    expect(suite.group.entries, hasLength(1));
+
+    expect(suite.suite, completion(equals(innerSuite)));
+    var liveTest = (suite.group.entries.single as Test).load(suite);
+    await liveTest.run();
+    expectTestPassed(liveTest);
+  });
+
+  group('changeSuite()', () {
+    test('returns a new load suite with the same properties', () {
+      var suite = LoadSuite(
+          'name', SuiteConfiguration.empty, suitePlatform, () => innerSuite);
+      expect(suite.group.entries, hasLength(1));
+
+      var newSuite = suite.changeSuite((suite) => suite);
+      expect(newSuite.platform.runtime, equals(Runtime.vm));
+      expect(newSuite.group.entries.single.name,
+          equals(suite.group.entries.single.name));
+    });
+
+    test('changes the inner suite', () async {
+      var suite = LoadSuite(
+          'name', SuiteConfiguration.empty, suitePlatform, () => innerSuite);
+      expect(suite.group.entries, hasLength(1));
+
+      var newInnerSuite = runnerSuite(Group.root([]));
+      var newSuite = suite.changeSuite((suite) => newInnerSuite);
+      expect(newSuite.suite, completion(equals(newInnerSuite)));
+
+      var liveTest = (suite.group.entries.single as Test).load(suite);
+      await liveTest.run();
+      expectTestPassed(liveTest);
+    });
+
+    test("doesn't run change() if the suite is null", () async {
+      var suite = LoadSuite(
+          'name', SuiteConfiguration.empty, suitePlatform, () => null);
+      expect(suite.group.entries, hasLength(1));
+
+      var newSuite = suite.changeSuite(expectAsync1((_) {
+        return null;
+      }, count: 0));
+      expect(newSuite.suite, completion(isNull));
+
+      var liveTest = (suite.group.entries.single as Test).load(suite);
+      await liveTest.run();
+      expectTestPassed(liveTest);
+    });
+  });
+
+  group('getSuite()', () {
+    test('runs the test and returns the suite', () {
+      var suite = LoadSuite.forSuite(innerSuite);
+      expect(suite.group.entries, hasLength(1));
+
+      expect(suite.getSuite(), completion(equals(innerSuite)));
+    });
+
+    test('forwards errors to the future', () {
+      var suite = LoadSuite(
+          'name', SuiteConfiguration.empty, suitePlatform, () => throw 'error');
+      expect(suite.group.entries, hasLength(1));
+
+      expect(suite.getSuite(), throwsA('error'));
+    });
+  });
+}
diff --git a/pkgs/test/test/runner/loader_test.dart b/pkgs/test/test/runner/loader_test.dart
new file mode 100644
index 0000000..8d1d4ad
--- /dev/null
+++ b/pkgs/test/test/runner/loader_test.dart
@@ -0,0 +1,293 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:async';
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:test_api/src/backend/compiler.dart';
+import 'package:test_api/src/backend/runtime.dart';
+import 'package:test_api/src/backend/state.dart';
+import 'package:test_api/src/backend/test.dart';
+import 'package:test_core/src/runner/compiler_selection.dart';
+import 'package:test_core/src/runner/load_suite.dart';
+import 'package:test_core/src/runner/loader.dart';
+import 'package:test_core/src/runner/runner_suite.dart';
+import 'package:test_core/src/runner/runner_test.dart';
+import 'package:test_core/src/runner/suite.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../utils.dart';
+
+late Loader _loader;
+
+final _tests = '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {});
+  test("failure", () => throw TestFailure('oh no'));
+  test("error", () => throw 'oh no');
+}
+''';
+
+void main() {
+  setUp(() async {
+    _loader = Loader();
+  });
+
+  tearDown(() => _loader.close());
+
+  group('.loadFile()', () {
+    late RunnerSuite suite;
+    group('with empty configuration', () {
+      setUp(() async {
+        await d.file('a_test.dart', _tests).create();
+        var suites = await _loader
+            .loadFile(
+                p.join(d.sandbox, 'a_test.dart'), SuiteConfiguration.empty)
+            .toList();
+        expect(suites, hasLength(1));
+        var loadSuite = suites.first;
+        suite = (await loadSuite.getSuite())!;
+      });
+
+      test('returns a suite with the file path and platform', () {
+        expect(suite.path, equals(p.join(d.sandbox, 'a_test.dart')));
+        expect(suite.platform.runtime, equals(Runtime.vm));
+        expect(suite.platform.compiler, equals(Runtime.vm.defaultCompiler));
+      });
+
+      test('returns entries with the correct names and platforms', () {
+        expect(suite.group.entries, hasLength(3));
+        expect(suite.group.entries[0].name, equals('success'));
+        expect(suite.group.entries[1].name, equals('failure'));
+        expect(suite.group.entries[2].name, equals('error'));
+      });
+
+      test('can load and run a successful test', () {
+        var liveTest = (suite.group.entries[0] as RunnerTest).load(suite);
+
+        expectStates(liveTest, [
+          const State(Status.running, Result.success),
+          const State(Status.complete, Result.success)
+        ]);
+        expectErrors(liveTest, []);
+
+        return liveTest.run().whenComplete(() => liveTest.close());
+      });
+
+      test('can load and run a failing test', () {
+        var liveTest = (suite.group.entries[1] as RunnerTest).load(suite);
+        expectSingleFailure(liveTest);
+        return liveTest.run().whenComplete(() => liveTest.close());
+      });
+    });
+
+    group('with compiler selection', () {
+      Future<List<LoadSuite>> loadSuitesWithConfig(
+          SuiteConfiguration suiteConfiguration) async {
+        await d.file('a_test.dart', _tests).create();
+        return _loader
+            .loadFile(p.join(d.sandbox, 'a_test.dart'), suiteConfiguration)
+            .toList();
+      }
+
+      test('with a single compiler selection, uses the selected compiler',
+          () async {
+        var suites = await loadSuitesWithConfig(suiteConfiguration(
+            compilerSelections: [CompilerSelection.parse('source')]));
+        expect(suites, hasLength(1));
+        var loadSuite = suites.first;
+        suite = (await loadSuite.getSuite())!;
+        expect(suite.path, equals(p.join(d.sandbox, 'a_test.dart')));
+        expect(suite.platform.runtime, equals(Runtime.vm));
+        expect(suite.platform.compiler, equals(Compiler.source));
+      });
+
+      test('with multiple compiler selections, returns a suite for each',
+          () async {
+        var suites = await loadSuitesWithConfig(suiteConfiguration(
+            compilerSelections: [
+              CompilerSelection.parse('source'),
+              CompilerSelection.parse('kernel')
+            ]));
+
+        expect(suites, hasLength(2));
+        var runnerSuites =
+            await Future.wait([for (var suite in suites) suite.getSuite()]);
+        expect(
+            runnerSuites,
+            unorderedEquals([
+              isA<RunnerSuite>()
+                  .having(
+                      (s) => s.platform.runtime, 'The vm runtime', Runtime.vm)
+                  .having((s) => s.platform.compiler, 'The source compiler',
+                      Compiler.source),
+              isA<RunnerSuite>()
+                  .having(
+                      (s) => s.platform.runtime, 'The vm runtime', Runtime.vm)
+                  .having((s) => s.platform.compiler, 'The kernel compiler',
+                      Compiler.kernel),
+            ]));
+      });
+
+      test('with unsupported compiler selections, uses the default compiler',
+          () async {
+        var suites =
+            await loadSuitesWithConfig(suiteConfiguration(compilerSelections: [
+          CompilerSelection.parse('dart2js'),
+        ]));
+        expect(suites, hasLength(1));
+        var loadSuite = suites.first;
+        suite = (await loadSuite.getSuite())!;
+        expect(suite.path, equals(p.join(d.sandbox, 'a_test.dart')));
+        expect(suite.platform.runtime, equals(Runtime.vm));
+        expect(suite.platform.compiler, equals(Runtime.vm.defaultCompiler));
+      });
+
+      test('compiler selections support matching boolean selectors', () async {
+        var suites =
+            await loadSuitesWithConfig(suiteConfiguration(compilerSelections: [
+          CompilerSelection.parse('vm:source'),
+        ]));
+        expect(suites, hasLength(1));
+        var loadSuite = suites.first;
+        suite = (await loadSuite.getSuite())!;
+        expect(suite.path, equals(p.join(d.sandbox, 'a_test.dart')));
+        expect(suite.platform.runtime, equals(Runtime.vm));
+        expect(suite.platform.compiler, equals(Compiler.source));
+      });
+
+      test('compiler selections support unmatched boolean selectors', () async {
+        var suites =
+            await loadSuitesWithConfig(suiteConfiguration(compilerSelections: [
+          CompilerSelection.parse('browser:source'),
+        ]));
+        expect(suites, hasLength(1));
+        var loadSuite = suites.first;
+        suite = (await loadSuite.getSuite())!;
+        expect(suite.path, equals(p.join(d.sandbox, 'a_test.dart')));
+        expect(suite.platform.runtime, equals(Runtime.vm));
+        expect(suite.platform.compiler,
+            allOf(Runtime.vm.defaultCompiler, isNot(Compiler.source)));
+      });
+    });
+  });
+
+  group('.loadDir()', () {
+    test('ignores non-Dart files', () async {
+      await d.file('a_test.txt', _tests).create();
+      expect(_loader.loadDir(d.sandbox, SuiteConfiguration.empty).toList(),
+          completion(isEmpty));
+    });
+
+    test("ignores files that don't end in _test.dart", () async {
+      await d.file('test.dart', _tests).create();
+      expect(_loader.loadDir(d.sandbox, SuiteConfiguration.empty).toList(),
+          completion(isEmpty));
+    });
+
+    group('with suites loaded from a directory', () {
+      late List<RunnerSuite> suites;
+      setUp(() async {
+        await d.file('a_test.dart', _tests).create();
+        await d.file('another_test.dart', _tests).create();
+        await d.dir('dir', [d.file('sub_test.dart', _tests)]).create();
+
+        suites = await _loader
+            .loadDir(d.sandbox, SuiteConfiguration.empty)
+            .asyncMap((loadSuite) async => (await loadSuite.getSuite())!)
+            .toList();
+      });
+
+      test('gives those suites the correct paths', () {
+        expect(
+            suites.map((suite) => suite.path),
+            unorderedEquals([
+              p.join(d.sandbox, 'a_test.dart'),
+              p.join(d.sandbox, 'another_test.dart'),
+              p.join(d.sandbox, 'dir', 'sub_test.dart')
+            ]));
+      });
+
+      test('can run tests in those suites', () {
+        var suite =
+            suites.firstWhere((suite) => suite.path!.contains('a_test'));
+        var liveTest = (suite.group.entries[1] as RunnerTest).load(suite);
+        expectSingleFailure(liveTest);
+        return liveTest.run().whenComplete(() => liveTest.close());
+      });
+    });
+  });
+
+  test('a print in a loaded file is piped through the LoadSuite', () async {
+    await d.file('a_test.dart', '''
+      void main() {
+        print('print within test');
+      }
+    ''').create();
+    var suites = await _loader
+        .loadFile(p.join(d.sandbox, 'a_test.dart'), SuiteConfiguration.empty)
+        .toList();
+    expect(suites, hasLength(1));
+    var loadSuite = suites.first;
+
+    var liveTest = (loadSuite.group.entries.single as Test).load(loadSuite);
+    expect(liveTest.onMessage.first.then((message) => message.text),
+        completion(equals('print within test')));
+    await liveTest.run();
+    expectTestPassed(liveTest);
+  });
+
+  group('LoadException', () {
+    test('suites can be retried', () async {
+      var numRetries = 5;
+
+      await d.file('a_test.dart', '''
+      import 'hello.dart';
+
+      void main() {}
+    ''').create();
+
+      var firstFailureCompleter = Completer<void>();
+
+      // After the first load failure we create the missing dependency.
+      unawaited(firstFailureCompleter.future.then((_) async {
+        await d.file('hello.dart', '''
+      String get message => 'hello';
+    ''').create();
+      }));
+
+      await runZoned(() async {
+        var suites = await _loader
+            .loadFile(p.join(d.sandbox, 'a_test.dart'),
+                suiteConfiguration(retry: numRetries))
+            .toList();
+        expect(suites, hasLength(1));
+        var loadSuite = suites.first;
+        var suite = (await loadSuite.getSuite())!;
+        expect(suite.path, equals(p.join(d.sandbox, 'a_test.dart')));
+        expect(suite.platform.runtime, equals(Runtime.vm));
+      }, zoneSpecification:
+          ZoneSpecification(print: (_, parent, zone, message) {
+        if (message.contains('Retrying load of') &&
+            !firstFailureCompleter.isCompleted) {
+          firstFailureCompleter.complete(null);
+        }
+        parent.print(zone, message);
+      }));
+
+      expect(firstFailureCompleter.isCompleted, true);
+    });
+  });
+
+  // TODO: Test load suites. Don't forget to test that prints in loaded files
+  // are piped through the suite. Also for browser tests!
+}
diff --git a/pkgs/test/test/runner/name_test.dart b/pkgs/test/test/runner/name_test.dart
new file mode 100644
index 0000000..bd4dc21
--- /dev/null
+++ b/pkgs/test/test/runner/name_test.dart
@@ -0,0 +1,460 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+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;
+
+import '../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  group('with test.dart?name="name" query', () {
+    test('selects tests with matching names', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("selected 1", () {});
+          test("nope", () => throw TestFailure("oh no"));
+          test("selected 2", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?name=selected']);
+
+      expect(
+        test.stdout,
+        emitsThrough(contains('+2: All tests passed!')),
+      );
+
+      await test.shouldExit(0);
+    });
+
+    test('supports RegExp syntax', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test 1", () {});
+          test("test 2", () => throw TestFailure("oh no"));
+          test("test 3", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?name=test [13]']);
+
+      expect(
+        test.stdout,
+        emitsThrough(contains('+2: All tests passed!')),
+      );
+
+      await test.shouldExit(0);
+    });
+
+    test('applies only to the associated file', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("selected 1", () {});
+          test("selected 2", () => throw TestFailure("oh no"));
+        }
+      ''').create();
+
+      await d.file('test2.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("selected 1", () => throw TestFailure("oh no"));
+          test("selected 2", () {});
+        }
+      ''').create();
+
+      var test = await runTest(
+        ['test.dart?name=selected 1', 'test2.dart?name=selected 2'],
+      );
+
+      expect(
+        test.stdout,
+        emitsThrough(contains('+2: All tests passed!')),
+      );
+      await test.shouldExit(0);
+    });
+
+    test('selects more narrowly when passed multiple times', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("selected 1", () {});
+          test("nope", () => throw TestFailure("oh no"));
+          test("selected 2", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?name=selected&name=1']);
+
+      expect(
+        test.stdout,
+        emitsThrough(contains('+1: All tests passed!')),
+      );
+      await test.shouldExit(0);
+    });
+
+    test('applies to directories', () async {
+      await d.dir('dir', [
+        d.file('first_test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("selected 1", () {});
+          test("selected 2", () => throw TestFailure("oh no"));
+        }
+      '''),
+        d.file('second_test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("selected 1", () {});
+          test("selected 2", () => throw TestFailure("oh no"));
+        }
+      ''')
+      ]).create();
+
+      var test = await runTest(['dir?name=selected 1']);
+
+      expect(
+        test.stdout,
+        emitsThrough(contains('+2: All tests passed!')),
+      );
+      await test.shouldExit(0);
+    });
+
+    test('produces an error when no tests match', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?name=no']);
+
+      expect(
+        test.stderr,
+        emitsThrough(contains('No tests were found.')),
+      );
+
+      await test.shouldExit(exit_codes.noTestsRan);
+    });
+
+    test("doesn't filter out load exceptions", () async {
+      var test = await runTest(['file?name=name']);
+      expect(
+        test.stdout,
+        containsInOrder([
+          '-1: loading file [E]',
+          '  Failed to load "file": Does not exist.'
+        ]),
+      );
+
+      await test.shouldExit(1);
+    });
+  });
+
+  group('with test.dart?full-name query,', () {
+    test('matches with the complete test name', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("selected", () {});
+          test("nope", () => throw TestFailure("oh no"));
+          test("selected nope", () => throw TestFailure("oh no"));
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?full-name=selected']);
+
+      expect(
+        test.stdout,
+        emitsThrough(contains('+1: All tests passed!')),
+      );
+      await test.shouldExit(0);
+    });
+
+    test("doesn't support RegExp syntax", () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test 1", () => throw TestFailure("oh no"));
+          test("test 2", () => throw TestFailure("oh no"));
+          test("test [12]", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?full-name=test [12]']);
+
+      expect(
+        test.stdout,
+        emitsThrough(contains('+1: All tests passed!')),
+      );
+      await test.shouldExit(0);
+    });
+
+    test('applies only to the associated file', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("selected 1", () {});
+          test("selected 2", () => throw TestFailure("oh no"));
+        }
+      ''').create();
+
+      await d.file('test2.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("selected 1", () => throw TestFailure("oh no"));
+          test("selected 2", () {});
+        }
+      ''').create();
+
+      var test = await runTest(
+        ['test.dart?full-name=selected 1', 'test2.dart?full-name=selected 2'],
+      );
+
+      expect(
+        test.stdout,
+        emitsThrough(contains('+2: All tests passed!')),
+      );
+      await test.shouldExit(0);
+    });
+
+    test('produces an error when no tests match', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart?full-name=no match']);
+
+      expect(
+        test.stderr,
+        emitsThrough(contains('No tests were found.')),
+      );
+      await test.shouldExit(exit_codes.noTestsRan);
+    });
+  });
+
+  test('test?name="name" and --name narrow the selection', () async {
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("selected 1", () {});
+        test("nope 1", () => throw TestFailure("oh no"));
+        test("selected 2", () => throw TestFailure("oh no"));
+        test("nope 2", () => throw TestFailure("oh no"));
+      }
+    ''').create();
+
+    var test = await runTest(['--name', '1', 'test.dart?name=selected']);
+
+    expect(
+      test.stdout,
+      emitsThrough(contains('+1: All tests passed!')),
+    );
+    await test.shouldExit(0);
+  });
+
+  test('test?name="name" and test?full-name="name" throws', () async {
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("selected 1", () {});
+        test("nope 1", () => throw TestFailure("oh no"));
+        test("selected 2", () => throw TestFailure("oh no"));
+        test("nope 2", () => throw TestFailure("oh no"));
+      }
+    ''').create();
+
+    var test = await runTest(['test.dart?name=selected&full-name=selected 1']);
+
+    await test.shouldExit(64);
+  });
+
+  group('with the --name flag,', () {
+    test('selects tests with matching names', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("selected 1", () {});
+          test("nope", () => throw TestFailure("oh no"));
+          test("selected 2", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['--name', 'selected', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('+2: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('supports RegExp syntax', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test 1", () {});
+          test("test 2", () => throw TestFailure("oh no"));
+          test("test 3", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['--name', 'test [13]', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('+2: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('selects more narrowly when passed multiple times', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("selected 1", () {});
+          test("nope", () => throw TestFailure("oh no"));
+          test("selected 2", () {});
+        }
+      ''').create();
+
+      var test =
+          await runTest(['--name', 'selected', '--name', '1', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('produces an error when no tests match', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['--name', 'no match', 'test.dart']);
+      expect(
+          test.stderr,
+          emitsThrough(
+              contains('No tests match regular expression "no match".')));
+      await test.shouldExit(exit_codes.noTestsRan);
+    });
+
+    test("doesn't filter out load exceptions", () async {
+      var test = await runTest(['--name', 'name', 'file']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading file [E]',
+            '  Failed to load "file": Does not exist.'
+          ]));
+      await test.shouldExit(1);
+    });
+  });
+
+  group('with the --plain-name flag,', () {
+    test('selects tests with matching names', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("selected 1", () {});
+          test("nope", () => throw TestFailure("oh no"));
+          test("selected 2", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['--plain-name', 'selected', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('+2: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test("doesn't support RegExp syntax", () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test 1", () => throw TestFailure("oh no"));
+          test("test 2", () => throw TestFailure("oh no"));
+          test("test [12]", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['--plain-name', 'test [12]', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('selects more narrowly when passed multiple times', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("selected 1", () {});
+          test("nope", () => throw TestFailure("oh no"));
+          test("selected 2", () {});
+        }
+      ''').create();
+
+      var test = await runTest(
+          ['--plain-name', 'selected', '--plain-name', '1', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('produces an error when no tests match', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['--plain-name', 'no match', 'test.dart']);
+      expect(test.stderr, emitsThrough(contains('No tests match "no match".')));
+      await test.shouldExit(exit_codes.noTestsRan);
+    });
+  });
+
+  test('--name and --plain-name together narrow the selection', () async {
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("selected 1", () {});
+        test("nope", () => throw TestFailure("oh no"));
+        test("selected 2", () {});
+      }
+    ''').create();
+
+    var test =
+        await runTest(['--name', '.....', '--plain-name', 'e', 'test.dart']);
+    expect(test.stdout, emitsThrough(contains('+2: All tests passed!')));
+    await test.shouldExit(0);
+  });
+}
diff --git a/pkgs/test/test/runner/node/runner_test.dart b/pkgs/test/test/runner/node/runner_test.dart
new file mode 100644
index 0000000..82d2321
--- /dev/null
+++ b/pkgs/test/test/runner/node/runner_test.dart
@@ -0,0 +1,469 @@
+// 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.
+
+@TestOn('vm')
+@Tags(['node'])
+library;
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:test/src/runner/executable_settings.dart';
+import 'package:test/test.dart';
+import 'package:test_core/src/util/io.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../../io.dart';
+
+final _success = '''
+  import 'package:test/test.dart';
+
+  void main() {
+    test("success", () {});
+  }
+''';
+
+final _failure = '''
+  import 'package:test/test.dart';
+
+  void main() {
+    test("failure", () => throw TestFailure("oh no"));
+  }
+''';
+
+({int major, String full})? _nodeVersion;
+
+({int major, String full}) _readNodeVersion() {
+  final process = Process.runSync(
+    ExecutableSettings(
+      linuxExecutable: 'node',
+      macOSExecutable: 'node',
+      windowsExecutable: 'node.exe',
+    ).executable,
+    ['--version'],
+    stdoutEncoding: utf8,
+  );
+  if (process.exitCode != 0) {
+    throw const OSError('Could not run node --version');
+  }
+
+  final version = RegExp(r'v(\d+)\..*');
+  final parsed = version.firstMatch(process.stdout as String)!;
+  return (major: int.parse(parsed.group(1)!), full: process.stdout);
+}
+
+String? skipBelowMajorNodeVersion(int minimumMajorVersion) {
+  final (:major, :full) = _nodeVersion ??= _readNodeVersion();
+  if (major < minimumMajorVersion) {
+    return 'This test requires Node $minimumMajorVersion.x or later, '
+        'but is running on $full';
+  }
+
+  return null;
+}
+
+String? skipAboveMajorNodeVersion(int maximumMajorVersion) {
+  final (:major, :full) = _nodeVersion ??= _readNodeVersion();
+  if (major > maximumMajorVersion) {
+    return 'This test requires Node $maximumMajorVersion.x or older, '
+        'but is running on $full';
+  }
+
+  return null;
+}
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  group('fails gracefully if', () {
+    test('a test file fails to compile', () async {
+      await d.file('test.dart', 'invalid Dart file').create();
+      var test = await runTest(['-p', 'node', 'test.dart']);
+
+      expect(
+          test.stdout,
+          containsInOrder([
+            'Error: Compilation failed.',
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart": dart2js failed.'
+          ]));
+      await test.shouldExit(1);
+    });
+
+    test('a test file throws', () async {
+      await d.file('test.dart', "void main() => throw 'oh no';").create();
+
+      var test = await runTest(['-p', 'node', 'test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart": oh no'
+          ]));
+      await test.shouldExit(1);
+    });
+
+    test("a test file doesn't have a main defined", () async {
+      await d.file('test.dart', 'void foo() {}').create();
+
+      var test = await runTest(['-p', 'node', 'test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart": No top-level main() function defined.'
+          ]));
+      await test.shouldExit(1);
+    }, skip: 'https://github.com/dart-lang/test/issues/894');
+
+    test('a test file has a non-function main', () async {
+      await d.file('test.dart', 'int main;').create();
+
+      var test = await runTest(['-p', 'node', 'test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart": Top-level main getter is not a function.'
+          ]));
+      await test.shouldExit(1);
+    }, skip: 'https://github.com/dart-lang/test/issues/894');
+
+    test('a test file has a main with arguments', () async {
+      await d.file('test.dart', 'void main(arg) {}').create();
+
+      var test = await runTest(['-p', 'node', 'test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart": Top-level main() function takes arguments.'
+          ]));
+      await test.shouldExit(1);
+    });
+  });
+
+  group('runs successful tests', () {
+    test('on Node and the VM', () async {
+      await d.file('test.dart', _success).create();
+      var test = await runTest(['-p', 'node', '-p', 'vm', 'test.dart']);
+
+      expect(test.stdout, emitsThrough(contains('+2: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    // Regression test; this broke in 0.12.0-beta.9.
+    test('on a file in a subdirectory', () async {
+      await d.dir('dir', [d.file('test.dart', _success)]).create();
+
+      var test = await runTest(['-p', 'node', 'dir/test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('compiled with dart2wasm', () async {
+      await d.file('test.dart', _success).create();
+      var test =
+          await runTest(['-p', 'node', '--compiler', 'dart2wasm', 'test.dart']);
+
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    }, skip: skipBelowMajorNodeVersion(22));
+  });
+
+  test('defines a node environment constant', () async {
+    await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () {
+            expect(const bool.fromEnvironment("node"), isTrue);
+          });
+        }
+      ''').create();
+
+    var test = await runTest(['-p', 'node', 'test.dart']);
+    expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+    await test.shouldExit(0);
+  });
+
+  test('runs failing tests that fail only on node', () async {
+    await d.file('test.dart', '''
+        import 'package:path/path.dart' as p;
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () {
+            if (const bool.fromEnvironment("node")) {
+              throw TestFailure("oh no");
+            }
+          });
+        }
+      ''').create();
+
+    var test =
+        await runTest(['-p', 'node', '-p', 'vm', '-c', 'dart2js', 'test.dart']);
+    expect(test.stdout, emitsThrough(contains('+1 -1: Some tests failed.')));
+    await test.shouldExit(1);
+  });
+
+  test('runs failing tests that fail only on node (with dart2wasm)', () async {
+    await d.file('test.dart', '''
+        import 'package:path/path.dart' as p;
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () {
+            if (const bool.fromEnvironment("node")) {
+              throw TestFailure("oh no");
+            }
+          });
+        }
+      ''').create();
+
+    var test = await runTest([
+      '-p',
+      'node',
+      '-p',
+      'vm',
+      '-c',
+      'dart2js',
+      '-c',
+      'dart2wasm',
+      'test.dart'
+    ]);
+    expect(test.stdout, emitsThrough(contains('+1 -2: Some tests failed.')));
+    await test.shouldExit(1);
+  }, skip: skipBelowMajorNodeVersion(22));
+
+  test(
+    'gracefully handles wasm errors on old node versions',
+    () async {
+      // Old Node.JS versions can't read the WebAssembly modules emitted by
+      // dart2wasm. The node process exits before connecting to the server
+      // opened by the test runner, leading to timeouts. So, this is a
+      // regression test for https://github.com/dart-lang/test/pull/2259#issuecomment-2307868442
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("test", () {
+            // Should pass on newer node versions
+          });
+        }
+      ''').create();
+
+      var test = await runTest(['-p', 'node', '-c', 'dart2wasm', 'test.dart']);
+      expect(
+        test.stdout,
+        emitsInOrder([
+          emitsThrough(
+              contains('Node exited before connecting to the test channel.')),
+          emitsThrough(contains('-1: Some tests failed.')),
+        ]),
+      );
+      await test.shouldExit(1);
+    },
+    skip: skipAboveMajorNodeVersion(21),
+  );
+
+  test('forwards prints from the Node test', () async {
+    await d.file('test.dart', '''
+      import 'dart:async';
+
+      import 'package:test/test.dart';
+
+      void main() {
+        test("test", () {
+          print("Hello,");
+          return Future(() => print("world!"));
+        });
+      }
+    ''').create();
+
+    var test = await runTest(['-p', 'node', 'test.dart']);
+    expect(test.stdout, emitsInOrder([emitsThrough('Hello,'), 'world!']));
+    await test.shouldExit(0);
+  });
+
+  test('forwards raw JS prints from the Node test', () async {
+    await d.file('test.dart', '''
+      import 'dart:async';
+
+      import 'package:js/js.dart';
+      import 'package:test/test.dart';
+
+      @JS("console.log")
+      external void log(value);
+
+      void main() {
+        test("test", () {
+          log("Hello,");
+          return Future(() => log("world!"));
+        });
+      }
+    ''').create();
+
+    var test = await runTest(['-p', 'node', 'test.dart']);
+    expect(test.stdout, emitsInOrder([emitsThrough('Hello,'), 'world!']));
+    await test.shouldExit(0);
+  });
+
+  test('dartifies stack traces for JS-compiled tests by default', () async {
+    await d.file('test.dart', _failure).create();
+
+    var test = await runTest(['-p', 'node', '--verbose-trace', 'test.dart']);
+    expect(test.stdout,
+        containsInOrder([' main.<fn>', 'package:test', 'dart:async/zone.dart']),
+        skip: 'https://github.com/dart-lang/sdk/issues/41949');
+    await test.shouldExit(1);
+  });
+
+  test("doesn't dartify stack traces for JS-compiled tests with --js-trace",
+      () async {
+    await d.file('test.dart', _failure).create();
+
+    var test = await runTest(
+        ['-p', 'node', '--verbose-trace', '--js-trace', 'test.dart']);
+    expect(test.stdoutStream(), neverEmits(endsWith(' main.<fn>')));
+    expect(test.stdoutStream(), neverEmits(contains('package:test')));
+    expect(test.stdoutStream(), neverEmits(contains('dart:async/zone.dart')));
+    expect(test.stdout, emitsThrough(contains('-1: Some tests failed.')));
+    await test.shouldExit(1);
+  });
+
+  test('supports node_modules in the package directory', () async {
+    await d.dir('node_modules', [
+      d.dir('my_module', [d.file('index.js', 'module.exports.value = 12;')])
+    ]).create();
+
+    await d.file('test.dart', '''
+      import 'package:js/js.dart';
+      import 'package:test/test.dart';
+
+      @JS()
+      external MyModule require(String name);
+
+      @JS()
+      class MyModule {
+        external int get value;
+      }
+
+      void main() {
+        test("can load from a module", () {
+          expect(require("my_module").value, equals(12));
+        });
+      }
+    ''').create();
+
+    var test = await runTest(['-p', 'node', 'test.dart']);
+    expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+    await test.shouldExit(0);
+  });
+
+  group('with onPlatform', () {
+    test('respects matching Skips', () async {
+      await d.file('test.dart', '''
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("fail", () => throw 'oh no', onPlatform: {"node": Skip()});
+        }
+      ''').create();
+
+      var test = await runTest(['-p', 'node', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('+0 ~1: All tests skipped.')));
+      await test.shouldExit(0);
+    });
+
+    test('ignores non-matching Skips', () async {
+      await d.file('test.dart', '''
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("success", () {}, onPlatform: {"browser": Skip()});
+        }
+      ''').create();
+
+      var test = await runTest(['-p', 'node', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('matches the current OS', () async {
+      await d.file('test.dart', '''
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("fail", () => throw 'oh no',
+              onPlatform: {"${currentOS.identifier}": Skip()});
+        }
+      ''').create();
+
+      var test = await runTest(['-p', 'node', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('+0 ~1: All tests skipped.')));
+      await test.shouldExit(0);
+    });
+
+    test("doesn't match a different OS", () async {
+      await d.file('test.dart', '''
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("success", () {}, onPlatform: {"$otherOS": Skip()});
+        }
+      ''').create();
+
+      var test = await runTest(['-p', 'node', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+  });
+
+  group('with an @OnPlatform annotation', () {
+    test('respects matching Skips', () async {
+      await d.file('test.dart', '''
+        @OnPlatform(const {"js": const Skip()})
+
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("fail", () => throw 'oh no');
+        }
+      ''').create();
+
+      var test = await runTest(['-p', 'node', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('~1: All tests skipped.')));
+      await test.shouldExit(0);
+    });
+
+    test('ignores non-matching Skips', () async {
+      await d.file('test.dart', '''
+        @OnPlatform(const {"vm": const Skip()})
+
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("success", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['-p', 'node', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+  });
+}
diff --git a/pkgs/test/test/runner/parse_metadata_test.dart b/pkgs/test/test/runner/parse_metadata_test.dart
new file mode 100644
index 0000000..a8ffa9c
--- /dev/null
+++ b/pkgs/test/test/runner/parse_metadata_test.dart
@@ -0,0 +1,326 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'package:test/test.dart';
+import 'package:test_api/src/backend/platform_selector.dart';
+import 'package:test_api/src/backend/runtime.dart';
+import 'package:test_api/src/backend/suite_platform.dart';
+import 'package:test_core/src/runner/parse_metadata.dart';
+
+final _path = 'test.dart';
+
+void main() {
+  test('returns empty metadata for an empty file', () {
+    var metadata = parseMetadata(_path, '', {});
+    expect(metadata.testOn, equals(PlatformSelector.all));
+    expect(metadata.timeout.scaleFactor, equals(1));
+  });
+
+  test('ignores irrelevant annotations', () {
+    var metadata =
+        parseMetadata(_path, '@Fblthp\n@Fblthp.foo\nlibrary foo;', {});
+    expect(metadata.testOn, equals(PlatformSelector.all));
+  });
+
+  test('parses a prefixed annotation', () {
+    var metadata = parseMetadata(
+        _path,
+        "@foo.TestOn('vm')\n"
+        "import 'package:test/test.dart' as foo;",
+        {});
+    expect(metadata.testOn.evaluate(SuitePlatform(Runtime.vm, compiler: null)),
+        isTrue);
+    expect(
+        metadata.testOn.evaluate(SuitePlatform(Runtime.chrome, compiler: null)),
+        isFalse);
+  });
+
+  group('@TestOn:', () {
+    test('parses a valid annotation', () {
+      var metadata = parseMetadata(_path, "@TestOn('vm')\nlibrary foo;", {});
+      expect(
+          metadata.testOn.evaluate(SuitePlatform(Runtime.vm, compiler: null)),
+          isTrue);
+      expect(
+          metadata.testOn
+              .evaluate(SuitePlatform(Runtime.chrome, compiler: null)),
+          isFalse);
+    });
+
+    test('ignores a constructor named TestOn', () {
+      var metadata =
+          parseMetadata(_path, "@foo.TestOn('foo')\nlibrary foo;", {});
+      expect(metadata.testOn, equals(PlatformSelector.all));
+    });
+
+    group('throws an error for', () {
+      test('multiple @TestOns', () {
+        expect(
+            () => parseMetadata(
+                _path, "@TestOn('foo')\n@TestOn('bar')\nlibrary foo;", {}),
+            throwsFormatException);
+      });
+    });
+  });
+
+  group('@Timeout:', () {
+    test('parses a valid duration annotation', () {
+      var metadata = parseMetadata(_path, '''
+@Timeout(const Duration(
+    hours: 1,
+    minutes: 2,
+    seconds: 3,
+    milliseconds: 4,
+    microseconds: 5))
+
+library foo;
+''', {});
+      expect(
+          metadata.timeout.duration,
+          equals(const Duration(
+              hours: 1,
+              minutes: 2,
+              seconds: 3,
+              milliseconds: 4,
+              microseconds: 5)));
+    });
+
+    test('parses a valid duration omitting const', () {
+      var metadata = parseMetadata(_path, '''
+@Timeout(Duration(
+    hours: 1,
+    minutes: 2,
+    seconds: 3,
+    milliseconds: 4,
+    microseconds: 5))
+
+library foo;
+''', {});
+      expect(
+          metadata.timeout.duration,
+          equals(const Duration(
+              hours: 1,
+              minutes: 2,
+              seconds: 3,
+              milliseconds: 4,
+              microseconds: 5)));
+    });
+
+    test('parses a valid duration with an import prefix', () {
+      var metadata = parseMetadata(_path, '''
+@Timeout(core.Duration(
+    hours: 1,
+    minutes: 2,
+    seconds: 3,
+    milliseconds: 4,
+    microseconds: 5))
+import 'dart:core' as core;
+''', {});
+      expect(
+          metadata.timeout.duration,
+          equals(const Duration(
+              hours: 1,
+              minutes: 2,
+              seconds: 3,
+              milliseconds: 4,
+              microseconds: 5)));
+    });
+
+    test('parses a valid int factor annotation', () {
+      var metadata = parseMetadata(_path, '''
+@Timeout.factor(1)
+
+library foo;
+''', {});
+      expect(metadata.timeout.scaleFactor, equals(1));
+    });
+
+    test('parses a valid int factor annotation with an import prefix', () {
+      var metadata = parseMetadata(_path, '''
+@test.Timeout.factor(1)
+import 'package:test/test.dart' as test;
+''', {});
+      expect(metadata.timeout.scaleFactor, equals(1));
+    });
+
+    test('parses a valid double factor annotation', () {
+      var metadata = parseMetadata(_path, '''
+@Timeout.factor(0.5)
+
+library foo;
+''', {});
+      expect(metadata.timeout.scaleFactor, equals(0.5));
+    });
+
+    test('parses a valid Timeout.none annotation', () {
+      var metadata = parseMetadata(_path, '''
+@Timeout.none
+
+library foo;
+''', {});
+      expect(metadata.timeout, same(Timeout.none));
+    });
+
+    test('ignores a constructor named Timeout', () {
+      var metadata =
+          parseMetadata(_path, "@foo.Timeout('foo')\nlibrary foo;", {});
+      expect(metadata.timeout.scaleFactor, equals(1));
+    });
+
+    group('throws an error for', () {
+      test('multiple @Timeouts', () {
+        expect(
+            () => parseMetadata(_path,
+                '@Timeout.factor(1)\n@Timeout.factor(2)\nlibrary foo;', {}),
+            throwsFormatException);
+      });
+    });
+  });
+
+  group('@Skip:', () {
+    test('parses a valid annotation', () {
+      var metadata = parseMetadata(_path, '@Skip()\nlibrary foo;', {});
+      expect(metadata.skip, isTrue);
+      expect(metadata.skipReason, isNull);
+    });
+
+    test('parses a valid annotation with a reason', () {
+      var metadata = parseMetadata(_path, "@Skip('reason')\nlibrary foo;", {});
+      expect(metadata.skip, isTrue);
+      expect(metadata.skipReason, equals('reason'));
+    });
+
+    test('ignores a constructor named Skip', () {
+      var metadata = parseMetadata(_path, "@foo.Skip('foo')\nlibrary foo;", {});
+      expect(metadata.skip, isFalse);
+    });
+
+    group('throws an error for', () {
+      test('multiple @Skips', () {
+        expect(
+            () => parseMetadata(
+                _path, "@Skip('foo')\n@Skip('bar')\nlibrary foo;", {}),
+            throwsFormatException);
+      });
+    });
+  });
+
+  group('@Tags:', () {
+    test('parses a valid annotation', () {
+      var metadata = parseMetadata(_path, "@Tags(['a'])\nlibrary foo;", {});
+      expect(metadata.tags, equals(['a']));
+    });
+
+    test('ignores a constructor named Tags', () {
+      var metadata = parseMetadata(_path, "@foo.Tags(['a'])\nlibrary foo;", {});
+      expect(metadata.tags, isEmpty);
+    });
+
+    group('throws an error for', () {
+      test('multiple @Tags', () {
+        expect(
+            () => parseMetadata(
+                _path, "@Tags(['a'])\n@Tags(['b'])\nlibrary foo;", {}),
+            throwsFormatException);
+      });
+
+      test('String interpolation', () {
+        expect(
+            () => parseMetadata(
+                _path, "@Tags(['\$a'])\nlibrary foo;\nconst a = 'a';", {}),
+            throwsFormatException);
+      });
+    });
+  });
+
+  group('@OnPlatform:', () {
+    test('parses a valid annotation', () {
+      var metadata = parseMetadata(_path, '''
+@OnPlatform({
+  'chrome': Timeout.factor(2),
+  'vm': [Skip(), Timeout.factor(3)]
+})
+library foo;''', {});
+
+      var key = metadata.onPlatform.keys.first;
+      expect(
+          key.evaluate(SuitePlatform(Runtime.chrome, compiler: null)), isTrue);
+      expect(key.evaluate(SuitePlatform(Runtime.vm, compiler: null)), isFalse);
+      var value = metadata.onPlatform.values.first;
+      expect(value.timeout.scaleFactor, equals(2));
+
+      key = metadata.onPlatform.keys.last;
+      expect(key.evaluate(SuitePlatform(Runtime.vm, compiler: null)), isTrue);
+      expect(
+          key.evaluate(SuitePlatform(Runtime.chrome, compiler: null)), isFalse);
+      value = metadata.onPlatform.values.last;
+      expect(value.skip, isTrue);
+      expect(value.timeout.scaleFactor, equals(3));
+    });
+
+    test('parses a valid annotation with an import prefix', () {
+      var metadata = parseMetadata(_path, '''
+@test.OnPlatform({
+  'chrome': test.Timeout.factor(2),
+  'vm': [test.Skip(), test.Timeout.factor(3)]
+})
+import 'package:test/test.dart' as test;
+''', {});
+
+      var key = metadata.onPlatform.keys.first;
+      expect(
+          key.evaluate(SuitePlatform(Runtime.chrome, compiler: null)), isTrue);
+      expect(key.evaluate(SuitePlatform(Runtime.vm, compiler: null)), isFalse);
+      var value = metadata.onPlatform.values.first;
+      expect(value.timeout.scaleFactor, equals(2));
+
+      key = metadata.onPlatform.keys.last;
+      expect(key.evaluate(SuitePlatform(Runtime.vm, compiler: null)), isTrue);
+      expect(
+          key.evaluate(SuitePlatform(Runtime.chrome, compiler: null)), isFalse);
+      value = metadata.onPlatform.values.last;
+      expect(value.skip, isTrue);
+      expect(value.timeout.scaleFactor, equals(3));
+    });
+
+    test('ignores a constructor named OnPlatform', () {
+      var metadata =
+          parseMetadata(_path, "@foo.OnPlatform('foo')\nlibrary foo;", {});
+      expect(metadata.testOn, equals(PlatformSelector.all));
+    });
+
+    group('throws an error for', () {
+      test('a map with a unparseable key', () {
+        expect(
+            () => parseMetadata(
+                _path, "@OnPlatform({'invalid': Skip()})\nlibrary foo;", {}),
+            throwsFormatException);
+      });
+
+      test('a map with an invalid value', () {
+        expect(
+            () => parseMetadata(_path,
+                "@OnPlatform({'vm': const TestOn('vm')})\nlibrary foo;", {}),
+            throwsFormatException);
+      });
+
+      test('a map with an invalid value in a list', () {
+        expect(
+            () => parseMetadata(_path,
+                "@OnPlatform({'vm': [const TestOn('vm')]})\nlibrary foo;", {}),
+            throwsFormatException);
+      });
+
+      test('multiple @OnPlatforms', () {
+        expect(
+            () => parseMetadata(
+                _path, '@OnPlatform({})\n@OnPlatform({})\nlibrary foo;', {}),
+            throwsFormatException);
+      });
+    });
+  });
+}
diff --git a/pkgs/test/test/runner/pause_after_load_test.dart b/pkgs/test/test/runner/pause_after_load_test.dart
new file mode 100644
index 0000000..faba441
--- /dev/null
+++ b/pkgs/test/test/runner/pause_after_load_test.dart
@@ -0,0 +1,283 @@
+// 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.
+
+@TestOn('vm')
+@OnPlatform({'windows': Skip('https://github.com/dart-lang/test/issues/1613')})
+library;
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('pauses the test runner for each file until the user presses enter',
+      () async {
+    await d.file('test1.dart', '''
+import 'package:test/test.dart';
+
+void main() {
+  print('loaded test 1!');
+
+  test("success", () {});
+}
+''').create();
+
+    await d.file('test2.dart', '''
+import 'package:test/test.dart';
+
+void main() {
+
+  print('loaded test 2!');
+  test("success", () {});
+}
+''').create();
+
+    var test = await runTest(
+        ['--pause-after-load', '-p', 'chrome', 'test1.dart', 'test2.dart']);
+    await expectLater(test.stdout, emitsThrough('loaded test 1!'));
+    await expectLater(test.stdout, emitsThrough(equalsIgnoringWhitespace('''
+      The test runner is paused. Open the dev console in Chrome and set
+      breakpoints. Once you're finished, return to this terminal and press
+      Enter.
+    ''')));
+
+    var nextLineFired = false;
+    unawaited(test.stdout.next.then(expectAsync1((line) {
+      expect(line, contains('+0: test1.dart: success'));
+      nextLineFired = true;
+    })));
+
+    // Wait a little bit to be sure that the tests don't start running without
+    // our input.
+    await Future<void>.delayed(const Duration(seconds: 2));
+    expect(nextLineFired, isFalse);
+
+    test.stdin.writeln();
+
+    await expectLater(test.stdout, emitsThrough('loaded test 2!'));
+    await expectLater(test.stdout, emitsThrough(equalsIgnoringWhitespace('''
+      The test runner is paused. Open the dev console in Chrome and set
+      breakpoints. Once you're finished, return to this terminal and press
+      Enter.
+    ''')));
+
+    nextLineFired = false;
+    unawaited(test.stdout.next.then(expectAsync1((line) {
+      expect(line, contains('+1: test2.dart: success'));
+      nextLineFired = true;
+    })));
+
+    // Wait a little bit to be sure that the tests don't start running without
+    // our input.
+    await Future<void>.delayed(const Duration(seconds: 2));
+    expect(nextLineFired, isFalse);
+
+    test.stdin.writeln();
+    await expectLater(
+        test.stdout, emitsThrough(contains('+2: All tests passed!')));
+    await test.shouldExit(0);
+  }, tags: 'chrome');
+
+  test('pauses the test runner for each platform until the user presses enter',
+      () async {
+    await d.file('test.dart', '''
+import 'package:test/test.dart';
+
+void main() {
+  print('loaded test!');
+
+  test("success", () {});
+}
+''').create();
+
+    var test = await runTest([
+      '--pause-after-load',
+      '-p',
+      'firefox',
+      '-p',
+      'chrome',
+      '-p',
+      'vm',
+      'test.dart'
+    ]);
+    await expectLater(test.stdout, emitsThrough('loaded test!'));
+    await expectLater(test.stdout, emitsThrough(equalsIgnoringWhitespace('''
+      The test runner is paused. Open the dev console in Firefox and set
+      breakpoints. Once you're finished, return to this terminal and press
+      Enter.
+    ''')));
+
+    var nextLineFired = false;
+    unawaited(test.stdout.next.then(expectAsync1((line) {
+      expect(line, contains('+0: [Firefox, Dart2Js] success'));
+      nextLineFired = true;
+    })));
+
+    // Wait a little bit to be sure that the tests don't start running without
+    // our input.
+    await Future<void>.delayed(const Duration(seconds: 2));
+    expect(nextLineFired, isFalse);
+
+    test.stdin.writeln();
+
+    await expectLater(test.stdout, emitsThrough('loaded test!'));
+    await expectLater(
+        test.stdout,
+        emitsThrough(emitsInOrder([
+          'The test runner is paused. Open the dev console in Chrome and set '
+              "breakpoints. Once you're finished, return to this terminal and "
+              'press Enter.'
+        ])));
+
+    nextLineFired = false;
+    unawaited(test.stdout.next.then(expectAsync1((line) {
+      expect(line, contains('+1: [Chrome, Dart2Js] success'));
+      nextLineFired = true;
+    })));
+
+    // Wait a little bit to be sure that the tests don't start running without
+    // our input.
+    await Future<void>.delayed(const Duration(seconds: 2));
+    expect(nextLineFired, isFalse);
+
+    test.stdin.writeln();
+    await expectLater(test.stdout, emitsThrough('loaded test!'));
+    await expectLater(
+        test.stdout,
+        emitsThrough(emitsInOrder([
+          'The test runner is paused. Open the Observatory and set '
+              "breakpoints. Once you're finished, return to this terminal "
+              'and press Enter.'
+        ])));
+
+    nextLineFired = false;
+    unawaited(test.stdout.next.then(expectAsync1((line) {
+      expect(line, contains('+2: [VM, Kernel] success'));
+      nextLineFired = true;
+    })));
+
+    // Wait a little bit to be sure that the tests don't start running without
+    // our input.
+    await Future<void>.delayed(const Duration(seconds: 2));
+    expect(nextLineFired, isFalse);
+
+    test.stdin.writeln();
+
+    await expectLater(
+        test.stdout, emitsThrough(contains('+3: All tests passed!')));
+    await test.shouldExit(0);
+  }, tags: ['firefox', 'chrome', 'vm']);
+
+  test('stops immediately if killed while paused', () async {
+    await d.file('test.dart', '''
+import 'package:test/test.dart';
+
+void main() {
+  print('loaded test!');
+
+  test("success", () {});
+}
+''').create();
+
+    var test =
+        await runTest(['--pause-after-load', '-p', 'chrome', 'test.dart']);
+    await expectLater(test.stdout, emitsThrough('loaded test!'));
+    await expectLater(test.stdout, emitsThrough(equalsIgnoringWhitespace('''
+      The test runner is paused. Open the dev console in Chrome and set
+      breakpoints. Once you're finished, return to this terminal and press
+      Enter.
+    ''')));
+
+    test.signal(ProcessSignal.sigterm);
+    await test.shouldExit();
+    await expectLater(test.stderr, emitsDone);
+  }, tags: 'chrome', testOn: '!windows');
+
+  test('disables timeouts', () async {
+    await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  print('loaded test 1!');
+
+  test("success", () async {
+    await Future.delayed(Duration.zero);
+  }, timeout: Timeout(Duration.zero));
+}
+''').create();
+
+    var test = await runTest(
+        ['--pause-after-load', '-p', 'chrome', '-n', 'success', 'test.dart']);
+    await expectLater(test.stdout, emitsThrough('loaded test 1!'));
+    await expectLater(test.stdout, emitsThrough(equalsIgnoringWhitespace('''
+      The test runner is paused. Open the dev console in Chrome and set
+      breakpoints. Once you're finished, return to this terminal and press
+      Enter.
+    ''')));
+
+    var nextLineFired = false;
+    unawaited(test.stdout.next.then(expectAsync1((line) {
+      expect(line, contains('+0: success'));
+      nextLineFired = true;
+    })));
+
+    // Wait a little bit to be sure that the tests don't start running without
+    // our input.
+    await Future<void>.delayed(const Duration(seconds: 2));
+    expect(nextLineFired, isFalse);
+
+    test.stdin.writeln();
+    await expectLater(
+        test.stdout, emitsThrough(contains('+1: All tests passed!')));
+    await test.shouldExit(0);
+  }, tags: 'chrome');
+
+  // Regression test for #304.
+  test('supports test name patterns', () async {
+    await d.file('test.dart', '''
+import 'package:test/test.dart';
+
+void main() {
+  print('loaded test 1!');
+
+  test("failure 1", () {});
+  test("success", () {});
+  test("failure 2", () {});
+}
+''').create();
+
+    var test = await runTest(
+        ['--pause-after-load', '-p', 'chrome', '-n', 'success', 'test.dart']);
+    await expectLater(test.stdout, emitsThrough('loaded test 1!'));
+    await expectLater(test.stdout, emitsThrough(equalsIgnoringWhitespace('''
+      The test runner is paused. Open the dev console in Chrome and set
+      breakpoints. Once you're finished, return to this terminal and press
+      Enter.
+    ''')));
+
+    var nextLineFired = false;
+    unawaited(test.stdout.next.then(expectAsync1((line) {
+      expect(line, contains('+0: success'));
+      nextLineFired = true;
+    })));
+
+    // Wait a little bit to be sure that the tests don't start running without
+    // our input.
+    await Future<void>.delayed(const Duration(seconds: 2));
+    expect(nextLineFired, isFalse);
+
+    test.stdin.writeln();
+    await expectLater(
+        test.stdout, emitsThrough(contains('+1: All tests passed!')));
+    await test.shouldExit(0);
+  }, tags: 'chrome');
+}
diff --git a/pkgs/test/test/runner/precompiled_test.dart b/pkgs/test/test/runner/precompiled_test.dart
new file mode 100644
index 0000000..51dde28
--- /dev/null
+++ b/pkgs/test/test/runner/precompiled_test.dart
@@ -0,0 +1,264 @@
+// 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.
+
+@TestOn('vm')
+@OnPlatform({'windows': Skip('https://github.com/dart-lang/test/issues/1617')})
+library;
+
+import 'dart:async';
+import 'dart:io';
+import 'dart:isolate';
+
+import 'package:node_preamble/preamble.dart' as preamble;
+import 'package:package_config/package_config.dart';
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+import 'package:test_process/test_process.dart';
+
+import '../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  group('browser tests', () {
+    setUpAll(() async {
+      await _precompileBrowserTest('test.dart');
+    });
+
+    test('run a precompiled version of a test rather than recompiling',
+        () async {
+      var test = await runTest([
+        '-p',
+        'chrome',
+        '--precompiled=precompiled/',
+        'test.dart',
+      ]);
+      expect(test.stdout,
+          containsInOrder(['+0: success', '+1: All tests passed!']));
+      await test.shouldExit(0);
+    });
+
+    test('run two precompiled tests', () async {
+      await _precompileBrowserTest('test_2.dart');
+      var test = await runTest(concurrency: 2, [
+        '-p',
+        'chrome',
+        '--precompiled=precompiled/',
+        'test.dart',
+        'test_2.dart',
+      ]);
+      expect(test.stdout, containsInOrder(['+2: All tests passed!']));
+      await test.shouldExit(0);
+    });
+
+    test('can use the json reporter', () async {
+      var test = await runTest([
+        '-p',
+        'chrome',
+        '--precompiled=precompiled/',
+        'test.dart',
+        '-r',
+        'json'
+      ]);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '{"testID":3,"result":"success"',
+            '{"success":true,"type":"done"'
+          ]));
+      await test.shouldExit(0);
+    });
+  }, tags: const ['chrome']);
+
+  group('node tests', () {
+    setUp(() async {
+      await d.dir('test', [
+        d.file('test.dart', '''
+          import "package:test/src/bootstrap/node.dart";
+          import "package:test/test.dart";
+
+          void main() {
+            internalBootstrapNodeTest(() => () => test("success", () {
+              expect(true, isTrue);
+            }));
+          }''')
+      ]).create();
+      await _writePackagesFile();
+
+      var jsPath = p.join(d.sandbox, 'test', 'test.dart.node_test.dart.js');
+      var dart2js = await TestProcess.start(
+          Platform.resolvedExecutable,
+          [
+            'compile',
+            'js',
+            '--packages=${await Isolate.packageConfig}',
+            p.join('test', 'test.dart'),
+            '--out=$jsPath',
+          ],
+          workingDirectory: d.sandbox);
+      await dart2js.shouldExit(0);
+
+      var jsFile = File(jsPath);
+      await jsFile.writeAsString(
+          preamble.getPreamble(minified: true) + await jsFile.readAsString());
+
+      await d.dir('test', [d.file('test.dart', 'invalid dart}')]).create();
+    });
+
+    test('run a precompiled version of a test rather than recompiling',
+        () async {
+      var test = await runTest([
+        '-p',
+        'node',
+        '--precompiled',
+        d.sandbox,
+        p.join('test', 'test.dart')
+      ]);
+      expect(test.stdout,
+          containsInOrder(['+0: success', '+1: All tests passed!']));
+      await test.shouldExit(0);
+    });
+
+    test('can use the json reporter', () async {
+      var test = await runTest([
+        '-p',
+        'node',
+        '--precompiled',
+        d.sandbox,
+        p.join('test', 'test.dart'),
+        '-r',
+        'json'
+      ]);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '{"testID":3,"result":"success"',
+            '{"success":true,"type":"done"'
+          ]));
+      await test.shouldExit(0);
+    });
+  }, tags: const ['node']);
+
+  group('vm tests', () {
+    setUp(() async {
+      await d.dir('test', [
+        d.file('test.dart', '''
+          import "package:test/test.dart";
+          void main() {
+            test("true is true", () {
+              expect(true, isTrue);
+            });
+          }
+        '''),
+        d.file('test.dart.vm_test.dart', '''
+          import "dart:isolate";
+          import "package:test_core/src/bootstrap/vm.dart";
+          import "test.dart" as test;
+          void main(_, SendPort message) {
+            internalBootstrapVmTest(() => test.main, message);
+          }
+        '''),
+      ]).create();
+      await _writePackagesFile();
+    });
+
+    test('run in the precompiled directory', () async {
+      var test = await runTest(
+          ['-p', 'vm', '--precompiled=${d.sandbox}', 'test/test.dart']);
+      expect(test.stdout,
+          containsInOrder(['+0: true is true', '+1: All tests passed!']));
+      await test.shouldExit(0);
+    });
+
+    test('can load precompiled dill files if available', () async {
+      // Create the snapshot in the sandbox directory.
+      var snapshotProcess = await runDart([
+        '--snapshot_kind=script',
+        '--snapshot=test/test.dart.vm_test.vm.app.dill',
+        'test/test.dart.vm_test.dart'
+      ]);
+      await snapshotProcess.shouldExit(0);
+
+      // Modify the original test so it would fail if it actually got ran, this
+      // makes sure the test fails if the dill file isn't loaded.
+      var testFile = File(p.join(d.sandbox, 'test', 'test.dart'));
+      expect(await testFile.exists(), isTrue);
+      var originalContent = await testFile.readAsString();
+      await testFile
+          .writeAsString(originalContent.replaceAll('isTrue', 'isFalse'));
+
+      // Actually invoke the test with the dill file.
+      var testProcess = await runTest(
+          ['-p', 'vm', '--precompiled=${d.sandbox}', 'test/test.dart']);
+      expect(testProcess.stdout,
+          containsInOrder(['+0: true is true', '+1: All tests passed!']));
+      await testProcess.shouldExit(0);
+    });
+
+    test('can use the json reporter', () async {
+      var test = await runTest([
+        '-p',
+        'vm',
+        '--precompiled=${d.sandbox}',
+        'test/test.dart',
+        '-r',
+        'json'
+      ]);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '{"testID":3,"result":"success"',
+            '{"success":true,"type":"done"'
+          ]));
+      await test.shouldExit(0);
+    });
+  });
+}
+
+Future<void> _writePackagesFile() async {
+  var config = (await findPackageConfig(Directory.current))!;
+  await d.dir('.dart_tool').create();
+  await savePackageConfig(config, Directory(d.sandbox));
+}
+
+Future<void> _precompileBrowserTest(String testPath) async {
+  var tmpDir = await Directory.systemTemp.createTemp('browser_test');
+  var file = File.fromUri(tmpDir.uri.resolve('precompiled.dart'));
+  await file.writeAsString('''
+        import "package:test/bootstrap/browser.dart";
+        import "package:test/test.dart";
+
+        main(_) {
+          internalBootstrapBrowserTest(() => () => test("success", () {}));
+        }
+      ''');
+
+  await d.dir('precompiled', [
+    d.file(p.setExtension(testPath, 'html'), '''
+          <!DOCTYPE html>
+          <html>
+          <head>
+            <title>test Test</title>
+            <script src="$testPath.browser_test.dart.js"></script>
+          </head>
+          </html>
+        ''')
+  ]).create();
+
+  var dart2js = await TestProcess.start(
+      Platform.resolvedExecutable,
+      [
+        'compile',
+        'js',
+        ...Platform.executableArguments,
+        '--packages=${(await Isolate.packageConfig)!.toFilePath()}',
+        file.path,
+        '--out=precompiled/$testPath.browser_test.dart.js'
+      ],
+      workingDirectory: d.sandbox);
+  await dart2js.shouldExit(0);
+
+  await d.file(testPath, 'invalid dart}').create();
+}
diff --git a/pkgs/test/test/runner/retry_test.dart b/pkgs/test/test/runner/retry_test.dart
new file mode 100644
index 0000000..0e16fef
--- /dev/null
+++ b/pkgs/test/test/runner/retry_test.dart
@@ -0,0 +1,231 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('respects --no-retry flag with retry option', () async {
+    await d.file('test.dart', '''
+          import 'dart:async';
+
+          import 'package:test/test.dart';
+
+          var attempt = 0;
+          void main() {
+            test("eventually passes", () {
+               attempt++;
+               if(attempt <= 1 ) {
+                 throw TestFailure("oh no");
+               }
+            }, retry: 1);
+          }
+          ''').create();
+
+    var test = await runTest(['test.dart', '--no-retry']);
+    expect(test.stdout, emitsThrough(contains('-1: Some tests failed.')));
+    await test.shouldExit(1);
+  });
+
+  test('respects --no-retry flag with @Retry declaration', () async {
+    await d.file('test.dart', '''
+          @Retry(3)
+
+          import 'dart:async';
+
+          import 'package:test/test.dart';
+
+          var attempt = 0;
+          void main() {
+            test("eventually passes", () {
+               attempt++;
+               if(attempt <= 1 ) {
+                 throw TestFailure("oh no");
+               }
+            });
+          }
+          ''').create();
+
+    var test = await runTest(['test.dart', '--no-retry']);
+    expect(test.stdout, emitsThrough(contains('-1: Some tests failed.')));
+    await test.shouldExit(1);
+  });
+
+  test('respects top-level @Retry declarations', () async {
+    await d.file('test.dart', '''
+          @Retry(3)
+
+          import 'dart:async';
+
+          import 'package:test/test.dart';
+
+          var attempt = 0;
+          void main() {
+            test("failure", () {
+               attempt++;
+               if(attempt <= 3) {
+                 throw TestFailure("oh no");
+               }
+            });
+          }
+          ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+    await test.shouldExit(0);
+  });
+
+  test('respects group retry declarations', () async {
+    await d.file('test.dart', '''
+          import 'dart:async';
+
+          import 'package:test/test.dart';
+
+          var attempt = 0;
+          void main() {
+            group("retry", () {
+              test("failure", () {
+                 attempt++;
+                 if(attempt <= 3) {
+                   throw TestFailure("oh no");
+                 }
+              });
+             }, retry: 3);
+          }
+          ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+    await test.shouldExit(0);
+  });
+
+  test('tests are not retried after they have already been reported successful',
+      () async {
+    await d.file('test.dart', '''
+              import 'dart:async';
+
+              import 'package:test/test.dart';
+
+              void main() {
+                var completer1 = Completer();
+                var completer2 = Completer();
+                test("first", () {
+                  completer1.future.then((_) {
+                    completer2.complete();
+                    throw "oh no";
+                  });
+                }, retry: 2);
+
+                test("second", () async {
+                  completer1.complete();
+                  await completer2.future;
+                });
+              }
+          ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(
+        test.stdout,
+        emitsThrough(
+            contains('This test failed after it had already completed')));
+    await test.shouldExit(1);
+  });
+
+  group('retries tests', () {
+    test('and eventually passes for valid tests', () async {
+      await d.file('test.dart', '''
+              import 'dart:async';
+
+              import 'package:test/test.dart';
+
+              var attempt = 0;
+              void main() {
+                test("eventually passes", () {
+                 attempt++;
+                 if(attempt <= 2) {
+                   throw TestFailure("oh no");
+                 }
+                }, retry: 2);
+              }
+          ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('and ignores previous errors', () async {
+      await d.file('test.dart', '''
+              import 'dart:async';
+
+              import 'package:test/test.dart';
+
+              var attempt = 0;
+              Completer completer = Completer();
+              void main() {
+                test("failure", () async {
+                  attempt++;
+                  if (attempt == 1) {
+                    completer.future.then((_) => throw 'some error');
+                    throw TestFailure("oh no");
+                  }
+                  completer.complete(null);
+                  await Future((){});
+                }, retry: 1);
+              }
+          ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('and eventually fails for invalid tests', () async {
+      await d.file('test.dart', '''
+              import 'dart:async';
+
+              import 'package:test/test.dart';
+
+              void main() {
+                test("failure", () {
+                 throw TestFailure("oh no");
+                }, retry: 2);
+              }
+          ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdout, emitsThrough(contains('-1: Some tests failed.')));
+      await test.shouldExit(1);
+    });
+
+    test('only after a failure', () async {
+      await d.file('test.dart', '''
+              import 'dart:async';
+
+              import 'package:test/test.dart';
+
+              var attempt = 0;
+              void main() {
+                test("eventually passes", () {
+                attempt++;
+                if (attempt != 2){
+                 throw TestFailure("oh no");
+                }
+                }, retry: 5);
+          }
+          ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+  });
+}
diff --git a/pkgs/test/test/runner/runner_test.dart b/pkgs/test/test/runner/runner_test.dart
new file mode 100644
index 0000000..379e2a7
--- /dev/null
+++ b/pkgs/test/test/runner/runner_test.dart
@@ -0,0 +1,871 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:io';
+import 'dart:math' as math;
+
+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;
+
+import '../io.dart';
+
+final _success = '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {});
+}
+''';
+
+final _failure = '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("failure", () => throw TestFailure("oh no"));
+}
+''';
+
+final _asyncFailure = '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("failure", () async {
+    await Future(() {}).then((_) {
+      throw 'oh no';
+    });
+  });
+}
+''';
+
+final _defaultConcurrency = math.max(1, Platform.numberOfProcessors ~/ 2);
+
+final _usage = '''
+Usage: dart test [files or directories...]
+
+-h, --help                            Show this usage information.
+    --version                         Show the package:test version.
+
+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.
+-N, --plain-name                      A plain-text substring of the name of the test to run.
+                                      If passed multiple times, tests must match all substrings.
+-t, --tags                            Run only tests with all of the specified tags.
+                                      Supports boolean selector syntax.
+-x, --exclude-tags                    Don't run tests with any of the specified tags.
+                                      Supports boolean selector syntax.
+    --[no-]run-skipped                Run skipped tests instead of skipping them.
+
+Running Tests:
+-p, --platform                        The platform(s) on which to run the tests.
+                                      $_runtimes.
+                                      Each platform supports the following compilers:
+$_runtimeCompilers
+-c, --compiler                        The compiler(s) to use to run tests, supported compilers are [dart2js, dart2wasm, exe, kernel, source].
+                                      Each platform has a default compiler but may support other compilers.
+                                      You can target a compiler to a specific platform using arguments of the following form [<platform-selector>:]<compiler>.
+                                      If a platform is specified but no given compiler is supported for that platform, then it will use its default compiler.
+-P, --preset                          The configuration preset(s) to use.
+-j, --concurrency=<threads>           The number of concurrent test suites run.
+                                      (defaults to "$_defaultConcurrency")
+    --total-shards                    The total number of invocations of the test runner being run.
+    --shard-index                     The index of this test runner invocation (of --total-shards).
+    --timeout                         The default test timeout. For example: 15s, 2x, none
+                                      (defaults to "30s")
+    --ignore-timeouts                 Ignore all timeouts (useful if debugging)
+    --pause-after-load                Pause for debugging before any tests execute.
+                                      Implies --concurrency=1, --debug, and --ignore-timeouts.
+                                      Currently only supported for browser tests.
+    --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         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.
+    --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.
+    --[no-]fail-fast                  Stop running tests after the first failure.
+
+Output:
+-r, --reporter=<option>               Set how to print test results.
+
+          [compact]                   A single line, updated continuously.
+          [expanded] (default)        A separate line for each update.
+          [failures-only]             A separate line for failing tests with no output for passing tests
+          [github]                    A custom reporter for GitHub Actions (the default reporter when running on GitHub Actions).
+          [json]                      A machine-readable format (see https://dart.dev/go/test-docs/json_reporter.md).
+          [silent]                    A reporter with no output. May be useful when only the exit code is meaningful.
+
+    --file-reporter                   Enable an additional reporter writing 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 _runtimes = '[vm (default), chrome, firefox'
+    '${Platform.isMacOS ? ', safari' : ''}'
+    ', edge, node]';
+
+final _runtimeCompilers = [
+  '[vm]: kernel (default), source, exe',
+  '[chrome]: dart2js (default), dart2wasm',
+  '[firefox]: dart2js (default), dart2wasm',
+  if (Platform.isMacOS) '[safari]: dart2js (default)',
+  '[edge]: dart2js (default)',
+  '[node]: dart2js (default), dart2wasm',
+].map((str) => '                                      $str').join('\n');
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('prints help information', () async {
+    var test = await runTest(['--help']);
+    expectStdoutEquals(test, '''
+Runs tests in this package.
+
+$_usage''');
+    await test.shouldExit(0);
+  });
+
+  group('fails gracefully if', () {
+    test('an invalid option is passed', () async {
+      var test = await runTest(['--asdf']);
+      expectStderrEquals(test, '''
+Could not find an option named "--asdf".
+
+$_usage''');
+      await test.shouldExit(exit_codes.usage);
+    });
+
+    test('a non-existent file is passed', () async {
+      var test = await runTest(['file']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading file [E]',
+            'Failed to load "file": Does not exist.'
+          ]));
+      await test.shouldExit(1);
+    });
+
+    test("the default directory doesn't exist", () async {
+      var test = await runTest([]);
+      expectStderrEquals(test, '''
+No test files were passed and the default "test/" directory doesn't exist.
+
+$_usage''');
+      await test.shouldExit(exit_codes.data);
+    });
+
+    test('a test file fails to load', () async {
+      await d.file('test.dart', 'invalid Dart file').create();
+      var test = await runTest(['test.dart']);
+
+      expect(
+          test.stdout,
+          containsInOrder([
+            'Failed to load "test.dart":',
+            "test.dart:1:9: Error: Expected ';' after this.",
+            'invalid Dart file'
+          ]));
+
+      await test.shouldExit(1);
+    });
+
+    // This syntax error is detected lazily, and so requires some extra
+    // machinery to support.
+    test('a test file fails to parse due to a missing semicolon', () async {
+      await d.file('test.dart', 'void main() {foo}').create();
+      var test = await runTest(['test.dart']);
+
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart":',
+            "test.dart:1:14: Error: Expected ';' after this"
+          ]));
+
+      await test.shouldExit(1);
+    });
+
+    // This is slightly different from the above test because it's an error
+    // that's caught first by the analyzer when it's used to parse the file.
+    test('a test file fails to parse', () async {
+      await d.file('test.dart', '@TestOn)').create();
+      var test = await runTest(['test.dart']);
+
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart":',
+            "test.dart:1:8: Error: Expected a declaration, but got ')'",
+            '@TestOn)',
+          ]));
+
+      await test.shouldExit(1);
+    });
+
+    test("an annotation's contents are invalid", () async {
+      await d.file('test.dart', "@TestOn('zim')\nlibrary foo;").create();
+      var test = await runTest(['test.dart']);
+
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart":',
+            'Error on line 1, column 10: Undefined variable.',
+            "@TestOn('zim')",
+            '         ^^^'
+          ]));
+      await test.shouldExit(1);
+    });
+
+    test('a test file throws', () async {
+      await d.file('test.dart', "void main() => throw 'oh no';").create();
+      var test = await runTest(['test.dart']);
+
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart": oh no'
+          ]));
+      await test.shouldExit(1);
+    });
+
+    test("a test file doesn't have a main defined", () async {
+      await d.file('test.dart', 'void foo() {}').create();
+      var test = await runTest(['test.dart']);
+
+      expect(
+          test.stdout,
+          emitsThrough(
+            contains('-1: loading test.dart [E]'),
+          ));
+      expect(
+          test.stdout,
+          emitsThrough(anyOf([
+            contains("Error: Getter not found: 'main'"),
+            contains("Error: Undefined name 'main'"),
+          ])));
+
+      await test.shouldExit(1);
+    });
+
+    test('a test file has a non-function main', () async {
+      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,
+          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);
+    });
+
+    test('a test file has a main with arguments', () async {
+      await d.file('test.dart', 'void main(arg) {}').create();
+      var test = await runTest(['test.dart']);
+
+      expect(
+          test.stdout,
+          containsInOrder([
+            '-1: loading test.dart [E]',
+            'Failed to load "test.dart": Top-level main() function takes arguments.'
+          ]));
+      await test.shouldExit(1);
+    });
+
+    test('multiple load errors occur', () async {
+      await d.file('test.dart', 'invalid Dart file').create();
+      var test = await runTest(['test.dart', 'nonexistent.dart']);
+
+      expect(
+          await test.stdoutStream().toList(),
+          containsAll([
+            contains('loading nonexistent.dart [E]'),
+            contains('Failed to load "nonexistent.dart": Does not exist'),
+            contains('loading test.dart [E]'),
+            contains('Failed to load "test.dart"'),
+          ]));
+
+      await test.shouldExit(1);
+    });
+
+    // TODO(nweiz): test what happens when a test file is unreadable once issue
+    // 15078 is fixed.
+  });
+
+  group('runs successful tests', () {
+    test('defined in a single file', () async {
+      await d.file('test.dart', _success).create();
+      var test = await runTest(['test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('defined in a directory', () async {
+      for (var i = 0; i < 3; i++) {
+        await d.file('${i}_test.dart', _success).create();
+      }
+
+      var test = await runTest(['.']);
+      expect(test.stdout, emitsThrough(contains('+3: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('defaulting to the test directory', () async {
+      await d
+          .dir(
+              'test',
+              Iterable.generate(3, (i) {
+                return d.file('${i}_test.dart', _success);
+              }))
+          .create();
+
+      var test = await runTest([]);
+      expect(test.stdout, emitsThrough(contains('+3: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('directly', () async {
+      await d.file('test.dart', _success).create();
+      var test = await runDart(['test.dart']);
+
+      expect(test.stdout, emitsThrough(contains('All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    // Regression test; this broke in 0.12.0-beta.9.
+    test('on a file in a subdirectory', () async {
+      await d.dir('dir', [d.file('test.dart', _success)]).create();
+
+      var test = await runTest(['dir/test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('given a file: uri', () async {
+      await d.file('test.dart', _success).create();
+      var fileUri = p.toUri(d.path('test.dart')).toString();
+      expect(fileUri, startsWith('file:///'));
+      var test = await runTest([fileUri]);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('with platform specific relative paths', () async {
+      await d.dir('foo', [d.file('test.dart', _success)]).create();
+      var test = await runTest([p.join('foo', 'test.dart')]);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('with platform specific absolute paths', () async {
+      await d.dir('foo', [d.file('test.dart', _success)]).create();
+      var test = await runTest([d.path(p.join('foo', 'test.dart'))]);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('with platform specific relative paths containing query params',
+        () async {
+      await d.dir('foo', [d.file('test.dart', _success)]).create();
+      var test = await runTest(['${p.join('foo', 'test.dart')}?line=6']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+  });
+
+  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('respects the chain-stack-traces flag', () async {
+      await d.file('test.dart', _asyncFailure).create();
+
+      var test = await runTest(['test.dart', '--chain-stack-traces']);
+      expect(test.stdout, emitsThrough(contains('asynchronous gap')));
+      await test.shouldExit(1);
+    });
+
+    test('defaults to not chaining stack traces', () async {
+      await d.file('test.dart', _asyncFailure).create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '00:00 +0: failure',
+            '00:00 +0 -1: failure [E]',
+            'oh no',
+            'test.dart 8:7  main.<fn>.<fn>',
+          ]));
+      await test.shouldExit(1);
+    });
+
+    test('defined in a single file', () async {
+      await d.file('test.dart', _failure).create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdout, emitsThrough(contains('-1: Some tests failed.')));
+      await test.shouldExit(1);
+    });
+
+    test('defined in a directory', () async {
+      for (var i = 0; i < 3; i++) {
+        await d.file('${i}_test.dart', _failure).create();
+      }
+
+      var test = await runTest(['.']);
+      expect(test.stdout, emitsThrough(contains('-3: Some tests failed.')));
+      await test.shouldExit(1);
+    });
+
+    test('defaulting to the test directory', () async {
+      await d
+          .dir(
+              'test',
+              Iterable.generate(3, (i) {
+                return d.file('${i}_test.dart', _failure);
+              }))
+          .create();
+
+      var test = await runTest([]);
+      expect(test.stdout, emitsThrough(contains('-3: Some tests failed.')));
+      await test.shouldExit(1);
+    });
+
+    test('directly', () async {
+      await d.file('test.dart', _failure).create();
+      var test = await runDart(['test.dart']);
+      expect(test.stdout, emitsThrough(contains('Some tests failed.')));
+      await test.shouldExit(255);
+    });
+  });
+
+  test('runs tests even when a file fails to load', () async {
+    await d.file('test.dart', _success).create();
+
+    var test = await runTest(['test.dart', 'nonexistent.dart']);
+    expect(test.stdout, emitsThrough(contains('+1 -1: Some tests failed.')));
+    await test.shouldExit(1);
+  });
+
+  group('with a top-level @Skip declaration', () {
+    setUp(() async {
+      await d.file('test.dart', '''
+        @Skip()
+
+        import 'dart:async';
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test('success', () {});
+          test('explicitly unskipped', skip: false, () {});
+        }
+      ''').create();
+    });
+
+    test('skips all tests', () async {
+      var test = await runTest(['test.dart']);
+      expect(test.stdout, emitsThrough(contains('+0 ~1: All tests skipped.')));
+      await test.shouldExit(0);
+    });
+
+    test('runs all tests with --run-skipped', () async {
+      var test = await runTest(['--run-skipped', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('+2: All tests passed!')));
+      await test.shouldExit(0);
+    });
+  });
+
+  group('with onPlatform', () {
+    test('respects matching Skips', () async {
+      await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("fail", () => throw 'oh no', onPlatform: {"vm": Skip()});
+}
+''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdout, emitsThrough(contains('+0 ~1: All tests skipped.')));
+      await test.shouldExit(0);
+    });
+
+    test('ignores non-matching Skips', () async {
+      await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {}, onPlatform: {"chrome": Skip()});
+}
+''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('respects matching Timeouts', () async {
+      await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("fail", () async {
+    await Future.delayed(Duration.zero);
+    throw 'oh no';
+  }, onPlatform: {
+    "vm": Timeout(Duration.zero)
+  });
+}
+''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder(
+              ['Test timed out after 0 seconds.', '-1: Some tests failed.']));
+      await test.shouldExit(1);
+    });
+
+    test('ignores non-matching Timeouts', () async {
+      await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {}, onPlatform: {
+    "chrome": Timeout(Duration(seconds: 0))
+  });
+}
+''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('applies matching platforms in order', () async {
+      await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {}, onPlatform: {
+    "vm": Skip("first"),
+    "vm || windows": Skip("second"),
+    "vm || linux": Skip("third"),
+    "vm || mac-os": Skip("fourth"),
+    "vm || android": Skip("fifth")
+  });
+}
+''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdoutStream(), neverEmits(contains('Skip: first')));
+      expect(test.stdoutStream(), neverEmits(contains('Skip: second')));
+      expect(test.stdoutStream(), neverEmits(contains('Skip: third')));
+      expect(test.stdoutStream(), neverEmits(contains('Skip: fourth')));
+      expect(test.stdout, emitsThrough(contains('Skip: fifth')));
+      await test.shouldExit(0);
+    });
+
+    test('applies platforms to a group', () async {
+      await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  group("group", () {
+    test("success", () {});
+  }, onPlatform: {
+    "vm": Skip()
+  });
+}
+''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdout, emitsThrough(contains('All tests skipped.')));
+      await test.shouldExit(0);
+    });
+  });
+
+  group('with an @OnPlatform annotation', () {
+    test('respects matching Skips', () async {
+      await d.file('test.dart', '''
+@OnPlatform(const {"vm": const Skip()})
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("fail", () => throw 'oh no');
+}
+''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdout, emitsThrough(contains('+0 ~1: All tests skipped.')));
+      await test.shouldExit(0);
+    });
+
+    test('ignores non-matching Skips', () async {
+      await d.file('test.dart', '''
+@OnPlatform(const {"chrome": const Skip()})
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {});
+}
+''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('respects matching Timeouts', () async {
+      await d.file('test.dart', '''
+@OnPlatform(const {
+  "vm": const Timeout(const Duration(seconds: 0))
+})
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("fail", () async {
+    await Future.delayed(Duration.zero);
+    throw 'oh no';
+  });
+}
+''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder(
+              ['Test timed out after 0 seconds.', '-1: Some tests failed.']));
+      await test.shouldExit(1);
+    });
+
+    test('ignores non-matching Timeouts', () async {
+      await d.file('test.dart', '''
+@OnPlatform(const {
+  "chrome": const Timeout(const Duration(seconds: 0))
+})
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("success", () {});
+}
+''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+  });
+
+  test('with the --color flag, uses colors', () async {
+    await d.file('test.dart', _failure).create();
+    var test = await runTest(['--color', 'test.dart']);
+    // This is the color code for red.
+    expect(test.stdout, emitsThrough(contains('\u001b[31m')));
+    await test.shouldExit();
+  });
+
+  group('runs tests successfully more than once when calling runTests', () {
+    test('defined in a single file', () async {
+      await d.file('test.dart', _success).create();
+      await d.file('runner.dart', '''
+import 'package:test_core/src/executable.dart' as test;
+
+void main(List<String> args) async {
+  await test.runTests(args);
+  await test.runTests(args);
+  test.completeShutdown();
+}''').create();
+      var test = await runDart([
+        'runner.dart',
+        '--no-color',
+        '--reporter',
+        'compact',
+        '--',
+        'test.dart',
+      ], description: 'dart runner.dart -- test.dart');
+      expect(
+          test.stdout,
+          emitsThrough(containsInOrder([
+            '+0: loading test.dart',
+            '+0: success',
+            '+1: success',
+            'All tests passed!'
+          ])));
+      expect(
+          test.stdout,
+          emitsThrough(containsInOrder([
+            '+0: loading test.dart',
+            '+0: success',
+            '+1: success',
+            '+1: All tests passed!',
+          ])));
+      await test.shouldExit(0);
+    });
+  }, onPlatform: const {
+    'windows': Skip('https://github.com/dart-lang/test/issues/1615')
+  });
+
+  group('language experiments', () {
+    group('are inherited from the executable arguments', () {
+      setUp(() async {
+        await d.file('test.dart', '''
+// @dart=2.10
+import 'package:test/test.dart';
+
+// Compile time error if the experiment is enabled
+int x;
+
+void main() {
+  test('x is null', () {
+    expect(x, isNull);
+  });
+}
+''').create();
+      });
+
+      for (var platform in ['vm', 'chrome']) {
+        test('on the $platform platform', () async {
+          var test = await runTest(['test.dart', '-p', platform],
+              vmArgs: ['--enable-experiment=non-nullable']);
+
+          await expectLater(test.stdout, emitsThrough(contains('int x;')));
+          await test.shouldExit(1);
+
+          // Test that they can be removed on subsequent runs as well
+          test = await runTest(['test.dart', '-p', platform]);
+          await expectLater(
+              test.stdout, emitsThrough(contains('+1: All tests passed!')));
+          await test.shouldExit(0);
+        }, skip: 'https://github.com/dart-lang/test/issues/1813');
+      }
+    });
+  });
+
+  group('runs tests after changing directories', () {
+    setUp(() async {
+      await d.file('a_test.dart', '''
+@TestOn('vm')
+import 'dart:io';
+
+import 'package:test/test.dart';
+
+void main() {
+  test('changes directory', () {
+    Directory.current = Directory.current.parent;
+  });
+}
+''').create();
+      await d.file('b_test.dart', '''
+import 'package:test/test.dart';
+
+void main() {
+  test('passes', () {
+    expect(true, true);
+  });
+}
+''').create();
+    });
+    test('on the VM platform', () async {
+      var test = await runTest(['-p', 'vm', 'a_test.dart', 'b_test.dart']);
+      await expectLater(
+          test.stdout, emitsThrough(contains('+2: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('on the browser platform', () async {
+      var test =
+          await runTest(['-p', 'vm,chrome', 'a_test.dart', 'b_test.dart']);
+      await expectLater(
+          test.stdout, emitsThrough(contains('+3: All tests passed!')));
+      await test.shouldExit(0);
+    }, skip: 'https://github.com/dart-lang/test/issues/1803');
+  });
+}
diff --git a/pkgs/test/test/runner/set_up_all_test.dart b/pkgs/test/test/runner/set_up_all_test.dart
new file mode 100644
index 0000000..d1259e1
--- /dev/null
+++ b/pkgs/test/test/runner/set_up_all_test.dart
@@ -0,0 +1,98 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('an error causes the run to fail', () async {
+    await d.file('test.dart', r'''
+        import 'package:test/test.dart';
+
+        void main() {
+          setUpAll(() => throw "oh no");
+
+          test("test", () {});
+        }
+        ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, emitsThrough(contains('-1: (setUpAll) [E]')));
+    expect(test.stdout, emitsThrough(contains('-1: Some tests failed.')));
+    await test.shouldExit(1);
+  });
+
+  test("doesn't run if no tests in the group are selected", () async {
+    await d.file('test.dart', r'''
+        import 'package:test/test.dart';
+
+        void main() {
+          group("with setUpAll", () {
+            setUpAll(() => throw "oh no");
+
+            test("test", () {});
+          });
+
+          group("without setUpAll", () {
+            test("test", () {});
+          });
+        }
+        ''').create();
+
+    var test = await runTest(['test.dart', '--name', 'without']);
+    expect(test.stdout, neverEmits(contains('(setUpAll)')));
+    await test.shouldExit(0);
+  });
+
+  test("doesn't run if no tests in the group match the platform", () async {
+    await d.file('test.dart', r'''
+        import 'package:test/test.dart';
+
+        void main() {
+          group("group1", () {
+            setUpAll(() => throw "oh no");
+
+            test("with", () {}, testOn: "browser");
+          });
+
+          group("group2", () {
+            test("without", () {});
+          });
+        }
+        ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, neverEmits(contains('(setUpAll)')));
+    await test.shouldExit(0);
+  });
+
+  test("doesn't run if the group doesn't match the platform", () async {
+    await d.file('test.dart', r'''
+        import 'package:test/test.dart';
+
+        void main() {
+          group("group1", () {
+            setUpAll(() => throw "oh no");
+
+            test("with", () {});
+          }, testOn: "browser");
+
+          group("group2", () {
+            test("without", () {});
+          });
+        }
+        ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, neverEmits(contains('(setUpAll)')));
+    await test.shouldExit(0);
+  });
+}
diff --git a/pkgs/test/test/runner/shard_test.dart b/pkgs/test/test/runner/shard_test.dart
new file mode 100644
index 0000000..8d4be86
--- /dev/null
+++ b/pkgs/test/test/runner/shard_test.dart
@@ -0,0 +1,183 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+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;
+
+import '../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('divides all the tests among the available shards', () async {
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("test 1", () {});
+        test("test 2", () {});
+        test("test 3", () {});
+        test("test 4", () {});
+        test("test 5", () {});
+        test("test 6", () {});
+        test("test 7", () {});
+        test("test 8", () {});
+        test("test 9", () {});
+        test("test 10", () {});
+      }
+    ''').create();
+
+    var test =
+        await runTest(['test.dart', '--shard-index=0', '--total-shards=3']);
+    expect(
+        test.stdout,
+        containsInOrder([
+          '+0: test 1',
+          '+1: test 2',
+          '+2: test 3',
+          '+3: All tests passed!'
+        ]));
+    await test.shouldExit(0);
+
+    test = await runTest(['test.dart', '--shard-index=1', '--total-shards=3']);
+    expect(
+        test.stdout,
+        containsInOrder([
+          '+0: test 4',
+          '+1: test 5',
+          '+2: test 6',
+          '+3: test 7',
+          '+4: All tests passed!'
+        ]));
+    await test.shouldExit(0);
+
+    test = await runTest(['test.dart', '--shard-index=2', '--total-shards=3']);
+    expect(
+        test.stdout,
+        containsInOrder([
+          '+0: test 8',
+          '+1: test 9',
+          '+2: test 10',
+          '+3: All tests passed!'
+        ]));
+    await test.shouldExit(0);
+  });
+
+  test('shards each suite', () async {
+    await d.file('1_test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("test 1.1", () {});
+        test("test 1.2", () {});
+        test("test 1.3", () {});
+      }
+    ''').create();
+
+    await d.file('2_test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("test 2.1", () {});
+        test("test 2.2", () {});
+        test("test 2.3", () {});
+      }
+    ''').create();
+
+    var test = await runTest(['.', '--shard-index=0', '--total-shards=3']);
+    expect(
+        test.stdout,
+        emitsInOrder([
+          emitsAnyOf([
+            containsInOrder(
+                ['+0: ./1_test.dart: test 1.1', '+1: ./2_test.dart: test 2.1']),
+            containsInOrder(
+                ['+0: ./2_test.dart: test 2.1', '+1: ./1_test.dart: test 1.1'])
+          ]),
+          contains('+2: All tests passed!')
+        ]));
+    await test.shouldExit(0);
+
+    test = await runTest(['.', '--shard-index=1', '--total-shards=3']);
+    expect(
+        test.stdout,
+        emitsInOrder([
+          emitsAnyOf([
+            containsInOrder(
+                ['+0: ./1_test.dart: test 1.2', '+1: ./2_test.dart: test 2.2']),
+            containsInOrder(
+                ['+0: ./2_test.dart: test 2.2', '+1: ./1_test.dart: test 1.2'])
+          ]),
+          contains('+2: All tests passed!')
+        ]));
+    await test.shouldExit(0);
+
+    test = await runTest(['.', '--shard-index=2', '--total-shards=3']);
+    expect(
+        test.stdout,
+        emitsInOrder([
+          emitsAnyOf([
+            containsInOrder(
+                ['+0: ./1_test.dart: test 1.3', '+1: ./2_test.dart: test 2.3']),
+            containsInOrder(
+                ['+0: ./2_test.dart: test 2.3', '+1: ./1_test.dart: test 1.3'])
+          ]),
+          contains('+2: All tests passed!')
+        ]));
+    await test.shouldExit(0);
+  });
+
+  test('an empty shard reports success', () async {
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("test 1", () {});
+        test("test 2", () {});
+      }
+    ''').create();
+
+    var test =
+        await runTest(['test.dart', '--shard-index=1', '--total-shards=3']);
+    expect(test.stdout, emitsThrough('No tests ran.'));
+    await test.shouldExit(79);
+  });
+
+  group('reports an error if', () {
+    test('--shard-index is provided alone', () async {
+      var test = await runTest(['--shard-index=1']);
+      expect(
+          test.stderr,
+          emits(
+              '--shard-index and --total-shards may only be passed together.'));
+      await test.shouldExit(exit_codes.usage);
+    });
+
+    test('--total-shards is provided alone', () async {
+      var test = await runTest(['--total-shards=5']);
+      expect(
+          test.stderr,
+          emits(
+              '--shard-index and --total-shards may only be passed together.'));
+      await test.shouldExit(exit_codes.usage);
+    });
+
+    test('--shard-index is negative', () async {
+      var test = await runTest(['--shard-index=-1', '--total-shards=5']);
+      expect(test.stderr, emits('--shard-index may not be negative.'));
+      await test.shouldExit(exit_codes.usage);
+    });
+
+    test('--shard-index is equal to --total-shards', () async {
+      var test = await runTest(['--shard-index=5', '--total-shards=5']);
+      expect(test.stderr,
+          emits('--shard-index must be less than --total-shards.'));
+      await test.shouldExit(exit_codes.usage);
+    });
+  });
+}
diff --git a/pkgs/test/test/runner/signal_test.dart b/pkgs/test/test/runner/signal_test.dart
new file mode 100644
index 0000000..d14ee97
--- /dev/null
+++ b/pkgs/test/test/runner/signal_test.dart
@@ -0,0 +1,236 @@
+// 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.
+
+// Windows doesn't support sending signals.
+@TestOn('vm && !windows')
+library;
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+import 'package:test_process/test_process.dart';
+
+import '../io.dart';
+
+String get _tempDir => p.join(d.sandbox, 'tmp');
+
+// This test is inherently prone to race conditions. If it fails, it will likely
+// do so flakily, but if it succeeds, it will succeed consistently. The tests
+// represent a best effort to kill the test runner at certain times during its
+// execution.
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  setUp(() => d.dir('tmp').create());
+
+  group('during loading,', () {
+    test('cleans up if killed while loading a VM test', () async {
+      await d.file('test.dart', '''
+void main() {
+  print("in test.dart");
+  // Spin for a long time so the test is probably killed while still loading.
+  for (var i = 0; i < 100000000; i++) {}
+}
+''').create();
+
+      var test = await _runTest(['test.dart']);
+      await expectLater(test.stdout, emitsThrough('in test.dart'));
+      await signalAndQuit(test);
+
+      expectTempDirEmpty();
+    });
+
+    test('cleans up if killed while loading a browser test', () async {
+      await d.file('test.dart', 'void main() {}').create();
+
+      var test = await _runTest(['-p', 'chrome', 'test.dart']);
+      await expectLater(
+          test.stdout, emitsThrough(endsWith('loading test.dart')));
+      await signalAndQuit(test);
+
+      expectTempDirEmpty(skip: 'Failing on Travis.');
+    }, tags: 'chrome');
+
+    test('exits immediately if ^C is sent twice', () async {
+      await d.file('test.dart', '''
+void main() {
+  print("in test.dart");
+  while (true) {}
+}
+''').create();
+
+      var test = await _runTest(['test.dart']);
+      await expectLater(test.stdout, emitsThrough('in test.dart'));
+      test.signal(ProcessSignal.sigterm);
+
+      // TODO(nweiz): Sending two signals in close succession can cause the
+      // second one to be ignored, so we wait a bit before the second
+      // one. Remove this hack when issue 23047 is fixed.
+      await Future<void>.delayed(const Duration(seconds: 1));
+
+      await signalAndQuit(test);
+    });
+  });
+
+  group('during test running', () {
+    test('waits for a VM test to finish running', () async {
+      await d.file('test.dart', '''
+import 'dart:async';
+import 'dart:io';
+
+import 'package:test/test.dart';
+
+void main() {
+  tearDownAll(() {
+    File("output_all").writeAsStringSync("ran tearDownAll");
+  });
+
+  tearDown(() => File("output").writeAsStringSync("ran tearDown"));
+
+  test("test", () {
+    print("running test");
+    return Future.delayed(Duration(seconds: 1));
+  });
+}
+''').create();
+
+      var test = await _runTest(['test.dart']);
+      await expectLater(test.stdout, emitsThrough('running test'));
+      await signalAndQuit(test);
+
+      await d.file('output', 'ran tearDown').validate();
+      await d.file('output_all', 'ran tearDownAll').validate();
+      expectTempDirEmpty();
+    });
+
+    test('waits for an active tearDownAll to finish running', () async {
+      await d.file('test.dart', '''
+import 'dart:async';
+import 'dart:io';
+
+import 'package:test/test.dart';
+
+void main() {
+  tearDownAll(() async {
+    print("running tearDownAll");
+    await Future.delayed(Duration(seconds: 1));
+    File("output").writeAsStringSync("ran tearDownAll");
+  });
+
+  test("test", () {});
+}
+''').create();
+
+      var test = await _runTest(['test.dart']);
+      await expectLater(test.stdout, emitsThrough('running tearDownAll'));
+      await signalAndQuit(test);
+
+      await d.file('output', 'ran tearDownAll').validate();
+      expectTempDirEmpty();
+    });
+
+    test('kills a browser test immediately', () async {
+      await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("test", () {
+    print("running test");
+
+    // Allow an event loop to pass so the preceding print can be handled.
+    return Future(() {
+      // Loop forever so that if the test isn't stopped while running, it never
+      // stops.
+      while (true) {}
+    });
+  });
+}
+''').create();
+
+      var test = await _runTest(['-p', 'chrome', 'test.dart']);
+      await expectLater(test.stdout, emitsThrough('running test'));
+      await signalAndQuit(test);
+
+      expectTempDirEmpty(skip: 'Failing on Travis.');
+    }, tags: 'chrome');
+
+    test('kills a VM test immediately if ^C is sent twice', () async {
+      await d.file('test.dart', '''
+import 'package:test/test.dart';
+
+void main() {
+  test("test", () {
+    print("running test");
+    while (true) {}
+  });
+}
+''').create();
+
+      var test = await _runTest(['test.dart']);
+      await expectLater(test.stdout, emitsThrough('running test'));
+      test.signal(ProcessSignal.sigterm);
+
+      // TODO(nweiz): Sending two signals in close succession can cause the
+      // second one to be ignored, so we wait a bit before the second
+      // one. Remove this hack when issue 23047 is fixed.
+      await Future<void>.delayed(const Duration(seconds: 1));
+      await signalAndQuit(test);
+    });
+
+    test('causes expectAsync() to always throw an error immediately', () async {
+      await d.file('test.dart', '''
+import 'dart:async';
+import 'dart:io';
+
+import 'package:test/test.dart';
+
+void main() {
+  var expectAsyncThrewError = false;
+
+  tearDown(() {
+    File("output").writeAsStringSync(expectAsyncThrewError.toString());
+  });
+
+  test("test", () async {
+    print("running test");
+
+    await Future.delayed(Duration(seconds: 1));
+    try {
+      expectAsync0(() {});
+    } catch (_) {
+      expectAsyncThrewError = true;
+    }
+  });
+}
+''').create();
+
+      var test = await _runTest(['test.dart']);
+      await expectLater(test.stdout, emitsThrough('running test'));
+      await signalAndQuit(test);
+
+      await d.file('output', 'true').validate();
+      expectTempDirEmpty();
+    });
+  });
+}
+
+Future<TestProcess> _runTest(List<String> args, {bool forwardStdio = false}) =>
+    runTest(args,
+        environment: {'_UNITTEST_TEMP_DIR': _tempDir},
+        forwardStdio: forwardStdio);
+
+Future<void> signalAndQuit(TestProcess test) async {
+  test.signal(ProcessSignal.sigterm);
+  await test.shouldExit();
+  await expectLater(test.stderr, emitsDone);
+}
+
+void expectTempDirEmpty({Object? skip}) {
+  expect(Directory(_tempDir).listSync(), isEmpty, skip: skip);
+}
diff --git a/pkgs/test/test/runner/skip_expect_test.dart b/pkgs/test/test/runner/skip_expect_test.dart
new file mode 100644
index 0000000..e22332c
--- /dev/null
+++ b/pkgs/test/test/runner/skip_expect_test.dart
@@ -0,0 +1,270 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  group('a skipped expect', () {
+    test('marks the test as skipped', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("skipped", () => expect(1, equals(2), skip: true));
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(test.stdout, emitsThrough(contains('~1: All tests skipped.')));
+      await test.shouldExit(0);
+    });
+
+    test('prints the skip reason if there is one', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("skipped", () => expect(1, equals(2),
+              reason: "1 is 2", skip: "is failing"));
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '+0: skipped',
+            '  Skip expect: is failing',
+            '~1: All tests skipped.'
+          ]));
+      await test.shouldExit(0);
+    });
+
+    test("prints the expect reason if there's no skip reason", () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("skipped", () => expect(1, equals(2),
+              reason: "1 is 2", skip: true));
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '+0: skipped',
+            '  Skip expect (1 is 2).',
+            '~1: All tests skipped.'
+          ]));
+      await test.shouldExit(0);
+    });
+
+    test('prints the matcher description if there are no reasons', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("skipped", () => expect(1, equals(2), skip: true));
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '+0: skipped',
+            '  Skip expect (<2>).',
+            '~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", () {
+            expect(1, equals(2), skip: true);
+            expect(1, equals(2));
+          });
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder([
+            '+0: failing',
+            '  Skip expect (<2>).',
+            '+0 -1: failing [E]',
+            '  Expected: <2>',
+            '    Actual: <1>',
+            '+0 -1: Some tests failed.'
+          ]));
+      await test.shouldExit(1);
+    });
+  });
+
+  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 a matching library which informs the test runner',
+            'of pending async work.',
+            '+1 -1: Some tests failed.'
+          ]));
+      await test.shouldExit(1);
+    });
+  });
+
+  group('errors', () {
+    test('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();
+              expect(1, equals(2), skip: true);
+            });
+          });
+
+          // 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 a matching library which informs the test runner',
+            'of pending async work.',
+            '+1 -1: Some tests failed.'
+          ]));
+      await test.shouldExit(1);
+    });
+
+    test('when an invalid type is used for skip', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("failing", () {
+            expect(1, equals(2), skip: 10);
+          });
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stdout,
+          containsInOrder(
+              ['Invalid argument (skip)', '+0 -1: Some tests failed.']));
+      await test.shouldExit(1);
+    });
+  });
+}
diff --git a/pkgs/test/test/runner/solo_test.dart b/pkgs/test/test/runner/solo_test.dart
new file mode 100644
index 0000000..87f6f25
--- /dev/null
+++ b/pkgs/test/test/runner/solo_test.dart
@@ -0,0 +1,67 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('only runs the tests marked as solo', () async {
+    await d.file('test.dart', '''
+          import 'dart:async';
+
+          import 'package:test/test.dart';
+
+          void main() {
+            test("passes", () {
+              expect(true, isTrue);
+            }, solo: true);
+            test("failed", () {
+              throw 'some error';
+            });
+          }
+          ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, emitsThrough(contains('+1 ~1: All tests passed!')));
+    await test.shouldExit(0);
+  });
+
+  test('only runs groups marked as solo', () async {
+    await d.file('test.dart', '''
+          import 'dart:async';
+
+          import 'package:test/test.dart';
+
+          void main() {
+            group('solo', () {
+              test("first pass", () {
+                expect(true, isTrue);
+              });
+              test("second pass", () {
+                expect(true, isTrue);
+              });
+            }, solo: true);
+            group('no solo', () {
+              test("failure", () {
+                throw 'some error';
+              });
+              test("another failure", () {
+                throw 'some error';
+              });
+            });
+          }
+          ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, emitsThrough(contains('+2 ~1: All tests passed!')));
+    await test.shouldExit(0);
+  });
+}
diff --git a/pkgs/test/test/runner/tag_test.dart b/pkgs/test/test/runner/tag_test.dart
new file mode 100644
index 0000000..da9dca7
--- /dev/null
+++ b/pkgs/test/test/runner/tag_test.dart
@@ -0,0 +1,370 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  setUp(() async {
+    await d.file('test.dart', '''
+      import 'package:test/test.dart';
+
+      void main() {
+        test("no tags", () {});
+        test("a", () {}, tags: "a");
+        test("b", () {}, tags: "b");
+        test("bc", () {}, tags: ["b", "c"]);
+      }
+    ''').create();
+  });
+
+  group('--tags', () {
+    test('runs all tests when no tags are specified', () async {
+      var test = await runTest(['test.dart']);
+      expect(test.stdout, tagWarnings(['a', 'b', 'c']));
+      expect(test.stdout, emitsThrough(contains(': no tags')));
+      expect(test.stdout, emitsThrough(contains(': a')));
+      expect(test.stdout, emitsThrough(contains(': b')));
+      expect(test.stdout, emitsThrough(contains(': bc')));
+      expect(test.stdout, emitsThrough(contains('+4: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('runs a test with only a specified tag', () async {
+      var test = await runTest(['--tags=a', 'test.dart']);
+      expect(test.stdout, tagWarnings(['b', 'c']));
+      expect(test.stdout, emitsThrough(contains(': a')));
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('runs a test with a specified tag among others', () async {
+      var test = await runTest(['--tags=c', 'test.dart']);
+      expect(test.stdout, tagWarnings(['a', 'b']));
+      expect(test.stdout, emitsThrough(contains(': bc')));
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('with multiple tags, runs only tests matching all of them', () async {
+      var test = await runTest(['--tags=b,c', 'test.dart']);
+      expect(test.stdout, tagWarnings(['a']));
+      expect(test.stdout, emitsThrough(contains(': bc')));
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('supports boolean selector syntax', () async {
+      var test = await runTest(['--tags=b || c', 'test.dart']);
+      expect(test.stdout, tagWarnings(['a']));
+      expect(test.stdout, emitsThrough(contains(': b')));
+      expect(test.stdout, emitsThrough(contains(': bc')));
+      expect(test.stdout, emitsThrough(contains('+2: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('prints no warnings when all tags are specified', () async {
+      var test = await runTest(['--tags=a,b,c', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains('No tests ran.')));
+      await test.shouldExit(79);
+    });
+  });
+
+  group('--exclude-tags', () {
+    test("dosn't run a test with only an excluded tag", () async {
+      var test = await runTest(['--exclude-tags=a', 'test.dart']);
+      expect(test.stdout, tagWarnings(['b', 'c']));
+      expect(test.stdout, emitsThrough(contains(': no tags')));
+      expect(test.stdout, emitsThrough(contains(': b')));
+      expect(test.stdout, emitsThrough(contains(': bc')));
+      expect(test.stdout, emitsThrough(contains('+3: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test("doesn't run a test with an excluded tag among others", () async {
+      var test = await runTest(['--exclude-tags=c', 'test.dart']);
+      expect(test.stdout, tagWarnings(['a', 'b']));
+      expect(test.stdout, emitsThrough(contains(': no tags')));
+      expect(test.stdout, emitsThrough(contains(': a')));
+      expect(test.stdout, emitsThrough(contains(': b')));
+      expect(test.stdout, emitsThrough(contains('+3: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test("dosn't load a suite with an excluded tag", () async {
+      await d.file('test.dart', '''
+        @Tags(const ["a"])
+
+        import 'package:test/test.dart';
+
+        void main() {
+          throw "error";
+        }
+      ''').create();
+
+      var test = await runTest(['--exclude-tags=a', 'test.dart']);
+      expect(test.stdout, emits('No tests ran.'));
+      await test.shouldExit(79);
+    });
+
+    test('allows unused tags', () async {
+      var test = await runTest(['--exclude-tags=b,z', 'test.dart']);
+      expect(test.stdout, tagWarnings(['a', 'c']));
+      expect(test.stdout, emitsThrough(contains(': no tags')));
+      expect(test.stdout, emitsThrough(contains(': a')));
+      expect(test.stdout, emitsThrough(contains('+2: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('supports boolean selector syntax', () async {
+      var test = await runTest(['--exclude-tags=b && c', 'test.dart']);
+      expect(test.stdout, tagWarnings(['a']));
+      expect(test.stdout, emitsThrough(contains(': no tags')));
+      expect(test.stdout, emitsThrough(contains(': a')));
+      expect(test.stdout, emitsThrough(contains(': b')));
+      expect(test.stdout, emitsThrough(contains('+3: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('prints no warnings when all tags are specified', () async {
+      var test = await runTest(['--exclude-tags=a,b,c', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains(': no tags')));
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+  });
+
+  group('with a tagged group', () {
+    setUp(() async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          group("a", () {
+            test("in", () {});
+          }, tags: "a");
+
+          test("out", () {});
+        }
+      ''').create();
+    });
+
+    test('includes tags specified on the group', () async {
+      var test = await runTest(['-x', 'a', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains(': out')));
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test('excludes tags specified on the group', () async {
+      var test = await runTest(['-t', 'a', 'test.dart']);
+      expect(test.stdout, emitsThrough(contains(': a in')));
+      expect(test.stdout, emitsThrough(contains('+1: All tests passed!')));
+      await test.shouldExit(0);
+    });
+  });
+
+  test('respects top-level @Tags annotations', () async {
+    await d.file('test.dart', '''
+      @Tags(const ['a'])
+      import 'package:test/test.dart';
+
+      void main() {
+        test("foo", () {});
+      }
+    ''').create();
+
+    var test = await runTest(['-x', 'a', 'test.dart']);
+    expect(test.stdout, emitsThrough(contains('No tests ran')));
+    await test.shouldExit(79);
+  });
+
+  group('warning formatting', () {
+    test('for multiple tags', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("foo", () {}, tags: ["a", "b"]);
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stdout,
+          emitsThrough(lines(
+              'Warning: Tags were used that weren\'t specified in dart_test.yaml.\n'
+              '  a was used in the test "foo"\n'
+              '  b was used in the test "foo"')));
+      await test.shouldExit(0);
+    });
+
+    test('for multiple tests', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("foo", () {}, tags: "a");
+          test("bar", () {}, tags: "a");
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stdout,
+          emitsThrough(lines(
+              'Warning: A tag was used that wasn\'t specified in dart_test.yaml.\n'
+              '  a was used in:\n'
+              '    the test "foo"\n'
+              '    the test "bar"')));
+      await test.shouldExit(0);
+    });
+
+    test('for groups', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          group("group", () {
+            test("foo", () {});
+            test("bar", () {});
+          }, tags: "a");
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stdout,
+          emitsThrough(lines(
+              'Warning: A tag was used that wasn\'t specified in dart_test.yaml.\n'
+              '  a was used in the group "group"')));
+      await test.shouldExit(0);
+    });
+
+    test('for suites', () async {
+      await d.file('test.dart', '''
+        @Tags(const ["a"])
+        import 'package:test/test.dart';
+
+        void main() {
+          test("foo", () {});
+          test("bar", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stdout,
+          emitsThrough(lines(
+              'Warning: A tag was used that wasn\'t specified in dart_test.yaml.\n'
+              '  a was used in the suite itself')));
+      await test.shouldExit(0);
+    });
+
+    test("doesn't double-print a tag warning", () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("foo", () {}, tags: "a");
+        }
+      ''').create();
+
+      var test = await runTest(['-p', 'vm,chrome', 'test.dart']);
+      expect(
+          test.stdout,
+          emitsThrough(lines(
+              'Warning: A tag was used that wasn\'t specified in dart_test.yaml.\n'
+              '  a was used in the test "foo"')));
+      expect(test.stdout, neverEmits(startsWith('Warning:')));
+      await test.shouldExit(0);
+    }, tags: 'chrome');
+  });
+
+  group('invalid tags', () {
+    test('are disallowed by test()', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          test("foo", () {}, tags: "a b");
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stdout,
+          emitsThrough(
+              '  Failed to load "test.dart": Invalid argument(s): Invalid tag "a '
+              'b". Tags must be (optionally hyphenated) Dart identifiers.'));
+      await test.shouldExit(1);
+    });
+
+    test('are disallowed by group()', () async {
+      await d.file('test.dart', '''
+        import 'package:test/test.dart';
+
+        void main() {
+          group("group", () {
+            test("foo", () {});
+          }, tags: "a b");
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stdout,
+          emitsThrough(
+              '  Failed to load "test.dart": Invalid argument(s): Invalid tag "a '
+              'b". Tags must be (optionally hyphenated) Dart identifiers.'));
+      await test.shouldExit(1);
+    });
+
+    test('are disallowed by @Tags()', () async {
+      await d.file('test.dart', '''
+        @Tags(const ["a b"])
+
+        import 'package:test/test.dart';
+
+        void main() {
+          test("foo", () {});
+        }
+      ''').create();
+
+      var test = await runTest(['test.dart']);
+      expect(
+          test.stdout,
+          emitsThrough(lines('  Failed to load "test.dart":\n'
+              '  Error on line 1, column 22: Invalid tag name. Tags must be '
+              '(optionally hyphenated) Dart identifiers.')));
+      await test.shouldExit(1);
+    });
+  });
+}
+
+/// Returns a [StreamMatcher] that asserts that a test emits warnings for [tags]
+/// in order.
+StreamMatcher tagWarnings(List<String> tags) => emitsInOrder([
+      emitsThrough(
+          "Warning: ${tags.length == 1 ? 'A tag was' : 'Tags were'} used that "
+          "${tags.length == 1 ? "wasn't" : "weren't"} specified in "
+          'dart_test.yaml.'),
+
+      for (var tag in tags) emitsThrough(startsWith('  $tag was used in')),
+
+      // Consume until the end of the warning block, and assert that it has no
+      // further tags than the ones we specified.
+      mayEmitMultiple(isNot(anyOf([contains(' was used in'), isEmpty]))),
+      isEmpty,
+    ]);
+
+/// Returns a [StreamMatcher] that matches the lines of [string] in order.
+StreamMatcher lines(String string) => emitsInOrder(string.split('\n'));
diff --git a/pkgs/test/test/runner/tear_down_all_test.dart b/pkgs/test/test/runner/tear_down_all_test.dart
new file mode 100644
index 0000000..93ede4f
--- /dev/null
+++ b/pkgs/test/test/runner/tear_down_all_test.dart
@@ -0,0 +1,98 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('an error causes the run to fail', () async {
+    await d.file('test.dart', r'''
+        import 'package:test/test.dart';
+
+        void main() {
+          tearDownAll(() => throw "oh no");
+
+          test("test", () {});
+        }
+        ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, emitsThrough(contains('-1: (tearDownAll) [E]')));
+    expect(test.stdout, emitsThrough(contains('-1: Some tests failed.')));
+    await test.shouldExit(1);
+  });
+
+  test("doesn't run if no tests in the group are selected", () async {
+    await d.file('test.dart', r'''
+        import 'package:test/test.dart';
+
+        void main() {
+          group("with tearDownAll", () {
+            tearDownAll(() => throw "oh no");
+
+            test("test", () {});
+          });
+
+          group("without tearDownAll", () {
+            test("test", () {});
+          });
+        }
+        ''').create();
+
+    var test = await runTest(['test.dart', '--name', 'without']);
+    expect(test.stdout, neverEmits(contains('(tearDownAll)')));
+    await test.shouldExit(0);
+  });
+
+  test("doesn't run if no tests in the group match the platform", () async {
+    await d.file('test.dart', r'''
+        import 'package:test/test.dart';
+
+        void main() {
+          group("group1", () {
+            tearDownAll(() => throw "oh no");
+
+            test("with", () {}, testOn: "browser");
+          });
+
+          group("group2", () {
+            test("without", () {});
+          });
+        }
+        ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, neverEmits(contains('(tearDownAll)')));
+    await test.shouldExit(0);
+  });
+
+  test("doesn't run if the group doesn't match the platform", () async {
+    await d.file('test.dart', r'''
+        import 'package:test/test.dart';
+
+        void main() {
+          group("group1", () {
+            tearDownAll(() => throw "oh no");
+
+            test("with", () {});
+          }, testOn: "browser");
+
+          group("group2", () {
+            test("without", () {});
+          });
+        }
+        ''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(test.stdout, neverEmits(contains('(tearDownAll)')));
+    await test.shouldExit(0);
+  });
+}
diff --git a/pkgs/test/test/runner/test_chain_test.dart b/pkgs/test/test/runner/test_chain_test.dart
new file mode 100644
index 0000000..ad814de
--- /dev/null
+++ b/pkgs/test/test/runner/test_chain_test.dart
@@ -0,0 +1,84 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:convert';
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  setUp(() async {
+    await d.file('test.dart', '''
+            import 'dart:async';
+
+            import 'package:test/test.dart';
+
+            void main() {
+              test("failure", () async{
+                await Future((){});
+                await Future((){});
+                throw "oh no";
+              });
+            }
+            ''').create();
+  });
+  test('folds packages contained in the except list', () async {
+    await d
+        .file(
+            'dart_test.yaml',
+            jsonEncode({
+              'fold_stack_frames': {
+                'except': ['stream_channel']
+              }
+            }))
+        .create();
+    var test = await runTest(['test.dart']);
+    expect(test.stdoutStream(), neverEmits(contains('package:stream_channel')));
+    await test.shouldExit(1);
+  });
+
+  test('by default folds both stream_channel and test packages', () async {
+    var test = await runTest(['test.dart']);
+    expect(test.stdoutStream(), neverEmits(contains('package:test')));
+    expect(test.stdoutStream(), neverEmits(contains('package:stream_channel')));
+    await test.shouldExit(1);
+  });
+
+  test('folds all packages not contained in the only list', () async {
+    await d
+        .file(
+            'dart_test.yaml',
+            jsonEncode({
+              'fold_stack_frames': {
+                'only': ['test']
+              }
+            }))
+        .create();
+    var test = await runTest(['test.dart']);
+    expect(test.stdoutStream(), neverEmits(contains('package:stream_channel')));
+    await test.shouldExit(1);
+  });
+
+  test('does not fold packages in the only list', () async {
+    await d
+        .file(
+            'dart_test.yaml',
+            jsonEncode({
+              'fold_stack_frames': {
+                'only': ['test_api']
+              }
+            }))
+        .create();
+    var test = await runTest(['test.dart']);
+    expect(test.stdoutStream(), emitsThrough(contains('package:test_api')));
+    await test.shouldExit(1);
+  });
+}
diff --git a/pkgs/test/test/runner/test_on_test.dart b/pkgs/test/test/runner/test_on_test.dart
new file mode 100644
index 0000000..96baf0b
--- /dev/null
+++ b/pkgs/test/test/runner/test_on_test.dart
@@ -0,0 +1,226 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:isolate';
+
+import 'package:package_config/package_config.dart';
+import 'package:test/test.dart';
+import 'package:test_core/src/util/io.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../io.dart';
+
+void main() {
+  late PackageConfig currentPackageConfig;
+
+  setUpAll(() async {
+    await precompileTestExecutable();
+    currentPackageConfig =
+        await loadPackageConfigUri((await Isolate.packageConfig)!);
+  });
+
+  setUp(() async {
+    await d
+        .file('package_config.json',
+            jsonEncode(PackageConfig.toJson(currentPackageConfig)))
+        .create();
+  });
+
+  group('for suite', () {
+    test('runs a test suite on a matching platform', () async {
+      await _writeTestFile('vm_test.dart', suiteTestOn: 'vm');
+
+      var test = await runTest(['vm_test.dart']);
+      expect(test.stdout, emitsThrough(contains('All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test("doesn't run a test suite on a non-matching platform", () async {
+      await _writeTestFile('vm_test.dart', suiteTestOn: 'vm');
+
+      var test = await runTest(['--platform', 'chrome', 'vm_test.dart']);
+      expect(test.stdout, emitsThrough(contains('No tests ran.')));
+      await test.shouldExit(79);
+    }, tags: 'chrome');
+
+    test('runs a test suite on a matching operating system', () async {
+      await _writeTestFile('os_test.dart', suiteTestOn: currentOS.identifier);
+
+      var test = await runTest(['os_test.dart']);
+      expect(test.stdout, emitsThrough(contains('All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test("doesn't run a test suite on a non-matching operating system",
+        () async {
+      await _writeTestFile('os_test.dart',
+          suiteTestOn: otherOS, loadable: false);
+
+      var test = await runTest(['os_test.dart']);
+      expect(test.stdout, emitsThrough(contains('No tests ran.')));
+      await test.shouldExit(79);
+    });
+
+    test('only loads matching files when loading as a group', () async {
+      await _writeTestFile('vm_test.dart', suiteTestOn: 'vm');
+      await _writeTestFile('browser_test.dart',
+          suiteTestOn: 'browser', loadable: false);
+      await _writeTestFile('this_os_test.dart',
+          suiteTestOn: currentOS.identifier);
+      await _writeTestFile('other_os_test.dart',
+          suiteTestOn: otherOS, loadable: false);
+
+      var test = await runTest(['.']);
+      expect(test.stdout, emitsThrough(contains('+2: All tests passed!')));
+      await test.shouldExit(0);
+    });
+  });
+
+  group('for group', () {
+    test('runs a VM group on the VM', () async {
+      await _writeTestFile('vm_test.dart', groupTestOn: 'vm');
+
+      var test = await runTest(['vm_test.dart']);
+      expect(test.stdout, emitsThrough(contains('All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test("doesn't run a Browser group on the VM", () async {
+      await _writeTestFile('browser_test.dart', groupTestOn: 'browser');
+
+      var test = await runTest(['browser_test.dart']);
+      expect(test.stdout, emitsThrough(contains('No tests ran.')));
+      await test.shouldExit(79);
+    });
+
+    test('runs a browser group on a browser', () async {
+      await _writeTestFile('browser_test.dart', groupTestOn: 'browser');
+
+      var test = await runTest(['--platform', 'chrome', 'browser_test.dart']);
+      expect(test.stdout, emitsThrough(contains('All tests passed!')));
+      await test.shouldExit(0);
+    }, tags: 'chrome');
+
+    test("doesn't run a VM group on a browser", () async {
+      await _writeTestFile('vm_test.dart', groupTestOn: 'vm');
+
+      var test = await runTest(['--platform', 'chrome', 'vm_test.dart']);
+      expect(test.stdout, emitsThrough(contains('No tests ran.')));
+      await test.shouldExit(79);
+    }, tags: 'chrome');
+  });
+
+  group('for test', () {
+    test('runs a VM test on the VM', () async {
+      await _writeTestFile('vm_test.dart', testTestOn: 'vm');
+
+      var test = await runTest(['vm_test.dart']);
+      expect(test.stdout, emitsThrough(contains('All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test("doesn't run a browser test on the VM", () async {
+      await _writeTestFile('browser_test.dart', testTestOn: 'browser');
+
+      var test = await runTest(['browser_test.dart']);
+      expect(test.stdout, emitsThrough(contains('No tests ran.')));
+      await test.shouldExit(79);
+    });
+
+    test('runs a browser test on a browser', () async {
+      await _writeTestFile('browser_test.dart', testTestOn: 'browser');
+
+      var test = await runTest(['--platform', 'chrome', 'browser_test.dart']);
+      expect(test.stdout, emitsThrough(contains('All tests passed!')));
+      await test.shouldExit(0);
+    }, tags: 'chrome');
+
+    test("doesn't run a VM test on a browser", () async {
+      await _writeTestFile('vm_test.dart', testTestOn: 'vm');
+
+      var test = await runTest(['--platform', 'chrome', 'vm_test.dart']);
+      expect(test.stdout, emitsThrough(contains('No tests ran.')));
+      await test.shouldExit(79);
+    }, tags: 'chrome');
+  });
+
+  group('with suite, group, and test selectors', () {
+    test('runs the test if all selectors match', () async {
+      await _writeTestFile('vm_test.dart',
+          suiteTestOn: '!browser', groupTestOn: '!js', testTestOn: 'vm');
+
+      var test = await runTest(['vm_test.dart']);
+      expect(test.stdout, emitsThrough(contains('All tests passed!')));
+      await test.shouldExit(0);
+    });
+
+    test("doesn't runs the test if the suite doesn't match", () async {
+      await _writeTestFile('vm_test.dart',
+          suiteTestOn: 'browser', groupTestOn: '!js', testTestOn: 'vm');
+
+      var test = await runTest(['vm_test.dart']);
+      expect(test.stdout, emitsThrough(contains('No tests ran.')));
+      await test.shouldExit(79);
+    });
+
+    test("doesn't runs the test if the group doesn't match", () async {
+      await _writeTestFile('vm_test.dart',
+          suiteTestOn: '!browser', groupTestOn: 'browser', testTestOn: 'vm');
+
+      var test = await runTest(['vm_test.dart']);
+      expect(test.stdout, emitsThrough(contains('No tests ran.')));
+      await test.shouldExit(79);
+    });
+
+    test("doesn't runs the test if the test doesn't match", () async {
+      await _writeTestFile('vm_test.dart',
+          suiteTestOn: '!browser', groupTestOn: '!js', testTestOn: 'browser');
+
+      var test = await runTest(['vm_test.dart']);
+      expect(test.stdout, emitsThrough(contains('No tests ran.')));
+      await test.shouldExit(79);
+    });
+  });
+}
+
+/// Writes a test file with some platform selectors to [filename].
+///
+/// Each of [suiteTestOn], [groupTestOn], and [testTestOn] is a platform
+/// selector that's suite-, group-, and test-level respectively. If [loadable]
+/// is `false`, the test file will be made unloadable on the Dart VM.
+Future<void> _writeTestFile(String filename,
+    {String? suiteTestOn,
+    String? groupTestOn,
+    String? testTestOn,
+    bool loadable = true}) {
+  var buffer = StringBuffer();
+  if (suiteTestOn != null) buffer.writeln("@TestOn('$suiteTestOn')");
+  if (!loadable) buffer.writeln("import 'dart:js_util';");
+
+  buffer
+    ..writeln("import 'package:test/test.dart';")
+    ..writeln('void main() {')
+    ..writeln("  group('group', () {");
+
+  if (testTestOn != null) {
+    buffer.writeln("    test('test', () {}, testOn: '$testTestOn');");
+  } else {
+    buffer.writeln("    test('test', () {});");
+  }
+
+  if (groupTestOn != null) {
+    buffer.writeln("  }, testOn: '$groupTestOn');");
+  } else {
+    buffer.writeln('  });');
+  }
+
+  buffer.writeln('}');
+
+  return d.file(filename, buffer.toString()).create();
+}
diff --git a/pkgs/test/test/runner/timeout_test.dart b/pkgs/test/test/runner/timeout_test.dart
new file mode 100644
index 0000000..6f4b0e7
--- /dev/null
+++ b/pkgs/test/test/runner/timeout_test.dart
@@ -0,0 +1,185 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+
+import '../io.dart';
+
+void main() {
+  setUpAll(precompileTestExecutable);
+
+  test('respects top-level @Timeout declarations', () async {
+    await d.file('test.dart', '''
+@Timeout(const Duration(seconds: 0))
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("timeout", () async {
+    await Future.delayed(Duration.zero);
+  });
+}
+''').create();
+
+    var test = await runTest(['test.dart']);
+    expect(
+        test.stdout,
+        containsInOrder(
+            ['Test timed out after 0 seconds.', '-1: Some tests failed.']));
+    await test.shouldExit(1);
+  });
+
+  test('respects the --timeout flag', () async {
+    await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("timeout", () async {
+    await Future.delayed(Duration.zero);
+  });
+}
+''').create();
+
+    var test = await runTest(['--timeout=0s', 'test.dart']);
+    expect(
+        test.stdout,
+        containsInOrder(
+            ['Test timed out after 0 seconds.', '-1: Some tests failed.']));
+    await test.shouldExit(1);
+  });
+
+  test('timeout is reset with each retry', () async {
+    await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  var runCount = 0;
+  test("timeout", () async {
+    runCount++;
+    if (runCount <=2) {
+      await Future.delayed(Duration(milliseconds: 1000));
+    }
+  }, retry: 3);
+}
+''').create();
+
+    var test = await runTest(['--timeout=400ms', 'test.dart']);
+    expect(
+        test.stdout,
+        containsInOrder([
+          'Test timed out after 0.4 seconds.',
+          'Test timed out after 0.4 seconds.',
+          '+1: All tests passed!'
+        ]));
+    await test.shouldExit(0);
+  });
+
+  test('the --timeout flag applies on top of the default 30s timeout',
+      () async {
+    await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("no timeout", () async {
+    await Future.delayed(Duration(milliseconds: 250));
+  });
+
+  test("timeout", () async {
+    await Future.delayed(Duration(milliseconds: 750));
+  });
+}
+''').create();
+
+    // This should make the timeout about 500ms, which should cause exactly one
+    // test to fail.
+    var test = await runTest(['--timeout=0.016x', 'test.dart']);
+    expect(
+        test.stdout,
+        containsInOrder(
+            ['Test timed out after 0.4 seconds.', '-1: Some tests failed.']));
+    await test.shouldExit(1);
+  });
+
+  test('times out teardown callbacks', () async {
+    await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  tearDown(() async {
+    await Completer<void>().future;
+  });
+
+  test('timeout in teardown', () async {
+    // nothing
+  });
+}
+''').create();
+
+    var test = await runTest(['--timeout=50ms', 'test.dart']);
+    expect(
+        test.stdout,
+        containsInOrder(
+            ['Test timed out after 0 seconds.', '-1: Some tests failed.']));
+    await test.shouldExit(1);
+  });
+
+  test('times out after failing test', () async {
+    await d.file('test.dart', '''
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  tearDown(() async {
+    await Completer<void>().future;
+  });
+
+  test('timeout in teardown', () async {
+    expect(true, false);
+  });
+}
+''').create();
+
+    var test = await runTest(['--timeout=50ms', 'test.dart']);
+    expect(
+        test.stdout,
+        containsInOrder(
+            ['Test timed out after 0 seconds.', '-1: Some tests failed.']));
+    await test.shouldExit(1);
+  });
+
+  test('are ignored with --ignore-timeouts', () async {
+    await d.file('test.dart', '''
+@Timeout(const Duration(seconds: 0))
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+
+void main() {
+  test("timeout", () async {
+    await Future.delayed(Duration(milliseconds: 10));
+  });
+}
+''').create();
+
+    var test = await runTest(['test.dart', '--ignore-timeouts']);
+    expect(test.stdout, containsInOrder(['+1: All tests passed!']));
+    await test.shouldExit(0);
+  });
+}
diff --git a/pkgs/test/test/util/one_off_handler_test.dart b/pkgs/test/test/util/one_off_handler_test.dart
new file mode 100644
index 0000000..c10a7af
--- /dev/null
+++ b/pkgs/test/test/util/one_off_handler_test.dart
@@ -0,0 +1,74 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:test/src/util/one_off_handler.dart';
+import 'package:test/test.dart';
+
+void main() {
+  late OneOffHandler handler;
+  setUp(() => handler = OneOffHandler());
+
+  Future<shelf.Response> handle(shelf.Request request) =>
+      Future.sync(() => handler.handler(request));
+
+  test('returns a 404 for a root URL', () async {
+    var request = shelf.Request('GET', Uri.parse('http://localhost/'));
+    expect((await handle(request)).statusCode, equals(404));
+  });
+
+  test('returns a 404 for an unhandled URL', () async {
+    var request = shelf.Request('GET', Uri.parse('http://localhost/1'));
+    expect((await handle(request)).statusCode, equals(404));
+  });
+
+  test('passes a request to a handler only once', () async {
+    var path = handler.create(expectAsync1((request) {
+      expect(request.method, equals('GET'));
+      return shelf.Response.ok('good job!');
+    }));
+
+    var request = shelf.Request('GET', Uri.parse('http://localhost/$path'));
+    var response = await handle(request);
+    expect(response.statusCode, equals(200));
+    expect(response.readAsString(), completion(equals('good job!')));
+
+    request = shelf.Request('GET', Uri.parse('http://localhost/$path'));
+    expect((await handle(request)).statusCode, equals(404));
+  });
+
+  test('passes requests to the correct handlers', () async {
+    var path1 = handler.create(expectAsync1((request) {
+      expect(request.method, equals('GET'));
+      return shelf.Response.ok('one');
+    }));
+
+    var path2 = handler.create(expectAsync1((request) {
+      expect(request.method, equals('GET'));
+      return shelf.Response.ok('two');
+    }));
+
+    var path3 = handler.create(expectAsync1((request) {
+      expect(request.method, equals('GET'));
+      return shelf.Response.ok('three');
+    }));
+
+    var request = shelf.Request('GET', Uri.parse('http://localhost/$path2'));
+    var response = await handle(request);
+    expect(response.statusCode, equals(200));
+    expect(response.readAsString(), completion(equals('two')));
+
+    request = shelf.Request('GET', Uri.parse('http://localhost/$path1'));
+    response = await handle(request);
+    expect(response.statusCode, equals(200));
+    expect(response.readAsString(), completion(equals('one')));
+
+    request = shelf.Request('GET', Uri.parse('http://localhost/$path3'));
+    response = await handle(request);
+    expect(response.statusCode, equals(200));
+    expect(response.readAsString(), completion(equals('three')));
+  });
+}
diff --git a/pkgs/test/test/util/path_handler_test.dart b/pkgs/test/test/util/path_handler_test.dart
new file mode 100644
index 0000000..9a5d98b
--- /dev/null
+++ b/pkgs/test/test/util/path_handler_test.dart
@@ -0,0 +1,78 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:shelf/shelf.dart' as shelf;
+import 'package:test/src/util/path_handler.dart';
+import 'package:test/test.dart';
+
+void main() {
+  late PathHandler handler;
+  setUp(() => handler = PathHandler());
+
+  Future<shelf.Response> localHandler(shelf.Request request) =>
+      Future.sync(() => handler.handler(request));
+
+  test('returns a 404 for a root URL', () async {
+    var request = shelf.Request('GET', Uri.parse('http://localhost/'));
+    expect((await localHandler(request)).statusCode, equals(404));
+  });
+
+  test('returns a 404 for an unregistered URL', () async {
+    var request = shelf.Request('GET', Uri.parse('http://localhost/foo'));
+    expect((await localHandler(request)).statusCode, equals(404));
+  });
+
+  test('runs a handler for an exact URL', () async {
+    var request = shelf.Request('GET', Uri.parse('http://localhost/foo'));
+    handler.add('foo', expectAsync1((request) {
+      expect(request.handlerPath, equals('/foo'));
+      expect(request.url.path, isEmpty);
+      return shelf.Response.ok('good job!');
+    }));
+
+    var response = await localHandler(request);
+    expect(response.statusCode, equals(200));
+    expect(response.readAsString(), completion(equals('good job!')));
+  });
+
+  test('runs a handler for a suffix', () async {
+    var request = shelf.Request('GET', Uri.parse('http://localhost/foo/bar'));
+    handler.add('foo', expectAsync1((request) {
+      expect(request.handlerPath, equals('/foo/'));
+      expect(request.url.path, 'bar');
+      return shelf.Response.ok('good job!');
+    }));
+
+    var response = await localHandler(request);
+    expect(response.statusCode, equals(200));
+    expect(response.readAsString(), completion(equals('good job!')));
+  });
+
+  test('runs the longest matching handler', () async {
+    var request =
+        shelf.Request('GET', Uri.parse('http://localhost/foo/bar/baz'));
+
+    handler.add(
+        'foo',
+        expectAsync1((_) {
+          return shelf.Response.notFound('fake');
+        }, count: 0));
+    handler.add('foo/bar', expectAsync1((request) {
+      expect(request.handlerPath, equals('/foo/bar/'));
+      expect(request.url.path, 'baz');
+      return shelf.Response.ok('good job!');
+    }));
+    handler.add(
+        'foo/bar/baz/bang',
+        expectAsync1((_) {
+          return shelf.Response.notFound('fake');
+        }, count: 0));
+
+    var response = await localHandler(request);
+    expect(response.statusCode, equals(200));
+    expect(response.readAsString(), completion(equals('good job!')));
+  });
+}
diff --git a/pkgs/test/test/util/string_literal_iterator_test.dart b/pkgs/test/test/util/string_literal_iterator_test.dart
new file mode 100644
index 0000000..03c8e34
--- /dev/null
+++ b/pkgs/test/test/util/string_literal_iterator_test.dart
@@ -0,0 +1,250 @@
+// 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.
+
+@TestOn('vm')
+library;
+
+import 'package:analyzer/dart/analysis/utilities.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:test/test.dart';
+import 'package:test_core/src/util/string_literal_iterator.dart';
+
+final _offset = 'final str = '.length;
+
+void main() {
+  group('returns simple characters in', () {
+    test('a single simple string', () {
+      var iter = _parse('"abc"');
+
+      expect(() => iter.current, throwsA(isA<TypeError>()));
+      expect(iter.offset, equals(_offset));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('a'));
+      expect(iter.offset, equals(_offset + 1));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('b'));
+      expect(iter.offset, equals(_offset + 2));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('c'));
+      expect(iter.offset, equals(_offset + 3));
+
+      expect(iter.moveNext(), isFalse);
+      expect(() => iter.current, throwsA(isA<TypeError>()));
+      expect(iter.offset, equals(_offset + 4));
+    });
+
+    test('a raw string', () {
+      var iter = _parse('r"abc"');
+
+      expect(() => iter.current, throwsA(isA<TypeError>()));
+      expect(iter.offset, equals(_offset + 1));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('a'));
+      expect(iter.offset, equals(_offset + 2));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('b'));
+      expect(iter.offset, equals(_offset + 3));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('c'));
+      expect(iter.offset, equals(_offset + 4));
+
+      expect(iter.moveNext(), isFalse);
+      expect(() => iter.current, throwsA(isA<TypeError>()));
+      expect(iter.offset, equals(_offset + 5));
+    });
+
+    test('a multiline string', () {
+      var iter = _parse('"""ab\ncd"""');
+
+      expect(() => iter.current, throwsA(isA<TypeError>()));
+      expect(iter.offset, equals(_offset + 2));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('a'));
+      expect(iter.offset, equals(_offset + 3));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('b'));
+      expect(iter.offset, equals(_offset + 4));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('\n'));
+      expect(iter.offset, equals(_offset + 5));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('c'));
+      expect(iter.offset, equals(_offset + 6));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('d'));
+      expect(iter.offset, equals(_offset + 7));
+
+      expect(iter.moveNext(), isFalse);
+      expect(() => iter.current, throwsA(isA<TypeError>()));
+      expect(iter.offset, equals(_offset + 8));
+    });
+
+    test('a raw multiline string', () {
+      var iter = _parse('r"""ab\ncd"""');
+
+      expect(() => iter.current, throwsA(isA<TypeError>()));
+      expect(iter.offset, equals(_offset + 3));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('a'));
+      expect(iter.offset, equals(_offset + 4));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('b'));
+      expect(iter.offset, equals(_offset + 5));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('\n'));
+      expect(iter.offset, equals(_offset + 6));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('c'));
+      expect(iter.offset, equals(_offset + 7));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('d'));
+      expect(iter.offset, equals(_offset + 8));
+
+      expect(iter.moveNext(), isFalse);
+      expect(() => iter.current, throwsA(isA<TypeError>()));
+      expect(iter.offset, equals(_offset + 9));
+    });
+
+    test('adjacent strings', () {
+      var iter = _parse('"ab" r"cd" """ef\ngh"""');
+
+      expect(() => iter.current, throwsA(isA<TypeError>()));
+      expect(iter.offset, equals(_offset));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('a'));
+      expect(iter.offset, equals(_offset + 1));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('b'));
+      expect(iter.offset, equals(_offset + 2));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('c'));
+      expect(iter.offset, equals(_offset + 7));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('d'));
+      expect(iter.offset, equals(_offset + 8));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('e'));
+      expect(iter.offset, equals(_offset + 14));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('f'));
+      expect(iter.offset, equals(_offset + 15));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('\n'));
+      expect(iter.offset, equals(_offset + 16));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('g'));
+      expect(iter.offset, equals(_offset + 17));
+
+      expect(iter.moveNext(), isTrue);
+      expect(iter.current, _isRune('h'));
+      expect(iter.offset, equals(_offset + 18));
+
+      expect(iter.moveNext(), isFalse);
+      expect(() => iter.current, throwsA(isA<TypeError>()));
+      expect(iter.offset, equals(_offset + 19));
+    });
+  });
+
+  group('parses an escape sequence for', () {
+    test('a newline', () => _expectEscape(r'\n', '\n'));
+    test('a carriage return', () => _expectEscape(r'\r', '\r'));
+    test('a form feed', () => _expectEscape(r'\f', '\f'));
+    test('a backspace', () => _expectEscape(r'\b', '\b'));
+    test('a tab', () => _expectEscape(r'\t', '\t'));
+    test('a vertical tab', () => _expectEscape(r'\v', '\v'));
+    test('a quote', () => _expectEscape(r'\"', '"'));
+    test('a backslash', () => _expectEscape(r'\\', '\\'));
+
+    test('a hex character', () {
+      _expectEscape(r'\x62', 'b');
+      _expectEscape(r'\x7A', 'z');
+      _expectEscape(r'\x7a', 'z');
+    });
+
+    test('a fixed-length unicode character',
+        () => _expectEscape(r'\u0062', 'b'));
+
+    test('a short variable-length unicode character',
+        () => _expectEscape(r'\u{62}', 'b'));
+
+    test('a long variable-length unicode character',
+        () => _expectEscape(r'\u{000062}', 'b'));
+  });
+
+  group('throws an ArgumentError for', () {
+    test('interpolation', () {
+      expect(() => _parse(r'"$foo"'), throwsArgumentError);
+    });
+
+    test('interpolation in an adjacent string', () {
+      expect(() => _parse(r'"foo" "$bar" "baz"'), throwsArgumentError);
+    });
+  });
+}
+
+/// Asserts that [escape] is parsed as [value].
+void _expectEscape(String escape, String value) {
+  var iter = _parse('"a${escape}b"');
+
+  expect(() => iter.current, throwsA(isA<TypeError>()));
+  expect(iter.offset, equals(_offset));
+
+  expect(iter.moveNext(), isTrue);
+  expect(iter.current, _isRune('a'));
+  expect(iter.offset, equals(_offset + 1));
+
+  expect(iter.moveNext(), isTrue);
+  expect(iter.current, _isRune(value));
+  expect(iter.offset, equals(_offset + 2));
+
+  expect(iter.moveNext(), isTrue);
+  expect(iter.current, _isRune('b'));
+  expect(iter.offset, equals(_offset + escape.length + 2));
+
+  expect(iter.moveNext(), isFalse);
+  expect(() => iter.current, throwsA(isA<TypeError>()));
+  expect(iter.offset, equals(_offset + escape.length + 3));
+}
+
+/// Returns a matcher that asserts that the given rune is the rune for [char].
+Matcher _isRune(String char) {
+  return predicate((rune) {
+    return rune is int && String.fromCharCode(rune) == char;
+  }, 'is the rune "$char"');
+}
+
+/// Parses [dart], which should be a string literal, into a
+/// [StringLiteralIterator].
+StringLiteralIterator _parse(String dart) {
+  var declaration = parseString(content: 'final str = $dart;')
+      .unit
+      .declarations
+      .single as TopLevelVariableDeclaration;
+  var literal = declaration.variables.variables.single.initializer;
+  return StringLiteralIterator(literal as StringLiteral);
+}
diff --git a/pkgs/test/test/utils.dart b/pkgs/test/test/utils.dart
new file mode 100644
index 0000000..23d5b7e
--- /dev/null
+++ b/pkgs/test/test/utils.dart
@@ -0,0 +1,287 @@
+// 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.
+
+import 'dart:async';
+import 'dart:collection';
+
+import 'package:boolean_selector/boolean_selector.dart';
+import 'package:glob/glob.dart';
+import 'package:test/test.dart';
+import 'package:test_api/src/backend/declarer.dart';
+import 'package:test_api/src/backend/group.dart';
+import 'package:test_api/src/backend/group_entry.dart';
+import 'package:test_api/src/backend/live_test.dart';
+import 'package:test_api/src/backend/platform_selector.dart';
+import 'package:test_api/src/backend/runtime.dart';
+import 'package:test_api/src/backend/state.dart';
+import 'package:test_api/src/backend/suite_platform.dart';
+import 'package:test_core/src/runner/application_exception.dart';
+import 'package:test_core/src/runner/compiler_selection.dart';
+import 'package:test_core/src/runner/configuration.dart';
+import 'package:test_core/src/runner/configuration/custom_runtime.dart';
+import 'package:test_core/src/runner/configuration/runtime_settings.dart';
+import 'package:test_core/src/runner/engine.dart';
+import 'package:test_core/src/runner/load_suite.dart';
+import 'package:test_core/src/runner/plugin/environment.dart';
+import 'package:test_core/src/runner/runner_suite.dart';
+import 'package:test_core/src/runner/runtime_selection.dart';
+import 'package:test_core/src/runner/suite.dart';
+
+/// A dummy suite platform to use for testing suites.
+final suitePlatform = SuitePlatform(Runtime.vm, compiler: null);
+
+// The last state change detected via [expectStates].
+State? _lastState;
+
+/// Asserts that exactly [states] will be emitted via [liveTest.onStateChange].
+///
+/// The most recent emitted state is stored in [_lastState].
+void expectStates(LiveTest liveTest, Iterable<State> statesIter) {
+  var states = Queue.of(statesIter);
+  liveTest.onStateChange.listen(expectAsync1((state) {
+    _lastState = state;
+    expect(state, equals(states.removeFirst()));
+  }, count: states.length, max: states.length));
+}
+
+/// Asserts that errors will be emitted via [liveTest.onError] that match
+/// [validators], in order.
+void expectErrors(
+    LiveTest liveTest, Iterable<void Function(Object)> validatorsIter) {
+  var validators = Queue.of(validatorsIter);
+  liveTest.onError.listen(expectAsync1((error) {
+    validators.removeFirst()(error.error);
+  }, count: validators.length, max: validators.length));
+}
+
+/// Asserts that [liveTest] will have a single failure with message `"oh no"`.
+void expectSingleFailure(LiveTest liveTest) {
+  expectStates(liveTest, [
+    const State(Status.running, Result.success),
+    const State(Status.complete, Result.failure)
+  ]);
+
+  expectErrors(liveTest, [
+    (error) {
+      expect(_lastState!.status, equals(Status.complete));
+      expect(error, _isTestFailure('oh no'));
+    }
+  ]);
+}
+
+/// Returns a matcher that matches a [TestFailure] with the given [message].
+///
+/// [message] can be a string or a [Matcher].
+Matcher _isTestFailure(Object message) => const TypeMatcher<TestFailure>()
+    .having((e) => e.message, 'message', message);
+
+/// Returns a matcher that matches a [ApplicationException] with the given
+/// [message].
+///
+/// [message] can be a string or a [Matcher].
+Matcher isApplicationException(Object message) =>
+    const TypeMatcher<ApplicationException>()
+        .having((e) => e.message, 'message', message);
+
+/// Asserts that [liveTest] has completed and passed.
+///
+/// If the test had any errors, they're surfaced nicely into the outer test.
+void expectTestPassed(LiveTest liveTest) {
+  // Since the test is expected to pass, we forward any current or future errors
+  // to the outer test, because they're definitely unexpected.
+  for (var error in liveTest.errors) {
+    registerException(error.error, error.stackTrace);
+  }
+  liveTest.onError.listen((error) {
+    registerException(error.error, error.stackTrace);
+  });
+
+  expect(liveTest.state.status, equals(Status.complete));
+  expect(liveTest.state.result, equals(Result.success));
+}
+
+/// Asserts that [liveTest] failed with a single [TestFailure] whose message
+/// matches [message].
+void expectTestFailed(LiveTest liveTest, Object message) {
+  expect(liveTest.state.status, equals(Status.complete));
+  expect(liveTest.state.result, equals(Result.failure));
+  expect(liveTest.errors, hasLength(1));
+  expect(liveTest.errors.first.error, _isTestFailure(message));
+}
+
+/// Runs [body] with a declarer and returns the declared entries.
+List<GroupEntry> declare(void Function() body) {
+  var declarer = Declarer()..declare(body);
+  return declarer.build().entries;
+}
+
+/// Runs [body] with a declarer and returns an engine that runs those tests.
+Engine declareEngine(
+  void Function() body, {
+  bool runSkipped = false,
+  String? coverage,
+  bool stopOnFirstFailure = false,
+}) {
+  var declarer = Declarer()..declare(body);
+  return Engine.withSuites(
+    [
+      RunnerSuite(
+          const PluginEnvironment(),
+          SuiteConfiguration.runSkipped(runSkipped),
+          declarer.build(),
+          suitePlatform)
+    ],
+    coverage: coverage,
+    stopOnFirstFailure: stopOnFirstFailure,
+  );
+}
+
+/// Returns a [RunnerSuite] with a default environment and configuration.
+RunnerSuite runnerSuite(Group root) => RunnerSuite(
+    const PluginEnvironment(), SuiteConfiguration.empty, root, suitePlatform);
+
+/// Returns a [LoadSuite] with a default configuration.
+LoadSuite loadSuite(String name, FutureOr<RunnerSuite> Function() body) =>
+    LoadSuite(name, SuiteConfiguration.empty, suitePlatform, body);
+
+SuiteConfiguration suiteConfiguration(
+        {bool? allowDuplicateTestNames,
+        bool? allowTestRandomization,
+        bool? jsTrace,
+        bool? runSkipped,
+        Iterable<String>? dart2jsArgs,
+        String? precompiledPath,
+        Iterable<CompilerSelection>? compilerSelections,
+        Iterable<RuntimeSelection>? runtimes,
+        Map<BooleanSelector, SuiteConfiguration>? tags,
+        Map<PlatformSelector, SuiteConfiguration>? onPlatform,
+        bool? ignoreTimeouts,
+
+        // Test-level configuration
+        Timeout? timeout,
+        bool? verboseTrace,
+        bool? chainStackTraces,
+        bool? skip,
+        int? retry,
+        String? skipReason,
+        PlatformSelector? testOn,
+        Iterable<String>? addTags}) =>
+    SuiteConfiguration(
+        allowDuplicateTestNames: allowDuplicateTestNames,
+        allowTestRandomization: allowTestRandomization,
+        jsTrace: jsTrace,
+        runSkipped: runSkipped,
+        dart2jsArgs: dart2jsArgs,
+        precompiledPath: precompiledPath,
+        compilerSelections: compilerSelections,
+        runtimes: runtimes,
+        tags: tags,
+        onPlatform: onPlatform,
+        ignoreTimeouts: ignoreTimeouts,
+        timeout: timeout,
+        verboseTrace: verboseTrace,
+        chainStackTraces: chainStackTraces,
+        skip: skip,
+        retry: retry,
+        skipReason: skipReason,
+        testOn: testOn,
+        addTags: addTags);
+
+Configuration configuration(
+        {bool? help,
+        String? customHtmlTemplatePath,
+        bool? version,
+        bool? pauseAfterLoad,
+        bool? debug,
+        bool? color,
+        String? configurationPath,
+        String? reporter,
+        Map<String, String>? fileReporters,
+        String? coverage,
+        int? concurrency,
+        int? shardIndex,
+        int? totalShards,
+        Map<String, Set<TestSelection>>? testSelections,
+        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? ignoreTimeouts,
+
+        // Suite-level configuration
+        bool? allowDuplicateTestNames,
+        bool? allowTestRandomization,
+        bool? jsTrace,
+        bool? runSkipped,
+        Iterable<String>? dart2jsArgs,
+        String? precompiledPath,
+        Iterable<Pattern>? globalPatterns,
+        Iterable<CompilerSelection>? compilerSelections,
+        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}) =>
+    Configuration(
+        help: help,
+        customHtmlTemplatePath: customHtmlTemplatePath,
+        version: version,
+        pauseAfterLoad: pauseAfterLoad,
+        debug: debug,
+        color: color,
+        configurationPath: configurationPath,
+        reporter: reporter,
+        fileReporters: fileReporters,
+        coverage: coverage,
+        concurrency: concurrency,
+        shardIndex: shardIndex,
+        totalShards: totalShards,
+        testSelections: testSelections,
+        foldTraceExcept: foldTraceExcept,
+        foldTraceOnly: foldTraceOnly,
+        filename: filename,
+        chosenPresets: chosenPresets,
+        presets: presets,
+        overrideRuntimes: overrideRuntimes,
+        defineRuntimes: defineRuntimes,
+        noRetry: noRetry,
+        ignoreTimeouts: ignoreTimeouts,
+        allowDuplicateTestNames: allowDuplicateTestNames,
+        allowTestRandomization: allowTestRandomization,
+        jsTrace: jsTrace,
+        runSkipped: runSkipped,
+        dart2jsArgs: dart2jsArgs,
+        precompiledPath: precompiledPath,
+        globalPatterns: globalPatterns,
+        compilerSelections: compilerSelections,
+        runtimes: runtimes,
+        includeTags: includeTags,
+        excludeTags: excludeTags,
+        tags: tags,
+        onPlatform: onPlatform,
+        testRandomizeOrderingSeed: testRandomizeOrderingSeed,
+        stopOnFirstFailure: false,
+        timeout: timeout,
+        verboseTrace: verboseTrace,
+        chainStackTraces: chainStackTraces,
+        skip: skip,
+        retry: retry,
+        skipReason: skipReason,
+        testOn: testOn,
+        addTags: addTags);
diff --git a/pkgs/test/tool/README.md b/pkgs/test/tool/README.md
new file mode 100644
index 0000000..bd9ee8b
--- /dev/null
+++ b/pkgs/test/tool/README.md
@@ -0,0 +1,7 @@
+`host.dart` is the source for `lib/src/runner/browser/static/host.dart.js`.
+
+To updated it, run:
+
+```console
+dart compile js tool/host.dart -o lib/src/runner/browser/static/host.dart.js
+```
diff --git a/pkgs/test/tool/host.dart b/pkgs/test/tool/host.dart
new file mode 100644
index 0000000..dc3e5ab
--- /dev/null
+++ b/pkgs/test/tool/host.dart
@@ -0,0 +1,252 @@
+// 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.
+
+@JS()
+library;
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:js/js.dart';
+import 'package:stack_trace/stack_trace.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/src/runner/browser/dom.dart' as dom;
+
+/// A class that exposes the test API to JS.
+///
+/// These are exposed so that tools like IDEs can interact with them via remote
+/// debugging.
+@JS()
+@anonymous
+@staticInterop
+class _JSApi {
+  external factory _JSApi(
+      {void Function() resume, void Function() restartCurrent});
+}
+
+extension _JSApiExtension on _JSApi {
+  /// Causes the test runner to resume running, as though the user had clicked
+  /// the "play" button.
+  // ignore: unused_element
+  external Function get resume;
+
+  /// Causes the test runner to restart the current test once it finishes
+  /// running.
+  // ignore: unused_element
+  external Function get restartCurrent;
+}
+
+/// Sets the top-level `dartTest` object so that it's visible to JS.
+@JS('dartTest')
+external set _jsApi(_JSApi api);
+
+/// The iframes created for each loaded test suite, indexed by the suite id.
+final _iframes = <int, dom.HTMLIFrameElement>{};
+
+/// Subscriptions created for each loaded test suite, indexed by the suite id.
+final _subscriptions = <int, StreamSubscription<void>>{};
+final _domSubscriptions = <int, dom.Subscription>{};
+
+/// The URL for the current page.
+final _currentUrl = Uri.parse(dom.window.location.href);
+
+/// Code that runs in the browser and loads test suites at the server's behest.
+///
+/// One instance of this runs for each browser. When the server tells it to load
+/// a test, it starts an iframe pointing at that test's code; from then on, it
+/// just relays messages between the two.
+///
+/// The browser uses two layers of [MultiChannel]s when communicating with the
+/// server:
+///
+///                                       server
+///                                         │
+///                                    (WebSocket)
+///                                         │
+///                    ┏━ host.html ━━━━━━━━┿━━━━━━━━━━━━━━━━━┓
+///                    ┃                    │                 ┃
+///                    ┃    ┌──────┬───MultiChannel─────┐     ┃
+///                    ┃    │      │      │      │      │     ┃
+///                    ┃   host  suite  suite  suite  suite   ┃
+///                    ┃           │      │      │      │     ┃
+///                    ┗━━━━━━━━━━━┿━━━━━━┿━━━━━━┿━━━━━━┿━━━━━┛
+///                                │      │      │      │
+///                                │     ...    ...    ...
+///                                │
+///                         (MessageChannel)
+///                                │
+///      ┏━ suite.html (in iframe) ┿━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+///      ┃                         │                          ┃
+///      ┃         ┌──────────MultiChannel┬─────────┐         ┃
+///      ┃         │          │     │     │         │         ┃
+///      ┃   RemoteListener  test  test  test  running test   ┃
+///      ┃                                                    ┃
+///      ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+///
+/// The host (this code) has a [MultiChannel] that splits the WebSocket
+/// connection with the server. One connection is used for the host itself to
+/// receive messages like "load a suite at this URL", and the rest are
+/// connected to each test suite's iframe via a [MessageChannel].
+///
+/// Each iframe runs a `RemoteListener` which creates its own [MultiChannel] on
+/// top of a [MessageChannel] connection. One connection is used for
+/// the `RemoteListener`, which sends messages like "here are all the tests in
+/// this suite". The rest are used for each test, receiving messages like
+/// "start running". A new connection is also created whenever a test begins
+/// running to send status messages about its progress.
+///
+/// It's of particular note that the suite's [MultiChannel] connection uses the
+/// host's purely as a transport layer; neither is aware that the other is also
+/// using [MultiChannel]. This is necessary, since the host doesn't share memory
+/// with the suites and thus can't share its [MultiChannel] with them, but it
+/// does mean that the server needs to be sure to nest its [MultiChannel]s at
+/// the same place the client does.
+void main() {
+  dom.window.console.log('Dart test runner browser host running');
+  if (_currentUrl.queryParameters['debug'] == 'true') {
+    dom.document.body!.classList.add('debug');
+  }
+
+  runZonedGuarded(() {
+    var serverChannel = _connectToServer();
+    serverChannel.stream.listen((message) {
+      switch (message) {
+        case {
+            'command': 'loadSuite',
+            'channel': final num channel,
+            'url': final String url,
+            'id': final num id
+          }:
+          var suiteChannel = serverChannel.virtualChannel(channel.toInt());
+          var iframeChannel = _connectToIframe(url, id.toInt());
+          suiteChannel.pipe(iframeChannel);
+        case {'command': 'displayPause'}:
+          dom.document.body!.classList.add('paused');
+        case {'command': 'resume'}:
+          dom.document.body!.classList.remove('paused');
+        case {'command': 'closeSuite', 'id': final id}:
+          _iframes.remove(id)!.remove();
+          _subscriptions.remove(id)?.cancel();
+          _domSubscriptions.remove(id)?.cancel();
+        default:
+          dom.window.console
+              .warn('Unhandled message from test runner: $message');
+      }
+    });
+
+    // Send periodic pings to the test runner so it can know when the browser is
+    // paused for debugging.
+    Timer.periodic(const Duration(seconds: 1),
+        (_) => serverChannel.sink.add({'command': 'ping'}));
+
+    var play = dom.document.querySelector('#play');
+    play!.addEventListener('click', allowInterop((_) {
+      if (!dom.document.body!.classList.contains('paused')) return;
+      dom.document.body!.classList.remove('paused');
+      serverChannel.sink.add({'command': 'resume'});
+    }));
+
+    _jsApi = _JSApi(resume: allowInterop(() {
+      if (!dom.document.body!.classList.contains('paused')) return;
+      dom.document.body!.classList.remove('paused');
+      serverChannel.sink.add({'command': 'resume'});
+    }), restartCurrent: allowInterop(() {
+      serverChannel.sink.add({'command': 'restart'});
+    }));
+  }, (error, stackTrace) {
+    dom.window.console.warn('$error\n${Trace.from(stackTrace).terse}');
+  });
+}
+
+/// Creates a [MultiChannel] connection to the server, using a [WebSocket] as
+/// the underlying protocol.
+MultiChannel<dynamic> _connectToServer() {
+  // The `managerUrl` query parameter contains the WebSocket URL of the remote
+  // [BrowserManager] with which this communicates.
+  var webSocket =
+      dom.createWebSocket(_currentUrl.queryParameters['managerUrl']!);
+
+  var controller = StreamChannelController<Object?>(sync: true);
+  webSocket.addEventListener('message', allowInterop((message) {
+    controller.local.sink
+        .add(jsonDecode((message as dom.MessageEvent).data as String));
+  }));
+
+  controller.local.stream
+      .listen((message) => webSocket.send(jsonEncode(message)));
+
+  return MultiChannel(controller.foreign);
+}
+
+/// Creates an iframe with `src` [url] and expects a message back to connect a
+/// message channel with the suite running in the frame.
+///
+/// [id] identifies the suite loaded in this iframe.
+///
+/// Before the frame is attached, adds a listener for `window.onMessage` which
+/// filters to only the messages coming from this frame (by it's URL) and
+/// expects the first message to be either an initialization message, (coming
+/// from the browser bootstrap message channel initialization), or a map with
+/// the key 'exception' set to true and details in the value for 'data' (coming
+/// from `dart.js` due to a load exception).
+///
+/// Legacy bootstrap implementations send a `{'ready': true}` message as a
+/// signal for this host to create a [MessageChannel] and send the port through
+/// the frame's `window.onMessage` channel.
+///
+/// Upcoming bootstrap implementations will send the string 'port' and include a
+/// port for a prepared [MessageChannel].
+///
+/// Returns a [StreamChannel] which will be connected to the frame once the
+/// message channel port is active.
+StreamChannel<dynamic> _connectToIframe(String url, int id) {
+  var suiteUrl = Uri.parse(url).removeFragment();
+  dom.window.console.log('Starting suite $suiteUrl');
+  var iframe = dom.createHTMLIFrameElement();
+  _iframes[id] = iframe;
+  var controller = StreamChannelController<Object?>(sync: true);
+
+  late dom.Subscription windowSubscription;
+  windowSubscription =
+      dom.Subscription(dom.window, 'message', allowInterop((dom.Event event) {
+    // A message on the Window can theoretically come from any website. It's
+    // very unlikely that a malicious site would care about hacking someone's
+    // unit tests, let alone be able to find the test server while it's
+    // running, but it's good practice to check the origin anyway.
+    var message = event as dom.MessageEvent;
+    if (message.origin != dom.window.location.origin) return;
+    // Disambiguate between frames for different test suites.
+    // Depending on the source type, the `location.href` may be missing.
+    if (message.source.location?.href != iframe.src) return;
+
+    message.stopPropagation();
+    windowSubscription.cancel();
+
+    switch (message.data) {
+      case 'port':
+        dom.window.console.log('Connecting channel for suite $suiteUrl');
+        // The frame is starting and sending a port to forward for the suite.
+        final port = message.ports.first;
+        assert(!_domSubscriptions.containsKey(id));
+        _domSubscriptions[id] =
+            dom.Subscription(port, 'message', allowInterop((event) {
+          controller.local.sink.add((event as dom.MessageEvent).data);
+        }));
+        port.start();
+
+        assert(!_subscriptions.containsKey(id));
+        _subscriptions[id] = controller.local.stream.listen(port.postMessage);
+      case {'exception': true, 'data': final data}:
+        // This message from `dart.js` indicates that an exception occurred
+        // loading the test.
+        controller.local.sink.add(data);
+    }
+  }));
+
+  iframe.src = url;
+  dom.document.body!.appendChild(iframe);
+  dom.window.console.log('Appended iframe with src $url');
+
+  return controller.foreign;
+}
diff --git a/pkgs/test_api/CHANGELOG.md b/pkgs/test_api/CHANGELOG.md
new file mode 100644
index 0000000..98d65ba
--- /dev/null
+++ b/pkgs/test_api/CHANGELOG.md
@@ -0,0 +1,339 @@
+## 0.7.4
+
+* Allow `analyzer: '>=6.0.0 <8.0.0'`
+* Increase SDK constraint to ^3.5.0.
+* Support running Node.js tests compiled with dart2wasm.
+
+## 0.7.3
+
+* Increase SDK constraint to ^3.4.0.
+
+## 0.7.2
+
+* Update min SDK constraint to 3.2.0.
+
+## 0.7.1
+
+- Added [`@doNotSubmit`](https://pub.dev/documentation/meta/latest/meta/doNotSubmit-constant.html) to `test(solo: ...)` and `group(solo: ...)`. In
+  practice, this means that code that was relying on ignoring deprecation
+  warnings and using `solo` or `group` with a `skip` parameter will now fail if
+  `dart analyze --fatal-infos` (or similar) is enabled.
+
+## 0.7.0
+
+- Deprecate `Runtime.internetExplorer`.
+- Added `dart2wasm` as a supported compiler for the `chrome` runtime.
+- **BREAKING**: Removed the `experimentalChromeWasm` runtime.
+- **BREAKING**: Removed `Runtime.isJS` and `Runtime.isWasm`, as this is now
+  based on the compiler and not the runtime.
+
+## 0.6.1
+
+- Drop support for null unsafe Dart, bump SDK constraint to `3.0.0`.
+- Make some implementation classes `final`. These classes were never intended to
+  be extended or implemented. `Metadata`, `PlatformSelector`, `RemoteListener`,
+  `Runtime`, `StackTraceFormatter`, `SuitePlatform`, `RemoteException`,
+  `TestHandle`, `OutstandingWork`, `OutsideTestException`, `OnPlatform`,
+  `Retry`, `Skip`, `Tags`, `TestOn`, `Timeout`.
+- Mark an implementation class `interface`: `StackTraceMapper`.
+- Change the `Compiler` class into an `enum`.
+- Make `Fake` a `mixin class`.
+- Allow the latest analyzer (6.x.x).
+
+## 0.6.0
+
+- Remove the `package:test_api/expect.dart' library. `test`will export from`package:matcher` directly.
+- Fix compatibility with wasm number semantics.
+
+## 0.5.2
+
+- Remove deprecation for the `scaffolding.dart` and `backend.dart` libraries.
+- Export `registerException` from the `scaffolding.dart` library.
+
+## 0.5.1
+
+- Handle a missing `'compiler'` value when running a test compiled against a
+  newer `test_api` than the runner back end is using. The expectation was that
+  the json protocol is only used across packages compatible with the same major
+  version of the `test_api` package, but `flutter test` does not check the
+  version of packages in the pub solve for user test code.
+
+## 0.5.0
+
+- Add `Compiler` class, exposed through `backend.dart`.
+- Support compiler identifiers in platform selectors.
+- Add `compiler` field to `SuitePlatform`. This will become required in the next
+  major release.
+- **BREAKING** Add required `defaultCompiler` and `supportedCompilers` fields
+  to `Runtime`.
+- Add `package:test_api/hooks_testing.dart` library for writing tests against
+  code that uses `package:test_api/hooks.dart`.
+- **BREAKING** Remove `ErrorFormatter`, `expectAsync`, `throws`, and `Throws`
+  from `package:test_api/test_api.dart`.
+
+## 0.4.18
+
+- Don't run `tearDown` until the test body and outstanding work is complete,
+  even if the test has already failed.
+
+## 0.4.17
+
+- Deprecate `throwsNullThrownError`, use `throwsA(isA<TypeError>())` instead.
+  The implementation has been changed to ease migrations.
+- Deprecate `throwsCyclicInitializationError` and replace the implementation
+  with `Throws(TypeMatcher<Error>())`. The specific exception no longer exists
+  and there is no guarantee about what type of error will be thrown.
+
+## 0.4.16
+
+- Add the `experimental-chrome-wasm` runtime. This is very unstable and will
+  eventually be deleted, to be replaced by a `--compiler` flag. See
+  https://github.com/dart-lang/test/issues/1776 for more information on future
+  plans
+- Add `isWasm` field to `Runtime` (defaults to `false`).
+
+## 0.4.15
+
+- Expand the pubspec description.
+- Support `package:matcher` version `0.12.13`.
+
+## 0.4.14
+
+- Require Dart >= 2.18.0
+- Support the latest `package:analyzer`.
+
+## 0.4.13
+
+- Fix `printOnFailure` output to be associated with the correct test.
+
+## 0.4.12
+
+- Internal cleanup.
+
+## 0.4.11
+
+- Support the latest version of `package:matcher`.
+
+## 0.4.10
+
+- Add `Target` to restrict `TestOn` annotation to library level.
+
+## 0.4.9
+
+- Add `ignoreTimeouts` option to `Suite`, which disables all timeouts for all
+  tests in that suite.
+
+## 0.4.8
+
+- `TestFailure` implements `Exception` for compatibility with
+  `only_throw_exceptions`.
+
+## 0.4.7
+
+- Remove logging about enabling the chain-stack-traces flag from the invoker.
+
+## 0.4.6
+
+- Give a better exception when using `markTestSkipped` outside of a test.
+- Format stack traces if a formatter is available when serializing tests
+  and groups from the remote listener.
+
+## 0.4.5
+
+- Add defaulting for older test backends that don't pass a configuration for
+  the `allow_duplicate_test_names` parameter to the remote listener.
+
+## 0.4.4
+
+- Allow disabling duplicate test or group names in the `Declarer`.
+
+## 0.4.3
+
+- Use the latest `package:matcher`.
+
+## 0.4.2
+
+- Update `analyzer` constraint to `>=1.5.0 <3.0.0`.
+
+## 0.4.1
+
+- Give a better error when `printOnFailure` is called from outside a test
+  zone.
+
+## 0.4.0
+
+- Add libraries `scaffolding.dart`, and `expect.dart` to allow importing as
+  subset of the normal surface area.
+- Add new APIs in `hooks.dart` to allow writing custom expectation frameworks
+  which integrate with the test runner.
+- Add examples to `throwsA` and make top-level `throws...` matchers refer to it.
+- Disable stack trace chaining by default.
+- Fix `expectAsync` function type checks.
+- Add `RemoteException`, `RemoteListener`, `StackTraceFormatter`, and
+  `StackTraceMapper` to `backend.dart`.
+- **Breaking** remove `Runtime.phantomJS`
+- **Breaking** Add callback to get the suite channel in the `beforeLoad`
+  callback of `RemoteListener.start`. This is now used in place of using zones
+  to communicate the value.
+
+## 0.3.0
+
+- **Breaking** `TestException.message` is now nullable.
+  - Fixes handling of `null` messages in remote exceptions.
+
+## 0.2.20
+
+- Fix some strong null safety mode errors in the original migration.
+
+## 0.2.19
+
+- Stable release for null safety.
+
+## 0.2.19-nullsafety.7
+
+- Expand upper bound constraints for some null safe migrated packages.
+
+## 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.
+
+## 0.2.18
+
+- Update to `matcher` version `0.12.9`.
+
+## 0.2.17
+
+- Add `languageVersionComment` on the `MetaData` class. This should only be
+  present for test suites.
+
+## 0.2.16
+
+- Deprecate `LiveTestController.liveTest`, the `LiveTestController` instance now
+  implements `LiveTest` and can be used directly.
+
+## 0.2.15
+
+- Cancel any StreamQueue that is created as a part of a stream matcher once it
+  is done matching.
+  - This fixes a bug where using a matcher on a custom stream controller and
+    then awaiting the `close()` method on that controller would hang.
+- Avoid causing the test runner to hang if there is a timeout during a
+  `tearDown` callback following a failing test case.
+
+## 0.2.14
+
+- Bump minimum SDK to `2.4.0` for safer usage of for-loop elements.
+
+## 0.2.13
+
+- Work around a bug in the `2.3.0` SDK by avoiding for-loop elements at the top
+  level.
+
+## 0.2.12
+
+- Link to docs on setting timeout when a test times out with the default
+  duration.
+- No longer directly depend on `package:pedantic`.
+
+## 0.2.11
+
+- Extend the timeout for synthetic tests, e.g. `tearDownAll`.
+
+## 0.2.10
+
+- Update to latest `package:matcher`. Improves output for instances of private
+  classes.
+
+## 0.2.9
+
+- Treat non-solo tests as skipped so they are properly reported.
+
+## 0.2.8
+
+- Remove logic which accounted for a race condition in state change. The logic
+  was required because `package:sse` used to not guarantee order. This is no
+  longer the case.
+
+## 0.2.7
+
+- Prepare for upcoming `Stream<List<int>>` changes in the Dart SDK.
+- Mark `package:test_api` as deprecated to prevent accidental use.
+
+## 0.2.6
+
+- Don't swallow exceptions from callbacks in `expectAsync*`.
+- Internal cleanup - fix lints.
+- Fixed a race condition that caused tests to occasionally fail during
+  `tearDownAll` with the message `(tearDownAll) - did not complete [E]`.
+
+## 0.2.5
+
+- Expose the `Metadata`, `PlatformSelector`, `Runtime`, and `SuitePlatform`
+  classes publicly through a new `backend.dart` import.
+
+## 0.2.4
+
+- Allow `stream_channel` version `2.0.0`.
+
+## 0.2.3
+
+- Update to matcher version `0.12.5`.
+
+## 0.2.2
+
+- Require Dart SDK `>=2.1.0`.
+
+## 0.2.1
+
+- Add `remote_listener.dart` and `suite_channel_manager.dart`.
+
+## 0.2.0
+
+- Remove "runner" extensions.
+
+## 0.1.1
+
+- Update `stack_trace_formatter` to fold `test_api` frames by default.
+
+## 0.1.0
+
+- Initial release of `test_api`. Provides the basic API for writing tests and
+  touch points for implementing a custom test runner.
diff --git a/pkgs/test_api/LICENSE b/pkgs/test_api/LICENSE
new file mode 100644
index 0000000..9972f6e
--- /dev/null
+++ b/pkgs/test_api/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2018, the Dart project authors. 
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google LLC nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/test_api/README.md b/pkgs/test_api/README.md
new file mode 100644
index 0000000..aa1af49
--- /dev/null
+++ b/pkgs/test_api/README.md
@@ -0,0 +1,8 @@
+[![pub package](https://img.shields.io/pub/v/test_api.svg)](https://pub.dev/packages/test_api)
+[![package publisher](https://img.shields.io/pub/publisher/test_api.svg)](https://pub.dev/packages/test_api/publisher)
+
+A minimal package for writing tests. At this time this package is not intended
+to be publicly used as the API will take time to stabilize.
+
+If you're interested in testing Dart code, you likely want to use
+[package:test](https://pub.dev/packages/test).
diff --git a/pkgs/test_api/dart_test.yaml b/pkgs/test_api/dart_test.yaml
new file mode 100644
index 0000000..faff3bb
--- /dev/null
+++ b/pkgs/test_api/dart_test.yaml
@@ -0,0 +1,46 @@
+# Fold frames from helper packages we use in our tests, but not from test
+# itself.
+fold_stack_frames:
+  except:
+  - shelf_test_handler
+  - stream_channel
+  - test_descriptor
+  - test_process
+
+presets:
+  # "-P terse-trace" folds frames from test's implementation to make the output
+  # less verbose when
+  terse-trace:
+    fold_stack_frames:
+      except: [test]
+
+tags:
+  browser:
+    timeout: 2x
+
+    # Browsers can sometimes randomly time out while starting, especially on
+    # Travis which is pretty slow. Don't retry locally because it makes
+    # debugging more annoying.
+    presets: {travis: {retry: 3}}
+
+  dart2js:
+    add_tags: [browser]
+    timeout: 2x
+
+  firefox: {add_tags: [dart2js]}
+  chrome: {add_tags: [dart2js]}
+  phantomjs: {add_tags: [dart2js]}
+
+  safari:
+    add_tags: [dart2js]
+    test_on: mac-os
+
+  # Tests that run pub. These tests may need to be excluded when there are local
+  # dependency_overrides.
+  pub:
+    timeout: 2x
+
+  # Tests that use Node.js. These tests may need to be excluded on systems that
+  # don't have Node installed.
+  node:
+    timeout: 2x
diff --git a/pkgs/test_api/lib/backend.dart b/pkgs/test_api/lib/backend.dart
new file mode 100644
index 0000000..5da47b6
--- /dev/null
+++ b/pkgs/test_api/lib/backend.dart
@@ -0,0 +1,13 @@
+// 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.
+
+export 'src/backend/compiler.dart' show Compiler;
+export 'src/backend/metadata.dart' show Metadata;
+export 'src/backend/platform_selector.dart' show PlatformSelector;
+export 'src/backend/remote_exception.dart' show RemoteException;
+export 'src/backend/remote_listener.dart' show RemoteListener;
+export 'src/backend/runtime.dart' show Runtime;
+export 'src/backend/stack_trace_formatter.dart' show StackTraceFormatter;
+export 'src/backend/stack_trace_mapper.dart' show StackTraceMapper;
+export 'src/backend/suite_platform.dart' show SuitePlatform;
diff --git a/pkgs/test_api/lib/fake.dart b/pkgs/test_api/lib/fake.dart
new file mode 100644
index 0000000..f8b2d80
--- /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;
+
+export 'src/frontend/fake.dart';
diff --git a/pkgs/test_api/lib/hooks.dart b/pkgs/test_api/lib/hooks.dart
new file mode 100644
index 0000000..126d322
--- /dev/null
+++ b/pkgs/test_api/lib/hooks.dart
@@ -0,0 +1,82 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+
+import 'src/backend/closed_exception.dart';
+import 'src/backend/invoker.dart';
+import 'src/backend/stack_trace_formatter.dart';
+
+export 'src/backend/test_failure.dart' show TestFailure;
+export 'src/scaffolding/utils.dart' show pumpEventQueue;
+
+final class TestHandle {
+  /// Returns handle for the currently running test.
+  ///
+  /// This must be called from within the zone that the test is running in. If
+  /// the current zone is not a test's zone throws [OutsideTestException].
+  static TestHandle get current {
+    final invoker = Invoker.current;
+    if (invoker == null) throw OutsideTestException();
+    return TestHandle._(
+        invoker, StackTraceFormatter.current ?? _defaultFormatter);
+  }
+
+  static final _defaultFormatter = StackTraceFormatter();
+
+  final Invoker _invoker;
+  final StackTraceFormatter _stackTraceFormatter;
+  TestHandle._(this._invoker, this._stackTraceFormatter);
+
+  String get name => _invoker.liveTest.test.name;
+
+  /// Whether this test has already completed successfully.
+  ///
+  /// If a callback originating from a test case is invoked after the test has
+  /// already passed it may be an indication of a test that fails to wait for
+  /// all work to be finished, or of an asynchronous callback that is called
+  /// more times or later than expected.
+  bool get shouldBeDone => _invoker.liveTest.state.shouldBeDone;
+
+  /// Marks this test as skipped.
+  ///
+  /// A skipped test may still fail if any exception is thrown, including
+  /// uncaught asynchronous errors.
+  void markSkipped(String message) {
+    if (_invoker.closed) throw ClosedException();
+    _invoker.skip(message);
+  }
+
+  /// Indicates that this test should not be considered done until the returned
+  /// [OutstandingWork] is marked as complete.
+  ///
+  /// The test may time out before the outstanding work completes.
+  OutstandingWork markPending() {
+    if (_invoker.closed) throw ClosedException();
+    return OutstandingWork._(_invoker, Zone.current);
+  }
+
+  /// Converts [stackTrace] to a [Chain] according to the current test's
+  /// configuration.
+  Chain formatStackTrace(StackTrace stackTrace) =>
+      _stackTraceFormatter.formatStackTrace(stackTrace);
+}
+
+final class OutstandingWork {
+  final Invoker _invoker;
+  final Zone _zone;
+  var _isComplete = false;
+  OutstandingWork._(this._invoker, this._zone) {
+    _invoker.addOutstandingCallback();
+  }
+  void complete() {
+    if (_isComplete) return;
+    _isComplete = true;
+    _zone.run(_invoker.removeOutstandingCallback);
+  }
+}
+
+final class OutsideTestException implements Exception {}
diff --git a/pkgs/test_api/lib/hooks_testing.dart b/pkgs/test_api/lib/hooks_testing.dart
new file mode 100644
index 0000000..b0a6154
--- /dev/null
+++ b/pkgs/test_api/lib/hooks_testing.dart
@@ -0,0 +1,157 @@
+// Copyright (c) 2023, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'src/backend/group.dart';
+import 'src/backend/invoker.dart';
+import 'src/backend/live_test.dart';
+import 'src/backend/metadata.dart';
+import 'src/backend/runtime.dart';
+import 'src/backend/state.dart';
+import 'src/backend/suite.dart';
+import 'src/backend/suite_platform.dart';
+
+@Deprecated('These classes are unused for monitoring tests. Use `State`.')
+export 'src/backend/state.dart' show Result, Status;
+export 'src/backend/test_failure.dart' show TestFailure;
+
+/// A monitor for the behavior of a callback when it is run as the body of a
+/// test case.
+///
+/// Allows running a callback as the body of a local test case and querying for
+/// the current [state], and [errors], and subscribing to future errors.
+///
+/// Use [run] to run a test body and query for the success or failure.
+///
+/// Use [start] to start a test and query for whether it has finished running.
+final class TestCaseMonitor {
+  final LiveTest _liveTest;
+  final _done = Completer<void>();
+  TestCaseMonitor._(FutureOr<void> Function() body)
+      : _liveTest = _createTest(body);
+
+  /// Run [body] as a test case and return a [TestCaseMonitor] with the result.
+  ///
+  /// The [state] will either [State.passed], [State.skipped], or
+  /// [State.failed], the test will no longer be running.
+  ///
+  /// {@template result-late-fail}
+  /// Note that a test can change state from [State.passed] to [State.failed]
+  /// if the test surfaces an unawaited asynchronous error.
+  /// {@endtemplate}
+  ///
+  /// ```dart
+  /// final monitor = await TestCaseMonitor.run(() {
+  ///   fail('oh no!');
+  /// });
+  /// assert(monitor.state == State.failed);
+  /// assert((monitor.errors.single.error as TestFailure).message == 'oh no!');
+  /// ```
+  static Future<TestCaseMonitor> run(FutureOr<void> Function() body) async {
+    final monitor = TestCaseMonitor.start(body);
+    await monitor.onDone;
+    return monitor;
+  }
+
+  /// Start [body] as a test case and return a [TestCaseMonitor] with the status
+  /// and result.
+  ///
+  /// The [state] will start as [State.pending] if queried synchronously, but it
+  /// will switch to [State.running]. After `onDone` completes the state will be
+  /// one of [State.passed], [State.skipped], or [State.failed].
+  ///
+  /// {@macro result-late-fail}
+  ///
+  /// ```dart
+  /// late void Function() completeWork;
+  /// final monitor = TestCaseMonitor.start(() {
+  ///   final outstandingWork = TestHandle.current.markPending();
+  ///   completeWork = outstandingWork.complete;
+  /// });
+  /// await pumpEventQueue();
+  /// assert(monitor.state == State.running);
+  /// completeWork();
+  /// await monitor.onDone;
+  /// assert(monitor.state == State.passed);
+  /// ```
+  static TestCaseMonitor start(FutureOr<void> Function() body) =>
+      TestCaseMonitor._(body).._start();
+
+  void _start() {
+    _liveTest.run().whenComplete(_done.complete);
+  }
+
+  /// A future that completes after this test has finished running, or has
+  /// surfaced an error.
+  Future<void> get onDone => _done.future;
+
+  /// The running and success or failure status for the test case.
+  State get state {
+    final status = _liveTest.state.status;
+    if (status == Status.pending) return State.pending;
+    if (status == Status.running) return State.running;
+    final result = _liveTest.state.result;
+    if (result == Result.skipped) return State.skipped;
+    if (result == Result.success) return State.passed;
+    // result == Result.failure || result == Result.error
+    return State.failed;
+  }
+
+  /// The errors surfaced by the test.
+  ///
+  /// A test with any errors will have a [state] of [State.failed].
+  ///
+  /// {@macro result-late-fail}
+  ///
+  /// A test may have more than one error if there were unhandled asynchronous
+  /// errors surfaced after the test is done.
+  Iterable<AsyncError> get errors => _liveTest.errors;
+
+  /// A stream of errors surfaced by the test.
+  ///
+  /// This stream will not close, asynchronous errors may be surfaced within the
+  /// test's error zone at any point.
+  Stream<AsyncError> get onError => _liveTest.onError;
+}
+
+/// Returns a local [LiveTest] that runs [body].
+LiveTest _createTest(FutureOr<void> Function() body) {
+  var test = LocalTest('test', Metadata(chainStackTraces: true), body);
+  var suite = Suite(Group.root([test]), _suitePlatform, ignoreTimeouts: false);
+  return test.load(suite);
+}
+
+/// A dummy suite platform to use for testing suites.
+final _suitePlatform =
+    SuitePlatform(Runtime.vm, compiler: Runtime.vm.defaultCompiler);
+
+/// The running and success state of a test monitored by a [TestCaseMonitor].
+enum State {
+  /// The test is has not yet started.
+  pending,
+
+  /// The test is running and has not yet failed.
+  running,
+
+  /// The test has completed without any error.
+  ///
+  /// This implies that the test body has completed, and no error has surfaced
+  /// *yet*. However, it this doesn't mean that the test won't fail in the
+  /// future.
+  passed,
+
+  /// The test, or some part of it, has been skipped.
+  ///
+  /// This does not imply that the test has not had an error, but if there are
+  /// errors they are ignored.
+  skipped,
+
+  /// The test has failed.
+  ///
+  /// An test fails when any exception, typically a [TestFailure], is thrown in
+  /// the test's zone. A test that has failed may still have additional errors
+  /// that surface as unhandled asynchronous errors.
+  failed,
+}
diff --git a/pkgs/test_api/lib/scaffolding.dart b/pkgs/test_api/lib/scaffolding.dart
new file mode 100644
index 0000000..9d864bb
--- /dev/null
+++ b/pkgs/test_api/lib/scaffolding.dart
@@ -0,0 +1,23 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// {@canonicalFor on_platform.OnPlatform}
+/// {@canonicalFor retry.Retry}
+/// {@canonicalFor skip.Skip}
+/// {@canonicalFor tags.Tags}
+/// {@canonicalFor test_on.TestOn}
+/// {@canonicalFor timeout.Timeout}
+library;
+
+export 'src/backend/configuration/on_platform.dart' show OnPlatform;
+export 'src/backend/configuration/retry.dart' show Retry;
+export 'src/backend/configuration/skip.dart' show Skip;
+export 'src/backend/configuration/tags.dart' show Tags;
+export 'src/backend/configuration/test_on.dart' show TestOn;
+export 'src/backend/configuration/timeout.dart' show Timeout;
+export 'src/scaffolding/spawn_hybrid.dart' show spawnHybridCode, spawnHybridUri;
+export 'src/scaffolding/test_structure.dart'
+    show addTearDown, group, setUp, setUpAll, tearDown, tearDownAll, test;
+export 'src/scaffolding/utils.dart'
+    show markTestSkipped, printOnFailure, pumpEventQueue, registerException;
diff --git a/pkgs/test_api/lib/src/backend/closed_exception.dart b/pkgs/test_api/lib/src/backend/closed_exception.dart
new file mode 100644
index 0000000..08a45a0
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/closed_exception.dart
@@ -0,0 +1,12 @@
+// 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.
+
+/// An exception thrown by various front-end methods when the test framework has
+/// been closed and a test must shut down as soon as possible.
+class ClosedException implements Exception {
+  ClosedException();
+
+  @override
+  String toString() => 'This test has been closed.';
+}
diff --git a/pkgs/test_api/lib/src/backend/compiler.dart b/pkgs/test_api/lib/src/backend/compiler.dart
new file mode 100644
index 0000000..41c253d
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/compiler.dart
@@ -0,0 +1,59 @@
+// Copyright (c) 2023, 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.
+
+/// All the Dart compilers supported by the test runner.
+enum Compiler {
+  /// The production Dart to Javascript compiler (whole world, optimizing).
+  dart2js('Dart2Js', 'dart2js'),
+
+  /// Experimental Dart to Wasm compiler.
+  dart2wasm('Dart2Wasm', 'dart2wasm'),
+
+  /// Compiles dart code to a native executable.
+  exe('Exe', 'exe'),
+
+  /// The standard compiler for vm tests, compiles tests to kernel before
+  /// running them on the VM.
+  kernel('Kernel', 'kernel'),
+
+  /// Runs tests directly from source, with no precompilation.
+  source('Source', 'source');
+
+  /// The compilers that are supported by the test runner by default.
+  static const List<Compiler> builtIn = [
+    Compiler.dart2js,
+    Compiler.dart2wasm,
+    Compiler.exe,
+    Compiler.kernel,
+    Compiler.source,
+  ];
+
+  /// The human-friendly name of the compiler.
+  final String name;
+
+  /// The identifier used to look up the compiler.
+  final String identifier;
+
+  const Compiler(this.name, this.identifier);
+
+  /// Converts a JSON-safe representation generated by [serialize] back into a
+  /// [Compiler].
+  ///
+  /// Note that custom [Compiler] implementations are not supported.
+  factory Compiler.deserialize(Object serialized) => builtIn
+      .firstWhere((compiler) => compiler.identifier == serialized as String);
+
+  /// Converts [this] into a JSON-safe object that can be converted back to a
+  /// [Compiler] using [Compiler.deserialize].
+  Object serialize() => identifier;
+
+  @override
+  String toString() => name;
+}
+
+extension CompileTarget on Compiler {
+  bool get isJS => this == Compiler.dart2js;
+
+  bool get isWasm => this == Compiler.dart2wasm;
+}
diff --git a/pkgs/test_api/lib/src/backend/configuration/on_platform.dart b/pkgs/test_api/lib/src/backend/configuration/on_platform.dart
new file mode 100644
index 0000000..02a5254
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/configuration/on_platform.dart
@@ -0,0 +1,17 @@
+// 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.
+
+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})
+final class OnPlatform {
+  final Map<String, dynamic> annotationsByPlatform;
+
+  const OnPlatform(this.annotationsByPlatform);
+}
diff --git a/pkgs/test_api/lib/src/backend/configuration/retry.dart b/pkgs/test_api/lib/src/backend/configuration/retry.dart
new file mode 100644
index 0000000..1811814
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/configuration/retry.dart
@@ -0,0 +1,18 @@
+// 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.
+
+import 'package:meta/meta_meta.dart';
+
+/// An annotation for marking a test suite to be retried.
+///
+/// A suite-level retry configuration will enable retries for every test in the
+/// suite, unless the group or test is configured with a more specific retry.
+@Target({TargetKind.library})
+final class Retry {
+  /// The number of times the tests in the suite will be retried.
+  final int count;
+
+  /// Marks all tests in a test suite to be retried.
+  const Retry(this.count);
+}
diff --git a/pkgs/test_api/lib/src/backend/configuration/skip.dart b/pkgs/test_api/lib/src/backend/configuration/skip.dart
new file mode 100644
index 0000000..3927994
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/configuration/skip.dart
@@ -0,0 +1,18 @@
+// 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.
+
+import 'package:meta/meta_meta.dart';
+
+/// An annotation for marking a test suite as skipped.
+@Target({TargetKind.library})
+final class Skip {
+  /// The reason the test suite is skipped, or `null` if no reason is given.
+  final String? reason;
+
+  /// Marks a suite as skipped.
+  ///
+  /// If [reason] is passed, it's included in the test output as the reason the
+  /// test is skipped.
+  const Skip([this.reason]);
+}
diff --git a/pkgs/test_api/lib/src/backend/configuration/tags.dart b/pkgs/test_api/lib/src/backend/configuration/tags.dart
new file mode 100644
index 0000000..dbd03f3
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/configuration/tags.dart
@@ -0,0 +1,21 @@
+// 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.
+
+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/pkgs/test/README.md#tagging-tests
+@Target({TargetKind.library})
+final class Tags {
+  /// The tags for the test suite.
+  Set<String> get tags => _tags.toSet();
+
+  final Iterable<String> _tags;
+
+  /// Applies a set of user-defined tags to a test suite.
+  const Tags(this._tags);
+}
diff --git a/pkgs/test_api/lib/src/backend/configuration/test_on.dart b/pkgs/test_api/lib/src/backend/configuration/test_on.dart
new file mode 100644
index 0000000..dee7f40
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/configuration/test_on.dart
@@ -0,0 +1,18 @@
+// 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.
+
+import 'package:meta/meta_meta.dart';
+
+/// An annotation indicating which platforms a test suite supports.
+///
+/// For the full syntax of [expression], see [the README][].
+///
+/// [the README]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors
+@Target({TargetKind.library})
+final class TestOn {
+  /// The expression specifying the platform.
+  final String expression;
+
+  const TestOn(this.expression);
+}
diff --git a/pkgs/test_api/lib/src/backend/configuration/timeout.dart b/pkgs/test_api/lib/src/backend/configuration/timeout.dart
new file mode 100644
index 0000000..e51d520
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/configuration/timeout.dart
@@ -0,0 +1,155 @@
+// 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.
+
+import 'package:meta/meta_meta.dart';
+import 'package:string_scanner/string_scanner.dart';
+
+/// A regular expression that matches text until a letter or whitespace.
+///
+/// This is intended to scan through a number without actually encoding the full
+/// Dart number grammar. It doesn't stop on "e" because that can be a component
+/// of numbers.
+final _untilUnit = RegExp(r'[^a-df-z\s]+', caseSensitive: false);
+
+/// A regular expression that matches a time unit.
+final _unit = RegExp(r'([um]s|[dhms])', caseSensitive: false);
+
+/// A regular expression that matches a section of whitespace.
+final _whitespace = RegExp(r'\s+');
+
+/// A class representing a modification to the default timeout for a test.
+///
+/// By default, a test will time out after 30 seconds. With [Timeout], that
+/// can be overridden entirely; with [Timeout.factor], it can be scaled
+/// relative to the default.
+@Target({TargetKind.library})
+final class Timeout {
+  /// A constant indicating that a test should never time out.
+  static const none = Timeout._none();
+
+  /// The timeout duration.
+  ///
+  /// 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;
+
+  /// The timeout factor.
+  ///
+  /// The default timeout will be multiplied by this to get the new timeout.
+  /// Thus a factor of 2 means that the test will take twice as long to time
+  /// out, and a factor of 0.5 means that it will time out twice as quickly.
+  ///
+  /// This is `null` for timeouts with a non-null [duration] and for
+  /// [Timeout.none].
+  final num? scaleFactor;
+
+  /// Declares an absolute timeout that overrides the default.
+  const Timeout(this.duration) : scaleFactor = null;
+
+  /// Declares a relative timeout that scales the default.
+  const Timeout.factor(this.scaleFactor) : duration = null;
+
+  const Timeout._none()
+      : scaleFactor = null,
+        duration = null;
+
+  /// Parse the timeout from a user-provided string.
+  ///
+  /// This supports the following formats:
+  ///
+  /// * `Number "x"`, which produces a relative timeout with the given scale
+  ///   factor.
+  ///
+  /// * `(Number ("d" | "h" | "m" | "s" | "ms" | "us") (" ")?)+`, which produces
+  ///   an absolute timeout with the duration given by the sum of the given
+  ///   units.
+  ///
+  /// * `"none"`, which produces [Timeout.none].
+  ///
+  /// Throws a [FormatException] if [timeout] is not in a valid format
+  factory Timeout.parse(String timeout) {
+    var scanner = StringScanner(timeout);
+
+    // First check for the string "none".
+    if (scanner.scan('none')) {
+      scanner.expectDone();
+      return Timeout.none;
+    }
+
+    // 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]!);
+
+    // A number followed by "x" is a scale factor.
+    if (scanner.scan('x') || scanner.scan('X')) {
+      scanner.expectDone();
+      return Timeout.factor(number);
+    }
+
+    // Parse time units until none are left. The condition is in the middle of
+    // the loop because we've already parsed the first number.
+    var microseconds = 0.0;
+    while (true) {
+      scanner.expect(_unit, name: 'unit');
+      microseconds += _microsecondsFor(number, scanner.lastMatch![0]!);
+
+      scanner.scan(_whitespace);
+
+      // Scan the next number, if it's available.
+      if (!scanner.scan(_untilUnit)) break;
+      number = double.parse(scanner.lastMatch![0]!);
+    }
+
+    scanner.expectDone();
+    return Timeout(Duration(microseconds: microseconds.round()));
+  }
+
+  /// Returns the number of microseconds in [number] [unit]s.
+  static double _microsecondsFor(double number, String unit) => switch (unit) {
+        'd' => number * 24 * 60 * 60 * 1000000,
+        'h' => number * 60 * 60 * 1000000,
+        'm' => number * 60 * 1000000,
+        's' => number * 1000000,
+        'ms' => number * 1000,
+        'us' => number,
+        _ => throw ArgumentError('Unknown unit $unit.'),
+      };
+
+  /// Returns a new [Timeout] that merges [this] with [other].
+  ///
+  /// [Timeout.none] takes precedence over everything. If timeout is
+  /// [Timeout.none] and [other] declares a [duration], that takes precedence.
+  /// Otherwise, this timeout's [duration] or [factor] are multiplied by
+  /// [other]'s [factor].
+  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!);
+  }
+
+  /// Returns a new [Duration] from applying [this] to [base].
+  ///
+  /// If this is [none], returns `null`.
+  Duration? apply(Duration base) {
+    if (this == none) return null;
+    return duration ?? base * scaleFactor!;
+  }
+
+  @override
+  int get hashCode => duration.hashCode ^ 5 * scaleFactor.hashCode;
+
+  @override
+  bool operator ==(Object other) =>
+      other is Timeout &&
+      other.duration == duration &&
+      other.scaleFactor == scaleFactor;
+
+  @override
+  String toString() {
+    if (duration != null) return duration.toString();
+    if (scaleFactor != null) return '${scaleFactor}x';
+    return 'none';
+  }
+}
diff --git a/pkgs/test_api/lib/src/backend/declarer.dart b/pkgs/test_api/lib/src/backend/declarer.dart
new file mode 100644
index 0000000..76f7563
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/declarer.dart
@@ -0,0 +1,433 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:collection/collection.dart';
+import 'package:stack_trace/stack_trace.dart';
+
+import 'configuration/timeout.dart';
+import 'group.dart';
+import 'group_entry.dart';
+import 'invoker.dart';
+import 'metadata.dart';
+import 'test.dart';
+
+/// A class that manages the state of tests as they're declared.
+///
+/// A nested tree of Declarers tracks the current group, set-up, and tear-down
+/// functions. Each Declarer in the tree corresponds to a group. This tree is
+/// tracked by a zone-scoped "current" Declarer; the current declarer can be set
+/// for a block using [Declarer.declare], and it can be accessed using
+/// [Declarer.current].
+class Declarer {
+  /// The parent declarer, or `null` if this corresponds to the root group.
+  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;
+
+  /// The metadata for this group, including the metadata of any parent groups
+  /// and of the test suite.
+  final Metadata _metadata;
+
+  /// The set of variables that are valid for platform selectors, in addition to
+  /// the built-in variables that are allowed everywhere.
+  final Set<String> _platformVariables;
+
+  /// The stack trace for this group.
+  ///
+  /// This is `null` for the root (implicit) group.
+  final Trace? _trace;
+
+  /// Whether to collect stack traces for [GroupEntry]s.
+  final bool _collectTraces;
+
+  /// Whether to disable retries of tests.
+  final bool _noRetry;
+
+  /// The set-up functions to run for each test in this group.
+  final _setUps = <dynamic Function()>[];
+
+  /// The tear-down functions to run for each test in this group.
+  final _tearDowns = <dynamic Function()>[];
+
+  /// The set-up functions to run once for this group.
+  final _setUpAlls = <dynamic Function()>[];
+
+  /// The default timeout for synthetic tests.
+  final _timeout = const Timeout(Duration(minutes: 12));
+
+  /// The trace for the first call to [setUpAll].
+  ///
+  /// 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;
+
+  /// The tear-down functions to run once for this group.
+  final _tearDownAlls = <void Function()>[];
+
+  /// The trace for the first call to [tearDownAll].
+  ///
+  /// All [tearDownAll]s are run in a single logical test, so they can only have
+  /// one trace. The first trace matches [_setUpAllTrace].
+  Trace? _tearDownAllTrace;
+
+  /// The children of this group, either tests or sub-groups.
+  ///
+  /// All modifications to this must go through [_addEntry].
+  final _entries = <GroupEntry>[];
+
+  /// Whether [build] has been called for this declarer.
+  bool _built = false;
+
+  /// The tests and/or groups that have been flagged as solo.
+  final _soloEntries = <GroupEntry>[];
+
+  /// 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?;
+
+  /// All the test and group names that have been declared in the entire suite.
+  ///
+  /// If duplicate test names are allowed, this is not tracked and it will be
+  /// `null`.
+  final Set<String>? _seenNames;
+
+  /// Whether this declarer is running in a standalone test executation.
+  ///
+  /// The full test runner awaits asynchronous `main` declarations, and so
+  /// asynchronous work can be performed in between calls to `group`, and `test`
+  /// etc. When running as a standalone file tests are run synchronously
+  /// following the first call to declare a test, so all tests must be declared
+  /// synchronously starting at that point. Track whether we are running in this
+  /// more limited mode to customize the error message for tests declared late.
+  final bool _isStandalone;
+
+  /// Creates a new declarer for the root group.
+  ///
+  /// This is the implicit group that exists outside of any calls to `group()`.
+  /// If [metadata] is passed, it's used as the metadata for the implicit root
+  /// group.
+  ///
+  /// The [platformVariables] are the set of variables that are valid for
+  /// platform selectors in test and group metadata, in addition to the built-in
+  /// variables that are allowed everywhere.
+  ///
+  /// If [collectTraces] is `true`, this will set [GroupEntry.trace] for all
+  /// entries built by the declarer. Note that this can be noticeably slow when
+  /// thousands of tests are being declared (see #457).
+  ///
+  /// If [noRetry] is `true` tests will be run at most once.
+  ///
+  /// If [allowDuplicateTestNames] is `false`, then a
+  /// [DuplicateTestNameException] will be thrown if two tests (or groups) have
+  /// the same name.
+  Declarer({
+    Metadata? metadata,
+    Set<String>? platformVariables,
+    bool collectTraces = false,
+    bool noRetry = false,
+    String? fullTestName,
+    // TODO: Change the default https://github.com/dart-lang/test/issues/1571
+    bool allowDuplicateTestNames = true,
+    bool isStandalone = false,
+  }) : this._(
+          null,
+          null,
+          metadata ?? Metadata(),
+          platformVariables ?? const UnmodifiableSetView.empty(),
+          collectTraces,
+          null,
+          noRetry,
+          fullTestName,
+          allowDuplicateTestNames ? null : <String>{},
+          isStandalone,
+        );
+
+  Declarer._(
+    this._parent,
+    this._name,
+    this._metadata,
+    this._platformVariables,
+    this._collectTraces,
+    this._trace,
+    this._noRetry,
+    this._fullTestName,
+    this._seenNames,
+    this._isStandalone,
+  );
+
+  /// Runs [body] with this declarer as [Declarer.current].
+  ///
+  /// Returns the return value of [body].
+  T declare<T>(T Function() body) =>
+      runZoned(body, zoneValues: {#test.declarer: this});
+
+  /// Defines a test case with the given name and body.
+  void test(String name, dynamic Function() body,
+      {String? testOn,
+      Timeout? timeout,
+      Object? skip,
+      Map<String, dynamic>? onPlatform,
+      Object? tags,
+      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,
+        skip: skip,
+        onPlatform: onPlatform,
+        tags: tags,
+        retry: _noRetry ? 0 : retry);
+    newMetadata.validatePlatformSelectors(_platformVariables);
+    var metadata = _metadata.merge(newMetadata);
+    _addEntry(LocalTest(fullName, metadata, () async {
+      var parents = <Declarer>[];
+      for (Declarer? declarer = this;
+          declarer != null;
+          declarer = declarer._parent) {
+        parents.add(declarer);
+      }
+
+      // Register all tear-down functions in all declarers. Iterate through
+      // parents outside-in so that the Invoker gets the functions in the order
+      // they were declared in source.
+      for (var declarer in parents.reversed) {
+        for (var tearDown in declarer._tearDowns) {
+          Invoker.current!.addTearDown(tearDown);
+        }
+      }
+
+      await runZoned(() async {
+        await _runSetUps();
+        await body();
+      },
+          // Make the declarer visible to running tests so that they'll throw
+          // useful errors when calling `test()` and `group()` within a test.
+          zoneValues: {#test.declarer: this});
+    }, trace: _collectTraces ? Trace.current(2) : null, guarded: false));
+
+    if (solo) {
+      _soloEntries.add(_entries.last);
+    }
+  }
+
+  /// Creates a group of tests.
+  void group(String name, void Function() body,
+      {String? testOn,
+      Timeout? timeout,
+      Object? skip,
+      Map<String, dynamic>? onPlatform,
+      Object? tags,
+      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,
+        skip: skip,
+        onPlatform: onPlatform,
+        tags: tags,
+        retry: _noRetry ? 0 : retry);
+    newMetadata.validatePlatformSelectors(_platformVariables);
+    var metadata = _metadata.merge(newMetadata);
+    var trace = _collectTraces ? Trace.current(2) : null;
+
+    var declarer = Declarer._(
+      this,
+      fullTestPrefix,
+      metadata,
+      _platformVariables,
+      _collectTraces,
+      trace,
+      _noRetry,
+      _fullTestName,
+      _seenNames,
+      _isStandalone,
+    );
+    declarer.declare(() {
+      // Cast to dynamic to avoid the analyzer complaining about us using the
+      // result of a void method.
+      var result = (body as dynamic)();
+      if (result is! Future) return;
+      throw ArgumentError('Groups may not be async.');
+    });
+    _addEntry(declarer.build());
+
+    if (solo || declarer._solo) {
+      _soloEntries.add(_entries.last);
+    }
+  }
+
+  /// Returns [name] prefixed with this declarer's group name.
+  String _prefix(String name) => _name == null ? name : '$_name $name';
+
+  /// Registers a function to be run before each test in this group.
+  void setUp(dynamic Function() callback) {
+    _checkNotBuilt('setUp');
+    _setUps.add(callback);
+  }
+
+  /// Registers a function to be run after each test in this group.
+  void tearDown(dynamic Function() callback) {
+    _checkNotBuilt('tearDown');
+    _tearDowns.add(callback);
+  }
+
+  /// Registers a function to be run once before all tests.
+  void setUpAll(dynamic Function() callback) {
+    _checkNotBuilt('setUpAll');
+    if (_collectTraces) _setUpAllTrace ??= Trace.current(2);
+    _setUpAlls.add(callback);
+  }
+
+  /// Registers a function to be run once after all tests.
+  void tearDownAll(dynamic Function() callback) {
+    _checkNotBuilt('tearDownAll');
+    if (_collectTraces) _tearDownAllTrace ??= Trace.current(2);
+    _tearDownAlls.add(callback);
+  }
+
+  /// Like [tearDownAll], but called from within a running [setUpAll] test to
+  /// dynamically add a [tearDownAll].
+  void addTearDownAll(dynamic Function() callback) =>
+      _tearDownAlls.add(callback);
+
+  /// Finalizes and returns the group being declared.
+  ///
+  /// **Note**: The tests in this group must be run in a [Invoker.guard]
+  /// context; otherwise, test errors won't be captured.
+  Group build() {
+    _checkNotBuilt('build');
+
+    _built = true;
+    var entries = _entries.map((entry) {
+      if (_solo && !_soloEntries.contains(entry)) {
+        entry = LocalTest(
+            entry.name,
+            entry.metadata
+                .change(skip: true, skipReason: 'does not have "solo"'),
+            () {});
+      }
+      return entry;
+    }).toList();
+
+    return Group(_name ?? '', entries,
+        metadata: _metadata,
+        trace: _trace,
+        setUpAll: _setUpAll,
+        tearDownAll: _tearDownAll);
+  }
+
+  /// Throws a [StateError] if [build] has been called.
+  ///
+  /// [name] should be the name of the method being called.
+  void _checkNotBuilt(String name) {
+    if (!_built) return;
+    final restrictionMessage = _isStandalone
+        ? 'When running a test as an executable directly '
+            '(not as a suite by the test runner), '
+            'tests must be declared in a synchronous block.\n'
+            'If async work is required before any tests are run '
+            'use a `setUpAll` callback.\n'
+            'If async work cannot be avoided before declaring tests, '
+            'all async events must be complete before declaring the first test.'
+        : 'If async work is required before any tests are run '
+            'use a `setUpAll` callback.\n'
+            'If async work cannot be avoided before declaring tests it must '
+            'all be awaited within the Future returned from `main`.';
+    throw StateError("Can't call $name() once tests have begun running.\n"
+        '$restrictionMessage');
+  }
+
+  /// Run the set-up functions for this and any parent groups.
+  ///
+  /// If no set-up functions are declared, this returns a [Future] that
+  /// completes immediately.
+  Future _runSetUps() async {
+    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], or `null`.
+  Test? get _setUpAll {
+    if (_setUpAlls.isEmpty) return null;
+
+    return LocalTest(_prefix('(setUpAll)'), _metadata.change(timeout: _timeout),
+        () {
+      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], 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;
+
+    return LocalTest(
+        _prefix('(tearDownAll)'), _metadata.change(timeout: _timeout), () {
+      return runZoned(() => Invoker.current!.runTearDowns(_tearDownAlls),
+          // Make the declarer visible to running scaffolds so they can add to
+          // the declarer's `tearDownAll()` list.
+          zoneValues: {#test.declarer: this});
+    }, trace: _tearDownAllTrace, guarded: false, isScaffoldAll: true);
+  }
+
+  void _addEntry(GroupEntry entry) {
+    if (_seenNames?.add(entry.name) == false) {
+      throw DuplicateTestNameException(entry.name);
+    }
+    _entries.add(entry);
+  }
+}
+
+/// 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.\n\n'
+      'See https://github.com/dart-lang/test/blob/master/pkgs/test/doc/'
+      'configuration.md#allow_test_randomization for info on enabling this.';
+}
diff --git a/pkgs/test_api/lib/src/backend/group.dart b/pkgs/test_api/lib/src/backend/group.dart
new file mode 100644
index 0000000..9a28875
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/group.dart
@@ -0,0 +1,90 @@
+// 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.
+
+import 'package:stack_trace/stack_trace.dart';
+
+import 'group_entry.dart';
+import 'metadata.dart';
+import 'suite_platform.dart';
+import 'test.dart';
+
+/// A group contains one or more tests and subgroups.
+///
+/// It includes metadata that applies to all contained tests.
+class Group implements GroupEntry {
+  @override
+  final String name;
+
+  @override
+  final Metadata metadata;
+
+  @override
+  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('', 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;
+
+  /// A test to run after all tests in the group.
+  ///
+  /// This is `null` if no `tearDown` callbacks were declared.
+  final Test? tearDownAll;
+
+  /// The number of tests (recursively) in this group.
+  int get testCount {
+    if (_testCount != null) return _testCount!;
+    _testCount = entries.fold<int>(
+        0, (count, entry) => count + (entry is Group ? entry.testCount : 1));
+    return _testCount!;
+  }
+
+  int? _testCount;
+
+  Group(this.name, Iterable<GroupEntry> entries,
+      {Metadata? metadata, this.trace, this.setUpAll, this.tearDownAll})
+      : entries = List<GroupEntry>.unmodifiable(entries),
+        metadata = metadata ?? Metadata();
+
+  @override
+  Group? forPlatform(SuitePlatform platform) {
+    if (!metadata.testOn.evaluate(platform)) return null;
+    var newMetadata = metadata.forPlatform(platform);
+    var filtered = _map((entry) => entry.forPlatform(platform));
+    if (filtered.isEmpty && entries.isNotEmpty) return null;
+    return Group(name, filtered,
+        metadata: newMetadata,
+        trace: trace,
+        setUpAll: setUpAll,
+        tearDownAll: tearDownAll);
+  }
+
+  @override
+  Group? filter(bool Function(Test) callback) {
+    var filtered = _map((entry) => entry.filter(callback));
+    if (filtered.isEmpty && entries.isNotEmpty) return null;
+    return Group(name, filtered,
+        metadata: metadata,
+        trace: trace,
+        setUpAll: setUpAll,
+        tearDownAll: tearDownAll);
+  }
+
+  /// 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) {
+    return entries
+        .map((entry) => callback(entry))
+        .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
new file mode 100644
index 0000000..a6f30bd
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/group_entry.dart
@@ -0,0 +1,39 @@
+// 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.
+
+import 'package:stack_trace/stack_trace.dart';
+
+import 'metadata.dart';
+import 'suite_platform.dart';
+import 'test.dart';
+
+/// A [Test] or [Group].
+abstract class GroupEntry {
+  /// The name of the entry, including the prefixes from any containing
+  /// [Group]s.
+  ///
+  /// This will be empty for the root group.
+  String get name;
+
+  /// The metadata for the entry, including the metadata from any containing
+  /// [Group]s.
+  Metadata get metadata;
+
+  /// 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;
+
+  /// 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);
+
+  /// 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);
+}
diff --git a/pkgs/test_api/lib/src/backend/invoker.dart b/pkgs/test_api/lib/src/backend/invoker.dart
new file mode 100644
index 0000000..58f1001
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/invoker.dart
@@ -0,0 +1,464 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+
+import 'closed_exception.dart';
+import 'declarer.dart';
+import 'group.dart';
+import 'live_test.dart';
+import 'live_test_controller.dart';
+import 'message.dart';
+import 'metadata.dart';
+import 'state.dart';
+import 'suite.dart';
+import 'suite_platform.dart';
+import 'test.dart';
+import 'test_failure.dart';
+import 'util/pretty_print.dart';
+
+/// A test in this isolate.
+class LocalTest extends Test {
+  @override
+  final String name;
+
+  @override
+  final Metadata metadata;
+
+  @override
+  final Trace? trace;
+
+  /// Whether this is a test defined using `setUpAll()` or `tearDownAll()`.
+  final bool isScaffoldAll;
+
+  /// The test body.
+  final void Function() _body;
+
+  /// Whether the test is run in its own error zone.
+  final bool _guarded;
+
+  /// Creates a new [LocalTest].
+  ///
+  /// If [guarded] is `true`, the test is run in its own error zone, and any
+  /// errors that escape that zone cause the test to fail. If it's `false`, it's
+  /// the caller's responsibility to invoke [LiveTest.run] in the context of a
+  /// call to [Invoker.guard].
+  LocalTest(this.name, this.metadata, this._body,
+      {this.trace, bool guarded = true, this.isScaffoldAll = false})
+      : _guarded = guarded;
+
+  LocalTest._(this.name, this.metadata, this._body, this.trace, this._guarded,
+      this.isScaffoldAll);
+
+  /// Loads a single runnable instance of this test.
+  @override
+  LiveTest load(Suite suite, {Iterable<Group>? groups}) {
+    var invoker = Invoker._(suite, this, groups: groups, guarded: _guarded);
+    return invoker.liveTest;
+  }
+
+  @override
+  Test? forPlatform(SuitePlatform platform) {
+    if (!metadata.testOn.evaluate(platform)) return null;
+    return LocalTest._(name, metadata.forPlatform(platform), _body, trace,
+        _guarded, isScaffoldAll);
+  }
+}
+
+/// The class responsible for managing the lifecycle of a single local test.
+///
+/// The current invoker is accessible within the zone scope of the running test
+/// using [Invoker.current]. It's used to track asynchronous callbacks and
+/// report asynchronous errors.
+class Invoker {
+  /// The live test being driven by the invoker.
+  ///
+  /// This provides a view into the state of the test being executed.
+  LiveTest get liveTest => _controller;
+  late final LiveTestController _controller;
+
+  /// Whether to run this test in its own error zone.
+  final bool _guarded;
+
+  /// Whether the user code is allowed to interact with the invoker despite it
+  /// being closed.
+  ///
+  /// A test is generally closed because the runner is shutting down (in
+  /// response to a signal) or because the test's suite is finished.
+  /// Typically calls to [addTearDown] and [addOutstandingCallback] are only
+  /// allowed before the test is closed. Tear down callbacks, however, are
+  /// allowed to perform these interactions to facilitate resource cleanup on a
+  /// best-effort basis, so the invoker is made to appear open only within the
+  /// zones running the teardown callbacks.
+  bool get _forceOpen => Zone.current[_forceOpenForTearDownKey] as bool;
+
+  /// An opaque object used as a key in the zone value map to identify
+  /// [_forceOpen].
+  ///
+  /// This is an instance variable to ensure that multiple invokers don't step
+  /// on one anothers' toes.
+  final _forceOpenForTearDownKey = Object();
+
+  /// Whether the test has been closed.
+  ///
+  /// Once the test is closed, [expect] and [expectAsync] will throw
+  /// [ClosedException]s whenever accessed to help the test stop executing as
+  /// soon as possible.
+  bool get closed => !_forceOpen && _onCloseCompleter.isCompleted;
+
+  /// A future that completes once the test has been closed.
+  Future<void> get onClose => _onCloseCompleter.future;
+  final _onCloseCompleter = Completer<void>();
+
+  /// The test being run.
+  LocalTest get _test => liveTest.test as LocalTest;
+
+  /// The outstanding callback counter for the current zone.
+  _AsyncCounter get _outstandingCallbacks {
+    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.');
+  }
+
+  /// All the zones created by [_waitForOutstandingCallbacks], in the order they
+  /// were created.
+  ///
+  /// This is used to throw timeout errors in the most recent zone.
+  final _outstandingCallbackZones = <Zone>[];
+
+  /// An opaque object used as a key in the zone value map to identify
+  /// [_outstandingCallbacks].
+  ///
+  /// This is an instance variable to ensure that multiple invokers don't step
+  /// on one anothers' toes.
+  final _counterKey = Object();
+
+  /// The number of times this [liveTest] has been run.
+  int _runCount = 0;
+
+  /// 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 {
+    // TODO(nweiz): Use a private symbol when dart2js supports it (issue 17526).
+    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<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?;
+        if (invoker != null) {
+          self.parent!.run(() => invoker._handleError(zone, error, stackTrace));
+        } else {
+          self.parent!.handleUncaughtError(error, stackTrace);
+        }
+      }));
+
+  /// The timer for tracking timeouts.
+  ///
+  /// This will be `null` until the test starts running.
+  Timer? _timeoutTimer;
+
+  /// The tear-down functions to run when this test finishes.
+  final _tearDowns = <void Function()>[];
+
+  /// Messages to print if and when this test fails.
+  final _printsOnFailure = <String>[];
+
+  Invoker._(Suite suite, LocalTest test,
+      {Iterable<Group>? groups, bool guarded = true})
+      : _guarded = guarded {
+    _controller = LiveTestController(
+        suite, test, _onRun, _onCloseCompleter.complete,
+        groups: groups);
+  }
+
+  /// Runs [callback] after this test completes.
+  ///
+  /// The [callback] may return a [Future]. Like all tear-downs, callbacks are
+  /// run in the reverse of the order they're declared.
+  void addTearDown(dynamic Function() callback) {
+    if (closed) throw ClosedException();
+
+    if (_test.isScaffoldAll) {
+      Declarer.current!.addTearDownAll(callback);
+    } else {
+      _tearDowns.add(callback);
+    }
+  }
+
+  /// Tells the invoker that there's a callback running that it should wait for
+  /// before considering the test successful.
+  ///
+  /// Each call to [addOutstandingCallback] should be followed by a call to
+  /// [removeOutstandingCallback] once the callback is no longer running. Note
+  /// that only successful tests wait for outstanding callbacks; as soon as a
+  /// test experiences an error, any further calls to [addOutstandingCallback]
+  /// or [removeOutstandingCallback] will do nothing.
+  ///
+  /// Throws a [ClosedException] if this test has been closed.
+  void addOutstandingCallback() {
+    if (closed) throw ClosedException();
+    _outstandingCallbacks.increment();
+  }
+
+  /// Tells the invoker that a callback declared with [addOutstandingCallback]
+  /// is no longer running.
+  void removeOutstandingCallback() {
+    heartbeat();
+    _outstandingCallbacks.decrement();
+  }
+
+  /// Run [tearDowns] in reverse order.
+  ///
+  /// An exception thrown in a tearDown callback will cause the test to fail, if
+  /// it isn't already failing, but it won't prevent the remaining callbacks
+  /// from running. This invoker will not be closeable within the zone that the
+  /// teardowns are running in.
+  Future<void> runTearDowns(List<FutureOr<void> Function()> tearDowns) {
+    heartbeat();
+    return runZoned(() async {
+      while (tearDowns.isNotEmpty) {
+        var completer = Completer<void>();
+
+        addOutstandingCallback();
+        _waitForOutstandingCallbacks(() {
+          Future.sync(tearDowns.removeLast()).whenComplete(completer.complete);
+        }).then((_) => removeOutstandingCallback()).unawaited;
+
+        await completer.future;
+      }
+    }, zoneValues: {_forceOpenForTearDownKey: true});
+  }
+
+  /// Runs [fn] and completes once [fn] and all outstanding callbacks registered
+  /// within [fn] have completed.
+  ///
+  /// Outstanding callbacks registered within [fn] will *not* be registered as
+  /// outstanding callback outside of [fn].
+  Future<void> _waitForOutstandingCallbacks(FutureOr<void> Function() fn) {
+    heartbeat();
+
+    Zone? zone;
+    var counter = _AsyncCounter();
+    runZoned(() async {
+      zone = Zone.current;
+      _outstandingCallbackZones.add(zone!);
+      try {
+        await fn();
+      } finally {
+        counter.decrement();
+      }
+    }, zoneValues: {_counterKey: counter});
+
+    return counter.onZero.whenComplete(() {
+      _outstandingCallbackZones.remove(zone!);
+    });
+  }
+
+  /// Notifies the invoker that progress is being made.
+  ///
+  /// Each heartbeat resets the timeout timer. This helps ensure that
+  /// long-running tests that still make progress don't time out.
+  void heartbeat() {
+    if (liveTest.isComplete) return;
+    if (_timeoutTimer != null) _timeoutTimer!.cancel();
+    if (liveTest.suite.ignoreTimeouts == true) return;
+
+    const defaultTimeout = Duration(seconds: 30);
+    var timeout = liveTest.test.metadata.timeout.apply(defaultTimeout);
+    if (timeout == null) return;
+    String message() {
+      var message = 'Test timed out after ${niceDuration(timeout)}.';
+      if (timeout == defaultTimeout) {
+        message += ' See https://pub.dev/packages/test#timeouts';
+      }
+      return message;
+    }
+
+    _timeoutTimer = Zone.root.createTimer(timeout, () {
+      _outstandingCallbackZones.last.run(() {
+        _handleError(Zone.current, TimeoutException(message(), timeout));
+        _outstandingCallbacks.complete();
+      });
+    });
+  }
+
+  /// Marks the current test as skipped.
+  ///
+  /// If passed, [message] is emitted as a skip message.
+  ///
+  /// 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]) {
+    if (liveTest.state.shouldBeDone) {
+      // Set the state explicitly so we don't get an extra error about the test
+      // failing after being complete.
+      _controller.setState(const State(Status.complete, Result.error));
+      throw 'This test was marked as skipped after it had already completed.\n'
+          'Make sure to use a matching library which informs the test runner\n'
+          'of pending async work.';
+    }
+
+    if (message != null) _controller.message(Message.skip(message));
+    // TODO: error if the test is already complete.
+    _controller.setState(const State(Status.pending, Result.skipped));
+  }
+
+  /// Prints [message] if and when this test fails.
+  void printOnFailure(String message) {
+    message = message.trim();
+    if (liveTest.state.result.isFailing) {
+      _print('\n$message');
+    } else {
+      _printsOnFailure.add(message);
+    }
+  }
+
+  /// Notifies the invoker of an asynchronous error.
+  ///
+  /// The [zone] is the zone in which the error was thrown.
+  void _handleError(Zone zone, Object error, [StackTrace? stackTrace]) {
+    // Ignore errors propagated from previous test runs
+    if (_runCount != zone[#runCount]) return;
+
+    // Get the chain information from the zone in which the error was thrown.
+    zone.run(() {
+      if (stackTrace == null) {
+        stackTrace = Chain.current();
+      } else {
+        stackTrace = Chain.forTrace(stackTrace!);
+      }
+    });
+
+    // Store these here because they'll change when we set the state below.
+    var shouldBeDone = liveTest.state.shouldBeDone;
+
+    if (error is! TestFailure) {
+      _controller.setState(const State(Status.complete, Result.error));
+    } else if (liveTest.state.result != Result.error) {
+      _controller.setState(const State(Status.complete, Result.failure));
+    }
+
+    _controller.addError(error, stackTrace!);
+
+    if (_printsOnFailure.isNotEmpty) {
+      _print(_printsOnFailure.join('\n\n'));
+      _printsOnFailure.clear();
+    }
+
+    // If a test was supposed to be done but then had an error, that indicates
+    // that it was poorly-written and could be flaky.
+    if (!shouldBeDone) return;
+
+    // However, users don't think of load tests as "tests", so the error isn't
+    // helpful for them.
+    if (liveTest.suite.isLoadSuite) return;
+
+    _handleError(
+        zone,
+        'This test failed after it had already completed.\n'
+        'Make sure to use a matching library which informs the test runner\n'
+        'of pending async work.',
+        stackTrace);
+  }
+
+  /// The method that's run when the test is started.
+  void _onRun() {
+    _controller.setState(const State(Status.running, Result.success));
+
+    _runCount++;
+    Chain.capture(() {
+      _guardIfGuarded(() {
+        runZoned(() async {
+          // Run the test asynchronously so that the "running" state change
+          // has a chance to hit its event handler(s) before the test produces
+          // an error. If an error is emitted before the first state change is
+          // handled, we can end up with [onError] callbacks firing before the
+          // corresponding [onStateChange], which violates the timing
+          // guarantees.
+          //
+          // Use the event loop over the microtask queue to avoid starvation.
+          await Future(() {});
+
+          await _waitForOutstandingCallbacks(_test._body);
+          await _waitForOutstandingCallbacks(() => runTearDowns(_tearDowns));
+
+          if (_timeoutTimer != null) _timeoutTimer!.cancel();
+
+          if (liveTest.state.result != Result.success &&
+              _runCount < liveTest.test.metadata.retry + 1) {
+            _controller.message(Message.print('Retry: ${liveTest.test.name}'));
+            _onRun();
+            return;
+          }
+
+          _controller.setState(State(Status.complete, liveTest.state.result));
+
+          _controller.completer.complete();
+        },
+            zoneValues: {
+              #test.invoker: this,
+              _forceOpenForTearDownKey: false,
+              #runCount: _runCount,
+            },
+            zoneSpecification:
+                ZoneSpecification(print: (_, __, ___, line) => _print(line)));
+      });
+    }, when: liveTest.test.metadata.chainStackTraces, errorZone: false);
+  }
+
+  /// Runs [callback], in a [Invoker.guard] context if [_guarded] is `true`.
+  void _guardIfGuarded(void Function() callback) {
+    if (_guarded) {
+      Invoker.guard(callback);
+    } else {
+      callback();
+    }
+  }
+
+  /// Prints [text] as a message to [_controller].
+  void _print(String text) => _controller.message(Message.print(text));
+}
+
+/// A manually incremented/decremented counter that completes a [Future] the
+/// first time it reaches zero or is forcefully completed.
+class _AsyncCounter {
+  var _count = 1;
+
+  /// A Future that completes the first time the counter reaches 0.
+  Future<void> get onZero => _completer.future;
+  final _completer = Completer<void>();
+
+  void increment() {
+    _count++;
+  }
+
+  void decrement() {
+    _count--;
+    if (_count != 0) return;
+    if (_completer.isCompleted) return;
+    _completer.complete();
+  }
+
+  /// Force [onZero] to complete.
+  ///
+  /// No effect if [onZero] has already completed.
+  void complete() {
+    if (!_completer.isCompleted) _completer.complete();
+  }
+}
+
+extension<T> on Future<T> {
+  void get unawaited {}
+}
diff --git a/pkgs/test_api/lib/src/backend/live_test.dart b/pkgs/test_api/lib/src/backend/live_test.dart
new file mode 100644
index 0000000..683e87c
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/live_test.dart
@@ -0,0 +1,148 @@
+// 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.
+
+import 'dart:async';
+
+import 'group.dart';
+import 'message.dart';
+import 'state.dart';
+import 'suite.dart';
+import 'test.dart';
+
+/// A runnable instance of a test.
+///
+/// This is distinct from [Test] in order to keep [Test] immutable. Running a
+/// test requires state, and [LiveTest] provides a view of the state of the test
+/// as it runs.
+///
+/// If the state changes, [state] will be updated before [onStateChange] fires.
+/// Likewise, if an error is caught, it will be added to [errors] before being
+/// emitted via [onError]. If an error causes a state change, [onStateChange]
+/// will fire before [onError]. If an error or other state change causes the
+/// test to complete, [onComplete] will complete after [onStateChange] and
+/// [onError] fire.
+abstract class LiveTest {
+  /// The suite within which this test is being run.
+  Suite get suite;
+
+  /// The groups within which this test is being run, from the outermost to the
+  /// innermost.
+  ///
+  /// This will always contain at least the implicit top-level group.
+  List<Group> get groups;
+
+  /// The running test.
+  Test get test;
+
+  /// The current state of the running test.
+  ///
+  /// This starts as [Status.pending] and [Result.success]. It will be updated
+  /// before [onStateChange] fires.
+  ///
+  /// Note that even if this is marked [Status.complete], the test may still be
+  /// running code asynchronously. A test is considered complete either once it
+  /// hits its first error or when all [expectAsync] callbacks have been called
+  /// and any returned [Future] has completed, but it's possible for further
+  /// processing to happen, which may cause further errors. It's even possible
+  /// for a test that was marked [Status.complete] and [Result.success] to be
+  /// marked as [Result.error] later.
+  State get state;
+
+  /// Returns whether this test has completed.
+  ///
+  /// This is equivalent to [state.status] being [Status.complete].
+  ///
+  /// Note that even if this returns `true`, the test may still be
+  /// running code asynchronously. A test is considered complete either once it
+  /// hits its first error or when all [expectAsync] callbacks have been called
+  /// and any returned [Future] has completed, but it's possible for further
+  /// processing to happen, which may cause further errors.
+  bool get isComplete => state.status == Status.complete;
+
+  // A stream that emits a new [State] whenever [state] changes.
+  //
+  // This will only ever emit a [State] if it's different than the previous
+  // [state]. It will emit an event after [state] has been updated. Note that
+  // since this is an asynchronous stream, it's possible for [state] not to
+  // match the [State] that it emits within the [Stream.listen] callback.
+  Stream<State> get onStateChange;
+
+  /// An unmodifiable list of all errors that have been caught while running
+  /// this test.
+  ///
+  /// This will be updated before [onError] fires. These errors are not
+  /// guaranteed to have the same types as when they were thrown; for example,
+  /// they may need to be serialized across isolate boundaries. The stack traces
+  /// will be [Chain]s.
+  List<AsyncError> get errors;
+
+  /// A stream that emits a new [AsyncError] whenever an error is caught.
+  ///
+  /// This will emit an event after [errors] is updated. These errors are not
+  /// guaranteed to have the same types as when they were thrown; for example,
+  /// they may need to be serialized across isolate boundaries. The stack traces
+  /// will be [Chain]s.
+  Stream<AsyncError> get onError;
+
+  /// A stream that emits messages produced by the test.
+  Stream<Message> get onMessage;
+
+  /// A [Future] that completes once the test is complete.
+  ///
+  /// This will complete after [onStateChange] has fired, and after [onError]
+  /// has fired if the test completes because of an error. It's the same as the
+  /// [Future] returned by [run].
+  ///
+  /// Note that even once this completes, the test may still be running code
+  /// asynchronously. A test is considered complete either once it hits its
+  /// first error or when all [expectAsync] callbacks have been called and any
+  /// returned [Future] has completed, but it's possible for further processing
+  /// to happen, which may cause further errors.
+  Future get onComplete;
+
+  /// The name of this live test without any group prefixes.
+  String get individualName {
+    var group = groups.last;
+    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
+    // to represent skipping the entire group.
+    if (test.name.length == group.name.length) return '';
+
+    return test.name.substring(group.name.length + 1);
+  }
+
+  /// Loads a copy of this [LiveTest] that's able to be run again.
+  LiveTest copy() => test.load(suite, groups: groups);
+
+  /// Signals that this test should start running as soon as possible.
+  ///
+  /// A test may not start running immediately for various reasons specific to
+  /// the means by which it's defined. Until it starts running, [state] will
+  /// continue to be marked [Status.pending].
+  ///
+  /// This returns the same [Future] as [onComplete]. It may not be called more
+  /// than once.
+  Future run();
+
+  /// Signals that this test should stop emitting events and release any
+  /// resources it may have allocated.
+  ///
+  /// Once [close] is called, [onComplete] will complete if it hasn't already
+  /// and [onStateChange] and [onError] will close immediately. This means that,
+  /// if the test was running at the time [close] is called, it will never emit
+  /// a [Status.complete] state-change event. Once a test is closed, [expect]
+  /// and [expectAsync] will throw a [ClosedException] to help the test
+  /// terminate as quickly as possible.
+  ///
+  /// This doesn't automatically happen after the test completes because there
+  /// may be more asynchronous work going on in the background that could
+  /// produce new errors.
+  ///
+  /// Returns a [Future] that completes once all resources are released *and*
+  /// the test has completed. This allows the caller to wait until the test's
+  /// tear-down logic has run.
+  Future close();
+}
diff --git a/pkgs/test_api/lib/src/backend/live_test_controller.dart b/pkgs/test_api/lib/src/backend/live_test_controller.dart
new file mode 100644
index 0000000..956c293
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/live_test_controller.dart
@@ -0,0 +1,178 @@
+// 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.
+
+import 'dart:async';
+import 'dart:collection';
+
+import 'package:stack_trace/stack_trace.dart';
+
+import 'group.dart';
+import 'live_test.dart';
+import 'message.dart';
+import 'state.dart';
+import 'suite.dart';
+import 'test.dart';
+
+/// A concrete [LiveTest] that enforces some lifecycle guarantees.
+///
+/// This automatically handles some of [LiveTest]'s guarantees, but for the most
+/// part it's the caller's responsibility to make sure everything gets
+/// dispatched in the correct order.
+class LiveTestController extends LiveTest {
+  @Deprecated('Use this instance instead')
+  LiveTest get liveTest => this;
+
+  @override
+  final Suite suite;
+
+  @override
+  final List<Group> groups;
+
+  @override
+  final Test test;
+
+  /// The function that will actually start the test running.
+  final void Function() _onRun;
+
+  /// A function to run when the test is closed.
+  ///
+  /// This may be `null`.
+  final void Function() _onClose;
+
+  /// The list of errors caught by the test.
+  final _errors = <AsyncError>[];
+
+  @override
+  List<AsyncError> get errors => UnmodifiableListView(_errors);
+
+  /// The current state of the test.
+  @override
+  State state = const State(Status.pending, Result.success);
+
+  /// The controller for [onStateChange].
+  ///
+  /// This is synchronous to ensure that events are well-ordered across multiple
+  /// streams.
+  final _onStateChange = StreamController<State>.broadcast(sync: true);
+  @override
+  Stream<State> get onStateChange => _onStateChange.stream;
+
+  /// The controller for [onError].
+  ///
+  /// This is synchronous to ensure that events are well-ordered across multiple
+  /// streams.
+  final _onError = StreamController<AsyncError>.broadcast(sync: true);
+  @override
+  Stream<AsyncError> get onError => _onError.stream;
+
+  /// The controller for [onMessage].
+  ///
+  /// This is synchronous to ensure that events are well-ordered across multiple
+  /// streams.
+  final _onMessage = StreamController<Message>.broadcast(sync: true);
+  @override
+  Stream<Message> get onMessage => _onMessage.stream;
+
+  final completer = Completer<void>();
+
+  /// Whether [run] has been called.
+  var _runCalled = false;
+
+  /// Whether [close] has been called.
+  bool get _isClosed => _onError.isClosed;
+
+  /// Creates a new controller for a [LiveTest].
+  ///
+  /// [test] is the test being run; [suite] is the suite that contains it.
+  ///
+  /// [onRun] is a function that's called from [LiveTest.run]. It should start
+  /// the test running. The controller takes care of ensuring that
+  /// [LiveTest.run] isn't called more than once and that [LiveTest.onComplete]
+  /// is returned.
+  ///
+  /// [onClose] is a function that's called the first time [LiveTest.close] is
+  /// called. It should clean up any resources that have been allocated for the
+  /// test and ensure that the test finishes quickly if it's still running. It
+  /// will only be called if [onRun] has been called first.
+  ///
+  /// 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})
+      : groups = groups == null ? [suite.group] : List.unmodifiable(groups);
+
+  /// Adds an error to the [LiveTest].
+  ///
+  /// 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(Object error, StackTrace? stackTrace) {
+    if (_isClosed) return;
+
+    var asyncError = AsyncError(
+        error, Chain.forTrace(stackTrace ?? StackTrace.fromString('')));
+    _errors.add(asyncError);
+    _onError.add(asyncError);
+  }
+
+  /// Sets the current state of the [LiveTest] to [newState].
+  ///
+  /// If [newState] is different than the old state, this both sets
+  /// [LiveTest.state] and emits the new state via [LiveTest.onStateChanged]. If
+  /// it's not different, this does nothing.
+  void setState(State newState) {
+    if (_isClosed) return;
+    if (state == newState) return;
+
+    state = newState;
+    _onStateChange.add(newState);
+  }
+
+  /// Emits message over [LiveTest.onMessage].
+  void message(Message message) {
+    if (_onMessage.hasListener) {
+      _onMessage.add(message);
+    } else {
+      // Make sure all messages get surfaced one way or another to aid in
+      // debugging.
+      Zone.root.print(message.text);
+    }
+  }
+
+  @override
+  Future<void> run() {
+    if (_runCalled) {
+      throw StateError('LiveTest.run() may not be called more than once.');
+    } else if (_isClosed) {
+      throw StateError('LiveTest.run() may not be called for a closed '
+          'test.');
+    }
+    _runCalled = true;
+
+    _onRun();
+    return onComplete;
+  }
+
+  /// Returns a future that completes when the test is complete.
+  ///
+  /// We also wait for the state to transition to Status.complete.
+  @override
+  Future<void> get onComplete => completer.future;
+
+  @override
+  Future<void> close() {
+    if (_isClosed) return onComplete;
+
+    _onStateChange.close();
+    _onError.close();
+
+    if (_runCalled) {
+      _onClose();
+    } else {
+      completer.complete();
+    }
+
+    return onComplete;
+  }
+}
diff --git a/pkgs/test_api/lib/src/backend/message.dart b/pkgs/test_api/lib/src/backend/message.dart
new file mode 100644
index 0000000..a027460
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/message.dart
@@ -0,0 +1,41 @@
+// 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.
+
+/// A message emitted by a test.
+///
+/// A message encompasses any textual information that should be presented to
+/// the user. Reporters are encouraged to visually distinguish different message
+/// types.
+class Message {
+  final MessageType type;
+
+  final String text;
+
+  Message(this.type, this.text);
+
+  Message.print(this.text) : type = MessageType.print;
+  Message.skip(this.text) : type = MessageType.skip;
+}
+
+class MessageType {
+  /// A message explicitly printed by the user's test.
+  static const print = MessageType._('print');
+
+  /// A message indicating that a test, or some portion of one, was skipped.
+  static const skip = MessageType._('skip');
+
+  /// The name of the message type.
+  final String name;
+
+  factory MessageType.parse(String name) => switch (name) {
+        'print' => MessageType.print,
+        'skip' => MessageType.skip,
+        _ => throw ArgumentError('Invalid message type "$name".'),
+      };
+
+  const MessageType._(this.name);
+
+  @override
+  String toString() => name;
+}
diff --git a/pkgs/test_api/lib/src/backend/metadata.dart b/pkgs/test_api/lib/src/backend/metadata.dart
new file mode 100644
index 0000000..d0663b1
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/metadata.dart
@@ -0,0 +1,420 @@
+// 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.
+
+import 'package:boolean_selector/boolean_selector.dart';
+import 'package:collection/collection.dart';
+
+import 'configuration/skip.dart';
+import 'configuration/timeout.dart';
+import 'platform_selector.dart';
+import 'suite_platform.dart';
+import 'util/identifier_regex.dart';
+import 'util/pretty_print.dart';
+
+/// Metadata for a test or test suite.
+///
+/// This metadata comes from declarations on the test itself; it doesn't include
+/// configuration from the user.
+final class Metadata {
+  /// Empty metadata with only default values.
+  ///
+  /// Using this is slightly more efficient than manually constructing a new
+  /// metadata with no arguments.
+  static final empty = Metadata._();
+
+  /// The selector indicating which platforms the suite supports.
+  final PlatformSelector testOn;
+
+  /// The modification to the timeout for the test or suite.
+  final Timeout timeout;
+
+  /// Whether the test or suite should be skipped.
+  bool get skip => _skip ?? false;
+  final bool? _skip;
+
+  /// The reason the test or suite should be skipped, if given.
+  final String? skipReason;
+
+  /// Whether to use verbose stack traces.
+  bool get verboseTrace => _verboseTrace ?? false;
+  final bool? _verboseTrace;
+
+  /// Whether to chain stack traces.
+  bool get chainStackTraces => _chainStackTraces ?? _verboseTrace ?? false;
+  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;
+
+  /// Platform-specific metadata.
+  ///
+  /// Each key identifies a platform, and its value identifies the specific
+  /// metadata for that platform. These can be applied by calling [forPlatform].
+  final Map<PlatformSelector, Metadata> onPlatform;
+
+  /// Metadata that applies only when specific tags are applied.
+  ///
+  /// Tag-specific metadata is applied when merging this with other metadata.
+  /// Note that unlike [onPlatform], the base metadata takes precedence over any
+  /// tag-specific metadata.
+  ///
+  /// This is guaranteed not to have any keys that match [tags]; those are
+  /// resolved when the metadata is constructed.
+  final Map<BooleanSelector, Metadata> forTag;
+
+  /// The language version comment, if one is present.
+  ///
+  /// Only available for test suites and not individual tests.
+  final String? languageVersionComment;
+
+  /// Parses a user-provided map into the value for [onPlatform].
+  static Map<PlatformSelector, Metadata> _parseOnPlatform(
+      Map<String, dynamic>? onPlatform) {
+    if (onPlatform == null) return {};
+
+    var result = <PlatformSelector, Metadata>{};
+    onPlatform.forEach((platform, metadata) {
+      var selector = PlatformSelector.parse(platform);
+      if (metadata is Timeout || metadata is Skip) {
+        result[selector] = _parsePlatformOptions(platform, [metadata]);
+      } else if (metadata is List) {
+        result[selector] = _parsePlatformOptions(platform, metadata);
+      } else {
+        throw ArgumentError('Metadata for platform "$platform" must be a '
+            'Timeout, Skip, or List of those; was "$metadata".');
+      }
+    });
+    return result;
+  }
+
+  static Metadata _parsePlatformOptions(
+      String platform, List<dynamic> metadata) {
+    Timeout? timeout;
+    dynamic skip;
+    for (var metadatum in metadata) {
+      if (metadatum is Timeout) {
+        if (timeout != null) {
+          throw ArgumentError('Only a single Timeout may be declared for '
+              '"$platform".');
+        }
+
+        timeout = metadatum;
+      } else if (metadatum is Skip) {
+        if (skip != null) {
+          throw ArgumentError('Only a single Skip may be declared for '
+              '"$platform".');
+        }
+
+        skip = metadatum.reason ?? true;
+      } else {
+        throw ArgumentError('Metadata for platform "$platform" must be a '
+            'Timeout, Skip, or List of those; was "$metadata".');
+      }
+    }
+
+    return Metadata.parse(timeout: timeout, skip: skip);
+  }
+
+  /// Parses a user-provided [String] or [Iterable] into the value for [tags].
+  ///
+  /// Throws an [ArgumentError] if [tags] is not a [String] or an [Iterable].
+  static Set<String> _parseTags(Object? tags) {
+    if (tags == null) return {};
+    if (tags is String) return {tags};
+    if (tags is! Iterable) {
+      throw ArgumentError.value(
+          tags, 'tags', 'must be either a String or an Iterable.');
+    }
+
+    if (tags.any((tag) => tag is! String)) {
+      throw ArgumentError.value(tags, 'tags', 'must contain only Strings.');
+    }
+
+    return Set.from(tags);
+  }
+
+  /// Creates new Metadata.
+  ///
+  /// [testOn] defaults to [PlatformSelector.all].
+  ///
+  /// If [forTag] contains metadata that applies to [tags], that metadata is
+  /// 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}) {
+    // Returns metadata without forTag resolved at all.
+    Metadata unresolved() => Metadata._(
+        testOn: testOn,
+        timeout: timeout,
+        skip: skip,
+        verboseTrace: verboseTrace,
+        chainStackTraces: chainStackTraces,
+        retry: retry,
+        skipReason: skipReason,
+        tags: tags,
+        onPlatform: onPlatform,
+        forTag: forTag,
+        languageVersionComment: languageVersionComment);
+
+    // If there's no tag-specific metadata, or if none of it applies, just
+    // return the metadata as-is.
+    if (forTag == null || tags == null) return unresolved();
+    tags = Set.from(tags);
+    forTag = Map.from(forTag);
+
+    // Otherwise, resolve the tag-specific components. Doing this eagerly means
+    // we only have to resolve suite- or group-level tags once, rather than
+    // 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 (merged == empty) return unresolved();
+    return merged.merge(unresolved());
+  }
+
+  /// Creates new Metadata.
+  ///
+  /// Unlike [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,
+        timeout = timeout ?? const Timeout.factor(1),
+        _skip = skip,
+        _verboseTrace = verboseTrace,
+        _chainStackTraces = chainStackTraces,
+        _retry = retry,
+        tags = UnmodifiableSetView(tags == null ? {} : tags.toSet()),
+        onPlatform =
+            onPlatform == null ? const {} : UnmodifiableMapView(onPlatform),
+        forTag = forTag == null ? const {} : UnmodifiableMapView(forTag) {
+    if (retry != null) RangeError.checkNotNegative(retry, 'retry');
+    _validateTags();
+  }
+
+  /// Creates a new Metadata, but with fields parsed from caller-friendly values
+  /// where applicable.
+  ///
+  /// Throws a [FormatException] if any field is invalid.
+  Metadata.parse(
+      {String? testOn,
+      Timeout? timeout,
+      dynamic skip,
+      bool? verboseTrace,
+      bool? chainStackTraces,
+      int? retry,
+      Map<String, dynamic>? onPlatform,
+      Object? /* String|Iterable<String> */ tags,
+      this.languageVersionComment})
+      : testOn = testOn == null
+            ? PlatformSelector.all
+            : PlatformSelector.parse(testOn),
+        timeout = timeout ?? const Timeout.factor(1),
+        _skip = skip == null ? null : skip != false,
+        _verboseTrace = verboseTrace,
+        _chainStackTraces = chainStackTraces,
+        _retry = retry,
+        skipReason = skip is String ? skip : null,
+        onPlatform = _parseOnPlatform(onPlatform),
+        tags = _parseTags(tags),
+        forTag = const {} {
+    if (skip != null && skip is! String && skip is! bool) {
+      throw ArgumentError('"skip" must be a String or a bool, was "$skip".');
+    }
+
+    if (retry != null) RangeError.checkNotNegative(retry, 'retry');
+
+    _validateTags();
+  }
+
+  /// Deserializes the result of [Metadata.serialize] into a new [Metadata].
+  Metadata.deserialize(Map serialized)
+      : testOn = serialized['testOn'] == null
+            ? 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 num?)?.toInt(),
+        tags = Set.from(serialized['tags'] as Iterable),
+        onPlatform = {
+          for (var pair in serialized['onPlatform'] as List)
+            PlatformSelector.parse(pair.first as String):
+                Metadata.deserialize(pair.last as Map)
+        },
+        forTag = (serialized['forTag'] as Map).map((key, nested) => MapEntry(
+            BooleanSelector.parse(key as String),
+            Metadata.deserialize(nested as Map))),
+        languageVersionComment =
+            serialized['languageVersionComment'] as String?;
+
+  /// Deserializes timeout from the format returned by [_serializeTimeout].
+  static Timeout _deserializeTimeout(Object? serialized) {
+    if (serialized == 'none') return Timeout.none;
+    var scaleFactor = (serialized as Map)['scaleFactor'];
+    if (scaleFactor != null) return Timeout.factor(scaleFactor as num);
+    return Timeout(
+        Duration(microseconds: (serialized['duration'] as num).toInt()));
+  }
+
+  /// Throws an [ArgumentError] if any tags in [tags] aren't hyphenated
+  /// identifiers.
+  void _validateTags() {
+    var invalidTags = tags
+        .where((tag) => !tag.contains(anchoredHyphenatedIdentifier))
+        .map((tag) => '"$tag"')
+        .toList();
+
+    if (invalidTags.isEmpty) return;
+
+    throw ArgumentError("Invalid ${pluralize('tag', invalidTags.length)} "
+        '${toSentence(invalidTags)}. Tags must be (optionally hyphenated) '
+        'Dart identifiers.');
+  }
+
+  /// Throws a [FormatException] if any [PlatformSelector]s use any variables
+  /// that don't appear either in [validVariables] or in the set of variables
+  /// that are known to be valid for all selectors.
+  void validatePlatformSelectors(Set<String> validVariables) {
+    testOn.validate(validVariables);
+    onPlatform.forEach((selector, metadata) {
+      selector.validate(validVariables);
+      metadata.validatePlatformSelectors(validVariables);
+    });
+  }
+
+  /// Return a new [Metadata] that merges [this] with [other].
+  ///
+  /// If the two [Metadata]s have conflicting properties, [other] wins. If
+  /// either has a [forTag] metadata for one of the other's tags, that metadata
+  /// is merged as well.
+  Metadata merge(Metadata other) => Metadata(
+      testOn: testOn.intersection(other.testOn),
+      timeout: timeout.merge(other.timeout),
+      skip: other._skip ?? _skip,
+      skipReason: other.skipReason ?? skipReason,
+      verboseTrace: other._verboseTrace ?? _verboseTrace,
+      chainStackTraces: other._chainStackTraces ?? _chainStackTraces,
+      retry: other._retry ?? _retry,
+      tags: tags.union(other.tags),
+      onPlatform: mergeMaps(onPlatform, other.onPlatform,
+          value: (metadata1, metadata2) => metadata1.merge(metadata2)),
+      forTag: mergeMaps(forTag, other.forTag,
+          value: (metadata1, metadata2) => metadata1.merge(metadata2)),
+      languageVersionComment:
+          other.languageVersionComment ?? languageVersionComment);
+
+  /// 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}) {
+    testOn ??= this.testOn;
+    timeout ??= this.timeout;
+    skip ??= _skip;
+    verboseTrace ??= _verboseTrace;
+    chainStackTraces ??= _chainStackTraces;
+    retry ??= _retry;
+    skipReason ??= this.skipReason;
+    onPlatform ??= this.onPlatform;
+    tags ??= this.tags;
+    forTag ??= this.forTag;
+    languageVersionComment ??= this.languageVersionComment;
+    return Metadata(
+        testOn: testOn,
+        timeout: timeout,
+        skip: skip,
+        verboseTrace: verboseTrace,
+        chainStackTraces: chainStackTraces,
+        skipReason: skipReason,
+        onPlatform: onPlatform,
+        tags: tags,
+        forTag: forTag,
+        retry: retry,
+        languageVersionComment: languageVersionComment);
+  }
+
+  /// Returns a copy of [this] with all platform-specific metadata from
+  /// [onPlatform] resolved.
+  Metadata forPlatform(SuitePlatform platform) {
+    if (onPlatform.isEmpty) return this;
+
+    var metadata = this;
+    onPlatform.forEach((platformSelector, platformMetadata) {
+      if (!platformSelector.evaluate(platform)) return;
+      metadata = metadata.merge(platformMetadata);
+    });
+    return metadata.change(onPlatform: {});
+  }
+
+  /// Serializes [this] into a JSON-safe object that can be deserialized using
+  /// [Metadata.deserialize].
+  Map<String, dynamic> serialize() {
+    // Make this a list to guarantee that the order is preserved.
+    var serializedOnPlatform = <List<Object>>[];
+    onPlatform.forEach((key, value) {
+      serializedOnPlatform.add([key.toString(), value.serialize()]);
+    });
+
+    return {
+      'testOn': testOn == PlatformSelector.all ? null : testOn.toString(),
+      'timeout': _serializeTimeout(timeout),
+      'skip': _skip,
+      'skipReason': skipReason,
+      'verboseTrace': _verboseTrace,
+      'chainStackTraces': _chainStackTraces,
+      'retry': _retry,
+      'tags': tags.toList(),
+      'onPlatform': serializedOnPlatform,
+      'forTag': forTag.map((selector, metadata) =>
+          MapEntry(selector.toString(), metadata.serialize())),
+      'languageVersionComment': languageVersionComment,
+    };
+  }
+
+  /// Serializes timeout into a JSON-safe object.
+  dynamic _serializeTimeout(Timeout timeout) {
+    if (timeout == Timeout.none) return 'none';
+    return {
+      '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
new file mode 100644
index 0000000..a86c1e7
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/operating_system.dart
@@ -0,0 +1,74 @@
+// 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.
+
+/// An enum of all operating systems supported by Dart.
+///
+/// This is used for selecting which operating systems a test can run on. Even
+/// for browser tests, this indicates the operating system of the machine
+/// running the test runner.
+class OperatingSystem {
+  /// Microsoft Windows.
+  static const windows = OperatingSystem._('Windows', 'windows');
+
+  /// Mac OS X.
+  static const macOS = OperatingSystem._('OS X', 'mac-os');
+
+  /// GNU/Linux.
+  static const linux = OperatingSystem._('Linux', 'linux');
+
+  /// Android.
+  ///
+  /// Since this is the operating system the test runner is running on, this
+  /// won't be true when testing remotely on an Android browser.
+  static const android = OperatingSystem._('Android', 'android');
+
+  /// iOS.
+  ///
+  /// Since this is the operating system the test runner is running on, this
+  /// won't be true when testing remotely on an iOS browser.
+  static const iOS = OperatingSystem._('iOS', 'ios');
+
+  /// No operating system.
+  ///
+  /// This is used when running in the browser, or if an unrecognized operating
+  /// system is used. It can't be referenced by name in platform selectors.
+  static const none = OperatingSystem._('none', 'none');
+
+  /// A list of all instances of [OperatingSystem] other than [none].
+  static const all = [windows, macOS, linux, android, iOS];
+
+  /// Finds an operating system by its name.
+  ///
+  /// If no operating system is found, returns [none].
+  static OperatingSystem find(String identifier) =>
+      all.firstWhere((platform) => platform.identifier == identifier,
+          orElse: () => none);
+
+  /// Finds an operating system by the return value from `dart:io`'s
+  /// `Platform.operatingSystem`.
+  ///
+  /// If no operating system is found, returns [none].
+  static OperatingSystem findByIoName(String name) => switch (name) {
+        'windows' => windows,
+        'macos' => macOS,
+        'linux' => linux,
+        'android' => android,
+        'ios' => iOS,
+        _ => none,
+      };
+
+  /// The human-friendly of the operating system.
+  final String name;
+
+  /// The identifier used to look up the operating system.
+  final String identifier;
+
+  /// Whether this is a POSIX-ish operating system.
+  bool get isPosix => this != windows && this != none;
+
+  const OperatingSystem._(this.name, this.identifier);
+
+  @override
+  String toString() => name;
+}
diff --git a/pkgs/test_api/lib/src/backend/platform_selector.dart b/pkgs/test_api/lib/src/backend/platform_selector.dart
new file mode 100644
index 0000000..c65dc8b
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/platform_selector.dart
@@ -0,0 +1,116 @@
+// 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.
+
+import 'package:boolean_selector/boolean_selector.dart';
+import 'package:source_span/source_span.dart';
+
+import 'compiler.dart';
+import 'operating_system.dart';
+import 'runtime.dart';
+import 'suite_platform.dart';
+
+/// The set of variable names that are valid for all platform selectors.
+final _universalValidVariables = {
+  'posix',
+  'dart-vm',
+  'browser',
+  'js',
+  'blink',
+  'google',
+  'wasm',
+  for (var runtime in Runtime.builtIn) runtime.identifier,
+  for (var compiler in Compiler.builtIn) compiler.identifier,
+  for (var os in OperatingSystem.all) os.identifier,
+};
+
+/// An expression for selecting certain platforms, including operating systems
+/// and browsers.
+///
+/// This uses the [boolean selector][] syntax.
+///
+/// [boolean selector]: https://pub.dev/packages/boolean_selector
+final class PlatformSelector {
+  /// A selector that declares that a test can be run on all platforms.
+  static const all = PlatformSelector._(BooleanSelector.all);
+
+  /// The boolean selector used to implement this selector.
+  final BooleanSelector _inner;
+
+  /// The source span from which this selector was parsed.
+  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])
+      : _inner =
+            _wrapFormatException(() => BooleanSelector.parse(selector), span),
+        _span = span;
+
+  const PlatformSelector._(this._inner) : _span = null;
+
+  /// Runs [body] and wraps any [FormatException] it throws in a
+  /// [SourceSpanFormatException] using [span].
+  ///
+  /// If [span] is `null`, runs [body] as-is.
+  static T _wrapFormatException<T>(T Function() body, [SourceSpan? span]) {
+    if (span == null) return body();
+
+    try {
+      return body();
+    } on FormatException catch (error) {
+      throw SourceSpanFormatException(error.message, span);
+    }
+  }
+
+  /// Throws a [FormatException] if this selector uses any variables that don't
+  /// appear either in [validVariables] or in the set of variables that are
+  /// known to be valid for all selectors.
+  void validate(Set<String> validVariables) {
+    if (identical(this, all)) return;
+
+    _wrapFormatException(
+        () => _inner.validate((name) =>
+            _universalValidVariables.contains(name) ||
+            validVariables.contains(name)),
+        _span);
+  }
+
+  /// Returns whether the selector matches the given [platform].
+  bool evaluate(SuitePlatform platform) =>
+      _inner.evaluate((String variable) => switch (variable) {
+            _
+                when variable == platform.runtime.identifier ||
+                    variable == platform.runtime.parent?.identifier ||
+                    variable == platform.os.identifier ||
+                    variable == platform.compiler.identifier =>
+              true,
+            'dart-vm' => platform.runtime.isDartVM,
+            'browser' => platform.runtime.isBrowser,
+            'js' => platform.compiler.isJS,
+            'blink' => platform.runtime.isBlink,
+            'posix' => platform.os.isPosix,
+            'google' => platform.inGoogle,
+            'wasm' => platform.compiler.isWasm,
+            _ => false,
+          });
+
+  /// Returns a new [PlatformSelector] that matches only platforms matched by
+  /// both [this] and [other].
+  PlatformSelector intersection(PlatformSelector other) {
+    if (other == PlatformSelector.all) return this;
+    return PlatformSelector._(_inner.intersection(other._inner));
+  }
+
+  @override
+  String toString() => _inner.toString();
+
+  @override
+  bool operator ==(Object other) =>
+      other is PlatformSelector && _inner == other._inner;
+
+  @override
+  int get hashCode => _inner.hashCode;
+}
diff --git a/pkgs/test_api/lib/src/backend/remote_exception.dart b/pkgs/test_api/lib/src/backend/remote_exception.dart
new file mode 100644
index 0000000..0d70561
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/remote_exception.dart
@@ -0,0 +1,88 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+
+import 'test_failure.dart';
+
+/// An exception that was thrown remotely.
+///
+/// This could be an exception thrown in a different isolate, a different
+/// process, or on an entirely different computer.
+final class RemoteException implements Exception {
+  /// The original exception's message, if it had one.
+  ///
+  /// If the original exception was a plain string, this will contain that
+  /// string.
+  final String? message;
+
+  /// The value of the original exception's `runtimeType.toString()`.
+  final String type;
+
+  /// The value of the original exception's `toString()`.
+  final String _toString;
+
+  /// Serializes [error] and [stackTrace] into a JSON-safe object.
+  ///
+  /// Other than JSON- and isolate-safety, no guarantees are made about the
+  /// serialized format.
+  static Map<String, dynamic> serialize(dynamic error, StackTrace stackTrace) {
+    String? message;
+    if (error is String) {
+      message = error;
+    } else {
+      try {
+        message = error.message.toString();
+      } on NoSuchMethodError catch (_) {
+        // Do nothing.
+      }
+    }
+
+    final supertype = (error is TestFailure) ? 'TestFailure' : null;
+
+    return {
+      'message': message,
+      'type': error.runtimeType.toString(),
+      'supertype': supertype,
+      'toString': error.toString(),
+      'stackChain': Chain.forTrace(stackTrace).toString()
+    };
+  }
+
+  /// Deserializes an exception serialized with [RemoteException.serialize].
+  ///
+  /// The returned [AsyncError] is guaranteed to have a [RemoteException] as its
+  /// error and a [Chain] as its stack trace.
+  static AsyncError deserialize(Map serialized) {
+    return AsyncError(_deserializeException(serialized),
+        Chain.parse(serialized['stackChain'] as String));
+  }
+
+  /// Deserializes the exception portion of [serialized].
+  static RemoteException _deserializeException(Map serialized) {
+    final message = serialized['message'] as String?;
+    final type = serialized['type'] as String;
+    final toString = serialized['toString'] as String;
+
+    return switch (serialized['supertype'] as String?) {
+      'TestFailure' => _RemoteTestFailure(message, type, toString),
+      _ => RemoteException._(message, type, toString),
+    };
+  }
+
+  RemoteException._(this.message, this.type, this._toString);
+
+  @override
+  String toString() => _toString;
+}
+
+/// A subclass of [RemoteException] that implements [TestFailure].
+///
+/// It's important to preserve [TestFailure]-ness, because tests have different
+/// results depending on whether an exception was a failure or an error.
+final class _RemoteTestFailure extends RemoteException implements TestFailure {
+  _RemoteTestFailure(super.message, super.type, super.toString) : super._();
+}
diff --git a/pkgs/test_api/lib/src/backend/remote_listener.dart b/pkgs/test_api/lib/src/backend/remote_listener.dart
new file mode 100644
index 0000000..66a0814
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/remote_listener.dart
@@ -0,0 +1,276 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:term_glyph/term_glyph.dart' as glyph;
+
+import 'declarer.dart';
+import 'group.dart';
+import 'invoker.dart';
+import 'live_test.dart';
+import 'metadata.dart';
+import 'remote_exception.dart';
+import 'stack_trace_formatter.dart';
+import 'suite.dart';
+import 'suite_channel_manager.dart';
+import 'suite_platform.dart';
+import 'test.dart';
+
+final class RemoteListener {
+  /// The test suite to run.
+  final Suite _suite;
+
+  /// The zone to forward prints to, or `null` if prints shouldn't be forwarded.
+  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.
+  ///
+  /// The main function is wrapped in a closure so that we can handle it being
+  /// undefined here rather than in the generated code.
+  ///
+  /// Once that's done, this starts listening for commands about which tests to
+  /// run.
+  ///
+  /// If [hidePrints] is `true` (the default), calls to `print()` within this
+  /// suite will not be forwarded to the parent zone's print handler. However,
+  /// the caller may want them to be forwarded in (for example) a browser
+  /// context where they'll be visible in the development console.
+  ///
+  /// If [beforeLoad] is passed, it's called before the tests have been declared
+  /// for this worker.
+  static StreamChannel<Object?> start(Function Function() getMain,
+      {bool hidePrints = true,
+      Future Function(
+              StreamChannel<Object?> Function(String name) suiteChannel)?
+          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<Object?>(controller.local);
+
+    var verboseChain = true;
+
+    var printZone = hidePrints ? null : Zone.current;
+    var spec = ZoneSpecification(print: (_, __, ___, line) {
+      if (printZone != null) printZone.print(line);
+      channel.sink.add({'type': 'print', 'line': line});
+    });
+
+    final suiteChannelManager = SuiteChannelManager();
+    StackTraceFormatter().asCurrent(() {
+      runZonedGuarded(() async {
+        Function? main;
+        try {
+          main = getMain();
+        } on NoSuchMethodError catch (_) {
+          _sendLoadException(channel, 'No top-level main() function defined.');
+          return;
+        } catch (error, stackTrace) {
+          _sendError(channel, error, stackTrace, verboseChain);
+          return;
+        }
+
+        if (main is! FutureOr<void> Function()) {
+          _sendLoadException(
+              channel, 'Top-level main() function takes arguments.');
+          return;
+        }
+
+        var queue = StreamQueue(channel.stream);
+        var message = await queue.next as Map;
+        assert(message['type'] == 'initial');
+
+        queue.rest.cast<Map>().listen((message) {
+          if (message['type'] == 'close') {
+            controller.local.sink.close();
+            return;
+          }
+
+          assert(message['type'] == 'suiteChannel');
+          suiteChannelManager.connectIn(message['name'] as String,
+              channel.virtualChannel((message['id'] as num).toInt()));
+        });
+
+        if ((message['asciiGlyphs'] as bool?) ?? false) glyph.ascii = true;
+        var metadata = Metadata.deserialize(message['metadata'] as Map);
+        verboseChain = metadata.verboseTrace;
+        var declarer = Declarer(
+          metadata: metadata,
+          platformVariables: Set.from(message['platformVariables'] as Iterable),
+          collectTraces: message['collectTraces'] as bool,
+          noRetry: message['noRetry'] as bool,
+          // TODO: Change to non-nullable https://github.com/dart-lang/test/issues/1591
+          allowDuplicateTestNames:
+              message['allowDuplicateTestNames'] as bool? ?? true,
+        );
+        StackTraceFormatter.current!.configure(
+            except: _deserializeSet(message['foldTraceExcept'] as List),
+            only: _deserializeSet(message['foldTraceOnly'] as List));
+
+        if (beforeLoad != null) {
+          await beforeLoad(suiteChannelManager.connectOut);
+        }
+
+        await declarer.declare(main);
+
+        var suite = Suite(
+          declarer.build(),
+          SuitePlatform.deserialize(message['platform'] as Object),
+          path: message['path'] as String,
+          ignoreTimeouts: message['ignoreTimeouts'] as bool? ?? false,
+        );
+
+        runZoned(() {
+          Invoker.guard(
+              () => RemoteListener._(suite, printZone)._listen(channel));
+        },
+            // Make the declarer visible to running tests so that they'll throw
+            // 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});
+      }, (error, stackTrace) {
+        _sendError(channel, error, stackTrace, verboseChain);
+      }, zoneSpecification: spec);
+    });
+
+    return controller.foreign;
+  }
+
+  /// 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);
+  }
+
+  /// Sends a message over [channel] indicating that the tests failed to load.
+  ///
+  /// [message] should describe the failure.
+  static void _sendLoadException(StreamChannel channel, String message) {
+    channel.sink.add({'type': 'loadException', 'message': message});
+  }
+
+  /// Sends a message over [channel] indicating an error from user code.
+  static void _sendError(StreamChannel channel, Object error,
+      StackTrace stackTrace, bool verboseChain) {
+    channel.sink.add({
+      'type': 'error',
+      'error': RemoteException.serialize(
+          error,
+          StackTraceFormatter.current!
+              .formatStackTrace(stackTrace, verbose: verboseChain))
+    });
+  }
+
+  RemoteListener._(this._suite, this._printZone);
+
+  /// Send information about [_suite] across [channel] and start listening for
+  /// commands to run the tests.
+  void _listen(MultiChannel channel) {
+    channel.sink.add({
+      'type': 'success',
+      'root': _serializeGroup(channel, _suite.group, [])
+    });
+  }
+
+  /// Serializes [group] into a JSON-safe map.
+  ///
+  /// [parents] lists the groups that contain [group].
+  Map _serializeGroup(
+      MultiChannel channel, Group group, Iterable<Group> parents) {
+    parents = parents.toList()..add(group);
+    return {
+      'type': 'group',
+      'name': group.name,
+      'metadata': group.metadata.serialize(),
+      'trace': group.trace == null
+          ? null
+          : StackTraceFormatter.current
+                  ?.formatStackTrace(group.trace!)
+                  .toString() ??
+              group.trace?.toString(),
+      'setUpAll': _serializeTest(channel, group.setUpAll, parents),
+      'tearDownAll': _serializeTest(channel, group.tearDownAll, parents),
+      'entries': group.entries.map((entry) {
+        return entry is Group
+            ? _serializeGroup(channel, entry, parents)
+            : _serializeTest(channel, entry as Test, parents);
+      }).toList()
+    };
+  }
+
+  /// Serializes [test] into a JSON-safe map.
+  ///
+  /// [groups] lists the groups that contain [test]. Returns `null` if [test]
+  /// is `null`.
+  Map? _serializeTest(
+      MultiChannel channel, Test? test, Iterable<Group>? groups) {
+    if (test == null) return null;
+
+    var testChannel = channel.virtualChannel();
+    testChannel.stream.listen((message) {
+      assert(message['command'] == 'run');
+      _runLiveTest(test.load(_suite, groups: groups),
+          channel.virtualChannel((message['channel'] as num).toInt()));
+    });
+
+    return {
+      'type': 'test',
+      'name': test.name,
+      'metadata': test.metadata.serialize(),
+      'trace': test.trace == null
+          ? null
+          : StackTraceFormatter.current
+                  ?.formatStackTrace(test.trace!)
+                  .toString() ??
+              test.trace?.toString(),
+      'channel': testChannel.id
+    };
+  }
+
+  /// Runs [liveTest] and sends the results across [channel].
+  void _runLiveTest(LiveTest liveTest, MultiChannel channel) {
+    channel.stream.listen((message) {
+      assert(message['command'] == 'close');
+      liveTest.close();
+    });
+
+    liveTest.onStateChange.listen((state) {
+      channel.sink.add({
+        'type': 'state-change',
+        'status': state.status.name,
+        'result': state.result.name
+      });
+    });
+
+    liveTest.onError.listen((asyncError) {
+      channel.sink.add({
+        'type': 'error',
+        'error': RemoteException.serialize(
+            asyncError.error,
+            StackTraceFormatter.current!.formatStackTrace(asyncError.stackTrace,
+                verbose: liveTest.test.metadata.verboseTrace))
+      });
+    });
+
+    liveTest.onMessage.listen((message) {
+      if (_printZone != null) _printZone.print(message.text);
+      channel.sink.add({
+        'type': 'message',
+        'message-type': message.type.name,
+        'text': message.text
+      });
+    });
+
+    runZoned(() {
+      liveTest.run().then((_) => channel.sink.add({'type': 'complete'}));
+    }, zoneValues: {#test.runner.test_channel: channel});
+  }
+}
diff --git a/pkgs/test_api/lib/src/backend/runtime.dart b/pkgs/test_api/lib/src/backend/runtime.dart
new file mode 100644
index 0000000..ceef227
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/runtime.dart
@@ -0,0 +1,193 @@
+// 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.
+
+import 'compiler.dart';
+
+/// An enum of all Dart runtimes supported by the test runner.
+final class Runtime {
+  // When adding new runtimes, be sure to update the baseline and derived
+  // variable tests in test/backend/platform_selector/evaluate_test.
+
+  /// The command-line Dart VM.
+  static const Runtime vm = Runtime('VM', 'vm', Compiler.kernel,
+      [Compiler.kernel, Compiler.source, Compiler.exe],
+      isDartVM: true);
+
+  /// Google Chrome.
+  static const Runtime chrome = Runtime('Chrome', 'chrome', Compiler.dart2js,
+      [Compiler.dart2js, Compiler.dart2wasm],
+      isBrowser: true, isBlink: true);
+
+  /// Mozilla Firefox.
+  static const Runtime firefox = Runtime('Firefox', 'firefox', Compiler.dart2js,
+      [Compiler.dart2js, Compiler.dart2wasm],
+      isBrowser: true);
+
+  /// Apple Safari.
+  static const Runtime safari = Runtime(
+      'Safari', 'safari', Compiler.dart2js, [Compiler.dart2js],
+      isBrowser: true);
+
+  /// Microsoft Internet Explorer.
+  @Deprecated('Internet Explorer is no longer supported')
+  static const Runtime internetExplorer = Runtime(
+      'Internet Explorer', 'ie', Compiler.dart2js, [Compiler.dart2js],
+      isBrowser: true);
+
+  /// Microsoft Edge (based on Chromium).
+  static const Runtime edge = Runtime(
+      'Microsoft Edge', 'edge', Compiler.dart2js, [Compiler.dart2js],
+      isBrowser: true, isBlink: true);
+
+  /// The command-line Node.js VM.
+  static const Runtime nodeJS = Runtime('Node.js', 'node', Compiler.dart2js,
+      [Compiler.dart2js, Compiler.dart2wasm]);
+
+  /// The platforms that are supported by the test runner by default.
+  static const List<Runtime> builtIn = [
+    Runtime.vm,
+    Runtime.chrome,
+    Runtime.firefox,
+    Runtime.safari,
+    Runtime.edge,
+    Runtime.nodeJS,
+  ];
+
+  /// The human-friendly name of the platform.
+  final String name;
+
+  /// The identifier used to look up the platform.
+  final String identifier;
+
+  /// The parent platform that this is based on, or `null` if there is no
+  /// parent.
+  final Runtime? parent;
+
+  /// Returns whether this is a child of another platform.
+  bool get isChild => parent != null;
+
+  /// Whether this platform runs the Dart VM in any capacity.
+  final bool isDartVM;
+
+  /// Whether this platform is a browser.
+  final bool isBrowser;
+
+  /// Whether this platform uses the Blink rendering engine.
+  final bool isBlink;
+
+  /// Whether this platform has no visible window.
+  final bool isHeadless;
+
+  /// Returns the platform this is based on, or [this] if it's not based on
+  /// anything.
+  ///
+  /// That is, returns [parent] if it's non-`null` or [this] if it's `null`.
+  Runtime get root => parent ?? this;
+
+  /// The default compiler to use with this runtime.
+  final Compiler defaultCompiler;
+
+  /// All the supported compilers for this runtime.
+  final List<Compiler> supportedCompilers;
+
+  const Runtime(
+      this.name, this.identifier, this.defaultCompiler, this.supportedCompilers,
+      {this.isDartVM = false,
+      this.isBrowser = false,
+      this.isBlink = false,
+      this.isHeadless = false})
+      : parent = null;
+
+  Runtime._child(this.name, this.identifier, this.defaultCompiler,
+      this.supportedCompilers, Runtime this.parent)
+      : isDartVM = parent.isDartVM,
+        isBrowser = parent.isBrowser,
+        isBlink = parent.isBlink,
+        isHeadless = parent.isHeadless;
+
+  /// Converts a JSON-safe representation generated by [serialize] back into a
+  /// [Runtime].
+  factory Runtime.deserialize(Object serialized) {
+    if (serialized is String) {
+      return builtIn
+          .firstWhere((platform) => platform.identifier == serialized);
+    }
+
+    var map = serialized as Map;
+    var name = map['name'] as String;
+    var identifier = map['identifier'] as String;
+    var defaultCompiler =
+        Compiler.deserialize(map['defaultCompiler'] as Object);
+    var supportedCompilers = [
+      for (var compiler in map['supportedCompilers'] as List)
+        Compiler.deserialize(compiler as Object),
+    ];
+
+    var parent = map['parent'];
+    if (parent != null) {
+      // Note that the returned platform's [parent] won't necessarily be `==` to
+      // a separately-deserialized parent platform. This should be fine, though,
+      // since we only deserialize platforms in the remote execution context
+      // where they're only used to evaluate platform selectors.
+      return Runtime._child(name, identifier, defaultCompiler,
+          supportedCompilers, Runtime.deserialize(parent as Object));
+    }
+
+    return Runtime(name, identifier, defaultCompiler, supportedCompilers,
+        isDartVM: map['isDartVM'] as bool,
+        isBrowser: map['isBrowser'] as bool,
+        isBlink: map['isBlink'] as bool,
+        isHeadless: map['isHeadless'] as bool);
+  }
+
+  /// Converts [this] into a JSON-safe object that can be converted back to a
+  /// [Runtime] using [Runtime.deserialize].
+  Object serialize() {
+    if (builtIn.contains(this)) return identifier;
+
+    if (parent != null) {
+      return {
+        'name': name,
+        'defaultCompiler': defaultCompiler.serialize(),
+        'supportedCompilers': [
+          for (var compiler in supportedCompilers) compiler.serialize(),
+        ],
+        'identifier': identifier,
+        'parent': parent!.serialize()
+      };
+    }
+
+    return {
+      'name': name,
+      'defaultCompiler': defaultCompiler.serialize(),
+      'supportedCompilers': [
+        for (var compiler in supportedCompilers) compiler.serialize(),
+      ],
+      'identifier': identifier,
+      'isDartVM': isDartVM,
+      'isBrowser': isBrowser,
+      'isBlink': isBlink,
+      'isHeadless': isHeadless,
+      // TODO(https://github.com/dart-lang/test/issues/2146): Remove this.
+      'isJS': isBrowser || this == Runtime.nodeJS,
+      // TODO(https://github.com/dart-lang/test/issues/2146): Remove this.
+      'isWasm': false,
+    };
+  }
+
+  /// Returns a child of [this] that counts as both this platform's identifier
+  /// and the new [identifier].
+  ///
+  /// This may not be called on a platform that's already a child.
+  Runtime extend(String name, String identifier) {
+    if (parent == null) {
+      return Runtime._child(
+          name, identifier, defaultCompiler, supportedCompilers, this);
+    }
+    throw StateError('A child platform may not be extended.');
+  }
+
+  @override
+  String toString() => name;
+}
diff --git a/pkgs/test_api/lib/src/backend/stack_trace_formatter.dart b/pkgs/test_api/lib/src/backend/stack_trace_formatter.dart
new file mode 100644
index 0000000..a495e89
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/stack_trace_formatter.dart
@@ -0,0 +1,75 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+
+import 'invoker.dart';
+import 'stack_trace_mapper.dart';
+
+/// The key used to look up [StackTraceFormatter.current] in a zone.
+final _currentKey = Object();
+
+/// A class that tracks how to format a stack trace according to the user's
+/// configuration.
+///
+/// This can convert JavaScript stack traces to Dart using source maps, and fold
+/// irrelevant frames out of the stack trace.
+final class StackTraceFormatter {
+  /// A class that converts [trace] into a Dart stack trace, or `null` to use it
+  /// as-is.
+  StackTraceMapper? _mapper;
+
+  /// The set of packages to fold when producing terse [Chain]s.
+  var _except = {'matcher', 'stream_channel', 'test', 'test_api'};
+
+  /// If non-empty, all packages not in this list will be folded when producing
+  /// terse [Chain]s.
+  var _only = <String>{};
+
+  /// 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?;
+
+  /// Runs [body] with this as [StackTraceFormatter.current].
+  ///
+  /// This is zone-scoped, so this will be the current configuration in any
+  /// asynchronous callbacks transitively created by [body].
+  T asCurrent<T>(T Function() body) =>
+      runZoned(body, zoneValues: {_currentKey: this});
+
+  /// Configure how stack traces are formatted.
+  ///
+  /// The [mapper] is used to convert JavaScript traces into Dart traces. The
+  /// [except] set indicates packages whose frames should be folded away. If
+  /// [only] is non-empty, it indicates packages whose frames should *not* be
+  /// folded away.
+  void configure(
+      {StackTraceMapper? mapper, Set<String>? except, Set<String>? only}) {
+    if (mapper != null) _mapper = mapper;
+    if (except != null) _except = except;
+    if (only != null) _only = only;
+  }
+
+  /// Converts [stackTrace] to a [Chain] and formats it according to the user's
+  /// preferences.
+  ///
+  /// 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;
+
+    var chain =
+        Chain.forTrace(_mapper?.mapStackTrace(stackTrace) ?? stackTrace);
+    if (verbose) return chain;
+
+    return chain.foldFrames((frame) {
+      if (_only.isNotEmpty) return !_only.contains(frame.package);
+      return _except.contains(frame.package);
+    }, terse: true);
+  }
+}
diff --git a/pkgs/test_api/lib/src/backend/stack_trace_mapper.dart b/pkgs/test_api/lib/src/backend/stack_trace_mapper.dart
new file mode 100644
index 0000000..b091bbe
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/stack_trace_mapper.dart
@@ -0,0 +1,12 @@
+// 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.
+
+/// A class for mapping JS stack traces to Dart stack traces using source maps.
+abstract interface class StackTraceMapper {
+  /// Converts [trace] into a Dart stack trace.
+  StackTrace mapStackTrace(StackTrace trace);
+
+  /// Returns a Map representation which is suitable for JSON serialization.
+  Map<String, dynamic> serialize();
+}
diff --git a/pkgs/test_api/lib/src/backend/state.dart b/pkgs/test_api/lib/src/backend/state.dart
new file mode 100644
index 0000000..5a37654
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/state.dart
@@ -0,0 +1,109 @@
+// 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.
+
+/// The state of a [LiveTest].
+///
+/// A test's state is made up of two components, its [status] and its [result].
+/// The [status] represents where the test is in its process of running; the
+/// [result] represents the outcome as far as its known.
+class State {
+  /// Where the test is in its process of running.
+  final Status status;
+
+  /// The outcome of the test, as far as it's known.
+  ///
+  /// Note that if [status] is [Status.pending], [result] will always be
+  /// [Result.success] since the test hasn't yet had a chance to fail.
+  final Result result;
+
+  /// Whether a test in this state is expected to be done running code.
+  ///
+  /// If [status] is [Status.complete] and [result] doesn't indicate an error, a
+  /// properly-written test case should not be running any more code. However,
+  /// it may have started asynchronous processes without notifying the test
+  /// runner.
+  bool get shouldBeDone => status == Status.complete && result.isPassing;
+
+  const State(this.status, this.result);
+
+  @override
+  bool operator ==(Object other) =>
+      other is State && status == other.status && result == other.result;
+
+  @override
+  int get hashCode => status.hashCode ^ (7 * result.hashCode);
+
+  @override
+  String toString() {
+    if (status == Status.pending) return 'pending';
+    if (status == Status.complete) return result.toString();
+    if (result == Result.success) return 'running';
+    return 'running with $result';
+  }
+}
+
+/// Where the test is in its process of running.
+enum Status {
+  /// The test has not yet begun running.
+  pending,
+
+  /// The test is currently running.
+  running,
+
+  /// The test has finished running.
+  ///
+  /// Note that even if the test is marked [complete], it may still be running
+  /// code asynchronously. A test is considered complete either once it hits its
+  /// first error or when all [expectAsync] callbacks have been called and any
+  /// returned [Future] has completed, but it's possible for further processing
+  /// to happen, which may cause further errors.
+  complete;
+
+  factory Status.parse(String name) => Status.values.byName(name);
+
+  @override
+  String toString() => name;
+}
+
+/// The outcome of the test, as far as it's known.
+enum Result {
+  /// The test has not yet failed in any way.
+  ///
+  /// Note that this doesn't mean that the test won't fail in the future.
+  success,
+
+  /// The test, or some part of it, has been skipped.
+  ///
+  /// This implies that the test hasn't failed *yet*. However, it this doesn't
+  /// mean that the test won't fail in the future.
+  skipped,
+
+  /// The test has failed.
+  ///
+  /// A failure is specifically caused by a [TestFailure] being thrown; any
+  /// other exception causes an error.
+  failure,
+
+  /// The test has crashed.
+  ///
+  /// Any exception other than a [TestFailure] is considered to be an error.
+  error;
+
+  /// Whether this is a passing result.
+  ///
+  /// A test is considered to have passed if it's a success or if it was
+  /// skipped.
+  bool get isPassing => this == success || this == skipped;
+
+  /// Whether this is a failing result.
+  ///
+  /// A test is considered to have failed if it experiences a failure or an
+  /// error.
+  bool get isFailing => !isPassing;
+
+  factory Result.parse(String name) => Result.values.byName(name);
+
+  @override
+  String toString() => name;
+}
diff --git a/pkgs/test_api/lib/src/backend/suite.dart b/pkgs/test_api/lib/src/backend/suite.dart
new file mode 100644
index 0000000..910f184
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/suite.dart
@@ -0,0 +1,62 @@
+// 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.
+
+import 'group.dart';
+import 'metadata.dart';
+import 'suite_platform.dart';
+import 'test.dart';
+
+/// A test suite.
+///
+/// A test suite is a set of tests that are intended to be run together and that
+/// share default configuration.
+class Suite {
+  /// The platform on which the suite is running.
+  final SuitePlatform platform;
+
+  /// The path to the Dart test suite, or `null` if that path is unknown.
+  final String? path;
+
+  /// The metadata associated with this test suite.
+  ///
+  /// This is a shortcut for [group.metadata].
+  Metadata get metadata => group.metadata;
+
+  /// The top-level group for this test suite.
+  final Group group;
+
+  /// Whether or not to ignore test timeouts.
+  final bool ignoreTimeouts;
+
+  /// Creates a new suite containing [group].
+  ///
+  /// If [platform] and/or [os] are passed, [group] is filtered to match that
+  /// platform information.
+  ///
+  /// If [os] is passed without [platform], throws an [ArgumentError].
+  Suite(Group group, this.platform, {this.ignoreTimeouts = false, this.path})
+      : group = _filterGroup(group, platform);
+
+  /// Returns [entries] filtered according to [platform] and [os].
+  ///
+  /// Gracefully handles [platform] being null.
+  static Group _filterGroup(Group group, SuitePlatform platform) {
+    var filtered = group.forPlatform(platform);
+    if (filtered != null) return filtered;
+    return Group.root([], metadata: group.metadata);
+  }
+
+  /// Returns a new suite with all tests matching [test] removed.
+  ///
+  /// Unlike [GroupEntry.filter], this never returns `null`. If all entries are
+  /// filtered out, it returns an empty suite.
+  Suite filter(bool Function(Test) callback) {
+    var filtered = group.filter(callback);
+    filtered ??= Group.root([], metadata: metadata);
+    return Suite(filtered, platform,
+        ignoreTimeouts: ignoreTimeouts, path: path);
+  }
+
+  bool get isLoadSuite => false;
+}
diff --git a/pkgs/test_api/lib/src/backend/suite_channel_manager.dart b/pkgs/test_api/lib/src/backend/suite_channel_manager.dart
new file mode 100644
index 0000000..26a21c9
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/suite_channel_manager.dart
@@ -0,0 +1,44 @@
+// 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.
+
+import 'package:stream_channel/stream_channel.dart';
+
+/// A class that connects incoming and outgoing channels with the same names.
+class SuiteChannelManager {
+  /// Connections from the test runner that have yet to connect to corresponding
+  /// calls to [connectOut].
+  final _incomingConnections = <String, StreamChannel<Object?>>{};
+
+  /// Connections from calls to [connectOut] that have yet to connect to
+  /// corresponding connections from the test runner.
+  final _outgoingConnections = <String, StreamChannelCompleter<Object?>>{};
+
+  /// The channel names that have already been used.
+  final _names = <String>{};
+
+  /// Creates a connection to the test runnner's channel with the given [name].
+  StreamChannel<Object?> connectOut(String name) {
+    if (_incomingConnections.containsKey(name)) {
+      return _incomingConnections[name]!;
+    } else if (_names.contains(name)) {
+      throw StateError('Duplicate suiteChannel() connection "$name".');
+    } else {
+      _names.add(name);
+      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<Object?> channel) {
+    if (_outgoingConnections.containsKey(name)) {
+      _outgoingConnections.remove(name)!.setChannel(channel);
+    } else if (_incomingConnections.containsKey(name)) {
+      throw StateError('Duplicate RunnerSuite.channel() connection "$name".');
+    } else {
+      _incomingConnections[name] = channel;
+    }
+  }
+}
diff --git a/pkgs/test_api/lib/src/backend/suite_platform.dart b/pkgs/test_api/lib/src/backend/suite_platform.dart
new file mode 100644
index 0000000..baab6ce
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/suite_platform.dart
@@ -0,0 +1,70 @@
+// 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.
+
+import 'compiler.dart';
+import 'operating_system.dart';
+import 'runtime.dart';
+
+/// The platform on which a test suite is loaded.
+final class SuitePlatform {
+  /// The runtime that hosts the suite.
+  final Runtime runtime;
+
+  /// The operating system on which the suite is running.
+  ///
+  /// This will always be [OperatingSystem.none] if `runtime.isBrowser` is
+  /// true.
+  final OperatingSystem os;
+
+  /// Whether we're running on Google-internal infrastructure.
+  final bool inGoogle;
+
+  /// The compiler that should used for this platform.
+  final Compiler compiler;
+
+  /// Creates a new platform with the given [runtime] and [os], which defaults
+  /// to [OperatingSystem.none].
+  ///
+  /// Throws an [ArgumentError] if [runtime] is a browser and [os] is not
+  /// `null` or [OperatingSystem.none].
+  ///
+  /// If [compiler] is `null`, then the default compiler for [runtime] will be
+  /// used.
+  SuitePlatform(this.runtime,
+      {
+      // TODO(https://github.com/dart-lang/test/issues/1935): make required
+      Compiler? compiler,
+      this.os = OperatingSystem.none,
+      this.inGoogle = false})
+      : compiler = compiler ?? runtime.defaultCompiler {
+    if (runtime.isBrowser && os != OperatingSystem.none) {
+      throw ArgumentError('No OS should be passed for runtime "$runtime".');
+    }
+    if (!runtime.supportedCompilers.contains(this.compiler)) {
+      throw ArgumentError(
+          'The platform $runtime does not support the compiler ${this.compiler}');
+    }
+  }
+
+  /// Converts a JSON-safe representation generated by [serialize] back into a
+  /// [SuitePlatform].
+  factory SuitePlatform.deserialize(Object serialized) {
+    var map = serialized as Map;
+    return SuitePlatform(Runtime.deserialize(map['runtime'] as Object),
+        compiler: map.containsKey('compiler')
+            ? Compiler.deserialize(map['compiler'] as Object)
+            : null,
+        os: OperatingSystem.find(map['os'] as String),
+        inGoogle: map['inGoogle'] as bool);
+  }
+
+  /// Converts [this] into a JSON-safe object that can be converted back to a
+  /// [SuitePlatform] using [SuitePlatform.deserialize].
+  Object serialize() => {
+        'runtime': runtime.serialize(),
+        'compiler': compiler.serialize(),
+        'os': os.identifier,
+        'inGoogle': inGoogle
+      };
+}
diff --git a/pkgs/test_api/lib/src/backend/test.dart b/pkgs/test_api/lib/src/backend/test.dart
new file mode 100644
index 0000000..5b5e73a
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/test.dart
@@ -0,0 +1,42 @@
+// 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.
+
+import 'package:stack_trace/stack_trace.dart';
+
+import 'group.dart';
+import 'group_entry.dart';
+import 'live_test.dart';
+import 'metadata.dart';
+import 'suite.dart';
+import 'suite_platform.dart';
+
+/// A single test.
+///
+/// A test is immutable and stateless, which means that it can't be run
+/// directly. To run one, load a live version using [Test.load] and run it using
+/// [LiveTest.run].
+abstract class Test implements GroupEntry {
+  @override
+  String get name;
+
+  @override
+  Metadata get metadata;
+
+  @override
+  Trace? get trace;
+
+  /// Loads a live version of this test, which can be used to run it a single
+  /// time.
+  ///
+  /// [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});
+
+  @override
+  Test? forPlatform(SuitePlatform platform);
+
+  @override
+  Test? filter(bool Function(Test) callback) => callback(this) ? this : null;
+}
diff --git a/pkgs/test_api/lib/src/backend/test_failure.dart b/pkgs/test_api/lib/src/backend/test_failure.dart
new file mode 100644
index 0000000..4d9fc79
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/test_failure.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// An exception thrown when a test assertion fails.
+class TestFailure implements Exception {
+  final String? message;
+
+  TestFailure(this.message);
+
+  @override
+  String toString() => message.toString();
+}
diff --git a/pkgs/test_api/lib/src/backend/util/identifier_regex.dart b/pkgs/test_api/lib/src/backend/util/identifier_regex.dart
new file mode 100644
index 0000000..6426641
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/util/identifier_regex.dart
@@ -0,0 +1,9 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// A regular expression matching a full string as a hyphenated identifier.
+///
+/// This is like a standard Dart identifier, except that it can also contain
+/// hyphens.
+final anchoredHyphenatedIdentifier = RegExp(r'^[a-zA-Z_-][a-zA-Z0-9_-]*$');
diff --git a/pkgs/test_api/lib/src/backend/util/pretty_print.dart b/pkgs/test_api/lib/src/backend/util/pretty_print.dart
new file mode 100644
index 0000000..5c011c5
--- /dev/null
+++ b/pkgs/test_api/lib/src/backend/util/pretty_print.dart
@@ -0,0 +1,45 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// Returns [name] if [number] is 1, or the plural of [name] otherwise.
+///
+/// By default, this just adds "s" to the end of [name] to get the plural. If
+/// [plural] is passed, that's used instead.
+String pluralize(String name, int number, {String? plural}) {
+  if (number == 1) return name;
+  if (plural != null) return plural;
+  return '${name}s';
+}
+
+/// Returns a sentence fragment listing the elements of [iter].
+///
+/// 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 = '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 ${iter.last}';
+}
+
+/// Returns a human-friendly representation of [duration].
+String niceDuration(Duration duration) {
+  var minutes = duration.inMinutes;
+  var seconds = duration.inSeconds % 60;
+  var decaseconds = (duration.inMilliseconds % 1000) ~/ 100;
+
+  var buffer = StringBuffer();
+  if (minutes != 0) buffer.write('$minutes minutes');
+
+  if (minutes == 0 || seconds != 0) {
+    if (minutes != 0) buffer.write(', ');
+    buffer.write(seconds);
+    if (decaseconds != 0) buffer.write('.$decaseconds');
+    buffer.write(' seconds');
+  }
+
+  return buffer.toString();
+}
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..e3e0ea6
--- /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 exercised 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 mixin class Fake {
+  @override
+  dynamic noSuchMethod(Invocation invocation) {
+    throw UnimplementedError(invocation.memberName.toString().split('"')[1]);
+  }
+}
diff --git a/pkgs/test_api/lib/src/remote_listener.dart b/pkgs/test_api/lib/src/remote_listener.dart
new file mode 100644
index 0000000..f2df134
--- /dev/null
+++ b/pkgs/test_api/lib/src/remote_listener.dart
@@ -0,0 +1,5 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+export 'backend/remote_listener.dart' show RemoteListener;
diff --git a/pkgs/test_api/lib/src/scaffolding/spawn_hybrid.dart b/pkgs/test_api/lib/src/scaffolding/spawn_hybrid.dart
new file mode 100644
index 0000000..7222d52
--- /dev/null
+++ b/pkgs/test_api/lib/src/scaffolding/spawn_hybrid.dart
@@ -0,0 +1,179 @@
+// 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.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:async/async.dart';
+import 'package:stream_channel/stream_channel.dart';
+
+import '../backend/remote_exception.dart';
+import '../utils.dart';
+import 'test_structure.dart' show addTearDown;
+
+/// A transformer that handles messages from the spawned isolate and ensures
+/// that messages sent to it are JSON-encodable.
+///
+/// The spawned isolate sends three kinds of messages. Data messages are emitted
+/// as data events, error messages are emitted as error events, and print
+/// messages are printed using `print()`.
+// package:test will only send a `Map` across this channel, but users of
+// `hybridMain` can send any json encodeable type.
+final _transformer = StreamChannelTransformer<dynamic, dynamic>(
+    StreamTransformer.fromHandlers(handleData: (message, sink) {
+  switch (message['type'] as String) {
+    case 'data':
+      sink.add(message['data']);
+      break;
+
+    case 'print':
+      print(message['line']);
+      break;
+
+    case 'error':
+      var error = RemoteException.deserialize(message['error'] as Map);
+      sink.addError(error.error, error.stackTrace);
+      break;
+  }
+}), StreamSinkTransformer.fromHandlers(handleData: (message, sink) {
+  // This is called synchronously from the user's `Sink.add()` call, so if
+  // [ensureJsonEncodable] throws here they'll get a helpful stack trace.
+  ensureJsonEncodable(message);
+  sink.add(message);
+}));
+
+/// Spawns a VM isolate for the given [uri], which may be a [Uri] or a [String].
+///
+/// This allows browser tests to spawn servers with which they can communicate
+/// to test client/server interactions. It can also be used by VM tests to
+/// easily spawn an isolate.
+///
+/// The Dart file at [uri] must define a top-level `hybridMain()` function that
+/// takes a `StreamChannel` argument and, optionally, an `Object` argument to
+/// which [message] will be passed. Note that [message] must be JSON-encodable.
+/// For example:
+///
+/// ```dart
+/// import "package:stream_channel/stream_channel.dart";
+///
+/// hybridMain(StreamChannel channel, Object message) {
+///   // ...
+/// }
+/// ```
+///
+/// If [uri] is relative, it will be interpreted relative to the `file:` URL for
+/// the test suite being executed. If it's root-relative (that is, if it begins
+/// with `/`) it will be interpreted relative to the root of the package (the
+/// directory that contains `pubspec.yaml`, *not* the `test/` directory). If
+/// it's a `package:` URL, it will be resolved using the current package's
+/// dependency constellation.
+///
+/// Returns a [StreamChannel] that's connected to the channel passed to
+/// `hybridMain()`. Only JSON-encodable objects may be sent through this
+/// channel. If the channel is closed, the hybrid isolate is killed. If the
+/// isolate is killed, the channel's stream will emit a "done" event.
+///
+/// Any unhandled errors loading or running the hybrid isolate will be emitted
+/// as errors over the channel's stream. Any calls to `print()` in the hybrid
+/// isolate will be printed as though they came from the test that created the
+/// isolate.
+///
+/// Code in the hybrid isolate is not considered to be running in a test
+/// context, so it can't access test functions like `expect()` and
+/// `expectAsync()`.
+///
+/// By default, the hybrid isolate is automatically killed when the test
+/// finishes running. If [stayAlive] is `true`, it won't be killed until the
+/// entire test suite finishes running.
+///
+/// **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(
+  Object uri, {
+  Object? message,
+  bool stayAlive = false,
+}) {
+  if (uri is String) {
+    // Ensure that it can be parsed as a uri.
+    Uri.parse(uri);
+  } else if (uri is! Uri) {
+    throw ArgumentError.value(uri, 'uri', 'must be a Uri or a String.');
+  }
+  return _spawn(uri.toString(), message, stayAlive: stayAlive);
+}
+
+/// Spawns a VM isolate that runs the given [dartCode], which is loaded as the
+/// contents of a Dart library.
+///
+/// This allows browser tests to spawn servers with which they can communicate
+/// to test client/server interactions. It can also be used by VM tests to
+/// easily spawn an isolate.
+///
+/// The [dartCode] must define a top-level `hybridMain()` function that takes a
+/// `StreamChannel` argument and, optionally, an `Object` argument to which
+/// [message] will be passed. Note that [message] must be JSON-encodable. For
+/// example:
+///
+/// ```dart
+/// import "package:stream_channel/stream_channel.dart";
+///
+/// hybridMain(StreamChannel channel, Object message) {
+///   // ...
+/// }
+/// ```
+///
+/// Returns a [StreamChannel] that's connected to the channel passed to
+/// `hybridMain()`. Only JSON-encodable objects may be sent through this
+/// channel. If the channel is closed, the hybrid isolate is killed. If the
+/// isolate is killed, the channel's stream will emit a "done" event.
+///
+/// Any unhandled errors loading or running the hybrid isolate will be emitted
+/// as errors over the channel's stream. Any calls to `print()` in the hybrid
+/// isolate will be printed as though they came from the test that created the
+/// isolate.
+///
+/// Code in the hybrid isolate is not considered to be running in a test
+/// context, so it can't access test functions like `expect()` and
+/// `expectAsync()`.
+///
+/// By default, the hybrid isolate is automatically killed when the test
+/// finishes running. If [stayAlive] is `true`, it won't be killed until the
+/// entire test suite finishes running.
+///
+/// **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}) {
+  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.
+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 "dart test".');
+  }
+
+  ensureJsonEncodable(message);
+
+  var virtualChannel = channel.virtualChannel();
+  StreamChannel isolateChannel = virtualChannel;
+  channel.sink.add({
+    'type': 'spawn-hybrid-uri',
+    'url': uri,
+    'message': message,
+    'channel': virtualChannel.id
+  });
+
+  if (!stayAlive) {
+    var disconnector = Disconnector<void>();
+    addTearDown(() => disconnector.disconnect());
+    isolateChannel = isolateChannel.transform(disconnector);
+  }
+
+  return isolateChannel.transform(_transformer);
+}
diff --git a/pkgs/test_api/lib/src/scaffolding/test_structure.dart b/pkgs/test_api/lib/src/scaffolding/test_structure.dart
new file mode 100644
index 0000000..642e013
--- /dev/null
+++ b/pkgs/test_api/lib/src/scaffolding/test_structure.dart
@@ -0,0 +1,251 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:meta/meta.dart';
+
+import '../backend/configuration/timeout.dart';
+import '../backend/declarer.dart';
+import '../backend/invoker.dart';
+
+// test_core does not support running tests directly, so the Declarer should
+// always be on the Zone.
+Declarer get _declarer => Zone.current[#test.declarer] as Declarer;
+
+// TODO(nweiz): This and other top-level functions should throw exceptions if
+// they're called after the declarer has finished declaring.
+/// Creates a new test case with the given description (converted to a string)
+/// and body.
+///
+/// The description will be added to the descriptions of any surrounding
+/// [group]s. If [testOn] is passed, it's parsed as a [platform selector][]; the
+/// test will only be run on matching platforms.
+///
+/// [platform selector]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors
+///
+/// If [timeout] is passed, it's used to modify or replace the default timeout
+/// of 30 seconds. Timeout modifications take precedence in suite-group-test
+/// order, so [timeout] will also modify any timeouts set on the group or suite.
+///
+/// If [skip] is a String or `true`, the test is skipped. If it's a String, it
+/// should explain why the test is skipped; this reason will be printed instead
+/// of running the test.
+///
+/// If [tags] is passed, it declares user-defined tags that are applied to the
+/// test. These tags can be used to select or skip the test on the command line,
+/// or to do bulk test configuration. All tags should be declared in the
+/// [package configuration file][configuring tags]. The parameter can be an
+/// [Iterable] of tag names, or a [String] representing a single tag.
+///
+/// If [retry] is passed, the test will be retried the provided number of times
+/// before being marked as a failure.
+///
+/// [configuring tags]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#configuring-tags
+///
+/// [onPlatform] allows tests to be configured on a platform-by-platform
+/// basis. It's a map from strings that are parsed as [PlatformSelector]s to
+/// annotation classes: [Timeout], [Skip], or lists of those. These
+/// annotations apply only on the given platforms. For example:
+///
+///     test('potentially slow test', () {
+///       // ...
+///     }, onPlatform: {
+///       // This test is especially slow on Windows.
+///       'windows': Timeout.factor(2),
+///       'browser': [
+///         Skip('TODO: add browser support'),
+///         // This will be slow on browsers once it works on them.
+///         Timeout.factor(2)
+///       ]
+///     });
+///
+/// If multiple platforms match, the annotations apply in order as through
+/// they were in nested groups.
+///
+/// If the `solo` flag is `true`, only tests and groups marked as
+/// "solo" will be be run. This only restricts tests *within this test
+/// suite*—tests in other suites will run as normal. We recommend that users
+/// avoid this flag if possible and instead use the test runner flag `-n` to
+/// filter tests by name.
+@isTest
+void test(Object? description, dynamic Function() body,
+    {String? testOn,
+    Timeout? timeout,
+    Object? skip,
+    Object? tags,
+    Map<String, dynamic>? onPlatform,
+    int? retry,
+    // TODO(https://github.com/dart-lang/test/issues/2205): Remove deprecated.
+    @Deprecated('Debug only') @doNotSubmit bool solo = false}) {
+  _declarer.test(description.toString(), body,
+      testOn: testOn,
+      timeout: timeout,
+      skip: skip,
+      onPlatform: onPlatform,
+      tags: tags,
+      retry: retry,
+      solo: solo);
+
+  // Force dart2js not to inline this function. We need it to be separate from
+  // `main()` in JS stack traces in order to properly determine the line and
+  // column where the test was defined. See sdk#26705.
+  return;
+  return; // ignore: dead_code
+}
+
+/// Creates a group of tests.
+///
+/// A group's description (converted to a string) is included in the descriptions
+/// of any tests or sub-groups it contains. [setUp] and [tearDown] are also scoped
+/// to the containing group.
+///
+/// If [testOn] is passed, it's parsed as a [platform selector][]; the test will
+/// only be run on matching platforms.
+///
+/// [platform selector]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors
+///
+/// If [timeout] is passed, it's used to modify or replace the default timeout
+/// of 30 seconds. Timeout modifications take precedence in suite-group-test
+/// order, so [timeout] will also modify any timeouts set on the suite, and will
+/// be modified by any timeouts set on individual tests.
+///
+/// If [skip] is a String or `true`, the group is skipped. If it's a String, it
+/// should explain why the group is skipped; this reason will be printed instead
+/// of running the group's tests.
+///
+/// If [tags] is passed, it declares user-defined tags that are applied to the
+/// test. These tags can be used to select or skip the test on the command line,
+/// or to do bulk test configuration. All tags should be declared in the
+/// [package configuration file][configuring tags]. The parameter can be an
+/// [Iterable] of tag names, or a [String] representing a single tag.
+///
+/// [configuring tags]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#configuring-tags
+///
+/// [onPlatform] allows groups to be configured on a platform-by-platform
+/// basis. It's a map from strings that are parsed as [PlatformSelector]s to
+/// annotation classes: [Timeout], [Skip], or lists of those. These
+/// annotations apply only on the given platforms. For example:
+///
+///     group('potentially slow tests', () {
+///       // ...
+///     }, onPlatform: {
+///       // These tests are especially slow on Windows.
+///       'windows': Timeout.factor(2),
+///       'browser': [
+///         Skip('TODO: add browser support'),
+///         // They'll be slow on browsers once it works on them.
+///         Timeout.factor(2)
+///       ]
+///     });
+///
+/// If multiple platforms match, the annotations apply in order as through
+/// they were in nested groups.
+///
+/// If the `solo` flag is `true`, only tests and groups marked as
+/// "solo" will be be run. This only restricts tests *within this test
+/// suite*—tests in other suites will run as normal. We recommend that users
+/// avoid this flag if possible, and instead use the test runner flag `-n` to
+/// filter tests by name.
+@isTestGroup
+void group(Object? description, dynamic Function() body,
+    {String? testOn,
+    Timeout? timeout,
+    Object? skip,
+    Object? tags,
+    Map<String, dynamic>? onPlatform,
+    int? retry,
+    // TODO(https://github.com/dart-lang/test/issues/2205): Remove deprecated.
+    @Deprecated('Debug only') @doNotSubmit bool solo = false}) {
+  _declarer.group(description.toString(), body,
+      testOn: testOn,
+      timeout: timeout,
+      skip: skip,
+      tags: tags,
+      onPlatform: onPlatform,
+      retry: retry,
+      solo: solo);
+
+  // Force dart2js not to inline this function. We need it to be separate from
+  // `main()` in JS stack traces in order to properly determine the line and
+  // column where the test was defined. See sdk#26705.
+  return;
+  return; // ignore: dead_code
+}
+
+/// Registers a function to be run before tests.
+///
+/// This function will be called before each test is run. [callback] may be
+/// asynchronous; if so, it must return a [Future].
+///
+/// If this is called within a test group, it applies only to tests in that
+/// group. [callback] will be run after any set-up callbacks in parent groups or
+/// at the top level.
+///
+/// Each callback at the top level or in a given group will be run in the order
+/// they were declared.
+void setUp(dynamic Function() callback) => _declarer.setUp(callback);
+
+/// Registers a function to be run after tests.
+///
+/// This function will be called after each test is run. [callback] may be
+/// asynchronous; if so, it must return a [Future].
+///
+/// If this is called within a test group, it applies only to tests in that
+/// group. [callback] will be run before any tear-down callbacks in parent
+/// groups or at the top level.
+///
+/// Each callback at the top level or in a given group will be run in the
+/// reverse of the order they were declared.
+///
+/// See also [addTearDown], which adds tear-downs to a running test.
+void tearDown(dynamic Function() callback) => _declarer.tearDown(callback);
+
+/// Registers a function to be run after the current test.
+///
+/// This is called within a running test, and adds a tear-down only for the
+/// current test. It allows testing libraries to add cleanup logic as soon as
+/// there's something to clean up.
+///
+/// The [callback] is run before any callbacks registered with [tearDown]. Like
+/// [tearDown], the most recently registered callback is run first.
+///
+/// If this is called from within a [setUpAll] or [tearDownAll] callback, it
+/// instead runs the function after *all* tests in the current test suite.
+void addTearDown(dynamic Function() callback) {
+  if (Invoker.current == null) {
+    throw StateError('addTearDown() may only be called within a test.');
+  }
+
+  Invoker.current!.addTearDown(callback);
+}
+
+/// Registers a function to be run once before all tests.
+///
+/// [callback] may be asynchronous; if so, it must return a [Future].
+///
+/// If this is called within a test group, [callback] will run before all tests
+/// in that group. It will be run after any [setUpAll] callbacks in parent
+/// groups or at the top level. It won't be run if none of the tests in the
+/// group are run.
+///
+/// **Note**: This function makes it very easy to accidentally introduce hidden
+/// dependencies between tests that should be isolated. In general, you should
+/// prefer [setUp], and only use [setUpAll] if the callback is prohibitively
+/// slow.
+void setUpAll(dynamic Function() callback) => _declarer.setUpAll(callback);
+
+/// Registers a function to be run once after all tests.
+///
+/// If this is called within a test group, [callback] will run after all tests
+/// in that group. It will be run before any [tearDownAll] callbacks in parent
+/// groups or at the top level. It won't be run if none of the tests in the
+/// group are run.
+///
+/// **Note**: This function makes it very easy to accidentally introduce hidden
+/// dependencies between tests that should be isolated. In general, you should
+/// prefer [tearDown], and only use [tearDownAll] if the callback is
+/// prohibitively slow.
+void tearDownAll(dynamic Function() callback) =>
+    _declarer.tearDownAll(callback);
diff --git a/pkgs/test_api/lib/src/scaffolding/utils.dart b/pkgs/test_api/lib/src/scaffolding/utils.dart
new file mode 100644
index 0000000..134dc49
--- /dev/null
+++ b/pkgs/test_api/lib/src/scaffolding/utils.dart
@@ -0,0 +1,51 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import '../backend/invoker.dart';
+
+/// Returns a [Future] that completes after the [event loop][] has run the given
+/// number of [times] (20 by default).
+///
+/// [event loop]: https://medium.com/dartlang/dart-asynchronous-programming-isolates-and-event-loops-bffc3e296a6a
+///
+/// Awaiting this approximates waiting until all asynchronous work (other than
+/// work that's waiting for external resources) completes.
+Future pumpEventQueue({int times = 20}) {
+  if (times == 0) return Future.value();
+  // Use the event loop to allow the microtask queue to finish.
+  return Future(() => pumpEventQueue(times: times - 1));
+}
+
+/// Registers an exception that was caught for the current test.
+void registerException(Object error,
+    [StackTrace stackTrace = StackTrace.empty]) {
+  // This will usually forward directly to [Invoker.current.handleError], but
+  // going through the zone API allows other zones to consistently see errors.
+  Zone.current.handleUncaughtError(error, stackTrace);
+}
+
+/// Prints [message] if and when the current test fails.
+///
+/// This is intended for test infrastructure to provide debugging information
+/// without cluttering the output for successful tests. Note that unlike
+/// [print], each individual message passed to [printOnFailure] will be
+/// separated by a blank line.
+void printOnFailure(String message) {
+  _currentInvoker.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) => _currentInvoker..skip(message);
+
+Invoker get _currentInvoker =>
+    Invoker.current ??
+    (throw StateError(
+        'There is no current invoker. Please make sure that you are making the '
+        'call inside a test zone.'));
diff --git a/pkgs/test_api/lib/src/utils.dart b/pkgs/test_api/lib/src/utils.dart
new file mode 100644
index 0000000..3bf7518
--- /dev/null
+++ b/pkgs/test_api/lib/src/utils.dart
@@ -0,0 +1,27 @@
+// Copyright (c) 2013, 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.
+
+/// Throws an [ArgumentError] if [message] isn't recursively JSON-safe.
+void ensureJsonEncodable(Object? message) {
+  if (message == null ||
+      message is String ||
+      message is num ||
+      message is bool) {
+    // JSON-encodable, hooray!
+  } else if (message is List) {
+    for (var element in message) {
+      ensureJsonEncodable(element);
+    }
+  } else if (message is Map) {
+    message.forEach((key, value) {
+      if (key is! String) {
+        throw ArgumentError("$message can't be JSON-encoded.");
+      }
+
+      ensureJsonEncodable(value);
+    });
+  } else {
+    throw ArgumentError.value("$message can't be JSON-encoded.");
+  }
+}
diff --git a/pkgs/test_api/lib/test_api.dart b/pkgs/test_api/lib/test_api.dart
new file mode 100644
index 0000000..75c1540
--- /dev/null
+++ b/pkgs/test_api/lib/test_api.dart
@@ -0,0 +1,10 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@Deprecated('package:test_api is not intended for general use. '
+    'Please use package:test.')
+library;
+
+export 'hooks.dart' show TestFailure;
+export 'scaffolding.dart';
diff --git a/pkgs/test_api/mono_pkg.yaml b/pkgs/test_api/mono_pkg.yaml
new file mode 100644
index 0000000..534d9c3
--- /dev/null
+++ b/pkgs/test_api/mono_pkg.yaml
@@ -0,0 +1,19 @@
+# See https://pub.dev/packages/mono_repo
+
+sdk:
+- dev
+- pubspec
+
+stages:
+- analyze_and_format:
+  - group:
+    - format
+    - analyze: --fatal-infos
+    sdk:
+    - dev
+- unit_test:
+  - group:
+    - command: dart test --preset travis -x browser
+      os:
+      - linux
+      - windows
diff --git a/pkgs/test_api/pubspec.yaml b/pkgs/test_api/pubspec.yaml
new file mode 100644
index 0000000..af5cb93
--- /dev/null
+++ b/pkgs/test_api/pubspec.yaml
@@ -0,0 +1,29 @@
+name: test_api
+version: 0.7.4
+description: >-
+  The user facing API for structuring Dart tests and checking expectations.
+repository: https://github.com/dart-lang/test/tree/master/pkgs/test_api
+resolution: workspace
+
+environment:
+  sdk: ^3.5.0
+
+dependencies:
+  async: ^2.5.0
+  boolean_selector: ^2.1.0
+  collection: ^1.15.0
+  meta: ^1.14.0
+  source_span: ^1.8.0
+  stack_trace: ^1.10.0
+  stream_channel: ^2.1.0
+  string_scanner: ^1.1.0
+  term_glyph: ^1.2.0
+
+dev_dependencies:
+  analyzer: '>=6.0.0 <8.0.0'
+  fake_async: ^1.2.0
+  glob: ^2.0.0
+  graphs: ^2.0.0
+  path: ^1.8.0
+  test: any
+  test_core: any
diff --git a/pkgs/test_api/test/backend/declarer_test.dart b/pkgs/test_api/test/backend/declarer_test.dart
new file mode 100644
index 0000000..f095ad0
--- /dev/null
+++ b/pkgs/test_api/test/backend/declarer_test.dart
@@ -0,0 +1,703 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:test_api/src/backend/declarer.dart';
+import 'package:test_api/src/backend/group.dart';
+import 'package:test_api/src/backend/invoker.dart';
+import 'package:test_api/src/backend/suite.dart';
+import 'package:test_api/src/backend/test.dart';
+
+import '../utils.dart';
+
+late Suite _suite;
+
+void main() {
+  setUp(() {
+    _suite = Suite(Group.root([]), suitePlatform, ignoreTimeouts: false);
+  });
+
+  group('.test()', () {
+    test('declares a test with a description and body', () async {
+      var bodyRun = false;
+      var tests = declare(() {
+        test('description', () {
+          bodyRun = true;
+        });
+      });
+
+      expect(tests, hasLength(1));
+      expect(tests.single.name, equals('description'));
+
+      await _runTest(tests[0] as Test);
+      expect(bodyRun, isTrue);
+    });
+
+    test('declares a test with an object as the description', () async {
+      var tests = declare(() {
+        test(Object, () {});
+      });
+
+      expect(tests.single.name, equals('Object'));
+    });
+
+    test('declares multiple tests', () {
+      var tests = declare(() {
+        test('description 1', () {});
+        test('description 2', () {});
+        test('description 3', () {});
+      });
+
+      expect(tests, hasLength(3));
+      expect(tests[0].name, equals('description 1'));
+      expect(tests[1].name, equals('description 2'));
+      expect(tests[2].name, equals('description 3'));
+    });
+  });
+
+  group('.setUp()', () {
+    test('is run before all tests', () async {
+      var setUpRun = false;
+      var tests = declare(() {
+        setUp(() => setUpRun = true);
+
+        test(
+            'description 1',
+            expectAsync0(() {
+              expect(setUpRun, isTrue);
+              setUpRun = false;
+            }, max: 1));
+
+        test(
+            'description 2',
+            expectAsync0(() {
+              expect(setUpRun, isTrue);
+              setUpRun = false;
+            }, max: 1));
+      });
+
+      await _runTest(tests[0] as Test);
+      await _runTest(tests[1] as Test);
+    });
+
+    test('can return a Future', () {
+      var setUpRun = false;
+      var tests = declare(() {
+        setUp(() {
+          return Future(() => setUpRun = true);
+        });
+
+        test(
+            'description',
+            expectAsync0(() {
+              expect(setUpRun, isTrue);
+            }, max: 1));
+      });
+
+      return _runTest(tests.single as Test);
+    });
+
+    test('runs in call order within a group', () async {
+      var firstSetUpRun = false;
+      var secondSetUpRun = false;
+      var thirdSetUpRun = false;
+      var tests = declare(() {
+        setUp(expectAsync0(() async {
+          expect(secondSetUpRun, isFalse);
+          expect(thirdSetUpRun, isFalse);
+          firstSetUpRun = true;
+        }));
+
+        setUp(expectAsync0(() async {
+          expect(firstSetUpRun, isTrue);
+          expect(thirdSetUpRun, isFalse);
+          secondSetUpRun = true;
+        }));
+
+        setUp(expectAsync0(() async {
+          expect(firstSetUpRun, isTrue);
+          expect(secondSetUpRun, isTrue);
+          thirdSetUpRun = true;
+        }));
+
+        test('description', expectAsync0(() {
+          expect(firstSetUpRun, isTrue);
+          expect(secondSetUpRun, isTrue);
+          expect(thirdSetUpRun, isTrue);
+        }));
+      });
+
+      await _runTest(tests.single as Test);
+    });
+  });
+
+  group('.tearDown()', () {
+    test('is run after all tests', () async {
+      late bool tearDownRun;
+      var tests = declare(() {
+        setUp(() => tearDownRun = false);
+        tearDown(() => tearDownRun = true);
+
+        test(
+            'description 1',
+            expectAsync0(() {
+              expect(tearDownRun, isFalse);
+            }, max: 1));
+
+        test(
+            'description 2',
+            expectAsync0(() {
+              expect(tearDownRun, isFalse);
+            }, max: 1));
+      });
+
+      await _runTest(tests[0] as Test);
+      expect(tearDownRun, isTrue);
+      await _runTest(tests[1] as Test);
+      expect(tearDownRun, isTrue);
+    });
+
+    test('is run after an out-of-band failure', () async {
+      late bool tearDownRun;
+      var tests = declare(() {
+        setUp(() => tearDownRun = false);
+        tearDown(() => tearDownRun = true);
+
+        test(
+            'description 1',
+            expectAsync0(() {
+              Invoker.current!.addOutstandingCallback();
+              Future(() => throw TestFailure('oh no'))
+                  .whenComplete(Invoker.current!.removeOutstandingCallback);
+            }, max: 1));
+      });
+
+      await _runTest(tests.single as Test, shouldFail: true);
+      expect(tearDownRun, isTrue);
+    });
+
+    test('can return a Future', () async {
+      var tearDownRun = false;
+      var tests = declare(() {
+        tearDown(() {
+          return Future(() => tearDownRun = true);
+        });
+
+        test(
+            'description',
+            expectAsync0(() {
+              expect(tearDownRun, isFalse);
+            }, max: 1));
+      });
+
+      await _runTest(tests.single as Test);
+      expect(tearDownRun, isTrue);
+    });
+
+    test("isn't run until there are no outstanding callbacks", () async {
+      var outstandingCallbackRemoved = false;
+      var outstandingCallbackRemovedBeforeTeardown = false;
+      var tests = declare(() {
+        tearDown(() {
+          outstandingCallbackRemovedBeforeTeardown = outstandingCallbackRemoved;
+        });
+
+        test('description', () {
+          Invoker.current!.addOutstandingCallback();
+          pumpEventQueue().then((_) {
+            outstandingCallbackRemoved = true;
+            Invoker.current!.removeOutstandingCallback();
+          });
+        });
+      });
+
+      await _runTest(tests.single as Test);
+      expect(outstandingCallbackRemovedBeforeTeardown, isTrue);
+    });
+
+    test("isn't run until test body completes after out-of-band error",
+        () async {
+      var hasTestFinished = false;
+      var hasTestFinishedBeforeTeardown = false;
+      var tests = declare(() {
+        tearDown(() {
+          hasTestFinishedBeforeTeardown = hasTestFinished;
+        });
+
+        test('description', () {
+          Future<Never>.error('oh no');
+          return pumpEventQueue().then((_) {
+            hasTestFinished = true;
+          });
+        });
+      });
+
+      await _runTest(tests.single as Test, shouldFail: true);
+      expect(hasTestFinishedBeforeTeardown, isTrue);
+    });
+
+    test("doesn't complete until there are no outstanding callbacks", () async {
+      var outstandingCallbackRemoved = false;
+      var tests = declare(() {
+        tearDown(() {
+          Invoker.current!.addOutstandingCallback();
+          pumpEventQueue().then((_) {
+            outstandingCallbackRemoved = true;
+            Invoker.current!.removeOutstandingCallback();
+          });
+        });
+
+        test('description', () {});
+      });
+
+      await _runTest(tests.single as Test);
+      expect(outstandingCallbackRemoved, isTrue);
+    });
+
+    test('runs in reverse call order within a group', () async {
+      var firstTearDownRun = false;
+      var secondTearDownRun = false;
+      var thirdTearDownRun = false;
+      var tests = declare(() {
+        tearDown(expectAsync0(() async {
+          expect(secondTearDownRun, isTrue);
+          expect(thirdTearDownRun, isTrue);
+          firstTearDownRun = true;
+        }));
+
+        tearDown(expectAsync0(() async {
+          expect(firstTearDownRun, isFalse);
+          expect(thirdTearDownRun, isTrue);
+          secondTearDownRun = true;
+        }));
+
+        tearDown(expectAsync0(() async {
+          expect(firstTearDownRun, isFalse);
+          expect(secondTearDownRun, isFalse);
+          thirdTearDownRun = true;
+        }));
+
+        test(
+            'description',
+            expectAsync0(() {
+              expect(firstTearDownRun, isFalse);
+              expect(secondTearDownRun, isFalse);
+              expect(thirdTearDownRun, isFalse);
+            }, max: 1));
+      });
+
+      await _runTest(tests.single as Test);
+    });
+
+    test('runs further tearDowns in a group even if one fails', () async {
+      var tests = declare(() {
+        tearDown(expectAsync0(() {}));
+
+        tearDown(() async {
+          throw 'error';
+        });
+
+        test('description', expectAsync0(() {}));
+      });
+
+      await _runTest(tests.single as Test, shouldFail: true);
+    });
+
+    test('runs in the same error zone as the test', () {
+      return expectTestsPass(() {
+        late Zone testBodyZone;
+
+        tearDown(() {
+          final tearDownZone = Zone.current;
+          expect(tearDownZone.inSameErrorZone(testBodyZone), isTrue,
+              reason: 'The tear down callback is in a different error zone '
+                  'than the test body.');
+        });
+
+        test('test', () {
+          testBodyZone = Zone.current;
+        });
+      });
+    });
+  });
+
+  group('in a group,', () {
+    test("tests inherit the group's description", () {
+      var entries = declare(() {
+        group('group', () {
+          test('description', () {});
+        });
+      });
+
+      expect(entries, hasLength(1));
+      var testGroup = entries.single as Group;
+      expect(testGroup.name, equals('group'));
+      expect(testGroup.entries, hasLength(1));
+      expect(testGroup.entries.single, const TypeMatcher<Test>());
+      expect(testGroup.entries.single.name, 'group description');
+    });
+
+    test("tests inherit the group's description when it's not a string", () {
+      var entries = declare(() {
+        group(Object, () {
+          test('description', () {});
+        });
+      });
+
+      expect(entries, hasLength(1));
+      var testGroup = entries.single as Group;
+      expect(testGroup.name, equals('Object'));
+      expect(testGroup.entries, hasLength(1));
+      expect(testGroup.entries.single, const TypeMatcher<Test>());
+      expect(testGroup.entries.single.name, 'Object description');
+    });
+
+    test("a test's timeout factor is applied to the group's", () {
+      var entries = declare(() {
+        group('group', () {
+          test('test', () {}, timeout: const Timeout.factor(3));
+        }, timeout: const Timeout.factor(2));
+      });
+
+      expect(entries, hasLength(1));
+      var testGroup = entries.single as Group;
+      expect(testGroup.metadata.timeout.scaleFactor, equals(2));
+      expect(testGroup.entries, hasLength(1));
+      expect(testGroup.entries.single, const TypeMatcher<Test>());
+      expect(testGroup.entries.single.metadata.timeout.scaleFactor, equals(6));
+    });
+
+    test("a test's timeout factor is applied to the group's duration", () {
+      var entries = declare(() {
+        group('group', () {
+          test('test', () {}, timeout: const Timeout.factor(2));
+        }, timeout: const Timeout(Duration(seconds: 10)));
+      });
+
+      expect(entries, hasLength(1));
+      var testGroup = entries.single as Group;
+      expect(testGroup.metadata.timeout.duration,
+          equals(const Duration(seconds: 10)));
+      expect(testGroup.entries, hasLength(1));
+      expect(testGroup.entries.single, const TypeMatcher<Test>());
+      expect(testGroup.entries.single.metadata.timeout.duration,
+          equals(const Duration(seconds: 20)));
+    });
+
+    test("a test's timeout duration is applied over the group's", () {
+      var entries = declare(() {
+        group('group', () {
+          test('test', () {}, timeout: const Timeout(Duration(seconds: 15)));
+        }, timeout: const Timeout(Duration(seconds: 10)));
+      });
+
+      expect(entries, hasLength(1));
+      var testGroup = entries.single as Group;
+      expect(testGroup.metadata.timeout.duration,
+          equals(const Duration(seconds: 10)));
+      expect(testGroup.entries, hasLength(1));
+      expect(testGroup.entries.single, const TypeMatcher<Test>());
+      expect(testGroup.entries.single.metadata.timeout.duration,
+          equals(const Duration(seconds: 15)));
+    });
+
+    test('disallows asynchronous groups', () async {
+      declare(() {
+        expect(() => group('group', () async {}), throwsArgumentError);
+      });
+    });
+
+    group('.setUp()', () {
+      test('is scoped to the group', () async {
+        var setUpRun = false;
+        var entries = declare(() {
+          group('group', () {
+            setUp(() => setUpRun = true);
+
+            test(
+                'description 1',
+                expectAsync0(() {
+                  expect(setUpRun, isTrue);
+                  setUpRun = false;
+                }, max: 1));
+          });
+
+          test(
+              'description 2',
+              expectAsync0(() {
+                expect(setUpRun, isFalse);
+                setUpRun = false;
+              }, max: 1));
+        });
+
+        await _runTest((entries[0] as Group).entries.single as Test);
+        await _runTest(entries[1] as Test);
+      });
+
+      test('runs from the outside in', () {
+        var outerSetUpRun = false;
+        var middleSetUpRun = false;
+        var innerSetUpRun = false;
+        var entries = declare(() {
+          setUp(expectAsync0(() {
+            expect(middleSetUpRun, isFalse);
+            expect(innerSetUpRun, isFalse);
+            outerSetUpRun = true;
+          }, max: 1));
+
+          group('middle', () {
+            setUp(expectAsync0(() {
+              expect(outerSetUpRun, isTrue);
+              expect(innerSetUpRun, isFalse);
+              middleSetUpRun = true;
+            }, max: 1));
+
+            group('inner', () {
+              setUp(expectAsync0(() {
+                expect(outerSetUpRun, isTrue);
+                expect(middleSetUpRun, isTrue);
+                innerSetUpRun = true;
+              }, max: 1));
+
+              test(
+                  'description',
+                  expectAsync0(() {
+                    expect(outerSetUpRun, isTrue);
+                    expect(middleSetUpRun, isTrue);
+                    expect(innerSetUpRun, isTrue);
+                  }, max: 1));
+            });
+          });
+        });
+
+        var middleGroup = entries.single as Group;
+        var innerGroup = middleGroup.entries.single as Group;
+        return _runTest(innerGroup.entries.single as Test);
+      });
+
+      test('handles Futures when chained', () {
+        var outerSetUpRun = false;
+        var innerSetUpRun = false;
+        var entries = declare(() {
+          setUp(expectAsync0(() {
+            expect(innerSetUpRun, isFalse);
+            return Future(() => outerSetUpRun = true);
+          }, max: 1));
+
+          group('inner', () {
+            setUp(expectAsync0(() {
+              expect(outerSetUpRun, isTrue);
+              return Future(() => innerSetUpRun = true);
+            }, max: 1));
+
+            test(
+                'description',
+                expectAsync0(() {
+                  expect(outerSetUpRun, isTrue);
+                  expect(innerSetUpRun, isTrue);
+                }, max: 1));
+          });
+        });
+
+        var innerGroup = entries.single as Group;
+        return _runTest(innerGroup.entries.single as Test);
+      });
+
+      test("inherits group's tags", () {
+        var tests = declare(() {
+          group('outer', () {
+            group('inner', () {
+              test('with tags', () {}, tags: 'd');
+            }, tags: ['b', 'c']);
+          }, tags: 'a');
+        });
+
+        var outerGroup = tests.single as Group;
+        var innerGroup = outerGroup.entries.single as Group;
+        var testWithTags = innerGroup.entries.single;
+        expect(outerGroup.metadata.tags, unorderedEquals(['a']));
+        expect(innerGroup.metadata.tags, unorderedEquals(['a', 'b', 'c']));
+        expect(
+            testWithTags.metadata.tags, unorderedEquals(['a', 'b', 'c', 'd']));
+      });
+
+      test('throws on invalid tags', () {
+        expect(() {
+          declare(() {
+            group('a', () {}, tags: 1);
+          });
+        }, throwsArgumentError);
+      });
+    });
+
+    group('.tearDown()', () {
+      test('is scoped to the group', () async {
+        late bool tearDownRun;
+        var entries = declare(() {
+          setUp(() => tearDownRun = false);
+
+          group('group', () {
+            tearDown(() => tearDownRun = true);
+
+            test(
+                'description 1',
+                expectAsync0(() {
+                  expect(tearDownRun, isFalse);
+                }, max: 1));
+          });
+
+          test(
+              'description 2',
+              expectAsync0(() {
+                expect(tearDownRun, isFalse);
+              }, max: 1));
+        });
+
+        var testGroup = entries[0] as Group;
+        await _runTest(testGroup.entries.single as Test);
+        expect(tearDownRun, isTrue);
+        await _runTest(entries[1] as Test);
+        expect(tearDownRun, isFalse);
+      });
+
+      test('runs from the inside out', () async {
+        var innerTearDownRun = false;
+        var middleTearDownRun = false;
+        var outerTearDownRun = false;
+        var entries = declare(() {
+          tearDown(expectAsync0(() {
+            expect(innerTearDownRun, isTrue);
+            expect(middleTearDownRun, isTrue);
+            outerTearDownRun = true;
+          }, max: 1));
+
+          group('middle', () {
+            tearDown(expectAsync0(() {
+              expect(innerTearDownRun, isTrue);
+              expect(outerTearDownRun, isFalse);
+              middleTearDownRun = true;
+            }, max: 1));
+
+            group('inner', () {
+              tearDown(expectAsync0(() {
+                expect(outerTearDownRun, isFalse);
+                expect(middleTearDownRun, isFalse);
+                innerTearDownRun = true;
+              }, max: 1));
+
+              test(
+                  'description',
+                  expectAsync0(() {
+                    expect(outerTearDownRun, isFalse);
+                    expect(middleTearDownRun, isFalse);
+                    expect(innerTearDownRun, isFalse);
+                  }, max: 1));
+            });
+          });
+        });
+
+        var middleGroup = entries.single as Group;
+        var innerGroup = middleGroup.entries.single as Group;
+        await _runTest(innerGroup.entries.single as Test);
+        expect(innerTearDownRun, isTrue);
+        expect(middleTearDownRun, isTrue);
+        expect(outerTearDownRun, isTrue);
+      });
+
+      test('handles Futures when chained', () async {
+        var outerTearDownRun = false;
+        var innerTearDownRun = false;
+        var entries = declare(() {
+          tearDown(expectAsync0(() {
+            expect(innerTearDownRun, isTrue);
+            return Future(() => outerTearDownRun = true);
+          }, max: 1));
+
+          group('inner', () {
+            tearDown(expectAsync0(() {
+              expect(outerTearDownRun, isFalse);
+              return Future(() => innerTearDownRun = true);
+            }, max: 1));
+
+            test(
+                'description',
+                expectAsync0(() {
+                  expect(outerTearDownRun, isFalse);
+                  expect(innerTearDownRun, isFalse);
+                }, max: 1));
+          });
+        });
+
+        var innerGroup = entries.single as Group;
+        await _runTest(innerGroup.entries.single as Test);
+        expect(innerTearDownRun, isTrue);
+        expect(outerTearDownRun, isTrue);
+      });
+
+      test('runs outer callbacks even when inner ones fail', () async {
+        var outerTearDownRun = false;
+        var entries = declare(() {
+          tearDown(() {
+            return Future(() => outerTearDownRun = true);
+          });
+
+          group('inner', () {
+            tearDown(() {
+              throw 'inner error';
+            });
+
+            test(
+                'description',
+                expectAsync0(() {
+                  expect(outerTearDownRun, isFalse);
+                }, max: 1));
+          });
+        });
+
+        var innerGroup = entries.single as Group;
+        await _runTest(innerGroup.entries.single as Test, shouldFail: true);
+        expect(outerTearDownRun, isTrue);
+      });
+    });
+  });
+
+  group('duplicate names', () {
+    test('can be enabled', () {
+      expect(
+          () => declare(() {
+                test('a', expectAsync0(() {}, count: 0));
+                test('a', expectAsync0(() {}, count: 0));
+              }, allowDuplicateTestNames: false),
+          throwsA(isA<DuplicateTestNameException>()
+              .having((e) => e.name, 'name', 'a')));
+    });
+
+    test('are allowed by default', () {
+      expect(
+          declare(() {
+            test('a', expectAsync0(() {}, count: 0));
+            test('a', expectAsync0(() {}, count: 0));
+          }).map((e) => e.name),
+          equals(['a', 'a']));
+    });
+  });
+}
+
+/// Runs [test].
+///
+/// This automatically sets up an `onError` listener to ensure that the test
+/// doesn't throw any invisible exceptions.
+Future _runTest(Test test, {bool shouldFail = false}) {
+  var liveTest = test.load(_suite);
+
+  if (shouldFail) {
+    liveTest.onError.listen(expectAsync1((_) {}));
+  } else {
+    liveTest.onError.listen((e) => registerException(e.error, e.stackTrace));
+  }
+
+  return liveTest.run();
+}
diff --git a/pkgs/test_api/test/backend/invoker_test.dart b/pkgs/test_api/test/backend/invoker_test.dart
new file mode 100644
index 0000000..ef08b38
--- /dev/null
+++ b/pkgs/test_api/test/backend/invoker_test.dart
@@ -0,0 +1,581 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:test_api/src/backend/group.dart';
+import 'package:test_api/src/backend/invoker.dart';
+import 'package:test_api/src/backend/live_test.dart';
+import 'package:test_api/src/backend/message.dart';
+import 'package:test_api/src/backend/metadata.dart';
+import 'package:test_api/src/backend/state.dart';
+import 'package:test_api/src/backend/suite.dart';
+
+import '../utils.dart';
+
+void main() {
+  late Suite suite;
+  setUp(() {
+    lastState = null;
+    suite = Suite(Group.root([]), suitePlatform, ignoreTimeouts: false);
+  });
+
+  group('Invoker.current', () {
+    var invoker = Invoker.current;
+    test('returns null outside of a test body', () {
+      expect(invoker, isNull);
+    });
+
+    test('returns the current invoker in a test body', () async {
+      late Invoker invoker;
+      var liveTest = _localTest(() {
+        invoker = Invoker.current!;
+      }).load(suite);
+      liveTest.onError.listen(expectAsync1((_) {}, count: 0));
+
+      await liveTest.run();
+      expect(invoker.liveTest, equals(liveTest));
+    });
+
+    test('returns the current invoker in a test body after the test completes',
+        () async {
+      Status? status;
+      var completer = Completer<Invoker>();
+      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;
+          completer.complete(Invoker.current);
+        });
+      }).load(suite);
+      liveTest.onError.listen(expectAsync1((_) {}, count: 0));
+
+      expect(liveTest.run(), completes);
+      var invoker = await completer.future;
+      expect(invoker.liveTest, equals(liveTest));
+      expect(status, equals(Status.complete));
+    });
+  });
+
+  group('in a successful test,', () {
+    test('the state changes from pending to running to complete', () async {
+      late State stateInTest;
+      late LiveTest liveTest;
+      liveTest = _localTest(() {
+        stateInTest = liveTest.state;
+      }).load(suite);
+      liveTest.onError.listen(expectAsync1((_) {}, count: 0));
+
+      expect(liveTest.state.status, equals(Status.pending));
+      expect(liveTest.state.result, equals(Result.success));
+
+      var future = liveTest.run();
+
+      expect(liveTest.state.status, equals(Status.running));
+      expect(liveTest.state.result, equals(Result.success));
+
+      await future;
+
+      expect(stateInTest.status, equals(Status.running));
+      expect(stateInTest.result, equals(Result.success));
+
+      expect(liveTest.state.status, equals(Status.complete));
+      expect(liveTest.state.result, equals(Result.success));
+    });
+
+    test('onStateChange fires for each state change', () {
+      var liveTest = _localTest(() {}).load(suite);
+      liveTest.onError.listen(expectAsync1((_) {}, count: 0));
+
+      var first = true;
+      liveTest.onStateChange.listen(expectAsync1((state) {
+        if (first) {
+          expect(state.status, equals(Status.running));
+          first = false;
+        } else {
+          expect(state.status, equals(Status.complete));
+        }
+        expect(state.result, equals(Result.success));
+      }, count: 2, max: 2));
+
+      return liveTest.run();
+    });
+
+    test('onComplete completes once the test body is done', () {
+      var testRun = false;
+      var liveTest = _localTest(() {
+        testRun = true;
+      }).load(suite);
+
+      expect(liveTest.onComplete.then((_) {
+        expect(testRun, isTrue);
+      }), completes);
+
+      return liveTest.run();
+    });
+  });
+
+  group('in a test with failures,', () {
+    test('a synchronous throw is reported and causes the test to fail', () {
+      var liveTest = _localTest(() {
+        throw TestFailure('oh no');
+      }).load(suite);
+
+      expectSingleFailure(liveTest);
+      return liveTest.run();
+    });
+
+    test('a synchronous reported failure causes the test to fail', () {
+      var liveTest = _localTest(() {
+        registerException(TestFailure('oh no'));
+      }).load(suite);
+
+      expectSingleFailure(liveTest);
+      return liveTest.run();
+    });
+
+    test('a failure reported asynchronously during the test causes it to fail',
+        () {
+      var liveTest = _localTest(() {
+        Invoker.current!.addOutstandingCallback();
+        Future(() => registerException(TestFailure('oh no')))
+            .whenComplete(Invoker.current!.removeOutstandingCallback);
+      }).load(suite);
+
+      expectSingleFailure(liveTest);
+      return liveTest.run();
+    });
+
+    test('a failure thrown asynchronously during the test causes it to fail',
+        () {
+      var liveTest = _localTest(() {
+        Invoker.current!.addOutstandingCallback();
+        Future(() => throw TestFailure('oh no'))
+            .whenComplete(Invoker.current!.removeOutstandingCallback);
+      }).load(suite);
+
+      expectSingleFailure(liveTest);
+      return liveTest.run();
+    });
+
+    test('a failure reported asynchronously after the test causes it to error',
+        () {
+      var liveTest = _localTest(() {
+        Future(() => registerException(TestFailure('oh no')));
+      }).load(suite);
+
+      expectStates(liveTest, [
+        const State(Status.running, Result.success),
+        const State(Status.complete, Result.success),
+        const State(Status.complete, Result.failure),
+        const State(Status.complete, Result.error)
+      ]);
+
+      expectErrors(liveTest, [
+        (error) {
+          expect(
+              lastState, equals(const State(Status.complete, Result.failure)));
+          expect(error, isTestFailure('oh no'));
+        },
+        (error) {
+          expect(lastState, equals(const State(Status.complete, Result.error)));
+          expect(
+              error,
+              equals('This test failed after it had already completed.\n'
+                  'Make sure to use a matching library which informs the '
+                  'test runner\nof pending async work.'));
+        }
+      ]);
+
+      return liveTest.run();
+    });
+
+    test('multiple asynchronous failures are reported', () {
+      var liveTest = _localTest(() {
+        Invoker.current!.addOutstandingCallback();
+        Future(() => throw TestFailure('one'));
+        Future(() => throw TestFailure('two'));
+        Future(() => throw TestFailure('three'));
+        Future(() => throw TestFailure('four'))
+            .whenComplete(Invoker.current!.removeOutstandingCallback);
+      }).load(suite);
+
+      expectStates(liveTest, [
+        const State(Status.running, Result.success),
+        const State(Status.complete, Result.failure)
+      ]);
+
+      expectErrors(liveTest, [
+        (error) {
+          expect(lastState?.status, equals(Status.complete));
+          expect(error, isTestFailure('one'));
+        },
+        (error) {
+          expect(error, isTestFailure('two'));
+        },
+        (error) {
+          expect(error, isTestFailure('three'));
+        },
+        (error) {
+          expect(error, isTestFailure('four'));
+        }
+      ]);
+
+      return liveTest.run();
+    });
+
+    test("a failure after an error doesn't change the state of the test", () {
+      var liveTest = _localTest(() {
+        Future(() => throw TestFailure('fail'));
+        throw 'error';
+      }).load(suite);
+
+      expectStates(liveTest, [
+        const State(Status.running, Result.success),
+        const State(Status.complete, Result.error)
+      ]);
+
+      expectErrors(liveTest, [
+        (error) {
+          expect(lastState, equals(const State(Status.complete, Result.error)));
+          expect(error, equals('error'));
+        },
+        (error) {
+          expect(error, isTestFailure('fail'));
+        }
+      ]);
+
+      return liveTest.run();
+    });
+  });
+
+  group('in a test with errors,', () {
+    test('a synchronous throw is reported and causes the test to error', () {
+      var liveTest = _localTest(() {
+        throw 'oh no';
+      }).load(suite);
+
+      expectSingleError(liveTest);
+      return liveTest.run();
+    });
+
+    test('a synchronous reported error causes the test to error', () {
+      var liveTest = _localTest(() {
+        registerException('oh no');
+      }).load(suite);
+
+      expectSingleError(liveTest);
+      return liveTest.run();
+    });
+
+    test('an error reported asynchronously during the test causes it to error',
+        () {
+      var liveTest = _localTest(() {
+        Invoker.current!.addOutstandingCallback();
+        Future(() => registerException('oh no'))
+            .whenComplete(Invoker.current!.removeOutstandingCallback);
+      }).load(suite);
+
+      expectSingleError(liveTest);
+      return liveTest.run();
+    });
+
+    test('an error thrown asynchronously during the test causes it to error',
+        () {
+      var liveTest = _localTest(() {
+        Invoker.current!.addOutstandingCallback();
+        Future(() => throw 'oh no')
+            .whenComplete(Invoker.current!.removeOutstandingCallback);
+      }).load(suite);
+
+      expectSingleError(liveTest);
+      return liveTest.run();
+    });
+
+    test('an error reported asynchronously after the test causes it to error',
+        () {
+      var liveTest = _localTest(() {
+        Future(() => registerException('oh no'));
+      }).load(suite);
+
+      expectStates(liveTest, [
+        const State(Status.running, Result.success),
+        const State(Status.complete, Result.success),
+        const State(Status.complete, Result.error)
+      ]);
+
+      expectErrors(liveTest, [
+        (error) {
+          expect(lastState, equals(const State(Status.complete, Result.error)));
+          expect(error, equals('oh no'));
+        },
+        (error) {
+          expect(
+              error,
+              equals('This test failed after it had already completed.\n'
+                  'Make sure to use a matching library which informs the '
+                  'test runner\nof pending async work.'));
+        }
+      ]);
+
+      return liveTest.run();
+    });
+
+    test('multiple asynchronous errors are reported', () {
+      var liveTest = _localTest(() {
+        Invoker.current!.addOutstandingCallback();
+        Future(() => throw 'one');
+        Future(() => throw 'two');
+        Future(() => throw 'three');
+        Future(() => throw 'four')
+            .whenComplete(Invoker.current!.removeOutstandingCallback);
+      }).load(suite);
+
+      expectStates(liveTest, [
+        const State(Status.running, Result.success),
+        const State(Status.complete, Result.error)
+      ]);
+
+      expectErrors(liveTest, [
+        (error) {
+          expect(lastState?.status, equals(Status.complete));
+          expect(error, equals('one'));
+        },
+        (error) {
+          expect(error, equals('two'));
+        },
+        (error) {
+          expect(error, equals('three'));
+        },
+        (error) {
+          expect(error, equals('four'));
+        }
+      ]);
+
+      return liveTest.run();
+    });
+
+    test('an error after a failure changes the state of the test', () {
+      var liveTest = _localTest(() {
+        Future(() => throw 'error');
+        throw TestFailure('fail');
+      }).load(suite);
+
+      expectStates(liveTest, [
+        const State(Status.running, Result.success),
+        const State(Status.complete, Result.failure),
+        const State(Status.complete, Result.error)
+      ]);
+
+      expectErrors(liveTest, [
+        (error) {
+          expect(
+              lastState, equals(const State(Status.complete, Result.failure)));
+          expect(error, isTestFailure('fail'));
+        },
+        (error) {
+          expect(lastState, equals(const State(Status.complete, Result.error)));
+          expect(error, equals('error'));
+        }
+      ]);
+
+      return liveTest.run();
+    });
+  });
+
+  test("a test doesn't complete until there are no outstanding callbacks",
+      () async {
+    var outstandingCallbackRemoved = false;
+    var liveTest = _localTest(() {
+      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();
+      });
+    }).load(suite);
+
+    liveTest.onError.listen(expectAsync1((_) {}, count: 0));
+
+    await liveTest.run();
+    expect(outstandingCallbackRemoved, isTrue);
+  });
+
+  test("a test's prints are captured and reported", () {
+    expect(() {
+      var liveTest = _localTest(() {
+        print('Hello,');
+        return Future(() => print('world!'));
+      }).load(suite);
+
+      expect(
+          liveTest.onMessage.take(2).toList().then((messages) {
+            expect(messages[0].type, equals(MessageType.print));
+            expect(messages[0].text, equals('Hello,'));
+            expect(messages[1].type, equals(MessageType.print));
+            expect(messages[1].text, equals('world!'));
+          }),
+          completes);
+
+      return liveTest.run();
+    }, prints(isEmpty));
+  });
+
+  group('timeout:', () {
+    test('A test can be timed out', () {
+      var liveTest = _localTest(() {
+        Invoker.current!.addOutstandingCallback();
+      },
+              metadata: Metadata(
+                  chainStackTraces: true,
+                  timeout: const Timeout(Duration.zero)))
+          .load(suite);
+
+      expectStates(liveTest, [
+        const State(Status.running, Result.success),
+        const State(Status.complete, Result.error)
+      ]);
+
+      expectErrors(liveTest, [
+        (error) {
+          expect(lastState!.status, equals(Status.complete));
+          expect(error, const TypeMatcher<TimeoutException>());
+        }
+      ]);
+
+      liveTest.run();
+    });
+
+    test('can be ignored', () {
+      suite = Suite(Group.root([]), suitePlatform, ignoreTimeouts: true);
+      var liveTest = _localTest(() async {
+        await Future<void>.delayed(const Duration(milliseconds: 10));
+      },
+              metadata: Metadata(
+                  chainStackTraces: true,
+                  timeout: const Timeout(Duration.zero)))
+          .load(suite);
+
+      expectStates(liveTest, [
+        const State(Status.running, Result.success),
+        const State(Status.complete, Result.success)
+      ]);
+
+      liveTest.run();
+    });
+  });
+
+  group('runTearDowns', () {
+    test('runs multiple tear downs', () async {
+      var firstTearDownStarted = false;
+      var secondTearDownStarted = false;
+      await Invoker.current!.runTearDowns([
+        () {
+          firstTearDownStarted = true;
+        },
+        () {
+          secondTearDownStarted = true;
+        }
+      ]);
+      expect(secondTearDownStarted, isTrue);
+      expect(firstTearDownStarted, isTrue);
+    });
+
+    test('waits for the future returned tear downs to complete', () async {
+      var firstTearDownWork = Completer<void>();
+      var secondTearDownStarted = false;
+      var result = Invoker.current!.runTearDowns([
+        () {
+          secondTearDownStarted = true;
+        },
+        () async {
+          await firstTearDownWork.future;
+        },
+      ]);
+      await pumpEventQueue();
+      expect(secondTearDownStarted, isFalse);
+      firstTearDownWork.complete();
+      await result;
+      expect(secondTearDownStarted, isTrue);
+    });
+
+    test('allows next tear down to run while there are still prior callbacks',
+        () async {
+      var firstTearDownAsyncWork = Completer<void>();
+      var secondTearDownStarted = false;
+      unawaited(Invoker.current!.runTearDowns([
+        () {
+          secondTearDownStarted = true;
+        },
+        () {
+          Invoker.current!.addOutstandingCallback();
+          firstTearDownAsyncWork.future
+              .whenComplete(Invoker.current!.removeOutstandingCallback);
+        },
+      ]));
+      await pumpEventQueue();
+      expect(secondTearDownStarted, isTrue);
+      firstTearDownAsyncWork.complete();
+    });
+
+    test('forwards errors to the enclosing test but does not end it', () async {
+      var liveTest = _localTest(() async {
+        Invoker.current!.addOutstandingCallback();
+        await Invoker.current!.runTearDowns([
+          () {
+            throw 'oh no';
+          }
+        ]);
+      }).load(suite);
+
+      expectStates(liveTest, [
+        const State(Status.running, Result.success),
+        const State(Status.complete, Result.error)
+      ]);
+
+      var isComplete = false;
+      unawaited(liveTest.run().then((_) => isComplete = true));
+      await pumpEventQueue();
+      expect(liveTest.state.status, equals(Status.complete));
+      expect(isComplete, isFalse);
+    });
+  });
+
+  group('printOnFailure:', () {
+    test("doesn't print anything if the test succeeds", () async {
+      var liveTest = _localTest(() {
+        Invoker.current!.printOnFailure('only on failure');
+      }).load(suite);
+      liveTest.onError.listen(expectAsync1((_) {}, count: 0));
+
+      liveTest.onMessage.listen(expectAsync1((_) {}, count: 0));
+
+      await liveTest.run();
+    });
+
+    test('prints if the test fails', () async {
+      var liveTest = _localTest(() {
+        Invoker.current!.printOnFailure('only on failure');
+        expect(true, isFalse);
+      }).load(suite);
+      liveTest.onError.listen(expectAsync1((_) {}, count: 1));
+
+      liveTest.onMessage.listen(expectAsync1((message) {
+        expect(message.type, equals(MessageType.print));
+        expect(message.text, equals('only on failure'));
+      }, count: 1));
+
+      await liveTest.run();
+    });
+  });
+}
+
+LocalTest _localTest(dynamic Function() body, {Metadata? metadata}) {
+  metadata ??= Metadata(chainStackTraces: true);
+  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
new file mode 100644
index 0000000..f42a535
--- /dev/null
+++ b/pkgs/test_api/test/backend/metadata_test.dart
@@ -0,0 +1,257 @@
+// 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.
+
+import 'package:boolean_selector/boolean_selector.dart';
+import 'package:test/test.dart';
+import 'package:test_api/src/backend/metadata.dart';
+import 'package:test_api/src/backend/platform_selector.dart';
+import 'package:test_api/src/backend/runtime.dart';
+import 'package:test_api/src/backend/suite_platform.dart';
+
+void main() {
+  group('tags', () {
+    test('parses an Iterable', () {
+      expect(
+          Metadata.parse(tags: ['a', 'b']).tags, unorderedEquals(['a', 'b']));
+    });
+
+    test('parses a String', () {
+      expect(Metadata.parse(tags: 'a').tags, unorderedEquals(['a']));
+    });
+
+    test('parses null', () {
+      expect(Metadata.parse().tags, unorderedEquals([]));
+    });
+
+    test('parse refuses an invalid type', () {
+      expect(() => Metadata.parse(tags: 1), throwsArgumentError);
+    });
+
+    test('parse refuses an invalid type in a list', () {
+      expect(() => Metadata.parse(tags: [1]), throwsArgumentError);
+    });
+
+    test('merges tags by computing the union of the two tag sets', () {
+      var merged = Metadata(tags: ['a', 'b']).merge(Metadata(tags: ['b', 'c']));
+      expect(merged.tags, unorderedEquals(['a', 'b', 'c']));
+    });
+
+    test('serializes and deserializes tags', () {
+      var metadata = Metadata(tags: ['a', 'b']).serialize();
+      expect(Metadata.deserialize(metadata).tags, unorderedEquals(['a', 'b']));
+    });
+  });
+
+  group('constructor', () {
+    test("returns the normal metadata if there's no forTag", () {
+      var metadata = Metadata(verboseTrace: true, tags: ['foo', 'bar']);
+      expect(metadata.verboseTrace, isTrue);
+      expect(metadata.tags, equals(['foo', 'bar']));
+    });
+
+    test("returns the normal metadata if there's no tags", () {
+      var metadata = Metadata(
+          verboseTrace: true,
+          forTag: {BooleanSelector.parse('foo'): Metadata(skip: true)});
+      expect(metadata.verboseTrace, isTrue);
+      expect(metadata.skip, isFalse);
+      expect(metadata.forTag, contains(BooleanSelector.parse('foo')));
+      expect(metadata.forTag[BooleanSelector.parse('foo')]?.skip, isTrue);
+    });
+
+    test("returns the normal metadata if forTag doesn't match tags", () {
+      var metadata = Metadata(
+          verboseTrace: true,
+          tags: ['bar', 'baz'],
+          forTag: {BooleanSelector.parse('foo'): Metadata(skip: true)});
+
+      expect(metadata.verboseTrace, isTrue);
+      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);
+    });
+
+    test('resolves forTags that match tags', () {
+      var metadata = Metadata(verboseTrace: true, tags: [
+        'foo',
+        'bar',
+        'baz'
+      ], forTag: {
+        BooleanSelector.parse('foo'): Metadata(skip: true),
+        BooleanSelector.parse('baz'): Metadata(timeout: Timeout.none),
+        BooleanSelector.parse('qux'): Metadata(skipReason: 'blah')
+      });
+
+      expect(metadata.verboseTrace, isTrue);
+      expect(metadata.skip, isTrue);
+      expect(metadata.skipReason, isNull);
+      expect(metadata.timeout, equals(Timeout.none));
+      expect(metadata.tags, unorderedEquals(['foo', 'bar', 'baz']));
+      expect(metadata.forTag.keys, equals([BooleanSelector.parse('qux')]));
+    });
+
+    test('resolves forTags that adds a behavioral tag', () {
+      var metadata = Metadata(tags: [
+        'foo'
+      ], forTag: {
+        BooleanSelector.parse('baz'): Metadata(skip: true),
+        BooleanSelector.parse('bar'):
+            Metadata(verboseTrace: true, tags: ['baz']),
+        BooleanSelector.parse('foo'): Metadata(tags: ['bar'])
+      });
+
+      expect(metadata.verboseTrace, isTrue);
+      expect(metadata.skip, isTrue);
+      expect(metadata.tags, unorderedEquals(['foo', 'bar', 'baz']));
+      expect(metadata.forTag, isEmpty);
+    });
+
+    test('resolves forTags that adds circular tags', () {
+      var metadata = Metadata(tags: [
+        'foo'
+      ], forTag: {
+        BooleanSelector.parse('foo'): Metadata(tags: ['bar']),
+        BooleanSelector.parse('bar'): Metadata(tags: ['baz']),
+        BooleanSelector.parse('baz'): Metadata(tags: ['foo'])
+      });
+
+      expect(metadata.tags, unorderedEquals(['foo', 'bar', 'baz']));
+      expect(metadata.forTag, isEmpty);
+    });
+
+    test('base metadata takes precedence over forTags', () {
+      var metadata = Metadata(verboseTrace: true, tags: [
+        'foo'
+      ], forTag: {
+        BooleanSelector.parse('foo'): Metadata(verboseTrace: false)
+      });
+
+      expect(metadata.verboseTrace, isTrue);
+    });
+  });
+
+  group('onPlatform', () {
+    test('parses a valid map', () {
+      var metadata = Metadata.parse(onPlatform: {
+        'chrome': const Timeout.factor(2),
+        'vm': [const Skip(), const Timeout.factor(3)]
+      });
+
+      var key = metadata.onPlatform.keys.first;
+      expect(
+          key.evaluate(SuitePlatform(Runtime.chrome, compiler: null)), isTrue);
+      expect(key.evaluate(SuitePlatform(Runtime.vm, compiler: null)), isFalse);
+      var value = metadata.onPlatform.values.first;
+      expect(value.timeout.scaleFactor, equals(2));
+
+      key = metadata.onPlatform.keys.last;
+      expect(key.evaluate(SuitePlatform(Runtime.vm, compiler: null)), isTrue);
+      expect(
+          key.evaluate(SuitePlatform(Runtime.chrome, compiler: null)), isFalse);
+      value = metadata.onPlatform.values.last;
+      expect(value.skip, isTrue);
+      expect(value.timeout.scaleFactor, equals(3));
+    });
+
+    test('refuses an invalid value', () {
+      expect(() {
+        Metadata.parse(onPlatform: {'chrome': const TestOn('chrome')});
+      }, throwsArgumentError);
+    });
+
+    test('refuses an invalid value in a list', () {
+      expect(() {
+        Metadata.parse(onPlatform: {
+          'chrome': [const TestOn('chrome')]
+        });
+      }, throwsArgumentError);
+    });
+
+    test('refuses an invalid platform selector', () {
+      expect(() {
+        Metadata.parse(onPlatform: {'vm &&': const Skip()});
+      }, throwsFormatException);
+    });
+
+    test('refuses multiple Timeouts', () {
+      expect(() {
+        Metadata.parse(onPlatform: {
+          'chrome': [const Timeout.factor(2), const Timeout.factor(3)]
+        });
+      }, throwsArgumentError);
+    });
+
+    test('refuses multiple Skips', () {
+      expect(() {
+        Metadata.parse(onPlatform: {
+          'chrome': [const Skip(), const Skip()]
+        });
+      }, throwsArgumentError);
+    });
+  });
+
+  group('validatePlatformSelectors', () {
+    test('succeeds if onPlatform uses valid platforms', () {
+      Metadata.parse(onPlatform: {'vm || browser': const Skip()})
+          .validatePlatformSelectors({'vm'});
+    });
+
+    test('succeeds if testOn uses valid platforms', () {
+      Metadata.parse(testOn: 'vm || browser').validatePlatformSelectors({'vm'});
+    });
+
+    test('succeeds if testOn uses valid compilers', () {
+      Metadata.parse(testOn: 'dart2js || kernel').validatePlatformSelectors({});
+    });
+
+    test('fails if onPlatform uses an invalid platform', () {
+      expect(() {
+        Metadata.parse(onPlatform: {'unknown': const Skip()})
+            .validatePlatformSelectors({'vm'});
+      }, throwsFormatException);
+    });
+
+    test('fails if testOn uses an invalid platform', () {
+      expect(() {
+        Metadata.parse(testOn: 'unknown').validatePlatformSelectors({'vm'});
+      }, throwsFormatException);
+    });
+
+    test('fails if testOn uses an invalid compiler', () {
+      expect(() {
+        Metadata.parse(testOn: 'foo2bar').validatePlatformSelectors({});
+      }, throwsFormatException);
+    });
+  });
+
+  group('change', () {
+    test('preserves all fields if no parameters are passed', () {
+      var metadata = Metadata(
+          testOn: PlatformSelector.parse('linux'),
+          timeout: const Timeout.factor(2),
+          skip: true,
+          skipReason: 'just because',
+          verboseTrace: true,
+          tags: [
+            'foo',
+            'bar'
+          ],
+          onPlatform: {
+            PlatformSelector.parse('mac-os'): Metadata(skip: false)
+          },
+          forTag: {
+            BooleanSelector.parse('slow'):
+                Metadata(timeout: const Timeout.factor(4))
+          });
+      expect(metadata.serialize(), equals(metadata.change().serialize()));
+    });
+
+    test('updates a changed field', () {
+      var metadata = Metadata(timeout: const Timeout.factor(2));
+      expect(metadata.change(timeout: const Timeout.factor(3)).timeout,
+          equals(const Timeout.factor(3)));
+    });
+  });
+}
diff --git a/pkgs/test_api/test/frontend/add_tear_down_test.dart b/pkgs/test_api/test/frontend/add_tear_down_test.dart
new file mode 100644
index 0000000..de4bb55
--- /dev/null
+++ b/pkgs/test_api/test/frontend/add_tear_down_test.dart
@@ -0,0 +1,798 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+import 'package:test/test.dart';
+
+import '../utils.dart';
+
+void main() {
+  group('in a test', () {
+    test('runs after the test body', () {
+      return expectTestsPass(() {
+        var test1Run = false;
+        var tearDownRun = false;
+        test('test 1', () {
+          addTearDown(() {
+            expect(test1Run, isTrue);
+            expect(tearDownRun, isFalse);
+            tearDownRun = true;
+          });
+
+          expect(tearDownRun, isFalse);
+          test1Run = true;
+        });
+
+        test('test 2', () {
+          expect(tearDownRun, isTrue);
+        });
+      });
+    });
+
+    test('multiples run in reverse order', () {
+      return expectTestsPass(() {
+        var tearDown1Run = false;
+        var tearDown2Run = false;
+        var tearDown3Run = false;
+
+        test('test 1', () {
+          addTearDown(() {
+            expect(tearDown1Run, isFalse);
+            expect(tearDown2Run, isTrue);
+            expect(tearDown3Run, isTrue);
+            tearDown1Run = true;
+          });
+
+          addTearDown(() {
+            expect(tearDown1Run, isFalse);
+            expect(tearDown2Run, isFalse);
+            expect(tearDown3Run, isTrue);
+            tearDown2Run = true;
+          });
+
+          addTearDown(() {
+            expect(tearDown1Run, isFalse);
+            expect(tearDown2Run, isFalse);
+            expect(tearDown3Run, isFalse);
+            tearDown3Run = true;
+          });
+
+          expect(tearDown1Run, isFalse);
+          expect(tearDown2Run, isFalse);
+          expect(tearDown3Run, isFalse);
+        });
+
+        test('test 2', () {
+          expect(tearDown1Run, isTrue);
+          expect(tearDown2Run, isTrue);
+          expect(tearDown3Run, isTrue);
+        });
+      });
+    });
+
+    test('can be called in addTearDown', () {
+      return expectTestsPass(() {
+        var tearDown2Run = false;
+        var tearDown3Run = false;
+
+        test('test 1', () {
+          addTearDown(() {
+            expect(tearDown2Run, isTrue);
+            expect(tearDown3Run, isFalse);
+            tearDown3Run = true;
+          });
+
+          addTearDown(() {
+            addTearDown(() {
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isFalse);
+              tearDown2Run = true;
+            });
+          });
+        });
+
+        test('test 2', () {
+          expect(tearDown2Run, isTrue);
+          expect(tearDown3Run, isTrue);
+        });
+      });
+    });
+
+    test('can be called in tearDown', () {
+      return expectTestsPass(() {
+        var tearDown2Run = false;
+        var tearDown3Run = false;
+
+        tearDown(() {
+          expect(tearDown2Run, isTrue);
+          expect(tearDown3Run, isFalse);
+          tearDown3Run = true;
+        });
+
+        tearDown(() {
+          tearDown2Run = false;
+          tearDown3Run = false;
+
+          addTearDown(() {
+            expect(tearDown2Run, isFalse);
+            expect(tearDown3Run, isFalse);
+            tearDown2Run = true;
+          });
+        });
+
+        test('test 1', () {});
+
+        test('test 2', () {
+          expect(tearDown2Run, isTrue);
+          expect(tearDown3Run, isTrue);
+        });
+      });
+    });
+
+    test('runs before a normal tearDown', () {
+      return expectTestsPass(() {
+        var groupTearDownRun = false;
+        var testTearDownRun = false;
+        group('group', () {
+          tearDown(() {
+            expect(testTearDownRun, isTrue);
+            expect(groupTearDownRun, isFalse);
+            groupTearDownRun = true;
+          });
+
+          test('test 1', () {
+            addTearDown(() {
+              expect(groupTearDownRun, isFalse);
+              expect(testTearDownRun, isFalse);
+              testTearDownRun = true;
+            });
+
+            expect(groupTearDownRun, isFalse);
+            expect(testTearDownRun, isFalse);
+          });
+        });
+
+        test('test 2', () {
+          expect(groupTearDownRun, isTrue);
+          expect(testTearDownRun, isTrue);
+        });
+      });
+    });
+
+    test('runs in the same error zone as the test', () {
+      return expectTestsPass(() {
+        test('test', () {
+          final testBodyZone = Zone.current;
+
+          addTearDown(() {
+            final tearDownZone = Zone.current;
+            expect(tearDownZone.inSameErrorZone(testBodyZone), isTrue,
+                reason: 'The tear down callback is in a different error zone '
+                    'than the test body.');
+          });
+        });
+      });
+    });
+
+    group('asynchronously', () {
+      test('blocks additional test tearDowns on in-band async', () {
+        return expectTestsPass(() {
+          var tearDown1Run = false;
+          var tearDown2Run = false;
+          var tearDown3Run = false;
+          test('test', () {
+            addTearDown(() async {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isTrue);
+              expect(tearDown3Run, isTrue);
+              await pumpEventQueue();
+              tearDown1Run = true;
+            });
+
+            addTearDown(() async {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isTrue);
+              await pumpEventQueue();
+              tearDown2Run = true;
+            });
+
+            addTearDown(() async {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isFalse);
+              await pumpEventQueue();
+              tearDown3Run = true;
+            });
+
+            expect(tearDown1Run, isFalse);
+            expect(tearDown2Run, isFalse);
+            expect(tearDown3Run, isFalse);
+          });
+        });
+      });
+
+      test("doesn't block additional test tearDowns on out-of-band async", () {
+        return expectTestsPass(() {
+          var tearDown1Run = false;
+          var tearDown2Run = false;
+          var tearDown3Run = false;
+          test('test', () {
+            addTearDown(() {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isFalse);
+
+              expect(Future(() {
+                tearDown1Run = true;
+              }), completes);
+            });
+
+            addTearDown(() {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isFalse);
+
+              expect(Future(() {
+                tearDown2Run = true;
+              }), completes);
+            });
+
+            addTearDown(() {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isFalse);
+
+              expect(Future(() {
+                tearDown3Run = true;
+              }), completes);
+            });
+
+            expect(tearDown1Run, isFalse);
+            expect(tearDown2Run, isFalse);
+            expect(tearDown3Run, isFalse);
+          });
+        });
+      });
+
+      test('blocks additional group tearDowns on in-band async', () {
+        return expectTestsPass(() {
+          var groupTearDownRun = false;
+          var testTearDownRun = false;
+          tearDown(() async {
+            expect(groupTearDownRun, isFalse);
+            expect(testTearDownRun, isTrue);
+            await pumpEventQueue();
+            groupTearDownRun = true;
+          });
+
+          test('test', () {
+            addTearDown(() async {
+              expect(groupTearDownRun, isFalse);
+              expect(testTearDownRun, isFalse);
+              await pumpEventQueue();
+              testTearDownRun = true;
+            });
+
+            expect(groupTearDownRun, isFalse);
+            expect(testTearDownRun, isFalse);
+          });
+        });
+      });
+
+      test("doesn't block additional group tearDowns on out-of-band async", () {
+        return expectTestsPass(() {
+          var groupTearDownRun = false;
+          var testTearDownRun = false;
+          tearDown(() {
+            expect(groupTearDownRun, isFalse);
+            expect(testTearDownRun, isFalse);
+
+            expect(Future(() {
+              groupTearDownRun = true;
+            }), completes);
+          });
+
+          test('test', () {
+            addTearDown(() {
+              expect(groupTearDownRun, isFalse);
+              expect(testTearDownRun, isFalse);
+
+              expect(Future(() {
+                testTearDownRun = true;
+              }), completes);
+            });
+
+            expect(groupTearDownRun, isFalse);
+            expect(testTearDownRun, isFalse);
+          });
+        });
+      });
+
+      test('blocks further tests on in-band async', () {
+        return expectTestsPass(() {
+          var tearDownRun = false;
+          test('test 1', () {
+            addTearDown(() async {
+              expect(tearDownRun, isFalse);
+              await pumpEventQueue();
+              tearDownRun = true;
+            });
+          });
+
+          test('test 2', () {
+            expect(tearDownRun, isTrue);
+          });
+        });
+      });
+
+      test('blocks further tests on out-of-band async', () {
+        return expectTestsPass(() {
+          var tearDownRun = false;
+          test('test 1', () {
+            addTearDown(() async {
+              expect(tearDownRun, isFalse);
+              expect(
+                  pumpEventQueue().then((_) {
+                    tearDownRun = true;
+                  }),
+                  completes);
+            });
+          });
+
+          test('after', () {
+            expect(tearDownRun, isTrue);
+          });
+        });
+      });
+    });
+
+    group('with an error', () {
+      test('reports the error', () async {
+        var engine = declareEngine(() {
+          test('test', () {
+            addTearDown(() => throw TestFailure('fail'));
+          });
+        });
+
+        var queue = StreamQueue(engine.onTestStarted);
+        var liveTestFuture = queue.next;
+
+        expect(await engine.run(), isFalse);
+
+        var liveTest = await liveTestFuture;
+        expect(liveTest.test.name, equals('test'));
+        expectTestFailed(liveTest, 'fail');
+      });
+
+      test('runs further test tearDowns', () async {
+        // Declare this in the outer test so if it doesn't run, the outer test
+        // will fail.
+        var shouldRun = expectAsync0(() {});
+
+        var engine = declareEngine(() {
+          test('test', () {
+            addTearDown(() => throw 'error');
+            addTearDown(shouldRun);
+          });
+        });
+
+        expect(await engine.run(), isFalse);
+      });
+
+      test('runs further group tearDowns', () async {
+        // Declare this in the outer test so if it doesn't run, the outer test
+        // will fail.
+        var shouldRun = expectAsync0(() {});
+
+        var engine = declareEngine(() {
+          tearDown(shouldRun);
+
+          test('test', () {
+            addTearDown(() => throw 'error');
+          });
+        });
+
+        expect(await engine.run(), isFalse);
+      });
+    });
+  });
+
+  group('in setUpAll()', () {
+    test('runs after all tests', () async {
+      var test1Run = false;
+      var test2Run = false;
+      var tearDownRun = false;
+      await expectTestsPass(() {
+        setUpAll(() {
+          addTearDown(() {
+            expect(test1Run, isTrue);
+            expect(test2Run, isTrue);
+            expect(tearDownRun, isFalse);
+            tearDownRun = true;
+          });
+        });
+
+        test('test 1', () {
+          test1Run = true;
+          expect(tearDownRun, isFalse);
+        });
+
+        test('test 2', () {
+          test2Run = true;
+          expect(tearDownRun, isFalse);
+        });
+      });
+
+      expect(test1Run, isTrue);
+      expect(test2Run, isTrue);
+      expect(tearDownRun, isTrue);
+    });
+
+    test('multiples run in reverse order', () async {
+      var tearDown1Run = false;
+      var tearDown2Run = false;
+      var tearDown3Run = false;
+      await expectTestsPass(() {
+        setUpAll(() {
+          addTearDown(() {
+            expect(tearDown1Run, isFalse);
+            expect(tearDown2Run, isTrue);
+            expect(tearDown3Run, isTrue);
+            tearDown1Run = true;
+          });
+
+          addTearDown(() {
+            expect(tearDown1Run, isFalse);
+            expect(tearDown2Run, isFalse);
+            expect(tearDown3Run, isTrue);
+            tearDown2Run = true;
+          });
+
+          addTearDown(() {
+            expect(tearDown1Run, isFalse);
+            expect(tearDown2Run, isFalse);
+            expect(tearDown3Run, isFalse);
+            tearDown3Run = true;
+          });
+
+          expect(tearDown1Run, isFalse);
+          expect(tearDown2Run, isFalse);
+          expect(tearDown3Run, isFalse);
+        });
+
+        test('test', () {
+          expect(tearDown1Run, isFalse);
+          expect(tearDown2Run, isFalse);
+          expect(tearDown3Run, isFalse);
+        });
+      });
+
+      expect(tearDown1Run, isTrue);
+      expect(tearDown2Run, isTrue);
+      expect(tearDown3Run, isTrue);
+    });
+
+    test('can be called in addTearDown', () async {
+      var tearDown2Run = false;
+      var tearDown3Run = false;
+      await expectTestsPass(() {
+        setUpAll(() {
+          addTearDown(() {
+            expect(tearDown2Run, isTrue);
+            expect(tearDown3Run, isFalse);
+            tearDown3Run = true;
+          });
+
+          addTearDown(() {
+            addTearDown(() {
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isFalse);
+              tearDown2Run = true;
+            });
+          });
+        });
+
+        test('test', () {
+          expect(tearDown2Run, isFalse);
+          expect(tearDown3Run, isFalse);
+        });
+      });
+
+      expect(tearDown2Run, isTrue);
+      expect(tearDown3Run, isTrue);
+    });
+
+    test('can be called in tearDownAll', () async {
+      var tearDown2Run = false;
+      var tearDown3Run = false;
+      await expectTestsPass(() {
+        tearDownAll(() {
+          expect(tearDown2Run, isTrue);
+          expect(tearDown3Run, isFalse);
+          tearDown3Run = true;
+        });
+
+        tearDownAll(() {
+          tearDown2Run = false;
+          tearDown3Run = false;
+
+          addTearDown(() {
+            expect(tearDown2Run, isFalse);
+            expect(tearDown3Run, isFalse);
+            tearDown2Run = true;
+          });
+        });
+
+        test('test', () {});
+      });
+
+      expect(tearDown2Run, isTrue);
+      expect(tearDown3Run, isTrue);
+    });
+
+    test('runs before a normal tearDownAll', () async {
+      var groupTearDownRun = false;
+      var testTearDownRun = false;
+      await expectTestsPass(() {
+        tearDownAll(() {
+          expect(testTearDownRun, isTrue);
+          expect(groupTearDownRun, isFalse);
+          groupTearDownRun = true;
+        });
+
+        setUpAll(() {
+          addTearDown(() {
+            expect(groupTearDownRun, isFalse);
+            expect(testTearDownRun, isFalse);
+            testTearDownRun = true;
+          });
+        });
+
+        test('test', () {
+          expect(groupTearDownRun, isFalse);
+          expect(testTearDownRun, isFalse);
+        });
+      });
+
+      expect(groupTearDownRun, isTrue);
+      expect(testTearDownRun, isTrue);
+    });
+
+    test('runs in the same error zone as the setUpAll', () async {
+      return expectTestsPass(() {
+        setUpAll(() {
+          final setUpAllZone = Zone.current;
+
+          addTearDown(() {
+            final tearDownZone = Zone.current;
+            expect(tearDownZone.inSameErrorZone(setUpAllZone), isTrue,
+                reason: 'The tear down callback is in a different error zone '
+                    'than the set up all callback.');
+          });
+        });
+
+        test('test', () {});
+      });
+    });
+
+    group('asynchronously', () {
+      test('blocks additional tearDowns on in-band async', () async {
+        var tearDown1Run = false;
+        var tearDown2Run = false;
+        var tearDown3Run = false;
+        await expectTestsPass(() {
+          setUpAll(() {
+            addTearDown(() async {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isTrue);
+              expect(tearDown3Run, isTrue);
+              await pumpEventQueue();
+              tearDown1Run = true;
+            });
+
+            addTearDown(() async {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isTrue);
+              await pumpEventQueue();
+              tearDown2Run = true;
+            });
+
+            addTearDown(() async {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isFalse);
+              await pumpEventQueue();
+              tearDown3Run = true;
+            });
+          });
+
+          test('test', () {
+            expect(tearDown1Run, isFalse);
+            expect(tearDown2Run, isFalse);
+            expect(tearDown3Run, isFalse);
+          });
+        });
+
+        expect(tearDown1Run, isTrue);
+        expect(tearDown2Run, isTrue);
+        expect(tearDown3Run, isTrue);
+      });
+
+      test("doesn't block additional tearDowns on out-of-band async", () async {
+        var tearDown1Run = false;
+        var tearDown2Run = false;
+        var tearDown3Run = false;
+        await expectTestsPass(() {
+          setUpAll(() {
+            addTearDown(() {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isFalse);
+
+              expect(Future(() {
+                tearDown1Run = true;
+              }), completes);
+            });
+
+            addTearDown(() {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isFalse);
+
+              expect(Future(() {
+                tearDown2Run = true;
+              }), completes);
+            });
+
+            addTearDown(() {
+              expect(tearDown1Run, isFalse);
+              expect(tearDown2Run, isFalse);
+              expect(tearDown3Run, isFalse);
+
+              expect(Future(() {
+                tearDown3Run = true;
+              }), completes);
+            });
+          });
+
+          test('test', () {
+            expect(tearDown1Run, isFalse);
+            expect(tearDown2Run, isFalse);
+            expect(tearDown3Run, isFalse);
+          });
+        });
+
+        expect(tearDown1Run, isTrue);
+        expect(tearDown2Run, isTrue);
+        expect(tearDown3Run, isTrue);
+      });
+
+      test('blocks additional tearDownAlls on in-band async', () async {
+        var groupTearDownRun = false;
+        var testTearDownRun = false;
+        await expectTestsPass(() {
+          tearDownAll(() async {
+            expect(groupTearDownRun, isFalse);
+            expect(testTearDownRun, isTrue);
+            await pumpEventQueue();
+            groupTearDownRun = true;
+          });
+
+          setUpAll(() {
+            addTearDown(() async {
+              expect(groupTearDownRun, isFalse);
+              expect(testTearDownRun, isFalse);
+              await pumpEventQueue();
+              testTearDownRun = true;
+            });
+          });
+
+          test('test', () {
+            expect(groupTearDownRun, isFalse);
+            expect(testTearDownRun, isFalse);
+          });
+        });
+
+        expect(groupTearDownRun, isTrue);
+        expect(testTearDownRun, isTrue);
+      });
+
+      test("doesn't block additional tearDownAlls on out-of-band async",
+          () async {
+        var groupTearDownRun = false;
+        var testTearDownRun = false;
+        await expectTestsPass(() {
+          tearDownAll(() {
+            expect(groupTearDownRun, isFalse);
+            expect(testTearDownRun, isFalse);
+
+            expect(Future(() {
+              groupTearDownRun = true;
+            }), completes);
+          });
+
+          setUpAll(() {
+            addTearDown(() {
+              expect(groupTearDownRun, isFalse);
+              expect(testTearDownRun, isFalse);
+
+              expect(Future(() {
+                testTearDownRun = true;
+              }), completes);
+            });
+          });
+
+          test('test', () {
+            expect(groupTearDownRun, isFalse);
+            expect(testTearDownRun, isFalse);
+          });
+        });
+
+        expect(groupTearDownRun, isTrue);
+        expect(testTearDownRun, isTrue);
+      });
+    });
+
+    group('with an error', () {
+      test('reports the error', () async {
+        var engine = declareEngine(() {
+          setUpAll(() {
+            addTearDown(() => throw TestFailure('fail'));
+          });
+
+          test('test', () {});
+        });
+
+        var queue = StreamQueue(engine.onTestStarted);
+        unawaited(queue.skip(2));
+        var liveTestFuture = queue.next;
+
+        expect(await engine.run(), isFalse);
+
+        var liveTest = await liveTestFuture;
+        expect(liveTest.test.name, equals('(tearDownAll)'));
+        expectTestFailed(liveTest, 'fail');
+      });
+
+      test('runs further tearDowns', () async {
+        // Declare this in the outer test so if it doesn't run, the outer test
+        // will fail.
+        var shouldRun = expectAsync0(() {});
+
+        var engine = declareEngine(() {
+          setUpAll(() {
+            addTearDown(() => throw 'error');
+            addTearDown(shouldRun);
+          });
+
+          test('test', () {});
+        });
+
+        expect(await engine.run(), isFalse);
+      });
+
+      test('runs further tearDownAlls', () async {
+        // Declare this in the outer test so if it doesn't run, the outer test
+        // will fail.
+        var shouldRun = expectAsync0(() {});
+
+        var engine = declareEngine(() {
+          tearDownAll(shouldRun);
+
+          setUpAll(() {
+            addTearDown(() => throw 'error');
+          });
+
+          test('test', () {});
+        });
+
+        expect(await engine.run(), isFalse);
+      });
+    });
+  });
+}
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..7dfb306
--- /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(const TypeMatcher<UnimplementedError>()));
+  });
+  test('getter', () {
+    expect(() => fake.x, throwsA(const TypeMatcher<UnimplementedError>()));
+  });
+  test('setter', () {
+    expect(() => fake.x = 0, throwsA(const TypeMatcher<UnimplementedError>()));
+  });
+  test('operator', () {
+    expect(() => fake + 1, throwsA(const 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/set_up_all_test.dart b/pkgs/test_api/test/frontend/set_up_all_test.dart
new file mode 100644
index 0000000..60d4dc8
--- /dev/null
+++ b/pkgs/test_api/test/frontend/set_up_all_test.dart
@@ -0,0 +1,328 @@
+// 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.
+
+import 'package:async/async.dart';
+import 'package:test/test.dart';
+
+import '../utils.dart';
+
+void main() {
+  test('runs once before all tests', () {
+    return expectTestsPass(() {
+      var setUpAllRun = false;
+      setUpAll(() {
+        expect(setUpAllRun, isFalse);
+        setUpAllRun = true;
+      });
+
+      test('test 1', () {
+        expect(setUpAllRun, isTrue);
+      });
+
+      test('test 2', () {
+        expect(setUpAllRun, isTrue);
+      });
+    });
+  });
+
+  test('runs once per group, outside-in', () {
+    return expectTestsPass(() {
+      var setUpAll1Run = false;
+      var setUpAll2Run = false;
+      var setUpAll3Run = false;
+      setUpAll(() {
+        expect(setUpAll1Run, isFalse);
+        expect(setUpAll2Run, isFalse);
+        expect(setUpAll3Run, isFalse);
+        setUpAll1Run = true;
+      });
+
+      group('mid', () {
+        setUpAll(() {
+          expect(setUpAll1Run, isTrue);
+          expect(setUpAll2Run, isFalse);
+          expect(setUpAll3Run, isFalse);
+          setUpAll2Run = true;
+        });
+
+        group('inner', () {
+          setUpAll(() {
+            expect(setUpAll1Run, isTrue);
+            expect(setUpAll2Run, isTrue);
+            expect(setUpAll3Run, isFalse);
+            setUpAll3Run = true;
+          });
+
+          test('test', () {
+            expect(setUpAll1Run, isTrue);
+            expect(setUpAll2Run, isTrue);
+            expect(setUpAll3Run, isTrue);
+          });
+        });
+      });
+    });
+  });
+
+  test('runs before setUps', () {
+    return expectTestsPass(() {
+      var setUpAllRun = false;
+      setUp(() {
+        expect(setUpAllRun, isTrue);
+      });
+
+      setUpAll(() {
+        expect(setUpAllRun, isFalse);
+        setUpAllRun = true;
+      });
+
+      setUp(() {
+        expect(setUpAllRun, isTrue);
+      });
+
+      test('test', () {
+        expect(setUpAllRun, isTrue);
+      });
+    });
+  });
+
+  test('multiples run in order', () {
+    return expectTestsPass(() {
+      var setUpAll1Run = false;
+      var setUpAll2Run = false;
+      var setUpAll3Run = false;
+      setUpAll(() {
+        expect(setUpAll1Run, isFalse);
+        expect(setUpAll2Run, isFalse);
+        expect(setUpAll3Run, isFalse);
+        setUpAll1Run = true;
+      });
+
+      setUpAll(() {
+        expect(setUpAll1Run, isTrue);
+        expect(setUpAll2Run, isFalse);
+        expect(setUpAll3Run, isFalse);
+        setUpAll2Run = true;
+      });
+
+      setUpAll(() {
+        expect(setUpAll1Run, isTrue);
+        expect(setUpAll2Run, isTrue);
+        expect(setUpAll3Run, isFalse);
+        setUpAll3Run = true;
+      });
+
+      test('test', () {
+        expect(setUpAll1Run, isTrue);
+        expect(setUpAll2Run, isTrue);
+        expect(setUpAll3Run, isTrue);
+      });
+    });
+  });
+
+  group('asynchronously', () {
+    test('blocks additional setUpAlls on in-band async', () {
+      return expectTestsPass(() {
+        var setUpAll1Run = false;
+        var setUpAll2Run = false;
+        var setUpAll3Run = false;
+        setUpAll(() async {
+          expect(setUpAll1Run, isFalse);
+          expect(setUpAll2Run, isFalse);
+          expect(setUpAll3Run, isFalse);
+          await pumpEventQueue();
+          setUpAll1Run = true;
+        });
+
+        setUpAll(() async {
+          expect(setUpAll1Run, isTrue);
+          expect(setUpAll2Run, isFalse);
+          expect(setUpAll3Run, isFalse);
+          await pumpEventQueue();
+          setUpAll2Run = true;
+        });
+
+        setUpAll(() async {
+          expect(setUpAll1Run, isTrue);
+          expect(setUpAll2Run, isTrue);
+          expect(setUpAll3Run, isFalse);
+          await pumpEventQueue();
+          setUpAll3Run = true;
+        });
+
+        test('test', () {
+          expect(setUpAll1Run, isTrue);
+          expect(setUpAll2Run, isTrue);
+          expect(setUpAll3Run, isTrue);
+        });
+      });
+    });
+
+    test("doesn't block additional setUpAlls on out-of-band async", () {
+      return expectTestsPass(() {
+        var setUpAll1Run = false;
+        var setUpAll2Run = false;
+        var setUpAll3Run = false;
+        setUpAll(() {
+          expect(setUpAll1Run, isFalse);
+          expect(setUpAll2Run, isFalse);
+          expect(setUpAll3Run, isFalse);
+
+          expect(
+              pumpEventQueue().then((_) {
+                setUpAll1Run = true;
+              }),
+              completes);
+        });
+
+        setUpAll(() {
+          expect(setUpAll1Run, isFalse);
+          expect(setUpAll2Run, isFalse);
+          expect(setUpAll3Run, isFalse);
+
+          expect(
+              pumpEventQueue().then((_) {
+                setUpAll2Run = true;
+              }),
+              completes);
+        });
+
+        setUpAll(() {
+          expect(setUpAll1Run, isFalse);
+          expect(setUpAll2Run, isFalse);
+          expect(setUpAll3Run, isFalse);
+
+          expect(
+              pumpEventQueue().then((_) {
+                setUpAll3Run = true;
+              }),
+              completes);
+        });
+
+        test('test', () {
+          expect(setUpAll1Run, isTrue);
+          expect(setUpAll2Run, isTrue);
+          expect(setUpAll3Run, isTrue);
+        });
+      });
+    });
+  });
+
+  test("isn't run for a skipped group", () async {
+    // Declare this in the outer test so if it runs, the outer test will fail.
+    var shouldNotRun = expectAsync0(() {}, count: 0);
+
+    var engine = declareEngine(() {
+      group('skipped', () {
+        setUpAll(shouldNotRun);
+
+        test('test', () {});
+      }, skip: true);
+    });
+
+    await engine.run();
+    expect(engine.liveTests, hasLength(1));
+    expect(engine.skipped, hasLength(1));
+    expect(engine.liveTests, equals(engine.skipped));
+  });
+
+  test('is emitted through Engine.onTestStarted', () async {
+    var engine = declareEngine(() {
+      setUpAll(() {});
+
+      test('test', () {});
+    });
+
+    var queue = StreamQueue(engine.onTestStarted);
+    var setUpAllFuture = queue.next;
+    var liveTestFuture = queue.next;
+
+    await engine.run();
+
+    var setUpAllLiveTest = await setUpAllFuture;
+    expect(setUpAllLiveTest.test.name, equals('(setUpAll)'));
+    expectTestPassed(setUpAllLiveTest);
+
+    // The fake test for setUpAll should be removed from the engine's live
+    // test list so that reporters don't display it as a passed test.
+    expect(engine.liveTests, isNot(contains(setUpAllLiveTest)));
+    expect(engine.passed, isNot(contains(setUpAllLiveTest)));
+    expect(engine.failed, isNot(contains(setUpAllLiveTest)));
+    expect(engine.skipped, isNot(contains(setUpAllLiveTest)));
+    expect(engine.active, isNot(contains(setUpAllLiveTest)));
+
+    var liveTest = await liveTestFuture;
+    expectTestPassed(await liveTestFuture);
+    expect(engine.liveTests, contains(liveTest));
+    expect(engine.passed, contains(liveTest));
+  });
+
+  group('with an error', () {
+    test('reports the error and remains in Engine.liveTests', () async {
+      var engine = declareEngine(() {
+        setUpAll(() => throw TestFailure('fail'));
+
+        test('test', () {});
+      });
+
+      var queue = StreamQueue(engine.onTestStarted);
+      var setUpAllFuture = queue.next;
+
+      expect(await engine.run(), isFalse);
+
+      var setUpAllLiveTest = await setUpAllFuture;
+      expect(setUpAllLiveTest.test.name, equals('(setUpAll)'));
+      expectTestFailed(setUpAllLiveTest, 'fail');
+
+      // The fake test for setUpAll should be removed from the engine's live
+      // test list so that reporters don't display it as a passed test.
+      expect(engine.liveTests, contains(setUpAllLiveTest));
+      expect(engine.failed, contains(setUpAllLiveTest));
+      expect(engine.passed, isNot(contains(setUpAllLiveTest)));
+      expect(engine.skipped, isNot(contains(setUpAllLiveTest)));
+      expect(engine.active, isNot(contains(setUpAllLiveTest)));
+    });
+
+    test("doesn't run tests in the group", () async {
+      // Declare this in the outer test so if it runs, the outer test will fail.
+      var shouldNotRun = expectAsync0(() {}, count: 0);
+
+      var engine = declareEngine(() {
+        setUpAll(() => throw 'error');
+
+        test('test', shouldNotRun);
+      });
+
+      expect(await engine.run(), isFalse);
+    });
+
+    test("doesn't run inner groups", () async {
+      // Declare this in the outer test so if it runs, the outer test will fail.
+      var shouldNotRun = expectAsync0(() {}, count: 0);
+
+      var engine = declareEngine(() {
+        setUpAll(() => throw 'error');
+
+        group('group', () {
+          test('test', shouldNotRun);
+        });
+      });
+
+      expect(await engine.run(), isFalse);
+    });
+
+    test("doesn't run further setUpAlls", () async {
+      // Declare this in the outer test so if it runs, the outer test will fail.
+      var shouldNotRun = expectAsync0(() {}, count: 0);
+
+      var engine = declareEngine(() {
+        setUpAll(() => throw 'error');
+        setUpAll(shouldNotRun);
+
+        test('test', shouldNotRun);
+      });
+
+      expect(await engine.run(), isFalse);
+    });
+  });
+}
diff --git a/pkgs/test_api/test/frontend/tear_down_all_test.dart b/pkgs/test_api/test/frontend/tear_down_all_test.dart
new file mode 100644
index 0000000..d6d209f
--- /dev/null
+++ b/pkgs/test_api/test/frontend/tear_down_all_test.dart
@@ -0,0 +1,372 @@
+// 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.
+
+import 'package:async/async.dart';
+import 'package:test/test.dart';
+
+import '../utils.dart';
+
+void main() {
+  test('runs once after all tests', () {
+    return expectTestsPass(() {
+      var test1Run = false;
+      var test2Run = false;
+      var tearDownAllRun = false;
+      tearDownAll(() {
+        expect(test1Run, isTrue);
+        expect(test2Run, isTrue);
+        expect(tearDownAllRun, isFalse);
+        tearDownAllRun = true;
+      });
+
+      test('test 1', () {
+        expect(tearDownAllRun, isFalse);
+        test1Run = true;
+      });
+
+      test('test 2', () {
+        expect(tearDownAllRun, isFalse);
+        test2Run = true;
+      });
+    });
+  });
+
+  test('runs once per group, inside-out', () {
+    return expectTestsPass(() {
+      var tearDownAll1Run = false;
+      var tearDownAll2Run = false;
+      var tearDownAll3Run = false;
+      var testRun = false;
+      tearDownAll(() {
+        expect(tearDownAll1Run, isFalse);
+        expect(tearDownAll2Run, isTrue);
+        expect(tearDownAll3Run, isTrue);
+        expect(testRun, isTrue);
+        tearDownAll1Run = true;
+      });
+
+      group('mid', () {
+        tearDownAll(() {
+          expect(tearDownAll1Run, isFalse);
+          expect(tearDownAll2Run, isFalse);
+          expect(tearDownAll3Run, isTrue);
+          expect(testRun, isTrue);
+          tearDownAll2Run = true;
+        });
+
+        group('inner', () {
+          tearDownAll(() {
+            expect(tearDownAll1Run, isFalse);
+            expect(tearDownAll2Run, isFalse);
+            expect(tearDownAll3Run, isFalse);
+            expect(testRun, isTrue);
+            tearDownAll3Run = true;
+          });
+
+          test('test', () {
+            expect(tearDownAll1Run, isFalse);
+            expect(tearDownAll2Run, isFalse);
+            expect(tearDownAll3Run, isFalse);
+            testRun = true;
+          });
+        });
+      });
+    });
+  });
+
+  test('runs after tearDowns', () {
+    return expectTestsPass(() {
+      var tearDown1Run = false;
+      var tearDown2Run = false;
+      var tearDownAllRun = false;
+      tearDown(() {
+        expect(tearDownAllRun, isFalse);
+        tearDown1Run = true;
+      });
+
+      tearDownAll(() {
+        expect(tearDown1Run, isTrue);
+        expect(tearDown2Run, isTrue);
+        expect(tearDownAllRun, isFalse);
+        tearDownAllRun = true;
+      });
+
+      tearDown(() {
+        expect(tearDownAllRun, isFalse);
+        tearDown2Run = true;
+      });
+
+      test('test', () {
+        expect(tearDownAllRun, isFalse);
+      });
+    });
+  });
+
+  test('multiples run in reverse order', () {
+    return expectTestsPass(() {
+      var tearDownAll1Run = false;
+      var tearDownAll2Run = false;
+      var tearDownAll3Run = false;
+      tearDownAll(() {
+        expect(tearDownAll1Run, isFalse);
+        expect(tearDownAll2Run, isTrue);
+        expect(tearDownAll3Run, isTrue);
+        tearDownAll1Run = true;
+      });
+
+      tearDownAll(() {
+        expect(tearDownAll1Run, isFalse);
+        expect(tearDownAll2Run, isFalse);
+        expect(tearDownAll3Run, isTrue);
+        tearDownAll2Run = true;
+      });
+
+      tearDownAll(() {
+        expect(tearDownAll1Run, isFalse);
+        expect(tearDownAll2Run, isFalse);
+        expect(tearDownAll3Run, isFalse);
+        tearDownAll3Run = true;
+      });
+
+      test('test', () {
+        expect(tearDownAll1Run, isFalse);
+        expect(tearDownAll2Run, isFalse);
+        expect(tearDownAll3Run, isFalse);
+      });
+    });
+  });
+
+  group('asynchronously', () {
+    test('blocks additional tearDownAlls on in-band async', () {
+      return expectTestsPass(() {
+        var tearDownAll1Run = false;
+        var tearDownAll2Run = false;
+        var tearDownAll3Run = false;
+        tearDownAll(() async {
+          expect(tearDownAll1Run, isFalse);
+          expect(tearDownAll2Run, isTrue);
+          expect(tearDownAll3Run, isTrue);
+          await pumpEventQueue();
+          tearDownAll1Run = true;
+        });
+
+        tearDownAll(() async {
+          expect(tearDownAll1Run, isFalse);
+          expect(tearDownAll2Run, isFalse);
+          expect(tearDownAll3Run, isTrue);
+          await pumpEventQueue();
+          tearDownAll2Run = true;
+        });
+
+        tearDownAll(() async {
+          expect(tearDownAll1Run, isFalse);
+          expect(tearDownAll2Run, isFalse);
+          expect(tearDownAll3Run, isFalse);
+          await pumpEventQueue();
+          tearDownAll3Run = true;
+        });
+
+        test('test', () {
+          expect(tearDownAll1Run, isFalse);
+          expect(tearDownAll2Run, isFalse);
+          expect(tearDownAll3Run, isFalse);
+        });
+      });
+    });
+
+    test("doesn't block additional tearDownAlls on out-of-band async", () {
+      return expectTestsPass(() {
+        var tearDownAll1Run = false;
+        var tearDownAll2Run = false;
+        var tearDownAll3Run = false;
+        tearDownAll(() {
+          expect(tearDownAll1Run, isFalse);
+          expect(tearDownAll2Run, isFalse);
+          expect(tearDownAll3Run, isFalse);
+
+          expect(Future(() {
+            tearDownAll1Run = true;
+          }), completes);
+        });
+
+        tearDownAll(() {
+          expect(tearDownAll1Run, isFalse);
+          expect(tearDownAll2Run, isFalse);
+          expect(tearDownAll3Run, isFalse);
+
+          expect(Future(() {
+            tearDownAll2Run = true;
+          }), completes);
+        });
+
+        tearDownAll(() {
+          expect(tearDownAll1Run, isFalse);
+          expect(tearDownAll2Run, isFalse);
+          expect(tearDownAll3Run, isFalse);
+
+          expect(Future(() {
+            tearDownAll3Run = true;
+          }), completes);
+        });
+
+        test('test', () {
+          expect(tearDownAll1Run, isFalse);
+          expect(tearDownAll2Run, isFalse);
+          expect(tearDownAll3Run, isFalse);
+        });
+      });
+    });
+
+    test('blocks further tests on in-band async', () {
+      return expectTestsPass(() {
+        var tearDownAllRun = false;
+        group('group', () {
+          tearDownAll(() async {
+            expect(tearDownAllRun, isFalse);
+            await pumpEventQueue();
+            tearDownAllRun = true;
+          });
+
+          test('test', () {});
+        });
+
+        test('after', () {
+          expect(tearDownAllRun, isTrue);
+        });
+      });
+    });
+
+    test('blocks further tests on out-of-band async', () {
+      return expectTestsPass(() {
+        var tearDownAllRun = false;
+        group('group', () {
+          tearDownAll(() async {
+            expect(tearDownAllRun, isFalse);
+            expect(
+                pumpEventQueue().then((_) {
+                  tearDownAllRun = true;
+                }),
+                completes);
+          });
+
+          test('test', () {});
+        });
+
+        test('after', () {
+          expect(tearDownAllRun, isTrue);
+        });
+      });
+    });
+  });
+
+  test("isn't run for a skipped group", () async {
+    // Declare this in the outer test so if it runs, the outer test will fail.
+    var shouldNotRun = expectAsync0(() {}, count: 0);
+
+    var engine = declareEngine(() {
+      group('skipped', () {
+        tearDownAll(shouldNotRun);
+
+        test('test', () {});
+      }, skip: true);
+    });
+
+    await engine.run();
+    expect(engine.liveTests, hasLength(1));
+    expect(engine.skipped, hasLength(1));
+    expect(engine.liveTests, equals(engine.skipped));
+  });
+
+  test('is emitted through Engine.onTestStarted', () async {
+    var engine = declareEngine(() {
+      tearDownAll(() {});
+
+      test('test', () {});
+    });
+
+    var queue = StreamQueue(engine.onTestStarted);
+    var liveTestFuture = queue.next;
+    var tearDownAllFuture = queue.next;
+
+    await engine.run();
+
+    var tearDownAllLiveTest = await tearDownAllFuture;
+    expect(tearDownAllLiveTest.test.name, equals('(tearDownAll)'));
+    expectTestPassed(tearDownAllLiveTest);
+
+    // The fake test for tearDownAll should be removed from the engine's live
+    // test list so that reporters don't display it as a passed test.
+    expect(engine.liveTests, isNot(contains(tearDownAllLiveTest)));
+    expect(engine.passed, isNot(contains(tearDownAllLiveTest)));
+    expect(engine.failed, isNot(contains(tearDownAllLiveTest)));
+    expect(engine.skipped, isNot(contains(tearDownAllLiveTest)));
+    expect(engine.active, isNot(contains(tearDownAllLiveTest)));
+
+    var liveTest = await liveTestFuture;
+    expectTestPassed(await liveTestFuture);
+    expect(engine.liveTests, contains(liveTest));
+    expect(engine.passed, contains(liveTest));
+  });
+
+  group('with an error', () {
+    test('reports the error and remains in Engine.liveTests', () async {
+      var engine = declareEngine(() {
+        tearDownAll(() => throw TestFailure('fail'));
+
+        test('test', () {});
+      });
+
+      var queue = StreamQueue(engine.onTestStarted);
+      expect(queue.next, completes);
+      var tearDownAllFuture = queue.next;
+
+      expect(await engine.run(), isFalse);
+
+      var tearDownAllLiveTest = await tearDownAllFuture;
+      expect(tearDownAllLiveTest.test.name, equals('(tearDownAll)'));
+      expectTestFailed(tearDownAllLiveTest, 'fail');
+
+      // The fake test for tearDownAll should be removed from the engine's live
+      // test list so that reporters don't display it as a passed test.
+      expect(engine.liveTests, contains(tearDownAllLiveTest));
+      expect(engine.failed, contains(tearDownAllLiveTest));
+      expect(engine.passed, isNot(contains(tearDownAllLiveTest)));
+      expect(engine.skipped, isNot(contains(tearDownAllLiveTest)));
+      expect(engine.active, isNot(contains(tearDownAllLiveTest)));
+    });
+
+    test('runs further tearDownAlls', () async {
+      // Declare this in the outer test so if it doesn't runs, the outer test
+      // will fail.
+      var shouldRun = expectAsync0(() {});
+
+      var engine = declareEngine(() {
+        tearDownAll(() => throw 'error');
+        tearDownAll(shouldRun);
+
+        test('test', () {});
+      });
+
+      expect(await engine.run(), isFalse);
+    });
+
+    test('runs outer tearDownAlls', () async {
+      // Declare this in the outer test so if it doesn't runs, the outer test
+      // will fail.
+      var shouldRun = expectAsync0(() {});
+
+      var engine = declareEngine(() {
+        tearDownAll(shouldRun);
+
+        group('group', () {
+          tearDownAll(() => throw 'error');
+
+          test('test', () {});
+        });
+      });
+
+      expect(await engine.run(), isFalse);
+    });
+  });
+}
diff --git a/pkgs/test_api/test/frontend/timeout_test.dart b/pkgs/test_api/test/frontend/timeout_test.dart
new file mode 100644
index 0000000..fdfd611
--- /dev/null
+++ b/pkgs/test_api/test/frontend/timeout_test.dart
@@ -0,0 +1,81 @@
+// 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.
+
+import 'package:test/test.dart';
+
+void main() {
+  group('Timeout.parse', () {
+    group('for "none"', () {
+      test('successfully parses', () {
+        expect(Timeout.parse('none'), equals(Timeout.none));
+      });
+
+      test('rejects invalid input', () {
+        expect(() => Timeout.parse(' none'), throwsFormatException);
+        expect(() => Timeout.parse('none '), throwsFormatException);
+        expect(() => Timeout.parse('xnone'), throwsFormatException);
+        expect(() => Timeout.parse('nonex'), throwsFormatException);
+        expect(() => Timeout.parse('noxe'), throwsFormatException);
+      });
+    });
+
+    group('for a relative timeout', () {
+      test('successfully parses', () {
+        expect(Timeout.parse('1x'), equals(const Timeout.factor(1)));
+        expect(Timeout.parse('2.5x'), equals(const Timeout.factor(2.5)));
+        expect(Timeout.parse('1.2e3x'), equals(const Timeout.factor(1.2e3)));
+      });
+
+      test('rejects invalid input', () {
+        expect(() => Timeout.parse('.x'), throwsFormatException);
+        expect(() => Timeout.parse('x'), throwsFormatException);
+        expect(() => Timeout.parse('ax'), throwsFormatException);
+        expect(() => Timeout.parse('1x '), throwsFormatException);
+        expect(() => Timeout.parse('1x5m'), throwsFormatException);
+      });
+    });
+
+    group('for an absolute timeout', () {
+      test('successfully parses all supported units', () {
+        expect(Timeout.parse('2d'), equals(const Timeout(Duration(days: 2))));
+        expect(Timeout.parse('2h'), equals(const Timeout(Duration(hours: 2))));
+        expect(
+            Timeout.parse('2m'), equals(const Timeout(Duration(minutes: 2))));
+        expect(
+            Timeout.parse('2s'), equals(const Timeout(Duration(seconds: 2))));
+        expect(Timeout.parse('2ms'),
+            equals(const Timeout(Duration(milliseconds: 2))));
+        expect(Timeout.parse('2us'),
+            equals(const Timeout(Duration(microseconds: 2))));
+      });
+
+      test('supports non-integer units', () {
+        expect(Timeout.parse('2.73d'),
+            equals(Timeout(const Duration(days: 1) * 2.73)));
+      });
+
+      test('supports multiple units', () {
+        expect(
+            Timeout.parse('1d 2h3m  4s5ms\t6us'),
+            equals(const Timeout(Duration(
+                days: 1,
+                hours: 2,
+                minutes: 3,
+                seconds: 4,
+                milliseconds: 5,
+                microseconds: 6))));
+      });
+
+      test('rejects invalid input', () {
+        expect(() => Timeout.parse('.d'), throwsFormatException);
+        expect(() => Timeout.parse('d'), throwsFormatException);
+        expect(() => Timeout.parse('ad'), throwsFormatException);
+        expect(() => Timeout.parse('1z'), throwsFormatException);
+        expect(() => Timeout.parse('1u'), throwsFormatException);
+        expect(() => Timeout.parse('1d5x'), throwsFormatException);
+        expect(() => Timeout.parse('1d*5m'), throwsFormatException);
+      });
+    });
+  });
+}
diff --git a/pkgs/test_api/test/import_restrictions_test.dart b/pkgs/test_api/test/import_restrictions_test.dart
new file mode 100644
index 0000000..3fd9ffd
--- /dev/null
+++ b/pkgs/test_api/test/import_restrictions_test.dart
@@ -0,0 +1,126 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+import 'dart:isolate';
+
+import 'package:analyzer/dart/analysis/analysis_context.dart';
+import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
+import 'package:analyzer/dart/analysis/results.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:glob/glob.dart';
+import 'package:glob/list_local_fs.dart';
+import 'package:graphs/graphs.dart';
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+void main() {
+  late _ImportCheck importCheck;
+  setUpAll(() async {
+    importCheck = await _ImportCheck.create();
+  });
+  group('backend', () {
+    test('must not import from other subdirectories', () async {
+      final entryPoints = [
+        _testApiLibrary('backend.dart'),
+        ...await _ImportCheck.findEntrypointsUnder(
+            _testApiLibrary('src/backend'))
+      ];
+      await for (final source
+          in importCheck.transitiveSamePackageSources(entryPoints)) {
+        for (final import in source.imports) {
+          expect(import.pathSegments.skip(1).take(2), ['src', 'backend'],
+              reason: 'Invalid import from ${source.uri} : $import');
+        }
+      }
+    });
+  });
+}
+
+Uri _testApiLibrary(String path) => Uri.parse('package:test_api/$path');
+
+class _ImportCheck {
+  final AnalysisContext _context;
+
+  static Future<Iterable<Uri>> findEntrypointsUnder(Uri uri) async {
+    if (!uri.path.endsWith('/')) {
+      uri = uri.replace(path: '${uri.path}/');
+    }
+    final directory = p.fromUri(await Isolate.resolvePackageUri(uri));
+    return Glob('./**')
+        .listSync(root: directory)
+        .whereType<File>()
+        .map((f) => uri.resolve(p.url.relative(f.path, from: directory)));
+  }
+
+  static Future<_ImportCheck> create() async {
+    final context = await _createAnalysisContext();
+    return _ImportCheck._(context);
+  }
+
+  static Future<AnalysisContext> _createAnalysisContext() async {
+    final libUri = Uri.parse('package:graphs/');
+    final libPath = await _pathForUri(libUri);
+    final packagePath = p.dirname(libPath);
+
+    final contexts =
+        AnalysisContextCollection(includedPaths: [packagePath]).contexts;
+    if (contexts.length != 1) {
+      throw StateError('Expected to find exactly one context, got $contexts');
+    }
+    return contexts.first;
+  }
+
+  static Future<String> _pathForUri(Uri uri) async {
+    final fileUri = await Isolate.resolvePackageUri(uri);
+    if (fileUri == null || !fileUri.isScheme('file')) {
+      throw StateError('Expected to resolve $uri to a file URI, got $fileUri');
+    }
+    return p.fromUri(fileUri);
+  }
+
+  _ImportCheck._(this._context);
+
+  Stream<_Source> transitiveSamePackageSources(Iterable<Uri> entryPoints) {
+    assert(entryPoints.every((e) => e.scheme == 'package'));
+    final package = entryPoints.first.pathSegments.first;
+    assert(entryPoints.skip(1).every((e) => e.pathSegments.first == package));
+    return crawlAsync<Uri, _Source>(
+        entryPoints,
+        (uri) async => _Source(uri, await _findImports(uri, package)),
+        (_, source) => source.imports);
+  }
+
+  Future<Set<Uri>> _findImports(Uri uri, String restrictToPackage) async {
+    var path = await _pathForUri(uri);
+    var analysisSession = _context.currentSession;
+    var parseResult = analysisSession.getParsedUnit(path) as ParsedUnitResult;
+    assert(parseResult.content.isNotEmpty,
+        'Tried to read an invalid library $uri');
+    return parseResult.unit.directives
+        .whereType<UriBasedDirective>()
+        .map((d) => d.uri.stringValue!)
+        .where((uri) => !uri.startsWith('dart:'))
+        .map((import) => _resolveImport(import, uri))
+        .where((import) => import.pathSegments.first == restrictToPackage)
+        .toSet();
+  }
+
+  static Uri _resolveImport(String import, Uri from) {
+    if (import.startsWith('package:')) return Uri.parse(import);
+    assert(from.scheme == 'package');
+    final package = from.pathSegments.first;
+    final fromPath = p.joinAll(from.pathSegments.skip(1));
+    final path = p.normalize(p.join(p.dirname(fromPath), import));
+    return Uri.parse('package:${p.join(package, path)}');
+  }
+}
+
+class _Source {
+  final Uri uri;
+  final Set<Uri> imports;
+
+  _Source(this.uri, this.imports);
+}
diff --git a/pkgs/test_api/test/utils.dart b/pkgs/test_api/test/utils.dart
new file mode 100644
index 0000000..97058e3
--- /dev/null
+++ b/pkgs/test_api/test/utils.dart
@@ -0,0 +1,146 @@
+// 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.
+
+import 'dart:collection';
+
+import 'package:test/test.dart';
+import 'package:test_api/src/backend/declarer.dart';
+import 'package:test_api/src/backend/group_entry.dart';
+import 'package:test_api/src/backend/live_test.dart';
+import 'package:test_api/src/backend/runtime.dart';
+import 'package:test_api/src/backend/state.dart';
+import 'package:test_api/src/backend/suite_platform.dart';
+import 'package:test_core/src/runner/engine.dart';
+import 'package:test_core/src/runner/plugin/environment.dart';
+import 'package:test_core/src/runner/runner_suite.dart';
+import 'package:test_core/src/runner/suite.dart';
+
+/// A dummy suite platform to use for testing suites.
+final suitePlatform =
+    SuitePlatform(Runtime.vm, compiler: Runtime.vm.defaultCompiler);
+
+// The last state change detected via [expectStates].
+State? lastState;
+
+/// Asserts that exactly [states] will be emitted via [liveTest.onStateChange].
+///
+/// The most recent emitted state is stored in [_lastState].
+void expectStates(LiveTest liveTest, Iterable<State> statesIter) {
+  var states = Queue.of(statesIter);
+  liveTest.onStateChange.listen(expectAsync1((state) {
+    lastState = state;
+    expect(state, equals(states.removeFirst()));
+  }, count: states.length, max: states.length));
+}
+
+/// Asserts that errors will be emitted via [liveTest.onError] that match
+/// [validators], in order.
+void expectErrors(
+    LiveTest liveTest, Iterable<void Function(Object)> validatorsIter) {
+  var validators = Queue.of(validatorsIter);
+  liveTest.onError.listen(expectAsync1((error) {
+    validators.removeFirst()(error.error);
+  }, count: validators.length, max: validators.length));
+}
+
+/// Asserts that [liveTest] will have a single failure with message `"oh no"`.
+void expectSingleFailure(LiveTest liveTest) {
+  expectStates(liveTest, [
+    const State(Status.running, Result.success),
+    const State(Status.complete, Result.failure)
+  ]);
+
+  expectErrors(liveTest, [
+    (error) {
+      expect(lastState?.status, equals(Status.complete));
+      expect(error, isTestFailure('oh no'));
+    }
+  ]);
+}
+
+/// Asserts that [liveTest] will have a single error, the string `"oh no"`.
+void expectSingleError(LiveTest liveTest) {
+  expectStates(liveTest, [
+    const State(Status.running, Result.success),
+    const State(Status.complete, Result.error)
+  ]);
+
+  expectErrors(liveTest, [
+    (error) {
+      expect(lastState?.status, equals(Status.complete));
+      expect(error, equals('oh no'));
+    }
+  ]);
+}
+
+/// Returns a matcher that matches a [TestFailure] with the given [message].
+///
+/// [message] can be a string or a [Matcher].
+Matcher isTestFailure(Object message) => const TypeMatcher<TestFailure>()
+    .having((e) => e.message, 'message', message);
+
+/// Asserts that [liveTest] has completed and passed.
+///
+/// If the test had any errors, they're surfaced nicely into the outer test.
+void expectTestPassed(LiveTest liveTest) {
+  // Since the test is expected to pass, we forward any current or future errors
+  // to the outer test, because they're definitely unexpected.
+  for (var error in liveTest.errors) {
+    registerException(error.error, error.stackTrace);
+  }
+  liveTest.onError.listen((error) {
+    registerException(error.error, error.stackTrace);
+  });
+
+  expect(liveTest.state.status, equals(Status.complete));
+  expect(liveTest.state.result, equals(Result.success));
+}
+
+/// Asserts that [liveTest] failed with a single [TestFailure] whose message
+/// matches [message].
+void expectTestFailed(LiveTest liveTest, Object message) {
+  expect(liveTest.state.status, equals(Status.complete));
+  expect(liveTest.state.result, equals(Result.failure));
+  expect(liveTest.errors, hasLength(1));
+  expect(liveTest.errors.first.error, isTestFailure(message));
+}
+
+/// Runs [body] with a declarer, runs all the declared tests, and asserts that
+/// they pass.
+///
+/// This is typically used to run multiple tests where later tests make
+/// assertions about the results of previous ones.
+Future expectTestsPass(void Function() body) async {
+  var engine = declareEngine(body);
+  var success = await engine.run();
+
+  for (var test in engine.liveTests) {
+    expectTestPassed(test);
+  }
+
+  expect(success, isTrue);
+}
+
+/// Runs [body] with a declarer and returns the declared entries.
+List<GroupEntry> declare(
+  void Function() body, {
+  // TODO: Change the default https://github.com/dart-lang/test/issues/1571
+  bool allowDuplicateTestNames = true,
+}) {
+  var declarer = Declarer(allowDuplicateTestNames: allowDuplicateTestNames)
+    ..declare(body);
+  return declarer.build().entries;
+}
+
+/// Runs [body] with a declarer and returns an engine that runs those tests.
+Engine declareEngine(void Function() body, {bool runSkipped = false}) {
+  var declarer = Declarer()..declare(body);
+  return Engine.withSuites([
+    RunnerSuite(
+        const PluginEnvironment(),
+        SuiteConfiguration.runSkipped(runSkipped),
+        declarer.build(),
+        suitePlatform)
+  ]);
+}
diff --git a/pkgs/test_core/CHANGELOG.md b/pkgs/test_core/CHANGELOG.md
new file mode 100644
index 0000000..4c305bc
--- /dev/null
+++ b/pkgs/test_core/CHANGELOG.md
@@ -0,0 +1,668 @@
+## 0.6.8
+
+* Fix hang when running multiple precompiled browser tests.
+
+## 0.6.7
+
+* Update the `package:vm_service` constraint to allow version `15.x`.
+
+## 0.6.6
+
+* Allow `analyzer: '>=6.0.0 <8.0.0'`
+* Fix dart2wasm tests on windows.
+* Increase SDK constraint to ^3.5.0.
+* Allow passing additional arguments to `dart compile wasm`.
+
+## 0.6.5
+
+* Increase SDK constraint to ^3.4.0.
+* Ensure we don't create files ending in a `.`, this breaks windows.
+
+## 0.6.4
+
+* Enable asserts for `dart2wasm` tests.
+
+## 0.6.3
+
+* Update min SDK constraint to 3.2.0.
+* Fix testing with `dart2wasm` - use `dart compile wasm` instead of depending on
+  SDK internals
+
+## 0.6.2
+
+* Add `@doNotSubmit` to more declarations of the `solo` parameter.
+
+## 0.6.1
+
+* Handle missing package configs.
+* Document the silent reporter in CLI help output.
+* Support enabling experiments with the dart2wasm compiler.
+
+## 0.6.0
+
+* Handle paths with leading `/` when spawning test isolates.
+* Added `dart2wasm` as a supported compiler for the `chrome` runtime.
+* **BREAKING**: Removed the `experimentalChromeWasm` runtime.
+* **BREAKING**: Removed `Runtime.isJS` and `Runtime.isWasm`, as this is now
+  based on the compiler and not the runtime.
+* **BREAKING**: Removed `Configuration.pubServeUrl` and support for it.
+* Fix running of tests defined under `lib/` with relative imports to other
+  libraries in the package.
+* Update the `package:frontend_server_client` constraint to allow version
+  `4.0.0`.
+* Update the `package:vm_service` constraint to allow version `14.x`.
+
+## 0.5.9
+
+* Update the vm_service constraint to allow version `13.x`.
+
+## 0.5.8
+
+* Move scaffolding definitions to a non-deprecated library.
+* Allow omitting the `Compiler` argument to `currentPlatform`.
+
+## 0.5.7
+
+* Pass --disable-program-split to dart2js to fix tests which use deferred
+  loading.
+* Add a 'silent' reporter option. Keep it hidden in the CLI args help since it
+  is not useful in the general case, but can be useful for tests of the test
+  runner.
+* Update to `package:vm_service` `12.0.0`
+
+## 0.5.6
+
+* Add support for discontinuing after the first failing test with `--fail-fast`.
+
+## 0.5.5
+
+* Change "compiling <path>" to "loading <path>" message in all cases. Surface
+  the "loading" messages in the situations where previously only "compiling"
+  message would be used.
+
+## 0.5.4
+
+* Drop support for null unsafe Dart, bump SDK constraint to `3.0.0`.
+* Add `final` modifier on some implementation classes: `Configuration`,
+  `CustomRuntime`,`RuntimeSettings`, `SuiteConfiguration`.
+* Fix the `root_` fields in the JSON reporter when running a test on Windows
+  with an absolute path.
+* Allow the latest analyzer (6.x.x).
+
+## 0.5.3
+
+* Fix compatibility with wasm number semantics.
+
+## 0.5.2
+
+* Use the version `0.5.2` of `packge:test_api`.
+
+## 0.5.1
+
+* Start adding experimental support for native_assets.yaml, when
+  `--enable-experiment=native_assets` is passed.
+
+## 0.5.0
+
+* Support the `--compiler` flag, which can be used to configure which compiler
+  to use.
+  * To specify a compiler by platform, the argument supports platform selectors
+    through this syntax `[<platform>:]<compiler>`. For example the command line
+    argument `--compiler vm:source` would run all vm tests from source instead
+    of compiling to kernel first.
+  * If no given compiler is compatible for a platform, it will use its default
+    compiler instead.
+* Add support for `-c exe` (the native executable compiler) to the vm platform.
+* Add `Compiler` class, exposed through `backend.dart`.
+* Support compiler identifiers in platform selectors.
+* List the supported compilers for each platform in the usage text.
+* Update all reporters to print the compiler along with the platform name
+  when configured to print the platform. Extend the logic for printing platofrm
+  information to do so if any compilers are explicitly configured.
+* Deprecate `--use-data-isolate-strategy`. It is now an alias for `-c vm:source`
+  which is roughly equivalent. If this is breaking for you please file an issue.
+* **BREAKING** Add required `defaultCompiler` and `supportedCompilers` fields
+  to `Runtime`.
+* **BREAKING** Add required `compiler` field to `SuitePlatform`.
+* **BREAKING** Add required `compilerSelections` argument to some
+  `Configuration` and `SuiteConfiguration` constructors.
+* **BREAKING** Custom platform plugins need to respect the compiler option
+  given through the `SuitePlatform` argument to `PlatformPlugin.load`. This is
+  not statically breaking but it will be confusing for users if it isn't
+  supported.
+* **BREAKING** Remove `useDataIsolateStrategy` field from `Configuration`.
+* **BREAKING** Stop exporting APIs from `package:matcher/expect.dart`.
+
+## 0.4.24
+
+* Fix running paths by absolute path (with drive letter) on windows.
+
+## 0.4.23
+
+* Avoid empty expandable groups for tests without extra output in Github
+  reporter.
+* Support running tests by absolute file uri.
+* Update `vm_service` constraint to `>=6.0.0 <12.0.0`.
+
+## 0.4.22
+
+* Don't run `tearDown` until the test body and outstanding work is complete,
+  even if the test has already failed.
+* Update `vm_service` constraint to `>=6.0.0 <11.0.0`.
+
+## 0.4.21
+
+* Move `includeTags` and `excludeTags` from `SuiteConfiguration` to
+  `Configuration`.
+* Merge command lines args repeating the same test path to run the suite one
+  time with all the test cases across the different arguments.
+* Fix VM tests which run after some test has changed the working directory.
+  There are still issues with browser tests after changing directory.
+
+## 0.4.20
+
+* Fix an issue with the github reporter where tests that fail asynchronously
+  after they've completed would show up as succeeded tests.
+* Support the latest `package:test_api`.
+* Refactor `CompilerPool` to be abstract, add wasm compiler pool.
+
+## 0.4.19
+
+* Support `package:matcher` version `0.12.13`.
+* Require Dart SDK version 2.18.
+
+## 0.4.18
+
+* Support the latest `package:test_api`.
+* Support the latest `package:analyzer`.
+
+## 0.4.17
+
+* Support the latest `package:test_api`.
+* Support the latest `package:frontend_server_client`.
+
+## 0.4.16
+
+* Make the labels for test loading more readable in the compact and expanded
+  reporters, use gray instead of black.
+* Print a command to re-run the failed test after each failure in the compact
+  reporter.
+* Fix the package config path used when running pre-compiled vm tests.
+
+## 0.4.15
+
+* Support the latest `package:test_api`.
+
+## 0.4.14
+
+* Update the github reporter to output the platform in the test names when
+  multiple platforms are used.
+* Fix `spawnHybridUri` support for `package:` uris.
+
+## 0.4.13
+
+* Re-publish changes from 0.4.12.
+* Stop relying on setUpAllName and tearDownAllName constants from test_api.
+
+## 0.4.12 (retracted)
+
+* Remove wait for VM platform isolate exits.
+* Drop `dart2jsPath` configuration support.
+* Allow loading tests under a path with the directory named `packages`.
+* Require analyzer version `3.3.0`, and allow version `4.x`.
+
+## 0.4.11
+
+* Update `vm_service` constraint to `>=6.0.0 <9.0.0`.
+
+## 0.4.10
+
+* Update `analyzer` constraint to `>=2.14.0 <3.0.0`.
+* Add an `--ignore-timeouts` command line flag, which disables all timeouts
+  for all tests.
+* Experimental: Add a VM service extension `ext.test.pauseAfterTests` which
+  configures VM platform tests to pause for debugging after tests are run,
+  before the test isolates are killed.
+
+## 0.4.9
+
+* Wait for paused VM platform isolates before shutdown.
+
+## 0.4.8
+
+* Add logging about enabling stack trace chaining to the compact and expanded
+  reporters (moved from the invoker). This will now only be logged once after
+  all tests have ran.
+
+## 0.4.7
+
+* Fix parsing of file paths into a URI on windows.
+
+## 0.4.6
+
+* Support query parameters `name`, `full-name`, `line`, and `col` on test paths,
+  which will apply the filters to only those test suites.
+  * All specified filters must match for a test to run.
+  * Global filters (ie: `--name`) are also still respected and must match.
+  * The `line` and `col` will match if any frame from the test trace matches
+    (the test trace is the current stack trace where `test` is invoked).
+* Support the latest `test_api`.
+
+## 0.4.5
+
+* Use newer analyzer APIs.
+
+## 0.4.4
+
+* Support the latest `test_api`.
+
+## 0.4.3
+
+* Add an option to disallow duplicate test or group names in `directRunTests`.
+* Add configuration to disallow duplicate test and group names by default. See
+  the [docs][allow_duplicate_test_names] for more information.
+* Remove dependency on pedantic.
+
+[allow_duplicate_test_names]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#allow_duplicate_test_names
+
+## 0.4.2
+
+* Re-use the cached dill file from previous runs on subsequent runs.
+
+## 0.4.1
+
+* Use the latest `package:matcher`.
+
+## 0.4.0
+
+* **BREAKING**: All parameters to the `SuiteConfiguration` and `Configuration`
+  constructors are now required. Some specialized constructors have been added
+  for the common cases where a subset are intended to be provided.
+* **BREAKING**: Remove support for `FORCE_TEST_EXIT`.
+* Report incomplete tests as errors in the JSON reporter when the run is
+  canceled early.
+* Don't log the --test-randomization-ordering-seed if using the json reporter.
+* Add a new exit code, 79, which is used when no tests were ran.
+  * Previously you would have gotten either exit code 1 or 65 (65 if you had
+    provided a test name regex).
+* When no tests were ran but tags were provided, list the tag configuration.
+* Update `analyzer` constraint to `>=1.0.0 <3.0.0`.
+
+## 0.3.29
+
+* Fix a bug where a tag level configuration would cause test suites with that
+  tag to ignore the `--test-randomize-ordering-seed` argument.
+
+## 0.3.28
+
+* Add `time` field to the json reporters `allSuites` event type so that all
+  event types can be unified.
+
+## 0.3.27
+
+* Restore the `Configuration.loadFromString` constructor.
+
+## 0.3.26
+
+* Give a better error when `printOnFailure` is called from outside a test
+  zone.
+
+## 0.3.25
+
+* Support the latest vm_service release (`7.0.0`).
+
+## 0.3.24
+
+* Fix race condition between compilation of vm tests and the running of
+  isolates.
+
+## 0.3.23
+
+* Forward experiment args from the runner executable to the compiler with the
+  new vm test loading strategy.
+
+## 0.3.22
+
+* Fix a windows issue with the new loading strategy.
+
+## 0.3.21
+
+* Fix an issue where you couldn't have tests compiled in both sound and
+  unsound null safety modes.
+
+## 0.3.20
+
+* Add library `scaffolding.dart` to allow importing a subset of the normal
+  surface area.
+* Remove `suiteChannel`. This is now handled by an additional argument to the
+  `beforeLoad` callback in `serializeSuite`.
+* Disable stack trace chaining by default.
+* Change the default way VM tests are launched and ran to greatly speed up
+  loading performance.
+  * You can force the old strategy with `--use-data-isolate-strategy` flag if
+    you run into issues, but please also file a bug.
+* Improve the error message for `hybridMain` functions with an incompatible
+  StreamChannel parameter type.
+* Change the `message` argument to `PlatformPlugin.load` to `Map<String,
+  Object?>`. In an upcoming release this will be required as the type for this
+  argument when passed through to `deserializeSuite`.
+
+## 0.3.19
+
+* ~~Disable stack trace chaining by default.~~
+
+## 0.3.18
+
+* Update `spawnHybridCode` to default to the current packages language version.
+* Update to the latest `test_api`.
+
+## 0.3.17
+
+* Complete the null safety migration.
+
+## 0.3.16
+
+* Allow package:io version 1.0.0.
+
+## 0.3.14
+
+* Handle issue closing `stdin` during shutdown.
+
+## 0.3.13
+
+* Allow the latest analyzer `1.0.0`.
+
+## 0.3.12
+
+* Stable null safety release.
+
+## 0.3.12-nullsafety.17
+
+* Use the `test_api` for stable null safety.
+
+## 0.3.12-nullsafety.16
+
+* Expand upper bound constraints for some null safe migrated packages.
+
+## 0.3.12-nullsafety.15
+
+* Support the latest vm_service release (`6.x.x`).
+
+## 0.3.12-nullsafety.14
+
+* Support the latest coverage release (`0.15.x`).
+
+## 0.3.12-nullsafety.13
+
+* Allow the latest args release (`2.x`).
+
+## 0.3.12-nullsafety.12
+
+* Allow the latest glob release (`2.x`).
+
+## 0.3.12-nullsafety.11
+
+* Fix `spawnHybridUri` on windows.
+* Allow `package:yaml` version `3.x.x`.
+
+## 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+4 (Backport)
+
+* Fix `spawnHybridUri` on windows.
+
+## 0.3.11+3 (Backport)
+
+* Support `package:analyzer` version `0.41.x`.
+
+## 0.3.11+2 (Backport)
+
+* Fix `spawnHybridUri` to respect language versioning of the spawned uri.
+
+## 0.3.11+1
+
+* Allow analyzer 0.40.x.
+
+## 0.3.11
+
+* Update to `matcher` version `0.12.9`.
+
+## 0.3.10
+
+* Prepare for `unawaited` from `package:meta`.
+
+## 0.3.9
+
+* Ignore a null `RunnerSuite` rather than throw an error.
+
+## 0.3.8
+
+* 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.7
+
+* Support the latest `package:coverage`.
+
+## 0.3.6
+
+* Expose the `Configuration` class and related classes through `backend.dart`.
+
+## 0.3.5
+
+* 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.
+
+## 0.3.3
+
+* Support latest `package:vm_service`.
+
+## 0.3.2
+
+* Drop the `package_resolver` dependency.
+
+## 0.3.1
+
+* Support latest `package:vm_service`.
+* Enable asserts in code running through `spawnHybrid` APIs.
+* Exit with a non-zero code if no tests were ran, whether due to skips or having
+  no tests defined.
+
+## 0.3.0
+
+* Bump minimum SDK to `2.4.0` for safer usage of for-loop elements.
+* Deprecate `PhantomJS` and provide warning when used. Support for `PhantomJS`
+  will be removed in version `2.0.0`.
+* Differentiate between test-randomize-ordering-seed not set and 0 being chosen
+  as the random seed.
+* `deserializeSuite` now takes an optional `gatherCoverage` callback.
+* Support retrying of entire test suites when they fail to load.
+* Fix the `compiling` message in precompiled mode so it says `loading` instead,
+  which is more accurate.
+* Change the behavior of the concurrency setting so that loading and running
+  don't have separate pools.
+  * The loading and running of a test are now done with the same resource, and
+    the concurrency setting uniformly affects each. With `-j1` only a single
+    test will ever be loaded at a time.
+  * Previously the loading pool was 2x larger than the actual concurrency
+    setting which could cause flaky tests due to tests being loaded while
+    other tests were running, even with `-j1`.
+* Avoid printing uncaught errors within `spawnHybridUri`.
+
+## 0.2.18
+
+* Allow `test_api` `0.2.13` to work around a bug in the SDK version `2.3.0`.
+
+## 0.2.17
+
+* Add `file_reporters` configuration option and `--file-reporter` CLI option to
+  allow specifying a separate reporter that writes to a file.
+
+## 0.2.16
+
+* Internal cleanup.
+* Add `customHtmlTemplateFile` configuration option to allow sharing an
+  html template between tests
+* Depend on the latest `test_api`.
+
+## 0.2.15
+
+* Add a `StringSink` argument to reporters to prepare for reporting to a file.
+* Add --test-randomize-ordering-seed` argument to randomize test
+execution order based on a provided seed
+* Depend on the latest `test_api`.
+
+## 0.2.14
+
+* Support the latest `package:analyzer`.
+* Update to latest `package:matcher`. Improves output for instances of private
+  classes.
+
+## 0.2.13
+
+* Depend on the latest `package:test_api`.
+
+## 0.2.12
+
+* Conditionally import coverage logic in `engine.dart`. This ensures the engine
+  is platform agnostic.
+
+## 0.2.11
+
+* Implement code coverage gathering for VM tests.
+
+## 0.2.10
+
+* Add a `--debug` argument for running the VM/Chrome in debug mode.
+
+## 0.2.9+2
+
+* Depend on the latest `test_api`.
+
+## 0.2.9+1
+
+* Allow the latest `package:vm_service`.
+
+## 0.2.9
+
+* Mark `package:test_core` as deprecated to prevent accidental use.
+* Depend on the latest `test_api`.
+
+## 0.2.8
+
+* Depend on `vm_service` instead of `vm_service_lib`.
+* Drop dependency on `pub_semver`.
+* Allow `analyzer` version `0.38.x`.
+
+## 0.2.7
+
+* Depend on `vm_service_lib` instead of `vm_service_client`.
+* Depend on latest `package:analyzer`.
+
+## 0.2.6
+
+* Internal cleanup - fix lints.
+* Use the latest `test_api`.
+
+## 0.2.5
+
+* Fix an issue where non-completed tests were considered passing.
+* Updated `compact` and `expanded` reporters to display non-completed tests.
+
+## 0.2.4
+
+* Avoid `dart:isolate` imports on code loaded in tests.
+* Expose the `parseMetadata` function publicly through a new `backend.dart`
+  import, as well as re-exporting `package:test_api/backend.dart`.
+
+## 0.2.3
+
+* Switch import for `IsolateChannel` for forwards compatibility with `2.0.0`.
+
+## 0.2.2
+
+* Allow `analyzer` version `0.36.x`.
+* Update to matcher version `0.12.5`.
+
+## 0.2.1+1
+
+* Allow `analyzer` version `0.35.x`.
+
+## 0.2.1
+
+* Require Dart SDK `>=2.1.0`.
+* Require latest `test_api`.
+
+## 0.2.0
+
+* Remove `remote_listener.dart` and `suite_channel_manager.dart` from runner
+  and depend on them from `test_api`.
+
+## 0.1.0
+
+* Initial release of `test_core`. Provides the basic API for writing and running
+  tests on the VM.
diff --git a/pkgs/test_core/LICENSE b/pkgs/test_core/LICENSE
new file mode 100644
index 0000000..9972f6e
--- /dev/null
+++ b/pkgs/test_core/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2018, the Dart project authors. 
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google LLC nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/pkgs/test_core/README.md b/pkgs/test_core/README.md
new file mode 100644
index 0000000..67f21a8
--- /dev/null
+++ b/pkgs/test_core/README.md
@@ -0,0 +1,11 @@
+[![pub package](https://img.shields.io/pub/v/test_core.svg)](https://pub.dev/packages/test_core)
+[![package publisher](https://img.shields.io/pub/publisher/test_core.svg)](https://pub.dev/packages/test_core/publisher)
+
+A minimal package for writing and running tests as well as extensions for
+implementing a custom test runner.
+
+If you're interested in testing Dart code, you likely want to use
+[package:test](https://pub.dev/packages/test).
+
+At this time this package is not intended to be used publicly. The API is not
+yet stable.
diff --git a/pkgs/test_core/lib/backend.dart b/pkgs/test_core/lib/backend.dart
new file mode 100644
index 0000000..f5a3b46
--- /dev/null
+++ b/pkgs/test_core/lib/backend.dart
@@ -0,0 +1,16 @@
+// 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.
+
+@Deprecated('package:test_core is not intended for general use. '
+    'Please use package:test.')
+library;
+
+export 'package:test_api/backend.dart'
+    show Compiler, Metadata, PlatformSelector, Runtime, SuitePlatform;
+
+export 'src/runner/configuration.dart' show Configuration;
+export 'src/runner/configuration/custom_runtime.dart' show CustomRuntime;
+export 'src/runner/configuration/runtime_settings.dart' show RuntimeSettings;
+export 'src/runner/parse_metadata.dart' show parseMetadata;
+export 'src/runner/suite.dart' show SuiteConfiguration;
diff --git a/pkgs/test_core/lib/scaffolding.dart b/pkgs/test_core/lib/scaffolding.dart
new file mode 100644
index 0000000..90304d2
--- /dev/null
+++ b/pkgs/test_core/lib/scaffolding.dart
@@ -0,0 +1,9 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@Deprecated('package:test_core is not intended for general use. '
+    'Please use package:test.')
+library;
+
+export 'src/scaffolding.dart';
diff --git a/pkgs/test_core/lib/src/bootstrap/vm.dart b/pkgs/test_core/lib/src/bootstrap/vm.dart
new file mode 100644
index 0000000..05604f1
--- /dev/null
+++ b/pkgs/test_core/lib/src/bootstrap/vm.dart
@@ -0,0 +1,50 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:developer';
+import 'dart:io';
+import 'dart:isolate';
+
+import 'package:stream_channel/isolate_channel.dart';
+import 'package:stream_channel/stream_channel.dart';
+
+import '../runner/plugin/remote_platform_helpers.dart';
+import '../runner/plugin/shared_platform_helpers.dart';
+
+/// Bootstraps a vm test to communicate with the test runner over an isolate.
+void internalBootstrapVmTest(Function Function() getMain, SendPort sendPort) {
+  var platformChannel =
+      MultiChannel<Object?>(IsolateChannel<Object?>.connectSend(sendPort));
+  var testControlChannel = platformChannel.virtualChannel()
+    ..pipe(serializeSuite(getMain));
+  platformChannel.sink.add(testControlChannel.id);
+
+  platformChannel.stream.forEach((message) {
+    assert(message == 'debug');
+    debugger(message: 'Paused by test runner');
+    platformChannel.sink.add('done');
+  });
+}
+
+/// Bootstraps a native executable test to communicate with the test runner over
+/// a socket.
+void internalBootstrapNativeTest(
+    Function Function() getMain, List<String> args) async {
+  if (args.length != 2) {
+    throw StateError(
+        'Expected exactly two args, a host and a port, but got $args');
+  }
+  var socket = await Socket.connect(args[0], int.parse(args[1]));
+  var platformChannel = MultiChannel<Object?>(jsonSocketStreamChannel(socket));
+  var testControlChannel = platformChannel.virtualChannel()
+    ..pipe(serializeSuite(getMain));
+  platformChannel.sink.add(testControlChannel.id);
+
+  unawaited(platformChannel.stream.forEach((message) {
+    assert(message == 'debug');
+    debugger(message: 'Paused by test runner');
+    platformChannel.sink.add('done');
+  }));
+}
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..c574257
--- /dev/null
+++ b/pkgs/test_core/lib/src/direct_run.dart
@@ -0,0 +1,134 @@
+// 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:async';
+import 'dart:collection';
+
+import 'package:path/path.dart' as p;
+import 'package:test_api/backend.dart';
+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 '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/os.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,
+        // TODO: Change the default https://github.com/dart-lang/test/issues/1571
+        bool allowDuplicateTestNames = true}) =>
+    _directRunTests(testMain,
+        reporterFactory: reporterFactory,
+        allowDuplicateTestNames: allowDuplicateTestNames);
+
+/// 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,
+        allowDuplicateTestNames: false);
+
+Future<bool> _directRunTests(FutureOr<void> Function() testMain,
+    {Reporter Function(Engine)? reporterFactory,
+    String? fullTestName,
+    required bool allowDuplicateTestNames}) async {
+  reporterFactory ??= (engine) => ExpandedReporter.watch(engine, PrintSink(),
+      color: Configuration.empty.color, printPath: false, printPlatform: false);
+  final declarer = Declarer(
+      fullTestName: fullTestName,
+      allowDuplicateTestNames: allowDuplicateTestNames);
+  await declarer.declare(testMain);
+
+  final suite = RunnerSuite(
+      const PluginEnvironment(),
+      SuiteConfiguration.empty,
+      declarer.build(),
+      SuitePlatform(Runtime.vm, compiler: null, 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}) ??
+      false;
+
+  if (fullTestName != null) {
+    final testCount = engine.liveTests.length;
+    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 unskippedTestNames = <String>{};
+  while (toVisit.isNotEmpty) {
+    final current = toVisit.removeLast();
+    if (current is Group) {
+      toVisit.addAll(current.entries.reversed);
+    } else if (current is Test) {
+      if (current.metadata.skip) continue;
+      unskippedTestNames.add(current.name);
+    } else {
+      throw StateError('Unhandled Group Entry: ${current.runtimeType}');
+    }
+  }
+  return unskippedTestNames;
+}
+
+/// 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
new file mode 100644
index 0000000..eeefd16
--- /dev/null
+++ b/pkgs/test_core/lib/src/executable.dart
@@ -0,0 +1,196 @@
+// 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.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:async/async.dart';
+import 'package:path/path.dart' as p;
+import 'package:source_span/source_span.dart';
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test_api/src/backend/util/pretty_print.dart'; // ignore: implementation_imports
+
+import 'runner.dart';
+import 'runner/application_exception.dart';
+import 'runner/configuration.dart';
+import 'runner/no_tests_found_exception.dart';
+import 'runner/version.dart';
+import 'util/errors.dart';
+import 'util/exit_codes.dart' as exit_codes;
+import 'util/io.dart';
+
+StreamSubscription? signalSubscription;
+bool isShutdown = false;
+
+/// Returns the path to the global test configuration file.
+final String _globalConfigPath = () {
+  if (Platform.environment.containsKey('DART_TEST_CONFIG')) {
+    return Platform.environment['DART_TEST_CONFIG']!;
+  } else if (Platform.operatingSystem == 'windows') {
+    return p.join(Platform.environment['LOCALAPPDATA']!, 'DartTest.yaml');
+  } else {
+    return '${Platform.environment['HOME']}/.dart_test.yaml';
+  }
+}();
+
+Future<void> main(List<String> args) async {
+  await _execute(args);
+  completeShutdown();
+}
+
+// ignore: unreachable_from_main
+Future<void> runTests(List<String> args) async {
+  await _execute(args);
+}
+
+void completeShutdown() {
+  if (isShutdown) return;
+  if (signalSubscription != null) {
+    signalSubscription!.cancel();
+    signalSubscription = null;
+  }
+  isShutdown = true;
+  cancelStdinLines();
+}
+
+Future<void> _execute(List<String> args) async {
+  /// A merged stream of all signals that tell the test runner to shut down
+  /// gracefully.
+  ///
+  /// Signals will only be captured as long as this has an active subscription.
+  /// Otherwise, they'll be handled by Dart's default signal handler, which
+  /// terminates the program immediately.
+  final signals = Platform.isWindows
+      ? ProcessSignal.sigint.watch()
+      : Platform.isFuchsia // Signals don't exist on Fuchsia.
+          ? const Stream<Never>.empty()
+          : StreamGroup.merge(
+              [ProcessSignal.sigterm.watch(), ProcessSignal.sigint.watch()]);
+
+  Configuration configuration;
+  try {
+    configuration = Configuration.parse(args);
+  } on FormatException catch (error) {
+    _printUsage(error.message);
+    exitCode = exit_codes.usage;
+    return;
+  }
+
+  if (configuration.help) {
+    _printUsage();
+    return;
+  }
+
+  if (configuration.version) {
+    var version = testVersion;
+    if (version == null) {
+      stderr.writeln("Couldn't find version number.");
+      exitCode = exit_codes.data;
+    } else {
+      print(version);
+    }
+    return;
+  }
+
+  try {
+    var fileConfiguration = Configuration.empty;
+    if (File(_globalConfigPath).existsSync()) {
+      fileConfiguration = fileConfiguration
+          .merge(Configuration.load(_globalConfigPath, global: true));
+    }
+
+    if (File(configuration.configurationPath).existsSync()) {
+      fileConfiguration = fileConfiguration
+          .merge(Configuration.load(configuration.configurationPath));
+    }
+
+    configuration = fileConfiguration.merge(configuration);
+  } on SourceSpanFormatException catch (error) {
+    stderr.writeln(error.toString(color: configuration.color));
+    exitCode = exit_codes.data;
+    return;
+  } on FormatException catch (error) {
+    stderr.writeln(error.message);
+    exitCode = exit_codes.data;
+    return;
+  } on IOException catch (error) {
+    stderr.writeln(error.toString());
+    exitCode = exit_codes.noInput;
+    return;
+  }
+
+  var undefinedPresets = configuration.chosenPresets
+      .where((preset) => !configuration.knownPresets.contains(preset))
+      .toList();
+  if (undefinedPresets.isNotEmpty) {
+    _printUsage("Undefined ${pluralize('preset', undefinedPresets.length)} "
+        "${toSentence(undefinedPresets.map((preset) => '"$preset"'))}.");
+    exitCode = exit_codes.usage;
+    return;
+  }
+
+  if (!configuration.explicitPaths &&
+      !Directory(configuration.testSelections.keys.single).existsSync()) {
+    _printUsage('No test files were passed and the default "test/" '
+        "directory doesn't exist.");
+    exitCode = exit_codes.data;
+    return;
+  }
+
+  Runner? runner;
+
+  signalSubscription ??= signals.listen((signal) async {
+    completeShutdown();
+    await runner?.close();
+  });
+
+  try {
+    runner = Runner(configuration);
+    exitCode = (await runner.run()) ? 0 : 1;
+  } on ApplicationException catch (error) {
+    stderr.writeln(error.message);
+    exitCode = exit_codes.data;
+  } on SourceSpanFormatException catch (error) {
+    stderr.writeln(error.toString(color: configuration.color));
+    exitCode = exit_codes.data;
+  } on FormatException catch (error) {
+    stderr.writeln(error.message);
+    exitCode = exit_codes.data;
+  } on NoTestsFoundException catch (error) {
+    stderr.writeln(error.message);
+    exitCode = exit_codes.noTestsRan;
+  } catch (error, stackTrace) {
+    stderr.writeln(getErrorMessage(error));
+    stderr.writeln(Trace.from(stackTrace).terse);
+    stderr.writeln('This is an unexpected error. Please file an issue at '
+        'http://github.com/dart-lang/test\n'
+        'with the stack trace and instructions for reproducing the error.');
+    exitCode = exit_codes.software;
+  } finally {
+    await runner?.close();
+  }
+
+  return;
+}
+
+/// Print usage information for this command.
+///
+/// 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]) {
+  var output = stdout;
+
+  var message = 'Runs tests in this package.';
+  if (error != null) {
+    message = error;
+    output = stderr;
+  }
+
+  output.write('''${wordWrap(message)}
+
+Usage: dart test [files or directories...]
+
+${Configuration.usage}
+''');
+}
diff --git a/pkgs/test_core/lib/src/platform.dart b/pkgs/test_core/lib/src/platform.dart
new file mode 100644
index 0000000..af457b4
--- /dev/null
+++ b/pkgs/test_core/lib/src/platform.dart
@@ -0,0 +1,16 @@
+// 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.
+
+export 'package:test_api/backend.dart' show Runtime, SuitePlatform;
+export 'package:test_core/src/runner/configuration.dart' show Configuration;
+export 'package:test_core/src/runner/environment.dart'
+    show Environment, PluginEnvironment;
+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
new file mode 100644
index 0000000..ee648d4
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner.dart
@@ -0,0 +1,475 @@
+// 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.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:async/async.dart';
+import 'package:boolean_selector/boolean_selector.dart';
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test_api/backend.dart'
+    show PlatformSelector, Runtime, SuitePlatform;
+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/operating_system.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/test.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/util/pretty_print.dart'; // ignore: implementation_imports
+
+import 'runner/configuration.dart';
+import 'runner/configuration/reporters.dart';
+import 'runner/debugger.dart';
+import 'runner/engine.dart';
+import 'runner/load_exception.dart';
+import 'runner/load_suite.dart';
+import 'runner/loader.dart';
+import 'runner/no_tests_found_exception.dart';
+import 'runner/reporter.dart';
+import 'runner/reporter/compact.dart';
+import 'runner/reporter/expanded.dart';
+import 'runner/reporter/multiplex.dart';
+import 'runner/runner_suite.dart';
+import 'util/io.dart';
+
+/// 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
+/// the other, as well as printing out tests with a [CompactReporter] or an
+/// [ExpandedReporter].
+class Runner {
+  /// The test runner configuration.
+  final _config = Configuration.current;
+
+  /// The loader that loads the test suites from the filesystem.
+  final _loader = Loader();
+
+  /// The engine that runs the test suites.
+  final Engine _engine;
+
+  /// The reporter that's emitting the test runner's results.
+  final Reporter _reporter;
+
+  /// The subscription to the stream returned by [_loadSuites].
+  StreamSubscription? _suiteSubscription;
+
+  /// The set of suite paths for which [_warnForUnknownTags] has already been
+  /// called.
+  ///
+  /// This is used to avoid printing duplicate warnings when a suite is loaded
+  /// on multiple platforms.
+  final _tagWarningSuites = <String>{};
+
+  /// The current debug operation, if any.
+  ///
+  /// This is stored so that we can cancel it when the runner is closed.
+  CancelableOperation? _debugOperation;
+
+  /// The memoizer for ensuring [close] only runs once.
+  final _closeMemo = AsyncMemoizer<void>();
+  bool get _closed => _closeMemo.hasRun;
+
+  /// Sinks created for each file reporter (if there are any).
+  final List<IOSink> _sinks;
+
+  /// Creates a new runner based on [configuration].
+  factory Runner(Configuration config) => config.asCurrent(() {
+        var engine = Engine(
+          concurrency: config.concurrency,
+          coverage: config.coverage,
+          testRandomizeOrderingSeed: config.testRandomizeOrderingSeed,
+          stopOnFirstFailure: config.stopOnFirstFailure,
+        );
+
+        var sinks = <IOSink>[];
+        Reporter createFileReporter(String reporterName, String filepath) {
+          final sink =
+              (File(filepath)..createSync(recursive: true)).openWrite();
+          sinks.add(sink);
+          return allReporters[reporterName]!.factory(config, engine, sink);
+        }
+
+        return Runner._(
+          engine,
+          MultiplexReporter([
+            // Standard reporter.
+            allReporters[config.reporter]!.factory(config, engine, stdout),
+            // File reporters.
+            for (var reporter in config.fileReporters.keys)
+              createFileReporter(reporter, config.fileReporters[reporter]!),
+          ]),
+          sinks,
+        );
+      });
+
+  Runner._(this._engine, this._reporter, this._sinks);
+
+  /// Starts the runner.
+  ///
+  /// This starts running tests and printing their progress. It returns whether
+  /// or not they ran successfully.
+  Future<bool> run() => _config.asCurrent(() async {
+        if (_closed) {
+          throw StateError('run() may not be called on a closed Runner.');
+        }
+
+        _warnForUnsupportedPlatforms();
+
+        var suites = _loadSuites();
+
+        if (_config.coverage != null) {
+          await Directory(_config.coverage!).create(recursive: true);
+        }
+
+        bool? success;
+        if (_config.pauseAfterLoad) {
+          success = await _loadThenPause(suites);
+        } else {
+          var subscription =
+              _suiteSubscription = suites.listen(_engine.suiteSink.add);
+          var results = await Future.wait(<Future>[
+            subscription
+                .asFuture<void>()
+                .then((_) => _engine.suiteSink.close()),
+            _engine.run()
+          ], eagerError: true);
+          success = results.last as bool?;
+        }
+
+        if (_closed) return false;
+
+        if (_engine.passed.isEmpty &&
+            _engine.failed.isEmpty &&
+            _engine.skipped.isEmpty) {
+          if (_config.globalPatterns.isNotEmpty) {
+            var patterns = toSentence(_config.globalPatterns.map((pattern) =>
+                pattern is RegExp
+                    ? 'regular expression "${pattern.pattern}"'
+                    : '"$pattern"'));
+            throw NoTestsFoundException('No tests match $patterns.');
+          } else if (_config.includeTags != BooleanSelector.all ||
+              _config.excludeTags != BooleanSelector.none) {
+            throw NoTestsFoundException(
+                'No tests match the requested tag selectors:\n'
+                '  include: "${_config.includeTags}"\n'
+                '  exclude: "${_config.excludeTags}"');
+          } else {
+            throw NoTestsFoundException('No tests were found.');
+          }
+        }
+
+        return (success ?? false) &&
+            (_engine.passed.isNotEmpty || _engine.skipped.isNotEmpty);
+      });
+
+  /// Emits a warning if the user is trying to run on a platform that's
+  /// unsupported for the entire package.
+  void _warnForUnsupportedPlatforms() {
+    var testOn = _config.suiteDefaults.metadata.testOn;
+    if (testOn == PlatformSelector.all) return;
+
+    var unsupportedRuntimes = _config.suiteDefaults.runtimes
+        .map(_loader.findRuntime)
+        .whereType<Runtime>()
+        .where((runtime) => !testOn.evaluate(currentPlatform(runtime, null)))
+        .toList();
+
+    if (unsupportedRuntimes.isEmpty) return;
+
+    // Human-readable names for all unsupported runtimes.
+    var unsupportedNames = <String>[];
+
+    // If the user tried to run on one or more unsupported browsers, figure out
+    // whether we should warn about the individual browsers or whether all
+    // browsers are unsupported.
+    var unsupportedBrowsers =
+        unsupportedRuntimes.where((platform) => platform.isBrowser);
+    if (unsupportedBrowsers.isNotEmpty) {
+      var supportsAnyBrowser = _loader.allRuntimes
+          .where((runtime) => runtime.isBrowser)
+          .any((runtime) => testOn.evaluate(currentPlatform(runtime, null)));
+
+      if (supportsAnyBrowser) {
+        unsupportedNames
+            .addAll(unsupportedBrowsers.map((runtime) => runtime.name));
+      } else {
+        unsupportedNames.add('browsers');
+      }
+    }
+
+    // If the user tried to run on the VM and it's not supported, figure out if
+    // that's because of the current OS or whether the VM is unsupported.
+    if (unsupportedRuntimes.contains(Runtime.vm)) {
+      var supportsAnyOS = OperatingSystem.all.any((os) => testOn.evaluate(
+          SuitePlatform(Runtime.vm,
+              compiler: null, os: os, inGoogle: inGoogle)));
+
+      if (supportsAnyOS) {
+        unsupportedNames.add(currentOS.name);
+      } else {
+        unsupportedNames.add('the Dart VM');
+      }
+    }
+
+    warn("this package doesn't support running tests on "
+        '${toSentence(unsupportedNames, conjunction: 'or')}.');
+  }
+
+  /// Closes the runner.
+  ///
+  /// This stops any future test suites from running. It will wait for any
+  /// currently-running VM tests, in case they have stuff to clean up on the
+  /// filesystem.
+  Future close() => _closeMemo.runOnce(() async {
+        Timer? timer;
+        if (!_engine.isIdle) {
+          // Wait a bit to print this message, since printing it eagerly looks weird
+          // if the tests then finish immediately.
+          timer = Timer(const Duration(seconds: 1), () {
+            // Pause the reporter while we print to ensure that we don't interfere
+            // with its output.
+            _reporter.pause();
+            print('Waiting for current test(s) to finish.');
+            print('Press Control-C again to terminate immediately.');
+            _reporter.resume();
+          });
+        }
+
+        await _debugOperation?.cancel();
+        await _suiteSubscription?.cancel();
+
+        _suiteSubscription = null;
+
+        // Make sure we close the engine *before* the loader. Otherwise,
+        // LoadSuites provided by the loader may get into bad states.
+        //
+        // We close the loader's browsers while we're closing the engine because
+        // 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()]);
+        timer?.cancel();
+        await _loader.close();
+
+        // Flush any IOSinks created for file reporters.
+        await Future.wait(_sinks.map((s) => s.flush().then((_) => s.close())));
+        _sinks.clear();
+      });
+
+  /// Return a stream of [LoadSuite]s in [_config.testSelections].
+  ///
+  /// Only tests that match [_config.patterns] will be included in the
+  /// suites once they're loaded.
+  Stream<LoadSuite> _loadSuites() {
+    return StreamGroup.merge(_config.testSelections.entries.map((pathEntry) {
+      final testPath = pathEntry.key;
+      final testSelections = pathEntry.value;
+      final suiteConfig = _config.suiteDefaults.selectTests(testSelections);
+      if (Directory(testPath).existsSync()) {
+        return _loader.loadDir(testPath, suiteConfig);
+      } else if (File(testPath).existsSync()) {
+        return _loader.loadFile(testPath, suiteConfig);
+      } else {
+        return Stream.fromIterable([
+          LoadSuite.forLoadException(
+            LoadException(testPath, 'Does not exist.'),
+            suiteConfig,
+          ),
+        ]);
+      }
+    })).map((loadSuite) {
+      return loadSuite.changeSuite((suite) {
+        _warnForUnknownTags(suite);
+
+        return _shardSuite(suite.filter((test) {
+          // Skip any tests that don't match all the global patterns.
+          if (!_config.globalPatterns
+              .every((pattern) => test.name.contains(pattern))) {
+            return false;
+          }
+
+          // If the user provided tags, skip tests that don't match all of them.
+          if (!_config.includeTags.evaluate(test.metadata.tags.contains)) {
+            return false;
+          }
+
+          // Skip tests that do match any tags the user wants to exclude.
+          if (_config.excludeTags.evaluate(test.metadata.tags.contains)) {
+            return false;
+          }
+
+          final testSelections = suite.config.testSelections;
+          assert(testSelections.isNotEmpty, 'Tests should have been selected');
+          return testSelections.any((selection) {
+            // Skip tests that don't match all the suite specific patterns.
+            if (!selection.testPatterns
+                .every((pattern) => test.name.contains(pattern))) {
+              return false;
+            }
+            // Skip tests that don't start on `line` or `col` if specified.
+            var line = selection.line;
+            var col = selection.col;
+            if (line == null && col == null) return true;
+            var trace = test.trace;
+            if (trace == null) {
+              throw StateError(
+                  'Cannot filter by line/column for this test suite, no stack'
+                  'trace available.');
+            }
+            var path = suite.path;
+            if (path == null) {
+              throw StateError(
+                  'Cannot filter by line/column for this test suite, no suite'
+                  'path available.');
+            }
+            // The absolute path as it will appear in stack traces.
+            var absoluteSuitePath = File(path).absolute.uri.toFilePath();
+
+            bool matchLineAndCol(Frame frame) {
+              switch (frame.uri.scheme) {
+                case 'file':
+                  if (frame.uri.toFilePath() != absoluteSuitePath) {
+                    return false;
+                  }
+                case 'package':
+                  // It is unlikely that the test case is specified in a
+                  // package: URI. Ignore this case.
+                  return false;
+                default:
+                  // Now we can assume that the kernel is compiled using
+                  // --filesystem-scheme.
+                  // In this case, because we don't know the --filesystem-root, as
+                  // long as the file path matches we assume it is the same file.
+                  if (!absoluteSuitePath.endsWith(frame.uri.path)) {
+                    return false;
+                  }
+              }
+              if (line != null && frame.line != line) {
+                return false;
+              }
+              if (col != null && frame.column != col) {
+                return false;
+              }
+              return true;
+            }
+
+            return trace.frames.any(matchLineAndCol);
+          });
+        }));
+      });
+    });
+  }
+
+  /// Prints a warning for any unknown tags referenced in [suite] or its
+  /// children.
+  void _warnForUnknownTags(Suite suite) {
+    if (_tagWarningSuites.contains(suite.path)) return;
+    _tagWarningSuites.add(suite.path!);
+
+    var unknownTags = _collectUnknownTags(suite);
+    if (unknownTags.isEmpty) return;
+
+    var yellow = _config.color ? '\u001b[33m' : '';
+    var bold = _config.color ? '\u001b[1m' : '';
+    var noColor = _config.color ? '\u001b[0m' : '';
+
+    var buffer = StringBuffer()
+      ..write('${yellow}Warning:$noColor ')
+      ..write(unknownTags.length == 1 ? 'A tag was ' : 'Tags were ')
+      ..write('used that ')
+      ..write(unknownTags.length == 1 ? "wasn't " : "weren't ")
+      ..writeln('specified in dart_test.yaml.');
+
+    unknownTags.forEach((tag, entries) {
+      buffer.write('  $bold$tag$noColor was used in');
+
+      if (entries.length == 1) {
+        buffer.writeln(' ${_entryDescription(entries.single)}');
+        return;
+      }
+
+      buffer.write(':');
+      for (var entry in entries) {
+        buffer.write('\n    ${_entryDescription(entry)}');
+      }
+      buffer.writeln();
+    });
+
+    print(buffer.toString());
+  }
+
+  /// Collects all tags used by [suite] or its children that aren't also passed
+  /// on the command line.
+  ///
+  /// This returns a map from tag names to lists of entries that use those tags.
+  Map<String, List<GroupEntry>> _collectUnknownTags(Suite suite) {
+    var unknownTags = <String, List<GroupEntry>>{};
+    var currentTags = <String>{};
+
+    void collect(GroupEntry entry) {
+      var newTags = <String>{};
+      for (var unknownTag
+          in entry.metadata.tags.difference(_config.knownTags)) {
+        if (currentTags.contains(unknownTag)) continue;
+        unknownTags.putIfAbsent(unknownTag, () => []).add(entry);
+        newTags.add(unknownTag);
+      }
+
+      if (entry is! Group) return;
+
+      currentTags.addAll(newTags);
+      for (var child in entry.entries) {
+        collect(child);
+      }
+      currentTags.removeAll(newTags);
+    }
+
+    collect(suite.group);
+    return unknownTags;
+  }
+
+  /// 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.isNotEmpty) return 'the group "${entry.name}"';
+    return 'the suite itself';
+  }
+
+  /// If sharding is enabled, filters [suite] to only include the tests that
+  /// should be run in this shard.
+  ///
+  /// We just take a slice of the tests in each suite corresponding to the shard
+  /// index. This makes the tests pretty tests across shards, and since the
+  /// tests are continuous, makes us more likely to be able to re-use
+  /// `setUpAll()` logic.
+  RunnerSuite _shardSuite(RunnerSuite suite) {
+    if (_config.totalShards == null) return suite;
+
+    var shardSize = suite.group.testCount / _config.totalShards!;
+    var shardIndex = _config.shardIndex!;
+    var shardStart = (shardSize * shardIndex).round();
+    var shardEnd = (shardSize * (shardIndex + 1)).round();
+
+    var count = -1;
+    var filtered = suite.filter((test) {
+      count++;
+      return count >= shardStart && count < shardEnd;
+    });
+
+    return filtered;
+  }
+
+  /// Loads each suite in [suites] in order, pausing after load for runtimes
+  /// that support debugging.
+  Future<bool> _loadThenPause(Stream<LoadSuite> suites) async {
+    var subscription = _suiteSubscription = suites.asyncMap((loadSuite) async {
+      var operation = _debugOperation = debug(_engine, _reporter, loadSuite);
+      await operation.valueOrCancellation();
+    }).listen(null);
+
+    var results = await Future.wait(<Future>[
+      subscription.asFuture<void>().then((_) => _engine.suiteSink.close()),
+      _engine.run()
+    ], eagerError: true);
+    return results.last as bool;
+  }
+}
diff --git a/pkgs/test_core/lib/src/runner/application_exception.dart b/pkgs/test_core/lib/src/runner/application_exception.dart
new file mode 100644
index 0000000..23155a2
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/application_exception.dart
@@ -0,0 +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.
+
+/// An expected exception caused by user-controllable circumstances.
+class ApplicationException implements Exception {
+  final String message;
+
+  ApplicationException(this.message);
+
+  @override
+  String toString() => message;
+}
diff --git a/pkgs/test_core/lib/src/runner/compiler_pool.dart b/pkgs/test_core/lib/src/runner/compiler_pool.dart
new file mode 100644
index 0000000..52aa238
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/compiler_pool.dart
@@ -0,0 +1,57 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+import 'package:meta/meta.dart';
+import 'package:pool/pool.dart';
+
+import 'configuration.dart';
+import 'suite.dart';
+
+/// A pool of compiler instances.
+///
+/// This limits the number of compiler instances running concurrently.
+abstract class CompilerPool {
+  /// The test runner configuration.
+  final config = Configuration.current;
+
+  /// The internal pool that controls the number of process running at once.
+  final Pool _pool;
+
+  /// Whether [close] has been called.
+  bool get closed => _closeMemo.hasRun;
+
+  /// The memoizer for running [close] exactly once.
+  final _closeMemo = AsyncMemoizer<void>();
+
+  /// Creates a compiler pool that multiple instances of a compiler at once.
+  CompilerPool() : _pool = Pool(Configuration.current.concurrency);
+
+  /// Compiles [code] to [path] using [_pool] and [compileInternal].
+  ///
+  /// Should not be overridden.
+  Future<void> compile(
+          String code, String path, SuiteConfiguration suiteConfig) =>
+      _pool.withResource(() {
+        if (closed) return null;
+        return compileInternal(code, path, suiteConfig);
+      });
+
+  /// The actual function a given compiler pool should implement to compile a
+  /// suite.
+  @protected
+  Future<void> compileInternal(
+      String code, String path, SuiteConfiguration suiteConfig);
+
+  /// Shuts down the compiler pool, invoking `closeInternal` exactly once.
+  ///
+  /// Should not be overridden.
+  Future<void> close() => _closeMemo.runOnce(closeInternal);
+
+  /// The actual function to shut down the compiler pool, invoked exactly once.
+  @protected
+  Future<void> closeInternal();
+}
diff --git a/pkgs/test_core/lib/src/runner/compiler_selection.dart b/pkgs/test_core/lib/src/runner/compiler_selection.dart
new file mode 100644
index 0000000..70ab553
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/compiler_selection.dart
@@ -0,0 +1,61 @@
+// Copyright (c) 2023, 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:source_span/source_span.dart';
+import 'package:test_api/backend.dart';
+
+/// A compiler with which the user has chosen to run tests.
+class CompilerSelection {
+  /// The chosen compiler to use.
+  final Compiler compiler;
+
+  /// The location in the configuration file of this compiler string, or `null`
+  /// if it was defined outside a configuration file (for example, on the
+  /// command line).
+  final SourceSpan? span;
+
+  /// The platform selector for which platforms this compiler should apply to,
+  /// if specified. Defaults to all platforms where the compiler is supported.
+  final PlatformSelector? platformSelector;
+
+  CompilerSelection(String compiler,
+      {required this.platformSelector, required this.span})
+      : compiler = Compiler.builtIn.firstWhere((c) => c.identifier == compiler);
+
+  factory CompilerSelection.parse(String option, {SourceSpan? parentSpan}) {
+    var parts = option.split(':');
+    switch (parts.length) {
+      case 1:
+        _checkValidCompiler(option, parentSpan);
+        return CompilerSelection(option,
+            platformSelector: null, span: parentSpan);
+      case 2:
+        var compiler = parts[1];
+        _checkValidCompiler(compiler, parentSpan);
+        return CompilerSelection(compiler,
+            platformSelector: PlatformSelector.parse(parts[0]),
+            span: parentSpan);
+      default:
+        throw ArgumentError.value(
+            option,
+            '--compiler',
+            'Must be of the format [<boolean-selector>:]<compiler>, but got '
+                'more than one `:`.');
+    }
+  }
+
+  @override
+  bool operator ==(Object other) =>
+      other is CompilerSelection && other.compiler == compiler;
+
+  @override
+  int get hashCode => compiler.hashCode;
+}
+
+void _checkValidCompiler(String compiler, SourceSpan? span) {
+  if (Compiler.builtIn.any((c) => c.identifier == compiler)) return;
+  throw SourceSpanFormatException(
+      'Invalid compiler `$compiler`, must be one of ${Compiler.builtIn.map((c) => c.identifier).join(', ')}',
+      span);
+}
diff --git a/pkgs/test_core/lib/src/runner/configuration.dart b/pkgs/test_core/lib/src/runner/configuration.dart
new file mode 100644
index 0000000..8ba4aa8
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/configuration.dart
@@ -0,0 +1,1088 @@
+// 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.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:boolean_selector/boolean_selector.dart';
+import 'package:collection/collection.dart';
+import 'package:glob/glob.dart';
+import 'package:path/path.dart' as p;
+import 'package:source_span/source_span.dart';
+import 'package:test_api/scaffolding.dart' show Timeout;
+import 'package:test_api/src/backend/platform_selector.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
+
+import '../util/io.dart';
+import 'compiler_selection.dart';
+import 'configuration/args.dart' as args;
+import 'configuration/custom_runtime.dart';
+import 'configuration/load.dart';
+import 'configuration/reporters.dart';
+import 'configuration/runtime_settings.dart';
+import 'configuration/utils.dart';
+import 'configuration/values.dart';
+import 'runtime_selection.dart';
+import 'suite.dart';
+
+export 'suite.dart' show TestSelection;
+
+/// The key used to look up [Configuration.current] in a zone.
+final _currentKey = Object();
+
+/// A class that encapsulates the command-line configuration of the test runner.
+class Configuration {
+  /// An empty configuration with only default values.
+  ///
+  /// Using this is slightly more efficient than manually constructing a new
+  /// configuration with no arguments.
+  static final empty = Configuration._unsafe();
+
+  /// The usage string for the command-line arguments.
+  static String get usage => args.usage;
+
+  /// Whether `--help` was passed.
+  bool get help => _help ?? false;
+  final bool? _help;
+
+  /// Custom HTML template file.
+  final String? customHtmlTemplatePath;
+
+  /// Whether `--version` was passed.
+  bool get version => _version ?? false;
+  final bool? _version;
+
+  /// Whether to pause for debugging after loading each test suite.
+  bool get pauseAfterLoad => _pauseAfterLoad ?? false;
+  final bool? _pauseAfterLoad;
+
+  /// Whether to run browsers in their respective debug modes
+  bool get debug => pauseAfterLoad || (_debug ?? false) || coverage != null;
+  final bool? _debug;
+
+  /// The output folder for coverage gathering
+  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;
+
+  /// The name of the reporter to use to display results.
+  String get reporter => _reporter ?? defaultReporter;
+  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.
+  final Map<String, String> fileReporters;
+
+  /// Whether to disable retries of tests.
+  bool get noRetry => _noRetry ?? false;
+  final bool? _noRetry;
+
+  /// Whether to use command-line color escapes.
+  bool get color => _color ?? canUseSpecialChars;
+  final bool? _color;
+
+  /// How many tests to run concurrently.
+  int get concurrency =>
+      pauseAfterLoad ? 1 : (_concurrency ?? defaultConcurrency);
+  final int? _concurrency;
+
+  /// The index of the current shard, if sharding is in use, or `null` if it's
+  /// not.
+  ///
+  /// Sharding is a technique that allows the Google internal test framework to
+  /// easily split a test run across multiple workers without requiring the
+  /// tests to be modified by the user. When sharding is in use, the runner gets
+  /// a shard index (this field) and a total number of shards, and is expected
+  /// to provide the following guarantees:
+  ///
+  /// * Running the same invocation of the runner, with the same shard index and
+  ///   total shards, will run the same set of tests.
+  /// * 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;
+
+  /// The total number of shards, if sharding is in use, or `null` if it's not.
+  ///
+  /// See [shardIndex] for details.
+  final int? totalShards;
+
+  /// The list of packages to fold when producing [StackTrace]s.
+  Set<String> get foldTraceExcept => _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;
+
+  /// The paths from which to load tests, and the test cases to run.
+  Map<String, Set<TestSelection>> get testSelections =>
+      _testSelections ??
+      const {
+        'test': {TestSelection()}
+      };
+  final Map<String, Set<TestSelection>>? _testSelections;
+
+  /// Whether the load paths were passed explicitly or the default was used.
+  bool get explicitPaths => _testSelections != null;
+
+  /// The glob matching the basename of tests to run.
+  ///
+  /// This is used to find tests within a directory.
+  Glob get filename => _filename ?? defaultFilename;
+  final Glob? _filename;
+
+  /// The set of presets to use.
+  ///
+  /// Any chosen presets for the parent configuration are added to the chosen
+  /// preset sets for child configurations as well.
+  ///
+  /// Note that the order of this set matters.
+  final Set<String> chosenPresets;
+
+  /// The set of tags that have been declared in any way in this configuration.
+  late final Set<String> knownTags = UnmodifiableSetView({
+    ...includeTags.variables,
+    ...excludeTags.variables,
+    ...suiteDefaults.knownTags,
+    for (var configuration in presets.values) ...configuration.knownTags
+  });
+
+  /// Only run tests whose tags match this selector.
+  ///
+  /// When [merge]d, this is intersected with the other configuration's included
+  /// tags.
+  final BooleanSelector includeTags;
+
+  /// Do not run tests whose tags match this selector.
+  ///
+  /// When [merge]d, this is unioned with the other configuration's
+  /// excluded tags.
+  final BooleanSelector excludeTags;
+
+  /// Configuration presets.
+  ///
+  /// These are configurations that can be explicitly selected by the user via
+  /// the command line. Preset configuration takes precedence over the base
+  /// configuration.
+  ///
+  /// This is guaranteed not to have any keys that match [chosenPresets]; those
+  /// are resolved when the configuration is constructed.
+  final Map<String, Configuration> presets;
+
+  /// All preset names that are known to be valid.
+  ///
+  /// This includes presets that have already been resolved.
+  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;
+
+  /// Runtimes defined by the user in terms of existing runtimes.
+  final Map<String, CustomRuntime> defineRuntimes;
+
+  /// The default suite-level configuration.
+  final SuiteConfiguration suiteDefaults;
+
+  /// The set of patterns to check test names against in all suites that run.
+  final Set<Pattern> globalPatterns;
+
+  /// The seed used to generate randomness for test case shuffling.
+  ///
+  /// If null or zero no shuffling will occur.
+  /// The same seed will shuffle the tests in the same way every time.
+  final int? testRandomizeOrderingSeed;
+
+  final bool? _stopOnFirstFailure;
+
+  /// Whether to stop running subsequent tests after a test fails.
+  bool get stopOnFirstFailure => _stopOnFirstFailure ?? false;
+
+  /// Returns the current configuration, or a default configuration if no
+  /// current configuration is set.
+  ///
+  /// The current configuration is set using [asCurrent].
+  static Configuration get current =>
+      Zone.current[_currentKey] as Configuration? ?? Configuration._unsafe();
+
+  /// Parses the configuration from [args].
+  ///
+  /// Throws a [FormatException] if [args] are invalid.
+  factory Configuration.parse(List<String> arguments) => args.parse(arguments);
+
+  /// Loads configuration from [path].
+  ///
+  /// If [global] is `true`, this restricts the configuration to rules that are
+  /// supported globally.
+  ///
+  /// Throws an [IOException] if [path] does not exist or cannot be read. Throws
+  /// a [FormatException] if the file contents are invalid.
+  factory Configuration.load(String path, {bool global = false}) {
+    final content = File(path).readAsStringSync();
+    final sourceUrl = p.toUri(path);
+    return parse(content, global: global, sourceUrl: sourceUrl);
+  }
+
+  /// Parses configuration from YAML formatted [content].
+  ///
+  /// If [global] is `true`, this restricts the configuration to rules that are
+  /// supported globally.
+  ///
+  /// If [sourceUrl] is provided it will be set as the source url for the yaml
+  /// document.
+  ///
+  /// Throws a [FormatException] if the content is invalid.
+  factory Configuration.loadFromString(String content,
+          {bool global = false, Uri? sourceUrl}) =>
+      parse(content, global: global, sourceUrl: sourceUrl);
+
+  factory Configuration(
+      {required bool? help,
+      required String? customHtmlTemplatePath,
+      required bool? version,
+      required bool? pauseAfterLoad,
+      required bool? debug,
+      required bool? color,
+      required String? configurationPath,
+      required String? reporter,
+      required Map<String, String>? fileReporters,
+      required String? coverage,
+      required int? concurrency,
+      required int? shardIndex,
+      required int? totalShards,
+      required Map<String, Set<TestSelection>>? testSelections,
+      required Iterable<String>? foldTraceExcept,
+      required Iterable<String>? foldTraceOnly,
+      required Glob? filename,
+      required Iterable<String>? chosenPresets,
+      required Map<String, Configuration>? presets,
+      required Map<String, RuntimeSettings>? overrideRuntimes,
+      required Map<String, CustomRuntime>? defineRuntimes,
+      required bool? noRetry,
+      required int? testRandomizeOrderingSeed,
+      required bool? stopOnFirstFailure,
+
+      // Suite-level configuration
+      required bool? allowDuplicateTestNames,
+      required bool? allowTestRandomization,
+      required bool? jsTrace,
+      required bool? runSkipped,
+      required Iterable<String>? dart2jsArgs,
+      required String? precompiledPath,
+      required Iterable<Pattern>? globalPatterns,
+      required Iterable<CompilerSelection>? compilerSelections,
+      required Iterable<RuntimeSelection>? runtimes,
+      required BooleanSelector? includeTags,
+      required BooleanSelector? excludeTags,
+      required Map<BooleanSelector, SuiteConfiguration>? tags,
+      required Map<PlatformSelector, SuiteConfiguration>? onPlatform,
+      required bool? ignoreTimeouts,
+
+      // Test-level configuration
+      required Timeout? timeout,
+      required bool? verboseTrace,
+      required bool? chainStackTraces,
+      required bool? skip,
+      required int? retry,
+      required String? skipReason,
+      required PlatformSelector? testOn,
+      required Iterable<String>? addTags}) {
+    var chosenPresetSet = chosenPresets?.toSet();
+    var configuration = Configuration._(
+        help: help,
+        customHtmlTemplatePath: customHtmlTemplatePath,
+        version: version,
+        pauseAfterLoad: pauseAfterLoad,
+        debug: debug,
+        color: color,
+        configurationPath: configurationPath,
+        reporter: reporter,
+        fileReporters: fileReporters,
+        coverage: coverage,
+        concurrency: concurrency,
+        shardIndex: shardIndex,
+        totalShards: totalShards,
+        testSelections: testSelections,
+        foldTraceExcept: foldTraceExcept,
+        foldTraceOnly: foldTraceOnly,
+        filename: filename,
+        chosenPresets: chosenPresetSet,
+        presets: _withChosenPresets(presets, chosenPresetSet),
+        overrideRuntimes: overrideRuntimes,
+        defineRuntimes: defineRuntimes,
+        noRetry: noRetry,
+        testRandomizeOrderingSeed: testRandomizeOrderingSeed,
+        stopOnFirstFailure: stopOnFirstFailure,
+        includeTags: includeTags,
+        excludeTags: excludeTags,
+        globalPatterns: globalPatterns,
+        suiteDefaults: SuiteConfiguration(
+            allowDuplicateTestNames: allowDuplicateTestNames,
+            allowTestRandomization: allowTestRandomization,
+            jsTrace: jsTrace,
+            runSkipped: runSkipped,
+            dart2jsArgs: dart2jsArgs,
+            precompiledPath: precompiledPath,
+            compilerSelections: compilerSelections,
+            runtimes: runtimes,
+            tags: tags,
+            onPlatform: onPlatform,
+            ignoreTimeouts: ignoreTimeouts,
+
+            // Test-level configuration
+            timeout: timeout,
+            verboseTrace: verboseTrace,
+            chainStackTraces: chainStackTraces,
+            skip: skip,
+            retry: retry,
+            skipReason: skipReason,
+            testOn: testOn,
+            addTags: addTags));
+    return configuration._resolvePresets();
+  }
+
+  /// A constructor that doesn't require all of its options to be passed.
+  ///
+  /// This should only be used in situations where you really only want to
+  /// configure a specific restricted set of options.
+  factory Configuration._unsafe(
+          {bool? help,
+          String? customHtmlTemplatePath,
+          bool? version,
+          bool? pauseAfterLoad,
+          bool? debug,
+          bool? color,
+          String? configurationPath,
+          String? reporter,
+          Map<String, String>? fileReporters,
+          String? coverage,
+          int? concurrency,
+          int? shardIndex,
+          int? totalShards,
+          Map<String, Set<TestSelection>>? testSelections,
+          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,
+          int? testRandomizeOrderingSeed,
+          bool? stopOnFirstFailure,
+
+          // Suite-level configuration
+          bool? allowDuplicateTestNames,
+          bool? allowTestRandomization,
+          bool? jsTrace,
+          bool? runSkipped,
+          Iterable<String>? dart2jsArgs,
+          String? precompiledPath,
+          Iterable<Pattern>? globalPatterns,
+          Iterable<CompilerSelection>? compilerSelections,
+          Iterable<RuntimeSelection>? runtimes,
+          BooleanSelector? includeTags,
+          BooleanSelector? excludeTags,
+          Map<BooleanSelector, SuiteConfiguration>? tags,
+          Map<PlatformSelector, SuiteConfiguration>? onPlatform,
+          bool? ignoreTimeouts,
+
+          // Test-level configuration
+          Timeout? timeout,
+          bool? verboseTrace,
+          bool? chainStackTraces,
+          bool? skip,
+          int? retry,
+          String? skipReason,
+          PlatformSelector? testOn,
+          Iterable<String>? addTags}) =>
+      Configuration(
+          help: help,
+          customHtmlTemplatePath: customHtmlTemplatePath,
+          version: version,
+          pauseAfterLoad: pauseAfterLoad,
+          debug: debug,
+          color: color,
+          configurationPath: configurationPath,
+          reporter: reporter,
+          fileReporters: fileReporters,
+          coverage: coverage,
+          concurrency: concurrency,
+          shardIndex: shardIndex,
+          totalShards: totalShards,
+          testSelections: testSelections,
+          foldTraceExcept: foldTraceExcept,
+          foldTraceOnly: foldTraceOnly,
+          filename: filename,
+          chosenPresets: chosenPresets,
+          presets: presets,
+          overrideRuntimes: overrideRuntimes,
+          defineRuntimes: defineRuntimes,
+          noRetry: noRetry,
+          testRandomizeOrderingSeed: testRandomizeOrderingSeed,
+          stopOnFirstFailure: stopOnFirstFailure,
+          allowDuplicateTestNames: allowDuplicateTestNames,
+          allowTestRandomization: allowTestRandomization,
+          jsTrace: jsTrace,
+          runSkipped: runSkipped,
+          dart2jsArgs: dart2jsArgs,
+          precompiledPath: precompiledPath,
+          globalPatterns: globalPatterns,
+          compilerSelections: compilerSelections,
+          runtimes: runtimes,
+          includeTags: includeTags,
+          excludeTags: excludeTags,
+          tags: tags,
+          onPlatform: onPlatform,
+          ignoreTimeouts: ignoreTimeouts,
+          timeout: timeout,
+          verboseTrace: verboseTrace,
+          chainStackTraces: chainStackTraces,
+          skip: skip,
+          retry: retry,
+          skipReason: skipReason,
+          testOn: testOn,
+          addTags: addTags);
+
+  /// Suite level configuration allowed in the global test config file.
+  ///
+  /// This is per-user configuration and should be limited as such, it should
+  /// not contain options that would change the pass/fail result of any given
+  /// test, or change which tests would run.
+  factory Configuration.globalTest({
+    required bool? verboseTrace,
+    required bool? jsTrace,
+    required Timeout? timeout,
+    required Map<String, Configuration>? presets,
+    required bool? chainStackTraces,
+    required Iterable<String>? foldTraceExcept,
+    required Iterable<String>? foldTraceOnly,
+  }) =>
+      Configuration(
+        foldTraceExcept: foldTraceExcept,
+        foldTraceOnly: foldTraceOnly,
+        jsTrace: jsTrace,
+        timeout: timeout,
+        verboseTrace: verboseTrace,
+        chainStackTraces: chainStackTraces,
+        help: null,
+        customHtmlTemplatePath: null,
+        version: null,
+        pauseAfterLoad: null,
+        debug: null,
+        color: null,
+        configurationPath: null,
+        reporter: null,
+        fileReporters: null,
+        coverage: null,
+        concurrency: null,
+        shardIndex: null,
+        totalShards: null,
+        testSelections: null,
+        filename: null,
+        chosenPresets: null,
+        presets: presets,
+        overrideRuntimes: null,
+        defineRuntimes: null,
+        noRetry: null,
+        testRandomizeOrderingSeed: null,
+        stopOnFirstFailure: null,
+        ignoreTimeouts: null,
+        allowDuplicateTestNames: null,
+        allowTestRandomization: null,
+        runSkipped: null,
+        dart2jsArgs: null,
+        precompiledPath: null,
+        globalPatterns: null,
+        compilerSelections: null,
+        runtimes: null,
+        includeTags: null,
+        excludeTags: null,
+        tags: null,
+        onPlatform: null,
+        skip: null,
+        retry: null,
+        skipReason: null,
+        testOn: null,
+        addTags: null,
+      );
+
+  /// Suite level configuration that is not allowed in the global test
+  /// config file.
+  ///
+  /// This configuration may alter the pass/fail result of a test run, and thus
+  /// should only be configured per package and not at the global level (global
+  /// config is user specific).
+  factory Configuration.localTest({
+    required bool? skip,
+    required int? retry,
+    required String? skipReason,
+    required PlatformSelector? testOn,
+    required Iterable<String>? addTags,
+    required bool? allowDuplicateTestNames,
+    required bool? allowTestRandomization,
+  }) =>
+      Configuration(
+        allowDuplicateTestNames: allowDuplicateTestNames,
+        allowTestRandomization: allowTestRandomization,
+        skip: skip,
+        retry: retry,
+        skipReason: skipReason,
+        testOn: testOn,
+        addTags: addTags,
+        help: null,
+        customHtmlTemplatePath: null,
+        version: null,
+        pauseAfterLoad: null,
+        debug: null,
+        color: null,
+        configurationPath: null,
+        reporter: null,
+        fileReporters: null,
+        coverage: null,
+        concurrency: null,
+        shardIndex: null,
+        totalShards: null,
+        testSelections: null,
+        foldTraceExcept: null,
+        foldTraceOnly: null,
+        filename: null,
+        chosenPresets: null,
+        presets: null,
+        overrideRuntimes: null,
+        defineRuntimes: null,
+        noRetry: null,
+        testRandomizeOrderingSeed: null,
+        stopOnFirstFailure: null,
+        jsTrace: null,
+        runSkipped: null,
+        dart2jsArgs: null,
+        precompiledPath: null,
+        globalPatterns: null,
+        compilerSelections: null,
+        runtimes: null,
+        includeTags: null,
+        excludeTags: null,
+        tags: null,
+        onPlatform: null,
+        ignoreTimeouts: null,
+        timeout: null,
+        verboseTrace: null,
+        chainStackTraces: null,
+      );
+
+  /// Runner configuration that is allowed in the global test config file.
+  ///
+  /// This is per-user configuration and should be limited as such, it should
+  /// not contain options that would change the pass/fail result of any given
+  /// test, or change which tests would run.
+  ///
+  /// Note that [customHtmlTemplatePath] violates this rule, and really should
+  /// not be configurable globally.
+  factory Configuration.globalRunner(
+          {required bool? pauseAfterLoad,
+          required String? customHtmlTemplatePath,
+          required bool? runSkipped,
+          required String? reporter,
+          required Map<String, String>? fileReporters,
+          required int? concurrency,
+          required Iterable<CompilerSelection>? compilerSelections,
+          required Iterable<RuntimeSelection>? runtimes,
+          required Iterable<String>? chosenPresets,
+          required Map<String, RuntimeSettings>? overrideRuntimes}) =>
+      Configuration(
+        customHtmlTemplatePath: customHtmlTemplatePath,
+        pauseAfterLoad: pauseAfterLoad,
+        runSkipped: runSkipped,
+        reporter: reporter,
+        fileReporters: fileReporters,
+        concurrency: concurrency,
+        compilerSelections: compilerSelections,
+        runtimes: runtimes,
+        chosenPresets: chosenPresets,
+        overrideRuntimes: overrideRuntimes,
+        help: null,
+        version: null,
+        debug: null,
+        color: null,
+        configurationPath: null,
+        coverage: null,
+        shardIndex: null,
+        totalShards: null,
+        testSelections: null,
+        foldTraceExcept: null,
+        foldTraceOnly: null,
+        filename: null,
+        presets: null,
+        defineRuntimes: null,
+        noRetry: null,
+        testRandomizeOrderingSeed: null,
+        stopOnFirstFailure: null,
+        allowDuplicateTestNames: null,
+        allowTestRandomization: null,
+        jsTrace: null,
+        dart2jsArgs: null,
+        precompiledPath: null,
+        globalPatterns: null,
+        includeTags: null,
+        excludeTags: null,
+        tags: null,
+        onPlatform: null,
+        ignoreTimeouts: null,
+        timeout: null,
+        verboseTrace: null,
+        chainStackTraces: null,
+        skip: null,
+        retry: null,
+        skipReason: null,
+        testOn: null,
+        addTags: null,
+      );
+
+  /// Runner configuration that is not allowed in the global test config file.
+  ///
+  /// This configuration may alter the pass/fail result of a test run, and thus
+  /// should only be configured per package and not at the global level (global
+  /// config is user specific).
+  factory Configuration.localRunner(
+          {required Iterable<Pattern>? globalPatterns,
+          required Map<String, Set<TestSelection>>? testSelections,
+          required Glob? filename,
+          required BooleanSelector? includeTags,
+          required BooleanSelector? excludeTags,
+          required Map<String, CustomRuntime>? defineRuntimes}) =>
+      Configuration(
+          globalPatterns: globalPatterns,
+          testSelections: testSelections,
+          filename: filename,
+          includeTags: includeTags,
+          excludeTags: excludeTags,
+          defineRuntimes: defineRuntimes,
+          help: null,
+          customHtmlTemplatePath: null,
+          version: null,
+          pauseAfterLoad: null,
+          debug: null,
+          color: null,
+          configurationPath: null,
+          reporter: null,
+          fileReporters: null,
+          coverage: null,
+          concurrency: null,
+          shardIndex: null,
+          totalShards: null,
+          foldTraceExcept: null,
+          foldTraceOnly: null,
+          chosenPresets: null,
+          presets: null,
+          overrideRuntimes: null,
+          noRetry: null,
+          testRandomizeOrderingSeed: null,
+          stopOnFirstFailure: null,
+          allowDuplicateTestNames: null,
+          allowTestRandomization: null,
+          jsTrace: null,
+          runSkipped: null,
+          dart2jsArgs: null,
+          precompiledPath: null,
+          compilerSelections: null,
+          runtimes: null,
+          tags: null,
+          onPlatform: null,
+          ignoreTimeouts: null,
+          timeout: null,
+          verboseTrace: null,
+          chainStackTraces: null,
+          skip: null,
+          retry: null,
+          skipReason: null,
+          testOn: null,
+          addTags: null);
+
+  /// A specialized constructor for configuring only `onPlatform`.
+  factory Configuration.onPlatform(
+          Map<PlatformSelector, SuiteConfiguration> onPlatform) =>
+      Configuration._unsafe(onPlatform: onPlatform);
+
+  factory Configuration.tags(Map<BooleanSelector, SuiteConfiguration> tags) =>
+      Configuration._unsafe(tags: tags);
+
+  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,
+        config.change(
+            chosenPresets: config.chosenPresets.union(chosenPresets))));
+  }
+
+  /// Creates new Configuration.
+  ///
+  /// Unlike [Configuration.new], this assumes [presets] is already resolved.
+  Configuration._(
+      {required bool? help,
+      required this.customHtmlTemplatePath,
+      required bool? version,
+      required bool? pauseAfterLoad,
+      required bool? debug,
+      required bool? color,
+      required String? configurationPath,
+      required String? reporter,
+      required Map<String, String>? fileReporters,
+      required this.coverage,
+      required int? concurrency,
+      required this.shardIndex,
+      required this.totalShards,
+      required Map<String, Set<TestSelection>>? testSelections,
+      required Iterable<String>? foldTraceExcept,
+      required Iterable<String>? foldTraceOnly,
+      required Glob? filename,
+      required Iterable<String>? chosenPresets,
+      required Map<String, Configuration>? presets,
+      required Map<String, RuntimeSettings>? overrideRuntimes,
+      required Map<String, CustomRuntime>? defineRuntimes,
+      required bool? noRetry,
+      required this.testRandomizeOrderingSeed,
+      required bool? stopOnFirstFailure,
+      required BooleanSelector? includeTags,
+      required BooleanSelector? excludeTags,
+      required Iterable<Pattern>? globalPatterns,
+      required SuiteConfiguration? suiteDefaults})
+      : _help = help,
+        _version = version,
+        _pauseAfterLoad = pauseAfterLoad,
+        _debug = debug,
+        _color = color,
+        _configurationPath = configurationPath,
+        _reporter = reporter,
+        fileReporters = fileReporters ?? {},
+        _concurrency = concurrency,
+        _testSelections = testSelections == null || testSelections.isEmpty
+            ? null
+            : Map.unmodifiable(testSelections),
+        _foldTraceExcept = _set(foldTraceExcept),
+        _foldTraceOnly = _set(foldTraceOnly),
+        _filename = filename,
+        chosenPresets = UnmodifiableSetView(chosenPresets?.toSet() ?? {}),
+        presets = _map(presets),
+        overrideRuntimes = _map(overrideRuntimes),
+        defineRuntimes = _map(defineRuntimes),
+        _noRetry = noRetry,
+        includeTags = includeTags ?? BooleanSelector.all,
+        excludeTags = excludeTags ?? BooleanSelector.none,
+        globalPatterns = globalPatterns == null
+            ? const {}
+            : UnmodifiableSetView(globalPatterns.toSet()),
+        _stopOnFirstFailure = stopOnFirstFailure,
+        suiteDefaults = (() {
+          var config = suiteDefaults ?? SuiteConfiguration.empty;
+          if (pauseAfterLoad == true) {
+            return config.change(ignoreTimeouts: true);
+          }
+          return config;
+        }()) {
+    if (_filename != null && _filename.context.style != p.style) {
+      throw ArgumentError(
+          "filename's context must match the current operating system, was "
+          '${_filename.context.style}.');
+    }
+
+    if ((shardIndex == null) != (totalShards == null)) {
+      throw ArgumentError(
+          'shardIndex and totalShards may only be passed together.');
+    } else if (shardIndex != null) {
+      RangeError.checkValueInInterval(
+          shardIndex!, 0, totalShards! - 1, 'shardIndex');
+    }
+  }
+
+  /// Creates a new [Configuration] that takes its configuration from
+  /// [SuiteConfiguration].
+  factory Configuration.fromSuiteConfiguration(
+          SuiteConfiguration suiteConfig) =>
+      Configuration._(
+        suiteDefaults: suiteConfig,
+        globalPatterns: null,
+        help: null,
+        customHtmlTemplatePath: null,
+        version: null,
+        pauseAfterLoad: null,
+        debug: null,
+        color: null,
+        configurationPath: null,
+        reporter: null,
+        fileReporters: null,
+        coverage: null,
+        concurrency: null,
+        shardIndex: null,
+        totalShards: null,
+        testSelections: null,
+        foldTraceExcept: null,
+        foldTraceOnly: null,
+        filename: null,
+        chosenPresets: null,
+        presets: null,
+        overrideRuntimes: null,
+        defineRuntimes: null,
+        noRetry: null,
+        testRandomizeOrderingSeed: null,
+        stopOnFirstFailure: null,
+        includeTags: null,
+        excludeTags: null,
+      );
+
+  /// Returns a set from [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;
+    return set;
+  }
+
+  /// Returns an unmodifiable copy of [input] or an empty unmodifiable map.
+  static Map<K, V> _map<K, V>(Map<K, V>? input) {
+    input ??= {};
+    return Map.unmodifiable(input);
+  }
+
+  /// Runs [body] with this as [Configuration.current].
+  ///
+  /// This is zone-scoped, so this will be the current configuration in any
+  /// asynchronous callbacks transitively created by [body].
+  T asCurrent<T>(T Function() body) =>
+      runZoned(body, zoneValues: {_currentKey: this});
+
+  /// Throws a [FormatException] if this refers to any undefined runtimes.
+  void validateRuntimes(List<Runtime> allRuntimes) {
+    // We don't need to verify [customRuntimes] here because those runtimes
+    // already need to be verified and resolved to create [allRuntimes].
+
+    for (var settings in overrideRuntimes.values) {
+      if (!allRuntimes
+          .any((runtime) => runtime.identifier == settings.identifier)) {
+        throw SourceSpanFormatException(
+            'Unknown platform "${settings.identifier}".',
+            settings.identifierSpan);
+      }
+    }
+
+    suiteDefaults.validateRuntimes(allRuntimes);
+    for (var config in presets.values) {
+      config.validateRuntimes(allRuntimes);
+    }
+  }
+
+  /// Merges this with [other].
+  ///
+  /// For most fields, if both configurations have values set, [other]'s value
+  /// takes precedence. However, certain fields are merged together instead.
+  /// This is indicated in those fields' documentation.
+  Configuration merge(Configuration other) {
+    if (this == Configuration.empty) return other;
+    if (other == Configuration.empty) return this;
+
+    var foldTraceOnly = other._foldTraceOnly ?? _foldTraceOnly;
+    var foldTraceExcept = other._foldTraceExcept ?? _foldTraceExcept;
+    if (_foldTraceOnly != null) {
+      if (other._foldTraceExcept != null) {
+        foldTraceOnly = _foldTraceOnly.difference(other._foldTraceExcept);
+      } else if (other._foldTraceOnly != null) {
+        foldTraceOnly = other._foldTraceOnly.intersection(_foldTraceOnly);
+      }
+    } else if (_foldTraceExcept != null) {
+      if (other._foldTraceOnly != null) {
+        foldTraceOnly = other._foldTraceOnly.difference(_foldTraceExcept);
+      } else if (other._foldTraceExcept != null) {
+        foldTraceExcept = other._foldTraceExcept.union(_foldTraceExcept);
+      }
+    }
+
+    var result = Configuration._(
+        help: other._help ?? _help,
+        customHtmlTemplatePath:
+            other.customHtmlTemplatePath ?? customHtmlTemplatePath,
+        version: other._version ?? _version,
+        pauseAfterLoad: other._pauseAfterLoad ?? _pauseAfterLoad,
+        debug: other._debug ?? _debug,
+        color: other._color ?? _color,
+        configurationPath: other._configurationPath ?? _configurationPath,
+        reporter: other._reporter ?? _reporter,
+        fileReporters: mergeMaps(fileReporters, other.fileReporters),
+        coverage: other.coverage ?? coverage,
+        concurrency: other._concurrency ?? _concurrency,
+        shardIndex: other.shardIndex ?? shardIndex,
+        totalShards: other.totalShards ?? totalShards,
+        testSelections: other._testSelections ?? _testSelections,
+        foldTraceExcept: foldTraceExcept,
+        foldTraceOnly: foldTraceOnly,
+        filename: other._filename ?? _filename,
+        chosenPresets: chosenPresets.union(other.chosenPresets),
+        presets: _mergeConfigMaps(presets, other.presets),
+        overrideRuntimes: mergeUnmodifiableMaps(
+            overrideRuntimes, other.overrideRuntimes,
+            value: (settings1, settings2) => RuntimeSettings(
+                settings1.identifier,
+                settings1.identifierSpan,
+                [...settings1.settings, ...settings2.settings])),
+        defineRuntimes:
+            mergeUnmodifiableMaps(defineRuntimes, other.defineRuntimes),
+        noRetry: other._noRetry ?? _noRetry,
+        testRandomizeOrderingSeed:
+            other.testRandomizeOrderingSeed ?? testRandomizeOrderingSeed,
+        stopOnFirstFailure: other._stopOnFirstFailure ?? _stopOnFirstFailure,
+        includeTags: includeTags.intersection(other.includeTags),
+        excludeTags: excludeTags.union(other.excludeTags),
+        globalPatterns: globalPatterns.union(other.globalPatterns),
+        suiteDefaults: suiteDefaults.merge(other.suiteDefaults));
+    result = result._resolvePresets();
+
+    // Make sure the merged config preserves any presets that were chosen and
+    // discarded.
+    result._knownPresets = knownPresets.union(other.knownPresets);
+    return result;
+  }
+
+  /// Returns a copy of this configuration with the given fields updated.
+  ///
+  /// 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? debug,
+      bool? color,
+      String? configurationPath,
+      String? reporter,
+      Map<String, String>? fileReporters,
+      String? coverage,
+      int? concurrency,
+      int? shardIndex,
+      int? totalShards,
+      Map<String, Set<TestSelection>>? testSelections,
+      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,
+      int? testRandomizeOrderingSeed,
+      bool? ignoreTimeouts,
+
+      // Suite-level configuration
+      bool? allowDuplicateTestNames,
+      bool? jsTrace,
+      bool? runSkipped,
+      Iterable<String>? dart2jsArgs,
+      String? precompiledPath,
+      Iterable<RuntimeSelection>? runtimes,
+      BooleanSelector? includeTags,
+      BooleanSelector? excludeTags,
+      Map<BooleanSelector, SuiteConfiguration>? tags,
+      Map<PlatformSelector, SuiteConfiguration>? onPlatform,
+
+      // Test-level configuration
+      Timeout? timeout,
+      bool? verboseTrace,
+      bool? chainStackTraces,
+      bool? skip,
+      String? skipReason,
+      PlatformSelector? testOn,
+      Iterable<String>? addTags}) {
+    var config = Configuration._(
+        help: help ?? _help,
+        customHtmlTemplatePath:
+            customHtmlTemplatePath ?? this.customHtmlTemplatePath,
+        version: version ?? _version,
+        pauseAfterLoad: pauseAfterLoad ?? _pauseAfterLoad,
+        debug: debug ?? _debug,
+        color: color ?? _color,
+        configurationPath: configurationPath ?? _configurationPath,
+        reporter: reporter ?? _reporter,
+        fileReporters: fileReporters ?? this.fileReporters,
+        coverage: coverage ?? this.coverage,
+        concurrency: concurrency ?? _concurrency,
+        shardIndex: shardIndex ?? this.shardIndex,
+        totalShards: totalShards ?? this.totalShards,
+        testSelections: testSelections ?? _testSelections,
+        foldTraceExcept: exceptPackages ?? _foldTraceExcept,
+        foldTraceOnly: onlyPackages ?? _foldTraceOnly,
+        filename: filename ?? _filename,
+        chosenPresets: chosenPresets ?? this.chosenPresets,
+        presets: presets ?? this.presets,
+        overrideRuntimes: overrideRuntimes ?? this.overrideRuntimes,
+        defineRuntimes: defineRuntimes ?? this.defineRuntimes,
+        noRetry: noRetry ?? _noRetry,
+        testRandomizeOrderingSeed:
+            testRandomizeOrderingSeed ?? this.testRandomizeOrderingSeed,
+        stopOnFirstFailure: _stopOnFirstFailure,
+        includeTags: includeTags,
+        excludeTags: excludeTags,
+        globalPatterns: globalPatterns,
+        suiteDefaults: suiteDefaults.change(
+          allowDuplicateTestNames: allowDuplicateTestNames,
+          jsTrace: jsTrace,
+          runSkipped: runSkipped,
+          dart2jsArgs: dart2jsArgs,
+          precompiledPath: precompiledPath,
+          runtimes: runtimes,
+          tags: tags,
+          onPlatform: onPlatform,
+          timeout: timeout,
+          verboseTrace: verboseTrace,
+          chainStackTraces: chainStackTraces,
+          skip: skip,
+          skipReason: skipReason,
+          testOn: testOn,
+          addTags: addTags,
+          ignoreTimeouts: ignoreTimeouts,
+        ));
+    return config._resolvePresets();
+  }
+
+  /// Merges two maps whose values are [Configuration]s.
+  ///
+  /// Any overlapping keys in the maps have their configurations merged in the
+  /// returned map.
+  Map<String, Configuration> _mergeConfigMaps(
+          Map<String, Configuration> map1, Map<String, Configuration> map2) =>
+      mergeMaps(map1, map2,
+          value: (config1, config2) => config1.merge(config2));
+
+  /// Returns a copy of this [Configuration] with all [chosenPresets] resolved
+  /// against [presets].
+  Configuration _resolvePresets() {
+    if (chosenPresets.isEmpty || presets.isEmpty) return this;
+
+    var newPresets = Map<String, Configuration>.from(presets);
+    var merged = chosenPresets.fold(
+        empty,
+        (Configuration merged, preset) =>
+            merged.merge(newPresets.remove(preset) ?? Configuration.empty));
+
+    if (merged == empty) return this;
+    var result = change(presets: newPresets).merge(merged);
+
+    // Make sure the configuration knows about presets that were selected and
+    // thus removed from [newPresets].
+    result._knownPresets =
+        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
new file mode 100644
index 0000000..e981bed
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/configuration/args.dart
@@ -0,0 +1,426 @@
+// 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.
+
+import 'dart:io';
+import 'dart:math';
+
+import 'package:args/args.dart';
+import 'package:boolean_selector/boolean_selector.dart';
+import 'package:test_api/backend.dart';
+import 'package:test_api/scaffolding.dart' show Timeout;
+
+import '../../util/io.dart';
+import '../compiler_selection.dart';
+import '../configuration.dart';
+import '../runtime_selection.dart';
+import 'reporters.dart';
+import 'values.dart';
+
+/// The parser used to parse the command-line arguments.
+final ArgParser _parser = (() {
+  var parser = ArgParser(allowTrailingOptions: true);
+
+  var allRuntimes = Runtime.builtIn.toList()..remove(Runtime.vm);
+  if (!Platform.isMacOS) allRuntimes.remove(Runtime.safari);
+
+  parser.addFlag('help',
+      abbr: 'h', negatable: false, help: 'Show this usage information.');
+  parser.addFlag('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.addMultiOption('name',
+      abbr: 'n',
+      help: 'A substring of the name of the test to run.\n'
+          'Regular expression syntax is supported.\n'
+          'If passed multiple times, tests must match all substrings.',
+      splitCommas: false);
+  parser.addMultiOption('plain-name',
+      abbr: 'N',
+      help: 'A plain-text substring of the name of the test to run.\n'
+          'If passed multiple times, tests must match all substrings.',
+      splitCommas: false);
+  parser.addMultiOption('tags',
+      abbr: 't',
+      help: 'Run only tests with all of the specified tags.\n'
+          'Supports boolean selector syntax.');
+  parser.addMultiOption('tag', hide: true);
+  parser.addMultiOption('exclude-tags',
+      abbr: 'x',
+      help: "Don't run tests with any of the specified tags.\n"
+          'Supports boolean selector syntax.');
+  parser.addMultiOption('exclude-tag', hide: true);
+  parser.addFlag('run-skipped',
+      help: 'Run skipped tests instead of skipping them.');
+
+  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
+  // avoid conflicting with [SuitePlatform]. We decided not to also change the
+  // UI to avoid a painful migration.
+  parser.addMultiOption('platform',
+      abbr: 'p',
+      help: 'The platform(s) on which to run the tests.\n'
+          '[vm (default), '
+          '${allRuntimes.map((runtime) => runtime.identifier).join(", ")}].\n'
+          'Each platform supports the following compilers:\n'
+          '${Runtime.vm.supportedCompilersText}\n'
+          '${allRuntimes.map((r) => r.supportedCompilersText).join('\n')}');
+  parser.addMultiOption('compiler',
+      abbr: 'c',
+      help: 'The compiler(s) to use to run tests, supported compilers are '
+          '[${Compiler.builtIn.map((c) => c.identifier).join(', ')}].\n'
+          'Each platform has a default compiler but may support other '
+          'compilers.\n'
+          'You can target a compiler to a specific platform using arguments '
+          'of the following form [<platform-selector>:]<compiler>.\n'
+          'If a platform is specified but no given compiler is supported for '
+          'that platform, then it will use its default compiler.');
+  parser.addMultiOption('preset',
+      abbr: 'P', help: 'The configuration preset(s) to use.');
+  parser.addOption('concurrency',
+      abbr: 'j',
+      help: 'The number of concurrent test suites run.',
+      defaultsTo: defaultConcurrency.toString(),
+      valueHelp: 'threads');
+  parser.addOption('total-shards',
+      help: 'The total number of invocations of the test runner being run.');
+  parser.addOption('shard-index',
+      help: 'The index of this test runner invocation (of --total-shards).');
+  parser.addOption('pub-serve',
+      help: '[Removed] The port of a pub serve instance serving "test/".',
+      valueHelp: 'port',
+      hide: true);
+  parser.addOption('timeout',
+      help: 'The default test timeout. For example: 15s, 2x, none',
+      defaultsTo: '30s');
+  parser.addFlag('ignore-timeouts',
+      help: 'Ignore all timeouts (useful if debugging)', negatable: false);
+  parser.addFlag('pause-after-load',
+      help: 'Pause for debugging before any tests execute.\n'
+          'Implies --concurrency=1, --debug, and --ignore-timeouts.\n'
+          'Currently only supported for browser tests.',
+      negatable: false);
+  parser.addFlag('debug',
+      help: 'Run the VM and Chrome tests in debug mode.', negatable: false);
+  parser.addOption('coverage',
+      help: 'Gather coverage and output it to the specified directory.\n'
+          'Implies --debug.',
+      valueHelp: 'directory');
+  parser.addFlag('chain-stack-traces',
+      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: false);
+  parser.addFlag('no-retry',
+      help: "Don't rerun tests that have retry set.",
+      defaultsTo: false,
+      negatable: false);
+  parser.addFlag('use-data-isolate-strategy',
+      help: '**DEPRECATED**: This is now just an alias for --compiler source.',
+      defaultsTo: false,
+      hide: true,
+      negatable: false);
+  parser.addOption('test-randomize-ordering-seed',
+      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.');
+  parser.addFlag('fail-fast',
+      help: 'Stop running tests after the first failure.\n');
+
+  var reporterDescriptions = <String, String>{
+    for (final MapEntry(:key, :value) in allReporters.entries)
+      key: value.description
+  };
+
+  parser.addSeparator('Output:');
+  parser.addOption('reporter',
+      abbr: 'r',
+      help: 'Set how to print test results.',
+      defaultsTo: defaultReporter,
+      allowed: allReporters.keys,
+      allowedHelp: reporterDescriptions,
+      valueHelp: 'option');
+  parser.addOption('file-reporter',
+      help: 'Enable an additional reporter writing test results to a file.\n'
+          'Should be in the form <reporter>:<filepath>, '
+          'Example: "json:reports/tests.json"');
+  parser.addFlag('verbose-trace',
+      negatable: false, help: 'Emit stack traces with core library frames.');
+  parser.addFlag('js-trace',
+      negatable: false,
+      help: 'Emit raw JavaScript stack traces for browser tests.');
+  parser.addFlag('color',
+      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.
+
+  parser.addOption('configuration',
+      help: 'The path to the configuration file.', hide: true);
+  parser.addMultiOption('dart2js-args',
+      help: 'Extra arguments to pass to dart2js.', hide: true);
+
+  // If we're running test/dir/my_test.dart, we'll look for
+  // test/dir/my_test.dart.html in the precompiled directory.
+  parser.addOption('precompiled',
+      help: 'The path to a mirror of the package directory containing HTML '
+          'that points to precompiled JS.',
+      hide: true);
+
+  return parser;
+})();
+
+/// The usage string for the command-line arguments.
+String get usage => _parser.usage;
+
+/// Parses the configuration from [args].
+///
+/// Throws a [FormatException] if [args] are invalid.
+Configuration parse(List<String> args) => _Parser(args).parse();
+
+void _parseTestSelection(
+    String option, Map<String, Set<TestSelection>> selections) {
+  if (Platform.isWindows) {
+    // If given a path that starts with what looks like a drive letter, convert it
+    // into a file scheme URI. We can't parse using `Uri.file` because we do
+    // support query parameters which aren't valid file uris.
+    if (option.indexOf(':') == 1) {
+      option = 'file:///$option';
+    }
+  }
+  final uri = Uri.parse(option);
+  final path = Uri.decodeComponent(uri.path).stripDriveLetterLeadingSlash;
+  final names = uri.queryParametersAll['name'];
+  final fullName = uri.queryParameters['full-name'];
+  final line = uri.queryParameters['line'];
+  final col = uri.queryParameters['col'];
+
+  if (names != null && names.isNotEmpty && fullName != null) {
+    throw const FormatException(
+      'Cannot specify both "name=<...>" and "full-name=<...>".',
+    );
+  }
+  final selection = TestSelection(
+    testPatterns: fullName != null
+        ? {RegExp('^${RegExp.escape(fullName)}\$')}
+        : {
+            if (names != null)
+              for (var name in names) RegExp(name)
+          },
+    line: line == null ? null : int.parse(line),
+    col: col == null ? null : int.parse(col),
+  );
+
+  selections.update(path, (selections) => selections..add(selection),
+      ifAbsent: () => {selection});
+}
+
+/// A class for parsing an argument list.
+///
+/// This is used to provide access to the arg results across helper methods.
+class _Parser {
+  /// The parsed options.
+  final ArgResults _options;
+
+  _Parser(List<String> args) : _options = _parser.parse(args);
+
+  List<String> _readMulti(String name) => _options[name] as List<String>;
+
+  /// Returns the parsed configuration.
+  Configuration parse() {
+    var patterns = [
+      for (var value in _readMulti('name'))
+        _wrapFormatException(value, () => RegExp(value), optionName: 'name'),
+      ..._readMulti('plain-name'),
+    ];
+
+    var includeTags = {..._readMulti('tags'), ..._readMulti('tag')}
+        .fold<BooleanSelector>(BooleanSelector.all, (selector, tag) {
+      return selector.intersection(BooleanSelector.parse(tag));
+    });
+
+    var excludeTags = {
+      ..._readMulti('exclude-tags'),
+      ..._readMulti('exclude-tag')
+    }.fold<BooleanSelector>(BooleanSelector.none, (selector, tag) {
+      return selector.union(BooleanSelector.parse(tag));
+    });
+
+    var shardIndex = _parseOption('shard-index', int.parse);
+    var totalShards = _parseOption('total-shards', int.parse);
+    if ((shardIndex == null) != (totalShards == null)) {
+      throw const FormatException(
+          '--shard-index and --total-shards may only be passed together.');
+    } else if (shardIndex != null) {
+      if (shardIndex < 0) {
+        throw const FormatException('--shard-index may not be negative.');
+      } else if (shardIndex >= totalShards!) {
+        throw const FormatException(
+            '--shard-index must be less than --total-shards.');
+      }
+    }
+
+    var reporter = _ifParsed('reporter') as String?;
+
+    var testRandomizeOrderingSeed =
+        _parseOption('test-randomize-ordering-seed', (value) {
+      var seed = value == 'random'
+          ? Random().nextInt(4294967295)
+          : int.parse(value).toUnsigned(32);
+
+      // TODO(#1547): Less hacky way of not breaking the json reporter
+      if (reporter != 'json') {
+        print('Shuffling test order with --test-randomize-ordering-seed=$seed');
+      }
+
+      return seed;
+    });
+
+    var color = _ifParsed<bool>('color') ?? canUseSpecialChars;
+
+    var runtimes =
+        _ifParsed<List<String>>('platform')?.map(RuntimeSelection.new).toList();
+    var compilerSelections = _ifParsed<List<String>>('compiler')
+        ?.map(CompilerSelection.parse)
+        .toList();
+    if (_ifParsed<bool>('use-data-isolate-strategy') == true) {
+      compilerSelections ??= [];
+      compilerSelections.add(CompilerSelection.parse('vm:source'));
+    }
+
+    final paths = _options.rest.isEmpty ? null : _options.rest;
+
+    Map<String, Set<TestSelection>>? selections;
+    if (paths != null) {
+      selections = {};
+      for (final path in paths) {
+        _parseTestSelection(path, selections);
+      }
+    }
+
+    if (_options.wasParsed('pub-serve')) {
+      throw ArgumentError(
+          'The --pub-serve is no longer supported, if you require it please '
+          'open an issue at https://github.com/dart-lang/test/issues/new.');
+    }
+
+    return Configuration(
+        help: _ifParsed('help'),
+        version: _ifParsed('version'),
+        verboseTrace: _ifParsed('verbose-trace'),
+        chainStackTraces: _ifParsed('chain-stack-traces'),
+        jsTrace: _ifParsed('js-trace'),
+        pauseAfterLoad: _ifParsed('pause-after-load'),
+        debug: _ifParsed('debug'),
+        color: color,
+        configurationPath: _ifParsed('configuration'),
+        dart2jsArgs: _ifParsed('dart2js-args'),
+        precompiledPath: _ifParsed<String>('precompiled'),
+        reporter: reporter,
+        fileReporters: _parseFileReporterOption(),
+        coverage: _ifParsed('coverage'),
+        concurrency: _parseOption('concurrency', int.parse),
+        shardIndex: shardIndex,
+        totalShards: totalShards,
+        timeout: _parseOption('timeout', Timeout.parse),
+        globalPatterns: patterns,
+        compilerSelections: compilerSelections,
+        runtimes: runtimes,
+        runSkipped: _ifParsed('run-skipped'),
+        chosenPresets: _ifParsed('preset'),
+        testSelections: selections,
+        includeTags: includeTags,
+        excludeTags: excludeTags,
+        noRetry: _ifParsed('no-retry'),
+        testRandomizeOrderingSeed: testRandomizeOrderingSeed,
+        ignoreTimeouts: _ifParsed('ignore-timeouts'),
+        stopOnFirstFailure: _ifParsed('fail-fast'),
+        // Config that isn't supported on the command line
+        addTags: null,
+        allowTestRandomization: null,
+        allowDuplicateTestNames: null,
+        customHtmlTemplatePath: null,
+        defineRuntimes: null,
+        filename: null,
+        foldTraceExcept: null,
+        foldTraceOnly: null,
+        onPlatform: null,
+        overrideRuntimes: null,
+        presets: null,
+        retry: null,
+        skip: null,
+        skipReason: null,
+        testOn: null,
+        tags: null);
+  }
+
+  /// Returns the parsed option for [name], or `null` if none was parsed.
+  ///
+  /// If the user hasn't explicitly chosen a value, we want to pass null values
+  /// to [Configuration.new] so that it considers those fields unset when
+  /// merging with configuration from the config file.
+  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) {
+    if (!_options.wasParsed(name)) return null;
+
+    var value = _options[name];
+    if (value == null) return null;
+
+    return _wrapFormatException(value, () => parse(value as String),
+        optionName: name);
+  }
+
+  Map<String, String>? _parseFileReporterOption() =>
+      _parseOption('file-reporter', (value) {
+        if (!value.contains(':')) {
+          throw const FormatException(
+              'option must be in the form <reporter>:<filepath>, e.g. '
+              '"json:reports/tests.json"');
+        }
+        final sep = value.indexOf(':');
+        final reporter = value.substring(0, sep);
+        if (!allReporters.containsKey(reporter)) {
+          throw FormatException('"$reporter" is not a supported reporter');
+        }
+        return {reporter: value.substring(sep + 1)};
+      });
+
+  /// Runs [parse], and wraps any [FormatException] it throws with additional
+  /// information.
+  T _wrapFormatException<T>(Object? value, T Function() parse,
+      {String? optionName}) {
+    try {
+      return parse();
+    } on FormatException catch (error) {
+      throw FormatException(
+          'Couldn\'t parse ${optionName == null ? '' : '--$optionName '}"$value": '
+          '${error.message}');
+    }
+  }
+}
+
+extension _RuntimeDescription on Runtime {
+  String get supportedCompilersText {
+    var message = StringBuffer('[$identifier]: ');
+    message.write('${defaultCompiler.identifier} (default)');
+    for (var compiler in supportedCompilers) {
+      if (compiler == defaultCompiler) continue;
+      message.write(', ${compiler.identifier}');
+    }
+    return message.toString();
+  }
+}
diff --git a/pkgs/test_core/lib/src/runner/configuration/custom_runtime.dart b/pkgs/test_core/lib/src/runner/configuration/custom_runtime.dart
new file mode 100644
index 0000000..d1bd028
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/configuration/custom_runtime.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:source_span/source_span.dart';
+import 'package:yaml/yaml.dart';
+
+/// A user-defined test runtime, based on an existing runtime but with
+/// different configuration.
+final class CustomRuntime {
+  /// The human-friendly name of the runtime.
+  final String name;
+
+  /// The location that [name] was defined in the configuration file.
+  final SourceSpan nameSpan;
+
+  /// The identifier used to look up the runtime.
+  final String identifier;
+
+  /// The location that [identifier] was defined in the configuration file.
+  final SourceSpan identifierSpan;
+
+  /// The identifier of the runtime that this extends.
+  final String parent;
+
+  /// The location that [parent] was defined in the configuration file.
+  final SourceSpan parentSpan;
+
+  /// The user's settings for this runtime.
+  final YamlMap settings;
+
+  CustomRuntime(this.name, this.nameSpan, this.identifier, this.identifierSpan,
+      this.parent, this.parentSpan, this.settings);
+}
diff --git a/pkgs/test_core/lib/src/runner/configuration/load.dart b/pkgs/test_core/lib/src/runner/configuration/load.dart
new file mode 100644
index 0000000..9877db6
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/configuration/load.dart
@@ -0,0 +1,656 @@
+// 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.
+
+import 'dart:io';
+
+import 'package:boolean_selector/boolean_selector.dart';
+import 'package:glob/glob.dart';
+import 'package:path/path.dart' as p;
+import 'package:source_span/source_span.dart';
+import 'package:test_api/scaffolding.dart' show Timeout;
+import 'package:test_api/src/backend/operating_system.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/platform_selector.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/util/identifier_regex.dart'; // ignore: implementation_imports
+import 'package:yaml/yaml.dart';
+
+import '../../util/errors.dart';
+import '../../util/io.dart';
+import '../../util/pretty_print.dart';
+import '../compiler_selection.dart';
+import '../configuration.dart';
+import '../runtime_selection.dart';
+import '../suite.dart';
+import 'custom_runtime.dart';
+import 'reporters.dart';
+import 'runtime_settings.dart';
+
+/// A regular expression matching a Dart identifier.
+///
+/// This also matches a package name, since they must be Dart identifiers.
+final _identifierRegExp = RegExp(r'[a-zA-Z_]\w*');
+
+/// A regular expression matching allowed package names.
+///
+/// This allows dot-separated valid Dart identifiers. The dots are there for
+/// compatibility with Google's internal Dart packages, but they may not be used
+/// when publishing a package to pub.dev.
+final _packageName =
+    RegExp('^${_identifierRegExp.pattern}(\\.${_identifierRegExp.pattern})*\$');
+
+/// Parses configuration from YAML formatted [content].
+///
+/// If [global] is `true`, this restricts the configuration file to only rules
+/// that are supported globally.
+///
+/// If [sourceUrl] is provided then that will be set as the source url for
+/// the yaml document.
+///
+/// Throws a [FormatException] if the configuration is invalid.
+Configuration parse(String content, {Uri? sourceUrl, bool global = false}) {
+  var document = loadYamlNode(content, sourceUrl: sourceUrl);
+
+  if (document.value == null) return Configuration.empty;
+
+  if (document is! Map) {
+    throw SourceSpanFormatException(
+        'The configuration must be a YAML map.', document.span, content);
+  }
+
+  var loader =
+      _ConfigurationLoader(document as YamlMap, content, global: global);
+  return loader.load();
+}
+
+/// A helper for [load] that tracks the YAML document.
+class _ConfigurationLoader {
+  /// The parsed configuration document.
+  final YamlMap _document;
+
+  /// The source string for [_document].
+  ///
+  /// Used for error reporting.
+  final String _source;
+
+  /// Whether this is parsing the global configuration file.
+  final bool _global;
+
+  /// Whether runner configuration is allowed at this level.
+  final bool _runnerConfig;
+
+  _ConfigurationLoader(this._document, this._source,
+      {bool global = false, bool runnerConfig = true})
+      : _global = global,
+        _runnerConfig = runnerConfig;
+
+  /// Loads the configuration in [_document].
+  Configuration load() => _loadIncludeConfig()
+      .merge(_loadGlobalTestConfig())
+      .merge(_loadLocalTestConfig())
+      .merge(_loadGlobalRunnerConfig())
+      .merge(_loadLocalRunnerConfig());
+
+  /// If an `include` node is contained in [node], merges and returns [config].
+  Configuration _loadIncludeConfig() {
+    if (!_runnerConfig) {
+      _disallow('include');
+      return Configuration.empty;
+    }
+
+    var includeNode = _document.nodes['include'];
+    if (includeNode == null) return Configuration.empty;
+
+    var includePath = _parseNode(includeNode, 'include path', p.fromUri);
+    var basePath =
+        p.join(p.dirname(p.fromUri(_document.span.sourceUrl)), includePath);
+    try {
+      return Configuration.load(basePath);
+    } on FileSystemException catch (error) {
+      throw SourceSpanFormatException(
+          getErrorMessage(error), includeNode.span, _source);
+    }
+  }
+
+  /// Loads test configuration that's allowed in the global configuration file.
+  Configuration _loadGlobalTestConfig() {
+    var verboseTrace = _getBool('verbose_trace');
+    var chainStackTraces = _getBool('chain_stack_traces');
+    var foldStackFrames = _loadFoldedStackFrames();
+    var jsTrace = _getBool('js_trace');
+
+    var timeout = _parseValue('timeout', Timeout.parse);
+
+    var onPlatform = _getMap('on_platform',
+        key: (keyNode) => _parseNode(keyNode, 'on_platform key',
+            (value) => PlatformSelector.parse(value, keyNode.span)),
+        value: (valueNode) =>
+            _nestedConfig(valueNode, 'on_platform value', runnerConfig: false));
+
+    var onOS = _getMap('on_os',
+        key: (keyNode) {
+          _validate(keyNode, 'on_os key must be a string.',
+              (value) => value is String);
+
+          var os = OperatingSystem.find(keyNode.value as String);
+          if (os != OperatingSystem.none) return os;
+
+          throw SourceSpanFormatException(
+              'Invalid on_os key: No such operating system.',
+              keyNode.span,
+              _source);
+        },
+        value: (valueNode) => _nestedConfig(valueNode, 'on_os value'));
+
+    var presets = _getMap('presets',
+        key: (keyNode) => _parseIdentifierLike(keyNode, 'presets key'),
+        value: (valueNode) => _nestedConfig(valueNode, 'presets value'));
+
+    var config = Configuration.globalTest(
+            verboseTrace: verboseTrace,
+            jsTrace: jsTrace,
+            timeout: timeout,
+            presets: presets,
+            chainStackTraces: chainStackTraces,
+            foldTraceExcept: foldStackFrames['except'],
+            foldTraceOnly: foldStackFrames['only'])
+        .merge(_extractPresets<PlatformSelector>(
+            onPlatform, Configuration.onPlatform));
+
+    var osConfig = onOS[currentOS];
+    return osConfig == null ? config : config.merge(osConfig);
+  }
+
+  /// Loads test configuration that's not allowed in the global configuration
+  /// file.
+  ///
+  /// If [_global] is `true`, this will error if there are any local test-level
+  /// configuration fields.
+  Configuration _loadLocalTestConfig() {
+    if (_global) {
+      _disallow('skip');
+      _disallow('retry');
+      _disallow('test_on');
+      _disallow('add_tags');
+      _disallow('tags');
+      _disallow('allow_test_randomization');
+      _disallow('allow_duplicate_test_names');
+      return Configuration.empty;
+    }
+
+    var skipRaw = _getValue('skip', 'boolean or string',
+        (value) => (value is bool?) || value is String?);
+    String? skipReason;
+    bool? skip;
+    if (skipRaw is String) {
+      skipReason = skipRaw;
+      skip = true;
+    } else {
+      skip = skipRaw as bool?;
+    }
+
+    var testOn = _parsePlatformSelector('test_on');
+
+    var addTags = _getList(
+        'add_tags', (tagNode) => _parseIdentifierLike(tagNode, 'Tag name'));
+
+    var tags = _getMap('tags',
+        key: (keyNode) =>
+            _parseNode(keyNode, 'tags key', BooleanSelector.parse),
+        value: (valueNode) =>
+            _nestedConfig(valueNode, 'tag value', runnerConfig: false));
+
+    var retry = _getNonNegativeInt('retry');
+
+    var allowTestRandomization = _getBool('allow_test_randomization');
+
+    var allowDuplicateTestNames = _getBool('allow_duplicate_test_names');
+
+    return Configuration.localTest(
+            skip: skip,
+            retry: retry,
+            skipReason: skipReason,
+            testOn: testOn,
+            addTags: addTags,
+            allowTestRandomization: allowTestRandomization,
+            allowDuplicateTestNames: allowDuplicateTestNames)
+        .merge(_extractPresets<BooleanSelector>(tags, Configuration.tags));
+  }
+
+  /// Loads runner configuration that's allowed in the global configuration
+  /// file.
+  ///
+  /// If [_runnerConfig] is `false`, this will error if there are any
+  /// runner-level configuration fields.
+  Configuration _loadGlobalRunnerConfig() {
+    if (!_runnerConfig) {
+      _disallow('pause_after_load');
+      _disallow('reporter');
+      _disallow('file_reporters');
+      _disallow('concurrency');
+      _disallow('names');
+      _disallow('plain_names');
+      _disallow('platforms');
+      _disallow('add_presets');
+      _disallow('override_platforms');
+      _disallow('include');
+      return Configuration.empty;
+    }
+
+    var pauseAfterLoad = _getBool('pause_after_load');
+    var runSkipped = _getBool('run_skipped');
+
+    var reporter = _getString('reporter');
+    if (reporter != null && !allReporters.keys.contains(reporter)) {
+      _error('Unknown reporter "$reporter".', 'reporter');
+    }
+
+    var fileReporters = _getMap('file_reporters', key: (keyNode) {
+      _validate(keyNode, 'file_reporters key must be a string',
+          (value) => value is String);
+      final reporter = keyNode.value as String;
+      if (!allReporters.keys.contains(reporter)) {
+        _error('Unknown reporter "$reporter".', 'file_reporters');
+      }
+      return reporter;
+    }, value: (valueNode) {
+      _validate(valueNode, 'file_reporters value must be a string',
+          (value) => value is String);
+      return valueNode.value as String;
+    });
+
+    var concurrency = _getInt('concurrency');
+
+    // The UI term "platform" corresponds with the implementation term
+    // "runtime". The [Runtime] class used to be called [TestPlatform], but it
+    // was changed to avoid conflicting with [SuitePlatform]. We decided not to
+    // also change the UI to avoid a painful migration.
+    var runtimes = _getList(
+        'platforms',
+        (runtimeNode) => RuntimeSelection(
+            _parseIdentifierLike(runtimeNode, 'Platform name'),
+            runtimeNode.span));
+
+    var compilerSelections = _getList(
+        'compilers',
+        (node) => _parseNode(
+            node,
+            'compiler',
+            (option) =>
+                CompilerSelection.parse(option, parentSpan: node.span)));
+
+    var chosenPresets = _getList('add_presets',
+        (presetNode) => _parseIdentifierLike(presetNode, 'Preset name'));
+
+    var overrideRuntimes = _loadOverrideRuntimes();
+
+    var customHtmlTemplatePath = _getString('custom_html_template_path');
+
+    return Configuration.globalRunner(
+        pauseAfterLoad: pauseAfterLoad,
+        customHtmlTemplatePath: customHtmlTemplatePath,
+        runSkipped: runSkipped,
+        reporter: reporter,
+        fileReporters: fileReporters,
+        concurrency: concurrency,
+        compilerSelections: compilerSelections,
+        runtimes: runtimes,
+        chosenPresets: chosenPresets,
+        overrideRuntimes: overrideRuntimes);
+  }
+
+  /// Loads the `override_platforms` field.
+  Map<String, RuntimeSettings> _loadOverrideRuntimes() {
+    var runtimesNode =
+        _getNode('override_platforms', 'map', (value) => value is Map)
+            as YamlMap?;
+    if (runtimesNode == null) return const {};
+
+    var runtimes = <String, RuntimeSettings>{};
+    runtimesNode.nodes.forEach((identifierNode, valueNode) {
+      var yamlNode = identifierNode as YamlNode;
+      var identifier = _parseIdentifierLike(yamlNode, 'Platform identifier');
+
+      _validate(valueNode, 'Platform definition must be a map.',
+          (value) => value is Map);
+      var map = valueNode as YamlMap;
+
+      var settings = _expect(map, 'settings');
+      _validate(settings, 'Must be a map.', (value) => value is Map);
+
+      runtimes[identifier] =
+          RuntimeSettings(identifier, yamlNode.span, [settings as YamlMap]);
+    });
+    return runtimes;
+  }
+
+  /// Loads runner configuration that's not allowed in the global configuration
+  /// file.
+  ///
+  /// If [_runnerConfig] is `false` or if [_global] is `true`, this will error
+  /// if there are any local test-level configuration fields.
+  Configuration _loadLocalRunnerConfig() {
+    if (!_runnerConfig || _global) {
+      _disallow('names');
+      _disallow('plain_names');
+      _disallow('paths');
+      _disallow('filename');
+      _disallow('include_tags');
+      _disallow('exclude_tags');
+      _disallow('define_platforms');
+      return Configuration.empty;
+    }
+
+    var patterns = _getList('names', (nameNode) {
+      _validate(nameNode, 'Names must be strings.', (value) => value is String);
+      return _parseNode(nameNode, 'name', RegExp.new);
+    })
+      ..addAll(_getList('plain_names', (nameNode) {
+        _validate(
+            nameNode, 'Names must be strings.', (value) => value is String);
+        return _parseNode(nameNode, 'name', RegExp.new);
+      }));
+
+    var paths = _getList('paths', (pathNode) {
+      _validate(pathNode, 'Paths must be strings.', (value) => value is String);
+      _validate(pathNode, 'Paths must be relative.',
+          (value) => p.url.isRelative(value as String));
+      return _parseNode(pathNode, 'path', p.fromUri);
+    });
+
+    var filename = _parseValue('filename', Glob.new);
+
+    var includeTags = _parseBooleanSelector('include_tags');
+    var excludeTags = _parseBooleanSelector('exclude_tags');
+
+    var defineRuntimes = _loadDefineRuntimes();
+
+    return Configuration.localRunner(
+        globalPatterns: patterns,
+        testSelections: {
+          for (var path in paths) path: const {TestSelection()}
+        },
+        filename: filename,
+        includeTags: includeTags,
+        excludeTags: excludeTags,
+        defineRuntimes: defineRuntimes);
+  }
+
+  /// Returns a map representation of the `fold_stack_frames` configuration.
+  ///
+  /// The key `except` will correspond to the list of packages to fold.
+  /// The key `only` will correspond to the list of packages to keep in a
+  /// test [Chain].
+  Map<String, List<String>> _loadFoldedStackFrames() {
+    var foldOptionSet = false;
+    return _getMap('fold_stack_frames', key: (keyNode) {
+      _validate(keyNode, 'Must be a string', (value) => value is String);
+      _validate(keyNode, 'Must be "only" or "except".',
+          (value) => value == 'only' || value == 'except');
+
+      if (foldOptionSet) {
+        throw SourceSpanFormatException(
+            'Can only contain one of "only" or "except".',
+            keyNode.span,
+            _source);
+      }
+      foldOptionSet = true;
+      return keyNode.value as String;
+    }, value: (valueNode) {
+      _validate(
+          valueNode,
+          'Folded packages must be strings.',
+          (valueList) =>
+              valueList is YamlList &&
+              valueList.every((value) => value is String));
+
+      _validate(
+          valueNode,
+          'Invalid package name.',
+          (valueList) => (valueList as Iterable)
+              .every((value) => _packageName.hasMatch(value as String)));
+
+      return List<String>.from(valueNode.value as Iterable);
+    });
+  }
+
+  /// Loads the `define_platforms` field.
+  Map<String, CustomRuntime> _loadDefineRuntimes() {
+    var runtimesNode =
+        _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 yamlNode = identifierNode as YamlNode;
+      var identifier = _parseIdentifierLike(yamlNode, 'Platform identifier');
+
+      _validate(valueNode, 'Platform definition must be a map.',
+          (value) => value is Map);
+      var map = valueNode as YamlMap;
+
+      var nameNode = _expect(map, 'name');
+      _validate(nameNode, 'Must be a string.', (value) => value is String);
+      var name = nameNode.value as String;
+
+      var parentNode = _expect(map, 'extends');
+      var parent = _parseIdentifierLike(parentNode, 'Platform parent');
+
+      var settings = _expect(map, 'settings');
+      _validate(settings, 'Must be a map.', (value) => value is Map);
+
+      runtimes[identifier] = CustomRuntime(name, nameNode.span, identifier,
+          yamlNode.span, parent, parentNode.span, settings as YamlMap);
+    });
+    return runtimes;
+  }
+
+  /// Throws an exception with [message] if [test] returns `false` when passed
+  /// [node]'s value.
+  void _validate(YamlNode node, String message, bool Function(dynamic) test) {
+    if (test(node.value)) return;
+    throw SourceSpanFormatException(message, node.span, _source);
+  }
+
+  /// Returns the value of the node at [field].
+  ///
+  /// If [typeTest] returns `false` for that value, instead throws an error
+  /// complaining that the field is not a [typeName].
+  Object? _getValue(
+      String field, String typeName, bool Function(dynamic) typeTest) {
+    var value = _document[field];
+    if (value == null || typeTest(value)) return value;
+    _error('$field must be ${a(typeName)}.', field);
+  }
+
+  /// Returns the YAML node at [field].
+  ///
+  /// If [typeTest] returns `false` for that node's value, instead throws an
+  /// error complaining that the field is not a [typeName].
+  ///
+  /// 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;
+    _validate(node, '$field must be ${a(typeName)}.', typeTest);
+    return node;
+  }
+
+  /// Asserts that [field] is an int and returns its value.
+  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) {
+        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?;
+
+  /// Asserts that [field] is a string and returns its value.
+  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?;
+    if (node == null) return [];
+    return node.nodes.map(forElement).toList();
+  }
+
+  /// Asserts that [field] is a map and runs [key] and [value] for each pair.
+  ///
+  /// 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?;
+    if (node == null) return {};
+
+    key ??= (keyNode) {
+      _validate(
+          keyNode, '$field keys must be strings.', (value) => value is String);
+
+      return keyNode.value as K;
+    };
+
+    value ??= (valueNode) {
+      _validate(valueNode, '$field values must be strings.',
+          (value) => value is String);
+
+      return valueNode.value as V;
+    };
+
+    return node.nodes.map((keyNode, valueNode) =>
+        MapEntry(key!(keyNode as YamlNode), value!(valueNode)));
+  }
+
+  /// Verifies that [node]'s value is an optionally hyphenated Dart identifier,
+  /// and returns it
+  String _parseIdentifierLike(YamlNode node, String name) {
+    _validate(node, '$name must be a string.', (value) => value is String);
+    _validate(node, '$name must be an (optionally hyphenated) Dart identifier.',
+        (value) => (value as String).contains(anchoredHyphenatedIdentifier));
+    return node.value as String;
+  }
+
+  /// Parses [node]'s value as a boolean selector.
+  BooleanSelector? _parseBooleanSelector(String name) =>
+      _parseValue(name, BooleanSelector.parse);
+
+  /// Parses [node]'s value as a platform selector.
+  PlatformSelector? _parsePlatformSelector(String field) {
+    var node = _document.nodes[field];
+    if (node == null) return null;
+    return _parseNode(
+        node, field, (value) => PlatformSelector.parse(value, node.span));
+  }
+
+  /// Asserts that [node] is a string, passes its value to [parse], and returns
+  /// the result.
+  ///
+  /// If [parse] throws a [FormatException], it's wrapped to include [node]'s
+  /// span.
+  T _parseNode<T>(YamlNode node, String name, T Function(String) parse) {
+    _validate(node, '$name must be a string.', (value) => value is String);
+
+    try {
+      return parse(node.value as String);
+    } on FormatException catch (error) {
+      throw SourceSpanFormatException(
+          'Invalid $name: ${error.message}', node.span, _source);
+    }
+  }
+
+  /// Asserts that [field] is a string, passes it to [parse], and returns the
+  /// result.
+  ///
+  /// If [parse] throws a [FormatException], it's wrapped to include [field]'s
+  /// span.
+  T? _parseValue<T>(String field, T Function(String) parse) {
+    var node = _document.nodes[field];
+    if (node == null) return null;
+    return _parseNode(node, field, parse);
+  }
+
+  /// Parses a nested configuration document.
+  ///
+  /// [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}) {
+    if (node == null || node.value == null) return Configuration.empty;
+
+    _validate(node, '$name must be a map.', (value) => value is Map);
+    var loader = _ConfigurationLoader(node as YamlMap, _source,
+        global: _global, runnerConfig: runnerConfig ?? _runnerConfig);
+    return loader.load();
+  }
+
+  /// Takes a map that contains [Configuration]s and extracts any
+  /// preset-specific configuration into a parent [Configuration].
+  ///
+  /// This is needed because parameters to [Configuration.new] such as
+  /// `onPlatform` take maps to [SuiteConfiguration]s. [SuiteConfiguration]
+  /// doesn't support preset-specific configuration, so this extracts the preset
+  /// logic into a parent [Configuration], leaving only maps to
+  /// [SuiteConfiguration]s. The [create] function is used to construct
+  /// [Configuration]s from the resolved maps.
+  Configuration _extractPresets<T>(Map<T, Configuration> map,
+      Configuration Function(Map<T, SuiteConfiguration>) create) {
+    if (map.isEmpty) return Configuration.empty;
+
+    var base = <T, SuiteConfiguration>{};
+    var presets = <String, Map<T, SuiteConfiguration>>{};
+    map.forEach((key, config) {
+      base[key] = config.suiteDefaults;
+      config.presets.forEach((preset, presetConfig) {
+        presets.putIfAbsent(preset, () => {})[key] = presetConfig.suiteDefaults;
+      });
+    });
+
+    if (presets.isEmpty) {
+      return base.isEmpty ? Configuration.empty : create(base);
+    } else {
+      var newPresets = presets.map((key, map) => MapEntry(key, create(map)));
+      return create(base).change(presets: newPresets);
+    }
+  }
+
+  /// Asserts that [map] has a field named [field] and returns it.
+  YamlNode _expect(YamlMap map, String field) {
+    var node = map.nodes[field];
+    if (node != null) return node;
+
+    throw SourceSpanFormatException(
+        'Missing required field "$field".', map.span, _source);
+  }
+
+  /// Throws an error if a field named [field] exists at this level.
+  void _disallow(String field) {
+    if (!_document.containsKey(field)) return;
+
+    throw SourceSpanFormatException(
+        "$field isn't supported here.",
+        // We need the key as a [YamlNode] to get its span.
+        (_document.nodes.keys.firstWhere((key) => key.value == field)
+                as YamlNode)
+            .span,
+        _source);
+  }
+
+  /// Throws a [SourceSpanFormatException] with [message] about [field].
+  Never _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
new file mode 100644
index 0000000..0f0fe01
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/configuration/reporters.dart
@@ -0,0 +1,91 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:collection';
+import 'dart:io';
+
+import '../../util/io.dart';
+import '../configuration.dart';
+import '../engine.dart';
+import '../reporter.dart';
+import '../reporter/compact.dart';
+import '../reporter/expanded.dart';
+import '../reporter/failures_only.dart';
+import '../reporter/github.dart';
+import '../reporter/json.dart';
+
+/// Constructs a reporter for the provided engine with the provided
+/// configuration.
+typedef ReporterFactory = Reporter Function(Configuration, Engine, StringSink);
+
+/// Container for a reporter description and corresponding factory.
+class ReporterDetails {
+  final String description;
+  final ReporterFactory factory;
+  ReporterDetails(this.description, this.factory);
+}
+
+/// All reporters and their corresponding details.
+final UnmodifiableMapView<String, ReporterDetails> allReporters =
+    UnmodifiableMapView<String, ReporterDetails>(_allReporters);
+
+final _allReporters = <String, ReporterDetails>{
+  'expanded': ReporterDetails(
+      'A separate line for each update.',
+      (config, engine, sink) => ExpandedReporter.watch(engine, sink,
+          color: config.color,
+          printPath: config.testSelections.length > 1 ||
+              Directory(config.testSelections.keys.single).existsSync(),
+          printPlatform: config.suiteDefaults.runtimes.length > 1 ||
+              config.suiteDefaults.compilerSelections != null)),
+  'compact': ReporterDetails(
+      'A single line, updated continuously.',
+      (config, engine, sink) => CompactReporter.watch(engine, sink,
+          color: config.color,
+          printPath: config.testSelections.length > 1 ||
+              Directory(config.testSelections.keys.single).existsSync(),
+          printPlatform: config.suiteDefaults.runtimes.length > 1 ||
+              config.suiteDefaults.compilerSelections != null)),
+  'failures-only': ReporterDetails(
+      'A separate line for failing tests with no output for passing tests',
+      (config, engine, sink) => FailuresOnlyReporter.watch(engine, sink,
+          color: config.color,
+          printPath: config.testSelections.length > 1 ||
+              Directory(config.testSelections.keys.single).existsSync(),
+          printPlatform: config.suiteDefaults.runtimes.length > 1 ||
+              config.suiteDefaults.compilerSelections != null)),
+  'github': ReporterDetails(
+      'A custom reporter for GitHub Actions '
+      '(the default reporter when running on GitHub Actions).',
+      (config, engine, sink) => GithubReporter.watch(engine, sink,
+          printPath: config.testSelections.length > 1 ||
+              Directory(config.testSelections.keys.single).existsSync(),
+          printPlatform: config.suiteDefaults.runtimes.length > 1 ||
+              config.suiteDefaults.compilerSelections != null)),
+  'json': ReporterDetails(
+      'A machine-readable format (see '
+      'https://dart.dev/go/test-docs/json_reporter.md).',
+      (config, engine, sink) =>
+          JsonReporter.watch(engine, sink, isDebugRun: config.debug)),
+  'silent': ReporterDetails(
+      'A reporter with no output. '
+      'May be useful when only the exit code is meaningful.',
+      (config, engine, sink) => SilentReporter()),
+};
+
+final defaultReporter = inTestTests
+    ? 'expanded'
+    : inGithubContext
+        ? 'github'
+        : canUseSpecialChars
+            ? 'compact'
+            : 'expanded';
+
+/// **Do not call this function without express permission from the test package
+/// authors**.
+///
+/// This globally registers a reporter.
+void registerReporter(String name, ReporterDetails reporterDetails) {
+  _allReporters[name] = reporterDetails;
+}
diff --git a/pkgs/test_core/lib/src/runner/configuration/runtime_settings.dart b/pkgs/test_core/lib/src/runner/configuration/runtime_settings.dart
new file mode 100644
index 0000000..e366b4d
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/configuration/runtime_settings.dart
@@ -0,0 +1,26 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:source_span/source_span.dart';
+import 'package:yaml/yaml.dart';
+
+import '../plugin/customizable_platform.dart';
+
+/// User-defined settings for a built-in test runtime.
+final class RuntimeSettings {
+  /// The identifier used to look up the runtime being overridden.
+  final String identifier;
+
+  /// The location that [identifier] was defined in the configuration file.
+  final SourceSpan identifierSpan;
+
+  /// The user's settings for this runtime.
+  ///
+  /// This is a list of settings, from most global to most specific, that will
+  /// eventually be merged using [CustomizablePlatform.mergePlatformSettings].
+  final List<YamlMap> settings;
+
+  RuntimeSettings(this.identifier, this.identifierSpan, List<YamlMap> settings)
+      : settings = List.unmodifiable(settings);
+}
diff --git a/pkgs/test_core/lib/src/runner/configuration/utils.dart b/pkgs/test_core/lib/src/runner/configuration/utils.dart
new file mode 100644
index 0000000..d47526c
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/configuration/utils.dart
@@ -0,0 +1,16 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:collection/collection.dart';
+
+/// Like [mergeMaps], but assumes both maps are unmodifiable and so avoids
+/// creating a new map unnecessarily.
+///
+/// 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}) {
+  if (map1.isEmpty) return map2;
+  if (map2.isEmpty) return map1;
+  return mergeMaps(map1, map2, value: value);
+}
diff --git a/pkgs/test_core/lib/src/runner/configuration/values.dart b/pkgs/test_core/lib/src/runner/configuration/values.dart
new file mode 100644
index 0000000..6e2f50a
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/configuration/values.dart
@@ -0,0 +1,19 @@
+// 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.
+
+import 'dart:io';
+import 'dart:math' as math;
+
+import 'package:glob/glob.dart';
+
+/// The default number of test suites to run at once.
+///
+/// This defaults to half the available processors, since presumably some of
+/// them will be used for the OS and other processes.
+final defaultConcurrency = math.max(1, Platform.numberOfProcessors ~/ 2);
+
+/// The default filename pattern.
+///
+/// This is stored here so that we don't have to recompile it multiple times.
+final defaultFilename = Glob('*_test.dart');
diff --git a/pkgs/test_core/lib/src/runner/console.dart b/pkgs/test_core/lib/src/runner/console.dart
new file mode 100644
index 0000000..3a4d6fa
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/console.dart
@@ -0,0 +1,109 @@
+// 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.
+
+import 'dart:async';
+import 'dart:io';
+import 'dart:math' as math;
+
+import 'package:async/async.dart';
+
+import '../util/io.dart';
+
+/// An interactive console for taking user commands.
+class Console {
+  /// The registered commands.
+  final _commands = <String, _Command>{};
+
+  /// The pending next line of standard input, if we're waiting on one.
+  CancelableOperation? _nextLine;
+
+  /// Whether the console is currently running.
+  bool _running = false;
+
+  /// The terminal escape for red text, or the empty string if this is Windows
+  /// or not outputting to a terminal.
+  final String _red;
+
+  /// The terminal escape for bold text, or the empty string if this is
+  /// Windows or not outputting to a terminal.
+  final String _bold;
+
+  /// The terminal escape for removing test coloring, or the empty string if
+  /// this is Windows or not outputting to a terminal.
+  final String _noColor;
+
+  /// Creates a new [Console].
+  ///
+  /// If [color] is true, this uses Unix terminal colors.
+  Console({bool color = true})
+      : _red = color ? '\u001b[31m' : '',
+        _bold = color ? '\u001b[1m' : '',
+        _noColor = color ? '\u001b[0m' : '' {
+    registerCommand('help', 'Displays this help information.', _displayHelp);
+  }
+
+  /// Registers a command to be run whenever the user types [name].
+  ///
+  /// The [description] should be a one-line description of the command to print
+  /// in the help output. The [body] callback will be called when the user types
+  /// the command.
+  void registerCommand(
+      String name, String description, FutureOr<void> Function() body) {
+    if (_commands.containsKey(name)) {
+      throw ArgumentError('The console already has a command named "$name".');
+    }
+
+    _commands[name] = (name: name, description: description, body: body);
+  }
+
+  /// Starts running the console.
+  ///
+  /// This prints the initial prompt and loops while waiting for user input.
+  void start() {
+    _running = true;
+    unawaited(() async {
+      while (_running) {
+        stdout.write('> ');
+        _nextLine = stdinLines.cancelable((queue) => queue.next);
+        var commandName = await _nextLine!.value;
+        _nextLine = null;
+
+        var command = _commands[commandName];
+        if (command == null) {
+          stderr.writeln(
+              '${_red}Unknown command $_bold$commandName$_noColor$_red.'
+              '$_noColor');
+        } else {
+          await command.body();
+        }
+      }
+    }());
+  }
+
+  /// Stops the console running.
+  void stop() {
+    _running = false;
+    if (_nextLine != null) {
+      stdout.writeln();
+      _nextLine!.cancel();
+    }
+  }
+
+  /// Displays the help info for the console commands.
+  void _displayHelp() {
+    var maxCommandLength =
+        _commands.values.map((command) => command.name.length).reduce(math.max);
+
+    for (var command in _commands.values) {
+      var name = command.name.padRight(maxCommandLength + 4);
+      print('$_bold$name$_noColor${command.description}');
+    }
+  }
+}
+
+typedef _Command = ({
+  String name,
+  String description,
+  FutureOr<void> Function() body,
+});
diff --git a/pkgs/test_core/lib/src/runner/coverage.dart b/pkgs/test_core/lib/src/runner/coverage.dart
new file mode 100644
index 0000000..841bdd7
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/coverage.dart
@@ -0,0 +1,24 @@
+// 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.
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+
+import 'live_suite_controller.dart';
+
+/// Collects coverage and outputs to the [coveragePath] path.
+Future<void> writeCoverage(
+    String coveragePath, LiveSuiteController controller) async {
+  var suite = controller.liveSuite.suite;
+  var coverage = await controller.liveSuite.suite.gatherCoverage();
+  final outfile = File(p.join(coveragePath,
+      '${suite.path}.${suite.platform.runtime.name.toLowerCase()}.json'))
+    ..createSync(recursive: true);
+  final out = outfile.openWrite();
+  out.write(json.encode(coverage));
+  await out.flush();
+  await out.close();
+}
diff --git a/pkgs/test_core/lib/src/runner/coverage_stub.dart b/pkgs/test_core/lib/src/runner/coverage_stub.dart
new file mode 100644
index 0000000..64f69c7
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/coverage_stub.dart
@@ -0,0 +1,10 @@
+// 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.
+
+import 'live_suite_controller.dart';
+
+Future<void> writeCoverage(
+        String coveragePath, LiveSuiteController controller) =>
+    throw UnsupportedError(
+        'Coverage is only supported through the test runner.');
diff --git a/pkgs/test_core/lib/src/runner/dart2js_compiler_pool.dart b/pkgs/test_core/lib/src/runner/dart2js_compiler_pool.dart
new file mode 100644
index 0000000..a13a65a
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/dart2js_compiler_pool.dart
@@ -0,0 +1,130 @@
+// Copyright (c) 2022, 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';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+
+import '../util/dart.dart';
+import '../util/io.dart';
+import '../util/package_config.dart';
+import 'compiler_pool.dart';
+import 'suite.dart';
+
+/// A regular expression matching the first status line printed by dart2js.
+final _dart2jsStatus =
+    RegExp(r'^Dart file \(.*\) compiled to JavaScript: .*\n?');
+
+/// A pool of `dart2js` instances.
+///
+/// This limits the number of compiler instances running concurrently.
+class Dart2JsCompilerPool extends CompilerPool {
+  /// Extra arguments to pass to dart2js.
+  final List<String> _extraArgs;
+
+  /// The currently-active dart2js processes.
+  final _processes = <Process>{};
+
+  /// Creates a compiler pool that multiple instances of `dart2js` at once.
+  Dart2JsCompilerPool([Iterable<String>? extraArgs])
+      : _extraArgs = extraArgs?.toList() ?? const [];
+
+  /// Compiles [code] to [path].
+  ///
+  /// This wraps the Dart code in the standard browser-testing wrapper.
+  ///
+  /// The returned [Future] will complete once the `dart2js` process completes
+  /// *and* all its output has been printed to the command line.
+  @override
+  Future compileInternal(
+      String code, String path, SuiteConfiguration suiteConfig) {
+    return withTempDir((dir) async {
+      var wrapperPath = p.join(dir, 'runInBrowser.dart');
+      File(wrapperPath).writeAsStringSync(code);
+
+      var args = [
+        'compile',
+        'js',
+        for (var experiment in enabledExperiments)
+          '--enable-experiment=$experiment',
+        '--enable-asserts',
+        wrapperPath,
+        '--out=$path',
+        '--packages=${await packageConfigUri}',
+        '--disable-program-split',
+        ..._extraArgs,
+        ...suiteConfig.dart2jsArgs
+      ];
+
+      if (config.color) args.add('--enable-diagnostic-colors');
+
+      var process = await Process.start(Platform.resolvedExecutable, args);
+      if (closed) {
+        process.kill();
+        return;
+      }
+
+      _processes.add(process);
+
+      /// Wait until the process is entirely done to print out any output.
+      /// This can produce a little extra time for users to wait with no
+      /// update, but it also avoids some really nasty-looking interleaved
+      /// output. Write both stdout and stderr to the same buffer in case
+      /// they're intended to be printed in order.
+      var buffer = StringBuffer();
+
+      await Future.wait([
+        process.stdout.transform(utf8.decoder).forEach(buffer.write),
+        process.stderr.transform(utf8.decoder).forEach(buffer.write),
+      ]);
+
+      var exitCode = await process.exitCode;
+      _processes.remove(process);
+      if (closed) return;
+
+      var output = buffer.toString().replaceFirst(_dart2jsStatus, '');
+      if (output.isNotEmpty) print(output);
+
+      if (exitCode != 0) throw 'dart2js failed.';
+
+      _fixSourceMap('$path.map');
+    });
+  }
+
+  /// Fix up the source map at [mapPath] so that it points to absolute file:
+  /// URIs that are resolvable by the browser.
+  void _fixSourceMap(String mapPath) {
+    var map = jsonDecode(File(mapPath).readAsStringSync()) as Map;
+    var root = map['sourceRoot'] as String;
+
+    final mapUri = p.toUri(mapPath);
+    map.cast<String, List<Object?>>().update(
+          'sources',
+          (sources) => [
+            for (var source in sources)
+              switch (Uri.parse('$root$source')) {
+                Uri(hasScheme: true) && final uri => uri.toString(),
+                Uri(:final path) when path.endsWith('/runInBrowser.dart') => '',
+                final uri => mapUri.resolveUri(uri).toString(),
+              }
+          ],
+        );
+
+    File(mapPath).writeAsStringSync(jsonEncode(map));
+  }
+
+  /// Closes the compiler pool.
+  ///
+  /// This kills all currently-running compilers and ensures that no more will
+  /// be started. It returns a [Future] that completes once all the compilers
+  /// have been killed and all resources released.
+  @override
+  Future<void> closeInternal() async {
+    await Future.wait(_processes.map((process) async {
+      process.kill();
+      await process.exitCode;
+    }));
+  }
+}
diff --git a/pkgs/test_core/lib/src/runner/debugger.dart b/pkgs/test_core/lib/src/runner/debugger.dart
new file mode 100644
index 0000000..5e59d88
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/debugger.dart
@@ -0,0 +1,219 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+
+import '../util/async.dart';
+import '../util/io.dart';
+import 'configuration.dart';
+import 'console.dart';
+import 'engine.dart';
+import 'load_suite.dart';
+import 'reporter.dart';
+import 'runner_suite.dart';
+
+/// Runs [loadSuite] in debugging mode.
+///
+/// Runs the suite's tests using [engine]. The [reporter] should already be
+/// watching [engine], and the [config] should contain the user configuration
+/// for the test runner.
+///
+/// Returns a [CancelableOperation] that will complete once the suite has
+/// finished running. If the operation is canceled, the debugger will clean up
+/// any resources it allocated.
+CancelableOperation debug(
+    Engine engine, Reporter reporter, LoadSuite loadSuite) {
+  _Debugger? debugger;
+  var canceled = false;
+  return CancelableOperation.fromFuture(() async {
+    engine.suiteSink.add(loadSuite.changeSuite((runnerSuite) {
+      engine.pause();
+      return runnerSuite;
+    }));
+
+    var suite = await loadSuite.suite;
+    if (canceled || suite == null) return;
+
+    await (debugger = _Debugger(engine, reporter, suite)).run();
+  }(), onCancel: () {
+    canceled = true;
+    // Make sure the load test finishes so the engine can close.
+    engine.resume();
+    debugger?.close();
+  });
+}
+
+// TODO(nweiz): Test using the console and restarting a test once sdk#25369 is
+// fixed and the VM service client is released
+/// A debugger for a single test suite.
+class _Debugger {
+  /// The test runner configuration.
+  final _config = Configuration.current;
+
+  /// The engine that will run the suite.
+  final Engine _engine;
+
+  /// The reporter that's reporting [_engine]'s progress.
+  final Reporter _reporter;
+
+  /// The suite to run.
+  final RunnerSuite _suite;
+
+  /// The console through which the user can control the debugger.
+  ///
+  /// This is only visible when the test environment is paused, so as not to
+  /// overlap with the reporter's reporting.
+  final Console _console;
+
+  /// A completer that's used to manually unpause the test if the debugger is
+  /// closed.
+  final _pauseCompleter = CancelableCompleter<void>();
+
+  /// The subscription to [_suite.onDebugging].
+  StreamSubscription<bool>? _onDebuggingSubscription;
+
+  /// The subscription to [_suite.environment.onRestart].
+  late final StreamSubscription _onRestartSubscription;
+
+  /// Whether [close] has been called.
+  bool _closed = false;
+
+  bool get _json => _config.reporter == 'json';
+
+  _Debugger(this._engine, this._reporter, this._suite)
+      : _console = Console(color: Configuration.current.color) {
+    _console.registerCommand('restart',
+        'Restart the current test after it finishes running.', _restartTest);
+
+    _onRestartSubscription = _suite.environment.onRestart.listen((_) {
+      _restartTest();
+    });
+  }
+
+  /// Runs the debugger.
+  ///
+  /// This prints information about the suite's debugger, then once the user has
+  /// had a chance to set breakpoints, runs the suite's tests.
+  Future run() async {
+    try {
+      await _pause();
+      if (_closed) return;
+
+      _onDebuggingSubscription = _suite.onDebugging.listen((debugging) {
+        if (debugging) {
+          _onDebugging();
+        } else {
+          _onNotDebugging();
+        }
+      });
+
+      _engine.resume();
+      await _engine.onIdle.first;
+    } finally {
+      close();
+    }
+  }
+
+  /// Prints URLs for the [_suite]'s debugger and waits for the user to tell the
+  /// suite to run.
+  Future _pause() async {
+    if (!_suite.environment.supportsDebugging) return;
+
+    try {
+      if (!_json) {
+        _reporter.pause();
+
+        var bold = _config.color ? '\u001b[1m' : '';
+        var yellow = _config.color ? '\u001b[33m' : '';
+        var noColor = _config.color ? '\u001b[0m' : '';
+        print('');
+
+        var runtime = _suite.platform.runtime;
+        if (runtime.isDartVM) {
+          var url = _suite.environment.observatoryUrl;
+          if (url == null) {
+            print('${yellow}Observatory URL not found.$noColor');
+          } else {
+            print('Observatory URL: $bold$url$noColor');
+          }
+        }
+
+        if (runtime.isHeadless && !runtime.isDartVM) {
+          var url = _suite.environment.remoteDebuggerUrl;
+          if (url == null) {
+            print('${yellow}Remote debugger URL not found.$noColor');
+          } else {
+            print('Remote debugger URL: $bold$url$noColor');
+          }
+        }
+
+        var buffer = StringBuffer('${bold}The test runner is paused.$noColor ');
+        if (runtime.isDartVM) {
+          buffer.write('Open the Observatory ');
+        } else {
+          if (!runtime.isHeadless) {
+            buffer.write('Open the dev console in $runtime ');
+          } else {
+            buffer.write('Open the remote debugger ');
+          }
+        }
+
+        buffer.write("and set breakpoints. Once you're finished, return to "
+            'this terminal and press Enter.');
+
+        print(wordWrap(buffer.toString()));
+      }
+
+      await inCompletionOrder([
+        _suite.environment.displayPause(),
+        stdinLines.cancelable((queue) => queue.next),
+        _pauseCompleter.operation
+      ]).first;
+    } finally {
+      if (!_json) _reporter.resume();
+    }
+  }
+
+  /// Handles the environment pausing to debug.
+  ///
+  /// This starts the interactive console.
+  void _onDebugging() {
+    if (!_json) _reporter.pause();
+
+    if (!_json) {
+      print('\nEntering debugging console. Type "help" for help.');
+    }
+    _console.start();
+  }
+
+  /// Handles the environment starting up again.
+  ///
+  /// This closes the interactive console.
+  void _onNotDebugging() {
+    if (!_json) _reporter.resume();
+    _console.stop();
+  }
+
+  /// Restarts the current test.
+  void _restartTest() {
+    if (_engine.active.isEmpty) return;
+    var liveTest = _engine.active.single;
+    _engine.restartTest(liveTest);
+    if (!_json) {
+      print(wordWrap(
+          'Will restart "${liveTest.test.name}" once it finishes running.'));
+    }
+  }
+
+  /// Closes the debugger and releases its resources.
+  void close() {
+    _pauseCompleter.complete();
+    _closed = true;
+    _onDebuggingSubscription?.cancel();
+    _onRestartSubscription.cancel();
+    _console.stop();
+  }
+}
diff --git a/pkgs/test_core/lib/src/runner/engine.dart b/pkgs/test_core/lib/src/runner/engine.dart
new file mode 100644
index 0000000..6a63627
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/engine.dart
@@ -0,0 +1,553 @@
+// 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.
+
+import 'dart:async';
+import 'dart:math';
+
+import 'package:async/async.dart' hide Result;
+import 'package:collection/collection.dart';
+import 'package:pool/pool.dart';
+import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/invoker.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/live_test_controller.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/message.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/test.dart'; // ignore: implementation_imports
+
+import 'coverage_stub.dart' if (dart.library.io) 'coverage.dart';
+import 'live_suite.dart';
+import 'live_suite_controller.dart';
+import 'load_suite.dart';
+import 'runner_suite.dart';
+import 'util/iterable_set.dart';
+
+/// An [Engine] manages a run that encompasses multiple test suites.
+///
+/// Test suites are provided by passing them into [suiteSink]. Once all suites
+/// have been provided, the user should close [suiteSink] to indicate this.
+/// [run] won't terminate until [suiteSink] is closed. Suites will be run in the
+/// order they're provided to [suiteSink]. Tests within those suites will
+/// likewise be run in the order they're declared.
+///
+/// The current status of every test is visible via [liveTests]. [onTestStarted]
+/// can also be used to be notified when a test is about to be run.
+///
+/// The engine has some special logic for [LoadSuite]s and the tests they
+/// contain, referred to as "load tests". Load tests exist to provide visibility
+/// into the process of loading test files. As long as that process is
+/// proceeding normally users usually don't care about it, so the engine does
+/// not include them in [liveTests] and other collections.
+/// If a load test fails, it will be added to [failed] and [liveTests].
+///
+/// The test suite loaded by a load suite will be automatically be run by the
+/// engine; it doesn't need to be added to [suiteSink] manually.
+///
+/// Load tests will always be emitted through [onTestStarted] so users can watch
+/// their event streams once they start running.
+class Engine {
+  /// Whether [run] has been called yet.
+  var _runCalled = false;
+
+  /// Whether [close] has been called.
+  var _closed = false;
+
+  /// Whether [close] was called before all the tests finished running.
+  ///
+  /// 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;
+
+  /// The coverage output directory.
+  String? _coverage;
+
+  /// The seed used to generate randomness for test case shuffling.
+  ///
+  /// If null or zero no shuffling will occur.
+  /// The same seed will shuffle the tests in the same way every time.
+  int? testRandomizeOrderingSeed;
+
+  /// Whether to stop running tests after a failure.
+  bool _stopOnFirstFailure;
+
+  /// A pool that limits the number of test suites running concurrently.
+  final Pool _runPool;
+
+  /// A completer that will complete when this engine is unpaused.
+  ///
+  /// `null` if this engine is not paused.
+  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;
+
+  /// 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 {
+    await Future.wait(<Future>[_group.future, _runPool.done], eagerError: true);
+    if (_closedBeforeDone!) return null;
+    return liveTests.every((liveTest) =>
+        liveTest.state.result.isPassing &&
+        liveTest.state.status == Status.complete);
+  }
+
+  /// A group of futures for each test suite.
+  final _group = FutureGroup<void>();
+
+  /// All of the engine's stream subscriptions.
+  final _subscriptions = <StreamSubscription>{};
+
+  /// A sink used to pass [RunnerSuite]s in to the engine to run.
+  ///
+  /// Suites may be added as quickly as they're available; the Engine will only
+  /// run as many as necessary at a time based on its concurrency settings.
+  ///
+  /// Suites added to the sink will be closed by the engine based on its
+  /// internal logic.
+  Sink<RunnerSuite> get suiteSink => DelegatingSink(_suiteController.sink);
+  final _suiteController = StreamController<RunnerSuite>();
+
+  /// All the [RunnerSuite]s added to [suiteSink] so far.
+  ///
+  /// Note that if a [LoadSuite] is added, this will only contain that suite,
+  /// not the suite it loads.
+  Set<RunnerSuite> get addedSuites => UnmodifiableSetView(_addedSuites);
+  final _addedSuites = <RunnerSuite>{};
+
+  /// A broadcast stream that emits each [RunnerSuite] as it's added to the
+  /// engine via [suiteSink].
+  ///
+  /// Note that if a [LoadSuite] is added, this will only return that suite, not
+  /// the suite it loads.
+  ///
+  /// This is guaranteed to fire after the suite is added to [addedSuites].
+  Stream<RunnerSuite> get onSuiteAdded => _onSuiteAddedController.stream;
+  final _onSuiteAddedController = StreamController<RunnerSuite>.broadcast();
+
+  /// A broadcast stream that emits each [LiveSuite] as it's loaded.
+  ///
+  /// Note that unlike [onSuiteAdded], for suites that are loaded using
+  /// [LoadSuite]s, both the [LoadSuite] and the suite it loads will eventually
+  /// be emitted by this stream.
+  Stream<LiveSuite> get onSuiteStarted => _onSuiteStartedController.stream;
+  final _onSuiteStartedController = StreamController<LiveSuite>.broadcast();
+
+  /// All the currently-known tests that have run or are running.
+  ///
+  /// These are [LiveTest]s, representing the in-progress state of each test.
+  /// Tests that have not yet begun running are marked [Status.pending]; tests
+  /// that have finished are marked [Status.complete].
+  ///
+  /// This is guaranteed to contain the same tests as the union of [passed],
+  /// [skipped], [failed], and [active].
+  ///
+  /// [LiveTest.run] must not be called on these tests.
+  Set<LiveTest> get liveTests =>
+      UnionSet.from([passed, skipped, failed, IterableSet(active)],
+          disjoint: true);
+
+  /// A stream that emits each [LiveTest] as it's about to start running.
+  ///
+  /// This is guaranteed to fire before [LiveTest.onStateChange] first fires.
+  Stream<LiveTest> get onTestStarted => _onTestStartedGroup.stream;
+  final _onTestStartedGroup = StreamGroup<LiveTest>.broadcast();
+
+  /// The set of tests that have completed and been marked as passing.
+  Set<LiveTest> get passed => _passedGroup.set;
+  final _passedGroup = UnionSetController<LiveTest>(disjoint: true);
+
+  /// The set of tests that have completed and been marked as skipped.
+  Set<LiveTest> get skipped => _skippedGroup.set;
+  final _skippedGroup = UnionSetController<LiveTest>(disjoint: true);
+
+  /// The set of tests that have completed and been marked as failing or error.
+  Set<LiveTest> get failed => _failedGroup.set;
+  final _failedGroup = UnionSetController<LiveTest>(disjoint: true);
+
+  /// The tests that are still running, in the order they began running.
+  List<LiveTest> get active => UnmodifiableListView(_active);
+  final _active = QueueList<LiveTest>();
+
+  /// The suites that are still loading, in the order they began.
+  List<LiveTest> get activeSuiteLoads =>
+      UnmodifiableListView(_activeSuiteLoads);
+  final _activeSuiteLoads = <LiveTest>{};
+
+  /// The set of tests that have been marked for restarting.
+  ///
+  /// This is always a subset of [active]. Once a test in here has finished
+  /// running, it's run again.
+  final _restarted = <LiveTest>{};
+
+  /// Whether this engine is idle—that is, not currently executing a test.
+  bool get isIdle => _group.isIdle;
+
+  /// A broadcast stream that fires an event whenever [isIdle] switches from
+  /// `false` to `true`.
+  Stream get onIdle => _group.onIdle;
+
+  /// Creates an [Engine] that will run all tests provided via [suiteSink].
+  ///
+  /// [concurrency] controls how many suites are loaded and ran at once, and
+  /// defaults to 1.
+  ///
+  /// [testRandomizeOrderingSeed] configures test case shuffling within each
+  /// test suite.
+  /// Any non-zero value will enable shuffling using this value as a seed.
+  /// Omitting this argument or passing `0` disables shuffling.
+  ///
+  /// [coverage] specifies a directory to output coverage information.
+  ///
+  /// If [stopOnFirstFailure] then a single failing test will cause the engine
+  /// to [close] and stop ruunning further tests.
+  Engine({
+    int? concurrency,
+    String? coverage,
+    this.testRandomizeOrderingSeed,
+    bool stopOnFirstFailure = false,
+  })  : _runPool = Pool(concurrency ?? 1),
+        _stopOnFirstFailure = stopOnFirstFailure,
+        _coverage = coverage {
+    _group.future.then((_) {
+      _onTestStartedGroup.close();
+      _onSuiteStartedController.close();
+      _closedBeforeDone ??= false;
+    }).onError((_, __) {
+      // Don't top-level errors. They'll be thrown via [success] anyway.
+    });
+  }
+
+  /// Creates an [Engine] that will run all tests in [suites].
+  ///
+  /// An engine constructed this way will automatically close its [suiteSink],
+  /// meaning that no further suites may be provided.
+  ///
+  /// [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, bool stopOnFirstFailure = false}) {
+    var engine = Engine(
+      concurrency: concurrency,
+      coverage: coverage,
+      stopOnFirstFailure: stopOnFirstFailure,
+    );
+    for (var suite in suites) {
+      engine.suiteSink.add(suite);
+    }
+    engine.suiteSink.close();
+    return engine;
+  }
+
+  /// Runs all tests in all suites defined by this engine.
+  ///
+  /// 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.
+  ///
+  /// 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;
+
+    var subscription = _suiteController.stream.listen(null);
+    subscription
+      ..onData((suite) {
+        _addedSuites.add(suite);
+        _onSuiteAddedController.add(suite);
+
+        _group.add(() async {
+          var resource = await _runPool.request();
+          LiveSuiteController? controller;
+          try {
+            if (suite is LoadSuite) {
+              await _onUnpaused;
+              controller = await _addLoadSuite(suite);
+              if (controller == null) return;
+            } else {
+              controller = LiveSuiteController(suite);
+            }
+
+            _addLiveSuite(controller.liveSuite);
+
+            if (_closed) return;
+            await _runGroup(controller, controller.liveSuite.suite.group, []);
+            controller.noMoreLiveTests();
+            if (_coverage != null) await writeCoverage(_coverage!, controller);
+          } finally {
+            resource.allowRelease(() => controller?.close());
+          }
+        }());
+      })
+      ..onDone(() {
+        _subscriptions.remove(subscription);
+        _onSuiteAddedController.close();
+        _group.close();
+        _runPool.close();
+      });
+    _subscriptions.add(subscription);
+
+    return success;
+  }
+
+  /// Runs all the entries in [group] in sequence.
+  ///
+  /// [suiteController] is the controller fo the suite that contains [group].
+  /// [parents] is a list of groups that contain [group]. It may be modified,
+  /// but it's guaranteed to be in its original state once this function has
+  /// finished.
+  Future _runGroup(LiveSuiteController suiteController, Group group,
+      List<Group> parents) async {
+    parents.add(group);
+    try {
+      var suiteConfig = suiteController.liveSuite.suite.config;
+      var skipGroup = !suiteConfig.runSkipped && group.metadata.skip;
+      var setUpAllSucceeded = true;
+      if (!skipGroup && group.setUpAll != null) {
+        var liveTest = group.setUpAll!
+            .load(suiteController.liveSuite.suite, groups: parents);
+        await _runLiveTest(suiteController, liveTest, countSuccess: false);
+        setUpAllSucceeded = liveTest.state.result.isPassing;
+      }
+
+      if (!_closed && setUpAllSucceeded) {
+        // shuffle the group entries
+        var entries = group.entries.toList();
+        if (suiteConfig.allowTestRandomization &&
+            testRandomizeOrderingSeed != null &&
+            testRandomizeOrderingSeed! > 0) {
+          entries.shuffle(Random(testRandomizeOrderingSeed));
+        }
+
+        for (var entry in entries) {
+          if (_closed) return;
+
+          if (entry is Group) {
+            await _runGroup(suiteController, entry, parents);
+          } else if (!suiteConfig.runSkipped && entry.metadata.skip) {
+            await _runSkippedTest(suiteController, entry as Test, parents);
+          } else {
+            var test = entry as Test;
+            await _runLiveTest(suiteController,
+                test.load(suiteController.liveSuite.suite, groups: parents));
+          }
+        }
+      }
+
+      // 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!
+            .load(suiteController.liveSuite.suite, groups: parents);
+        await _runLiveTest(suiteController, liveTest, countSuccess: false);
+        if (_closed) await liveTest.close();
+      }
+    } finally {
+      parents.remove(group);
+    }
+  }
+
+  /// Runs [liveTest] using [suiteController].
+  ///
+  /// If [countSuccess] is `true` (the default), the test is put into [passed]
+  /// if it succeeds. Otherwise, it's removed from [liveTests] entirely.
+  Future _runLiveTest(LiveSuiteController suiteController, LiveTest liveTest,
+      {bool countSuccess = true}) async {
+    await _onUnpaused;
+    _active.add(liveTest);
+
+    var subscription = liveTest.onStateChange.listen(null);
+    subscription
+      ..onData((state) {
+        if (state.status != Status.complete) return;
+        _active.remove(liveTest);
+      })
+      ..onDone(() {
+        _subscriptions.remove(subscription);
+      });
+    _subscriptions.add(subscription);
+
+    suiteController.reportLiveTest(liveTest, countSuccess: countSuccess);
+
+    // Schedule a microtask to ensure that [onTestStarted] fires before the
+    // first [LiveTest.onStateChange] event.
+    await Future.microtask(liveTest.run);
+
+    // Once the test finishes, use [new Future] to do a coarse-grained event
+    // loop pump to avoid starving non-microtask events.
+    await Future(() {});
+
+    if (!_restarted.contains(liveTest)) {
+      if (_stopOnFirstFailure && liveTest.state.result.isFailing) {
+        unawaited(close());
+      }
+      return;
+    }
+    await _runLiveTest(suiteController, liveTest.copy(),
+        countSuccess: countSuccess);
+    _restarted.remove(liveTest);
+  }
+
+  /// Runs a dummy [LiveTest] for a test marked as "skip".
+  ///
+  /// [suiteController] is the controller for the suite that contains [test].
+  /// [parents] is a list of groups that contain [test].
+  Future _runSkippedTest(LiveSuiteController suiteController, Test test,
+      List<Group> parents) async {
+    await _onUnpaused;
+    var skipped = LocalTest(test.name, test.metadata, () {}, trace: test.trace);
+
+    late LiveTestController controller;
+    controller =
+        LiveTestController(suiteController.liveSuite.suite, skipped, () {
+      controller.setState(const State(Status.running, Result.success));
+      controller.setState(const State(Status.running, Result.skipped));
+
+      if (skipped.metadata.skipReason != null) {
+        controller
+            .message(Message.skip('Skip: ${skipped.metadata.skipReason}'));
+      }
+
+      controller.setState(const State(Status.complete, Result.skipped));
+      controller.completer.complete();
+    }, () {}, groups: parents);
+
+    return await _runLiveTest(suiteController, controller);
+  }
+
+  /// Closes [liveTest] and tells the engine to re-run it once it's done
+  /// running.
+  ///
+  /// Returns the same future as [LiveTest.close].
+  Future restartTest(LiveTest liveTest) async {
+    if (_activeSuiteLoads.contains(liveTest)) {
+      throw ArgumentError("Can't restart a load test.");
+    }
+
+    if (!_active.contains(liveTest)) {
+      throw StateError("Can't restart inactive test "
+          '"${liveTest.test.name}".');
+    }
+
+    _restarted.add(liveTest);
+    _active.remove(liveTest);
+    await liveTest.close();
+  }
+
+  /// 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 {
+    var controller = LiveSuiteController(suite);
+    _addLiveSuite(controller.liveSuite);
+
+    var liveTest = suite.test.load(suite);
+    _activeSuiteLoads.add(liveTest);
+
+    var subscription = liveTest.onStateChange.listen(null);
+    subscription
+      ..onData((state) {
+        if (state.status != Status.complete) return;
+        _activeSuiteLoads.remove(liveTest);
+      })
+      ..onDone(() {
+        _subscriptions.remove(subscription);
+      });
+    _subscriptions.add(subscription);
+
+    controller.reportLiveTest(liveTest, countSuccess: false);
+    controller.noMoreLiveTests();
+
+    // Schedule a microtask to ensure that [onTestStarted] fires before the
+    // first [LiveTest.onStateChange] event.
+    await Future.microtask(liveTest.run);
+
+    var innerSuite = await suite.suite;
+    if (innerSuite == null) return null;
+
+    var innerController = LiveSuiteController(innerSuite);
+    unawaited(innerController.liveSuite.onClose.whenComplete(() {
+      // When the main suite is closed, close the load suite and its test as
+      // well. This doesn't release any resources, but it does close streams
+      // which indicates that the load test won't experience an error in the
+      // future.
+      liveTest.close();
+      controller.close();
+    }));
+
+    return innerController;
+  }
+
+  /// Add [liveSuite] and the information it exposes to the engine's
+  /// informational streams and collections.
+  void _addLiveSuite(LiveSuite liveSuite) {
+    _onSuiteStartedController.add(liveSuite);
+
+    _onTestStartedGroup.add(liveSuite.onTestStarted);
+    _passedGroup.add(liveSuite.passed);
+    _skippedGroup.add(liveSuite.skipped);
+    _failedGroup.add(liveSuite.failed);
+  }
+
+  /// Pauses the engine.
+  ///
+  /// This pauses all streams and keeps any new suites from being loaded or
+  /// tests from being run until [resume] is called.
+  ///
+  /// This does nothing if the engine is already paused. Pauses are *not*
+  /// cumulative.
+  void pause() {
+    if (_pauseCompleter != null) return;
+    _pauseCompleter = Completer();
+    for (var subscription in _subscriptions) {
+      subscription.pause();
+    }
+  }
+
+  void resume() {
+    if (_pauseCompleter == null) return;
+    _pauseCompleter!.complete();
+    _pauseCompleter = null;
+    for (var subscription in _subscriptions) {
+      subscription.resume();
+    }
+  }
+
+  /// Signals that the caller is done paying attention to test results and the
+  /// engine should release any resources it has allocated.
+  ///
+  /// Any actively-running tests are also closed. VM tests are allowed to finish
+  /// running so that any modifications they've made to the filesystem can be
+  /// cleaned up.
+  ///
+  /// **Note that closing the engine is not the same as closing [suiteSink].**
+  /// Closing [suiteSink] indicates that no more input will be provided, closing
+  /// the engine indicates that no more output should be emitted.
+  Future close() async {
+    _closed = true;
+    if (_closedBeforeDone != null) _closedBeforeDone = true;
+    await _suiteController.close();
+    await _onSuiteAddedController.close();
+
+    // Close the running tests first so that we're sure to wait for them to
+    // finish before we close their suites and cause them to become unloaded.
+    var allLiveTests = liveTests.toSet()..addAll(_activeSuiteLoads);
+    var futures = allLiveTests.map((liveTest) => liveTest.close()).toList();
+
+    // Closing the run pool will close the test suites as soon as their tests
+    // are done. For browser suites this is effectively immediate since their
+    // tests shut down as soon as they're closed, but for VM suites we may need
+    // to wait for tearDowns or tearDownAlls to run.
+    futures.add(_runPool.close());
+    await Future.wait(futures, eagerError: true);
+  }
+}
diff --git a/pkgs/test_core/lib/src/runner/environment.dart b/pkgs/test_core/lib/src/runner/environment.dart
new file mode 100644
index 0000000..7fcba66
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/environment.dart
@@ -0,0 +1,55 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+
+/// The abstract class of environments in which test suites are
+/// loaded—specifically, browsers and the Dart VM.
+abstract class Environment {
+  /// Whether this environment supports interactive debugging.
+  bool get supportsDebugging;
+
+  /// 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;
+
+  /// The URL of the remote debugger for this environment, or `null` if it isn't
+  /// enabled.
+  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.
+  ///
+  /// Never emits an error, and never closes.
+  Stream get onRestart;
+
+  /// Displays information indicating that the test runner is paused.
+  ///
+  /// The returned operation will complete when the user takes action within the
+  /// environment that should unpause the runner. If the runner is unpaused
+  /// elsewhere, the operation should be canceled.
+  CancelableOperation displayPause();
+}
+
+/// The default environment for platform plugins.
+class PluginEnvironment implements Environment {
+  @override
+  final supportsDebugging = false;
+  @override
+  Stream get onRestart => StreamController<void>.broadcast().stream;
+
+  const PluginEnvironment();
+
+  @override
+  Uri? get observatoryUrl => null;
+
+  @override
+  Uri? get remoteDebuggerUrl => null;
+
+  @override
+  CancelableOperation displayPause() => throw UnsupportedError(
+      'PluginEnvironment.displayPause is not supported.');
+}
diff --git a/pkgs/test_core/lib/src/runner/hack_register_platform.dart b/pkgs/test_core/lib/src/runner/hack_register_platform.dart
new file mode 100644
index 0000000..f5a618d
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/hack_register_platform.dart
@@ -0,0 +1,36 @@
+// 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.
+
+import 'dart:async';
+import 'dart:collection';
+
+import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
+import 'platform.dart';
+
+/// The functions to use to load [_platformPlugins] in all loaders.
+///
+/// **Do not access this outside the test package**.
+final platformCallbacks =
+    UnmodifiableMapView<Runtime, FutureOr<PlatformPlugin> Function()>(
+        _platformCallbacks);
+final _platformCallbacks = <Runtime, FutureOr<PlatformPlugin> Function()>{};
+
+/// **Do not call this function without express permission from the test package
+/// authors**.
+///
+/// Registers a [PlatformPlugin] for [runtimes].
+///
+/// This globally registers a plugin for all [Loader]s. When the runner first
+/// requests that a suite be loaded for one of the given runtimes, this will
+/// call [plugin] to load the platform plugin. It may return either a
+/// [PlatformPlugin] or a `Future<PlatformPlugin>`. That plugin is then
+/// preserved and used to load all suites for all matching runtimes.
+///
+/// This overwrites the default plugins for those runtimes.
+void registerPlatformPlugin(
+    Iterable<Runtime> runtimes, FutureOr<PlatformPlugin> Function() plugin) {
+  for (var runtime in runtimes) {
+    _platformCallbacks[runtime] = plugin;
+  }
+}
diff --git a/pkgs/test_core/lib/src/runner/hybrid_listener.dart b/pkgs/test_core/lib/src/runner/hybrid_listener.dart
new file mode 100644
index 0000000..6a521db
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/hybrid_listener.dart
@@ -0,0 +1,96 @@
+// 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.
+
+import 'dart:async';
+import 'dart:isolate';
+
+import 'package:async/async.dart';
+import 'package:stack_trace/stack_trace.dart';
+import 'package:stream_channel/isolate_channel.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test_api/backend.dart' show RemoteException;
+import 'package:test_api/src/utils.dart'; // ignore: implementation_imports
+
+/// A sink transformer that wraps data and error events so that errors can be
+/// decoded after being JSON-serialized.
+final _transformer = StreamSinkTransformer<dynamic, dynamic>.fromHandlers(
+    handleData: (data, sink) {
+  ensureJsonEncodable(data);
+  sink.add({'type': 'data', 'data': data});
+}, handleError: (error, stackTrace, sink) {
+  sink.add(
+      {'type': 'error', 'error': RemoteException.serialize(error, stackTrace)});
+});
+
+/// Runs the body of a hybrid isolate and communicates its messages, errors, and
+/// prints to the main isolate.
+///
+/// The [getMain] function returns the `hybridMain()` method. It's wrapped in a
+/// closure so that, if the method undefined, we can catch the error and notify
+/// the caller of it.
+///
+/// The [data] argument contains two values: a [SendPort] that communicates with
+/// the main isolate, and a message to pass to `hybridMain()`.
+void listen(Function Function() getMain, List data) {
+  var channel = IsolateChannel<Object?>.connectSend(data.first as SendPort);
+  var message = data.last;
+
+  Chain.capture(() {
+    runZoned(() {
+      dynamic /*Function*/ main;
+      try {
+        main = getMain();
+      } on NoSuchMethodError catch (_) {
+        _sendError(channel, 'No top-level hybridMain() function defined.');
+        return;
+      } catch (error, stackTrace) {
+        _sendError(channel, error, stackTrace);
+        return;
+      }
+
+      if (main is! Function) {
+        _sendError(channel, 'Top-level hybridMain is not a function.');
+        return;
+      } else if (main is! void Function(StreamChannel) &&
+          main is! void Function(StreamChannel, Never)) {
+        if (main is void Function(StreamChannel<Never>) ||
+            main is void Function(StreamChannel<Never>, Never)) {
+          _sendError(
+              channel,
+              'The first parameter to the top-level hybridMain() must be a '
+              'StreamChannel<dynamic> or StreamChannel<Object?>. More specific '
+              'types such as StreamChannel<Object> are not supported.');
+        } else {
+          _sendError(channel,
+              'Top-level hybridMain() function must take one or two arguments.');
+        }
+        return;
+      }
+
+      // Wrap [channel] before passing it to user code so that we can wrap
+      // errors and distinguish user data events from control events sent by the
+      // listener.
+      var transformedChannel = channel.transformSink(_transformer);
+      if (main is void Function(StreamChannel)) {
+        main(transformedChannel);
+      } else {
+        main(transformedChannel, message);
+      }
+    }, zoneSpecification: ZoneSpecification(print: (_, __, ___, line) {
+      channel.sink.add({'type': 'print', 'line': line});
+    }));
+  }, onError: (error, stackTrace) async {
+    _sendError(channel, error, stackTrace);
+    await channel.sink.close();
+    Isolate.current.kill();
+  });
+}
+
+/// Sends a message over [channel] indicating an error from user code.
+void _sendError(StreamChannel channel, Object 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
new file mode 100644
index 0000000..771655d
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/live_suite.dart
@@ -0,0 +1,77 @@
+// 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.
+
+import 'package:collection/collection.dart';
+
+import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports
+
+import 'runner_suite.dart';
+
+/// A view of the execution of a test suite.
+///
+/// This is distinct from [Suite] because it represents the progress of running
+/// a suite rather than the suite's contents. It provides events and collections
+/// that give the caller a view into the suite's current state.
+abstract class LiveSuite {
+  /// The suite that's being run.
+  RunnerSuite get suite;
+
+  /// A [Future] that completes once the suite is complete.
+  ///
+  /// Note that even once this completes, the suite may still be running code
+  /// asynchronously. A suite is considered complete once all of its tests are
+  /// complete, but it's possible for a test to continue running even after it's
+  /// been marked complete—see [LiveTest.isComplete] for details.
+  ///
+  /// The [onClose] future can be used to determine when the suite and its tests
+  /// are guaranteed to emit no more events.
+  Future get onComplete;
+
+  /// Whether the suite has been closed.
+  ///
+  /// If this is `true`, no code is running for the suite or any of its tests.
+  /// At this point, the caller can be sure that the suites' tests are all in
+  /// fixed states that will not change in the future.
+  bool get isClosed;
+
+  /// A [Future] that completes when the suite has been closed.
+  ///
+  /// Once this completes, no code is running for the suite or any of its tests.
+  /// At this point, the caller can be sure that the suites' tests are all in
+  /// fixed states that will not change in the future.
+  Future get onClose;
+
+  /// All the currently-known tests in this suite that have run or are running.
+  ///
+  /// This is guaranteed to contain the same tests as the union of [passed],
+  /// [skipped], [failed], and [active].
+  Set<LiveTest> get liveTests => UnionSet.from([
+        passed,
+        skipped,
+        failed,
+        if (active != null) {active!}
+      ]);
+
+  /// A stream that emits each [LiveTest] in this suite as it's about to start
+  /// running.
+  ///
+  /// This is guaranteed to fire before [LiveTest.onStateChange] first fires. It
+  /// will close once all tests the user has selected are run.
+  Stream<LiveTest> get onTestStarted;
+
+  /// The set of tests in this suite that have completed and been marked as
+  /// passing.
+  Set<LiveTest> get passed;
+
+  /// The set of tests in this suite that have completed and been marked as
+  /// skipped.
+  Set<LiveTest> get skipped;
+
+  /// The set of tests in this suite that have completed and been marked as
+  /// failing or error.
+  Set<LiveTest> get failed;
+
+  /// The currently running test in this suite, or `null` if no test is running.
+  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
new file mode 100644
index 0000000..15bc797
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/live_suite_controller.dart
@@ -0,0 +1,154 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:async/async.dart' hide Result;
+import 'package:collection/collection.dart';
+import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports
+
+import 'live_suite.dart';
+import 'runner_suite.dart';
+
+/// An implementation of [LiveSuite] that's controlled by a
+/// [LiveSuiteController].
+class _LiveSuite extends LiveSuite {
+  final LiveSuiteController _controller;
+
+  @override
+  RunnerSuite get suite => _controller._suite;
+
+  @override
+  Future get onComplete => _controller._onCompleteGroup.future;
+
+  @override
+  bool get isClosed => _controller._onCloseCompleter.isCompleted;
+
+  @override
+  Future get onClose => _controller._onCloseCompleter.future;
+
+  @override
+  Stream<LiveTest> get onTestStarted =>
+      _controller._onTestStartedController.stream;
+
+  @override
+  Set<LiveTest> get passed => UnmodifiableSetView(_controller._passed);
+
+  @override
+  Set<LiveTest> get skipped => UnmodifiableSetView(_controller._skipped);
+
+  @override
+  Set<LiveTest> get failed => UnmodifiableSetView(_controller._failed);
+
+  @override
+  LiveTest? get active => _controller._active;
+
+  _LiveSuite(this._controller);
+}
+
+/// A controller that drives a [LiveSuite].
+///
+/// This is a utility class to make it easier for [Engine] to create the
+/// [LiveSuite]s exposed by various APIs. The [LiveSuite] is accessible through
+/// [LiveSuiteController.liveSuite]. When a live test is run, it should be
+/// passed to [reportLiveTest], and once tests are finished being run for this
+/// suite, [noMoreLiveTests] should be called. Once the suite should be torn
+/// down, [close] should be called.
+class LiveSuiteController {
+  /// The [LiveSuite] being controlled.
+  late final liveSuite = _LiveSuite(this);
+
+  /// The suite that's being run.
+  final RunnerSuite _suite;
+
+  /// The future group that backs [LiveSuite.onComplete].
+  ///
+  /// This contains all the futures from tests that are run in this suite.
+  final _onCompleteGroup = FutureGroup<void>();
+
+  /// The completer that backs [LiveSuite.onClose].
+  ///
+  /// This is completed when the live suite is closed.
+  final _onCloseCompleter = Completer<void>();
+
+  /// The controller for [LiveSuite.onTestStarted].
+  final _onTestStartedController =
+      StreamController<LiveTest>.broadcast(sync: true);
+
+  /// The set that backs [LiveTest.passed].
+  final _passed = <LiveTest>{};
+
+  /// The set that backs [LiveTest.skipped].
+  final _skipped = <LiveTest>{};
+
+  /// The set that backs [LiveTest.failed].
+  final _failed = <LiveTest>{};
+
+  /// The test exposed through [LiveTest.active].
+  LiveTest? _active;
+
+  /// Creates a controller for a live suite representing running the tests in
+  /// [suite].
+  ///
+  /// Once this is called, the controller assumes responsibility for closing the
+  /// suite. The caller should call [LiveSuiteController.close] rather than
+  /// calling [RunnerSuite.close] directly.
+  LiveSuiteController(this._suite);
+
+  /// Reports the status of [liveTest] through [liveSuite].
+  ///
+  /// The live test is assumed to be a member of this suite. If [countSuccess]
+  /// is `true` (the default), the test is put into [passed] if it succeeds.
+  /// Otherwise, it's removed from [liveTests] entirely.
+  ///
+  /// Throws a [StateError] if called after [noMoreLiveTests].
+  void reportLiveTest(LiveTest liveTest, {bool countSuccess = true}) {
+    if (_onTestStartedController.isClosed) {
+      throw StateError("Can't call reportLiveTest() after noMoreTests().");
+    }
+
+    assert(liveTest.suite == _suite);
+    assert(_active == null);
+
+    _active = liveTest;
+
+    liveTest.onStateChange.listen((state) {
+      if (state.status != Status.complete) return;
+      _active = null;
+
+      if (state.result == Result.skipped) {
+        _skipped.add(liveTest);
+      } else if (state.result != Result.success) {
+        _passed.remove(liveTest);
+        _failed.add(liveTest);
+      } else if (countSuccess) {
+        _passed.add(liveTest);
+        // A passing test that was once failing was retried
+        _failed.remove(liveTest);
+      }
+    });
+
+    _onTestStartedController.add(liveTest);
+
+    _onCompleteGroup.add(liveTest.onComplete);
+  }
+
+  /// Indicates that all the live tests that are going to be provided for this
+  /// suite have already been provided.
+  void noMoreLiveTests() {
+    _onTestStartedController.close();
+    _onCompleteGroup.close();
+  }
+
+  /// Closes the underlying suite.
+  Future close() => _closeMemo.runOnce(() async {
+        try {
+          await _suite.close();
+        } finally {
+          _onCloseCompleter.complete();
+        }
+      });
+  final _closeMemo = AsyncMemoizer<void>();
+}
diff --git a/pkgs/test_core/lib/src/runner/load_exception.dart b/pkgs/test_core/lib/src/runner/load_exception.dart
new file mode 100644
index 0000000..3fbd462
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/load_exception.dart
@@ -0,0 +1,34 @@
+// 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.
+
+import 'package:source_span/source_span.dart';
+
+import '../util/errors.dart';
+
+class LoadException implements Exception {
+  final String path;
+
+  final Object innerError;
+
+  LoadException(this.path, this.innerError);
+
+  @override
+  String toString({bool color = false}) {
+    var buffer = StringBuffer();
+    if (color) buffer.write('\u001b[31m'); // red
+    buffer.write('Failed to load "$path":');
+    if (color) buffer.write('\u001b[0m'); // no color
+
+    var innerString = getErrorMessage(innerError);
+    if (innerError is SourceSpanException) {
+      innerString = (innerError as SourceSpanException)
+          .toString(color: color)
+          .replaceFirst(' of $path', '');
+    }
+
+    buffer.write(innerString.contains('\n') ? '\n' : ' ');
+    buffer.write(innerString);
+    return buffer.toString();
+  }
+}
diff --git a/pkgs/test_core/lib/src/runner/load_suite.dart b/pkgs/test_core/lib/src/runner/load_suite.dart
new file mode 100644
index 0000000..633739a
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/load_suite.dart
@@ -0,0 +1,220 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test_api/scaffolding.dart' show Timeout;
+import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/invoker.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/metadata.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/test.dart'; // ignore: implementation_imports
+
+import '../util/io_stub.dart' if (dart.library.io) '../util/io.dart';
+import 'load_exception.dart';
+import 'plugin/environment.dart';
+import 'runner_suite.dart';
+import 'suite.dart';
+
+/// The timeout for loading a test suite.
+///
+/// We want this to be long enough that even a very large application being
+/// compiled with dart2js doesn't trigger it, but short enough that it fires
+/// before the host kills it. For example, Google's Forge service has a
+/// 15-minute timeout.
+final _timeout = const Duration(minutes: 12);
+
+/// A [Suite] emitted by a [Loader] that provides a test-like interface for
+/// loading a test file.
+///
+/// This is used to expose the current status of test loading to the user. It's
+/// important to provide users visibility into what's taking a long time and
+/// where failures occur. And since some tests may be loaded at the same time as
+/// others are run, it's useful to provide that visibility in the form of a test
+/// suite so that it can integrate well into the existing reporting interface
+/// without too much extra logic.
+///
+/// A suite is constructed with logic necessary to produce a test suite. As with
+/// a normal test body, this logic isn't run until [LiveTest.run] is called. The
+/// suite itself is returned by [suite] once it's available, but any errors or
+/// prints will be emitted through the running [LiveTest].
+class LoadSuite extends Suite implements RunnerSuite {
+  @override
+  final environment = const PluginEnvironment();
+  @override
+  final SuiteConfiguration config;
+  @override
+  final isDebugging = false;
+  @override
+  final onDebugging = StreamController<bool>().stream;
+
+  @override
+  bool get isLoadSuite => true;
+
+  /// A future that completes to the loaded suite once the suite's test has been
+  /// run and completed successfully.
+  ///
+  /// 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)?.suite;
+
+  /// 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<({RunnerSuite suite, Zone 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 => group.entries.single as Test;
+
+  /// Creates a load suite named [name] on [platform].
+  ///
+  /// [body] may return either a [RunnerSuite] or a [Future] that completes to a
+  /// [RunnerSuite]. Its return value is forwarded through [suite], although if
+  /// it throws an error that will be forwarded through the suite's test.
+  ///
+  /// 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}) {
+    var completer = Completer<({RunnerSuite suite, Zone zone})?>.sync();
+    return LoadSuite._(name, config, platform, () {
+      var invoker = Invoker.current;
+      invoker!.addOutstandingCallback();
+
+      unawaited(() async {
+        RunnerSuite? suite;
+        try {
+          suite = await body();
+        } catch (_) {
+          invoker.removeOutstandingCallback();
+          rethrow;
+        }
+        if (completer.isCompleted) {
+          // If the load test has already been closed, close the suite it
+          // generated.
+          await suite?.close();
+          return;
+        }
+
+        completer.complete(
+            suite == null ? null : (suite: suite, zone: Zone.current));
+        invoker.removeOutstandingCallback();
+      }());
+
+      // If the test completes before the body callback, either an out-of-band
+      // error occurred or the test was canceled. Either way, we return a `null`
+      // suite.
+      invoker.liveTest.onComplete.then((_) {
+        if (!completer.isCompleted) completer.complete();
+      });
+
+      // If the test is forcibly closed, let it complete, since load tests don't
+      // have timeouts.
+      invoker.onClose.then((_) => invoker.removeOutstandingCallback());
+    }, completer.future, path: path, ignoreTimeouts: config.ignoreTimeouts);
+  }
+
+  /// A utility constructor for a load suite that just throws [exception].
+  ///
+  /// The suite's name will be based on [exception]'s path.
+  factory LoadSuite.forLoadException(
+      LoadException exception, SuiteConfiguration? config,
+      {SuitePlatform? platform, StackTrace? stackTrace}) {
+    stackTrace ??= Trace.current();
+
+    return LoadSuite(
+        'loading ${exception.path}',
+        config ?? SuiteConfiguration.empty,
+        platform ?? currentPlatform(Runtime.vm, null),
+        () => Future.error(exception, stackTrace),
+        path: exception.path);
+  }
+
+  /// A utility constructor for a load suite that just emits [suite].
+  factory LoadSuite.forSuite(RunnerSuite suite) {
+    return LoadSuite(
+        'loading ${suite.path}', suite.config, suite.platform, () => suite,
+        path: suite.path);
+  }
+
+  LoadSuite._(String name, this.config, SuitePlatform platform,
+      void Function() body, this._suiteAndZone,
+      {required bool ignoreTimeouts, String? path})
+      : super(
+            Group.root(
+                [LocalTest(name, Metadata(timeout: Timeout(_timeout)), body)]),
+            platform,
+            path: path,
+            ignoreTimeouts: ignoreTimeouts);
+
+  /// A constructor used by [changeSuite].
+  LoadSuite._changeSuite(LoadSuite old, this._suiteAndZone)
+      : config = old.config,
+        super(old.group, old.platform,
+            path: old.path, ignoreTimeouts: old.ignoreTimeouts);
+
+  /// A constructor used by [filter].
+  LoadSuite._filtered(LoadSuite old, Group filtered)
+      : config = old.config,
+        _suiteAndZone = old._suiteAndZone,
+        super(old.group, old.platform,
+            path: old.path, ignoreTimeouts: old.ignoreTimeouts);
+
+  /// Creates a new [LoadSuite] that's identical to this one, but that
+  /// transforms [suite] once it's loaded.
+  ///
+  /// If [suite] completes to `null`, [change] won't be run. [change] is run
+  /// within the load test's zone, so any errors or prints it emits will be
+  /// associated with that test.
+  LoadSuite changeSuite(RunnerSuite? Function(RunnerSuite) change) {
+    return LoadSuite._changeSuite(this, _suiteAndZone.then((pair) {
+      if (pair == null) return null;
+
+      var (:suite, :zone) = pair;
+      RunnerSuite? newSuite;
+      zone.runGuarded(() {
+        newSuite = change(suite);
+      });
+      return newSuite == null ? null : (suite: newSuite!, zone: zone);
+    }));
+  }
+
+  /// Runs the test and returns the suite.
+  ///
+  /// Rather than emitting errors through a [LiveTest], this just pipes them
+  /// through the return value.
+  Future<RunnerSuite?> getSuite() async {
+    var liveTest = test.load(this);
+    liveTest.onMessage.listen((message) => print(message.text));
+    await liveTest.run();
+
+    if (liveTest.errors.isEmpty) return await suite;
+
+    var error = liveTest.errors.first;
+    await Future<void>.error(error.error, error.stackTrace);
+    throw 'unreachable';
+  }
+
+  @override
+  LoadSuite filter(bool Function(Test) callback) {
+    var filtered = group.filter(callback);
+    filtered ??= Group.root([], metadata: metadata);
+    return LoadSuite._filtered(this, filtered);
+  }
+
+  @override
+  Future close() async {}
+
+  @override
+  Future<Map<String, dynamic>> gatherCoverage() =>
+      throw UnsupportedError('Coverage is not supported for LoadSuite tests.');
+}
diff --git a/pkgs/test_core/lib/src/runner/loader.dart b/pkgs/test_core/lib/src/runner/loader.dart
new file mode 100644
index 0000000..c37105d
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/loader.dart
@@ -0,0 +1,297 @@
+// 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.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:async/async.dart';
+import 'package:path/path.dart' as p;
+import 'package:source_span/source_span.dart';
+import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/invoker.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
+import 'package:yaml/yaml.dart';
+
+import '../util/io.dart';
+import 'compiler_selection.dart';
+import 'configuration.dart';
+import 'hack_register_platform.dart';
+import 'load_exception.dart';
+import 'load_suite.dart';
+import 'parse_metadata.dart';
+import 'platform.dart';
+import 'plugin/customizable_platform.dart';
+import 'plugin/environment.dart';
+import 'runner_suite.dart';
+import 'suite.dart';
+import 'vm/platform.dart';
+
+/// A class for finding test files and loading them into a runnable form.
+class Loader {
+  /// The test runner configuration.
+  final _config = Configuration.current;
+
+  /// All suites that have been created by the loader.
+  final _suites = <RunnerSuite>{};
+
+  /// Memoizers for platform plugins, indexed by the runtimes they support.
+  final _platformPlugins = <Runtime, AsyncMemoizer<PlatformPlugin>>{};
+
+  /// The functions to use to load [_platformPlugins].
+  ///
+  /// These are passed to the plugins' async memoizers when a plugin is needed.
+  final _platformCallbacks = <Runtime, FutureOr<PlatformPlugin> Function()>{};
+
+  /// A map of all runtimes registered in [_platformCallbacks], indexed by
+  /// their string identifiers.
+  final _runtimesByIdentifier = <String, Runtime>{};
+
+  /// The user-provided settings for runtimes, as a list of settings that will
+  /// be merged together using [CustomizablePlatform.mergePlatformSettings].
+  final _runtimeSettings = <Runtime, List<YamlMap>>{};
+
+  /// The user-provided settings for runtimes.
+  final _parsedRuntimeSettings = <Runtime, Object>{};
+
+  /// All plaforms supported by this [Loader].
+  List<Runtime> get allRuntimes => List.unmodifiable(_platformCallbacks.keys);
+
+  /// The runtime variables supported by this loader, in addition the default
+  /// variables that are always supported.
+  Iterable<String> get _runtimeVariables =>
+      _platformCallbacks.keys.map((runtime) => runtime.identifier);
+
+  /// Creates a new loader that loads tests on platforms defined in
+  /// [Configuration.current].
+  Loader() {
+    _registerPlatformPlugin([Runtime.vm], VMPlatform.new);
+
+    platformCallbacks.forEach((runtime, plugin) {
+      _registerPlatformPlugin([runtime], plugin);
+    });
+
+    _registerCustomRuntimes();
+
+    _config.validateRuntimes(allRuntimes);
+
+    _registerRuntimeOverrides();
+  }
+
+  /// Registers a [PlatformPlugin] for [runtimes].
+  void _registerPlatformPlugin(
+      Iterable<Runtime> runtimes, FutureOr<PlatformPlugin> Function() plugin) {
+    var memoizer = AsyncMemoizer<PlatformPlugin>();
+    for (var runtime in runtimes) {
+      _platformPlugins[runtime] = memoizer;
+      _platformCallbacks[runtime] = plugin;
+      _runtimesByIdentifier[runtime.identifier] = runtime;
+    }
+  }
+
+  /// Registers user-defined runtimes from [Configuration.defineRuntimes].
+  void _registerCustomRuntimes() {
+    for (var customRuntime in _config.defineRuntimes.values) {
+      if (_runtimesByIdentifier.containsKey(customRuntime.identifier)) {
+        throw SourceSpanFormatException(
+            wordWrap(
+                'The platform "${customRuntime.identifier}" already exists. '
+                'Use override_platforms to override it.'),
+            customRuntime.identifierSpan);
+      }
+
+      var parent = _runtimesByIdentifier[customRuntime.parent];
+      if (parent == null) {
+        throw SourceSpanFormatException(
+            'Unknown platform.', customRuntime.parentSpan);
+      }
+
+      var runtime = parent.extend(customRuntime.name, customRuntime.identifier);
+      _platformPlugins[runtime] = _platformPlugins[parent]!;
+      _platformCallbacks[runtime] = _platformCallbacks[parent]!;
+      _runtimesByIdentifier[runtime.identifier] = runtime;
+
+      _runtimeSettings[runtime] = [customRuntime.settings];
+    }
+  }
+
+  /// Registers users' runtime settings from [Configuration.overrideRuntimes].
+  void _registerRuntimeOverrides() {
+    for (var settings in _config.overrideRuntimes.values) {
+      var runtime = _runtimesByIdentifier[settings.identifier];
+      _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];
+
+  /// Loads all test suites in [dir] according to [suiteConfig].
+  ///
+  /// This will load tests from files that match the global configuration's
+  /// filename glob. Any tests that fail to load will be emitted as
+  /// [LoadException]s.
+  ///
+  /// This emits [LoadSuite]s that must then be run to emit the actual
+  /// [RunnerSuite]s defined in the file.
+  Stream<LoadSuite> loadDir(String dir, SuiteConfiguration suiteConfig) {
+    return StreamGroup.merge(
+        Directory(dir).listSync(recursive: true).map((entry) {
+      if (entry is! File || !_config.filename.matches(p.basename(entry.path))) {
+        return const Stream.empty();
+      }
+
+      return loadFile(entry.path, suiteConfig);
+    }));
+  }
+
+  /// Loads a test suite from the file at [path] according to [suiteConfig].
+  ///
+  /// This emits [LoadSuite]s that must then be run to emit the actual
+  /// [RunnerSuite]s defined in the file.
+  ///
+  /// This will emit a [LoadException] if the file fails to load.
+  Stream<LoadSuite> loadFile(
+      String path, SuiteConfiguration suiteConfig) async* {
+    try {
+      suiteConfig = suiteConfig.merge(SuiteConfiguration.fromMetadata(
+          parseMetadata(
+              path, File(path).readAsStringSync(), _runtimeVariables.toSet())));
+    } on ArgumentError catch (_) {
+      // Ignore the analyzer's error, since its formatting is much worse than
+      // the VM's or dart2js's.
+    } on FormatException catch (error, stackTrace) {
+      yield LoadSuite.forLoadException(LoadException(path, error), suiteConfig,
+          stackTrace: stackTrace);
+      return;
+    }
+
+    if (_config.excludeTags.evaluate(suiteConfig.metadata.tags.contains)) {
+      return;
+    }
+
+    for (var runtimeName in suiteConfig.runtimes) {
+      var runtime = findRuntime(runtimeName);
+      if (runtime == null) {
+        throw ArgumentError.value(runtimeName, 'platform', 'Unknown platform');
+      }
+      final compilers = {
+        for (var selection
+            in suiteConfig.compilerSelections ?? <CompilerSelection>[])
+          if (runtime.supportedCompilers.contains(selection.compiler) &&
+              (selection.platformSelector == null ||
+                  selection.platformSelector!
+                      .evaluate(currentPlatform(runtime, selection.compiler))))
+            selection.compiler,
+      };
+      if (compilers.isEmpty) compilers.add(runtime.defaultCompiler);
+
+      for (var compiler in compilers) {
+        var platform = currentPlatform(runtime, compiler);
+        if (!suiteConfig.metadata.testOn.evaluate(platform)) continue;
+
+        var platformConfig = suiteConfig.forPlatform(platform);
+
+        // Don't load a skipped suite.
+        if (platformConfig.metadata.skip && !platformConfig.runSkipped) {
+          yield LoadSuite.forSuite(RunnerSuite(
+              const PluginEnvironment(),
+              platformConfig,
+              Group.root([LocalTest('(suite)', platformConfig.metadata, () {})],
+                  metadata: platformConfig.metadata),
+              platform,
+              path: path));
+          continue;
+        }
+
+        yield LoadSuite('loading $path', platformConfig, platform, () async {
+          var memo = _platformPlugins[platform.runtime]!;
+
+          var retriesLeft = suiteConfig.metadata.retry;
+          while (true) {
+            try {
+              var plugin =
+                  await memo.runOnce(_platformCallbacks[platform.runtime]!);
+              _customizePlatform(plugin, platform.runtime);
+              var suite = await plugin.load(path, platform, platformConfig,
+                  {'platformVariables': _runtimeVariables.toList()});
+              if (suite != null) _suites.add(suite);
+              return suite;
+            } on Object catch (error, stackTrace) {
+              if (retriesLeft > 0) {
+                retriesLeft--;
+                print('Retrying load of $path in 1s ($retriesLeft remaining)');
+                await Future<void>.delayed(const Duration(seconds: 1));
+                continue;
+              }
+              if (error is LoadException) {
+                rethrow;
+              }
+              await Future<void>.error(LoadException(path, error), stackTrace);
+              return null;
+            }
+          }
+        }, path: path);
+      }
+    }
+  }
+
+  /// Passes user-defined settings to [plugin] if necessary.
+  void _customizePlatform(PlatformPlugin plugin, Runtime runtime) {
+    var parsed = _parsedRuntimeSettings[runtime];
+    if (parsed != null) {
+      (plugin as CustomizablePlatform).customizePlatform(runtime, parsed);
+      return;
+    }
+
+    var settings = _runtimeSettings[runtime];
+    if (settings == null) return;
+
+    if (plugin is CustomizablePlatform) {
+      parsed = settings
+          .map(plugin.parsePlatformSettings)
+          .reduce(plugin.mergePlatformSettings);
+      plugin.customizePlatform(runtime, parsed);
+      _parsedRuntimeSettings[runtime] = parsed;
+    } else {
+      String identifier;
+      SourceSpan span;
+      if (runtime.isChild) {
+        identifier = runtime.parent!.identifier;
+        span = _config.defineRuntimes[runtime.identifier]!.parentSpan;
+      } else {
+        identifier = runtime.identifier;
+        span = _config.overrideRuntimes[runtime.identifier]!.identifierSpan;
+      }
+
+      throw SourceSpanFormatException(
+          'The "$identifier" platform can\'t be customized.', span);
+    }
+  }
+
+  Future closeEphemeral() async {
+    await Future.wait(_platformPlugins.values.map((memo) async {
+      if (!memo.hasRun) return;
+      await (await memo.future).closeEphemeral();
+    }));
+  }
+
+  /// Closes the loader and releases all resources allocated by it.
+  Future close() => _closeMemo.runOnce(() async {
+        await Future.wait([
+          Future.wait(_platformPlugins.values.map((memo) async {
+            if (!memo.hasRun) return;
+            await (await memo.future).close();
+          })),
+          Future.wait(_suites.map((suite) => suite.close()))
+        ]);
+
+        _platformPlugins.clear();
+        _platformCallbacks.clear();
+        _suites.clear();
+      });
+  final _closeMemo = AsyncMemoizer<void>();
+}
diff --git a/pkgs/test_core/lib/src/runner/no_tests_found_exception.dart b/pkgs/test_core/lib/src/runner/no_tests_found_exception.dart
new file mode 100644
index 0000000..0f65837
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/no_tests_found_exception.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// An expected exception thrown when there were no tests found to run.
+class NoTestsFoundException implements Exception {
+  final String message;
+
+  NoTestsFoundException(this.message);
+
+  @override
+  String toString() => message;
+}
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..0ce77d3
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/package_version.dart
@@ -0,0 +1,23 @@
+// 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:package_config/package_config.dart';
+import 'package:path/path.dart' as p;
+
+import '../util/detaching_future.dart';
+import '../util/package_config.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 packageConfigUri);
+  var rootPackage = packageConfig.packageOf(Uri.file(p.absolute('foo.dart')));
+  if (rootPackage == null) return '// <unknown-language-version>';
+  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
new file mode 100644
index 0000000..fd2b5ee
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/parse_metadata.dart
@@ -0,0 +1,514 @@
+// 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.
+
+import 'package:analyzer/dart/analysis/utilities.dart';
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:path/path.dart' as p;
+import 'package:source_span/source_span.dart';
+import 'package:test_api/scaffolding.dart' show Timeout;
+import 'package:test_api/src/backend/metadata.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/platform_selector.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/util/identifier_regex.dart'; // ignore: implementation_imports
+
+import '../util/dart.dart';
+
+/// Parse the test metadata for the test file at [path] with [contents].
+///
+/// The [platformVariables] are the set of variables that are valid for platform
+/// selectors in suite metadata, in addition to the built-in variables that are
+/// allowed everywhere.
+///
+/// Throws an [AnalysisError] if parsing fails or a [FormatException] if the
+/// test annotations are incorrect.
+Metadata parseMetadata(
+        String path, String contents, Set<String> platformVariables) =>
+    _Parser(path, contents, platformVariables).parse();
+
+/// A parser for test suite metadata.
+class _Parser {
+  /// The path to the test suite.
+  final String _path;
+
+  /// The set of variables that are valid for platform selectors, in addition to
+  /// the built-in variables that are allowed everywhere.
+  final Set<String> _platformVariables;
+
+  /// All annotations at the top of the file.
+  late final List<Annotation> _annotations;
+
+  /// All prefixes defined by imports in this file.
+  late final Set<String> _prefixes;
+
+  /// The actual contents of the file.
+  final String _contents;
+
+  /// The language version override comment if one was present, otherwise null.
+  String? _languageVersionComment;
+
+  _Parser(this._path, this._contents, this._platformVariables) {
+    var result =
+        parseString(content: _contents, path: _path, throwIfDiagnostics: false);
+    var directives = result.unit.directives;
+    _annotations = directives.isEmpty ? [] : directives.first.metadata;
+    _languageVersionComment = result.unit.languageVersionToken?.value();
+
+    // We explicitly *don't* just look for "package:test" imports here,
+    // because it could be re-exported from another library.
+    _prefixes = directives
+        .map((directive) {
+          if (directive is ImportDirective) {
+            return directive.prefix?.name;
+          } else {
+            return null;
+          }
+        })
+        .whereType<String>()
+        .toSet();
+  }
+
+  /// Parses the metadata.
+  Metadata parse() {
+    Timeout? timeout;
+    PlatformSelector? testOn;
+    Object? /*String|bool*/ skip;
+    Map<PlatformSelector, Metadata>? onPlatform;
+    Set<String>? tags;
+    int? retry;
+
+    for (var annotation in _annotations) {
+      var pair =
+          _resolveConstructor(annotation.name, annotation.constructorName);
+      var (name, constructorName) = pair;
+
+      if (name == 'TestOn') {
+        _assertSingle(testOn, 'TestOn', annotation);
+        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);
+      } else if (name == 'OnPlatform') {
+        _assertSingle(onPlatform, 'OnPlatform', annotation);
+        onPlatform = _parseOnPlatform(annotation);
+      } else if (name == 'Tags') {
+        _assertSingle(tags, 'Tags', annotation);
+        tags = _parseTags(annotation);
+      } else if (name == 'Retry') {
+        retry = _parseRetry(annotation);
+      }
+    }
+
+    return Metadata(
+        testOn: testOn,
+        timeout: timeout,
+        skip: skip == null ? null : true,
+        skipReason: skip is String ? skip : null,
+        onPlatform: onPlatform,
+        tags: tags,
+        retry: retry,
+        languageVersionComment: _languageVersionComment);
+  }
+
+  /// Parses a `@TestOn` annotation.
+  ///
+  /// [annotation] is the annotation.
+  PlatformSelector _parseTestOn(Annotation annotation) =>
+      _parsePlatformSelector(annotation.arguments!.arguments.first);
+
+  /// Parses an [expression] that should contain a string representing a
+  /// [PlatformSelector].
+  PlatformSelector _parsePlatformSelector(Expression expression) {
+    var literal = _parseString(expression);
+    return _contextualize(
+        literal,
+        () => PlatformSelector.parse(literal.stringValue!)
+          ..validate(_platformVariables));
+  }
+
+  /// Parses a `@Retry` annotation.
+  ///
+  /// [annotation] is the annotation.
+  int _parseRetry(Annotation annotation) =>
+      _parseInt(annotation.arguments!.arguments.first);
+
+  /// Parses a `@Timeout` annotation.
+  ///
+  /// [annotation] is the annotation. [constructorName] is the name of the named
+  /// constructor for the annotation, if any.
+  Timeout _parseTimeout(Annotation annotation, String? constructorName) {
+    if (constructorName == 'none') {
+      return Timeout.none;
+    }
+
+    var args = annotation.arguments!.arguments;
+    if (constructorName == null) return Timeout(_parseDuration(args.first));
+    return Timeout.factor(_parseNum(args.first));
+  }
+
+  /// Parses a `Timeout` constructor.
+  Timeout _parseTimeoutConstructor(Expression constructor) {
+    var name = _findConstructorName(constructor, 'Timeout');
+    var arguments = _parseArguments(constructor);
+    if (name == null) return Timeout(_parseDuration(arguments.first));
+    if (name == 'factor') return Timeout.factor(_parseNum(arguments.first));
+    throw SourceSpanFormatException('Invalid timeout', _spanFor(constructor));
+  }
+
+  /// Parses a `@Skip` annotation.
+  ///
+  /// [annotation] is the annotation.
+  ///
+  /// Returns either `true` or a reason string.
+  dynamic _parseSkip(Annotation annotation) {
+    var args = annotation.arguments!.arguments;
+    return args.isEmpty ? true : _parseString(args.first).stringValue;
+  }
+
+  /// Parses a `Skip` constructor.
+  ///
+  /// Returns either `true` or a reason string.
+  dynamic _parseSkipConstructor(Expression constructor) {
+    _findConstructorName(constructor, 'Skip');
+    var arguments = _parseArguments(constructor);
+    return arguments.isEmpty ? true : _parseString(arguments.first).stringValue;
+  }
+
+  /// Parses a `@Tags` annotation.
+  ///
+  /// [annotation] is the annotation.
+  Set<String> _parseTags(Annotation annotation) {
+    return _parseList(annotation.arguments!.arguments.first)
+        .map((tagExpression) {
+      var name = _parseString(tagExpression).stringValue!;
+      if (name.contains(anchoredHyphenatedIdentifier)) return name;
+
+      throw SourceSpanFormatException(
+          'Invalid tag name. Tags must be (optionally hyphenated) Dart '
+          'identifiers.',
+          _spanFor(tagExpression));
+    }).toSet();
+  }
+
+  /// Parses an `@OnPlatform` annotation.
+  ///
+  /// [annotation] is the annotation.
+  Map<PlatformSelector, Metadata> _parseOnPlatform(Annotation annotation) {
+    return _parseMap(annotation.arguments!.arguments.first,
+        key: _parsePlatformSelector, value: (value) {
+      var expressions = <AstNode>[];
+      if (value is ListLiteral) {
+        expressions = _parseList(value);
+      } else if (value is InstanceCreationExpression ||
+          value is PrefixedIdentifier ||
+          value is MethodInvocation) {
+        expressions = [value];
+      } else {
+        throw SourceSpanFormatException(
+            'Expected a Timeout, Skip, or List of those.', _spanFor(value));
+      }
+
+      Timeout? timeout;
+      Object? skip;
+      for (var expression in expressions) {
+        if (expression is InstanceCreationExpression) {
+          var className = expression.constructorName.type.name2.lexeme;
+
+          if (className == 'Timeout') {
+            _assertSingle(timeout, 'Timeout', expression);
+            timeout = _parseTimeoutConstructor(expression);
+            continue;
+          } else if (className == 'Skip') {
+            _assertSingle(skip, 'Skip', expression);
+            skip = _parseSkipConstructor(expression);
+            continue;
+          }
+        } else if (expression is PrefixedIdentifier &&
+            expression.prefix.name == 'Timeout') {
+          if (expression.identifier.name != 'none') {
+            throw SourceSpanFormatException(
+                'Undefined value.', _spanFor(expression));
+          }
+
+          _assertSingle(timeout, 'Timeout', expression);
+          timeout = Timeout.none;
+          continue;
+        } else if (expression is MethodInvocation) {
+          var className =
+              _typeNameFromMethodInvocation(expression, ['Timeout', 'Skip']);
+          if (className == 'Timeout') {
+            _assertSingle(timeout, 'Timeout', expression);
+            timeout = _parseTimeoutConstructor(expression);
+            continue;
+          } else if (className == 'Skip') {
+            _assertSingle(skip, 'Skip', expression);
+            skip = _parseSkipConstructor(expression);
+            continue;
+          }
+        }
+
+        throw SourceSpanFormatException(
+            'Expected a Timeout or Skip.', _spanFor(expression));
+      }
+
+      return Metadata.parse(timeout: timeout, skip: skip);
+    });
+  }
+
+  /// Parses a `const Duration` expression.
+  Duration _parseDuration(Expression expression) {
+    _findConstructorName(expression, 'Duration');
+
+    var arguments = _parseArguments(expression);
+    var values = _parseNamedArguments(arguments)
+        .map((key, value) => MapEntry(key, _parseInt(value)));
+
+    return Duration(
+        days: values['days'] ?? 0,
+        hours: values['hours'] ?? 0,
+        minutes: values['minutes'] ?? 0,
+        seconds: values['seconds'] ?? 0,
+        milliseconds: values['milliseconds'] ?? 0,
+        microseconds: values['microseconds'] ?? 0);
+  }
+
+  Map<String, Expression> _parseNamedArguments(
+          NodeList<Expression> arguments) =>
+      {
+        for (var a in arguments.whereType<NamedExpression>())
+          a.name.label.name: a.expression
+      };
+
+  /// Asserts that [existing] is null.
+  ///
+  /// [name] is the name of the annotation and [node] is its location, used for
+  /// error reporting.
+  void _assertSingle(Object? existing, String name, AstNode node) {
+    if (existing == null) return;
+    throw SourceSpanFormatException(
+        'Only a single $name may be used.', _spanFor(node));
+  }
+
+  NodeList<Expression> _parseArguments(Expression expression) {
+    if (expression is InstanceCreationExpression) {
+      return expression.argumentList.arguments;
+    }
+    if (expression is MethodInvocation) {
+      return expression.argumentList.arguments;
+    }
+    throw SourceSpanFormatException(
+        'Expected an instantiation', _spanFor(expression));
+  }
+
+  /// Resolves a constructor name from its type [identifier] and its
+  /// [constructorName].
+  ///
+  /// Since the parsed file isn't fully resolved, this is necessary to
+  /// disambiguate between prefixed names and named constructors.
+  (String, String?) _resolveConstructor(
+      Identifier identifier, SimpleIdentifier? constructorName) {
+    // The syntax is ambiguous between named constructors and prefixed
+    // annotations, so we need to resolve that ambiguity using the known
+    // prefixes. The analyzer parses "new x.y()" as prefix "x", annotation "y",
+    // and named constructor null. It parses "new x.y.z()" as prefix "x",
+    // annotation "y", and named constructor "z".
+    String className;
+    String? namedConstructor;
+    if (identifier is PrefixedIdentifier &&
+        !_prefixes.contains(identifier.prefix.name) &&
+        constructorName == null) {
+      className = identifier.prefix.name;
+      namedConstructor = identifier.identifier.name;
+    } else {
+      className = identifier is PrefixedIdentifier
+          ? identifier.identifier.name
+          : identifier.name;
+      if (constructorName != null) namedConstructor = constructorName.name;
+    }
+    return (className, namedConstructor);
+  }
+
+  /// Parses a constructor invocation for [className].
+  ///
+  /// Returns the name of the named constructor used, or null if the default
+  /// constructor is used.
+  /// If [expression] is not an instantiation of a [className] throws.
+  String? _findConstructorName(Expression expression, String className) {
+    if (expression is InstanceCreationExpression) {
+      return _findConstructorNameFromInstantiation(expression, className);
+    }
+    if (expression is MethodInvocation) {
+      return _findConstructorNameFromMethod(expression, className);
+    }
+    throw SourceSpanFormatException(
+        'Expected a $className.', _spanFor(expression));
+  }
+
+  String? _findConstructorNameFromInstantiation(
+      InstanceCreationExpression constructor, String className) {
+    var actualClassName = constructor.constructorName.type.name2.lexeme;
+    var constructorName = constructor.constructorName.name?.name;
+
+    if (actualClassName != className) {
+      throw SourceSpanFormatException(
+          'Expected a $className.', _spanFor(constructor));
+    }
+
+    return constructorName;
+  }
+
+  String? _findConstructorNameFromMethod(
+      MethodInvocation constructor, String className) {
+    var target = constructor.target;
+    if (target != null) {
+      // target could be an import prefix or a different class. Assume that
+      // named constructor on a different class won't match the class name we
+      // are looking for.
+      // Example: `test.Timeout()`
+      if (constructor.methodName.name == className) return null;
+      // target is an optionally prefixed class, method is named constructor
+      // Examples: `Timeout.factor(2)`, `test.Timeout.factor(2)`
+      String? parsedName;
+      if (target is SimpleIdentifier) parsedName = target.name;
+      if (target is PrefixedIdentifier) parsedName = target.identifier.name;
+      if (parsedName != className) {
+        throw SourceSpanFormatException(
+            'Expected a $className.', _spanFor(constructor));
+      }
+      return constructor.methodName.name;
+    }
+    // No target, must be an unnamed constructor
+    // Example `Timeout()`
+    if (constructor.methodName.name != className) {
+      throw SourceSpanFormatException(
+          'Expected a $className.', _spanFor(constructor));
+    }
+    return null;
+  }
+
+  /// Returns a type from [candidates] that _may_ be a type instantiated by
+  /// [constructor].
+  ///
+  /// This can be fooled - for instance the invocation `foo.Bar()` may look like
+  /// a prefixed instantiation of a `Bar` even though it is a named constructor
+  /// instantiation of a `foo`, or a method invocation on a variable `foo`, or
+  /// ...
+  ///
+  /// Similarly `Baz.another` may look like the named constructor invocation of
+  /// a `Baz`even though it is a prefixed instantiation of an `another`, or a
+  /// method invocation on a variable `Baz`, or ...
+  String? _typeNameFromMethodInvocation(
+      MethodInvocation constructor, List<String> candidates) {
+    var methodName = constructor.methodName.name;
+    // Examples: `Timeout()`, `test.Timeout()`
+    if (candidates.contains(methodName)) return methodName;
+    var target = constructor.target;
+    // Example: `SomeOtherClass()`
+    if (target == null) return null;
+    if (target is SimpleIdentifier) {
+      // Example: `Timeout.factor()`
+      if (candidates.contains(target.name)) return target.name;
+    }
+    if (target is PrefixedIdentifier) {
+      // Looks  like `some_prefix.SomeTarget.someMethod` - "SomeTarget" is the
+      // only potential type name.
+      // Example: `test.Timeout.factor()`
+      if (candidates.contains(target.identifier.name)) {
+        return target.identifier.name;
+      }
+    }
+    return null;
+  }
+
+  /// Parses a Map literal.
+  ///
+  /// By default, returns [Expression] keys and values. These can be overridden
+  /// with the [key] and [value] parameters.
+  Map<K, V> _parseMap<K, V>(Expression expression,
+      {K Function(Expression)? key, V Function(Expression)? value}) {
+    key ??= (expression) => expression as K;
+    value ??= (expression) => expression as V;
+
+    if (expression is! SetOrMapLiteral) {
+      throw SourceSpanFormatException('Expected a Map.', _spanFor(expression));
+    }
+
+    var map = <K, V>{};
+    for (var element in expression.elements) {
+      if (element is MapLiteralEntry) {
+        map[key(element.key)] = value(element.value);
+      } else {
+        throw SourceSpanFormatException(
+            'Expected a map entry.', _spanFor(element));
+      }
+    }
+    return map;
+  }
+
+  /// Parses a List literal.
+  List<Expression> _parseList(Expression expression) {
+    if (expression is! ListLiteral) {
+      throw SourceSpanFormatException('Expected a List.', _spanFor(expression));
+    }
+
+    var list = expression;
+
+    return list.elements.map((e) {
+      if (e is! Expression) {
+        throw SourceSpanFormatException(
+            'Expected only literal elements.', _spanFor(e));
+      }
+      return e;
+    }).toList();
+  }
+
+  /// Parses a constant number literal.
+  num _parseNum(Expression expression) {
+    if (expression is IntegerLiteral && expression.value != null) {
+      return expression.value!;
+    }
+    if (expression is DoubleLiteral) return expression.value;
+    throw SourceSpanFormatException('Expected a number.', _spanFor(expression));
+  }
+
+  /// Parses a constant int literal.
+  int _parseInt(Expression expression) {
+    if (expression is IntegerLiteral && expression.value != null) {
+      return expression.value!;
+    }
+    throw SourceSpanFormatException(
+        'Expected an integer.', _spanFor(expression));
+  }
+
+  /// Parses a constant String literal.
+  StringLiteral _parseString(Expression expression) {
+    if (expression is StringLiteral && expression.stringValue != null) {
+      return expression;
+    }
+    throw SourceSpanFormatException(
+        'Expected a String literal.', _spanFor(expression));
+  }
+
+  /// Creates a [SourceSpan] for [node].
+  SourceSpan _spanFor(AstNode node) {
+    // Load a SourceFile from scratch here since we're only ever going to emit
+    // one error per file anyway.
+    return SourceFile.fromString(_contents, url: p.toUri(_path))
+        .span(node.offset, node.end);
+  }
+
+  /// Runs [fn] and contextualizes any [SourceSpanFormatException]s that occur
+  /// in it relative to [literal].
+  T _contextualize<T>(StringLiteral literal, T Function() fn) {
+    try {
+      return fn();
+    } on SourceSpanFormatException catch (error) {
+      var file = SourceFile.fromString(_contents, url: p.toUri(_path));
+      var span = contextualizeSpan(error.span!, literal, file);
+      if (span == null) rethrow;
+      throw SourceSpanFormatException(error.message, span);
+    }
+  }
+}
diff --git a/pkgs/test_core/lib/src/runner/platform.dart b/pkgs/test_core/lib/src/runner/platform.dart
new file mode 100644
index 0000000..3048052
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/platform.dart
@@ -0,0 +1,43 @@
+// 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.
+
+import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
+
+import 'environment.dart';
+import 'runner_suite.dart';
+import 'suite.dart';
+
+/// A class that defines a platform for which test suites can be loaded.
+///
+/// A minimal plugin must define [loadChannel], which connects to a client in
+/// which the tests are defined. This is enough to support most of the test
+/// runner's functionality.
+///
+/// In order to support interactive debugging, a plugin must override [load] as
+/// well, which returns a [RunnerSuite] that can contain a custom [Environment]
+/// and control debugging metadata such as [RunnerSuite.isDebugging] and
+/// [RunnerSuite.onDebugging]. The plugin must create this suite by calling the
+/// [deserializeSuite] helper function.
+///
+/// A platform plugin can be registered by passing it to [Loader.new]'s
+/// `plugins` parameter.
+abstract class PlatformPlugin {
+  /// Loads the runner suite for the test file at [path] using [platform], with
+  /// [suiteConfig] encoding the suite-specific configuration.
+  ///
+  /// By default, this just calls [loadChannel] and passes its result to
+  /// [deserializeSuite]. However, it can be overridden to provide more
+  /// fine-grained control over the [RunnerSuite], including providing a custom
+  /// implementation of [Environment].
+  ///
+  /// 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,
+      SuiteConfiguration suiteConfig, Map<String, Object?> message);
+
+  Future closeEphemeral() async {}
+
+  Future close() 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
new file mode 100644
index 0000000..0ded7ae
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/plugin/customizable_platform.dart
@@ -0,0 +1,55 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
+import 'package:yaml/yaml.dart';
+
+import './../platform.dart';
+
+/// An interface for [PlatformPlugin]s that support per-platform customization.
+///
+/// If a [PlatformPlugin] implements this, the user will be able to override the
+/// [Runtime]s it supports using the
+/// [`override_platforms`][override_platforms] configuration field, and define
+/// new runtimes based on them using the [`define_platforms`][define_platforms]
+/// field. The custom settings will be passed to the plugin using
+/// [customizePlatform].
+///
+/// [override_platforms]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#override_platforms
+/// [define_platforms]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#define_platforms
+///
+/// Plugins that implement this **must** support children of recognized runtimes
+/// (created by [Runtime.extend]) in their [load] methods.
+abstract class CustomizablePlatform<T extends Object> extends PlatformPlugin {
+  /// Parses user-provided [settings] for a custom platform into a
+  /// plugin-defined format.
+  ///
+  /// The [settings] come from a user's configuration file. The parsed output
+  /// will be passed to [customizePlatform].
+  ///
+  /// Subclasses should throw [SourceSpanFormatException]s if [settings]
+  /// contains invalid configuration. Unrecognized fields should be ignored if
+  /// possible.
+  T parsePlatformSettings(YamlMap settings);
+
+  /// Merges [settings1] with [settings2] and returns a new settings object that
+  /// includes the configuration of both.
+  ///
+  /// When the settings conflict, [settings2] should take priority.
+  ///
+  /// This is used to merge global settings with local settings, or a custom
+  /// platform's settings with its parent's.
+  T mergePlatformSettings(T settings1, T settings2);
+
+  /// Defines user-provided [settings] for [runtime].
+  ///
+  /// The [runtime] is a runtime this plugin was declared to accept when
+  /// registered with [Loader.registerPlatformPlugin], or a runtime whose
+  /// [Runtime.parent] is one of those runtimes. Subclasses should customize the
+  /// behavior for these runtimes when [loadChannel] or [load] is called with
+  /// the given [runtime], using the [settings] which are parsed by
+  /// [parsePlatformSettings]. This is guaranteed to be called before either
+  /// `load` method.
+  void customizePlatform(Runtime runtime, T settings);
+}
diff --git a/pkgs/test_core/lib/src/runner/plugin/environment.dart b/pkgs/test_core/lib/src/runner/plugin/environment.dart
new file mode 100644
index 0000000..7558d62
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/plugin/environment.dart
@@ -0,0 +1,29 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+
+import '../environment.dart';
+
+/// The default environment for platform plugins.
+class PluginEnvironment implements Environment {
+  @override
+  final supportsDebugging = false;
+  @override
+  Stream<void> get onRestart => StreamController<void>.broadcast().stream;
+
+  const PluginEnvironment();
+
+  @override
+  Uri? get observatoryUrl => null;
+
+  @override
+  Uri? get remoteDebuggerUrl => null;
+
+  @override
+  CancelableOperation displayPause() => throw UnsupportedError(
+      'PluginEnvironment.displayPause is not supported.');
+}
diff --git a/pkgs/test_core/lib/src/runner/plugin/platform_helpers.dart b/pkgs/test_core/lib/src/runner/plugin/platform_helpers.dart
new file mode 100644
index 0000000..450dedb
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/plugin/platform_helpers.dart
@@ -0,0 +1,162 @@
+// 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.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:stack_trace/stack_trace.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test_api/backend.dart'
+    show Metadata, RemoteException, SuitePlatform;
+import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/test.dart'; // ignore: implementation_imports
+
+import '../configuration.dart';
+import '../environment.dart';
+import '../load_exception.dart';
+import '../runner_suite.dart';
+import '../runner_test.dart';
+import '../suite.dart';
+
+/// A helper method for creating a [RunnerSuiteController] containing tests
+/// that communicate over [channel].
+///
+/// This returns a controller so that the caller has a chance to control the
+/// runner suite's debugging state based on plugin-specific logic.
+///
+/// If the suite is closed, this will close [channel].
+///
+/// The [message] parameter is an opaque object passed from the runner to
+/// [PlatformPlugin.load]. Plugins shouldn't interact with it other than to pass
+/// it on to [deserializeSuite].
+///
+/// If [mapper] is passed, it will be used to adjust stack traces for any errors
+/// emitted by tests.
+///
+/// [gatherCoverage] is a callback which returns a hit-map containing merged
+/// coverage report suitable for use with `package:coverage`.
+RunnerSuiteController deserializeSuite(
+    String path,
+    SuitePlatform platform,
+    SuiteConfiguration suiteConfig,
+    Environment environment,
+    StreamChannel<Object?> channel,
+    Object /*Map<String, Object?>*/ message,
+    {Future<Map<String, dynamic>> Function()? gatherCoverage}) {
+  var disconnector = Disconnector<Object?>();
+  var suiteChannel = MultiChannel<Object?>(channel.transform(disconnector));
+
+  suiteChannel.sink.add(<String, Object?>{
+    'type': 'initial',
+    'platform': platform.serialize(),
+    'metadata': suiteConfig.metadata.serialize(),
+    'asciiGlyphs': Platform.isWindows,
+    'path': path,
+    'collectTraces': Configuration.current.reporter == 'json' ||
+        Configuration.current.fileReporters.containsKey('json') ||
+        suiteConfig.testSelections.any(
+            (selection) => selection.line != null || selection.col != null),
+    'noRetry': Configuration.current.noRetry,
+    'foldTraceExcept': Configuration.current.foldTraceExcept.toList(),
+    'foldTraceOnly': Configuration.current.foldTraceOnly.toList(),
+    'allowDuplicateTestNames': suiteConfig.allowDuplicateTestNames,
+    'ignoreTimeouts': suiteConfig.ignoreTimeouts,
+    ...message as Map<String, dynamic>,
+  });
+
+  var completer = Completer<Group>();
+
+  var loadSuiteZone = Zone.current;
+  void handleError(Object error, StackTrace stackTrace) {
+    disconnector.disconnect();
+
+    if (completer.isCompleted) {
+      // If we've already provided a controller, send the error to the
+      // LoadSuite. This will cause the virtual load test to fail, which will
+      // notify the user of the error.
+      loadSuiteZone.handleUncaughtError(error, stackTrace);
+    } else {
+      completer.completeError(error, stackTrace);
+    }
+  }
+
+  suiteChannel.stream.cast<Map<String, Object?>>().listen(
+      (response) {
+        switch (response['type'] as String) {
+          case 'print':
+            print(response['line']);
+            break;
+
+          case 'loadException':
+            handleError(LoadException(path, response['message'] as Object),
+                Trace.current());
+            break;
+
+          case 'error':
+            var asyncError = RemoteException.deserialize(
+                response['error'] as Map<String, dynamic>);
+            handleError(
+                LoadException(path, asyncError.error), asyncError.stackTrace);
+            break;
+
+          case 'success':
+            var deserializer = _Deserializer(suiteChannel);
+            completer.complete(
+                deserializer.deserializeGroup(response['root'] as Map));
+            break;
+        }
+      },
+      onError: handleError,
+      onDone: () {
+        if (completer.isCompleted) return;
+        completer.completeError(
+            LoadException(path, 'Connection closed before test suite loaded.'),
+            Trace.current());
+      });
+
+  return RunnerSuiteController(
+      environment, suiteConfig, suiteChannel, completer.future, platform,
+      path: path,
+      onClose: () => disconnector.disconnect().onError(handleError),
+      gatherCoverage: gatherCoverage);
+}
+
+/// A utility class for storing state while deserializing tests.
+class _Deserializer {
+  /// The channel over which tests communicate.
+  final MultiChannel _channel;
+
+  _Deserializer(this._channel);
+
+  /// Deserializes [group] into a concrete [Group].
+  Group deserializeGroup(Map group) {
+    var metadata = Metadata.deserialize(group['metadata'] as Map);
+    return Group(
+        group['name'] as String,
+        (group['entries'] as List).map((entry) {
+          var map = entry as Map;
+          if (map['type'] == 'group') return deserializeGroup(map);
+          return _deserializeTest(map)!;
+        }),
+        metadata: metadata,
+        trace: group['trace'] == null
+            ? null
+            : Trace.parse(group['trace'] as String),
+        setUpAll: _deserializeTest(group['setUpAll'] as Map?),
+        tearDownAll: _deserializeTest(group['tearDownAll'] as Map?));
+  }
+
+  /// Deserializes [test] into a concrete [Test] class.
+  ///
+  /// Returns `null` if [test] is `null`.
+  Test? _deserializeTest(Map? test) {
+    if (test == null) return null;
+
+    var metadata = Metadata.deserialize(test['metadata'] as Map);
+    var trace =
+        test['trace'] == null ? null : Trace.parse(test['trace'] as String);
+    var testChannel = _channel.virtualChannel((test['channel'] as num).toInt());
+    return RunnerTest(test['name'] as String, metadata, trace, testChannel);
+  }
+}
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
new file mode 100644
index 0000000..8eb2b76
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/plugin/remote_platform_helpers.dart
@@ -0,0 +1,51 @@
+// 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.
+
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test_api/backend.dart'
+    show RemoteListener, StackTraceFormatter, StackTraceMapper;
+
+/// Returns a channel that will emit a serialized representation of the tests
+/// defined in [getMain].
+///
+/// This channel is used to control the tests. Platform plugins should forward
+/// it `deserializeSuite`. It's guaranteed to communicate using only
+/// JSON-serializable values.
+///
+/// Any errors thrown within [getMain], synchronously or not, will be forwarded
+/// to the load test for this suite. Prints will similarly be forwarded to that
+/// test's print stream.
+///
+/// If [hidePrints] is `true` (the default), calls to `print()` within this
+/// suite will not be forwarded to the parent zone's print handler. However, the
+/// caller may want them to be forwarded in (for example) a browser context
+/// where they'll be visible in the development console.
+///
+/// If [beforeLoad] is passed, it's called before the tests have been declared
+/// for this worker.
+StreamChannel<Object?> serializeSuite(Function Function() getMain,
+        {bool hidePrints = true,
+        Future Function(
+                StreamChannel<Object?> Function(String name) suiteChannel)?
+            beforeLoad}) =>
+    RemoteListener.start(
+      getMain,
+      hidePrints: hidePrints,
+      beforeLoad: beforeLoad,
+    );
+
+/// Sets the stack trace mapper for the current test suite.
+///
+/// This is used to convert JavaScript stack traces into their Dart equivalents
+/// using source maps. It should be set before any tests run, usually in the
+/// `onLoad()` callback to [serializeSuite].
+void setStackTraceMapper(StackTraceMapper mapper) {
+  var formatter = StackTraceFormatter.current;
+  if (formatter == null) {
+    throw StateError(
+        'setStackTraceMapper() may only be called within a test worker.');
+  }
+
+  formatter.configure(mapper: mapper);
+}
diff --git a/pkgs/test_core/lib/src/runner/plugin/shared_platform_helpers.dart b/pkgs/test_core/lib/src/runner/plugin/shared_platform_helpers.dart
new file mode 100644
index 0000000..4fdcf23
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/plugin/shared_platform_helpers.dart
@@ -0,0 +1,21 @@
+// Copyright (c) 2023, 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';
+import 'dart:io';
+
+import 'package:async/async.dart';
+import 'package:stream_channel/stream_channel.dart';
+
+/// Converts a raw [Socket] into a [StreamChannel] of JSON objects.
+///
+/// JSON messages are separated by newlines.
+StreamChannel<Object?> jsonSocketStreamChannel(Socket socket) =>
+    StreamChannel.withGuarantees(socket, socket)
+        .cast<List<int>>()
+        .transform(StreamChannelTransformer.fromCodec(utf8))
+        .transformStream(const LineSplitter())
+        .transformSink(StreamSinkTransformer.fromHandlers(
+            handleData: (original, sink) => sink.add('$original\n')))
+        .transform(jsonDocument);
diff --git a/pkgs/test_core/lib/src/runner/reporter.dart b/pkgs/test_core/lib/src/runner/reporter.dart
new file mode 100644
index 0000000..b179337
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/reporter.dart
@@ -0,0 +1,31 @@
+// 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.
+
+/// An interface for classes that watch the progress of an Engine and report it
+/// to the user.
+///
+/// A reporter should subscribe to the Engine's events as soon as it's created.
+abstract class Reporter {
+  /// Pauses the reporter's output.
+  ///
+  /// Subclasses should buffer any events from the engine while they're paused.
+  /// They should also ensure that this does nothing if the reporter is already
+  /// paused.
+  void pause();
+
+  /// Resumes the reporter's output after being [paused].
+  ///
+  /// Subclasses should ensure that this does nothing if the reporter isn't
+  /// paused.
+  void resume();
+}
+
+/// A reporter that prints nothing.
+class SilentReporter implements Reporter {
+  @override
+  void pause() {}
+
+  @override
+  void resume() {}
+}
diff --git a/pkgs/test_core/lib/src/runner/reporter/compact.dart b/pkgs/test_core/lib/src/runner/reporter/compact.dart
new file mode 100644
index 0000000..13ccd11
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/reporter/compact.dart
@@ -0,0 +1,446 @@
+// 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.
+
+import 'dart:async';
+import 'dart:io';
+import 'dart:isolate';
+
+import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/message.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports
+
+import '../../util/io.dart';
+import '../../util/pretty_print.dart';
+import '../../util/pretty_print.dart' as utils;
+import '../engine.dart';
+import '../load_exception.dart';
+import '../load_suite.dart';
+import '../reporter.dart';
+
+/// A reporter that prints test results to the console in a single
+/// continuously-updating line.
+class CompactReporter implements Reporter {
+  /// 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.
+  final String _green;
+
+  /// The terminal escape for red text, or the empty string if this is Windows
+  /// or not outputting to a terminal.
+  final String _red;
+
+  /// The terminal escape for yellow text, or the empty string if this is
+  /// Windows or not outputting to a terminal.
+  final String _yellow;
+
+  /// The terminal escape for gray text, or the empty string if this is
+  /// Windows or not outputting to a terminal.
+  final String _gray;
+
+  /// The terminal escape code for cyan text, or the empty string if this is
+  /// Windows or not outputting to a terminal.
+  final String _cyan;
+
+  /// The terminal escape for bold text, or the empty string if this is
+  /// Windows or not outputting to a terminal.
+  final String _bold;
+
+  /// The terminal escape for removing test coloring, or the empty string if
+  /// this is Windows or not outputting to a terminal.
+  final String _noColor;
+
+  /// The engine used to run the tests.
+  final Engine _engine;
+
+  /// 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();
+
+  /// Whether we've started [_stopwatch].
+  ///
+  /// We can't just use `_stopwatch.isRunning` because the stopwatch is stopped
+  /// when the reporter is paused.
+  var _stopwatchStarted = false;
+
+  /// The size of `_engine.passed` last time a progress notification was
+  /// printed.
+  int _lastProgressPassed = 0;
+
+  /// 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;
+
+  /// The duration of the test run in seconds last time a progress notification
+  /// was printed.
+  int? _lastProgressElapsed;
+
+  /// The message printed for the last progress notification.
+  String? _lastProgressMessage;
+
+  /// The suffix added to the last progress notification.
+  String? _lastProgressSuffix;
+
+  /// Whether the message printed for the last progress notification was
+  /// truncated.
+  bool? _lastProgressTruncated;
+
+  // Whether a newline has been printed since the last progress line.
+  var _printedNewline = true;
+
+  /// Whether the reporter is paused.
+  var _paused = false;
+
+  // Whether a notice should be logged about enabling stack trace chaining at
+  // the end of all tests running.
+  var _shouldPrintStackTraceChainingNotice = 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 to the
+  /// terminal.
+  ///
+  /// 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,
+      {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[90m' : '',
+        _cyan = color ? '\u001b[36m' : '',
+        _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.
+    _subscriptions.add(_engine.success.asStream().listen(_onDone));
+  }
+
+  @override
+  void pause() {
+    if (_paused) return;
+    _paused = true;
+
+    if (!_printedNewline) _sink.writeln('');
+    _printedNewline = true;
+    _stopwatch.stop();
+
+    // Force the next message to be printed, even if it's identical to the
+    // previous one. If the reporter was paused, text was probably printed
+    // during the pause.
+    _lastProgressMessage = null;
+
+    for (var subscription in _subscriptions) {
+      subscription.pause();
+    }
+  }
+
+  @override
+  void resume() {
+    if (!_paused) return;
+    _paused = false;
+
+    if (_stopwatchStarted) _stopwatch.start();
+
+    for (var subscription in _subscriptions) {
+      subscription.resume();
+    }
+  }
+
+  void _cancel() {
+    for (var subscription in _subscriptions) {
+      subscription.cancel();
+    }
+    _subscriptions.clear();
+  }
+
+  /// A callback called when the engine begins running [liveTest].
+  void _onTestStarted(LiveTest liveTest) {
+    if (!_stopwatchStarted) {
+      _stopwatchStarted = true;
+      _stopwatch.start();
+
+      // Keep updating the time even when nothing else is happening.
+      _subscriptions.add(Stream<void>.periodic(const Duration(seconds: 1))
+          .listen((_) => _progressLine(_lastProgressMessage ?? '')));
+    }
+
+    // If this is the first test or suite load to start, print a progress line
+    // so the user knows what's running.
+    if ((_engine.active.length == 1 && _engine.active.first == liveTest) ||
+        (_engine.active.isEmpty &&
+            _engine.activeSuiteLoads.length == 1 &&
+            _engine.activeSuiteLoads.first == liveTest)) {
+      _progressLine(_description(liveTest));
+    }
+
+    _subscriptions.add(liveTest.onStateChange
+        .listen((state) => _onStateChange(liveTest, state)));
+
+    _subscriptions.add(liveTest.onError
+        .listen((error) => _onError(liveTest, error.error, error.stackTrace)));
+
+    _subscriptions.add(liveTest.onMessage.listen((message) {
+      _progressLine(_description(liveTest), truncate: false);
+      if (!_printedNewline) _sink.writeln('');
+      _printedNewline = true;
+
+      var text = message.text;
+      if (message.type == MessageType.skip) text = '  $_yellow$text$_noColor';
+      _sink.writeln(text);
+    }));
+
+    liveTest.onComplete.then((_) {
+      var result = liveTest.state.result;
+      if (result != Result.error && result != Result.failure) return;
+      var quotedName = Platform.isWindows
+          ? '"${liveTest.test.name.replaceAll('"', '"""')}"'
+          : "'${liveTest.test.name.replaceAll("'", r"'\''")}'";
+      _sink.writeln('');
+      _sink.writeln('$_bold${_cyan}To run this test again:$_noColor '
+          '${Platform.executable} test ${liveTest.suite.path} '
+          '-p ${liveTest.suite.platform.runtime.identifier} '
+          '--plain-name $quotedName');
+    });
+  }
+
+  /// A callback called when [liveTest]'s state becomes [state].
+  void _onStateChange(LiveTest liveTest, State state) {
+    if (state.status != Status.complete) return;
+
+    // Errors are printed in [onError]; no need to print them here as well.
+    if (state.result == Result.failure) return;
+    if (state.result == Result.error) return;
+
+    // Always display the name of the oldest active test, unless testing
+    // is finished in which case display the last test to complete.
+    if (_engine.active.isEmpty) {
+      _progressLine(_description(liveTest));
+    } else {
+      _progressLine(_description(_engine.active.first));
+    }
+  }
+
+  /// A callback called when [liveTest] throws [error].
+  void _onError(LiveTest liveTest, Object error, StackTrace stackTrace) {
+    if (!liveTest.test.metadata.chainStackTraces &&
+        !liveTest.suite.isLoadSuite) {
+      _shouldPrintStackTraceChainingNotice = true;
+    }
+
+    if (liveTest.state.status != Status.complete) return;
+
+    _progressLine(_description(liveTest),
+        truncate: false, suffix: ' $_bold$_red[E]$_noColor');
+    if (!_printedNewline) _sink.writeln('');
+    _printedNewline = true;
+
+    if (error is! LoadException) {
+      _sink.writeln(indent(error.toString()));
+      _sink.writeln(indent('$stackTrace'));
+      return;
+    }
+
+    // TODO - what type is this?
+    _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 &&
+        error.innerError is! IsolateSpawnException &&
+        error.innerError is! FormatException &&
+        error.innerError is! String) {
+      _sink.writeln(indent('$stackTrace'));
+    }
+  }
+
+  /// A callback called when the engine is finished running tests.
+  ///
+  /// [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();
+    _stopwatch.stop();
+
+    // A null success value indicates that the engine was closed before the
+    // tests finished running, probably because of a signal from the user. We
+    // shouldn't print summary information, we should just make sure the
+    // terminal cursor is on its own line.
+    if (success == null) {
+      if (!_printedNewline) _sink.writeln('');
+      _printedNewline = true;
+      return;
+    }
+
+    if (_engine.liveTests.isEmpty) {
+      if (!_printedNewline) _sink.write('\r');
+      var message = 'No tests ran.';
+      _sink.write(message);
+
+      // Add extra padding to overwrite any load messages.
+      if (!_printedNewline) _sink.write(' ' * (lineLength - message.length));
+      _sink.writeln('');
+    } else if (!success) {
+      for (var liveTest in _engine.active) {
+        _progressLine(_description(liveTest),
+            truncate: false,
+            suffix: ' - did not complete $_bold$_red[E]$_noColor');
+        _sink.writeln('');
+      }
+      _progressLine('Some tests failed.', color: _red);
+      _sink.writeln('');
+    } else if (_engine.passed.isEmpty) {
+      _progressLine('All tests skipped.');
+      _sink.writeln('');
+    } else {
+      _progressLine('All tests passed!');
+      _sink.writeln('');
+    }
+
+    if (_shouldPrintStackTraceChainingNotice) {
+      _sink
+        ..writeln('')
+        ..writeln('Consider enabling the flag chain-stack-traces to '
+            'receive more detailed exceptions.\n'
+            "For example, 'dart test --chain-stack-traces'.");
+    }
+  }
+
+  /// Prints a line representing the current state of the tests.
+  ///
+  /// [message] goes after the progress report, and may be truncated to fit the
+  /// entire line within [lineLength]. If [color] is passed, it's used as the
+  /// 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}) {
+    var elapsed = _stopwatch.elapsed.inSeconds;
+
+    // Print nothing if nothing has changed since the last progress line.
+    if (_engine.passed.length == _lastProgressPassed &&
+        _engine.skipped.length == _lastProgressSkipped &&
+        _engine.failed.length == _lastProgressFailed &&
+        message == _lastProgressMessage &&
+        // Don't re-print just because a suffix was removed.
+        (suffix == null || suffix == _lastProgressSuffix) &&
+        // Don't re-print just because the message became re-truncated, because
+        // that doesn't add information.
+        (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.
+        (_printedNewline || elapsed == _lastProgressElapsed)) {
+      return false;
+    }
+
+    _lastProgressPassed = _engine.passed.length;
+    _lastProgressSkipped = _engine.skipped.length;
+    _lastProgressFailed = _engine.failed.length;
+    _lastProgressElapsed = elapsed;
+    _lastProgressMessage = message;
+    _lastProgressSuffix = suffix;
+    _lastProgressTruncated = truncate;
+
+    if (suffix != null) message += suffix;
+    color ??= '';
+    var duration = _stopwatch.elapsed;
+    var buffer = StringBuffer();
+
+    // \r moves back to the beginning of the current line.
+    buffer.write('\r${_timeString(duration)} ');
+    buffer.write(_green);
+    buffer.write('+');
+    buffer.write(_engine.passed.length);
+    buffer.write(_noColor);
+
+    if (_engine.skipped.isNotEmpty) {
+      buffer.write(_yellow);
+      buffer.write(' ~');
+      buffer.write(_engine.skipped.length);
+      buffer.write(_noColor);
+    }
+
+    if (_engine.failed.isNotEmpty) {
+      buffer.write(_red);
+      buffer.write(' -');
+      buffer.write(_engine.failed.length);
+      buffer.write(_noColor);
+    }
+
+    buffer.write(': ');
+    buffer.write(color);
+
+    // Ensure the line fits within [lineLength]. [buffer] includes the color
+    // escape sequences too. Because these sequences are not visible characters,
+    // we make sure they are not counted towards the limit.
+    var length = withoutColors(buffer.toString()).length;
+    if (truncate) message = utils.truncate(message, lineLength - length);
+    buffer.write(message);
+    buffer.write(_noColor);
+
+    // Pad the rest of the line so that it looks erased.
+    buffer.write(' ' * (lineLength - withoutColors(buffer.toString()).length));
+    _sink.write(buffer.toString());
+
+    _printedNewline = false;
+    return true;
+  }
+
+  /// Returns a representation of [duration] as `MM:SS`.
+  String _timeString(Duration duration) {
+    return "${duration.inMinutes.toString().padLeft(2, '0')}:"
+        "${(duration.inSeconds % 60).toString().padLeft(2, '0')}";
+  }
+
+  /// Returns a description of [liveTest].
+  ///
+  /// This differs from the test's own description in that it may also include
+  /// the suite's name.
+  String _description(LiveTest liveTest) {
+    var name = liveTest.test.name;
+
+    if (_printPath &&
+        liveTest.suite is! LoadSuite &&
+        liveTest.suite.path != null) {
+      name = '${liveTest.suite.path}: $name';
+    }
+
+    if (_printPlatform) {
+      name = '[${liveTest.suite.platform.runtime.name}, '
+          '${liveTest.suite.platform.compiler.name}] $name';
+    }
+
+    if (liveTest.suite is LoadSuite) name = '$_bold$_gray$name$_noColor';
+
+    return name;
+  }
+}
diff --git a/pkgs/test_core/lib/src/runner/reporter/expanded.dart b/pkgs/test_core/lib/src/runner/reporter/expanded.dart
new file mode 100644
index 0000000..be2016c
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/reporter/expanded.dart
@@ -0,0 +1,347 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/message.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports
+
+import '../../util/pretty_print.dart';
+import '../engine.dart';
+import '../load_exception.dart';
+import '../load_suite.dart';
+import '../reporter.dart';
+
+/// A reporter that prints each test on its own line.
+///
+/// This is currently used in place of [CompactReporter] by `lib/test.dart`,
+/// which can't transitively import `dart:io` but still needs access to a runner
+/// so that test files can be run directly. This means that until issue 6943 is
+/// fixed, this must not import `dart:io`.
+class ExpandedReporter implements Reporter {
+  /// 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.
+  final String _green;
+
+  /// The terminal escape for red text, or the empty string if this is Windows
+  /// or not outputting to a terminal.
+  final String _red;
+
+  /// The terminal escape for yellow text, or the empty string if this is
+  /// Windows or not outputting to a terminal.
+  final String _yellow;
+
+  /// The terminal escape for gray text, or the empty string if this is
+  /// Windows or not outputting to a terminal.
+  final String _gray;
+
+  /// The terminal escape for bold text, or the empty string if this is
+  /// Windows or not outputting to a terminal.
+  final String _bold;
+
+  /// The terminal escape for removing test coloring, or the empty string if
+  /// this is Windows or not outputting to a terminal.
+  final String _noColor;
+
+  /// The engine used to run the tests.
+  final Engine _engine;
+
+  /// 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();
+
+  /// The size of `_engine.passed` last time a progress notification was
+  /// printed.
+  int _lastProgressPassed = 0;
+
+  /// The size of `_engine.skipped` last time a progress notification was
+  /// printed.
+  int _lastProgressSkipped = 0;
+
+  /// The size of `_engine.failed` last time a progress notification was
+  /// printed.
+  int _lastProgressFailed = 0;
+
+  /// The message printed for the last progress notification.
+  String _lastProgressMessage = '';
+
+  /// The suffix added to the last progress notification.
+  String? _lastProgressSuffix;
+
+  /// Whether the reporter is paused.
+  var _paused = false;
+
+  // Whether a notice should be logged about enabling stack trace chaining at
+  // the end of all tests running.
+  var _shouldPrintStackTraceChainingNotice = 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 to the
+  /// terminal.
+  ///
+  /// 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 ExpandedReporter watch(Engine engine, StringSink sink,
+          {required bool color,
+          required bool printPath,
+          required bool printPlatform}) =>
+      ExpandedReporter._(engine, sink,
+          color: color, printPath: printPath, printPlatform: printPlatform);
+
+  ExpandedReporter._(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[90m' : '',
+        _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.
+    _subscriptions.add(_engine.success.asStream().listen(_onDone));
+  }
+
+  @override
+  void pause() {
+    if (_paused) return;
+    _paused = true;
+
+    _stopwatch.stop();
+
+    for (var subscription in _subscriptions) {
+      subscription.pause();
+    }
+  }
+
+  @override
+  void resume() {
+    if (!_paused) return;
+
+    _stopwatch.start();
+
+    for (var subscription in _subscriptions) {
+      subscription.resume();
+    }
+  }
+
+  void _cancel() {
+    for (var subscription in _subscriptions) {
+      subscription.cancel();
+    }
+    _subscriptions.clear();
+  }
+
+  /// A callback called when the engine begins running [liveTest].
+  void _onTestStarted(LiveTest liveTest) {
+    if (liveTest.suite is! LoadSuite) {
+      if (!_stopwatch.isRunning) _stopwatch.start();
+
+      // If this is the first non-load test to start, print a progress line so
+      // the user knows what's running.
+      if (_engine.active.length == 1) _progressLine(_description(liveTest));
+
+      // The engine surfaces load tests when there are no other tests running,
+      // but because the expanded reporter's output is always visible, we don't
+      // emit information about them unless they fail.
+      _subscriptions.add(liveTest.onStateChange
+          .listen((state) => _onStateChange(liveTest, state)));
+    } else if (_engine.active.isEmpty &&
+        _engine.activeSuiteLoads.length == 1 &&
+        _engine.activeSuiteLoads.first == liveTest &&
+        liveTest.test.name.startsWith('loading ')) {
+      // Print a progress line for ongoing suite loading synthetic test since it
+      // may be slow (or stuck) depending on the platform.
+      _progressLine(_description(liveTest));
+    }
+
+    _subscriptions.add(liveTest.onError
+        .listen((error) => _onError(liveTest, error.error, error.stackTrace)));
+
+    _subscriptions.add(liveTest.onMessage.listen((message) {
+      _progressLine(_description(liveTest));
+      var text = message.text;
+      if (message.type == MessageType.skip) text = '  $_yellow$text$_noColor';
+      _sink.writeln(text);
+    }));
+  }
+
+  /// A callback called when [liveTest]'s state becomes [state].
+  void _onStateChange(LiveTest liveTest, State state) {
+    if (state.status != Status.complete) return;
+
+    // If any tests are running, display the name of the oldest active
+    // test.
+    if (_engine.active.isNotEmpty) {
+      _progressLine(_description(_engine.active.first));
+    }
+  }
+
+  /// A callback called when [liveTest] throws [error].
+  void _onError(LiveTest liveTest, Object error, StackTrace stackTrace) {
+    if (!liveTest.test.metadata.chainStackTraces &&
+        !liveTest.suite.isLoadSuite) {
+      _shouldPrintStackTraceChainingNotice = true;
+    }
+
+    if (liveTest.state.status != Status.complete) return;
+
+    _progressLine(_description(liveTest), suffix: ' $_bold$_red[E]$_noColor');
+
+    if (error is! LoadException) {
+      _sink
+        ..writeln(indent('$error'))
+        ..writeln(indent('$stackTrace'));
+      return;
+    }
+
+    // TODO - what type is this?
+    _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) {
+      _sink.writeln(indent('$stackTrace'));
+    }
+  }
+
+  /// A callback called when the engine is finished running tests.
+  ///
+  /// [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();
+    // 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.
+    if (success == null) return;
+
+    if (_engine.liveTests.isEmpty) {
+      _sink.writeln('No tests ran.');
+    } else if (!success) {
+      for (var liveTest in _engine.active) {
+        _progressLine(_description(liveTest),
+            suffix: ' - did not complete $_bold$_red[E]$_noColor');
+      }
+      _progressLine('Some tests failed.', color: _red);
+    } else if (_engine.passed.isEmpty) {
+      _progressLine('All tests skipped.');
+    } else {
+      _progressLine('All tests passed!');
+    }
+
+    if (_shouldPrintStackTraceChainingNotice) {
+      _sink
+        ..writeln('')
+        ..writeln('Consider enabling the flag chain-stack-traces to '
+            'receive more detailed exceptions.\n'
+            "For example, 'dart test --chain-stack-traces'.");
+    }
+  }
+
+  /// Prints a line representing the current state of the tests.
+  ///
+  /// [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}) {
+    // Print nothing if nothing has changed since the last progress line.
+    if (_engine.passed.length == _lastProgressPassed &&
+        _engine.skipped.length == _lastProgressSkipped &&
+        _engine.failed.length == _lastProgressFailed &&
+        message == _lastProgressMessage &&
+        // Don't re-print just because a suffix was removed.
+        (suffix == null || suffix == _lastProgressSuffix)) {
+      return;
+    }
+
+    _lastProgressPassed = _engine.passed.length;
+    _lastProgressSkipped = _engine.skipped.length;
+    _lastProgressFailed = _engine.failed.length;
+    _lastProgressMessage = message;
+    _lastProgressSuffix = suffix;
+
+    if (suffix != null) message += suffix;
+    color ??= '';
+    var duration = _stopwatch.elapsed;
+    var buffer = StringBuffer();
+
+    // \r moves back to the beginning of the current line.
+    buffer.write('${_timeString(duration)} ');
+    buffer.write(_green);
+    buffer.write('+');
+    buffer.write(_engine.passed.length);
+    buffer.write(_noColor);
+
+    if (_engine.skipped.isNotEmpty) {
+      buffer.write(_yellow);
+      buffer.write(' ~');
+      buffer.write(_engine.skipped.length);
+      buffer.write(_noColor);
+    }
+
+    if (_engine.failed.isNotEmpty) {
+      buffer.write(_red);
+      buffer.write(' -');
+      buffer.write(_engine.failed.length);
+      buffer.write(_noColor);
+    }
+
+    buffer.write(': ');
+    buffer.write(color);
+    buffer.write(message);
+    buffer.write(_noColor);
+
+    _sink.writeln(buffer.toString());
+  }
+
+  /// Returns a representation of [duration] as `MM:SS`.
+  String _timeString(Duration duration) {
+    return "${duration.inMinutes.toString().padLeft(2, '0')}:"
+        "${(duration.inSeconds % 60).toString().padLeft(2, '0')}";
+  }
+
+  /// Returns a description of [liveTest].
+  ///
+  /// This differs from the test's own description in that it may also include
+  /// the suite's name.
+  String _description(LiveTest liveTest) {
+    var name = liveTest.test.name;
+
+    if (_printPath &&
+        liveTest.suite is! LoadSuite &&
+        liveTest.suite.path != null) {
+      name = '${liveTest.suite.path}: $name';
+    }
+
+    if (_printPlatform) {
+      name = '[${liveTest.suite.platform.runtime.name}, '
+          '${liveTest.suite.platform.compiler.name}] $name';
+    }
+
+    if (liveTest.suite is LoadSuite) name = '$_bold$_gray$name$_noColor';
+
+    return name;
+  }
+}
diff --git a/pkgs/test_core/lib/src/runner/reporter/failures_only.dart b/pkgs/test_core/lib/src/runner/reporter/failures_only.dart
new file mode 100644
index 0000000..b6b85e7
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/reporter/failures_only.dart
@@ -0,0 +1,295 @@
+// Copyright (c) 2024, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/message.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports
+
+import '../../util/pretty_print.dart';
+import '../engine.dart';
+import '../load_exception.dart';
+import '../load_suite.dart';
+import '../reporter.dart';
+
+/// A reporter that only prints when a test fails.
+class FailuresOnlyReporter implements Reporter {
+  /// 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.
+  final String _green;
+
+  /// The terminal escape for red text, or the empty string if this is Windows
+  /// or not outputting to a terminal.
+  final String _red;
+
+  /// The terminal escape for yellow text, or the empty string if this is
+  /// Windows or not outputting to a terminal.
+  final String _yellow;
+
+  /// The terminal escape for gray text, or the empty string if this is
+  /// Windows or not outputting to a terminal.
+  final String _gray;
+
+  /// The terminal escape for bold text, or the empty string if this is
+  /// Windows or not outputting to a terminal.
+  final String _bold;
+
+  /// The terminal escape for removing test coloring, or the empty string if
+  /// this is Windows or not outputting to a terminal.
+  final String _noColor;
+
+  /// The engine used to run the tests.
+  final Engine _engine;
+
+  /// 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;
+
+  /// The size of `_engine.passed` last time a progress notification was
+  /// printed.
+  int _lastProgressPassed = 0;
+
+  /// The size of `_engine.skipped` last time a progress notification was
+  /// printed.
+  int _lastProgressSkipped = 0;
+
+  /// The size of `_engine.failed` last time a progress notification was
+  /// printed.
+  int _lastProgressFailed = 0;
+
+  /// The message printed for the last progress notification.
+  String _lastProgressMessage = '';
+
+  /// The suffix added to the last progress notification.
+  String? _lastProgressSuffix;
+
+  /// Whether the reporter is paused.
+  var _paused = false;
+
+  // Whether a notice should be logged about enabling stack trace chaining at
+  // the end of all tests running.
+  var _shouldPrintStackTraceChainingNotice = 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 to the
+  /// terminal.
+  ///
+  /// 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 FailuresOnlyReporter watch(Engine engine, StringSink sink,
+          {required bool color,
+          required bool printPath,
+          required bool printPlatform}) =>
+      FailuresOnlyReporter._(engine, sink,
+          color: color, printPath: printPath, printPlatform: printPlatform);
+
+  FailuresOnlyReporter._(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[90m' : '',
+        _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.
+    _subscriptions.add(_engine.success.asStream().listen(_onDone));
+  }
+
+  @override
+  void pause() {
+    if (_paused) return;
+    _paused = true;
+
+    for (var subscription in _subscriptions) {
+      subscription.pause();
+    }
+  }
+
+  @override
+  void resume() {
+    if (!_paused) return;
+
+    for (var subscription in _subscriptions) {
+      subscription.resume();
+    }
+  }
+
+  void _cancel() {
+    for (var subscription in _subscriptions) {
+      subscription.cancel();
+    }
+    _subscriptions.clear();
+  }
+
+  /// A callback called when the engine begins running [liveTest].
+  void _onTestStarted(LiveTest liveTest) {
+    _subscriptions.add(liveTest.onError
+        .listen((error) => _onError(liveTest, error.error, error.stackTrace)));
+
+    _subscriptions.add(liveTest.onMessage.listen((message) {
+      // TODO - Should this suppress output? Behave like printOnFailure?
+      _progressLine(_description(liveTest));
+      var text = message.text;
+      if (message.type == MessageType.skip) text = '  $_yellow$text$_noColor';
+      _sink.writeln(text);
+    }));
+  }
+
+  /// A callback called when [liveTest] throws [error].
+  void _onError(LiveTest liveTest, Object error, StackTrace stackTrace) {
+    if (!liveTest.test.metadata.chainStackTraces &&
+        !liveTest.suite.isLoadSuite) {
+      _shouldPrintStackTraceChainingNotice = true;
+    }
+
+    if (liveTest.state.status != Status.complete) return;
+
+    _progressLine(_description(liveTest), suffix: ' $_bold$_red[E]$_noColor');
+
+    if (error is! LoadException) {
+      _sink
+        ..writeln(indent('$error'))
+        ..writeln(indent('$stackTrace'));
+      return;
+    }
+
+    // TODO - what type is this?
+    _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) {
+      _sink.writeln(indent('$stackTrace'));
+    }
+  }
+
+  /// A callback called when the engine is finished running tests.
+  ///
+  /// [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();
+    // 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.
+    if (success == null) return;
+
+    if (_engine.liveTests.isEmpty) {
+      _sink.writeln('No tests ran.');
+    } else if (!success) {
+      for (var liveTest in _engine.active) {
+        _progressLine(_description(liveTest),
+            suffix: ' - did not complete $_bold$_red[E]$_noColor');
+      }
+      _progressLine('Some tests failed.', color: _red);
+    } else if (_engine.passed.isEmpty) {
+      _progressLine('All tests skipped.');
+    } else {
+      _progressLine('All tests passed!');
+    }
+
+    if (_shouldPrintStackTraceChainingNotice) {
+      _sink
+        ..writeln('')
+        ..writeln('Consider enabling the flag chain-stack-traces to '
+            'receive more detailed exceptions.\n'
+            "For example, 'dart test --chain-stack-traces'.");
+    }
+  }
+
+  /// Prints a line representing the current state of the tests.
+  ///
+  /// [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}) {
+    // Print nothing if nothing has changed since the last progress line.
+    if (_engine.passed.length == _lastProgressPassed &&
+        _engine.skipped.length == _lastProgressSkipped &&
+        _engine.failed.length == _lastProgressFailed &&
+        message == _lastProgressMessage &&
+        // Don't re-print just because a suffix was removed.
+        (suffix == null || suffix == _lastProgressSuffix)) {
+      return;
+    }
+
+    _lastProgressPassed = _engine.passed.length;
+    _lastProgressSkipped = _engine.skipped.length;
+    _lastProgressFailed = _engine.failed.length;
+    _lastProgressMessage = message;
+    _lastProgressSuffix = suffix;
+
+    if (suffix != null) message += suffix;
+    color ??= '';
+    var buffer = StringBuffer();
+
+    buffer.write(_green);
+    buffer.write('+');
+    buffer.write(_engine.passed.length);
+    buffer.write(_noColor);
+
+    if (_engine.skipped.isNotEmpty) {
+      buffer.write(_yellow);
+      buffer.write(' ~');
+      buffer.write(_engine.skipped.length);
+      buffer.write(_noColor);
+    }
+
+    if (_engine.failed.isNotEmpty) {
+      buffer.write(_red);
+      buffer.write(' -');
+      buffer.write(_engine.failed.length);
+      buffer.write(_noColor);
+    }
+
+    buffer.write(': ');
+    buffer.write(color);
+    buffer.write(message);
+    buffer.write(_noColor);
+
+    _sink.writeln(buffer.toString());
+  }
+
+  /// Returns a description of [liveTest].
+  ///
+  /// This differs from the test's own description in that it may also include
+  /// the suite's name.
+  String _description(LiveTest liveTest) {
+    var name = liveTest.test.name;
+
+    if (_printPath &&
+        liveTest.suite is! LoadSuite &&
+        liveTest.suite.path != null) {
+      name = '${liveTest.suite.path}: $name';
+    }
+
+    if (_printPlatform) {
+      name = '[${liveTest.suite.platform.runtime.name}, '
+          '${liveTest.suite.platform.compiler.name}] $name';
+    }
+
+    if (liveTest.suite is LoadSuite) name = '$_bold$_gray$name$_noColor';
+
+    return name;
+  }
+}
diff --git a/pkgs/test_core/lib/src/runner/reporter/github.dart b/pkgs/test_core/lib/src/runner/reporter/github.dart
new file mode 100644
index 0000000..5249817
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/reporter/github.dart
@@ -0,0 +1,239 @@
+// Copyright (c) 2022, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// ignore_for_file: implementation_imports
+
+import 'dart:async';
+
+import 'package:test_api/src/backend/live_test.dart';
+import 'package:test_api/src/backend/message.dart';
+import 'package:test_api/src/backend/state.dart';
+import 'package:test_api/src/backend/util/pretty_print.dart';
+
+import '../engine.dart';
+import '../load_suite.dart';
+import '../reporter.dart';
+
+/// A reporter that prints test output using formatting for Github Actions.
+///
+/// See
+/// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
+/// for a description of the output format, and
+/// https://github.com/dart-lang/test/issues/1415 for discussions about this
+/// implementation.
+class GithubReporter implements Reporter {
+  /// The engine used to run the tests.
+  final Engine _engine;
+
+  /// 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;
+
+  /// Whether the reporter is paused.
+  var _paused = false;
+
+  /// The set of all subscriptions to various streams.
+  final _subscriptions = <StreamSubscription>{};
+
+  final StringSink _sink;
+
+  final Map<LiveTest, List<Message>> _testMessages = {};
+
+  final Set<LiveTest> _completedTests = {};
+
+  /// Watches the tests run by [engine] and prints their results as JSON.
+  static GithubReporter watch(
+    Engine engine,
+    StringSink sink, {
+    required bool printPath,
+    required bool printPlatform,
+  }) =>
+      GithubReporter._(engine, sink, printPath, printPlatform);
+
+  GithubReporter._(
+      this._engine, this._sink, this._printPath, this._printPlatform) {
+    _subscriptions.add(_engine.onTestStarted.listen(_onTestStarted));
+    _subscriptions.add(_engine.success.asStream().listen(_onDone));
+
+    // Add a spacer between pre-test output and the test results.
+    _sink.writeln();
+  }
+
+  @override
+  void pause() {
+    if (_paused) return;
+    _paused = true;
+
+    for (var subscription in _subscriptions) {
+      subscription.pause();
+    }
+  }
+
+  @override
+  void resume() {
+    if (!_paused) return;
+    _paused = false;
+
+    for (var subscription in _subscriptions) {
+      subscription.resume();
+    }
+  }
+
+  void _cancel() {
+    for (var subscription in _subscriptions) {
+      subscription.cancel();
+    }
+    _subscriptions.clear();
+  }
+
+  /// A callback called when the engine begins running [liveTest].
+  void _onTestStarted(LiveTest liveTest) {
+    // Convert the future to a stream so that the subscription can be paused or
+    // canceled.
+    _subscriptions.add(
+        liveTest.onComplete.asStream().listen((_) => _onComplete(liveTest)));
+
+    _subscriptions.add(liveTest.onError
+        .listen((error) => _onError(liveTest, error.error, error.stackTrace)));
+
+    // Collect messages from tests as they are emitted.
+    _subscriptions.add(liveTest.onMessage.listen((message) {
+      if (_completedTests.contains(liveTest)) {
+        // The test has already completed and it's previous messages were
+        // written out; ensure this post-completion output is not lost.
+        _sink.writeln(message.text);
+      } else {
+        _testMessages.putIfAbsent(liveTest, () => []).add(message);
+      }
+    }));
+  }
+
+  /// A callback called when [liveTest] finishes running.
+  void _onComplete(LiveTest test) {
+    final errors = test.errors;
+    final messages = _testMessages[test] ?? [];
+    final skipped = test.state.result == Result.skipped;
+    final failed = errors.isNotEmpty;
+    final loadSuite = test.suite is LoadSuite;
+    final synthetic = loadSuite ||
+        test.individualName == '(setUpAll)' ||
+        test.individualName == '(tearDownAll)';
+
+    // Mark this test as having completed.
+    _completedTests.add(test);
+
+    // Don't emit any info for loadSuite, setUpAll, or tearDownAll tests
+    // unless they contain errors or other info.
+    if (synthetic && (errors.isEmpty && messages.isEmpty)) {
+      return;
+    }
+
+    // For now, we use the same icon for both tests and test-like structures
+    // (loadSuite, setUpAll, tearDownAll).
+    var defaultIcon = synthetic ? _GithubMarkup.passed : _GithubMarkup.passed;
+    final prefix = failed
+        ? _GithubMarkup.failed
+        : skipped
+            ? _GithubMarkup.skipped
+            : defaultIcon;
+    final statusSuffix = failed
+        ? ' (failed)'
+        : skipped
+            ? ' (skipped)'
+            : '';
+
+    var name = test.test.name;
+    if (!loadSuite) {
+      if (_printPath && test.suite.path != null) {
+        name = '${test.suite.path}: $name';
+      }
+    }
+    if (_printPlatform) {
+      name = '[${test.suite.platform.runtime.name}, '
+          '${test.suite.platform.compiler.name}] $name';
+    }
+    if (messages.isEmpty && errors.isEmpty) {
+      _sink.writeln('$prefix $name$statusSuffix');
+    } else {
+      _sink.writeln(_GithubMarkup.startGroup('$prefix $name$statusSuffix'));
+      for (var message in messages) {
+        _sink.writeln(message.text);
+      }
+      for (var error in errors) {
+        _sink.writeln('${error.error}');
+        _sink.writeln(error.stackTrace.toString().trimRight());
+      }
+      _sink.writeln(_GithubMarkup.endGroup);
+    }
+  }
+
+  /// A callback called when [test] throws [error].
+  void _onError(LiveTest test, Object error, StackTrace stackTrace) {
+    if (_completedTests.contains(test)) {
+      final loadSuite = test.suite is LoadSuite;
+
+      final prefix = _GithubMarkup.failed;
+      final statusSuffix = ' (failed after test completion)';
+
+      var name = test.test.name;
+      if (!loadSuite) {
+        if (_printPath && test.suite.path != null) {
+          name = '${test.suite.path}: $name';
+        }
+      }
+      if (_printPlatform) {
+        name = '[${test.suite.platform.runtime.name}, '
+            '${test.suite.platform.compiler.name}] $name';
+      }
+
+      _sink.writeln(_GithubMarkup.startGroup('$prefix $name$statusSuffix'));
+      _sink.writeln('$error');
+      _sink.writeln(stackTrace.toString().trimRight());
+      _sink.writeln(_GithubMarkup.endGroup);
+    }
+  }
+
+  void _onDone(bool? success) {
+    _cancel();
+
+    _sink.writeln();
+
+    final hadFailures = _engine.failed.isNotEmpty;
+    final message = StringBuffer('${_engine.passed.length} '
+        '${pluralize('test', _engine.passed.length)} passed');
+    if (_engine.failed.isNotEmpty) {
+      message.write(', ${_engine.failed.length} failed');
+    }
+    if (_engine.skipped.isNotEmpty) {
+      message.write(', ${_engine.skipped.length} skipped');
+    }
+    message.write('.');
+    _sink.writeln(
+      hadFailures
+          ? _GithubMarkup.error(message.toString())
+          : '${_GithubMarkup.success} $message',
+    );
+  }
+}
+
+abstract class _GithubMarkup {
+  // Char sets avilable at https://www.compart.com/en/unicode/.
+  static const String passed = '✅';
+  static const String skipped = '❎';
+  static const String failed = '❌';
+  // The 'synthetic' icon is currently not used but is something to consider in
+  // order to draw a distinction between user tests and test-like supporting
+  // infrastructure.
+  // static const String synthetic = '⏺';
+  static const String success = '🎉';
+
+  static String startGroup(String title) =>
+      '::group::${title.replaceAll('\n', ' ')}';
+
+  static final String endGroup = '::endgroup::';
+
+  static String error(String message) => '::error::$message';
+}
diff --git a/pkgs/test_core/lib/src/runner/reporter/json.dart b/pkgs/test_core/lib/src/runner/reporter/json.dart
new file mode 100644
index 0000000..672f806
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/reporter/json.dart
@@ -0,0 +1,324 @@
+// 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.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io' show pid;
+
+import 'package:collection/collection.dart';
+import 'package:path/path.dart' as p;
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test_api/hooks.dart' // ignore: implementation_imports
+    show
+        TestFailure;
+import 'package:test_api/src/backend/compiler.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/metadata.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports
+
+import '../../platform.dart';
+import '../engine.dart';
+import '../load_suite.dart';
+import '../reporter.dart';
+import '../version.dart';
+
+/// A reporter that prints machine-readable JSON-formatted test results.
+class JsonReporter implements Reporter {
+  /// Whether the test runner will pause for debugging.
+  final bool _isDebugRun;
+
+  /// The engine used to run the tests.
+  final Engine _engine;
+
+  /// A stopwatch that tracks the duration of the full run.
+  final _stopwatch = Stopwatch();
+
+  /// Whether we've started [_stopwatch].
+  ///
+  /// We can't just use `_stopwatch.isRunning` because the stopwatch is stopped
+  /// when the reporter is paused.
+  var _stopwatchStarted = false;
+
+  /// An expando that associates unique IDs with [LiveTest]s.
+  final _liveTestIDs = <LiveTest, int>{};
+
+  /// An expando that associates unique IDs with [Suite]s.
+  final _suiteIDs = <Suite, int>{};
+
+  /// An expando that associates unique IDs with [Group]s.
+  final _groupIDs = <Group, int>{};
+
+  /// 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,
+          {required bool isDebugRun}) =>
+      JsonReporter._(engine, sink, isDebugRun);
+
+  JsonReporter._(this._engine, this._sink, this._isDebugRun) {
+    _subscriptions.add(_engine.onTestStarted.listen(_onTestStarted));
+
+    // 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: () {
+      _emit('allSuites', {
+        'count': _engine.addedSuites.length,
+        'time': _stopwatch.elapsed.inMilliseconds
+      });
+    }));
+
+    _emit('start',
+        {'protocolVersion': '0.1.1', 'runnerVersion': testVersion, 'pid': pid});
+  }
+
+  @override
+  void pause() {
+    if (_paused) return;
+    _paused = true;
+
+    _stopwatch.stop();
+
+    for (var subscription in _subscriptions) {
+      subscription.pause();
+    }
+  }
+
+  @override
+  void resume() {
+    if (!_paused) return;
+    _paused = false;
+
+    if (_stopwatchStarted) _stopwatch.start();
+
+    for (var subscription in _subscriptions) {
+      subscription.resume();
+    }
+  }
+
+  void _cancel() {
+    for (var subscription in _subscriptions) {
+      subscription.cancel();
+    }
+    _subscriptions.clear();
+  }
+
+  /// A callback called when the engine begins running [liveTest].
+  void _onTestStarted(LiveTest liveTest) {
+    if (!_stopwatchStarted) {
+      _stopwatchStarted = true;
+      _stopwatch.start();
+    }
+
+    var suiteID = _idForSuite(liveTest.suite);
+
+    // Don't emit groups for load suites. They're always empty and they provide
+    // unnecessary clutter.
+    var groupIDs = liveTest.suite is LoadSuite
+        ? <int>[]
+        : _idsForGroups(liveTest.groups, liveTest.suite);
+
+    var suiteConfig = _configFor(liveTest.suite);
+    var id = _nextID++;
+    _liveTestIDs[liveTest] = id;
+    _emit('testStart', {
+      'test': {
+        'id': id,
+        'name': liveTest.test.name,
+        'suiteID': suiteID,
+        'groupIDs': groupIDs,
+        'metadata': _serializeMetadata(suiteConfig, liveTest.test.metadata),
+        ..._frameInfo(suiteConfig, liveTest.test.trace, liveTest.suite.platform,
+            liveTest.suite.path!),
+      }
+    });
+
+    // Convert the future to a stream so that the subscription can be paused or
+    // canceled.
+    _subscriptions.add(
+        liveTest.onComplete.asStream().listen((_) => _onComplete(liveTest)));
+
+    _subscriptions.add(liveTest.onError
+        .listen((error) => _onError(liveTest, error.error, error.stackTrace)));
+
+    _subscriptions.add(liveTest.onMessage.listen((message) {
+      _emit('print', {
+        'testID': id,
+        'messageType': message.type.name,
+        'message': message.text
+      });
+    }));
+  }
+
+  /// Returns an ID for [suite].
+  ///
+  /// If [suite] doesn't have an ID yet, this assigns one and emits a new event
+  /// for that suite.
+  int _idForSuite(Suite suite) {
+    if (_suiteIDs.containsKey(suite)) return _suiteIDs[suite]!;
+
+    var id = _nextID++;
+    _suiteIDs[suite] = id;
+
+    // Give the load suite's suite the same ID, because it doesn't have any
+    // different metadata.
+    if (suite is LoadSuite) {
+      suite.suite.then((runnerSuite) {
+        if (runnerSuite == null) return;
+        _suiteIDs[runnerSuite] = id;
+        if (!_isDebugRun) return;
+
+        // TODO(nweiz): test this when we have a library for communicating with
+        // the Chrome remote debugger, or when we have VM debug support.
+        _emit('debug', {
+          'suiteID': id,
+          'observatory': runnerSuite.environment.observatoryUrl?.toString(),
+          'remoteDebugger':
+              runnerSuite.environment.remoteDebuggerUrl?.toString(),
+        });
+      });
+    }
+
+    _emit('suite', {
+      'suite': <String, Object?>{
+        'id': id,
+        'platform': suite.platform.runtime.identifier,
+        'path': suite.path
+      }
+    });
+    return id;
+  }
+
+  /// Returns a list of the IDs for all the groups in [groups], which are
+  /// contained in the suite identified by [suiteID].
+  ///
+  /// 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;
+    return groups.map((group) {
+      if (_groupIDs.containsKey(group)) {
+        return parentID = _groupIDs[group]!;
+      }
+
+      var id = _nextID++;
+      _groupIDs[group] = id;
+
+      var suiteConfig = _configFor(suite);
+      _emit('group', {
+        'group': {
+          'id': id,
+          'suiteID': _idForSuite(suite),
+          'parentID': parentID,
+          'name': group.name,
+          'metadata': _serializeMetadata(suiteConfig, group.metadata),
+          'testCount': group.testCount,
+          ..._frameInfo(suiteConfig, group.trace, suite.platform, suite.path!)
+        }
+      });
+      parentID = id;
+      return id;
+    }).toList();
+  }
+
+  /// Serializes [metadata] into a JSON-protocol-compatible map.
+  Map _serializeMetadata(SuiteConfiguration suiteConfig, Metadata metadata) =>
+      suiteConfig.runSkipped
+          ? {'skip': false, 'skipReason': null}
+          : {'skip': metadata.skip, 'skipReason': metadata.skipReason};
+
+  /// A callback called when [liveTest] finishes running.
+  void _onComplete(LiveTest liveTest) {
+    _emit('testDone', {
+      'testID': _liveTestIDs[liveTest],
+      'result': _normalizeTestResult(liveTest),
+      'skipped': liveTest.state.result == Result.skipped,
+      'hidden': !_engine.liveTests.contains(liveTest)
+    });
+  }
+
+  String _normalizeTestResult(LiveTest liveTest) {
+    // For backwards-compatibility, report skipped tests as successes.
+    if (liveTest.state.result == Result.skipped) return 'success';
+    // if test is still active, it was probably cancelled
+    if (_engine.active.contains(liveTest)) return 'error';
+    return liveTest.state.result.toString();
+  }
+
+  /// A callback called when [liveTest] throws [error].
+  void _onError(LiveTest liveTest, Object error, StackTrace stackTrace) {
+    _emit('error', {
+      'testID': _liveTestIDs[liveTest],
+      'error': error.toString(),
+      'stackTrace': '$stackTrace',
+      'isFailure': error is TestFailure
+    });
+  }
+
+  /// A callback called when the engine is finished running tests.
+  ///
+  /// [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();
+    _stopwatch.stop();
+
+    _emit('done', {'success': success});
+  }
+
+  /// Returns the configuration for [suite].
+  ///
+  /// If [suite] is a [RunnerSuite], this returns [RunnerSuite.config].
+  /// Otherwise, it returns [SuiteConfiguration.empty].
+  SuiteConfiguration _configFor(Suite suite) =>
+      suite is RunnerSuite ? suite.config : SuiteConfiguration.empty;
+
+  /// Emits an event with the given type and attributes.
+  void _emit(String type, Map attributes) {
+    attributes['type'] = type;
+    attributes['time'] = _stopwatch.elapsed.inMilliseconds;
+    _sink.writeln(jsonEncode(attributes));
+  }
+
+  /// Returns a map with the line, column, and URL information for the first
+  /// frame of [trace], as well as the first line in the original file.
+  ///
+  /// If javascript traces are enabled and the test is on a javascript platform,
+  /// or if the [trace] is null or empty, then the line, column, and url will
+  /// all be `null`.
+  Map<String, dynamic> _frameInfo(SuiteConfiguration suiteConfig, Trace? trace,
+      SuitePlatform platform, String suitePath) {
+    var absoluteSuitePath = p.canonicalize(p.absolute(suitePath));
+    var frame = trace?.frames.first;
+    if (frame == null || (suiteConfig.jsTrace && platform.compiler.isJS)) {
+      return {'line': null, 'column': null, 'url': null};
+    }
+
+    var rootFrame = trace?.frames.firstWhereOrNull((frame) =>
+        frame.uri.scheme == 'file' &&
+        p.canonicalize(frame.uri.toFilePath()) == absoluteSuitePath);
+    return {
+      'line': frame.line,
+      'column': frame.column,
+      'url': frame.uri.toString(),
+      if (rootFrame != null && rootFrame != frame) ...{
+        'root_line': rootFrame.line,
+        'root_column': rootFrame.column,
+        'root_url': rootFrame.uri.toString(),
+      }
+    };
+  }
+}
diff --git a/pkgs/test_core/lib/src/runner/reporter/multiplex.dart b/pkgs/test_core/lib/src/runner/reporter/multiplex.dart
new file mode 100644
index 0000000..59a9841
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/reporter/multiplex.dart
@@ -0,0 +1,25 @@
+// 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 '../reporter.dart';
+
+class MultiplexReporter implements Reporter {
+  Iterable<Reporter> delegates;
+
+  MultiplexReporter(this.delegates);
+
+  @override
+  void pause() {
+    for (var d in delegates) {
+      d.pause();
+    }
+  }
+
+  @override
+  void resume() {
+    for (var d in delegates) {
+      d.resume();
+    }
+  }
+}
diff --git a/pkgs/test_core/lib/src/runner/runner_suite.dart b/pkgs/test_core/lib/src/runner/runner_suite.dart
new file mode 100644
index 0000000..dcceb6b
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/runner_suite.dart
@@ -0,0 +1,175 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/test.dart'; // ignore: implementation_imports
+
+import 'environment.dart';
+import 'suite.dart';
+
+/// A suite produced and consumed by the test runner that has runner-specific
+/// logic and lifecycle management.
+///
+/// This is separated from [Suite] because the backend library (which will
+/// eventually become its own package) is primarily for test code itself to use,
+/// for which the [RunnerSuite] APIs don't make sense.
+///
+/// A [RunnerSuite] can be produced and controlled using a
+/// [RunnerSuiteController].
+class RunnerSuite extends Suite {
+  final RunnerSuiteController _controller;
+
+  /// The environment in which this suite runs.
+  Environment get environment => _controller._environment;
+
+  /// The configuration for this suite.
+  SuiteConfiguration get config => _controller._config;
+
+  /// Whether the suite is paused for debugging.
+  ///
+  /// When using a dev inspector, this may also mean that the entire browser is
+  /// paused.
+  bool get isDebugging => _controller._isDebugging;
+
+  /// A broadcast stream that emits an event whenever the suite is paused for
+  /// debugging or resumed afterwards.
+  ///
+  /// The event is `true` when debugging starts and `false` when it ends.
+  Stream<bool> get onDebugging => _controller._onDebuggingController.stream;
+
+  /// A shortcut constructor for creating a [RunnerSuite] that never goes into
+  /// debugging mode and doesn't support suite channels.
+  factory RunnerSuite(Environment environment, SuiteConfiguration config,
+      Group group, SuitePlatform platform,
+      {String? path, void Function()? onClose}) {
+    var controller =
+        RunnerSuiteController._local(environment, config, onClose: onClose);
+    var suite = RunnerSuite._(controller, group, platform, path: path);
+    controller._suite = Future.value(suite);
+    return suite;
+  }
+
+  RunnerSuite._(this._controller, Group group, SuitePlatform platform,
+      {String? path})
+      : super(group, platform,
+            path: path, ignoreTimeouts: _controller._config.ignoreTimeouts);
+
+  @override
+  RunnerSuite filter(bool Function(Test) callback) {
+    var filtered = group.filter(callback);
+    filtered ??= Group.root([], metadata: metadata);
+    return RunnerSuite._(_controller, filtered, platform, path: path);
+  }
+
+  /// Closes the suite and releases any resources associated with it.
+  Future close() => _controller._close();
+
+  /// Collects a hit-map containing merged coverage.
+  ///
+  /// Result is suitable for input to the coverage formatters provided by
+  /// `package:coverage`.
+  Future<Map<String, dynamic>> gatherCoverage() async =>
+      (await _controller._gatherCoverage?.call()) ?? {};
+}
+
+/// A class that exposes and controls a [RunnerSuite].
+class RunnerSuiteController {
+  /// The suite controlled by this controller.
+  Future<RunnerSuite> get suite => _suite;
+  late final Future<RunnerSuite> _suite;
+
+  /// The backing value for [suite.environment].
+  final Environment _environment;
+
+  /// The configuration for this suite.
+  final SuiteConfiguration _config;
+
+  /// A channel that communicates with the remote suite.
+  final MultiChannel? _suiteChannel;
+
+  /// The function to call when the suite is closed.
+  final FutureOr<void> Function()? _onClose;
+
+  /// The backing value for [suite.isDebugging].
+  bool _isDebugging = false;
+
+  /// The controller for [suite.onDebugging].
+  final _onDebuggingController = StreamController<bool>.broadcast();
+
+  /// The channel names that have already been used.
+  final _channelNames = <String>{};
+
+  /// Collects a hit-map containing merged coverage.
+  final Future<Map<String, dynamic>> Function()? _gatherCoverage;
+
+  RunnerSuiteController(this._environment, this._config, this._suiteChannel,
+      Future<Group> groupFuture, SuitePlatform platform,
+      {String? path,
+      void Function()? onClose,
+      Future<Map<String, dynamic>> Function()? gatherCoverage})
+      : _onClose = onClose,
+        _gatherCoverage = gatherCoverage {
+    _suite = groupFuture
+        .then((group) => RunnerSuite._(this, group, platform, path: path));
+  }
+
+  /// Used by [RunnerSuite.new] to create a runner suite that's not loaded from
+  /// an external source.
+  RunnerSuiteController._local(this._environment, this._config,
+      {void Function()? onClose,
+      Future<Map<String, dynamic>> Function()? gatherCoverage})
+      : _suiteChannel = null,
+        _onClose = onClose,
+        _gatherCoverage = gatherCoverage;
+
+  /// Sets whether the suite is paused for debugging.
+  ///
+  /// If this is different than [suite.isDebugging], this will automatically
+  /// send out an event along [suite.onDebugging].
+  void setDebugging(bool debugging) {
+    if (debugging == _isDebugging) return;
+    _isDebugging = debugging;
+    _onDebuggingController.add(debugging);
+  }
+
+  /// Returns a channel that communicates with the remote suite.
+  ///
+  /// This connects to a channel created by code in the test worker calling the
+  /// `suiteChannel` argument from a `beforeLoad` callback to `serializeSuite`
+  /// with the same name.
+  /// It can be used used to send and receive any JSON-serializable object.
+  ///
+  /// This is exposed on the [RunnerSuiteController] so that runner plugins can
+  /// communicate with the workers they spawn before the associated [suite] is
+  /// fully loaded.
+  StreamChannel channel(String name) {
+    if (!_channelNames.add(name)) {
+      throw StateError('Duplicate RunnerSuite.channel() connection "$name".');
+    }
+
+    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;
+  }
+
+  /// The backing function for [suite.close].
+  Future _close() => _closeMemo.runOnce(() async {
+        await _onDebuggingController.close();
+        var onClose = _onClose;
+        if (onClose != null) await onClose();
+      });
+  final _closeMemo = AsyncMemoizer<void>();
+}
diff --git a/pkgs/test_core/lib/src/runner/runner_test.dart b/pkgs/test_core/lib/src/runner/runner_test.dart
new file mode 100644
index 0000000..2823983
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/runner_test.dart
@@ -0,0 +1,108 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test_api/backend.dart'
+    show Metadata, RemoteException, SuitePlatform;
+import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/live_test_controller.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/message.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/test.dart'; // ignore: implementation_imports
+
+import 'spawn_hybrid.dart';
+
+/// A test running remotely, controlled by a stream channel.
+class RunnerTest extends Test {
+  @override
+  final String name;
+  @override
+  final Metadata metadata;
+  @override
+  final Trace? trace;
+
+  /// The channel used to communicate with the test's `RemoteListener`.
+  final MultiChannel _channel;
+
+  RunnerTest(this.name, this.metadata, this.trace, this._channel);
+
+  @override
+  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));
+
+      testChannel = _channel.virtualChannel();
+      _channel.sink.add({'command': 'run', 'channel': testChannel.id});
+
+      testChannel.stream.listen((message) {
+        final msg = message as Map;
+        switch (msg['type'] as String) {
+          case 'error':
+            var asyncError = RemoteException.deserialize(
+                msg['error'] as Map<String, dynamic>);
+            var stackTrace = asyncError.stackTrace;
+            controller.addError(asyncError.error, stackTrace);
+            break;
+
+          case 'state-change':
+            controller.setState(State(Status.parse(msg['status'] as String),
+                Result.parse(msg['result'] as String)));
+            break;
+
+          case 'message':
+            controller.message(Message(
+                MessageType.parse(msg['message-type'] as String),
+                msg['text'] as String));
+            break;
+
+          case 'complete':
+            controller.completer.complete();
+            break;
+
+          case 'spawn-hybrid-uri':
+            // When we kill the isolate that the test lives in, that will close
+            // this virtual channel and cause the spawned isolate to close as
+            // well.
+            spawnHybridUri(msg['url'] as String, msg['message'], suite).pipe(
+                testChannel.virtualChannel((msg['channel'] as num).toInt()));
+            break;
+        }
+      }, onDone: () {
+        // When the test channel closes—presumably because the browser
+        // closed—mark the test as complete no matter what.
+        if (controller.completer.isCompleted) return;
+        controller.completer.complete();
+      });
+    }, () {
+      // If the test has finished running, just disconnect the channel.
+      if (controller.completer.isCompleted) {
+        testChannel.sink.close();
+        return;
+      }
+
+      unawaited(() async {
+        // If the test is still running, send it a message telling it to shut
+        // down ASAP. This causes the [Invoker] to eagerly throw exceptions
+        // whenever the test touches it.
+        testChannel.sink.add({'command': 'close'});
+        await controller.completer.future;
+        await testChannel.sink.close();
+      }());
+    }, groups: groups);
+    return controller;
+  }
+
+  @override
+  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
new file mode 100644
index 0000000..7f64a5c
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/runtime_selection.dart
@@ -0,0 +1,25 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:source_span/source_span.dart';
+
+/// A runtime on which the user has chosen to run tests.
+class RuntimeSelection {
+  /// The name of the runtime.
+  final String name;
+
+  /// 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;
+
+  RuntimeSelection(this.name, [this.span]);
+
+  @override
+  bool operator ==(Object other) =>
+      other is RuntimeSelection && other.name == name;
+
+  @override
+  int get hashCode => name.hashCode;
+}
diff --git a/pkgs/test_core/lib/src/runner/spawn_hybrid.dart b/pkgs/test_core/lib/src/runner/spawn_hybrid.dart
new file mode 100644
index 0000000..ed236b0
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/spawn_hybrid.dart
@@ -0,0 +1,167 @@
+// 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.
+
+import 'dart:io';
+import 'dart:isolate';
+
+import 'package:analyzer/dart/analysis/utilities.dart';
+import 'package:async/async.dart';
+import 'package:path/path.dart' as p;
+import 'package:stream_channel/isolate_channel.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test_api/backend.dart' show RemoteException;
+import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports
+
+import '../util/dart.dart' as dart;
+import '../util/package_config.dart';
+import 'package_version.dart';
+
+/// Spawns a hybrid isolate from [url] with the given [message], and returns a
+/// [StreamChannel] that communicates with it.
+///
+/// This connects the main isolate to the hybrid isolate, whereas
+/// `lib/src/frontend/spawn_hybrid.dart` connects the test isolate to the main
+/// isolate.
+///
+/// If [uri] is relative, it will be interpreted relative to the `file:` URL
+/// for [suite]. If it's root-relative (that is, if it begins with `/`) it will
+/// be interpreted relative to the root of the package (the directory that
+/// contains `pubspec.yaml`, *not* the `test/` directory). If it's a `package:`
+/// URL, it will be resolved using the current package's dependency
+/// constellation.
+StreamChannel spawnHybridUri(String url, Object? message, Suite suite) {
+  return StreamChannelCompleter.fromFuture(() async {
+    url = await _normalizeUrl(url, suite);
+    var port = ReceivePort();
+    var onExitPort = ReceivePort();
+    try {
+      var code = '''
+        ${await _languageVersionCommentFor(url)}
+
+        import "package:test_core/src/runner/hybrid_listener.dart";
+
+        import "${url.replaceAll(r'$', '%24')}" as lib;
+
+        void main(_, List data) => listen(() => lib.hybridMain, data);
+      ''';
+
+      var isolate = await dart.runInIsolate(code, [port.sendPort, message],
+          onExit: onExitPort.sendPort);
+
+      // Ensure that we close [port] and [channel] when the isolate exits.
+      var disconnector = Disconnector<void>();
+      onExitPort.listen((_) {
+        disconnector.disconnect();
+        port.close();
+        onExitPort.close();
+      });
+
+      return IsolateChannel<Object?>.connectReceive(port)
+          .transform(disconnector)
+          .transformSink(StreamSinkTransformer.fromHandlers(handleDone: (sink) {
+        // If the user closes the stream channel, kill the isolate.
+        isolate.kill();
+        port.close();
+        onExitPort.close();
+        sink.close();
+      }));
+    } catch (error, stackTrace) {
+      port.close();
+      onExitPort.close();
+
+      // Make sure any errors in spawning the isolate are forwarded to the test.
+      return StreamChannel(
+          Stream.fromFuture(Future.value({
+            'type': 'error',
+            'error': RemoteException.serialize(error, stackTrace)
+          })),
+          NullStreamSink<void>());
+    }
+  }());
+}
+
+/// Normalizes [url] to an absolute url, resolving `package:` urls with the
+/// current package config.
+///
+/// If [url] has a scheme other than `package:`, then it is returned as is.
+///
+/// Follows the rules for relative/absolute paths outlined in [spawnHybridUri].
+Future<String> _normalizeUrl(String url, Suite suite) async {
+  final parsedUri = Uri.parse(url);
+
+  switch (parsedUri.scheme) {
+    case '':
+      var isRootRelative = parsedUri.path.startsWith('/');
+
+      if (isRootRelative) {
+        // We assume that the current path is the package root. `pub run`
+        // enforces this currently, but at some point it would probably be good
+        // to pass in an explicit root.
+        return p.url
+            .join(p.toUri(p.current).toString(), parsedUri.path.substring(1));
+      } else {
+        var suitePath = suite.path!;
+        return p.url.join(
+            p.url.dirname(p.toUri(p.absolute(suitePath)).toString()),
+            parsedUri.toString());
+      }
+    case 'package':
+      final resolvedUri = await Isolate.resolvePackageUri(parsedUri);
+      if (resolvedUri == null) {
+        throw ArgumentError.value(
+            url, 'uri', 'Could not resolve the package URI');
+      }
+      return resolvedUri.toString();
+    default:
+      return url;
+  }
+}
+
+/// Computes the a language version comment for the library at [uri].
+///
+/// If there is a language version comment in the file, that is returned.
+///
+/// If the URI has a `data` scheme, a comment representing the language version of
+/// the current package is returned.
+///
+/// Otherwise a comment representing the default version from the
+/// [currentPackageConfig] is returned.
+///
+/// If no default language version is known (the URI scheme is not recognized
+/// for instance), then an empty string is returned.
+Future<String> _languageVersionCommentFor(String url) async {
+  var parsedUri = Uri.parse(url);
+
+  // Returns the explicit language version comment if one exists.
+  var result = parseString(
+      content: await _readUri(parsedUri),
+      path: parsedUri.scheme == 'data' ? null : p.fromUri(parsedUri),
+      throwIfDiagnostics: false);
+  var languageVersionComment = result.unit.languageVersionToken?.value();
+  if (languageVersionComment != null) return languageVersionComment.toString();
+
+  // Returns the default language version for the package if one exists.
+  if (parsedUri.scheme.isEmpty || parsedUri.scheme == 'file') {
+    var packageConfig = await currentPackageConfig;
+    var package = packageConfig.packageOf(parsedUri);
+    var version = package?.languageVersion;
+    if (version != null) return '// @dart=$version';
+  }
+
+  // Returns the root package language version for `data` URIs. These are
+  // assumed to be from `spawnHybridCode` calls.
+  if (parsedUri.scheme == 'data') {
+    return await rootPackageLanguageVersionComment;
+  }
+
+  // Fall back on no language comment.
+  return '';
+}
+
+Future<String> _readUri(Uri uri) async => switch (uri.scheme) {
+      '' || 'file' => await File.fromUri(uri).readAsString(),
+      'data' => uri.data!.contentAsString(),
+      _ => throw ArgumentError.value(uri, 'uri',
+          'Only data and file uris (as well as relative paths) are supported'),
+    };
diff --git a/pkgs/test_core/lib/src/runner/suite.dart b/pkgs/test_core/lib/src/runner/suite.dart
new file mode 100644
index 0000000..b5ee0be
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/suite.dart
@@ -0,0 +1,498 @@
+// 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.
+
+import 'package:boolean_selector/boolean_selector.dart';
+import 'package:collection/collection.dart';
+import 'package:source_span/source_span.dart';
+import 'package:test_api/scaffolding.dart' show Timeout;
+import 'package:test_api/src/backend/metadata.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/platform_selector.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
+
+import 'compiler_selection.dart';
+import 'runtime_selection.dart';
+
+/// A filter on tests cases to run within a test suite.
+///
+/// The tests that match a test selection should match all available criteria. A
+/// suite run may include multiple test selections, and a test should run if it
+/// matches any test selection.
+///
+/// An empty [TestSelection()] will run all tests in the suite.
+final class TestSelection {
+  /// The patterns to check against test case names.
+  ///
+  /// Only run tests which match all the patterns.
+  final Set<Pattern> testPatterns;
+
+  /// Only run tests that originate from this line in the test suite.
+  final int? line;
+
+  /// Only run tests that originate from this column in the test suite.
+  final int? col;
+
+  const TestSelection({this.testPatterns = const {}, this.line, this.col});
+}
+
+/// Suite-level configuration.
+///
+/// This tracks configuration that can differ from suite to suite.
+final class SuiteConfiguration {
+  /// Empty configuration with only default values.
+  ///
+  /// Using this is slightly more efficient than manually constructing a new
+  /// configuration with no arguments.
+  static final empty = SuiteConfiguration._(
+      allowDuplicateTestNames: null,
+      allowTestRandomization: null,
+      jsTrace: null,
+      runSkipped: null,
+      dart2jsArgs: null,
+      testSelections: const {},
+      precompiledPath: null,
+      runtimes: null,
+      compilerSelections: null,
+      tags: null,
+      onPlatform: null,
+      metadata: null,
+      ignoreTimeouts: null);
+
+  /// Whether or not duplicate test (or group) names are allowed within the same
+  /// test suite.
+  //
+  // TODO: Change the default https://github.com/dart-lang/test/issues/1571
+  bool get allowDuplicateTestNames => _allowDuplicateTestNames ?? true;
+  final bool? _allowDuplicateTestNames;
+
+  /// Whether test randomization should be allowed for this test.
+  bool get allowTestRandomization => _allowTestRandomization ?? true;
+  final bool? _allowTestRandomization;
+
+  /// Whether JavaScript stack traces should be left as-is or converted to
+  /// Dart-like traces.
+  bool get jsTrace => _jsTrace ?? false;
+  final bool? _jsTrace;
+
+  /// Whether skipped tests should be run.
+  bool get runSkipped => _runSkipped ?? false;
+  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;
+
+  /// Additional arguments to pass to dart2js.
+  ///
+  /// Note that this if multiple suites run the same JavaScript on different
+  /// runtimes, and they have different [dart2jsArgs], only one (undefined)
+  /// suite's arguments will be used.
+  final List<String> dart2jsArgs;
+
+  /// The selections for which tests to run in a suite.
+  ///
+  /// If empty, no tests have been selected for this suite.
+  /// Tests must be selected once for a suite before the suite can run.
+  /// When merging suite configurations, only one, or neither, should have tests
+  /// already selected.
+  ///
+  /// A test should run within a suite if it matches any selection in
+  /// [testSelections].
+  final Set<TestSelection> testSelections;
+
+  /// The set of compiler selections for running tests.
+  final List<CompilerSelection>? compilerSelections;
+
+  /// 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;
+
+  /// Configuration for particular tags.
+  ///
+  /// The keys are tag selectors, and the values are configurations for tests
+  /// whose tags match those selectors.
+  final Map<BooleanSelector, SuiteConfiguration> tags;
+
+  /// Configuration for particular platforms.
+  ///
+  /// The keys are platform selectors, and the values are configurations for
+  /// those platforms. These configuration should only contain test-level
+  /// configuration fields, but that isn't enforced.
+  final Map<PlatformSelector, SuiteConfiguration> onPlatform;
+
+  /// The global test metadata derived from this configuration.
+  Metadata get metadata {
+    if (tags.isEmpty && onPlatform.isEmpty) return _metadata;
+    return _metadata.change(
+        forTag: tags.map((key, config) => MapEntry(key, config.metadata)),
+        onPlatform:
+            onPlatform.map((key, config) => MapEntry(key, config.metadata)));
+  }
+
+  final Metadata _metadata;
+
+  /// The set of tags that have been declared in any way in this configuration.
+  late final Set<String> knownTags = UnmodifiableSetView({
+    ..._metadata.tags,
+    for (var selector in tags.keys) ...selector.variables,
+    for (var configuration in tags.values) ...configuration.knownTags,
+    for (var configuration in onPlatform.values) ...configuration.knownTags,
+  });
+
+  /// Whether or not timeouts should be ignored.
+  final bool? _ignoreTimeouts;
+  bool get ignoreTimeouts => _ignoreTimeouts ?? false;
+
+  factory SuiteConfiguration(
+      {required bool? allowDuplicateTestNames,
+      required bool? allowTestRandomization,
+      required bool? jsTrace,
+      required bool? runSkipped,
+      required Iterable<String>? dart2jsArgs,
+      required String? precompiledPath,
+      required Iterable<CompilerSelection>? compilerSelections,
+      required Iterable<RuntimeSelection>? runtimes,
+      required Map<BooleanSelector, SuiteConfiguration>? tags,
+      required Map<PlatformSelector, SuiteConfiguration>? onPlatform,
+      required bool? ignoreTimeouts,
+
+      // Test-level configuration
+      required Timeout? timeout,
+      required bool? verboseTrace,
+      required bool? chainStackTraces,
+      required bool? skip,
+      required int? retry,
+      required String? skipReason,
+      required PlatformSelector? testOn,
+      required Iterable<String>? addTags}) {
+    var config = SuiteConfiguration._(
+        allowDuplicateTestNames: allowDuplicateTestNames,
+        allowTestRandomization: allowTestRandomization,
+        jsTrace: jsTrace,
+        runSkipped: runSkipped,
+        dart2jsArgs: dart2jsArgs,
+        testSelections: const {},
+        precompiledPath: precompiledPath,
+        compilerSelections: compilerSelections,
+        runtimes: runtimes,
+        tags: tags,
+        onPlatform: onPlatform,
+        ignoreTimeouts: ignoreTimeouts,
+        metadata: Metadata(
+            timeout: timeout,
+            verboseTrace: verboseTrace,
+            chainStackTraces: chainStackTraces,
+            skip: skip,
+            retry: retry,
+            skipReason: skipReason,
+            testOn: testOn,
+            tags: addTags));
+    return config._resolveTags();
+  }
+
+  /// A constructor that doesn't require all of its options to be passed.
+  ///
+  /// This should only be used in situations where you really only want to
+  /// configure a specific restricted set of options.
+  factory SuiteConfiguration._unsafe(
+          {bool? allowDuplicateTestNames,
+          bool? allowTestRandomization,
+          bool? jsTrace,
+          bool? runSkipped,
+          Iterable<String>? dart2jsArgs,
+          String? precompiledPath,
+          Iterable<CompilerSelection>? compilerSelections,
+          Iterable<RuntimeSelection>? runtimes,
+          Map<BooleanSelector, SuiteConfiguration>? tags,
+          Map<PlatformSelector, SuiteConfiguration>? onPlatform,
+          bool? ignoreTimeouts,
+
+          // Test-level configuration
+          Timeout? timeout,
+          bool? verboseTrace,
+          bool? chainStackTraces,
+          bool? skip,
+          int? retry,
+          String? skipReason,
+          PlatformSelector? testOn,
+          Iterable<String>? addTags}) =>
+      SuiteConfiguration(
+          allowDuplicateTestNames: allowDuplicateTestNames,
+          allowTestRandomization: allowTestRandomization,
+          jsTrace: jsTrace,
+          runSkipped: runSkipped,
+          dart2jsArgs: dart2jsArgs,
+          precompiledPath: precompiledPath,
+          compilerSelections: compilerSelections,
+          runtimes: runtimes,
+          tags: tags,
+          onPlatform: onPlatform,
+          ignoreTimeouts: ignoreTimeouts,
+          timeout: timeout,
+          verboseTrace: verboseTrace,
+          chainStackTraces: chainStackTraces,
+          skip: skip,
+          retry: retry,
+          skipReason: skipReason,
+          testOn: testOn,
+          addTags: addTags);
+
+  /// A specialized constructor for only configuring the runtimes.
+  factory SuiteConfiguration.runtimes(Iterable<RuntimeSelection> runtimes) =>
+      SuiteConfiguration._unsafe(runtimes: runtimes);
+
+  /// A specialized constructor for only configuring runSkipped.
+  factory SuiteConfiguration.runSkipped(bool runSkipped) =>
+      SuiteConfiguration._unsafe(runSkipped: runSkipped);
+
+  /// A specialized constructor for only configuring the timeout.
+  factory SuiteConfiguration.timeout(Timeout timeout) =>
+      SuiteConfiguration._unsafe(timeout: timeout);
+
+  /// Creates new SuiteConfiguration.
+  ///
+  /// Unlike [SuiteConfiguration.new], this assumes [tags] is already
+  /// resolved.
+  SuiteConfiguration._({
+    required bool? allowDuplicateTestNames,
+    required bool? allowTestRandomization,
+    required bool? jsTrace,
+    required bool? runSkipped,
+    required Iterable<String>? dart2jsArgs,
+    required this.testSelections,
+    required this.precompiledPath,
+    required Iterable<CompilerSelection>? compilerSelections,
+    required Iterable<RuntimeSelection>? runtimes,
+    required Map<BooleanSelector, SuiteConfiguration>? tags,
+    required Map<PlatformSelector, SuiteConfiguration>? onPlatform,
+    required Metadata? metadata,
+    required bool? ignoreTimeouts,
+  })  : _allowDuplicateTestNames = allowDuplicateTestNames,
+        _allowTestRandomization = allowTestRandomization,
+        _jsTrace = jsTrace,
+        _runSkipped = runSkipped,
+        dart2jsArgs = _list(dart2jsArgs) ?? const [],
+        _runtimes = _list(runtimes),
+        compilerSelections = _list(compilerSelections),
+        tags = _map(tags),
+        onPlatform = _map(onPlatform),
+        _ignoreTimeouts = ignoreTimeouts,
+        _metadata = metadata ?? Metadata.empty;
+
+  /// Creates a new [SuiteConfiguration] that takes its configuration from
+  /// [metadata].
+  factory SuiteConfiguration.fromMetadata(Metadata metadata) =>
+      SuiteConfiguration._(
+        tags: metadata.forTag.map((key, child) =>
+            MapEntry(key, SuiteConfiguration.fromMetadata(child))),
+        onPlatform: metadata.onPlatform.map((key, child) =>
+            MapEntry(key, SuiteConfiguration.fromMetadata(child))),
+        metadata: metadata.change(forTag: {}, onPlatform: {}),
+        allowDuplicateTestNames: null,
+        allowTestRandomization: null,
+        jsTrace: null,
+        runSkipped: null,
+        dart2jsArgs: null,
+        testSelections: const {},
+        precompiledPath: null,
+        runtimes: null,
+        compilerSelections: null,
+        ignoreTimeouts: null,
+      );
+
+  /// Returns an unmodifiable copy of [input].
+  ///
+  /// If [input] is `null` or empty, this returns `null`.
+  static List<T>? _list<T>(Iterable<T>? input) {
+    if (input == null) return null;
+    var list = List<T>.unmodifiable(input);
+    if (list.isEmpty) return null;
+    return list;
+  }
+
+  /// 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 <Never, Never>{};
+    return Map.unmodifiable(input);
+  }
+
+  /// Merges this with [other].
+  ///
+  /// For most fields, if both configurations have values set, [other]'s value
+  /// takes precedence. However, certain fields are merged together instead.
+  /// This is indicated in those fields' documentation.
+  SuiteConfiguration merge(SuiteConfiguration other) {
+    if (this == SuiteConfiguration.empty) return other;
+    if (other == SuiteConfiguration.empty) return this;
+    assert(testSelections.isEmpty || other.testSelections.isEmpty);
+
+    var config = SuiteConfiguration._(
+        allowDuplicateTestNames:
+            other._allowDuplicateTestNames ?? _allowDuplicateTestNames,
+        allowTestRandomization:
+            other._allowTestRandomization ?? _allowTestRandomization,
+        jsTrace: other._jsTrace ?? _jsTrace,
+        runSkipped: other._runSkipped ?? _runSkipped,
+        dart2jsArgs: dart2jsArgs.toList()..addAll(other.dart2jsArgs),
+        testSelections:
+            testSelections.isEmpty ? other.testSelections : testSelections,
+        precompiledPath: other.precompiledPath ?? precompiledPath,
+        compilerSelections: other.compilerSelections ?? compilerSelections,
+        runtimes: other._runtimes ?? _runtimes,
+        tags: _mergeConfigMaps(tags, other.tags),
+        onPlatform: _mergeConfigMaps(onPlatform, other.onPlatform),
+        ignoreTimeouts: other._ignoreTimeouts ?? _ignoreTimeouts,
+        metadata: metadata.merge(other.metadata));
+    return config._resolveTags();
+  }
+
+  /// Returns a copy of this configuration with the given fields updated.
+  ///
+  /// Note that unlike [merge], this has no merging behavior—the old value is
+  /// always replaced by the new one.
+  SuiteConfiguration change(
+      {bool? allowDuplicateTestNames,
+      bool? allowTestRandomization,
+      bool? jsTrace,
+      bool? runSkipped,
+      Iterable<String>? dart2jsArgs,
+      String? precompiledPath,
+      Iterable<CompilerSelection>? compilerSelections,
+      Iterable<RuntimeSelection>? runtimes,
+      Map<BooleanSelector, SuiteConfiguration>? tags,
+      Map<PlatformSelector, SuiteConfiguration>? onPlatform,
+      bool? ignoreTimeouts,
+
+      // Test-level configuration
+      Timeout? timeout,
+      bool? verboseTrace,
+      bool? chainStackTraces,
+      bool? skip,
+      int? retry,
+      String? skipReason,
+      PlatformSelector? testOn,
+      Iterable<String>? addTags}) {
+    var config = SuiteConfiguration._(
+        allowDuplicateTestNames:
+            allowDuplicateTestNames ?? _allowDuplicateTestNames,
+        allowTestRandomization:
+            allowTestRandomization ?? _allowTestRandomization,
+        jsTrace: jsTrace ?? _jsTrace,
+        runSkipped: runSkipped ?? _runSkipped,
+        dart2jsArgs: dart2jsArgs?.toList() ?? this.dart2jsArgs,
+        testSelections: testSelections,
+        precompiledPath: precompiledPath ?? this.precompiledPath,
+        compilerSelections: compilerSelections ?? this.compilerSelections,
+        runtimes: runtimes ?? _runtimes,
+        tags: tags ?? this.tags,
+        onPlatform: onPlatform ?? this.onPlatform,
+        ignoreTimeouts: ignoreTimeouts ?? _ignoreTimeouts,
+        metadata: _metadata.change(
+            timeout: timeout,
+            verboseTrace: verboseTrace,
+            chainStackTraces: chainStackTraces,
+            skip: skip,
+            retry: retry,
+            skipReason: skipReason,
+            testOn: testOn,
+            tags: addTags?.toSet()));
+    return config._resolveTags();
+  }
+
+  /// Assign the selection of tests that should run for this suite.
+  ///
+  /// Test selections must be chosen only once per suite, once a
+  /// SuiteConfiguration has test slections this method should not be called
+  /// again.
+  ///
+  /// [testSelections] must not be empty.
+  SuiteConfiguration selectTests(Set<TestSelection> testSelections) {
+    assert(this.testSelections.isEmpty);
+    assert(testSelections.isNotEmpty);
+    return SuiteConfiguration._(
+        testSelections: testSelections,
+        allowDuplicateTestNames: _allowDuplicateTestNames,
+        allowTestRandomization: _allowTestRandomization,
+        jsTrace: _jsTrace,
+        runSkipped: _runSkipped,
+        dart2jsArgs: dart2jsArgs,
+        precompiledPath: precompiledPath,
+        compilerSelections: compilerSelections,
+        runtimes: _runtimes,
+        tags: tags,
+        onPlatform: onPlatform,
+        ignoreTimeouts: _ignoreTimeouts,
+        metadata: _metadata);
+  }
+
+  /// Throws a [FormatException] if this refers to any undefined runtimes.
+  void validateRuntimes(List<Runtime> allRuntimes) {
+    var validVariables =
+        allRuntimes.map((runtime) => runtime.identifier).toSet();
+    _metadata.validatePlatformSelectors(validVariables);
+
+    var runtimes = _runtimes;
+    if (runtimes != null) {
+      for (var selection in runtimes) {
+        if (!allRuntimes
+            .any((runtime) => runtime.identifier == selection.name)) {
+          if (selection.span != null) {
+            throw SourceSpanFormatException(
+                'Unknown platform "${selection.name}".', selection.span);
+          } else {
+            throw FormatException('Unknown platform "${selection.name}".');
+          }
+        }
+      }
+    }
+
+    onPlatform.forEach((selector, config) {
+      selector.validate(validVariables);
+      config.validateRuntimes(allRuntimes);
+    });
+  }
+
+  /// Returns a copy of this with all platform-specific configuration from
+  /// [onPlatform] resolved.
+  SuiteConfiguration forPlatform(SuitePlatform platform) {
+    if (onPlatform.isEmpty) return this;
+
+    var config = this;
+    onPlatform.forEach((platformSelector, platformConfig) {
+      if (!platformSelector.evaluate(platform)) return;
+      config = config.merge(platformConfig);
+    });
+    return config.change(onPlatform: {});
+  }
+
+  /// Merges two maps whose values are [SuiteConfiguration]s.
+  ///
+  /// Any overlapping keys in the maps have their configurations merged in the
+  /// returned map.
+  Map<T, SuiteConfiguration> _mergeConfigMaps<T>(
+          Map<T, SuiteConfiguration> map1, Map<T, SuiteConfiguration> map2) =>
+      mergeMaps(map1, map2,
+          value: (config1, config2) => config1.merge(config2));
+
+  SuiteConfiguration _resolveTags() {
+    // If there's no tag-specific configuration, or if none of it applies, just
+    // return the configuration as-is.
+    if (_metadata.tags.isEmpty || tags.isEmpty) return this;
+
+    // Otherwise, resolve the tag-specific components.
+    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)!);
+    });
+
+    if (merged == empty) return this;
+    return change(tags: newTags).merge(merged);
+  }
+}
diff --git a/pkgs/test_core/lib/src/runner/util/iterable_set.dart b/pkgs/test_core/lib/src/runner/util/iterable_set.dart
new file mode 100644
index 0000000..09335ba
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/util/iterable_set.dart
@@ -0,0 +1,44 @@
+// 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.
+
+import 'dart:collection';
+
+import 'package:collection/collection.dart';
+
+/// An unmodifiable [Set] view backed by an arbitrary [Iterable].
+///
+/// Note that contrary to most APIs that take iterables, this does not convert
+/// its argument to another collection before use. This means that if it's
+/// lazily-generated, that generation will happen for every operation.
+///
+/// Note also that set operations that are usually expected to be `O(1)` or
+/// `O(log(n))`, such as [contains], may be `O(n)` for many underlying iterable
+/// types. As such, this should only be used for small iterables.
+class IterableSet<E> with SetMixin<E>, UnmodifiableSetMixin<E> {
+  /// The base iterable that set operations forward to.
+  final Iterable<E> _base;
+
+  @override
+  int get length => _base.length;
+
+  @override
+  Iterator<E> get iterator => _base.iterator;
+
+  /// Creates a [Set] view of [base].
+  IterableSet(this._base);
+
+  @override
+  bool contains(Object? element) => _base.contains(element);
+
+  @override
+  E? lookup(Object? element) {
+    for (var e in _base) {
+      if (e == element) return e;
+    }
+    return null;
+  }
+
+  @override
+  Set<E> toSet() => _base.toSet();
+}
diff --git a/pkgs/test_core/lib/src/runner/version.dart b/pkgs/test_core/lib/src/runner/version.dart
new file mode 100644
index 0000000..3eab242
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/version.dart
@@ -0,0 +1,60 @@
+// 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.
+
+import 'dart:io';
+
+import 'package:yaml/yaml.dart';
+
+/// The version number of the test runner, or `null` if it couldn't be loaded.
+///
+/// This is a semantic version, optionally followed by a space and additional
+/// data about its source.
+final String? testVersion = (() {
+  dynamic lockfile;
+  try {
+    lockfile = loadYaml(File('pubspec.lock').readAsStringSync());
+  } on FormatException catch (_) {
+    return null;
+  } on IOException catch (_) {
+    return null;
+  }
+
+  if (lockfile is! Map) return null;
+  var packages = lockfile['packages'];
+  if (packages is! Map) return null;
+  var package = packages['test'];
+  if (package is! Map) return null;
+
+  var source = package['source'];
+  if (source is! String) return null;
+
+  switch (source) {
+    case 'hosted':
+      var version = package['version'];
+      return (version is String) ? version : null;
+
+    case 'git':
+      var version = package['version'];
+      if (version is! String) return null;
+      var description = package['description'];
+      if (description is! Map) return null;
+      var ref = description['resolved-ref'];
+      if (ref is! String) return null;
+
+      return '$version (${ref.substring(0, 7)})';
+
+    case 'path':
+      var version = package['version'];
+      if (version is! String) return null;
+      var description = package['description'];
+      if (description is! Map) return null;
+      var path = description['path'];
+      if (path is! String) return null;
+
+      return '$version (from $path)';
+
+    default:
+      return null;
+  }
+})();
diff --git a/pkgs/test_core/lib/src/runner/vm/environment.dart b/pkgs/test_core/lib/src/runner/vm/environment.dart
new file mode 100644
index 0000000..76cc2fd
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/vm/environment.dart
@@ -0,0 +1,42 @@
+// 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.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+import 'package:vm_service/vm_service.dart';
+
+import '../environment.dart'; // ignore: implementation_imports
+
+/// The environment in which VM tests are loaded.
+class VMEnvironment implements Environment {
+  @override
+  final supportsDebugging = true;
+  @override
+  final Uri observatoryUrl;
+
+  /// The VM service isolate object used to control this isolate.
+  final IsolateRef _isolate;
+  final VmService _client;
+
+  VMEnvironment(this.observatoryUrl, this._isolate, this._client);
+
+  @override
+  Uri? get remoteDebuggerUrl => null;
+
+  @override
+  Stream<void> get onRestart => StreamController<void>.broadcast().stream;
+
+  @override
+  CancelableOperation<void> displayPause() {
+    var completer =
+        CancelableCompleter<void>(onCancel: () => _client.resume(_isolate.id!));
+
+    completer.complete(_client.pause(_isolate.id!).then((_) => _client
+        .onDebugEvent
+        .firstWhere((event) => event.kind == EventKind.kResume)));
+
+    return completer.operation;
+  }
+}
diff --git a/pkgs/test_core/lib/src/runner/vm/platform.dart b/pkgs/test_core/lib/src/runner/vm/platform.dart
new file mode 100644
index 0000000..506909d
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/vm/platform.dart
@@ -0,0 +1,357 @@
+// 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.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:developer';
+import 'dart:io';
+import 'dart:isolate';
+
+import 'package:async/async.dart';
+import 'package:coverage/coverage.dart';
+import 'package:path/path.dart' as p;
+import 'package:stream_channel/isolate_channel.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test_api/backend.dart';
+import 'package:vm_service/vm_service.dart' hide Isolate;
+import 'package:vm_service/vm_service_io.dart';
+
+import '../../runner/configuration.dart';
+import '../../runner/environment.dart';
+import '../../runner/load_exception.dart';
+import '../../runner/platform.dart';
+import '../../runner/plugin/platform_helpers.dart';
+import '../../runner/plugin/shared_platform_helpers.dart';
+import '../../runner/runner_suite.dart';
+import '../../runner/suite.dart';
+import '../../util/io.dart';
+import '../../util/package_config.dart';
+import '../package_version.dart';
+import 'environment.dart';
+import 'test_compiler.dart';
+
+var _shouldPauseAfterTests = false;
+
+/// A platform that loads tests in isolates spawned within this Dart process.
+class VMPlatform extends PlatformPlugin {
+  /// The test runner configuration.
+  final _config = Configuration.current;
+  final _compiler = TestCompiler(
+      p.join(p.current, '.dart_tool', 'test', 'incremental_kernel'));
+  final _closeMemo = AsyncMemoizer<void>();
+  final _tempDir = Directory.systemTemp.createTempSync('dart_test.vm.');
+
+  @override
+  Future<RunnerSuite?> load(String path, SuitePlatform platform,
+      SuiteConfiguration suiteConfig, Map<String, Object?> message) async {
+    assert(platform.runtime == Runtime.vm);
+
+    _setupPauseAfterTests();
+
+    MultiChannel outerChannel;
+    var cleanupCallbacks = <void Function()>[];
+    Isolate? isolate;
+    if (platform.compiler == Compiler.exe) {
+      var serverSocket = await ServerSocket.bind('localhost', 0);
+      Process process;
+      try {
+        process =
+            await _spawnExecutable(path, suiteConfig.metadata, serverSocket);
+      } catch (error) {
+        unawaited(serverSocket.close());
+        rethrow;
+      }
+      process.stdout.listen(stdout.add);
+      process.stderr.listen(stderr.add);
+      var socket = await serverSocket.first;
+      outerChannel = MultiChannel<Object?>(jsonSocketStreamChannel(socket));
+      cleanupCallbacks
+        ..add(serverSocket.close)
+        ..add(process.kill);
+    } else {
+      var receivePort = ReceivePort();
+      try {
+        isolate = await _spawnIsolate(path, receivePort.sendPort,
+            suiteConfig.metadata, platform.compiler);
+        if (isolate == null) return null;
+      } catch (error) {
+        receivePort.close();
+        rethrow;
+      }
+      outerChannel = MultiChannel(IsolateChannel.connectReceive(receivePort));
+      cleanupCallbacks.add(isolate.kill);
+    }
+    cleanupCallbacks.add(outerChannel.sink.close);
+
+    VmService? client;
+    StreamSubscription<Event>? eventSub;
+    // Typical test interaction will go across `channel`, `outerChannel` adds
+    // additional communication directly between the test bootstrapping and this
+    // platform to enable pausing after tests for debugging.
+    var outerQueue = StreamQueue(outerChannel.stream);
+    var channelId = (await outerQueue.next) as int;
+    var channel = outerChannel.virtualChannel(channelId).transformStream(
+        StreamTransformer.fromHandlers(handleDone: (sink) async {
+      if (_shouldPauseAfterTests) {
+        outerChannel.sink.add('debug');
+        await outerQueue.next;
+      }
+      for (var fn in cleanupCallbacks) {
+        fn();
+      }
+      unawaited(eventSub?.cancel());
+      unawaited(client?.dispose());
+      sink.close();
+    }));
+
+    Environment? environment;
+    IsolateRef? isolateRef;
+    if (_config.debug) {
+      if (platform.compiler == Compiler.exe) {
+        throw UnsupportedError(
+            'Unable to debug tests compiled to `exe` (tried to debug $path with '
+            'the `exe` compiler).');
+      }
+      var info =
+          await Service.controlWebServer(enable: true, silenceOutput: true);
+      // ignore: deprecated_member_use, Remove when SDK constraint is at 3.2.0
+      var isolateID = Service.getIsolateID(isolate!)!;
+
+      var libraryPath = (await absoluteUri(path)).toString();
+      var serverUri = info.serverUri!;
+      client = await vmServiceConnectUri(_wsUriFor(serverUri).toString());
+      var isolateNumber = int.parse(isolateID.split('/').last);
+      isolateRef = (await client.getVM())
+          .isolates!
+          .firstWhere((isolate) => isolate.number == isolateNumber.toString());
+      await client.setName(isolateRef.id!, path);
+      var libraryRef = (await client.getIsolate(isolateRef.id!))
+          .libraries!
+          .firstWhere((library) => library.uri == libraryPath);
+      var url = _observatoryUrlFor(serverUri, isolateRef.id!, libraryRef.id!);
+      environment = VMEnvironment(url, isolateRef, client);
+    }
+
+    environment ??= const PluginEnvironment();
+
+    var controller = deserializeSuite(
+        path, platform, suiteConfig, environment, channel.cast(), message,
+        gatherCoverage: () => _gatherCoverage(environment!));
+
+    if (isolateRef != null) {
+      await client!.streamListen('Debug');
+      eventSub = client.onDebugEvent.listen((event) {
+        if (event.kind == EventKind.kResume) {
+          controller.setDebugging(false);
+        } else if (event.kind == EventKind.kPauseInterrupted ||
+            event.kind == EventKind.kPauseBreakpoint ||
+            event.kind == EventKind.kPauseException) {
+          controller.setDebugging(true);
+        }
+      });
+    }
+
+    return await controller.suite;
+  }
+
+  @override
+  Future close() => _closeMemo.runOnce(() => Future.wait([
+        _compiler.dispose(),
+        _tempDir.deleteWithRetry(),
+      ]));
+
+  /// Compiles [path] to a native executable and spawns it as a process.
+  ///
+  /// Sets up a communication channel as well by passing command line arguments
+  /// for the host and port of [socket].
+  Future<Process> _spawnExecutable(
+      String path, Metadata suiteMetadata, ServerSocket socket) async {
+    if (_config.suiteDefaults.precompiledPath != null) {
+      throw UnsupportedError(
+          'Precompiled native executable tests are not supported at this time');
+    }
+    var executable = await _compileToNative(path, suiteMetadata);
+    return await Process.start(
+        executable, [socket.address.host, socket.port.toString()]);
+  }
+
+  /// Compiles [path] to a native executable using `dart compile exe`.
+  Future<String> _compileToNative(String path, Metadata suiteMetadata) async {
+    var bootstrapPath = await _bootstrapNativeTestFile(
+        path,
+        suiteMetadata.languageVersionComment ??
+            await rootPackageLanguageVersionComment);
+    var output = File(p.setExtension(bootstrapPath, '.exe'));
+    var processResult = await Process.run(Platform.resolvedExecutable, [
+      'compile',
+      'exe',
+      bootstrapPath,
+      '--output',
+      output.path,
+      '--packages',
+      (await packageConfigUri).toFilePath(),
+    ]);
+    if (processResult.exitCode != 0 || !(await output.exists())) {
+      throw LoadException(path, '''
+exitCode: ${processResult.exitCode}
+stdout: ${processResult.stdout}
+stderr: ${processResult.stderr}''');
+    }
+    return output.path;
+  }
+
+  /// Spawns an isolate with the current configuration and passes it [message].
+  ///
+  /// This isolate connects an [IsolateChannel] to [message] and sends the
+  /// serialized tests over that channel.
+  ///
+  /// Returns `null` if an exception occurs but [close] has already been called.
+  Future<Isolate?> _spawnIsolate(String path, SendPort message,
+      Metadata suiteMetadata, Compiler compiler) async {
+    try {
+      var precompiledPath = _config.suiteDefaults.precompiledPath;
+      if (precompiledPath != null) {
+        return _spawnPrecompiledIsolate(
+            path, message, precompiledPath, compiler);
+      }
+      return switch (compiler) {
+        Compiler.kernel => _spawnIsolateWithUri(
+            await _compileToKernel(path, suiteMetadata), message),
+        Compiler.source => _spawnIsolateWithUri(
+            await _bootstrapIsolateTestFile(
+                path,
+                suiteMetadata.languageVersionComment ??
+                    await rootPackageLanguageVersionComment),
+            message),
+        _ => throw StateError(
+            'Unsupported compiler $compiler for the VM platform'),
+      };
+    } catch (_) {
+      if (_closeMemo.hasRun) return null;
+      rethrow;
+    }
+  }
+
+  /// Compiles [path] to kernel and returns the uri to the compiled dill.
+  Future<Uri> _compileToKernel(String path, Metadata suiteMetadata) async {
+    final response =
+        await _compiler.compile(await absoluteUri(path), suiteMetadata);
+    var compiledDill = response.kernelOutputUri?.toFilePath();
+    if (compiledDill == null || response.errorCount > 0) {
+      throw LoadException(path, response.compilerOutput ?? 'unknown error');
+    }
+    return absoluteUri(compiledDill);
+  }
+
+  /// Runs [uri] in an isolate, passing [message].
+  Future<Isolate> _spawnIsolateWithUri(Uri uri, SendPort message) async {
+    return await Isolate.spawnUri(uri, [], message,
+        packageConfig: await packageConfigUri, checked: true);
+  }
+
+  Future<Isolate> _spawnPrecompiledIsolate(String testPath, SendPort message,
+      String precompiledPath, Compiler compiler) async {
+    var testUri =
+        await absoluteUri('${p.join(precompiledPath, testPath)}.vm_test.dart');
+    testUri = testUri.replace(path: testUri.path.stripDriveLetterLeadingSlash);
+
+    switch (compiler) {
+      case Compiler.kernel:
+        // Load `.dill` files from their absolute file path.
+        var dillUri = (await Isolate.resolvePackageUri(testUri.replace(
+            path:
+                '${testUri.path.substring(0, testUri.path.length - '.dart'.length)}'
+                '.vm.app.dill')))!;
+        if (await File.fromUri(dillUri).exists()) {
+          testUri = dillUri;
+        }
+        // TODO: Compile to kernel manually here? Otherwise we aren't compiling
+        // with kernel when we technically should be, based on the compiler
+        // setting.
+        break;
+      case Compiler.source:
+        // Just leave test uri as is.
+        break;
+      default:
+        throw StateError('Unsupported compiler for the VM platform $compiler.');
+    }
+    File? packageConfig =
+        File(p.join(precompiledPath, '.dart_tool/package_config.json'));
+    if (!(await packageConfig.exists())) {
+      packageConfig = File(p.join(precompiledPath, '.packages'));
+      if (!(await packageConfig.exists())) {
+        packageConfig = null;
+      }
+    }
+    return await Isolate.spawnUri(testUri, [], message,
+        packageConfig: packageConfig?.uri, checked: true);
+  }
+
+  /// Bootstraps the test at [testPath] and writes its contents to a temporary
+  /// file.
+  ///
+  /// Returns the [Uri] to the created file.
+  Future<Uri> _bootstrapIsolateTestFile(
+      String testPath, String languageVersionComment) async {
+    var file = File(p.join(
+        _tempDir.path, p.setExtension(testPath, '.bootstrap.isolate.dart')));
+    if (!file.existsSync()) {
+      file
+        ..createSync(recursive: true)
+        ..writeAsStringSync(testBootstrapContents(
+          testUri: await absoluteUri(testPath),
+          languageVersionComment: languageVersionComment,
+          packageConfigUri: await packageConfigUri,
+          testType: VmTestType.isolate,
+        ));
+    }
+    return file.uri;
+  }
+
+  /// Bootstraps the test at [testPath] for native execution and writes its
+  /// contents to a temporary file.
+  ///
+  /// Returns the path to the created file.
+  Future<String> _bootstrapNativeTestFile(
+      String testPath, String languageVersionComment) async {
+    var file = File(p.join(
+        _tempDir.path, p.setExtension(testPath, '.bootstrap.native.dart')));
+    if (!file.existsSync()) {
+      file
+        ..createSync(recursive: true)
+        ..writeAsStringSync(testBootstrapContents(
+          testUri: await absoluteUri(testPath),
+          languageVersionComment: languageVersionComment,
+          packageConfigUri: await packageConfigUri,
+          testType: VmTestType.process,
+        ));
+    }
+    return file.path;
+  }
+}
+
+Future<Map<String, dynamic>> _gatherCoverage(Environment environment) async {
+  final isolateId = Uri.parse(environment.observatoryUrl!.fragment)
+      .queryParameters['isolateId'];
+  return await collect(environment.observatoryUrl!, false, false, false, {},
+      isolateIds: {isolateId!});
+}
+
+Uri _wsUriFor(Uri observatoryUrl) =>
+    observatoryUrl.replace(scheme: 'ws').resolve('ws');
+
+Uri _observatoryUrlFor(Uri base, String isolateId, String id) => base.replace(
+    fragment: Uri(
+        path: '/inspect',
+        queryParameters: {'isolateId': isolateId, 'objectId': id}).toString());
+
+var _hasRegistered = false;
+void _setupPauseAfterTests() {
+  if (_hasRegistered) return;
+  _hasRegistered = true;
+  registerExtension('ext.test.pauseAfterTests', (_, __) async {
+    _shouldPauseAfterTests = true;
+    return ServiceExtensionResponse.result(jsonEncode({}));
+  });
+}
diff --git a/pkgs/test_core/lib/src/runner/vm/test_compiler.dart b/pkgs/test_core/lib/src/runner/vm/test_compiler.dart
new file mode 100644
index 0000000..56fbbd2
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/vm/test_compiler.dart
@@ -0,0 +1,238 @@
+// 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:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:async/async.dart';
+import 'package:frontend_server_client/frontend_server_client.dart';
+import 'package:path/path.dart' as p;
+import 'package:pool/pool.dart';
+import 'package:test_api/backend.dart';
+
+import '../../util/dart.dart';
+import '../../util/io.dart';
+import '../../util/package_config.dart';
+import '../package_version.dart';
+
+class CompilationResponse {
+  final String? compilerOutput;
+  final int errorCount;
+  final Uri? kernelOutputUri;
+
+  const CompilationResponse(
+      {this.compilerOutput, this.errorCount = 0, this.kernelOutputUri});
+
+  static const _wasShutdown = CompilationResponse(
+      errorCount: 1, compilerOutput: 'Compiler no longer active.');
+}
+
+class TestCompiler {
+  final _closeMemo = AsyncMemoizer<void>();
+
+  /// Each language version that appears in test files gets its own compiler,
+  /// to ensure that all language modes are supported (such as sound and
+  /// unsound null safety).
+  final _compilerForLanguageVersion =
+      <String, _TestCompilerForLanguageVersion>{};
+
+  /// A prefix used for the dill files for each compiler that is created.
+  final String _dillCachePrefix;
+
+  /// No work is done until the first call to [compile] is received, at which
+  /// point the compiler process is started.
+  TestCompiler(this._dillCachePrefix);
+
+  /// Compiles [mainDart], using a separate compiler per language version of
+  /// the tests.
+  Future<CompilationResponse> compile(Uri mainDart, Metadata metadata) async {
+    if (_closeMemo.hasRun) return CompilationResponse._wasShutdown;
+    var languageVersionComment = metadata.languageVersionComment ??
+        await rootPackageLanguageVersionComment;
+    var compiler = _compilerForLanguageVersion.putIfAbsent(
+        languageVersionComment,
+        () => _TestCompilerForLanguageVersion(
+            _dillCachePrefix, languageVersionComment));
+    return compiler.compile(mainDart);
+  }
+
+  Future<void> dispose() => _closeMemo.runOnce(() => Future.wait([
+        for (var compiler in _compilerForLanguageVersion.values)
+          compiler.dispose(),
+      ]));
+}
+
+class _TestCompilerForLanguageVersion {
+  final _closeMemo = AsyncMemoizer<void>();
+  final _compilePool = Pool(1);
+  final String _dillCachePath;
+  FrontendServerClient? _frontendServerClient;
+  final String _languageVersionComment;
+  late final _outputDill =
+      File(p.join(_outputDillDirectory.path, 'output.dill'));
+  final _outputDillDirectory =
+      Directory.systemTemp.createTempSync('dart_test.kernel.');
+  // Used to create unique file names for final kernel files.
+  int _compileNumber = 0;
+  // The largest incremental dill file we created, will be cached under
+  // the `.dart_tool` dir at the end of compilation.
+  File? _dillToCache;
+
+  _TestCompilerForLanguageVersion(
+      String dillCachePrefix, this._languageVersionComment)
+      : _dillCachePath = '$dillCachePrefix.'
+            '${_dillCacheSuffix(_languageVersionComment, enabledExperiments)}';
+
+  Future<CompilationResponse> compile(Uri mainUri) =>
+      _compilePool.withResource(() => _compile(mainUri));
+
+  Future<CompilationResponse> _compile(Uri mainUri) async {
+    _compileNumber++;
+    if (_closeMemo.hasRun) return CompilationResponse._wasShutdown;
+    CompileResult? compilerOutput;
+    final tempFile = File(p.join(_outputDillDirectory.path, 'test.dart'))
+      ..writeAsStringSync(testBootstrapContents(
+        testUri: mainUri,
+        packageConfigUri: await packageConfigUri,
+        languageVersionComment: _languageVersionComment,
+        testType: VmTestType.isolate,
+      ));
+    final testCache = File(_dillCachePath);
+
+    try {
+      if (_frontendServerClient == null) {
+        if (await testCache.exists()) {
+          await testCache.copy(_outputDill.path);
+        }
+        compilerOutput = await _createCompiler(tempFile.uri);
+      } else {
+        compilerOutput =
+            await _frontendServerClient!.compile(<Uri>[tempFile.uri]);
+      }
+    } catch (e, s) {
+      if (_closeMemo.hasRun) return CompilationResponse._wasShutdown;
+      return CompilationResponse(errorCount: 1, compilerOutput: '$e\n$s');
+    } finally {
+      _frontendServerClient?.accept();
+      _frontendServerClient?.reset();
+    }
+
+    // The client is guaranteed initialized at this point.
+    final outputPath = compilerOutput?.dillOutput;
+    if (outputPath == null) {
+      return CompilationResponse(
+          compilerOutput: compilerOutput?.compilerOutputLines.join('\n'),
+          errorCount: compilerOutput?.errorCount ?? 0);
+    }
+
+    final outputFile = File(outputPath);
+    final kernelReadyToRun =
+        await outputFile.copy('${tempFile.path}_$_compileNumber.dill');
+    // Keep the `_dillToCache` file up-to-date and use the size of the
+    // kernel file as an approximation for how many packages are included.
+    // Larger files are preferred, since re-using more packages will reduce the
+    // number of files the frontend server needs to load and parse.
+    if (_dillToCache == null ||
+        (_dillToCache!.lengthSync() < kernelReadyToRun.lengthSync())) {
+      _dillToCache = kernelReadyToRun;
+    }
+
+    return CompilationResponse(
+        compilerOutput: compilerOutput?.compilerOutputLines.join('\n'),
+        errorCount: compilerOutput?.errorCount ?? 0,
+        kernelOutputUri: kernelReadyToRun.absolute.uri);
+  }
+
+  Future<CompileResult?> _createCompiler(Uri testUri) async {
+    final platformDill = 'lib/_internal/vm_platform_strong.dill';
+    final sdkRoot =
+        p.relative(p.dirname(p.dirname(Platform.resolvedExecutable)));
+    final packageConfigUriAwaited = await packageConfigUri;
+
+    // If we have native assets for the host os in JIT mode, they are here.
+    Uri? nativeAssetsYaml;
+    if (enabledExperiments.contains('native-assets')) {
+      nativeAssetsYaml = packageConfigUriAwaited.resolve('native_assets.yaml');
+      if (!await File.fromUri(nativeAssetsYaml).exists()) {
+        nativeAssetsYaml = null;
+      }
+    }
+
+    var client = _frontendServerClient = await FrontendServerClient.start(
+      testUri.toString(),
+      _outputDill.path,
+      platformDill,
+      enabledExperiments: enabledExperiments,
+      sdkRoot: sdkRoot,
+      packagesJson: packageConfigUriAwaited.toFilePath(),
+      nativeAssets: nativeAssetsYaml?.toFilePath(),
+      printIncrementalDependencies: false,
+    );
+    return client.compile();
+  }
+
+  Future<void> dispose() => _closeMemo.runOnce(() async {
+        await _compilePool.close();
+        if (_dillToCache != null) {
+          var testCache = File(_dillCachePath);
+          if (!testCache.parent.existsSync()) {
+            testCache.parent.createSync(recursive: true);
+          }
+          _dillToCache!.copySync(_dillCachePath);
+        }
+        _frontendServerClient?.kill();
+        _frontendServerClient = null;
+        if (_outputDillDirectory.existsSync()) {
+          await _outputDillDirectory.deleteWithRetry();
+        }
+      });
+}
+
+/// Computes a unique dill cache suffix for each [languageVersionComment]
+/// and [enabledExperiments] combination.
+String _dillCacheSuffix(
+    String languageVersionComment, List<String> enabledExperiments) {
+  var identifierString =
+      StringBuffer(languageVersionComment.replaceAll(' ', ''));
+  for (var experiment in enabledExperiments) {
+    identifierString.writeln(experiment);
+  }
+  return base64.encode(utf8.encode(identifierString.toString()));
+}
+
+/// Creates bootstrap file contents for running [testUri].
+///
+/// The [bootstrapType] argument should be either 'Vm' or 'Native' depending on
+/// which `internalBootstrap*Test` function should be used.
+String testBootstrapContents({
+  required Uri testUri,
+  required String languageVersionComment,
+  required Uri packageConfigUri,
+  required VmTestType testType,
+}) {
+  final (mainArgs, forwardedArgName, bootstrapType) = switch (testType) {
+    VmTestType.isolate => ('_, SendPort sendPort', 'sendPort', 'Vm'),
+    VmTestType.process => ('List<String> args', 'args', 'Native'),
+  };
+  return '''
+    $languageVersionComment
+
+    import 'dart:isolate';
+
+    import 'package:test_core/src/bootstrap/vm.dart';
+
+    import '$testUri' as test;
+
+    // This variable is read at runtime through the VM service and is unsafe to
+    // remove.
+    const packageConfigLocation = '$packageConfigUri';
+
+    void main($mainArgs) {
+      internalBootstrap${bootstrapType}Test(() => test.main, $forwardedArgName);
+    }
+  ''';
+}
+
+enum VmTestType { isolate, process }
diff --git a/pkgs/test_core/lib/src/runner/wasm_compiler_pool.dart b/pkgs/test_core/lib/src/runner/wasm_compiler_pool.dart
new file mode 100644
index 0000000..c4318f5
--- /dev/null
+++ b/pkgs/test_core/lib/src/runner/wasm_compiler_pool.dart
@@ -0,0 +1,96 @@
+// Copyright (c) 2022, 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';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+
+import '../util/dart.dart';
+import '../util/io.dart';
+import '../util/package_config.dart';
+import 'compiler_pool.dart';
+import 'suite.dart';
+
+/// A pool of `dart2wasm` compiler instances.
+///
+/// This limits the number of compiler instances running concurrently.
+class WasmCompilerPool extends CompilerPool {
+  /// Extra arguments to pass to `dart compile js`.
+  final List<String> _extraArgs;
+
+  /// The currently-active dart2wasm processes.
+  final _processes = <Process>{};
+
+  WasmCompilerPool([this._extraArgs = const []]);
+
+  /// Compiles [code] to [path].
+  ///
+  /// This wraps the Dart code in the standard browser-testing wrapper.
+  ///
+  /// The returned [Future] will complete once the `dart2wasm` process completes
+  /// *and* all its output has been printed to the command line.
+  @override
+  Future compileInternal(
+      String code, String path, SuiteConfiguration suiteConfig) {
+    return withTempDir((dir) async {
+      final wrapperPath = p.join(dir, 'main.dart');
+      File(wrapperPath).writeAsStringSync(code);
+      final outWasmPath = '$path.wasm';
+      final process = await Process.start(Platform.resolvedExecutable, [
+        'compile',
+        'wasm',
+        '--enable-asserts',
+        '--packages=${(await packageConfigUri).toFilePath()}',
+        for (var experiment in enabledExperiments)
+          '--enable-experiment=$experiment',
+        '-O0',
+        ..._extraArgs,
+        '-o',
+        outWasmPath,
+        wrapperPath,
+      ]);
+      if (closed) {
+        process.kill();
+        return;
+      }
+
+      _processes.add(process);
+
+      /// Wait until the process is entirely done to print out any output.
+      /// This can produce a little extra time for users to wait with no
+      /// update, but it also avoids some really nasty-looking interleaved
+      /// output. Write both stdout and stderr to the same buffer in case
+      /// they're intended to be printed in order.
+      var buffer = StringBuffer();
+
+      await Future.wait([
+        process.stdout.transform(utf8.decoder).forEach(buffer.write),
+        process.stderr.transform(utf8.decoder).forEach(buffer.write),
+      ]);
+
+      var exitCode = await process.exitCode;
+      _processes.remove(process);
+      if (closed) return;
+
+      var output = buffer.toString();
+      if (output.isNotEmpty) print(output);
+
+      if (exitCode != 0) throw StateError('dart2wasm failed.');
+    });
+  }
+
+  /// Closes the compiler pool.
+  ///
+  /// This kills all currently-running compilers and ensures that no more will
+  /// be started. It returns a [Future] that completes once all the compilers
+  /// have been killed and all resources released.
+  @override
+  Future<void> closeInternal() async {
+    await Future.wait(_processes.map((process) async {
+      process.kill();
+      await process.exitCode;
+    }));
+  }
+}
diff --git a/pkgs/test_core/lib/src/scaffolding.dart b/pkgs/test_core/lib/src/scaffolding.dart
new file mode 100644
index 0000000..31b3d89
--- /dev/null
+++ b/pkgs/test_core/lib/src/scaffolding.dart
@@ -0,0 +1,295 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:meta/meta.dart' show doNotSubmit, isTest, isTestGroup;
+import 'package:path/path.dart' as p;
+import 'package:test_api/backend.dart';
+import 'package:test_api/scaffolding.dart' show Timeout, pumpEventQueue;
+import 'package:test_api/src/backend/declarer.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/invoker.dart'; // ignore: implementation_imports
+
+import 'runner/engine.dart';
+import 'runner/plugin/environment.dart';
+import 'runner/reporter/expanded.dart';
+import 'runner/runner_suite.dart';
+import 'runner/suite.dart';
+import 'util/os.dart';
+import 'util/print_sink.dart';
+
+// Hide implementations which don't support being run directly.
+// This file is an almost direct copy of import below, but with the global
+// declarer added.
+export 'package:test_api/scaffolding.dart'
+    hide group, setUp, setUpAll, tearDown, tearDownAll, test;
+
+/// The global declarer.
+///
+/// This is used if a test file is run directly, rather than through the runner.
+Declarer? _globalDeclarer;
+
+/// Gets the declarer for the current scope.
+///
+/// When using the runner, this returns the [Zone]-scoped declarer that's set by
+/// [RemoteListener]. If the test file is run directly, this returns
+/// [_globalDeclarer] (and sets it up on the first call).
+Declarer get _declarer {
+  var declarer = Declarer.current;
+  if (declarer != null) return declarer;
+  if (_globalDeclarer != null) return _globalDeclarer!;
+
+  // Since there's no Zone-scoped declarer, the test file is being run directly.
+  // In order to run the tests, we set up our own Declarer via
+  // [_globalDeclarer], and pump the event queue as a best effort to wait for
+  // all tests to be defined before starting them.
+  _globalDeclarer = Declarer(isStandalone: true);
+
+  () async {
+    await pumpEventQueue();
+
+    var suite = RunnerSuite(
+        const PluginEnvironment(),
+        SuiteConfiguration.empty,
+        _globalDeclarer!.build(),
+        SuitePlatform(Runtime.vm, compiler: null, os: currentOSGuess),
+        path: p.prettyUri(Uri.base));
+
+    var engine = Engine();
+    engine.suiteSink.add(suite);
+    engine.suiteSink.close();
+    ExpandedReporter.watch(engine, PrintSink(),
+        color: true, printPath: false, printPlatform: false);
+
+    var success = await runZoned(() => Invoker.guard(engine.run),
+        zoneValues: {#test.declarer: _globalDeclarer});
+    if (success == true) return null;
+    print('');
+    unawaited(Future.error('Dummy exception to set exit code.'));
+  }();
+
+  return _globalDeclarer!;
+}
+
+// TODO(nweiz): This and other top-level functions should throw exceptions if
+// they're called after the declarer has finished declaring.
+/// Creates a new test case with the given description (converted to a string)
+/// and body.
+///
+/// The description will be added to the descriptions of any surrounding
+/// [group]s. If [testOn] is passed, it's parsed as a [platform selector][]; the
+/// test will only be run on matching platforms.
+///
+/// [platform selector]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors
+///
+/// If [timeout] is passed, it's used to modify or replace the default timeout
+/// of 30 seconds. Timeout modifications take precedence in suite-group-test
+/// order, so [timeout] will also modify any timeouts set on the group or suite.
+///
+/// If [skip] is a String or `true`, the test is skipped. If it's a String, it
+/// should explain why the test is skipped; this reason will be printed instead
+/// of running the test. If a call to [test] is nested within a [group], a
+/// non-null `skip` parameter for the `test` will take precedence over the skip
+/// parameter in the `group`. For instance, if a `group` is set to `skip: true`,
+/// but a `test` within it is configured as `skip: false`, the `test` will not
+/// be skipped. A suite level `@Skip()` annotation cannot be overridden with
+/// `skip` arguments to `test` or `group`.
+///
+/// If [tags] is passed, it declares user-defined tags that are applied to the
+/// test. These tags can be used to select or skip the test on the command line,
+/// or to do bulk test configuration. All tags should be declared in the
+/// [package configuration file][configuring tags]. The parameter can be an
+/// [Iterable] of tag names, or a [String] representing a single tag.
+///
+/// If [retry] is passed, the test will be retried the provided number of times
+/// before being marked as a failure.
+///
+/// [configuring tags]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#configuring-tags
+///
+/// [onPlatform] allows tests to be configured on a platform-by-platform
+/// basis. It's a map from strings that are parsed as [PlatformSelector]s to
+/// annotation classes: [Timeout], [Skip], or lists of those. These
+/// annotations apply only on the given platforms. For example:
+///
+///     test('potentially slow test', () {
+///       // ...
+///     }, onPlatform: {
+///       // This test is especially slow on Windows.
+///       'windows': Timeout.factor(2),
+///       'browser': [
+///         Skip('TODO: add browser support'),
+///         // This will be slow on browsers once it works on them.
+///         Timeout.factor(2)
+///       ]
+///     });
+///
+/// If multiple platforms match, the annotations apply in order as through
+/// they were in nested groups.
+///
+/// If the `solo` flag is `true`, only tests and groups marked as
+/// "solo" will be be run. This only restricts tests *within this test
+/// suite*—tests in other suites will run as normal. We recommend that users
+/// avoid this flag if possible and instead use the test runner flag `-n` to
+/// filter tests by name.
+@isTest
+void test(Object? description, dynamic Function() body,
+    {String? testOn,
+    Timeout? timeout,
+    Object? skip,
+    Object? tags,
+    Map<String, dynamic>? onPlatform,
+    int? retry,
+    // TODO(https://github.com/dart-lang/test/issues/2205): Remove deprecated.
+    @Deprecated('Debug only') @doNotSubmit bool solo = false}) {
+  _declarer.test(description.toString(), body,
+      testOn: testOn,
+      timeout: timeout,
+      skip: skip,
+      onPlatform: onPlatform,
+      tags: tags,
+      retry: retry,
+      solo: solo);
+
+  // Force dart2js not to inline this function. We need it to be separate from
+  // `main()` in JS stack traces in order to properly determine the line and
+  // column where the test was defined. See sdk#26705.
+  return;
+  return; // ignore: dead_code
+}
+
+/// Creates a group of tests.
+///
+/// A group's description (converted to a string) is included in the descriptions
+/// of any tests or sub-groups it contains. [setUp] and [tearDown] are also scoped
+/// to the containing group.
+///
+/// If [testOn] is passed, it's parsed as a [platform selector][]; the test will
+/// only be run on matching platforms.
+///
+/// [platform selector]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors
+///
+/// If [timeout] is passed, it's used to modify or replace the default timeout
+/// of 30 seconds. Timeout modifications take precedence in suite-group-test
+/// order, so [timeout] will also modify any timeouts set on the suite, and will
+/// be modified by any timeouts set on individual tests.
+///
+/// If [skip] is a String or `true`, the group is skipped. If it's a String, it
+/// should explain why the group is skipped; this reason will be printed instead
+/// of running the group's tests.
+///
+/// If [tags] is passed, it declares user-defined tags that are applied to the
+/// test. These tags can be used to select or skip the test on the command line,
+/// or to do bulk test configuration. All tags should be declared in the
+/// [package configuration file][configuring tags]. The parameter can be an
+/// [Iterable] of tag names, or a [String] representing a single tag.
+///
+/// [configuring tags]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#configuring-tags
+///
+/// [onPlatform] allows groups to be configured on a platform-by-platform
+/// basis. It's a map from strings that are parsed as [PlatformSelector]s to
+/// annotation classes: [Timeout], [Skip], or lists of those. These
+/// annotations apply only on the given platforms. For example:
+///
+///     group('potentially slow tests', () {
+///       // ...
+///     }, onPlatform: {
+///       // These tests are especially slow on Windows.
+///       'windows': Timeout.factor(2),
+///       'browser': [
+///         Skip('TODO: add browser support'),
+///         // They'll be slow on browsers once it works on them.
+///         Timeout.factor(2)
+///       ]
+///     });
+///
+/// If multiple platforms match, the annotations apply in order as through
+/// they were in nested groups.
+///
+/// If the `solo` flag is `true`, only tests and groups marked as
+/// "solo" will be be run. This only restricts tests *within this test
+/// suite*—tests in other suites will run as normal. We recommend that users
+/// avoid this flag if possible, and instead use the test runner flag `-n` to
+/// filter tests by name.
+@isTestGroup
+void group(Object? description, dynamic Function() body,
+    {String? testOn,
+    Timeout? timeout,
+    Object? skip,
+    Object? tags,
+    Map<String, dynamic>? onPlatform,
+    int? retry,
+    // TODO(https://github.com/dart-lang/test/issues/2205): Remove deprecated.
+    @Deprecated('Debug only') @doNotSubmit bool solo = false}) {
+  _declarer.group(description.toString(), body,
+      testOn: testOn,
+      timeout: timeout,
+      skip: skip,
+      tags: tags,
+      onPlatform: onPlatform,
+      retry: retry,
+      solo: solo);
+
+  // Force dart2js not to inline this function. We need it to be separate from
+  // `main()` in JS stack traces in order to properly determine the line and
+  // column where the test was defined. See sdk#26705.
+  return;
+  return; // ignore: dead_code
+}
+
+/// Registers a function to be run before tests.
+///
+/// This function will be called before each test is run. [callback] may be
+/// asynchronous; if so, it must return a [Future].
+///
+/// If this is called within a test group, it applies only to tests in that
+/// group. [callback] will be run after any set-up callbacks in parent groups or
+/// at the top level.
+///
+/// Each callback at the top level or in a given group will be run in the order
+/// they were declared.
+void setUp(dynamic Function() callback) => _declarer.setUp(callback);
+
+/// Registers a function to be run after tests.
+///
+/// This function will be called after each test is run. [callback] may be
+/// asynchronous; if so, it must return a [Future].
+///
+/// If this is called within a test group, it applies only to tests in that
+/// group. [callback] will be run before any tear-down callbacks in parent
+/// groups or at the top level.
+///
+/// Each callback at the top level or in a given group will be run in the
+/// reverse of the order they were declared.
+///
+/// See also [addTearDown], which adds tear-downs to a running test.
+void tearDown(dynamic Function() callback) => _declarer.tearDown(callback);
+
+/// Registers a function to be run once before all tests.
+///
+/// [callback] may be asynchronous; if so, it must return a [Future].
+///
+/// If this is called within a test group, [callback] will run before all tests
+/// in that group. It will be run after any [setUpAll] callbacks in parent
+/// groups or at the top level. It won't be run if none of the tests in the
+/// group are run.
+///
+/// **Note**: This function makes it very easy to accidentally introduce hidden
+/// dependencies between tests that should be isolated. In general, you should
+/// prefer [setUp], and only use [setUpAll] if the callback is prohibitively
+/// slow.
+void setUpAll(dynamic Function() callback) => _declarer.setUpAll(callback);
+
+/// Registers a function to be run once after all tests.
+///
+/// If this is called within a test group, [callback] will run after all tests
+/// in that group. It will be run before any [tearDownAll] callbacks in parent
+/// groups or at the top level. It won't be run if none of the tests in the
+/// group are run.
+///
+/// **Note**: This function makes it very easy to accidentally introduce hidden
+/// dependencies between tests that should be isolated. In general, you should
+/// prefer [tearDown], and only use [tearDownAll] if the callback is
+/// prohibitively slow.
+void tearDownAll(dynamic Function() callback) =>
+    _declarer.tearDownAll(callback);
diff --git a/pkgs/test_core/lib/src/util/async.dart b/pkgs/test_core/lib/src/util/async.dart
new file mode 100644
index 0000000..036d2b1
--- /dev/null
+++ b/pkgs/test_core/lib/src/util/async.dart
@@ -0,0 +1,32 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+
+import 'package:async/async.dart';
+
+/// Returns a single-subscription stream that emits the results of [operations]
+/// in the order they complete.
+///
+/// If the subscription is canceled, any pending operations are canceled as
+/// well.
+Stream<T> inCompletionOrder<T>(Iterable<CancelableOperation<T>> operations) {
+  var operationSet = operations.toSet();
+  var controller = StreamController<T>(
+      sync: true,
+      onCancel: () =>
+          Future.wait(operationSet.map((operation) => operation.cancel())));
+
+  for (var operation in operationSet) {
+    operation.value
+        .then((value) => controller.add(value))
+        .onError(controller.addError)
+        .whenComplete(() {
+      operationSet.remove(operation);
+      if (operationSet.isEmpty) controller.close();
+    });
+  }
+
+  return controller.stream;
+}
diff --git a/pkgs/test_core/lib/src/util/dart.dart b/pkgs/test_core/lib/src/util/dart.dart
new file mode 100644
index 0000000..5a1546c
--- /dev/null
+++ b/pkgs/test_core/lib/src/util/dart.dart
@@ -0,0 +1,91 @@
+// 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.
+
+import 'dart:convert';
+import 'dart:io';
+import 'dart:isolate';
+
+import 'package:analyzer/dart/ast/ast.dart';
+import 'package:source_span/source_span.dart';
+
+import '../util/package_config.dart';
+import 'string_literal_iterator.dart';
+
+/// Runs [code] in an isolate.
+///
+/// [code] should be the contents of a Dart entrypoint. It may contain imports;
+/// they will be resolved in the same context as the host isolate. [message] is
+/// 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 =>
+    Isolate.spawnUri(
+        Uri.dataFromString(code, mimeType: 'application/dart', encoding: utf8),
+        [],
+        message,
+        packageConfig: await packageConfigUri,
+        checked: true,
+        onExit: onExit);
+
+/// Takes a span whose source is the value of a string that has been parsed from
+/// a Dart file and returns the corresponding span from within that Dart file.
+///
+/// For example, suppose a Dart file contains `@Eval("1 + a")`. The
+/// [StringLiteral] `"1 + a"` is extracted; this is [context]. Its contents are
+/// then parsed, producing an error pointing to [span]:
+///
+///     line 1, column 5:
+///     1 + a
+///         ^
+///
+/// This span isn't very useful, since it only shows the location within the
+/// [StringLiteral]'s value. So it's passed to [contextualizeSpan] along with
+/// [context] and [file] (which contains the source of the entire Dart file),
+/// which then returns:
+///
+///     line 4, column 12 of file.dart:
+///     @Eval("1 + a")
+///                ^
+///
+/// This properly handles multiline literals, adjacent literals, and literals
+/// containing escape sequences. It does not support interpolated literals.
+///
+/// This will return `null` if [context] contains an invalid string or does not
+/// contain [span].
+SourceSpan? contextualizeSpan(
+    SourceSpan span, StringLiteral context, SourceFile file) {
+  var contextRunes = StringLiteralIterator(context)..moveNext();
+
+  for (var i = 0; i < span.start.offset; i++) {
+    if (!contextRunes.moveNext()) return null;
+  }
+
+  var start = contextRunes.offset;
+  for (var spanRune in span.text.runes) {
+    if (spanRune != contextRunes.current) return null;
+    contextRunes.moveNext();
+  }
+
+  return file.span(start, contextRunes.offset);
+}
+
+/// 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/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/errors.dart b/pkgs/test_core/lib/src/util/errors.dart
new file mode 100644
index 0000000..7ebea6e
--- /dev/null
+++ b/pkgs/test_core/lib/src/util/errors.dart
@@ -0,0 +1,14 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// A regular expression to match the exception prefix that some exceptions'
+/// [Object.toString] values contain.
+final _exceptionPrefix = RegExp(r'^([A-Z][a-zA-Z]*)?(Exception|Error): ');
+
+/// Get a string description of an exception.
+///
+/// Many exceptions include the exception class name at the beginning of their
+/// [toString], so we remove that if it exists.
+String getErrorMessage(Object error) =>
+    error.toString().replaceFirst(_exceptionPrefix, '');
diff --git a/pkgs/test_core/lib/src/util/exit_codes.dart b/pkgs/test_core/lib/src/util/exit_codes.dart
new file mode 100644
index 0000000..7c8b2db
--- /dev/null
+++ b/pkgs/test_core/lib/src/util/exit_codes.dart
@@ -0,0 +1,59 @@
+// 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.
+
+/// Exit code constants.
+///
+/// From [the BSD sysexits manpage][manpage]. Not every constant here is used.
+///
+/// [manpage]: http://www.freebsd.org/cgi/man.cgi?query=sysexits
+/// The command completely successfully.
+const success = 0;
+
+/// The command was used incorrectly.
+const usage = 64;
+
+/// The input data was incorrect.
+const data = 65;
+
+/// An input file did not exist or was unreadable.
+const noInput = 66;
+
+/// The user specified did not exist.
+const noUser = 67;
+
+/// The host specified did not exist.
+const noHost = 68;
+
+/// A service is unavailable.
+const unavailable = 69;
+
+/// An internal software error has been detected.
+const software = 70;
+
+/// An operating system error has been detected.
+const os = 71;
+
+/// Some system file did not exist or was unreadable.
+const osFile = 72;
+
+/// A user-specified output file cannot be created.
+const cantCreate = 73;
+
+/// An error occurred while doing I/O on some file.
+const io = 74;
+
+/// Temporary failure, indicating something that is not really an error.
+const tempFail = 75;
+
+/// The remote system returned something invalid during a protocol exchange.
+const protocol = 76;
+
+/// The user did not have sufficient permissions.
+const noPerm = 77;
+
+/// Something was unconfigured or mis-configured.
+const config = 78;
+
+/// No tests were ran.
+const noTestsRan = 79;
diff --git a/pkgs/test_core/lib/src/util/io.dart b/pkgs/test_core/lib/src/util/io.dart
new file mode 100644
index 0000000..a082dd3
--- /dev/null
+++ b/pkgs/test_core/lib/src/util/io.dart
@@ -0,0 +1,267 @@
+// 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.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:core' as core;
+import 'dart:core';
+import 'dart:io';
+import 'dart:math';
+
+import 'package:async/async.dart';
+import 'package:path/path.dart' as p;
+import 'package:test_api/src/backend/compiler.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/operating_system.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
+
+import 'pretty_print.dart';
+
+/// The default line length for output when there isn't a terminal attached to
+/// stdout.
+const _defaultLineLength = 200;
+
+/// Whether the test runner is running on Google-internal infrastructure.
+final bool inGoogle = Platform.version.contains('(google3)');
+
+/// The maximum line length for output.
+final int lineLength = () {
+  try {
+    return stdout.terminalColumns;
+  } on UnsupportedError {
+    // This can throw an [UnsupportedError] if we're running in a JS context
+    // where `dart:io` is unavailable.
+    return _defaultLineLength;
+  } on StdoutException {
+    return _defaultLineLength;
+  }
+}();
+
+/// The root directory of the Dart SDK.
+final String sdkDir = p.dirname(p.dirname(Platform.resolvedExecutable));
+
+/// The current operating system.
+final currentOS = OperatingSystem.findByIoName(Platform.operatingSystem);
+
+/// Returns a [SuitePlatform] with the given [runtime], and with
+/// [SuitePlatform.os] and [inGoogle] determined automatically.
+///
+/// If [runtime] is a browser, this will set [SuitePlatform.os] to
+/// [OperatingSystem.none].
+// TODO: https://github.com/dart-lang/test/issues/2119 - require compiler
+SuitePlatform currentPlatform(Runtime runtime, [Compiler? compiler]) =>
+    SuitePlatform(runtime,
+        compiler: compiler,
+        os: runtime.isBrowser ? OperatingSystem.none : currentOS,
+        inGoogle: inGoogle);
+
+/// A transformer that decodes bytes using UTF-8 and splits them on newlines.
+final lineSplitter = StreamTransformer<List<int>, String>(
+    (stream, cancelOnError) => utf8.decoder
+        .bind(stream)
+        .transform(const LineSplitter())
+        .listen(null, cancelOnError: cancelOnError));
+
+/// A queue of lines of standard input.
+///
+/// Also returns an empty stream for Fuchsia since Fuchsia components can't
+/// access stdin.
+StreamQueue<String> get stdinLines =>
+    _stdinLines ??= StreamQueue(Platform.isFuchsia
+        ? const Stream<String>.empty()
+        : lineSplitter.bind(stdin));
+
+StreamQueue<String>? _stdinLines;
+
+/// Call cancel on [stdinLines], but only if it's been accessed previously.
+void cancelStdinLines() => _stdinLines?.cancel(immediate: true);
+
+/// Whether this is being run as a subprocess in the test package's own tests.
+bool inTestTests = Platform.environment['_DART_TEST_TESTING'] == 'true';
+
+/// The root directory below which to nest temporary directories created by the
+/// test runner.
+///
+/// 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']!
+    : Directory.systemTemp.path;
+
+/// Whether or not the current terminal supports ansi escape codes.
+///
+/// Otherwise only printable ASCII characters should be used.
+bool get canUseSpecialChars =>
+    (!Platform.isWindows || stdout.supportsAnsiEscapes) && !inTestTests;
+
+/// Detect whether we're running in a Github Actions context.
+///
+/// See
+/// https://docs.github.com/en/actions/learn-github-actions/environment-variables.
+bool get inGithubContext => Platform.environment['GITHUB_ACTIONS'] == 'true';
+
+/// Creates a temporary directory and returns its path.
+String createTempDir() =>
+    Directory(_tempDir).createTempSync('dart_test_').resolveSymbolicLinksSync();
+
+/// Creates a temporary directory and passes its path to [fn].
+///
+/// Once the [Future] returned by [fn] completes, the temporary directory and
+/// all its contents are deleted. [fn] can also return `null`, in which case
+/// the temporary directory is deleted immediately afterwards.
+///
+/// Returns a future that completes to the value that the future returned from
+/// [fn] completes to.
+Future withTempDir(Future Function(String) fn) {
+  return Future.sync(() {
+    var tempDir = createTempDir();
+    return Future.sync(() => fn(tempDir))
+        .whenComplete(() => Directory(tempDir).deleteWithRetry());
+  });
+}
+
+/// Wraps [text] so that it fits within [lineLength].
+///
+/// This preserves existing newlines and doesn't consider terminal color escapes
+/// part of a word's length. It only splits words on spaces, not on other sorts
+/// of whitespace.
+String wordWrap(String text) {
+  return text.split('\n').map((originalLine) {
+    var buffer = StringBuffer();
+    var lengthSoFar = 0;
+    for (var word in originalLine.split(' ')) {
+      var wordLength = withoutColors(word).length;
+      if (wordLength > lineLength) {
+        if (lengthSoFar != 0) buffer.writeln();
+        buffer.writeln(word);
+      } else if (lengthSoFar == 0) {
+        buffer.write(word);
+        lengthSoFar = wordLength;
+      } else if (lengthSoFar + 1 + wordLength > lineLength) {
+        buffer.writeln();
+        buffer.write(word);
+        lengthSoFar = wordLength;
+      } else {
+        buffer.write(' $word');
+        lengthSoFar += 1 + wordLength;
+      }
+    }
+    return buffer.toString();
+  }).join('\n');
+}
+
+/// Print a warning containing [message].
+///
+/// This automatically wraps lines if they get too long. If [color] is passed,
+/// it controls whether the warning header is color; otherwise, it defaults to
+/// [canUseSpecialChars].
+///
+/// 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}) {
+  color ??= canUseSpecialChars;
+  var header = color ? '\u001b[33mWarning:\u001b[0m' : 'Warning:';
+  (print ? core.print : stderr.writeln)(wordWrap('$header $message\n'));
+}
+
+/// Repeatedly finds a probably-unused port on localhost and passes it to
+/// [tryPort] until it binds successfully.
+///
+/// [tryPort] should return a non-`null` value or a Future completing to a
+/// non-`null` value once it binds successfully. This value will be returned
+/// by [getUnusedPort] in turn.
+///
+/// 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 extends Object>(
+    FutureOr<T> Function(int port) tryPort) async {
+  T? value;
+  await Future.doWhile(() async {
+    value = await tryPort(await getUnsafeUnusedPort());
+    return value == null;
+  });
+  return value!;
+}
+
+/// Whether this computer supports binding to IPv6 addresses.
+var _maySupportIPv6 = true;
+
+/// Returns a port that is probably, but not definitely, not in use.
+///
+/// This has a built-in race condition: another process may bind this port at
+/// any time after this call has returned. If at all possible, callers should
+/// use [getUnusedPort] instead.
+Future<int> getUnsafeUnusedPort() async {
+  late int port;
+  if (_maySupportIPv6) {
+    try {
+      final socket = await ServerSocket.bind(InternetAddress.loopbackIPv6, 0,
+          v6Only: true);
+      port = socket.port;
+      await socket.close();
+    } on SocketException {
+      _maySupportIPv6 = false;
+    }
+  }
+  if (!_maySupportIPv6) {
+    final socket = await RawServerSocket.bind(InternetAddress.loopbackIPv4, 0);
+    port = socket.port;
+    await socket.close();
+  }
+  return port;
+}
+
+/// Returns the full URL of the Chrome remote debugger for the main page.
+///
+/// This takes the [base] remote debugger URL (which points to a browser-wide
+/// page) and uses its JSON API to find the resolved URL for debugging the host
+/// page.
+Future<Uri> getRemoteDebuggerUrl(Uri base) async {
+  try {
+    var client = HttpClient();
+    var request = await client.getUrl(base.resolve('/json/list'));
+    var response = await request.close();
+    var jsonObject =
+        await json.fuse(utf8).decoder.bind(response).single as List;
+    return base
+        .resolve((jsonObject.first as Map)['devtoolsFrontendUrl'] as String);
+  } catch (_) {
+    // If we fail to talk to the remote debugger protocol, give up and return
+    // the raw URL rather than crashing.
+    return base;
+  }
+}
+
+extension RetryDelete on FileSystemEntity {
+  Future<void> deleteWithRetry() async {
+    var attempt = 0;
+    while (true) {
+      try {
+        await delete(recursive: true);
+        return;
+      } on FileSystemException {
+        if (attempt == 2) rethrow;
+        attempt++;
+        await Future<void>.delayed(
+            Duration(milliseconds: pow(10, attempt).toInt()));
+      }
+    }
+  }
+}
+
+extension WindowsFilePaths on String {
+  /// Strip out the leading slash before the drive letter on windows.
+  ///
+  /// In some windows environments full paths get passed with `/` before the
+  /// drive letter. Normalize paths to exclude this slash when it exists.
+  String get stripDriveLetterLeadingSlash {
+    if (Platform.isWindows &&
+        startsWith('/') &&
+        length >= 3 &&
+        this[2] == ':') {
+      return substring(1);
+    }
+    return this;
+  }
+}
diff --git a/pkgs/test_core/lib/src/util/io_stub.dart b/pkgs/test_core/lib/src/util/io_stub.dart
new file mode 100644
index 0000000..7953877
--- /dev/null
+++ b/pkgs/test_core/lib/src/util/io_stub.dart
@@ -0,0 +1,11 @@
+// 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.
+
+import 'package:test_api/src/backend/compiler.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
+import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports
+
+SuitePlatform currentPlatform(Runtime runtime, Compiler? compiler) =>
+    throw UnsupportedError(
+        'Getting the current platform is only supported where dart:io exists');
diff --git a/pkgs/test_core/lib/src/util/os.dart b/pkgs/test_core/lib/src/util/os.dart
new file mode 100644
index 0000000..3db1608
--- /dev/null
+++ b/pkgs/test_core/lib/src/util/os.dart
@@ -0,0 +1,30 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:path/path.dart' as p;
+import 'package:test_api/src/backend/operating_system.dart'; // ignore: implementation_imports
+
+/// Directories that are specific to OS X.
+///
+/// This is used to try to distinguish OS X and Linux in [currentOSGuess].
+final _macOSDirectories = {
+  '/Applications',
+  '/Library',
+  '/Network',
+  '/System',
+  '/Users',
+};
+
+/// Returns the best guess for the current operating system without using
+/// `dart:io`.
+///
+/// This is useful for running test files directly and skipping tests as
+/// appropriate. The only OS-specific information we have is the current path,
+/// which we try to use to figure out the OS.
+final OperatingSystem currentOSGuess = (() {
+  if (p.style == p.Style.url) return OperatingSystem.none;
+  if (p.style == p.Style.windows) return OperatingSystem.windows;
+  if (_macOSDirectories.any(p.current.startsWith)) return OperatingSystem.macOS;
+  return OperatingSystem.linux;
+})();
diff --git a/pkgs/test_core/lib/src/util/package_config.dart b/pkgs/test_core/lib/src/util/package_config.dart
new file mode 100644
index 0000000..c33dda6
--- /dev/null
+++ b/pkgs/test_core/lib/src/util/package_config.dart
@@ -0,0 +1,39 @@
+// 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:io';
+import 'dart:isolate';
+
+import 'package:package_config/package_config.dart';
+import 'package:path/path.dart' as p;
+
+/// The [PackageConfig] parsed from the current isolates package config file.
+final Future<PackageConfig> currentPackageConfig = () async {
+  return loadPackageConfigUri(await packageConfigUri);
+}();
+
+final Future<Uri> packageConfigUri = () async {
+  var uri = await Isolate.packageConfig;
+  if (uri == null) {
+    throw StateError('Unable to find a package config');
+  }
+  return uri;
+}();
+
+final _originalWorkingDirectory = Directory.current.uri;
+
+/// Returns an `package:` URI for [path] if it is in a package, otherwise
+/// returns an absolute file URI.
+Future<Uri> absoluteUri(String path) async {
+  final uri = p.toUri(path);
+  final absoluteUri =
+      uri.isAbsolute ? uri : _originalWorkingDirectory.resolveUri(uri);
+  try {
+    final packageConfig = await currentPackageConfig;
+    return packageConfig.toPackageUri(absoluteUri) ?? absoluteUri;
+  } on StateError {
+    // Workaround for a missing package config.
+    return absoluteUri;
+  }
+}
diff --git a/pkgs/test_core/lib/src/util/pretty_print.dart b/pkgs/test_core/lib/src/util/pretty_print.dart
new file mode 100644
index 0000000..f425629
--- /dev/null
+++ b/pkgs/test_core/lib/src/util/pretty_print.dart
@@ -0,0 +1,71 @@
+// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+/// A regular expression matching terminal color codes.
+final _colorCode = RegExp('\u001b\\[[0-9;]+m');
+
+/// Returns [str] without any color codes.
+String withoutColors(String str) => str.replaceAll(_colorCode, '');
+
+/// A regular expression matching a single vowel.
+final _vowel = RegExp('[aeiou]');
+
+/// Returns [noun] with an indefinite article ("a" or "an") added, based on
+/// whether its first letter is a vowel.
+String a(String noun) => noun.startsWith(_vowel) ? 'an $noun' : 'a $noun';
+
+/// Indent each line in [string] by 2 spaces.
+String indent(String text) {
+  var lines = text.split('\n');
+  if (lines.length == 1) return '  $text';
+
+  var buffer = StringBuffer();
+
+  for (var line in lines.take(lines.length - 1)) {
+    buffer.writeln('  $line');
+  }
+  buffer.write('  ${lines.last}');
+  return buffer.toString();
+}
+
+/// Truncates [text] to fit within [maxLength].
+///
+/// This will try to truncate along word boundaries and preserve words both at
+/// the beginning and the end of [text].
+String truncate(String text, int maxLength) {
+  // Return the full message if it fits.
+  if (text.length <= maxLength) return text;
+
+  // If we can fit the first and last three words, do so.
+  var words = text.split(' ');
+  if (words.length > 1) {
+    var i = words.length;
+    var length = words.first.length + 4;
+    do {
+      i--;
+      length += 1 + words[i].length;
+    } while (length <= maxLength && i > 0);
+    if (length > maxLength || i == 0) i++;
+    if (i < words.length - 4) {
+      // Require at least 3 words at the end.
+      var buffer = StringBuffer();
+      buffer.write(words.first);
+      buffer.write(' ...');
+      for (; i < words.length; i++) {
+        buffer.write(' ');
+        buffer.write(words[i]);
+      }
+      return buffer.toString();
+    }
+  }
+
+  // Otherwise truncate to return the trailing text, but attempt to start at
+  // the beginning of a word.
+  var result = text.substring(text.length - maxLength + 4);
+  var firstSpace = result.indexOf(' ');
+  if (firstSpace > 0) {
+    result = result.substring(firstSpace);
+  }
+  return '...$result';
+}
diff --git a/pkgs/test_core/lib/src/util/print_sink.dart b/pkgs/test_core/lib/src/util/print_sink.dart
new file mode 100644
index 0000000..ebb2fe8
--- /dev/null
+++ b/pkgs/test_core/lib/src/util/print_sink.dart
@@ -0,0 +1,39 @@
+// 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.
+
+class PrintSink implements StringSink {
+  final _buffer = StringBuffer();
+
+  @override
+  void write(Object? obj) {
+    _buffer.write(obj);
+    _flush();
+  }
+
+  @override
+  void writeAll(Iterable objects, [String separator = '']) {
+    _buffer.writeAll(objects, separator);
+    _flush();
+  }
+
+  @override
+  void writeCharCode(int charCode) {
+    _buffer.writeCharCode(charCode);
+    _flush();
+  }
+
+  @override
+  void writeln([Object? obj = '']) {
+    _buffer.writeln(obj ?? '');
+    _flush();
+  }
+
+  /// [print] if the content available ends with a newline.
+  void _flush() {
+    if ('$_buffer'.endsWith('\n')) {
+      print(_buffer);
+      _buffer.clear();
+    }
+  }
+}
diff --git a/pkgs/test_core/lib/src/util/stack_trace_mapper.dart b/pkgs/test_core/lib/src/util/stack_trace_mapper.dart
new file mode 100644
index 0000000..9d4248d
--- /dev/null
+++ b/pkgs/test_core/lib/src/util/stack_trace_mapper.dart
@@ -0,0 +1,81 @@
+// 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.
+
+import 'package:source_map_stack_trace/source_map_stack_trace.dart' as mapper;
+import 'package:source_maps/source_maps.dart';
+import 'package:test_api/backend.dart' show StackTraceMapper;
+
+/// A class for mapping JS stack traces to Dart stack traces using source maps.
+class JSStackTraceMapper implements StackTraceMapper {
+  /// The parsed source map.
+  ///
+  /// This is initialized lazily in `mapStackTrace()`.
+  Mapping? _mapping;
+
+  /// The same package resolution information as was passed to dart2js.
+  final Map<String, Uri>? _packageMap;
+
+  /// The URL of the SDK root from which dart2js loaded its sources.
+  final Uri? _sdkRoot;
+
+  /// The contents of the source map.
+  final String _mapContents;
+
+  /// The URL of the source map.
+  final Uri? _mapUrl;
+
+  JSStackTraceMapper(this._mapContents,
+      {Uri? mapUrl, Map<String, Uri>? packageMap, Uri? sdkRoot})
+      : _mapUrl = mapUrl,
+        _packageMap = packageMap,
+        _sdkRoot = sdkRoot;
+
+  /// Converts [trace] into a Dart stack trace.
+  @override
+  StackTrace mapStackTrace(StackTrace trace) {
+    var mapping = _mapping ??= parseExtended(_mapContents, mapUrl: _mapUrl);
+    return mapper.mapStackTrace(mapping, trace,
+        packageMap: _packageMap, sdkRoot: _sdkRoot);
+  }
+
+  /// Returns a Map representation which is suitable for JSON serialization.
+  @override
+  Map<String, dynamic> serialize() {
+    return {
+      'mapContents': _mapContents,
+      'sdkRoot': _sdkRoot?.toString(),
+      'packageConfigMap': _serializePackageConfigMap(_packageMap),
+      'mapUrl': _mapUrl?.toString(),
+    };
+  }
+
+  /// Returns a [StackTraceMapper] contained in the provided serialized
+  /// representation.
+  static StackTraceMapper? deserialize(Map? serialized) {
+    if (serialized == null) return null;
+    var deserialized = _deserializePackageConfigMap(
+        (serialized['packageConfigMap'] as Map).cast<String, String>());
+
+    return JSStackTraceMapper(serialized['mapContents'] as String,
+        sdkRoot: Uri.parse(serialized['sdkRoot'] as String),
+        packageMap: deserialized,
+        mapUrl: Uri.parse(serialized['mapUrl'] as String));
+  }
+
+  /// Converts a [packageConfigMap] into a format suitable for JSON
+  /// serialization.
+  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) {
+    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
new file mode 100644
index 0000000..d58f64a
--- /dev/null
+++ b/pkgs/test_core/lib/src/util/string_literal_iterator.dart
@@ -0,0 +1,245 @@
+// 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.
+
+import 'dart:collection';
+
+import 'package:analyzer/dart/ast/ast.dart';
+
+// ASCII character codes.
+
+const _zero = 0x30;
+const _nine = 0x39;
+const _backslash = 0x5C;
+const _openCurly = 0x7B;
+const _closeCurly = 0x7D;
+const _capitalA = 0x41;
+const _capitalZ = 0x5A;
+const _a = 0x61;
+const _n = 0x6E;
+const _r = 0x72;
+const _f = 0x66;
+const _b = 0x62;
+const _t = 0x74;
+const _u = 0x75;
+const _v = 0x76;
+const _x = 0x78;
+const _z = 0x7A;
+const _newline = 0xA;
+const _carriageReturn = 0xD;
+const _formFeed = 0xC;
+const _backspace = 0x8;
+const _tab = 0x9;
+const _verticalTab = 0xB;
+
+/// An iterator over the runes in the value of a [StringLiteral].
+///
+/// In addition to exposing the values of the runes themselves, this also
+/// exposes the offset of the current rune in the Dart source file.
+class StringLiteralIterator implements Iterator<int> {
+  @override
+  int get current => _current!;
+  int? _current;
+
+  /// The offset of the beginning of [current] in the Dart source file that
+  /// contains the string literal.
+  ///
+  /// Before iteration begins, this points to the character before the first
+  /// rune.
+  int get offset => _offset;
+  late 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;
+
+  /// All [SimpleStringLiteral]s that compose the input literal.
+  ///
+  /// If the input literal is itself a [SimpleStringLiteral], this just contains
+  /// that literal; otherwise, the literal is an [AdjacentStrings], and this
+  /// contains its component literals.
+  final _strings = Queue<SimpleStringLiteral>();
+
+  /// Whether this is a raw string that begins with `r`.
+  ///
+  /// This is necessary for knowing how to parse escape sequences.
+  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;
+
+  /// The result of the last call to `_runes.moveNext`.
+  bool _runesHasCurrent = false;
+
+  /// Creates a new [StringLiteralIterator] iterating over the contents of
+  /// [literal].
+  ///
+  /// Throws an [ArgumentError] if [literal] contains interpolated strings.
+  StringLiteralIterator(StringLiteral literal) {
+    if (literal is StringInterpolation) {
+      throw ArgumentError("Can't iterate over an interpolated string.");
+    } else if (literal is SimpleStringLiteral) {
+      _strings.add(literal);
+    } else {
+      assert(literal is AdjacentStrings);
+
+      for (var string in (literal as AdjacentStrings).strings) {
+        if (string is StringInterpolation) {
+          throw ArgumentError("Can't iterate over an interpolated string.");
+        }
+        _strings.add(string as SimpleStringLiteral);
+      }
+    }
+
+    _offset = _strings.first.contentsOffset - 1;
+  }
+
+  @override
+  bool moveNext() {
+    // If we're at beginning of a [SimpleStringLiteral], move forward until
+    // there's actually text to consume.
+    while (_runes == null || !_runesHasCurrent) {
+      if (_strings.isEmpty) {
+        // Move the offset past the end of the text.
+        _offset = _nextOffset!;
+        _current = null;
+        return false;
+      }
+
+      var string = _strings.removeFirst();
+      var start = string.contentsOffset - string.offset;
+
+      // Compensate for the opening and closing quotes.
+      var end = start +
+          string.literal.lexeme.length -
+          2 * (string.isMultiline ? 3 : 1) -
+          (string.isRaw ? 1 : 0);
+      var text = string.literal.lexeme.substring(start, end);
+
+      _nextOffset = string.contentsOffset;
+      _isRaw = string.isRaw;
+      _runes = text.runes.iterator;
+      _runesHasCurrent = _runes!.moveNext();
+    }
+
+    _offset = _nextOffset!;
+    _current = _nextRune();
+    if (_current != null) return true;
+
+    // If we encounter a parse failure, stop moving forward immediately.
+    _strings.clear();
+    return false;
+  }
+
+  /// Consume and return the next rune.
+  int? _nextRune() {
+    if (_isRaw! || _runes!.current != _backslash) {
+      var rune = _runes!.current;
+      _moveRunesNext();
+      return (rune < 0) ? null : rune;
+    }
+
+    if (!_moveRunesNext()) return null;
+    return _parseEscapeSequence();
+  }
+
+  /// Parse an escape sequence in the underlying Dart text.
+  ///
+  /// This assumes that a backslash has already been consumed. It leaves the
+  /// [_runes] cursor on the first character after the escape sequence.
+  int? _parseEscapeSequence() {
+    switch (_runes!.current) {
+      case _n:
+        _moveRunesNext();
+        return _newline;
+      case _r:
+        _moveRunesNext();
+        return _carriageReturn;
+      case _f:
+        _moveRunesNext();
+        return _formFeed;
+      case _b:
+        _moveRunesNext();
+        return _backspace;
+      case _t:
+        _moveRunesNext();
+        return _tab;
+      case _v:
+        _moveRunesNext();
+        return _verticalTab;
+      case _x:
+        if (!_moveRunesNext()) return null;
+        return _parseHex(2);
+      case _u:
+        if (!_moveRunesNext()) return null;
+        if (_runes!.current != _openCurly) return _parseHex(4);
+        if (!_moveRunesNext()) return null;
+
+        var number = _parseHexSequence();
+        if (_runes!.current != _closeCurly) return null;
+        if (!_moveRunesNext()) return null;
+        return number;
+      default:
+        var rune = _runes!.current;
+        _moveRunesNext();
+        return rune;
+    }
+  }
+
+  /// Parse a variable-length sequence of hexadecimal digits and returns their
+  /// value as an [int].
+  ///
+  /// This parses digits as they appear in a unicode escape sequence: one to six
+  /// hex digits.
+  int? _parseHexSequence() {
+    var number = _parseHexDigit(_runes!.current);
+    if (number == null) return null;
+    if (!_moveRunesNext()) return null;
+
+    for (var i = 0; i < 5; i++) {
+      var digit = _parseHexDigit(_runes!.current);
+      if (digit == null) break;
+      number = number! * 16 + digit;
+      if (!_moveRunesNext()) return null;
+    }
+
+    return number;
+  }
+
+  /// Parses [digits] hexadecimal digits and returns their value as an [int].
+  int? _parseHex(int digits) {
+    var number = 0;
+    for (var i = 0; i < digits; i++) {
+      if (_runes!.current == -1) return null;
+      var digit = _parseHexDigit(_runes!.current);
+      if (digit == null) return null;
+      number = number * 16 + digit;
+      _moveRunesNext();
+    }
+    return number;
+  }
+
+  /// Parses a single hexadecimal digit.
+  int? _parseHexDigit(int rune) {
+    if (rune < _zero) return null;
+    if (rune <= _nine) return rune - _zero;
+    if (rune < _capitalA) return null;
+    if (rune <= _capitalZ) return 10 + rune - _capitalA;
+    if (rune < _a) return null;
+    if (rune <= _z) return 10 + rune - _a;
+    return null;
+  }
+
+  /// Move [_runes] to the next rune and update [_nextOffset].
+  bool _moveRunesNext() {
+    var result = _runesHasCurrent = _runes!.moveNext();
+    _nextOffset = _nextOffset! + 1;
+    return result;
+  }
+}
diff --git a/pkgs/test_core/lib/test_core.dart b/pkgs/test_core/lib/test_core.dart
new file mode 100644
index 0000000..b1df09d
--- /dev/null
+++ b/pkgs/test_core/lib/test_core.dart
@@ -0,0 +1,11 @@
+// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@Deprecated('package:test_core is not intended for general use. '
+    'Please use package:test.')
+library;
+
+export 'package:test_api/hooks.dart' show TestFailure;
+
+export 'scaffolding.dart';
diff --git a/pkgs/test_core/mono_pkg.yaml b/pkgs/test_core/mono_pkg.yaml
new file mode 100644
index 0000000..9ef7927
--- /dev/null
+++ b/pkgs/test_core/mono_pkg.yaml
@@ -0,0 +1,17 @@
+# See https://pub.dev/packages/mono_repo
+
+sdk:
+- dev
+
+stages:
+- analyze_and_format:
+  - group:
+    - format
+    - analyze: --fatal-infos
+  - group:
+    - analyze
+    sdk: pubspec
+- unit_test:
+  - group:
+    - command: dart test
+    sdk: [dev, pubspec]
diff --git a/pkgs/test_core/pubspec.yaml b/pkgs/test_core/pubspec.yaml
new file mode 100644
index 0000000..d08e24c
--- /dev/null
+++ b/pkgs/test_core/pubspec.yaml
@@ -0,0 +1,35 @@
+name: test_core
+version: 0.6.8
+description: A basic library for writing tests and running them on the VM.
+repository: https://github.com/dart-lang/test/tree/master/pkgs/test_core
+resolution: workspace
+
+environment:
+  sdk: ^3.5.0
+
+dependencies:
+  analyzer: '>=6.0.0 <8.0.0'
+  args: ^2.0.0
+  async: ^2.5.0
+  boolean_selector: ^2.1.0
+  collection: ^1.15.0
+  coverage: ^1.0.0
+  frontend_server_client: '>=3.2.0 <5.0.0'
+  glob: ^2.0.0
+  io: ^1.0.0
+  meta: ^1.3.0
+  package_config: ^2.0.0
+  path: ^1.8.0
+  pool: ^1.5.0
+  source_map_stack_trace: ^2.1.0
+  source_maps: ^0.10.10
+  source_span: ^1.8.0
+  stack_trace: ^1.10.0
+  stream_channel: ^2.1.0
+  # Use an exact version until the test_api package is stable.
+  test_api: 0.7.4
+  vm_service: ">=6.0.0 <16.0.0"
+  yaml: ^3.0.0
+
+dev_dependencies:
+  test: any
diff --git a/pkgs/test_core/test/runner/vm/test_compiler_test.dart b/pkgs/test_core/test/runner/vm/test_compiler_test.dart
new file mode 100644
index 0000000..a6e50ef
--- /dev/null
+++ b/pkgs/test_core/test/runner/vm/test_compiler_test.dart
@@ -0,0 +1,25 @@
+// Copyright (c) 2024, 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';
+
+import 'package:test/test.dart';
+import 'package:test_core/src/runner/vm/test_compiler.dart';
+
+void main() {
+  group('VM test templates', () {
+    test('include package config URI variable', () async {
+      // This variable is read through the VM service and should not be removed.
+      final template = testBootstrapContents(
+        testUri: Uri.file('foo.dart'),
+        languageVersionComment: '// version comment',
+        packageConfigUri: Uri.file('package_config.json'),
+        testType: VmTestType.isolate,
+      );
+      final lines = LineSplitter.split(template).map((line) => line.trim());
+      expect(lines,
+          contains("const packageConfigLocation = 'package_config.json';"));
+    });
+  });
+}
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..2e08374
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,14 @@
+name: test_workspace
+publish_to: none
+environment:
+  sdk: ^3.5.0
+workspace:
+  - integration_tests/regression
+  - integration_tests/spawn_hybrid
+  - integration_tests/wasm
+  - pkgs/checks
+  - pkgs/test
+  - pkgs/test_api
+  - pkgs/test_core
+dev_dependencies:
+  dart_flutter_team_lints: ^3.1.0
diff --git a/tool/ci.sh b/tool/ci.sh
new file mode 100755
index 0000000..040c70d
--- /dev/null
+++ b/tool/ci.sh
@@ -0,0 +1,167 @@
+#!/bin/bash
+# Created with package:mono_repo v6.6.2
+
+# Support built in commands on windows out of the box.
+
+# When it is a flutter repo (check the pubspec.yaml for "sdk: flutter")
+# then "flutter pub" is called instead of "dart pub".
+# This assumes that the Flutter SDK has been installed in a previous step.
+function pub() {
+  if grep -Fq "sdk: flutter" "${PWD}/pubspec.yaml"; then
+    command flutter pub "$@"
+  else
+    command dart pub "$@"
+  fi
+}
+
+function format() {
+  command dart format "$@"
+}
+
+# When it is a flutter repo (check the pubspec.yaml for "sdk: flutter")
+# then "flutter analyze" is called instead of "dart analyze".
+# This assumes that the Flutter SDK has been installed in a previous step.
+function analyze() {
+  if grep -Fq "sdk: flutter" "${PWD}/pubspec.yaml"; then
+    command flutter analyze "$@"
+  else
+    command dart analyze "$@"
+  fi
+}
+
+if [[ -z ${PKGS} ]]; then
+  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! - TERMINATING JOB\033[0m'
+  exit 64
+fi
+
+SUCCESS_COUNT=0
+declare -a FAILURES
+
+for PKG in ${PKGS}; do
+  echo -e "\033[1mPKG: ${PKG}\033[22m"
+  EXIT_CODE=0
+  pushd "${PKG}" >/dev/null || EXIT_CODE=$?
+
+  if [[ ${EXIT_CODE} -ne 0 ]]; then
+    echo -e "\033[31mPKG: '${PKG}' does not exist - TERMINATING JOB\033[0m"
+    exit 64
+  fi
+
+  dart pub upgrade || EXIT_CODE=$?
+
+  if [[ ${EXIT_CODE} -ne 0 ]]; then
+    echo -e "\033[31mPKG: ${PKG}; 'dart pub upgrade' - FAILED  (${EXIT_CODE})\033[0m"
+    FAILURES+=("${PKG}; 'dart pub upgrade'")
+  else
+    for TASK in "$@"; do
+      EXIT_CODE=0
+      echo
+      echo -e "\033[1mPKG: ${PKG}; TASK: ${TASK}\033[22m"
+      case ${TASK} in
+      analyze_0)
+        echo 'dart analyze --fatal-infos'
+        dart analyze --fatal-infos || EXIT_CODE=$?
+        ;;
+      analyze_1)
+        echo 'dart analyze'
+        dart analyze || EXIT_CODE=$?
+        ;;
+      command_00)
+        echo 'dart test'
+        dart test || EXIT_CODE=$?
+        ;;
+      command_01)
+        echo 'xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 0'
+        xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 0 || EXIT_CODE=$?
+        ;;
+      command_02)
+        echo 'xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 1'
+        xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 1 || EXIT_CODE=$?
+        ;;
+      command_03)
+        echo 'xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 2'
+        xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 2 || EXIT_CODE=$?
+        ;;
+      command_04)
+        echo 'xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 3'
+        xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 3 || EXIT_CODE=$?
+        ;;
+      command_05)
+        echo 'xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 4'
+        xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 4 || EXIT_CODE=$?
+        ;;
+      command_06)
+        echo 'dart test --preset travis --total-shards 5 --shard-index 0'
+        dart test --preset travis --total-shards 5 --shard-index 0 || EXIT_CODE=$?
+        ;;
+      command_07)
+        echo 'dart test --preset travis --total-shards 5 --shard-index 1'
+        dart test --preset travis --total-shards 5 --shard-index 1 || EXIT_CODE=$?
+        ;;
+      command_08)
+        echo 'dart test --preset travis --total-shards 5 --shard-index 2'
+        dart test --preset travis --total-shards 5 --shard-index 2 || EXIT_CODE=$?
+        ;;
+      command_09)
+        echo 'dart test --preset travis --total-shards 5 --shard-index 3'
+        dart test --preset travis --total-shards 5 --shard-index 3 || EXIT_CODE=$?
+        ;;
+      command_10)
+        echo 'dart test --preset travis --total-shards 5 --shard-index 4'
+        dart test --preset travis --total-shards 5 --shard-index 4 || EXIT_CODE=$?
+        ;;
+      command_11)
+        echo 'dart test --preset travis -x browser'
+        dart test --preset travis -x browser || EXIT_CODE=$?
+        ;;
+      format)
+        echo 'dart format --output=none --set-exit-if-changed .'
+        dart format --output=none --set-exit-if-changed . || EXIT_CODE=$?
+        ;;
+      test_1)
+        echo 'dart test -p chrome,vm,node'
+        dart test -p chrome,vm,node || EXIT_CODE=$?
+        ;;
+      test_2)
+        echo 'dart test --timeout=60s'
+        dart test --timeout=60s || 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
+
+if [ ${#FAILURES[@]} -ne 0 ]; then
+  exit 1
+fi