runMatrix function

Future<int> runMatrix(
  1. List<SmfModule> modules, {
  2. required String directory,
  3. Map<String, String?> roleOptions = const {},
  4. MatrixAppTests appTests = const MatrixAppTests([]),
  5. MatrixSelection selection = const MatrixSelection(),
  6. MatrixCommands commands = const MatrixCommands(),
})

Generates every app of the matrixOf of modules with roleOptions in directory, with the options of CI, analyzes each with flutter analyze, and, in an app that some of the tests of appTests apply to, adds them and runs every test of the app with flutter test. Returns the exit code: 0 if every app was generated with every module and every step that the options of CI do not leave for later, has no issue and passes its tests, each of the tests applies to some app, they check the contract of their roles with every provider (see MatrixAppTests.roleProblems), and none of them reads the name or the options of an app (see MatrixAppTests.modeProblems); 1 otherwise.

It checks the apps of selection, every app of the matrix by default, such as those with some names, only the apps with every module, or a share of them (see MatrixSelection), and runs the appTests that apply to them. A problem of the selection, such as a name that no app of the matrix has, is a problem of the run too. The apps keep their numbers in the matrix when only some are checked.

A run for one app with every module by its name (NamedEveryModuleApp), as a job of CI checks its app of the plan, has a matrix with that app alone among the apps with every module. The other jobs check the others, so such a run holds the appTests against the matrix with all of them: a test that applies only to an app with every module that another job checks, such as one for another value of a mode option, is no problem there.

The apps stay in directory, with the tests. commands run for each app, and their log gets what happens.

Implementation

Future<int> runMatrix(
  List<SmfModule> modules, {
  required String directory,
  Map<String, String?> roleOptions = const {},
  MatrixAppTests appTests = const MatrixAppTests([]),
  MatrixSelection selection = const MatrixSelection(),
  MatrixCommands commands = const MatrixCommands(),
}) async {
  final run = _MatrixRun(
    directory: directory,
    appTests: appTests.tests,
    // coverage:ignore-start
    // The defaults print to the terminal, create the apps with smf and
    // analyze them with Flutter, as the runs of the matrix in CI do; the
    // tests give their own.
    say: commands.log ?? _print,
    create: commands.create ?? _smfCreate(modules),
    analyze:
        commands.analyze ?? (directory) => _flutter(['analyze'], directory),
    // coverage:ignore-end
    test: commands.test ?? runAppTests,
  );
  final (:apps, :failed) = await matrixOf(
    modules,
    roleOptions: roleOptions,
    everyModuleApps: selection.everyModuleApps,
  );
  final problems = [
    for (final result in failed)
      '${result.contractCase}: ${result.errors.join('; ')}',
    ...selection.problemsOf(apps),
  ];
  final checked = <MatrixApp>[];
  for (final (index, app) in selection.of(apps)) {
    checked.add(app);
    // An app keeps its number in the matrix when only some are checked.
    problems.addAll(await run.check(app, 'app_${index + 1}'));
  }
  // Tests that apply to no app would leave CI without saying so. Those of
  // the apps that are not checked run where the whole matrix is, and the
  // matrix of a run for one app with every module lacks the others.
  final all = switch (selection.everyModuleApps) {
    NamedEveryModuleApp() =>
      (await matrixOf(modules, roleOptions: roleOptions)).apps,
    _ => apps,
  };
  for (final test in appTests.tests) {
    if (!all.any(test.appliesTo)) {
      problems.add('The tests of ${test.directory} apply to no app.');
    }
  }
  problems
    ..addAll(appTests.roleProblems(modules, all))
    ..addAll(appTests.modeProblems(all));
  run.say('\n${checked.length} apps generated in $directory.');
  return _exitCode(problems, run.say);
}