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