Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
147 lines
5.3 KiB
PHP
147 lines
5.3 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace LIT\MCP\Tests\Unit\Architecture;
|
||
|
||
use FilesystemIterator;
|
||
use PHPUnit\Framework\TestCase;
|
||
use RecursiveDirectoryIterator;
|
||
use RecursiveIteratorIterator;
|
||
|
||
/**
|
||
* The generic core knows no Divi class: nothing below the CORE_PATHS imports or names a class of
|
||
* LIT\MCP\Integrations\Divi. Divi plugs in through ports (AbilityGuard, ToolRefusal, BuilderDetector,
|
||
* MediaUsageSource, EnvironmentComponent, ContentSchema's excluded post types) that Plugin.php – the composition
|
||
* root, outside these paths – wires up. Divi's own names (et_* …) are DiviBoundaryTest's business, not this test's.
|
||
*/
|
||
final class CoreIndependenceTest extends TestCase
|
||
{
|
||
/** Directories below src/ that must not use Divi classes. */
|
||
private const CORE_PATHS = [
|
||
'Abilities',
|
||
'Integrations/Core',
|
||
'Integrations/WordPress',
|
||
'Integrations/Media',
|
||
'Admin',
|
||
'Guides',
|
||
'Server',
|
||
];
|
||
|
||
/** Files inside CORE_PATHS that may use Divi classes, each with its reason. */
|
||
private const EXCEPTIONS = [
|
||
'Admin/DiviTab.php' => 'The settings tab "Divi" is Divi UI: release state, admin check and release/withdraw.',
|
||
];
|
||
|
||
public function testTheCoreUsesNoDiviClass(): void
|
||
{
|
||
$offenders = [];
|
||
foreach (self::coreFiles() as $relative => $path) {
|
||
if (isset(self::EXCEPTIONS[$relative])) {
|
||
continue;
|
||
}
|
||
$names = self::diviNames((string) file_get_contents($path));
|
||
if ($names !== []) {
|
||
$offenders[] = $relative . ': ' . implode(', ', $names);
|
||
}
|
||
}
|
||
|
||
self::assertSame([], $offenders, "Der Kern darf keine Divi-Klasse benutzen (Port über Plugin.php verdrahten):\n"
|
||
. implode("\n", $offenders));
|
||
}
|
||
|
||
public function testEveryExceptionExistsAndStillNeedsIt(): void
|
||
{
|
||
$files = self::coreFiles();
|
||
foreach (array_keys(self::EXCEPTIONS) as $relative) {
|
||
self::assertArrayHasKey($relative, $files, $relative . ' gibt es nicht mehr – Ausnahme streichen');
|
||
self::assertNotSame(
|
||
[],
|
||
self::diviNames((string) file_get_contents($files[$relative])),
|
||
$relative . ' braucht die Ausnahme nicht mehr – streichen'
|
||
);
|
||
}
|
||
}
|
||
|
||
public function testTheScanFindsImportsQualifiedNamesGroupUsesAndClassStrings(): void
|
||
{
|
||
$source = <<<'PHP'
|
||
<?php
|
||
use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||
use LIT\MCP\Integrations\{Media\Finder, Divi\Pages\DiviPages};
|
||
$a = \LIT\MCP\Integrations\Divi\Schema\DiviModules::isAvailable();
|
||
$b = 'LIT\\MCP\\Integrations\\Divi\\Design\\DesignSystemReader';
|
||
$c = "LIT\\MCP\\Integrations\\Divi\\Library\\LibraryAccess";
|
||
PHP;
|
||
|
||
self::assertSame(
|
||
[
|
||
'Divi\Pages\DiviPages',
|
||
'LIT\MCP\Integrations\Divi\Design\DesignSystemReader',
|
||
'LIT\MCP\Integrations\Divi\Library\LibraryAccess',
|
||
'LIT\MCP\Integrations\Divi\Release\ReleaseGate',
|
||
'LIT\MCP\Integrations\Divi\Schema\DiviModules',
|
||
],
|
||
self::diviNames($source)
|
||
);
|
||
}
|
||
|
||
public function testTheScanIgnoresCommentsOtherNamespacesAndPlainWords(): void
|
||
{
|
||
$source = <<<'PHP'
|
||
<?php
|
||
// use LIT\MCP\Integrations\Divi\Release\ReleaseGate;
|
||
/** @see \LIT\MCP\Integrations\Divi\Pages\DiviPages */
|
||
use LIT\MCP\Integrations\Media\MediaUsageSource;
|
||
use LIT\MCP\Abilities\DiviMarkup;
|
||
$x = 'Divi';
|
||
$y = 'Ein Divi-Seitenaufbau';
|
||
PHP;
|
||
|
||
self::assertSame([], self::diviNames($source));
|
||
}
|
||
|
||
/** @return list<string> Sorted, unique names of Divi classes the source uses. */
|
||
private static function diviNames(string $source): array
|
||
{
|
||
$names = [];
|
||
foreach (token_get_all($source) as $token) {
|
||
if (! is_array($token)) {
|
||
continue;
|
||
}
|
||
$text = match ($token[0]) {
|
||
T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED => ltrim($token[1], '\\'),
|
||
T_CONSTANT_ENCAPSED_STRING => ltrim(str_replace('\\\\', '\\', substr($token[1], 1, -1)), '\\'),
|
||
default => '',
|
||
};
|
||
if (preg_match('/^(?:LIT\\\\MCP\\\\Integrations\\\\)?Divi\\\\[A-Za-z0-9_\\\\]+$/', $text) === 1) {
|
||
$names[] = $text;
|
||
}
|
||
}
|
||
$names = array_values(array_unique($names));
|
||
sort($names);
|
||
|
||
return $names;
|
||
}
|
||
|
||
/** @return array<string, string> Path below src/ => absolute path of every PHP file in CORE_PATHS. */
|
||
private static function coreFiles(): array
|
||
{
|
||
$root = dirname(__DIR__, 3) . '/src/';
|
||
$files = [];
|
||
foreach (self::CORE_PATHS as $directory) {
|
||
$iterator = new RecursiveIteratorIterator(
|
||
new RecursiveDirectoryIterator($root . $directory, 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;
|
||
}
|
||
}
|