Add v3 artifact test

This commit is contained in:
crazywhalecc
2026-04-12 13:29:06 +08:00
parent 661b0fe887
commit 4671be623b
4 changed files with 1878 additions and 0 deletions

View File

@@ -0,0 +1,548 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Artifact;
use PHPUnit\Framework\TestCase;
use StaticPHP\Artifact\Artifact;
use StaticPHP\Artifact\ArtifactCache;
use StaticPHP\Artifact\Downloader\DownloadResult;
use StaticPHP\Exception\SPCInternalException;
/**
* @internal
*/
class ArtifactCacheTest extends TestCase
{
private string $tempDir;
private string $cacheFile;
protected function setUp(): void
{
parent::setUp();
$this->tempDir = sys_get_temp_dir() . '/artifact_cache_test_' . uniqid();
mkdir($this->tempDir, 0755, true);
$this->cacheFile = $this->tempDir . '/.cache.json';
}
protected function tearDown(): void
{
parent::tearDown();
if (is_dir($this->tempDir)) {
$this->removeDirectory($this->tempDir);
}
}
// ==================== Constructor ====================
public function testConstructorCreatesFileWhenNotExists(): void
{
$this->assertFalse(file_exists($this->cacheFile));
new ArtifactCache($this->cacheFile);
$this->assertTrue(file_exists($this->cacheFile));
$this->assertSame('[]', file_get_contents($this->cacheFile));
}
public function testConstructorReadsExistingCacheFile(): void
{
$existing = ['openssl' => ['source' => null, 'binary' => []]];
file_put_contents($this->cacheFile, json_encode($existing));
$cache = new ArtifactCache($this->cacheFile);
$this->assertSame([], $cache->getAllBinaryInfo('openssl'));
}
public function testConstructorHandlesEmptyExistingFile(): void
{
file_put_contents($this->cacheFile, '');
$cache = new ArtifactCache($this->cacheFile);
$this->assertEmpty($cache->getCachedArtifactNames());
}
// ==================== isSourceDownloaded ====================
public function testIsSourceDownloadedReturnsFalseWhenNotCached(): void
{
$cache = new ArtifactCache($this->cacheFile);
$this->assertFalse($cache->isSourceDownloaded('non-existent'));
}
public function testIsSourceDownloadedReturnsFalseWhenCacheEntryHasNullSource(): void
{
file_put_contents($this->cacheFile, json_encode([
'my-pkg' => ['source' => null, 'binary' => []],
]));
$cache = new ArtifactCache($this->cacheFile);
$this->assertFalse($cache->isSourceDownloaded('my-pkg'));
}
public function testIsSourceDownloadedReturnsTrueForLocalTypeWhenDirExists(): void
{
$localDir = $this->tempDir . '/local-source';
mkdir($localDir, 0755, true);
$this->writeCacheData([
'my-pkg' => [
'source' => [
'lock_type' => 'source',
'cache_type' => 'local',
'dirname' => $localDir,
'extract' => null,
'hash' => null,
'time' => time(),
],
'binary' => [],
],
]);
$cache = new ArtifactCache($this->cacheFile);
$this->assertTrue($cache->isSourceDownloaded('my-pkg'));
}
public function testIsSourceDownloadedReturnsFalseForLocalTypeWhenDirNotExists(): void
{
$this->writeCacheData([
'my-pkg' => [
'source' => [
'lock_type' => 'source',
'cache_type' => 'local',
'dirname' => '/non/existent/path/xyz',
'extract' => null,
'hash' => null,
'time' => time(),
],
'binary' => [],
],
]);
$cache = new ArtifactCache($this->cacheFile);
$this->assertFalse($cache->isSourceDownloaded('my-pkg'));
}
public function testIsSourceDownloadedReturnsFalseForArchiveTypeWhenFileNotExists(): void
{
$this->writeCacheData([
'my-pkg' => [
'source' => [
'lock_type' => 'source',
'cache_type' => 'archive',
'filename' => 'non-existent-file.tar.gz',
'extract' => null,
'hash' => 'abc123',
'time' => time(),
],
'binary' => [],
],
]);
$cache = new ArtifactCache($this->cacheFile);
$this->assertFalse($cache->isSourceDownloaded('my-pkg'));
}
// ==================== isBinaryDownloaded ====================
public function testIsBinaryDownloadedReturnsFalseWhenNotCached(): void
{
$cache = new ArtifactCache($this->cacheFile);
$this->assertFalse($cache->isBinaryDownloaded('non-existent', 'linux-x86_64'));
}
public function testIsBinaryDownloadedReturnsFalseWhenPlatformNotCached(): void
{
$this->writeCacheData([
'my-pkg' => ['source' => null, 'binary' => []],
]);
$cache = new ArtifactCache($this->cacheFile);
$this->assertFalse($cache->isBinaryDownloaded('my-pkg', 'linux-x86_64'));
}
public function testIsBinaryDownloadedReturnsTrueForLocalTypeWhenDirExists(): void
{
$localDir = $this->tempDir . '/local-binary';
mkdir($localDir, 0755, true);
$this->writeCacheData([
'my-pkg' => [
'source' => null,
'binary' => [
'linux-x86_64' => [
'lock_type' => 'binary',
'cache_type' => 'local',
'dirname' => $localDir,
'extract' => null,
'hash' => null,
'time' => time(),
],
],
],
]);
$cache = new ArtifactCache($this->cacheFile);
$this->assertTrue($cache->isBinaryDownloaded('my-pkg', 'linux-x86_64'));
}
// ==================== lock ====================
public function testLockWithLocalSourceType(): void
{
$localDir = $this->tempDir . '/local-pkg';
mkdir($localDir, 0755, true);
$cache = new ArtifactCache($this->cacheFile);
$downloadResult = DownloadResult::local($localDir, [], null, '1.0.0');
$cache->lock('my-pkg', 'source', $downloadResult);
$info = $cache->getSourceInfo('my-pkg');
$this->assertNotNull($info);
$this->assertSame('source', $info['lock_type']);
$this->assertSame('local', $info['cache_type']);
$this->assertSame($localDir, $info['dirname']);
}
public function testLockWithLocalBinaryTypePersistsCorrectPlatform(): void
{
$localDir = $this->tempDir . '/local-bin';
mkdir($localDir, 0755, true);
$cache = new ArtifactCache($this->cacheFile);
$downloadResult = DownloadResult::local($localDir, [], null, '1.0.0');
$cache->lock('my-pkg', 'binary', $downloadResult, 'linux-x86_64');
$info = $cache->getBinaryInfo('my-pkg', 'linux-x86_64');
$this->assertNotNull($info);
$this->assertSame('binary', $info['lock_type']);
$this->assertSame('linux-x86_64', $info['platform']);
}
public function testLockWithBinaryTypeThrowsWhenPlatformIsNull(): void
{
$localDir = $this->tempDir . '/local-bin2';
mkdir($localDir, 0755, true);
$cache = new ArtifactCache($this->cacheFile);
$downloadResult = DownloadResult::local($localDir, [], null);
$this->expectException(SPCInternalException::class);
$cache->lock('my-pkg', 'binary', $downloadResult, null);
}
public function testLockPersistsCacheToFile(): void
{
$localDir = $this->tempDir . '/persist-test';
mkdir($localDir, 0755, true);
$cache = new ArtifactCache($this->cacheFile);
$downloadResult = DownloadResult::local($localDir, []);
$cache->lock('my-pkg', 'source', $downloadResult);
// Read file contents to verify persisted
$persisted = json_decode(file_get_contents($this->cacheFile), true);
$this->assertArrayHasKey('my-pkg', $persisted);
$this->assertNotNull($persisted['my-pkg']['source']);
}
// ==================== getSourceInfo ====================
public function testGetSourceInfoReturnsNullWhenNotCached(): void
{
$cache = new ArtifactCache($this->cacheFile);
$this->assertNull($cache->getSourceInfo('non-existent'));
}
public function testGetSourceInfoReturnsNullWhenSourceIsNull(): void
{
$this->writeCacheData(['my-pkg' => ['source' => null, 'binary' => []]]);
$cache = new ArtifactCache($this->cacheFile);
$this->assertNull($cache->getSourceInfo('my-pkg'));
}
public function testGetSourceInfoReturnsData(): void
{
$this->writeCacheData([
'my-pkg' => [
'source' => ['lock_type' => 'source', 'cache_type' => 'local', 'dirname' => '/tmp', 'extract' => null, 'hash' => null, 'time' => 0],
'binary' => [],
],
]);
$cache = new ArtifactCache($this->cacheFile);
$info = $cache->getSourceInfo('my-pkg');
$this->assertIsArray($info);
$this->assertSame('local', $info['cache_type']);
}
// ==================== getBinaryInfo ====================
public function testGetBinaryInfoReturnsNullWhenNotCached(): void
{
$cache = new ArtifactCache($this->cacheFile);
$this->assertNull($cache->getBinaryInfo('non-existent', 'linux-x86_64'));
}
public function testGetBinaryInfoReturnsDataForPlatform(): void
{
$this->writeCacheData([
'my-pkg' => [
'source' => null,
'binary' => [
'linux-x86_64' => ['lock_type' => 'binary', 'cache_type' => 'local', 'dirname' => '/tmp', 'extract' => null, 'hash' => null, 'time' => 0],
],
],
]);
$cache = new ArtifactCache($this->cacheFile);
$info = $cache->getBinaryInfo('my-pkg', 'linux-x86_64');
$this->assertIsArray($info);
$this->assertSame('local', $info['cache_type']);
}
public function testGetBinaryInfoReturnsNullForDifferentPlatform(): void
{
$this->writeCacheData([
'my-pkg' => [
'source' => null,
'binary' => [
'linux-x86_64' => ['lock_type' => 'binary', 'cache_type' => 'local', 'dirname' => '/tmp', 'extract' => null, 'hash' => null, 'time' => 0],
],
],
]);
$cache = new ArtifactCache($this->cacheFile);
$this->assertNull($cache->getBinaryInfo('my-pkg', 'macos-aarch64'));
}
// ==================== getAllBinaryInfo ====================
public function testGetAllBinaryInfoReturnsEmptyWhenNone(): void
{
$cache = new ArtifactCache($this->cacheFile);
$this->assertSame([], $cache->getAllBinaryInfo('non-existent'));
}
public function testGetAllBinaryInfoReturnsAllPlatforms(): void
{
$this->writeCacheData([
'my-pkg' => [
'source' => null,
'binary' => [
'linux-x86_64' => ['lock_type' => 'binary', 'cache_type' => 'local', 'dirname' => '/tmp', 'extract' => null, 'hash' => null, 'time' => 0],
'macos-aarch64' => ['lock_type' => 'binary', 'cache_type' => 'local', 'dirname' => '/tmp', 'extract' => null, 'hash' => null, 'time' => 0],
],
],
]);
$cache = new ArtifactCache($this->cacheFile);
$all = $cache->getAllBinaryInfo('my-pkg');
$this->assertCount(2, $all);
$this->assertArrayHasKey('linux-x86_64', $all);
$this->assertArrayHasKey('macos-aarch64', $all);
}
// ==================== getCacheFullPath ====================
public function testGetCacheFullPathForArchiveType(): void
{
$cache = new ArtifactCache($this->cacheFile);
$info = ['cache_type' => 'archive', 'filename' => 'openssl-3.0.tar.gz'];
$expected = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . 'openssl-3.0.tar.gz';
$this->assertSame($expected, $cache->getCacheFullPath($info));
}
public function testGetCacheFullPathForGitType(): void
{
$cache = new ArtifactCache($this->cacheFile);
$info = ['cache_type' => 'git', 'dirname' => 'my-repo'];
$expected = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . 'my-repo';
$this->assertSame($expected, $cache->getCacheFullPath($info));
}
public function testGetCacheFullPathForLocalType(): void
{
$cache = new ArtifactCache($this->cacheFile);
$info = ['cache_type' => 'local', 'dirname' => '/absolute/path/to/dir'];
$this->assertSame('/absolute/path/to/dir', $cache->getCacheFullPath($info));
}
public function testGetCacheFullPathForFileType(): void
{
$cache = new ArtifactCache($this->cacheFile);
$info = ['cache_type' => 'file', 'filename' => 'some-tool.exe'];
$expected = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . 'some-tool.exe';
$this->assertSame($expected, $cache->getCacheFullPath($info));
}
public function testGetCacheFullPathThrowsForUnknownType(): void
{
$cache = new ArtifactCache($this->cacheFile);
$info = ['cache_type' => 'unknown-type'];
$this->expectException(SPCInternalException::class);
$cache->getCacheFullPath($info);
}
// ==================== removeSource ====================
public function testRemoveSourceIsNoOpWhenNotCached(): void
{
$cache = new ArtifactCache($this->cacheFile);
// Should not throw
$cache->removeSource('non-existent');
$this->assertNull($cache->getSourceInfo('non-existent'));
}
public function testRemoveSourceRemovesCacheEntry(): void
{
$this->writeCacheData([
'my-pkg' => [
'source' => ['lock_type' => 'source', 'cache_type' => 'local', 'dirname' => '/tmp', 'extract' => null, 'hash' => null, 'time' => 0],
'binary' => [],
],
]);
$cache = new ArtifactCache($this->cacheFile);
$cache->removeSource('my-pkg');
$this->assertNull($cache->getSourceInfo('my-pkg'));
}
public function testRemoveSourcePersistsToFile(): void
{
$this->writeCacheData([
'my-pkg' => [
'source' => ['lock_type' => 'source', 'cache_type' => 'local', 'dirname' => '/tmp', 'extract' => null, 'hash' => null, 'time' => 0],
'binary' => [],
],
]);
$cache = new ArtifactCache($this->cacheFile);
$cache->removeSource('my-pkg');
$persisted = json_decode(file_get_contents($this->cacheFile), true);
$this->assertNull($persisted['my-pkg']['source']);
}
// ==================== removeBinary ====================
public function testRemoveBinaryIsNoOpWhenNotCached(): void
{
$cache = new ArtifactCache($this->cacheFile);
// Should not throw
$cache->removeBinary('non-existent', 'linux-x86_64');
$this->assertNull($cache->getBinaryInfo('non-existent', 'linux-x86_64'));
}
public function testRemoveBinaryRemovesPlatformEntry(): void
{
$this->writeCacheData([
'my-pkg' => [
'source' => null,
'binary' => [
'linux-x86_64' => ['lock_type' => 'binary', 'cache_type' => 'local', 'dirname' => '/tmp', 'extract' => null, 'hash' => null, 'time' => 0],
'macos-aarch64' => ['lock_type' => 'binary', 'cache_type' => 'local', 'dirname' => '/tmp', 'extract' => null, 'hash' => null, 'time' => 0],
],
],
]);
$cache = new ArtifactCache($this->cacheFile);
$cache->removeBinary('my-pkg', 'linux-x86_64');
$this->assertNull($cache->getBinaryInfo('my-pkg', 'linux-x86_64'));
// Other platform should still be there
$this->assertNotNull($cache->getBinaryInfo('my-pkg', 'macos-aarch64'));
}
// ==================== getCachedArtifactNames ====================
public function testGetCachedArtifactNamesReturnsEmptyWhenNoCacheFile(): void
{
$cache = new ArtifactCache($this->cacheFile);
$this->assertSame([], $cache->getCachedArtifactNames());
}
public function testGetCachedArtifactNamesReturnsAllNames(): void
{
$this->writeCacheData([
'openssl' => ['source' => null, 'binary' => []],
'zlib' => ['source' => null, 'binary' => []],
'brotli' => ['source' => null, 'binary' => []],
]);
$cache = new ArtifactCache($this->cacheFile);
$names = $cache->getCachedArtifactNames();
$this->assertCount(3, $names);
$this->assertContains('openssl', $names);
$this->assertContains('zlib', $names);
$this->assertContains('brotli', $names);
}
// ==================== save ====================
public function testSavePersistsInMemoryCacheToFile(): void
{
$localDir = $this->tempDir . '/save-test-dir';
mkdir($localDir, 0755, true);
$cache = new ArtifactCache($this->cacheFile);
// Lock an artifact so cache has data
$downloadResult = DownloadResult::local($localDir, []);
$cache->lock('my-pkg', 'source', $downloadResult);
// Overwrite cache file to simulate external change
file_put_contents($this->cacheFile, json_encode([]));
// Save should re-write in-memory state
$cache->save();
$persisted = json_decode(file_get_contents($this->cacheFile), true);
$this->assertArrayHasKey('my-pkg', $persisted);
}
// ==================== Helpers ====================
private function writeCacheData(array $data): void
{
file_put_contents($this->cacheFile, json_encode($data));
}
private function removeDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . '/' . $file;
if (is_dir($path)) {
$this->removeDirectory($path);
} else {
unlink($path);
}
}
rmdir($dir);
}
}

