Merge pull request #835 from crazywhalecc/chore/test-and-validate

Chore: PHPUnit test & docs & PHPDoc for vendor mode
This commit is contained in:
Jerry Ma
2025-07-29 11:08:53 +08:00
committed by GitHub
parent 6c3ff7da32
commit fafe7d5778
27 changed files with 2112 additions and 186 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

@@ -44,6 +44,35 @@ class ConfigValidatorTest extends TestCase
'type' => 'url',
'url' => 'https://example.com',
],
'source7' => [
'type' => 'url',
'url' => 'https://example.com',
'filename' => 'test.tar.gz',
'path' => 'test/path',
'provide-pre-built' => true,
'prefer-stable' => false,
'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);
@@ -83,6 +112,47 @@ class ConfigValidatorTest extends TestCase
'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 {
@@ -112,9 +182,38 @@ class ConfigValidatorTest extends TestCase
'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' => []]);
ConfigValidator::validateLibs($good_libs, ['source1' => [], 'source2' => [], 'source3' => [], 'source4' => [], 'source5' => []]);
$this->assertTrue(true);
} catch (ValidationException $e) {
$this->fail($e->getMessage());
@@ -193,6 +292,20 @@ class ConfigValidatorTest extends TestCase
} 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);
}
}
/**
@@ -200,15 +313,459 @@ class ConfigValidatorTest extends TestCase
*/
public function testValidateExts(): void
{
ConfigValidator::validateExts([]);
// 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
{
ConfigValidator::validatePkgs([]);
// 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="(?<file>.*\.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);
}
}
}

View File

@@ -14,15 +14,29 @@ use SPC\util\DependencyUtil;
*/
final class DependencyUtilTest extends TestCase
{
public function testGetExtLibsByDeps(): void
private array $originalConfig;
protected function setUp(): void
{
// setup
$bak = [
// Save original configuration
$this->originalConfig = [
'source' => Config::$source,
'lib' => Config::$lib,
'ext' => Config::$ext,
];
// example
}
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',
@@ -73,14 +87,15 @@ final class DependencyUtilTest extends TestCase
'lib-depends' => ['libeee'],
],
];
// test getExtLibsByDeps (notmal test with ext-depends and lib-depends)
// 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 dep order
// Test dependency order
$this->assertIsInt($b = array_search('libbbb', $libs));
$this->assertIsInt($c = array_search('libccc', $libs));
$this->assertIsInt($a = array_search('libaaa', $libs));
@@ -88,10 +103,6 @@ final class DependencyUtilTest extends TestCase
$this->assertTrue($b < $a);
$this->assertTrue($c < $a);
$this->assertTrue($c < $b);
// restore
Config::$source = $bak['source'];
Config::$lib = $bak['lib'];
Config::$ext = $bak['ext'];
}
public function testNotExistExtException(): void

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'],
];
}
}