Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.33% covered (success)
93.33%
238 / 255
60.00% covered (warning)
60.00%
9 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
TestsCommand
93.33% covered (success)
93.33%
238 / 255
60.00% covered (warning)
60.00%
9 / 15
65.21
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 configure
100.00% covered (success)
100.00%
48 / 48
100.00% covered (success)
100.00%
1 / 1
1
 execute
97.33% covered (success)
97.33%
73 / 75
0.00% covered (danger)
0.00%
0 / 1
18
 resolveProcessResultContext
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 forceAgentReporter
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 resolveStructuredProcessResultPayload
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
6.01
 withStructuredCoverageValidationContext
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
4.01
 decodeStructuredProcessOutput
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
7.77
 resolvePath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDefaultTestsPath
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 normalizeProjectRelativePath
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 resolveBootstrapPath
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 resolveMinimumCoverage
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
5.03
 configureCoverageArguments
66.67% covered (warning)
66.67%
18 / 27
0.00% covered (danger)
0.00%
0 / 1
10.37
 validateMinimumCoverage
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5/**
6 * Fast Forward Development Tools for PHP projects.
7 *
8 * This file is part of fast-forward/dev-tools project.
9 *
10 * @author   Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
11 * @license  https://opensource.org/licenses/MIT MIT License
12 *
13 * @see      https://github.com/php-fast-forward/
14 * @see      https://github.com/php-fast-forward/dev-tools
15 * @see      https://github.com/php-fast-forward/dev-tools/issues
16 * @see      https://php-fast-forward.github.io/dev-tools/
17 * @see      https://datatracker.ietf.org/doc/html/rfc2119
18 */
19
20namespace FastForward\DevTools\Console\Command;
21
22use JsonException;
23use FastForward\DevTools\Console\Command\Traits\LogsCommandResults;
24use FastForward\DevTools\Console\Input\HasCacheOption;
25use FastForward\DevTools\Console\Input\HasJsonOption;
26use FastForward\DevTools\Composer\Json\ComposerJsonInterface;
27use FastForward\DevTools\Filesystem\FilesystemInterface;
28use FastForward\DevTools\Path\DevToolsPathResolver;
29use FastForward\DevTools\PhpUnit\Bootstrap\BootstrapShimGenerator;
30use FastForward\DevTools\PhpUnit\Coverage\CoverageSummaryLoaderInterface;
31use FastForward\DevTools\Process\ProcessBuilderInterface;
32use FastForward\DevTools\Process\ProcessQueueInterface;
33use FastForward\DevTools\Path\ManagedWorkspace;
34use FastForward\DevTools\Project\ProjectCapabilitiesResolverInterface;
35use InvalidArgumentException;
36use Psr\Log\LogLevel;
37use RuntimeException;
38use Symfony\Component\Config\FileLocatorInterface;
39use Symfony\Component\Console\Attribute\AsCommand;
40use Symfony\Component\Console\Command\Command;
41use Symfony\Component\Console\Input\InputArgument;
42use Symfony\Component\Console\Input\InputInterface;
43use Symfony\Component\Console\Input\InputOption;
44use Symfony\Component\Console\Output\BufferedOutput;
45use Symfony\Component\Console\Output\OutputInterface;
46use Symfony\Component\Process\Process;
47
48use function is_numeric;
49use function Safe\json_decode;
50use function Safe\preg_match;
51
52/**
53 * Facilitates the execution of the PHPUnit testing framework.
54 * This class MUST NOT be overridden and SHALL configure testing parameters dynamically.
55 */
56#[AsCommand(name: 'reports:tests', description: 'Runs PHPUnit tests.', aliases: ['phpunit', 'tests'])]
57 class TestsCommand extends Command
58{
59    use HasCacheOption;
60    use HasJsonOption;
61    use LogsCommandResults;
62
63    private const string AGENT_ENVIRONMENT_VARIABLE = 'AI_AGENT';
64
65    private const string AGENT_ENVIRONMENT_VALUE = 'fast-forward/dev-tools';
66
67    private const string PROCESS_LABEL = 'Running PHPUnit Tests';
68
69    /**
70     * @var string identifies the local configuration file for PHPUnit processes
71     */
72    public const string CONFIG = 'phpunit.xml';
73
74    /**
75     * @param CoverageSummaryLoaderInterface $coverageSummaryLoader the loader used for `coverage-php` summaries
76     * @param ComposerJsonInterface $composer the composer.json reader for autoload information
77     * @param FilesystemInterface $filesystem the filesystem utility used for path resolution
78     * @param BootstrapShimGenerator $bootstrapShimGenerator the generator used to build the PHPUnit bootstrap shim
79     * @param FileLocatorInterface $fileLocator the file locator used to resolve PHPUnit configuration
80     * @param ProcessBuilderInterface $processBuilder the builder used to assemble the PHPUnit process
81     * @param ProcessQueueInterface $processQueue the queue used to execute PHPUnit
82     * @param ProjectCapabilitiesResolverInterface $projectCapabilitiesResolver the project capability resolver
83     */
84    public function __construct(
85        private  CoverageSummaryLoaderInterface $coverageSummaryLoader,
86        private  ComposerJsonInterface $composer,
87        private  FilesystemInterface $filesystem,
88        private  BootstrapShimGenerator $bootstrapShimGenerator,
89        private  FileLocatorInterface $fileLocator,
90        private  ProcessBuilderInterface $processBuilder,
91        private  ProcessQueueInterface $processQueue,
92        private  ProjectCapabilitiesResolverInterface $projectCapabilitiesResolver,
93    ) {
94        parent::__construct();
95    }
96
97    /**
98     * Configures the testing command input constraints.
99     *
100     * The method MUST specify valid arguments for testing paths, caching directories,
101     * bootstrap scripts, and coverage instructions. It SHALL align with robust testing standards.
102     *
103     * @return void
104     */
105    protected function configure(): void
106    {
107        $this->setHelp('This command runs PHPUnit to execute your tests.');
108        $this
109            ->addJsonOption()
110            ->addCacheOption('Whether to enable PHPUnit result caching.')
111            ->addCacheDirOption(
112                description: 'Path to the PHPUnit cache directory.',
113                default: ManagedWorkspace::getCacheDirectory(ManagedWorkspace::PHPUNIT),
114            )
115            ->addArgument(
116                name: 'path',
117                mode: InputArgument::OPTIONAL,
118                description: 'Path to the tests directory.',
119                default: ProjectCapabilitiesResolverInterface::DEFAULT_TESTS_PATH,
120            )
121            ->addOption(
122                name: 'bootstrap',
123                shortcut: 'b',
124                mode: InputOption::VALUE_OPTIONAL,
125                description: 'Path to the bootstrap file.',
126                default: './vendor/autoload.php',
127            )
128            ->addOption(
129                name: 'coverage',
130                shortcut: 'c',
131                mode: InputOption::VALUE_OPTIONAL,
132                description: 'Whether to generate code coverage reports.',
133            )
134            ->addOption(
135                name: 'coverage-summary',
136                mode: InputOption::VALUE_NONE,
137                description: 'Whether to show only the summary for text coverage output.',
138            )
139            ->addOption(
140                name: 'filter',
141                shortcut: 'f',
142                mode: InputOption::VALUE_OPTIONAL,
143                description: 'Filter which tests to run based on a pattern.',
144            )
145            ->addOption(
146                name: 'min-coverage',
147                mode: InputOption::VALUE_REQUIRED,
148                description: 'Minimum line coverage percentage required for a successful run.',
149            )
150            ->addOption(
151                name: 'progress',
152                mode: InputOption::VALUE_NONE,
153                description: 'Whether to enable progress output from PHPUnit.',
154            );
155    }
156
157    /**
158     * Triggers the PHPUnit engine based on resolved paths and settings.
159     *
160     * The method MUST assemble the necessary commands to initiate PHPUnit securely.
161     * It SHOULD optionally construct advanced configuration arguments such as caching and coverage.
162     *
163     * @param InputInterface $input the runtime instruction set from the CLI
164     * @param OutputInterface $output the console feedback relay
165     *
166     * @return int the status integer describing the termination code
167     */
168    protected function execute(InputInterface $input, OutputInterface $output): int
169    {
170        $structuredOutput = $this->isJsonOutput($input);
171        $processOutput = $structuredOutput ? new BufferedOutput() : $output;
172        $cacheEnabled = $this->isCacheEnabled($input);
173
174        $this->log('Running PHPUnit tests...', $input);
175
176        try {
177            $minimumCoverage = $this->resolveMinimumCoverage($input);
178        } catch (InvalidArgumentException $invalidArgumentException) {
179            return $this->failure($invalidArgumentException->getMessage(), $input, [
180                'output' => $processOutput,
181            ]);
182        }
183
184        $testsPath = (string) $input->getArgument('path');
185        $projectCapabilities = $this->projectCapabilitiesResolver->resolve(testsPath: $testsPath);
186
187        if (! $projectCapabilities->hasTestsPath() && ! $this->isDefaultTestsPath($testsPath)) {
188            return $this->failure('Tests path not found: {path}', $input, [
189                'output' => $processOutput,
190                'path' => $this->filesystem->getAbsolutePath($testsPath),
191            ]);
192        }
193
194        if (! $projectCapabilities->canRunTests()) {
195            return $this->success(
196                'Skipping PHPUnit tests because no tests directory or PHP source files were detected.',
197                $input,
198                [
199                    'output' => $processOutput,
200                ],
201                LogLevel::WARNING,
202            );
203        }
204
205        $processBuilder = $this->processBuilder
206            ->withArgument('--configuration', $this->fileLocator->locate(self::CONFIG))
207            ->withArgument('--bootstrap', $this->resolveBootstrapPath($input))
208            ->withArgument('--display-deprecations')
209            ->withArgument('--display-phpunit-deprecations')
210            ->withArgument('--display-incomplete')
211            ->withArgument('--display-skipped');
212
213        if (! $input->getOption('progress') || $structuredOutput) {
214            $processBuilder = $processBuilder->withArgument('--no-progress');
215        }
216
217        if (! $structuredOutput) {
218            $processBuilder = $processBuilder->withArgument('--colors=always');
219        }
220
221        if ($cacheEnabled) {
222            $processBuilder = $processBuilder->withArgument(
223                '--cache-result',
224            )->withArgument('--cache-directory', $this->resolvePath($input, 'cache-dir'));
225        } else {
226            $processBuilder = $processBuilder->withArgument('--do-not-cache-result');
227        }
228
229        [$processBuilder, $coverageReportPath] = $this->configureCoverageArguments(
230            $input,
231            $processBuilder,
232            null !== $minimumCoverage,
233        );
234
235        if ($input->getOption('filter')) {
236            $processBuilder = $processBuilder->withArgument('--filter', $input->getOption('filter'));
237        }
238
239        $process = $processBuilder
240            ->withArgument($input->getArgument('path'))
241            ->build([DevToolsPathResolver::getPreferredToolBinaryPath('phpunit')]);
242
243        if ($structuredOutput) {
244            $this->forceAgentReporter($process);
245        }
246
247        $this->processQueue->add(process: $process, label: self::PROCESS_LABEL);
248
249        $result = $this->processQueue->run($processOutput);
250        $processResultContext = $this->resolveProcessResultContext($processOutput, $result, $structuredOutput);
251
252        if (self::SUCCESS !== $result || null === $minimumCoverage || null === $coverageReportPath) {
253            if (self::SUCCESS === $result) {
254                return $this->success('PHPUnit tests completed successfully.', $input, $processResultContext);
255            }
256
257            return $this->failure('PHPUnit tests failed.', $input, $processResultContext);
258        }
259
260        [$validationResult, $message, $coverageContext] = $this->validateMinimumCoverage(
261            $coverageReportPath,
262            $minimumCoverage,
263        );
264
265        if ($structuredOutput) {
266            $processResultContext = $this->withStructuredCoverageValidationContext(
267                $processResultContext,
268                $coverageContext,
269                $minimumCoverage,
270                $validationResult,
271                $message,
272            );
273        }
274
275        if (self::SUCCESS === $validationResult) {
276            return $this->success($message, $input, [...$processResultContext, ...$coverageContext]);
277        }
278
279        return $this->failure($message, $input, [...$processResultContext, ...$coverageContext]);
280    }
281
282    /**
283     * Builds structured context for the executed PHPUnit process.
284     *
285     * @param OutputInterface $processOutput the output sink used while the process ran
286     * @param int $exitCode the exit code returned by the process queue
287     * @param bool $structuredOutput whether the command captured subprocess output for structured logging
288     *
289     * @return array<string, array<string, mixed>|OutputInterface>
290     */
291    private function resolveProcessResultContext(
292        OutputInterface $processOutput,
293        int $exitCode,
294        bool $structuredOutput,
295    ): array {
296        if ($structuredOutput) {
297            return [
298                'output' => $this->resolveStructuredProcessResultPayload($processOutput, $exitCode),
299            ];
300        }
301
302        return [
303            'output' => $processOutput,
304        ];
305    }
306
307    /**
308     * Forces the PHPUnit subprocess to expose the agent reporter payload.
309     *
310     * @param Process $process the configured PHPUnit process
311     *
312     * @return void
313     */
314    private function forceAgentReporter(Process $process): void
315    {
316        $env = $process->getEnv();
317
318        if (\array_key_exists(self::AGENT_ENVIRONMENT_VARIABLE, $env) || false !== getenv(
319            self::AGENT_ENVIRONMENT_VARIABLE
320        )) {
321            return;
322        }
323
324        // Intentionally reuse the reporter's existing agent detection path for
325        // structured DevTools output instead of maintaining a separate
326        // integration that would need to mirror the plugin behavior.
327        $env[self::AGENT_ENVIRONMENT_VARIABLE] = self::AGENT_ENVIRONMENT_VALUE;
328        $process->setEnv($env);
329    }
330
331    /**
332     * Builds the structured payload that will be emitted for agent-oriented runs.
333     *
334     * @param OutputInterface $processOutput the output sink used while the process ran
335     * @param int $exitCode the exit code returned by the process queue
336     *
337     * @return array<string, mixed> the structured process payload
338     */
339    private function resolveStructuredProcessResultPayload(OutputInterface $processOutput, int $exitCode): array
340    {
341        $payload = [
342            'result' => self::SUCCESS === $exitCode ? 'success' : 'failure',
343        ];
344
345        if (! $processOutput instanceof BufferedOutput) {
346            return $payload;
347        }
348
349        $rawOutput = trim($processOutput->fetch());
350
351        if ('' === $rawOutput) {
352            return $payload;
353        }
354
355        [$decoded, $supplementalOutput] = $this->decodeStructuredProcessOutput($rawOutput);
356
357        if (\is_array($decoded)) {
358            $payload = [
359                ...$decoded,
360                'result' => $payload['result'],
361            ];
362        }
363
364        if (null !== $supplementalOutput) {
365            $payload['raw_output'] = $supplementalOutput;
366        }
367
368        return $payload;
369    }
370
371    /**
372     * Appends minimum-coverage validation data to the structured PHPUnit output payload.
373     *
374     * @param array<string, mixed> $context the command result context
375     * @param array<string, float|int|string|null> $coverageContext structured coverage metrics
376     * @param float $minimumCoverage the required coverage percentage
377     * @param int $validationResult the post-PHPUnit validation status
378     * @param string $message the validation message
379     *
380     * @return array<string, mixed> the enriched structured command context
381     */
382    private function withStructuredCoverageValidationContext(
383        array $context,
384        array $coverageContext,
385        float $minimumCoverage,
386        int $validationResult,
387        string $message,
388    ): array {
389        if (! isset($context['output']) || ! \is_array($context['output'])) {
390            return $context;
391        }
392
393        $payload = $context['output'];
394        $payload['coverage'] = [
395            ...$coverageContext,
396            'minimum' => $minimumCoverage,
397        ];
398
399        if (self::SUCCESS !== $validationResult) {
400            $payload['message'] = $message;
401            $payload['result'] = 'failure';
402        }
403
404        $context['output'] = $payload;
405
406        return $context;
407    }
408
409    /**
410     * Attempts to decode structured PHPUnit output while preserving any
411     * non-JSON prelude that was emitted before the final reporter payload.
412     *
413     * @param string $rawOutput the captured subprocess output
414     *
415     * @return array{array<string, mixed>|null, string|null} decoded payload and preserved supplemental output
416     */
417    private function decodeStructuredProcessOutput(string $rawOutput): array
418    {
419        try {
420            $decoded = json_decode($rawOutput, true);
421
422            return [\is_array($decoded) ? $decoded : null, null];
423        } catch (JsonException) {
424        }
425
426        if (1 !== preg_match('/^(?P<prefix>.*?)(?P<payload>\{\s*"result".*)$/s', $rawOutput, $matches)) {
427            return [null, $rawOutput];
428        }
429
430        try {
431            $decoded = json_decode($matches['payload'], true);
432        } catch (JsonException) {
433            return [null, $rawOutput];
434        }
435
436        if (! \is_array($decoded)) {
437            return [null, $rawOutput];
438        }
439
440        $prefix = trim($matches['prefix']);
441
442        return [$decoded, '' === $prefix ? null : $prefix];
443    }
444
445    /**
446     * Safely constructs an absolute path tied to a defined capability option.
447     *
448     * The method MUST compute absolute properties based on the supplied input parameters.
449     * It SHALL strictly return a securely bounded path string.
450     *
451     * @param InputInterface $input the raw parameter definitions
452     * @param string $option the requested option key to resolve
453     *
454     * @return string validated absolute path string
455     */
456    private function resolvePath(InputInterface $input, string $option): string
457    {
458        return $this->filesystem->getAbsolutePath($input->getOption($option));
459    }
460
461    /**
462     * Detects whether a tests path option still points at the default project tests directory.
463     *
464     * @param string $testsPath the tests path argument received from the CLI
465     *
466     * @return bool true when the provided path is equivalent to the default tests directory
467     */
468    private function isDefaultTestsPath(string $testsPath): bool
469    {
470        return $this->normalizeProjectRelativePath($testsPath) === $this->normalizeProjectRelativePath(
471            ProjectCapabilitiesResolverInterface::DEFAULT_TESTS_PATH
472        );
473    }
474
475    /**
476     * Normalizes a project-relative path for resilient default-option comparisons.
477     *
478     * @param string $path the project-relative path to normalize
479     *
480     * @return string the normalized project-relative path
481     */
482    private function normalizeProjectRelativePath(string $path): string
483    {
484        $normalizedPath = str_replace('\\', '/', $path);
485
486        while (str_starts_with($normalizedPath, './')) {
487            $normalizedPath = substr($normalizedPath, 2);
488        }
489
490        return rtrim($normalizedPath, '/');
491    }
492
493    /**
494     * Creates the bootstrap shim path passed to PHPUnit.
495     *
496     * @param InputInterface $input the raw parameter definitions
497     *
498     * @return string the generated bootstrap shim path
499     */
500    private function resolveBootstrapPath(InputInterface $input): string
501    {
502        return $this->bootstrapShimGenerator->generate(
503            $this->resolvePath($input, 'bootstrap'),
504            $this->resolvePath($input, 'cache-dir'),
505        );
506    }
507
508    /**
509     * @param InputInterface $input the raw parameter definitions
510     *
511     * @return float|null the validated minimum coverage percentage, if configured
512     */
513    private function resolveMinimumCoverage(InputInterface $input): ?float
514    {
515        $minimumCoverage = $input->getOption('min-coverage');
516
517        if (null === $minimumCoverage) {
518            return null;
519        }
520
521        if (! is_numeric($minimumCoverage)) {
522            throw new InvalidArgumentException('The --min-coverage option MUST be a numeric percentage.');
523        }
524
525        $minimumCoverage = (float) $minimumCoverage;
526
527        if (0.0 > $minimumCoverage || 100.0 < $minimumCoverage) {
528            throw new InvalidArgumentException('The --min-coverage option MUST be between 0 and 100.');
529        }
530
531        return $minimumCoverage;
532    }
533
534    /**
535     * @param InputInterface $input the raw parameter definitions
536     * @param ProcessBuilderInterface $processBuilder the process builder to extend with coverage arguments
537     * @param bool $requiresCoverageReport indicates whether a `coverage-php` report is required
538     *
539     * @return array{ProcessBuilderInterface, string|null} the extended builder and generated `coverage-php` report path
540     */
541    private function configureCoverageArguments(
542        InputInterface $input,
543        ProcessBuilderInterface $processBuilder,
544        bool $requiresCoverageReport,
545    ): array {
546        $coverageOption = $input->getOption('coverage');
547
548        if (null === $coverageOption && ! $requiresCoverageReport) {
549            return [$processBuilder, null];
550        }
551
552        $coveragePath = null !== $coverageOption
553            ? $this->resolvePath($input, 'coverage')
554            : (
555                $this->isCacheEnabled($input)
556                    ? $this->resolvePath($input, 'cache-dir')
557                    : ManagedWorkspace::getOutputDirectory(ManagedWorkspace::COVERAGE)
558            );
559
560        foreach ($this->composer->getAutoload('psr-4') as $path) {
561            $processBuilder = $processBuilder->withArgument(
562                '--coverage-filter',
563                $this->filesystem->getAbsolutePath($path)
564            );
565        }
566
567        if (null !== $coverageOption) {
568            $processBuilder = $processBuilder
569                ->withArgument('--coverage-text')
570                ->withArgument('--coverage-html', $coveragePath)
571                ->withArgument('--testdox-html', $coveragePath . '/testdox.html')
572                ->withArgument('--coverage-clover', $coveragePath . '/clover.xml')
573                ->withArgument('--log-junit', $coveragePath . '/junit.xml');
574
575            if ($input->getOption('coverage-summary')) {
576                $processBuilder = $processBuilder->withArgument('--only-summary-for-coverage-text');
577            }
578        }
579
580        $coverageReportPath = $coveragePath . '/coverage.php';
581        $processBuilder = $processBuilder->withArgument('--coverage-php', $coverageReportPath);
582
583        return [$processBuilder, $coverageReportPath];
584    }
585
586    /**
587     * @param string $coverageReportPath the generated `coverage-php` report path
588     * @param float $minimumCoverage the required line coverage percentage
589     *
590     * @return array{int, string, array<string, float|int|string|null>} validation result, human message, and structured coverage context
591     */
592    private function validateMinimumCoverage(string $coverageReportPath, float $minimumCoverage): array
593    {
594        try {
595            $coverageSummary = $this->coverageSummaryLoader->load($coverageReportPath);
596        } catch (RuntimeException $runtimeException) {
597            return [
598                self::FAILURE,
599                $runtimeException->getMessage(),
600                [
601                    'line_coverage' => null,
602                    'covered_lines' => null,
603                    'total_lines' => null,
604                ],
605            ];
606        }
607
608        $message = \sprintf(
609            'Minimum line coverage of %01.2F%% %s. Current coverage: %s (%d/%d lines).',
610            $minimumCoverage,
611            $coverageSummary->percentage() >= $minimumCoverage ? 'satisfied' : 'was not met',
612            $coverageSummary->percentageAsString(),
613            $coverageSummary->executedLines(),
614            $coverageSummary->executableLines(),
615        );
616
617        return [
618            $coverageSummary->percentage() >= $minimumCoverage ? self::SUCCESS : self::FAILURE,
619            $message,
620            [
621                'line_coverage' => $coverageSummary->percentage(),
622                'covered_lines' => $coverageSummary->executedLines(),
623                'total_lines' => $coverageSummary->executableLines(),
624            ],
625        ];
626    }
627}