Files
XC_VM/tests/Unit/ModuleLoaderTest.php
T
Divarion-D 881991e52b chore(psr4): phase 3 (Core hubs) — namespace Core/Module
Move Core/Module into XcVm\Core\Module (BaseModule, ModuleInterface,
MigratableInterface, ModuleLoader, ModuleManager, NavbarRegistry, NavbarItem,
CoreNavbarProvider) and Core/Module/Contract into XcVm\Core\Module\Contract
(the 6 provider interfaces). Completes the Core hub layer.

- Namespace 14 files (8 root + 6 Contract). Root files import the contracts via
  'use XcVm\Core\Module\Contract\...'; NavbarProviderInterface imports the
  root NavbarRegistry. Qualify still-global deps with leading backslash
  (\ServiceContainer, \CommandRegistry, \ModuleState, \ServerEnvironment,
  \EventDispatcher, \ListensTo, the Module exceptions, \ZipArchive,
  \InvalidArgumentException, \RuntimeException) — Container/Events/Enum/Exception
  migrate later. Migrated deps (Router, StreamPipeline, ...) keep their use.
- Rewrite the modules' 'use BaseModule/NavbarRegistry/NavbarItem;' → FQCN; add
  'use XcVm\Core\Module\...;' to other referrers across src/ and tests/.
- Fix pre-existing leading-backslash refs to the FQCN.
- Tests: qualify the module fixtures' generated 'use ModuleInterface;' /
  'use BaseModule;' / 'use NavbarRegistry;' and the registerRoutes type hint to
  FQCN; add real top-level use imports to the fixture-builder tests; update
  InterfaceContractTest registerNavbar param-type to the FQCN. Repaired
  use-inserter mis-placements in the two tests that declare a namespace() method.
- Public/index.php: class_exists('ModuleLoader') → class_exists(ModuleLoader::class).
- phpstan-baseline.neon regenerated (292→292; no new/unknown-class errors).

Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; the 4 real modules
load and resolve as XcVm\Core\Module\ModuleInterface; leading-backslash
re-sweep across all migrated classes is clean.
2026-06-24 22:05:10 +03:00

107 lines
3.8 KiB
PHP

<?php
use XcVm\Core\Module\ModuleLoader;
use XcVm\Core\Module\NavbarRegistry;
use XcVm\Core\Module\ModuleInterface;
use XcVm\Core\Http\Router;
use PHPUnit\Framework\TestCase;
final class ModuleLoaderTest extends TestCase {
public function testLoadAllThrowsWhenDependencyMissing(): void {
$root = $this->createModulesRoot();
$this->createModule($root, 'missing-alpha', [
'dependencies' => ['missing-module'],
]);
$loader = new ModuleLoader();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('requires missing dependency missing-module');
$loader->loadAll($root);
}
public function testLoadAllThrowsWhenDependenciesAreCyclic(): void {
$root = $this->createModulesRoot();
$this->createModule($root, 'cycle-alpha', ['dependencies' => ['cycle-beta']]);
$this->createModule($root, 'cycle-beta', ['dependencies' => ['cycle-alpha']]);
$loader = new ModuleLoader();
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('cyclic module dependency detected');
$loader->loadAll($root);
}
public function testLoadAllSortsModulesByDependencies(): void {
$root = $this->createModulesRoot();
$this->createModule($root, 'order-base');
$this->createModule($root, 'order-feature', ['dependencies' => ['order-base']]);
$loader = new ModuleLoader();
$loader->loadAll($root);
$this->assertSame(['order-base', 'order-feature'], array_keys($loader->getModules()));
}
public function testLoadAllSkipsForeignEnvironmentModules(): void {
$root = $this->createModulesRoot();
$this->createModule($root, 'env-main-only', ['environment' => 'main']);
$this->createModule($root, 'env-lb-only', ['environment' => 'lb']);
$loader = new ModuleLoader();
$loader->loadAll($root);
$this->assertTrue($loader->isLoaded('env-main-only'));
$this->assertFalse($loader->isLoaded('env-lb-only'));
}
private function createModulesRoot(): string {
$path = sys_get_temp_dir() . '/xc_vm_modules_test_' . bin2hex(random_bytes(6));
mkdir($path, 0775, true);
return $path;
}
private function createModule(string $root, string $name, array $manifestOverrides = []): void {
$modulePath = $root . '/' . $name;
mkdir($modulePath, 0775, true);
$pascal = implode('', array_map('ucfirst', explode('-', $name)));
$className = $pascal . 'Module';
$namespace = 'XcVm\\Module\\' . $pascal;
$manifest = array_merge([
'name' => $name,
'description' => 'test module',
'version' => '1.0.0',
'requires_core' => '>=2.0',
'environment' => 'main',
'dependencies' => [],
'has_navbar' => false,
'has_settings' => false,
], $manifestOverrides);
file_put_contents($modulePath . '/module.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
$php = '<?php' . "\n"
. 'namespace ' . $namespace . ';' . "\n"
. 'use XcVm\Core\Module\ModuleInterface;' . "\n"
. 'use ServiceContainer;' . "\n"
. 'use Router;' . "\n"
. 'use CommandRegistry;' . "\n"
. 'use XcVm\Core\Module\NavbarRegistry;' . "\n"
. 'class ' . $className . ' implements ModuleInterface {' . "\n"
. "\tpublic function getName(): string { return '{$name}'; }" . "\n"
. "\tpublic function getVersion(): string { return '1.0.0'; }" . "\n"
. "\tpublic function boot(ServiceContainer \$container): void {}" . "\n"
. "\tpublic function registerRoutes(\\XcVm\\Core\\Http\\Router \$router): void {}" . "\n"
. "\tpublic function registerCommands(CommandRegistry \$registry): void {}" . "\n"
. "\tpublic function getEventSubscribers(): array { return []; }" . "\n"
. "\tpublic function install(): void {}" . "\n"
. "\tpublic function uninstall(): void {}" . "\n"
. "\tpublic function registerNavbar(NavbarRegistry \$registry): void {}" . "\n"
. '}' . "\n";
file_put_contents($modulePath . '/' . $className . '.php', $php);
}
}