Add unit tests for GlobalEnvManager, PkgConfigUtil, SPCTarget, UnixShell, and WindowsCmd

This commit is contained in:
crazywhalecc
2025-07-29 00:56:35 +08:00
parent e126d53053
commit a151b0a761
7 changed files with 834 additions and 1 deletions

View File

@@ -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 {

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\util;
use PHPUnit\Framework\TestCase;
use SPC\exception\RuntimeException;
use SPC\util\GlobalEnvManager;
/**
* @internal
*/
final class GlobalEnvManagerTest extends TestCase
{
private array $originalEnv;
protected function setUp(): void
{
// Save original environment variables
$this->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'],
];
}
}

View File

@@ -0,0 +1,206 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\util;
use PHPUnit\Framework\TestCase;
use SPC\exception\RuntimeException;
use SPC\util\PkgConfigUtil;
/**
* @internal
*/
final class PkgConfigUtilTest extends TestCase
{
private static string $originalPath;
private static string $fakePkgConfigPath;
public static function setUpBeforeClass(): void
{
parent::setUpBeforeClass();
// Save original PATH
self::$originalPath = getenv('PATH');
// Create fake pkg-config directory
self::$fakePkgConfigPath = sys_get_temp_dir() . '/fake-pkg-config-' . uniqid();
mkdir(self::$fakePkgConfigPath, 0755, true);
// Create fake pkg-config executable
self::createFakePkgConfig();
// Add fake pkg-config to PATH
putenv('PATH=' . self::$fakePkgConfigPath . ':' . self::$originalPath);
}
public static function tearDownAfterClass(): void
{
// Restore original PATH
putenv('PATH=' . self::$originalPath);
// Clean up fake pkg-config
if (is_dir(self::$fakePkgConfigPath)) {
self::removeDirectory(self::$fakePkgConfigPath);
}
parent::tearDownAfterClass();
}
/**
* @dataProvider validPackageProvider
*/
public function testGetCflagsWithValidPackage(string $package, string $expectedCflags): void
{
$result = PkgConfigUtil::getCflags($package);
$this->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);
}
}

View File

@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\util;
use SPC\exception\WrongUsageException;
use SPC\util\SPCTarget;
/**
* @internal
*/
final class SPCTargetTest extends TestBase
{
private array $originalEnv;
protected function setUp(): void
{
// Save original environment variables
$this->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');
}
}

100
tests/SPC/util/TestBase.php Normal file
View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\util;
use PHPUnit\Framework\TestCase;
/**
* Base test class for util tests with output suppression
*/
abstract class TestBase extends TestCase
{
protected $outputBuffer;
protected function setUp(): void
{
parent::setUp();
$this->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];
}
}

View File

@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\util;
use SPC\exception\RuntimeException;
use SPC\util\UnixShell;
/**
* @internal
*/
final class UnixShellTest extends TestBase
{
public function testConstructorOnWindows(): void
{
if (PHP_OS_FAMILY !== 'Windows') {
$this->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'],
];
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\util;
use SPC\exception\RuntimeException;
use SPC\util\WindowsCmd;
/**
* @internal
*/
final class WindowsCmdTest extends TestBase
{
public function testConstructorOnUnix(): void
{
if (PHP_OS_FAMILY === 'Windows') {
$this->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'],
];
}
}