View File

@@ -0,0 +1,351 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Artifact;
use PHPUnit\Framework\TestCase;
use StaticPHP\Artifact\Artifact;
use StaticPHP\Artifact\ArtifactDownloader;
use StaticPHP\Config\ArtifactConfig;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Registry\ArtifactLoader;
/**
* @internal
*/
class ArtifactDownloaderTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
// Reset ArtifactConfig and ArtifactLoader static state
$reflection = new \ReflectionClass(ArtifactConfig::class);
$property = $reflection->getProperty('artifact_configs');
$property->setAccessible(true);
$property->setValue(null, []);
$loaderReflection = new \ReflectionClass(ArtifactLoader::class);
$loaderProperty = $loaderReflection->getProperty('artifacts');
$loaderProperty->setAccessible(true);
$loaderProperty->setValue(null, null);
}
protected function tearDown(): void
{
parent::tearDown();
$reflection = new \ReflectionClass(ArtifactConfig::class);
$property = $reflection->getProperty('artifact_configs');
$property->setAccessible(true);
$property->setValue(null, []);
$loaderReflection = new \ReflectionClass(ArtifactLoader::class);
$loaderProperty = $loaderReflection->getProperty('artifacts');
$loaderProperty->setAccessible(true);
$loaderProperty->setValue(null, null);
}
// ==================== DOWNLOADERS constant ====================
public function testDownloadersConstantHasExpectedKeys(): void
{
$expectedKeys = ['bitbuckettag', 'filelist', 'git', 'ghrel', 'ghtar', 'ghtagtar', 'local', 'pie', 'pecl', 'url', 'php-release', 'hosted'];
foreach ($expectedKeys as $key) {
$this->assertArrayHasKey($key, ArtifactDownloader::DOWNLOADERS, "Missing downloader key: {$key}");
}
}
// ==================== Constructor options ====================
public function testConstructWithDefaultOptions(): void
{
$downloader = new ArtifactDownloader([], false);
$this->assertSame(0, $downloader->getRetry());
$this->assertFalse($downloader->interactive);
$this->assertEmpty($downloader->getArtifacts());
}
public function testConstructWithParallelOption(): void
{
$downloader = new ArtifactDownloader(['parallel' => 4], false);
// parallel is internal but setParallel/getArtifacts reveals behavior; check via setParallel chainability
// Indirect verification: setParallel with same value returns $this
$this->assertSame($downloader, $downloader->setParallel(4));
}
public function testConstructWithRetryOption(): void
{
$downloader = new ArtifactDownloader(['retry' => 3], false);
$this->assertSame(3, $downloader->getRetry());
}
public function testConstructWithNegativeRetryClampedToZero(): void
{
$downloader = new ArtifactDownloader(['retry' => -5], false);
$this->assertSame(0, $downloader->getRetry());
}
public function testConstructWithPreferSourceBoolOption(): void
{
// prefer-source=true sets default to FETCH_PREFER_SOURCE (0)
$downloader = new ArtifactDownloader(['prefer-source' => true], false);
$this->assertSame(0, $downloader->getRetry()); // sanity check, object created fine
}
public function testConstructWithPreferBinaryBoolOption(): void
{
$downloader = new ArtifactDownloader(['prefer-binary' => true], false);
$this->assertNotNull($downloader);
}
public function testConstructWithPreferPreBuiltBoolOption(): void
{
$downloader = new ArtifactDownloader(['prefer-pre-built' => true], false);
$this->assertNotNull($downloader);
}
public function testConstructWithSourceOnlyBoolOption(): void
{
$downloader = new ArtifactDownloader(['source-only' => true], false);
$this->assertNotNull($downloader);
}
public function testConstructWithBinaryOnlyBoolOption(): void
{
$downloader = new ArtifactDownloader(['binary-only' => true], false);
$this->assertNotNull($downloader);
}
public function testConstructWithIgnoreCacheBoolOption(): void
{
$downloader = new ArtifactDownloader(['ignore-cache' => true], false);
$this->assertNotNull($downloader);
}
public function testConstructWithIgnoreCacheStringOptionParsesNames(): void
{
$downloader = new ArtifactDownloader(['ignore-cache' => 'openssl,zlib'], false);
$this->assertNotNull($downloader);
}
public function testConstructWithIgnoreCacheSourcesBackwardCompat(): void
{
$downloader = new ArtifactDownloader(['ignore-cache-sources' => true], false);
$this->assertNotNull($downloader);
}
public function testConstructWithCustomUrlOptionAddsToIgnoreCache(): void
{
$downloader = new ArtifactDownloader(
['custom-url' => ['openssl:https://custom.example.com/openssl.tar.gz']],
false
);
$this->assertNotNull($downloader);
}
public function testConstructWithCustomGitOption(): void
{
$downloader = new ArtifactDownloader(
['custom-git' => ['php-src:master:https://github.com/php/php-src.git']],
false
);
$this->assertNotNull($downloader);
}
public function testConstructWithCustomLocalOption(): void
{
$downloader = new ArtifactDownloader(
['custom-local' => ['my-lib:/tmp/my-lib-source']],
false
);
$this->assertNotNull($downloader);
}
public function testConstructWithNoAltOption(): void
{
$downloader = new ArtifactDownloader(['no-alt' => true], false);
$this->assertNotNull($downloader);
}
// ==================== getRetry ====================
public function testGetRetryDefaultsToZero(): void
{
$downloader = new ArtifactDownloader([], false);
$this->assertSame(0, $downloader->getRetry());
}
public function testGetRetryReturnsConfiguredValue(): void
{
$downloader = new ArtifactDownloader(['retry' => 5], false);
$this->assertSame(5, $downloader->getRetry());
}
// ==================== getOption ====================
public function testGetOptionReturnsConfiguredValue(): void
{
$downloader = new ArtifactDownloader(['retry' => 2, 'parallel' => 3], false);
$this->assertSame(2, $downloader->getOption('retry'));
$this->assertSame(3, $downloader->getOption('parallel'));
}
public function testGetOptionReturnsDefaultWhenNotSet(): void
{
$downloader = new ArtifactDownloader([], false);
$this->assertNull($downloader->getOption('non-existent'));
$this->assertSame('default-val', $downloader->getOption('non-existent', 'default-val'));
}
// ==================== getArtifacts ====================
public function testGetArtifactsReturnsEmptyInitially(): void
{
$downloader = new ArtifactDownloader([], false);
$this->assertSame([], $downloader->getArtifacts());
}
// ==================== add ====================
public function testAddWithArtifactObjectAddsToList(): void
{
$downloader = new ArtifactDownloader([], false);
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$downloader->add($artifact);
$artifacts = $downloader->getArtifacts();
$this->assertArrayHasKey('my-pkg', $artifacts);
$this->assertSame($artifact, $artifacts['my-pkg']);
}
public function testAddReturnsSelf(): void
{
$downloader = new ArtifactDownloader([], false);
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$result = $downloader->add($artifact);
$this->assertSame($downloader, $result);
}
public function testAddDoesNotAddDuplicateArtifact(): void
{
$downloader = new ArtifactDownloader([], false);
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$downloader->add($artifact);
$downloader->add($artifact);
$this->assertCount(1, $downloader->getArtifacts());
}
public function testAddWithStringNameLooksUpFromArtifactLoader(): void
{
$this->injectArtifactConfig('my-lib', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$downloader = new ArtifactDownloader([], false);
$downloader->add('my-lib');
$artifacts = $downloader->getArtifacts();
$this->assertArrayHasKey('my-lib', $artifacts);
}
public function testAddWithStringNameThrowsForNonExistentArtifact(): void
{
$downloader = new ArtifactDownloader([], false);
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage("Artifact 'non-existent' not found");
$downloader->add('non-existent');
}
// ==================== addArtifacts ====================
public function testAddArtifactsAddsMultipleAtOnce(): void
{
$downloader = new ArtifactDownloader([], false);
$a1 = new Artifact('pkg-a', ['source' => ['type' => 'url', 'url' => 'https://example.com/a.tar.gz']]);
$a2 = new Artifact('pkg-b', ['source' => ['type' => 'url', 'url' => 'https://example.com/b.tar.gz']]);
$downloader->addArtifacts([$a1, $a2]);
$this->assertCount(2, $downloader->getArtifacts());
$this->assertArrayHasKey('pkg-a', $downloader->getArtifacts());
$this->assertArrayHasKey('pkg-b', $downloader->getArtifacts());
}
public function testAddArtifactsReturnsSelf(): void
{
$downloader = new ArtifactDownloader([], false);
$result = $downloader->addArtifacts([]);
$this->assertSame($downloader, $result);
}
// ==================== setParallel ====================
public function testSetParallelReturnsSelf(): void
{
$downloader = new ArtifactDownloader([], false);
$result = $downloader->setParallel(3);
$this->assertSame($downloader, $result);
}
public function testSetParallelEnforcesMinimumOfOne(): void
{
$downloader = new ArtifactDownloader([], false);
$downloader->setParallel(0);
// No direct getter for parallel, but verifying it doesn't throw
$this->assertSame($downloader, $downloader->setParallel(0));
}
public function testSetParallelAcceptsNormalValue(): void
{
$downloader = new ArtifactDownloader([], false);
$result = $downloader->setParallel(5);
$this->assertSame($downloader, $result);
}
// ==================== Helpers ====================
private function injectArtifactConfig(string $name, array $config): void
{
$reflection = new \ReflectionClass(ArtifactConfig::class);
$property = $reflection->getProperty('artifact_configs');
$property->setAccessible(true);
$configs = $property->getValue(null) ?? [];
$configs[$name] = $config;
$property->setValue(null, $configs);
}
}

