├── .github ├── dependabot.yml └── workflows │ └── test-package.yml ├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── example └── example.dart ├── lib └── test_process.dart ├── pubspec.yaml └── test └── test_process_test.dart /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Set update schedule for GitHub Actions 2 | # See https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/keeping-your-actions-up-to-date-with-dependabot 3 | 4 | version: 2 5 | updates: 6 | 7 | - package-ecosystem: github-actions 8 | directory: / 9 | schedule: 10 | interval: monthly 11 | labels: 12 | - autosubmit 13 | groups: 14 | github-actions: 15 | patterns: 16 | - "*" 17 | -------------------------------------------------------------------------------- /.github/workflows/test-package.yml: -------------------------------------------------------------------------------- 1 | name: Dart CI 2 | 3 | on: 4 | # Run on PRs and pushes to the default branch. 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | schedule: 10 | - cron: "0 0 * * 0" 11 | 12 | env: 13 | PUB_ENVIRONMENT: bot.github 14 | 15 | jobs: 16 | # Check code formatting and static analysis on a single OS (linux) 17 | # against Dart dev. 18 | analyze: 19 | runs-on: ubuntu-latest 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | sdk: [dev] 24 | steps: 25 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 26 | - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 27 | with: 28 | sdk: ${{ matrix.sdk }} 29 | - id: install 30 | name: Install dependencies 31 | run: dart pub get 32 | - name: Check formatting 33 | run: dart format --output=none --set-exit-if-changed . 34 | if: always() && steps.install.outcome == 'success' 35 | - name: Analyze code 36 | run: dart analyze --fatal-infos 37 | if: always() && steps.install.outcome == 'success' 38 | 39 | # Run tests on a matrix consisting of two dimensions: 40 | # 1. OS: ubuntu-latest, (macos-latest, windows-latest) 41 | # 2. release: dev 42 | test: 43 | needs: analyze 44 | runs-on: ${{ matrix.os }} 45 | strategy: 46 | fail-fast: false 47 | matrix: 48 | # Add macos-latest and/or windows-latest if relevant for this package. 49 | os: [ubuntu-latest] 50 | sdk: [3.1, dev] 51 | steps: 52 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 53 | - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 54 | with: 55 | sdk: ${{ matrix.sdk }} 56 | - id: install 57 | name: Install dependencies 58 | run: dart pub get 59 | - name: Run VM tests 60 | run: dart test --platform vm 61 | if: always() && steps.install.outcome == 'success' 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .buildlog 2 | .DS_Store 3 | .idea 4 | .settings/ 5 | build/ 6 | packages 7 | .packages 8 | pubspec.lock 9 | .dart_tool/ 10 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Google Inc. 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.1.1-wip 2 | 3 | * Require Dart 3.1. 4 | 5 | ## 2.1.0 6 | 7 | - Remove the expectation that the process exits during the normal test body. 8 | The process will still be killed during teardown if it has not exited. The 9 | check can be manually restored with `shouldExit()`. 10 | 11 | ## 2.0.3 12 | 13 | - Populate the pubspec `repository` field. 14 | - Fixed examples in `readme.md`. 15 | - Added `example/example.dart` 16 | - Require Dart >=2.17 17 | 18 | ## 2.0.2 19 | 20 | - Reverted `meta` constraint to `^1.3.0`. 21 | 22 | ## 2.0.1 23 | 24 | - Update `meta` constraint to `>=1.3.0 <3.0.0`. 25 | 26 | ## 2.0.0 27 | 28 | - Migrate to null safety. 29 | 30 | ## 1.0.6 31 | 32 | - Require Dart >=2.1 33 | 34 | ## 1.0.5 35 | 36 | - Don't allow the test to time out as long as the process is emitting output. 37 | 38 | ## 1.0.4 39 | 40 | - Set max SDK version to `<3.0.0`, and adjust other dependencies. 41 | 42 | ## 1.0.3 43 | 44 | - Support test `1.x.x`. 45 | 46 | ## 1.0.2 47 | 48 | - Update SDK version to 2.0.0-dev.17.0 49 | 50 | ## 1.0.1 51 | 52 | - Declare support for `async` 2.0.0. 53 | 54 | ## 1.0.0 55 | 56 | - Added `pid` and `exitCode` getters to `TestProcess`. 57 | 58 | ## 1.0.0-rc.2 59 | 60 | - Subclassed `TestProcess`es now emit log output based on the superclass's 61 | standard IO streams rather than the subclass's. This matches the documented 62 | behavior. 63 | 64 | ## 1.0.0-rc.1 65 | 66 | - Initial release candidate. 67 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at 2 | the end). 3 | 4 | ### Before you contribute 5 | Before we can use your code, you must sign the 6 | [Google Individual Contributor License Agreement](https://cla.developers.google.com/about/google-individual) 7 | (CLA), which you can do online. The CLA is necessary mainly because you own the 8 | copyright to your changes, even after your contribution becomes part of our 9 | codebase, so we need your permission to use and distribute your code. We also 10 | need to be sure of various other things—for instance that you'll tell us if you 11 | know that your code infringes on other people's patents. You don't have to sign 12 | the CLA until after you've submitted your code for review and a member has 13 | approved it, but you must do it before we can put your code into our codebase. 14 | 15 | Before you start working on a larger contribution, you should get in touch with 16 | us first through the issue tracker with your idea so that we can help out and 17 | possibly guide you. Coordinating up front makes it much easier to avoid 18 | frustration later on. 19 | 20 | ### Code reviews 21 | All submissions, including submissions by project members, require review. 22 | 23 | ### File headers 24 | All files in the project must start with the following header. 25 | 26 | // Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file 27 | // for details. All rights reserved. Use of this source code is governed by a 28 | // BSD-style license that can be found in the LICENSE file. 29 | 30 | ### The small print 31 | Contributions made by corporations are covered by a different agreement than the 32 | one above, the 33 | [Software Grant and Corporate Contributor License Agreement](https://developers.google.com/open-source/cla/corporate). 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017, the Dart project authors. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following 11 | disclaimer in the documentation and/or other materials provided 12 | with the distribution. 13 | * Neither the name of Google LLC nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > This repo has moved to https://github.com/dart-lang/test/tree/master/pkgs/test_process 3 | 4 | [![Dart CI](https://github.com/dart-lang/test_process/actions/workflows/test-package.yml/badge.svg)](https://github.com/dart-lang/test_process/actions/workflows/test-package.yml) 5 | [![pub package](https://img.shields.io/pub/v/test_process.svg)](https://pub.dev/packages/test_process) 6 | [![package publisher](https://img.shields.io/pub/publisher/test_process.svg)](https://pub.dev/packages/test_process/publisher) 7 | 8 | A package for testing subprocesses. 9 | 10 | This exposes a [`TestProcess`][TestProcess] class that wraps `dart:io`'s 11 | [`Process`][Process] class and makes it easy to read standard output 12 | line-by-line. `TestProcess` works the same as `Process` in many ways, but there 13 | are a few major differences. 14 | 15 | [TestProcess]: https://pub.dev/documentation/test_process/latest/test_process/TestProcess-class.html 16 | [Process]: https://api.dart.dev/stable/dart-io/Process-class.html 17 | 18 | ## Standard Output 19 | 20 | `Process.stdout` and `Process.stderr` are binary streams, which is the most 21 | general API but isn't the most helpful when working with a program that produces 22 | plain text. Instead, [`TestProcess.stdout`][stdout] and 23 | [`TestProcess.stderr`][stderr] emit a string for each line of output the process 24 | produces. What's more, they're [`StreamQueue`][StreamQueue]s, which means 25 | they provide a *pull-based API*. For example: 26 | 27 | [stdout]: https://pub.dev/documentation/test_process/latest/test_process/TestProcess/stdout.html 28 | [stderr]: https://pub.dev/documentation/test_process/latest/test_process/TestProcess/stderr.html 29 | [StreamQueue]: https://pub.dev/documentation/async/latest/async/StreamQueue-class.html 30 | 31 | ```dart 32 | import 'package:test/test.dart'; 33 | import 'package:test_process/test_process.dart'; 34 | 35 | void main() { 36 | test('pub get gets dependencies', () async { 37 | // TestProcess.start() works just like Process.start() from dart:io. 38 | var process = await TestProcess.start('dart', ['pub', 'get']); 39 | 40 | // StreamQueue.next returns the next line emitted on standard out. 41 | var firstLine = await process.stdout.next; 42 | expect(firstLine, equals('Resolving dependencies...')); 43 | 44 | // Each call to StreamQueue.next moves one line further. 45 | String next; 46 | do { 47 | next = await process.stdout.next; 48 | } while (next != 'Got dependencies!'); 49 | 50 | // Assert that the process exits with code 0. 51 | await process.shouldExit(0); 52 | }); 53 | } 54 | ``` 55 | 56 | The `test` package's [stream matchers][] have built-in support for 57 | `StreamQueues`, which makes them perfect for making assertions about a process's 58 | output. We can use this to clean up the previous example: 59 | 60 | [stream matchers]: https://github.com/dart-lang/test#stream-matchers 61 | 62 | ```dart 63 | import 'package:test/test.dart'; 64 | import 'package:test_process/test_process.dart'; 65 | 66 | void main() { 67 | test('pub get gets dependencies', () async { 68 | var process = await TestProcess.start('dart', ['pub', 'get']); 69 | 70 | // Each stream matcher will consume as many lines as it matches from a 71 | // StreamQueue, and no more, so it's safe to use them in sequence. 72 | await expectLater(process.stdout, emits('Resolving dependencies...')); 73 | 74 | // The emitsThrough matcher matches and consumes any number of lines, as 75 | // long as they end with one matching the argument. 76 | await expectLater(process.stdout, emitsThrough('Got dependencies!')); 77 | 78 | await process.shouldExit(0); 79 | }); 80 | } 81 | ``` 82 | 83 | If you want to access the standard output streams without consuming any values 84 | from the queues, you can use the [`stdoutStream()`][stdoutStream] and 85 | [`stderrStream()`][stderrStream] methods. Each time you call one of these, it 86 | produces an entirely new stream that replays the corresponding output stream 87 | from the beginning, regardless of what's already been produced by `stdout`, 88 | `stderr`, or other calls to the stream method. 89 | 90 | [stdoutStream]: https://pub.dev/documentation/test_process/latest/test_process/TestProcess/stdoutStream.html 91 | [stderrStream]: https://pub.dev/documentation/test_process/latest/test_process/TestProcess/stderrStream.html 92 | 93 | ## Signals and Termination 94 | 95 | The way signaling works is different from `dart:io` as well. `TestProcess` still 96 | has a [`kill()`][kill] method, but it defaults to `SIGKILL` on Mac OS and Linux 97 | to ensure (as best as possible) that processes die without leaving behind 98 | zombies. If you want to send a particular signal (which is unsupported on 99 | Windows), you can do so by explicitly calling [`signal()`][signal]. 100 | 101 | [kill]: https://pub.dev/documentation/test_process/latest/test_process/TestProcess/kill.html 102 | [signal]: https://pub.dev/documentation/test_process/latest/test_process/TestProcess/signal.html 103 | 104 | In addition to [`exitCode`][exitCode], which works the same as in `dart:io`, 105 | `TestProcess` also adds a new method named [`shouldExit()`][shouldExit]. This 106 | lets tests wait for a process to exit, and (if desired) assert what particular 107 | exit code it produced. 108 | 109 | [exitCode]: https://pub.dev/documentation/test_process/latest/test_process/TestProcess/exitCode.html 110 | [shouldExit]: https://pub.dev/documentation/test_process/latest/test_process/TestProcess/shouldExit.html 111 | 112 | ## Debugging Output 113 | 114 | When a test using `TestProcess` fails, it will print all the output produced by 115 | that process. This makes it much easier to figure out what went wrong and why. 116 | The debugging output uses a header based on the process's invocation by 117 | default, but you can pass in custom `description` parameters to 118 | [`TestProcess.start()`][start] to control the headers. 119 | 120 | [start]: https://pub.dev/documentation/test_process/latest/test_process/TestProcess/start.html 121 | 122 | `TestProcess` will also produce debugging output as the test runs if you pass 123 | `forwardStdio: true` to `TestProcess.start()`. This can be particularly useful 124 | when you're using an interactive debugger and you want to figure out what a 125 | process is doing before the test finishes and the normal debugging output is 126 | printed. 127 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # https://dart.dev/tools/analysis#the-analysis-options-file 2 | include: package:dart_flutter_team_lints/analysis_options.yaml 3 | 4 | analyzer: 5 | language: 6 | strict-casts: true 7 | strict-inference: true 8 | strict-raw-types: true 9 | 10 | linter: 11 | rules: 12 | - avoid_unused_constructor_parameters 13 | - cancel_subscriptions 14 | - literal_only_boolean_expressions 15 | - missing_whitespace_between_adjacent_strings 16 | - no_adjacent_strings_in_list 17 | - no_runtimeType_toString 18 | - unnecessary_await_in_return 19 | -------------------------------------------------------------------------------- /example/example.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'package:test/test.dart'; 6 | import 'package:test_process/test_process.dart'; 7 | 8 | void main() { 9 | test('pub get gets dependencies', () async { 10 | // TestProcess.start() works just like Process.start() from dart:io. 11 | var process = await TestProcess.start('dart', ['pub', 'get']); 12 | 13 | // StreamQueue.next returns the next line emitted on standard out. 14 | var firstLine = await process.stdout.next; 15 | expect(firstLine, equals('Resolving dependencies...')); 16 | 17 | // Each call to StreamQueue.next moves one line further. 18 | String next; 19 | do { 20 | next = await process.stdout.next; 21 | } while (next != 'Got dependencies!'); 22 | 23 | // Assert that the process exits with code 0. 24 | await process.shouldExit(0); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /lib/test_process.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:convert'; 6 | import 'dart:io'; 7 | 8 | import 'package:async/async.dart'; 9 | import 'package:meta/meta.dart'; 10 | import 'package:path/path.dart' as p; 11 | import 'package:test/test.dart'; 12 | 13 | /// A wrapper for [Process] that provides a convenient API for testing its 14 | /// standard IO and interacting with it from a test. 15 | /// 16 | /// If the test fails, this will automatically print out any stdout and stderr 17 | /// from the process to aid debugging. 18 | /// 19 | /// This may be extended to provide custom implementations of [stdoutStream] and 20 | /// [stderrStream]. These will automatically be picked up by the [stdout] and 21 | /// [stderr] queues, but the debug log will still contain the original output. 22 | class TestProcess { 23 | /// The underlying process. 24 | final Process _process; 25 | 26 | /// A human-friendly description of this process. 27 | final String description; 28 | 29 | /// A [StreamQueue] that emits each line of stdout from the process. 30 | /// 31 | /// A copy of the underlying stream can be retrieved using [stdoutStream]. 32 | late final StreamQueue stdout = StreamQueue(stdoutStream()); 33 | 34 | /// A [StreamQueue] that emits each line of stderr from the process. 35 | /// 36 | /// A copy of the underlying stream can be retrieved using [stderrStream]. 37 | late final StreamQueue stderr = StreamQueue(stderrStream()); 38 | 39 | /// A splitter that can emit new copies of [stdout]. 40 | final StreamSplitter _stdoutSplitter; 41 | 42 | /// A splitter that can emit new copies of [stderr]. 43 | final StreamSplitter _stderrSplitter; 44 | 45 | /// The standard input sink for this process. 46 | IOSink get stdin => _process.stdin; 47 | 48 | /// A buffer of mixed stdout and stderr lines. 49 | final List _log = []; 50 | 51 | /// Whether [_log] has been passed to [printOnFailure] yet. 52 | bool _loggedOutput = false; 53 | 54 | /// Returns a [Future] which completes to the exit code of the process, once 55 | /// it completes. 56 | Future get exitCode => _process.exitCode; 57 | 58 | /// The process ID of the process. 59 | int get pid => _process.pid; 60 | 61 | /// Completes to [_process]'s exit code if it's exited, otherwise completes to 62 | /// `null` immediately. 63 | Future get _exitCodeOrNull => exitCode 64 | .then((value) => value) 65 | .timeout(Duration.zero, onTimeout: () => null); 66 | 67 | /// Starts a process. 68 | /// 69 | /// [executable], [arguments], [workingDirectory], and [environment] have the 70 | /// same meaning as for [Process.start]. 71 | /// 72 | /// [description] is a string description of this process; it defaults to the 73 | /// command-line invocation. [encoding] is the [Encoding] that will be used 74 | /// for the process's input and output; it defaults to [utf8]. 75 | /// 76 | /// If [forwardStdio] is `true`, the process's stdout and stderr will be 77 | /// printed to the console as they appear. This is only intended to be set 78 | /// temporarily to help when debugging test failures. 79 | static Future start( 80 | String executable, Iterable arguments, 81 | {String? workingDirectory, 82 | Map? environment, 83 | bool includeParentEnvironment = true, 84 | bool runInShell = false, 85 | String? description, 86 | Encoding encoding = utf8, 87 | bool forwardStdio = false}) async { 88 | var process = await Process.start(executable, arguments.toList(), 89 | workingDirectory: workingDirectory, 90 | environment: environment, 91 | includeParentEnvironment: includeParentEnvironment, 92 | runInShell: runInShell); 93 | 94 | if (description == null) { 95 | var humanExecutable = p.isWithin(p.current, executable) 96 | ? p.relative(executable) 97 | : executable; 98 | description = "$humanExecutable ${arguments.join(" ")}"; 99 | } 100 | 101 | return TestProcess(process, description, 102 | encoding: encoding, forwardStdio: forwardStdio); 103 | } 104 | 105 | /// Creates a [TestProcess] for [process]. 106 | /// 107 | /// The [description], [encoding], and [forwardStdio] are the same as those to 108 | /// [start]. 109 | /// 110 | /// This is protected, which means it should only be called by subclasses. 111 | @protected 112 | TestProcess(Process process, this.description, 113 | {Encoding encoding = utf8, bool forwardStdio = false}) 114 | : _process = process, 115 | _stdoutSplitter = StreamSplitter(process.stdout 116 | .transform(encoding.decoder) 117 | .transform(const LineSplitter())), 118 | _stderrSplitter = StreamSplitter(process.stderr 119 | .transform(encoding.decoder) 120 | .transform(const LineSplitter())) { 121 | addTearDown(_tearDown); 122 | 123 | _process.exitCode.whenComplete(_logOutput); 124 | 125 | // Listen eagerly so that the lines are interleaved properly between the two 126 | // streams. 127 | // 128 | // Call [split] explicitly because we don't want to log overridden 129 | // [stdoutStream] or [stderrStream] output. 130 | _stdoutSplitter.split().listen((line) { 131 | _heartbeat(); 132 | if (forwardStdio) print(line); 133 | _log.add(' $line'); 134 | }); 135 | 136 | _stderrSplitter.split().listen((line) { 137 | _heartbeat(); 138 | if (forwardStdio) print(line); 139 | _log.add('[e] $line'); 140 | }); 141 | } 142 | 143 | /// A callback that's run when the test completes. 144 | Future _tearDown() async { 145 | // If the process is already dead, do nothing. 146 | if (await _exitCodeOrNull != null) return; 147 | 148 | _process.kill(ProcessSignal.sigkill); 149 | 150 | // Log output now rather than waiting for the exitCode callback so that 151 | // it's visible even if we time out waiting for the process to die. 152 | await _logOutput(); 153 | } 154 | 155 | /// Formats the contents of [_log] and passes them to [printOnFailure]. 156 | Future _logOutput() async { 157 | if (_loggedOutput) return; 158 | _loggedOutput = true; 159 | 160 | var exitCodeOrNull = await _exitCodeOrNull; 161 | 162 | // Wait a timer tick to ensure that all available lines have been flushed to 163 | // [_log]. 164 | await Future.delayed(Duration.zero); 165 | 166 | var buffer = StringBuffer(); 167 | buffer.write('Process `$description` '); 168 | if (exitCodeOrNull == null) { 169 | buffer.writeln('was killed with SIGKILL in a tear-down. Output:'); 170 | } else { 171 | buffer.writeln('exited with exitCode $exitCodeOrNull. Output:'); 172 | } 173 | 174 | buffer.writeln(_log.join('\n')); 175 | printOnFailure(buffer.toString()); 176 | } 177 | 178 | /// Returns a copy of [stdout] as a single-subscriber stream. 179 | /// 180 | /// Each time this is called, it will return a separate copy that will start 181 | /// from the beginning of the process. 182 | /// 183 | /// This can be overridden by subclasses to return a derived standard output 184 | /// stream. This stream will then be used for [stdout]. 185 | Stream stdoutStream() => _stdoutSplitter.split(); 186 | 187 | /// Returns a copy of [stderr] as a single-subscriber stream. 188 | /// 189 | /// Each time this is called, it will return a separate copy that will start 190 | /// from the beginning of the process. 191 | /// 192 | /// This can be overridden by subclasses to return a derived standard output 193 | /// stream. This stream will then be used for [stderr]. 194 | Stream stderrStream() => _stderrSplitter.split(); 195 | 196 | /// Sends [signal] to the process. 197 | /// 198 | /// This is meant for sending specific signals. If you just want to kill the 199 | /// process, use [kill] instead. 200 | /// 201 | /// Throws an [UnsupportedError] on Windows. 202 | void signal(ProcessSignal signal) { 203 | if (Platform.isWindows) { 204 | throw UnsupportedError( 205 | "TestProcess.signal() isn't supported on Windows."); 206 | } 207 | 208 | _process.kill(signal); 209 | } 210 | 211 | /// Kills the process (with SIGKILL on POSIX operating systems), and returns a 212 | /// future that completes once it's dead. 213 | /// 214 | /// If this is called after the process is already dead, it does nothing. 215 | Future kill() async { 216 | _process.kill(ProcessSignal.sigkill); 217 | await exitCode; 218 | } 219 | 220 | /// Waits for the process to exit, and verifies that the exit code matches 221 | /// [expectedExitCode] (if given). 222 | /// 223 | /// If this is called after the process is already dead, it verifies its 224 | /// existing exit code. 225 | Future shouldExit([Object? expectedExitCode]) async { 226 | var exitCode = await this.exitCode; 227 | if (expectedExitCode == null) return; 228 | expect(exitCode, expectedExitCode, 229 | reason: 'Process `$description` had an unexpected exit code.'); 230 | } 231 | 232 | /// Signal to the test runner that the test is still making progress and 233 | /// shouldn't time out. 234 | void _heartbeat() { 235 | // Interacting with the test runner's asynchronous expectation logic will 236 | // notify it that the test is alive. 237 | expectAsync0(() {})(); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: test_process 2 | version: 2.1.1-wip 3 | description: 4 | "Test processes: starting; validating stdout and stderr; checking exit code" 5 | repository: https://github.com/dart-lang/test_process 6 | 7 | environment: 8 | sdk: ^3.1.0 9 | 10 | dependencies: 11 | async: ^2.5.0 12 | meta: ^1.3.0 13 | path: ^1.8.0 14 | test: ^1.16.6 15 | 16 | dev_dependencies: 17 | dart_flutter_team_lints: ^3.0.0 18 | test_descriptor: ^2.0.0 19 | -------------------------------------------------------------------------------- /test/test_process_test.dart: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file 2 | // for details. All rights reserved. Use of this source code is governed by a 3 | // BSD-style license that can be found in the LICENSE file. 4 | 5 | import 'dart:io'; 6 | 7 | import 'package:path/path.dart' as p; 8 | import 'package:test/test.dart'; 9 | import 'package:test_descriptor/test_descriptor.dart' as d; 10 | import 'package:test_process/test_process.dart'; 11 | 12 | final throwsTestFailure = throwsA(isA()); 13 | 14 | void main() { 15 | group('shouldExit()', () { 16 | test('succeeds when the process exits with the given exit code', () async { 17 | var process = await startDartProcess('exitCode = 42;'); 18 | expect(process.exitCode, completion(equals(42))); 19 | await process.shouldExit(greaterThan(12)); 20 | }); 21 | 22 | test('fails when the process exits with a different exit code', () async { 23 | var process = await startDartProcess('exitCode = 1;'); 24 | expect(process.exitCode, completion(equals(1))); 25 | expect(process.shouldExit(greaterThan(12)), throwsTestFailure); 26 | }); 27 | 28 | test('allows any exit code without an assertion', () async { 29 | var process = await startDartProcess('exitCode = 1;'); 30 | expect(process.exitCode, completion(equals(1))); 31 | await process.shouldExit(); 32 | }); 33 | }); 34 | 35 | test('kill() stops the process', () async { 36 | var process = await startDartProcess('while (true);'); 37 | 38 | // Should terminate. 39 | await process.kill(); 40 | }); 41 | 42 | group('stdout and stderr', () { 43 | test("expose the process's standard io", () async { 44 | var process = await startDartProcess(r''' 45 | print("hello"); 46 | stderr.writeln("hi"); 47 | print("\nworld"); 48 | '''); 49 | 50 | expect(process.stdout, emitsInOrder(['hello', '', 'world', emitsDone])); 51 | expect(process.stderr, emitsInOrder(['hi', emitsDone])); 52 | await process.shouldExit(0); 53 | }); 54 | 55 | test('close when the process exits', () async { 56 | var process = await startDartProcess(''); 57 | expect(expectLater(process.stdout, emits('hello')), throwsTestFailure); 58 | expect(expectLater(process.stderr, emits('world')), throwsTestFailure); 59 | await process.shouldExit(0); 60 | }); 61 | }); 62 | 63 | test("stdoutStream() and stderrStream() copy the process's standard io", 64 | () async { 65 | var process = await startDartProcess(r''' 66 | print("hello"); 67 | stderr.writeln("hi"); 68 | print("\nworld"); 69 | '''); 70 | 71 | expect(process.stdoutStream(), 72 | emitsInOrder(['hello', '', 'world', emitsDone])); 73 | expect(process.stdoutStream(), 74 | emitsInOrder(['hello', '', 'world', emitsDone])); 75 | 76 | expect(process.stderrStream(), emitsInOrder(['hi', emitsDone])); 77 | expect(process.stderrStream(), emitsInOrder(['hi', emitsDone])); 78 | 79 | await process.shouldExit(0); 80 | 81 | expect(process.stdoutStream(), 82 | emitsInOrder(['hello', '', 'world', emitsDone])); 83 | expect(process.stderrStream(), emitsInOrder(['hi', emitsDone])); 84 | }); 85 | 86 | test('stdin writes to the process', () async { 87 | var process = await startDartProcess(r''' 88 | stdinLines.listen((line) => print("> $line")); 89 | '''); 90 | 91 | process.stdin.writeln('hello'); 92 | await expectLater(process.stdout, emits('> hello')); 93 | process.stdin.writeln('world'); 94 | await expectLater(process.stdout, emits('> world')); 95 | await process.kill(); 96 | }); 97 | 98 | test('signal sends a signal to the process', () async { 99 | var process = await startDartProcess(r''' 100 | ProcessSignal.sighup.watch().listen((_) => print("HUP")); 101 | print("ready"); 102 | '''); 103 | 104 | await expectLater(process.stdout, emits('ready')); 105 | process.signal(ProcessSignal.sighup); 106 | await expectLater(process.stdout, emits('HUP')); 107 | await process.kill(); 108 | }, testOn: '!windows'); 109 | 110 | test('allows a long-running process', () async { 111 | await startDartProcess(r''' 112 | await Future.delayed(Duration(minutes: 10)); 113 | '''); 114 | // Test should not time out. 115 | }); 116 | } 117 | 118 | /// Starts a Dart process running [script] in a main method. 119 | Future startDartProcess(String script) { 120 | var dartPath = p.join(d.sandbox, 'test.dart'); 121 | File(dartPath).writeAsStringSync(''' 122 | import 'dart:async'; 123 | import 'dart:convert'; 124 | import 'dart:io'; 125 | 126 | var stdinLines = stdin 127 | .transform(utf8.decoder) 128 | .transform(new LineSplitter()); 129 | 130 | void main() { 131 | $script 132 | } 133 | '''); 134 | 135 | return TestProcess.start(Platform.executable, ['--enable-asserts', dartPath]); 136 | } 137 | --------------------------------------------------------------------------------