Skip to content

Commit 07c11b2

Browse files
committed
Add mutation tests config and kill surviving mutants to reach 90% MSI
Adds infection.json5 configuration and 65 new/extended PHPUnit tests across DataCollector, Entity, EventListener, Factory, Fixture, and Serializer suites. Brings the mutation score from below threshold to 90% MSI (870 killed, 95 escaped, 42 timed out).
1 parent 3797fdd commit 07c11b2

11 files changed

Lines changed: 1061 additions & 10 deletions

File tree

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"friends-of-behat/mink-extension": "^2.7",
7272
"friends-of-behat/symfony-extension": "^2.4",
7373
"friendsofphp/php-cs-fixer": "^v3",
74+
"infection/infection": "^0.31.1",
7475
"justinrainbow/json-schema": "^5.4",
7576
"league/flysystem": "^3.11",
7677
"league/flysystem-memory": "^3.0.0",
@@ -105,7 +106,8 @@
105106
"sort-packages": true,
106107
"allow-plugins": {
107108
"phpstan/extension-installer": true,
108-
"symfony/flex": true
109+
"symfony/flex": true,
110+
"infection/extension-installer": true
109111
}
110112
},
111113
"autoload": {

infection.json5

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"$schema": "vendor/infection/infection/resources/schema.json",
3+
"source": {
4+
"directories": [
5+
"src"
6+
]
7+
},
8+
"logs": {
9+
"text": "build/logs/infection.log",
10+
"summary": "build/logs/infection-summary.log",
11+
"json": "build/logs/infection.json",
12+
"summaryJson": "build/logs/infection-summary.json"
13+
},
14+
"mutators": {
15+
"@default": true
16+
},
17+
"testFramework": "phpunit",
18+
"testFrameworkOptions": "--exclude-group=functional",
19+
"initialTestsPhpOptions": "-d memory_limit=512M",
20+
"phpUnit": {
21+
"configDir": ".",
22+
"customPath": "build/phpunit-for-infection.php"
23+
},
24+
"minMsi": 90,
25+
"minCoveredMsi": 90,
26+
"threads": 4
27+
}

