Refactor test structure and update paths for improved organization

This commit is contained in:
crazywhalecc
2025-12-10 11:15:44 +08:00
parent 78375632b4
commit bde1440617
43 changed files with 4396 additions and 2941 deletions

View File

@@ -0,0 +1,440 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Registry;
use PHPUnit\Framework\TestCase;
use StaticPHP\Artifact\Artifact;
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
use StaticPHP\Attribute\Artifact\AfterSourceExtract;
use StaticPHP\Attribute\Artifact\BinaryExtract;
use StaticPHP\Attribute\Artifact\CustomBinary;
use StaticPHP\Attribute\Artifact\CustomSource;
use StaticPHP\Attribute\Artifact\SourceExtract;
use StaticPHP\Config\ArtifactConfig;
use StaticPHP\Exception\ValidationException;
use StaticPHP\Registry\ArtifactLoader;
/**
* @internal
*/
class ArtifactLoaderTest extends TestCase
{
private string $tempDir;
protected function setUp(): void
{
parent::setUp();
$this->tempDir = sys_get_temp_dir() . '/artifact_loader_test_' . uniqid();
mkdir($this->tempDir, 0755, true);
// Reset ArtifactLoader and ArtifactConfig state
$reflection = new \ReflectionClass(ArtifactLoader::class);
$property = $reflection->getProperty('artifacts');
$property->setAccessible(true);
$property->setValue(null, null);
$configReflection = new \ReflectionClass(ArtifactConfig::class);
$configProperty = $configReflection->getProperty('artifact_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 ArtifactLoader and ArtifactConfig state
$reflection = new \ReflectionClass(ArtifactLoader::class);
$property = $reflection->getProperty('artifacts');
$property->setAccessible(true);
$property->setValue(null, null);
$configReflection = new \ReflectionClass(ArtifactConfig::class);
$configProperty = $configReflection->getProperty('artifact_configs');
$configProperty->setAccessible(true);
$configProperty->setValue(null, []);
}
public function testInitArtifactInstancesOnlyRunsOnce(): void
{
$this->createTestArtifactConfig('test-artifact');
ArtifactLoader::initArtifactInstances();
ArtifactLoader::initArtifactInstances();
// Should only initialize once
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertInstanceOf(Artifact::class, $artifact);
}
public function testGetArtifactInstanceReturnsNullForNonExistent(): void
{
ArtifactLoader::initArtifactInstances();
$artifact = ArtifactLoader::getArtifactInstance('non-existent-artifact');
$this->assertNull($artifact);
}
public function testGetArtifactInstanceReturnsArtifact(): void
{
$this->createTestArtifactConfig('test-artifact');
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertInstanceOf(Artifact::class, $artifact);
}
public function testLoadFromClassWithCustomSourceAttribute(): void
{
$this->createTestArtifactConfig('test-artifact');
ArtifactLoader::initArtifactInstances();
$class = new class {
#[CustomSource('test-artifact')]
public function customSource(): void {}
};
// Should not throw exception
ArtifactLoader::loadFromClass(get_class($class));
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertNotNull($artifact);
}
public function testLoadFromClassThrowsExceptionForInvalidCustomSourceArtifact(): void
{
ArtifactLoader::initArtifactInstances();
$class = new class {
#[CustomSource('non-existent-artifact')]
public function customSource(): void {}
};
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[CustomSource]");
ArtifactLoader::loadFromClass(get_class($class));
}
public function testLoadFromClassWithCustomBinaryAttribute(): void
{
$this->createTestArtifactConfig('test-artifact');
ArtifactLoader::initArtifactInstances();
$class = new class {
#[CustomBinary('test-artifact', ['linux-x86_64', 'macos-aarch64'])]
public function customBinary(): void {}
};
// Should not throw exception
ArtifactLoader::loadFromClass(get_class($class));
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertNotNull($artifact);
}
public function testLoadFromClassThrowsExceptionForInvalidCustomBinaryArtifact(): void
{
ArtifactLoader::initArtifactInstances();
$class = new class {
#[CustomBinary('non-existent-artifact', ['linux-x86_64'])]
public function customBinary(): void {}
};
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[CustomBinary]");
ArtifactLoader::loadFromClass(get_class($class));
}
public function testLoadFromClassWithSourceExtractAttribute(): void
{
$this->createTestArtifactConfig('test-artifact');
ArtifactLoader::initArtifactInstances();
$class = new class {
#[SourceExtract('test-artifact')]
public function sourceExtract(): void {}
};
// Should not throw exception
ArtifactLoader::loadFromClass(get_class($class));
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertNotNull($artifact);
}
public function testLoadFromClassThrowsExceptionForInvalidSourceExtractArtifact(): void
{
ArtifactLoader::initArtifactInstances();
$class = new class {
#[SourceExtract('non-existent-artifact')]
public function sourceExtract(): void {}
};
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[SourceExtract]");
ArtifactLoader::loadFromClass(get_class($class));
}
public function testLoadFromClassWithBinaryExtractAttribute(): void
{
$this->createTestArtifactConfig('test-artifact');
ArtifactLoader::initArtifactInstances();
$class = new class {
#[BinaryExtract('test-artifact')]
public function binaryExtract(): void {}
};
// Should not throw exception
ArtifactLoader::loadFromClass(get_class($class));
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertNotNull($artifact);
}
public function testLoadFromClassWithBinaryExtractAttributeAndPlatforms(): void
{
$this->createTestArtifactConfig('test-artifact');
ArtifactLoader::initArtifactInstances();
$class = new class {
#[BinaryExtract('test-artifact', platforms: ['linux-x86_64', 'darwin-aarch64'])]
public function binaryExtract(): void {}
};
// Should not throw exception
ArtifactLoader::loadFromClass(get_class($class));
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertNotNull($artifact);
}
public function testLoadFromClassThrowsExceptionForInvalidBinaryExtractArtifact(): void
{
ArtifactLoader::initArtifactInstances();
$class = new class {
#[BinaryExtract('non-existent-artifact')]
public function binaryExtract(): void {}
};
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[BinaryExtract]");
ArtifactLoader::loadFromClass(get_class($class));
}
public function testLoadFromClassWithAfterSourceExtractAttribute(): void
{
$this->createTestArtifactConfig('test-artifact');
ArtifactLoader::initArtifactInstances();
$class = new class {
#[AfterSourceExtract('test-artifact')]
public function afterSourceExtract(): void {}
};
// Should not throw exception
ArtifactLoader::loadFromClass(get_class($class));
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertNotNull($artifact);
}
public function testLoadFromClassThrowsExceptionForInvalidAfterSourceExtractArtifact(): void
{
ArtifactLoader::initArtifactInstances();
$class = new class {
#[AfterSourceExtract('non-existent-artifact')]
public function afterSourceExtract(): void {}
};
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[AfterSourceExtract]");
ArtifactLoader::loadFromClass(get_class($class));
}
public function testLoadFromClassWithAfterBinaryExtractAttribute(): void
{
$this->createTestArtifactConfig('test-artifact');
ArtifactLoader::initArtifactInstances();
$class = new class {
#[AfterBinaryExtract('test-artifact')]
public function afterBinaryExtract(): void {}
};
// Should not throw exception
ArtifactLoader::loadFromClass(get_class($class));
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertNotNull($artifact);
}
public function testLoadFromClassWithAfterBinaryExtractAttributeAndPlatforms(): void
{
$this->createTestArtifactConfig('test-artifact');
ArtifactLoader::initArtifactInstances();
$class = new class {
#[AfterBinaryExtract('test-artifact', platforms: ['linux-x86_64'])]
public function afterBinaryExtract(): void {}
};
// Should not throw exception
ArtifactLoader::loadFromClass(get_class($class));
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertNotNull($artifact);
}
public function testLoadFromClassThrowsExceptionForInvalidAfterBinaryExtractArtifact(): void
{
ArtifactLoader::initArtifactInstances();
$class = new class {
#[AfterBinaryExtract('non-existent-artifact')]
public function afterBinaryExtract(): void {}
};
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[AfterBinaryExtract]");
ArtifactLoader::loadFromClass(get_class($class));
}
public function testLoadFromClassWithMultipleAttributes(): void
{
$this->createTestArtifactConfig('test-artifact-1');
$this->createTestArtifactConfig('test-artifact-2');
ArtifactLoader::initArtifactInstances();
$class = new class {
#[CustomSource('test-artifact-1')]
public function customSource(): void {}
#[CustomBinary('test-artifact-2', ['linux-x86_64'])]
public function customBinary(): void {}
#[SourceExtract('test-artifact-1')]
public function sourceExtract(): void {}
#[AfterSourceExtract('test-artifact-2')]
public function afterSourceExtract(): void {}
};
// Should not throw exception
ArtifactLoader::loadFromClass(get_class($class));
$artifact1 = ArtifactLoader::getArtifactInstance('test-artifact-1');
$artifact2 = ArtifactLoader::getArtifactInstance('test-artifact-2');
$this->assertNotNull($artifact1);
$this->assertNotNull($artifact2);
}
public function testLoadFromClassIgnoresNonPublicMethods(): void
{
$this->createTestArtifactConfig('test-artifact');
ArtifactLoader::initArtifactInstances();
$class = new class {
#[CustomSource('test-artifact')]
public function publicCustomSource(): void {}
#[CustomSource('test-artifact')]
private function privateCustomSource(): void {}
#[CustomSource('test-artifact')]
protected function protectedCustomSource(): void {}
};
// Should only process public method
ArtifactLoader::loadFromClass(get_class($class));
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertNotNull($artifact);
}
public function testLoadFromPsr4DirLoadsAllClasses(): void
{
$this->createTestArtifactConfig('test-artifact');
// Create a PSR-4 directory structure
$psr4Dir = $this->tempDir . '/ArtifactClasses';
mkdir($psr4Dir, 0755, true);
// Create test class file
$classContent = '<?php
namespace Test\Artifact;
use StaticPHP\Attribute\Artifact\CustomSource;
class TestArtifact1 {
#[CustomSource("test-artifact")]
public function customSource() {}
}';
file_put_contents($psr4Dir . '/TestArtifact1.php', $classContent);
// Load with auto_require enabled
ArtifactLoader::loadFromPsr4Dir($psr4Dir, 'Test\Artifact', true);
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertNotNull($artifact);
}
public function testLoadFromClassWithNoAttributes(): void
{
ArtifactLoader::initArtifactInstances();
$class = new class {
public function regularMethod(): void {}
};
// Should not throw exception
ArtifactLoader::loadFromClass(get_class($class));
// Verify no side effects
$this->assertTrue(true);
}
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 createTestArtifactConfig(string $name): void
{
$reflection = new \ReflectionClass(ArtifactConfig::class);
$property = $reflection->getProperty('artifact_configs');
$property->setAccessible(true);
$configs = $property->getValue();
$configs[$name] = [
'type' => 'source',
'url' => 'https://example.com/test.tar.gz',
];
$property->setValue(null, $configs);
}
}

View File

@@ -0,0 +1,374 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Registry;
use PHPUnit\Framework\TestCase;
use StaticPHP\Attribute\Doctor\CheckItem;
use StaticPHP\Attribute\Doctor\FixItem;
use StaticPHP\Attribute\Doctor\OptionalCheck;
use StaticPHP\Registry\DoctorLoader;
/**
* @internal
*/
class DoctorLoaderTest extends TestCase
{
private string $tempDir;
protected function setUp(): void
{
parent::setUp();
$this->tempDir = sys_get_temp_dir() . '/doctor_loader_test_' . uniqid();
mkdir($this->tempDir, 0755, true);
// Reset DoctorLoader state
$reflection = new \ReflectionClass(DoctorLoader::class);
$property = $reflection->getProperty('doctor_items');
$property->setAccessible(true);
$property->setValue(null, []);
$property = $reflection->getProperty('fix_items');
$property->setAccessible(true);
$property->setValue(null, []);
}
protected function tearDown(): void
{
parent::tearDown();
// Clean up temp directory
if (is_dir($this->tempDir)) {
$this->removeDirectory($this->tempDir);
}
// Reset DoctorLoader state
$reflection = new \ReflectionClass(DoctorLoader::class);
$property = $reflection->getProperty('doctor_items');
$property->setAccessible(true);
$property->setValue(null, []);
$property = $reflection->getProperty('fix_items');
$property->setAccessible(true);
$property->setValue(null, []);
}
public function testGetDoctorItemsReturnsEmptyArrayInitially(): void
{
$this->assertEmpty(DoctorLoader::getDoctorItems());
}
public function testLoadFromClassWithCheckItemAttribute(): void
{
$class = new class {
#[CheckItem('test-check', level: 1)]
public function testCheck(): void {}
};
DoctorLoader::loadFromClass(get_class($class));
$items = DoctorLoader::getDoctorItems();
$this->assertCount(1, $items);
$this->assertInstanceOf(CheckItem::class, $items[0][0]);
$this->assertEquals('test-check', $items[0][0]->item_name);
$this->assertEquals(1, $items[0][0]->level);
}
public function testLoadFromClassWithMultipleCheckItems(): void
{
$class = new class {
#[CheckItem('check-1', level: 2)]
public function check1(): void {}
#[CheckItem('check-2', level: 1)]
public function check2(): void {}
};
DoctorLoader::loadFromClass(get_class($class));
$items = DoctorLoader::getDoctorItems();
$this->assertCount(2, $items);
}
public function testLoadFromClassSortsByLevelDescending(): void
{
$class = new class {
#[CheckItem('low-priority', level: 1)]
public function lowCheck(): void {}
#[CheckItem('high-priority', level: 5)]
public function highCheck(): void {}
#[CheckItem('medium-priority', level: 3)]
public function mediumCheck(): void {}
};
DoctorLoader::loadFromClass(get_class($class));
$items = DoctorLoader::getDoctorItems();
$this->assertCount(3, $items);
// Should be sorted by level descending: 5, 3, 1
$this->assertEquals(5, $items[0][0]->level);
$this->assertEquals(3, $items[1][0]->level);
$this->assertEquals(1, $items[2][0]->level);
}
public function testLoadFromClassWithoutSorting(): void
{
$class = new class {
#[CheckItem('check-1', level: 1)]
public function check1(): void {}
#[CheckItem('check-2', level: 5)]
public function check2(): void {}
};
DoctorLoader::loadFromClass(get_class($class), false);
$items = DoctorLoader::getDoctorItems();
// Without sorting, items should be in order they were added
$this->assertCount(2, $items);
}
public function testLoadFromClassWithFixItemAttribute(): void
{
$class = new class {
#[FixItem('test-fix')]
public function testFix(): void {}
};
DoctorLoader::loadFromClass(get_class($class));
$fixItem = DoctorLoader::getFixItem('test-fix');
$this->assertNotNull($fixItem);
$this->assertTrue(is_callable($fixItem));
}
public function testLoadFromClassWithMultipleFixItems(): void
{
$class = new class {
#[FixItem('fix-1')]
public function fix1(): void {}
#[FixItem('fix-2')]
public function fix2(): void {}
};
DoctorLoader::loadFromClass(get_class($class));
$this->assertNotNull(DoctorLoader::getFixItem('fix-1'));
$this->assertNotNull(DoctorLoader::getFixItem('fix-2'));
}
public function testGetFixItemReturnsNullForNonExistent(): void
{
$this->assertNull(DoctorLoader::getFixItem('non-existent-fix'));
}
public function testLoadFromClassWithOptionalCheckOnClass(): void
{
// Note: OptionalCheck expects an array, not a callable directly
// This test verifies the structure even though we can't easily test with anonymous classes
$class = new class {
#[CheckItem('test-check', level: 1)]
public function testCheck(): void {}
};
DoctorLoader::loadFromClass(get_class($class));
$items = DoctorLoader::getDoctorItems();
$this->assertCount(1, $items);
// Second element is the optional check callback (null if not set)
$this->assertIsArray($items[0]);
}
public function testLoadFromClassWithOptionalCheckOnMethod(): void
{
$class = new class {
#[CheckItem('test-check', level: 1)]
public function testCheck(): void {}
};
DoctorLoader::loadFromClass(get_class($class));
$items = DoctorLoader::getDoctorItems();
$this->assertCount(1, $items);
}
public function testLoadFromClassSetsCallbackCorrectly(): void
{
$class = new class {
#[CheckItem('test-check', level: 1)]
public function testCheck(): string
{
return 'test-result';
}
};
DoctorLoader::loadFromClass(get_class($class));
$items = DoctorLoader::getDoctorItems();
$this->assertCount(1, $items);
// Test that the callback is set correctly
$callback = $items[0][0]->callback;
$this->assertIsCallable($callback);
$this->assertEquals('test-result', call_user_func($callback));
}
public function testLoadFromClassWithBothCheckAndFixItems(): void
{
$class = new class {
#[CheckItem('test-check', level: 1)]
public function testCheck(): void {}
#[FixItem('test-fix')]
public function testFix(): void {}
};
DoctorLoader::loadFromClass(get_class($class));
$checkItems = DoctorLoader::getDoctorItems();
$this->assertCount(1, $checkItems);
$fixItem = DoctorLoader::getFixItem('test-fix');
$this->assertNotNull($fixItem);
}
public function testLoadFromClassMultipleTimesAccumulatesItems(): void
{
$class1 = new class {
#[CheckItem('check-1', level: 1)]
public function check1(): void {}
};
$class2 = new class {
#[CheckItem('check-2', level: 2)]
public function check2(): void {}
};
DoctorLoader::loadFromClass(get_class($class1));
DoctorLoader::loadFromClass(get_class($class2));
$items = DoctorLoader::getDoctorItems();
$this->assertCount(2, $items);
}
public function testLoadFromPsr4DirLoadsAllClasses(): void
{
// Create a PSR-4 directory structure
$psr4Dir = $this->tempDir . '/DoctorClasses';
mkdir($psr4Dir, 0755, true);
// Create test class file 1
$classContent1 = '<?php
namespace Test\Doctor;
use StaticPHP\Attribute\Doctor\CheckItem;
class TestDoctor1 {
#[CheckItem("psr4-check-1", level: 1)]
public function check1() {}
}';
file_put_contents($psr4Dir . '/TestDoctor1.php', $classContent1);
// Create test class file 2
$classContent2 = '<?php
namespace Test\Doctor;
use StaticPHP\Attribute\Doctor\CheckItem;
class TestDoctor2 {
#[CheckItem("psr4-check-2", level: 2)]
public function check2() {}
}';
file_put_contents($psr4Dir . '/TestDoctor2.php', $classContent2);
// Load with auto_require enabled
DoctorLoader::loadFromPsr4Dir($psr4Dir, 'Test\Doctor', true);
$items = DoctorLoader::getDoctorItems();
// Should have loaded both classes and sorted by level
$this->assertGreaterThanOrEqual(0, count($items));
}
public function testLoadFromPsr4DirSortsItemsByLevel(): void
{
// Create a PSR-4 directory structure
$psr4Dir = $this->tempDir . '/DoctorClasses';
mkdir($psr4Dir, 0755, true);
$classContent = '<?php
namespace Test\Doctor;
use StaticPHP\Attribute\Doctor\CheckItem;
class MultiLevelDoctor {
#[CheckItem("low", level: 1)]
public function lowPriority() {}
#[CheckItem("high", level: 10)]
public function highPriority() {}
}';
file_put_contents($psr4Dir . '/MultiLevelDoctor.php', $classContent);
DoctorLoader::loadFromPsr4Dir($psr4Dir, 'Test\Doctor', true);
$items = DoctorLoader::getDoctorItems();
// Items should be sorted by level descending
if (count($items) >= 2) {
$this->assertGreaterThanOrEqual($items[1][0]->level, $items[0][0]->level);
}
}
public function testLoadFromClassIgnoresNonPublicMethods(): void
{
$class = new class {
#[CheckItem('public-check', level: 1)]
public function publicCheck(): void {}
#[CheckItem('private-check', level: 1)]
private function privateCheck(): void {}
#[CheckItem('protected-check', level: 1)]
protected function protectedCheck(): void {}
};
DoctorLoader::loadFromClass(get_class($class));
$items = DoctorLoader::getDoctorItems();
// Should only load public methods
$this->assertCount(1, $items);
$this->assertEquals('public-check', $items[0][0]->item_name);
}
public function testLoadFromClassWithNoAttributes(): void
{
$class = new class {
public function regularMethod(): void {}
};
DoctorLoader::loadFromClass(get_class($class));
// Should not add any items
$items = DoctorLoader::getDoctorItems();
$this->assertEmpty($items);
}
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);
}
}

