| // Copyright 2020 The Flutter Authors. 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: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; |
| final testPath = Platform.isWindows ? r'C:\tmp\foo' : '/tmp/foo'; |
| |
| setUp(() { |
| fakeProcessManager = FakeProcessManager((String value) {}); |
| fs = MemoryFileSystem( |
| style: Platform.isWindows |
| ? FileSystemStyle.windows |
| : FileSystemStyle.posix); |
| processRunner = ProcessRunner( |
| processManager: fakeProcessManager, |
| defaultWorkingDirectory: fs.directory(testPath), |
| ); |
| processPool = ProcessPool(processRunner: processRunner, printReport: null); |
| }); |
| |
| test('startWorkers works', () async { |
| 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'), |
| ]; |
| await for (final WorkerJob _ in processPool.startWorkers(jobs)) {} |
| fakeProcessManager.verifyCalls(calls.keys); |
| }); |
| test('runToCompletion works', () async { |
| 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'), |
| ]; |
| await processPool.runToCompletion(jobs); |
| fakeProcessManager.verifyCalls(calls.keys); |
| }); |
| test('failed tests report results', () async { |
| final calls = <FakeInvocationRecord, List<ProcessResult>>{ |
| FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], |
| workingDirectory: testPath): <ProcessResult>[ |
| ProcessResult(0, -1, 'output1', 'stderr1'), |
| ], |
| }; |
| fakeProcessManager.fakeResults = calls; |
| final jobs = <WorkerJob>[ |
| WorkerJob(<String>['command', 'arg1', 'arg2'], name: 'job 1'), |
| ]; |
| final completed = await processPool.runToCompletion(jobs); |
| expect(completed.first.result.exitCode, equals(-1)); |
| expect(completed.first.result.stdout, equals('output1')); |
| expect(completed.first.result.stderr, equals('stderr1')); |
| expect(completed.first.result.output, equals('output1stderr1')); |
| }); |
| test('failed tests throw when failOk is false', () async { |
| final calls = <FakeInvocationRecord, List<ProcessResult>>{ |
| FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], |
| workingDirectory: testPath): <ProcessResult>[ |
| ProcessResult(0, -1, 'output1', 'stderr1'), |
| ], |
| }; |
| fakeProcessManager.fakeResults = calls; |
| final jobs = <WorkerJob>[ |
| WorkerJob(<String>['command', 'arg1', 'arg2'], |
| name: 'job 1', failOk: false), |
| ]; |
| expect(() async { |
| await processPool.runToCompletion(jobs); |
| }, throwsException); |
| }); |
| test('Commands that throw exceptions report results', () async { |
| fakeProcessManager = |
| FakeProcessManager((String value) {}, commandsThrow: true); |
| processRunner = ProcessRunner(processManager: fakeProcessManager); |
| processPool = ProcessPool(processRunner: processRunner, printReport: null); |
| final calls = <FakeInvocationRecord, List<ProcessResult>>{ |
| FakeInvocationRecord(<String>['command', 'arg1', 'arg2'], |
| workingDirectory: testPath): <ProcessResult>[ |
| ProcessResult(0, -1, 'output1', 'stderr1'), |
| ], |
| }; |
| fakeProcessManager.fakeResults = calls; |
| final jobs = <WorkerJob>[ |
| WorkerJob(<String>['command', 'arg1', 'arg2'], name: 'job 1'), |
| ]; |
| final completed = await processPool.runToCompletion(jobs); |
| expect(completed.first.result, equals(ProcessRunnerResult.failed)); |
| expect(completed.first.exception, isNotNull); |
| }); |
| |
| test('Commands in task groups run in order, but parallel with other groups', |
| () async { |
| final calls = <FakeInvocationRecord, List<ProcessResult>>{ |
| FakeInvocationRecord(<String>['commandA1', 'arg1', 'arg2'], |
| workingDirectory: testPath): <ProcessResult>[ |
| ProcessResult(0, 0, 'outputA1', 'stderrA1'), |
| ], |
| FakeInvocationRecord(<String>['commandB1', 'arg1', 'arg2'], |
| workingDirectory: testPath): <ProcessResult>[ |
| ProcessResult(0, 0, 'outputB1', 'stderrB1'), |
| ], |
| FakeInvocationRecord(<String>['commandA2', 'arg1', 'arg2'], |
| workingDirectory: testPath): <ProcessResult>[ |
| ProcessResult(0, 0, 'outputA2', 'stderrA2'), |
| ], |
| FakeInvocationRecord(<String>['commandB2', 'arg1', 'arg2'], |
| workingDirectory: testPath): <ProcessResult>[ |
| ProcessResult(0, -1, 'outputB2', 'stderrB2'), |
| ], |
| FakeInvocationRecord(<String>['commandA3', 'arg1', 'arg2'], |
| workingDirectory: testPath): <ProcessResult>[ |
| ProcessResult(0, 0, 'outputA3', 'stderrA3'), |
| ], |
| FakeInvocationRecord(<String>['commandB3', 'arg1', 'arg2'], |
| workingDirectory: testPath): <ProcessResult>[ |
| ProcessResult(0, 0, 'outputB3', 'stderrB3'), |
| ], |
| }; |
| fakeProcessManager.fakeResults = calls; |
| final jobs = <WorkerJobGroup>[ |
| WorkerJobGroup( |
| <WorkerJob>[ |
| WorkerJob(<String>['commandA1', 'arg1', 'arg2'], name: 'job A1'), |
| WorkerJob(<String>['commandA2', 'arg1', 'arg2'], name: 'job A2'), |
| WorkerJob(<String>['commandA3', 'arg1', 'arg2'], name: 'job A3'), |
| ], |
| name: 'Group A', |
| ), |
| WorkerJobGroup( |
| <WorkerJob>[ |
| WorkerJob(<String>['commandB1', 'arg1', 'arg2'], name: 'job B1'), |
| WorkerJob(<String>['commandB2', 'arg1', 'arg2'], name: 'job B2'), |
| WorkerJob(<String>['commandB3', 'arg1', 'arg2'], name: 'job B3'), |
| ], |
| name: 'Group B', |
| ), |
| ]; |
| final completed = await processPool.runToCompletion(jobs); |
| expect(completed.length, equals(6)); |
| // Command B2 failed with -1, so B3 should also fail. |
| expect( |
| completed |
| .where((WorkerJob job) => job.result.exitCode != 0) |
| .map((WorkerJob job) => job.name), |
| unorderedEquals(<String>['job B2', 'job B3']), |
| ); |
| expect( |
| completed |
| .where((WorkerJob job) => job.exception == null) |
| .map((WorkerJob job) => job.name), |
| unorderedEquals(<String>['job A1', 'job B1', 'job A2', 'job A3']), |
| ); |
| expect( |
| completed |
| .where((WorkerJob job) => job.result.exitCode == 0) |
| .map((WorkerJob job) => job.name), |
| unorderedEquals(<String>['job B1', 'job A1', 'job A2', 'job A3']), |
| ); |
| // Either group A or B can come first, but the individual group tasks should |
| // be in order. |
| expect( |
| <String>[completed[0].name, completed[1].name], |
| unorderedEquals(<String>['job A1', 'job B1']), |
| ); |
| expect( |
| <String>[completed[2].name, completed[3].name], |
| unorderedEquals(<String>['job A2', 'job B2']), |
| ); |
| expect( |
| <String>[completed[4].name, completed[5].name], |
| unorderedEquals(<String>['job A3', 'job B3']), |
| ); |
| }); |
| test('Commands in task groups can depend on other groups', () async { |
| final calls = <FakeInvocationRecord, List<ProcessResult>>{ |
| FakeInvocationRecord(<String>['commandA1', 'arg1', 'arg2'], |
| workingDirectory: testPath): <ProcessResult>[ |
| ProcessResult(0, 0, 'outputA1', 'stderrA1'), |
| ], |
| FakeInvocationRecord(<String>['commandB1', 'arg1', 'arg2'], |
| workingDirectory: testPath): <ProcessResult>[ |
| ProcessResult(0, 0, 'outputB1', 'stderrB1'), |
| ], |
| FakeInvocationRecord(<String>['commandA2', 'arg1', 'arg2'], |
| workingDirectory: testPath): <ProcessResult>[ |
| ProcessResult(0, 0, 'outputA2', 'stderrA2'), |
| ], |
| FakeInvocationRecord(<String>['commandB2', 'arg1', 'arg2'], |
| workingDirectory: testPath): <ProcessResult>[ |
| ProcessResult(0, -1, 'outputB2', 'stderrB2'), |
| ], |
| FakeInvocationRecord(<String>['commandA3', 'arg1', 'arg2'], |
| workingDirectory: testPath): <ProcessResult>[ |
| ProcessResult(0, 0, 'outputA3', 'stderrA3'), |
| ], |
| FakeInvocationRecord(<String>['commandB3', 'arg1', 'arg2'], |
| workingDirectory: testPath): <ProcessResult>[ |
| ProcessResult(0, 0, 'outputB3', 'stderrB3'), |
| ], |
| }; |
| fakeProcessManager.fakeResults = calls; |
| final groupA = WorkerJobGroup( |
| <DependentJob>[ |
| WorkerJob(<String>['commandA1', 'arg1', 'arg2'], name: 'job A1'), |
| WorkerJob(<String>['commandA2', 'arg1', 'arg2'], name: 'job A2'), |
| WorkerJob(<String>['commandA3', 'arg1', 'arg2'], name: 'job A3'), |
| ], |
| name: 'Group A', |
| ); |
| final groupB = WorkerJobGroup( |
| <DependentJob>[ |
| WorkerJob(<String>['commandB1', 'arg1', 'arg2'], name: 'job B1'), |
| WorkerJob(<String>['commandB2', 'arg1', 'arg2'], name: 'job B2'), |
| WorkerJob(<String>['commandB3', 'arg1', 'arg2'], name: 'job B3'), |
| ], |
| name: 'Group B', |
| ); |
| groupB.addDependency(groupA); |
| final jobs = <DependentJob>[groupA, groupB]; |
| final completed = await processPool.runToCompletion(jobs); |
| expect(completed.length, equals(6)); |
| // Make sure they executed in the correct order. |
| expect( |
| completed.map<String>((WorkerJob job) => job.name), |
| equals(<String>[ |
| 'job A1', |
| 'job A2', |
| 'job A3', |
| 'job B1', |
| 'job B2', |
| 'job B3' |
| ])); |
| // Command B2 failed with -1, so B3 should also fail. |
| expect( |
| completed |
| .where((WorkerJob job) => job.result.exitCode != 0) |
| .map((WorkerJob job) => job.name), |
| unorderedEquals(<String>['job B2', 'job B3']), |
| ); |
| expect( |
| completed |
| .where((WorkerJob job) => job.exception == null) |
| .map((WorkerJob job) => job.name), |
| unorderedEquals(<String>['job A1', 'job B1', 'job A2', 'job A3']), |
| ); |
| expect( |
| completed |
| .where((WorkerJob job) => job.result.exitCode == 0) |
| .map((WorkerJob job) => job.name), |
| unorderedEquals(<String>['job B1', 'job A1', 'job A2', 'job A3']), |
| ); |
| }); |
| test("Jobs can't depend on themselves", () async { |
| final job = |
| WorkerJob(<String>['commandA1', 'arg1', 'arg2'], name: 'job A1'); |
| |
| ProcessRunnerException? exception; |
| try { |
| job.addDependency(job); |
| } on ProcessRunnerException catch (e) { |
| exception = e; |
| } |
| expect(exception, isNotNull); |
| expect(exception!.message, equals('A job cannot depend on itself')); |
| }); |
| test("Jobs can't depend on each other directly", () async { |
| final jobA = |
| WorkerJob(<String>['commandA1', 'arg1', 'arg2'], name: 'job A1'); |
| final jobB = |
| WorkerJob(<String>['commandB1', 'arg1', 'arg2'], name: 'job B1'); |
| |
| ProcessRunnerException? exception; |
| try { |
| jobA.addDependency(jobB); |
| jobB.addDependency(jobA); |
| } on ProcessRunnerException catch (e) { |
| exception = e; |
| } |
| expect(exception, isNotNull); |
| expect( |
| exception!.message, |
| equals('job B1 is already a dependency of job A1, no cycle allowed'), |
| ); |
| }); |
| test("Jobs can't depend on each other indirectly", () 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'], |
| workingDirectory: testPath): <ProcessResult>[ |
| ProcessResult(0, 0, 'outputA1', 'stderrA1'), |
| ], |
| FakeInvocationRecord(<String>['commandB1', 'arg1', 'arg2'], |
| workingDirectory: testPath): <ProcessResult>[ |
| ProcessResult(0, 0, 'outputB1', 'stderrB1'), |
| ], |
| FakeInvocationRecord(<String>['commandC1', 'arg1', 'arg2'], |
| workingDirectory: testPath): <ProcessResult>[ |
| ProcessResult(0, 0, 'outputC1', 'stderrC1'), |
| ], |
| }; |
| fakeProcessManager.fakeResults = calls; |
| final jobA = |
| WorkerJob(<String>['commandA1', 'arg1', 'arg2'], name: 'job A1'); |
| final jobB = |
| WorkerJob(<String>['commandB1', 'arg1', 'arg2'], name: 'job B1'); |
| final jobC = |
| WorkerJob(<String>['commandC1', 'arg1', 'arg2'], name: 'job C1'); |
| |
| ProcessRunnerException? exception; |
| try { |
| jobA.addDependency(jobB); |
| jobB.addDependency(jobC); |
| jobC.addDependency(jobA); |
| await processPool.runToCompletion(<DependentJob>[jobA, jobB, jobC]); |
| } on ProcessRunnerException catch (e) { |
| exception = e; |
| } |
| expect(exception, isNotNull, reason: 'Should have thrown an exception'); |
| expect( |
| exception!.message, |
| equals( |
| 'Illegal dependency loop detected:\n' |
| ' job A1\n' |
| ' job B1\n' |
| ' job C1\n' |
| ' job A1', |
| ), |
| ); |
| }); |
| |
| 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', |
| ); |
| }); |
| } |