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