Do some cleanup and add tests (#39)
diff --git a/.github/workflows/process_runner.yml b/.github/workflows/process_runner.yml index 32faff2..32529d5 100644 --- a/.github/workflows/process_runner.yml +++ b/.github/workflows/process_runner.yml
@@ -2,9 +2,9 @@ on: push: - branches: [master] + branches: [main] pull_request: - branches: [master] + branches: [main] workflow_dispatch: jobs:
diff --git a/CHANGELOG.md b/CHANGELOG.md index 904250d..038bb4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md
@@ -1,5 +1,18 @@ # Change Log for `process_runner` +## 4.2.2 + +- Refactoring the main `run` method in `ProcessRunner` into smaller, more + manageable private methods. +- Enhancing `FakeProcessManager` to differentiate commands based on their + working directory and the `runInShell` parameter, and adding more + comprehensive tests for it. +- Making the addition of dependencies in `ProcessPool` idempotent (do nothing if + the dependency already exists). +- Updating the CI to track the main branch. +- Adding a number of new tests to cover `stdin` handling, `runInShell` behavior, + and exception scenarios. + ## 4.2.1 - Updates analysis_options.yaml to modern lints, and updates formatting.
diff --git a/lib/src/process_pool.dart b/lib/src/process_pool.dart index 93481c7..5e48286 100644 --- a/lib/src/process_pool.dart +++ b/lib/src/process_pool.dart
@@ -59,7 +59,7 @@ throw ProcessRunnerException('A job cannot depend on itself'); } if (_dependsOn.contains(job)) { - throw ProcessRunnerException('$job is already a dependency of $this'); + return; } if (job._dependsOn.contains(this)) { throw ProcessRunnerException( @@ -225,13 +225,16 @@ WorkerJobGroup(Iterable<DependentJob> jobs, {Iterable<DependentJob>? dependsOn, this.name = 'Group'}) : assert(jobs.isNotEmpty), - jobs = <DependentJob>[...jobs], + jobs = jobs.toList(), super(dependsOn: <DependentJob>{ ...jobs.toSet(), if (dependsOn != null) ...dependsOn }) { // Make sure they run in series, and they depend on anything that the group // depends on. + if (dependsOn != null) { + this.jobs.first.addDependencies(dependsOn); + } for (var i = 1; i < this.jobs.length; i++) { this.jobs[i].addDependency(this.jobs[i - 1]); }
diff --git a/lib/src/process_runner.dart b/lib/src/process_runner.dart index aef2637..2ec7868 100644 --- a/lib/src/process_runner.dart +++ b/lib/src/process_runner.dart
@@ -220,81 +220,26 @@ stderr.write( 'Running "${commandLine.join(' ')}" in ${workingDirectory.path}.\n'); } + + final process = await _startProcess( + commandLine, workingDirectory, runInShell, startMode); + final stdoutOutput = <int>[]; final stderrOutput = <int>[]; final combinedOutput = <int>[]; - final stdoutComplete = Completer<void>(); - final stderrComplete = Completer<void>(); - final stdinComplete = Completer<void>(); + final completers = _streamProcessOutput( + process, + stdin, + stdoutOutput, + stderrOutput, + combinedOutput, + printOutput, + startMode, + ); - late Process process; - Future<int> allComplete() async { - if (stdin != null) { - await stdinComplete.future; - await process.stdin.close(); - } - await stderrComplete.future; - await stdoutComplete.future; - return startMode == ProcessStartMode.normal - ? process.exitCode - : Future<int>.value(0); - } + final exitCode = + await _waitForProcess(process, startMode, completers, stdin); - try { - process = await processManager.start( - commandLine, - workingDirectory: workingDirectory.absolute.path, - environment: environment, - runInShell: runInShell, - mode: startMode, - ); - if (startMode == ProcessStartMode.normal || - startMode == ProcessStartMode.detachedWithStdio) { - if (stdin != null) { - stdin.listen((List<int> data) { - process.stdin.add(data); - }, onDone: () async => stdinComplete.complete()); - } - process.stdout.listen( - (List<int> event) { - stdoutOutput.addAll(event); - combinedOutput.addAll(event); - if (printOutput!) { - stdout.add(event); - } - }, - onDone: () async => stdoutComplete.complete(), - ); - process.stderr.listen( - (List<int> event) { - stderrOutput.addAll(event); - combinedOutput.addAll(event); - if (printOutput!) { - stderr.add(event); - } - }, - onDone: () async => stderrComplete.complete(), - ); - } else { - // For "detached", we can't wait for anything to complete. - stdinComplete.complete(); - stdoutComplete.complete(); - stderrComplete.complete(); - } - } on ProcessException catch (e) { - final message = - 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} ' - 'failed with:\n$e'; - throw ProcessRunnerException(message); - // ignore: avoid_catching_errors - } on ArgumentError catch (e) { - final message = - 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} ' - 'failed with:\n$e'; - throw ProcessRunnerException(message); - } - - final exitCode = await allComplete(); if (exitCode != 0 && !failOk) { final message = 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} ' @@ -320,4 +265,103 @@ decoder: decoder, ); } + + Future<Process> _startProcess( + List<String> commandLine, + Directory workingDirectory, + bool runInShell, + ProcessStartMode startMode, + ) async { + try { + return await processManager.start( + commandLine, + workingDirectory: workingDirectory.absolute.path, + environment: environment, + runInShell: runInShell, + mode: startMode, + ); + } on ProcessException catch (e) { + final message = + 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} ' + 'failed with:\n$e'; + throw ProcessRunnerException(message); + // ignore: avoid_catching_errors + } on ArgumentError catch (e) { + final message = + 'Running "${commandLine.join(' ')}" in ${workingDirectory.path} ' + 'failed with:\n$e'; + throw ProcessRunnerException(message); + } + } + + List<Completer<void>> _streamProcessOutput( + Process process, + Stream<List<int>>? stdin, + List<int> stdoutOutput, + List<int> stderrOutput, + List<int> combinedOutput, + bool printOutput, + ProcessStartMode startMode, + ) { + final stdoutComplete = Completer<void>(); + final stderrComplete = Completer<void>(); + final stdinComplete = Completer<void>(); + + if (startMode == ProcessStartMode.normal || + startMode == ProcessStartMode.detachedWithStdio) { + if (stdin != null) { + stdin.listen((List<int> data) { + process.stdin.add(data); + }, onDone: () async => stdinComplete.complete()); + } else { + stdinComplete.complete(); + } + process.stdout.listen( + (List<int> event) { + stdoutOutput.addAll(event); + combinedOutput.addAll(event); + if (printOutput) { + stdout.add(event); + } + }, + onDone: () async => stdoutComplete.complete(), + ); + process.stderr.listen( + (List<int> event) { + stderrOutput.addAll(event); + combinedOutput.addAll(event); + if (printOutput) { + stderr.add(event); + } + }, + onDone: () async => stderrComplete.complete(), + ); + } else { + stdinComplete.complete(); + stdoutComplete.complete(); + stderrComplete.complete(); + } + return [stdinComplete, stdoutComplete, stderrComplete]; + } + + Future<int> _waitForProcess( + Process process, + ProcessStartMode startMode, + List<Completer<void>> completers, + Stream<List<int>>? stdin, + ) async { + final stdinComplete = completers[0]; + final stdoutComplete = completers[1]; + final stderrComplete = completers[2]; + + if (stdin != null) { + await stdinComplete.future; + await process.stdin.close(); + } + await stderrComplete.future; + await stdoutComplete.future; + return startMode == ProcessStartMode.normal + ? process.exitCode + : Future<int>.value(0); + } }
diff --git a/lib/test/fake_process_manager.dart b/lib/test/fake_process_manager.dart index e78fc12..25d9b60 100644 --- a/lib/test/fake_process_manager.dart +++ b/lib/test/fake_process_manager.dart
@@ -13,9 +13,52 @@ test_package.TypeMatcher<T> isInstanceOf<T>() => isA<T>(); class FakeInvocationRecord { - FakeInvocationRecord(this.invocation, [this.workingDirectory]); + FakeInvocationRecord( + this.invocation, { + this.workingDirectory, + this.runInShell = false, + }); final List<String> invocation; final String? workingDirectory; + final bool runInShell; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + if (other is! FakeInvocationRecord) { + return false; + } + if (other.workingDirectory != workingDirectory) { + return false; + } + if (other.runInShell != runInShell) { + return false; + } + if (other.invocation.length != invocation.length) { + return false; + } + for (var i = 0; i < invocation.length; ++i) { + if (other.invocation[i] != invocation[i]) { + return false; + } + } + return true; + } + + @override + int get hashCode => + Object.hashAll([workingDirectory, runInShell, ...invocation]); + + @override + String toString() { + return 'FakeInvocationRecord(invocation: $invocation, ' + 'workingDirectory: $workingDirectory, runInShell: $runInShell)'; + } } /// A mock that can be used to fake a process manager that runs commands and @@ -31,7 +74,7 @@ /// Set to true if all commands run with this process manager should throw an /// exception. - final bool commandsThrow; + 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 @@ -66,52 +109,54 @@ ProcessResult _popResult(FakeInvocationRecord command) { expect(fakeResults, isNotEmpty); - late List<ProcessResult> foundResult; - late FakeInvocationRecord foundCommand; - for (final fakeCommand in fakeResults.keys) { - if (fakeCommand.invocation.length != command.invocation.length) { - continue; - } - var listsIdentical = true; - for (var i = 0; i < fakeCommand.invocation.length; ++i) { - if (fakeCommand.invocation[i] != command.invocation[i]) { - listsIdentical = false; - break; - } - } - if (listsIdentical) { - foundResult = fakeResults[fakeCommand]!; - foundCommand = fakeCommand; - break; - } - } + final foundResult = fakeResults[command]; expect(foundResult, isNotNull, reason: '$command not found in expected results.'); expect(foundResult, isNotEmpty); - return fakeResults[foundCommand]?.removeAt(0) ?? - ProcessResult(0, 0, '', ''); + return fakeResults[command]!.removeAt(0); } FakeProcess _popProcess(FakeInvocationRecord command) => FakeProcess(_popResult(command), stdinResults); Future<Process> _nextProcess( - List<String> invocation, String? workingDirectory) async { - final record = FakeInvocationRecord(invocation, workingDirectory); + List<String> invocation, { + String? workingDirectory, + bool runInShell = false, + }) async { + final record = FakeInvocationRecord( + invocation, + workingDirectory: workingDirectory, + runInShell: runInShell, + ); invocations.add(record); return Future<Process>.value(_popProcess(record)); } ProcessResult _nextResultSync( - List<String> invocation, String? workingDirectory) { - final record = FakeInvocationRecord(invocation, workingDirectory); + List<String> invocation, { + String? workingDirectory, + bool runInShell = false, + }) { + final record = FakeInvocationRecord( + invocation, + workingDirectory: workingDirectory, + runInShell: runInShell, + ); invocations.add(record); return _popResult(record); } Future<ProcessResult> _nextResult( - List<String> invocation, String? workingDirectory) async { - final record = FakeInvocationRecord(invocation, workingDirectory); + List<String> invocation, { + String? workingDirectory, + bool runInShell = false, + }) async { + final record = FakeInvocationRecord( + invocation, + workingDirectory: workingDirectory, + runInShell: runInShell, + ); invocations.add(record); return Future<ProcessResult>.value(_popResult(record)); } @@ -139,7 +184,11 @@ if (commandsThrow) { throw const ProcessException('failed_executable', <String>[]); } - return _nextResult(command as List<String>, workingDirectory); + return _nextResult( + command as List<String>, + workingDirectory: workingDirectory, + runInShell: runInShell, + ); } @override @@ -155,7 +204,11 @@ if (commandsThrow) { throw const ProcessException('failed_executable', <String>[]); } - return _nextResultSync(command as List<String>, workingDirectory); + return _nextResultSync( + command as List<String>, + workingDirectory: workingDirectory, + runInShell: runInShell, + ); } @override @@ -170,7 +223,11 @@ if (commandsThrow) { throw const ProcessException('failed_executable', <String>[]); } - return _nextProcess(command as List<String>, workingDirectory); + return _nextProcess( + command as List<String>, + workingDirectory: workingDirectory, + runInShell: runInShell, + ); } }
diff --git a/pubspec.yaml b/pubspec.yaml index 74e91aa..02f0f49 100644 --- a/pubspec.yaml +++ b/pubspec.yaml
@@ -4,7 +4,7 @@ name: process_runner description: A process invocation abstraction for Dart that manages a multi-process queue. -version: 4.2.1 +version: 4.2.2 repository: https://github.com/google/process_runner issue_tracker: https://github.com/google/process_runner/issues documentation: https://github.com/google/process_runner/blob/master/process_runner/README.md @@ -24,7 +24,7 @@ dart_flutter_team_lints: ^3.5.2 environment: - sdk: ">=2.12.0 <4.0.0" + sdk: ">=2.14.0 <4.0.0" binaries: process_runner:
diff --git a/test/src/fake_process_manager_test.dart b/test/src/fake_process_manager_test.dart index 5060361..0488584 100644 --- a/test/src/fake_process_manager_test.dart +++ b/test/src/fake_process_manager_test.dart
@@ -5,24 +5,145 @@ import 'dart:convert'; import 'dart:io'; +import 'package:file/memory.dart'; +import 'package:process_runner/process_runner.dart'; import 'package:process_runner/test/fake_process_manager.dart'; import 'package:test/test.dart' hide TypeMatcher, isInstanceOf; void main() { - group('ArchivePublisher', () { + group('FakeProcessManager', () { final stdinCaptured = <String>[]; void captureStdin(String item) { stdinCaptured.add(item); } - var processManager = FakeProcessManager(captureStdin); + late MemoryFileSystem fs; + late FakeProcessManager processManager; + late ProcessRunner processRunner; setUp(() async { + fs = MemoryFileSystem( + style: Platform.isWindows + ? FileSystemStyle.windows + : FileSystemStyle.posix); processManager = FakeProcessManager(captureStdin); + processRunner = ProcessRunner( + processManager: processManager, + defaultWorkingDirectory: fs.currentDirectory); }); - tearDown(() async {}); + tearDown(() async { + stdinCaptured.clear(); + }); + test('fakes processes', () async { + processManager.fakeResults = <FakeInvocationRecord, List<ProcessResult>>{ + FakeInvocationRecord( + <String>['command', 'arg1', 'arg2'], + workingDirectory: fs.currentDirectory.path, + ): <ProcessResult>[ + ProcessResult(0, 0, 'output1', ''), + ], + FakeInvocationRecord( + <String>['command2', 'arg1', 'arg2'], + workingDirectory: fs.currentDirectory.path, + ): <ProcessResult>[ + ProcessResult(0, 0, 'output2', ''), + ], + }; + var result = + await processRunner.runProcess(<String>['command', 'arg1', 'arg2']); + expect(result.stdout, equals('output1')); + result = + await processRunner.runProcess(<String>['command2', 'arg1', 'arg2']); + expect(result.stdout, equals('output2')); + processManager.verifyCalls(<FakeInvocationRecord>[ + FakeInvocationRecord( + <String>['command', 'arg1', 'arg2'], + workingDirectory: fs.currentDirectory.path, + ), + FakeInvocationRecord( + <String>['command2', 'arg1', 'arg2'], + workingDirectory: fs.currentDirectory.path, + ), + ]); + }); + test('distinguishes commands by working directory', () async { + processManager.fakeResults = <FakeInvocationRecord, List<ProcessResult>>{ + FakeInvocationRecord( + <String>['command', 'arg'], + workingDirectory: Directory('wd1').absolute.path, + ): <ProcessResult>[ + ProcessResult(0, 0, 'output1', ''), + ], + FakeInvocationRecord( + <String>['command', 'arg'], + workingDirectory: Directory('wd2').absolute.path, + ): <ProcessResult>[ + ProcessResult(0, 0, 'output2', ''), + ], + }; + var result = await processRunner.runProcess( + <String>['command', 'arg'], + workingDirectory: Directory('wd1'), + ); + expect(result.stdout, equals('output1')); + result = await processRunner.runProcess( + <String>['command', 'arg'], + workingDirectory: Directory('wd2'), + ); + expect(result.stdout, equals('output2')); + processManager.verifyCalls(<FakeInvocationRecord>[ + FakeInvocationRecord( + <String>['command', 'arg'], + workingDirectory: Directory('wd1').absolute.path, + ), + FakeInvocationRecord( + <String>['command', 'arg'], + workingDirectory: Directory('wd2').absolute.path, + ), + ]); + }); + test('distinguishes commands by runInShell', () async { + processManager.fakeResults = <FakeInvocationRecord, List<ProcessResult>>{ + FakeInvocationRecord( + <String>['command', 'arg'], + workingDirectory: fs.currentDirectory.path, + runInShell: true, + ): <ProcessResult>[ + ProcessResult(0, 0, 'output1', ''), + ], + FakeInvocationRecord( + <String>['command', 'arg'], + workingDirectory: fs.currentDirectory.path, + runInShell: false, + ): <ProcessResult>[ + ProcessResult(0, 0, 'output2', ''), + ], + }; + var result = await processRunner.runProcess( + <String>['command', 'arg'], + runInShell: true, + ); + expect(result.stdout, equals('output1')); + result = await processRunner.runProcess( + <String>['command', 'arg'], + runInShell: false, + ); + expect(result.stdout, equals('output2')); + processManager.verifyCalls(<FakeInvocationRecord>[ + FakeInvocationRecord( + <String>['command', 'arg'], + workingDirectory: fs.currentDirectory.path, + runInShell: true, + ), + FakeInvocationRecord( + <String>['command', 'arg'], + workingDirectory: fs.currentDirectory.path, + runInShell: false, + ), + ]); + }); test('start works', () async { final calls = <FakeInvocationRecord, List<ProcessResult>>{ FakeInvocationRecord(<String>['command', 'arg1', 'arg2']): @@ -114,5 +235,29 @@ } processManager.verifyCalls(calls.keys.toList()); }); + + test('run throws when commandsThrow is true', () async { + processManager = FakeProcessManager(captureStdin, commandsThrow: true); + expect( + () async => await processManager.run(<String>['command']), + throwsA(isA<ProcessException>()), + ); + }); + + test('runSync throws when commandsThrow is true', () async { + processManager = FakeProcessManager(captureStdin, commandsThrow: true); + expect( + () => processManager.runSync(<String>['command']), + throwsA(isA<ProcessException>()), + ); + }); + + test('start throws when commandsThrow is true', () async { + processManager = FakeProcessManager(captureStdin, commandsThrow: true); + expect( + () async => await processManager.start(<String>['command']), + throwsA(isA<ProcessException>()), + ); + }); }); }
diff --git a/test/src/process_pool_test.dart b/test/src/process_pool_test.dart index 3bb11da..9a964f9 100644 --- a/test/src/process_pool_test.dart +++ b/test/src/process_pool_test.dart
@@ -4,11 +4,14 @@ import 'dart:io'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; import 'package:process_runner/process_runner.dart'; import 'package:process_runner/test/fake_process_manager.dart'; import 'package:test/test.dart'; void main() { + late FileSystem fs; late FakeProcessManager fakeProcessManager; late ProcessRunner processRunner; late ProcessPool processPool; @@ -16,17 +19,21 @@ setUp(() { fakeProcessManager = FakeProcessManager((String value) {}); + fs = MemoryFileSystem( + style: Platform.isWindows + ? FileSystemStyle.windows + : FileSystemStyle.posix); processRunner = ProcessRunner( processManager: fakeProcessManager, - defaultWorkingDirectory: Directory(testPath), + defaultWorkingDirectory: fs.directory(testPath), ); processPool = ProcessPool(processRunner: processRunner, printReport: null); }); test('startWorkers works', () async { final calls = <FakeInvocationRecord, List<ProcessResult>>{ - FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, 0, 'output1', ''), ], }; @@ -39,8 +46,8 @@ }); test('runToCompletion works', () async { final calls = <FakeInvocationRecord, List<ProcessResult>>{ - FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, 0, 'output1', ''), ], }; @@ -53,8 +60,8 @@ }); test('failed tests report results', () async { final calls = <FakeInvocationRecord, List<ProcessResult>>{ - FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, -1, 'output1', 'stderr1'), ], }; @@ -70,8 +77,8 @@ }); test('failed tests throw when failOk is false', () async { final calls = <FakeInvocationRecord, List<ProcessResult>>{ - FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, -1, 'output1', 'stderr1'), ], }; @@ -90,8 +97,8 @@ processRunner = ProcessRunner(processManager: fakeProcessManager); processPool = ProcessPool(processRunner: processRunner, printReport: null); final calls = <FakeInvocationRecord, List<ProcessResult>>{ - FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, -1, 'output1', 'stderr1'), ], }; @@ -106,32 +113,29 @@ test('Commands in task groups run in order, but parallel with other groups', () async { - fakeProcessManager = FakeProcessManager((String value) {}); - processRunner = ProcessRunner(processManager: fakeProcessManager); - processPool = ProcessPool(processRunner: processRunner, printReport: null); final calls = <FakeInvocationRecord, List<ProcessResult>>{ - FakeInvocationRecord(<String>['commandA1', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['commandA1', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, 0, 'outputA1', 'stderrA1'), ], - FakeInvocationRecord(<String>['commandB1', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['commandB1', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, 0, 'outputB1', 'stderrB1'), ], - FakeInvocationRecord(<String>['commandA2', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['commandA2', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, 0, 'outputA2', 'stderrA2'), ], - FakeInvocationRecord(<String>['commandB2', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['commandB2', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, -1, 'outputB2', 'stderrB2'), ], - FakeInvocationRecord(<String>['commandA3', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['commandA3', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, 0, 'outputA3', 'stderrA3'), ], - FakeInvocationRecord(<String>['commandB3', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['commandB3', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, 0, 'outputB3', 'stderrB3'), ], }; @@ -191,32 +195,29 @@ ); }); test('Commands in task groups can depend on other groups', () async { - fakeProcessManager = FakeProcessManager((String value) {}); - processRunner = ProcessRunner(processManager: fakeProcessManager); - processPool = ProcessPool(processRunner: processRunner, printReport: null); final calls = <FakeInvocationRecord, List<ProcessResult>>{ - FakeInvocationRecord(<String>['commandA1', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['commandA1', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, 0, 'outputA1', 'stderrA1'), ], - FakeInvocationRecord(<String>['commandB1', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['commandB1', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, 0, 'outputB1', 'stderrB1'), ], - FakeInvocationRecord(<String>['commandA2', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['commandA2', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, 0, 'outputA2', 'stderrA2'), ], - FakeInvocationRecord(<String>['commandB2', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['commandB2', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, -1, 'outputB2', 'stderrB2'), ], - FakeInvocationRecord(<String>['commandA3', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['commandA3', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, 0, 'outputA3', 'stderrA3'), ], - FakeInvocationRecord(<String>['commandB3', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['commandB3', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, 0, 'outputB3', 'stderrB3'), ], }; @@ -309,16 +310,16 @@ processRunner = ProcessRunner(processManager: fakeProcessManager); processPool = ProcessPool(processRunner: processRunner, printReport: null); final calls = <FakeInvocationRecord, List<ProcessResult>>{ - FakeInvocationRecord(<String>['commandA1', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['commandA1', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, 0, 'outputA1', 'stderrA1'), ], - FakeInvocationRecord(<String>['commandB1', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['commandB1', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, 0, 'outputB1', 'stderrB1'), ], - FakeInvocationRecord(<String>['commandC1', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['commandC1', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, 0, 'outputC1', 'stderrC1'), ], }; @@ -351,4 +352,58 @@ ), ); }); + + test('throws when a job depends on a job not in the pool', () async { + final jobA = WorkerJob(<String>['commandA'], name: 'job A'); + final jobB = WorkerJob(<String>['commandB'], name: 'job B'); + jobA.addDependency(jobB); + final jobs = <DependentJob>[jobA]; + expect( + () => processPool.runToCompletion(jobs), + throwsA(isA<ProcessRunnerException>().having( + (e) => e.message, + 'message', + contains("job A has dependent jobs that aren't scheduled to be run"), + )), + ); + }); + + test('WorkerJob with stdin works', () async { + final stdinCaptured = <String>[]; + fakeProcessManager = FakeProcessManager(stdinCaptured.add); + processRunner = ProcessRunner( + processManager: fakeProcessManager, + defaultWorkingDirectory: fs.directory(testPath), + ); + processPool = ProcessPool(processRunner: processRunner, printReport: null); + + final calls = <FakeInvocationRecord, List<ProcessResult>>{ + FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ + ProcessResult(0, 0, 'output1', ''), + ], + }; + fakeProcessManager.fakeResults = calls; + final jobs = <WorkerJob>[ + WorkerJob( + <String>['command', 'arg1', 'arg2'], + name: 'job 1', + stdin: Stream<String>.fromIterable(<String>['input']), + ), + ]; + await processPool.runToCompletion(jobs); + fakeProcessManager.verifyCalls(calls.keys); + expect(stdinCaptured, equals(<String>['input'])); + }); + + test('defaultReportToString formats correctly', () { + expect( + ProcessPool.defaultReportToString(100, 20, 10, 60, 10), + 'Jobs: 30% done, 20/100 completed, 10 in progress, 60 pending, 10 failed. \r', + ); + expect( + ProcessPool.defaultReportToString(0, 0, 0, 0, 0), + 'Jobs: 100% done, 0/0 completed, 0 in progress, 0 pending, 0 failed. \r', + ); + }); }
diff --git a/test/src/process_runner_test.dart b/test/src/process_runner_test.dart index b7b4d60..ef7c50f 100644 --- a/test/src/process_runner_test.dart +++ b/test/src/process_runner_test.dart
@@ -9,12 +9,18 @@ import 'package:test/test.dart'; void main() { - var fakeProcessManager = FakeProcessManager((String value) {}); + final stdinCaptured = <String>[]; + void captureStdin(String item) { + stdinCaptured.add(item); + } + + var fakeProcessManager = FakeProcessManager(captureStdin); var processRunner = ProcessRunner(processManager: fakeProcessManager); final testPath = Platform.isWindows ? r'C:\tmp\foo' : '/tmp/foo'; setUp(() { - fakeProcessManager = FakeProcessManager((String value) {}); + stdinCaptured.clear(); + fakeProcessManager = FakeProcessManager(captureStdin); processRunner = ProcessRunner( processManager: fakeProcessManager, defaultWorkingDirectory: Directory(testPath)); @@ -25,8 +31,8 @@ group('Output Capture', () { test('runProcess works', () async { final calls = <FakeInvocationRecord, List<ProcessResult>>{ - FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, 0, 'output1', ''), ], }; @@ -36,8 +42,8 @@ }); test('runProcess returns correct output', () async { final calls = <FakeInvocationRecord, List<ProcessResult>>{ - FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, 0, 'output1', 'stderr1'), ], }; @@ -51,8 +57,8 @@ }); test('runProcess fails properly', () async { final calls = <FakeInvocationRecord, List<ProcessResult>>{ - FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], ''): - <ProcessResult>[ + FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], + workingDirectory: ''): <ProcessResult>[ ProcessResult(0, -1, 'output1', 'stderr1'), ], }; @@ -63,8 +69,8 @@ }); test('runProcess returns the failed results properly', () async { final calls = <FakeInvocationRecord, List<ProcessResult>>{ - FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], testPath): - <ProcessResult>[ + FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ ProcessResult(0, -1, 'output1', 'stderr1'), ], }; @@ -75,5 +81,57 @@ expect(result.stderr, equals('stderr1')); expect(result.output, equals('output1stderr1')); }); + + test('runProcess with stdin works', () async { + final calls = <FakeInvocationRecord, List<ProcessResult>>{ + FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ + ProcessResult(0, 0, 'output1', ''), + ], + }; + fakeProcessManager.fakeResults = calls; + final stdin = Stream<List<int>>.fromIterable(<List<int>>[ + 'input'.codeUnits, + ]); + final result = await processRunner.runProcess( + calls.keys.first.invocation, + stdin: stdin, + ); + expect(result.stdout, equals('output1')); + expect(stdinCaptured, equals(<String>['input'])); + }); + + test('runProcess with runInShell works', () async { + final calls = <FakeInvocationRecord, List<ProcessResult>>{ + FakeInvocationRecord( + <String>['command', 'arg1', 'arg2'], + workingDirectory: testPath, + runInShell: true, + ): <ProcessResult>[ + ProcessResult(0, 0, 'output1', ''), + ], + }; + fakeProcessManager.fakeResults = calls; + await processRunner.runProcess( + calls.keys.first.invocation, + runInShell: true, + ); + fakeProcessManager.verifyCalls(calls.keys); + }); + + test('runProcess throws when process manager throws', () async { + fakeProcessManager.commandsThrow = true; + final calls = <FakeInvocationRecord, List<ProcessResult>>{ + FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], + workingDirectory: testPath): <ProcessResult>[ + ProcessResult(0, -1, 'output1', 'stderr1'), + ], + }; + fakeProcessManager.fakeResults = calls; + await expectLater( + () => processRunner.runProcess(calls.keys.first.invocation), + throwsA(isA<ProcessRunnerException>()), + ); + }); }); }