Add the package implementation. (#1)

diff --git a/lib/test_process.dart b/lib/test_process.dart
new file mode 100644
index 0000000..b4f01c3
--- /dev/null
+++ b/lib/test_process.dart
@@ -0,0 +1,224 @@
+// 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:async';
+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 retreived using [stdoutStream].
+  StreamQueue<String> get stdout => _stdout;
+  StreamQueue<String> _stdout;
+
+  /// A [StreamQueue] that emits each line of stderr from the process.
+  ///
+  /// A copy of the underlying stream can be retreived using [stderrStream].
+  StreamQueue<String> get stderr => _stderr;
+  StreamQueue<String> _stderr;
+
+  /// 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 _log = <String>[];
+
+  /// Whether [_log] has been passed to [printOnFailure] yet.
+  bool _loggedOutput = false;
+
+  /// Completes to [_process]'s exit code if it's exited, otherwise completes to
+  /// `null` immediately.
+  Future<int> get _exitCodeOrNull async =>
+      await _process.exitCode.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,
+      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(" ")}";
+    }
+
+    encoding ??= UTF8;
+    return new 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,
+          bool forwardStdio: false})
+      : _process = process,
+        _stdoutSplitter = new StreamSplitter(process.stdout
+            .transform(encoding.decoder).transform(const LineSplitter())),
+        _stderrSplitter = new StreamSplitter(process.stderr
+            .transform(encoding.decoder).transform(const LineSplitter())) {
+    addTearDown(_tearDown);
+    expect(_process.exitCode.then((_) => _logOutput()), completes,
+        reason: "Process `$description` never exited.");
+
+    _stdout = new StreamQueue(stdoutStream());
+    _stderr = new StreamQueue(stderrStream());
+
+    // Listen eagerly so that the lines are interleaved properly between the two
+    // streams.
+    stdoutStream().listen((line) {
+      if (forwardStdio) print(line);
+      _log.add("    $line");
+    });
+
+    stderrStream().listen((line) {
+      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 exitCode = await _exitCodeOrNull;
+
+    // Wait a timer tick to ensure that all available lines have been flushed to
+    // [_log].
+    await new Future.delayed(Duration.ZERO);
+
+    var buffer = new StringBuffer();
+    buffer.write("Process `$description` ");
+    if ((await _exitCodeOrNull) == null) {
+      buffer.writeln("was killed with SIGKILL in a tear-down. Output:");
+    } else {
+      buffer.writeln("exited with exitCode $exitCode. 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 new 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 _process.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([expectedExitCode]) async {
+    var exitCode = await _process.exitCode;
+    if (expectedExitCode == null) return;
+    expect(exitCode, expectedExitCode,
+        reason: "Process `$description` had an unexpected exit code.");
+  }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 6ca6e3a..48f5b84 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: test_process
-version: 1.0.0-dev
+version: 1.0.0-rc.1
 description: A library for testing subprocesses.
 author: Dart Team <misc@dartlang.org>
 homepage: https://github.com/dart-lang/test_process
@@ -8,4 +8,10 @@
   sdk: '>=1.8.0 <2.0.0'
 
 dependencies:
+  async: "^1.12.0"
+  meta: ">=0.9.0 <2.0.0"
+  path: "^1.0.0"
   test: "^0.12.19"
+
+dev_dependencies:
+  test_descriptor: "^1.0.0"
diff --git a/test/test_process_test.dart b/test/test_process_test.dart
new file mode 100644
index 0000000..8acf2a6
--- /dev/null
+++ b/test/test_process_test.dart
@@ -0,0 +1,131 @@
+// 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:async';
+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(new isInstanceOf<TestFailure>());
+
+void main() {
+  group("shouldExit()", () {
+    test("succeeds when the process exits with the given exit code", () async {
+      var process = await startDartProcess('exitCode = 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.shouldExit(greaterThan(12)), throwsTestFailure);
+    });
+
+    test("allows any exit code without an assertion", () async {
+      var process = await startDartProcess('exitCode = 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'));
+    process.kill();
+  }, testOn: "!windows");
+}
+
+/// Starts a Dart process running [script] in a main method.
+Future<TestProcess> startDartProcess(String script) {
+  var dartPath = p.join(d.sandbox, 'test.dart');
+  new 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, ['--checked', dartPath]);
+}