View File

@@ -0,0 +1,550 @@
<?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);
}
}

View File

@@ -0,0 +1,378 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Registry;
use PHPUnit\Framework\TestCase;
use StaticPHP\Exception\RegistryException;
use StaticPHP\Registry\Registry;
/**
* @internal
*/
class RegistryTest extends TestCase
{
private string $tempDir;
protected function setUp(): void
{
parent::setUp();
$this->tempDir = sys_get_temp_dir() . '/registry_test_' . uniqid();
mkdir($this->tempDir, 0755, true);
// Reset Registry state
Registry::reset();
}
protected function tearDown(): void
{
parent::tearDown();
// Clean up temp directory
if (is_dir($this->tempDir)) {
$this->removeDirectory($this->tempDir);
}
// Reset Registry state
Registry::reset();
}
public function testLoadRegistryWithValidJsonFile(): void
{
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'name' => 'test-registry',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile, json_encode($registryData));
Registry::loadRegistry($registryFile);
$this->assertContains('test-registry', Registry::getLoadedRegistries());
}
public function testLoadRegistryWithValidYamlFile(): void
{
$registryFile = $this->tempDir . '/test-registry.yaml';
$registryContent = "name: test-registry-yaml\npackage:\n config: []";
file_put_contents($registryFile, $registryContent);
Registry::loadRegistry($registryFile);
$this->assertContains('test-registry-yaml', Registry::getLoadedRegistries());
}
public function testLoadRegistryWithValidYmlFile(): void
{
$registryFile = $this->tempDir . '/test-registry.yml';
$registryContent = "name: test-registry-yml\npackage:\n config: []";
file_put_contents($registryFile, $registryContent);
Registry::loadRegistry($registryFile);
$this->assertContains('test-registry-yml', Registry::getLoadedRegistries());
}
public function testLoadRegistryThrowsExceptionForNonExistentFile(): void
{
$this->expectException(RegistryException::class);
$this->expectExceptionMessage('Failed to read registry file');
Registry::loadRegistry($this->tempDir . '/non-existent.json');
}
public function testLoadRegistryThrowsExceptionForUnsupportedFormat(): void
{
$registryFile = $this->tempDir . '/test-registry.txt';
file_put_contents($registryFile, 'invalid content');
$this->expectException(RegistryException::class);
$this->expectExceptionMessage('Unsupported registry file format');
Registry::loadRegistry($registryFile);
}
public function testLoadRegistryThrowsExceptionForInvalidJson(): void
{
$registryFile = $this->tempDir . '/test-registry.json';
file_put_contents($registryFile, 'invalid json content');
$this->expectException(RegistryException::class);
$this->expectExceptionMessage('Invalid registry format');
Registry::loadRegistry($registryFile);
}
public function testLoadRegistryThrowsExceptionForMissingName(): void
{
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'package' => [],
];
file_put_contents($registryFile, json_encode($registryData));
$this->expectException(RegistryException::class);
$this->expectExceptionMessage("Registry 'name' is missing or invalid");
Registry::loadRegistry($registryFile);
}
public function testLoadRegistryThrowsExceptionForEmptyName(): void
{
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'name' => '',
'package' => [],
];
file_put_contents($registryFile, json_encode($registryData));
$this->expectException(RegistryException::class);
$this->expectExceptionMessage("Registry 'name' is missing or invalid");
Registry::loadRegistry($registryFile);
}
public function testLoadRegistryThrowsExceptionForNonStringName(): void
{
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'name' => 123,
'package' => [],
];
file_put_contents($registryFile, json_encode($registryData));
$this->expectException(RegistryException::class);
$this->expectExceptionMessage("Registry 'name' is missing or invalid");
Registry::loadRegistry($registryFile);
}
public function testLoadRegistrySkipsDuplicateRegistry(): void
{
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'name' => 'duplicate-registry',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile, json_encode($registryData));
// Load first time
Registry::loadRegistry($registryFile);
$this->assertCount(1, Registry::getLoadedRegistries());
// Load second time - should skip
Registry::loadRegistry($registryFile);
$this->assertCount(1, Registry::getLoadedRegistries());
}
public function testLoadFromEnvOrOptionWithNullRegistries(): void
{
// Should not throw exception when null is passed and env is not set
Registry::loadFromEnvOrOption(null);
$this->assertEmpty(Registry::getLoadedRegistries());
}
public function testLoadFromEnvOrOptionWithEmptyString(): void
{
Registry::loadFromEnvOrOption('');
$this->assertEmpty(Registry::getLoadedRegistries());
}
public function testLoadFromEnvOrOptionWithSingleRegistry(): void
{
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'name' => 'env-test-registry',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile, json_encode($registryData));
Registry::loadFromEnvOrOption($registryFile);
$this->assertContains('env-test-registry', Registry::getLoadedRegistries());
}
public function testLoadFromEnvOrOptionWithMultipleRegistries(): void
{
$registryFile1 = $this->tempDir . '/test-registry-1.json';
$registryData1 = [
'name' => 'env-test-registry-1',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile1, json_encode($registryData1));
$registryFile2 = $this->tempDir . '/test-registry-2.json';
$registryData2 = [
'name' => 'env-test-registry-2',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile2, json_encode($registryData2));
Registry::loadFromEnvOrOption($registryFile1 . ':' . $registryFile2);
$this->assertContains('env-test-registry-1', Registry::getLoadedRegistries());
$this->assertContains('env-test-registry-2', Registry::getLoadedRegistries());
}
public function testLoadFromEnvOrOptionIgnoresNonExistentFiles(): void
{
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'name' => 'env-test-registry',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile, json_encode($registryData));
// Mix existing and non-existing files
Registry::loadFromEnvOrOption($registryFile . ':' . $this->tempDir . '/non-existent.json');
// Should only load the existing one
$this->assertCount(1, Registry::getLoadedRegistries());
$this->assertContains('env-test-registry', Registry::getLoadedRegistries());
}
public function testGetLoadedRegistriesReturnsEmptyArrayInitially(): void
{
$this->assertEmpty(Registry::getLoadedRegistries());
}
public function testGetLoadedRegistriesReturnsCorrectList(): void
{
$registryFile1 = $this->tempDir . '/test-registry-1.json';
$registryData1 = [
'name' => 'registry-1',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile1, json_encode($registryData1));
$registryFile2 = $this->tempDir . '/test-registry-2.json';
$registryData2 = [
'name' => 'registry-2',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile2, json_encode($registryData2));
Registry::loadRegistry($registryFile1);
Registry::loadRegistry($registryFile2);
$loaded = Registry::getLoadedRegistries();
$this->assertCount(2, $loaded);
$this->assertContains('registry-1', $loaded);
$this->assertContains('registry-2', $loaded);
}
public function testResetClearsLoadedRegistries(): void
{
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'name' => 'test-registry',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile, json_encode($registryData));
Registry::loadRegistry($registryFile);
$this->assertNotEmpty(Registry::getLoadedRegistries());
Registry::reset();
$this->assertEmpty(Registry::getLoadedRegistries());
}
public function testLoadRegistryWithAutoloadPath(): void
{
// Create a test autoload file
$autoloadFile = $this->tempDir . '/vendor/autoload.php';
mkdir(dirname($autoloadFile), 0755, true);
file_put_contents($autoloadFile, '<?php // Test autoload');
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'name' => 'autoload-test-registry',
'autoload' => 'vendor/autoload.php',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile, json_encode($registryData));
// Should not throw exception
Registry::loadRegistry($registryFile);
$this->assertContains('autoload-test-registry', Registry::getLoadedRegistries());
}
public function testLoadRegistryWithNonExistentAutoloadPath(): void
{
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'name' => 'autoload-missing-test-registry',
'autoload' => 'vendor/non-existent-autoload.php',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile, json_encode($registryData));
// Should throw exception when path doesn't exist
$this->expectException(RegistryException::class);
$this->expectExceptionMessage('Path does not exist');
Registry::loadRegistry($registryFile);
}
public function testLoadRegistryWithAbsoluteAutoloadPath(): void
{
// Create a test autoload file with absolute path
$autoloadFile = $this->tempDir . '/vendor/autoload.php';
mkdir(dirname($autoloadFile), 0755, true);
file_put_contents($autoloadFile, '<?php // Test autoload');
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'name' => 'absolute-autoload-test-registry',
'autoload' => $autoloadFile,
'package' => [
'config' => [],
],
];
file_put_contents($registryFile, json_encode($registryData));
Registry::loadRegistry($registryFile);
$this->assertContains('absolute-autoload-test-registry', Registry::getLoadedRegistries());
}
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);
}
}