diff --git a/.github/ISSUE_TEMPLATE/test_process.md b/.github/ISSUE_TEMPLATE/test_process.md new file mode 100644 index 000000000..9f492b8cb --- /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 000000000..2c0da6de0 --- /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 25b50aab7..5b80a2811 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,4 @@ literate API. | [test](pkgs/test/) | A full featured library for writing and running Dart tests across platforms. | [![pub package](https://img.shields.io/pub/v/test.svg)](https://pub.dev/packages/test) | | [test_api](pkgs/test_api/) | | [![pub package](https://img.shields.io/pub/v/test_api.svg)](https://pub.dev/packages/test_api) | | [test_core](pkgs/test_core/) | | [![pub package](https://img.shields.io/pub/v/test_core.svg)](https://pub.dev/packages/test_core) | +| [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/test_process/.gitignore b/pkgs/test_process/.gitignore new file mode 100644 index 000000000..0659a3398 --- /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 000000000..e8063a8cd --- /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 + +Google Inc. diff --git a/pkgs/test_process/CHANGELOG.md b/pkgs/test_process/CHANGELOG.md new file mode 100644 index 000000000..b2672905b --- /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 000000000..aa86769bc --- /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 000000000..0d4e5f510 --- /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 000000000..5607754d7 --- /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 000000000..22175f470 --- /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 000000000..0441fb114 --- /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 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 stderr = StreamQueue(stderrStream()); + + /// A splitter that can emit new copies of [stdout]. + final StreamSplitter _stdoutSplitter; + + /// A splitter that can emit new copies of [stderr]. + final StreamSplitter _stderrSplitter; + + /// The standard input sink for this process. + IOSink get stdin => _process.stdin; + + /// A buffer of mixed stdout and stderr lines. + final List _log = []; + + /// 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 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 get _exitCodeOrNull => exitCode + .then((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 start( + String executable, Iterable arguments, + {String? workingDirectory, + Map? 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 _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 _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.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 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 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 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 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 000000000..60158d461 --- /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 000000000..9cfb779a9 --- /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()); + +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 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]); +}