View File

@@ -0,0 +1,229 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Artifact;
use PHPUnit\Framework\TestCase;
use StaticPHP\Artifact\Artifact;
use StaticPHP\Artifact\ArtifactCache;
use StaticPHP\Artifact\ArtifactExtractor;
use StaticPHP\Config\ArtifactConfig;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Registry\ArtifactLoader;
/**
* @internal
*/
class ArtifactExtractorTest extends TestCase
{
private string $tempDir;
private string $cacheFile;
protected function setUp(): void
{
parent::setUp();
$this->tempDir = sys_get_temp_dir() . '/artifact_extractor_test_' . uniqid();
mkdir($this->tempDir, 0755, true);
$this->cacheFile = $this->tempDir . '/.cache.json';
file_put_contents($this->cacheFile, json_encode([]));
$reflection = new \ReflectionClass(ArtifactConfig::class);
$property = $reflection->getProperty('artifact_configs');
$property->setAccessible(true);
$property->setValue(null, []);
$loaderReflection = new \ReflectionClass(ArtifactLoader::class);
$loaderProperty = $loaderReflection->getProperty('artifacts');
$loaderProperty->setAccessible(true);
$loaderProperty->setValue(null, null);
ApplicationContext::reset();
}
protected function tearDown(): void
{
parent::tearDown();
if (is_dir($this->tempDir)) {
$this->removeDirectory($this->tempDir);
}
$reflection = new \ReflectionClass(ArtifactConfig::class);
$property = $reflection->getProperty('artifact_configs');
$property->setAccessible(true);
$property->setValue(null, []);
$loaderReflection = new \ReflectionClass(ArtifactLoader::class);
$loaderProperty = $loaderReflection->getProperty('artifacts');
$loaderProperty->setAccessible(true);
$loaderProperty->setValue(null, null);
ApplicationContext::reset();
}
// ==================== Constructor ====================
public function testConstructorStoresProvidedCache(): void
{
$cache = new ArtifactCache($this->cacheFile);
$extractor = new ArtifactExtractor($cache, false);
// Verify the extractor was created without error; it holds the cache internally
$this->assertInstanceOf(ArtifactExtractor::class, $extractor);
}
public function testConstructorDefaultsInteractiveToTrue(): void
{
$cache = new ArtifactCache($this->cacheFile);
$extractor = new ArtifactExtractor($cache);
$this->assertInstanceOf(ArtifactExtractor::class, $extractor);
}
// ==================== extractForPackages ====================
public function testExtractForPackagesWithEmptyArrayDoesNothing(): void
{
$cache = new ArtifactCache($this->cacheFile);
$extractor = new ArtifactExtractor($cache, false);
// Should complete without exception
$extractor->extractForPackages([]);
$this->assertTrue(true);
}
public function testExtractForPackagesDeduplicatesArtifacts(): void
{
ApplicationContext::initialize();
$artifactConfig = ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']];
$this->injectArtifactConfig('shared-lib', $artifactConfig);
$artifact = new Artifact('shared-lib', $artifactConfig);
// Create two mock packages that share the same artifact
$pkg1 = $this->createMockPackage($artifact);
$pkg2 = $this->createMockPackage($artifact);
$cache = new ArtifactCache($this->cacheFile);
// Partial mock to verify extract is called exactly once despite two packages
$extractor = $this->getMockBuilder(ArtifactExtractor::class)
->setConstructorArgs([$cache, false])
->onlyMethods(['extract'])
->getMock();
$extractor->expects($this->once())
->method('extract')
->with($artifact, false)
->willReturn(SPC_STATUS_ALREADY_EXTRACTED);
$extractor->extractForPackages([$pkg1, $pkg2]);
}
public function testExtractForPackagesSkipsPackagesWithNoArtifact(): void
{
$pkgWithoutArtifact = $this->createMockPackage(null);
$cache = new ArtifactCache($this->cacheFile);
$extractor = $this->getMockBuilder(ArtifactExtractor::class)
->setConstructorArgs([$cache, false])
->onlyMethods(['extract'])
->getMock();
// extract should NOT be called when no artifact
$extractor->expects($this->never())->method('extract');
$extractor->extractForPackages([$pkgWithoutArtifact]);
}
// ==================== extract ====================
public function testExtractReturnsAlreadyExtractedForSecondCall(): void
{
ApplicationContext::initialize();
$artifactConfig = ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']];
$artifact = new Artifact('my-pkg', $artifactConfig);
$cache = $this->createMock(ArtifactCache::class);
$cache->method('getSourceInfo')->willReturn(null);
$cache->method('getBinaryInfo')->willReturn(null);
$cache->method('isBinaryDownloaded')->willReturn(false);
ApplicationContext::set(ArtifactCache::class, $cache);
$extractor = new ArtifactExtractor($cache, false);
// Pre-populate the extracted map for 'my-pkg' via reflection
$reflection = new \ReflectionClass(ArtifactExtractor::class);
$extractedProperty = $reflection->getProperty('extracted');
$extractedProperty->setAccessible(true);
$extractedProperty->setValue($extractor, ['my-pkg' => true]);
$result = $extractor->extract($artifact, false);
$this->assertSame(SPC_STATUS_ALREADY_EXTRACTED, $result);
}
public function testExtractWithStringNameLooksUpFromArtifactLoader(): void
{
ApplicationContext::initialize();
$artifactConfig = ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']];
$this->injectArtifactConfig('my-pkg', $artifactConfig);
$cache = $this->createMock(ArtifactCache::class);
$cache->method('getSourceInfo')->willReturn(null);
$cache->method('getBinaryInfo')->willReturn(null);
$cache->method('isBinaryDownloaded')->willReturn(false);
ApplicationContext::set(ArtifactCache::class, $cache);
$extractor = new ArtifactExtractor($cache, false);
// Pre-populate the extracted map so we don't need actual downloads
$reflection = new \ReflectionClass(ArtifactExtractor::class);
$extractedProperty = $reflection->getProperty('extracted');
$extractedProperty->setAccessible(true);
$extractedProperty->setValue($extractor, ['my-pkg' => true]);
$result = $extractor->extract('my-pkg', false);
$this->assertSame(SPC_STATUS_ALREADY_EXTRACTED, $result);
}
// ==================== Helpers ====================
/**
* Create a mock Package object that returns the given artifact from getArtifact().
*/
private function createMockPackage(?Artifact $artifact): \StaticPHP\Package\Package
{
$mock = $this->createMock(\StaticPHP\Package\Package::class);
$mock->method('getArtifact')->willReturn($artifact);
return $mock;
}
private function injectArtifactConfig(string $name, array $config): void
{
$reflection = new \ReflectionClass(ArtifactConfig::class);
$property = $reflection->getProperty('artifact_configs');
$property->setAccessible(true);
$configs = $property->getValue(null) ?? [];
$configs[$name] = $config;
$property->setValue(null, $configs);
}
private function removeDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . '/' . $file;
if (is_dir($path)) {
$this->removeDirectory($path);
} else {
unlink($path);
}
}
rmdir($dir);
}
}

