packages/requirements/tests/Unit/Ears/ValidatorTest.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\LiteralMask;
14use Requirements\Ears\SystemResponse;
15use Requirements\Ears\Validator;
16use Requirements\Ears\Wording;
17use Requirements\Input\InvalidInputException;
18
19#[CoversClass(Validator::class)]
20#[UsesClass(LiteralMask::class)]
21#[UsesClass(ConditionOrder::class)]
22#[UsesClass(SystemResponse::class)]
23#[UsesClass(Wording::class)]
24#[Small]
25final class ValidatorTest extends TestCase
26{
27    #[DataProvider('providerClauseOrderAccepted')]
28    public function testValidateAcceptsClauseOrder(string $statement): void
29    {
30        (new Validator())->validate($statement);
31        $this->addToAssertionCount(1);
32    }
33
34    /**
35     * @return array<string, array{string}>
36     */
37    public static function providerClauseOrderAccepted(): array
38    {
39        return [
40            'complex' => ['While recording is enabled, When a token is read, the reader shall preserve its position.'],
41        ];
42    }
43
44    #[DataProvider('providerClauseOrderRejected')]
45    public function testValidateRejectsClauseOrder(string $statement): void
46    {
47        $this->expectException(InvalidInputException::class);
48        $this->expectExceptionMessage('EARS:');
49        (new Validator())->validate($statement);
50    }
51
52    /**
53     * @return array<string, array{string}>
54     */
55    public static function providerClauseOrderRejected(): array
56    {
57        return [
58            'wrong order' => ['When input ends, while tracing is enabled, the reader shall return the tree.'],
59            'optional after state' => ['While tracing is enabled, where tracing exists, the reader shall return the tree.'],
60            'unknown keyword' => ['Unless input ends, the reader shall return the tree.'],
61        ];
62    }
63
64    #[DataProvider('providerSystemResponseAccepted')]
65    public function testValidateAcceptsSystemResponse(string $statement): void
66    {
67        (new Validator())->validate($statement);
68        $this->addToAssertionCount(1);
69    }
70
71    /**
72     * @return array<string, array{string}>
73     */
74    public static function providerSystemResponseAccepted(): array
75    {
76        return [
77            'ubiquitous' => ['The reader shall preserve positions.'],
78            'no terminal punctuation' => ['The reader shall preserve positions'],
79        ];
80    }
81
82    #[DataProvider('providerSystemResponseRejected')]
83    public function testValidateRejectsSystemResponse(string $statement): void
84    {
85        $this->expectException(InvalidInputException::class);
86        $this->expectExceptionMessage('EARS:');
87        (new Validator())->validate($statement);
88    }
89
90    /**
91     * @return array<string, array{string}>
92     */
93    public static function providerSystemResponseRejected(): array
94    {
95        return [
96            'no system' => ['The shall record positions.'],
97            'no article' => ['When input ends, reader shall return the tree.'],
98            'no response' => ['The reader shall .'],
99            'missing shall' => ['The reader will return the tree.'],
100        ];
101    }
102
103    #[DataProvider('providerStateDrivenAccepted')]
104    public function testValidateAcceptsStateDriven(string $statement): void
105    {
106        (new Validator())->validate($statement);
107        $this->addToAssertionCount(1);
108    }
109
110    /**
111     * @return array<string, array{string}>
112     */
113    public static function providerStateDrivenAccepted(): array
114    {
115        return [
116            'state' => ['While recording is enabled, the reader shall preserve positions.'],
117        ];
118    }
119
120    #[DataProvider('providerStateDrivenRejected')]
121    public function testValidateRejectsStateDriven(string $statement): void
122    {
123        $this->expectException(InvalidInputException::class);
124        $this->expectExceptionMessage('EARS:');
125        (new Validator())->validate($statement);
126    }
127
128    /**
129     * @return array<string, array{string}>
130     */
131    public static function providerStateDrivenRejected(): array
132    {
133        return [
134            'no condition text' => ['While ..., the reader shall return the tree.'],
135        ];
136    }
137
138    #[DataProvider('providerEventDrivenAccepted')]
139    public function testValidateAcceptsEventDriven(string $statement): void
140    {
141        (new Validator())->validate($statement);
142        $this->addToAssertionCount(1);
143    }
144
145    /**
146     * @return array<string, array{string}>
147     */
148    public static function providerEventDrivenAccepted(): array
149    {
150        return [
151            'event' => ['When a token is read, the reader shall preserve its position.'],
152            'case insensitive' => ['WHEN input ends, THE reader SHALL emit the tree.'],
153            'comma in slot' => ['When a name, number or literal is read, the reader shall record its position.'],
154        ];
155    }
156
157    #[DataProvider('providerEventDrivenRejected')]
158    public function testValidateRejectsEventDriven(string $statement): void
159    {
160        $this->expectException(InvalidInputException::class);
161        $this->expectExceptionMessage('EARS:');
162        (new Validator())->validate($statement);
163    }
164
165    /**
166     * @return array<string, array{string}>
167     */
168    public static function providerEventDrivenRejected(): array
169    {
170        return [
171            'no condition' => ['When , the reader shall return the tree.'],
172            'missing comma' => ['When input ends the reader shall return the tree.'],
173        ];
174    }
175
176    #[DataProvider('providerOptionalFeatureAccepted')]
177    public function testValidateAcceptsOptionalFeature(string $statement): void
178    {
179        (new Validator())->validate($statement);
180        $this->addToAssertionCount(1);
181    }
182
183    /**
184     * @return array<string, array{string}>
185     */
186    public static function providerOptionalFeatureAccepted(): array
187    {
188        return [
189            'optional' => ['Where tracing is enabled, the reader shall preserve positions.'],
190            'optional complex' => ['Where tracing is enabled, while recording is enabled, when a token is read, the reader shall preserve its position.'],
191        ];
192    }
193
194    #[DataProvider('providerOptionalFeatureRejected')]
195    public function testValidateRejectsOptionalFeature(string $statement): void
196    {
197        $this->expectException(InvalidInputException::class);
198        $this->expectExceptionMessage('EARS:');
199        (new Validator())->validate($statement);
200    }
201
202    /**
203     * @return array<string, array{string}>
204     */
205    public static function providerOptionalFeatureRejected(): array
206    {
207        return [
208            'empty feature' => ['Where , the reader shall preserve positions.'],
209        ];
210    }
211
212    #[DataProvider('providerUnwantedBehaviourAccepted')]
213    public function testValidateAcceptsUnwantedBehaviour(string $statement): void
214    {
215        (new Validator())->validate($statement);
216        $this->addToAssertionCount(1);
217    }
218
219    /**
220     * @return array<string, array{string}>
221     */
222    public static function providerUnwantedBehaviourAccepted(): array
223    {
224        return [
225            'unwanted' => ['If input is invalid, then the reader shall report an error.'],
226        ];
227    }
228
229    #[DataProvider('providerUnwantedBehaviourRejected')]
230    public function testValidateRejectsUnwantedBehaviour(string $statement): void
231    {
232        $this->expectException(InvalidInputException::class);
233        $this->expectExceptionMessage('EARS:');
234        (new Validator())->validate($statement);
235    }
236
237    /**
238     * @return array<string, array{string}>
239     */
240    public static function providerUnwantedBehaviourRejected(): array
241    {
242        return [
243            'missing then' => ['If input ends, the reader shall return the tree.'],
244            'unexpected then' => ['When input ends, then the reader shall return the tree.'],
245            'then without if' => ['Then the reader shall return the tree.'],
246            'empty unwanted trigger' => ['If , then the reader shall report an error.'],
247        ];
248    }
249
250    #[DataProvider('providerComplexAccepted')]
251    public function testValidateAcceptsComplex(string $statement): void
252    {
253        (new Validator())->validate($statement);
254        $this->addToAssertionCount(1);
255    }
256
257    /**
258     * @return array<string, array{string}>
259     */
260    public static function providerComplexAccepted(): array
261    {
262        return [
263            'complex' => ['While recording is enabled, When a token is read, the reader shall preserve its position.'],
264            'optional complex' => ['Where tracing is enabled, while recording is enabled, when a token is read, the reader shall preserve its position.'],
265        ];
266    }
267
268    #[DataProvider('providerComplexRejected')]
269    public function testValidateRejectsComplex(string $statement): void
270    {
271        $this->expectException(InvalidInputException::class);
272        $this->expectExceptionMessage('EARS:');
273        (new Validator())->validate($statement);
274    }
275
276    /**
277     * @return array<string, array{string}>
278     */
279    public static function providerComplexRejected(): array
280    {
281        return [
282            'wrong order' => ['When input ends, while tracing is enabled, the reader shall return the tree.'],
283        ];
284    }
285
286    #[DataProvider('providerCardinalityAccepted')]
287    public function testValidateAcceptsCardinality(string $statement): void
288    {
289        (new Validator())->validate($statement);
290        $this->addToAssertionCount(1);
291    }
292
293    /**
294     * @return array<string, array{string}>
295     */
296    public static function providerCardinalityAccepted(): array
297    {
298        return [
299            'ubiquitous' => ['The reader shall preserve positions.'],
300            'multiple preconditions and responses' => ['While recording is enabled, while input is available, the reader shall preserve positions and report progress.'],
301        ];
302    }
303
304    #[DataProvider('providerCardinalityRejected')]
305    public function testValidateRejectsCardinality(string $statement): void
306    {
307        $this->expectException(InvalidInputException::class);
308        $this->expectExceptionMessage('EARS:');
309        (new Validator())->validate($statement);
310    }
311
312    /**
313     * @return array<string, array{string}>
314     */
315    public static function providerCardinalityRejected(): array
316    {
317        return [
318            'two triggers' => ['When input ends, when a token is read, the reader shall return the tree.'],
319            'mixed triggers' => ['When input ends, if input is invalid, then the reader shall return the tree.'],
320            'two systems' => ['The reader shall return the tree, the printer shall print it.'],
321            'second shall' => ['The reader shall return the tree and shall print it.'],
322        ];
323    }
324
325    #[DataProvider('providerComplexUnwantedBehaviourAccepted')]
326    public function testValidateAcceptsComplexUnwantedBehaviour(string $statement): void
327    {
328        (new Validator())->validate($statement);
329        $this->addToAssertionCount(1);
330    }
331
332    /**
333     * @return array<string, array{string}>
334     */
335    public static function providerComplexUnwantedBehaviourAccepted(): array
336    {
337        return [
338            'complex unwanted' => ['While recording is enabled, If input is invalid, then the reader shall report an error.'],
339        ];
340    }
341
342    #[DataProvider('providerComplexUnwantedBehaviourRejected')]
343    public function testValidateRejectsComplexUnwantedBehaviour(string $statement): void
344    {
345        $this->expectException(InvalidInputException::class);
346        $this->expectExceptionMessage('EARS:');
347        (new Validator())->validate($statement);
348    }
349
350    /**
351     * @return array<string, array{string}>
352     */
353    public static function providerComplexUnwantedBehaviourRejected(): array
354    {
355        return [
356            'complex missing then' => ['While recording is enabled, if input is invalid, the reader shall report an error.'],
357            'complex missing if' => ['While recording is enabled, then the reader shall report an error.'],
358            'complex empty trigger' => ['While recording is enabled, if , then the reader shall report an error.'],
359        ];
360    }
361
362    #[DataProvider('providerLiteralsAccepted')]
363    public function testValidateAcceptsLiterals(string $statement): void
364    {
365        (new Validator())->validate($statement);
366        $this->addToAssertionCount(1);
367    }
368
369    /**
370     * @return array<string, array{string}>
371     */
372    public static function providerLiteralsAccepted(): array
373    {
374        return [
375            'quoted keywords' => ['When "if, then the" is read, the reader shall emit a "shall" token.'],
376            'apostrophe' => ["The reader shall preserve the user's input."],
377            'code literal' => ['The reader shall preserve `shall, the` literally.'],
378        ];
379    }
380
381    #[DataProvider('providerLiteralsRejected')]
382    public function testValidateRejectsLiterals(string $statement): void
383    {
384        $this->expectException(InvalidInputException::class);
385        $this->expectExceptionMessage('EARS:');
386        (new Validator())->validate($statement);
387    }
388
389    /**
390     * @return array<string, array{string}>
391     */
392    public static function providerLiteralsRejected(): array
393    {
394        return [
395            'open literal' => ['The reader shall emit a "token.'],
396        ];
397    }
398}
399