verifyTaskInputsAndOutputsConsistency function
Future<DeletionTasksByTask>
verifyTaskInputsAndOutputsConsistency(
- Map<
String, TaskWithDeps> taskMap
Verify that all tasks in taskMap
have inputs/outputs that are mutually
consistent.
If a task uses the outputs of another task as its inputs, then it must have an explicit dependency on the other task.
If a deletion task affects inputs or outputs of another task, then it must execute on a phase that precedes those other tasks.
Any inconsistencies will cause a DartleException to be thrown by this method.
Returns the tasks affected by deletion tasks, so that the engine can detect when it must run a task due to deletions.
Implementation
Future<DeletionTasksByTask> verifyTaskInputsAndOutputsConsistency(
Map<String, TaskWithDeps> taskMap) async {
final inputsByTask = <TaskWithDeps, FileCollection>{};
final outputsByTask = <TaskWithDeps, FileCollection>{};
final deletionsByTask = <TaskWithDeps, FileCollection>{};
// will return relationship between deletion tasks and others
final tasksAffectedByDeletion = <String, Set<String>>{};
for (var task in taskMap.values) {
final rc = task.runCondition;
if (rc is FilesCondition) {
inputsByTask[task] = rc.inputs;
outputsByTask[task] = rc.outputs;
deletionsByTask[task] = rc.deletions;
}
}
final dependencyErrors = <String>{};
final phaseErrors = <String>{};
// 1. a task's inputs may only include another's outputs if it depends on it
inputsByTask.forEach((task, ins) {
outputsByTask.forEach((otherTask, otherOuts) {
if (task.name == otherTask.name ||
task.dependencySet.contains(otherTask.name)) return;
final intersectInsOuts = ins.intersection(otherOuts);
if (intersectInsOuts.isNotEmpty) {
dependencyErrors
.add("Task '${task.name}' must dependOn '${otherTask.name}' "
'(clashing outputs: $intersectInsOuts)');
}
});
});
// 2. deletion tasks must be in a phase that precedes tasks whose
// inputs/outputs are deleted by them
inputsByTask.forEach((task, ins) {
deletionsByTask.forEach((deletionTask, deletedFiles) {
final outs = outputsByTask[task]!;
bool addErrorIfNotEmpty(Set<String> intersection, String io) {
if (intersection.isNotEmpty) {
tasksAffectedByDeletion.accumulate(task.name, deletionTask.name);
if (!task.phase.isAfter(deletionTask.phase)) {
phaseErrors.add("Task '${deletionTask.name}' "
"(phase '${deletionTask.phase.name}') deletes $io of "
"'${task.name}' (phase '${task.phase.name}'): $intersection");
return true;
}
}
return false;
}
if (!addErrorIfNotEmpty(ins.intersection(deletedFiles), 'inputs')) {
addErrorIfNotEmpty(outs.intersection(deletedFiles), 'outputs');
}
});
});
if (dependencyErrors.isNotEmpty || phaseErrors.isNotEmpty) {
final error = StringBuffer();
if (dependencyErrors.isNotEmpty) {
error.writeln("The following tasks have implicit dependencies due to "
"their inputs depending on other tasks' outputs:");
error.writeln(dependencyErrors.map((e) => ' * $e.').join('\n'));
error.writeln('\nPlease add the dependencies explicitly.');
}
if (phaseErrors.isNotEmpty) {
if (error.isNotEmpty) {
error.writeln();
}
error.writeln("The following tasks delete inputs or outputs of another "
"task that does not run on a later phase, hence could corrupt those "
"tasks execution:");
error.writeln(phaseErrors.map((e) => ' * $e.').join('\n'));
error.writeln('\nPlease change the task phases so that deletion tasks '
"run on earlier phases (typically 'setup') than other tasks.");
}
throw DartleException(message: error.toString());
}
return tasksAffectedByDeletion;
}