Merge branch 'nullsafety' into nullsafety
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5cf9bc0..e20bfdc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Change Log for `process_runner`
 
+## 4.0.0-nullsafety.2
+
+* Rebase onto non-nullsafety version 3.1.0 to pick up those changes. 
+
 ## 4.0.0-nullsafety.1
 
 * Expand the sdk constraint to `<2.11.0`.
@@ -8,6 +12,14 @@
 
 * Convert to non-nullable by default, enable null-safety experiment for Dart.
 
+## 3.1.0
+
+* Add `exception` to the `WorkerJob` so that when commands fail to run, the
+  exception output can be seen.
+* Fixed a problem with the default output function where it didn't count
+  failed jobs as finished.
+* Removed dependency on mockito and args to match nullsafety version.
+
 ## 3.0.0
 
 * Breaking change to change the `result` given in the `ProcessRunnerException`
diff --git a/lib/src/process_pool.dart b/lib/src/process_pool.dart
index 1a57bb3..5997d0c 100644
--- a/lib/src/process_pool.dart
+++ b/lib/src/process_pool.dart
@@ -72,6 +72,10 @@
   /// same as the [ProcessPool.encoding] that was set on the pool.
   ProcessRunnerResult? result;
 
+  /// Once the job is complete, if it had an exception while running, this
+  /// member contains the exception.
+  Exception? exception;
+
   @override
   String toString() {
     return command.join(' ');
@@ -163,7 +167,7 @@
     int pending,
     int failed,
   ) {
-    final String percent = total == 0 ? '100' : ((100 * completed) ~/ total).toString().padLeft(3);
+    final String percent = total == 0 ? '100' : ((100 * (completed + failed)) ~/ total).toString().padLeft(3);
     final String completedStr = completed.toString().padLeft(3);
     final String totalStr = total.toString().padRight(3);
     final String inProgressStr = inProgress.toString().padLeft(2);
@@ -190,6 +194,7 @@
         stderr.writeln('\nJob $job failed: $e');
       }
       job.result = e.result;
+      job.exception = e;
       _failedJobs.add(job);
     } finally {
       _inProgressJobs--;
diff --git a/pubspec.yaml b/pubspec.yaml
index d6192d1..5bf9c18 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -3,7 +3,7 @@
 # found in the LICENSE file.
 
 name: process_runner
-version: 4.0.0-nullsafety.1
+version: 4.0.0-nullsafety.2
 description: A process invocation astraction for Dart that manages a multiprocess queue.
 homepage: https://github.com/google/process_runner
 
diff --git a/test/src/fake_process_manager.dart b/test/src/fake_process_manager.dart
index 6198907..7a01e35 100644
--- a/test/src/fake_process_manager.dart
+++ b/test/src/fake_process_manager.dart
@@ -20,12 +20,16 @@
 ///
 /// Call [verifyCalls] to verify that each desired call occurred.
 class FakeProcessManager implements ProcessManager {
-  FakeProcessManager(this.stdinResults);
+  FakeProcessManager(this.stdinResults, {this.commandsThrow = false});
 
   /// The callback that will be called each time stdin input is supplied to
   /// a call.
   final StringReceivedCallback stdinResults;
 
+  /// Set to true if all commands run with this process manager should throw an
+  /// exception.
+  final bool commandsThrow;
+
   /// The list of results that will be sent back, organized by the command line
   /// that will produce them. Each command line has a list of returned stdout
   /// output that will be returned on each successive call.
@@ -106,34 +110,49 @@
   }
 
   @override
-  Future<ProcessResult> run(List<dynamic> command,
-      {String? workingDirectory,
-      Map<String, String>? environment,
-      bool includeParentEnvironment = true,
-      bool runInShell = false,
-      Encoding stdoutEncoding = systemEncoding,
-      Encoding stderrEncoding = systemEncoding}) {
+  Future<ProcessResult> run(
+    List<dynamic> command, {
+    String? workingDirectory,
+    Map<String, String>? environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    Encoding stdoutEncoding = systemEncoding,
+    Encoding stderrEncoding = systemEncoding,
+  }) {
+    if (commandsThrow) {
+      throw const ProcessException('failed_executable', <String>[]);
+    }
     return _nextResult(command as List<String>);
   }
 
   @override
-  ProcessResult runSync(List<dynamic> command,
-      {String? workingDirectory,
-      Map<String, String>? environment,
-      bool includeParentEnvironment = true,
-      bool runInShell = false,
-      Encoding stdoutEncoding = systemEncoding,
-      Encoding stderrEncoding = systemEncoding}) {
+  ProcessResult runSync(
+    List<dynamic> command, {
+    String? workingDirectory,
+    Map<String, String>? environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    Encoding stdoutEncoding = systemEncoding,
+    Encoding stderrEncoding = systemEncoding,
+  }) {
+    if (commandsThrow) {
+      throw const ProcessException('failed_executable', <String>[]);
+    }
     return _nextResultSync(command as List<String>);
   }
 
   @override
-  Future<Process> start(List<dynamic> command,
-      {String? workingDirectory,
-      Map<String, String>? environment,
-      bool includeParentEnvironment = true,
-      bool runInShell = false,
-      ProcessStartMode mode = ProcessStartMode.normal}) {
+  Future<Process> start(
+    List<dynamic> command, {
+    String? workingDirectory,
+    Map<String, String>? environment,
+    bool includeParentEnvironment = true,
+    bool runInShell = false,
+    ProcessStartMode mode = ProcessStartMode.normal,
+  }) {
+    if (commandsThrow) {
+      throw const ProcessException('failed_executable', <String>[]);
+    }
     return _nextProcess(command as List<String>);
   }
 }
diff --git a/test/src/process_pool_test.dart b/test/src/process_pool_test.dart
index fa7c310..1fc0fba 100644
--- a/test/src/process_pool_test.dart
+++ b/test/src/process_pool_test.dart
@@ -65,5 +65,22 @@
       expect(completed.first.result?.stderr, equals('stderr1'));
       expect(completed.first.result?.output, equals('output1stderr1'));
     });
+    test('Commands the throw exceptions report results', () async {
+      fakeProcessManager = FakeProcessManager((String value) {}, commandsThrow: true);
+      processRunner = ProcessRunner(processManager: fakeProcessManager);
+      processPool = ProcessPool(processRunner: processRunner, printReport: null);
+      final Map<List<String>, List<ProcessResult>> calls = <List<String>, List<ProcessResult>>{
+        <String>['command', 'arg1', 'arg2']: <ProcessResult>[
+          ProcessResult(0, -1, 'output1', 'stderr1'),
+        ],
+      };
+      fakeProcessManager.fakeResults = calls;
+      final List<WorkerJob> jobs = <WorkerJob>[
+        WorkerJob(<String>['command', 'arg1', 'arg2'], name: 'job 1'),
+      ];
+      final List<WorkerJob> completed = await processPool.runToCompletion(jobs);
+      expect(completed.first.result, isNull);
+      expect(completed.first.exception, isNotNull);
+    });
   });
 }