diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index d1c30090..b9eb063e 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -69,6 +69,6 @@ return (new PhpCsFixer\Config()) 'php_unit_data_provider_method_order' => false, ]) ->setFinder( - PhpCsFixer\Finder::create()->in([__DIR__ . '/src', __DIR__ . '/tests/SPC']) + PhpCsFixer\Finder::create()->in([__DIR__ . '/src', __DIR__ . '/tests/StaticPHP']) ) ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()); diff --git a/composer.json b/composer.json index eadd2732..fb17f9a0 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,7 @@ }, "autoload-dev": { "psr-4": { - "SPC\\Tests\\": "tests/SPC" + "Tests\\StaticPHP\\": "tests/StaticPHP" } }, "bin": [ diff --git a/src/StaticPHP/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php index f860ec0f..778c24f3 100644 --- a/src/StaticPHP/Artifact/ArtifactExtractor.php +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -352,8 +352,6 @@ class ArtifactExtractor * @param string $name Artifact name (for error messages) * @param string $source_file Path to the source file or directory * @param string $cache_type Cache type: archive, git, local - * - * @throws WrongUsageException if source file does not exist */ protected function validateSourceFile(string $name, string $source_file, string $cache_type): void { diff --git a/src/StaticPHP/DI/ApplicationContext.php b/src/StaticPHP/DI/ApplicationContext.php index 9b702ccb..a845fba2 100644 --- a/src/StaticPHP/DI/ApplicationContext.php +++ b/src/StaticPHP/DI/ApplicationContext.php @@ -35,8 +35,6 @@ class ApplicationContext * @param array $options Initialization options * - 'debug': Enable debug mode (disables compilation) * - 'definitions': Additional container definitions - * - * @throws \RuntimeException If already initialized */ public static function initialize(array $options = []): Container { @@ -60,7 +58,8 @@ class ApplicationContext self::$debug = $options['debug'] ?? false; self::$container = $builder->build(); - self::$invoker = new CallbackInvoker(self::$container); + // Get invoker from container to ensure singleton consistency + self::$invoker = self::$container->get(CallbackInvoker::class); return self::$container; } @@ -126,7 +125,8 @@ class ApplicationContext public static function getInvoker(): CallbackInvoker { if (self::$invoker === null) { - self::$invoker = new CallbackInvoker(self::getContainer()); + // Get from container to ensure singleton consistency + self::$invoker = self::getContainer()->get(CallbackInvoker::class); } return self::$invoker; } @@ -139,14 +139,18 @@ class ApplicationContext */ public static function invoke(callable $callback, array $context = []): mixed { - logger()->debug('[INVOKE] ' . (is_array($callback) ? (is_object($callback[0]) ? get_class($callback[0]) : $callback[0]) . '::' . $callback[1] : (is_string($callback) ? $callback : 'Closure'))); + if (function_exists('logger')) { + logger()->debug('[INVOKE] ' . (is_array($callback) ? (is_object($callback[0]) ? get_class($callback[0]) : $callback[0]) . '::' . $callback[1] : (is_string($callback) ? $callback : 'Closure'))); + } // get if callback has attribute PatchDescription $ref = new \ReflectionFunction(\Closure::fromCallable($callback)); $attributes = $ref->getAttributes(PatchDescription::class); foreach ($attributes as $attribute) { $attrInstance = $attribute->newInstance(); - logger()->info(ConsoleColor::magenta('[PATCH]') . ConsoleColor::green(" {$attrInstance->description}")); + if (function_exists('logger')) { + logger()->info(ConsoleColor::magenta('[PATCH]') . ConsoleColor::green(" {$attrInstance->description}")); + } } return self::getInvoker()->invoke($callback, $context); } diff --git a/src/StaticPHP/DI/CallbackInvoker.php b/src/StaticPHP/DI/CallbackInvoker.php index fa11a7f1..0d77f7aa 100644 --- a/src/StaticPHP/DI/CallbackInvoker.php +++ b/src/StaticPHP/DI/CallbackInvoker.php @@ -5,12 +5,13 @@ declare(strict_types=1); namespace StaticPHP\DI; use DI\Container; +use StaticPHP\Exception\SPCInternalException; /** * CallbackInvoker is responsible for invoking callbacks with automatic dependency injection. * It supports context-based parameter resolution, allowing temporary bindings without polluting the container. */ -class CallbackInvoker +readonly class CallbackInvoker { public function __construct( private Container $container @@ -34,8 +35,6 @@ class CallbackInvoker * @param array $context Context parameters (type => value or name => value) * * @return mixed The return value of the callback - * - * @throws \RuntimeException If a required parameter cannot be resolved */ public function invoke(callable $callback, array $context = []): mixed { @@ -64,8 +63,13 @@ class CallbackInvoker // 3. Look up in container by type if ($typeName !== null && !$this->isBuiltinType($typeName) && $this->container->has($typeName)) { - $args[] = $this->container->get($typeName); - continue; + try { + $args[] = $this->container->get($typeName); + continue; + } catch (\Throwable $e) { + // Container failed to resolve (e.g., missing constructor params) + // Fall through to try default value or nullable + } } // 4. Use default value if available @@ -81,7 +85,7 @@ class CallbackInvoker } // Cannot resolve parameter - throw new \RuntimeException( + throw new SPCInternalException( "Cannot resolve parameter '{$paramName}'" . ($typeName ? " of type '{$typeName}'" : '') . ' for callback invocation' @@ -120,19 +124,20 @@ class CallbackInvoker // If value is an object, add mappings for all parent classes and interfaces if (is_object($value)) { - $reflection = new \ReflectionClass($value); + $originalReflection = new \ReflectionClass($value); // Add concrete class - $expanded[$reflection->getName()] = $value; + $expanded[$originalReflection->getName()] = $value; // Add all parent classes + $reflection = $originalReflection; while ($parent = $reflection->getParentClass()) { $expanded[$parent->getName()] = $value; $reflection = $parent; } - // Add all interfaces - $interfaces = (new \ReflectionClass($value))->getInterfaceNames(); + // Add all interfaces - reuse original reflection + $interfaces = $originalReflection->getInterfaceNames(); foreach ($interfaces as $interface) { $expanded[$interface] = $value; } diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 96316887..1e5e27e8 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -404,8 +404,6 @@ class PackageInstaller /** * Validate that a package has required artifacts. - * - * @throws WrongUsageException if target/library package has no source or platform binary */ private function validatePackageArtifact(Package $package): void { diff --git a/src/StaticPHP/Registry/Registry.php b/src/StaticPHP/Registry/Registry.php index 71d53f82..4ae5df4f 100644 --- a/src/StaticPHP/Registry/Registry.php +++ b/src/StaticPHP/Registry/Registry.php @@ -25,7 +25,7 @@ class Registry */ public static function loadRegistry(string $registry_file, bool $auto_require = true): void { - $yaml = file_get_contents($registry_file); + $yaml = @file_get_contents($registry_file); if ($yaml === false) { throw new RegistryException("Failed to read registry file: {$registry_file}"); } diff --git a/tests/SPC/GlobalDefinesTest.php b/tests/SPC/GlobalDefinesTest.php deleted file mode 100644 index 956b35e2..00000000 --- a/tests/SPC/GlobalDefinesTest.php +++ /dev/null @@ -1,25 +0,0 @@ -assertTrue(defined('WORKING_DIR')); - } - - public function testInternalEnv(): void - { - require __DIR__ . '/../../src/globals/internal-env.php'; - $this->assertTrue(defined('GNU_ARCH')); - } -} diff --git a/tests/SPC/GlobalFunctionsTest.php b/tests/SPC/GlobalFunctionsTest.php deleted file mode 100644 index 6a8bd738..00000000 --- a/tests/SPC/GlobalFunctionsTest.php +++ /dev/null @@ -1,33 +0,0 @@ -assertEquals('abc', match_pattern('a*c', 'abc')); - $this->assertFalse(match_pattern('a*c', 'abcd')); - } - - public function testFExec(): void - { - $this->assertEquals('abc', f_exec('echo abc', $out, $ret)); - $this->assertEquals(0, $ret); - $this->assertEquals(['abc'], $out); - } - - public function testPatchPointInterrupt(): void - { - $except = patch_point_interrupt(0); - $this->assertInstanceOf(InterruptException::class, $except); - } -} diff --git a/tests/SPC/builder/BuilderTest.php b/tests/SPC/builder/BuilderTest.php deleted file mode 100644 index b4b6258f..00000000 --- a/tests/SPC/builder/BuilderTest.php +++ /dev/null @@ -1,246 +0,0 @@ -builder = BuilderProvider::makeBuilderByInput(new ArgvInput()); - [$extensions, $libs] = DependencyUtil::getExtsAndLibs(['mbregex']); - $this->builder->proveLibs($libs); - foreach ($extensions as $extension) { - $class = AttributeMapper::getExtensionClassByName($extension) ?? Extension::class; - $ext = new $class($extension, $this->builder); - $this->builder->addExt($ext); - } - foreach ($this->builder->getExts() as $ext) { - $ext->checkDependency(); - } - } - - public function testMakeBuilderByInput(): void - { - $this->assertInstanceOf(BuilderBase::class, BuilderProvider::makeBuilderByInput(new ArgvInput())); - $this->assertInstanceOf(BuilderBase::class, BuilderProvider::getBuilder()); - } - - public function testGetLibAndGetLibs() - { - $this->assertIsArray($this->builder->getLibs()); - $this->assertInstanceOf(LibraryBase::class, $this->builder->getLib('onig')); - } - - public function testGetExtAndGetExts() - { - $this->assertIsArray($this->builder->getExts()); - $this->assertInstanceOf(Extension::class, $this->builder->getExt('mbregex')); - } - - public function testMakeExtensionArgs() - { - $this->assertStringContainsString('--enable-mbstring', $this->builder->makeStaticExtensionArgs()); - } - - public function testIsLibsOnly() - { - // mbregex is not libs only - $this->assertFalse($this->builder->isLibsOnly()); - } - - public function testGetPHPVersionID() - { - if (file_exists(SOURCE_PATH . '/php-src/main/php_version.h')) { - $file = SOURCE_PATH . '/php-src/main/php_version.h'; - $cnt = preg_match('/PHP_VERSION_ID (\d+)/m', file_get_contents($file), $match); - if ($cnt !== 0) { - $this->assertEquals(intval($match[1]), $this->builder->getPHPVersionID()); - } else { - $this->expectException(WrongUsageException::class); - $this->builder->getPHPVersionID(); - } - } else { - $this->expectException(WrongUsageException::class); - $this->builder->getPHPVersionID(); - } - } - - public function testGetPHPVersion() - { - if (file_exists(SOURCE_PATH . '/php-src/main/php_version.h')) { - $file = SOURCE_PATH . '/php-src/main/php_version.h'; - $cnt = preg_match('/PHP_VERSION "(\d+\.\d+\.\d+(?:-[^"]+)?)/', file_get_contents($file), $match); - if ($cnt !== 0) { - $this->assertEquals($match[1], $this->builder->getPHPVersion()); - } else { - $this->expectException(WrongUsageException::class); - $this->builder->getPHPVersion(); - } - } else { - $this->expectException(WrongUsageException::class); - $this->builder->getPHPVersion(); - } - } - - public function testGetPHPVersionFromArchive() - { - $lock = file_exists(LockFile::LOCK_FILE) ? file_get_contents(LockFile::LOCK_FILE) : false; - if ($lock === false) { - $this->assertFalse($this->builder->getPHPVersionFromArchive()); - } else { - $lock = json_decode($lock, true); - $file = $lock['php-src']['filename'] ?? null; - if ($file === null) { - $this->assertFalse($this->builder->getPHPVersionFromArchive()); - } else { - $cnt = preg_match('/php-(\d+\.\d+\.\d+)/', $file, $match); - if ($cnt !== 0) { - $this->assertEquals($match[1], $this->builder->getPHPVersionFromArchive()); - } else { - $this->assertFalse($this->builder->getPHPVersionFromArchive()); - } - } - } - } - - public function testGetMicroVersion() - { - $file = FileSystem::convertPath(SOURCE_PATH . '/php-src/sapi/micro/php_micro.h'); - if (!file_exists($file)) { - $this->assertFalse($this->builder->getMicroVersion()); - } else { - $content = file_get_contents($file); - $ver = ''; - preg_match('/#define PHP_MICRO_VER_MAJ (\d)/m', $content, $match); - $ver .= $match[1] . '.'; - preg_match('/#define PHP_MICRO_VER_MIN (\d)/m', $content, $match); - $ver .= $match[1] . '.'; - preg_match('/#define PHP_MICRO_VER_PAT (\d)/m', $content, $match); - $ver .= $match[1]; - $this->assertEquals($ver, $this->builder->getMicroVersion()); - } - } - - public static function providerGetBuildTypeName(): array - { - return [ - [BUILD_TARGET_CLI, 'cli'], - [BUILD_TARGET_FPM, 'fpm'], - [BUILD_TARGET_MICRO, 'micro'], - [BUILD_TARGET_EMBED, 'embed'], - [BUILD_TARGET_FRANKENPHP, 'frankenphp'], - [BUILD_TARGET_ALL, 'cli, micro, fpm, embed, frankenphp, cgi'], - [BUILD_TARGET_CLI | BUILD_TARGET_EMBED, 'cli, embed'], - ]; - } - - /** - * @dataProvider providerGetBuildTypeName - */ - public function testGetBuildTypeName(int $target, string $name): void - { - $this->assertEquals($name, $this->builder->getBuildTypeName($target)); - } - - public function testGetOption() - { - // we cannot assure the option exists, so just tests default value - $this->assertEquals('foo', $this->builder->getOption('bar', 'foo')); - } - - public function testGetOptions() - { - $this->assertIsArray($this->builder->getOptions()); - } - - public function testSetOptionIfNotExist() - { - $this->assertEquals(null, $this->builder->getOption('bar')); - $this->builder->setOptionIfNotExist('bar', 'foo'); - $this->assertEquals('foo', $this->builder->getOption('bar')); - } - - public function testSetOption() - { - $this->assertEquals(null, $this->builder->getOption('bar')); - $this->builder->setOption('bar', 'foo'); - $this->assertEquals('foo', $this->builder->getOption('bar')); - } - - public function testGetEnvString() - { - $this->assertIsString($this->builder->getEnvString()); - putenv('TEST_SPC_BUILDER=foo'); - $this->assertStringContainsString('TEST_SPC_BUILDER=foo', $this->builder->getEnvString(['TEST_SPC_BUILDER'])); - } - - public function testValidateLibsAndExts() - { - $this->builder->validateLibsAndExts(); - $this->assertTrue(true); - } - - public static function providerEmitPatchPoint(): array - { - return [ - ['before-libs-extract'], - ['after-libs-extract'], - ['before-php-extract'], - ['after-php-extract'], - ['before-micro-extract'], - ['after-micro-extract'], - ['before-exts-extract'], - ['after-exts-extract'], - ['before-php-buildconf'], - ['before-php-configure'], - ['before-php-make'], - ['before-sanity-check'], - ]; - } - - /** - * @dataProvider providerEmitPatchPoint - */ - public function testEmitPatchPoint(string $point) - { - $code = 'builder->setOption('with-added-patch', ['/tmp/patch-point.' . $point . '.php']); - FileSystem::writeFile('/tmp/patch-point.' . $point . '.php', $code); - $this->expectOutputString('GOOD:' . $point); - $this->builder->emitPatchPoint($point); - } - - public function testEmitPatchPointNotExists() - { - $this->expectOutputRegex('/failed to run/'); - $this->expectException(WrongUsageException::class); - $this->builder->setOption('with-added-patch', ['/tmp/patch-point.not_exsssists.php']); - $this->builder->emitPatchPoint('not-exists'); - } -} diff --git a/tests/SPC/builder/ExtensionTest.php b/tests/SPC/builder/ExtensionTest.php deleted file mode 100644 index 89462065..00000000 --- a/tests/SPC/builder/ExtensionTest.php +++ /dev/null @@ -1,94 +0,0 @@ -proveLibs($libs); - foreach ($extensions as $extension) { - $class = AttributeMapper::getExtensionClassByName($extension) ?? Extension::class; - $ext = new $class($extension, $builder); - $builder->addExt($ext); - } - foreach ($builder->getExts() as $ext) { - $ext->checkDependency(); - } - $this->extension = $builder->getExt('mbregex'); - } - - public function testPatches() - { - $this->assertFalse($this->extension->patchBeforeBuildconf()); - $this->assertFalse($this->extension->patchBeforeConfigure()); - $this->assertFalse($this->extension->patchBeforeMake()); - } - - public function testGetExtensionDependency() - { - $this->assertEquals('mbstring', current($this->extension->getExtensionDependency())->getName()); - } - - public function testGetWindowsConfigureArg() - { - $this->assertEquals('', $this->extension->getWindowsConfigureArg()); - } - - public function testGetConfigureArg() - { - $this->assertEquals('', $this->extension->getUnixConfigureArg()); - } - - public function testGetExtVersion() - { - // only swoole has version, we cannot test it - $this->assertEquals(null, $this->extension->getExtVersion()); - } - - public function testGetDistName() - { - $this->assertEquals('mbregex', $this->extension->getName()); - } - - public function testRunCliCheckWindows() - { - if (is_unix()) { - $this->markTestSkipped('This test is for Windows only'); - } else { - $this->extension->runCliCheckWindows(); - $this->assertTrue(true); - } - } - - public function testGetName() - { - $this->assertEquals('mbregex', $this->extension->getName()); - } - - public function testGetUnixConfigureArg() - { - $this->assertEquals('', $this->extension->getUnixConfigureArg()); - } - - public function testGetEnableArg() - { - $this->assertEquals('', $this->extension->getEnableArg()); - } -} diff --git a/tests/SPC/builder/linux/SystemUtilTest.php b/tests/SPC/builder/linux/SystemUtilTest.php deleted file mode 100644 index 01d555d8..00000000 --- a/tests/SPC/builder/linux/SystemUtilTest.php +++ /dev/null @@ -1,60 +0,0 @@ -assertArrayHasKey('dist', $release); - $this->assertArrayHasKey('ver', $release); - $this->assertTrue($release['dist'] === 'alpine' && SystemUtil::isMuslDist() || $release['dist'] !== 'alpine' && !SystemUtil::isMuslDist()); - } - - public function testFindStaticLib() - { - $this->assertIsArray(SystemUtil::findStaticLib('ld-linux-x86-64.so.2')); - } - - public function testGetCpuCount() - { - $this->assertIsInt(SystemUtil::getCpuCount()); - } - - public function testFindHeader() - { - $this->assertIsArray(SystemUtil::findHeader('elf.h')); - } - - public function testGetSupportedDistros() - { - $this->assertIsArray(SystemUtil::getSupportedDistros()); - } - - public function testFindHeaders() - { - $this->assertIsArray(SystemUtil::findHeaders(['elf.h'])); - } - - public function testFindStaticLibs() - { - $this->assertIsArray(SystemUtil::findStaticLibs(['ld-linux-x86-64.so.2'])); - } -} diff --git a/tests/SPC/builder/macos/SystemUtilTest.php b/tests/SPC/builder/macos/SystemUtilTest.php deleted file mode 100644 index 4af764a7..00000000 --- a/tests/SPC/builder/macos/SystemUtilTest.php +++ /dev/null @@ -1,31 +0,0 @@ -assertIsInt(SystemUtil::getCpuCount()); - } - - public function testGetArchCFlags() - { - $this->assertEquals('--target=x86_64-apple-darwin', SystemUtil::getArchCFlags('x86_64')); - } -} diff --git a/tests/SPC/builder/unix/UnixSystemUtilTest.php b/tests/SPC/builder/unix/UnixSystemUtilTest.php deleted file mode 100644 index e17574af..00000000 --- a/tests/SPC/builder/unix/UnixSystemUtilTest.php +++ /dev/null @@ -1,42 +0,0 @@ - 'SPC\builder\linux\SystemUtil', - 'Darwin' => 'SPC\builder\macos\SystemUtil', - 'FreeBSD' => 'SPC\builder\freebsd\SystemUtil', - default => null, - }; - if ($util_class === null) { - self::markTestSkipped('This test is only for Unix'); - } - $this->util = new $util_class(); - } - - public function testFindCommand() - { - $this->assertIsString($this->util->findCommand('bash')); - } - - public function testMakeEnvVarString() - { - $this->assertEquals("PATH='/usr/bin' PKG_CONFIG='/usr/bin/pkg-config'", $this->util->makeEnvVarString(['PATH' => '/usr/bin', 'PKG_CONFIG' => '/usr/bin/pkg-config'])); - } -} diff --git a/tests/SPC/doctor/CheckListHandlerTest.php b/tests/SPC/doctor/CheckListHandlerTest.php deleted file mode 100644 index c6191e30..00000000 --- a/tests/SPC/doctor/CheckListHandlerTest.php +++ /dev/null @@ -1,24 +0,0 @@ -getValidCheckList(); - foreach ($id as $item) { - $this->assertInstanceOf('SPC\doctor\AsCheckItem', $item); - } - } -} diff --git a/tests/SPC/globals/GlobalFunctionsTest.php b/tests/SPC/globals/GlobalFunctionsTest.php deleted file mode 100644 index 818f9686..00000000 --- a/tests/SPC/globals/GlobalFunctionsTest.php +++ /dev/null @@ -1,94 +0,0 @@ -assertTrue(is_assoc_array(['a' => 1, 'b' => 2])); - $this->assertFalse(is_assoc_array([1, 2, 3])); - } - - public function testLogger(): void - { - $this->assertInstanceOf('Psr\Log\LoggerInterface', logger()); - } - - public function testArch2Gnu(): void - { - $this->assertEquals('x86_64', arch2gnu('x86_64')); - $this->assertEquals('x86_64', arch2gnu('x64')); - $this->assertEquals('x86_64', arch2gnu('amd64')); - $this->assertEquals('aarch64', arch2gnu('arm64')); - $this->assertEquals('aarch64', arch2gnu('aarch64')); - $this->expectException('SPC\exception\WrongUsageException'); - arch2gnu('armv7'); - } - - public function testQuote(): void - { - $this->assertEquals('"hello"', quote('hello')); - $this->assertEquals("'hello'", quote('hello', "'")); - } - - public function testFPassthru(): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('Windows not support f_passthru'); - } - $this->assertEquals(null, f_passthru('echo ""')); - $this->expectException(ExecutionException::class); - f_passthru('false'); - } - - public function testFPutenv(): void - { - $this->assertTrue(f_putenv('SPC_TEST_ENV=1')); - $this->assertEquals('1', getenv('SPC_TEST_ENV')); - } - - public function testShell(): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('Windows not support shell'); - } - $shell = shell(); - $this->assertInstanceOf('SPC\util\shell\UnixShell', $shell); - $this->assertInstanceOf('SPC\util\shell\UnixShell', $shell->cd('/')); - $this->assertInstanceOf('SPC\util\shell\UnixShell', $shell->exec('echo ""')); - $this->assertInstanceOf('SPC\util\shell\UnixShell', $shell->setEnv(['SPC_TEST_ENV' => '1'])); - - [$code, $out] = $shell->execWithResult('echo "_"'); - $this->assertEquals(0, $code); - $this->assertEquals('_', implode('', $out)); - - $this->expectException('SPC\exception\ExecutionException'); - $shell->exec('false'); - } -} diff --git a/tests/SPC/store/ConfigTest.php b/tests/SPC/store/ConfigTest.php deleted file mode 100644 index 84701cd4..00000000 --- a/tests/SPC/store/ConfigTest.php +++ /dev/null @@ -1,68 +0,0 @@ -assertTrue(is_assoc_array(Config::getExts())); - } - - public function testGetLib() - { - $this->assertIsArray(Config::getLib('zlib')); - match (PHP_OS_FAMILY) { - 'FreeBSD', 'Darwin', 'Linux' => $this->assertStringEndsWith('.a', Config::getLib('zlib', 'static-libs', [])[0]), - 'Windows' => $this->assertStringEndsWith('.lib', Config::getLib('zlib', 'static-libs', [])[0]), - default => null, - }; - } - - public function testGetExt() - { - $this->assertIsArray(Config::getExt('bcmath')); - $this->assertEquals('builtin', Config::getExt('bcmath', 'type')); - } - - public function testGetSources() - { - $this->assertTrue(is_assoc_array(Config::getSources())); - } - - public function testGetSource() - { - $this->assertIsArray(Config::getSource('php-src')); - } - - public function testGetLibs() - { - $this->assertTrue(is_assoc_array(Config::getLibs())); - } -} diff --git a/tests/SPC/store/CurlHookTest.php b/tests/SPC/store/CurlHookTest.php deleted file mode 100644 index 8b950da6..00000000 --- a/tests/SPC/store/CurlHookTest.php +++ /dev/null @@ -1,29 +0,0 @@ -assertEmpty($header); - } else { - $this->assertEquals(['Authorization: Bearer ' . getenv('GITHUB_TOKEN')], $header); - } - $header = []; - putenv('GITHUB_TOKEN=token'); - CurlHook::setupGithubToken('GET', 'https://example.com', $header); - $this->assertEquals(['Authorization: Bearer token'], $header); - } -} diff --git a/tests/SPC/store/DownloaderTest.php b/tests/SPC/store/DownloaderTest.php deleted file mode 100644 index 749fb385..00000000 --- a/tests/SPC/store/DownloaderTest.php +++ /dev/null @@ -1,113 +0,0 @@ -assertEquals( - 'https://api.github.com/repos/AOMediaCodec/libavif/tarball/v1.1.1', - Downloader::getLatestGithubTarball('libavif', [ - 'type' => 'ghtar', - 'repo' => 'AOMediaCodec/libavif', - ])[0] - ); - } - - public function testDownloadGit() - { - Downloader::downloadGit('setup-static-php', 'https://github.com/static-php/setup-static-php.git', 'main'); - $this->assertTrue(true); - - // test keyboard interrupt - try { - Downloader::downloadGit('setup-static-php', 'https://github.com/static-php/setup-static-php.git', 'SIGINT'); - } catch (InterruptException $e) { - $this->assertStringContainsString('interrupted', $e->getMessage()); - return; - } - $this->fail('Expected exception not thrown'); - } - - public function testDownloadFile() - { - Downloader::downloadFile('fake-file', 'https://fakecmd.com/curlDown', 'curlDown.exe'); - $this->assertTrue(true); - - // test keyboard interrupt - try { - Downloader::downloadFile('fake-file', 'https://fakecmd.com/curlDown', 'SIGINT'); - } catch (InterruptException $e) { - $this->assertStringContainsString('interrupted', $e->getMessage()); - return; - } - $this->fail('Expected exception not thrown'); - } - - public function testLockSource() - { - LockFile::lockSource('fake-file', ['source_type' => SPC_SOURCE_ARCHIVE, 'filename' => 'fake-file-name', 'move_path' => 'fake-path', 'lock_as' => 'fake-lock-as']); - $this->assertFileExists(LockFile::LOCK_FILE); - $json = json_decode(file_get_contents(LockFile::LOCK_FILE), true); - $this->assertIsArray($json); - $this->assertArrayHasKey('fake-file', $json); - $this->assertArrayHasKey('source_type', $json['fake-file']); - $this->assertArrayHasKey('filename', $json['fake-file']); - $this->assertArrayHasKey('move_path', $json['fake-file']); - $this->assertArrayHasKey('lock_as', $json['fake-file']); - $this->assertEquals(SPC_SOURCE_ARCHIVE, $json['fake-file']['source_type']); - $this->assertEquals('fake-file-name', $json['fake-file']['filename']); - $this->assertEquals('fake-path', $json['fake-file']['move_path']); - $this->assertEquals('fake-lock-as', $json['fake-file']['lock_as']); - } - - public function testGetLatestBitbucketTag() - { - $this->assertEquals( - 'abc.tar.gz', - Downloader::getLatestBitbucketTag('abc', [ - 'repo' => 'MATCHED/def', - ])[1] - ); - $this->assertEquals( - 'abc-1.0.0.tar.gz', - Downloader::getLatestBitbucketTag('abc', [ - 'repo' => 'abc/def', - ])[1] - ); - } - - public function testGetLatestGithubRelease() - { - $this->assertEquals( - 'ghreltest.tar.gz', - Downloader::getLatestGithubRelease('ghrel', [ - 'type' => 'ghrel', - 'repo' => 'ghreltest/ghrel', - 'match' => 'ghreltest.tar.gz', - ])[1] - ); - } - - public function testGetFromFileList() - { - $filelist = Downloader::getFromFileList('fake-filelist', [ - 'url' => 'https://fakecmd.com/filelist', - 'regex' => '/href="(?filelist-(?[^"]+)\.tar\.xz)"/', - ]); - $this->assertIsArray($filelist); - $this->assertEquals('filelist-4.7.0.tar.xz', $filelist[1]); - } -} diff --git a/tests/SPC/store/FileSystemTest.php b/tests/SPC/store/FileSystemTest.php deleted file mode 100644 index 13d2893f..00000000 --- a/tests/SPC/store/FileSystemTest.php +++ /dev/null @@ -1,176 +0,0 @@ -assertEquals('he11o', file_get_contents($file)); - - unlink($file); - } - - public function testFindCommandPath() - { - $this->assertNull(FileSystem::findCommandPath('randomtestxxxxx')); - if (PHP_OS_FAMILY === 'Windows') { - $this->assertIsString(FileSystem::findCommandPath('explorer')); - } elseif (in_array(PHP_OS_FAMILY, ['Linux', 'Darwin', 'FreeBSD'])) { - $this->assertIsString(FileSystem::findCommandPath('uname')); - } - } - - public function testReadFile() - { - $file = WORKING_DIR . '/.testread'; - file_put_contents($file, 'haha'); - $content = FileSystem::readFile($file); - $this->assertEquals('haha', $content); - @unlink($file); - } - - public function testReplaceFileUser() - { - $file = WORKING_DIR . '/.txt1'; - file_put_contents($file, 'hello'); - - FileSystem::replaceFileUser($file, function ($file) { - return str_replace('el', '55', $file); - }); - $this->assertEquals('h55lo', file_get_contents($file)); - - unlink($file); - } - - public function testExtname() - { - $this->assertEquals('exe', FileSystem::extname('/tmp/asd.exe')); - $this->assertEquals('', FileSystem::extname('/tmp/asd.')); - } - - public function testGetClassesPsr4() - { - $classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/builder/extension', 'SPC\builder\extension'); - foreach ($classes as $class) { - $this->assertIsString($class); - new \ReflectionClass($class); - } - } - - public function testConvertPath() - { - $this->assertEquals('phar://C:/pharfile.phar', FileSystem::convertPath('phar://C:/pharfile.phar')); - if (DIRECTORY_SEPARATOR === '\\') { - $this->assertEquals('C:\Windows\win.ini', FileSystem::convertPath('C:\Windows/win.ini')); - } - } - - public function testCreateDir() - { - FileSystem::createDir(WORKING_DIR . '/.testdir'); - $this->assertDirectoryExists(WORKING_DIR . '/.testdir'); - rmdir(WORKING_DIR . '/.testdir'); - } - - public function testReplaceFileStr() - { - $file = WORKING_DIR . '/.txt1'; - file_put_contents($file, 'hello'); - - FileSystem::replaceFileStr($file, 'el', '55'); - $this->assertEquals('h55lo', file_get_contents($file)); - - unlink($file); - } - - public function testResetDir() - { - // prepare fake git dir to test - FileSystem::createDir(WORKING_DIR . '/.fake_down_test'); - FileSystem::writeFile(WORKING_DIR . '/.fake_down_test/a.c', 'int main() { return 0; }'); - FileSystem::resetDir(WORKING_DIR . '/.fake_down_test'); - $this->assertFileDoesNotExist(WORKING_DIR . '/.fake_down_test/a.c'); - FileSystem::removeDir(WORKING_DIR . '/.fake_down_test'); - } - - public function testCopyDir() - { - // prepare fake git dir to test - FileSystem::createDir(WORKING_DIR . '/.fake_down_test'); - FileSystem::writeFile(WORKING_DIR . '/.fake_down_test/a.c', 'int main() { return 0; }'); - FileSystem::copyDir(WORKING_DIR . '/.fake_down_test', WORKING_DIR . '/.fake_down_test2'); - $this->assertDirectoryExists(WORKING_DIR . '/.fake_down_test2'); - $this->assertFileExists(WORKING_DIR . '/.fake_down_test2/a.c'); - FileSystem::removeDir(WORKING_DIR . '/.fake_down_test'); - FileSystem::removeDir(WORKING_DIR . '/.fake_down_test2'); - } - - public function testRemoveDir() - { - FileSystem::createDir(WORKING_DIR . '/.fake_down_test'); - $this->assertDirectoryExists(WORKING_DIR . '/.fake_down_test'); - FileSystem::removeDir(WORKING_DIR . '/.fake_down_test'); - $this->assertDirectoryDoesNotExist(WORKING_DIR . '/.fake_down_test'); - } - - public function testLoadConfigArray() - { - $arr = FileSystem::loadConfigArray('lib'); - $this->assertArrayHasKey('zlib', $arr); - } - - public function testIsRelativePath() - { - $this->assertTrue(FileSystem::isRelativePath('.')); - $this->assertTrue(FileSystem::isRelativePath('.\sdf')); - if (DIRECTORY_SEPARATOR === '\\') { - $this->assertFalse(FileSystem::isRelativePath('C:\asdasd/fwe\asd')); - } else { - $this->assertFalse(FileSystem::isRelativePath('/fwefwefewf')); - } - } - - public function testScanDirFiles() - { - $this->assertFalse(FileSystem::scanDirFiles('wfwefewfewf')); - $files = FileSystem::scanDirFiles(ROOT_DIR . '/config', true, true); - $this->assertContains('lib.json', $files); - } - - public function testWriteFile() - { - FileSystem::writeFile(WORKING_DIR . '/.txt', 'txt'); - $this->assertFileExists(WORKING_DIR . '/.txt'); - $this->assertEquals('txt', FileSystem::readFile(WORKING_DIR . '/.txt')); - unlink(WORKING_DIR . '/.txt'); - } -} diff --git a/tests/SPC/util/ConfigValidatorTest.php b/tests/SPC/util/ConfigValidatorTest.php deleted file mode 100644 index aba611a4..00000000 --- a/tests/SPC/util/ConfigValidatorTest.php +++ /dev/null @@ -1,767 +0,0 @@ - [ - 'type' => 'filelist', - 'url' => 'https://example.com', - 'regex' => '.*', - ], - 'source2' => [ - 'type' => 'git', - 'url' => 'https://example.com', - 'rev' => 'master', - ], - 'source3' => [ - 'type' => 'ghtagtar', - 'repo' => 'aaaa/bbbb', - ], - 'source4' => [ - 'type' => 'ghtar', - 'repo' => 'aaa/bbb', - 'path' => 'path/to/dir', - ], - 'source5' => [ - 'type' => 'ghrel', - 'repo' => 'aaa/bbb', - 'match' => '.*', - ], - 'source6' => [ - 'type' => 'url', - 'url' => 'https://example.com', - ], - 'source7' => [ - 'type' => 'url', - 'url' => 'https://example.com', - 'filename' => 'test.tar.gz', - 'path' => 'test/path', - 'provide-pre-built' => true, - 'license' => [ - 'type' => 'file', - 'path' => 'LICENSE', - ], - ], - 'source8' => [ - 'type' => 'url', - 'url' => 'https://example.com', - 'alt' => [ - 'type' => 'url', - 'url' => 'https://alt.example.com', - ], - ], - 'source9' => [ - 'type' => 'url', - 'url' => 'https://example.com', - 'alt' => false, - 'license' => [ - 'type' => 'text', - 'text' => 'MIT License', - ], - ], - ]; - try { - ConfigValidator::validateSource($good_source); - $this->assertTrue(true); - } catch (ValidationException $e) { - $this->fail($e->getMessage()); - } - } - - public function testValidateSourceBad(): void - { - $bad_source = [ - 'source1' => [ - 'type' => 'filelist', - 'url' => 'https://example.com', - // no regex - ], - 'source2' => [ - 'type' => 'git', - 'url' => true, // not string - 'rev' => 'master', - ], - 'source3' => [ - 'type' => 'ghtagtar', - 'url' => 'aaaa/bbbb', // not repo - ], - 'source4' => [ - 'type' => 'ghtar', - 'repo' => 'aaa/bbb', - 'path' => true, // not string - ], - 'source5' => [ - 'type' => 'ghrel', - 'repo' => 'aaa/bbb', - 'match' => 1, // not string - ], - 'source6' => [ - 'type' => 'url', // no url - ], - 'source7' => [ - 'type' => 'url', - 'url' => 'https://example.com', - 'provide-pre-built' => 'not boolean', // not boolean - ], - 'source8' => [ - 'type' => 'url', - 'url' => 'https://example.com', - 'prefer-stable' => 'not boolean', // not boolean - ], - 'source9' => [ - 'type' => 'url', - 'url' => 'https://example.com', - 'license' => 'not object', // not object - ], - 'source10' => [ - 'type' => 'url', - 'url' => 'https://example.com', - 'license' => [ - 'type' => 'invalid', // invalid type - ], - ], - 'source11' => [ - 'type' => 'url', - 'url' => 'https://example.com', - 'license' => [ - 'type' => 'file', // missing path - ], - ], - 'source12' => [ - 'type' => 'url', - 'url' => 'https://example.com', - 'license' => [ - 'type' => 'text', // missing text - ], - ], - 'source13' => [ - 'type' => 'url', - 'url' => 'https://example.com', - 'alt' => 'not object or boolean', // not object or boolean - ], - ]; - foreach ($bad_source as $name => $src) { - try { - ConfigValidator::validateSource([$name => $src]); - $this->fail("should throw ValidationException for source {$name}"); - } catch (ValidationException) { - $this->assertTrue(true); - } - } - } - - public function testValidateLibsGood(): void - { - $good_libs = [ - 'lib1' => [ - 'source' => 'source1', - ], - 'lib2' => [ - 'source' => 'source2', - 'lib-depends' => [ - 'lib1', - ], - ], - 'lib3' => [ - 'source' => 'source3', - 'lib-suggests' => [ - 'lib1', - ], - ], - 'lib4' => [ - 'source' => 'source4', - 'headers' => [ - 'header1.h', - 'header2.h', - ], - 'headers-windows' => [ - 'windows_header.h', - ], - 'bin-unix' => [ - 'binary1', - 'binary2', - ], - 'frameworks' => [ - 'CoreFoundation', - 'SystemConfiguration', - ], - ], - 'lib5' => [ - 'type' => 'package', - 'source' => 'source5', - 'pkg-configs' => [ - 'pkg1', - 'pkg2', - ], - ], - 'lib6' => [ - 'type' => 'root', - ], - ]; - try { - ConfigValidator::validateLibs($good_libs, ['source1' => [], 'source2' => [], 'source3' => [], 'source4' => [], 'source5' => []]); - $this->assertTrue(true); - } catch (ValidationException $e) { - $this->fail($e->getMessage()); - } - } - - public function testValidateLibsBad(): void - { - // lib.json is broken - try { - ConfigValidator::validateLibs('not array'); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // lib source not exists - try { - ConfigValidator::validateLibs(['lib1' => ['source' => 'source3']], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // lib.json is broken by not assoc array - try { - ConfigValidator::validateLibs(['lib1', 'lib2'], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // lib.json lib is not one of "lib", "package", "root", "target" - try { - ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'type' => 'not one of']], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // lib.json lib if it is "lib" or "package", it must have "source" - try { - ConfigValidator::validateLibs(['lib1' => ['type' => 'lib']], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // lib.json static-libs must be a list - try { - ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'static-libs-windows' => 'not list']], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // lib.json frameworks must be a list - try { - ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'frameworks' => 'not list']], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // source must be string - try { - ConfigValidator::validateLibs(['lib1' => ['source' => true]], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // lib-depends must be list - try { - ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'lib-depends' => ['a' => 'not list']]], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // lib-suggests must be list - try { - ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'lib-suggests' => ['a' => 'not list']]], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // headers must be list - try { - ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'headers' => 'not list']], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - // bin must be list - try { - ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'bin-unix' => 'not list']], ['source1' => [], 'source2' => []]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - } - - public function testValidateExts(): void - { - // Test valid extensions - $valid_exts = [ - 'ext1' => [ - 'type' => 'builtin', - ], - 'ext2' => [ - 'type' => 'external', - 'source' => 'source1', - ], - 'ext3' => [ - 'type' => 'external', - 'source' => 'source2', - 'arg-type' => 'enable', - 'lib-depends' => ['lib1'], - 'lib-suggests' => ['lib2'], - 'ext-depends-windows' => ['ext1'], - 'support' => [ - 'Windows' => 'wip', - 'BSD' => 'wip', - ], - 'notes' => true, - ], - 'ext4' => [ - 'type' => 'external', - 'source' => 'source3', - 'arg-type-unix' => 'with-path', - 'arg-type-windows' => 'with', - ], - ]; - ConfigValidator::validateExts($valid_exts); - - // Test invalid data - $this->expectException(ValidationException::class); - ConfigValidator::validateExts(null); - } - - public function testValidateExtsBad(): void - { - // Test invalid extension type - try { - ConfigValidator::validateExts(['ext1' => ['type' => 'invalid']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test external extension without source - try { - ConfigValidator::validateExts(['ext1' => ['type' => 'external']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test non-object extension - try { - ConfigValidator::validateExts(['ext1' => 'not object']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid source type - try { - ConfigValidator::validateExts(['ext1' => ['type' => 'external', 'source' => true]]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid support - try { - ConfigValidator::validateExts(['ext1' => ['type' => 'builtin', 'support' => 'not object']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid notes - try { - ConfigValidator::validateExts(['ext1' => ['type' => 'builtin', 'notes' => 'not boolean']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid lib-depends - try { - ConfigValidator::validateExts(['ext1' => ['type' => 'builtin', 'lib-depends' => 'not list']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid arg-type - try { - ConfigValidator::validateExts(['ext1' => ['type' => 'builtin', 'arg-type' => 'invalid']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid arg-type with suffix - try { - ConfigValidator::validateExts(['ext1' => ['type' => 'builtin', 'arg-type-unix' => 'invalid']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - } - - public function testValidatePkgs(): void - { - // Test valid packages (all supported types) - $valid_pkgs = [ - 'pkg1' => [ - 'type' => 'url', - 'url' => 'https://example.com/file.tar.gz', - ], - 'pkg2' => [ - 'type' => 'ghrel', - 'repo' => 'owner/repo', - 'match' => 'file.+\.tar\.gz', - ], - 'pkg3' => [ - 'type' => 'custom', - ], - 'pkg4' => [ - 'type' => 'url', - 'url' => 'https://example.com/archive.zip', - 'filename' => 'archive.zip', - 'path' => 'extract/path', - 'extract-files' => [ - 'source/file.exe' => '{pkg_root_path}/bin/file.exe', - 'source/lib.dll' => '{pkg_root_path}/lib/lib.dll', - ], - ], - 'pkg5' => [ - 'type' => 'ghrel', - 'repo' => 'owner/repo', - 'match' => 'release.+\.zip', - 'extract-files' => [ - 'binary' => '{pkg_root_path}/bin/binary', - ], - ], - 'pkg6' => [ - 'type' => 'filelist', - 'url' => 'https://example.com/filelist', - 'regex' => '/href="(?.*\.tar\.gz)"/', - ], - 'pkg7' => [ - 'type' => 'git', - 'url' => 'https://github.com/owner/repo.git', - 'rev' => 'main', - ], - 'pkg8' => [ - 'type' => 'git', - 'url' => 'https://github.com/owner/repo.git', - 'rev' => 'v1.0.0', - 'path' => 'subdir/path', - ], - 'pkg9' => [ - 'type' => 'ghtagtar', - 'repo' => 'owner/repo', - ], - 'pkg10' => [ - 'type' => 'ghtar', - 'repo' => 'owner/repo', - 'path' => 'subdir', - ], - ]; - ConfigValidator::validatePkgs($valid_pkgs); - - // Test invalid data - $this->expectException(ValidationException::class); - ConfigValidator::validatePkgs(null); - } - - public function testValidatePkgsBad(): void - { - // Test invalid package type - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'invalid']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test non-object package - try { - ConfigValidator::validatePkgs(['pkg1' => 'not object']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test filelist type without url - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'filelist', 'regex' => '.*']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test filelist type without regex - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'filelist', 'url' => 'https://example.com']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test git type without url - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'git', 'rev' => 'main']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test git type without rev - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'git', 'url' => 'https://github.com/owner/repo.git']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test ghtagtar type without repo - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'ghtagtar']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test ghtar type without repo - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'ghtar']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test url type without url - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'url']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test url type with non-string url - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'url', 'url' => true]]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test ghrel type without repo - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'ghrel', 'match' => 'pattern']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test ghrel type without match - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'ghrel', 'repo' => 'owner/repo']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test ghrel type with non-string repo - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'ghrel', 'repo' => true, 'match' => 'pattern']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test ghrel type with non-string match - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'ghrel', 'repo' => 'owner/repo', 'match' => 123]]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test git type with non-string path - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'git', 'url' => 'https://github.com/owner/repo.git', 'rev' => 'main', 'path' => 123]]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test url type with non-string filename - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'url', 'url' => 'https://example.com', 'filename' => 123]]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid extract-files (not object) - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'url', 'url' => 'https://example.com', 'extract-files' => 'not object']]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid extract-files mapping (non-string key) - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'url', 'url' => 'https://example.com', 'extract-files' => [123 => 'target']]]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid extract-files mapping (non-string value) - try { - ConfigValidator::validatePkgs(['pkg1' => ['type' => 'url', 'url' => 'https://example.com', 'extract-files' => ['source' => 123]]]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - } - - public function testValidatePreBuilt(): void - { - // Test valid pre-built configurations - $valid_prebuilt = [ - 'basic' => [ - 'repo' => 'static-php/static-php-cli-hosted', - 'match-pattern-linux' => '{name}-{arch}-{os}-{libc}-{libcver}.txz', - ], - 'full' => [ - 'repo' => 'static-php/static-php-cli-hosted', - 'prefer-stable' => true, - 'match-pattern-linux' => '{name}-{arch}-{os}-{libc}-{libcver}.txz', - 'match-pattern-macos' => '{name}-{arch}-{os}.txz', - 'match-pattern-windows' => '{name}-{arch}-{os}.tgz', - ], - 'prefer-stable-false' => [ - 'repo' => 'owner/repo', - 'prefer-stable' => false, - 'match-pattern-macos' => '{name}-{arch}-{os}.tar.gz', - ], - ]; - - foreach ($valid_prebuilt as $name => $config) { - try { - ConfigValidator::validatePreBuilt($config); - $this->assertTrue(true, "Config {$name} should be valid"); - } catch (ValidationException $e) { - $this->fail("Config {$name} should be valid but got: " . $e->getMessage()); - } - } - } - - public function testValidatePreBuiltBad(): void - { - // Test non-array data - try { - ConfigValidator::validatePreBuilt('invalid'); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test missing repo - try { - ConfigValidator::validatePreBuilt(['match-pattern-linux' => '{name}-{arch}-{os}-{libc}-{libcver}.txz']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid repo type - try { - ConfigValidator::validatePreBuilt(['repo' => 123, 'match-pattern-linux' => '{name}-{arch}-{os}-{libc}-{libcver}.txz']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid prefer-stable type - try { - ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'prefer-stable' => 'true', 'match-pattern-linux' => '{name}-{arch}-{os}-{libc}-{libcver}.txz']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test no match patterns - try { - ConfigValidator::validatePreBuilt(['repo' => 'owner/repo']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test invalid match pattern type - try { - ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'match-pattern-linux' => 123]); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test missing {name} placeholder - try { - ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'match-pattern-linux' => '{arch}-{os}-{libc}-{libcver}.txz']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test missing {arch} placeholder - try { - ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'match-pattern-linux' => '{name}-{os}-{libc}-{libcver}.txz']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test missing {os} placeholder - try { - ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'match-pattern-linux' => '{name}-{arch}-{libc}-{libcver}.txz']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test linux pattern missing {libc} placeholder - try { - ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'match-pattern-linux' => '{name}-{arch}-{os}-{libcver}.txz']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - - // Test linux pattern missing {libcver} placeholder - try { - ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'match-pattern-linux' => '{name}-{arch}-{os}-{libc}.txz']); - $this->fail('should throw ValidationException'); - } catch (ValidationException) { - $this->assertTrue(true); - } - } -} diff --git a/tests/SPC/util/DependencyUtilTest.php b/tests/SPC/util/DependencyUtilTest.php deleted file mode 100644 index 468f6eb2..00000000 --- a/tests/SPC/util/DependencyUtilTest.php +++ /dev/null @@ -1,113 +0,0 @@ -originalConfig = [ - 'source' => Config::$source, - 'lib' => Config::$lib, - 'ext' => Config::$ext, - ]; - } - - protected function tearDown(): void - { - // Restore original configuration - Config::$source = $this->originalConfig['source']; - Config::$lib = $this->originalConfig['lib']; - Config::$ext = $this->originalConfig['ext']; - } - - public function testGetExtLibsByDeps(): void - { - // Set up test data - Config::$source = [ - 'test1' => [ - 'type' => 'url', - 'url' => 'https://pecl.php.net/get/APCu', - 'filename' => 'apcu.tgz', - 'license' => [ - 'type' => 'file', - 'path' => 'LICENSE', - ], - ], - ]; - Config::$lib = [ - 'lib-base' => ['type' => 'root'], - 'php' => ['type' => 'root'], - 'libaaa' => [ - 'source' => 'test1', - 'static-libs' => ['libaaa.a'], - 'lib-depends' => ['libbbb', 'libccc'], - 'lib-suggests' => ['libeee'], - ], - 'libbbb' => [ - 'source' => 'test1', - 'static-libs' => ['libbbb.a'], - 'lib-suggests' => ['libccc'], - ], - 'libccc' => [ - 'source' => 'test1', - 'static-libs' => ['libccc.a'], - ], - 'libeee' => [ - 'source' => 'test1', - 'static-libs' => ['libeee.a'], - 'lib-suggests' => ['libfff'], - ], - 'libfff' => [ - 'source' => 'test1', - 'static-libs' => ['libfff.a'], - ], - ]; - Config::$ext = [ - 'ext-a' => [ - 'type' => 'builtin', - 'lib-depends' => ['libaaa'], - 'ext-suggests' => ['ext-b'], - ], - 'ext-b' => [ - 'type' => 'builtin', - 'lib-depends' => ['libeee'], - ], - ]; - - // Test dependency resolution - [$exts, $libs, $not_included] = DependencyUtil::getExtsAndLibs(['ext-a'], include_suggested_exts: true); - $this->assertContains('libbbb', $libs); - $this->assertContains('libccc', $libs); - $this->assertContains('ext-b', $exts); - $this->assertContains('ext-b', $not_included); - - // Test dependency order - $this->assertIsInt($b = array_search('libbbb', $libs)); - $this->assertIsInt($c = array_search('libccc', $libs)); - $this->assertIsInt($a = array_search('libaaa', $libs)); - // libbbb, libaaa - $this->assertTrue($b < $a); - $this->assertTrue($c < $a); - $this->assertTrue($c < $b); - } - - public function testNotExistExtException(): void - { - $this->expectException(WrongUsageException::class); - DependencyUtil::getExtsAndLibs(['sdsd']); - } -} diff --git a/tests/SPC/util/GlobalEnvManagerTest.php b/tests/SPC/util/GlobalEnvManagerTest.php deleted file mode 100644 index 1d9ed108..00000000 --- a/tests/SPC/util/GlobalEnvManagerTest.php +++ /dev/null @@ -1,143 +0,0 @@ -originalEnv = [ - 'BUILD_ROOT_PATH' => getenv('BUILD_ROOT_PATH'), - 'SPC_TARGET' => getenv('SPC_TARGET'), - 'SPC_LIBC' => getenv('SPC_LIBC'), - ]; - // Temporarily set private GlobalEnvManager::$initialized to false (use reflection) - $reflection = new \ReflectionClass(GlobalEnvManager::class); - $property = $reflection->getProperty('initialized'); - $property->setValue(null, false); - } - - protected function tearDown(): void - { - // Restore original environment variables - foreach ($this->originalEnv as $key => $value) { - if ($value === false) { - putenv($key); - } else { - putenv("{$key}={$value}"); - } - } - // Temporarily set private GlobalEnvManager::$initialized to false (use reflection) - $reflection = new \ReflectionClass(GlobalEnvManager::class); - $property = $reflection->getProperty('initialized'); - $property->setValue(null, true); - } - - public function testGetInitializedEnv(): void - { - // Test that getInitializedEnv returns an array - $result = GlobalEnvManager::getInitializedEnv(); - $this->assertIsArray($result); - } - - /** - * @dataProvider envVariableProvider - */ - public function testPutenv(string $envVar): void - { - // Test putenv functionality - GlobalEnvManager::putenv($envVar); - - $env = GlobalEnvManager::getInitializedEnv(); - $this->assertContains($envVar, $env); - $this->assertEquals(explode('=', $envVar, 2)[1], getenv(explode('=', $envVar, 2)[0])); - } - - /** - * @dataProvider pathProvider - */ - public function testAddPathIfNotExistsOnUnix(string $path): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('This test is for Unix systems only'); - } - - $originalPath = getenv('PATH'); - GlobalEnvManager::addPathIfNotExists($path); - - $newPath = getenv('PATH'); - $this->assertStringContainsString($path, $newPath); - } - - /** - * @dataProvider pathProvider - */ - public function testAddPathIfNotExistsWhenPathAlreadyExists(string $path): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('This test is for Unix systems only'); - } - - GlobalEnvManager::addPathIfNotExists($path); - $pathAfterFirstAdd = getenv('PATH'); - - GlobalEnvManager::addPathIfNotExists($path); - $pathAfterSecondAdd = getenv('PATH'); - - // Should not add the same path twice - $this->assertEquals($pathAfterFirstAdd, $pathAfterSecondAdd); - } - - public function testInitWithoutBuildRootPath(): void - { - // Temporarily unset BUILD_ROOT_PATH - putenv('BUILD_ROOT_PATH'); - - $this->expectException(SPCInternalException::class); - GlobalEnvManager::init(); - } - - public function testAfterInit(): void - { - // Set required environment variable - putenv('BUILD_ROOT_PATH=/test/path'); - putenv('SPC_SKIP_TOOLCHAIN_CHECK=true'); - - // Should not throw exception when SPC_SKIP_TOOLCHAIN_CHECK is true - GlobalEnvManager::afterInit(); - - $this->assertTrue(true); // Test passes if no exception is thrown - } - - public function envVariableProvider(): array - { - return [ - 'simple-env' => ['TEST_VAR=test_value'], - 'complex-env' => ['COMPLEX_VAR=complex_value_with_spaces'], - 'numeric-env' => ['NUMERIC_VAR=123'], - 'special-chars-env' => ['SPECIAL_VAR=test@#$%'], - ]; - } - - public function pathProvider(): array - { - return [ - 'simple-path' => ['/test/path'], - 'complex-path' => ['/usr/local/bin'], - 'home-path' => ['/home/user/bin'], - 'root-path' => ['/root/bin'], - ]; - } -} diff --git a/tests/SPC/util/LicenseDumperTest.php b/tests/SPC/util/LicenseDumperTest.php deleted file mode 100644 index ed914296..00000000 --- a/tests/SPC/util/LicenseDumperTest.php +++ /dev/null @@ -1,110 +0,0 @@ - Config::$source, - 'lib' => Config::$lib, - ]; - Config::$lib = [ - 'lib-base' => ['type' => 'root'], - 'php' => ['type' => 'root'], - 'fake_lib' => [ - 'source' => 'fake_lib', - ], - ]; - Config::$source = [ - 'fake_lib' => [ - 'license' => [ - 'type' => 'text', - 'text' => 'license', - ], - ], - ]; - - $dumper = new LicenseDumper(); - $dumper->addLibs(['fake_lib']); - $dumper->dump(self::DIRECTORY); - - $this->assertFileExists(self::DIRECTORY . '/lib_fake_lib_0.txt'); - // restore - Config::$source = $bak['source']; - Config::$lib = $bak['lib']; - } - - public function testDumpWithMultipleLicenses(): void - { - $bak = [ - 'source' => Config::$source, - 'lib' => Config::$lib, - ]; - Config::$lib = [ - 'lib-base' => ['type' => 'root'], - 'php' => ['type' => 'root'], - 'fake_lib' => [ - 'source' => 'fake_lib', - ], - ]; - Config::$source = [ - 'fake_lib' => [ - 'license' => [ - [ - 'type' => 'text', - 'text' => 'license', - ], - [ - 'type' => 'text', - 'text' => 'license', - ], - [ - 'type' => 'text', - 'text' => 'license', - ], - ], - ], - ]; - - $dumper = new LicenseDumper(); - $dumper->addLibs(['fake_lib']); - $dumper->dump(self::DIRECTORY); - - $this->assertFileExists(self::DIRECTORY . '/lib_fake_lib_0.txt'); - $this->assertFileExists(self::DIRECTORY . '/lib_fake_lib_1.txt'); - $this->assertFileExists(self::DIRECTORY . '/lib_fake_lib_2.txt'); - - // restore - Config::$source = $bak['source']; - Config::$lib = $bak['lib']; - } -} diff --git a/tests/SPC/util/PkgConfigUtilTest.php b/tests/SPC/util/PkgConfigUtilTest.php deleted file mode 100644 index 41de6d70..00000000 --- a/tests/SPC/util/PkgConfigUtilTest.php +++ /dev/null @@ -1,210 +0,0 @@ -assertEquals($expectedCflags, $result); - } - - /** - * @dataProvider validPackageProvider - */ - public function testGetLibsArrayWithValidPackage(string $package, string $expectedCflags, array $expectedLibs): void - { - $result = PkgConfigUtil::getLibsArray($package); - $this->assertEquals($expectedLibs, $result); - } - - /** - * @dataProvider invalidPackageProvider - */ - public function testGetCflagsWithInvalidPackage(string $package): void - { - $this->expectException(ExecutionException::class); - PkgConfigUtil::getCflags($package); - } - - /** - * @dataProvider invalidPackageProvider - */ - public function testGetLibsArrayWithInvalidPackage(string $package): void - { - $this->expectException(ExecutionException::class); - PkgConfigUtil::getLibsArray($package); - } - - public static function invalidPackageProvider(): array - { - return [ - 'invalid-package' => ['invalid-package'], - 'empty-string' => [''], - 'non-existent-package' => ['non-existent-package'], - ]; - } - - public static function validPackageProvider(): array - { - return [ - 'libxml2' => ['libxml-2.0', '-I/usr/include/libxml2', ['-lxml2', '']], - 'zlib' => ['zlib', '-I/usr/include', ['-lz', '']], - 'openssl' => ['openssl', '-I/usr/include/openssl', ['-lssl', '-lcrypto', '']], - ]; - } - - /** - * Create a fake pkg-config executable - */ - private static function createFakePkgConfig(): void - { - $pkgConfigScript = self::$fakePkgConfigPath . '/pkg-config'; - - $script = <<<'SCRIPT' -#!/bin/bash - -# Fake pkg-config script for testing -# Shift arguments to get the package name -shift - -case "$1" in - --cflags-only-other) - shift - case "$1" in - libxml-2.0) - echo "-I/usr/include/libxml2" - ;; - zlib) - echo "-I/usr/include" - ;; - openssl) - echo "-I/usr/include/openssl" - ;; - *) - echo "Package '$1' was not found in the pkg-config search path." >&2 - exit 1 - ;; - esac - ;; - --libs-only-l) - shift - case "$1" in - libxml-2.0) - echo "-lxml2" - ;; - zlib) - echo "-lz" - ;; - openssl) - echo "-lssl -lcrypto" - ;; - *) - echo "Package '$1' was not found in the pkg-config search path." >&2 - exit 1 - ;; - esac - ;; - --libs-only-other) - shift - case "$1" in - libxml-2.0) - echo "" - ;; - zlib) - echo "" - ;; - openssl) - echo "" - ;; - *) - echo "Package '$1' was not found in the pkg-config search path." >&2 - exit 1 - ;; - esac - ;; - *) - echo "Usage: pkg-config [OPTION] [PACKAGE]" >&2 - echo "Try 'pkg-config --help' for more information." >&2 - exit 1 - ;; -esac -SCRIPT; - - file_put_contents($pkgConfigScript, $script); - chmod($pkgConfigScript, 0755); - } - - /** - * Remove directory recursively - */ - private static 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)) { - self::removeDirectory($path); - } else { - unlink($path); - } - } - rmdir($dir); - } -} diff --git a/tests/SPC/util/SPCConfigUtilTest.php b/tests/SPC/util/SPCConfigUtilTest.php deleted file mode 100644 index c4b1427a..00000000 --- a/tests/SPC/util/SPCConfigUtilTest.php +++ /dev/null @@ -1,74 +0,0 @@ -assertInstanceOf(SPCConfigUtil::class, new SPCConfigUtil()); - $this->assertInstanceOf(SPCConfigUtil::class, new SPCConfigUtil(BuilderProvider::makeBuilderByInput(new ArgvInput()))); - } - - public function testConfig(): void - { - if (PHP_OS_FAMILY !== 'Linux') { - $this->markTestSkipped('SPCConfigUtil tests are only applicable on Linux.'); - } - // normal - $result = (new SPCConfigUtil())->config(['bcmath']); - $this->assertStringContainsString(BUILD_ROOT_PATH . '/include', $result['cflags']); - $this->assertStringContainsString(BUILD_ROOT_PATH . '/lib', $result['ldflags']); - $this->assertStringContainsString('-lphp', $result['libs']); - - // has cpp - $result = (new SPCConfigUtil())->config(['rar']); - $this->assertStringContainsString(PHP_OS_FAMILY === 'Darwin' ? '-lc++' : '-lstdc++', $result['libs']); - - // has libmimalloc.a in lib dir - // backup first - if (file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { - $bak = file_get_contents(BUILD_LIB_PATH . '/libmimalloc.a'); - @unlink(BUILD_LIB_PATH . '/libmimalloc.a'); - } - file_put_contents(BUILD_LIB_PATH . '/libmimalloc.a', ''); - $result = (new SPCConfigUtil())->config(['bcmath'], ['mimalloc']); - $this->assertStringStartsWith(BUILD_LIB_PATH . '/libmimalloc.a', $result['libs']); - @unlink(BUILD_LIB_PATH . '/libmimalloc.a'); - if (isset($bak)) { - file_put_contents(BUILD_LIB_PATH . '/libmimalloc.a', $bak); - } - } -} diff --git a/tests/SPC/util/SPCTargetTest.php b/tests/SPC/util/SPCTargetTest.php deleted file mode 100644 index 665b4045..00000000 --- a/tests/SPC/util/SPCTargetTest.php +++ /dev/null @@ -1,106 +0,0 @@ -originalEnv = [ - 'SPC_TARGET' => getenv('SPC_TARGET'), - 'SPC_LIBC' => getenv('SPC_LIBC'), - ]; - } - - protected function tearDown(): void - { - // Restore original environment variables - foreach ($this->originalEnv as $key => $value) { - if ($value === false) { - putenv($key); - } else { - putenv("{$key}={$value}"); - } - } - } - - /** - * @dataProvider libcProvider - */ - public function testGetLibc(string $libc, bool $expected): void - { - putenv("SPC_LIBC={$libc}"); - - $result = SPCTarget::getLibc(); - if ($libc === '') { - // When SPC_LIBC is set to empty string, getenv returns empty string, not false - $this->assertEquals('', $result); - } else { - $this->assertEquals($libc, $result); - } - } - - /** - * @dataProvider libcProvider - */ - public function testGetLibcVersion(string $libc): void - { - putenv("SPC_LIBC={$libc}"); - - $result = SPCTarget::getLibcVersion(); - // The actual result depends on the system, but it could be null if libc is not available - $this->assertIsStringOrNull($result); - } - - /** - * @dataProvider targetOSProvider - */ - public function testGetTargetOS(string $target, string $expected): void - { - putenv("SPC_TARGET={$target}"); - - $result = SPCTarget::getTargetOS(); - $this->assertEquals($expected, $result); - } - - public function testLibcListConstant(): void - { - $this->assertIsArray(SPCTarget::LIBC_LIST); - $this->assertContains('musl', SPCTarget::LIBC_LIST); - $this->assertContains('glibc', SPCTarget::LIBC_LIST); - } - - public function libcProvider(): array - { - return [ - 'musl' => ['musl', true], - 'glibc' => ['glibc', false], - 'empty' => ['', false], - ]; - } - - public function targetOSProvider(): array - { - return [ - 'linux-target' => ['native-linux', 'Linux'], - 'macos-target' => ['native-macos', 'Darwin'], - 'windows-target' => ['native-windows', 'Windows'], - 'empty-target' => ['', PHP_OS_FAMILY], - ]; - } - - private function assertIsStringOrNull($value): void - { - $this->assertTrue(is_string($value) || is_null($value), 'Value must be string or null'); - } -} diff --git a/tests/SPC/util/TestBase.php b/tests/SPC/util/TestBase.php deleted file mode 100644 index fd82ccfb..00000000 --- a/tests/SPC/util/TestBase.php +++ /dev/null @@ -1,100 +0,0 @@ -suppressOutput(); - } - - protected function tearDown(): void - { - $this->restoreOutput(); - parent::tearDown(); - } - - /** - * Suppress output during tests - */ - protected function suppressOutput(): void - { - // Start output buffering to capture PHP output - $this->outputBuffer = ob_start(); - } - - /** - * Restore output after tests - */ - protected function restoreOutput(): void - { - // Clean output buffer - if ($this->outputBuffer) { - ob_end_clean(); - } - } - - /** - * Create a UnixShell instance with debug disabled to suppress logs - */ - protected function createUnixShell(): \SPC\util\shell\UnixShell - { - return new \SPC\util\shell\UnixShell(false); - } - - /** - * Create a WindowsCmd instance with debug disabled to suppress logs - */ - protected function createWindowsCmd(): \SPC\util\shell\WindowsCmd - { - return new \SPC\util\shell\WindowsCmd(false); - } - - /** - * Run a test with output suppression - */ - protected function runWithOutputSuppression(callable $callback) - { - $this->suppressOutput(); - try { - return $callback(); - } finally { - $this->restoreOutput(); - } - } - - /** - * Execute a command with output suppression - */ - protected function execWithSuppression(string $command): array - { - $this->suppressOutput(); - try { - exec($command, $output, $returnCode); - return [$returnCode, $output]; - } finally { - $this->restoreOutput(); - } - } - - /** - * Execute a command with output redirected to /dev/null - */ - protected function execSilently(string $command): array - { - $command .= ' 2>/dev/null 1>/dev/null'; - exec($command, $output, $returnCode); - return [$returnCode, $output]; - } -} diff --git a/tests/SPC/util/UnixShellTest.php b/tests/SPC/util/UnixShellTest.php deleted file mode 100644 index f65e5a40..00000000 --- a/tests/SPC/util/UnixShellTest.php +++ /dev/null @@ -1,184 +0,0 @@ -markTestSkipped('This test is for Windows systems only'); - } - - $this->expectException(EnvironmentException::class); - $this->expectExceptionMessage('Windows cannot use UnixShell'); - - new UnixShell(); - } - - /** - * @dataProvider envProvider - */ - public function testSetEnv(array $env): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('This test is for Unix systems only'); - } - - $shell = $this->createUnixShell(); - $result = $shell->setEnv($env); - - $this->assertSame($shell, $result); - foreach ($env as $item) { - if (trim($item) !== '') { - $this->assertStringContainsString($item, $shell->getEnvString()); - } - } - } - - /** - * @dataProvider envProvider - */ - public function testAppendEnv(array $env): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('This test is for Unix systems only'); - } - - $shell = $this->createUnixShell(); - $shell->setEnv(['CFLAGS' => '-O2']); - - $shell->appendEnv($env); - - $this->assertStringContainsString('-O2', $shell->getEnvString()); - foreach ($env as $value) { - if (trim($value) !== '') { - $this->assertStringContainsString($value, $shell->getEnvString()); - } - } - } - - /** - * @dataProvider envProvider - */ - public function testGetEnvString(array $env): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('This test is for Unix systems only'); - } - - $shell = $this->createUnixShell(); - $shell->setEnv($env); - - $envString = $shell->getEnvString(); - - $hasNonEmptyValues = false; - foreach ($env as $key => $value) { - if (trim($value) !== '') { - $this->assertStringContainsString("{$key}=\"{$value}\"", $envString); - $hasNonEmptyValues = true; - } - } - - // If all values are empty, ensure we still have a test assertion - if (!$hasNonEmptyValues) { - $this->assertIsString($envString); - } - } - - public function testGetEnvStringWithEmptyEnv(): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('This test is for Unix systems only'); - } - - $shell = $this->createUnixShell(); - $envString = $shell->getEnvString(); - - $this->assertEquals('', trim($envString)); - } - - /** - * @dataProvider commandProvider - */ - public function testExecWithResult(string $command): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('This test is for Unix systems only'); - } - - $shell = $this->createUnixShell(); - [$code, $output] = $shell->execWithResult($command); - - $this->assertIsInt($code); - $this->assertIsArray($output); - } - - public function testExecWithResultWithLog(): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('This test is for Unix systems only'); - } - - $shell = $this->createUnixShell(); - [$code, $output] = $shell->execWithResult('echo "test"', false); - - $this->assertIsInt($code); - $this->assertIsArray($output); - $this->assertEquals(0, $code); - $this->assertEquals(['test'], $output); - } - - public function testExecWithResultWithCd(): void - { - if (PHP_OS_FAMILY === 'Windows') { - $this->markTestSkipped('This test is for Unix systems only'); - } - - $shell = $this->createUnixShell(); - $shell->cd('/tmp'); - - [$code, $output] = $shell->execWithResult('pwd'); - - $this->assertIsInt($code); - $this->assertEquals(0, $code); - $this->assertIsArray($output); - } - - public static function directoryProvider(): array - { - return [ - 'simple-directory' => ['/test/directory'], - 'home-directory' => ['/home/user'], - 'root-directory' => ['/root'], - 'tmp-directory' => ['/tmp'], - ]; - } - - public static function envProvider(): array - { - return [ - 'simple-env' => [['CFLAGS' => '-O2', 'LDFLAGS' => '-L/usr/lib']], - 'complex-env' => [['CXXFLAGS' => '-std=c++11', 'LIBS' => '-lz -lxml']], - 'empty-env' => [['CFLAGS' => '', 'LDFLAGS' => ' ']], - 'mixed-env' => [['CFLAGS' => '-O2', 'EMPTY_VAR' => '']], - ]; - } - - public static function commandProvider(): array - { - return [ - 'echo-command' => ['echo "test"'], - 'pwd-command' => ['pwd'], - 'ls-command' => ['ls -la'], - ]; - } -} diff --git a/tests/SPC/util/WindowsCmdTest.php b/tests/SPC/util/WindowsCmdTest.php deleted file mode 100644 index fb4ee3f2..00000000 --- a/tests/SPC/util/WindowsCmdTest.php +++ /dev/null @@ -1,68 +0,0 @@ -markTestSkipped('This test is for Unix systems only'); - } - - $this->expectException(SPCInternalException::class); - $this->expectExceptionMessage('Only windows can use WindowsCmd'); - - new WindowsCmd(); - } - - /** - * @dataProvider commandProvider - */ - public function testExecWithResult(string $command): void - { - if (PHP_OS_FAMILY !== 'Windows') { - $this->markTestSkipped('This test is for Windows systems only'); - } - - $cmd = $this->createWindowsCmd(); - [$code, $output] = $cmd->execWithResult($command); - - $this->assertIsInt($code); - $this->assertEquals(0, $code); - $this->assertIsArray($output); - $this->assertNotEmpty($output); - } - - public function testExecWithResultWithLog(): void - { - if (PHP_OS_FAMILY !== 'Windows') { - $this->markTestSkipped('This test is for Windows systems only'); - } - - $cmd = $this->createWindowsCmd(); - [$code, $output] = $cmd->execWithResult('echo test', false); - - $this->assertIsInt($code); - $this->assertIsArray($output); - $this->assertEquals(0, $code); - $this->assertEquals(['test'], $output); - } - - public static function commandProvider(): array - { - return [ - 'echo-command' => ['echo test'], - 'dir-command' => ['dir'], - 'cd-command' => ['cd'], - ]; - } -} diff --git a/tests/StaticPHP/Config/ArtifactConfigTest.php b/tests/StaticPHP/Config/ArtifactConfigTest.php new file mode 100644 index 00000000..dc396488 --- /dev/null +++ b/tests/StaticPHP/Config/ArtifactConfigTest.php @@ -0,0 +1,303 @@ +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); + } +} diff --git a/tests/StaticPHP/Config/ConfigTypeTest.php b/tests/StaticPHP/Config/ConfigTypeTest.php new file mode 100644 index 00000000..0990931e --- /dev/null +++ b/tests/StaticPHP/Config/ConfigTypeTest.php @@ -0,0 +1,196 @@ +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)); + } +} diff --git a/tests/StaticPHP/Config/ConfigValidatorTest.php b/tests/StaticPHP/Config/ConfigValidatorTest.php new file mode 100644 index 00000000..ae5544ae --- /dev/null +++ b/tests/StaticPHP/Config/ConfigValidatorTest.php @@ -0,0 +1,627 @@ +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); + } +} diff --git a/tests/StaticPHP/Config/PackageConfigTest.php b/tests/StaticPHP/Config/PackageConfigTest.php new file mode 100644 index 00000000..4072e39e --- /dev/null +++ b/tests/StaticPHP/Config/PackageConfigTest.php @@ -0,0 +1,434 @@ +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); + } +} diff --git a/tests/StaticPHP/DI/ApplicationContextTest.php b/tests/StaticPHP/DI/ApplicationContextTest.php new file mode 100644 index 00000000..3da63f47 --- /dev/null +++ b/tests/StaticPHP/DI/ApplicationContextTest.php @@ -0,0 +1,433 @@ +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); + } +} diff --git a/tests/StaticPHP/DI/CallbackInvokerTest.php b/tests/StaticPHP/DI/CallbackInvokerTest.php new file mode 100644 index 00000000..751a70eb --- /dev/null +++ b/tests/StaticPHP/DI/CallbackInvokerTest.php @@ -0,0 +1,629 @@ +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); + } +} diff --git a/tests/StaticPHP/Registry/ArtifactLoaderTest.php b/tests/StaticPHP/Registry/ArtifactLoaderTest.php new file mode 100644 index 00000000..75370dfe --- /dev/null +++ b/tests/StaticPHP/Registry/ArtifactLoaderTest.php @@ -0,0 +1,440 @@ +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 = '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); + } +} diff --git a/tests/StaticPHP/Registry/DoctorLoaderTest.php b/tests/StaticPHP/Registry/DoctorLoaderTest.php new file mode 100644 index 00000000..7817a880 --- /dev/null +++ b/tests/StaticPHP/Registry/DoctorLoaderTest.php @@ -0,0 +1,374 @@ +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 = 'assertGreaterThanOrEqual(0, count($items)); + } + + public function testLoadFromPsr4DirSortsItemsByLevel(): void + { + // Create a PSR-4 directory structure + $psr4Dir = $this->tempDir . '/DoctorClasses'; + mkdir($psr4Dir, 0755, true); + + $classContent = '= 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); + } +} diff --git a/tests/StaticPHP/Registry/PackageLoaderTest.php b/tests/StaticPHP/Registry/PackageLoaderTest.php new file mode 100644 index 00000000..7228b5ae --- /dev/null +++ b/tests/StaticPHP/Registry/PackageLoaderTest.php @@ -0,0 +1,550 @@ +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 = '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); + } +} diff --git a/tests/StaticPHP/Registry/RegistryTest.php b/tests/StaticPHP/Registry/RegistryTest.php new file mode 100644 index 00000000..a7dbd876 --- /dev/null +++ b/tests/StaticPHP/Registry/RegistryTest.php @@ -0,0 +1,378 @@ +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, '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, '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); + } +} diff --git a/tests/assets/filelist.gz b/tests/assets/filelist.gz deleted file mode 100644 index fb566ebf..00000000 Binary files a/tests/assets/filelist.gz and /dev/null differ diff --git a/tests/assets/github_api_AOMediaCodec_libavif_releases.json.gz b/tests/assets/github_api_AOMediaCodec_libavif_releases.json.gz deleted file mode 100644 index 31e0b126..00000000 Binary files a/tests/assets/github_api_AOMediaCodec_libavif_releases.json.gz and /dev/null differ diff --git a/tests/bootstrap.php b/tests/bootstrap.php index dcb4316e..ba917332 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,13 +1,9 @@ setLevel(LogLevel::ERROR);