[web-downloads] main: Add support to delete and promote series files to stable

From: Date: Thu, 25 Sep 2025 20:05:00 +0000
Subject: [web-downloads] main: Add support to delete and promote series files to stable
Groups: php.webmaster 
Request: Send a blank email to php-webmaster+get-33229@lists.php.net to get a copy of this message
Author: Shivam Mathur (shivammathur) Date: 2025-09-26T01:34:52+05:30 Commit: https://github.com/php/web-downloads/commit/66fe29e20afc67287231bbdf6a8629ae13c3d4c1 Raw diff: https://github.com/php/web-downloads/commit/66fe29e20afc67287231bbdf6a8629ae13c3d4c1.diff Add support to delete and promote series files to stable Changed paths: A .github/workflows/series-delete.yml A .github/workflows/series-stability.yml A src/Console/Command/SeriesDeleteCommand.php A src/Console/Command/SeriesStabilityCommand.php A src/Http/Controllers/SeriesDeleteController.php A src/Http/Controllers/SeriesStabilityController.php A tests/Console/Command/SeriesDeleteCommandTest.php A tests/Console/Command/SeriesStabilityCommandTest.php A tests/Http/Controllers/SeriesDeleteControllerTest.php A tests/Http/Controllers/SeriesStabilityControllerTest.php M API.md M routes.php Diff: diff --git a/.github/workflows/series-delete.yml b/.github/workflows/series-delete.yml new file mode 100644 index 0000000..1d3b605 --- /dev/null +++ b/.github/workflows/series-delete.yml @@ -0,0 +1,23 @@ +name: Delete series files +run-name: Series delete ${{ inputs.php_version }} +on: + workflow_dispatch: + inputs: + php_version: + description: 'PHP Version' + required: true + vs_version: + description: 'VS Version' + required: true +jobs: + test: + runs-on: ubuntu-latest + environment: downloads.php.net + steps: + - name: Run + run: | + curl \ + --request POST \ + --location https://downloads.php.net/api/series-delete \ + --header 'Authorization: Bearer ${{ secrets.AUTH_TOKEN }}' \ + --data '{ "php_version": "${{ inputs.php_version }}", "vs_version": "${{ inputs.vs_version }}" }' diff --git a/.github/workflows/series-stability.yml b/.github/workflows/series-stability.yml new file mode 100644 index 0000000..cfc41c6 --- /dev/null +++ b/.github/workflows/series-stability.yml @@ -0,0 +1,23 @@ +name: Promote series files to stable +run-name: Series stability ${{ inputs.php_version }} +on: + workflow_dispatch: + inputs: + php_version: + description: 'PHP Version' + required: true + vs_version: + description: 'VS Version' + required: true +jobs: + test: + runs-on: ubuntu-latest + environment: downloads.php.net + steps: + - name: Run + run: | + curl \ + --request POST \ + --location https://downloads.php.net/api/series-stability \ + --header 'Authorization: Bearer ${{ secrets.AUTH_TOKEN }}' \ + --data '{ "php_version": "${{ inputs.php_version }}", "vs_version": "${{ inputs.vs_version }}" }' diff --git a/API.md b/API.md index 0bdcbb8..1bc0735 100644 --- a/API.md +++ b/API.md @@ -144,3 +144,53 @@ curl -i -X POST \ }' \ https://downloads.php.net/api/series-init ``` + +--- + +### POST /api/series-delete + +- Auth: Required +- Purpose: Queue deletion of existing series files. +- Request body (JSON): + - php_version (string, required): Matches ^(\d+\.\d+|master)$. + - vs_version (string, required): Matches ^v[c|s]\d{2}$. +- Success: 200 OK, empty body. +- Errors: 400 with validation details if input is invalid. + +Example + +```bash +curl -i -X POST \ + -H "Authorization: Bearer $AUTH_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "php_version": "8.3", + "vs_version": "vs16" + }' \ + https://downloads.php.net/api/series-delete +``` + +--- + +### POST /api/series-stability + +- Auth: Required +- Purpose: Promote the staging series files to stable. +- Request body (JSON): + - php_version (string, required): Matches ^(\d+\.\d+|master)$. + - vs_version (string, required): Matches ^v[c|s]\d{2}$. +- Success: 200 OK, empty body. +- Errors: 400 with validation details if input is invalid. + +Example + +```bash +curl -i -X POST \ + -H "Authorization: Bearer $AUTH_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "php_version": "8.3", + "vs_version": "vs16" + }' \ + https://downloads.php.net/api/series-stability +``` diff --git a/routes.php b/routes.php index d8a5471..fb590a3 100644 --- a/routes.php +++ b/routes.php @@ -3,7 +3,9 @@ use App\Http\Controllers\IndexController; use App\Http\Controllers\PeclController; use App\Http\Controllers\PhpController; +use App\Http\Controllers\SeriesDeleteController; use App\Http\Controllers\SeriesInitController; +use App\Http\Controllers\SeriesStabilityController; use App\Http\Controllers\WinlibsController; use App\Router; @@ -13,4 +15,6 @@ $router->registerRoute('/api/winlibs', 'POST', WinlibsController::class, true); $router->registerRoute('/api/php', 'POST', PhpController::class, true); $router->registerRoute('/api/series-init', 'POST', SeriesInitController::class, true); +$router->registerRoute('/api/series-delete', 'POST', SeriesDeleteController::class, true); +$router->registerRoute('/api/series-stability', 'POST', SeriesStabilityController::class, true); $router->handleRequest(); diff --git a/src/Console/Command/SeriesDeleteCommand.php b/src/Console/Command/SeriesDeleteCommand.php new file mode 100644 index 0000000..d032dfb --- /dev/null +++ b/src/Console/Command/SeriesDeleteCommand.php @@ -0,0 +1,82 @@ +<?php + +namespace App\Console\Command; + +use App\Console\Command; +use Exception; + +class SeriesDeleteCommand extends Command +{ + protected string $signature = 'series:delete --base-directory= --builds-directory='; + protected string $description = 'Delete series files for libraries'; + + protected ?string $baseDirectory = null; + + public function handle(): int + { + try { + $this->baseDirectory = $this->getOption('base-directory'); + if (!$this->baseDirectory) { + throw new Exception('Base directory is required'); + } + + $buildsDirectory = $this->getOption('builds-directory'); + if (!$buildsDirectory) { + throw new Exception('Build directory is required'); + } + + $series_directory = $buildsDirectory . '/series'; + if(!is_dir($series_directory)) { + return Command::SUCCESS; + } + + $files = glob($series_directory . '/series-delete-*.json'); + + // We lock the files we are working on + // so that we don't process them again if the command is run again + $filteredFiles = []; + foreach ($files as $filepath) { + $lockFile = $filepath . '.lock'; + if (!file_exists($lockFile)) { + touch($lockFile); + $filteredFiles[] = $filepath; + } + } + + foreach ($filteredFiles as $filepath) { + $data = json_decode(file_get_contents($filepath), true, 512, JSON_THROW_ON_ERROR); + extract($data); + $this->deleteSeriesFiles($php_version, $vs_version); + unlink($filepath); + unlink($filepath . '.lock'); + } + return Command::SUCCESS; + } catch (Exception $e) { + echo $e->getMessage(); + return Command::FAILURE; + } + } + + /** + * @throws Exception + */ + private function deleteSeriesFiles( + string $php_version, + string $vs_version, + ): void + { + $baseDirectory = $this->baseDirectory . "/php-sdk/deps/series"; + + if (!is_dir($baseDirectory)) { + mkdir($baseDirectory, 0755, true); + } + foreach(['x86', 'x64'] as $arch) { + foreach(['stable', 'staging'] as $stability) { + $filePath = $baseDirectory . '/packages-' . $php_version . '-' . $vs_version . '-' . $arch . '-' . $stability . '.txt'; + if(file_exists($filePath)) { + unlink($filePath); + } + } + } + } +} \ No newline at end of file diff --git a/src/Console/Command/SeriesStabilityCommand.php b/src/Console/Command/SeriesStabilityCommand.php new file mode 100644 index 0000000..362cbca --- /dev/null +++ b/src/Console/Command/SeriesStabilityCommand.php @@ -0,0 +1,82 @@ +<?php + +namespace App\Console\Command; + +use App\Console\Command; +use Exception; + +class SeriesStabilityCommand extends Command +{ + protected string $signature = 'series:stability --base-directory= --builds-directory='; + protected string $description = 'Promote staging series files to stable for libraries'; + + protected ?string $baseDirectory = null; + + public function handle(): int + { + try { + $this->baseDirectory = $this->getOption('base-directory'); + if (!$this->baseDirectory) { + throw new Exception('Base directory is required'); + } + + $buildsDirectory = $this->getOption('builds-directory'); + if (!$buildsDirectory) { + throw new Exception('Build directory is required'); + } + + $series_directory = $buildsDirectory . '/series'; + if(!is_dir($series_directory)) { + return Command::SUCCESS; + } + + $files = glob($series_directory . '/series-stability-*.json'); + + // We lock the files we are working on + // so that we don't process them again if the command is run again + $filteredFiles = []; + foreach ($files as $filepath) { + $lockFile = $filepath . '.lock'; + if (!file_exists($lockFile)) { + touch($lockFile); + $filteredFiles[] = $filepath; + } + } + + foreach ($filteredFiles as $filepath) { + $data = json_decode(file_get_contents($filepath), true, 512, JSON_THROW_ON_ERROR); + extract($data); + $this->promoteSeriesFiles($php_version, $vs_version); + unlink($filepath); + unlink($filepath . '.lock'); + } + return Command::SUCCESS; + } catch (Exception $e) { + echo $e->getMessage(); + return Command::FAILURE; + } + } + + /** + * @throws Exception + */ + private function promoteSeriesFiles( + string $php_version, + string $vs_version, + ): void + { + $baseDirectory = $this->baseDirectory . "/php-sdk/deps/series"; + + if (!is_dir($baseDirectory)) { + mkdir($baseDirectory, 0755, true); + } + foreach(['x86', 'x64'] as $arch) { + $sourceFile = $baseDirectory . '/packages-' . $php_version . '-' . $vs_version . '-' . $arch . '-staging.txt'; + $destinationFile = $baseDirectory . '/packages-' . $php_version . '-' . $vs_version . '-' . $arch . '-stable.txt'; + if(!file_exists($sourceFile)) { + throw new Exception($sourceFile . ' does not exist'); + } + copy($sourceFile, $destinationFile); + } + } +} \ No newline at end of file diff --git a/src/Http/Controllers/SeriesDeleteController.php b/src/Http/Controllers/SeriesDeleteController.php new file mode 100644 index 0000000..0f54c0a --- /dev/null +++ b/src/Http/Controllers/SeriesDeleteController.php @@ -0,0 +1,37 @@ +<?php + +namespace App\Http\Controllers; + +use App\Http\BaseController; +use App\Validator; + +class SeriesDeleteController extends BaseController +{ + protected function validate(array $data): bool + { + $validator = new Validator([ + 'php_version' => 'required|string:regex:/^(\d+\.\d+|master)$/', + 'vs_version' => 'required|string|regex:/^v[c|s]\d{2}$/', + ]); + + $validator->validate($data); + + $valid = $validator->isValid(); + + if (!$valid) { + http_response_code(400); + echo 'Invalid request: ' . $validator; + } + + return $valid; + } + + protected function execute(array $data): void + { + extract($data); + $directory = getenv('BUILDS_DIRECTORY') . '/series'; + $hash = hash('sha256', $series) . strtotime('now'); + $file = $directory . '/series-delete-' . $hash . '.json'; + file_put_contents($file, json_encode($data)); + } +} \ No newline at end of file diff --git a/src/Http/Controllers/SeriesStabilityController.php b/src/Http/Controllers/SeriesStabilityController.php new file mode 100644 index 0000000..9b56840 --- /dev/null +++ b/src/Http/Controllers/SeriesStabilityController.php @@ -0,0 +1,37 @@ +<?php + +namespace App\Http\Controllers; + +use App\Http\BaseController; +use App\Validator; + +class SeriesStabilityController extends BaseController +{ + protected function validate(array $data): bool + { + $validator = new Validator([ + 'php_version' => 'required|string:regex:/^(\d+\.\d+|master)$/', + 'vs_version' => 'required|string|regex:/^v[c|s]\d{2}$/', + ]); + + $validator->validate($data); + + $valid = $validator->isValid(); + + if (!$valid) { + http_response_code(400); + echo 'Invalid request: ' . $validator; + } + + return $valid; + } + + protected function execute(array $data): void + { + extract($data); + $directory = getenv('BUILDS_DIRECTORY') . '/series'; + $hash = hash('sha256', $series) . strtotime('now'); + $file = $directory . '/series-stability-' . $hash . '.json'; + file_put_contents($file, json_encode($data)); + } +} \ No newline at end of file diff --git a/tests/Console/Command/SeriesDeleteCommandTest.php b/tests/Console/Command/SeriesDeleteCommandTest.php new file mode 100644 index 0000000..fbca7f2 --- /dev/null +++ b/tests/Console/Command/SeriesDeleteCommandTest.php @@ -0,0 +1,230 @@ +<?php + +namespace Console\Command; + +use App\Console\Command\SeriesDeleteCommand; +use App\Helpers\Helpers; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; + +class SeriesDeleteCommandTest extends TestCase +{ + private string $baseDirectory; + private string $buildsDirectory; + + protected function setUp(): void + { + parent::setUp(); + $this->baseDirectory = sys_get_temp_dir() . '/series_delete_base_' . uniqid(); + $this->buildsDirectory = sys_get_temp_dir() . '/series_delete_builds_' . uniqid(); + + mkdir($this->baseDirectory, 0755, true); + mkdir($this->buildsDirectory, 0755, true); + } + + protected function tearDown(): void + { + (new Helpers)->rmdirr($this->baseDirectory); + (new Helpers)->rmdirr($this->buildsDirectory); + parent::tearDown(); + } + + public static function versionProvider(): array + { + return [ + ['8.3', 'vs17'], + ['8.2', 'vs16'], + ]; + } + + public function testReturnsSuccessWhenNoSeriesDir(): void + { + $command = new SeriesDeleteCommand(); + $command->setOption('base-directory', $this->baseDirectory); + $command->setOption('builds-directory', $this->buildsDirectory); + + $result = $command->handle(); + + $this->assertSame(0, $result, 'Should return success when there is no builds/series directory.'); + } + + public function testMissingBaseDirectory(): void + { + $command = new SeriesDeleteCommand(); + $command->setOption('builds-directory', $this->buildsDirectory); + + ob_start(); + $result = $command->handle(); + $output = trim(ob_get_clean()); + + $this->assertSame(1, $result); + $this->assertSame('Base directory is required', $output); + } + + public function testMissingBuildsDirectory(): void + { + $command = new SeriesDeleteCommand(); + $command->setOption('base-directory', $this->baseDirectory); + + ob_start(); + $result = $command->handle(); + $output = trim(ob_get_clean()); + + $this->assertSame(1, $result); + $this->assertSame('Build directory is required', $output); + } + + #[DataProvider('versionProvider')] + public function testDeletesAllSeriesFilesAndCleansUpTask(string $phpVersion, string $vsVersion): void + { + $seriesBase = $this->baseDirectory . '/php-sdk/deps/series'; + mkdir($seriesBase, 0755, true); + + $paths = [ + "$seriesBase/packages-$phpVersion-$vsVersion-x86-stable.txt", + "$seriesBase/packages-$phpVersion-$vsVersion-x86-staging.txt", + "$seriesBase/packages-$phpVersion-$vsVersion-x64-stable.txt", + "$seriesBase/packages-$phpVersion-$vsVersion-x64-staging.txt", + ]; + foreach ($paths as $p) { + file_put_contents($p, "dummy"); + } + + $seriesDir = $this->buildsDirectory . '/series'; + mkdir($seriesDir, 0755, true); + $jsonPath = $seriesDir . '/series-delete-task1.json'; + file_put_contents($jsonPath, json_encode([ + 'php_version' => $phpVersion, + 'vs_version' => $vsVersion, + ])); + + clearstatcache(true); + + $command = new SeriesDeleteCommand(); + $command->setOption('base-directory', $this->baseDirectory); + $command->setOption('builds-directory', $this->buildsDirectory); + + $result = $command->handle(); + $this->assertSame(0, $result, 'Command should return success.'); + + foreach ($paths as $p) { + $this->assertFileDoesNotExist($p); + } + + $this->assertFileDoesNotExist($jsonPath); + $this->assertFileDoesNotExist($jsonPath . '.lock'); + } + + #[DataProvider('versionProvider')] + public function testIdempotentWhenSomeFilesMissing(string $phpVersion, string $vsVersion): void + { + $seriesBase = $this->baseDirectory . '/php-sdk/deps/series'; + mkdir($seriesBase, 0755, true); + + $existing = [ + "$seriesBase/packages-$phpVersion-$vsVersion-x86-stable.txt", + "$seriesBase/packages-$phpVersion-$vsVersion-x64-staging.txt", + ]; + foreach ($existing as $p) { + file_put_contents($p, "exists"); + } + $missing = [ + "$seriesBase/packages-$phpVersion-$vsVersion-x86-staging.txt", + "$seriesBase/packages-$phpVersion-$vsVersion-x64-stable.txt", + ]; + + $seriesDir = $this->buildsDirectory . '/series'; + mkdir($seriesDir, 0755, true); + $jsonPath = $seriesDir . '/series-delete-task2.json'; + file_put_contents($jsonPath, json_encode([ + 'php_version' => $phpVersion, + 'vs_version' => $vsVersion, + ])); + + clearstatcache(true); + + $command = new SeriesDeleteCommand(); + $command->setOption('base-directory', $this->baseDirectory); + $command->setOption('builds-directory', $this->buildsDirectory); + + $result = $command->handle(); + $this->assertSame(0, $result); + + foreach ($existing as $p) { + $this->assertFileDoesNotExist($p); + } + foreach ($missing as $p) { + $this->assertFileDoesNotExist($p); + } + + $this->assertFileDoesNotExist($jsonPath); + $this->assertFileDoesNotExist($jsonPath . '.lock'); + } + + public function testSkipsLockedJsonFile(): void + { + $phpVersion = '8.3'; + $vsVersion = 'vs17'; + + $seriesBase = $this->baseDirectory . '/php-sdk/deps/series'; + mkdir($seriesBase, 0755, true); + + $target = "$seriesBase/packages-$phpVersion-$vsVersion-x86-stable.txt"; + file_put_contents($target, 'keep'); + + $seriesDir = $this->buildsDirectory . '/series'; + mkdir($seriesDir, 0755, true); + $jsonPath = $seriesDir . '/series-delete-locked.json'; + file_put_contents($jsonPath, json_encode([ + 'php_version' => $phpVersion, + 'vs_version' => $vsVersion, + ])); + touch($jsonPath . '.lock'); + + clearstatcache(true); + + $command = new SeriesDeleteCommand(); + $command->setOption('base-directory', $this->baseDirectory); + $command->setOption('builds-directory', $this->buildsDirectory); + + $result = $command->handle(); + $this->assertSame(0, $result); + + $this->assertFileExists($jsonPath); + + $this->assertFileExists($target); + } + + public function testHandlesCorruptJson(): void + { + $phpVersion = '8.3'; + $vsVersion = 'vs17'; + + $seriesBase = $this->baseDirectory . '/php-sdk/deps/series'; + mkdir($seriesBase, 0755, true); + + $target = "$seriesBase/packages-$phpVersion-$vsVersion-x86-stable.txt"; + file_put_contents($target, 'data'); + + $seriesDir = $this->buildsDirectory . '/series'; + mkdir($seriesDir, 0755, true); + $jsonPath = $seriesDir . '/series-delete-bad.json'; + file_put_contents($jsonPath, '{corrupt json'); + + $command = new SeriesDeleteCommand(); + $command->setOption('base-directory', $this->baseDirectory); + $command->setOption('builds-directory', $this->buildsDirectory); + + ob_start(); + $result = $command->handle(); + $output = ob_get_clean(); + + $this->assertSame(1, $result, 'Corrupt JSON should return FAILURE.'); + $this->assertStringContainsString('Syntax error', $output); + + $this->assertFileExists($jsonPath); + $this->assertFileExists($jsonPath . '.lock'); + + $this->assertFileExists($target); + } +} diff --git a/tests/Console/Command/SeriesStabilityCommandTest.php b/tests/Console/Command/SeriesStabilityCommandTest.php new file mode 100644 index 0000000..cf186a6 --- /dev/null +++ b/tests/Console/Command/SeriesStabilityCommandTest.php @@ -0,0 +1,201 @@ +<?php + +namespace Console\Command; + +use App\Console\Command\SeriesStabilityCommand; +use App\Helpers\Helpers; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; + +class SeriesStabilityCommandTest extends TestCase +{ + private string $baseDirectory; + private string $buildsDirectory; + + protected function setUp(): void + { + parent::setUp(); + $this->baseDirectory = sys_get_temp_dir() . '/series_stability_base_' . uniqid(); + $this->buildsDirectory = sys_get_temp_dir() . '/series_stability_builds_' . uniqid(); + + mkdir($this->baseDirectory, 0755, true); + mkdir($this->buildsDirectory, 0755, true); + } + + protected function tearDown(): void + { + (new Helpers)->rmdirr($this->baseDirectory); + (new Helpers)->rmdirr($this->buildsDirectory); + parent::tearDown(); + } + + public static function versionProvider(): array + { + return [ + ['8.3', 'vs17'], + ['8.2', 'vs16'], + ]; + } + + public function testReturnsSuccessWhenNoSeriesDir(): void + { + $command = new SeriesStabilityCommand(); + $command->setOption('base-directory', $this->baseDirectory); + $command->setOption('builds-directory', $this->buildsDirectory); + + $result = $command->handle(); + + $this->assertSame(0, $result); + } + + public function testMissingBaseDirectory(): void + { + $command = new SeriesStabilityCommand(); + $command->setOption('builds-directory', $this->buildsDirectory); + + ob_start(); + $result = $command->handle(); + $output = trim(ob_get_clean()); + + $this->assertSame(1, $result); + $this->assertSame('Base directory is required', $output); + } + + public function testMissingBuildsDirectory(): void + { + $command = new SeriesStabilityCommand(); + $command->setOption('base-directory', $this->baseDirectory); + + ob_start(); + $result = $command->handle(); + $output = trim(ob_get_clean()); + + $this->assertSame(1, $result); + $this->assertSame('Build directory is required', $output); + } + + #[DataProvider('versionProvider')] + public function testPromotesStagingToStableAndCleansTask(string $phpVersion, string $vsVersion): void + { + $seriesBase = $this->baseDirectory . '/php-sdk/deps/series'; + mkdir($seriesBase, 0755, true); + + $srcX86 = "$seriesBase/packages-$phpVersion-$vsVersion-x86-staging.txt"; + $srcX64 = "$seriesBase/packages-$phpVersion-$vsVersion-x64-staging.txt"; + file_put_contents($srcX86, "x86-content"); + file_put_contents($srcX64, "x64-content"); + + $seriesDir = $this->buildsDirectory . '/series'; + mkdir($seriesDir, 0755, true); + $jsonPath = $seriesDir . '/series-stability-task1.json'; + file_put_contents($jsonPath, json_encode([ + 'php_version' => $phpVersion, + 'vs_version' => $vsVersion, + ])); + + $command = new SeriesStabilityCommand(); + $command->setOption('base-directory', $this->baseDirectory); + $command->setOption('builds-directory', $this->buildsDirectory); + + $result = $command->handle(); + $this->assertSame(0, $result); + + $dstX86 = "$seriesBase/packages-$phpVersion-$vsVersion-x86-stable.txt"; + $dstX64 = "$seriesBase/packages-$phpVersion-$vsVersion-x64-stable.txt"; + $this->assertFileExists($dstX86); + $this->assertFileExists($dstX64); + $this->assertSame("x86-content", file_get_contents($dstX86)); + $this->assertSame("x64-content", file_get_contents($dstX64)); + + $this->assertFileDoesNotExist($jsonPath); + $this->assertFileDoesNotExist($jsonPath . '.lock'); + } + + public function testSkipsLockedJsonFile(): void + { + $phpVersion = '8.3'; + $vsVersion = 'vs17'; + + $seriesBase = $this->baseDirectory . '/php-sdk/deps/series'; + mkdir($seriesBase, 0755, true); + file_put_contents("$seriesBase/packages-$phpVersion-$vsVersion-x86-staging.txt", "x86"); + file_put_contents("$seriesBase/packages-$phpVersion-$vsVersion-x64-staging.txt", "x64"); + + $seriesDir = $this->buildsDirectory . '/series'; + mkdir($seriesDir, 0755, true); + $jsonPath = $seriesDir . '/series-stability-locked.json'; + file_put_contents($jsonPath, json_encode([ + 'php_version' => $phpVersion, + 'vs_version' => $vsVersion, + ])); + touch($jsonPath . '.lock'); + + $command = new SeriesStabilityCommand(); + $command->setOption('base-directory', $this->baseDirectory); + $command->setOption('builds-directory', $this->buildsDirectory); + + $result = $command->handle(); + $this->assertSame(0, $result); + + $this->assertFileExists($jsonPath); + $this->assertFileExists($jsonPath . '.lock'); + $this->assertFileDoesNotExist("$seriesBase/packages-$phpVersion-$vsVersion-x86-stable.txt"); + $this->assertFileDoesNotExist("$seriesBase/packages-$phpVersion-$vsVersion-x64-stable.txt"); + } + + public function testHandlesCorruptJson(): void + { + $seriesDir = $this->buildsDirectory . '/series'; + mkdir($seriesDir, 0755, true); + $jsonPath = $seriesDir . '/series-stability-bad.json'; + file_put_contents($jsonPath, '{corrupt json'); + + $command = new SeriesStabilityCommand(); + $command->setOption('base-directory', $this->baseDirectory); + $command->setOption('builds-directory', $this->buildsDirectory); + + ob_start(); + $result = $command->handle(); + $output = ob_get_clean(); + + $this->assertSame(1, $result); + $this->assertStringContainsString('Syntax error', $output); + $this->assertFileExists($jsonPath); + $this->assertFileExists($jsonPath . '.lock'); + } + + public function testFailsWhenAnySourceStagingFileIsMissing(): void + { + $phpVersion = '8.3'; + $vsVersion = 'vs17'; + + $seriesBase = $this->baseDirectory . '/php-sdk/deps/series'; + mkdir($seriesBase, 0755, true); + file_put_contents("$seriesBase/packages-$phpVersion-$vsVersion-x86-staging.txt", "x86"); + + $seriesDir = $this->buildsDirectory . '/series'; + mkdir($seriesDir, 0755, true); + $jsonPath = $seriesDir . '/series-stability-task.json'; + file_put_contents($jsonPath, json_encode([ + 'php_version' => $phpVersion, + 'vs_version' => $vsVersion, + ])); + + $command = new SeriesStabilityCommand(); + $command->setOption('base-directory', $this->baseDirectory); + $command->setOption('builds-directory', $this->buildsDirectory); + + ob_start(); + $result = $command->handle(); + $output = trim(ob_get_clean()); + + $this->assertSame(1, $result); + $this->assertStringContainsString("$seriesBase/packages-$phpVersion-$vsVersion-x64-staging.txt does not exist", $output); + + $this->assertFileExists($jsonPath); + $this->assertFileExists($jsonPath . '.lock'); + $this->assertFileExists("$seriesBase/packages-$phpVersion-$vsVersion-x86-staging.txt"); + $this->assertFileExists("$seriesBase/packages-$phpVersion-$vsVersion-x86-stable.txt"); + $this->assertFileDoesNotExist("$seriesBase/packages-$phpVersion-$vsVersion-x64-stable.txt"); + } +} diff --git a/tests/Http/Controllers/SeriesDeleteControllerTest.php b/tests/Http/Controllers/SeriesDeleteControllerTest.php new file mode 100644 index 0000000..23fe8cb --- /dev/null +++ b/tests/Http/Controllers/SeriesDeleteControllerTest.php @@ -0,0 +1,37 @@ +<?php + +namespace Http\Controllers; + +use App\Http\Controllers\SeriesDeleteController; +use PHPUnit\Framework\TestCase; + +class MockSeriesDeleteController extends SeriesDeleteController { + protected function validate(array $data): bool { + return isset($data['key']); + } + + protected function execute(array $data): void { + echo "Executed"; + } + + public function handle(): void + { + $data = json_decode(file_get_contents($this->inputPath), true); + + if ($this->validate($data)) { + $this->execute($data); + } + } +} + +class SeriesDeleteControllerTest extends TestCase { + public function testHandleWithValidData() { + $data = json_encode(["key" => "value"]); + $tempFile = tempnam(sys_get_temp_dir(), 'phpunit'); + file_put_contents($tempFile, $data); + $controller = new MockSeriesDeleteController($tempFile); + $this->expectOutputString("Executed"); + $controller->handle(); + unlink($tempFile); + } +} diff --git a/tests/Http/Controllers/SeriesStabilityControllerTest.php b/tests/Http/Controllers/SeriesStabilityControllerTest.php new file mode 100644 index 0000000..45e69ca --- /dev/null +++ b/tests/Http/Controllers/SeriesStabilityControllerTest.php @@ -0,0 +1,38 @@ +<?php + +namespace Http\Controllers; + +use App\Http\Controllers\SeriesInitController; +use App\Http\Controllers\SeriesStabilityController; +use PHPUnit\Framework\TestCase; + +class MockSeriesStabilityController extends SeriesStabilityController { + protected function validate(array $data): bool { + return isset($data['key']); + } + + protected function execute(array $data): void { + echo "Executed"; + } + + public function handle(): void + { + $data = json_decode(file_get_contents($this->inputPath), true); + + if ($this->validate($data)) { + $this->execute($data); + } + } +} + +class SeriesStabilityControllerTest extends TestCase { + public function testHandleWithValidData() { + $data = json_encode(["key" => "value"]); + $tempFile = tempnam(sys_get_temp_dir(), 'phpunit'); + file_put_contents($tempFile, $data); + $controller = new MockSeriesStabilityController($tempFile); + $this->expectOutputString("Executed"); + $controller->handle(); + unlink($tempFile); + } +}

« previous php.webmaster (#33229) next »