Author: Shivam Mathur (shivammathur) Date: 2025-02-04T15:47:35+05:30 Commit: https://github.com/php/web-downloads/commit/676f111092dd99e605497b1a421d68e13399c282 Raw diff: https://github.com/php/web-downloads/commit/676f111092dd99e605497b1a421d68e13399c282.diff
Add a command to generate listing Add support to do DI in commands Fix autoloading in tests Changed paths: A src/Actions/GetListing.php A src/Actions/UpdateReleasesJson.php A src/Console/Command/GenerateListingCommand.php A tests/Actions/GetListingTest.php A tests/Actions/UpdateReleasesJsonTest.php A tests/Console/Command/GenerateListingCommandTest.php M phpunit.xml.dist M runner.php M src/Console/Command.php M src/Console/Command/PhpCommand.php M tests/CommandTest.php M tests/Console/Command/PhpCommandTest.php Diff: diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 659c34b..c4916f0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="UTF-8"?> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" - bootstrap="vendor/autoload.php" + bootstrap="autoloader.php" cacheDirectory=".phpunit.cache" executionOrder="depends,defects" shortenArraysForExportThreshold="10" diff --git a/runner.php b/runner.php index ca3638d..970cb9f 100644 --- a/runner.php +++ b/runner.php @@ -27,7 +27,8 @@ function discoverCommands(string $directory, $argc, $argv): array if ($file->isFile() && $file->getExtension() === 'php') { $className = getClassName($directory, $file); if (is_subclass_of($className, Command::class)) { - $instance = new $className($argc, $argv); + $instance = resolve($className); + $instance->setCliArguments($argc, $argv); $commands[$instance->getSignature()] = $instance; } } @@ -35,6 +36,28 @@ function discoverCommands(string $directory, $argc, $argv): array return $commands; } +function resolve(string $className) { + $reflection = new ReflectionClass($className); + $constructor = $reflection->getConstructor(); + if (!$constructor) { + return new $className; + } + $parameters = $constructor->getParameters(); + $dependencies = []; + foreach ($parameters as $parameter) { + $type = $parameter->getType(); + if ($type && !$type->isBuiltin()) { + $dependencyClass = $type->getName(); + $dependencies[] = resolve($dependencyClass); + } elseif ($parameter->isDefaultValueAvailable()) { + $dependencies[] = $parameter->getDefaultValue(); + } else { + throw new Exception("Cannot resolve dependency: " . $parameter->getName()); + } + } + return $reflection->newInstanceArgs($dependencies); +} + function listCommands(array $commands): void { echo "Available commands:\n"; diff --git a/src/Actions/GetListing.php b/src/Actions/GetListing.php new file mode 100644 index 0000000..c36e2dc --- /dev/null +++ b/src/Actions/GetListing.php @@ -0,0 +1,134 @@ +<?php + +namespace App\Actions; + +class GetListing +{ + public function handle(string $directory): array + { + $builds = glob($directory . '/php-[678].*[0-9]-latest.zip'); + if (empty($builds)) { + $builds = glob($directory . '/php-[678].*[0-9].zip'); + } + + $releases = []; + $sha256sums = $this->getSha256Sums($directory); + foreach ($builds as $file) { + $file_ori = $file; + $mtime = date('Y-M-d H:i:s', filemtime($file)); + + $parts = $this->parseFileName(basename($file)); + $key = ($parts['nts'] ? 'nts-' : 'ts-') . $parts['vc'] . '-' . $parts['arch']; + $version_short = $parts['version_short']; + if (!isset($releases['version'])) { + $releases[$version_short]['version'] = $parts['version']; + } + $releases[$version_short][$key]['mtime'] = $mtime; + $releases[$version_short][$key]['zip'] = [ + 'path' => basename($file_ori), + 'size' => $this->bytes2string(filesize($file_ori)), + 'sha256' => $sha256sums[strtolower(basename($file_ori))] + ]; + $namingPattern = $parts['version'] . ($parts['nts'] ? '-' . $parts['nts'] : '') . '-Win32-' . $parts['vc'] . '-' . $parts['arch'] . ($parts['ts'] ? '-' . $parts['ts'] : ''); + $build_types = [ + 'source' => 'php-' . $parts['version'] . '-src.zip', + 'debug_pack' => 'php-debug-pack-' . $namingPattern . '.zip', + 'devel_pack' => 'php-devel-pack-' . $namingPattern . '.zip', + 'installer' => 'php-' . $namingPattern . '.msi', + 'test_pack' => 'php-test-pack-' . $parts['version'] . '.zip', + ]; + foreach ($build_types as $type => $fileName) { + $filePath = $directory . '/' . $fileName; + if (file_exists($filePath)) { + if(in_array($type, ['test_pack', 'source'])) { + $releases[$version_short][$type] = [ + 'path' => $fileName, + 'size' => $this->bytes2string(filesize($filePath)), + 'sha256' => $sha256sums[strtolower(basename($file_ori))] + ]; + } else { + $releases[$version_short][$key][$type] = [ + 'path' => $fileName, + 'size' => $this->bytes2string(filesize($filePath)), + 'sha256' => $sha256sums[strtolower(basename($file_ori))] + ]; + } + } + } + } + return $releases; + } + + public function getSha256Sums($directory): array + { + $result = []; + if(!file_exists("$directory/sha256sum.txt")) { + file_put_contents("$directory/sha256sum.txt", ''); + } + $sha_file = fopen("$directory/sha256sum.txt", 'w'); + foreach (scandir($directory) as $filename) { + if (pathinfo($filename, PATHINFO_EXTENSION) !== 'zip') { + continue; + } + $sha256 = hash_file('sha256', "$directory/$filename"); + fwrite($sha_file, "$sha256 *$filename\n"); + $result[strtolower(basename($filename))] = $sha256; + } + fclose($sha_file); + return $result; + } + + public function bytes2string(int $size): string + { + $sizes = ['YB', 'ZB', 'EB', 'PB', 'TB', 'GB', 'MB', 'kB', 'B']; + + $total = count($sizes); + + while ($total-- && $size > 1024) $size /= 1024; + + return round($size, 2) . $sizes[$total]; + } + + public function parseFileName($fileName): array + { + $fileName = str_replace(['-Win32', '.zip'], ['', ''], $fileName); + + $parts = explode('-', $fileName); + if (is_numeric($parts[2]) || $parts[2] == 'dev') { + $version = $parts[1] . '-' . $parts[2]; + $nts = $parts[3] == 'nts' ? 'nts' : false; + if ($nts) { + $vc = $parts[4]; + $arch = $parts[5]; + } else { + $vc = $parts[3]; + $arch = $parts[4]; + } + } elseif ($parts[2] == 'nts') { + $nts = 'nts'; + $version = $parts[1]; + $vc = $parts[3]; + $arch = $parts[4]; + } else { + $nts = false; + $version = $parts[1]; + $vc = $parts[2]; + $arch = $parts[3]; + } + if (is_numeric($vc)) { + $vc = 'VC6'; + $arch = 'x86'; + } + $t = count($parts) - 1; + $ts = is_numeric($parts[$t]) ? $parts[$t] : false; + + return [ + 'version' => $version, + 'version_short' => substr($version, 0, 3), + 'nts' => $nts, + 'vc' => $vc, + 'arch' => $arch, + 'ts' => $ts + ]; + } +} \ No newline at end of file diff --git a/src/Actions/UpdateReleasesJson.php b/src/Actions/UpdateReleasesJson.php new file mode 100644 index 0000000..faad68e --- /dev/null +++ b/src/Actions/UpdateReleasesJson.php @@ -0,0 +1,36 @@ +<?php + +namespace App\Actions; + +use DateTimeImmutable; +use Exception; + +class UpdateReleasesJson +{ + /** + * @throws Exception + */ + public function handle(array $releases, string $directory): void + { + try { + foreach ($releases as &$release) { + foreach ($release as &$build_type) { + if (!is_array($build_type) || !isset($build_type['mtime'])) { + continue; + } + + $date = new DateTimeImmutable($build_type['mtime']); + $build_type['mtime'] = $date->format('c'); + } + unset($build_type); + } + unset($release); + file_put_contents( + $directory . '/releases.json', + json_encode($releases, JSON_PRETTY_PRINT) + ); + } catch (Exception $exception) { + throw new Exception('Failed to generate releases.json: ' . $exception->getMessage()); + } + } +} \ No newline at end of file diff --git a/src/Console/Command.php b/src/Console/Command.php index 56d9ac6..a59f9d1 100644 --- a/src/Console/Command.php +++ b/src/Console/Command.php @@ -9,21 +9,19 @@ abstract class Command public const INVALID = 2; protected string $signature = ''; - protected string $description = ''; + protected array $arguments = []; + protected array $options = []; - public function __construct( - protected ?int $argc = null, - protected ?array $argv = null, - protected array $arguments = [], - protected array $options = [], - ) { - if ($argc !== null && $argv !== null) { - $this->parse($argc, $argv); - } + public function __construct() { + // + } + + public function setCliArguments(int $argc, array $argv): void { + $this->parse($argc, $argv); } - abstract public function handle(); + abstract public function handle(): int; private function parse($argc, $argv): void { $pattern = '/\{(\w+)}|\{--(\w+)}/'; diff --git a/src/Console/Command/GenerateListingCommand.php b/src/Console/Command/GenerateListingCommand.php new file mode 100644 index 0000000..ecae3b5 --- /dev/null +++ b/src/Console/Command/GenerateListingCommand.php @@ -0,0 +1,38 @@ +<?php + +namespace App\Console\Command; + +use App\Actions\GetListing; +use App\Actions\UpdateReleasesJson; +use App\Console\Command; +use Exception; + +class GenerateListingCommand extends Command +{ + protected string $signature = 'php:add --directory='; + protected string $description = 'Generate Listing for PHP builds in a directory'; + + public function __construct( + protected GetListing $generateListing, + protected UpdateReleasesJson $updateReleasesJson, + ) { + parent::__construct(); + } + + public function handle(): int + { + try { + $directory = $this->getOption('directory'); + if (!$directory) { + throw new Exception('Directory is required'); + } + + $releases = $this->generateListing->handle($directory); + $this->updateReleasesJson->handle($releases, $directory); + return Command::SUCCESS; + } catch (Exception $e) { + echo $e->getMessage(); + return Command::FAILURE; + } + } +} \ No newline at end of file diff --git a/src/Console/Command/PhpCommand.php b/src/Console/Command/PhpCommand.php index 2d98190..86f8044 100644 --- a/src/Console/Command/PhpCommand.php +++ b/src/Console/Command/PhpCommand.php @@ -2,9 +2,10 @@ namespace App\Console\Command; +use App\Actions\GetListing; +use App\Actions\UpdateReleasesJson; use App\Console\Command; use App\Helpers\Helpers; -use DateTimeImmutable; use Exception; use ZipArchive; @@ -15,6 +16,13 @@ class PhpCommand extends Command protected ?string $baseDirectory = null; + public function __construct( + protected GetListing $generateListing, + protected UpdateReleasesJson $updateReleasesJson, + ) { + parent::__construct(); + } + public function handle(): int { try { @@ -71,7 +79,12 @@ public function handle(): int $this->moveBuild($tempDirectory, $destinationDirectory); - $this->generateListing($destinationDirectory); + $releases = $this->generateListing->handle($destinationDirectory); + + $this->updateReleasesJson->handle($releases, $destinationDirectory); + if ($destinationDirectory === $this->baseDirectory . '/releases') { + $this->updateLatestBuilds($releases, $destinationDirectory); + } (new Helpers)->rmdirr($tempDirectory); @@ -156,96 +169,6 @@ private function getFileVersion(string $file): string return str_replace('.zip', '', $parts[0]); } - /** - * @throws Exception - */ - private function generateListing(string $directory): void - { - $builds = glob($directory . '/php-[678].*[0-9]-latest.zip'); - if (empty($builds)) { - $builds = glob($directory . '/php-[678].*[0-9].zip'); - } - - $releases = []; - $sha256sums = $this->getSha256Sums($directory); - foreach ($builds as $file) { - $file_ori = $file; - $mtime = date('Y-M-d H:i:s', filemtime($file)); - - $parts = $this->parseFileName(basename($file)); - $key = ($parts['nts'] ? 'nts-' : 'ts-') . $parts['vc'] . '-' . $parts['arch']; - $version_short = $parts['version_short']; - if (!isset($releases['version'])) { - $releases[$version_short]['version'] = $parts['version']; - } - $releases[$version_short][$key]['mtime'] = $mtime; - $releases[$version_short][$key]['zip'] = [ - 'path' => basename($file_ori), - 'size' => $this->bytes2string(filesize($file_ori)), - 'sha256' => $sha256sums[strtolower(basename($file_ori))] - ]; - $namingPattern = $parts['version'] . ($parts['nts'] ? '-' . $parts['nts'] : '') . '-Win32-' . $parts['vc'] . '-' . $parts['arch'] . ($parts['ts'] ? '-' . $parts['ts'] : ''); - $build_types = [ - 'source' => 'php-' . $parts['version'] . '-src.zip', - 'debug_pack' => 'php-debug-pack-' . $namingPattern . '.zip', - 'devel_pack' => 'php-devel-pack-' . $namingPattern . '.zip', - 'installer' => 'php-' . $namingPattern . '.msi', - 'test_pack' => 'php-test-pack-' . $parts['version'] . '.zip', - ]; - foreach ($build_types as $type => $fileName) { - $filePath = $directory . '/' . $fileName; - if (file_exists($filePath)) { - if(in_array($type, ['test_pack', 'source'])) { - $releases[$version_short][$type] = [ - 'path' => $fileName, - 'size' => $this->bytes2string(filesize($filePath)), - 'sha256' => $sha256sums[strtolower(basename($file_ori))] - ]; - } else { - $releases[$version_short][$key][$type] = [ - 'path' => $fileName, - 'size' => $this->bytes2string(filesize($filePath)), - 'sha256' => $sha256sums[strtolower(basename($file_ori))] - ]; - } - } - } - } - - $this->updateReleasesJson($releases, $directory); - if ($directory === $this->baseDirectory . '/releases') { - $this->updateLatestBuilds($releases, $directory); - } - } - - /** - * @throws Exception - */ - private function updateReleasesJson(array $releases, string $directory): void - { - foreach ($releases as &$release) { - foreach ($release as &$build_type) { - if (!is_array($build_type) || !isset($build_type['mtime'])) { - continue; - } - - try { - $date = new DateTimeImmutable($build_type['mtime']); - $build_type['mtime'] = $date->format('c'); - } catch (Exception $exception) { - throw new Exception('Failed to generate releases.json: ' . $exception->getMessage()); - } - } - unset($build_type); - } - unset($release); - - file_put_contents( - $directory . '/releases.json', - json_encode($releases, JSON_PRETTY_PRINT) - ); - } - private function updateLatestBuilds($releases, $directory): void { if(!is_dir($directory . '/latest')) { @@ -262,77 +185,4 @@ private function updateLatestBuilds($releases, $directory): void }); } } - - private function getSha256Sums($directory): array - { - $result = []; - if(!file_exists("$directory/sha256sum.txt")) { - file_put_contents("$directory/sha256sum.txt", ''); - } - $sha_file = fopen("$directory/sha256sum.txt", 'w'); - foreach (scandir($directory) as $filename) { - if (pathinfo($filename, PATHINFO_EXTENSION) !== 'zip') { - continue; - } - $sha256 = hash_file('sha256', "$directory/$filename"); - fwrite($sha_file, "$sha256 *$filename\n"); - $result[strtolower(basename($filename))] = $sha256; - } - fclose($sha_file); - return $result; - } - - private function bytes2string(int $size): string - { - $sizes = ['YB', 'ZB', 'EB', 'PB', 'TB', 'GB', 'MB', 'kB', 'B']; - - $total = count($sizes); - - while ($total-- && $size > 1024) $size /= 1024; - - return round($size, 2) . $sizes[$total]; - } - - private function parseFileName($fileName): array - { - $fileName = str_replace(['-Win32', '.zip'], ['', ''], $fileName); - - $parts = explode('-', $fileName); - if (is_numeric($parts[2]) || $parts[2] == 'dev') { - $version = $parts[1] . '-' . $parts[2]; - $nts = $parts[3] == 'nts' ? 'nts' : false; - if ($nts) { - $vc = $parts[4]; - $arch = $parts[5]; - } else { - $vc = $parts[3]; - $arch = $parts[4]; - } - } elseif ($parts[2] == 'nts') { - $nts = 'nts'; - $version = $parts[1]; - $vc = $parts[3]; - $arch = $parts[4]; - } else { - $nts = false; - $version = $parts[1]; - $vc = $parts[2]; - $arch = $parts[3]; - } - if (is_numeric($vc)) { - $vc = 'VC6'; - $arch = 'x86'; - } - $t = count($parts) - 1; - $ts = is_numeric($parts[$t]) ? $parts[$t] : false; - - return [ - 'version' => $version, - 'version_short' => substr($version, 0, 3), - 'nts' => $nts, - 'vc' => $vc, - 'arch' => $arch, - 'ts' => $ts - ]; - } } \ No newline at end of file diff --git a/tests/Actions/GetListingTest.php b/tests/Actions/GetListingTest.php new file mode 100644 index 0000000..985bcf7 --- /dev/null +++ b/tests/Actions/GetListingTest.php @@ -0,0 +1,205 @@ +<?php + +namespace Actions; + +use App\Actions\GetListing; +use App\Helpers\Helpers; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; +use RuntimeException; + +class GetListingTest extends TestCase +{ + private string $tempDir; + private GetListing $getListing; + + protected function setUp(): void + { + $this->tempDir = sys_get_temp_dir() . '/get_listing_test_' . uniqid(); + if (!mkdir($this->tempDir) && !is_dir($this->tempDir)) { + throw new RuntimeException(sprintf('Directory "%s" was not created', $this->tempDir)); + } + $this->getListing = new GetListing(); + } + + protected function tearDown(): void + { + (new Helpers())->rmdirr($this->tempDir); + } + + public static function bytes2StringProvider(): array + { + return [ + [100, "100B"], + [1024, "1024B"], + [1025, "1kB"], + [5000, "4.88kB"], + [1048576, "1024kB"], + [1048577, "1MB"], + [1073741824, "1024MB"], + [1073741825, "1GB"], + [1099511627776, "1024GB"], + [1099511627777, "1TB"], + ]; + } + + public static function parseFileNameProvider(): array + { + return [ + 'with nts' => [ + 'php-7.4.0-nts-Win32-VC15-x64-latest.zip', + [ + 'version' => '7.4.0', + 'version_short' => '7.4', + 'nts' => 'nts', + 'vc' => 'VC15', + 'arch' => 'x64', + 'ts' => false, + ], + ], + 'without nts' => [ + 'php-7.4.0-Win32-VC15-x64-latest.zip', + [ + 'version' => '7.4.0', + 'version_short' => '7.4', + 'nts' => false, + 'vc' => 'VC15', + 'arch' => 'x64', + 'ts' => false, + ], + ], + 'with numeric vc' => [ + 'php-5.6.0-nts-Win32-7-x86-latest.zip', + [ + 'version' => '5.6.0', + 'version_short' => '5.6', + 'nts' => 'nts', + 'vc' => 'VC6', + 'arch' => 'x86', + 'ts' => false, + ], + ], + ]; + } + + #[DataProvider('bytes2StringProvider')] + public function testBytes2String(int $bytes, string $expected): void + { + $result = $this->getListing->bytes2string($bytes); + $this->assertEquals($expected, $result); + } + #[DataProvider('parseFileNameProvider')] + public function testParseFileName(string $fileName, array $expected): void + { + $parts = $this->getListing->parseFileName($fileName); + $this->assertEquals($expected, $parts); + } + + public function testGetSha256SumsCreatesFileAndReturnsHashes(): void + { + $dummyZip = $this->tempDir . '/dummy.zip'; + $content = "dummy content"; + file_put_contents($dummyZip, $content); + + $sums = $this->getListing->getSha256Sums($this->tempDir); + + $key = strtolower(basename($dummyZip)); + $expectedHash = hash_file('sha256', $dummyZip); + + $this->assertArrayHasKey($key, $sums); + $this->assertEquals($expectedHash, $sums[$key]); + + $shaFile = $this->tempDir . '/sha256sum.txt'; + $this->assertFileExists($shaFile); + $this->assertNotEmpty(file_get_contents($shaFile)); + } + + public function testHandleWithNoMatchingFiles(): void + { + $result = $this->getListing->handle($this->tempDir); + $this->assertEmpty($result, "Expected an empty result when no build files are present."); + } + + public function testHandleWithMatchingFiles(): void + { + $mainBuildFile = $this->tempDir . '/php-7.4.0-nts-Win32-VC15-x64-latest.zip'; + file_put_contents($mainBuildFile, 'build content'); + $fixedTime = 1609459200; // 2021-01-01 00:00:00 + touch($mainBuildFile, $fixedTime); + + $sourceFile = $this->tempDir . '/php-7.4.0-src.zip'; + $debugPackFile = $this->tempDir . '/php-debug-pack-7.4.0-nts-Win32-VC15-x64.zip'; + $develPackFile = $this->tempDir . '/php-devel-pack-7.4.0-nts-Win32-VC15-x64.zip'; + $installerFile = $this->tempDir . '/php-7.4.0-nts-Win32-VC15-x64.msi'; + $testPackFile = $this->tempDir . '/php-test-pack-7.4.0.zip'; + + file_put_contents($sourceFile, 'source content'); + file_put_contents($debugPackFile, 'debug content'); + file_put_contents($develPackFile, 'devel content'); + file_put_contents($installerFile, 'installer content'); + file_put_contents($testPackFile, 'test content'); + + $result = $this->getListing->handle($this->tempDir); + $versionShortKey = '7.4'; + $buildKey = 'nts-VC15-x64'; + + $this->assertArrayHasKey($versionShortKey, $result); + $versionData = $result[$versionShortKey]; + + $this->assertArrayHasKey('version', $versionData); + $this->assertEquals('7.4.0', $versionData['version']); + + $this->assertArrayHasKey($buildKey, $versionData); + $buildDetails = $versionData[$buildKey]; + + $expectedMtime = date('Y-M-d H:i:s', filemtime($mainBuildFile)); + $this->assertEquals($expectedMtime, $buildDetails['mtime']); + + $this->assertArrayHasKey('zip', $buildDetails); + $zipInfo = $buildDetails['zip']; + $this->assertEquals(basename($mainBuildFile), $zipInfo['path']); + + $expectedZipSize = $this->getListing->bytes2string(filesize($mainBuildFile)); + $this->assertEquals($expectedZipSize, $zipInfo['size']); + + $expectedSha256 = hash_file('sha256', $mainBuildFile); + $this->assertEquals($expectedSha256, $zipInfo['sha256']); + + $this->assertArrayHasKey('source', $versionData); + $sourceInfo = $versionData['source']; + $this->assertEquals(basename($sourceFile), $sourceInfo['path']); + $expectedSourceSize = $this->getListing->bytes2string(filesize($sourceFile)); + $this->assertEquals($expectedSourceSize, $sourceInfo['size']); + $this->assertEquals($expectedSha256, $sourceInfo['sha256']); + + $this->assertArrayHasKey('test_pack', $versionData); + $testPackInfo = $versionData['test_pack']; + $this->assertEquals(basename($testPackFile), $testPackInfo['path']); + $expectedTestPackSize = $this->getListing->bytes2string(filesize($testPackFile)); + $this->assertEquals($expectedTestPackSize, $testPackInfo['size']); + $this->assertEquals($expectedSha256, $testPackInfo['sha256']); + + $this->assertArrayHasKey('debug_pack', $buildDetails); + $debugInfo = $buildDetails['debug_pack']; + $this->assertEquals(basename($debugPackFile), $debugInfo['path']); + $expectedDebugSize = $this->getListing->bytes2string(filesize($debugPackFile)); + $this->assertEquals($expectedDebugSize, $debugInfo['size']); + $this->assertEquals($expectedSha256, $debugInfo['sha256']); + + $this->assertArrayHasKey('devel_pack', $buildDetails); + $develInfo = $buildDetails['devel_pack']; + $this->assertEquals(basename($develPackFile), $develInfo['path']); + $expectedDevelSize = $this->getListing->bytes2string(filesize($develPackFile)); + $this->assertEquals($expectedDevelSize, $develInfo['size']); + $this->assertEquals($expectedSha256, $develInfo['sha256']); + + $this->assertArrayHasKey('installer', $buildDetails); + $installerInfo = $buildDetails['installer']; + $this->assertEquals(basename($installerFile), $installerInfo['path']); + $expectedInstallerSize = $this->getListing->bytes2string(filesize($installerFile)); + $this->assertEquals($expectedInstallerSize, $installerInfo['size']); + $this->assertEquals($expectedSha256, $installerInfo['sha256']); + + $this->assertFileExists($this->tempDir . '/sha256sum.txt'); + } +} diff --git a/tests/Actions/UpdateReleasesJsonTest.php b/tests/Actions/UpdateReleasesJsonTest.php new file mode 100644 index 0000000..12093c6 --- /dev/null +++ b/tests/Actions/UpdateReleasesJsonTest.php @@ -0,0 +1,138 @@ +<?php +declare(strict_types=1); + +namespace Actions; + +use App\Actions\UpdateReleasesJson; +use App\Helpers\Helpers; +use DateTimeImmutable; +use Exception; +use PHPUnit\Framework\TestCase; + +class UpdateReleasesJsonTest extends TestCase +{ + private string $tempDir; + + /** + * @throws Exception + */ + protected function setUp(): void + { + date_default_timezone_set('UTC'); + $this->tempDir = sys_get_temp_dir() . '/update_releases_test_' . uniqid(); + if (!mkdir($this->tempDir, 0777, true) && !is_dir($this->tempDir)) { + throw new Exception(sprintf('Directory "%s" was not created', $this->tempDir)); + } + } + + protected function tearDown(): void + { + (new Helpers())->rmdirr($this->tempDir); + } + + /** + * @throws Exception + */ + public function testHandleValidReleases(): void + { + $releases = [ + '7.4' => [ + 'version' => '7.4.0', + 'nts-VC15-x64' => [ + 'mtime' => "2023-01-01 10:00:00", + 'zip' => [ + 'path' => 'php-7.4.0-nts-Win32-VC15-x64-latest.zip', + 'size' => '12kB', + 'sha256' => 'abcdef' + ], + 'debug_pack' => [ + 'mtime' => "2023-01-01 11:00:00", + 'path' => 'php-debug-pack-7.4.0-nts-Win32-VC15-x64.zip', + 'size' => '3kB', + 'sha256' => '123456' + ], + ], + 'source' => [ + 'path' => 'php-7.4.0-src.zip', + 'size' => '5MB', + 'sha256' => '987654' + ], + ], + ]; + + $updater = new UpdateReleasesJson(); + $updater->handle($releases, $this->tempDir); + + $jsonFile = $this->tempDir . '/releases.json'; + $this->assertFileExists($jsonFile); + + $jsonData = json_decode(file_get_contents($jsonFile), true); + $this->assertNotNull($jsonData, 'Decoded JSON should not be null.'); + + $expectedDate = (new DateTimeImmutable("2023-01-01 10:00:00"))->format('c'); + + $this->assertEquals( + $expectedDate, + $jsonData['7.4']['nts-VC15-x64']['mtime'], + 'Main build mtime should be in ISO 8601 format.' + ); + + $this->assertArrayHasKey('source', $jsonData['7.4']); + $this->assertArrayNotHasKey('mtime', $jsonData['7.4']['source']); + } + + public function testHandleWithInvalidMtimeThrowsException(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Failed to generate releases.json:'); + + $releases = [ + '7.4' => [ + 'nts-VC15-x64' => [ + 'mtime' => "invalid date string", + 'zip' => [ + 'path' => 'php-7.4.0-nts-Win32-VC15-x64-latest.zip', + 'size' => '12kB', + 'sha256' => 'abcdef' + ], + ], + ], + ]; + + (new UpdateReleasesJson())->handle($releases, $this->tempDir); + } + + /** + * @throws Exception + */ + public function testHandleWithNoMtime(): void + { + $releases = [ + '7.4' => [ + 'nts-VC15-x64' => [ + 'zip' => [ + 'path' => 'php-7.4.0-nts-Win32-VC15-x64-latest.zip', + 'size' => '12kB', + 'sha256' => 'abcdef' + ], + ], + ], + ]; + + $updater = new UpdateReleasesJson(); + $updater->handle($releases, $this->tempDir); + + $jsonFile = $this->tempDir . '/releases.json'; + $this->assertFileExists($jsonFile); + + $jsonData = json_decode(file_get_contents($jsonFile), true); + $this->assertNotNull($jsonData, 'Decoded JSON should not be null.'); + + $this->assertArrayHasKey('zip', $jsonData['7.4']['nts-VC15-x64']); + $this->assertEquals( + 'php-7.4.0-nts-Win32-VC15-x64-latest.zip', + $jsonData['7.4']['nts-VC15-x64']['zip']['path'] + ); + $this->assertArrayNotHasKey('mtime', $jsonData['7.4']['nts-VC15-x64']); + } +} diff --git a/tests/CommandTest.php b/tests/CommandTest.php index 3d0f799..ea3a03d 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -13,7 +13,8 @@ public function handle(): int { class CommandTest extends TestCase { public function testParseArgumentsAndOptions() { $argv = ["script.php", "value", "--option=optValue"]; - $command = new TestCommand(count($argv), $argv); + $command = new TestCommand(); + $command->setCliArguments(count($argv), $argv); $this->assertEquals("value", $command->getArgument("arg"), "Argument parsing failed."); $this->assertEquals("optValue", $command->getOption("option"), "Option parsing failed."); diff --git a/tests/Console/Command/GenerateListingCommandTest.php b/tests/Console/Command/GenerateListingCommandTest.php new file mode 100644 index 0000000..ec53b67 --- /dev/null +++ b/tests/Console/Command/GenerateListingCommandTest.php @@ -0,0 +1,96 @@ +<?php + +namespace Console\Command; + +use App\Actions\GetListing; +use App\Actions\UpdateReleasesJson; +use App\Console\Command as BaseCommand; +use App\Console\Command\GenerateListingCommand; +use Exception; +use PHPUnit\Framework\MockObject\Exception as MockObjectException; +use PHPUnit\Framework\TestCase; + +class GenerateListingCommandTest extends TestCase +{ + /** + * @throws MockObjectException + */ + public function testHandleWithoutDirectory(): void + { + $getListing = $this->createMock(GetListing::class); + $updateReleasesJson = $this->createMock(UpdateReleasesJson::class); + $command = new GenerateListingCommand($getListing, $updateReleasesJson); + $argv = ['script.php', 'php:add']; + $argc = count($argv); + $command->setCliArguments($argc, $argv); + + ob_start(); + $result = $command->handle(); + $output = ob_get_clean(); + + $this->assertStringContainsString('Directory is required', $output); + $this->assertEquals(BaseCommand::FAILURE, $result); + } + + /** + * @throws MockObjectException + */ + public function testHandleSuccess(): void + { + $directory = '/some/directory'; + $dummyReleases = ['dummy' => 'value']; + + $getListing = $this->createMock(GetListing::class); + $updateReleasesJson = $this->createMock(UpdateReleasesJson::class); + + $getListing->expects($this->once()) + ->method('handle') + ->with($directory) + ->willReturn($dummyReleases); + + $updateReleasesJson->expects($this->once()) + ->method('handle') + ->with($dummyReleases, $directory); + + $command = new GenerateListingCommand($getListing, $updateReleasesJson); + + $argv = ['script.php', 'php:add', '--directory=' . $directory]; + $argc = count($argv); + $command->setCliArguments($argc, $argv); + + $result = $command->handle(); + + $this->assertEquals(BaseCommand::SUCCESS, $result); + } + + /** + * @throws MockObjectException + */ + public function testHandleWhenExceptionThrown(): void + { + $directory = '/some/directory'; + + $getListing = $this->createMock(GetListing::class); + $updateReleasesJson = $this->createMock(UpdateReleasesJson::class); + + $getListing->expects($this->once()) + ->method('handle') + ->with($directory) + ->will($this->throwException(new Exception("Test exception"))); + + $updateReleasesJson->expects($this->never()) + ->method('handle'); + + $command = new GenerateListingCommand($getListing, $updateReleasesJson); + $argv = ['script.php', 'php:add', '--directory=' . $directory]; + $argc = count($argv); + $command->setCliArguments($argc, $argv); + + ob_start(); + $result = $command->handle(); + $output = ob_get_clean(); + + $this->assertStringContainsString("Test exception", $output); + $this->assertEquals(BaseCommand::FAILURE, $result); + } +} diff --git a/tests/Console/Command/PhpCommandTest.php b/tests/Console/Command/PhpCommandTest.php index cddce3b..5178423 100644 --- a/tests/Console/Command/PhpCommandTest.php +++ b/tests/Console/Command/PhpCommandTest.php @@ -2,6 +2,8 @@ namespace Console\Command; +use App\Actions\GetListing; +use App\Actions\UpdateReleasesJson; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use App\Console\Command\PhpCommand; @@ -94,7 +96,7 @@ private function stageBuilds(array $phpZips, $zipPath): void #[DataProvider('buildsProvider')] public function testCommandHandlesSuccessfulExecution(array $phpZips): void { - $command = new PhpCommand(); + $command = new PhpCommand((new GetListing()), (new UpdateReleasesJson())); $command->setOption('base-directory', $this->baseDirectory); $command->setOption('builds-directory', $this->buildsDirectory); @@ -110,7 +112,7 @@ public function testCommandHandlesSuccessfulExecution(array $phpZips): void public function testCommandHandlerWithMissingTestPackZip(): void { - $command = new PhpCommand(); + $command = new PhpCommand(new GetListing(), new UpdateReleasesJson()); $command->setOption('base-directory', $this->baseDirectory); $command->setOption('builds-directory', $this->buildsDirectory); @@ -124,7 +126,7 @@ public function testCommandHandlerWithMissingTestPackZip(): void public function testCommandHandlesMissingBaseDirectory(): void { - $command = new PhpCommand(); + $command = new PhpCommand(new GetListing(), new UpdateReleasesJson()); ob_start(); $result = $command->handle(); $output = ob_get_clean(); @@ -136,7 +138,7 @@ public function testFailsToOpenZip(): void { $zipPath = $this->buildsDirectory . '/php/broken.zip'; file_put_contents($zipPath, "invalid zip content"); - $command = new PhpCommand(); + $command = new PhpCommand(new GetListing(), new UpdateReleasesJson()); $command->setOption('base-directory', $this->baseDirectory); $command->setOption('builds-directory', $this->buildsDirectory); ob_start(); @@ -147,7 +149,7 @@ public function testFailsToOpenZip(): void public function testCleanupAfterCommand(): void { - $command = new PhpCommand(); + $command = new PhpCommand(new GetListing(), new UpdateReleasesJson()); $command->setOption('base-directory', $this->baseDirectory); $command->setOption('builds-directory', $this->buildsDirectory); $command->handle();