Merge branch 'master' into merge-test_descriptor-package
diff --git a/.github/ISSUE_TEMPLATE/test_process.md b/.github/ISSUE_TEMPLATE/test_process.md
new file mode 100644
index 0000000..9f492b8
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/test_process.md
@@ -0,0 +1,5 @@
+---
+name: "package:test_process"
+about: "Create a bug or file a feature request against package:test_process."
+labels: "package:test_process"
+---
\ No newline at end of file
diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml
index 9a3cd79..8cda374 100644
--- a/.github/workflows/dart.yml
+++ b/.github/workflows/dart.yml
@@ -70,36 +70,6 @@
         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:
@@ -146,17 +116,17 @@
         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`"
+  job_004:
+    name: "analyze_and_format; linux; Dart 3.5.0; PKGS: pkgs/checks, pkgs/matcher, 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"
+          key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/checks-pkgs/matcher-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;packages:pkgs/checks-pkgs/matcher-pkgs/test_core
             os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0
             os:ubuntu-latest;pub-cache-hosted
             os:ubuntu-latest
@@ -176,6 +146,15 @@
         run: dart analyze
         if: "always() && steps.pkgs_checks_pub_upgrade.conclusion == 'success'"
         working-directory: pkgs/checks
+      - 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
       - id: pkgs_test_core_pub_upgrade
         name: pkgs/test_core; dart pub upgrade
         run: dart pub upgrade
@@ -185,7 +164,7 @@
         run: dart analyze
         if: "always() && steps.pkgs_test_core_pub_upgrade.conclusion == 'success'"
         working-directory: pkgs/test_core
-  job_006:
+  job_005:
     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:
@@ -323,7 +302,7 @@
         run: dart analyze --fatal-infos
         if: "always() && steps.pkgs_test_core_pub_upgrade.conclusion == 'success'"
         working-directory: pkgs/test_core
-  job_007:
+  job_006:
     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:
@@ -347,7 +326,7 @@
         run: dart analyze --fatal-infos
         if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'"
         working-directory: integration_tests/wasm
-  job_008:
+  job_007:
     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:
@@ -371,7 +350,7 @@
         run: dart analyze --fatal-infos
         if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'"
         working-directory: integration_tests/wasm
-  job_009:
+  job_008:
     name: "unit_test; linux; Dart 3.3.0; PKG: pkgs/fake_async; `dart test`"
     runs-on: ubuntu-latest
     steps:
@@ -409,47 +388,7 @@
       - 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:
+  job_009:
     name: "unit_test; linux; Dart 3.5.0; PKG: integration_tests/regression; `dart test`"
     runs-on: ubuntu-latest
     steps:
@@ -487,8 +426,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_012:
+  job_010:
     name: "unit_test; linux; Dart 3.5.0; PKG: pkgs/checks; `dart test`"
     runs-on: ubuntu-latest
     steps:
@@ -526,8 +464,45 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_013:
+  job_011:
+    name: "unit_test; linux; Dart 3.5.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.5.0;packages:pkgs/matcher;commands:command_00"
+          restore-keys: |
+            os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/matcher
+            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_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_012:
     name: "unit_test; linux; Dart 3.5.0; PKG: pkgs/test_core; `dart test`"
     runs-on: ubuntu-latest
     steps:
@@ -565,8 +540,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_014:
+  job_013:
     name: "unit_test; linux; Dart 3.5.0; PKG: integration_tests/spawn_hybrid; `dart test -p chrome,vm,node`"
     runs-on: ubuntu-latest
     steps:
@@ -604,8 +578,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_015:
+  job_014:
     name: "unit_test; linux; Dart 3.5.0; PKG: integration_tests/wasm; `dart test --timeout=60s`"
     runs-on: ubuntu-latest
     steps:
@@ -643,8 +616,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_016:
+  job_015:
     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:
@@ -682,8 +654,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_017:
+  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 1`"
     runs-on: ubuntu-latest
     steps:
@@ -721,8 +692,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_018:
+  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 2`"
     runs-on: ubuntu-latest
     steps:
@@ -760,8 +730,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_019:
+  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 3`"
     runs-on: ubuntu-latest
     steps:
@@ -799,8 +768,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_020:
+  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 4`"
     runs-on: ubuntu-latest
     steps:
@@ -838,8 +806,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_021:
+  job_020:
     name: "unit_test; linux; Dart 3.5.0; PKG: pkgs/test_api; `dart test --preset travis -x browser`"
     runs-on: ubuntu-latest
     steps:
@@ -877,8 +844,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_022:
+  job_021:
     name: "unit_test; linux; Dart dev; PKG: integration_tests/regression; `dart test`"
     runs-on: ubuntu-latest
     steps:
@@ -916,8 +882,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_023:
+  job_022:
     name: "unit_test; linux; Dart dev; PKG: pkgs/checks; `dart test`"
     runs-on: ubuntu-latest
     steps:
@@ -955,8 +920,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_024:
+  job_023:
     name: "unit_test; linux; Dart dev; PKG: pkgs/fake_async; `dart test`"
     runs-on: ubuntu-latest
     steps:
@@ -994,8 +958,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_025:
+  job_024:
     name: "unit_test; linux; Dart dev; PKG: pkgs/matcher; `dart test`"
     runs-on: ubuntu-latest
     steps:
@@ -1033,8 +996,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_026:
+  job_025:
     name: "unit_test; linux; Dart dev; PKG: pkgs/test_core; `dart test`"
     runs-on: ubuntu-latest
     steps:
@@ -1072,8 +1034,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_027:
+  job_026:
     name: "unit_test; linux; Dart dev; PKG: integration_tests/spawn_hybrid; `dart test -p chrome,vm,node`"
     runs-on: ubuntu-latest
     steps:
@@ -1111,8 +1072,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_028:
+  job_027:
     name: "unit_test; linux; Dart dev; PKG: integration_tests/wasm; `dart test --timeout=60s`"
     runs-on: ubuntu-latest
     steps:
@@ -1150,8 +1110,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_029:
+  job_028:
     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:
@@ -1189,8 +1148,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_030:
+  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 1`"
     runs-on: ubuntu-latest
     steps:
@@ -1228,8 +1186,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_031:
+  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 2`"
     runs-on: ubuntu-latest
     steps:
@@ -1267,8 +1224,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_032:
+  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 3`"
     runs-on: ubuntu-latest
     steps:
