perform method
Perform steps in order, and check what each did against what it said.
Every step is previewed immediately before it runs, and the two are compared. The preview taken here is the one used for the comparison — not one the caller took earlier — so the check is about whether the step kept its word, not about how long the caller spent deciding.
A step that reports something other than it claimed does not stop the run: it did do something, later steps may depend on it, and stopping would leave the list half-done with no record of it. The discrepancy is collected and the caller decides what it is worth.
A step that throws does stop the run, because nothing can be said about what follows it.
Implementation
Future<Execution> perform(Iterable<Step> steps) async {
final outcomes = <Outcome>[];
final discrepancies = <Discrepancy>[];
final byStep = <Step, Outcome>{};
var index = 0;
for (final step in steps) {
final claimed = step.preview();
final Outcome actual;
try {
actual = await step.perform(StepContext(byStep));
} on Object catch (error, stackTrace) {
return Execution(
outcomes: outcomes,
discrepancies: discrepancies,
failure: StepFailure(
index: index,
claimed: claimed,
error: error,
stackTrace: stackTrace,
),
);
}
// A pending value that came back null is an answer; one whose key never
// arrived is not, and the step after this one would read past it.
final missing = claimed.pending
.where((name) => !actual.values.containsKey(name))
.toList();
final discrepancy = Discrepancy(
index: index,
claimed: claimed,
actual: actual,
missingValues: missing,
);
if (discrepancy.actedDifferently || missing.isNotEmpty) {
discrepancies.add(discrepancy);
}
outcomes.add(actual);
byStep[step] = actual;
index++;
}
return Execution(outcomes: outcomes, discrepancies: discrepancies);
}