|
| 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:async'; |
| 6 | +import 'dart:convert'; |
| 7 | +import 'dart:io'; |
| 8 | + |
| 9 | +import 'package:async/async.dart'; |
| 10 | +import 'package:meta/meta.dart'; |
| 11 | +import 'package:path/path.dart' as p; |
| 12 | +import 'package:test/test.dart'; |
| 13 | + |
| 14 | +/// A wrapper for [Process] that provides a convenient API for testing its |
| 15 | +/// standard IO and interacting with it from a test. |
| 16 | +/// |
| 17 | +/// If the test fails, this will automatically print out any stdout and stderr |
| 18 | +/// from the process to aid debugging. |
| 19 | +/// |
| 20 | +/// This may be extended to provide custom implementations of [stdoutStream] and |
| 21 | +/// [stderrStream]. These will automatically be picked up by the [stdout] and |
| 22 | +/// [stderr] queues, but the debug log will still contain the original output. |
| 23 | +class TestProcess { |
| 24 | + /// The underlying process. |
| 25 | + final Process _process; |
| 26 | + |
| 27 | + /// A human-friendly description of this process. |
| 28 | + final String description; |
| 29 | + |
| 30 | + /// A [StreamQueue] that emits each line of stdout from the process. |
| 31 | + /// |
| 32 | + /// A copy of the underlying stream can be retreived using [stdoutStream]. |
| 33 | + StreamQueue<String> get stdout => _stdout; |
| 34 | + StreamQueue<String> _stdout; |
| 35 | + |
| 36 | + /// A [StreamQueue] that emits each line of stderr from the process. |
| 37 | + /// |
| 38 | + /// A copy of the underlying stream can be retreived using [stderrStream]. |
| 39 | + StreamQueue<String> get stderr => _stderr; |
| 40 | + StreamQueue<String> _stderr; |
| 41 | + |
| 42 | + /// A splitter that can emit new copies of [stdout]. |
| 43 | + final StreamSplitter<String> _stdoutSplitter; |
| 44 | + |
| 45 | + /// A splitter that can emit new copies of [stderr]. |
| 46 | + final StreamSplitter<String> _stderrSplitter; |
| 47 | + |
| 48 | + /// The standard input sink for this process. |
| 49 | + IOSink get stdin => _process.stdin; |
| 50 | + |
| 51 | + /// A buffer of mixed stdout and stderr lines. |
| 52 | + final _log = <String>[]; |
| 53 | + |
| 54 | + /// Whether [_log] has been passed to [printOnFailure] yet. |
| 55 | + bool _loggedOutput = false; |
| 56 | + |
| 57 | + /// Completes to [_process]'s exit code if it's exited, otherwise completes to |
| 58 | + /// `null` immediately. |
| 59 | + Future<int> get _exitCodeOrNull async => |
| 60 | + await _process.exitCode.timeout(Duration.ZERO, onTimeout: () => null); |
| 61 | + |
| 62 | + /// Starts a process. |
| 63 | + /// |
| 64 | + /// [executable], [arguments], [workingDirectory], and [environment] have the |
| 65 | + /// same meaning as for [Process.start]. |
| 66 | + /// |
| 67 | + /// [description] is a string description of this process; it defaults to the |
| 68 | + /// command-line invocation. [encoding] is the [Encoding] that will be used |
| 69 | + /// for the process's input and output; it defaults to [UTF8]. |
| 70 | + /// |
| 71 | + /// If [forwardStdio] is `true`, the process's stdout and stderr will be |
| 72 | + /// printed to the console as they appear. This is only intended to be set |
| 73 | + /// temporarily to help when debugging test failures. |
| 74 | + static Future<TestProcess> start( |
| 75 | + String executable, |
| 76 | + Iterable<String> arguments, |
| 77 | + {String workingDirectory, |
| 78 | + Map<String, String> environment, |
| 79 | + bool includeParentEnvironment: true, |
| 80 | + bool runInShell: false, |
| 81 | + String description, |
| 82 | + Encoding encoding, |
| 83 | + bool forwardStdio: false}) async { |
| 84 | + var process = await Process.start(executable, arguments.toList(), |
| 85 | + workingDirectory: workingDirectory, |
| 86 | + environment: environment, |
| 87 | + includeParentEnvironment: includeParentEnvironment, |
| 88 | + runInShell: runInShell); |
| 89 | + |
| 90 | + if (description == null) { |
| 91 | + var humanExecutable = p.isWithin(p.current, executable) |
| 92 | + ? p.relative(executable) |
| 93 | + : executable; |
| 94 | + description = "$humanExecutable ${arguments.join(" ")}"; |
| 95 | + } |
| 96 | + |
| 97 | + encoding ??= UTF8; |
| 98 | + return new TestProcess( |
| 99 | + process, description, encoding: encoding, forwardStdio: forwardStdio); |
| 100 | + } |
| 101 | + |
| 102 | + /// Creates a [TestProcess] for [process]. |
| 103 | + /// |
| 104 | + /// The [description], [encoding], and [forwardStdio] are the same as those to |
| 105 | + /// [start]. |
| 106 | + /// |
| 107 | + /// This is protected, which means it should only be called by subclasses. |
| 108 | + @protected |
| 109 | + TestProcess(Process process, this.description, {Encoding encoding, |
| 110 | + bool forwardStdio: false}) |
| 111 | + : _process = process, |
| 112 | + _stdoutSplitter = new StreamSplitter(process.stdout |
| 113 | + .transform(encoding.decoder).transform(const LineSplitter())), |
| 114 | + _stderrSplitter = new StreamSplitter(process.stderr |
| 115 | + .transform(encoding.decoder).transform(const LineSplitter())) { |
| 116 | + addTearDown(_tearDown); |
| 117 | + expect(_process.exitCode.then((_) => _logOutput()), completes, |
| 118 | + reason: "Process `$description` never exited."); |
| 119 | + |
| 120 | + _stdout = new StreamQueue(stdoutStream()); |
| 121 | + _stderr = new StreamQueue(stderrStream()); |
| 122 | + |
| 123 | + // Listen eagerly so that the lines are interleaved properly between the two |
| 124 | + // streams. |
| 125 | + stdoutStream().listen((line) { |
| 126 | + if (forwardStdio) print(line); |
| 127 | + _log.add(" $line"); |
| 128 | + }); |
| 129 | + |
| 130 | + stderrStream().listen((line) { |
| 131 | + if (forwardStdio) print(line); |
| 132 | + _log.add("[e] $line"); |
| 133 | + }); |
| 134 | + } |
| 135 | + |
| 136 | + /// A callback that's run when the test completes. |
| 137 | + Future _tearDown() async { |
| 138 | + // If the process is already dead, do nothing. |
| 139 | + if (await _exitCodeOrNull != null) return; |
| 140 | + |
| 141 | + _process.kill(ProcessSignal.SIGKILL); |
| 142 | + |
| 143 | + // Log output now rather than waiting for the exitCode callback so that |
| 144 | + // it's visible even if we time out waiting for the process to die. |
| 145 | + await _logOutput(); |
| 146 | + } |
| 147 | + |
| 148 | + /// Formats the contents of [_log] and passes them to [printOnFailure]. |
| 149 | + Future _logOutput() async { |
| 150 | + if (_loggedOutput) return; |
| 151 | + _loggedOutput = true; |
| 152 | + |
| 153 | + var exitCode = await _exitCodeOrNull; |
| 154 | + |
| 155 | + // Wait a timer tick to ensure that all available lines have been flushed to |
| 156 | + // [_log]. |
| 157 | + await new Future.delayed(Duration.ZERO); |
| 158 | + |
| 159 | + var buffer = new StringBuffer(); |
| 160 | + buffer.write("Process `$description` "); |
| 161 | + if ((await _exitCodeOrNull) == null) { |
| 162 | + buffer.writeln("was killed with SIGKILL in a tear-down. Output:"); |
| 163 | + } else { |
| 164 | + buffer.writeln("exited with exitCode $exitCode. Output:"); |
| 165 | + } |
| 166 | + |
| 167 | + buffer.writeln(_log.join("\n")); |
| 168 | + printOnFailure(buffer.toString()); |
| 169 | + } |
| 170 | + |
| 171 | + /// Returns a copy of [stdout] as a single-subscriber stream. |
| 172 | + /// |
| 173 | + /// Each time this is called, it will return a separate copy that will start |
| 174 | + /// from the beginning of the process. |
| 175 | + /// |
| 176 | + /// This can be overridden by subclasses to return a derived standard output |
| 177 | + /// stream. This stream will then be used for [stdout]. |
| 178 | + Stream<String> stdoutStream() => _stdoutSplitter.split(); |
| 179 | + |
| 180 | + /// Returns a copy of [stderr] as a single-subscriber stream. |
| 181 | + /// |
| 182 | + /// Each time this is called, it will return a separate copy that will start |
| 183 | + /// from the beginning of the process. |
| 184 | + /// |
| 185 | + /// This can be overridden by subclasses to return a derived standard output |
| 186 | + /// stream. This stream will then be used for [stderr]. |
| 187 | + Stream<String> stderrStream() => _stderrSplitter.split(); |
| 188 | + |
| 189 | + /// Sends [signal] to the process. |
| 190 | + /// |
| 191 | + /// This is meant for sending specific signals. If you just want to kill the |
| 192 | + /// process, use [kill] instead. |
| 193 | + /// |
| 194 | + /// Throws an [UnsupportedError] on Windows. |
| 195 | + void signal(ProcessSignal signal) { |
| 196 | + if (Platform.isWindows) { |
| 197 | + throw new UnsupportedError( |
| 198 | + "TestProcess.signal() isn't supported on Windows."); |
| 199 | + } |
| 200 | + |
| 201 | + _process.kill(signal); |
| 202 | + } |
| 203 | + |
| 204 | + /// Kills the process (with SIGKILL on POSIX operating systems), and returns a |
| 205 | + /// future that completes once it's dead. |
| 206 | + /// |
| 207 | + /// If this is called after the process is already dead, it does nothing. |
| 208 | + Future kill() async { |
| 209 | + _process.kill(ProcessSignal.SIGKILL); |
| 210 | + await _process.exitCode; |
| 211 | + } |
| 212 | + |
| 213 | + /// Waits for the process to exit, and verifies that the exit code matches |
| 214 | + /// [expectedExitCode] (if given). |
| 215 | + /// |
| 216 | + /// If this is called after the process is already dead, it verifies its |
| 217 | + /// existing exit code. |
| 218 | + Future shouldExit([expectedExitCode]) async { |
| 219 | + var exitCode = await _process.exitCode; |
| 220 | + if (expectedExitCode == null) return; |
| 221 | + expect(exitCode, expectedExitCode, |
| 222 | + reason: "Process `$description` had an unexpected exit code."); |
| 223 | + } |
| 224 | +} |
0 commit comments