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