mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-03-19 05:14:52 +08:00
375 lines
11 KiB
PHP
375 lines
11 KiB
PHP
|
|
<?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);
|
||
|
|
}
|
||
|
|
}
|