View File

@@ -0,0 +1,750 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Artifact;
use PHPUnit\Framework\TestCase;
use StaticPHP\Artifact\Artifact;
use StaticPHP\Artifact\ArtifactCache;
use StaticPHP\Config\ArtifactConfig;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Exception\ValidationException;
use StaticPHP\Exception\WrongUsageException;
/**
* @internal
*/
class ArtifactTest extends TestCase
{
private string $tempDir;
protected function setUp(): void
{
parent::setUp();
$this->tempDir = sys_get_temp_dir() . '/artifact_test_' . uniqid();
mkdir($this->tempDir, 0755, true);
// Reset ArtifactConfig static state
$reflection = new \ReflectionClass(ArtifactConfig::class);
$property = $reflection->getProperty('artifact_configs');
$property->setAccessible(true);
$property->setValue(null, []);
// Reset DI container
ApplicationContext::reset();
}
protected function tearDown(): void
{
parent::tearDown();
if (is_dir($this->tempDir)) {
$this->removeDirectory($this->tempDir);
}
$reflection = new \ReflectionClass(ArtifactConfig::class);
$property = $reflection->getProperty('artifact_configs');
$property->setAccessible(true);
$property->setValue(null, []);
ApplicationContext::reset();
}
// ==================== Constants ====================
public function testConstantValues(): void
{
$this->assertSame(0, Artifact::FETCH_PREFER_SOURCE);
$this->assertSame(1, Artifact::FETCH_PREFER_BINARY);
$this->assertSame(2, Artifact::FETCH_ONLY_SOURCE);
$this->assertSame(3, Artifact::FETCH_ONLY_BINARY);
}
// ==================== Constructor ====================
public function testConstructWithInlineConfig(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$this->assertSame('my-pkg', $artifact->getName());
}
public function testConstructFallsBackToArtifactConfig(): void
{
$this->injectArtifactConfig('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$artifact = new Artifact('my-pkg');
$this->assertSame('my-pkg', $artifact->getName());
}
public function testConstructThrowsForNonExistentArtifact(): void
{
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage("Artifact 'non-existent' not found.");
new Artifact('non-existent');
}
// ==================== getName ====================
public function testGetName(): void
{
$artifact = new Artifact('openssl', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$this->assertSame('openssl', $artifact->getName());
}
// ==================== getDownloadConfig ====================
public function testGetDownloadConfigReturnsSection(): void
{
$config = ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'], 'binary' => []];
$artifact = new Artifact('my-pkg', $config);
$this->assertSame(['type' => 'url', 'url' => 'https://example.com/file.tar.gz'], $artifact->getDownloadConfig('source'));
}
public function testGetDownloadConfigReturnsNullForMissingSection(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$this->assertNull($artifact->getDownloadConfig('non-existent'));
}
// ==================== hasSource ====================
public function testHasSourceReturnsTrueWhenConfigHasSource(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$this->assertTrue($artifact->hasSource());
}
public function testHasSourceReturnsFalseWhenNoSource(): void
{
$artifact = new Artifact('my-pkg', ['binary' => []]);
$this->assertFalse($artifact->hasSource());
}
public function testHasSourceReturnsTrueWithCustomCallback(): void
{
$artifact = new Artifact('my-pkg', ['binary' => []]);
$artifact->setCustomSourceCallback(function () {});
$this->assertTrue($artifact->hasSource());
}
// ==================== hasPlatformBinary ====================
public function testHasPlatformBinaryReturnsTrueWhenConfigHasBinaryForCurrentPlatform(): void
{
$platform = $this->getCurrentPlatform();
$artifact = new Artifact('my-pkg', [
'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
'binary' => [$platform => ['type' => 'url', 'url' => 'https://example.com/bin.tar.gz']],
]);
$this->assertTrue($artifact->hasPlatformBinary());
}
public function testHasPlatformBinaryReturnsFalseWhenNoBinaryConfig(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$this->assertFalse($artifact->hasPlatformBinary());
}
public function testHasPlatformBinaryReturnsTrueWithCustomCallback(): void
{
$platform = $this->getCurrentPlatform();
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$artifact->setCustomBinaryCallback($platform, function () {});
$this->assertTrue($artifact->hasPlatformBinary());
}
// ==================== getBinaryPlatforms ====================
public function testGetBinaryPlatformsReturnsConfiguredPlatforms(): void
{
$artifact = new Artifact('my-pkg', [
'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
'binary' => [
'linux-x86_64' => ['type' => 'url', 'url' => 'https://example.com/linux.tar.gz'],
'macos-aarch64' => ['type' => 'url', 'url' => 'https://example.com/mac.tar.gz'],
],
]);
$platforms = $artifact->getBinaryPlatforms();
$this->assertContains('linux-x86_64', $platforms);
$this->assertContains('macos-aarch64', $platforms);
}
public function testGetBinaryPlatformsExcludesCustomTypeWithoutCallback(): void
{
$artifact = new Artifact('my-pkg', [
'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
'binary' => [
'linux-x86_64' => ['type' => 'custom'],
],
]);
// No custom callback registered, so custom-type platform should NOT be included
$platforms = $artifact->getBinaryPlatforms();
$this->assertNotContains('linux-x86_64', $platforms);
}
public function testGetBinaryPlatformsIncludesCustomTypeWhenCallbackRegistered(): void
{
$artifact = new Artifact('my-pkg', [
'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
'binary' => [
'linux-x86_64' => ['type' => 'custom'],
],
]);
$artifact->setCustomBinaryCallback('linux-x86_64', function () {});
$platforms = $artifact->getBinaryPlatforms();
$this->assertContains('linux-x86_64', $platforms);
}
public function testGetBinaryPlatformsIncludesCustomCallbackPlatforms(): void
{
$artifact = new Artifact('my-pkg', [
'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
]);
$artifact->setCustomBinaryCallback('linux-x86_64', function () {});
$platforms = $artifact->getBinaryPlatforms();
$this->assertContains('linux-x86_64', $platforms);
}
// ==================== getSourceDir ====================
public function testGetSourceDirDefaultsToSourcePathWithName(): void
{
$cache = $this->makeStubbedArtifactCache([]);
ApplicationContext::initialize();
ApplicationContext::set(ArtifactCache::class, $cache);
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$expected = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, SOURCE_PATH . '/my-pkg');
$this->assertSame($expected, $artifact->getSourceDir());
}
public function testGetSourceDirWithRelativeExtractInConfig(): void
{
$cache = $this->makeStubbedArtifactCache([]);
ApplicationContext::initialize();
ApplicationContext::set(ArtifactCache::class, $cache);
$artifact = new Artifact('my-pkg', [
'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz', 'extract' => 'my-pkg-1.0'],
]);
$expected = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, SOURCE_PATH . '/my-pkg-1.0');
$this->assertSame($expected, $artifact->getSourceDir());
}
public function testGetSourceDirWithAbsoluteExtractInConfig(): void
{
$cache = $this->makeStubbedArtifactCache([]);
ApplicationContext::initialize();
ApplicationContext::set(ArtifactCache::class, $cache);
$artifact = new Artifact('my-pkg', [
'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz', 'extract' => '/tmp/my-pkg-extract'],
]);
$expected = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, '/tmp/my-pkg-extract');
$this->assertSame($expected, $artifact->getSourceDir());
}
// ==================== getSourceRoot ====================
public function testGetSourceRootDefaultsToSourceDir(): void
{
$cache = $this->makeStubbedArtifactCache([]);
ApplicationContext::initialize();
ApplicationContext::set(ArtifactCache::class, $cache);
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$this->assertSame($artifact->getSourceDir(), $artifact->getSourceRoot());
}
public function testGetSourceRootUsesMetadataSourceRoot(): void
{
$cache = $this->makeStubbedArtifactCache([]);
ApplicationContext::initialize();
ApplicationContext::set(ArtifactCache::class, $cache);
$artifact = new Artifact('my-pkg', [
'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
'metadata' => ['source-root' => 'src'],
]);
$expected = $artifact->getSourceDir() . DIRECTORY_SEPARATOR . 'src';
$this->assertSame($expected, $artifact->getSourceRoot());
}
// ==================== getBinaryExtractConfig ====================
public function testGetBinaryExtractConfigDefaultsToStandard(): void
{
putenv('EMULATE_PLATFORM=linux-x86_64');
$artifact = new Artifact('my-pkg', [
'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
// no binary for linux-x86_64
]);
$config = $artifact->getBinaryExtractConfig();
$this->assertSame('standard', $config['mode']);
$this->assertSame(PKG_ROOT_PATH, $config['path']);
putenv('EMULATE_PLATFORM');
}
public function testGetBinaryExtractConfigWithHostedExtractReturnsBuildRootPath(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$config = $artifact->getBinaryExtractConfig(['extract' => 'hosted']);
$this->assertSame('standard', $config['mode']);
$this->assertSame(BUILD_ROOT_PATH, $config['path']);
}
public function testGetBinaryExtractConfigWithRelativeExtractInCacheInfo(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$config = $artifact->getBinaryExtractConfig(['extract' => 'subdir']);
$this->assertSame('standard', $config['mode']);
$this->assertStringContainsString('subdir', $config['path']);
}
public function testGetBinaryExtractConfigWithAbsoluteExtractInCacheInfo(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$config = $artifact->getBinaryExtractConfig(['extract' => '/tmp/absolute-path']);
$this->assertSame('standard', $config['mode']);
$this->assertStringContainsString('absolute-path', $config['path']);
}
public function testGetBinaryExtractConfigWithArrayReturnsSelective(): void
{
putenv('EMULATE_PLATFORM=linux-x86_64');
$fileMap = ['lib/libfoo.a' => '/usr/local/lib/libfoo.a'];
$artifact = new Artifact('my-pkg', [
'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
'binary' => [
'linux-x86_64' => ['type' => 'url', 'url' => 'https://example.com/bin.tar.gz', 'extract' => $fileMap],
],
]);
$config = $artifact->getBinaryExtractConfig();
$this->assertSame('selective', $config['mode']);
$this->assertNull($config['path']);
$this->assertSame($fileMap, $config['files']);
putenv('EMULATE_PLATFORM');
}
// ==================== getBinaryDir ====================
public function testGetBinaryDirDelegatesToGetBinaryExtractConfig(): void
{
putenv('EMULATE_PLATFORM=linux-x86_64');
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$this->assertSame(PKG_ROOT_PATH, $artifact->getBinaryDir());
putenv('EMULATE_PLATFORM');
}
// ==================== Custom source callbacks ====================
public function testSetAndGetCustomSourceCallback(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$cb = function () {};
$artifact->setCustomSourceCallback($cb);
$this->assertSame($cb, $artifact->getCustomSourceCallback());
}
public function testGetCustomSourceCallbackReturnsNullWhenNotSet(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$this->assertNull($artifact->getCustomSourceCallback());
}
public function testSetAndGetCustomSourceCheckUpdateCallback(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$cb = function () {};
$artifact->setCustomSourceCheckUpdateCallback($cb);
$this->assertSame($cb, $artifact->getCustomSourceCheckUpdateCallback());
}
public function testGetCustomSourceCheckUpdateCallbackReturnsNullWhenNotSet(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$this->assertNull($artifact->getCustomSourceCheckUpdateCallback());
}
// ==================== Custom binary callbacks ====================
public function testSetAndGetCustomBinaryCallback(): void
{
$platform = $this->getCurrentPlatform();
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$cb = function () {};
$artifact->setCustomBinaryCallback($platform, $cb);
$this->assertSame($cb, $artifact->getCustomBinaryCallback());
}
public function testGetCustomBinaryCallbackReturnsNullWhenNotSet(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$this->assertNull($artifact->getCustomBinaryCallback());
}
public function testSetCustomBinaryCallbackThrowsForInvalidPlatform(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$this->expectException(ValidationException::class);
$artifact->setCustomBinaryCallback('invalid-platform-string', function () {});
}
public function testSetAndGetCustomBinaryCheckUpdateCallback(): void
{
$platform = $this->getCurrentPlatform();
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$cb = function () {};
$artifact->setCustomBinaryCheckUpdateCallback($platform, $cb);
$this->assertSame($cb, $artifact->getCustomBinaryCheckUpdateCallback());
}
public function testSetCustomBinaryCheckUpdateCallbackThrowsForInvalidPlatform(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$this->expectException(ValidationException::class);
$artifact->setCustomBinaryCheckUpdateCallback('bad-platform', function () {});
}
// ==================== Source extract callbacks ====================
public function testSetAndGetSourceExtractCallback(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$cb = function () {};
$artifact->setSourceExtractCallback($cb);
$this->assertSame($cb, $artifact->getSourceExtractCallback());
}
public function testHasSourceExtractCallbackReturnsFalseWhenNotSet(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$this->assertFalse($artifact->hasSourceExtractCallback());
}
public function testHasSourceExtractCallbackReturnsTrueWhenSet(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$artifact->setSourceExtractCallback(function () {});
$this->assertTrue($artifact->hasSourceExtractCallback());
}
// ==================== Binary extract callbacks ====================
public function testSetAndGetBinaryExtractCallbackForCurrentPlatform(): void
{
$platform = $this->getCurrentPlatform();
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$cb = function () {};
$artifact->setBinaryExtractCallback($cb, [$platform]);
$this->assertSame($cb, $artifact->getBinaryExtractCallback());
}
public function testGetBinaryExtractCallbackReturnsNullForNonMatchingPlatform(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
// Register callback for a platform that is definitely NOT the current one
$otherPlatforms = array_diff(['linux-x86_64', 'linux-aarch64', 'macos-x86_64', 'macos-aarch64', 'windows-x86_64'], [$this->getCurrentPlatform()]);
$artifact->setBinaryExtractCallback(function () {}, array_values($otherPlatforms));
// Only returns null if none of the listed platforms match current
// Since current platform is excluded, all remaining are "other"
$this->assertNull($artifact->getBinaryExtractCallback());
}
public function testSetBinaryExtractCallbackWithEmptyPlatformsMatchesAll(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$cb = function () {};
$artifact->setBinaryExtractCallback($cb, []);
$this->assertSame($cb, $artifact->getBinaryExtractCallback());
}
public function testHasBinaryExtractCallbackReturnsFalseWhenNotSet(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$this->assertFalse($artifact->hasBinaryExtractCallback());
}
public function testHasBinaryExtractCallbackReturnsTrueWhenSet(): void
{
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$artifact->setBinaryExtractCallback(function () {});
$this->assertTrue($artifact->hasBinaryExtractCallback());
}
// ==================== After-extract callbacks ====================
public function testEmitAfterSourceExtractCallsAllCallbacks(): void
{
ApplicationContext::initialize();
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$called1 = false;
$called2 = false;
$artifact->addAfterSourceExtractCallback(function (string $target_path) use (&$called1) {
$called1 = true;
});
$artifact->addAfterSourceExtractCallback(function (string $target_path) use (&$called2) {
$called2 = true;
});
$artifact->emitAfterSourceExtract('/tmp/test-path');
$this->assertTrue($called1);
$this->assertTrue($called2);
}
public function testEmitAfterBinaryExtractCallsCallbackMatchingPlatform(): void
{
ApplicationContext::initialize();
$platform = $this->getCurrentPlatform();
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$called = false;
$artifact->addAfterBinaryExtractCallback(function (string $target_path, string $platform) use (&$called) {
$called = true;
}, [$platform]);
$artifact->emitAfterBinaryExtract('/tmp/test-path', $platform);
$this->assertTrue($called);
}
public function testEmitAfterBinaryExtractSkipsCallbackForNonMatchingPlatform(): void
{
ApplicationContext::initialize();
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$called = false;
$artifact->addAfterBinaryExtractCallback(function () use (&$called) {
$called = true;
}, ['windows-x86_64']);
$artifact->emitAfterBinaryExtract('/tmp/test-path', 'linux-x86_64');
$this->assertFalse($called);
}
public function testEmitAfterBinaryExtractWithEmptyPlatformsCallsForAnyPlatform(): void
{
ApplicationContext::initialize();
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$called = false;
$artifact->addAfterBinaryExtractCallback(function () use (&$called) {
$called = true;
}, []);
$artifact->emitAfterBinaryExtract('/tmp/test-path', 'linux-x86_64');
$this->assertTrue($called);
}
// ==================== isSourceDownloaded / isBinaryDownloaded delegation ====================
public function testIsSourceDownloadedDelegatesToArtifactCache(): void
{
$cache = $this->createMock(ArtifactCache::class);
$cache->expects($this->once())
->method('isSourceDownloaded')
->with('my-pkg', false)
->willReturn(true);
ApplicationContext::initialize();
ApplicationContext::set(ArtifactCache::class, $cache);
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$this->assertTrue($artifact->isSourceDownloaded());
}
public function testIsBinaryDownloadedDelegatesToArtifactCache(): void
{
$platform = $this->getCurrentPlatform();
$cache = $this->createMock(ArtifactCache::class);
$cache->expects($this->once())
->method('isBinaryDownloaded')
->with('my-pkg', $platform, false)
->willReturn(true);
ApplicationContext::initialize();
ApplicationContext::set(ArtifactCache::class, $cache);
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$this->assertTrue($artifact->isBinaryDownloaded($platform));
}
// ==================== shouldUseBinary ====================
public function testShouldUseBinaryReturnsFalseWhenNotDownloaded(): void
{
$platform = $this->getCurrentPlatform();
$cache = $this->createMock(ArtifactCache::class);
$cache->method('isBinaryDownloaded')->willReturn(false);
ApplicationContext::initialize();
ApplicationContext::set(ArtifactCache::class, $cache);
$artifact = new Artifact('my-pkg', [
'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
'binary' => [$platform => ['type' => 'url', 'url' => 'https://example.com/bin.tar.gz']],
]);
$this->assertFalse($artifact->shouldUseBinary());
}
public function testShouldUseBinaryReturnsFalseWhenNoBinaryConfig(): void
{
$cache = $this->createMock(ArtifactCache::class);
$cache->method('isBinaryDownloaded')->willReturn(true);
ApplicationContext::initialize();
ApplicationContext::set(ArtifactCache::class, $cache);
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$this->assertFalse($artifact->shouldUseBinary());
}
public function testShouldUseBinaryReturnsTrueWhenDownloadedAndHasBinaryConfig(): void
{
$platform = $this->getCurrentPlatform();
$cache = $this->createMock(ArtifactCache::class);
$cache->method('isBinaryDownloaded')->willReturn(true);
ApplicationContext::initialize();
ApplicationContext::set(ArtifactCache::class, $cache);
$artifact = new Artifact('my-pkg', [
'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
'binary' => [$platform => ['type' => 'url', 'url' => 'https://example.com/bin.tar.gz']],
]);
$this->assertTrue($artifact->shouldUseBinary());
}
// ==================== isSourceExtracted ====================
public function testIsSourceExtractedReturnsFalseWhenDirNotExists(): void
{
$cache = $this->createMock(ArtifactCache::class);
$cache->method('getSourceInfo')->willReturn(null);
ApplicationContext::initialize();
ApplicationContext::set(ArtifactCache::class, $cache);
// Use an artifact whose source dir doesn't exist on disk
$artifact = new Artifact('this-pkg-does-not-exist-on-disk-2xyz', [
'source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz'],
]);
$this->assertFalse($artifact->isSourceExtracted());
}
// ==================== emitCustomBinary ====================
public function testEmitCustomBinaryThrowsWhenNoBinaryCallbackDefined(): void
{
ApplicationContext::initialize();
$artifact = new Artifact('my-pkg', ['source' => ['type' => 'url', 'url' => 'https://example.com/file.tar.gz']]);
$this->expectException(SPCInternalException::class);
$artifact->emitCustomBinary();
}
// ==================== Helpers ====================
private function getCurrentPlatform(): string
{
$emulated = getenv('EMULATE_PLATFORM');
if ($emulated !== false) {
return $emulated;
}
$os = match (PHP_OS_FAMILY) {
'Darwin' => 'macos',
'Windows' => 'windows',
default => 'linux',
};
$arch = php_uname('m');
if ($arch === 'arm64') {
$arch = 'aarch64';
}
return "{$os}-{$arch}";
}
private function injectArtifactConfig(string $name, array $config): void
{
$reflection = new \ReflectionClass(ArtifactConfig::class);
$property = $reflection->getProperty('artifact_configs');
$property->setAccessible(true);
$configs = $property->getValue(null) ?? [];
$configs[$name] = $config;
$property->setValue(null, $configs);
}
/**
* Create a stub ArtifactCache that always returns null for source/binary info
* and delegates isSourceDownloaded/isBinaryDownloaded to return false.
*/
private function makeStubbedArtifactCache(array $sourceInfoMap): ArtifactCache
{
$cacheFile = $this->tempDir . '/test-cache.json';
file_put_contents($cacheFile, json_encode([]));
return new ArtifactCache($cacheFile);
}
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);
}
}