@@ -1306,8 +1262,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_033:
+  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 4`"
     runs-on: ubuntu-latest
     steps:
@@ -1345,8 +1300,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_034:
+  job_033:
     name: "unit_test; linux; Dart dev; PKG: pkgs/test_api; `dart test --preset travis -x browser`"
     runs-on: ubuntu-latest
     steps:
@@ -1384,8 +1338,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_035:
+  job_034:
     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:
@@ -1423,8 +1376,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_036:
+  job_035:
     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:
@@ -1462,8 +1414,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_037:
+  job_036:
     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:
@@ -1501,8 +1452,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_038:
+  job_037:
     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:
@@ -1540,8 +1490,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_039:
+  job_038:
     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:
@@ -1579,8 +1528,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_040:
+  job_039:
     name: "unit_test; windows; Dart 3.5.0; PKG: integration_tests/spawn_hybrid; `dart test -p chrome,vm,node`"
     runs-on: windows-latest
     steps:
@@ -1608,8 +1556,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_041:
+  job_040:
     name: "unit_test; windows; Dart 3.5.0; PKG: integration_tests/wasm; `dart test --timeout=60s`"
     runs-on: windows-latest
     steps:
@@ -1637,8 +1584,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_042:
+  job_041:
     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:
@@ -1666,8 +1612,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_043:
+  job_042:
     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:
@@ -1695,8 +1640,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_044:
+  job_043:
     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:
@@ -1724,8 +1668,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_045:
+  job_044:
     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:
@@ -1753,8 +1696,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_046:
+  job_045:
     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:
@@ -1782,8 +1724,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_047:
+  job_046:
     name: "unit_test; windows; Dart dev; PKG: integration_tests/spawn_hybrid; `dart test -p chrome,vm,node`"
     runs-on: windows-latest
     steps:
@@ -1811,8 +1752,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_048:
+  job_047:
     name: "unit_test; windows; Dart dev; PKG: integration_tests/wasm; `dart test --timeout=60s`"
     runs-on: windows-latest
     steps:
@@ -1840,8 +1780,7 @@
       - job_005
       - job_006
       - job_007
-      - job_008
-  job_049:
+  job_048:
     name: Notify failure
     runs-on: ubuntu-latest
     if: "(github.event_name == 'push' || github.event_name == 'schedule') && failure()"
@@ -1900,4 +1839,3 @@
       - job_045
       - job_046
       - job_047
-      - job_048
diff --git a/.github/workflows/test_process.yaml b/.github/workflows/test_process.yaml
new file mode 100644
index 0000000..2c0da6d
--- /dev/null
+++ b/.github/workflows/test_process.yaml
@@ -0,0 +1,72 @@
+name: package:test_process
+
+on:
+  # Run on PRs and pushes to the default branch.
+  push:
+    branches: [ master ]
+    paths:
+      - '.github/workflows/test_process.yaml'
+      - 'pkgs/test_process/**'
+  pull_request:
+    branches: [ master ]
+    paths:
+      - '.github/workflows/test_process.yaml'
+      - 'pkgs/test_process/**'
+  schedule:
+    - cron: "0 0 * * 0"
+
+env:
+  PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+  run:
+    working-directory: pkgs/test_process/
+
+jobs:
+  # Check code formatting and static analysis on a single OS (linux)
+  # against Dart dev.
+  analyze:
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: false
+      matrix:
+        sdk: [dev]
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: ${{ matrix.sdk }}
+      - id: install
+        name: Install dependencies
+        run: dart pub get
+      - name: Check formatting
+        run: dart format --output=none --set-exit-if-changed .
+        if: always() && steps.install.outcome == 'success'
+      - name: Analyze code
+        run: dart analyze --fatal-infos
+        if: always() && steps.install.outcome == 'success'
+
+  # Run tests on a matrix consisting of two dimensions:
+  # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+  # 2. release: dev
+  test:
+    needs: analyze
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        # Add macos-latest and/or windows-latest if relevant for this package.
+        os: [ubuntu-latest]
+        sdk: [3.1, dev]
+    steps:
+      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+      - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+        with:
+          sdk: ${{ matrix.sdk }}
+      - id: install
+        name: Install dependencies
+        run: dart pub get
+      - name: Run VM tests
+        run: dart test --platform vm
+        if: always() && steps.install.outcome == 'success'
diff --git a/README.md b/README.md
index 9ff29e9..f178ccb 100644
--- a/README.md
+++ b/README.md
@@ -23,3 +23,4 @@
 | [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) |
 | [test_descriptor](pkgs/test_descriptor/) | An API for defining and verifying files and directory structures. | [![pub package](https://img.shields.io/pub/v/test_descriptor.svg)](https://pub.dev/packages/test_descriptor) |
+| [test_process](pkgs/test_process/) | Test processes: starting; validating stdout and stderr; checking exit code | [![pub package](https://img.shields.io/pub/v/test_process.svg)](https://pub.dev/packages/test_process) |
diff --git a/pkgs/matcher/CHANGELOG.md b/pkgs/matcher/CHANGELOG.md
index 0522c4c..614e35a 100644
--- a/pkgs/matcher/CHANGELOG.md
+++ b/pkgs/matcher/CHANGELOG.md
@@ -1,6 +1,8 @@
 ## 0.12.18-wip
 
 * Remove some dynamic invocations.
+* Add explicit casts from `dynamic` values.
+* Require Dart 3.5
 
 ## 0.12.17
 
diff --git a/pkgs/matcher/analysis_options.yaml b/pkgs/matcher/analysis_options.yaml
index d183f7b..5564cc8 100644
--- a/pkgs/matcher/analysis_options.yaml
+++ b/pkgs/matcher/analysis_options.yaml
@@ -1,4 +1,4 @@
-include: package:lints/recommended.yaml
+include: package:dart_flutter_team_lints/analysis_options.yaml
 
 linter:
   rules:
diff --git a/pkgs/matcher/lib/expect.dart b/pkgs/matcher/lib/expect.dart
index c842d30..113f240 100644
--- a/pkgs/matcher/lib/expect.dart
+++ b/pkgs/matcher/lib/expect.dart
@@ -38,14 +38,14 @@
 export 'src/expect/stream_matcher.dart' show StreamMatcher;
 export 'src/expect/stream_matchers.dart'
     show
-        emitsDone,
         emits,
-        emitsError,
-        mayEmit,
         emitsAnyOf,
-        emitsInOrder,
+        emitsDone,
+        emitsError,
         emitsInAnyOrder,
+        emitsInOrder,
         emitsThrough,
+        mayEmit,
         mayEmitMultiple,
         neverEmits;
 export 'src/expect/throws_matcher.dart' show Throws, throws, throwsA;
diff --git a/pkgs/matcher/lib/mirror_matchers.dart b/pkgs/matcher/lib/mirror_matchers.dart
index 5b2f4b6..cf85e23 100644
--- a/pkgs/matcher/lib/mirror_matchers.dart
+++ b/pkgs/matcher/lib/mirror_matchers.dart
@@ -73,7 +73,7 @@
           .add('has property "$_name" with value ')
           .addDescriptionOf(matchState['value']);
       var innerDescription = StringDescription();
-      matchState['state'] ??= {};
+      matchState['state'] ??= <Object?, Object?>{};
       _matcher?.describeMismatch(matchState['value'], innerDescription,
           matchState['state'] as Map, verbose);
       if (innerDescription.length > 0) {
diff --git a/pkgs/matcher/lib/src/core_matchers.dart b/pkgs/matcher/lib/src/core_matchers.dart
index afb835b..936149e 100644
--- a/pkgs/matcher/lib/src/core_matchers.dart
+++ b/pkgs/matcher/lib/src/core_matchers.dart
@@ -14,7 +14,8 @@
   const _Empty();
 
   @override
-  bool matches(Object? item, Map matchState) => (item as dynamic).isEmpty;
+  bool matches(Object? item, Map matchState) =>
+      (item as dynamic).isEmpty as bool;
 
   @override
   Description describe(Description description) => description.add('empty');
@@ -27,7 +28,8 @@
   const _NotEmpty();
 
   @override
-  bool matches(Object? item, Map matchState) => (item as dynamic).isNotEmpty;
+  bool matches(Object? item, Map matchState) =>
+      (item as dynamic).isNotEmpty as bool;
 
   @override
   Description describe(Description description) => description.add('non-empty');
diff --git a/pkgs/matcher/lib/src/equals_matcher.dart b/pkgs/matcher/lib/src/equals_matcher.dart
index 5c4f4c5..36b3bf5 100644
--- a/pkgs/matcher/lib/src/equals_matcher.dart
+++ b/pkgs/matcher/lib/src/equals_matcher.dart
@@ -167,7 +167,7 @@
       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 = {};
+      var matchState = <Object?, Object?>{};
       if (expected.matches(actual, matchState)) return null;
       return _Mismatch(location, actual, (description, verbose) {
         var oldLength = description.length;
diff --git a/pkgs/matcher/lib/src/expect/expect.dart b/pkgs/matcher/lib/src/expect/expect.dart
index 8dd8cae..d8110d6 100644
--- a/pkgs/matcher/lib/src/expect/expect.dart
+++ b/pkgs/matcher/lib/src/expect/expect.dart
@@ -74,7 +74,10 @@
 
 /// The implementation of [expect] and [expectLater].
 Future _expect(Object? actual, Object? matcher,
-    {String? reason, skip, bool verbose = false, ErrorFormatter? formatter}) {
+    {String? reason,
+    Object? skip,
+    bool verbose = false,
+    ErrorFormatter? formatter}) {
   final test = TestHandle.current;
   formatter ??= (actual, matcher, reason, matchState, verbose) {
     var mismatchDescription = StringDescription();
@@ -133,7 +136,7 @@
     return Future.sync(() {});
   }
 
-  var matchState = {};
+  var matchState = <Object?, Object?>{};
   try {
     if ((matcher as Matcher).matches(actual, matchState)) {
       return Future.sync(() {});
diff --git a/pkgs/matcher/lib/src/expect/expect_async.dart b/pkgs/matcher/lib/src/expect/expect_async.dart
index 88cf6f2..a659c81 100644
--- a/pkgs/matcher/lib/src/expect/expect_async.dart
+++ b/pkgs/matcher/lib/src/expect/expect_async.dart
@@ -124,15 +124,17 @@
   /// 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)) {
+    if (_callback is void 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;
+    if (_callback is void Function(Never, Never, Never, Never, Never)) {
+      return max5;
+    }
+    if (_callback is void Function(Never, Never, Never, Never)) return max4;
+    if (_callback is void Function(Never, Never, Never)) return max3;
+    if (_callback is void Function(Never, Never)) return max2;
+    if (_callback is void Function(Never)) return max1;
+    if (_callback is void Function()) return max0;
 
     _outstandingWork?.complete();
     throw ArgumentError(
@@ -219,7 +221,8 @@
 @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;
+    _ExpectedFunction<Object?>(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).
@@ -415,7 +418,8 @@
 @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)
+    _ExpectedFunction<Object?>(callback, 0, -1,
+            id: id, reason: reason, isDone: isDone)
         .func;
 
 /// Informs the framework that the given [callback] of arity 0 is expected to be
diff --git a/pkgs/matcher/lib/src/expect/future_matchers.dart b/pkgs/matcher/lib/src/expect/future_matchers.dart
index 407b9b8..75655f0 100644
--- a/pkgs/matcher/lib/src/expect/future_matchers.dart
+++ b/pkgs/matcher/lib/src/expect/future_matchers.dart
@@ -58,7 +58,7 @@
         result = await _matcher.matchAsync(value) as String?;
         if (result == null) return null;
       } else {
-        var matchState = {};
+        var matchState = <Object?, Object?>{};
         if (_matcher.matches(value, matchState)) return null;
         result = _matcher
             .describeMismatch(value, StringDescription(), matchState, false)
diff --git a/pkgs/matcher/lib/src/expect/prints_matcher.dart b/pkgs/matcher/lib/src/expect/prints_matcher.dart
index 57ae95e..495d22c 100644
--- a/pkgs/matcher/lib/src/expect/prints_matcher.dart
+++ b/pkgs/matcher/lib/src/expect/prints_matcher.dart
@@ -33,7 +33,7 @@
   // synchronous.
   @override
   dynamic /*FutureOr<String>*/ matchAsync(Object? item) {
-    if (item is! Function()) return 'was not a unary Function';
+    if (item is! Object? Function()) return 'was not a unary Function';
 
     var buffer = StringBuffer();
     var result = runZoned(item,
@@ -53,7 +53,7 @@
   /// Verifies that [actual] matches [_matcher] and returns a [String]
   /// description of the failure if it doesn't.
   String? _check(String actual) {
-    var matchState = {};
+    var matchState = <Object?, Object?>{};
     if (_matcher.matches(actual, matchState)) return null;
 
     var result = _matcher
diff --git a/pkgs/matcher/lib/src/expect/stream_matcher.dart b/pkgs/matcher/lib/src/expect/stream_matcher.dart
index 0c1d852..253b6bf 100644
--- a/pkgs/matcher/lib/src/expect/stream_matcher.dart
+++ b/pkgs/matcher/lib/src/expect/stream_matcher.dart
@@ -155,7 +155,7 @@
           .listen(events.add, onDone: () => events.add(null));
 
       // Wait on a timer tick so all buffered events are emitted.
-      await Future.delayed(Duration.zero);
+      await Future<void>.delayed(Duration.zero);
       _unawaited(subscription.cancel());
 
       var eventsString = events.map((event) {
diff --git a/pkgs/matcher/lib/src/expect/stream_matchers.dart b/pkgs/matcher/lib/src/expect/stream_matchers.dart
index 02efff3..cff7f9f 100644
--- a/pkgs/matcher/lib/src/expect/stream_matchers.dart
+++ b/pkgs/matcher/lib/src/expect/stream_matchers.dart
@@ -34,7 +34,7 @@
   return StreamMatcher((queue) async {
     if (!await queue.hasNext) return '';
 
-    var matchState = {};
+    var matchState = <Object?, Object?>{};
     var actual = await queue.next;
     if (wrapped.matches(actual, matchState)) return null;
 
@@ -138,7 +138,7 @@
     if (consumedMost == null) {
       transaction.reject();
       if (firstError != null) {
-        await Future.error(firstError!, firstStackTrace);
+        await Future<Never>.error(firstError!, firstStackTrace);
       }
 
       var failureMessages = <String>[];
@@ -368,7 +368,9 @@
 
   if (consumedMost == null) {
     transaction.reject();
-    if (firstError != null) await Future.error(firstError!, firstStackTrace);
+    if (firstError != null) {
+      await Future<Never>.error(firstError!, firstStackTrace);
+    }
     return false;
   } else {
     transaction.commit(consumedMost!);
diff --git a/pkgs/matcher/lib/src/expect/throws_matcher.dart b/pkgs/matcher/lib/src/expect/throws_matcher.dart
index 37676ef..17a8c9e 100644
--- a/pkgs/matcher/lib/src/expect/throws_matcher.dart
+++ b/pkgs/matcher/lib/src/expect/throws_matcher.dart
@@ -116,10 +116,10 @@
 
   /// Verifies that [error] matches [_matcher] and returns a [String]
   /// description of the failure if it doesn't.
-  String? _check(error, StackTrace? trace) {
+  String? _check(Object error, StackTrace? trace) {
     if (_matcher == null) return null;
 
-    var matchState = {};
+    var matchState = <Object?, Object?>{};
     if (_matcher.matches(error, matchState)) return null;
 
     var result = _matcher
diff --git a/pkgs/matcher/lib/src/map_matchers.dart b/pkgs/matcher/lib/src/map_matchers.dart
index 4476d06..9bc9eb5 100644
--- a/pkgs/matcher/lib/src/map_matchers.dart
+++ b/pkgs/matcher/lib/src/map_matchers.dart
@@ -16,7 +16,7 @@
   @override
   bool matches(Object? item, Map matchState) =>
       // ignore: avoid_dynamic_calls
-      (item as dynamic).containsValue(_value);
+      (item as dynamic).containsValue(_value) as bool;
   @override
   Description describe(Description description) =>
       description.add('contains value ').addDescriptionOf(_value);
@@ -36,7 +36,7 @@
   @override
   bool matches(Object? item, Map matchState) =>
       // ignore: avoid_dynamic_calls
-      (item as dynamic).containsKey(_key) &&
+      ((item as dynamic).containsKey(_key) as bool) &&
       _valueMatcher.matches((item as dynamic)[_key], matchState);
 
   @override
diff --git a/pkgs/matcher/lib/src/operator_matchers.dart b/pkgs/matcher/lib/src/operator_matchers.dart
index 15e50ff..18f9537 100644
--- a/pkgs/matcher/lib/src/operator_matchers.dart
+++ b/pkgs/matcher/lib/src/operator_matchers.dart
@@ -59,7 +59,7 @@
       Map matchState, bool verbose) {
     var matcher = matchState['matcher'] as Matcher;
     matcher.describeMismatch(
-        item, mismatchDescription, matchState['state'], verbose);
+        item, mismatchDescription, matchState['state'] as Map, verbose);
     return mismatchDescription;
   }
 
diff --git a/pkgs/matcher/lib/src/order_matchers.dart b/pkgs/matcher/lib/src/order_matchers.dart
index 6fe7c76..7dc9e4f 100644
--- a/pkgs/matcher/lib/src/order_matchers.dart
+++ b/pkgs/matcher/lib/src/order_matchers.dart
@@ -78,9 +78,9 @@
   bool matches(Object? item, Map matchState) {
     if (item == _value) {
       return _equalValue;
-    } else if ((item as dynamic) < _value) {
+    } else if ((item as dynamic) < _value as bool) {
       return _lessThanValue;
-    } else if ((item as dynamic) > _value) {
+    } else if ((item as dynamic) > _value as bool) {
       return _greaterThanValue;
     } else {
       return false;
diff --git a/pkgs/matcher/lib/src/string_matchers.dart b/pkgs/matcher/lib/src/string_matchers.dart
index b819fa5..4e7dda5 100644
--- a/pkgs/matcher/lib/src/string_matchers.dart
+++ b/pkgs/matcher/lib/src/string_matchers.dart
@@ -61,7 +61,7 @@
       description.addDescriptionOf(_matchValue).add(' ignoring whitespace');
 
   @override
-  Description describeTypedMismatch(dynamic item,
+  Description describeTypedMismatch(String item,
       Description mismatchDescription, Map matchState, bool verbose) {
     return mismatchDescription
         .add('is ')
@@ -152,7 +152,7 @@
                 : throw ArgumentError('matches requires a regexp or string');
 
   @override
-  bool typedMatches(dynamic item, Map matchState) => _regexp.hasMatch(item);
+  bool typedMatches(String item, Map matchState) => _regexp.hasMatch(item);
 
   @override
   Description describe(Description description) =>
diff --git a/pkgs/matcher/lib/src/util.dart b/pkgs/matcher/lib/src/util.dart
index af0ba2c..511831e 100644
--- a/pkgs/matcher/lib/src/util.dart
+++ b/pkgs/matcher/lib/src/util.dart
@@ -43,8 +43,7 @@
   } 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));
+    return predicate((a) => (valueOrMatcher as dynamic)(a) as bool);
   } else {
     return equals(valueOrMatcher);
   }
diff --git a/pkgs/matcher/pubspec.yaml b/pkgs/matcher/pubspec.yaml
index 237e559..d29e1ab 100644
--- a/pkgs/matcher/pubspec.yaml
+++ b/pkgs/matcher/pubspec.yaml
@@ -6,7 +6,7 @@
 repository: https://github.com/dart-lang/test/tree/master/pkgs/matcher
 
 environment:
-  sdk: ^3.4.0
+  sdk: ^3.5.0
 
 dependencies:
   async: ^2.10.0
@@ -16,8 +16,8 @@
   test_api: ">=0.5.0 <0.8.0"
 
 dev_dependencies:
+  dart_flutter_team_lints: ^3.2.0
   fake_async: ^1.3.0
-  lints: ^3.0.0
   test: ^1.23.0
 
 dependency_overrides:
diff --git a/pkgs/matcher/test/core_matchers_test.dart b/pkgs/matcher/test/core_matchers_test.dart
index 04fc8b3..b20f31f 100644
--- a/pkgs/matcher/test/core_matchers_test.dart
+++ b/pkgs/matcher/test/core_matchers_test.dart
@@ -3,7 +3,7 @@
 // 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 'package:test/test.dart' show group, test;
 
 import 'test_utils.dart';
 
@@ -42,22 +42,22 @@
   });
 
   test('same', () {
-    var a = {};
-    var b = {};
+    var a = <Object?, Object?>{};
+    var b = <Object?, Object?>{};
     shouldPass(a, same(a));
     shouldFail(b, same(a), 'Expected: same instance as {} Actual: {}');
   });
 
   test('equals', () {
-    var a = {};
-    var b = {};
+    var a = <Object?, Object?>{};
+    var b = <Object?, Object?>{};
     shouldPass(a, equals(a));
     shouldPass(a, equals(b));
   });
 
   test('equals with null', () {
     Object? a; // null
-    var b = {};
+    var b = <Object?, Object?>{};
     shouldPass(a, equals(a));
     shouldFail(
         a, equals(b), 'Expected: {} Actual: <null> Which: expected a map');
@@ -87,7 +87,7 @@
   });
 
   test('anything', () {
-    var a = {};
+    var a = <Object?, Object?>{};
     shouldPass(0, anything);
     shouldPass(null, anything);
     shouldPass(a, anything);
@@ -107,8 +107,8 @@
   });
 
   test('hasLength', () {
-    var a = {};
-    var b = [];
+    var a = <Object?, Object?>{};
+    var b = <Object?>[];
     shouldPass(a, hasLength(0));
     shouldPass(b, hasLength(0));
     shouldPass('a', hasLength(1));
@@ -173,13 +173,13 @@
       ['foo', 'bar'],
       ['foo'],
       3,
-      []
+      <Object?>[]
     ];
     var expected1 = [
       ['foo', 'bar'],
       ['foo'],
       4,
-      []
+      <Object?>[]
     ];
     var reason1 = "Expected: [['foo', 'bar'], ['foo'], 4, []] "
         "Actual: [['foo', 'bar'], ['foo'], 3, []] "
@@ -189,13 +189,13 @@
       ['foo', 'barry'],
       ['foo'],
       4,
-      []
+      <Object?>[]
     ];
     var expected2 = [
       ['foo', 'bar'],
       ['foo'],
       4,
-      []
+      <Object?>[]
     ];
     var reason2 = "Expected: [['foo', 'bar'], ['foo'], 4, []] "
         "Actual: [['foo', 'barry'], ['foo'], 4, []] "
diff --git a/pkgs/matcher/test/expect_test.dart b/pkgs/matcher/test/expect_test.dart
index e2ef497..70ce579 100644
--- a/pkgs/matcher/test/expect_test.dart
+++ b/pkgs/matcher/test/expect_test.dart
@@ -18,7 +18,7 @@
     });
 
     test('contains an async error', () {
-      expect(expectLater(Future.error('oh no'), completion(isFalse)),
+      expect(expectLater(Future<Never>.error('oh no'), completion(isFalse)),
           throwsA('oh no'));
     });
   });
diff --git a/pkgs/matcher/test/having_test.dart b/pkgs/matcher/test/having_test.dart
index ddada77..26592cd 100644
--- a/pkgs/matcher/test/having_test.dart
+++ b/pkgs/matcher/test/having_test.dart
@@ -5,7 +5,7 @@
 // ignore_for_file: lines_longer_than_80_chars
 
 import 'package:matcher/matcher.dart';
-import 'package:test/test.dart' show test, expect, throwsA, group;
+import 'package:test/test.dart' show expect, group, test, throwsA;
 
 import 'test_utils.dart';
 
diff --git a/pkgs/matcher/test/iterable_matchers_test.dart b/pkgs/matcher/test/iterable_matchers_test.dart
index 7607d18..3cd78d7 100644
--- a/pkgs/matcher/test/iterable_matchers_test.dart
+++ b/pkgs/matcher/test/iterable_matchers_test.dart
@@ -83,13 +83,13 @@
     var d = [
       ['foo', 'bar'],
       ['foo'],
-      []
+      <Object?>[]
     ];
     var e = [
       ['foo', 'bar'],
       ['foo'],
       3,
-      []
+      <Object?>[]
     ];
     shouldPass(d, everyElement(anyOf(isEmpty, contains('foo'))));
     shouldFail(
@@ -196,7 +196,7 @@
           equals(2), // 2
           allOf([lessThan(3), isNot(0)]), // 1
           equals(0), // 0
-          predicate((int v) => v % 2 == 1), // 3
+          predicate((int v) => v.isOdd), // 3
           equals(5), // 5
         ]));
     shouldFail(
diff --git a/pkgs/matcher/test/map_matchers_test.dart b/pkgs/matcher/test/map_matchers_test.dart
index 4c699ab..f7dbafe 100644
--- a/pkgs/matcher/test/map_matchers_test.dart
+++ b/pkgs/matcher/test/map_matchers_test.dart
@@ -1,5 +1,5 @@
 import 'package:matcher/matcher.dart'
-    show contains, containsValue, containsPair;
+    show contains, containsPair, containsValue;
 import 'package:test/test.dart' show test;
 
 import 'test_utils.dart';
diff --git a/pkgs/matcher/test/matcher/completion_test.dart b/pkgs/matcher/test/matcher/completion_test.dart
index 9259cd1..ef8288e 100644
--- a/pkgs/matcher/test/matcher/completion_test.dart
+++ b/pkgs/matcher/test/matcher/completion_test.dart
@@ -20,13 +20,13 @@
     });
 
     test('succeeds when a future does not complete', () {
-      var completer = Completer();
+      var completer = Completer<Never>();
       expect(completer.future, doesNotComplete);
     });
 
     test('fails when a future does complete', () async {
       var monitor = await TestCaseMonitor.run(() {
-        var completer = Completer();
+        var completer = Completer<void>();
         completer.complete(null);
         expect(completer.future, doesNotComplete);
       });
@@ -39,7 +39,7 @@
 
     test('fails when a future completes after the expect', () async {
       var monitor = await TestCaseMonitor.run(() {
-        var completer = Completer();
+        var completer = Completer<void>();
         expect(completer.future, doesNotComplete);
         completer.complete(null);
       });
@@ -52,7 +52,7 @@
 
     test('fails when a future eventually completes', () async {
       var monitor = await TestCaseMonitor.run(() {
-        var completer = Completer();
+        var completer = Completer<void>();
         expect(completer.future, doesNotComplete);
         Future(() async {
           await pumpEventQueue(times: 10);
@@ -80,7 +80,7 @@
 
     test('with an error', () async {
       var monitor = await TestCaseMonitor.run(() {
-        expect(Future.error('X'), completes);
+        expect(Future<Never>.error('X'), completes);
       });
 
       expect(monitor.state, equals(State.failed));
@@ -89,7 +89,7 @@
 
     test('with a failure', () async {
       var monitor = await TestCaseMonitor.run(() {
-        expect(Future.error(TestFailure('oh no')), completes);
+        expect(Future<Never>.error(TestFailure('oh no')), completes);
       });
 
       expectTestFailed(monitor, 'oh no');
@@ -127,7 +127,7 @@
 
     test('with an error', () async {
       var monitor = await TestCaseMonitor.run(() {
-        expect(Future.error('X'), completion(isNull));
+        expect(Future<Never>.error('X'), completion(isNull));
       });
 
       expect(monitor.state, equals(State.failed));
@@ -136,7 +136,7 @@
 
     test('with a failure', () async {
       var monitor = await TestCaseMonitor.run(() {
-        expect(Future.error(TestFailure('oh no')), completion(isNull));
+        expect(Future<Never>.error(TestFailure('oh no')), completion(isNull));
       });
 
       expectTestFailed(monitor, 'oh no');
@@ -175,7 +175,7 @@
     });
 
     test("blocks expectLater's Future", () async {
-      var completer = Completer();
+      var completer = Completer<int>();
       var fired = false;
       unawaited(expectLater(completer.future, completion(equals(1))).then((_) {
         fired = true;
diff --git a/pkgs/matcher/test/matcher/prints_test.dart b/pkgs/matcher/test/matcher/prints_test.dart
index cbdb12a..a681413 100644
--- a/pkgs/matcher/test/matcher/prints_test.dart
+++ b/pkgs/matcher/test/matcher/prints_test.dart
@@ -158,7 +158,7 @@
     });
 
     test('describes a failure with no text nicely', () async {
-      void local() => Future.value();
+      void local() => Future<void>.value();
       var monitor = await TestCaseMonitor.run(() {
         expect(local, prints(contains('Goodbye')));
       });
@@ -186,7 +186,7 @@
     });
 
     test("blocks expectLater's Future", () async {
-      var completer = Completer();
+      var completer = Completer<void>();
       var fired = false;
 
       unawaited(expectLater(() {
diff --git a/pkgs/matcher/test/matcher/throws_test.dart b/pkgs/matcher/test/matcher/throws_test.dart
index 25b93a9..ff401b0 100644
--- a/pkgs/matcher/test/matcher/throws_test.dart
+++ b/pkgs/matcher/test/matcher/throws_test.dart
@@ -115,13 +115,13 @@
     group('[throws]', () {
       test('with a Future that throws an error', () {
         // ignore: deprecated_member_use_from_same_package
-        expect(Future.error('oh no'), throws);
+        expect(Future<Never>.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);
+          expect(Future<void>.value(), throws);
         });
 
         expectTestFailed(
@@ -136,7 +136,7 @@
 
       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);
+        expect(() => Future<Never>.error('oh no'), throws);
       });
 
       test("with a closure that returns a Future that doesn't throw", () async {
@@ -173,17 +173,17 @@
 
     group('[throwsA]', () {
       test('with a Future that throws an identical error', () {
-        expect(Future.error('oh no'), throwsA('oh no'));
+        expect(Future<Never>.error('oh no'), throwsA('oh no'));
       });
 
       test('with a Future that throws a matching error', () {
-        expect(Future.error(const FormatException('bad')),
+        expect(Future<Never>.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'));
+          expect(Future<void>.value(), throwsA('oh no'));
         });
 
         expectTestFailed(
@@ -198,7 +198,7 @@
 
       test('with a Future that throws the wrong error', () async {
         var monitor = await TestCaseMonitor.run(() {
-          expect(Future.error('aw dang'), throwsA('oh no'));
+          expect(Future<Never>.error('aw dang'), throwsA('oh no'));
         });
 
         expectTestFailed(
@@ -213,7 +213,7 @@
 
       test('with a closure that returns a Future that throws a matching error',
           () {
-        expect(() => Future.error(const FormatException('bad')),
+        expect(() => Future<Never>.error(const FormatException('bad')),
             throwsA(isFormatException));
       });
 
@@ -235,7 +235,7 @@
       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'));
+          expect(() => Future<Never>.error('aw dang'), throwsA('oh no'));
         });
 
         expectTestFailed(
@@ -264,7 +264,7 @@
       });
 
       test("blocks expectLater's Future", () async {
-        var completer = Completer();
+        var completer = Completer<Never>();
         var fired = false;
         unawaited(expectLater(completer.future, throwsArgumentError).then((_) {
           fired = true;
diff --git a/pkgs/matcher/test/mirror_matchers_test.dart b/pkgs/matcher/test/mirror_matchers_test.dart
index 06f0df4..18a9e8d 100644
--- a/pkgs/matcher/test/mirror_matchers_test.dart
+++ b/pkgs/matcher/test/mirror_matchers_test.dart
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 
 // ignore_for_file: deprecated_member_use_from_same_package
+// ignore_for_file: unreachable_from_main
 
 @TestOn('vm')
 library;
diff --git a/pkgs/matcher/test/never_called_test.dart b/pkgs/matcher/test/never_called_test.dart
index 4c83e39..39e09da 100644
--- a/pkgs/matcher/test/never_called_test.dart
+++ b/pkgs/matcher/test/never_called_test.dart
@@ -15,7 +15,7 @@
 
   test("doesn't throw if it isn't called", () async {
     var monitor = await TestCaseMonitor.run(() {
-      const Stream.empty().listen(neverCalled);
+      const Stream<Never>.empty().listen(neverCalled);
     });
 
     expectTestPassed(monitor);
diff --git a/pkgs/matcher/test/operator_matchers_test.dart b/pkgs/matcher/test/operator_matchers_test.dart
index f4b6d3a..46151e0 100644
--- a/pkgs/matcher/test/operator_matchers_test.dart
+++ b/pkgs/matcher/test/operator_matchers_test.dart
@@ -3,7 +3,7 @@
 // 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 'package:test/test.dart' show expect, test, throwsArgumentError;
 
 import 'test_utils.dart';
 
diff --git a/pkgs/matcher/test/pretty_print_test.dart b/pkgs/matcher/test/pretty_print_test.dart
index 184704b..62f3ab1 100644
--- a/pkgs/matcher/test/pretty_print_test.dart
+++ b/pkgs/matcher/test/pretty_print_test.dart
@@ -6,7 +6,7 @@
 
 import 'package:matcher/matcher.dart';
 import 'package:matcher/src/pretty_print.dart';
-import 'package:test/test.dart' show group, test, expect;
+import 'package:test/test.dart' show expect, group, test;
 
 class DefaultToString {}
 
diff --git a/pkgs/matcher/test/stream_matcher_test.dart b/pkgs/matcher/test/stream_matcher_test.dart
index c4af666..66e79c6 100644
--- a/pkgs/matcher/test/stream_matcher_test.dart
+++ b/pkgs/matcher/test/stream_matcher_test.dart
@@ -71,7 +71,7 @@
 
     test('rejects an empty stream', () {
       expect(
-          expectLater(const Stream.empty(), emits(1)),
+          expectLater(const Stream<Never>.empty(), emits(1)),
           throwsTestFailure(allOf([
             startsWith('Expected: should emit an event that <1>\n'),
             endsWith('   Which: emitted x Stream closed.\n')
@@ -96,7 +96,7 @@
 
   group('emitsDone', () {
     test('succeeds for an empty stream', () {
-      expect(const Stream.empty(), emitsDone);
+      expect(const Stream<Never>.empty(), emitsDone);
     });
 
     test('fails for a stream with events', () {
diff --git a/pkgs/matcher/test/string_matchers_test.dart b/pkgs/matcher/test/string_matchers_test.dart
index be9e768..0820015 100644
--- a/pkgs/matcher/test/string_matchers_test.dart
+++ b/pkgs/matcher/test/string_matchers_test.dart
@@ -3,7 +3,7 @@
 // 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 'package:test/test.dart' show expect, test;
 
 import 'test_utils.dart';
 
diff --git a/pkgs/matcher/test/type_matcher_test.dart b/pkgs/matcher/test/type_matcher_test.dart
index 99d4459..e0ca3d8 100644
--- a/pkgs/matcher/test/type_matcher_test.dart
+++ b/pkgs/matcher/test/type_matcher_test.dart
@@ -4,13 +4,13 @@
 
 // ignore_for_file: deprecated_member_use_from_same_package
 import 'package:matcher/matcher.dart';
-import 'package:test/test.dart' show test, group;
+import 'package:test/test.dart' show group, test;
 
 import 'test_utils.dart';
 
 void main() {
-  _test(isMap, {}, name: 'Map');
-  _test(isList, [], name: 'List');
+  _test(isMap, <Object?, Object?>{}, name: 'Map');
+  _test(isList, <Object?>[], name: 'List');
   _test(isArgumentError, ArgumentError());
   _test(isCastError, TypeError());
   _test<Exception>(isException, const FormatException());
diff --git a/pkgs/test/CHANGELOG.md b/pkgs/test/CHANGELOG.md
index afe8dec..ed12636 100644
--- a/pkgs/test/CHANGELOG.md
+++ b/pkgs/test/CHANGELOG.md
@@ -1,3 +1,11 @@
+## 1.25.14
+
+* Use secure random for url secrets.
+
+## 1.25.13
+
+* Allow the latest version of `package:matcher`.
+
 ## 1.25.12
 
 * Fix hang when running multiple precompiled browser tests.
diff --git a/pkgs/test/lib/src/util/math.dart b/pkgs/test/lib/src/util/math.dart
index d6bbbc7..f0c340c 100644
--- a/pkgs/test/lib/src/util/math.dart
+++ b/pkgs/test/lib/src/util/math.dart
@@ -2,19 +2,11 @@
 // for 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:math' as math;
 
-final _rand = math.Random();
+final _rand = math.Random.secure();
 
-/// 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';
+/// Returns a random 64 bit token suitable as a url secret.
+String randomUrlSecret() =>
+    base64Url.encode(List.generate(8, (_) => _rand.nextInt(256)));
diff --git a/pkgs/test/pubspec.yaml b/pkgs/test/pubspec.yaml
index 0ab65ad..1c79652 100644
--- a/pkgs/test/pubspec.yaml
+++ b/pkgs/test/pubspec.yaml
@@ -1,5 +1,5 @@
 name: test
-version: 1.25.12
+version: 1.25.14
 description: >-
   A full featured library for writing and running Dart tests across platforms.
 repository: https://github.com/dart-lang/test/tree/master/pkgs/test
@@ -20,7 +20,7 @@
 
   # 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'
+  matcher: '>=0.12.16 <0.12.18'
 
   node_preamble: ^2.0.0
   package_config: ^2.0.0
diff --git a/pkgs/test_process/.gitignore b/pkgs/test_process/.gitignore
new file mode 100644
index 0000000..0659a33
--- /dev/null
+++ b/pkgs/test_process/.gitignore
@@ -0,0 +1,9 @@
+.buildlog
+.DS_Store
+.idea
+.settings/
+build/
+packages
+.packages
+pubspec.lock
+.dart_tool/
diff --git a/pkgs/test_process/AUTHORS b/pkgs/test_process/AUTHORS
new file mode 100644
index 0000000..e8063a8
--- /dev/null
+++ b/pkgs/test_process/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/test_process/CHANGELOG.md b/pkgs/test_process/CHANGELOG.md
new file mode 100644
index 0000000..b267290
--- /dev/null
+++ b/pkgs/test_process/CHANGELOG.md
@@ -0,0 +1,67 @@
+## 2.1.1
+
+* Require Dart 3.1.
+* Move to `dart-lang/test` monorepo.
+
+## 2.1.0
+
+- Remove the expectation that the process exits during the normal test body.
+  The process will still be killed during teardown if it has not exited. The
+  check can be manually restored with `shouldExit()`.
+
+## 2.0.3
+
+- Populate the pubspec `repository` field.
+- Fixed examples in `readme.md`.
+- Added `example/example.dart`
+- Require Dart >=2.17
+
+## 2.0.2
+
+- Reverted `meta` constraint to `^1.3.0`.
+
+## 2.0.1
+
+- Update `meta` constraint to `>=1.3.0 <3.0.0`.
+
+## 2.0.0
+
+- Migrate to null safety.
+
+## 1.0.6
+
+- Require Dart >=2.1
+
+## 1.0.5
+
+- Don't allow the test to time out as long as the process is emitting output.
+
+## 1.0.4
+
+- Set max SDK version to `<3.0.0`, and adjust other dependencies.
+
+## 1.0.3
+
+- Support test `1.x.x`.
+
+## 1.0.2
+
+- Update SDK version to 2.0.0-dev.17.0
+
+## 1.0.1
+
+- Declare support for `async` 2.0.0.
+
+## 1.0.0
+
+- Added `pid` and `exitCode` getters to `TestProcess`.
+
+## 1.0.0-rc.2
+
+- Subclassed `TestProcess`es now emit log output based on the superclass's
+  standard IO streams rather than the subclass's. This matches the documented
+  behavior.
+
+## 1.0.0-rc.1
+
+- Initial release candidate.
diff --git a/pkgs/test_process/LICENSE b/pkgs/test_process/LICENSE
new file mode 100644
index 0000000..aa86769
--- /dev/null
+++ b/pkgs/test_process/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2017, 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_process/README.md b/pkgs/test_process/README.md
new file mode 100644
index 0000000..0d4e5f5
--- /dev/null
+++ b/pkgs/test_process/README.md
@@ -0,0 +1,123 @@
+[![Build Status](https://github.com/dart-lang/test/actions/workflows/test_process.yaml/badge.svg)](https://github.com/dart-lang/test/actions/workflows/test_process.yaml)
+[![pub package](https://img.shields.io/pub/v/test_process.svg)](https://pub.dev/packages/test_process)
+[![package publisher](https://img.shields.io/pub/publisher/test_process.svg)](https://pub.dev/packages/test_process/publisher)
+
+A package for testing subprocesses.
+
+This exposes a [`TestProcess`][TestProcess] class that wraps `dart:io`'s
+[`Process`][Process] class and makes it easy to read standard output
+line-by-line. `TestProcess` works the same as `Process` in many ways, but there
+are a few major differences.
+
+[TestProcess]: https://pub.dev/documentation/test_process/latest/test_process/TestProcess-class.html
+[Process]: https://api.dart.dev/stable/dart-io/Process-class.html
+
+## Standard Output
+
+`Process.stdout` and `Process.stderr` are binary streams, which is the most
+general API but isn't the most helpful when working with a program that produces
+plain text. Instead, [`TestProcess.stdout`][stdout] and
+[`TestProcess.stderr`][stderr] emit a string for each line of output the process
+produces. What's more, they're [`StreamQueue`][StreamQueue]s, which means
+they provide a *pull-based API*. For example:
+
+[stdout]: https://pub.dev/documentation/test_process/latest/test_process/TestProcess/stdout.html
+[stderr]: https://pub.dev/documentation/test_process/latest/test_process/TestProcess/stderr.html
+[StreamQueue]: https://pub.dev/documentation/async/latest/async/StreamQueue-class.html
+
+```dart
+import 'package:test/test.dart';
+import 'package:test_process/test_process.dart';
+
+void main() {
+  test('pub get gets dependencies', () async {
+    // TestProcess.start() works just like Process.start() from dart:io.
+    var process = await TestProcess.start('dart', ['pub', 'get']);
+
+    // StreamQueue.next returns the next line emitted on standard out.
+    var firstLine = await process.stdout.next;
+    expect(firstLine, equals('Resolving dependencies...'));
+
+    // Each call to StreamQueue.next moves one line further.
+    String next;
+    do {
+      next = await process.stdout.next;
+    } while (next != 'Got dependencies!');
+
+    // Assert that the process exits with code 0.
+    await process.shouldExit(0);
+  });
+}
+```
+
+The `test` package's [stream matchers][] have built-in support for
+`StreamQueues`, which makes them perfect for making assertions about a process's
+output. We can use this to clean up the previous example:
+
+[stream matchers]: https://github.com/dart-lang/test#stream-matchers
+
+```dart
+import 'package:test/test.dart';
+import 'package:test_process/test_process.dart';
+
+void main() {
+  test('pub get gets dependencies', () async {
+    var process = await TestProcess.start('dart', ['pub', 'get']);
+
+    // Each stream matcher will consume as many lines as it matches from a
+    // StreamQueue, and no more, so it's safe to use them in sequence.
+    await expectLater(process.stdout, emits('Resolving dependencies...'));
+
+    // The emitsThrough matcher matches and consumes any number of lines, as
+    // long as they end with one matching the argument.
+    await expectLater(process.stdout, emitsThrough('Got dependencies!'));
+
+    await process.shouldExit(0);
+  });
+}
+```
+
+If you want to access the standard output streams without consuming any values
+from the queues, you can use the [`stdoutStream()`][stdoutStream] and
+[`stderrStream()`][stderrStream] methods. Each time you call one of these, it
+produces an entirely new stream that replays the corresponding output stream
+from the beginning, regardless of what's already been produced by `stdout`,
+`stderr`, or other calls to the stream method.
+
+[stdoutStream]: https://pub.dev/documentation/test_process/latest/test_process/TestProcess/stdoutStream.html
+[stderrStream]: https://pub.dev/documentation/test_process/latest/test_process/TestProcess/stderrStream.html
+
+## Signals and Termination
+
+The way signaling works is different from `dart:io` as well. `TestProcess` still
+has a [`kill()`][kill] method, but it defaults to `SIGKILL` on Mac OS and Linux
+to ensure (as best as possible) that processes die without leaving behind
+zombies. If you want to send a particular signal (which is unsupported on
+Windows), you can do so by explicitly calling [`signal()`][signal].
+
+[kill]: https://pub.dev/documentation/test_process/latest/test_process/TestProcess/kill.html
+[signal]: https://pub.dev/documentation/test_process/latest/test_process/TestProcess/signal.html
+
+In addition to [`exitCode`][exitCode], which works the same as in `dart:io`,
+`TestProcess` also adds a new method named [`shouldExit()`][shouldExit]. This
+lets tests wait for a process to exit, and (if desired) assert what particular
+exit code it produced.
+
+[exitCode]: https://pub.dev/documentation/test_process/latest/test_process/TestProcess/exitCode.html
+[shouldExit]: https://pub.dev/documentation/test_process/latest/test_process/TestProcess/shouldExit.html
+
+## Debugging Output
+
+When a test using `TestProcess` fails, it will print all the output produced by
+that process. This makes it much easier to figure out what went wrong and why.
+The debugging output uses a header based on the process's invocation by
+default, but you can pass in custom `description` parameters to
+[`TestProcess.start()`][start] to control the headers.
+
+[start]: https://pub.dev/documentation/test_process/latest/test_process/TestProcess/start.html
+
+`TestProcess` will also produce debugging output as the test runs if you pass
+`forwardStdio: true` to `TestProcess.start()`. This can be particularly useful
+when you're using an interactive debugger and you want to figure out what a
+process is doing before the test finishes and the normal debugging output is
+printed.
diff --git a/pkgs/test_process/analysis_options.yaml b/pkgs/test_process/analysis_options.yaml
new file mode 100644
index 0000000..5607754
--- /dev/null
+++ b/pkgs/test_process/analysis_options.yaml
@@ -0,0 +1,18 @@
+# https://dart.dev/tools/analysis#the-analysis-options-file
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+  language:
+    strict-casts: true
+    strict-inference: true
+    strict-raw-types: true
+
+linter:
+  rules:
+    - avoid_unused_constructor_parameters
+    - cancel_subscriptions
+    - literal_only_boolean_expressions
+    - missing_whitespace_between_adjacent_strings
+    - no_adjacent_strings_in_list
+    - no_runtimeType_toString
+    - unnecessary_await_in_return
diff --git a/pkgs/test_process/example/example.dart b/pkgs/test_process/example/example.dart
new file mode 100644
index 0000000..22175f4
--- /dev/null
+++ b/pkgs/test_process/example/example.dart
@@ -0,0 +1,26 @@
+// 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:test/test.dart';
+import 'package:test_process/test_process.dart';
+
+void main() {
+  test('pub get gets dependencies', () async {
+    // TestProcess.start() works just like Process.start() from dart:io.
+    var process = await TestProcess.start('dart', ['pub', 'get']);
+
+    // StreamQueue.next returns the next line emitted on standard out.
+    var firstLine = await process.stdout.next;
+    expect(firstLine, equals('Resolving dependencies...'));
+
+    // Each call to StreamQueue.next moves one line further.
+    String next;
+    do {
+      next = await process.stdout.next;
+    } while (next != 'Got dependencies!');
+
+    // Assert that the process exits with code 0.
+    await process.shouldExit(0);
+  });
+}
diff --git a/pkgs/test_process/lib/test_process.dart b/pkgs/test_process/lib/test_process.dart
new file mode 100644
index 0000000..0441fb1
--- /dev/null
+++ b/pkgs/test_process/lib/test_process.dart
@@ -0,0 +1,239 @@
+// 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:convert';
+import 'dart:io';
+
+import 'package:async/async.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+
+/// A wrapper for [Process] that provides a convenient API for testing its
+/// standard IO and interacting with it from a test.
+///
+/// If the test fails, this will automatically print out any stdout and stderr
+/// from the process to aid debugging.
+///
+/// This may be extended to provide custom implementations of [stdoutStream] and
+/// [stderrStream]. These will automatically be picked up by the [stdout] and
+/// [stderr] queues, but the debug log will still contain the original output.
+class TestProcess {
+  /// The underlying process.
+  final Process _process;
+
+  /// A human-friendly description of this process.
+  final String description;
+
+  /// A [StreamQueue] that emits each line of stdout from the process.
+  ///
+  /// A copy of the underlying stream can be retrieved using [stdoutStream].
+  late final StreamQueue<String> stdout = StreamQueue(stdoutStream());
+
+  /// A [StreamQueue] that emits each line of stderr from the process.
+  ///
+  /// A copy of the underlying stream can be retrieved using [stderrStream].
+  late final StreamQueue<String> stderr = StreamQueue(stderrStream());
+
+  /// A splitter that can emit new copies of [stdout].
+  final StreamSplitter<String> _stdoutSplitter;
+
+  /// A splitter that can emit new copies of [stderr].
+  final StreamSplitter<String> _stderrSplitter;
+
+  /// The standard input sink for this process.
+  IOSink get stdin => _process.stdin;
+
+  /// A buffer of mixed stdout and stderr lines.
+  final List<String> _log = <String>[];
+
+  /// Whether [_log] has been passed to [printOnFailure] yet.
+  bool _loggedOutput = false;
+
+  /// Returns a [Future] which completes to the exit code of the process, once
+  /// it completes.
+  Future<int> get exitCode => _process.exitCode;
+
+  /// The process ID of the process.
+  int get pid => _process.pid;
+
+  /// Completes to [_process]'s exit code if it's exited, otherwise completes to
+  /// `null` immediately.
+  Future<int?> get _exitCodeOrNull => exitCode
+      .then<int?>((value) => value)
+      .timeout(Duration.zero, onTimeout: () => null);
+
+  /// Starts a process.
+  ///
+  /// [executable], [arguments], [workingDirectory], and [environment] have the
+  /// same meaning as for [Process.start].
+  ///
+  /// [description] is a string description of this process; it defaults to the
+  /// command-line invocation. [encoding] is the [Encoding] that will be used
+  /// for the process's input and output; it defaults to [utf8].
+  ///
+  /// If [forwardStdio] is `true`, the process's stdout and stderr will be
+  /// printed to the console as they appear. This is only intended to be set
+  /// temporarily to help when debugging test failures.
+  static Future<TestProcess> start(
+      String executable, Iterable<String> arguments,
+      {String? workingDirectory,
+      Map<String, String>? environment,
+      bool includeParentEnvironment = true,
+      bool runInShell = false,
+      String? description,
+      Encoding encoding = utf8,
+      bool forwardStdio = false}) async {
+    var process = await Process.start(executable, arguments.toList(),
+        workingDirectory: workingDirectory,
+        environment: environment,
+        includeParentEnvironment: includeParentEnvironment,
+        runInShell: runInShell);
+
+    if (description == null) {
+      var humanExecutable = p.isWithin(p.current, executable)
+          ? p.relative(executable)
+          : executable;
+      description = "$humanExecutable ${arguments.join(" ")}";
+    }
+
+    return TestProcess(process, description,
+        encoding: encoding, forwardStdio: forwardStdio);
+  }
+
+  /// Creates a [TestProcess] for [process].
+  ///
+  /// The [description], [encoding], and [forwardStdio] are the same as those to
+  /// [start].
+  ///
+  /// This is protected, which means it should only be called by subclasses.
+  @protected
+  TestProcess(Process process, this.description,
+      {Encoding encoding = utf8, bool forwardStdio = false})
+      : _process = process,
+        _stdoutSplitter = StreamSplitter(process.stdout
+            .transform(encoding.decoder)
+            .transform(const LineSplitter())),
+        _stderrSplitter = StreamSplitter(process.stderr
+            .transform(encoding.decoder)
+            .transform(const LineSplitter())) {
+    addTearDown(_tearDown);
+
+    _process.exitCode.whenComplete(_logOutput);
+
+    // Listen eagerly so that the lines are interleaved properly between the two
+    // streams.
+    //
+    // Call [split] explicitly because we don't want to log overridden
+    // [stdoutStream] or [stderrStream] output.
+    _stdoutSplitter.split().listen((line) {
+      _heartbeat();
+      if (forwardStdio) print(line);
+      _log.add('    $line');
+    });
+
+    _stderrSplitter.split().listen((line) {
+      _heartbeat();
+      if (forwardStdio) print(line);
+      _log.add('[e] $line');
+    });
+  }
+
+  /// A callback that's run when the test completes.
+  Future<void> _tearDown() async {
+    // If the process is already dead, do nothing.
+    if (await _exitCodeOrNull != null) return;
+
+    _process.kill(ProcessSignal.sigkill);
+
+    // Log output now rather than waiting for the exitCode callback so that
+    // it's visible even if we time out waiting for the process to die.
+    await _logOutput();
+  }
+
+  /// Formats the contents of [_log] and passes them to [printOnFailure].
+  Future<void> _logOutput() async {
+    if (_loggedOutput) return;
+    _loggedOutput = true;
+
+    var exitCodeOrNull = await _exitCodeOrNull;
+
+    // Wait a timer tick to ensure that all available lines have been flushed to
+    // [_log].
+    await Future<void>.delayed(Duration.zero);
+
+    var buffer = StringBuffer();
+    buffer.write('Process `$description` ');
+    if (exitCodeOrNull == null) {
+      buffer.writeln('was killed with SIGKILL in a tear-down. Output:');
+    } else {
+      buffer.writeln('exited with exitCode $exitCodeOrNull. Output:');
+    }
+
+    buffer.writeln(_log.join('\n'));
+    printOnFailure(buffer.toString());
+  }
+
+  /// Returns a copy of [stdout] as a single-subscriber stream.
+  ///
+  /// Each time this is called, it will return a separate copy that will start
+  /// from the beginning of the process.
+  ///
+  /// This can be overridden by subclasses to return a derived standard output
+  /// stream. This stream will then be used for [stdout].
+  Stream<String> stdoutStream() => _stdoutSplitter.split();
+
+  /// Returns a copy of [stderr] as a single-subscriber stream.
+  ///
+  /// Each time this is called, it will return a separate copy that will start
+  /// from the beginning of the process.
+  ///
+  /// This can be overridden by subclasses to return a derived standard output
+  /// stream. This stream will then be used for [stderr].
+  Stream<String> stderrStream() => _stderrSplitter.split();
+
+  /// Sends [signal] to the process.
+  ///
+  /// This is meant for sending specific signals. If you just want to kill the
+  /// process, use [kill] instead.
+  ///
+  /// Throws an [UnsupportedError] on Windows.
+  void signal(ProcessSignal signal) {
+    if (Platform.isWindows) {
+      throw UnsupportedError(
+          "TestProcess.signal() isn't supported on Windows.");
+    }
+
+    _process.kill(signal);
+  }
+
+  /// Kills the process (with SIGKILL on POSIX operating systems), and returns a
+  /// future that completes once it's dead.
+  ///
+  /// If this is called after the process is already dead, it does nothing.
+  Future<void> kill() async {
+    _process.kill(ProcessSignal.sigkill);
+    await exitCode;
+  }
+
+  /// Waits for the process to exit, and verifies that the exit code matches
+  /// [expectedExitCode] (if given).
+  ///
+  /// If this is called after the process is already dead, it verifies its
+  /// existing exit code.
+  Future<void> shouldExit([Object? expectedExitCode]) async {
+    var exitCode = await this.exitCode;
+    if (expectedExitCode == null) return;
+    expect(exitCode, expectedExitCode,
+        reason: 'Process `$description` had an unexpected exit code.');
+  }
+
+  /// Signal to the test runner that the test is still making progress and
+  /// shouldn't time out.
+  void _heartbeat() {
+    // Interacting with the test runner's asynchronous expectation logic will
+    // notify it that the test is alive.
+    expectAsync0(() {})();
+  }
+}
diff --git a/pkgs/test_process/pubspec.yaml b/pkgs/test_process/pubspec.yaml
new file mode 100644
index 0000000..60158d4
--- /dev/null
+++ b/pkgs/test_process/pubspec.yaml
@@ -0,0 +1,18 @@
+name: test_process
+version: 2.1.1
+description: |
+  Test processes: starting; validating stdout and stderr; checking exit code
+repository: https://github.com/dart-lang/test/tree/master/pkgs/test_process
+
+environment:
+  sdk: ^3.1.0
+
+dependencies:
+  async: ^2.5.0
+  meta: ^1.3.0
+  path: ^1.8.0
+  test: ^1.16.6
+
+dev_dependencies:
+  dart_flutter_team_lints: ^3.0.0
+  test_descriptor: ^2.0.0
diff --git a/pkgs/test_process/test/test_process_test.dart b/pkgs/test_process/test/test_process_test.dart
new file mode 100644
index 0000000..9cfb779
--- /dev/null
+++ b/pkgs/test_process/test/test_process_test.dart
@@ -0,0 +1,136 @@
+// 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: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';
+
+final throwsTestFailure = throwsA(isA<TestFailure>());
+
+void main() {
+  group('shouldExit()', () {
+    test('succeeds when the process exits with the given exit code', () async {
+      var process = await startDartProcess('exitCode = 42;');
+      expect(process.exitCode, completion(equals(42)));
+      await process.shouldExit(greaterThan(12));
+    });
+
+    test('fails when the process exits with a different exit code', () async {
+      var process = await startDartProcess('exitCode = 1;');
+      expect(process.exitCode, completion(equals(1)));
+      expect(process.shouldExit(greaterThan(12)), throwsTestFailure);
+    });
+
+    test('allows any exit code without an assertion', () async {
+      var process = await startDartProcess('exitCode = 1;');
+      expect(process.exitCode, completion(equals(1)));
+      await process.shouldExit();
+    });
+  });
+
+  test('kill() stops the process', () async {
+    var process = await startDartProcess('while (true);');
+
+    // Should terminate.
+    await process.kill();
+  });
+
+  group('stdout and stderr', () {
+    test("expose the process's standard io", () async {
+      var process = await startDartProcess(r'''
+        print("hello");
+        stderr.writeln("hi");
+        print("\nworld");
+      ''');
+
+      expect(process.stdout, emitsInOrder(['hello', '', 'world', emitsDone]));
+      expect(process.stderr, emitsInOrder(['hi', emitsDone]));
+      await process.shouldExit(0);
+    });
+
+    test('close when the process exits', () async {
+      var process = await startDartProcess('');
+      expect(expectLater(process.stdout, emits('hello')), throwsTestFailure);
+      expect(expectLater(process.stderr, emits('world')), throwsTestFailure);
+      await process.shouldExit(0);
+    });
+  });
+
+  test("stdoutStream() and stderrStream() copy the process's standard io",
+      () async {
+    var process = await startDartProcess(r'''
+      print("hello");
+      stderr.writeln("hi");
+      print("\nworld");
+    ''');
+
+    expect(process.stdoutStream(),
+        emitsInOrder(['hello', '', 'world', emitsDone]));
+    expect(process.stdoutStream(),
+        emitsInOrder(['hello', '', 'world', emitsDone]));
+
+    expect(process.stderrStream(), emitsInOrder(['hi', emitsDone]));
+    expect(process.stderrStream(), emitsInOrder(['hi', emitsDone]));
+
+    await process.shouldExit(0);
+
+    expect(process.stdoutStream(),
+        emitsInOrder(['hello', '', 'world', emitsDone]));
+    expect(process.stderrStream(), emitsInOrder(['hi', emitsDone]));
+  });
+
+  test('stdin writes to the process', () async {
+    var process = await startDartProcess(r'''
+      stdinLines.listen((line) => print("> $line"));
+    ''');
+
+    process.stdin.writeln('hello');
+    await expectLater(process.stdout, emits('> hello'));
+    process.stdin.writeln('world');
+    await expectLater(process.stdout, emits('> world'));
+    await process.kill();
+  });
+
+  test('signal sends a signal to the process', () async {
+    var process = await startDartProcess(r'''
+      ProcessSignal.sighup.watch().listen((_) => print("HUP"));
+      print("ready");
+    ''');
+
+    await expectLater(process.stdout, emits('ready'));
+    process.signal(ProcessSignal.sighup);
+    await expectLater(process.stdout, emits('HUP'));
+    await process.kill();
+  }, testOn: '!windows');
+
+  test('allows a long-running process', () async {
+    await startDartProcess(r'''
+      await Future.delayed(Duration(minutes: 10));
+    ''');
+    // Test should not time out.
+  });
+}
+
+/// Starts a Dart process running [script] in a main method.
+Future<TestProcess> startDartProcess(String script) {
+  var dartPath = p.join(d.sandbox, 'test.dart');
+  File(dartPath).writeAsStringSync('''
+    import 'dart:async';
+    import 'dart:convert';
+    import 'dart:io';
+
+    var stdinLines = stdin
+        .transform(utf8.decoder)
+        .transform(new LineSplitter());
+
+    void main() {
+      $script
+    }
+  ''');
+
+  return TestProcess.start(Platform.executable, ['--enable-asserts', dartPath]);
+}