From 4671be623bd0732a57804a5536f6d36d9f2f0708 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 12 Apr 2026 13:29:06 +0800 Subject: [PATCH] Add v3 artifact test --- .../StaticPHP/Artifact/ArtifactCacheTest.php | 548 +++++++++++++ .../Artifact/ArtifactDownloaderTest.php | 351 ++++++++ .../Artifact/ArtifactExtractorTest.php | 229 ++++++ tests/StaticPHP/Artifact/ArtifactTest.php | 750 ++++++++++++++++++ 4 files changed, 1878 insertions(+) create mode 100644 tests/StaticPHP/Artifact/ArtifactCacheTest.php create mode 100644 tests/StaticPHP/Artifact/ArtifactDownloaderTest.php create mode 100644 tests/StaticPHP/Artifact/ArtifactExtractorTest.php create mode 100644 tests/StaticPHP/Artifact/ArtifactTest.php diff --git a/tests/StaticPHP/Artifact/ArtifactCacheTest.php b/tests/StaticPHP/Artifact/ArtifactCacheTest.php new file mode 100644 index 00000000..36bcf006 --- /dev/null +++ b/tests/StaticPHP/Artifact/ArtifactCacheTest.php @@ -0,0 +1,548 @@ +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); + } +} diff --git a/tests/StaticPHP/Artifact/ArtifactDownloaderTest.php b/tests/StaticPHP/Artifact/ArtifactDownloaderTest.php new file mode 100644 index 00000000..919452d6 --- /dev/null +++ b/tests/StaticPHP/Artifact/ArtifactDownloaderTest.php @@ -0,0 +1,351 @@ +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); + } +} diff --git a/tests/StaticPHP/Artifact/ArtifactExtractorTest.php b/tests/StaticPHP/Artifact/ArtifactExtractorTest.php new file mode 100644 index 00000000..d95275a6 --- /dev/null +++ b/tests/StaticPHP/Artifact/ArtifactExtractorTest.php @@ -0,0 +1,229 @@ +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); + } +} diff --git a/tests/StaticPHP/Artifact/ArtifactTest.php b/tests/StaticPHP/Artifact/ArtifactTest.php new file mode 100644 index 00000000..e1cb8ccd --- /dev/null +++ b/tests/StaticPHP/Artifact/ArtifactTest.php @@ -0,0 +1,750 @@ +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); + } +}