packages/requirements/tests/Unit/Test/ProcessRunnerTest.php
1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Test;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\Medium;
9use PHPUnit\Framework\Attributes\UsesClass;
10use PHPUnit\Framework\TestCase;
11use Requirements\Test\JUnit;
12use Requirements\Test\ProcessRunner;
13use Requirements\Test\RunnerConfig;
14use Requirements\Test\TestResult;
15use Tests\Fake\ProjectDirectory;
16
17#[CoversClass(ProcessRunner::class)]
18#[UsesClass(RunnerConfig::class)]
19#[UsesClass(JUnit::class)]
20#[UsesClass(TestResult::class)]
21#[Medium]
22final class ProcessRunnerTest extends TestCase
23{
24 public function testRunFailsOnTimeoutAndMissingReport(): void
25 {
26 $project = new ProjectDirectory();
27 $runner = new ProcessRunner();
28 $config = new RunnerConfig('custom', [PHP_BINARY, '-r', 'usleep(500000);'], $project->directory, 0.05);
29 self::assertSame('error', $runner->run($config, static fn (string $directory): array => [])->status);
30 $config = new RunnerConfig('custom', [PHP_BINARY, '-r', 'exit(0);'], $project->directory);
31 self::assertSame('error', $runner->run($config, static fn (string $directory): array => [])->status);
32 }
33
34 public function testRunReportsTheTimeoutMessage(): void
35 {
36 $project = new ProjectDirectory();
37 $config = new RunnerConfig('custom', [PHP_BINARY, '-r', 'usleep(500000);'], $project->directory, 0.05);
38 $result = (new ProcessRunner())->run($config, static fn (string $directory): array => []);
39 self::assertSame(0, $result->tests);
40 self::assertStringContainsString('exceeded the timeout of 0.05 seconds', $result->message);
41 }
42
43 public function testRunReportsTheOutputOfARunWithoutReports(): void
44 {
45 $project = new ProjectDirectory();
46 $config = new RunnerConfig('custom', [PHP_BINARY, '-r', 'echo "to stdout\n"; fwrite(STDERR, "to stderr\n");'], $project->directory);
47 self::assertSame(['status' => 'error', 'tests' => 0, 'message' => "No executed tests in fresh JUnit reports. to stdout\nto stderr"], (new ProcessRunner())->run($config, static fn (string $directory): array => [])->toArray());
48 }
49
50 public function testRunReadsTheReportWrittenToTheFreshDirectory(): void
51 {
52 $project = new ProjectDirectory();
53 $config = new RunnerConfig('custom', [PHP_BINARY, '-r', 'file_put_contents($argv[1], "<testsuite><testcase name=\"a\"/><testcase name=\"b\"/></testsuite>");'], $project->directory);
54 $result = (new ProcessRunner())->run($config, static fn (string $directory): array => [$directory . '/report.xml']);
55 self::assertSame(['status' => 'passed', 'tests' => 2, 'message' => ''], $result->toArray());
56 }
57
58 public function testRunAppendsTheArgumentsToTheCommand(): void
59 {
60 $project = new ProjectDirectory();
61 $config = new RunnerConfig('custom', [PHP_BINARY, '-r', 'file_put_contents($argv[2], "<testsuite><testcase name=\"" . $argv[1] . "\"/></testsuite>"); echo $argv[1];', 'fixed'], $project->directory);
62 $result = (new ProcessRunner())->run($config, static fn (string $directory): array => [$directory . '/report.xml']);
63 self::assertSame('passed', $result->status);
64 }
65
66 public function testRunRunsInTheConfiguredDirectory(): void
67 {
68 $project = new ProjectDirectory();
69 $config = new RunnerConfig('custom', [PHP_BINARY, '-r', 'file_put_contents("cwd.txt", "here"); file_put_contents($argv[1], "<testsuite><testcase/></testsuite>");'], $project->directory);
70 (new ProcessRunner())->run($config, static fn (string $directory): array => [$directory . '/report.xml']);
71 self::assertSame('here', $project->read('cwd.txt'));
72 }
73
74 public function testRunReadsEveryXmlReportAndIgnoresOtherFiles(): void
75 {
76 $project = new ProjectDirectory();
77 $config = new RunnerConfig('custom', [PHP_BINARY, '-r', 'file_put_contents($argv[1] . "/a.xml", "<testsuite><testcase/></testsuite>"); file_put_contents($argv[1] . "/b.xml", "<testsuite><testcase/></testsuite>"); file_put_contents($argv[1] . "/notes.txt", "<testsuite><testcase><failure/></testcase></testsuite>");'], $project->directory);
78 $result = (new ProcessRunner())->run($config, static fn (string $directory): array => [$directory]);
79 self::assertSame(['status' => 'passed', 'tests' => 2, 'message' => ''], $result->toArray());
80 }
81
82 public function testRunFailsWithTheOutputOfAFailingExit(): void
83 {
84 $project = new ProjectDirectory();
85 $config = new RunnerConfig('custom', [PHP_BINARY, '-r', 'file_put_contents($argv[1], "<testsuite><testcase/></testsuite>"); echo "broken\n"; exit(3);'], $project->directory);
86 $result = (new ProcessRunner())->run($config, static fn (string $directory): array => [$directory . '/report.xml']);
87 self::assertSame(['status' => 'failed', 'tests' => 1, 'message' => 'broken'], $result->toArray());
88 }
89
90 public function testRunRemovesTheReportDirectory(): void
91 {
92 $project = new ProjectDirectory();
93 $config = new RunnerConfig('custom', [PHP_BINARY, '-r', 'file_put_contents($argv[2] . "/report.xml", "<testsuite><testcase/></testsuite>"); file_put_contents($argv[2] . "/log.txt", "x"); file_put_contents($argv[1], $argv[2]);', $project->path('seen.txt')], $project->directory);
94 (new ProcessRunner())->run($config, static fn (string $directory): array => [$directory]);
95 self::assertStringStartsWith(sys_get_temp_dir() . '/requirements-', $project->read('seen.txt'));
96 self::assertDirectoryDoesNotExist($project->read('seen.txt'));
97 }
98
99 public function testRunReportsACommandThatCannotStart(): void
100 {
101 $project = new ProjectDirectory();
102 $config = new RunnerConfig('custom', [PHP_BINARY, '-r', 'exit(0);'], $project->path('missing'));
103 $result = (new ProcessRunner())->run($config, static fn (string $directory): array => []);
104 self::assertSame('error', $result->status);
105 self::assertSame(0, $result->tests);
106 self::assertStringContainsString('cwd', $result->message);
107 }
108}
109