Merge `package:test_process` (#2425)
- [x] Move and fix workflow files, labeler.yaml, and badges in the
README.md
- [x] Rev the version of the package, so that pub.dev points to the
correct site
- [x] Add a line to the changelog:
```
* Move to `dart-lang/test` monorepo.
```
- [x] Add the package to the top-level readme of the monorepo:
```
| [test_process](pkgs/test_process/) | Test processes: starting; validating stdout and stderr; checking exit code | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Atest_process) | [](https://pub.dev/packages/test_process) |
```
- [ ] **Important!** Merge the PR with 'Create a merge commit' (enabling
then disabling the `Allow merge commits` admin setting)
- [x] Update the auto-publishing settings on
https://pub.dev/packages/test_process/admin
- [x] Add the following text to
https://github.com/dart-lang/test_process/:'
```
> [!IMPORTANT]
> This repo has moved to https://github.com/dart-lang/test/tree/master/pkgs/test_process
```
- [ ] Publish using the autopublish workflow
- [ ] Push tags to GitHub using
```
git tag --list 'test_process*' | xargs git push origin
```
- [ ] Close open PRs in dart-lang/test_process with the following
message:
```
Closing as the [dart-lang/test_process](https://github.com/dart-lang/test_process) repository is merged into the [dart-lang/test](https://github.com/dart-lang/test) monorepo. Please re-open this PR there!
```
- [ ] Transfer issues by running
```
dart run pkgs/repo_manage/bin/report.dart transfer-issues --source-repo dart-lang/test_process --target-repo dart-lang/test --add-label package:test_process --apply-changes
```
- [ ] Archive https://github.com/dart-lang/test_process/
---
- [x] I’ve reviewed the contributor guide and applied the relevant
portions to this PR.
<details>
<summary>Contribution guidelines:</summary><br>
- See our [contributor
guide](https://github.com/dart-lang/.github/blob/main/CONTRIBUTING.md)
for general expectations for PRs.
- Larger or significant changes should be discussed in an issue before
creating a PR.
- Contributions to our repos should follow the [Dart style
guide](https://dart.dev/guides/language/effective-dart) and use `dart
format`.
- Most changes should add an entry to the changelog and may need to [rev
the pubspec package
version](https://github.com/dart-lang/sdk/blob/main/docs/External-Package-Maintenance.md#making-a-change).
- Changes to packages require [corresponding
tests](https://github.com/dart-lang/.github/blob/main/CONTRIBUTING.md#Testing).
Note that many Dart repos have a weekly cadence for reviewing PRs -
please allow for some latency before initial review feedback.
</details>
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/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 25b50aa..5b80a28 100644
--- a/README.md
+++ b/README.md
@@ -22,3 +22,4 @@
| [test](pkgs/test/) | A full featured library for writing and running Dart tests across platforms. | [](https://pub.dev/packages/test) |
| [test_api](pkgs/test_api/) | | [](https://pub.dev/packages/test_api) |
| [test_core](pkgs/test_core/) | | [](https://pub.dev/packages/test_core) |
+| [test_process](pkgs/test_process/) | Test processes: starting; validating stdout and stderr; checking exit code | [](https://pub.dev/packages/test_process) |
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 @@
+[](https://github.com/dart-lang/test/actions/workflows/test_process.yaml)
+[](https://pub.dev/packages/test_process)
+[](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]);
+}