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,303 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Config;
use PHPUnit\Framework\TestCase;
use StaticPHP\Config\ArtifactConfig;
use StaticPHP\Exception\WrongUsageException;
/**
* @internal
*/
class ArtifactConfigTest extends TestCase
{
private string $tempDir;
/** @noinspection PhpExpressionResultUnusedInspection */
protected function setUp(): void
{
parent::setUp();
$this->tempDir = sys_get_temp_dir() . '/artifact_config_test_' . uniqid();
mkdir($this->tempDir, 0755, true);
// Reset static state
$reflection = new \ReflectionClass(ArtifactConfig::class);
$property = $reflection->getProperty('artifact_configs');
$property->setAccessible(true);
$property->setValue([]);
}
/** @noinspection PhpExpressionResultUnusedInspection */
protected function tearDown(): void
{
parent::tearDown();
// Clean up temp directory
if (is_dir($this->tempDir)) {
$this->removeDirectory($this->tempDir);
}
// Reset static state
$reflection = new \ReflectionClass(ArtifactConfig::class);
$property = $reflection->getProperty('artifact_configs');
$property->setAccessible(true);
$property->setValue([]);
}
public function testLoadFromDirThrowsExceptionWhenDirectoryDoesNotExist(): void
{
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage('Directory /nonexistent/path does not exist, cannot load artifact config.');
ArtifactConfig::loadFromDir('/nonexistent/path');
}
public function testLoadFromDirWithValidArtifactJson(): void
{
$artifactContent = json_encode([
'test-artifact' => [
'source' => 'https://example.com/file.tar.gz',
],
]);
file_put_contents($this->tempDir . '/artifact.json', $artifactContent);
ArtifactConfig::loadFromDir($this->tempDir);
$config = ArtifactConfig::get('test-artifact');
$this->assertIsArray($config);
$this->assertArrayHasKey('source', $config);
}
public function testLoadFromDirWithMultipleArtifactFiles(): void
{
$artifact1Content = json_encode([
'artifact-1' => [
'source' => 'https://example.com/file1.tar.gz',
],
]);
$artifact2Content = json_encode([
'artifact-2' => [
'source' => 'https://example.com/file2.tar.gz',
],
]);
file_put_contents($this->tempDir . '/artifact.ext.json', $artifact1Content);
file_put_contents($this->tempDir . '/artifact.lib.json', $artifact2Content);
file_put_contents($this->tempDir . '/artifact.json', json_encode(['artifact-3' => ['source' => 'custom']]));
ArtifactConfig::loadFromDir($this->tempDir);
$this->assertNotNull(ArtifactConfig::get('artifact-1'));
$this->assertNotNull(ArtifactConfig::get('artifact-2'));
$this->assertNotNull(ArtifactConfig::get('artifact-3'));
}
public function testLoadFromFileThrowsExceptionWhenFileCannotBeRead(): void
{
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage('Failed to read artifact config file:');
ArtifactConfig::loadFromFile('/nonexistent/file.json');
}
public function testLoadFromFileThrowsExceptionWhenJsonIsInvalid(): void
{
$file = $this->tempDir . '/invalid.json';
file_put_contents($file, 'not valid json{');
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage('Invalid JSON format in artifact config file:');
ArtifactConfig::loadFromFile($file);
}
public function testLoadFromFileWithValidJson(): void
{
$file = $this->tempDir . '/valid.json';
$content = json_encode([
'my-artifact' => [
'source' => [
'type' => 'url',
'url' => 'https://example.com/file.tar.gz',
],
],
]);
file_put_contents($file, $content);
ArtifactConfig::loadFromFile($file);
$config = ArtifactConfig::get('my-artifact');
$this->assertIsArray($config);
$this->assertArrayHasKey('source', $config);
}
public function testGetAllReturnsAllLoadedArtifacts(): void
{
$file = $this->tempDir . '/artifacts.json';
$content = json_encode([
'artifact-a' => ['source' => 'custom'],
'artifact-b' => ['source' => 'custom'],
'artifact-c' => ['source' => 'custom'],
]);
file_put_contents($file, $content);
ArtifactConfig::loadFromFile($file);
$all = ArtifactConfig::getAll();
$this->assertIsArray($all);
$this->assertCount(3, $all);
$this->assertArrayHasKey('artifact-a', $all);
$this->assertArrayHasKey('artifact-b', $all);
$this->assertArrayHasKey('artifact-c', $all);
}
public function testGetReturnsNullWhenArtifactNotFound(): void
{
$this->assertNull(ArtifactConfig::get('non-existent-artifact'));
}
public function testGetReturnsConfigWhenArtifactExists(): void
{
$file = $this->tempDir . '/artifacts.json';
$content = json_encode([
'test-artifact' => [
'source' => 'custom',
'binary' => 'custom',
],
]);
file_put_contents($file, $content);
ArtifactConfig::loadFromFile($file);
$config = ArtifactConfig::get('test-artifact');
$this->assertIsArray($config);
$this->assertEquals('custom', $config['source']);
$this->assertIsArray($config['binary']);
}
public function testLoadFromFileWithExpandedUrlInSource(): void
{
$file = $this->tempDir . '/artifacts.json';
$content = json_encode([
'test-artifact' => [
'source' => 'https://example.com/archive.tar.gz',
],
]);
file_put_contents($file, $content);
ArtifactConfig::loadFromFile($file);
$config = ArtifactConfig::get('test-artifact');
$this->assertIsArray($config);
$this->assertIsArray($config['source']);
$this->assertEquals('url', $config['source']['type']);
$this->assertEquals('https://example.com/archive.tar.gz', $config['source']['url']);
}
public function testLoadFromFileWithBinaryCustom(): void
{
$file = $this->tempDir . '/artifacts.json';
$content = json_encode([
'test-artifact' => [
'source' => 'custom',
'binary' => 'custom',
],
]);
file_put_contents($file, $content);
ArtifactConfig::loadFromFile($file);
$config = ArtifactConfig::get('test-artifact');
$this->assertIsArray($config['binary']);
$this->assertArrayHasKey('linux-x86_64', $config['binary']);
$this->assertArrayHasKey('macos-aarch64', $config['binary']);
$this->assertEquals('custom', $config['binary']['linux-x86_64']['type']);
}
public function testLoadFromFileWithBinaryHosted(): void
{
$file = $this->tempDir . '/artifacts.json';
$content = json_encode([
'test-artifact' => [
'source' => 'custom',
'binary' => 'hosted',
],
]);
file_put_contents($file, $content);
ArtifactConfig::loadFromFile($file);
$config = ArtifactConfig::get('test-artifact');
$this->assertIsArray($config['binary']);
$this->assertEquals('hosted', $config['binary']['linux-x86_64']['type']);
$this->assertEquals('hosted', $config['binary']['macos-aarch64']['type']);
}
public function testLoadFromFileWithBinaryPlatformSpecific(): void
{
$file = $this->tempDir . '/artifacts.json';
$content = json_encode([
'test-artifact' => [
'source' => 'custom',
'binary' => [
'linux-x86_64' => 'https://example.com/linux.tar.gz',
'macos-aarch64' => [
'type' => 'url',
'url' => 'https://example.com/macos.tar.gz',
],
],
],
]);
file_put_contents($file, $content);
ArtifactConfig::loadFromFile($file);
$config = ArtifactConfig::get('test-artifact');
$this->assertIsArray($config['binary']);
$this->assertEquals('url', $config['binary']['linux-x86_64']['type']);
$this->assertEquals('https://example.com/linux.tar.gz', $config['binary']['linux-x86_64']['url']);
$this->assertEquals('url', $config['binary']['macos-aarch64']['type']);
$this->assertEquals('https://example.com/macos.tar.gz', $config['binary']['macos-aarch64']['url']);
}
public function testLoadFromDirWithEmptyDirectory(): void
{
// Empty directory should not throw exception
ArtifactConfig::loadFromDir($this->tempDir);
$this->assertEquals([], ArtifactConfig::getAll());
}
public function testMultipleLoadsAppendConfigs(): void
{
$file1 = $this->tempDir . '/artifact1.json';
$file2 = $this->tempDir . '/artifact2.json';
file_put_contents($file1, json_encode(['art1' => ['source' => 'custom']]));
file_put_contents($file2, json_encode(['art2' => ['source' => 'custom']]));
ArtifactConfig::loadFromFile($file1);
ArtifactConfig::loadFromFile($file2);
$all = ArtifactConfig::getAll();
$this->assertCount(2, $all);
$this->assertArrayHasKey('art1', $all);
$this->assertArrayHasKey('art2', $all);
}
private function removeDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . '/' . $file;
is_dir($path) ? $this->removeDirectory($path) : unlink($path);
}
rmdir($dir);
}
}

