blob: 9a964f90a27fb191af4e86915258c23bf6b3d3ea [file] [log] [blame]
// 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',
);
});
}