packages/requirements/tests/Unit/Test/BehatRunnerTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Test;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\DataProvider;
9use PHPUnit\Framework\Attributes\Medium;
10use PHPUnit\Framework\Attributes\UsesClass;
11use PHPUnit\Framework\TestCase;
12use Requirements\Test\BehatRunner;
13use Requirements\Test\JUnit;
14use Requirements\Test\ProcessRunner;
15use Requirements\Test\RunnerConfig;
16use Requirements\Test\TestResult;
17use Tests\Fake\BehatSuite;
18use Tests\Fake\ProjectDirectory;
19
20#[CoversClass(BehatRunner::class)]
21#[UsesClass(ProcessRunner::class)]
22#[UsesClass(RunnerConfig::class)]
23#[UsesClass(JUnit::class)]
24#[UsesClass(TestResult::class)]
25#[Medium]
26final class BehatRunnerTest extends TestCase
27{
28    #[DataProvider('providerRunRunsScenarioAndOutlineAndRejectsUndefinedOrMissingTests')]
29    public function testRunRunsScenarioAndOutlineAndRejectsUndefinedOrMissingTests(int $line, string $status, int $tests): void
30    {
31        $project = new ProjectDirectory();
32        $command = BehatSuite::write($project->directory);
33        self::assertNotEmpty($command);
34        $config = new RunnerConfig('behat', $command, $project->directory);
35        $result = (new BehatRunner())->run($config, 'example.feature:' . $line);
36        self::assertSame($status, $result->status, $result->message);
37        self::assertSame($tests, $result->tests, $result->message);
38    }
39
40    /**
41     * @return list<array{int, string, int}>
42     */
43    public static function providerRunRunsScenarioAndOutlineAndRejectsUndefinedOrMissingTests(): array
44    {
45        return [[2, 'passed', 1], [4, 'failed', 1], [6, 'failed', 1], [8, 'passed', 2], [3, 'error', 0], [99, 'error', 0]];
46    }
47
48    public function testRunAcceptsAnAbsoluteFeaturePath(): void
49    {
50        $project = new ProjectDirectory();
51        $command = BehatSuite::write($project->directory);
52        self::assertNotEmpty($command);
53        $config = new RunnerConfig('behat', $command, $project->directory);
54        $result = (new BehatRunner())->run($config, $project->path('example.feature') . ':2');
55        self::assertSame('passed', $result->status, $result->message);
56        self::assertSame(1, $result->tests, $result->message);
57    }
58
59    #[DataProvider('providerRunRejectsMalformedTargets')]
60    public function testRunRejectsMalformedTargets(string $target): void
61    {
62        $project = new ProjectDirectory();
63        $project->put('-example.feature', "Feature: x\n  Scenario: y\n");
64        $config = new RunnerConfig('behat', [PHP_BINARY, '-r', 'file_put_contents("ran.txt", "ran");'], $project->directory);
65        self::assertSame(['status' => 'error', 'tests' => 0, 'message' => 'Behat targets must select one scenario by file.feature:line.'], (new BehatRunner())->run($config, $target)->toArray());
66        self::assertFileDoesNotExist($project->path('ran.txt'));
67    }
68
69    /**
70     * @return array<string, array{string}>
71     */
72    public static function providerRunRejectsMalformedTargets(): array
73    {
74        return [
75            'no line' => ['example.feature'],
76            'empty line' => ['example.feature:'],
77            'line zero' => ['example.feature:0'],
78            'leading zero' => ['example.feature:02'],
79            'not a feature' => ['example.txt:2'],
80            'no file name' => [':2'],
81            'option' => ['-example.feature:2'],
82            'trailing newline' => ["example.feature:2\n"],
83            'line range' => ['example.feature:2-4'],
84        ];
85    }
86
87    /**
88     * @param array<string, string> $files
89     */
90    #[DataProvider('providerRunRejectsLinesWithoutAScenarioHeader')]
91    public function testRunRejectsLinesWithoutAScenarioHeader(array $files, string $target): void
92    {
93        $project = new ProjectDirectory();
94        array_map($project->put(...), array_keys($files), $files);
95        $config = new RunnerConfig('behat', [PHP_BINARY, '-r', 'file_put_contents("ran.txt", "ran");'], $project->directory);
96        self::assertSame(['status' => 'error', 'tests' => 0, 'message' => 'The target line must be an English Scenario or Scenario Outline header.'], (new BehatRunner())->run($config, $target)->toArray());
97        self::assertFileDoesNotExist($project->path('ran.txt'));
98    }
99
100    /**
101     * @return array<string, array{array<string, string>, string}>
102     */
103    public static function providerRunRejectsLinesWithoutAScenarioHeader(): array
104    {
105        $feature = "Feature: Headers\n  Scenario:\n  Scenario:   \n  Background: setup\n  Scenarios: x\n  # Scenario: commented\n  Szenario: German\n";
106        return [
107            'missing file' => [[], 'missing.feature:1'],
108            'directory' => [['example.feature/inner.txt' => 'x'], 'example.feature:1'],
109            'feature line' => [['example.feature' => $feature], 'example.feature:1'],
110            'untitled scenario' => [['example.feature' => $feature], 'example.feature:2'],
111            'blank title' => [['example.feature' => $feature], 'example.feature:3'],
112            'background' => [['example.feature' => $feature], 'example.feature:4'],
113            'plural keyword' => [['example.feature' => $feature], 'example.feature:5'],
114            'comment' => [['example.feature' => $feature], 'example.feature:6'],
115            'other language' => [['example.feature' => $feature], 'example.feature:7'],
116            'after the last line' => [['example.feature' => $feature], 'example.feature:8'],
117        ];
118    }
119
120    public function testRunReadsTheHeaderAtTheExactLine(): void
121    {
122        $project = new ProjectDirectory();
123        $project->put('example.feature', "Feature: Offsets\nScenario: first\n  Given a step\n");
124        $project->put('report.xml', '<testsuite><testcase/></testsuite>');
125        $config = new RunnerConfig('behat', [PHP_BINARY, '-r', 'copy("report.xml", $argv[array_search("--out", $argv, true) + 1] . "/report.xml");', '--'], $project->directory);
126        self::assertSame(['status' => 'passed', 'tests' => 1, 'message' => ''], (new BehatRunner())->run($config, 'example.feature:2')->toArray());
127    }
128
129    public function testRunPassesTheTargetAfterTheOptions(): void
130    {
131        $project = new ProjectDirectory();
132        $project->put('example.feature', "Feature: Arguments\n  Scenario Outline: examples\n");
133        $config = new RunnerConfig('behat', [PHP_BINARY, '-r', 'file_put_contents("args.txt", implode(" ", array_slice($argv, 1)));', '--'], $project->directory);
134        (new BehatRunner())->run($config, 'example.feature:2');
135        self::assertMatchesRegularExpression('~^--strict --no-interaction --no-snippets --format junit --out /\S+ -- example\.feature:2$~D', $project->read('args.txt'));
136    }
137}
138