From a151b0a76140b2b8e6741cacfe15f1d0e9a01d86 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 29 Jul 2025 00:56:35 +0800 Subject: [PATCH] Add unit tests for GlobalEnvManager, PkgConfigUtil, SPCTarget, UnixShell, and WindowsCmd --- tests/SPC/builder/BuilderTest.php | 2 +- tests/SPC/util/GlobalEnvManagerTest.php | 135 ++++++++++++++++ tests/SPC/util/PkgConfigUtilTest.php | 206 ++++++++++++++++++++++++ tests/SPC/util/SPCTargetTest.php | 140 ++++++++++++++++ tests/SPC/util/TestBase.php | 100 ++++++++++++ tests/SPC/util/UnixShellTest.php | 184 +++++++++++++++++++++ tests/SPC/util/WindowsCmdTest.php | 68 ++++++++ 7 files changed, 834 insertions(+), 1 deletion(-) create mode 100644 tests/SPC/util/GlobalEnvManagerTest.php create mode 100644 tests/SPC/util/PkgConfigUtilTest.php create mode 100644 tests/SPC/util/SPCTargetTest.php create mode 100644 tests/SPC/util/TestBase.php create mode 100644 tests/SPC/util/UnixShellTest.php create mode 100644 tests/SPC/util/WindowsCmdTest.php diff --git a/tests/SPC/builder/BuilderTest.php b/tests/SPC/builder/BuilderTest.php index b80f4a0d..ac55375c 100644 --- a/tests/SPC/builder/BuilderTest.php +++ b/tests/SPC/builder/BuilderTest.php @@ -103,7 +103,7 @@ class BuilderTest extends TestCase { 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); + $cnt = preg_match('/PHP_VERSION "(\d+\.\d+\.\d+(?:-[^"]+)?)/', file_get_contents($file), $match); if ($cnt !== 0) { $this->assertEquals($match[1], $this->builder->getPHPVersion()); } else { diff --git a/tests/SPC/util/GlobalEnvManagerTest.php b/tests/SPC/util/GlobalEnvManagerTest.php new file mode 100644 index 00000000..28d60d34 --- /dev/null +++ b/tests/SPC/util/GlobalEnvManagerTest.php @@ -0,0 +1,135 @@ +originalEnv = [ + 'BUILD_ROOT_PATH' => getenv('BUILD_ROOT_PATH'), + '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}"); + } + } + } + + 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(RuntimeException::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/PkgConfigUtilTest.php b/tests/SPC/util/PkgConfigUtilTest.php new file mode 100644 index 00000000..6d8988fd --- /dev/null +++ b/tests/SPC/util/PkgConfigUtilTest.php @@ -0,0 +1,206 @@ +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(RuntimeException::class); + PkgConfigUtil::getCflags($package); + } + + /** + * @dataProvider invalidPackageProvider + */ + public function testGetLibsArrayWithInvalidPackage(string $package): void + { + $this->expectException(RuntimeException::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/SPCTargetTest.php b/tests/SPC/util/SPCTargetTest.php new file mode 100644 index 00000000..dc003932 --- /dev/null +++ b/tests/SPC/util/SPCTargetTest.php @@ -0,0 +1,140 @@ +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 testIsStatic(string $libc, bool $expected): void + { + putenv("SPC_LIBC={$libc}"); + + $result = SPCTarget::isStatic(); + $this->assertEquals($expected, $result); + } + + /** + * @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); + } + + /** + * @dataProvider invalidTargetProvider + */ + public function testGetTargetOSWithInvalidTarget(string $target): void + { + putenv("SPC_TARGET={$target}"); + + $this->expectException(WrongUsageException::class); + $this->expectExceptionMessage('Cannot parse target.'); + + SPCTarget::getTargetOS(); + } + + 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' => ['linux-x86_64', 'Linux'], + 'macos-target' => ['macos-x86_64', 'Darwin'], + 'windows-target' => ['windows-x86_64', 'Windows'], + 'empty-target' => ['', PHP_OS_FAMILY], + ]; + } + + public function invalidTargetProvider(): array + { + return [ + 'invalid-target' => ['invalid-target'], + 'unknown-target' => ['unknown-target'], + 'mixed-target' => ['mixed-target'], + ]; + } + + 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 new file mode 100644 index 00000000..c7e8b22d --- /dev/null +++ b/tests/SPC/util/TestBase.php @@ -0,0 +1,100 @@ +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\UnixShell + { + return new \SPC\util\UnixShell(false); + } + + /** + * Create a WindowsCmd instance with debug disabled to suppress logs + */ + protected function createWindowsCmd(): \SPC\util\WindowsCmd + { + return new \SPC\util\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 new file mode 100644 index 00000000..0f16ead4 --- /dev/null +++ b/tests/SPC/util/UnixShellTest.php @@ -0,0 +1,184 @@ +markTestSkipped('This test is for Windows systems only'); + } + + $this->expectException(RuntimeException::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 new file mode 100644 index 00000000..7d64d7e3 --- /dev/null +++ b/tests/SPC/util/WindowsCmdTest.php @@ -0,0 +1,68 @@ +markTestSkipped('This test is for Unix systems only'); + } + + $this->expectException(RuntimeException::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'], + ]; + } +}