runEnsembleYamlTestsCli function

Future<void> runEnsembleYamlTestsCli(
  1. List<String> arguments
)

Runs declarative YAML tests in an Ensemble app.

The host app must list ensemble_test_runner in dev_dependencies.

Options: --app-dir=

Implementation

Future<void> runEnsembleYamlTestsCli(List<String> arguments) async {
  final verbose = isVerboseCli(arguments);
  final quiet = arguments.contains('--quiet');
  final reportMode = _resolveReportMode(arguments);
  final jsonReport = reportMode == 'json';
  final junitReport = reportMode == 'junit';
  final machineReport = jsonReport || junitReport;
  final streamLiveOutput = !quiet && !machineReport;
  final reportFile = _resolveReportFile(arguments);
  final appDir = _resolveAppDir(arguments);
  final timeoutSeconds = _resolveTimeoutSeconds(arguments);
  final jobs = _resolveJobsOverride(arguments);
  final patcher = YamlTestAppPatcher(appDir);

  if (!Directory(appDir).existsSync()) {
    stderr.writeln('App directory not found: $appDir');
    exit(2);
  }

  if (!File('${appDir}/pubspec.yaml').existsSync()) {
    stderr.writeln('No pubspec.yaml in $appDir');
    exit(2);
  }

  if (arguments.contains('--doctor')) {
    final result = await EnsembleTestDoctor(appDir).run();
    stdout.writeln(result.lines.join('\n'));
    exit(result.hasErrors ? 1 : 0);
  }

  if (arguments.contains('--inspect-app')) {
    try {
      stdout.writeln(EnsembleAppInspector(appDir).inspect().toPrettyJson());
      exit(0);
    } on StateError catch (error) {
      stderr.writeln(error.message);
      exit(2);
    }
  }

  if (arguments.contains('--validate-only')) {
    final result = EnsembleTestValidator(appDir).validate();
    stdout.writeln(jsonReport ? result.toPrettyJson() : result.formatText());
    exit(result.hasErrors ? 2 : 0);
  }

  if (arguments.any((arg) =>
      arg == '--scaffold-test' || arg.startsWith('--scaffold-test='))) {
    try {
      final result = EnsembleTestScaffold(appDir).create(arguments);
      stdout.writeln(
        result.created
            ? 'Created ${result.path}'
            : 'Test file already exists: ${result.path}',
      );
      exit(result.created ? 0 : 2);
    } on StateError catch (error) {
      stderr.writeln(error.message);
      exit(2);
    }
  }

  final testsDirRelative = patcher.testsDirRelative;
  if (testsDirRelative == null) {
    stderr.writeln(
      'Could not find definitions.local.path in ensemble/ensemble-config.yaml.\n'
      'Declarative tests must live under definitions.local.path/tests.',
    );
    exit(2);
  }

  if (!patcher.hasTestYamlOnDisk) {
    stderr.writeln(
      'No declarative tests found. Add *.test.yaml files under '
      '$testsDirRelative/',
    );
    exit(2);
  }

  final artifactLock = _RunArtifactLock(appDir);
  if (!artifactLock.acquire()) {
    stderr.writeln(
      'Another Ensemble test run is already using the app artifact directory. '
      'Wait for it to finish before starting another run.',
    );
    exit(3);
  }

  var exitCode = 0;
  try {
    _writeStatus(
      'Preparing Ensemble YAML tests...',
      quiet: quiet,
      machineReport: machineReport,
    );
    patcher.enable();

    if (patcher.pubspecChanged) {
      _writeStatus(
        'Resolving Flutter dependencies...',
        quiet: quiet,
        machineReport: machineReport,
      );
      final pubGet = await _runProcess(
        'flutter',
        ['pub', 'get', '--suppress-analytics'],
        workingDirectory: appDir,
      );
      if (pubGet.exitCode != 0 || verbose) {
        _writeProcessStreams(pubGet);
      }
      if (pubGet.exitCode != 0) {
        exitCode = 2;
        return;
      }
    }

    _writeStatus(
      'Running Ensemble YAML tests...',
      quiet: quiet,
      machineReport: machineReport,
    );

    _cleanParallelRunArtifacts(appDir);

    if (!machineReport) {
      try {
        await _withHtmlReport(
          appDir,
          const EnsembleTestRunResult(results: []),
          wallTimeMs: 0,
          isSuiteRunning: true,
        );
      } catch (_) {}
    }
    final runSerial = jobs == 1;
    final serialWorkingDirectory = runSerial && patcher.hasTimerRewrites
        ? _prepareWorkerDirectory(appDir, 0, patcher)
        : appDir;
    final serialServiceOverrides = runSerial
        ? await _resolveServiceOverrides(
            patcher: patcher,
            usedPorts: <int>{},
          )
        : null;
    final testRun = runSerial
        ? await _runFlutterTestProcess(
            'flutter',
            _buildFlutterTestArgs(
              arguments,
              reportMode: reportMode,
              reportFile: reportFile,
              timeoutSeconds: timeoutSeconds,
              verbose: verbose,
              appLogPath: patcher.hasTimerRewrites
                  ? _appConsoleLogFile(appDir)
                  : _appConsoleLogPath(),
              appLogDisplayPath:
                  patcher.hasTimerRewrites ? _appConsoleLogPath() : null,
              artifactRoot:
                  patcher.hasTimerRewrites ? _artifactRootPath(appDir) : null,
              serviceOverrides: serialServiceOverrides,
            ),
            workingDirectory: serialWorkingDirectory,
            streamOutput: streamLiveOutput,
            verbose: verbose,
            appLogFile: _appConsoleLogFile(appDir),
          )
        : await _runParallelFlutterTests(
            arguments,
            appDir: appDir,
            patcher: patcher,
            jobs: jobs,
            reportMode: reportMode,
            reportFile: reportFile,
            timeoutSeconds: timeoutSeconds,
            quiet: quiet,
            machineReport: machineReport,
          );

    if (testRun.exitCode != 0 && !verbose) {
      final output = '${testRun.stdout ?? ''}\n${testRun.stderr ?? ''}';
      final rawJson = extractJsonReport(output);
      final json = jsonReport ? rawJson : '';
      final junit = junitReport ? extractJunitReport(output) : '';
      final report = machineReport
          ? ''
          : extractSuiteReport(
              output,
              includeScreenTracker: !streamLiveOutput,
            );
      final knownFailure = extractKnownFailure(output);
      if (junit.isNotEmpty) {
        stdout.writeln(junit);
      } else if (json.isNotEmpty) {
        stdout.writeln(json);
      } else if (report.isNotEmpty) {
        stdout.write(report);
        if (!report.endsWith('\n')) stdout.writeln();
      } else if (knownFailure.isNotEmpty) {
        stderr.writeln(knownFailure);
      } else {
        _writeProcessStreams(testRun);
      }
    } else if (!verbose && testRun.exitCode != 0) {
      _writeProcessStreams(testRun);
    } else if (verbose) {
      // Output was already streamed live.
    } else {
      final out = testRun.stdout?.toString() ?? '';
      final rawJson = extractJsonReport(out);
      final json = jsonReport ? rawJson : '';
      final junit = junitReport ? extractJunitReport(out) : '';
      final report = junit.isNotEmpty
          ? junit
          : json.isNotEmpty
              ? json
              : extractSuiteReport(
                  out,
                  includeScreenTracker: !streamLiveOutput,
                );
      if (report.isNotEmpty) {
        stdout.write(report);
        if (!report.endsWith('\n')) stdout.writeln();
      } else {
        _writeProcessStreams(testRun);
      }
      final err = testRun.stderr?.toString() ?? '';
      if (err.isNotEmpty && !isBenignFlutterTestStderr(err)) {
        stderr.write(err);
      }
    }

    final machineResult = _runResultFromProcessOutput(testRun);
    if (runSerial && machineResult != null) {
      if (machineReport) {
        await _recordHistory(appDir, machineResult);
      } else {
        await _withHtmlReport(appDir, machineResult);
      }
    }
    exitCode = machineResult == null
        ? (testRun.exitCode == 0 ? 0 : 1)
        : (machineResult.failedCount == 0 ? 0 : 1);
  } on StateError catch (error) {
    stderr.writeln(error.message);
    exitCode = 3;
  } finally {
    // Serial timer-rewrite runs also use a worker sandbox; always drop leftovers.
    _cleanWorkerDirectories(appDir);
    patcher.restore();
    artifactLock.release();
  }
  exit(exitCode);
}