Author: Shivam Mathur (shivammathur) Committer: GitHub (web-flow) Pusher: shivammathur Date: 2025-09-24T19:43:54+05:30
Commit: https://github.com/php/web-downloads/commit/840e683a6dd3b85f71604134b2f4346368ff4e20 Raw diff: https://github.com/php/web-downloads/commit/840e683a6dd3b85f71604134b2f4346368ff4e20.diff Add API to init series files (#4) Changed paths: A src/Console/Command/SeriesInitCommand.php A src/Http/Controllers/SeriesInitController.php A tests/Console/Command/SeriesInitCommandTest.php A tests/Http/Controllers/SeriesInitControllerTest.php M routes.php Diff: diff --git a/routes.php b/routes.php index 7491aa7..d8a5471 100644 --- a/routes.php +++ b/routes.php @@ -3,6 +3,7 @@ use App\Http\Controllers\IndexController; use App\Http\Controllers\PeclController; use App\Http\Controllers\PhpController; +use App\Http\Controllers\SeriesInitController; use App\Http\Controllers\WinlibsController; use App\Router; @@ -11,4 +12,5 @@ $router->registerRoute('/api/pecl', 'POST', PeclController::class, true); $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->handleRequest(); diff --git a/src/Console/Command/SeriesInitCommand.php b/src/Console/Command/SeriesInitCommand.php new file mode 100644 index 0000000..6d6405d --- /dev/null +++ b/src/Console/Command/SeriesInitCommand.php @@ -0,0 +1,85 @@ +<?php + +namespace App\Console\Command; + +use App\Console\Command; +use Exception; + +class SeriesInitCommand extends Command +{ + protected string $signature = 'series:init --base-directory= --builds-directory='; + protected string $description = 'Initialize 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-init-*.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->initSeriesFiles($series, $series_vs, $target_vs); + unlink($filepath); + unlink($filepath . '.lock'); + } + return Command::SUCCESS; + } catch (Exception $e) { + echo $e->getMessage(); + return Command::FAILURE; + } + } + + /** + * @throws Exception + */ + private function initSeriesFiles( + string $series, + string $series_vs, + string $target_vs + ): 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) { + $sourceSeries = 'packages-master-' . $series_vs . '-' . $arch . '-' . $stability . '.txt'; + if(!file_exists($baseDirectory . '/' . $sourceSeries)) { + throw new Exception("$baseDirectory/$sourceSeries not found"); + } + $destinationFileName = 'packages-' . $series . '-' . $target_vs . '-' . $arch . '-' . $stability . '.txt'; + copy($baseDirectory . '/' . $sourceSeries, $baseDirectory . '/' . $destinationFileName); + } + } + } +} \ No newline at end of file diff --git a/src/Http/Controllers/SeriesInitController.php b/src/Http/Controllers/SeriesInitController.php new file mode 100644 index 0000000..eeab09b --- /dev/null +++ b/src/Http/Controllers/SeriesInitController.php @@ -0,0 +1,39 @@ +<?php + +namespace App\Http\Controllers; + +use App\Http\BaseController; +use App\Validator; + +class SeriesInitController extends BaseController +{ + protected function validate(array $data): bool + { + $validator = new Validator([ + 'series' => 'required|string', + 'series_vs' => 'required|string|regex:/^v[c|s]\d{2}$/', + 'target_vs' => 'required|string|regex:/^v[c|s]\d{2}$/', + 'token' => 'required|string', + ]); + + $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-init-' . $hash . '.json'; + file_put_contents($file, json_encode($data)); + } +} \ No newline at end of file diff --git a/tests/Console/Command/SeriesInitCommandTest.php b/tests/Console/Command/SeriesInitCommandTest.php new file mode 100644 index 0000000..8b39e8f --- /dev/null +++ b/tests/Console/Command/SeriesInitCommandTest.php @@ -0,0 +1,238 @@ +<?php + +namespace Console\Command; + +use App\Console\Command\SeriesInitCommand; +use App\Helpers\Helpers; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; + +class SeriesInitCommandTest extends TestCase +{ + private string $baseDirectory; + private string $buildsDirectory; + + protected function setUp(): void + { + parent::setUp(); + $this->baseDirectory = sys_get_temp_dir() . '/series_init_base_' . uniqid(); + $this->buildsDirectory = sys_get_temp_dir() . '/series_init_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 caseProvider(): array + { + return [ + ['8.3', 'vs17', 'vs17'], + ['8.2', 'vs16', 'vs16'], + ]; + } + + public function testReturnsSuccessWhenNoSeriesDir(): void + { + $command = new SeriesInitCommand(); + $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 SeriesInitCommand(); + $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 SeriesInitCommand(); + $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('caseProvider')] + public function testProcessesJsonCopiesSeriesFilesAndCleansUp(string $series, string $series_vs, string $target_vs): void + { + $seriesBase = $this->baseDirectory . '/php-sdk/deps/series'; + mkdir($seriesBase, 0755, true); + + $sourceNames = [ + "packages-master-$series_vs-x86-stable.txt", + "packages-master-$series_vs-x86-staging.txt", + "packages-master-$series_vs-x64-stable.txt", + "packages-master-$series_vs-x64-staging.txt", + ]; + foreach ($sourceNames as $name) { + file_put_contents($seriesBase . '/' . $name, "content-of-$name"); + } + + $buildSeriesDir = $this->buildsDirectory . '/series'; + mkdir($buildSeriesDir, 0755, true); + $jsonPath = $buildSeriesDir . '/series-init-task1.json'; + file_put_contents($jsonPath, json_encode([ + 'series' => $series, + 'series_vs' => $series_vs, + 'target_vs' => $target_vs, + ])); + + $command = new SeriesInitCommand(); + $command->setOption('base-directory', $this->baseDirectory); + $command->setOption('builds-directory', $this->buildsDirectory); + + $result = $command->handle(); + $this->assertSame(0, $result, 'Command should return success.'); + + $destinations = [ + "packages-$series-$target_vs-x86-stable.txt", + "packages-$series-$target_vs-x86-staging.txt", + "packages-$series-$target_vs-x64-stable.txt", + "packages-$series-$target_vs-x64-staging.txt", + ]; + + foreach ($destinations as $dest) { + $destPath = $seriesBase . '/' . $dest; + $this->assertFileExists($destPath); + $srcName = str_replace("packages-$series-$target_vs", "packages-master-$series_vs", $dest); + $this->assertSame( + "content-of-$srcName", + file_get_contents($destPath), + "Destination $dest should have the same content as $srcName" + ); + } + + $this->assertFileDoesNotExist($jsonPath); + $this->assertFileDoesNotExist($jsonPath . '.lock'); + } + + public function testSkipsLockedJsonFile(): void + { + $series = '8.3'; + $series_vs = 'vs17'; + $target_vs = 'vs17'; + + $seriesBase = $this->baseDirectory . '/php-sdk/deps/series'; + mkdir($seriesBase, 0755, true); + foreach ([ + "packages-master-$series_vs-x86-stable.txt", + "packages-master-$series_vs-x86-staging.txt", + "packages-master-$series_vs-x64-stable.txt", + "packages-master-$series_vs-x64-staging.txt", + ] as $name) { + file_put_contents($seriesBase . '/' . $name, "ok"); + } + + $buildSeriesDir = $this->buildsDirectory . '/series'; + mkdir($buildSeriesDir, 0755, true); + $jsonPath = $buildSeriesDir . '/series-init-locked.json'; + file_put_contents($jsonPath, json_encode([ + 'series' => $series, + 'series_vs' => $series_vs, + 'target_vs' => $target_vs, + ])); + touch($jsonPath . '.lock'); + + $command = new SeriesInitCommand(); + $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-$series-$target_vs-x86-stable.txt"); + $this->assertFileDoesNotExist($seriesBase . "/packages-$series-$target_vs-x86-staging.txt"); + $this->assertFileDoesNotExist($seriesBase . "/packages-$series-$target_vs-x64-stable.txt"); + $this->assertFileDoesNotExist($seriesBase . "/packages-$series-$target_vs-x64-staging.txt"); + } + + public function testHandlesCorruptJson(): void + { + $seriesDir = $this->buildsDirectory . '/series'; + mkdir($seriesDir, 0755, true); + $jsonPath = $seriesDir . '/series-init-bad.json'; + file_put_contents($jsonPath, '{corrupt json'); + + $command = new SeriesInitCommand(); + $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', 'Lock is created before processing and should remain after failure.'); + } + + public function testFailsWhenAnySourceSeriesFileIsMissing(): void + { + $series = '8.3'; + $series_vs = 'vs17'; + $target_vs = 'vs17'; + + $seriesBase = $this->baseDirectory . '/php-sdk/deps/series'; + mkdir($seriesBase, 0755, true); + + file_put_contents($seriesBase . "/packages-master-$series_vs-x86-stable.txt", 'ok'); + + $seriesDir = $this->buildsDirectory . '/series'; + mkdir($seriesDir, 0755, true); + $jsonPath = $seriesDir . '/series-init-task.json'; + file_put_contents($jsonPath, json_encode([ + 'series' => $series, + 'series_vs' => $series_vs, + 'target_vs' => $target_vs, + ])); + + $command = new SeriesInitCommand(); + $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-master-$series_vs-x86-staging.txt not found", + $output + ); + + $this->assertFileExists($seriesBase . "/packages-$series-$target_vs-x86-stable.txt"); + + $this->assertFileDoesNotExist($seriesBase . "/packages-$series-$target_vs-x86-staging.txt"); + $this->assertFileDoesNotExist($seriesBase . "/packages-$series-$target_vs-x64-stable.txt"); + $this->assertFileDoesNotExist($seriesBase . "/packages-$series-$target_vs-x64-staging.txt"); + + $this->assertFileExists($jsonPath); + $this->assertFileExists($jsonPath . '.lock'); + } +} diff --git a/tests/Http/Controllers/SeriesInitControllerTest.php b/tests/Http/Controllers/SeriesInitControllerTest.php new file mode 100644 index 0000000..1aa7db3 --- /dev/null +++ b/tests/Http/Controllers/SeriesInitControllerTest.php @@ -0,0 +1,37 @@ +<?php + +namespace Http\Controllers; + +use App\Http\Controllers\SeriesInitController; +use PHPUnit\Framework\TestCase; + +class MockSeriesInitController extends SeriesInitController { + 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 SeriesInitControllerTest extends TestCase { + public function testHandleWithValidData() { + $data = json_encode(["key" => "value"]); + $tempFile = tempnam(sys_get_temp_dir(), 'phpunit'); + file_put_contents($tempFile, $data); + $controller = new MockSeriesInitController($tempFile); + $this->expectOutputString("Executed"); + $controller->handle(); + unlink($tempFile); + } +}
