packages/requirements/tests/Unit/Ears/ConditionOrderTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Ears;
6
7use PHPUnit\Framework\Attributes\CoversClass;
8use PHPUnit\Framework\Attributes\DataProvider;
9use PHPUnit\Framework\Attributes\Small;
10use PHPUnit\Framework\Attributes\UsesClass;
11use PHPUnit\Framework\TestCase;
12use Requirements\Ears\ConditionOrder;
13use Requirements\Ears\Wording;
14use Requirements\Input\InvalidInputException;
15
16#[CoversClass(ConditionOrder::class)]
17#[UsesClass(Wording::class)]
18#[Small]
19final class ConditionOrderTest extends TestCase
20{
21    /**
22     * @param list<string> $clauses
23     */
24    #[DataProvider('providerAcceptTakesClausesInOrder')]
25    public function testAcceptTakesClausesInOrder(array $clauses, ?string $trigger): void
26    {
27        $order = new ConditionOrder();
28        array_map($order->accept(...), $clauses);
29        self::assertSame($trigger, $order->trigger());
30    }
31
32    /**
33     * @return array<string, array{list<string>, ?string}>
34     */
35    public static function providerAcceptTakesClausesInOrder(): array
36    {
37        return [
38            'none' => [[], null],
39            'where' => [['Where tracing is enabled'], null],
40            'while' => [['While recording is enabled'], null],
41            'two whiles' => [['While recording is enabled', 'while input is available'], null],
42            'when' => [['When a token is read'], 'when'],
43            'if' => [['If input is invalid'], 'if'],
44            'uppercase keyword' => [['IF input is invalid'], 'if'],
45            'mixed case keyword' => [['wHeN input ends'], 'when'],
46            'surrounding space' => [["  When input ends \n"], 'when'],
47            'full order' => [['Where tracing is enabled', 'while recording is enabled', 'when a token is read'], 'when'],
48            'where then if' => [['Where tracing is enabled', 'If input is invalid'], 'if'],
49            'multiline text' => [["When a token\nis read"], 'when'],
50            'word containing shall' => [['When the shallow copy ends'], 'when'],
51            'word containing then' => [['When the thenar is read'], 'when'],
52        ];
53    }
54
55    public function testTriggerIsNullBeforeAnyClause(): void
56    {
57        self::assertNull((new ConditionOrder())->trigger());
58    }
59
60    public function testTriggerIsKeptAfterPrecedingClauses(): void
61    {
62        $order = new ConditionOrder();
63        $order->accept('While recording is enabled');
64        self::assertNull($order->trigger());
65        $order->accept('If input is invalid');
66        self::assertSame('if', $order->trigger());
67    }
68
69    #[DataProvider('providerAcceptRejectsMalformedClauses')]
70    public function testAcceptRejectsMalformedClauses(string $clause): void
71    {
72        $this->expectException(InvalidInputException::class);
73        $this->expectExceptionMessage('EARS: each condition needs Where, While, When or If and nonempty text.');
74        (new ConditionOrder())->accept($clause);
75    }
76
77    /**
78     * @return array<string, array{string}>
79     */
80    public static function providerAcceptRejectsMalformedClauses(): array
81    {
82        return [
83            'unknown keyword' => ['Unless input ends'],
84            'keyword prefix' => ['Whenever input ends'],
85            'keyword only' => ['When'],
86            'no space after keyword' => ['When,input ends'],
87            'keyword not first' => ['Then when input ends'],
88            'punctuation only' => ['When ...'],
89            'empty' => [''],
90        ];
91    }
92
93    /**
94     * @param list<string> $accepted
95     */
96    #[DataProvider('providerAcceptRejectsClausesOutOfOrder')]
97    public function testAcceptRejectsClausesOutOfOrder(array $accepted, string $clause): void
98    {
99        $order = new ConditionOrder();
100        array_map($order->accept(...), $accepted);
101        $this->expectException(InvalidInputException::class);
102        $this->expectExceptionMessage('EARS: use optional feature, preconditions, then at most one trigger (When or If), in that order.');
103        $order->accept($clause);
104    }
105
106    /**
107     * @return array<string, array{list<string>, string}>
108     */
109    public static function providerAcceptRejectsClausesOutOfOrder(): array
110    {
111        return [
112            'where after while' => [['While recording is enabled'], 'Where tracing is enabled'],
113            'second where' => [['Where tracing is enabled'], 'Where logging is enabled'],
114            'while after when' => [['When input ends'], 'While recording is enabled'],
115            'where after if' => [['If input is invalid'], 'Where tracing is enabled'],
116            'second when' => [['When input ends'], 'When a token is read'],
117            'if after when' => [['When input ends'], 'If input is invalid'],
118            'second if' => [['If input ends'], 'If input is invalid'],
119        ];
120    }
121
122    #[DataProvider('providerAcceptRejectsShallOrThenInConditions')]
123    public function testAcceptRejectsShallOrThenInConditions(string $clause): void
124    {
125        $this->expectException(InvalidInputException::class);
126        $this->expectExceptionMessage('EARS: shall belongs in the response; then must introduce the system clause after If.');
127        (new ConditionOrder())->accept($clause);
128    }
129
130    /**
131     * @return array<string, array{string}>
132     */
133    public static function providerAcceptRejectsShallOrThenInConditions(): array
134    {
135        return [
136            'shall' => ['When the reader shall stop'],
137            'then' => ['If input is invalid then the reader'],
138            'uppercase then' => ['If input is invalid THEN the reader'],
139            'where with shall' => ['Where the tracer SHALL run'],
140        ];
141    }
142}
143