packages/deriver/tests/Unit/Deriver/Evaluator/Domain/AbstractDomainTest.php
1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Evaluator\Domain;
6
7use Deriver\Deriver\Evaluator\Domain\AbstractDomain;
8use Deriver\Deriver\Evaluator\Domain\DomainFact;
9use Deriver\Deriver\Evaluator\Projection;
10use Deriver\Deriver\Evaluator\Term;
11use Deriver\Deriver\Evaluator\TermKind;
12use Override;
13use PHPUnit\Framework\Attributes\CoversClass;
14use PHPUnit\Framework\Attributes\Small;
15use PHPUnit\Framework\Attributes\UsesClass;
16use PHPUnit\Framework\TestCase;
17
18/**
19 * @covers \Deriver\Deriver\Evaluator\Domain\AbstractDomain
20 */
21#[CoversClass(AbstractDomain::class)]
22#[UsesClass(DomainFact::class)]
23#[UsesClass(Projection::class)]
24#[UsesClass(Term::class)]
25#[Small]
26final class AbstractDomainTest extends TestCase
27{
28 public function testIdNamesTheDomain(): void
29 {
30 self::assertSame('example.policy', (new class () implements AbstractDomain {
31 /**
32 * @return string Domain identity
33 */
34 #[Override]
35 public function id(): string
36 {
37 return 'example.policy';
38 }
39 /**
40 * @return string Semantic version
41 */
42 #[Override]
43 public function version(): string
44 {
45 return '1';
46 }
47 /**
48 * @return DomainFact Empty set
49 */
50 #[Override]
51 public function bottom(): DomainFact
52 {
53 return new DomainFact($this->id(), new Term(TermKind::Bottom));
54 }
55 /**
56 * @return DomainFact All policy records
57 */
58 #[Override]
59 public function top(): DomainFact
60 {
61 return new DomainFact($this->id(), new Term(TermKind::Top));
62 }
63 /**
64 * @param DomainFact $a Contained fact
65 * @param DomainFact $b Containing fact
66 * @return bool Flat-lattice inclusion
67 */
68 #[Override]
69 public function lessOrEqual(DomainFact $a, DomainFact $b): bool
70 {
71 return $a->representation->kind === TermKind::Bottom || $b->representation->kind === TermKind::Top || $a->representation->isConcrete() && $b->representation->isConcrete() && $a->representation->native() === $b->representation->native();
72 }
73 /**
74 * @param DomainFact $a First possibility
75 * @param DomainFact $b Second possibility
76 * @return DomainFact Inclusive alternative
77 */
78 #[Override]
79 public function join(DomainFact $a, DomainFact $b): DomainFact
80 {
81 if ($this->lessOrEqual($a, $b)) {
82 return $b;
83 }
84 return $this->lessOrEqual($b, $a) ? $a : $this->top();
85 }
86 /**
87 * @param DomainFact $previous Previous approximation
88 * @param DomainFact $next New approximation
89 * @return DomainFact Finite widening
90 */
91 #[Override]
92 public function widen(DomainFact $previous, DomainFact $next): DomainFact
93 {
94 return $this->join($previous, $next);
95 }
96 /**
97 * @param DomainFact $value Policy record
98 * @param Projection $projection Selected fields
99 * @return DomainFact Projected fact
100 */
101 #[Override]
102 public function project(DomainFact $value, Projection $projection): DomainFact
103 {
104 $term = $value->representation;
105 foreach ($projection->path as $key) {
106 $term = $term->operands[$key] ?? new Term(TermKind::Top);
107 }
108 return new DomainFact($this->id(), $term);
109 }
110 })->id());
111 }
112 public function testVersionNamesTheSemanticRevision(): void
113 {
114 self::assertSame('1', (new class () implements AbstractDomain {
115 /**
116 * @return string Domain identity
117 */
118 #[Override]
119 public function id(): string
120 {
121 return 'example.policy';
122 }
123 /**
124 * @return string Semantic version
125 */
126 #[Override]
127 public function version(): string
128 {
129 return '1';
130 }
131 /**
132 * @return DomainFact Empty set
133 */
134 #[Override]
135 public function bottom(): DomainFact
136 {
137 return new DomainFact($this->id(), new Term(TermKind::Bottom));
138 }
139 /**
140 * @return DomainFact All policy records
141 */
142 #[Override]
143 public function top(): DomainFact
144 {
145 return new DomainFact($this->id(), new Term(TermKind::Top));
146 }
147 /**
148 * @param DomainFact $a Contained fact
149 * @param DomainFact $b Containing fact
150 * @return bool Flat-lattice inclusion
151 */
152 #[Override]
153 public function lessOrEqual(DomainFact $a, DomainFact $b): bool
154 {
155 return $a->representation->kind === TermKind::Bottom || $b->representation->kind === TermKind::Top || $a->representation->isConcrete() && $b->representation->isConcrete() && $a->representation->native() === $b->representation->native();
156 }
157 /**
158 * @param DomainFact $a First possibility
159 * @param DomainFact $b Second possibility
160 * @return DomainFact Inclusive alternative
161 */
162 #[Override]
163 public function join(DomainFact $a, DomainFact $b): DomainFact
164 {
165 if ($this->lessOrEqual($a, $b)) {
166 return $b;
167 }
168 return $this->lessOrEqual($b, $a) ? $a : $this->top();
169 }
170 /**
171 * @param DomainFact $previous Previous approximation
172 * @param DomainFact $next New approximation
173 * @return DomainFact Finite widening
174 */
175 #[Override]
176 public function widen(DomainFact $previous, DomainFact $next): DomainFact
177 {
178 return $this->join($previous, $next);
179 }
180 /**
181 * @param DomainFact $value Policy record
182 * @param Projection $projection Selected fields
183 * @return DomainFact Projected fact
184 */
185 #[Override]
186 public function project(DomainFact $value, Projection $projection): DomainFact
187 {
188 $term = $value->representation;
189 foreach ($projection->path as $key) {
190 $term = $term->operands[$key] ?? new Term(TermKind::Top);
191 }
192 return new DomainFact($this->id(), $term);
193 }
194 })->version());
195 }
196 public function testBottomRepresentsNoPossibilities(): void
197 {
198 self::assertSame(TermKind::Bottom, (new class () implements AbstractDomain {
199 /**
200 * @return string Domain identity
201 */
202 #[Override]
203 public function id(): string
204 {
205 return 'example.policy';
206 }
207 /**
208 * @return string Semantic version
209 */
210 #[Override]
211 public function version(): string
212 {
213 return '1';
214 }
215 /**
216 * @return DomainFact Empty set
217 */
218 #[Override]
219 public function bottom(): DomainFact
220 {
221 return new DomainFact($this->id(), new Term(TermKind::Bottom));
222 }
223 /**
224 * @return DomainFact All policy records
225 */
226 #[Override]
227 public function top(): DomainFact
228 {
229 return new DomainFact($this->id(), new Term(TermKind::Top));
230 }
231 /**
232 * @param DomainFact $a Contained fact
233 * @param DomainFact $b Containing fact
234 * @return bool Flat-lattice inclusion
235 */
236 #[Override]
237 public function lessOrEqual(DomainFact $a, DomainFact $b): bool
238 {
239 return $a->representation->kind === TermKind::Bottom || $b->representation->kind === TermKind::Top || $a->representation->isConcrete() && $b->representation->isConcrete() && $a->representation->native() === $b->representation->native();
240 }
241 /**
242 * @param DomainFact $a First possibility
243 * @param DomainFact $b Second possibility
244 * @return DomainFact Inclusive alternative
245 */
246 #[Override]
247 public function join(DomainFact $a, DomainFact $b): DomainFact
248 {
249 if ($this->lessOrEqual($a, $b)) {
250 return $b;
251 }
252 return $this->lessOrEqual($b, $a) ? $a : $this->top();
253 }
254 /**
255 * @param DomainFact $previous Previous approximation
256 * @param DomainFact $next New approximation
257 * @return DomainFact Finite widening
258 */
259 #[Override]
260 public function widen(DomainFact $previous, DomainFact $next): DomainFact
261 {
262 return $this->join($previous, $next);
263 }
264 /**
265 * @param DomainFact $value Policy record
266 * @param Projection $projection Selected fields
267 * @return DomainFact Projected fact
268 */
269 #[Override]
270 public function project(DomainFact $value, Projection $projection): DomainFact
271 {
272 $term = $value->representation;
273 foreach ($projection->path as $key) {
274 $term = $term->operands[$key] ?? new Term(TermKind::Top);
275 }
276 return new DomainFact($this->id(), $term);
277 }
278 })->bottom()->representation->kind);
279 }
280 public function testTopRepresentsEveryPolicy(): void
281 {
282 self::assertSame(TermKind::Top, (new class () implements AbstractDomain {
283 /**
284 * @return string Domain identity
285 */
286 #[Override]
287 public function id(): string
288 {
289 return 'example.policy';
290 }
291 /**
292 * @return string Semantic version
293 */
294 #[Override]
295 public function version(): string
296 {
297 return '1';
298 }
299 /**
300 * @return DomainFact Empty set
301 */
302 #[Override]
303 public function bottom(): DomainFact
304 {
305 return new DomainFact($this->id(), new Term(TermKind::Bottom));
306 }
307 /**
308 * @return DomainFact All policy records
309 */
310 #[Override]
311 public function top(): DomainFact
312 {
313 return new DomainFact($this->id(), new Term(TermKind::Top));
314 }
315 /**
316 * @param DomainFact $a Contained fact
317 * @param DomainFact $b Containing fact
318 * @return bool Flat-lattice inclusion
319 */
320 #[Override]
321 public function lessOrEqual(DomainFact $a, DomainFact $b): bool
322 {
323 return $a->representation->kind === TermKind::Bottom || $b->representation->kind === TermKind::Top || $a->representation->isConcrete() && $b->representation->isConcrete() && $a->representation->native() === $b->representation->native();
324 }
325 /**
326 * @param DomainFact $a First possibility
327 * @param DomainFact $b Second possibility
328 * @return DomainFact Inclusive alternative
329 */
330 #[Override]
331 public function join(DomainFact $a, DomainFact $b): DomainFact
332 {
333 if ($this->lessOrEqual($a, $b)) {
334 return $b;
335 }
336 return $this->lessOrEqual($b, $a) ? $a : $this->top();
337 }
338 /**
339 * @param DomainFact $previous Previous approximation
340 * @param DomainFact $next New approximation
341 * @return DomainFact Finite widening
342 */
343 #[Override]
344 public function widen(DomainFact $previous, DomainFact $next): DomainFact
345 {
346 return $this->join($previous, $next);
347 }
348 /**
349 * @param DomainFact $value Policy record
350 * @param Projection $projection Selected fields
351 * @return DomainFact Projected fact
352 */
353 #[Override]
354 public function project(DomainFact $value, Projection $projection): DomainFact
355 {
356 $term = $value->representation;
357 foreach ($projection->path as $key) {
358 $term = $term->operands[$key] ?? new Term(TermKind::Top);
359 }
360 return new DomainFact($this->id(), $term);
361 }
362 })->top()->representation->kind);
363 }
364 public function testLessOrEqualIncludesBottomAndRejectsReverseInclusion(): void
365 {
366 $domain = new class () implements AbstractDomain {
367 /**
368 * @return string Domain identity
369 */
370 #[Override]
371 public function id(): string
372 {
373 return 'example.policy';
374 }
375 /**
376 * @return string Semantic version
377 */
378 #[Override]
379 public function version(): string
380 {
381 return '1';
382 }
383 /**
384 * @return DomainFact Empty set
385 */
386 #[Override]
387 public function bottom(): DomainFact
388 {
389 return new DomainFact($this->id(), new Term(TermKind::Bottom));
390 }
391 /**
392 * @return DomainFact All policy records
393 */
394 #[Override]
395 public function top(): DomainFact
396 {
397 return new DomainFact($this->id(), new Term(TermKind::Top));
398 }
399 /**
400 * @param DomainFact $a Contained fact
401 * @param DomainFact $b Containing fact
402 * @return bool Flat-lattice inclusion
403 */
404 #[Override]
405 public function lessOrEqual(DomainFact $a, DomainFact $b): bool
406 {
407 return $a->representation->kind === TermKind::Bottom || $b->representation->kind === TermKind::Top || $a->representation->isConcrete() && $b->representation->isConcrete() && $a->representation->native() === $b->representation->native();
408 }
409 /**
410 * @param DomainFact $a First possibility
411 * @param DomainFact $b Second possibility
412 * @return DomainFact Inclusive alternative
413 */
414 #[Override]
415 public function join(DomainFact $a, DomainFact $b): DomainFact
416 {
417 if ($this->lessOrEqual($a, $b)) {
418 return $b;
419 }
420 return $this->lessOrEqual($b, $a) ? $a : $this->top();
421 }
422 /**
423 * @param DomainFact $previous Previous approximation
424 * @param DomainFact $next New approximation
425 * @return DomainFact Finite widening
426 */
427 #[Override]
428 public function widen(DomainFact $previous, DomainFact $next): DomainFact
429 {
430 return $this->join($previous, $next);
431 }
432 /**
433 * @param DomainFact $value Policy record
434 * @param Projection $projection Selected fields
435 * @return DomainFact Projected fact
436 */
437 #[Override]
438 public function project(DomainFact $value, Projection $projection): DomainFact
439 {
440 $term = $value->representation;
441 foreach ($projection->path as $key) {
442 $term = $term->operands[$key] ?? new Term(TermKind::Top);
443 }
444 return new DomainFact($this->id(), $term);
445 }
446 };
447 self::assertTrue($domain->lessOrEqual($domain->bottom(), $domain->top()));
448 self::assertFalse($domain->lessOrEqual($domain->top(), $domain->bottom()));
449 }
450 public function testJoinPreservesBothDisjointRecords(): void
451 {
452 $domain = new class () implements AbstractDomain {
453 /**
454 * @return string Domain identity
455 */
456 #[Override]
457 public function id(): string
458 {
459 return 'example.policy';
460 }
461 /**
462 * @return string Semantic version
463 */
464 #[Override]
465 public function version(): string
466 {
467 return '1';
468 }
469 /**
470 * @return DomainFact Empty set
471 */
472 #[Override]
473 public function bottom(): DomainFact
474 {
475 return new DomainFact($this->id(), new Term(TermKind::Bottom));
476 }
477 /**
478 * @return DomainFact All policy records
479 */
480 #[Override]
481 public function top(): DomainFact
482 {
483 return new DomainFact($this->id(), new Term(TermKind::Top));
484 }
485 /**
486 * @param DomainFact $a Contained fact
487 * @param DomainFact $b Containing fact
488 * @return bool Flat-lattice inclusion
489 */
490 #[Override]
491 public function lessOrEqual(DomainFact $a, DomainFact $b): bool
492 {
493 return $a->representation->kind === TermKind::Bottom || $b->representation->kind === TermKind::Top || $a->representation->isConcrete() && $b->representation->isConcrete() && $a->representation->native() === $b->representation->native();
494 }
495 /**
496 * @param DomainFact $a First possibility
497 * @param DomainFact $b Second possibility
498 * @return DomainFact Inclusive alternative
499 */
500 #[Override]
501 public function join(DomainFact $a, DomainFact $b): DomainFact
502 {
503 if ($this->lessOrEqual($a, $b)) {
504 return $b;
505 }
506 return $this->lessOrEqual($b, $a) ? $a : $this->top();
507 }
508 /**
509 * @param DomainFact $previous Previous approximation
510 * @param DomainFact $next New approximation
511 * @return DomainFact Finite widening
512 */
513 #[Override]
514 public function widen(DomainFact $previous, DomainFact $next): DomainFact
515 {
516 return $this->join($previous, $next);
517 }
518 /**
519 * @param DomainFact $value Policy record
520 * @param Projection $projection Selected fields
521 * @return DomainFact Projected fact
522 */
523 #[Override]
524 public function project(DomainFact $value, Projection $projection): DomainFact
525 {
526 $term = $value->representation;
527 foreach ($projection->path as $key) {
528 $term = $term->operands[$key] ?? new Term(TermKind::Top);
529 }
530 return new DomainFact($this->id(), $term);
531 }
532 };
533 $a = new DomainFact($domain->id(), Term::fromNative(['ttl' => 1]));
534 $b = new DomainFact($domain->id(), Term::fromNative(['ttl' => 2]));
535 $join = $domain->join($a, $b);
536 self::assertTrue($domain->lessOrEqual($a, $join));
537 self::assertTrue($domain->lessOrEqual($b, $join));
538 }
539 public function testWidenIncludesThePreviousAndNextApproximation(): void
540 {
541 $domain = new class () implements AbstractDomain {
542 /**
543 * @return string Domain identity
544 */
545 #[Override]
546 public function id(): string
547 {
548 return 'example.policy';
549 }
550 /**
551 * @return string Semantic version
552 */
553 #[Override]
554 public function version(): string
555 {
556 return '1';
557 }
558 /**
559 * @return DomainFact Empty set
560 */
561 #[Override]
562 public function bottom(): DomainFact
563 {
564 return new DomainFact($this->id(), new Term(TermKind::Bottom));
565 }
566 /**
567 * @return DomainFact All policy records
568 */
569 #[Override]
570 public function top(): DomainFact
571 {
572 return new DomainFact($this->id(), new Term(TermKind::Top));
573 }
574 /**
575 * @param DomainFact $a Contained fact
576 * @param DomainFact $b Containing fact
577 * @return bool Flat-lattice inclusion
578 */
579 #[Override]
580 public function lessOrEqual(DomainFact $a, DomainFact $b): bool
581 {
582 return $a->representation->kind === TermKind::Bottom || $b->representation->kind === TermKind::Top || $a->representation->isConcrete() && $b->representation->isConcrete() && $a->representation->native() === $b->representation->native();
583 }
584 /**
585 * @param DomainFact $a First possibility
586 * @param DomainFact $b Second possibility
587 * @return DomainFact Inclusive alternative
588 */
589 #[Override]
590 public function join(DomainFact $a, DomainFact $b): DomainFact
591 {
592 if ($this->lessOrEqual($a, $b)) {
593 return $b;
594 }
595 return $this->lessOrEqual($b, $a) ? $a : $this->top();
596 }
597 /**
598 * @param DomainFact $previous Previous approximation
599 * @param DomainFact $next New approximation
600 * @return DomainFact Finite widening
601 */
602 #[Override]
603 public function widen(DomainFact $previous, DomainFact $next): DomainFact
604 {
605 return $this->join($previous, $next);
606 }
607 /**
608 * @param DomainFact $value Policy record
609 * @param Projection $projection Selected fields
610 * @return DomainFact Projected fact
611 */
612 #[Override]
613 public function project(DomainFact $value, Projection $projection): DomainFact
614 {
615 $term = $value->representation;
616 foreach ($projection->path as $key) {
617 $term = $term->operands[$key] ?? new Term(TermKind::Top);
618 }
619 return new DomainFact($this->id(), $term);
620 }
621 };
622 self::assertSame(TermKind::Top, $domain->widen($domain->bottom(), $domain->top())->representation->kind);
623 }
624 public function testProjectSelectsOneKnownPolicyField(): void
625 {
626 $domain = new class () implements AbstractDomain {
627 /**
628 * @return string Domain identity
629 */
630 #[Override]
631 public function id(): string
632 {
633 return 'example.policy';
634 }
635 /**
636 * @return string Semantic version
637 */
638 #[Override]
639 public function version(): string
640 {
641 return '1';
642 }
643 /**
644 * @return DomainFact Empty set
645 */
646 #[Override]
647 public function bottom(): DomainFact
648 {
649 return new DomainFact($this->id(), new Term(TermKind::Bottom));
650 }
651 /**
652 * @return DomainFact All policy records
653 */
654 #[Override]
655 public function top(): DomainFact
656 {
657 return new DomainFact($this->id(), new Term(TermKind::Top));
658 }
659 /**
660 * @param DomainFact $a Contained fact
661 * @param DomainFact $b Containing fact
662 * @return bool Flat-lattice inclusion
663 */
664 #[Override]
665 public function lessOrEqual(DomainFact $a, DomainFact $b): bool
666 {
667 return $a->representation->kind === TermKind::Bottom || $b->representation->kind === TermKind::Top || $a->representation->isConcrete() && $b->representation->isConcrete() && $a->representation->native() === $b->representation->native();
668 }
669 /**
670 * @param DomainFact $a First possibility
671 * @param DomainFact $b Second possibility
672 * @return DomainFact Inclusive alternative
673 */
674 #[Override]
675 public function join(DomainFact $a, DomainFact $b): DomainFact
676 {
677 if ($this->lessOrEqual($a, $b)) {
678 return $b;
679 }
680 return $this->lessOrEqual($b, $a) ? $a : $this->top();
681 }
682 /**
683 * @param DomainFact $previous Previous approximation
684 * @param DomainFact $next New approximation
685 * @return DomainFact Finite widening
686 */
687 #[Override]
688 public function widen(DomainFact $previous, DomainFact $next): DomainFact
689 {
690 return $this->join($previous, $next);
691 }
692 /**
693 * @param DomainFact $value Policy record
694 * @param Projection $projection Selected fields
695 * @return DomainFact Projected fact
696 */
697 #[Override]
698 public function project(DomainFact $value, Projection $projection): DomainFact
699 {
700 $term = $value->representation;
701 foreach ($projection->path as $key) {
702 $term = $term->operands[$key] ?? new Term(TermKind::Top);
703 }
704 return new DomainFact($this->id(), $term);
705 }
706 };
707 $fact = new DomainFact($domain->id(), Term::fromNative(['ttl' => 3]));
708 self::assertSame(3, $domain->project($fact, new Projection(['ttl']))->representation->native());
709 }
710}
711