From 62bc523d0a7d36cae6c9eb00984fad827dfdd493 Mon Sep 17 00:00:00 2001 From: Jerry Chen Date: Sat, 18 Apr 2026 04:31:49 +0800 Subject: [PATCH 01/13] [refactor] add strict_types, fix nullable exceptions, explicit proxy methods, HandlerFactory map --- src/Configs/Config.php | 2 + src/Constants/GhostscriptConstant.php | 2 + src/Constants/ImageTypeConstant.php | 2 + src/Exceptions/BaseException.php | 4 +- src/Exceptions/ConfigException.php | 4 +- src/Exceptions/HandlerException.php | 4 +- src/Exceptions/InvalidException.php | 4 +- src/Exceptions/NotFoundException.php | 4 +- src/Factories/HandlerFactory.php | 30 +++++-- src/Ghostscript.php | 125 +++++++++++++++----------- src/Handlers/BaseHandler.php | 6 ++ src/Handlers/ConvertHandler.php | 2 + src/Handlers/GetTotalPagesHandler.php | 4 + src/Handlers/GuessHandler.php | 2 + src/Handlers/MergeHandler.php | 14 ++- src/Handlers/SplitHandler.php | 9 +- src/Handlers/ToImageHandler.php | 18 +++- src/Helpers/PathHelper.php | 2 + src/Interfaces/BaseInterface.php | 2 + src/Interfaces/HandlerInterface.php | 2 + src/Traits/ConfigTrait.php | 6 +- src/Traits/FileSystemTrait.php | 2 + 22 files changed, 182 insertions(+), 68 deletions(-) diff --git a/src/Configs/Config.php b/src/Configs/Config.php index 6f468b2..08c540d 100644 --- a/src/Configs/Config.php +++ b/src/Configs/Config.php @@ -1,5 +1,7 @@ BaseHandler::class, + 'convert' => ConvertHandler::class, + 'guess' => GuessHandler::class, + 'getTotalPages' => GetTotalPagesHandler::class, + 'merge' => MergeHandler::class, + 'split' => SplitHandler::class, + 'toImage' => ToImageHandler::class, + ]; + /** * @param string $type - * + * * @return HandlerInterface - * + * * @throws NotFoundException */ public function create(string $type): HandlerInterface { - $class = 'Ordinary9843\\Handlers\\' . ucfirst($type) . 'Handler'; - if (!class_exists($class)) { - throw new NotFoundException('Class "' . $class . '" does not exist.', NotFoundException::CODE_CLASS); + if (!isset(self::HANDLER_MAP[$type])) { + throw new NotFoundException('Handler "' . $type . '" does not exist.', NotFoundException::CODE_CLASS); } + $class = self::HANDLER_MAP[$type]; + return new $class(); } } diff --git a/src/Ghostscript.php b/src/Ghostscript.php index 4d44e95..01f1175 100644 --- a/src/Ghostscript.php +++ b/src/Ghostscript.php @@ -1,27 +1,15 @@ arguments); } + public function convert(string $file, float $version): string + { + return $this->createHandler('convert')->execute($file, $version); + } + + public function guess(string $file): float + { + return $this->createHandler('guess')->execute($file); + } + + public function merge(string $path, string $filename, array $files, bool $isAutoConvert = true): string + { + return $this->createHandler('merge')->execute($path, $filename, $files, $isAutoConvert); + } + + public function split(string $file, string $path): array + { + return $this->createHandler('split')->execute($file, $path); + } + + public function toImage(string $file, string $path, string $type = ImageTypeConstant::JPEG): array + { + return $this->createHandler('toImage')->execute($file, $path, $type); + } + + public function getTotalPages(string $file): int + { + return $this->createHandler('getTotalPages')->execute($file); + } + + public function clearTmpFiles(bool $isForceClear = false, int $days = 7): void + { + $this->createBaseHandler()->clearTmpFiles($isForceClear, $days); + } + + public function setBinPath(string $binPath): void + { + $this->createBaseHandler()->setBinPath($binPath); + } + + public function getBinPath(): string + { + return $this->createBaseHandler()->getBinPath(); + } + + public function setTmpPath(string $tmpPath): void + { + $this->createBaseHandler()->setTmpPath($tmpPath); + } + + public function getTmpPath(): string + { + return $this->createBaseHandler()->getTmpPath(); + } + + public function setOptions(array $options): void + { + $this->createBaseHandler()->setOptions($options); + } + + public function getOptions(): array + { + return $this->createBaseHandler()->getOptions(); + } + /** * @param string $name * @param array $arguments - * + * * @return mixed - * + * * @throws InvalidException */ public function __call(string $name, array $arguments) { - switch ($name) { - case 'convert': - case 'guess': - case 'merge': - case 'split': - case 'toImage': - case 'getTotalPages': - $handler = $this->createHandler($name); - - return $handler->execute(...$arguments); - case 'getBinPath': - case 'getTmpPath': - case 'getOptions': - case 'clearTmpFiles': - $handler = $this->createBaseHandler(); - - return $handler->{$name}(); - case 'setBinPath': - case 'setTmpPath': - $handler = $this->createBaseHandler(); - - return $handler->{$name}(current($arguments)); - case 'setOptions': - $handler = $this->createBaseHandler(); - - return $handler->{$name}(...$arguments); - default: - throw new InvalidException('Invalid method: "' . $name . '".', InvalidException::CODE_METHOD, [ - 'name' => $name, - 'arguments' => $arguments - ]); - } + throw new InvalidException('Invalid method: "' . $name . '".', InvalidException::CODE_METHOD, [ + 'name' => $name, + 'arguments' => $arguments + ]); } /** * @param string $name - * + * * @return HandlerInterface */ private function createHandler(string $name): HandlerInterface diff --git a/src/Handlers/BaseHandler.php b/src/Handlers/BaseHandler.php index 8276680..68ee6c0 100644 --- a/src/Handlers/BaseHandler.php +++ b/src/Handlers/BaseHandler.php @@ -1,5 +1,7 @@ isFile($file)) { + return false; + } + if (strcasecmp(pathinfo($file, PATHINFO_EXTENSION), 'pdf') !== 0) { return false; } diff --git a/src/Handlers/ConvertHandler.php b/src/Handlers/ConvertHandler.php index 14f91da..57b60d5 100644 --- a/src/Handlers/ConvertHandler.php +++ b/src/Handlers/ConvertHandler.php @@ -1,5 +1,7 @@ validateBinPath(); $this->mapArguments($arguments); + $file = ''; + try { $file = PathHelper::convertPathSeparator($arguments['file']); if (!$this->isFile($file)) { diff --git a/src/Handlers/GuessHandler.php b/src/Handlers/GuessHandler.php index 418cdda..7806bc4 100644 --- a/src/Handlers/GuessHandler.php +++ b/src/Handlers/GuessHandler.php @@ -1,5 +1,7 @@ convertHandler = (new HandlerFactory())->create('convert'); $this->guessHandler = (new HandlerFactory())->create('guess'); } @@ -42,6 +45,8 @@ public function execute(...$arguments): string $this->validateBinPath(); $this->mapArguments($arguments); + $file = ''; + try { $path = PathHelper::convertPathSeparator($arguments['path']); $filename = PathHelper::convertPathSeparator($arguments['filename']); @@ -59,6 +64,11 @@ public function execute(...$arguments): string return true; }); + + if (empty($files)) { + throw new HandlerException('No valid PDF files to merge.', HandlerException::CODE_EXECUTE); + } + $output = shell_exec( $this->optionsToCommand( sprintf( @@ -77,7 +87,9 @@ public function execute(...$arguments): string return $file; } catch (BaseException $exception) { - $this->delete($file); + if ($file !== '') { + $this->delete($file); + } throw new HandlerException($exception->getMessage(), HandlerException::CODE_EXECUTE, [ 'arguments' => $arguments diff --git a/src/Handlers/SplitHandler.php b/src/Handlers/SplitHandler.php index ad03a99..f138f3d 100644 --- a/src/Handlers/SplitHandler.php +++ b/src/Handlers/SplitHandler.php @@ -1,5 +1,7 @@ getTotalPagesHandler = (new HandlerFactory())->create('getTotalPages'); } /** * @param array ...$arguments - * + * * @return array - * + * * @throws HandlerException * @throws InvalidException */ @@ -49,7 +52,7 @@ public function execute(...$arguments): array return array_map(function ($i) use ($path, $pdfFormatPath) { return $path . sprintf($pdfFormatPath, $i); - }, range(0, $totalPages - 1)); + }, range(1, $totalPages)); } catch (BaseException $exception) { throw new HandlerException($exception->getMessage(), HandlerException::CODE_EXECUTE, [ 'arguments' => $arguments diff --git a/src/Handlers/ToImageHandler.php b/src/Handlers/ToImageHandler.php index 797a192..98959fb 100644 --- a/src/Handlers/ToImageHandler.php +++ b/src/Handlers/ToImageHandler.php @@ -1,5 +1,7 @@ getTotalPagesHandler = (new HandlerFactory())->create('getTotalPages'); } /** * @param array ...$arguments - * + * * @return array - * + * * @throws HandlerException * @throws InvalidException */ @@ -41,6 +47,14 @@ public function execute(...$arguments): array $file = PathHelper::convertPathSeparator($arguments['file']); $path = PathHelper::convertPathSeparator($arguments['path']); $type = $arguments['type'] ? $arguments['type'] : ImageTypeConstant::JPEG; + + if (!in_array($type, self::ALLOWED_TYPES, true)) { + throw new InvalidException( + 'Invalid image type "' . $type . '". Allowed: ' . implode(', ', self::ALLOWED_TYPES) . '.', + InvalidException::CODE_FILE_TYPE + ); + } + $totalPages = $this->getTotalPagesHandler->execute($file); (!$this->isDir($path)) && $this->makeDir($path); $imageFormatPath = ($totalPages > 1) ? '/image_%d.' . $type : '/' . pathinfo($file, PATHINFO_FILENAME) . '.' . $type; diff --git a/src/Helpers/PathHelper.php b/src/Helpers/PathHelper.php index fa7b4cd..3b9ecd1 100644 --- a/src/Helpers/PathHelper.php +++ b/src/Helpers/PathHelper.php @@ -1,5 +1,7 @@ Date: Sat, 18 Apr 2026 04:32:09 +0800 Subject: [PATCH 02/13] [chore] update .gitignore for docker, repomix, claude artifacts and test outputs --- .gitignore | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7cb9e80..b1bda8c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,16 @@ vendor/ report/ files/split/parts/ files/merge/res.pdf +files/merge/single.pdf files/to-image/images/ composer.lock coverage.xml phpunit.xml .phpunit.result.cache .DS_Store -.env \ No newline at end of file +.env +.repomix/ +repomix-output.txt +.claude/ +Dockerfile +docker-compose.yml \ No newline at end of file From 4649490ba3b78f3ac8aff12b8473d3f5a209bf10 Mon Sep 17 00:00:00 2001 From: Jerry Chen Date: Sat, 18 Apr 2026 04:32:22 +0800 Subject: [PATCH 03/13] [feat] add pull_request trigger to build workflow --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3710514..e1f9b4b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,6 +2,8 @@ name: build on: push: branches: [master] + pull_request: + branches: [master] jobs: run: runs-on: ${{ matrix.operating-system }} From d597b3e9a48e26dd06d58f6ea80be641916c01f9 Mon Sep 17 00:00:00 2001 From: Jerry Chen Date: Sat, 18 Apr 2026 04:32:40 +0800 Subject: [PATCH 04/13] [test] add edge case tests for all handlers, factory, and path helper --- .../Factories/HandlerFactoryEdgeCaseTest.php | 33 +++++++ tests/Handlers/BaseHandlerEdgeCaseTest.php | 86 +++++++++++++++++++ tests/Handlers/ConvertHandlerEdgeCaseTest.php | 31 +++++++ .../GetTotalPagesHandlerEdgeCaseTest.php | 30 +++++++ tests/Handlers/GuessHandlerEdgeCaseTest.php | 50 +++++++++++ tests/Handlers/MergeHandlerEdgeCaseTest.php | 46 ++++++++++ tests/Handlers/SplitHandlerEdgeCaseTest.php | 54 ++++++++++++ tests/Handlers/ToImageHandlerEdgeCaseTest.php | 42 +++++++++ tests/Helpers/PathHelperEdgeCaseTest.php | 40 +++++++++ 9 files changed, 412 insertions(+) create mode 100644 tests/Factories/HandlerFactoryEdgeCaseTest.php create mode 100644 tests/Handlers/BaseHandlerEdgeCaseTest.php create mode 100644 tests/Handlers/ConvertHandlerEdgeCaseTest.php create mode 100644 tests/Handlers/GetTotalPagesHandlerEdgeCaseTest.php create mode 100644 tests/Handlers/GuessHandlerEdgeCaseTest.php create mode 100644 tests/Handlers/MergeHandlerEdgeCaseTest.php create mode 100644 tests/Handlers/SplitHandlerEdgeCaseTest.php create mode 100644 tests/Handlers/ToImageHandlerEdgeCaseTest.php create mode 100644 tests/Helpers/PathHelperEdgeCaseTest.php diff --git a/tests/Factories/HandlerFactoryEdgeCaseTest.php b/tests/Factories/HandlerFactoryEdgeCaseTest.php new file mode 100644 index 0000000..ab4fc33 --- /dev/null +++ b/tests/Factories/HandlerFactoryEdgeCaseTest.php @@ -0,0 +1,33 @@ +create('getTotalPages'); + $this->assertInstanceOf(GetTotalPagesHandler::class, $handler); + } + + public function testCreateWithUppercaseTypeShouldThrowNotFoundException(): void + { + $this->expectException(NotFoundException::class); + $this->expectExceptionCode(NotFoundException::CODE_CLASS); + (new HandlerFactory())->create('Convert'); + } + + public function testCreateWithUnknownTypeShouldThrowNotFoundException(): void + { + $this->expectException(NotFoundException::class); + $this->expectExceptionCode(NotFoundException::CODE_CLASS); + (new HandlerFactory())->create('nonExistentHandler'); + } +} diff --git a/tests/Handlers/BaseHandlerEdgeCaseTest.php b/tests/Handlers/BaseHandlerEdgeCaseTest.php new file mode 100644 index 0000000..0f2a499 --- /dev/null +++ b/tests/Handlers/BaseHandlerEdgeCaseTest.php @@ -0,0 +1,86 @@ +assertFalse($handler->isPdf('/nonexistent/path/file.pdf')); + } + + public function testIsPdfReturnsFalseForEmptyString(): void + { + $handler = new BaseHandler(); + $this->assertFalse($handler->isPdf('')); + } + + public function testIsPdfReturnsFalseForDirectoryPath(): void + { + $handler = new BaseHandler(); + $this->assertFalse($handler->isPdf(sys_get_temp_dir())); + } + + public function testGetTmpFileContainsTmpPath(): void + { + $handler = new BaseHandler(); + $tmpFile = $handler->getTmpFile(); + $this->assertStringStartsWith($handler->getTmpPath(), $tmpFile); + } + + public function testGetTmpFileWithCustomFilenameContainsPrefix(): void + { + $handler = new BaseHandler(); + $tmpFile = $handler->getTmpFile('custom'); + $this->assertStringContainsString('ghostscript_tmp_file_custom', $tmpFile); + $this->assertStringEndsWith('.pdf', $tmpFile); + } + + public function testForceClearDeletesTmpPrefixedFiles(): void + { + $handler = new BaseHandler(); + $tmpFile = tempnam($handler->getTmpPath(), BaseHandler::TMP_FILE_PREFIX); + @rename($tmpFile, $tmpFile . '.pdf'); + $tmpFile .= '.pdf'; + $handler->clearTmpFiles(true); + $this->assertFileDoesNotExist($tmpFile); + } + + public function testOptionsToCommandWithKeyValuePairs(): void + { + $handler = new BaseHandler(); + $handler->setOptions(['-dCompatibilityLevel' => '1.4']); + $result = $handler->optionsToCommand('gs'); + $this->assertEquals('gs -dCompatibilityLevel=1.4', $result); + } + + public function testOptionsToCommandWithNumericKeys(): void + { + $handler = new BaseHandler(); + $handler->setOptions(['-dSAFER', '-dBATCH']); + $result = $handler->optionsToCommand('gs'); + $this->assertEquals('gs -dSAFER -dBATCH', $result); + } + + public function testOptionsToCommandWithEmptyOptionsReturnsOriginal(): void + { + $handler = new BaseHandler(); + $this->assertEquals('gs -sDEVICE=pdfwrite', $handler->optionsToCommand('gs -sDEVICE=pdfwrite')); + } + + public function testValidateBinPathThrowsForNonExistentPath(): void + { + $this->expectException(InvalidException::class); + $this->expectExceptionCode(InvalidException::CODE_FILEPATH); + $handler = new BaseHandler(); + $handler->setBinPath('/nonexistent/gs'); + $handler->validateBinPath(); + } +} diff --git a/tests/Handlers/ConvertHandlerEdgeCaseTest.php b/tests/Handlers/ConvertHandlerEdgeCaseTest.php new file mode 100644 index 0000000..77d7479 --- /dev/null +++ b/tests/Handlers/ConvertHandlerEdgeCaseTest.php @@ -0,0 +1,31 @@ +setBinPath($this->getEnv('GS_BIN_PATH')); + $result = $handler->execute($file, 1.4); + $this->assertEquals($file, $result); + $this->assertFileExists($result); + } + + public function testExecuteWithEmptyFilePathThrowsHandlerException(): void + { + $this->expectException(HandlerException::class); + $this->expectExceptionCode(HandlerException::CODE_EXECUTE); + $handler = new ConvertHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $handler->execute('', 1.4); + } +} diff --git a/tests/Handlers/GetTotalPagesHandlerEdgeCaseTest.php b/tests/Handlers/GetTotalPagesHandlerEdgeCaseTest.php new file mode 100644 index 0000000..36c62f0 --- /dev/null +++ b/tests/Handlers/GetTotalPagesHandlerEdgeCaseTest.php @@ -0,0 +1,30 @@ +expectException(HandlerException::class); + $this->expectExceptionCode(HandlerException::CODE_EXECUTE); + $handler = new GetTotalPagesHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $handler->execute(''); + } + + public function testExecuteWithNonExistentFileThrowsHandlerException(): void + { + $this->expectException(HandlerException::class); + $this->expectExceptionCode(HandlerException::CODE_EXECUTE); + $handler = new GetTotalPagesHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $handler->execute('/nonexistent/file.pdf'); + } +} diff --git a/tests/Handlers/GuessHandlerEdgeCaseTest.php b/tests/Handlers/GuessHandlerEdgeCaseTest.php new file mode 100644 index 0000000..8362f84 --- /dev/null +++ b/tests/Handlers/GuessHandlerEdgeCaseTest.php @@ -0,0 +1,50 @@ +execute($file); + $this->assertEquals(0.0, $version); + } finally { + @unlink($file); + } + } + + public function testExecuteWithEmptyFilePathThrowsHandlerException(): void + { + $this->expectException(HandlerException::class); + $this->expectExceptionCode(HandlerException::CODE_EXECUTE); + $handler = new GuessHandler(); + $handler->execute(''); + } + + public function testExecuteWithNonPdfFileReturnsZeroOrThrows(): void + { + $file = tempnam(sys_get_temp_dir(), 'test'); + @rename($file, $file .= '.pdf'); + @file_put_contents($file, ''); + $handler = new GuessHandler(); + + try { + $version = $handler->execute($file); + $this->assertIsFloat($version); + } finally { + @unlink($file); + } + } +} diff --git a/tests/Handlers/MergeHandlerEdgeCaseTest.php b/tests/Handlers/MergeHandlerEdgeCaseTest.php new file mode 100644 index 0000000..b77246d --- /dev/null +++ b/tests/Handlers/MergeHandlerEdgeCaseTest.php @@ -0,0 +1,46 @@ +expectException(HandlerException::class); + $this->expectExceptionCode(HandlerException::CODE_EXECUTE); + $handler = new MergeHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $handler->execute(dirname(__DIR__, 2) . '/files/merge', 'res.pdf', []); + } + + public function testExecuteWithAllInvalidFilesThrowsHandlerException(): void + { + $this->expectException(HandlerException::class); + $this->expectExceptionCode(HandlerException::CODE_EXECUTE); + $handler = new MergeHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $handler->execute(dirname(__DIR__, 2) . '/files/merge', 'res.pdf', [ + dirname(__DIR__, 2) . '/files/merge/nonexistent1.pdf', + dirname(__DIR__, 2) . '/files/merge/nonexistent2.pdf', + ]); + } + + public function testExecuteWithOnlyOneValidFileMergesSuccessfully(): void + { + $path = dirname(__DIR__, 2) . '/files/merge'; + $filename = 'single.pdf'; + $handler = new MergeHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $result = $handler->execute($path, $filename, [ + dirname(__DIR__, 2) . '/files/merge/part_1.pdf', + dirname(__DIR__, 2) . '/files/merge/nonexistent.pdf', + ]); + $this->assertFileExists($result); + } +} diff --git a/tests/Handlers/SplitHandlerEdgeCaseTest.php b/tests/Handlers/SplitHandlerEdgeCaseTest.php new file mode 100644 index 0000000..377e94c --- /dev/null +++ b/tests/Handlers/SplitHandlerEdgeCaseTest.php @@ -0,0 +1,54 @@ +setBinPath($this->getEnv('GS_BIN_PATH')); + $outputPath = sys_get_temp_dir() . '/gs_split_test_' . uniqid(); + $parts = $handler->execute(dirname(__DIR__, 2) . '/files/split/test.pdf', $outputPath); + + $this->assertCount(3, $parts); + $this->assertStringEndsWith('/part_1.pdf', $parts[0]); + $this->assertStringEndsWith('/part_2.pdf', $parts[1]); + $this->assertStringEndsWith('/part_3.pdf', $parts[2]); + } + + public function testSplitOutputFilesAreCreated(): void + { + $handler = new SplitHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $outputPath = sys_get_temp_dir() . '/gs_split_test_' . uniqid(); + $parts = $handler->execute(dirname(__DIR__, 2) . '/files/split/test.pdf', $outputPath); + + foreach ($parts as $part) { + $this->assertFileExists($part); + } + } + + public function testExecuteWithNonExistentFileThrowsHandlerException(): void + { + $this->expectException(HandlerException::class); + $this->expectExceptionCode(HandlerException::CODE_EXECUTE); + $handler = new SplitHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $handler->execute('/nonexistent/file.pdf', sys_get_temp_dir()); + } + + public function testExecuteWithEmptyFilePathThrowsHandlerException(): void + { + $this->expectException(HandlerException::class); + $handler = new SplitHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $handler->execute('', sys_get_temp_dir()); + } +} diff --git a/tests/Handlers/ToImageHandlerEdgeCaseTest.php b/tests/Handlers/ToImageHandlerEdgeCaseTest.php new file mode 100644 index 0000000..3562796 --- /dev/null +++ b/tests/Handlers/ToImageHandlerEdgeCaseTest.php @@ -0,0 +1,42 @@ +expectException(HandlerException::class); + $this->expectExceptionCode(HandlerException::CODE_EXECUTE); + $handler = new ToImageHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $handler->execute( + dirname(__DIR__, 2) . '/files/to-image/test.pdf', + sys_get_temp_dir(), + 'bmp' + ); + } + + public function testExecuteWithEmptyFilePathThrowsHandlerException(): void + { + $this->expectException(HandlerException::class); + $handler = new ToImageHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $handler->execute('', sys_get_temp_dir(), 'jpeg'); + } + + public function testExecuteWithNonExistentFileThrowsHandlerException(): void + { + $this->expectException(HandlerException::class); + $this->expectExceptionCode(HandlerException::CODE_EXECUTE); + $handler = new ToImageHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $handler->execute('/nonexistent/file.pdf', sys_get_temp_dir(), 'jpeg'); + } +} diff --git a/tests/Helpers/PathHelperEdgeCaseTest.php b/tests/Helpers/PathHelperEdgeCaseTest.php new file mode 100644 index 0000000..833472e --- /dev/null +++ b/tests/Helpers/PathHelperEdgeCaseTest.php @@ -0,0 +1,40 @@ +assertEquals('', PathHelper::convertPathSeparator('')); + } + + public function testBackslashesAreConverted(): void + { + $result = PathHelper::convertPathSeparator('usr\\bin\\gs'); + $this->assertEquals(implode(DIRECTORY_SEPARATOR, ['usr', 'bin', 'gs']), $result); + } + + public function testMixedSeparatorsAreNormalized(): void + { + $result = PathHelper::convertPathSeparator('usr/bin\\gs'); + $this->assertEquals(implode(DIRECTORY_SEPARATOR, ['usr', 'bin', 'gs']), $result); + } + + public function testAlreadyNormalizedPathIsUnchanged(): void + { + $path = implode(DIRECTORY_SEPARATOR, ['usr', 'bin', 'gs']); + $this->assertEquals($path, PathHelper::convertPathSeparator($path)); + } + + public function testPathWithSpacesIsPreserved(): void + { + $result = PathHelper::convertPathSeparator('path/with spaces/file.pdf'); + $this->assertEquals(implode(DIRECTORY_SEPARATOR, ['path', 'with spaces', 'file.pdf']), $result); + } +} From 9fa442f770889db9458ff8c202b6efe0dbe46b81 Mon Sep 17 00:00:00 2001 From: Jerry Chen Date: Sat, 18 Apr 2026 04:39:43 +0800 Subject: [PATCH 05/13] [test] replace assertFileDoesNotExist with file_exists for phpunit 7.x compat --- tests/Handlers/BaseHandlerEdgeCaseTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Handlers/BaseHandlerEdgeCaseTest.php b/tests/Handlers/BaseHandlerEdgeCaseTest.php index 0f2a499..d8e14a7 100644 --- a/tests/Handlers/BaseHandlerEdgeCaseTest.php +++ b/tests/Handlers/BaseHandlerEdgeCaseTest.php @@ -50,7 +50,7 @@ public function testForceClearDeletesTmpPrefixedFiles(): void @rename($tmpFile, $tmpFile . '.pdf'); $tmpFile .= '.pdf'; $handler->clearTmpFiles(true); - $this->assertFileDoesNotExist($tmpFile); + $this->assertFalse(file_exists($tmpFile)); } public function testOptionsToCommandWithKeyValuePairs(): void From dd182a333442989faaa22e88496c27d467feb84a Mon Sep 17 00:00:00 2001 From: Jerry Chen Date: Sat, 18 Apr 2026 04:46:12 +0800 Subject: [PATCH 06/13] [test] merge edge case tests into existing test files --- .../Factories/HandlerFactoryEdgeCaseTest.php | 33 ------ tests/Factories/HandlerFactoryTest.php | 30 +++++ tests/Handlers/BaseHandlerEdgeCaseTest.php | 86 --------------- tests/Handlers/BaseHandlerTest.php | 104 ++++++++++++++++++ tests/Handlers/ConvertHandlerEdgeCaseTest.php | 31 ------ tests/Handlers/ConvertHandlerTest.php | 25 +++++ .../GetTotalPagesHandlerEdgeCaseTest.php | 30 ----- tests/Handlers/GetTotalPagesHandlerTest.php | 24 ++++ tests/Handlers/GuessHandlerEdgeCaseTest.php | 50 --------- tests/Handlers/GuessHandlerTest.php | 29 +++++ tests/Handlers/MergeHandlerEdgeCaseTest.php | 46 -------- tests/Handlers/MergeHandlerTest.php | 42 +++++++ tests/Handlers/SplitHandlerEdgeCaseTest.php | 54 --------- tests/Handlers/SplitHandlerTest.php | 54 +++++++++ tests/Handlers/ToImageHandlerEdgeCaseTest.php | 42 ------- tests/Handlers/ToImageHandlerTest.php | 39 +++++++ tests/Helpers/PathHelperEdgeCaseTest.php | 40 ------- tests/Helpers/PathHelperTest.php | 44 ++++++++ 18 files changed, 391 insertions(+), 412 deletions(-) delete mode 100644 tests/Factories/HandlerFactoryEdgeCaseTest.php delete mode 100644 tests/Handlers/BaseHandlerEdgeCaseTest.php delete mode 100644 tests/Handlers/ConvertHandlerEdgeCaseTest.php delete mode 100644 tests/Handlers/GetTotalPagesHandlerEdgeCaseTest.php delete mode 100644 tests/Handlers/GuessHandlerEdgeCaseTest.php delete mode 100644 tests/Handlers/MergeHandlerEdgeCaseTest.php delete mode 100644 tests/Handlers/SplitHandlerEdgeCaseTest.php delete mode 100644 tests/Handlers/ToImageHandlerEdgeCaseTest.php delete mode 100644 tests/Helpers/PathHelperEdgeCaseTest.php diff --git a/tests/Factories/HandlerFactoryEdgeCaseTest.php b/tests/Factories/HandlerFactoryEdgeCaseTest.php deleted file mode 100644 index ab4fc33..0000000 --- a/tests/Factories/HandlerFactoryEdgeCaseTest.php +++ /dev/null @@ -1,33 +0,0 @@ -create('getTotalPages'); - $this->assertInstanceOf(GetTotalPagesHandler::class, $handler); - } - - public function testCreateWithUppercaseTypeShouldThrowNotFoundException(): void - { - $this->expectException(NotFoundException::class); - $this->expectExceptionCode(NotFoundException::CODE_CLASS); - (new HandlerFactory())->create('Convert'); - } - - public function testCreateWithUnknownTypeShouldThrowNotFoundException(): void - { - $this->expectException(NotFoundException::class); - $this->expectExceptionCode(NotFoundException::CODE_CLASS); - (new HandlerFactory())->create('nonExistentHandler'); - } -} diff --git a/tests/Factories/HandlerFactoryTest.php b/tests/Factories/HandlerFactoryTest.php index 37c6d65..316ca6d 100644 --- a/tests/Factories/HandlerFactoryTest.php +++ b/tests/Factories/HandlerFactoryTest.php @@ -9,6 +9,7 @@ use Ordinary9843\Handlers\SplitHandler; use Ordinary9843\Handlers\ConvertHandler; use Ordinary9843\Handlers\ToImageHandler; +use Ordinary9843\Handlers\GetTotalPagesHandler; use Ordinary9843\Factories\HandlerFactory; use Ordinary9843\Exceptions\NotFoundException; @@ -77,4 +78,33 @@ public function testCreateBaseHandlerShouldThrowNotFoundException(): void $this->expectExceptionCode(NotFoundException::CODE_CLASS); (new HandlerFactory)->create(''); } + + /** + * @return void + */ + public function testCreateGetTotalPagesHandlerShouldSucceed(): void + { + $handler = (new HandlerFactory())->create('getTotalPages'); + $this->assertInstanceOf(GetTotalPagesHandler::class, $handler); + } + + /** + * @return void + */ + public function testCreateWithUppercaseTypeShouldThrowNotFoundException(): void + { + $this->expectException(NotFoundException::class); + $this->expectExceptionCode(NotFoundException::CODE_CLASS); + (new HandlerFactory())->create('Convert'); + } + + /** + * @return void + */ + public function testCreateWithUnknownTypeShouldThrowNotFoundException(): void + { + $this->expectException(NotFoundException::class); + $this->expectExceptionCode(NotFoundException::CODE_CLASS); + (new HandlerFactory())->create('nonExistentHandler'); + } } diff --git a/tests/Handlers/BaseHandlerEdgeCaseTest.php b/tests/Handlers/BaseHandlerEdgeCaseTest.php deleted file mode 100644 index d8e14a7..0000000 --- a/tests/Handlers/BaseHandlerEdgeCaseTest.php +++ /dev/null @@ -1,86 +0,0 @@ -assertFalse($handler->isPdf('/nonexistent/path/file.pdf')); - } - - public function testIsPdfReturnsFalseForEmptyString(): void - { - $handler = new BaseHandler(); - $this->assertFalse($handler->isPdf('')); - } - - public function testIsPdfReturnsFalseForDirectoryPath(): void - { - $handler = new BaseHandler(); - $this->assertFalse($handler->isPdf(sys_get_temp_dir())); - } - - public function testGetTmpFileContainsTmpPath(): void - { - $handler = new BaseHandler(); - $tmpFile = $handler->getTmpFile(); - $this->assertStringStartsWith($handler->getTmpPath(), $tmpFile); - } - - public function testGetTmpFileWithCustomFilenameContainsPrefix(): void - { - $handler = new BaseHandler(); - $tmpFile = $handler->getTmpFile('custom'); - $this->assertStringContainsString('ghostscript_tmp_file_custom', $tmpFile); - $this->assertStringEndsWith('.pdf', $tmpFile); - } - - public function testForceClearDeletesTmpPrefixedFiles(): void - { - $handler = new BaseHandler(); - $tmpFile = tempnam($handler->getTmpPath(), BaseHandler::TMP_FILE_PREFIX); - @rename($tmpFile, $tmpFile . '.pdf'); - $tmpFile .= '.pdf'; - $handler->clearTmpFiles(true); - $this->assertFalse(file_exists($tmpFile)); - } - - public function testOptionsToCommandWithKeyValuePairs(): void - { - $handler = new BaseHandler(); - $handler->setOptions(['-dCompatibilityLevel' => '1.4']); - $result = $handler->optionsToCommand('gs'); - $this->assertEquals('gs -dCompatibilityLevel=1.4', $result); - } - - public function testOptionsToCommandWithNumericKeys(): void - { - $handler = new BaseHandler(); - $handler->setOptions(['-dSAFER', '-dBATCH']); - $result = $handler->optionsToCommand('gs'); - $this->assertEquals('gs -dSAFER -dBATCH', $result); - } - - public function testOptionsToCommandWithEmptyOptionsReturnsOriginal(): void - { - $handler = new BaseHandler(); - $this->assertEquals('gs -sDEVICE=pdfwrite', $handler->optionsToCommand('gs -sDEVICE=pdfwrite')); - } - - public function testValidateBinPathThrowsForNonExistentPath(): void - { - $this->expectException(InvalidException::class); - $this->expectExceptionCode(InvalidException::CODE_FILEPATH); - $handler = new BaseHandler(); - $handler->setBinPath('/nonexistent/gs'); - $handler->validateBinPath(); - } -} diff --git a/tests/Handlers/BaseHandlerTest.php b/tests/Handlers/BaseHandlerTest.php index 0d48ca8..53350e6 100644 --- a/tests/Handlers/BaseHandlerTest.php +++ b/tests/Handlers/BaseHandlerTest.php @@ -200,4 +200,108 @@ public function testArgumentsMappingWhenProvidedInputs() 'arg2' => 'value2' ], $arguments); } + + /** + * @return void + */ + public function testIsPdfReturnsFalseForNonExistentFile(): void + { + $handler = new BaseHandler(); + $this->assertFalse($handler->isPdf('/nonexistent/path/file.pdf')); + } + + /** + * @return void + */ + public function testIsPdfReturnsFalseForEmptyString(): void + { + $handler = new BaseHandler(); + $this->assertFalse($handler->isPdf('')); + } + + /** + * @return void + */ + public function testIsPdfReturnsFalseForDirectoryPath(): void + { + $handler = new BaseHandler(); + $this->assertFalse($handler->isPdf(sys_get_temp_dir())); + } + + /** + * @return void + */ + public function testGetTmpFileContainsTmpPath(): void + { + $handler = new BaseHandler(); + $tmpFile = $handler->getTmpFile(); + $this->assertStringStartsWith($handler->getTmpPath(), $tmpFile); + } + + /** + * @return void + */ + public function testGetTmpFileWithCustomFilenameContainsPrefix(): void + { + $handler = new BaseHandler(); + $tmpFile = $handler->getTmpFile('custom'); + $this->assertStringContainsString('ghostscript_tmp_file_custom', $tmpFile); + $this->assertStringEndsWith('.pdf', $tmpFile); + } + + /** + * @return void + */ + public function testForceClearDeletesTmpPrefixedFiles(): void + { + $handler = new BaseHandler(); + $tmpFile = tempnam($handler->getTmpPath(), BaseHandler::TMP_FILE_PREFIX); + @rename($tmpFile, $tmpFile . '.pdf'); + $tmpFile .= '.pdf'; + $handler->clearTmpFiles(true); + $this->assertFalse(file_exists($tmpFile)); + } + + /** + * @return void + */ + public function testOptionsToCommandWithKeyValuePairs(): void + { + $handler = new BaseHandler(); + $handler->setOptions(['-dCompatibilityLevel' => '1.4']); + $result = $handler->optionsToCommand('gs'); + $this->assertEquals('gs -dCompatibilityLevel=1.4', $result); + } + + /** + * @return void + */ + public function testOptionsToCommandWithNumericKeys(): void + { + $handler = new BaseHandler(); + $handler->setOptions(['-dSAFER', '-dBATCH']); + $result = $handler->optionsToCommand('gs'); + $this->assertEquals('gs -dSAFER -dBATCH', $result); + } + + /** + * @return void + */ + public function testOptionsToCommandWithEmptyOptionsReturnsOriginal(): void + { + $handler = new BaseHandler(); + $this->assertEquals('gs -sDEVICE=pdfwrite', $handler->optionsToCommand('gs -sDEVICE=pdfwrite')); + } + + /** + * @return void + */ + public function testValidateBinPathThrowsForNonExistentPath(): void + { + $this->expectException(InvalidException::class); + $this->expectExceptionCode(InvalidException::CODE_FILEPATH); + $handler = new BaseHandler(); + $handler->setBinPath('/nonexistent/gs'); + $handler->validateBinPath(); + } } diff --git a/tests/Handlers/ConvertHandlerEdgeCaseTest.php b/tests/Handlers/ConvertHandlerEdgeCaseTest.php deleted file mode 100644 index 77d7479..0000000 --- a/tests/Handlers/ConvertHandlerEdgeCaseTest.php +++ /dev/null @@ -1,31 +0,0 @@ -setBinPath($this->getEnv('GS_BIN_PATH')); - $result = $handler->execute($file, 1.4); - $this->assertEquals($file, $result); - $this->assertFileExists($result); - } - - public function testExecuteWithEmptyFilePathThrowsHandlerException(): void - { - $this->expectException(HandlerException::class); - $this->expectExceptionCode(HandlerException::CODE_EXECUTE); - $handler = new ConvertHandler(); - $handler->setBinPath($this->getEnv('GS_BIN_PATH')); - $handler->execute('', 1.4); - } -} diff --git a/tests/Handlers/ConvertHandlerTest.php b/tests/Handlers/ConvertHandlerTest.php index 5b6aea7..75c622c 100644 --- a/tests/Handlers/ConvertHandlerTest.php +++ b/tests/Handlers/ConvertHandlerTest.php @@ -91,4 +91,29 @@ public function testExecuteFailedShouldThrowHandlerException(): void ]); $handler->execute($file, 1.5); } + + /** + * @return void + */ + public function testExecuteReturnsSameFilePathOnSuccess(): void + { + $file = dirname(__DIR__, 2) . '/files/convert/test.pdf'; + $handler = new ConvertHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $result = $handler->execute($file, 1.4); + $this->assertEquals($file, $result); + $this->assertFileExists($result); + } + + /** + * @return void + */ + public function testExecuteWithEmptyFilePathThrowsHandlerException(): void + { + $this->expectException(HandlerException::class); + $this->expectExceptionCode(HandlerException::CODE_EXECUTE); + $handler = new ConvertHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $handler->execute('', 1.4); + } } diff --git a/tests/Handlers/GetTotalPagesHandlerEdgeCaseTest.php b/tests/Handlers/GetTotalPagesHandlerEdgeCaseTest.php deleted file mode 100644 index 36c62f0..0000000 --- a/tests/Handlers/GetTotalPagesHandlerEdgeCaseTest.php +++ /dev/null @@ -1,30 +0,0 @@ -expectException(HandlerException::class); - $this->expectExceptionCode(HandlerException::CODE_EXECUTE); - $handler = new GetTotalPagesHandler(); - $handler->setBinPath($this->getEnv('GS_BIN_PATH')); - $handler->execute(''); - } - - public function testExecuteWithNonExistentFileThrowsHandlerException(): void - { - $this->expectException(HandlerException::class); - $this->expectExceptionCode(HandlerException::CODE_EXECUTE); - $handler = new GetTotalPagesHandler(); - $handler->setBinPath($this->getEnv('GS_BIN_PATH')); - $handler->execute('/nonexistent/file.pdf'); - } -} diff --git a/tests/Handlers/GetTotalPagesHandlerTest.php b/tests/Handlers/GetTotalPagesHandlerTest.php index 11186ac..4169150 100644 --- a/tests/Handlers/GetTotalPagesHandlerTest.php +++ b/tests/Handlers/GetTotalPagesHandlerTest.php @@ -74,4 +74,28 @@ public function testExecuteWhenFileTypeNotMatchShouldThrowHandlerException(): vo $handler->setBinPath($this->getEnv('GS_BIN_PATH')); $handler->execute($file); } + + /** + * @return void + */ + public function testExecuteWithEmptyFilePathThrowsHandlerException(): void + { + $this->expectException(HandlerException::class); + $this->expectExceptionCode(HandlerException::CODE_EXECUTE); + $handler = new GetTotalPagesHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $handler->execute(''); + } + + /** + * @return void + */ + public function testExecuteWithNonExistentFileThrowsHandlerException(): void + { + $this->expectException(HandlerException::class); + $this->expectExceptionCode(HandlerException::CODE_EXECUTE); + $handler = new GetTotalPagesHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $handler->execute('/nonexistent/file.pdf'); + } } diff --git a/tests/Handlers/GuessHandlerEdgeCaseTest.php b/tests/Handlers/GuessHandlerEdgeCaseTest.php deleted file mode 100644 index 8362f84..0000000 --- a/tests/Handlers/GuessHandlerEdgeCaseTest.php +++ /dev/null @@ -1,50 +0,0 @@ -execute($file); - $this->assertEquals(0.0, $version); - } finally { - @unlink($file); - } - } - - public function testExecuteWithEmptyFilePathThrowsHandlerException(): void - { - $this->expectException(HandlerException::class); - $this->expectExceptionCode(HandlerException::CODE_EXECUTE); - $handler = new GuessHandler(); - $handler->execute(''); - } - - public function testExecuteWithNonPdfFileReturnsZeroOrThrows(): void - { - $file = tempnam(sys_get_temp_dir(), 'test'); - @rename($file, $file .= '.pdf'); - @file_put_contents($file, ''); - $handler = new GuessHandler(); - - try { - $version = $handler->execute($file); - $this->assertIsFloat($version); - } finally { - @unlink($file); - } - } -} diff --git a/tests/Handlers/GuessHandlerTest.php b/tests/Handlers/GuessHandlerTest.php index 71c06ad..7e970b5 100644 --- a/tests/Handlers/GuessHandlerTest.php +++ b/tests/Handlers/GuessHandlerTest.php @@ -47,4 +47,33 @@ public function testExecuteShouldThrowInvalidException(): void $handler->setBinPath($this->getEnv('GS_BIN_PATH')); $handler->execute($file); } + + /** + * @return void + */ + public function testExecuteReturnsZeroForPdfWithoutVersionHeader(): void + { + $file = tempnam(sys_get_temp_dir(), 'test'); + @rename($file, $file .= '.pdf'); + @file_put_contents($file, 'This is not a real PDF, no version header.'); + $handler = new GuessHandler(); + + try { + $version = $handler->execute($file); + $this->assertEquals(0.0, $version); + } finally { + @unlink($file); + } + } + + /** + * @return void + */ + public function testExecuteWithEmptyFilePathThrowsHandlerException(): void + { + $this->expectException(HandlerException::class); + $this->expectExceptionCode(HandlerException::CODE_EXECUTE); + $handler = new GuessHandler(); + $handler->execute(''); + } } diff --git a/tests/Handlers/MergeHandlerEdgeCaseTest.php b/tests/Handlers/MergeHandlerEdgeCaseTest.php deleted file mode 100644 index b77246d..0000000 --- a/tests/Handlers/MergeHandlerEdgeCaseTest.php +++ /dev/null @@ -1,46 +0,0 @@ -expectException(HandlerException::class); - $this->expectExceptionCode(HandlerException::CODE_EXECUTE); - $handler = new MergeHandler(); - $handler->setBinPath($this->getEnv('GS_BIN_PATH')); - $handler->execute(dirname(__DIR__, 2) . '/files/merge', 'res.pdf', []); - } - - public function testExecuteWithAllInvalidFilesThrowsHandlerException(): void - { - $this->expectException(HandlerException::class); - $this->expectExceptionCode(HandlerException::CODE_EXECUTE); - $handler = new MergeHandler(); - $handler->setBinPath($this->getEnv('GS_BIN_PATH')); - $handler->execute(dirname(__DIR__, 2) . '/files/merge', 'res.pdf', [ - dirname(__DIR__, 2) . '/files/merge/nonexistent1.pdf', - dirname(__DIR__, 2) . '/files/merge/nonexistent2.pdf', - ]); - } - - public function testExecuteWithOnlyOneValidFileMergesSuccessfully(): void - { - $path = dirname(__DIR__, 2) . '/files/merge'; - $filename = 'single.pdf'; - $handler = new MergeHandler(); - $handler->setBinPath($this->getEnv('GS_BIN_PATH')); - $result = $handler->execute($path, $filename, [ - dirname(__DIR__, 2) . '/files/merge/part_1.pdf', - dirname(__DIR__, 2) . '/files/merge/nonexistent.pdf', - ]); - $this->assertFileExists($result); - } -} diff --git a/tests/Handlers/MergeHandlerTest.php b/tests/Handlers/MergeHandlerTest.php index 2ea7d7a..e625cdd 100644 --- a/tests/Handlers/MergeHandlerTest.php +++ b/tests/Handlers/MergeHandlerTest.php @@ -124,4 +124,46 @@ public function testExecuteFailedShouldThrowHandlerException(): void dirname(__DIR__, 2) . '/files/merge/part_3.pdf' ]); } + + /** + * @return void + */ + public function testExecuteWithEmptyFilesArrayThrowsHandlerException(): void + { + $this->expectException(HandlerException::class); + $this->expectExceptionCode(HandlerException::CODE_EXECUTE); + $handler = new MergeHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $handler->execute(dirname(__DIR__, 2) . '/files/merge', 'res.pdf', []); + } + + /** + * @return void + */ + public function testExecuteWithAllInvalidFilesThrowsHandlerException(): void + { + $this->expectException(HandlerException::class); + $this->expectExceptionCode(HandlerException::CODE_EXECUTE); + $handler = new MergeHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $handler->execute(dirname(__DIR__, 2) . '/files/merge', 'res.pdf', [ + dirname(__DIR__, 2) . '/files/merge/nonexistent1.pdf', + dirname(__DIR__, 2) . '/files/merge/nonexistent2.pdf', + ]); + } + + /** + * @return void + */ + public function testExecuteWithOnlyOneValidFileMergesSuccessfully(): void + { + $path = dirname(__DIR__, 2) . '/files/merge'; + $handler = new MergeHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $result = $handler->execute($path, 'single.pdf', [ + dirname(__DIR__, 2) . '/files/merge/part_1.pdf', + dirname(__DIR__, 2) . '/files/merge/nonexistent.pdf', + ]); + $this->assertFileExists($result); + } } diff --git a/tests/Handlers/SplitHandlerEdgeCaseTest.php b/tests/Handlers/SplitHandlerEdgeCaseTest.php deleted file mode 100644 index 377e94c..0000000 --- a/tests/Handlers/SplitHandlerEdgeCaseTest.php +++ /dev/null @@ -1,54 +0,0 @@ -setBinPath($this->getEnv('GS_BIN_PATH')); - $outputPath = sys_get_temp_dir() . '/gs_split_test_' . uniqid(); - $parts = $handler->execute(dirname(__DIR__, 2) . '/files/split/test.pdf', $outputPath); - - $this->assertCount(3, $parts); - $this->assertStringEndsWith('/part_1.pdf', $parts[0]); - $this->assertStringEndsWith('/part_2.pdf', $parts[1]); - $this->assertStringEndsWith('/part_3.pdf', $parts[2]); - } - - public function testSplitOutputFilesAreCreated(): void - { - $handler = new SplitHandler(); - $handler->setBinPath($this->getEnv('GS_BIN_PATH')); - $outputPath = sys_get_temp_dir() . '/gs_split_test_' . uniqid(); - $parts = $handler->execute(dirname(__DIR__, 2) . '/files/split/test.pdf', $outputPath); - - foreach ($parts as $part) { - $this->assertFileExists($part); - } - } - - public function testExecuteWithNonExistentFileThrowsHandlerException(): void - { - $this->expectException(HandlerException::class); - $this->expectExceptionCode(HandlerException::CODE_EXECUTE); - $handler = new SplitHandler(); - $handler->setBinPath($this->getEnv('GS_BIN_PATH')); - $handler->execute('/nonexistent/file.pdf', sys_get_temp_dir()); - } - - public function testExecuteWithEmptyFilePathThrowsHandlerException(): void - { - $this->expectException(HandlerException::class); - $handler = new SplitHandler(); - $handler->setBinPath($this->getEnv('GS_BIN_PATH')); - $handler->execute('', sys_get_temp_dir()); - } -} diff --git a/tests/Handlers/SplitHandlerTest.php b/tests/Handlers/SplitHandlerTest.php index ad35fc1..e217850 100644 --- a/tests/Handlers/SplitHandlerTest.php +++ b/tests/Handlers/SplitHandlerTest.php @@ -42,4 +42,58 @@ public function testExecuteFailedShouldShouldThrowHandlerException(): void ]); $handler->execute(dirname(__DIR__, 2) . '/files/split/test.pdf', '/tmp/mock/files'); } + + /** + * @return void + */ + public function testSplitOutputFilesStartAtPartOne(): void + { + $handler = new SplitHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $outputPath = sys_get_temp_dir() . '/gs_split_test_' . uniqid(); + $parts = $handler->execute(dirname(__DIR__, 2) . '/files/split/test.pdf', $outputPath); + + $this->assertCount(3, $parts); + $this->assertStringEndsWith('/part_1.pdf', $parts[0]); + $this->assertStringEndsWith('/part_2.pdf', $parts[1]); + $this->assertStringEndsWith('/part_3.pdf', $parts[2]); + } + + /** + * @return void + */ + public function testSplitOutputFilesAreCreated(): void + { + $handler = new SplitHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $outputPath = sys_get_temp_dir() . '/gs_split_test_' . uniqid(); + $parts = $handler->execute(dirname(__DIR__, 2) . '/files/split/test.pdf', $outputPath); + + foreach ($parts as $part) { + $this->assertFileExists($part); + } + } + + /** + * @return void + */ + public function testExecuteWithNonExistentFileThrowsHandlerException(): void + { + $this->expectException(HandlerException::class); + $this->expectExceptionCode(HandlerException::CODE_EXECUTE); + $handler = new SplitHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $handler->execute('/nonexistent/file.pdf', sys_get_temp_dir()); + } + + /** + * @return void + */ + public function testExecuteWithEmptyFilePathThrowsHandlerException(): void + { + $this->expectException(HandlerException::class); + $handler = new SplitHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $handler->execute('', sys_get_temp_dir()); + } } diff --git a/tests/Handlers/ToImageHandlerEdgeCaseTest.php b/tests/Handlers/ToImageHandlerEdgeCaseTest.php deleted file mode 100644 index 3562796..0000000 --- a/tests/Handlers/ToImageHandlerEdgeCaseTest.php +++ /dev/null @@ -1,42 +0,0 @@ -expectException(HandlerException::class); - $this->expectExceptionCode(HandlerException::CODE_EXECUTE); - $handler = new ToImageHandler(); - $handler->setBinPath($this->getEnv('GS_BIN_PATH')); - $handler->execute( - dirname(__DIR__, 2) . '/files/to-image/test.pdf', - sys_get_temp_dir(), - 'bmp' - ); - } - - public function testExecuteWithEmptyFilePathThrowsHandlerException(): void - { - $this->expectException(HandlerException::class); - $handler = new ToImageHandler(); - $handler->setBinPath($this->getEnv('GS_BIN_PATH')); - $handler->execute('', sys_get_temp_dir(), 'jpeg'); - } - - public function testExecuteWithNonExistentFileThrowsHandlerException(): void - { - $this->expectException(HandlerException::class); - $this->expectExceptionCode(HandlerException::CODE_EXECUTE); - $handler = new ToImageHandler(); - $handler->setBinPath($this->getEnv('GS_BIN_PATH')); - $handler->execute('/nonexistent/file.pdf', sys_get_temp_dir(), 'jpeg'); - } -} diff --git a/tests/Handlers/ToImageHandlerTest.php b/tests/Handlers/ToImageHandlerTest.php index af50a34..e575654 100644 --- a/tests/Handlers/ToImageHandlerTest.php +++ b/tests/Handlers/ToImageHandlerTest.php @@ -63,4 +63,43 @@ public function testExecuteFailedShouldThrowHandlerException(): void ]); $handler->execute(dirname(__DIR__, 2) . '/files/to-image/test.pdf', '/tmp/mock/files'); } + + /** + * @return void + */ + public function testExecuteWithInvalidImageTypeThrowsHandlerException(): void + { + $this->expectException(HandlerException::class); + $this->expectExceptionCode(HandlerException::CODE_EXECUTE); + $handler = new ToImageHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $handler->execute( + dirname(__DIR__, 2) . '/files/to-image/test.pdf', + sys_get_temp_dir(), + 'bmp' + ); + } + + /** + * @return void + */ + public function testExecuteWithEmptyFilePathThrowsHandlerException(): void + { + $this->expectException(HandlerException::class); + $handler = new ToImageHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $handler->execute('', sys_get_temp_dir(), 'jpeg'); + } + + /** + * @return void + */ + public function testExecuteWithNonExistentFileThrowsHandlerException(): void + { + $this->expectException(HandlerException::class); + $this->expectExceptionCode(HandlerException::CODE_EXECUTE); + $handler = new ToImageHandler(); + $handler->setBinPath($this->getEnv('GS_BIN_PATH')); + $handler->execute('/nonexistent/file.pdf', sys_get_temp_dir(), 'jpeg'); + } } diff --git a/tests/Helpers/PathHelperEdgeCaseTest.php b/tests/Helpers/PathHelperEdgeCaseTest.php deleted file mode 100644 index 833472e..0000000 --- a/tests/Helpers/PathHelperEdgeCaseTest.php +++ /dev/null @@ -1,40 +0,0 @@ -assertEquals('', PathHelper::convertPathSeparator('')); - } - - public function testBackslashesAreConverted(): void - { - $result = PathHelper::convertPathSeparator('usr\\bin\\gs'); - $this->assertEquals(implode(DIRECTORY_SEPARATOR, ['usr', 'bin', 'gs']), $result); - } - - public function testMixedSeparatorsAreNormalized(): void - { - $result = PathHelper::convertPathSeparator('usr/bin\\gs'); - $this->assertEquals(implode(DIRECTORY_SEPARATOR, ['usr', 'bin', 'gs']), $result); - } - - public function testAlreadyNormalizedPathIsUnchanged(): void - { - $path = implode(DIRECTORY_SEPARATOR, ['usr', 'bin', 'gs']); - $this->assertEquals($path, PathHelper::convertPathSeparator($path)); - } - - public function testPathWithSpacesIsPreserved(): void - { - $result = PathHelper::convertPathSeparator('path/with spaces/file.pdf'); - $this->assertEquals(implode(DIRECTORY_SEPARATOR, ['path', 'with spaces', 'file.pdf']), $result); - } -} diff --git a/tests/Helpers/PathHelperTest.php b/tests/Helpers/PathHelperTest.php index f7b58ea..03e1964 100644 --- a/tests/Helpers/PathHelperTest.php +++ b/tests/Helpers/PathHelperTest.php @@ -14,4 +14,48 @@ public function testPathShouldEqualOriginPathAfterConversion(): void { $this->assertEquals(implode(DIRECTORY_SEPARATOR, ['usr', 'bin', 'gs']), PathHelper::convertPathSeparator('usr/bin/gs')); } + + /** + * @return void + */ + public function testEmptyStringReturnsEmptyString(): void + { + $this->assertEquals('', PathHelper::convertPathSeparator('')); + } + + /** + * @return void + */ + public function testBackslashesAreConverted(): void + { + $result = PathHelper::convertPathSeparator('usr\\bin\\gs'); + $this->assertEquals(implode(DIRECTORY_SEPARATOR, ['usr', 'bin', 'gs']), $result); + } + + /** + * @return void + */ + public function testMixedSeparatorsAreNormalized(): void + { + $result = PathHelper::convertPathSeparator('usr/bin\\gs'); + $this->assertEquals(implode(DIRECTORY_SEPARATOR, ['usr', 'bin', 'gs']), $result); + } + + /** + * @return void + */ + public function testAlreadyNormalizedPathIsUnchanged(): void + { + $path = implode(DIRECTORY_SEPARATOR, ['usr', 'bin', 'gs']); + $this->assertEquals($path, PathHelper::convertPathSeparator($path)); + } + + /** + * @return void + */ + public function testPathWithSpacesIsPreserved(): void + { + $result = PathHelper::convertPathSeparator('path/with spaces/file.pdf'); + $this->assertEquals(implode(DIRECTORY_SEPARATOR, ['path', 'with spaces', 'file.pdf']), $result); + } } From 7f634caa0718d21e8b131a43e6de422f8d5199b0 Mon Sep 17 00:00:00 2001 From: Jerry Chen Date: Sat, 18 Apr 2026 04:46:27 +0800 Subject: [PATCH 07/13] [chore] upgrade actions/checkout to v4.2.2 and codecov-action to v5 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e1f9b4b..707c6c2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: ["7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"] name: PHP ${{ matrix.php-versions }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4.2.2 - name: Update advanced packaging tools run: sudo apt update - name: Install Ghostscript @@ -31,7 +31,7 @@ jobs: - name: Run tests run: composer test - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml From 3c484782e2ced0fff4e18f4d0ecc31a8fb3c0e47 Mon Sep 17 00:00:00 2001 From: Jerry Chen Date: Sat, 18 Apr 2026 04:48:31 +0800 Subject: [PATCH 08/13] [chore] simplify build workflow, add fail-fast false, upload coverage once --- .github/workflows/build.yml | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 707c6c2..6943467 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,32 +5,37 @@ on: pull_request: branches: [master] jobs: - run: - runs-on: ${{ matrix.operating-system }} + test: + runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - operating-system: [ubuntu-latest] - php-versions: - ["7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"] - name: PHP ${{ matrix.php-versions }} + php-version: ["7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"] + name: PHP ${{ matrix.php-version }} steps: - uses: actions/checkout@v4.2.2 - - name: Update advanced packaging tools - run: sudo apt update + - name: Install Ghostscript - run: sudo apt install ghostscript - - name: Install PHP + run: sudo apt-get update -qq && sudo apt-get install -y ghostscript + + - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ matrix.php-versions }} + php-version: ${{ matrix.php-version }} coverage: xdebug + tools: composer:v2 + - name: Install dependencies - run: composer self-update && composer install && composer dump-autoload - - name: Copy .env.example to .env + run: composer install --no-interaction --prefer-dist + + - name: Copy .env run: cp .env.example .env + - name: Run tests run: composer test + - name: Upload coverage to Codecov + if: matrix.php-version == '8.1' uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} From 5dda7402fbe82a811a75c335d84a5b33c11615b6 Mon Sep 17 00:00:00 2001 From: Jerry Chen Date: Sat, 18 Apr 2026 04:49:18 +0800 Subject: [PATCH 09/13] [chore] opt into Node.js 24 for GitHub Actions to suppress deprecation warnings --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6943467..6317cf6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,9 @@ on: branches: [master] pull_request: branches: [master] +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + jobs: test: runs-on: ubuntu-latest From c1057914238becb895c18f17301479bda3607641 Mon Sep 17 00:00:00 2001 From: Jerry Chen Date: Sat, 18 Apr 2026 04:51:34 +0800 Subject: [PATCH 10/13] [chore] upgrade actions/checkout to v4.3.1 for native Node.js 24 support --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6317cf6..8148c7f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: php-version: ["7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"] name: PHP ${{ matrix.php-version }} steps: - - uses: actions/checkout@v4.2.2 + - uses: actions/checkout@v4.3.1 - name: Install Ghostscript run: sudo apt-get update -qq && sudo apt-get install -y ghostscript From fdfdb9bdc67618f8bef9a032ff330db846080ec2 Mon Sep 17 00:00:00 2001 From: Jerry Chen Date: Sat, 18 Apr 2026 04:52:02 +0800 Subject: [PATCH 11/13] [chore] upload coverage only on PHP 8.4 (latest) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8148c7f..2450f7f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ jobs: run: composer test - name: Upload coverage to Codecov - if: matrix.php-version == '8.1' + if: matrix.php-version == '8.4' uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} From d3595ad6815b7a4eb63120ef114fd50a231a15bf Mon Sep 17 00:00:00 2001 From: Jerry Chen Date: Sat, 18 Apr 2026 04:53:48 +0800 Subject: [PATCH 12/13] [chore] upgrade actions/checkout to v5.0.1 (node24 native) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2450f7f..9a06861 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: php-version: ["7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2", "8.3", "8.4"] name: PHP ${{ matrix.php-version }} steps: - - uses: actions/checkout@v4.3.1 + - uses: actions/checkout@v5.0.1 - name: Install Ghostscript run: sudo apt-get update -qq && sudo apt-get install -y ghostscript From 7433d1c09f57bd975103e9c66e31966ae098fd82 Mon Sep 17 00:00:00 2001 From: Jerry Chen Date: Sat, 18 Apr 2026 05:02:17 +0800 Subject: [PATCH 13/13] [chore] upgrade codecov-action to v6, remove node24 workaround --- .github/workflows/build.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9a06861..dfe2c6a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,9 +4,6 @@ on: branches: [master] pull_request: branches: [master] -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - jobs: test: runs-on: ubuntu-latest @@ -39,7 +36,7 @@ jobs: - name: Upload coverage to Codecov if: matrix.php-version == '8.4' - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v6 with: token: ${{ secrets.CODECOV_TOKEN }} files: ./coverage.xml