View File

@@ -0,0 +1,196 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Config;
use PHPUnit\Framework\TestCase;
use StaticPHP\Config\ConfigType;
/**
* @internal
*/
class ConfigTypeTest extends TestCase
{
public function testConstantValues(): void
{
$this->assertEquals('list_array', ConfigType::LIST_ARRAY);
$this->assertEquals('assoc_array', ConfigType::ASSOC_ARRAY);
$this->assertEquals('string', ConfigType::STRING);
$this->assertEquals('bool', ConfigType::BOOL);
}
public function testPackageTypesConstant(): void
{
$expectedTypes = [
'library',
'php-extension',
'target',
'virtual-target',
];
$this->assertEquals($expectedTypes, ConfigType::PACKAGE_TYPES);
}
public function testValidateLicenseFieldWithValidFileType(): void
{
$license = [
'type' => 'file',
'path' => 'LICENSE',
];
$this->assertTrue(ConfigType::validateLicenseField($license));
}
public function testValidateLicenseFieldWithValidFileTypeArrayPath(): void
{
$license = [
'type' => 'file',
'path' => ['LICENSE', 'COPYING'],
];
$this->assertTrue(ConfigType::validateLicenseField($license));
}
public function testValidateLicenseFieldWithValidTextType(): void
{
$license = [
'type' => 'text',
'text' => 'MIT License',
];
$this->assertTrue(ConfigType::validateLicenseField($license));
}
public function testValidateLicenseFieldWithListOfLicenses(): void
{
$licenses = [
[
'type' => 'file',
'path' => 'LICENSE',
],
[
'type' => 'text',
'text' => 'MIT',
],
];
$this->assertTrue(ConfigType::validateLicenseField($licenses));
}
public function testValidateLicenseFieldWithEmptyList(): void
{
$licenses = [];
$this->assertTrue(ConfigType::validateLicenseField($licenses));
}
public function testValidateLicenseFieldReturnsFalseWhenNotAssocArray(): void
{
$this->assertFalse(ConfigType::validateLicenseField('string'));
$this->assertFalse(ConfigType::validateLicenseField(123));
$this->assertFalse(ConfigType::validateLicenseField(true));
}
public function testValidateLicenseFieldReturnsFalseWhenMissingType(): void
{
$license = [
'path' => 'LICENSE',
];
$this->assertFalse(ConfigType::validateLicenseField($license));
}
public function testValidateLicenseFieldReturnsFalseWithInvalidType(): void
{
$license = [
'type' => 'invalid',
'data' => 'something',
];
$this->assertFalse(ConfigType::validateLicenseField($license));
}
public function testValidateLicenseFieldReturnsFalseWhenFileTypeMissingPath(): void
{
$license = [
'type' => 'file',
];
$this->assertFalse(ConfigType::validateLicenseField($license));
}
public function testValidateLicenseFieldReturnsFalseWhenFileTypePathIsInvalid(): void
{
$license = [
'type' => 'file',
'path' => 123,
];
$this->assertFalse(ConfigType::validateLicenseField($license));
}
public function testValidateLicenseFieldReturnsFalseWhenTextTypeMissingText(): void
{
$license = [
'type' => 'text',
];
$this->assertFalse(ConfigType::validateLicenseField($license));
}
public function testValidateLicenseFieldReturnsFalseWhenTextTypeTextIsNotString(): void
{
$license = [
'type' => 'text',
'text' => ['array'],
];
$this->assertFalse(ConfigType::validateLicenseField($license));
}
public function testValidateLicenseFieldWithListContainingInvalidItem(): void
{
$licenses = [
[
'type' => 'file',
'path' => 'LICENSE',
],
[
'type' => 'text',
// missing 'text' field
],
];
$this->assertFalse(ConfigType::validateLicenseField($licenses));
}
public function testValidateLicenseFieldWithNestedListsOfLicenses(): void
{
$licenses = [
[
[
'type' => 'file',
'path' => 'LICENSE',
],
],
];
$this->assertTrue(ConfigType::validateLicenseField($licenses));
}
public function testValidateLicenseFieldWithNestedListContainingInvalidItem(): void
{
$licenses = [
[
[
'type' => 'file',
'path' => 'LICENSE',
],
'invalid-string-item',
],
];
$this->assertFalse(ConfigType::validateLicenseField($licenses));
}
}

View File

