static-php-cli/tests/StaticPHP/Registry/PackageLoaderTest.php

551 lines
18 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Registry;
use PHPUnit\Framework\TestCase;
use StaticPHP\Attribute\Package\Extension;
use StaticPHP\Attribute\Package\Library;
use StaticPHP\Attribute\Package\Target;
use StaticPHP\Config\PackageConfig;
use StaticPHP\Exception\RegistryException;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Package\LibraryPackage;
use StaticPHP\Package\PhpExtensionPackage;
use StaticPHP\Package\TargetPackage;
use StaticPHP\Registry\PackageLoader;
/**
* @internal
*/
class PackageLoaderTest extends TestCase
{
private string $tempDir;
protected function setUp(): void
{
parent::setUp();
$this->tempDir = sys_get_temp_dir() . '/package_loader_test_' . uniqid();
mkdir($this->tempDir, 0755, true);
// Reset PackageLoader state
$reflection = new \ReflectionClass(PackageLoader::class);
$property = $reflection->getProperty('packages');
$property->setAccessible(true);
$property->setValue(null, null);
$property = $reflection->getProperty('before_stages');
$property->setAccessible(true);
$property->setValue(null, []);
$property = $reflection->getProperty('after_stages');
$property->setAccessible(true);
$property->setValue(null, []);
$property = $reflection->getProperty('loaded_classes');
$property->setAccessible(true);
$property->setValue(null, []);
// Reset PackageConfig state
$configReflection = new \ReflectionClass(PackageConfig::class);
$configProperty = $configReflection->getProperty('package_configs');
$configProperty->setAccessible(true);
$configProperty->setValue(null, []);
}
protected function tearDown(): void
{
parent::tearDown();
// Clean up temp directory
if (is_dir($this->tempDir)) {
$this->removeDirectory($this->tempDir);
}
// Reset PackageLoader state
$reflection = new \ReflectionClass(PackageLoader::class);
$property = $reflection->getProperty('packages');
$property->setAccessible(true);
$property->setValue(null, null);
$property = $reflection->getProperty('before_stages');
$property->setAccessible(true);
$property->setValue(null, []);
$property = $reflection->getProperty('after_stages');
$property->setAccessible(true);
$property->setValue(null, []);
$property = $reflection->getProperty('loaded_classes');
$property->setAccessible(true);
$property->setValue(null, []);
// Reset PackageConfig state
$configReflection = new \ReflectionClass(PackageConfig::class);
$configProperty = $configReflection->getProperty('package_configs');
$configProperty->setAccessible(true);
$configProperty->setValue(null, []);
}
public function testInitPackageInstancesOnlyRunsOnce(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
PackageLoader::initPackageInstances();
// Should only initialize once
$this->assertTrue(PackageLoader::hasPackage('test-lib'));
}
public function testInitPackageInstancesCreatesLibraryPackage(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
$package = PackageLoader::getPackage('test-lib');
$this->assertInstanceOf(LibraryPackage::class, $package);
}
public function testInitPackageInstancesCreatesPhpExtensionPackage(): void
{
$this->createTestPackageConfig('test-ext', 'php-extension');
PackageLoader::initPackageInstances();
$package = PackageLoader::getPackage('test-ext');
$this->assertInstanceOf(PhpExtensionPackage::class, $package);
}
public function testInitPackageInstancesCreatesTargetPackage(): void
{
$this->createTestPackageConfig('test-target', 'target');
PackageLoader::initPackageInstances();
$package = PackageLoader::getPackage('test-target');
$this->assertInstanceOf(TargetPackage::class, $package);
}
public function testInitPackageInstancesCreatesVirtualTargetPackage(): void
{
$this->createTestPackageConfig('test-virtual-target', 'virtual-target');
PackageLoader::initPackageInstances();
$package = PackageLoader::getPackage('test-virtual-target');
$this->assertInstanceOf(TargetPackage::class, $package);
}
public function testInitPackageInstancesThrowsExceptionForUnknownType(): void
{
$this->createTestPackageConfig('test-unknown', 'unknown-type');
$this->expectException(RegistryException::class);
$this->expectExceptionMessage('has unknown type');
PackageLoader::initPackageInstances();
}
public function testHasPackageReturnsTrueForExistingPackage(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
$this->assertTrue(PackageLoader::hasPackage('test-lib'));
}
public function testHasPackageReturnsFalseForNonExistingPackage(): void
{
PackageLoader::initPackageInstances();
$this->assertFalse(PackageLoader::hasPackage('non-existent'));
}
public function testGetPackageReturnsPackage(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
$package = PackageLoader::getPackage('test-lib');
$this->assertInstanceOf(LibraryPackage::class, $package);
}
public function testGetPackageThrowsExceptionForNonExistingPackage(): void
{
PackageLoader::initPackageInstances();
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage('not found');
PackageLoader::getPackage('non-existent');
}
public function testGetTargetPackageReturnsTargetPackage(): void
{
$this->createTestPackageConfig('test-target', 'target');
PackageLoader::initPackageInstances();
$package = PackageLoader::getTargetPackage('test-target');
$this->assertInstanceOf(TargetPackage::class, $package);
}
public function testGetTargetPackageThrowsExceptionForNonTargetPackage(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage('is not a TargetPackage');
PackageLoader::getTargetPackage('test-lib');
}
public function testGetLibraryPackageReturnsLibraryPackage(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
$package = PackageLoader::getLibraryPackage('test-lib');
$this->assertInstanceOf(LibraryPackage::class, $package);
}
public function testGetLibraryPackageThrowsExceptionForNonLibraryPackage(): void
{
$this->createTestPackageConfig('ext-test-ext', 'php-extension');
PackageLoader::initPackageInstances();
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage('is not a LibraryPackage');
PackageLoader::getLibraryPackage('ext-test-ext');
}
public function testGetPackagesReturnsAllPackages(): void
{
$this->createTestPackageConfig('test-lib', 'library');
$this->createTestPackageConfig('test-ext', 'php-extension');
$this->createTestPackageConfig('test-target', 'target');
PackageLoader::initPackageInstances();
$packages = iterator_to_array(PackageLoader::getPackages());
$this->assertCount(3, $packages);
}
public function testGetPackagesWithTypeFilterReturnsFilteredPackages(): void
{
$this->createTestPackageConfig('test-lib', 'library');
$this->createTestPackageConfig('test-ext', 'php-extension');
$this->createTestPackageConfig('test-target', 'target');
PackageLoader::initPackageInstances();
$packages = iterator_to_array(PackageLoader::getPackages('library'));
$this->assertCount(1, $packages);
$this->assertArrayHasKey('test-lib', $packages);
}
public function testGetPackagesWithArrayTypeFilterReturnsFilteredPackages(): void
{
$this->createTestPackageConfig('test-lib', 'library');
$this->createTestPackageConfig('test-ext', 'php-extension');
$this->createTestPackageConfig('test-target', 'target');
PackageLoader::initPackageInstances();
$packages = iterator_to_array(PackageLoader::getPackages(['library', 'target']));
$this->assertCount(2, $packages);
$this->assertArrayHasKey('test-lib', $packages);
$this->assertArrayHasKey('test-target', $packages);
}
public function testLoadFromClassWithLibraryAttribute(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
$class = new #[Library('test-lib')] class {};
PackageLoader::loadFromClass(get_class($class));
$this->assertTrue(PackageLoader::hasPackage('test-lib'));
}
public function testLoadFromClassWithExtensionAttribute(): void
{
$this->createTestPackageConfig('ext-test-ext', 'php-extension');
PackageLoader::initPackageInstances();
$class = new #[Extension('ext-test-ext')] class {};
PackageLoader::loadFromClass(get_class($class));
$this->assertTrue(PackageLoader::hasPackage('ext-test-ext'));
}
public function testLoadFromClassWithTargetAttribute(): void
{
$this->createTestPackageConfig('test-target', 'target');
PackageLoader::initPackageInstances();
$class = new #[Target('test-target')] class {};
PackageLoader::loadFromClass(get_class($class));
$this->assertTrue(PackageLoader::hasPackage('test-target'));
}
public function testLoadFromClassThrowsExceptionForUndefinedPackage(): void
{
PackageLoader::initPackageInstances();
$class = new #[Library('undefined-lib')] class {};
$this->expectException(RegistryException::class);
$this->expectExceptionMessage('not defined in config');
PackageLoader::loadFromClass(get_class($class));
}
public function testLoadFromClassThrowsExceptionForTypeMismatch(): void
{
$this->createTestPackageConfig('ext-test-lib', 'library');
PackageLoader::initPackageInstances();
// Try to load with Extension attribute but config says library
$class = new #[Extension('ext-test-lib')] class {};
$this->expectException(RegistryException::class);
$this->expectExceptionMessage('type mismatch');
PackageLoader::loadFromClass(get_class($class));
}
public function testLoadFromClassSkipsDuplicateClasses(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
$className = get_class(new #[Library('test-lib')] class {});
// Load twice
PackageLoader::loadFromClass($className);
PackageLoader::loadFromClass($className);
// Should not throw exception
$this->assertTrue(PackageLoader::hasPackage('test-lib'));
}
public function testLoadFromClassWithNoPackageAttribute(): void
{
PackageLoader::initPackageInstances();
$class = new class {
public function regularMethod(): void {}
};
// Should not throw exception
PackageLoader::loadFromClass(get_class($class));
// Verify no side effects
$this->assertTrue(true);
}
public function testCheckLoadedStageEventsThrowsExceptionForUnknownPackage(): void
{
PackageLoader::initPackageInstances();
// Manually add a before_stage for non-existent package
$reflection = new \ReflectionClass(PackageLoader::class);
$property = $reflection->getProperty('before_stages');
$property->setAccessible(true);
$property->setValue(null, [
'non-existent-package' => [
'stage-name' => [[fn () => null, null]],
],
]);
$this->expectException(RegistryException::class);
$this->expectExceptionMessage('unknown package');
PackageLoader::checkLoadedStageEvents();
}
public function testCheckLoadedStageEventsThrowsExceptionForUnknownStage(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
// Manually add a before_stage for non-existent stage
$reflection = new \ReflectionClass(PackageLoader::class);
$property = $reflection->getProperty('before_stages');
$property->setAccessible(true);
$property->setValue(null, [
'test-lib' => [
'non-existent-stage' => [[fn () => null, null]],
],
]);
$this->expectException(RegistryException::class);
$this->expectExceptionMessage('is not registered');
PackageLoader::checkLoadedStageEvents();
}
public function testCheckLoadedStageEventsThrowsExceptionForUnknownOnlyWhenPackage(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
$package = PackageLoader::getPackage('test-lib');
$package->addStage('test-stage', fn () => null);
// Manually add a before_stage with unknown only_when_package_resolved
$reflection = new \ReflectionClass(PackageLoader::class);
$property = $reflection->getProperty('before_stages');
$property->setAccessible(true);
$property->setValue(null, [
'test-lib' => [
'test-stage' => [[fn () => null, 'non-existent-package']],
],
]);
$this->expectException(RegistryException::class);
$this->expectExceptionMessage('unknown only_when_package_resolved package');
PackageLoader::checkLoadedStageEvents();
}
public function testGetBeforeStageCallbacksReturnsCallbacks(): void
{
PackageLoader::initPackageInstances();
// Manually add some before_stage callbacks
$callback1 = fn () => 'callback1';
$callback2 = fn () => 'callback2';
$reflection = new \ReflectionClass(PackageLoader::class);
$property = $reflection->getProperty('before_stages');
$property->setAccessible(true);
$property->setValue(null, [
'test-package' => [
'test-stage' => [
[$callback1, null],
[$callback2, null],
],
],
]);
$callbacks = iterator_to_array(PackageLoader::getBeforeStageCallbacks('test-package', 'test-stage'));
$this->assertCount(2, $callbacks);
}
public function testGetAfterStageCallbacksReturnsCallbacks(): void
{
PackageLoader::initPackageInstances();
// Manually add some after_stage callbacks
$callback1 = fn () => 'callback1';
$callback2 = fn () => 'callback2';
$reflection = new \ReflectionClass(PackageLoader::class);
$property = $reflection->getProperty('after_stages');
$property->setAccessible(true);
$property->setValue(null, [
'test-package' => [
'test-stage' => [
[$callback1, null],
[$callback2, null],
],
],
]);
$callbacks = PackageLoader::getAfterStageCallbacks('test-package', 'test-stage');
$this->assertCount(2, $callbacks);
}
public function testGetBeforeStageCallbacksReturnsEmptyForNonExistentPackage(): void
{
PackageLoader::initPackageInstances();
$callbacks = iterator_to_array(PackageLoader::getBeforeStageCallbacks('non-existent', 'stage'));
$this->assertEmpty($callbacks);
}
public function testGetAfterStageCallbacksReturnsEmptyForNonExistentPackage(): void
{
PackageLoader::initPackageInstances();
$callbacks = PackageLoader::getAfterStageCallbacks('non-existent', 'stage');
$this->assertEmpty($callbacks);
}
public function testRegisterAllDefaultStagesRegistersForPhpExtensions(): void
{
$this->createTestPackageConfig('test-ext', 'php-extension');
PackageLoader::initPackageInstances();
PackageLoader::registerAllDefaultStages();
$package = PackageLoader::getPackage('test-ext');
$this->assertInstanceOf(PhpExtensionPackage::class, $package);
// Default stages should be registered (we can't easily verify this without accessing internal state)
}
public function testLoadFromPsr4DirLoadsAllClasses(): void
{
$this->createTestPackageConfig('test-lib', 'library');
// Create a PSR-4 directory structure
$psr4Dir = $this->tempDir . '/PackageClasses';
mkdir($psr4Dir, 0755, true);
// Create test class file
$classContent = '<?php
namespace Test\Package;
use StaticPHP\Attribute\Package\Library;
#[Library("test-lib")]
class TestPackage1 {
}';
file_put_contents($psr4Dir . '/TestPackage1.php', $classContent);
// Load with auto_require enabled
PackageLoader::loadFromPsr4Dir($psr4Dir, 'Test\Package', true);
$this->assertTrue(PackageLoader::hasPackage('test-lib'));
}
private function removeDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . '/' . $file;
if (is_dir($path)) {
$this->removeDirectory($path);
} else {
unlink($path);
}
}
rmdir($dir);
}
private function createTestPackageConfig(string $name, string $type): void
{
$reflection = new \ReflectionClass(PackageConfig::class);
$property = $reflection->getProperty('package_configs');
$property->setAccessible(true);
$configs = $property->getValue();
$configs[$name] = [
'type' => $type,
'deps' => [],
];
$property->setValue(null, $configs);
}
}