From 3422a77cfae6433a9d0232efb03fbebc37796681 Mon Sep 17 00:00:00 2001 From: macronom Date: Tue, 14 Jul 2026 13:14:00 +0200 Subject: [PATCH 1/6] Add PHPUnit setup and full SeriesTitle test coverage Set up PHPUnit 12 (phpunit.xml.dist, autoload-dev Catenvis\Tests) and run it as a fifth CI step. Cover SeriesTitle::pick completely: both modes, every fallback stage, empty/missing rows, and the default userLang. --- .github/workflows/ci.yml | 5 +- .gitignore | 1 + composer.json | 8 +- composer.lock | 1683 ++++++++++++++++++++++++++++++++++++- phpunit.xml.dist | 18 + tests/SeriesTitleTest.php | 91 ++ 6 files changed, 1803 insertions(+), 3 deletions(-) create mode 100644 phpunit.xml.dist create mode 100644 tests/SeriesTitleTest.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df796fb..48363a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,10 +31,13 @@ jobs: run: composer install --no-interaction --no-progress --prefer-dist - name: Lint PHP files - run: find src bin html templates -name '*.php' -print0 | xargs -0 -n1 -P4 php -l + run: find src bin html templates tests -name '*.php' -print0 | xargs -0 -n1 -P4 php -l - name: Static analysis (PHPStan) run: vendor/bin/phpstan analyse --no-progress - name: Check UI translations run: php bin/check_translations.php + + - name: Tests (PHPUnit) + run: vendor/bin/phpunit diff --git a/.gitignore b/.gitignore index f8565f9..666c672 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /.cache/ *.log /deploy/deploy.env +/.phpunit.result.cache diff --git a/composer.json b/composer.json index cad972b..25835ba 100644 --- a/composer.json +++ b/composer.json @@ -7,13 +7,19 @@ "php": ">=8.3" }, "require-dev": { - "phpstan/phpstan": "^1.11" + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^12.5" }, "autoload": { "psr-4": { "Catenvis\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Catenvis\\Tests\\": "tests/" + } + }, "config": { "optimize-autoloader": true } diff --git a/composer.lock b/composer.lock index 749afe7..ae3d9cc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,9 +4,244 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3434f49ce914a98f895c02c4a1490802", + "content-hash": "7b951f1da8a995fbcaf8361c65db7b3e", "packages": [], "packages-dev": [ + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.8.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" + }, + "time": "2026-07-04T14:30:18+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, { "name": "phpstan/phpstan", "version": "1.12.33", @@ -59,6 +294,1452 @@ } ], "time": "2026-02-28T20:30:03+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.5.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "186dab580576598076de6818596d12b61801880e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/186dab580576598076de6818596d12b61801880e", + "reference": "186dab580576598076de6818596d12b61801880e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.3", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.1.2", + "sebastian/lines-of-code": "^4.0.1", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.28" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2026-06-01T13:24:19+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T14:04:18+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:16+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.5.31", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "0608d157a284f15cc73b99a3327eff06b66a176d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0608d157a284f15cc73b99a3327eff06b66a176d", + "reference": "0608d157a284f15cc73b99a3327eff06b66a176d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-filter": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.7", + "phpunit/php-file-iterator": "^6.0.1", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.1", + "sebastian/comparator": "^7.1.8", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.1.2", + "sebastian/exporter": "^7.0.3", + "sebastian/global-state": "^8.0.3", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/recursion-context": "^7.0.1", + "sebastian/type": "^6.0.4", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.31" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-07-06T14:54:16+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/7d05781b13f7dec9043a629a21d086ed74582a15", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2026-05-17T05:29:34+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.1.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "7c65c1e79836812819705b473a90c12399542485" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7c65c1e79836812819705b473a90c12399542485", + "reference": "7c65c1e79836812819705b473a90c12399542485", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-05-21T04:45:25+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/9d32c685773823b1983e256ae4ecd48a10d6e439", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.26" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2026-05-25T13:40:20+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2026-05-20T04:37:17+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b164d3274d6537ab462591c5755f76a8f5b1aae9", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0.1" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.5.28" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2026-06-01T15:10:33+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d543b8ef219dcd8da262cbb958639a96bedba10e", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.7.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code", + "type": "tidelift" + } + ], + "time": "2026-05-19T16:22:07+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:44:59+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "82ff822c2edc46724be9f7411d3163021f602773" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/82ff822c2edc46724be9f7411d3163021f602773", + "reference": "82ff822c2edc46724be9f7411d3163021f602773", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2026-05-20T06:45:45+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:38+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" } ], "aliases": [], diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..216f239 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,18 @@ + + + + + tests + + + + + src + + + diff --git a/tests/SeriesTitleTest.php b/tests/SeriesTitleTest.php new file mode 100644 index 0000000..1017261 --- /dev/null +++ b/tests/SeriesTitleTest.php @@ -0,0 +1,91 @@ + English -> original, plus the "original" mode). + */ +final class SeriesTitleTest extends TestCase { + /** + * @param array $row + */ + #[DataProvider('titleCases')] + public function testPickSelectsExpectedTitle(array $row, string $lang, string $userLang, string $expected): void { + self::assertSame($expected, SeriesTitle::pick($row, $lang, $userLang)); + } + + /** + * The third argument defaults to 'en', so in the "original" mode an + * English production keeps its translated name without an explicit userLang. + */ + public function testPickUsesEnglishAsDefaultUserLanguage(): void { + $row = ['name' => 'Translated', 'original_name' => 'Original', 'original_language' => 'en']; + + self::assertSame('Translated', SeriesTitle::pick($row, 'original')); + } + + /** + * @return iterable, string, string, string}> + */ + public static function titleCases(): iterable { + // --- own-language mode ($lang != 'original'): name -> title_en -> original_name --- + yield 'own language present wins' => [ + ['name' => 'Der Herr der Ringe', 'title_en' => 'The Lord of the Rings', 'original_name' => 'The Lord of the Rings', 'original_language' => 'en'], + 'de', 'de', 'Der Herr der Ringe', + ]; + + yield 'falls back to English when own translation missing' => [ + ['name' => '', 'title_en' => 'The Lord of the Rings', 'original_name' => 'Original', 'original_language' => 'en'], + 'de', 'de', 'The Lord of the Rings', + ]; + + yield 'falls back to original name when own and English missing' => [ + ['name' => '', 'title_en' => '', 'original_name' => 'Original Name', 'original_language' => 'ja'], + 'de', 'de', 'Original Name', + ]; + + yield 'returns empty string when the row is empty' => [ + [], + 'de', 'de', '', + ]; + + // --- "original" mode, own-language production: name -> original_name --- + yield 'original mode keeps translated title for own-language production' => [ + ['name' => 'Deutscher Titel', 'original_name' => 'Origineel', 'original_language' => 'de'], + 'original', 'de', 'Deutscher Titel', + ]; + + yield 'original mode own-language falls back to original name when translation missing' => [ + ['name' => '', 'original_name' => 'Origineel', 'original_language' => 'nl'], + 'original', 'nl', 'Origineel', + ]; + + // --- "original" mode, foreign production: title_en -> original_name -> name --- + yield 'original mode prefers English then original for a foreign production' => [ + ['name' => 'Übersetzt', 'title_en' => 'English Title', 'original_name' => 'Original', 'original_language' => 'en'], + 'original', 'de', 'English Title', + ]; + + yield 'original mode foreign falls back to original name when English missing' => [ + ['name' => 'Übersetzt', 'title_en' => '', 'original_name' => 'Original', 'original_language' => 'ja'], + 'original', 'de', 'Original', + ]; + + yield 'original mode foreign uses translated name as last resort' => [ + ['name' => 'Übersetzt', 'title_en' => '', 'original_name' => '', 'original_language' => 'ja'], + 'original', 'de', 'Übersetzt', + ]; + + yield 'original mode returns empty string when nothing is set' => [ + ['original_language' => 'de'], + 'original', 'de', '', + ]; + } +} From bd0204ed28bfd9de514446b9f25d30e92e43b54a Mon Sep 17 00:00:00 2001 From: macronom Date: Tue, 14 Jul 2026 17:24:13 +0200 Subject: [PATCH 2/6] Add Request unit tests Cover Request parsing end to end: HTTP method and POST detection, path normalization (base-path stripping, query removal, trailing slash, percent-decoding, defaults) and the type-safe accessors (body-over-query precedence, trimming, numeric/type filters). --- tests/RequestTest.php | 139 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 tests/RequestTest.php diff --git a/tests/RequestTest.php b/tests/RequestTest.php new file mode 100644 index 0000000..5144575 --- /dev/null +++ b/tests/RequestTest.php @@ -0,0 +1,139 @@ + 'post']); + + self::assertSame('POST', $request->method()); + self::assertTrue($request->isPost()); + } + + public function testMethodDefaultsToGetWhenAbsent(): void { + $request = new Request([], [], []); + + self::assertSame('GET', $request->method()); + self::assertFalse($request->isPost()); + } + + /** + * @param array $server + */ + #[DataProvider('pathCases')] + public function testPathIsNormalized(array $server, string $basePath, string $expected): void { + $request = new Request([], [], $server, $basePath); + + self::assertSame($expected, $request->path()); + } + + /** + * @return iterable, string, string}> + */ + public static function pathCases(): iterable { + yield 'strips base path and query string' => [['REQUEST_URI' => '/catenvis/series/5?foo=bar'], '/catenvis', '/series/5']; + yield 'root after stripping base path' => [['REQUEST_URI' => '/catenvis/'], '/catenvis', '/']; + yield 'plain root' => [['REQUEST_URI' => '/'], '', '/']; + yield 'removes trailing slash' => [['REQUEST_URI' => '/series/5/'], '', '/series/5']; + yield 'decodes percent-encoding' => [['REQUEST_URI' => '/foo%20bar'], '', '/foo bar']; + yield 'leaves path unchanged when base path does not match' => [['REQUEST_URI' => '/series'], '/other', '/series']; + yield 'defaults to root when REQUEST_URI absent' => [[], '', '/']; + } + + public function testGetStringPrefersBodyAndTrims(): void { + $request = new Request(['x' => 'from-query'], ['x' => ' from-body '], []); + + self::assertSame('from-body', $request->getString('x')); + } + + public function testGetStringFallsBackToQueryThenDefault(): void { + $request = new Request(['x' => 'from-query'], [], []); + + self::assertSame('from-query', $request->getString('x')); + self::assertSame('fallback', $request->getString('missing', 'fallback')); + } + + public function testGetStringReturnsDefaultForNonScalar(): void { + $request = new Request([], ['x' => ['a', 'b']], []); + + self::assertSame('', $request->getString('x')); + } + + #[DataProvider('intCases')] + public function testGetInt(mixed $raw, int $expected): void { + $request = new Request([], ['x' => $raw], []); + + self::assertSame($expected, $request->getInt('x', -1)); + } + + /** + * @return iterable + */ + public static function intCases(): iterable { + yield 'numeric string' => ['42', 42]; + yield 'float string is truncated' => ['3.9', 3]; + yield 'non-numeric returns default' => ['abc', -1]; + yield 'array returns default' => [['1'], -1]; + } + + public function testGetIntFallsBackToDefaultWhenAbsent(): void { + $request = new Request([], [], []); + + self::assertSame(7, $request->getInt('missing', 7)); + } + + #[DataProvider('boolCases')] + public function testGetBool(mixed $raw, bool $expected): void { + $request = new Request([], ['x' => $raw], []); + + self::assertSame($expected, $request->getBool('x')); + } + + /** + * @return iterable + */ + public static function boolCases(): iterable { + yield 'string one' => ['1', true]; + yield 'int one' => [1, true]; + yield 'true boolean' => [true, true]; + yield 'string true' => ['true', true]; + yield 'string on' => ['on', true]; + yield 'string zero' => ['0', false]; + yield 'string false' => ['false', false]; + yield 'arbitrary value' => ['yes', false]; + } + + public function testGetBoolIsFalseWhenAbsent(): void { + $request = new Request([], [], []); + + self::assertFalse($request->getBool('missing')); + } + + public function testGetIntListFiltersToIntegers(): void { + $request = new Request([], ['ids' => ['1', '2', 'x', 3, '4.5']], []); + + self::assertSame([1, 2, 3, 4], $request->getIntList('ids')); + } + + public function testGetIntListReturnsEmptyForNonArray(): void { + $request = new Request([], ['ids' => '1'], []); + + self::assertSame([], $request->getIntList('ids')); + } + + public function testGetIntListPrefersBodyOverQuery(): void { + $request = new Request(['ids' => ['9']], ['ids' => ['1', '2']], []); + + self::assertSame([1, 2], $request->getIntList('ids')); + } +} From b9b930a1fb0cec27d35bd1960e8b0af69545eff8 Mon Sep 17 00:00:00 2001 From: macronom Date: Tue, 14 Jul 2026 17:26:44 +0200 Subject: [PATCH 3/6] Add Router unit tests Cover route matching: static and {name}-placeholder paths (single segment), multiple parameters, method matching and case-insensitive registration, anchoring, handler invocation and first-match-wins. --- tests/RouterTest.php | 119 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 tests/RouterTest.php diff --git a/tests/RouterTest.php b/tests/RouterTest.php new file mode 100644 index 0000000..e41a7ea --- /dev/null +++ b/tests/RouterTest.php @@ -0,0 +1,119 @@ +add('GET', '/series/{id}', static function (): void {}); + + self::assertFalse($router->dispatch($this->request('GET', '/unknown'))); + } + + public function testStaticRouteMatchesAndInvokesHandler(): void { + $router = new Router(); + $called = false; + $router->add('GET', '/import', static function () use (&$called): void { + $called = true; + }); + + self::assertTrue($router->dispatch($this->request('GET', '/import'))); + self::assertTrue($called); + } + + public function testPlaceholderRouteExtractsParameter(): void { + $router = new Router(); + $captured = null; + $router->add('GET', '/series/{id}', static function (Request $request, array $params) use (&$captured): void { + $captured = $params; + }); + + self::assertTrue($router->dispatch($this->request('GET', '/series/5'))); + self::assertSame(['id' => '5'], $captured); + } + + public function testMultiplePlaceholdersAreExtractedInOrder(): void { + $router = new Router(); + $captured = null; + $router->add('GET', '/series/{id}/season/{number}', static function (Request $request, array $params) use (&$captured): void { + $captured = $params; + }); + + self::assertTrue($router->dispatch($this->request('GET', '/series/5/season/2'))); + self::assertSame(['id' => '5', 'number' => '2'], $captured); + } + + public function testMethodMismatchDoesNotMatch(): void { + $router = new Router(); + $router->add('POST', '/follow', static function (): void {}); + + self::assertFalse($router->dispatch($this->request('GET', '/follow'))); + } + + public function testMethodRegistrationIsCaseInsensitive(): void { + $router = new Router(); + $called = false; + $router->add('get', '/', static function () use (&$called): void { + $called = true; + }); + + self::assertTrue($router->dispatch($this->request('GET', '/'))); + self::assertTrue($called); + } + + public function testPlaceholderMatchesSingleSegmentOnly(): void { + $router = new Router(); + $router->add('GET', '/series/{id}', static function (): void {}); + + self::assertFalse($router->dispatch($this->request('GET', '/series/5/extra'))); + } + + public function testStaticRouteIsAnchored(): void { + $router = new Router(); + $router->add('GET', '/series', static function (): void {}); + + self::assertFalse($router->dispatch($this->request('GET', '/series/5'))); + } + + public function testHandlerReceivesTheDispatchedRequest(): void { + $router = new Router(); + $request = $this->request('GET', '/import'); + $received = null; + $router->add('GET', '/import', static function (Request $r, array $params) use (&$received): void { + $received = $r; + }); + + $router->dispatch($request); + + self::assertSame($request, $received); + } + + public function testFirstMatchingRouteWins(): void { + $router = new Router(); + $order = []; + $router->add('GET', '/series/{id}', static function () use (&$order): void { + $order[] = 'first'; + }); + $router->add('GET', '/series/{slug}', static function () use (&$order): void { + $order[] = 'second'; + }); + + $router->dispatch($this->request('GET', '/series/5')); + + self::assertSame(['first'], $order); + } + + private function request(string $method, string $path): Request { + return new Request([], [], ['REQUEST_METHOD' => $method, 'REQUEST_URI' => $path]); + } +} From 11e1f8e6c997e9a570ce40ac0e4083051e57b62a Mon Sep 17 00:00:00 2001 From: macronom Date: Tue, 14 Jul 2026 17:28:15 +0200 Subject: [PATCH 4/6] Add Config unit tests Cover configuration handling: load() errors (missing file, non-array return), dot-notation lookup, defaults for missing keys and traversal into non-arrays, and isProduction(). --- tests/ConfigTest.php | 99 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 tests/ConfigTest.php diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php new file mode 100644 index 0000000..710bbe0 --- /dev/null +++ b/tests/ConfigTest.php @@ -0,0 +1,99 @@ + Temp fixture files to clean up. */ + private array $tempFiles = []; + + protected function tearDown(): void { + foreach ($this->tempFiles as $file) { + @unlink($file); + } + $this->tempFiles = []; + } + + public function testLoadThrowsWhenFileMissing(): void { + $this->expectException(RuntimeException::class); + + Config::load(sys_get_temp_dir() . '/catenvis-does-not-exist-' . __LINE__ . '.php'); + } + + public function testLoadThrowsWhenFileDoesNotReturnArray(): void { + $path = $this->writeConfigFile('expectException(RuntimeException::class); + + Config::load($path); + } + + public function testGetReturnsTopLevelValue(): void { + $config = $this->makeConfig(['base_url' => '/catenvis']); + + self::assertSame('/catenvis', $config->get('base_url')); + } + + public function testGetResolvesNestedValueViaDotNotation(): void { + $config = $this->makeConfig(['db' => ['host' => '127.0.0.1', 'port' => 3306]]); + + self::assertSame('127.0.0.1', $config->get('db.host')); + self::assertSame(3306, $config->get('db.port')); + } + + public function testGetReturnsWholeNestedArray(): void { + $config = $this->makeConfig(['db' => ['host' => '127.0.0.1']]); + + self::assertSame(['host' => '127.0.0.1'], $config->get('db')); + } + + public function testGetReturnsDefaultForMissingKey(): void { + $config = $this->makeConfig(['db' => ['host' => '127.0.0.1']]); + + self::assertNull($config->get('missing')); + self::assertSame('fallback', $config->get('missing', 'fallback')); + self::assertSame('fallback', $config->get('db.missing', 'fallback')); + } + + public function testGetReturnsDefaultWhenTraversingIntoNonArray(): void { + $config = $this->makeConfig(['db' => 'not-an-array']); + + self::assertSame('fallback', $config->get('db.host', 'fallback')); + } + + public function testIsProductionIsTrueForProductionEnvironment(): void { + $config = $this->makeConfig(['environment' => 'production']); + + self::assertTrue($config->isProduction()); + } + + public function testIsProductionIsFalseOtherwise(): void { + self::assertFalse($this->makeConfig(['environment' => 'development'])->isProduction()); + self::assertFalse($this->makeConfig([])->isProduction()); + } + + /** + * @param array $values + */ + private function makeConfig(array $values): Config { + return Config::load($this->writeConfigFile('tempFiles[] = $path; + + return $path; + } +} From 563fc1f22391d5a2a2a4087449df326fb8ef1d26 Mon Sep 17 00:00:00 2001 From: macronom Date: Tue, 14 Jul 2026 17:29:25 +0200 Subject: [PATCH 5/6] Add TmdbClient::languageCode unit tests Cover the static BCP 47 -> ISO-639-1 reduction: region tags, bare codes, lowercasing and the empty-string fallback to 'en'. --- tests/TmdbClientTest.php | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 tests/TmdbClientTest.php diff --git a/tests/TmdbClientTest.php b/tests/TmdbClientTest.php new file mode 100644 index 0000000..7f2b4ce --- /dev/null +++ b/tests/TmdbClientTest.php @@ -0,0 +1,33 @@ + + */ + public static function languageCases(): iterable { + yield 'BCP 47 tag reduced to ISO-639-1' => ['en-US', 'en']; + yield 'German region tag' => ['de-DE', 'de']; + yield 'Portuguese Brazil' => ['pt-BR', 'pt']; + yield 'bare code passes through' => ['fr', 'fr']; + yield 'uppercase input is lowercased' => ['ES', 'es']; + yield 'empty string falls back to en' => ['', 'en']; + } +} From 4bb4c3270df1a8382401165cab34a6b5e0384458 Mon Sep 17 00:00:00 2001 From: macronom Date: Tue, 14 Jul 2026 17:31:14 +0200 Subject: [PATCH 6/6] Add Translator unit tests Cover UI translation: catalog lookup with fallback to the English source (missing entry, missing file, broken JSON), metadata (__*) and empty-value filtering, and vsprintf arguments including positional placeholders for word order. --- tests/TranslatorTest.php | 117 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 tests/TranslatorTest.php diff --git a/tests/TranslatorTest.php b/tests/TranslatorTest.php new file mode 100644 index 0000000..b05febd --- /dev/null +++ b/tests/TranslatorTest.php @@ -0,0 +1,117 @@ +projectDir = sys_get_temp_dir() . '/catenvis-tr-' . uniqid(); + mkdir($this->projectDir . '/lang', 0777, true); + } + + protected function tearDown(): void { + foreach (glob($this->projectDir . '/lang/*') ?: [] as $file) { + @unlink($file); + } + @rmdir($this->projectDir . '/lang'); + @rmdir($this->projectDir); + } + + public function testEnglishSourceIsReturnedAsIs(): void { + $translator = new Translator($this->projectDir, 'en'); + + self::assertSame('Hello', $translator->translate('Hello')); + } + + public function testReturnsTranslationFromCatalog(): void { + $this->writeCatalog('de', ['Hello' => 'Hallo']); + $translator = new Translator($this->projectDir, 'de'); + + self::assertSame('Hallo', $translator->translate('Hello')); + } + + public function testFallsBackToSourceForMissingEntry(): void { + $this->writeCatalog('de', ['Hello' => 'Hallo']); + $translator = new Translator($this->projectDir, 'de'); + + self::assertSame('Goodbye', $translator->translate('Goodbye')); + } + + public function testFallsBackToSourceWhenCatalogFileMissing(): void { + $translator = new Translator($this->projectDir, 'fr'); + + self::assertSame('Hello', $translator->translate('Hello')); + } + + public function testFallsBackToSourceOnBrokenJson(): void { + file_put_contents($this->projectDir . '/lang/de.json', '{ this is not valid json'); + $translator = new Translator($this->projectDir, 'de'); + + // The translator logs the broken catalog via error_log; redirect that + // to a temp file so it does not clutter the test/CI output. + $previous = ini_set('error_log', $this->projectDir . '/lang/error.log'); + try { + self::assertSame('Hello', $translator->translate('Hello')); + } finally { + if ($previous !== false) { + ini_set('error_log', $previous); + } + } + } + + public function testMetadataKeysAreNotTranslated(): void { + $this->writeCatalog('de', ['__language__' => 'Deutsch', 'Hello' => 'Hallo']); + $translator = new Translator($this->projectDir, 'de'); + + self::assertSame('__language__', $translator->translate('__language__')); + self::assertSame('Hallo', $translator->translate('Hello')); + } + + public function testEmptyTranslationValueFallsBackToSource(): void { + $this->writeCatalog('de', ['Hello' => '']); + $translator = new Translator($this->projectDir, 'de'); + + self::assertSame('Hello', $translator->translate('Hello')); + } + + public function testAppliesVsprintfArguments(): void { + $this->writeCatalog('de', ['%d new episodes' => '%d neue Folgen']); + $translator = new Translator($this->projectDir, 'de'); + + self::assertSame('3 neue Folgen', $translator->translate('%d new episodes', 3)); + } + + public function testSupportsPositionalPlaceholdersForWordOrder(): void { + $this->writeCatalog('de', ['%1$s %2$d' => '%2$d. %1$s']); + $translator = new Translator($this->projectDir, 'de'); + + self::assertSame('16. Jan', $translator->translate('%1$s %2$d', 'Jan', 16)); + } + + public function testAppliesArgumentsToSourceTextWhenUntranslated(): void { + $translator = new Translator($this->projectDir, 'en'); + + self::assertSame('5 items', $translator->translate('%d items', 5)); + } + + /** + * @param array $entries + */ + private function writeCatalog(string $lang, array $entries): void { + file_put_contents( + $this->projectDir . '/lang/' . $lang . '.json', + (string) json_encode($entries, JSON_UNESCAPED_UNICODE) + ); + } +}