mirror of
https://github.com/Vateron-Media/XC_VM.git
synced 2026-09-25 20:01:56 +02:00
Move Core/Events into XcVm\Core\Events and its sub-trees: \Contract (StoppableEventInterface), \Auth, \Module, \Settings, \Stream (the event classes), plus root EventDispatcher, ListenerProvider, ListensTo, AbstractEvent. - Namespace 14 files across 6 namespaces; cross-namespace refs imported via use (AbstractEvent/EventDispatcher → Contract\StoppableEventInterface; Stream events → root AbstractEvent). Built-in \Attribute (ListensTo) and ioncube \XC_VM (PackageInstalledEvent) qualified. - Rewrite 'use ListensTo;' → FQCN; add use to referrers across src/ and tests/; fix leading-backslash refs (\EventDispatcher, \ListensTo, \PackageInstalledEvent from the Module commit) to their FQCNs. - Tests: add real top-level use imports to ModuleLoaderBootTest (EventDispatcher) and ListensToAttributeTest (ListensTo) — the use-inserter skipped/mis-placed them due to a namespace() method and a heredoc fixture already containing the FQCN. - phpstan-baseline.neon regenerated (292→292). Completes Core/Container + Core/Events. Verified: php -l clean; PHPStan no errors; PHPUnit 295/295; EventDispatcher resolves and AbstractEvent implements XcVm\Core\Events\Contract\StoppableEventInterface.
250 lines
9.2 KiB
PHP
250 lines
9.2 KiB
PHP
<?php
|
|
|
|
use XcVm\Core\Events\EventDispatcher;
|
|
use XcVm\Core\Container\ServiceContainer;
|
|
use XcVm\Core\Module\ModuleLoader;
|
|
use XcVm\Core\Module\NavbarRegistry;
|
|
use XcVm\Core\Module\BaseModule;
|
|
use XcVm\Core\Http\Router;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* ModuleLoader — Composer package discovery tests.
|
|
*
|
|
* Verifies that ModuleLoader::loadAll() discovers modules installed as
|
|
* Composer packages (type: xcvm-module) from vendor/composer/installed.json,
|
|
* in addition to modules in the local modules/ directory.
|
|
*
|
|
* Directory layout mirrored in the temp root:
|
|
*
|
|
* $root/
|
|
* modules/
|
|
* local-alpha/ — local module (classic)
|
|
* vendor/
|
|
* composer/
|
|
* installed.json — Composer 2.x manifest
|
|
* vateron/
|
|
* composer-beta/ — Composer-installed xcvm-module package
|
|
* module.json
|
|
* ComposerBetaModule.php
|
|
*/
|
|
final class ComposerDiscoveryTest extends TestCase {
|
|
|
|
private string $root;
|
|
|
|
protected function setUp(): void {
|
|
ServiceContainer::resetInstance();
|
|
Router::resetInstance();
|
|
NavbarRegistry::reset();
|
|
EventDispatcher::resetInstance();
|
|
|
|
$this->root = sys_get_temp_dir() . '/xc_vm_cdtest_' . bin2hex(random_bytes(6));
|
|
mkdir($this->root . '/modules', 0775, true);
|
|
mkdir($this->root . '/vendor/composer', 0775, true);
|
|
}
|
|
|
|
protected function tearDown(): void {
|
|
$this->removeTree($this->root);
|
|
ServiceContainer::resetInstance();
|
|
Router::resetInstance();
|
|
NavbarRegistry::reset();
|
|
EventDispatcher::resetInstance();
|
|
}
|
|
|
|
// ── Core behaviour ────────────────────────────────────────────
|
|
|
|
public function testLoadAllIgnoresVendorWhenInstalledJsonMissing(): void {
|
|
$this->createLocalModule('local-alpha');
|
|
// No installed.json in vendor/composer/ — just the directory
|
|
|
|
$loader = new ModuleLoader();
|
|
$loader->loadAll($this->root . '/modules');
|
|
|
|
$this->assertTrue($loader->isLoaded('local-alpha'));
|
|
$this->assertCount(1, $loader->getModules());
|
|
}
|
|
|
|
public function testLoadAllSkipsVendorPackagesWithWrongType(): void {
|
|
$this->createLocalModule('local-beta');
|
|
$this->writeInstalledJson([
|
|
['name' => 'vendor/other-lib', 'type' => 'library', 'install-path' => '../vendor/other-lib'],
|
|
]);
|
|
|
|
$loader = new ModuleLoader();
|
|
$loader->loadAll($this->root . '/modules');
|
|
|
|
$this->assertCount(1, $loader->getModules());
|
|
$this->assertTrue($loader->isLoaded('local-beta'));
|
|
}
|
|
|
|
public function testLoadAllDiscoversComposerModule(): void {
|
|
$this->createLocalModule('local-gamma');
|
|
$this->createComposerModule('composer-delta');
|
|
$this->writeInstalledJson([
|
|
['name' => 'vateron/composer-delta', 'type' => 'xcvm-module', 'install-path' => '../vateron/composer-delta'],
|
|
]);
|
|
|
|
$loader = new ModuleLoader();
|
|
$loader->loadAll($this->root . '/modules');
|
|
|
|
$this->assertTrue($loader->isLoaded('local-gamma'), 'local module must be loaded');
|
|
$this->assertTrue($loader->isLoaded('composer-delta'), 'Composer module must be loaded');
|
|
$this->assertCount(2, $loader->getModules());
|
|
}
|
|
|
|
public function testLoadAllDeduplicatesIfManifestAlreadyInLocalDir(): void {
|
|
// Even if someone manually symlinks a Composer package into modules/, it should not load twice.
|
|
$this->createComposerModule('dedup-epsilon');
|
|
$manifestPath = $this->root . '/vendor/vateron/dedup-epsilon/module.json';
|
|
|
|
// Also put the same path into the modules/ scan by symlinking (simulate duplicate)
|
|
$localLink = $this->root . '/modules/dedup-epsilon';
|
|
@mkdir($localLink, 0775, true);
|
|
copy($manifestPath, $localLink . '/module.json');
|
|
|
|
$this->writeInstalledJson([
|
|
['name' => 'vateron/dedup-epsilon', 'type' => 'xcvm-module', 'install-path' => '../vateron/dedup-epsilon'],
|
|
]);
|
|
|
|
// Both paths point to the same module name — loader should handle gracefully (isLoaded guard).
|
|
$loader = new ModuleLoader();
|
|
$loader->loadAll($this->root . '/modules');
|
|
|
|
// The module should be loaded exactly once
|
|
$this->assertTrue($loader->isLoaded('dedup-epsilon'));
|
|
$this->assertCount(1, $loader->getModules());
|
|
}
|
|
|
|
public function testLoadAllSupportsComposer1xFlatInstalledJson(): void {
|
|
$this->createComposerModule('legacy-zeta');
|
|
|
|
// Composer 1.x uses a flat array (no 'packages' key)
|
|
$installed = [
|
|
['name' => 'vateron/legacy-zeta', 'type' => 'xcvm-module', 'install-path' => '../vateron/legacy-zeta'],
|
|
];
|
|
file_put_contents(
|
|
$this->root . '/vendor/composer/installed.json',
|
|
json_encode($installed)
|
|
);
|
|
|
|
$loader = new ModuleLoader();
|
|
$loader->loadAll($this->root . '/modules');
|
|
|
|
$this->assertTrue($loader->isLoaded('legacy-zeta'));
|
|
}
|
|
|
|
public function testLoadAllSupportsModulePathExtra(): void {
|
|
// Package has module.json inside a subdirectory declared via extra.xcvm.module-path
|
|
$pkgDir = $this->root . '/vendor/vateron/nested-eta';
|
|
mkdir($pkgDir . '/src', 0775, true);
|
|
|
|
$pascal = 'NestedEtaModule';
|
|
$this->writeManifestAt($pkgDir . '/src/module.json', 'nested-eta');
|
|
$this->writeModuleClass($pkgDir . '/src', 'nested-eta');
|
|
|
|
$this->writeInstalledJson([
|
|
[
|
|
'name' => 'vateron/nested-eta',
|
|
'type' => 'xcvm-module',
|
|
'install-path' => '../vateron/nested-eta',
|
|
'extra' => ['xcvm' => ['module-path' => 'src']],
|
|
],
|
|
]);
|
|
|
|
$loader = new ModuleLoader();
|
|
$loader->loadAll($this->root . '/modules');
|
|
|
|
$this->assertTrue($loader->isLoaded('nested-eta'));
|
|
}
|
|
|
|
public function testLoadAllIgnoresComposerModuleWithMissingManifest(): void {
|
|
$this->createLocalModule('local-theta');
|
|
|
|
// Package directory exists but has no module.json
|
|
mkdir($this->root . '/vendor/vateron/no-manifest-iota', 0775, true);
|
|
|
|
$this->writeInstalledJson([
|
|
['name' => 'vateron/no-manifest-iota', 'type' => 'xcvm-module', 'install-path' => '../vateron/no-manifest-iota'],
|
|
]);
|
|
|
|
$loader = new ModuleLoader();
|
|
$loader->loadAll($this->root . '/modules');
|
|
|
|
$this->assertFalse($loader->isLoaded('no-manifest-iota'), 'package without module.json must be skipped');
|
|
$this->assertCount(1, $loader->getModules());
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────
|
|
|
|
private function pascal(string $name): string {
|
|
return implode('', array_map('ucfirst', explode('-', $name)));
|
|
}
|
|
|
|
private function writeManifestAt(string $path, string $name): void {
|
|
file_put_contents($path, json_encode([
|
|
'name' => $name,
|
|
'description' => 'composer discovery test',
|
|
'version' => '1.0.0',
|
|
'requires_core' => '>=2.0',
|
|
'environment' => 'main',
|
|
'dependencies' => [],
|
|
'has_navbar' => false,
|
|
'has_settings' => false,
|
|
]));
|
|
}
|
|
|
|
private function writeModuleClass(string $dir, string $name): void {
|
|
$pascal = $this->pascal($name);
|
|
$cls = $pascal . 'Module';
|
|
$namespace = 'XcVm\\Module\\' . $pascal;
|
|
|
|
file_put_contents($dir . '/' . $cls . '.php', "<?php\n"
|
|
. "namespace {$namespace};\n"
|
|
. "use XcVm\Core\Module\BaseModule;\n"
|
|
. "class {$cls} extends BaseModule {\n"
|
|
. "\tpublic function getName(): string { return '{$name}'; }\n"
|
|
. "\tpublic function getVersion(): string { return '1.0.0'; }\n"
|
|
. "}\n");
|
|
}
|
|
|
|
private function createLocalModule(string $name): void {
|
|
$dir = $this->root . '/modules/' . $name;
|
|
mkdir($dir, 0775, true);
|
|
$this->writeManifestAt($dir . '/module.json', $name);
|
|
$this->writeModuleClass($dir, $name);
|
|
}
|
|
|
|
private function createComposerModule(string $name): void {
|
|
$dir = $this->root . '/vendor/vateron/' . $name;
|
|
mkdir($dir, 0775, true);
|
|
$this->writeManifestAt($dir . '/module.json', $name);
|
|
$this->writeModuleClass($dir, $name);
|
|
}
|
|
|
|
/**
|
|
* Write vendor/composer/installed.json in Composer 2.x format.
|
|
*
|
|
* @param array[] $packages
|
|
*/
|
|
private function writeInstalledJson(array $packages): void {
|
|
file_put_contents(
|
|
$this->root . '/vendor/composer/installed.json',
|
|
json_encode(['packages' => $packages, 'dev-package-names' => []])
|
|
);
|
|
}
|
|
|
|
private function removeTree(string $dir): void {
|
|
if (!is_dir($dir)) {
|
|
return;
|
|
}
|
|
foreach (scandir($dir) as $item) {
|
|
if ($item === '.' || $item === '..') {
|
|
continue;
|
|
}
|
|
$path = $dir . '/' . $item;
|
|
is_dir($path) ? $this->removeTree($path) : unlink($path);
|
|
}
|
|
rmdir($dir);
|
|
}
|
|
}
|