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