@@ -0,0 +1,627 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Config;
use PHPUnit\Framework\TestCase;
use StaticPHP\Config\ConfigValidator;
use StaticPHP\Exception\ValidationException;
/**
* @internal
*/
class ConfigValidatorTest extends TestCase
{
public function testValidateAndLintArtifactsThrowsExceptionWhenDataIsNotArray(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('test.json is broken');
$data = 'not an array';
ConfigValidator::validateAndLintArtifacts('test.json', $data);
}
public function testValidateAndLintArtifactsWithCustomSource(): void
{
$data = [
'test-artifact' => [
'source' => 'custom',
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
$this->assertEquals('custom', $data['test-artifact']['source']);
}
public function testValidateAndLintArtifactsExpandsUrlString(): void
{
$data = [
'test-artifact' => [
'source' => 'https://example.com/file.tar.gz',
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
$this->assertIsArray($data['test-artifact']['source']);
$this->assertEquals('url', $data['test-artifact']['source']['type']);
$this->assertEquals('https://example.com/file.tar.gz', $data['test-artifact']['source']['url']);
}
public function testValidateAndLintArtifactsExpandsHttpUrlString(): void
{
$data = [
'test-artifact' => [
'source' => 'http://example.com/file.tar.gz',
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
$this->assertIsArray($data['test-artifact']['source']);
$this->assertEquals('url', $data['test-artifact']['source']['type']);
$this->assertEquals('http://example.com/file.tar.gz', $data['test-artifact']['source']['url']);
}
public function testValidateAndLintArtifactsWithSourceObject(): void
{
$data = [
'test-artifact' => [
'source' => [
'type' => 'git',
'url' => 'https://github.com/example/repo.git',
'rev' => 'main',
],
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
$this->assertIsArray($data['test-artifact']['source']);
$this->assertEquals('git', $data['test-artifact']['source']['type']);
}
public function testValidateAndLintArtifactsWithBinaryCustom(): void
{
$data = [
'test-artifact' => [
'binary' => 'custom',
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
$this->assertIsArray($data['test-artifact']['binary']);
$this->assertArrayHasKey('linux-x86_64', $data['test-artifact']['binary']);
$this->assertEquals('custom', $data['test-artifact']['binary']['linux-x86_64']['type']);
}
public function testValidateAndLintArtifactsWithBinaryHosted(): void
{
$data = [
'test-artifact' => [
'binary' => 'hosted',
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
$this->assertIsArray($data['test-artifact']['binary']);
$this->assertArrayHasKey('macos-aarch64', $data['test-artifact']['binary']);
$this->assertEquals('hosted', $data['test-artifact']['binary']['macos-aarch64']['type']);
}
public function testValidateAndLintArtifactsWithBinaryPlatformObject(): void
{
$data = [
'test-artifact' => [
'binary' => [
'linux-x86_64' => [
'type' => 'url',
'url' => 'https://example.com/binary.tar.gz',
],
],
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
$this->assertEquals('url', $data['test-artifact']['binary']['linux-x86_64']['type']);
}
public function testValidateAndLintArtifactsExpandsBinaryPlatformUrlString(): void
{
$data = [
'test-artifact' => [
'binary' => [
'linux-x86_64' => 'https://example.com/binary.tar.gz',
],
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
$this->assertIsArray($data['test-artifact']['binary']['linux-x86_64']);
$this->assertEquals('url', $data['test-artifact']['binary']['linux-x86_64']['type']);
$this->assertEquals('https://example.com/binary.tar.gz', $data['test-artifact']['binary']['linux-x86_64']['url']);
}
public function testValidateAndLintArtifactsWithSourceMirror(): void
{
$data = [
'test-artifact' => [
'source-mirror' => 'https://mirror.example.com/file.tar.gz',
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
$this->assertIsArray($data['test-artifact']['source-mirror']);
$this->assertEquals('url', $data['test-artifact']['source-mirror']['type']);
}
public function testValidateAndLintPackagesThrowsExceptionWhenDataIsNotArray(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('pkg.json is broken');
$data = 'not an array';
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesThrowsExceptionWhenPackageIsNotAssocArray(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Package [test-pkg] in pkg.json is not a valid associative array');
$data = [
'test-pkg' => ['list', 'array'],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesThrowsExceptionWhenTypeMissing(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Package [test-pkg] in pkg.json has invalid or missing 'type' field");
$data = [
'test-pkg' => [
'depends' => [],
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesThrowsExceptionWhenTypeInvalid(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Package [test-pkg] in pkg.json has invalid or missing 'type' field");
$data = [
'test-pkg' => [
'type' => 'invalid-type',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesWithValidLibraryType(): void
{
$data = [
'test-lib' => [
'type' => 'library',
'artifact' => 'test-artifact',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
$this->assertEquals('library', $data['test-lib']['type']);
}
public function testValidateAndLintPackagesWithValidPhpExtensionType(): void
{
$data = [
'test-ext' => [
'type' => 'php-extension',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
$this->assertEquals('php-extension', $data['test-ext']['type']);
}
public function testValidateAndLintPackagesWithValidTargetType(): void
{
$data = [
'test-target' => [
'type' => 'target',
'artifact' => 'test-artifact',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
$this->assertEquals('target', $data['test-target']['type']);
}
public function testValidateAndLintPackagesWithValidVirtualTargetType(): void
{
$data = [
'test-virtual' => [
'type' => 'virtual-target',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
$this->assertEquals('virtual-target', $data['test-virtual']['type']);
}
public function testValidateAndLintPackagesThrowsExceptionWhenLibraryMissingArtifact(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Package [test-lib] in pkg.json of type 'library' must have an 'artifact' field");
$data = [
'test-lib' => [
'type' => 'library',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesThrowsExceptionWhenTargetMissingArtifact(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Package [test-target] in pkg.json of type 'target' must have an 'artifact' field");
$data = [
'test-target' => [
'type' => 'target',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesWithPhpExtensionFields(): void
{
$data = [
'test-ext' => [
'type' => 'php-extension',
'php-extension' => [
'zend-extension' => false,
'build-shared' => true,
],
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
$this->assertIsArray($data['test-ext']['php-extension']);
}
public function testValidateAndLintPackagesThrowsExceptionWhenPhpExtensionIsNotObject(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Package test-ext [php-extension] must be an object');
$data = [
'test-ext' => [
'type' => 'php-extension',
'php-extension' => 'string',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesWithDependsField(): void
{
$data = [
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
'depends' => ['dep1', 'dep2'],
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
$this->assertIsArray($data['test-pkg']['depends']);
}
public function testValidateAndLintPackagesThrowsExceptionWhenDependsIsNotList(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Package test-pkg [depends] must be a list');
$data = [
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
'depends' => 'not-a-list',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesWithSuffixFields(): void
{
$data = [
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
'depends@linux' => ['linux-dep'],
'depends@windows' => ['windows-dep'],
'headers@unix' => ['header.h'],
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
$this->assertIsArray($data['test-pkg']['depends@linux']);
}
public function testValidateAndLintPackagesThrowsExceptionForInvalidSuffixFieldType(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Package test-pkg [headers@linux] must be a list');
$data = [
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
'headers@linux' => 'not-a-list',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesThrowsExceptionForUnknownField(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('package [test-pkg] has invalid field [unknown-field]');
$data = [
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
'unknown-field' => 'value',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesThrowsExceptionForUnknownPhpExtensionField(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('php-extension [test-ext] has invalid field [unknown]');
$data = [
'test-ext' => [
'type' => 'php-extension',
'php-extension' => [
'unknown' => 'value',
],
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidatePlatformStringWithValidPlatforms(): void
{
ConfigValidator::validatePlatformString('linux-x86_64');
ConfigValidator::validatePlatformString('linux-aarch64');
ConfigValidator::validatePlatformString('windows-x86_64');
ConfigValidator::validatePlatformString('windows-aarch64');
ConfigValidator::validatePlatformString('macos-x86_64');
ConfigValidator::validatePlatformString('macos-aarch64');
$this->assertTrue(true); // If no exception thrown, test passes
}
public function testValidatePlatformStringThrowsExceptionForInvalidFormat(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Invalid platform format 'invalid', expected format 'os-arch'");
ConfigValidator::validatePlatformString('invalid');
}
public function testValidatePlatformStringThrowsExceptionForTooManyParts(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Invalid platform format 'linux-x86_64-extra', expected format 'os-arch'");
ConfigValidator::validatePlatformString('linux-x86_64-extra');
}
public function testValidatePlatformStringThrowsExceptionForInvalidOS(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Invalid platform OS 'bsd' in platform 'bsd-x86_64'");
ConfigValidator::validatePlatformString('bsd-x86_64');
}
public function testValidatePlatformStringThrowsExceptionForInvalidArch(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Invalid platform architecture 'arm' in platform 'linux-arm'");
ConfigValidator::validatePlatformString('linux-arm');
}
public function testArtifactTypeFieldsConstant(): void
{
$this->assertArrayHasKey('filelist', ConfigValidator::ARTIFACT_TYPE_FIELDS);
$this->assertArrayHasKey('git', ConfigValidator::ARTIFACT_TYPE_FIELDS);
$this->assertArrayHasKey('ghtagtar', ConfigValidator::ARTIFACT_TYPE_FIELDS);
$this->assertArrayHasKey('url', ConfigValidator::ARTIFACT_TYPE_FIELDS);
$this->assertArrayHasKey('custom', ConfigValidator::ARTIFACT_TYPE_FIELDS);
}
public function testValidateAndLintArtifactsThrowsExceptionForInvalidArtifactType(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Artifact source object has unknown type 'invalid-type'");
$data = [
'test-artifact' => [
'source' => [
'type' => 'invalid-type',
],
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
}
public function testValidateAndLintArtifactsThrowsExceptionForMissingRequiredField(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Artifact source object of type 'git' must have required field 'url'");
$data = [
'test-artifact' => [
'source' => [
'type' => 'git',
'rev' => 'main',
// missing 'url'
],
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
}
public function testValidateAndLintArtifactsThrowsExceptionForMissingTypeInSource(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Artifact source object must have a valid 'type' field");
$data = [
'test-artifact' => [
'source' => [
'url' => 'https://example.com',
],
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
}
public function testValidateAndLintArtifactsWithAllArtifactTypes(): void
{
$data = [
'filelist-artifact' => [
'source' => [
'type' => 'filelist',
'url' => 'https://example.com/list',
'regex' => '/pattern/',
],
],
'git-artifact' => [
'source' => [
'type' => 'git',
'url' => 'https://github.com/example/repo.git',
'rev' => 'main',
],
],
'ghtagtar-artifact' => [
'source' => [
'type' => 'ghtagtar',
'repo' => 'example/repo',
],
],
'url-artifact' => [
'source' => [
'type' => 'url',
'url' => 'https://example.com/file.tar.gz',
],
],
'custom-artifact' => [
'source' => [
'type' => 'custom',
],
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
$this->assertIsArray($data);
}
public function testValidateAndLintPackagesWithAllFieldTypes(): void
{
$data = [
'test-pkg' => [
'type' => 'library',
'artifact' => 'test-artifact',
'depends' => ['dep1'],
'suggests' => ['sug1'],
'license' => [
'type' => 'file',
'path' => 'LICENSE',
],
'lang' => 'c',
'frameworks' => ['framework1'],
'headers' => ['header.h'],
'static-libs' => ['lib.a'],
'pkg-configs' => ['pkg.pc'],
'static-bins' => ['bin'],
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
$this->assertEquals('library', $data['test-pkg']['type']);
}
public function testValidateAndLintPackagesThrowsExceptionForWrongTypeString(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Package test-pkg [artifact] must be string');
$data = [
'test-pkg' => [
'type' => 'library',
'artifact' => ['not', 'a', 'string'],
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesThrowsExceptionForWrongTypeBool(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Package test-ext [zend-extension] must be boolean');
$data = [
'test-ext' => [
'type' => 'php-extension',
'php-extension' => [
'zend-extension' => 'not-a-bool',
],
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesThrowsExceptionForWrongTypeAssocArray(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Package test-pkg [support] must be an object');
$data = [
'test-pkg' => [
'type' => 'php-extension',
'php-extension' => [
'support' => 'not-an-object',
],
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
}

View File

@@ -0,0 +1,434 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Config;
use PHPUnit\Framework\TestCase;
use StaticPHP\Config\PackageConfig;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Runtime\SystemTarget;
/**
* @internal
*/
class PackageConfigTest extends TestCase
{
private string $tempDir;
protected function setUp(): void
{
parent::setUp();
$this->tempDir = sys_get_temp_dir() . '/package_config_test_' . uniqid();
mkdir($this->tempDir, 0755, true);
// Reset static state
$reflection = new \ReflectionClass(PackageConfig::class);
$property = $reflection->getProperty('package_configs');
$property->setAccessible(true);
$property->setValue([]);
}
protected function tearDown(): void
{
parent::tearDown();
// Clean up temp directory
if (is_dir($this->tempDir)) {
$this->removeDirectory($this->tempDir);
}
// Reset static state
$reflection = new \ReflectionClass(PackageConfig::class);
$property = $reflection->getProperty('package_configs');
$property->setAccessible(true);
$property->setValue([]);
}
public function testLoadFromDirThrowsExceptionWhenDirectoryDoesNotExist(): void
{
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage('Directory /nonexistent/path does not exist, cannot load pkg.json config.');
PackageConfig::loadFromDir('/nonexistent/path');
}
public function testLoadFromDirWithValidPkgJson(): void
{
$packageContent = json_encode([
'test-pkg' => [
'type' => 'library',
'artifact' => 'test-artifact',
],
]);
file_put_contents($this->tempDir . '/pkg.json', $packageContent);
PackageConfig::loadFromDir($this->tempDir);
$this->assertTrue(PackageConfig::isPackageExists('test-pkg'));
}
public function testLoadFromDirWithMultiplePackageFiles(): void
{
$pkg1Content = json_encode([
'pkg-1' => [
'type' => 'library',
'artifact' => 'artifact-1',
],
]);
$pkg2Content = json_encode([
'pkg-2' => [
'type' => 'php-extension',
],
]);
file_put_contents($this->tempDir . '/pkg.ext.json', $pkg1Content);
file_put_contents($this->tempDir . '/pkg.lib.json', $pkg2Content);
file_put_contents($this->tempDir . '/pkg.json', json_encode(['pkg-3' => ['type' => 'virtual-target']]));
PackageConfig::loadFromDir($this->tempDir);
$this->assertTrue(PackageConfig::isPackageExists('pkg-1'));
$this->assertTrue(PackageConfig::isPackageExists('pkg-2'));
$this->assertTrue(PackageConfig::isPackageExists('pkg-3'));
}
public function testLoadFromFileThrowsExceptionWhenFileCannotBeRead(): void
{
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage('Failed to read package config file:');
PackageConfig::loadFromFile('/nonexistent/file.json');
}
public function testLoadFromFileThrowsExceptionWhenJsonIsInvalid(): void
{
$file = $this->tempDir . '/invalid.json';
file_put_contents($file, 'not valid json{');
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage('Invalid JSON format in package config file:');
PackageConfig::loadFromFile($file);
}
public function testLoadFromFileWithValidJson(): void
{
$file = $this->tempDir . '/valid.json';
$content = json_encode([
'my-pkg' => [
'type' => 'library',
'artifact' => 'my-artifact',
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
$this->assertTrue(PackageConfig::isPackageExists('my-pkg'));
}
public function testIsPackageExistsReturnsFalseWhenPackageNotLoaded(): void
{
$this->assertFalse(PackageConfig::isPackageExists('non-existent'));
}
public function testIsPackageExistsReturnsTrueWhenPackageLoaded(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
$this->assertTrue(PackageConfig::isPackageExists('test-pkg'));
}
public function testGetAllReturnsAllLoadedPackages(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'pkg-a' => ['type' => 'virtual-target'],
'pkg-b' => ['type' => 'virtual-target'],
'pkg-c' => ['type' => 'virtual-target'],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
$all = PackageConfig::getAll();
$this->assertIsArray($all);
$this->assertCount(3, $all);
$this->assertArrayHasKey('pkg-a', $all);
$this->assertArrayHasKey('pkg-b', $all);
$this->assertArrayHasKey('pkg-c', $all);
}
public function testGetReturnsDefaultWhenPackageNotExists(): void
{
$result = PackageConfig::get('non-existent', 'field', 'default-value');
$this->assertEquals('default-value', $result);
}
public function testGetReturnsWholePackageWhenFieldNameIsNull(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
'depends' => ['dep1'],
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
$result = PackageConfig::get('test-pkg');
$this->assertIsArray($result);
$this->assertEquals('library', $result['type']);
$this->assertEquals('test', $result['artifact']);
}
public function testGetReturnsFieldValueWhenFieldExists(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'test-pkg' => [
'type' => 'library',
'artifact' => 'test-artifact',
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
$result = PackageConfig::get('test-pkg', 'artifact');
$this->assertEquals('test-artifact', $result);
}
public function testGetReturnsDefaultWhenFieldNotExists(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
$result = PackageConfig::get('test-pkg', 'non-existent-field', 'default');
$this->assertEquals('default', $result);
}
public function testGetWithSuffixFieldsOnLinux(): void
{
// Mock SystemTarget to return Linux
$mockTarget = $this->getMockBuilder(SystemTarget::class)
->disableOriginalConstructor()
->getMock();
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
'depends' => ['base-dep'],
'depends@linux' => ['linux-dep'],
'depends@unix' => ['unix-dep'],
'depends@windows' => ['windows-dep'],
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
// The get method will check SystemTarget::getTargetOS()
// On real Linux systems, it should return 'depends@linux' first
$result = PackageConfig::get('test-pkg', 'depends', []);
// Result should be one of the suffixed versions or base version
$this->assertIsArray($result);
}
public function testGetWithSuffixFieldsReturnsBasicFieldWhenNoSuffixMatch(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
'depends' => ['base-dep'],
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
$result = PackageConfig::get('test-pkg', 'depends');
$this->assertEquals(['base-dep'], $result);
}
public function testGetWithNonSuffixedFieldIgnoresSuffixes(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'test-pkg' => [
'type' => 'library',
'artifact' => 'test-artifact',
'artifact@linux' => 'linux-artifact', // This should be ignored
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
// 'artifact' is not in SUFFIX_ALLOWED_FIELDS, so it won't check suffixes
$result = PackageConfig::get('test-pkg', 'artifact');
$this->assertEquals('test-artifact', $result);
}
public function testGetAllSuffixAllowedFields(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
'depends@linux' => ['dep1'],
'suggests@macos' => ['sug1'],
'headers@unix' => ['header.h'],
'static-libs@windows' => ['lib.a'],
'static-bins@linux' => ['bin'],
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
// These are all suffix-allowed fields
$pkg = PackageConfig::get('test-pkg');
$this->assertArrayHasKey('depends@linux', $pkg);
$this->assertArrayHasKey('suggests@macos', $pkg);
$this->assertArrayHasKey('headers@unix', $pkg);
$this->assertArrayHasKey('static-libs@windows', $pkg);
$this->assertArrayHasKey('static-bins@linux', $pkg);
}
public function testLoadFromDirWithEmptyDirectory(): void
{
// Empty directory should not throw exception
PackageConfig::loadFromDir($this->tempDir);
$this->assertEquals([], PackageConfig::getAll());
}
public function testMultipleLoadsAppendConfigs(): void
{
$file1 = $this->tempDir . '/pkg1.json';
$file2 = $this->tempDir . '/pkg2.json';
file_put_contents($file1, json_encode(['pkg1' => ['type' => 'virtual-target']]));
file_put_contents($file2, json_encode(['pkg2' => ['type' => 'virtual-target']]));
PackageConfig::loadFromFile($file1);
PackageConfig::loadFromFile($file2);
$all = PackageConfig::getAll();
$this->assertCount(2, $all);
$this->assertArrayHasKey('pkg1', $all);
$this->assertArrayHasKey('pkg2', $all);
}
public function testGetWithComplexPhpExtensionPackage(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'test-ext' => [
'type' => 'php-extension',
'depends' => ['dep1'],
'php-extension' => [
'zend-extension' => false,
'build-shared' => true,
'build-static' => false,
],
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
$phpExt = PackageConfig::get('test-ext', 'php-extension');
$this->assertIsArray($phpExt);
$this->assertFalse($phpExt['zend-extension']);
$this->assertTrue($phpExt['build-shared']);
}
public function testGetReturnsNullAsDefaultWhenNotSpecified(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'test-pkg' => [
'type' => 'virtual-target',
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
$result = PackageConfig::get('test-pkg', 'non-existent');
$this->assertNull($result);
}
public function testLoadFromFileWithAllPackageTypes(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'library-pkg' => [
'type' => 'library',
'artifact' => 'lib-artifact',
],
'extension-pkg' => [
'type' => 'php-extension',
],
'target-pkg' => [
'type' => 'target',
'artifact' => 'target-artifact',
],
'virtual-pkg' => [
'type' => 'virtual-target',
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
$this->assertTrue(PackageConfig::isPackageExists('library-pkg'));
$this->assertTrue(PackageConfig::isPackageExists('extension-pkg'));
$this->assertTrue(PackageConfig::isPackageExists('target-pkg'));
$this->assertTrue(PackageConfig::isPackageExists('virtual-pkg'));
}
private function removeDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . '/' . $file;
is_dir($path) ? $this->removeDirectory($path) : unlink($path);
}
rmdir($dir);
}
}

View File

@@ -0,0 +1,433 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\DI;
use DI\Container;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\DI\CallbackInvoker;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
class ApplicationContextTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
// Reset ApplicationContext state before each test
ApplicationContext::reset();
}
protected function tearDown(): void
{
parent::tearDown();
// Reset ApplicationContext state after each test
ApplicationContext::reset();
}
public function testInitializeCreatesContainer(): void
{
$container = ApplicationContext::initialize();
$this->assertInstanceOf(Container::class, $container);
$this->assertSame($container, ApplicationContext::getContainer());
}
public function testInitializeWithDebugMode(): void
{
ApplicationContext::initialize(['debug' => true]);
$this->assertTrue(ApplicationContext::isDebug());
}
public function testInitializeWithoutDebugMode(): void
{
ApplicationContext::initialize(['debug' => false]);
$this->assertFalse(ApplicationContext::isDebug());
}
public function testInitializeWithCustomDefinitions(): void
{
$customValue = 'test_value';
ApplicationContext::initialize([
'definitions' => [
'test.service' => $customValue,
],
]);
$this->assertEquals($customValue, ApplicationContext::get('test.service'));
}
public function testInitializeThrowsExceptionWhenAlreadyInitialized(): void
{
ApplicationContext::initialize();
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('ApplicationContext already initialized');
ApplicationContext::initialize();
}
public function testGetContainerAutoInitializes(): void
{
// Don't call initialize
$container = ApplicationContext::getContainer();
$this->assertInstanceOf(Container::class, $container);
}
public function testGetReturnsServiceFromContainer(): void
{
ApplicationContext::initialize([
'definitions' => [
'test.key' => 'test_value',
],
]);
$this->assertEquals('test_value', ApplicationContext::get('test.key'));
}
public function testGetWithClassType(): void
{
ApplicationContext::initialize();
$container = ApplicationContext::get(Container::class);
$this->assertInstanceOf(Container::class, $container);
}
public function testGetContainerInterface(): void
{
ApplicationContext::initialize();
$container = ApplicationContext::get(ContainerInterface::class);
$this->assertInstanceOf(ContainerInterface::class, $container);
}
public function testHasReturnsTrueForExistingService(): void
{
ApplicationContext::initialize([
'definitions' => [
'test.service' => 'value',
],
]);
$this->assertTrue(ApplicationContext::has('test.service'));
}
public function testHasReturnsFalseForNonExistingService(): void
{
ApplicationContext::initialize();
$this->assertFalse(ApplicationContext::has('non.existing.service'));
}
public function testSetAddsServiceToContainer(): void
{
ApplicationContext::initialize();
ApplicationContext::set('dynamic.service', 'dynamic_value');
$this->assertTrue(ApplicationContext::has('dynamic.service'));
$this->assertEquals('dynamic_value', ApplicationContext::get('dynamic.service'));
}
public function testSetOverridesExistingService(): void
{
ApplicationContext::initialize([
'definitions' => [
'test.service' => 'original',
],
]);
ApplicationContext::set('test.service', 'updated');
$this->assertEquals('updated', ApplicationContext::get('test.service'));
}
public function testBindCommandContextSetsInputAndOutput(): void
{
ApplicationContext::initialize();
$input = $this->createMock(InputInterface::class);
$output = $this->createMock(OutputInterface::class);
$output->method('isDebug')->willReturn(false);
ApplicationContext::bindCommandContext($input, $output);
$this->assertSame($input, ApplicationContext::get(InputInterface::class));
$this->assertSame($output, ApplicationContext::get(OutputInterface::class));
}
public function testBindCommandContextSetsDebugMode(): void
{
ApplicationContext::initialize();
$input = $this->createMock(InputInterface::class);
$output = $this->createMock(OutputInterface::class);
$output->method('isDebug')->willReturn(true);
ApplicationContext::bindCommandContext($input, $output);
$this->assertTrue(ApplicationContext::isDebug());
}
public function testBindCommandContextWithNonDebugOutput(): void
{
ApplicationContext::initialize();
$input = $this->createMock(InputInterface::class);
$output = $this->createMock(OutputInterface::class);
$output->method('isDebug')->willReturn(false);
ApplicationContext::bindCommandContext($input, $output);
$this->assertFalse(ApplicationContext::isDebug());
}
public function testGetInvokerReturnsCallbackInvoker(): void
{
ApplicationContext::initialize();
$invoker = ApplicationContext::getInvoker();
$this->assertInstanceOf(CallbackInvoker::class, $invoker);
}
public function testGetInvokerReturnsSameInstance(): void
{
ApplicationContext::initialize();
$invoker1 = ApplicationContext::getInvoker();
$invoker2 = ApplicationContext::getInvoker();
$this->assertSame($invoker1, $invoker2);
}
public function testGetInvokerAutoInitializesContainer(): void
{
// Don't call initialize
$invoker = ApplicationContext::getInvoker();
$this->assertInstanceOf(CallbackInvoker::class, $invoker);
}
public function testInvokeCallsCallback(): void
{
ApplicationContext::initialize();
$called = false;
$callback = function () use (&$called) {
$called = true;
return 'result';
};
$result = ApplicationContext::invoke($callback);
$this->assertTrue($called);
$this->assertEquals('result', $result);
}
public function testInvokeWithContext(): void
{
ApplicationContext::initialize();
$callback = function (string $param) {
return $param;
};
$result = ApplicationContext::invoke($callback, ['param' => 'test_value']);
$this->assertEquals('test_value', $result);
}
public function testInvokeWithDependencyInjection(): void
{
ApplicationContext::initialize();
$callback = function (Container $container) {
return $container;
};
$result = ApplicationContext::invoke($callback);
$this->assertInstanceOf(Container::class, $result);
}
public function testInvokeWithArrayCallback(): void
{
ApplicationContext::initialize();
$object = new class {
public function method(): string
{
return 'called';
}
};
$result = ApplicationContext::invoke([$object, 'method']);
$this->assertEquals('called', $result);
}
public function testIsDebugDefaultsFalse(): void
{
ApplicationContext::initialize();
$this->assertFalse(ApplicationContext::isDebug());
}
public function testSetDebugChangesDebugMode(): void
{
ApplicationContext::initialize();
ApplicationContext::setDebug(true);
$this->assertTrue(ApplicationContext::isDebug());
ApplicationContext::setDebug(false);
$this->assertFalse(ApplicationContext::isDebug());
}
public function testResetClearsContainer(): void
{
ApplicationContext::initialize();
ApplicationContext::set('test.service', 'value');
ApplicationContext::reset();
// After reset, container should be reinitialized
$this->assertFalse(ApplicationContext::has('test.service'));
}
public function testResetClearsInvoker(): void
{
ApplicationContext::initialize();
$invoker1 = ApplicationContext::getInvoker();
ApplicationContext::reset();
$invoker2 = ApplicationContext::getInvoker();
$this->assertNotSame($invoker1, $invoker2);
}
public function testResetClearsDebugMode(): void
{
ApplicationContext::initialize(['debug' => true]);
$this->assertTrue(ApplicationContext::isDebug());
ApplicationContext::reset();
// After reset and reinit, debug should be false by default
ApplicationContext::initialize();
$this->assertFalse(ApplicationContext::isDebug());
}
public function testResetAllowsReinitialize(): void
{
ApplicationContext::initialize();
ApplicationContext::reset();
// Should not throw exception
$container = ApplicationContext::initialize(['debug' => true]);
$this->assertInstanceOf(Container::class, $container);
$this->assertTrue(ApplicationContext::isDebug());
}
public function testCallbackInvokerIsAvailableInContainer(): void
{
ApplicationContext::initialize();
$invoker = ApplicationContext::get(CallbackInvoker::class);
$this->assertInstanceOf(CallbackInvoker::class, $invoker);
}
public function testMultipleGetCallsReturnSameContainer(): void
{
$container1 = ApplicationContext::getContainer();
$container2 = ApplicationContext::getContainer();
$this->assertSame($container1, $container2);
}
public function testInitializeWithEmptyOptions(): void
{
$container = ApplicationContext::initialize([]);
$this->assertInstanceOf(Container::class, $container);
$this->assertFalse(ApplicationContext::isDebug());
}
public function testInitializeWithNullDefinitions(): void
{
$container = ApplicationContext::initialize(['definitions' => null]);
$this->assertInstanceOf(Container::class, $container);
}
public function testInitializeWithEmptyDefinitions(): void
{
$container = ApplicationContext::initialize(['definitions' => []]);
$this->assertInstanceOf(Container::class, $container);
}
public function testSetBeforeInitializeAutoInitializes(): void
{
// Don't call initialize
ApplicationContext::set('test.service', 'value');
$this->assertEquals('value', ApplicationContext::get('test.service'));
}
public function testHasBeforeInitializeAutoInitializes(): void
{
// Don't call initialize, should auto-initialize
$result = ApplicationContext::has(Container::class);
$this->assertTrue($result);
}
public function testGetBeforeInitializeAutoInitializes(): void
{
// Don't call initialize
$container = ApplicationContext::get(Container::class);
$this->assertInstanceOf(Container::class, $container);
}
public function testInvokerSingletonConsistency(): void
{
// Test fix for issue #3 and #4 - Invoker instance consistency
ApplicationContext::initialize();
$invoker1 = ApplicationContext::getInvoker();
$invoker2 = ApplicationContext::get(CallbackInvoker::class);
// Both should return the same instance
$this->assertSame($invoker1, $invoker2);
}
public function testInvokerSingletonConsistencyAfterReset(): void
{
ApplicationContext::initialize();
$invoker1 = ApplicationContext::getInvoker();
ApplicationContext::reset();
ApplicationContext::initialize();
$invoker2 = ApplicationContext::getInvoker();
$invoker3 = ApplicationContext::get(CallbackInvoker::class);
// After reset, should be new instance
$this->assertNotSame($invoker1, $invoker2);
// But getInvoker() and container should still be consistent
$this->assertSame($invoker2, $invoker3);
}
}

View File

@@ -0,0 +1,629 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\DI;
use DI\Container;
use PHPUnit\Framework\TestCase;
use StaticPHP\DI\CallbackInvoker;
/**
* Helper class that requires constructor parameters for testing
*/
class UnresolvableTestClass
{
public function __construct(
private string $requiredParam
) {}
}
/**
* @internal
*/
class CallbackInvokerTest extends TestCase
{
private Container $container;
private CallbackInvoker $invoker;
protected function setUp(): void
{
parent::setUp();
$this->container = new Container();
$this->invoker = new CallbackInvoker($this->container);
}
public function testInvokeSimpleCallbackWithoutParameters(): void
{
$callback = function () {
return 'result';
};
$result = $this->invoker->invoke($callback);
$this->assertEquals('result', $result);
}
public function testInvokeCallbackWithContextByTypeName(): void
{
$callback = function (string $param) {
return $param;
};
$result = $this->invoker->invoke($callback, ['string' => 'test_value']);
$this->assertEquals('test_value', $result);
}
public function testInvokeCallbackWithContextByParameterName(): void
{
$callback = function (string $myParam) {
return $myParam;
};
$result = $this->invoker->invoke($callback, ['myParam' => 'test_value']);
$this->assertEquals('test_value', $result);
}
public function testInvokeCallbackWithContextByTypeNameTakesPrecedence(): void
{
$callback = function (string $myParam) {
return $myParam;
};
// Type name should take precedence over parameter name
$result = $this->invoker->invoke($callback, [
'string' => 'by_type',
'myParam' => 'by_name',
]);
$this->assertEquals('by_type', $result);
}
public function testInvokeCallbackWithContainerResolution(): void
{
$this->container->set('test.service', 'service_value');
$callback = function (string $testService) {
return $testService;
};
// Should not resolve from container as 'test.service' is not a type
// Will try default value or null
$this->expectException(\RuntimeException::class);
$this->invoker->invoke($callback);
}
public function testInvokeCallbackWithClassTypeFromContainer(): void
{
$testObject = new \stdClass();
$testObject->value = 'test';
$this->container->set(\stdClass::class, $testObject);
$callback = function (\stdClass $obj) {
return $obj->value;
};
$result = $this->invoker->invoke($callback);
$this->assertEquals('test', $result);
}
public function testInvokeCallbackWithDefaultValue(): void
{
$callback = function (string $param = 'default_value') {
return $param;
};
$result = $this->invoker->invoke($callback);
$this->assertEquals('default_value', $result);
}
public function testInvokeCallbackWithNullableParameter(): void
{
$callback = function (?string $param) {
return $param ?? 'was_null';
};
$result = $this->invoker->invoke($callback);
$this->assertEquals('was_null', $result);
}
public function testInvokeCallbackThrowsExceptionForUnresolvableParameter(): void
{
$callback = function (string $required) {
return $required;
};
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage("Cannot resolve parameter 'required' of type 'string'");
$this->invoker->invoke($callback);
}
public function testInvokeCallbackThrowsExceptionForNonExistentClass(): void
{
// This test uses UnresolvableTestClass which has required constructor params
// Container.has() will return true but get() will throw InvalidDefinition
// So we test that container exceptions bubble up
$callback = function (UnresolvableTestClass $obj) {
return $obj;
};
$this->expectException(\Throwable::class);
$this->invoker->invoke($callback);
}
public function testInvokeCallbackWithMultipleParameters(): void
{
$callback = function (string $first, int $second, bool $third) {
return [$first, $second, $third];
};
$result = $this->invoker->invoke($callback, [
'first' => 'value1',
'second' => 42,
'third' => true,
]);
$this->assertEquals(['value1', 42, true], $result);
}
public function testInvokeCallbackWithMixedResolutionSources(): void
{
$this->container->set(\stdClass::class, new \stdClass());
$callback = function (
\stdClass $fromContainer,
string $fromContext,
int $withDefault = 100
) {
return [$fromContainer, $fromContext, $withDefault];
};
$result = $this->invoker->invoke($callback, ['fromContext' => 'context_value']);
$this->assertInstanceOf(\stdClass::class, $result[0]);
$this->assertEquals('context_value', $result[1]);
$this->assertEquals(100, $result[2]);
}
public function testExpandContextHierarchyWithObject(): void
{
// Create a simple parent-child relationship
$childClass = new \ArrayObject(['key' => 'value']);
$callback = function (\ArrayObject $obj) {
return $obj;
};
$result = $this->invoker->invoke($callback, [get_class($childClass) => $childClass]);
$this->assertSame($childClass, $result);
}
public function testExpandContextHierarchyWithInterface(): void
{
$object = new class implements \Countable {
public function count(): int
{
return 42;
}
};
$callback = function (\Countable $countable) {
return $countable->count();
};
$result = $this->invoker->invoke($callback, [get_class($object) => $object]);
$this->assertEquals(42, $result);
}
public function testExpandContextHierarchyWithMultipleInterfaces(): void
{
$object = new class implements \Countable, \IteratorAggregate {
public function count(): int
{
return 5;
}
public function getIterator(): \Traversable
{
return new \ArrayIterator([]);
}
};
$callback = function (\Countable $c, \IteratorAggregate $i) {
return [$c->count(), $i];
};
$result = $this->invoker->invoke($callback, ['obj' => $object]);
$this->assertEquals(5, $result[0]);
$this->assertInstanceOf(\IteratorAggregate::class, $result[1]);
}
public function testInvokeWithArrayCallback(): void
{
$testClass = new class {
public function method(string $param): string
{
return 'called_' . $param;
}
};
$result = $this->invoker->invoke([$testClass, 'method'], ['param' => 'test']);
$this->assertEquals('called_test', $result);
}
public function testInvokeWithStaticMethod(): void
{
$testClass = new class {
public static function staticMethod(string $param): string
{
return 'static_' . $param;
}
};
$className = get_class($testClass);
$result = $this->invoker->invoke([$className, 'staticMethod'], ['param' => 'value']);
$this->assertEquals('static_value', $result);
}
public function testInvokeWithCallableString(): void
{
$callback = 'Tests\StaticPHP\DI\testFunction';
if (!function_exists($callback)) {
eval('namespace Tests\StaticPHP\DI; function testFunction(string $param) { return "func_" . $param; }');
}
$result = $this->invoker->invoke($callback, ['param' => 'test']);
$this->assertEquals('func_test', $result);
}
public function testInvokeWithNoTypeHintedParameter(): void
{
$callback = function ($param) {
return $param;
};
$result = $this->invoker->invoke($callback, ['param' => 'value']);
$this->assertEquals('value', $result);
}
public function testInvokeWithNoTypeHintedParameterReturnsNull(): void
{
// Parameters without type hints are implicitly nullable in PHP
$callback = function ($param) {
return $param;
};
$result = $this->invoker->invoke($callback);
$this->assertNull($result);
}
public function testInvokeWithNoTypeHintAndValueInContext(): void
{
$callback = function ($param) {
return $param;
};
$result = $this->invoker->invoke($callback, ['param' => 'value']);
$this->assertEquals('value', $result);
}
public function testInvokeWithBuiltinTypes(): void
{
$callback = function (
string $str,
int $num,
float $decimal,
bool $flag,
array $arr
) {
return compact('str', 'num', 'decimal', 'flag', 'arr');
};
$result = $this->invoker->invoke($callback, [
'str' => 'test',
'num' => 42,
'decimal' => 3.14,
'flag' => true,
'arr' => [1, 2, 3],
]);
$this->assertEquals([
'str' => 'test',
'num' => 42,
'decimal' => 3.14,
'flag' => true,
'arr' => [1, 2, 3],
], $result);
}
public function testInvokeWithEmptyContext(): void
{
$callback = function () {
return 'no_params';
};
$result = $this->invoker->invoke($callback, []);
$this->assertEquals('no_params', $result);
}
public function testInvokePreservesCallbackReturnValue(): void
{
$callback = function () {
return ['key' => 'value', 'number' => 123];
};
$result = $this->invoker->invoke($callback);
$this->assertEquals(['key' => 'value', 'number' => 123], $result);
}
public function testInvokeWithNullReturnValue(): void
{
$callback = function () {
return null;
};
$result = $this->invoker->invoke($callback);
$this->assertNull($result);
}
public function testInvokeWithObjectInContext(): void
{
$obj = new \stdClass();
$obj->value = 'test';
$callback = function (\stdClass $param) {
return $param->value;
};
$result = $this->invoker->invoke($callback, ['param' => $obj]);
$this->assertEquals('test', $result);
}
public function testInvokeWithInheritanceInContext(): void
{
$exception = new \RuntimeException('test message');
$callback = function (\Exception $e) {
return $e->getMessage();
};
// RuntimeException should be resolved as Exception via hierarchy expansion
$result = $this->invoker->invoke($callback, ['exc' => $exception]);
$this->assertEquals('test message', $result);
}
public function testInvokeContextValueOverridesContainer(): void
{
$containerObj = new \stdClass();
$containerObj->source = 'container';
$this->container->set(\stdClass::class, $containerObj);
$contextObj = new \stdClass();
$contextObj->source = 'context';
$callback = function (\stdClass $obj) {
return $obj->source;
};
// Context should override container
$result = $this->invoker->invoke($callback, [\stdClass::class => $contextObj]);
$this->assertEquals('context', $result);
}
public function testInvokeWithDefaultValueNotUsedWhenContextProvided(): void
{
$callback = function (string $param = 'default') {
return $param;
};
$result = $this->invoker->invoke($callback, ['param' => 'from_context']);
$this->assertEquals('from_context', $result);
}
public function testInvokeWithMixedNullableAndRequired(): void
{
$callback = function (string $required, ?string $optional) {
return [$required, $optional];
};
$result = $this->invoker->invoke($callback, ['required' => 'value']);
$this->assertEquals(['value', null], $result);
}
public function testInvokeWithComplexObjectHierarchy(): void
{
// Use built-in PHP classes with inheritance
// ArrayIterator extends IteratorIterator implements ArrayAccess, SeekableIterator, Countable, Serializable
$arrayIterator = new \ArrayIterator(['test' => 'value']);
// Test that the object can be resolved via interface (Countable)
$callback1 = function (\Countable $test) {
return $test->count();
};
$result1 = $this->invoker->invoke($callback1, ['obj' => $arrayIterator]);
$this->assertEquals(1, $result1);
// Test that the object can be resolved via another interface (Iterator)
$callback2 = function (\Iterator $test) {
return $test;
};
$result2 = $this->invoker->invoke($callback2, ['obj' => $arrayIterator]);
$this->assertInstanceOf(\ArrayIterator::class, $result2);
// Test that the object can be resolved via concrete class
$callback3 = function (\ArrayIterator $test) {
return $test;
};
$result3 = $this->invoker->invoke($callback3, ['obj' => $arrayIterator]);
$this->assertSame($arrayIterator, $result3);
}
public function testInvokeWithNonObjectContextValues(): void
{
$callback = function (string $str, int $num, array $arr, bool $flag) {
return compact('str', 'num', 'arr', 'flag');
};
$context = [
'str' => 'hello',
'num' => 999,
'arr' => ['a', 'b'],
'flag' => false,
];
$result = $this->invoker->invoke($callback, $context);
$this->assertEquals($context, $result);
}
public function testInvokeParameterOrderMatters(): void
{
$callback = function (string $first, string $second, string $third) {
return [$first, $second, $third];
};
$result = $this->invoker->invoke($callback, [
'first' => 'A',
'second' => 'B',
'third' => 'C',
]);
$this->assertEquals(['A', 'B', 'C'], $result);
}
public function testInvokeWithUnionTypeThrowsException(): void
{
if (PHP_VERSION_ID < 80000) {
$this->markTestSkipped('Union types require PHP 8.0+');
}
$callback = eval('return function (string|int $param) { return $param; };');
// Union types are not ReflectionNamedType, should not be resolved from container
$this->expectException(\RuntimeException::class);
$this->invoker->invoke($callback);
}
public function testInvokeWithCallableType(): void
{
$callback = function (callable $fn) {
return $fn();
};
$result = $this->invoker->invoke($callback, [
'fn' => fn () => 'called',
]);
$this->assertEquals('called', $result);
}
public function testInvokeWithIterableType(): void
{
$callback = function (iterable $items) {
$result = [];
foreach ($items as $item) {
$result[] = $item;
}
return $result;
};
$result = $this->invoker->invoke($callback, [
'items' => [1, 2, 3],
]);
$this->assertEquals([1, 2, 3], $result);
}
public function testInvokeWithObjectType(): void
{
$callback = function (object $obj) {
return get_class($obj);
};
$testObj = new \stdClass();
$result = $this->invoker->invoke($callback, ['obj' => $testObj]);
$this->assertEquals('stdClass', $result);
}
public function testInvokeWithContainerExceptionFallsThrough(): void
{
// Test fix for issue #1 - Container exceptions should be caught
// and fall through to other resolution strategies
$callback = function (?UnresolvableTestClass $obj = null) {
return $obj;
};
// Should use default value (null) instead of throwing container exception
$result = $this->invoker->invoke($callback);
$this->assertNull($result);
}
public function testInvokeWithContainerExceptionAndNoFallback(): void
{
// When there's no fallback (no default, not nullable), should throw RuntimeException
$callback = function (UnresolvableTestClass $obj) {
return $obj;
};
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage("Cannot resolve parameter 'obj'");
$this->invoker->invoke($callback);
}
public function testExpandContextHierarchyPerformance(): void
{
// Test fix for issue #2 - Should not create duplicate ReflectionClass
// This is more of a code quality test, ensuring the fix doesn't break functionality
$obj = new \ArrayIterator(['a', 'b', 'c']);
$callback = function (
\ArrayIterator $asArrayIterator,
\Traversable $asTraversable,
\Countable $asCountable
) {
return [
get_class($asArrayIterator),
get_class($asTraversable),
get_class($asCountable),
];
};
$result = $this->invoker->invoke($callback, ['obj' => $obj]);
$this->assertEquals([
'ArrayIterator',
'ArrayIterator',
'ArrayIterator',
], $result);
}
}

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);
}
}