Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
122 lines
4.8 KiB
PHP
122 lines
4.8 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Release;
|
||
|
||
use FilesystemIterator;
|
||
use LIT\MCP\Integrations\Divi\Release\DiviAdapter;
|
||
use LIT\MCP\Integrations\Divi\Release\DiviAdapters;
|
||
use LIT\MCP\Integrations\Divi\Release\DiviTouches;
|
||
use PHPUnit\Framework\TestCase;
|
||
use RecursiveDirectoryIterator;
|
||
use RecursiveIteratorIterator;
|
||
use ReflectionClass;
|
||
|
||
/**
|
||
* Spec §3: only the adapters in DiviAdapters::CLASSES touch Divi, and every Divi name an adapter uses stands in its
|
||
* manifest (and nothing more).
|
||
*/
|
||
final class DiviBoundaryTest extends TestCase
|
||
{
|
||
/**
|
||
* Files that still touch Divi outside an adapter (paths below src/). Tasks 3–7 shrink this list to
|
||
* DiviCompatibility.php; Task 8 removes it together with testLegacyFilesStillTouchDivi.
|
||
*/
|
||
private const LEGACY = [
|
||
'Integrations/Divi/Design/DesignSystemReader.php',
|
||
'Integrations/Divi/DiviCompatibility.php',
|
||
'Integrations/Divi/Library/DiviLibraryStore.php',
|
||
'Integrations/Divi/Library/DiviRoles.php',
|
||
'Integrations/Divi/Library/LibraryItemSpec.php',
|
||
'Integrations/Divi/Library/NewLibraryEntry.php',
|
||
'Integrations/Divi/Pages/DiviFlags.php',
|
||
'Integrations/Divi/Pages/DiviPage.php',
|
||
'Integrations/Divi/Pages/DiviSaver.php',
|
||
'Integrations/Divi/Pages/DynamicContentMap.php',
|
||
'Integrations/Divi/Pages/WpLinkTargets.php',
|
||
'Integrations/Divi/Schema/DiviModules.php',
|
||
'Integrations/Divi/ThemeBuilder/DiviThemeBuilder.php',
|
||
'Integrations/Media/MediaUsageFinder.php',
|
||
'Integrations/WordPress/Content/PostPresenter.php',
|
||
'Integrations/WordPress/Schema/ContentSchema.php',
|
||
];
|
||
|
||
public function testOnlyAdaptersTouchDivi(): void
|
||
{
|
||
$offenders = [];
|
||
foreach (self::sourceFiles() as $relative => $path) {
|
||
$touches = DiviTouches::inFile($path);
|
||
if ($touches === [] || in_array($relative, self::LEGACY, true)) {
|
||
continue;
|
||
}
|
||
if (! in_array(self::classOf($relative), DiviAdapters::CLASSES, true)) {
|
||
$offenders[] = $relative . ': ' . implode(', ', array_unique(array_column($touches, 'name')));
|
||
}
|
||
}
|
||
|
||
self::assertSame([], $offenders, "Divi darf nur ein Adapter aus DiviAdapters::CLASSES berühren:\n"
|
||
. implode("\n", $offenders));
|
||
}
|
||
|
||
public function testLegacyFilesStillTouchDivi(): void
|
||
{
|
||
$files = self::sourceFiles();
|
||
foreach (self::LEGACY as $relative) {
|
||
self::assertArrayHasKey($relative, $files, $relative . ' gibt es nicht mehr – aus LEGACY streichen');
|
||
self::assertNotSame([], DiviTouches::inFile($files[$relative]), $relative . ' ist sauber – aus LEGACY streichen');
|
||
self::assertNotContains(self::classOf($relative), DiviAdapters::CLASSES, $relative . ' ist ein Adapter');
|
||
}
|
||
}
|
||
|
||
public function testAdaptersDeclareExactlyTheDiviNamesTheyUse(): void
|
||
{
|
||
foreach (DiviAdapters::CLASSES as $class) {
|
||
self::assertTrue(is_subclass_of($class, DiviAdapter::class), $class . ' implementiert DiviAdapter nicht');
|
||
$declared = $class::manifest()->names();
|
||
$used = array_values(array_unique(array_column(
|
||
DiviTouches::inFile((string) (new ReflectionClass($class))->getFileName()),
|
||
'name'
|
||
)));
|
||
|
||
self::assertSame([], array_values(array_diff($used, $declared)), $class . ': nicht im Manifest');
|
||
self::assertSame(
|
||
[],
|
||
array_values(array_diff(array_filter($declared, [DiviTouches::class, 'detects']), $used)),
|
||
$class . ': im Manifest, aber nicht benutzt'
|
||
);
|
||
}
|
||
}
|
||
|
||
public function testMirrorIdsAreUniqueAndFilesRelative(): void
|
||
{
|
||
$ids = [];
|
||
foreach (DiviAdapters::mirrors() as $mirror) {
|
||
self::assertNotContains($mirror->id, $ids, 'doppelte Mirror-ID ' . $mirror->id);
|
||
self::assertStringStartsNotWith('/', $mirror->file);
|
||
$ids[] = $mirror->id;
|
||
}
|
||
}
|
||
|
||
/** @return array<string, string> Path below src/ => absolute path. */
|
||
private static function sourceFiles(): array
|
||
{
|
||
$root = dirname(__DIR__, 5) . '/src/';
|
||
$files = [];
|
||
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS));
|
||
foreach ($iterator as $file) {
|
||
if ($file->getExtension() === 'php') {
|
||
$files[substr((string) $file->getPathname(), strlen($root))] = (string) $file->getPathname();
|
||
}
|
||
}
|
||
ksort($files);
|
||
|
||
return $files;
|
||
}
|
||
|
||
private static function classOf(string $relative): string
|
||
{
|
||
return 'LIT\\MCP\\' . str_replace(['/', '.php'], ['\\', ''], $relative);
|
||
}
|
||
}
|