packages/deriver/tests/Unit/Deriver/Providers/ProviderExpansionTest.php

1<?php
2
3declare (strict_types=1);
4
5namespace Tests\Unit\Deriver\Providers;
6
7use Deriver\Deriver\Contributions;
8use Deriver\Deriver\DeriverConfig;
9use Deriver\Deriver\Evaluator\Call\Provider\DispatchDecision;
10use Deriver\Deriver\Evaluator\Call\Provider\DispatchProvider;
11use Deriver\Deriver\Evaluator\Call\Provider\DispatchRequest;
12use Deriver\Deriver\Evaluator\Call\Provider\DispatchTarget;
13use Deriver\Deriver\Evaluator\Call\Provider\RefinementEvaluator;
14use Deriver\Deriver\Evaluator\Domain\AbstractDomain;
15use Deriver\Deriver\Evaluator\Domain\DomainFact;
16use Deriver\Deriver\Evaluator\Projection;
17use Deriver\Deriver\Evaluator\Request\EntryPoint;
18use Deriver\Deriver\Evaluator\TargetProfile;
19use Deriver\Deriver\Evaluator\Term;
20use Deriver\Deriver\Evaluator\TermKind;
21use Deriver\Deriver\Providers\DeclarationProvider;
22use Deriver\Deriver\Providers\DomainProvider;
23use Deriver\Deriver\Providers\EntryPointProvider;
24use Deriver\Deriver\Providers\EnvironmentProvider;
25use Deriver\Deriver\Providers\ProviderExpansion;
26use Deriver\Deriver\Sources\Path;
27use Deriver\Deriver\Sources\Source;
28use Deriver\Deriver\Sources\Sources;
29use Deriver\Exception\InvalidInputException;
30use Override;
31use PHPUnit\Framework\Attributes\CoversClass;
32use PHPUnit\Framework\Attributes\Small;
33use PHPUnit\Framework\Attributes\UsesClass;
34use PHPUnit\Framework\TestCase;
35
36/**
37 * @covers \Deriver\Deriver\Providers\ProviderExpansion
38 */
39#[CoversClass(ProviderExpansion::class)]
40#[UsesClass(DeriverConfig::class)]
41#[UsesClass(EntryPoint::class)]
42#[UsesClass(Sources::class)]
43#[UsesClass(Path::class)]
44#[UsesClass(Source::class)]
45#[UsesClass(TargetProfile::class)]
46#[UsesClass(Term::class)]
47#[Small]
48final class ProviderExpansionTest extends TestCase
49{
50    public function testCapturesContributionsBeforeSnapshotConstruction(): void
51    {
52        $inputs = new ProviderExpansion(new Sources([]), [], new Contributions(providers: [new class () implements DeclarationProvider, DispatchProvider, EnvironmentProvider, EntryPointProvider {
53            /**
54             * @return string Provider identity
55             */
56            #[Override]
57            public function id(): string
58            {
59                return 'example.container';
60            }
61            /**
62             * @return string Captured export version
63             */
64            #[Override]
65            public function version(): string
66            {
67                return '1';
68            }
69            /**
70             * @return Sources Explicit generated declaration and semantics
71             */
72            #[Override]
73            public function declarations(): Sources
74            {
75                return new Sources([new Source('generated/service.php', '<?php class GeneratedService{public function label(){return "service";}}')]);
76            }
77            /**
78             * @param DispatchRequest $request Evaluated call metadata
79             * @return DispatchDecision Exported binding, with explicit completeness
80             */
81            #[Override]
82            public function resolve(DispatchRequest $request): DispatchDecision
83            {
84                if ($request->method !== 'label' || ($request->receiver->attributes['type'] ?? '') !== 'Contract') {
85                    return new DispatchDecision();
86                }
87                return new DispatchDecision([new DispatchTarget('GeneratedService::label', new Term(TermKind::Object, 'container:shared-service', attributes: ['class' => 'GeneratedService']))], true, ['container:export-complete']);
88            }
89            /**
90             * @return array<string, Term> Captured settings
91             */
92            #[Override]
93            public function environment(): array
94            {
95                return ['APP_NAME' => Term::constant('captured')];
96            }
97            /**
98             * @return list<EntryPoint> Declared lifecycle entries
99             */
100            #[Override]
101            public function entries(): array
102            {
103                return [new EntryPoint('entry')];
104            }
105        }, new class () implements DomainProvider, RefinementEvaluator {
106            /**
107             * @return string Stable provider identity
108             */
109            #[Override]
110            public function id(): string
111            {
112                return 'example.policy-provider';
113            }
114            /**
115             * @return string Semantic version
116             */
117            #[Override]
118            public function version(): string
119            {
120                return '1';
121            }
122            /**
123             * @return list<AbstractDomain> Captured domains
124             */
125            #[Override]
126            public function domains(): array
127            {
128                return [new class () implements AbstractDomain {
129                    /**
130                     * @return string Domain identity
131                     */
132                    #[Override]
133                    public function id(): string
134                    {
135                        return 'example.policy';
136                    }
137                    /**
138                     * @return string Semantic version
139                     */
140                    #[Override]
141                    public function version(): string
142                    {
143                        return '1';
144                    }
145                    /**
146                     * @return DomainFact Empty set
147                     */
148                    #[Override]
149                    public function bottom(): DomainFact
150                    {
151                        return new DomainFact($this->id(), new Term(TermKind::Bottom));
152                    }
153                    /**
154                     * @return DomainFact All policy records
155                     */
156                    #[Override]
157                    public function top(): DomainFact
158                    {
159                        return new DomainFact($this->id(), new Term(TermKind::Top));
160                    }
161                    /**
162                     * @param DomainFact $a Contained fact
163                     * @param DomainFact $b Containing fact
164                     * @return bool Flat-lattice inclusion
165                     */
166                    #[Override]
167                    public function lessOrEqual(DomainFact $a, DomainFact $b): bool
168                    {
169                        return $a->representation->kind === TermKind::Bottom || $b->representation->kind === TermKind::Top || $a->representation->isConcrete() && $b->representation->isConcrete() && $a->representation->native() === $b->representation->native();
170                    }
171                    /**
172                     * @param DomainFact $a First possibility
173                     * @param DomainFact $b Second possibility
174                     * @return DomainFact Inclusive alternative
175                     */
176                    #[Override]
177                    public function join(DomainFact $a, DomainFact $b): DomainFact
178                    {
179                        if ($this->lessOrEqual($a, $b)) {
180                            return $b;
181                        }
182                        return $this->lessOrEqual($b, $a) ? $a : $this->top();
183                    }
184                    /**
185                     * @param DomainFact $previous Previous approximation
186                     * @param DomainFact $next New approximation
187                     * @return DomainFact Finite widening
188                     */
189                    #[Override]
190                    public function widen(DomainFact $previous, DomainFact $next): DomainFact
191                    {
192                        return $this->join($previous, $next);
193                    }
194                    /**
195                     * @param DomainFact $value Policy record
196                     * @param Projection $projection Selected fields
197                     * @return DomainFact Projected fact
198                     */
199                    #[Override]
200                    public function project(DomainFact $value, Projection $projection): DomainFact
201                    {
202                        $term = $value->representation;
203                        foreach ($projection->path as $key) {
204                            $term = $term->operands[$key] ?? new Term(TermKind::Top);
205                        }
206                        return new DomainFact($this->id(), $term);
207                    }
208                }];
209            }
210            /**
211             * @param Term $predicate Branch predicate
212             * @param bool $truth Selected polarity
213             * @return Term Guaranteed implication
214             */
215            #[Override]
216            public function refine(Term $predicate, bool $truth): Term
217            {
218                return $truth ? $predicate : new Term(TermKind::Unary, '!', [$predicate]);
219            }
220        }]));
221        self::assertSame('generated/service.php', $inputs->input[0]->path);
222        self::assertSame('captured', $inputs->environment['APP_NAME']->native());
223        self::assertSame('entry', $inputs->entries[0]->symbol);
224        self::assertCount(1, $inputs->contributions->domains);
225    }
226    public function testRejectsEnvironmentConflicts(): void
227    {
228        $this->expectException(InvalidInputException::class);
229        new ProviderExpansion(new Sources([]), ['APP_NAME' => Term::constant('conflict')], new Contributions(providers: [new class () implements DeclarationProvider, DispatchProvider, EnvironmentProvider, EntryPointProvider {
230            /**
231             * @return string Provider identity
232             */
233            #[Override]
234            public function id(): string
235            {
236                return 'example.container';
237            }
238            /**
239             * @return string Captured export version
240             */
241            #[Override]
242            public function version(): string
243            {
244                return '1';
245            }
246            /**
247             * @return Sources Explicit generated declaration and semantics
248             */
249            #[Override]
250            public function declarations(): Sources
251            {
252                return new Sources([new Source('generated/service.php', '<?php class GeneratedService{public function label(){return "service";}}')]);
253            }
254            /**
255             * @param DispatchRequest $request Evaluated call metadata
256             * @return DispatchDecision Exported binding, with explicit completeness
257             */
258            #[Override]
259            public function resolve(DispatchRequest $request): DispatchDecision
260            {
261                if ($request->method !== 'label' || ($request->receiver->attributes['type'] ?? '') !== 'Contract') {
262                    return new DispatchDecision();
263                }
264                return new DispatchDecision([new DispatchTarget('GeneratedService::label', new Term(TermKind::Object, 'container:shared-service', attributes: ['class' => 'GeneratedService']))], true, ['container:export-complete']);
265            }
266            /**
267             * @return array<string, Term> Captured settings
268             */
269            #[Override]
270            public function environment(): array
271            {
272                return ['APP_NAME' => Term::constant('captured')];
273            }
274            /**
275             * @return list<EntryPoint> Declared lifecycle entries
276             */
277            #[Override]
278            public function entries(): array
279            {
280                return [new EntryPoint('entry')];
281            }
282        }]));
283    }
284}
285