tests/DataCollector/CwaDataCollectorTest.php

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,258 @@ public function test_collect_with_exception_does_not_throw(): void
141141
// Should not throw; data is still accessible (empty)
142142
self::assertFalse($this->collector->isJwtCookiePresent());
143143
}
144+
145+
public function test_collect_records_publishable_queries(): void
146+
{
147+
$this->collectorData->recordPublishableQuery('App\Entity\Article', 'published-only', 'select');
148+
$this->collectorData->recordPublishableQuery('App\Entity\Post', 'draft', 'select');
149+
150+
$this->collector->collect(new Request(), new Response());
151+
152+
self::assertSame(2, $this->collector->getPublishableQueryCount());
153+
$queries = $this->collector->getPublishableQueries();
154+
self::assertCount(2, $queries);
155+
self::assertSame('App\Entity\Article', $queries[0]['class']);
156+
self::assertSame('published-only', $queries[0]['mode']);
157+
self::assertSame('select', $queries[0]['queryType']);
158+
}
159+
160+
public function test_collect_empty_publishable_queries(): void
161+
{
162+
$this->collector->collect(new Request(), new Response());
163+
164+
self::assertSame(0, $this->collector->getPublishableQueryCount());
165+
self::assertSame([], $this->collector->getPublishableQueries());
166+
}
167+
168+
public function test_collect_records_page_data_resolutions(): void
169+
{
170+
$this->collectorData->recordPageDataResolution('image', 'App\Entity\Article', null);
171+
$this->collectorData->recordPageDataResolution('content', null, 'not_found');
172+
173+
$this->collector->collect(new Request(), new Response());
174+
175+
self::assertSame(2, $this->collector->getPageDataResolutionCount());
176+
$resolutions = $this->collector->getPageDataResolutions();
177+
self::assertCount(2, $resolutions);
178+
self::assertSame('image', $resolutions[0]['property']);
179+
self::assertSame('App\Entity\Article', $resolutions[0]['resolvedClass']);
180+
self::assertNull($resolutions[0]['skipReason']);
181+
self::assertSame('content', $resolutions[1]['property']);
182+
self::assertNull($resolutions[1]['resolvedClass']);
183+
self::assertSame('not_found', $resolutions[1]['skipReason']);
184+
}
185+
186+
public function test_collect_empty_page_data_resolutions(): void
187+
{
188+
$this->collector->collect(new Request(), new Response());
189+
190+
self::assertSame(0, $this->collector->getPageDataResolutionCount());
191+
self::assertSame([], $this->collector->getPageDataResolutions());
192+
}
193+
194+
public function test_collect_records_invalidation_counts(): void
195+
{
196+
$this->collectorData->recordInvalidationCount('created');
197+
$this->collectorData->recordInvalidationCount('created');
198+
$this->collectorData->recordInvalidationCount('updated');
199+
$this->collectorData->recordInvalidationCount('deleted');
200+
201+
$this->collector->collect(new Request(), new Response());
202+
203+
self::assertSame(4, $this->collector->getTotalInvalidated());
204+
$counts = $this->collector->getInvalidationCounts();
205+
self::assertSame(2, $counts['created']);
206+
self::assertSame(1, $counts['updated']);
207+
self::assertSame(1, $counts['deleted']);
208+
}
209+
210+
public function test_collect_empty_invalidation_counts(): void
211+
{
212+
$this->collector->collect(new Request(), new Response());
213+
214+
self::assertSame(0, $this->collector->getTotalInvalidated());
215+
$counts = $this->collector->getInvalidationCounts();
216+
self::assertSame(0, $counts['created']);
217+
self::assertSame(0, $counts['updated']);
218+
self::assertSame(0, $counts['deleted']);
219+
}
220+
221+
public function test_collect_records_cache_purged_iris(): void
222+
{
223+
$this->collectorData->recordCachePurge(['/_api/_/pages/1', '/_api/_/layouts/1']);
224+
$this->collectorData->recordCachePurge(['/_api/_/pages/2']);
225+
226+
$this->collector->collect(new Request(), new Response());
227+
228+
self::assertSame(3, $this->collector->getCachePurgedCount());
229+
$iris = $this->collector->getCachePurgedIris();
230+
self::assertContains('/_api/_/pages/1', $iris);
231+
self::assertContains('/_api/_/layouts/1', $iris);
232+
self::assertContains('/_api/_/pages/2', $iris);
233+
}
234+
235+
public function test_collect_empty_cache_purged_iris(): void
236+
{
237+
$this->collector->collect(new Request(), new Response());
238+
239+
self::assertSame(0, $this->collector->getCachePurgedCount());
240+
self::assertSame([], $this->collector->getCachePurgedIris());
241+
}
242+
243+
public function test_collect_records_mercure_private_upgrades(): void
244+
{
245+
$this->collectorData->recordMercurePrivateUpgrade(['topic1', 'topic2'], 'App\Entity\Article');
246+
$this->collectorData->recordMercurePrivateUpgrade(['topic3'], 'App\Entity\Post');
247+
248+
$this->collector->collect(new Request(), new Response());
249+
250+
self::assertSame(2, $this->collector->getMercurePrivateUpgradeCount());
251+
$upgrades = $this->collector->getMercurePrivateUpgrades();
252+
self::assertCount(2, $upgrades);
253+
self::assertSame(['topic1', 'topic2'], $upgrades[0]['topics']);
254+
self::assertSame('App\Entity\Article', $upgrades[0]['resourceClass']);
255+
self::assertSame(['topic3'], $upgrades[1]['topics']);
256+
}
257+
258+
public function test_collect_empty_mercure_private_upgrades(): void
259+
{
260+
$this->collector->collect(new Request(), new Response());
261+
262+
self::assertSame(0, $this->collector->getMercurePrivateUpgradeCount());
263+
self::assertSame([], $this->collector->getMercurePrivateUpgrades());
264+
}
265+
266+
public function test_reset_also_clears_collector_data(): void
267+
{
268+
$this->collectorData->recordPublishableQuery('App\Entity\Article', 'published-only', 'select');
269+
$this->collectorData->recordPageDataResolution('image', 'App\Entity\Article', null);
270+
$this->collectorData->recordInvalidationCount('created');
271+
$this->collectorData->recordCachePurge(['/_api/_/pages/1']);
272+
$this->collectorData->recordMercurePrivateUpgrade(['topic1'], 'App\Entity\Article');
273+
274+
$this->collector->collect(new Request(), new Response());
275+
$this->collector->reset();
276+
277+
// Re-collect from now-empty collectorData to confirm it was cleared
278+
$this->collector->collect(new Request(), new Response());
279+
280+
self::assertSame(0, $this->collector->getPublishableQueryCount());
281+
self::assertSame(0, $this->collector->getPageDataResolutionCount());
282+
self::assertSame(0, $this->collector->getTotalInvalidated());
283+
self::assertSame(0, $this->collector->getCachePurgedCount());
284+
self::assertSame(0, $this->collector->getMercurePrivateUpgradeCount());
285+
}
286+
287+
public function test_is_jwt_cookie_present_returns_false_without_collect(): void
288+
{
289+
// Before collect(), data is empty — accessor must return false via ?? false fallback
290+
self::assertFalse($this->collector->isJwtCookiePresent());
291+
}
292+
293+
public function test_is_jwt_refresh_issued_returns_false_without_collect(): void
294+
{
295+
self::assertFalse($this->collector->isJwtRefreshIssued());
296+
}
297+
298+
public function test_is_jwt_cookie_cleared_returns_false_without_collect(): void
299+
{
300+
self::assertFalse($this->collector->isJwtCookieCleared());
301+
}
302+
303+
public function test_is_page_data_found_returns_false_without_collect(): void
304+
{
305+
self::assertFalse($this->collector->isPageDataFound());
306+
}
307+
308+
public function test_published_topics_count_returns_zero_without_collect(): void
309+
{
310+
self::assertSame(0, $this->collector->getPublishedTopicsCount());
311+
}
312+
313+
public function test_publishable_query_count_returns_zero_without_collect(): void
314+
{
315+
// Kills DecrementInteger/IncrementInteger/CastInt mutants on ?? 0 default
316+
self::assertSame(0, $this->collector->getPublishableQueryCount());
317+
}
318+
319+
public function test_page_data_resolution_count_returns_zero_without_collect(): void
320+
{
321+
self::assertSame(0, $this->collector->getPageDataResolutionCount());
322+
}
323+
324+
public function test_total_invalidated_returns_zero_without_collect(): void
325+
{
326+
self::assertSame(0, $this->collector->getTotalInvalidated());
327+
}
328+
329+
public function test_cache_purged_count_returns_zero_without_collect(): void
330+
{
331+
self::assertSame(0, $this->collector->getCachePurgedCount());
332+
}
333+
334+
public function test_mercure_private_upgrade_count_returns_zero_without_collect(): void
335+
{
336+
self::assertSame(0, $this->collector->getMercurePrivateUpgradeCount());
337+
}
338+
339+
public function test_invalidation_counts_returns_zeros_without_collect(): void
340+
{
341+
// Kills ArrayItemRemoval mutant on the default ['created' => 0, 'updated' => 0, 'deleted' => 0]
342+
$counts = $this->collector->getInvalidationCounts();
343+
self::assertSame(0, $counts['created']);
344+
self::assertSame(0, $counts['updated']);
345+
self::assertSame(0, $counts['deleted']);
346+
}
347+
348+
public function test_collect_records_more_than_one_publishable_query(): void
349+
{
350+
// Extra precision test: count must be 2, not 1 or -1 — kills Increment/Decrement on non-zero counts
351+
$this->collectorData->recordPublishableQuery('App\Entity\A', 'published-only', 'select');
352+
$this->collectorData->recordPublishableQuery('App\Entity\B', 'draft', 'select');
353+
354+
$this->collector->collect(new Request(), new Response());
355+
356+
self::assertSame(2, $this->collector->getPublishableQueryCount());
357+
}
358+
359+
public function test_collect_records_more_than_one_page_data_resolution(): void
360+
{
361+
$this->collectorData->recordPageDataResolution('image', 'App\Entity\Article', null);
362+
$this->collectorData->recordPageDataResolution('content', null, 'not_found');
363+
364+
$this->collector->collect(new Request(), new Response());
365+
366+
self::assertSame(2, $this->collector->getPageDataResolutionCount());
367+
}
368+
369+
public function test_collect_total_invalidated_sums_all_categories(): void
370+
{
371+
$this->collectorData->recordInvalidationCount('created');
372+
$this->collectorData->recordInvalidationCount('updated');
373+
$this->collectorData->recordInvalidationCount('deleted');
374+
375+
$this->collector->collect(new Request(), new Response());
376+
377+
self::assertSame(3, $this->collector->getTotalInvalidated());
378+
}
379+
380+
public function test_collect_cache_purged_count_reflects_total_iris(): void
381+
{
382+
$this->collectorData->recordCachePurge(['/_api/_/pages/1', '/_api/_/pages/2']);
383+
384+
$this->collector->collect(new Request(), new Response());
385+
386+
self::assertSame(2, $this->collector->getCachePurgedCount());
387+
}
388+
389+
public function test_collect_mercure_private_upgrade_count_reflects_entries(): void
390+
{
391+
$this->collectorData->recordMercurePrivateUpgrade(['topic1'], 'App\Entity\Article');
392+
$this->collectorData->recordMercurePrivateUpgrade(['topic2'], 'App\Entity\Post');
393+
394+
$this->collector->collect(new Request(), new Response());
395+
396+
self::assertSame(2, $this->collector->getMercurePrivateUpgradeCount());
397+
}
144398
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Silverback API Components Bundle Project
5+
*
6+
* (c) Daniel West <daniel@silverback.is>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Silverback\ApiComponentsBundle\Tests\Entity\Core;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Silverback\ApiComponentsBundle\Entity\Core\ComponentGroup;
16+
17+
#[\PHPUnit\Framework\Attributes\CoversClass(ComponentGroup::class)]
18+
class ComponentGroupTest extends TestCase
19+
{
20+
public function test_add_allowed_component_initialises_array_when_null(): void
21+
{
22+
$group = new ComponentGroup();
23+
// allowedComponents starts null
24+
self::assertNull($group->allowedComponents);
25+
26+
$group->addAllowedComponent('/_/some_components/123');
27+
28+
self::assertSame(['/_/some_components/123'], $group->allowedComponents);
29+
}
30+
31+
public function test_add_allowed_component_appends_to_existing_array(): void
32+
{
33+
$group = new ComponentGroup();
34+
$group->addAllowedComponent('/_/some_components/1');
35+
$group->addAllowedComponent('/_/some_components/2');
36+
37+
self::assertSame(['/_/some_components/1', '/_/some_components/2'], $group->allowedComponents);
38+
}
39+
40+
public function test_set_allowed_components_replaces_array(): void
41+
{
42+
$group = new ComponentGroup();
43+
$group->addAllowedComponent('/_/some_components/1');
44+
$group->setAllowedComponents(['/_/some_components/new']);
45+
46+
self::assertSame(['/_/some_components/new'], $group->allowedComponents);
47+
}
48+
49+
public function test_set_allowed_components_null_clears_array(): void
50+
{
51+
$group = new ComponentGroup();
52+
$group->addAllowedComponent('/_/some_components/1');
53+
$group->setAllowedComponents(null);
54+
55+
self::assertNull($group->allowedComponents);
56+
}
57+
}

0 commit comments

Comments
 (0)