diff --git a/.php-version b/.php-version index 0f197cf..6085eb8 100644 --- a/.php-version +++ b/.php-version @@ -1 +1 @@ -8.3.30 \ No newline at end of file +8.3.31 \ No newline at end of file diff --git a/composer.lock b/composer.lock index f5c58eb..c6ebbdc 100644 --- a/composer.lock +++ b/composer.lock @@ -894,16 +894,16 @@ }, { "name": "phpunit/phpunit", - "version": "12.5.23", + "version": "12.5.24", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969" + "reference": "d75dd30597caa80e72fad2ef7904601a30ef1046" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969", - "reference": "c54fcf3d6bcb6e96ac2f7e40097dc37b5f139969", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d75dd30597caa80e72fad2ef7904601a30ef1046", + "reference": "d75dd30597caa80e72fad2ef7904601a30ef1046", "shasum": "" }, "require": { @@ -972,7 +972,7 @@ "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.23" + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.24" }, "funding": [ { @@ -980,7 +980,7 @@ "type": "other" } ], - "time": "2026-04-18T06:12:49+00:00" + "time": "2026-05-01T04:21:04+00:00" }, { "name": "psr/container", @@ -2115,16 +2115,16 @@ }, { "name": "symfony/console", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707" + "reference": "d7d2b64a45a89d607865927b176fa51c33ddbb58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", - "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707", + "url": "https://api.github.com/repos/symfony/console/zipball/d7d2b64a45a89d607865927b176fa51c33ddbb58", + "reference": "d7d2b64a45a89d607865927b176fa51c33ddbb58", "shasum": "" }, "require": { @@ -2189,7 +2189,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.8" + "source": "https://github.com/symfony/console/tree/v7.4.9" }, "funding": [ { @@ -2209,20 +2209,20 @@ "type": "tidelift" } ], - "time": "2026-03-30T13:54:39+00:00" + "time": "2026-04-22T15:21:55+00:00" }, { "name": "symfony/css-selector", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "b055f228a4178a1d6774909903905e3475f3eac8" + "reference": "b75663ed96cf4756e28e3105476f220f92886cc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/b055f228a4178a1d6774909903905e3475f3eac8", - "reference": "b055f228a4178a1d6774909903905e3475f3eac8", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/b75663ed96cf4756e28e3105476f220f92886cc4", + "reference": "b75663ed96cf4756e28e3105476f220f92886cc4", "shasum": "" }, "require": { @@ -2258,7 +2258,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.4.8" + "source": "https://github.com/symfony/css-selector/tree/v7.4.9" }, "funding": [ { @@ -2278,20 +2278,20 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-04-18T13:18:21+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { @@ -2304,7 +2304,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -2329,7 +2329,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -2340,25 +2340,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-04-13T15:52:40+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "f57b899fa736fd71121168ef268f23c206083f0a" + "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f57b899fa736fd71121168ef268f23c206083f0a", - "reference": "f57b899fa736fd71121168ef268f23c206083f0a", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e4a2e29753c7801f7a8340e066cfa788f3bc8101", + "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101", "shasum": "" }, "require": { @@ -2410,7 +2414,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.8" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.9" }, "funding": [ { @@ -2430,20 +2434,20 @@ "type": "tidelift" } ], - "time": "2026-03-30T13:54:39+00:00" + "time": "2026-04-18T13:18:21+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", - "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", "shasum": "" }, "require": { @@ -2457,7 +2461,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -2490,7 +2494,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" }, "funding": [ { @@ -2501,12 +2505,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-01-05T13:30:16+00:00" }, { "name": "symfony/finder", @@ -2913,16 +2921,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.6.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", "shasum": "" }, "require": { @@ -2940,7 +2948,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -2976,7 +2984,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" }, "funding": [ { @@ -2996,7 +3004,7 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:30:57+00:00" + "time": "2026-03-28T09:44:51+00:00" }, { "name": "symfony/string", @@ -3178,16 +3186,16 @@ }, { "name": "symfony/yaml", - "version": "v7.4.8", + "version": "v7.4.10", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "c58fdf7b3d6c2995368264c49e4e8b05bcff2883" + "reference": "c660d6538545a3e8e65a5621ee3d7a6d352892c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/c58fdf7b3d6c2995368264c49e4e8b05bcff2883", - "reference": "c58fdf7b3d6c2995368264c49e4e8b05bcff2883", + "url": "https://api.github.com/repos/symfony/yaml/zipball/c660d6538545a3e8e65a5621ee3d7a6d352892c7", + "reference": "c660d6538545a3e8e65a5621ee3d7a6d352892c7", "shasum": "" }, "require": { @@ -3230,7 +3238,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.4.8" + "source": "https://github.com/symfony/yaml/tree/v7.4.10" }, "funding": [ { @@ -3250,7 +3258,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-05-05T08:01:55+00:00" }, { "name": "theseer/tokenizer", @@ -3711,42 +3719,42 @@ }, { "name": "ergebnis/agent-detector", - "version": "1.1.1", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/ergebnis/agent-detector.git", - "reference": "5b654a9f1ff8a5d2ce6a57568df5ae8801c87f64" + "reference": "e211f17928c8b95a51e06040792d57f5462fb271" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ergebnis/agent-detector/zipball/5b654a9f1ff8a5d2ce6a57568df5ae8801c87f64", - "reference": "5b654a9f1ff8a5d2ce6a57568df5ae8801c87f64", + "url": "https://api.github.com/repos/ergebnis/agent-detector/zipball/e211f17928c8b95a51e06040792d57f5462fb271", + "reference": "e211f17928c8b95a51e06040792d57f5462fb271", "shasum": "" }, "require": { "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0" }, "require-dev": { - "ergebnis/composer-normalize": "^2.50.0", + "ergebnis/composer-normalize": "^2.51.0", "ergebnis/license": "^2.7.0", "ergebnis/php-cs-fixer-config": "^6.60.2", "ergebnis/phpstan-rules": "^2.13.1", "ergebnis/phpunit-slow-test-detector": "^2.24.0", - "ergebnis/rector-rules": "^1.16.0", + "ergebnis/rector-rules": "^1.18.1", "fakerphp/faker": "^1.24.1", "infection/infection": "^0.26.6", "phpstan/extension-installer": "^1.4.3", - "phpstan/phpstan": "^2.1.46", + "phpstan/phpstan": "^2.1.54", "phpstan/phpstan-deprecation-rules": "^2.0.4", "phpstan/phpstan-phpunit": "^2.0.16", "phpstan/phpstan-strict-rules": "^2.0.10", "phpunit/phpunit": "^9.6.34", - "rector/rector": "^2.4.1" + "rector/rector": "^2.4.2" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.0-dev" + "dev-main": "1.2-dev" }, "composer-normalize": { "indent-size": 2, @@ -3776,7 +3784,7 @@ "security": "https://github.com/ergebnis/agent-detector/blob/main/.github/SECURITY.md", "source": "https://github.com/ergebnis/agent-detector" }, - "time": "2026-04-10T13:45:13+00:00" + "time": "2026-05-07T08:19:07+00:00" }, { "name": "evenement/evenement", @@ -4175,11 +4183,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.51", + "version": "2.1.54", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc3b523c45e714c70de2ac5113b958223b55dc59", - "reference": "dc3b523c45e714c70de2ac5113b958223b55dc59", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd", + "reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd", "shasum": "" }, "require": { @@ -4224,7 +4232,7 @@ "type": "github" } ], - "time": "2026-04-21T18:22:01+00:00" + "time": "2026-04-29T13:31:09+00:00" }, { "name": "phpstan/phpstan-webmozart-assert", @@ -4859,18 +4867,18 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "08cd07f04fb07fb4d316e956801d57b700cf7096" + "reference": "ec0c867c22e5b8d392815d48e4cc9de37470e8c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/08cd07f04fb07fb4d316e956801d57b700cf7096", - "reference": "08cd07f04fb07fb4d316e956801d57b700cf7096", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/ec0c867c22e5b8d392815d48e4cc9de37470e8c0", + "reference": "ec0c867c22e5b8d392815d48e4cc9de37470e8c0", "shasum": "" }, "conflict": { "3f/pygmentize": "<1.2", "adaptcms/adaptcms": "<=1.3", - "admidio/admidio": "<5.0.8", + "admidio/admidio": "<=5.0.8", "adodb/adodb-php": "<=5.22.9", "aheinze/cockpit": "<2.2", "aimeos/ai-admin-graphql": ">=2022.04.1,<2022.10.10|>=2023.04.1,<2023.10.6|>=2024.04.1,<2024.07.2", @@ -4924,14 +4932,14 @@ "awesome-support/awesome-support": "<=6.0.7", "aws/aws-sdk-php": "<=3.371.3", "ayacoo/redirect-tab": "<2.1.2|>=3,<3.1.7|>=4,<4.0.5", - "azuracast/azuracast": "<=0.23.3", + "azuracast/azuracast": "<=0.23.5", "b13/seo_basics": "<0.8.2", "backdrop/backdrop": "<=1.32", "backpack/crud": "<3.4.9", "backpack/filemanager": "<2.0.2|>=3,<3.0.9", "bacula-web/bacula-web": "<9.7.1", "badaso/core": "<=2.9.11", - "bagisto/bagisto": "<2.3.10", + "bagisto/bagisto": "<=2.3.15", "barrelstrength/sprout-base-email": "<1.2.7", "barrelstrength/sprout-forms": "<3.9", "barryvdh/laravel-translation-manager": "<0.6.8", @@ -4979,7 +4987,7 @@ "cesnet/simplesamlphp-module-proxystatistics": "<3.1", "chriskacerguis/codeigniter-restserver": "<=2.7.1", "chrome-php/chrome": "<1.14", - "ci4-cms-erp/ci4ms": "<0.31.5", + "ci4-cms-erp/ci4ms": "<=0.31.7", "civicrm/civicrm-core": ">=4.2,<4.2.9|>=4.3,<4.3.3", "ckeditor/ckeditor": "<4.25", "clickstorm/cs-seo": ">=6,<6.8|>=7,<7.5|>=8,<8.4|>=9,<9.3", @@ -5012,7 +5020,7 @@ "cpsit/typo3-mailqueue": "<0.4.5|>=0.5,<0.5.2", "craftcms/aws-s3": ">=2.0.2,<=2.2.4", "craftcms/azure-blob": ">=2.0.0.0-beta1,<=2.1", - "craftcms/cms": "<=4.17.8|>=5,<5.9.15", + "craftcms/cms": "<4.17.12|>=5,<5.9.18", "craftcms/commerce": ">=4,<4.11|>=5,<5.6", "craftcms/composer": ">=4.0.0.0-RC1-dev,<=4.10|>=5.0.0.0-RC1-dev,<=5.5.1", "craftcms/craft": ">=3.5,<=4.16.17|>=5.0.0.0-RC1-dev,<=5.8.21", @@ -5031,6 +5039,7 @@ "david-garcia/phpwhois": "<=4.3.1", "dbrisinajumi/d2files": "<1", "dcat/laravel-admin": "<=2.1.3|==2.2.0.0-beta|==2.2.2.0-beta", + "dedoc/scramble": ">=0.13.2,<0.13.22", "derhansen/fe_change_pwd": "<2.0.5|>=3,<3.0.3", "derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1|>=7,<7.4", "desperado/xml-bundle": "<=0.1.7", @@ -5052,7 +5061,7 @@ "doctrine/mongodb-odm": "<1.0.2", "doctrine/mongodb-odm-bundle": "<3.0.1", "doctrine/orm": ">=1,<1.2.4|>=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4", - "dolibarr/dolibarr": "<=22.0.4", + "dolibarr/dolibarr": "<=23.0.2", "dompdf/dompdf": "<2.0.4", "doublethreedigital/guest-entries": "<3.1.2", "dreamfactory/df-core": "<1.0.4", @@ -5131,7 +5140,7 @@ "ezsystems/repository-forms": ">=2.3,<2.3.2.1-dev|>=2.5,<2.5.15", "ezyang/htmlpurifier": "<=4.2", "facade/ignition": "<1.16.15|>=2,<2.4.2|>=2.5,<2.5.2", - "facturascripts/facturascripts": "<2025.81", + "facturascripts/facturascripts": "<=2025.92|>=2026,<=2026.1", "fastly/magento2": "<1.2.26", "feehi/cms": "<=2.1.1", "feehi/feehicms": "<=2.1.1", @@ -5154,6 +5163,7 @@ "flarum/nicknames": "<1.8.3", "flarum/sticky": ">=0.1.0.0-beta14,<=0.1.0.0-beta15", "flarum/tags": "<=0.1.0.0-beta13", + "flightphp/core": "<3.18.1", "floriangaerber/magnesium": "<0.3.1", "fluidtypo3/vhs": "<5.1.1", "fof/byobu": ">=0.3.0.0-beta2,<1.1.7", @@ -5177,14 +5187,16 @@ "froxlor/froxlor": "<2.3.6", "frozennode/administrator": "<=5.0.12", "fuel/core": "<1.8.1", - "funadmin/funadmin": "<=7.1.0.0-RC4", + "funadmin/funadmin": "<=7.1.0.0-RC6", "gaoming13/wechat-php-sdk": "<=1.10.2", "genix/cms": "<=1.1.11", "georgringer/news": "<1.3.3", "geshi/geshi": "<=1.0.9.1", "getformwork/formwork": "<=2.3.3", - "getgrav/grav": "<1.11.0.0-beta1", - "getkirby/cms": "<5.4", + "getgrav/grav": "<2.0.0.0-beta4", + "getgrav/grav-plugin-api": "<1.0.0.0-beta15", + "getgrav/grav-plugin-form": "<9.1", + "getkirby/cms": "<4.9|>=5,<5.4", "getkirby/kirby": "<3.9.8.3-dev|>=3.10,<3.10.1.2-dev|>=4,<4.7.1", "getkirby/panel": "<2.5.14", "getkirby/starterkit": "<=3.7.0.2", @@ -5243,8 +5255,9 @@ "innologi/typo3-appointments": "<2.0.6", "intelliants/subrion": "<4.2.2", "inter-mediator/inter-mediator": "==5.5", + "intercom/intercom-php": "==5.0.2", "invoiceninja/invoiceninja": "<5.13.4", - "ipl/web": "<0.10.1", + "ipl/web": "<=0.13", "islandora/crayfish": "<4.1", "islandora/islandora": ">=2,<2.4.1", "ivankristianto/phpwhois": "<=4.3", @@ -5282,7 +5295,7 @@ "kelvinmo/simplexrd": "<3.1.1", "kevinpapst/kimai2": "<1.16.7", "khodakhah/nodcms": "<=3.4.1", - "kimai/kimai": "<2.54", + "kimai/kimai": "<=2.55", "kitodo/presentation": "<3.2.3|>=3.3,<3.3.4", "klaviyo/magento2-extension": ">=1,<3", "knplabs/knp-snappy": "<=1.4.2", @@ -5340,7 +5353,7 @@ "maikuolan/phpmussel": ">=1,<1.6", "mainwp/mainwp": "<=4.4.3.3", "manogi/nova-tiptap": "<=3.2.6", - "mantisbt/mantisbt": "<2.28.1", + "mantisbt/mantisbt": "<2.28.2", "marcwillmann/turn": "<0.3.3", "markhuot/craftql": "<=1.3.7", "marshmallow/nova-tiptap": "<5.7", @@ -5350,6 +5363,7 @@ "mautic/core-lib": ">=1.0.0.0-beta,<4.4.13|>=5.0.0.0-alpha,<5.1.1", "mautic/grapes-js-builder-bundle": ">=4,<4.4.18|>=5,<5.2.9|>=6,<6.0.7", "maximebf/debugbar": "<1.19", + "mckenziearts/livewire-markdown-editor": "<1.3", "mdanter/ecc": "<2", "mediawiki/abuse-filter": "<1.39.9|>=1.40,<1.41.3|>=1.42,<1.42.2", "mediawiki/cargo": "<3.8.3", @@ -5374,6 +5388,7 @@ "miniorange/miniorange-saml": "<1.4.3", "miraheze/ts-portal": "<=33", "mittwald/typo3_forum": "<1.2.1", + "mix/mix": ">=2,<=2.2.17", "mobiledetect/mobiledetectlib": "<2.8.32", "modx/revolution": "<=3.1", "mojo42/jirafeau": "<4.4", @@ -5393,6 +5408,7 @@ "munkireport/softwareupdate": "<1.6", "mustache/mustache": ">=2,<2.14.1", "mwdelaney/wp-enable-svg": "<=0.2", + "nabeel/phpvms": "<7.0.6", "namshi/jose": "<2.2", "nasirkhan/laravel-starter": "<11.11", "nategood/httpful": "<1", @@ -5433,7 +5449,7 @@ "open-web-analytics/open-web-analytics": "<1.8.1", "opencart/opencart": ">=0", "openid/php-openid": "<2.3", - "openmage/magento-lts": "<20.17", + "openmage/magento-lts": "<=20.17", "opensolutions/vimbadmin": "<=3.0.15", "opensource-workshop/connect-cms": "<1.41.1|>=2,<2.41.1", "orchid/platform": ">=8,<14.43", @@ -5476,13 +5492,13 @@ "phpmailer/phpmailer": "<6.5", "phpmussel/phpmussel": ">=1,<1.6", "phpmyadmin/phpmyadmin": "<5.2.2", - "phpmyfaq/phpmyfaq": "<=4.1", + "phpmyfaq/phpmyfaq": "<=4.1.1", "phpoffice/common": "<0.2.9", "phpoffice/math": "<=0.2", "phpoffice/phpexcel": "<=1.8.2", - "phpoffice/phpspreadsheet": "<1.30|>=2,<2.1.12|>=2.2,<2.4|>=3,<3.10|>=4,<5", + "phpoffice/phpspreadsheet": "<=1.30.3|>=2,<=2.1.15|>=2.2,<=2.4.4|>=3,<=3.10.4|>=4,<=5.6", "phppgadmin/phppgadmin": "<=7.13", - "phpseclib/phpseclib": "<2.0.53|>=3,<3.0.51", + "phpseclib/phpseclib": "<=2.0.53|>=3,<=3.0.51", "phpservermon/phpservermon": "<3.6", "phpsysinfo/phpsysinfo": "<3.4.3", "phpunit/phpunit": "<8.5.52|>=9,<9.6.33|>=10,<10.5.62|>=11,<11.5.50|>=12,<12.5.8|>=12.5.21,<12.5.22|>=13.1.5,<13.1.6", @@ -5498,7 +5514,7 @@ "pimcore/demo": "<10.3", "pimcore/ecommerce-framework-bundle": "<1.0.10", "pimcore/perspective-editor": "<1.5.1", - "pimcore/pimcore": "<=11.5.14.1|>=12,<12.3.3", + "pimcore/pimcore": "<=11.5.14.1|>=12,<12.3.3|==12.3.3", "pimcore/web2print-tools-bundle": "<=5.2.1|>=6.0.0.0-RC1-dev,<=6.1", "piwik/piwik": "<1.11", "pixelfed/pixelfed": "<0.12.5", @@ -5512,9 +5528,9 @@ "prestashop/blockwishlist": ">=2,<2.1.1", "prestashop/contactform": ">=1.0.1,<4.3", "prestashop/gamification": "<2.3.2", - "prestashop/prestashop": "<8.2.5|>=9.0.0.0-alpha1,<9.1", + "prestashop/prestashop": "<8.2.6|>=9,<9.1.1", "prestashop/productcomments": "<5.0.2", - "prestashop/ps_checkout": "<4.4.1|>=5,<5.0.5", + "prestashop/ps_checkout": "<5.3", "prestashop/ps_contactinfo": "<=3.3.2", "prestashop/ps_emailsubscription": "<2.6.1", "prestashop/ps_facetedsearch": "<3.4.1", @@ -5551,6 +5567,7 @@ "rhukster/dom-sanitizer": "<1.0.10", "rmccue/requests": ">=1.6,<1.8", "roadiz/documents": "<2.3.42|>=2.4,<2.5.44|>=2.6,<2.6.28|>=2.7,<2.7.9", + "roadiz/openid": "<2.3.43|>=2.5,<2.5.45|>=2.6,<2.6.31|>=2.7,<2.7.18", "robrichards/xmlseclibs": "<3.1.5", "roots/soil": "<4.1", "roundcube/roundcubemail": "<1.5.10|>=1.6,<1.6.11|>=1.7.0.0-beta,<1.7.0.0-RC5-dev", @@ -5575,7 +5592,7 @@ "shopware/shopware": "<=5.7.17|>=6.4.6,<6.6.10.10-dev|>=6.7,<6.7.6.1-dev", "shopware/storefront": "<6.6.10.10-dev|>=6.7,<6.7.5.1-dev", "shopxo/shopxo": "<=6.4", - "showdoc/showdoc": "<2.10.4", + "showdoc/showdoc": "<3.8.1", "shuchkin/simplexlsx": ">=1.0.12,<1.1.13", "silverstripe-australia/advancedreports": ">=1,<=2", "silverstripe/admin": "<1.13.19|>=2,<2.1.8", @@ -5614,7 +5631,7 @@ "slim/slim": "<2.6", "slub/slub-events": "<3.0.3", "smarty/smarty": "<4.5.3|>=5,<5.1.1", - "snipe/snipe-it": "<8.3.7", + "snipe/snipe-it": "<8.4.1", "socalnick/scn-social-auth": "<1.15.2", "socialiteproviders/steam": "<1.1", "solspace/craft-freeform": "<4.1.29|>=5,<=5.14.6", @@ -5632,9 +5649,9 @@ "starcitizentools/short-description": ">=4,<4.0.1", "starcitizentools/tabber-neue": ">=1.9.1,<2.7.2|>=3,<3.1.1", "starcitizenwiki/embedvideo": "<=4", - "statamic/cms": "<5.73.20|>=6,<6.13", + "statamic/cms": "<5.73.21|>=6,<6.15", "stormpath/sdk": "<9.9.99", - "studio-42/elfinder": "<2.1.67", + "studio-42/elfinder": "<=2.1.67", "studiomitte/friendlycaptcha": "<0.1.4", "subhh/libconnect": "<7.0.8|>=8,<8.1", "sukohi/surpass": "<1", @@ -5706,7 +5723,7 @@ "thelia/thelia": ">=2.1,<2.1.3", "theonedemon/phpwhois": "<=4.2.5", "thinkcmf/thinkcmf": "<6.0.8", - "thorsten/phpmyfaq": "<4.1.1", + "thorsten/phpmyfaq": "<=4.1.1", "tikiwiki/tiki-manager": "<=17.1", "timber/timber": ">=0.16.6,<1.23.1|>=1.24,<1.24.1|>=2,<2.1", "tinymce/tinymce": "<7.2", @@ -5779,7 +5796,7 @@ "wallabag/wallabag": "<2.6.11", "wanglelecc/laracms": "<=1.0.3", "wapplersystems/a21glossary": "<=0.4.10", - "web-auth/webauthn-framework": ">=3.3,<3.3.4|>=4.5,<4.9|>=5.2,<5.2.4", + "web-auth/webauthn-framework": ">=3.3,<3.3.4|>=4.5,<4.9|>=5.2,<5.2.4|>=5.3,<5.3.1", "web-auth/webauthn-lib": ">=4.5,<4.9|>=5.2,<5.2.4", "web-auth/webauthn-symfony-bundle": ">=5.2,<5.2.4", "web-feet/coastercms": "==5.5", @@ -5788,7 +5805,7 @@ "webcoast/deferred-image-processing": "<1.0.2", "webklex/laravel-imap": "<5.3", "webklex/php-imap": "<5.3", - "webonyx/graphql-php": "<=15.31.4", + "webonyx/graphql-php": "<=15.32.2", "webpa/webpa": "<3.1.2", "webreinvent/vaahcms": "<=2.3.1", "wikibase/wikibase": "<=1.39.3", @@ -5818,7 +5835,7 @@ "yidashi/yii2cmf": "<=2", "yii2mod/yii2-cms": "<1.9.2", "yiisoft/yii": "<1.1.31", - "yiisoft/yii2": "<2.0.52", + "yiisoft/yii2": "<2.0.55", "yiisoft/yii2-authclient": "<2.2.15", "yiisoft/yii2-bootstrap": "<2.0.4", "yiisoft/yii2-dev": "<=2.0.45", @@ -5908,20 +5925,20 @@ "type": "tidelift" } ], - "time": "2026-04-24T17:22:29+00:00" + "time": "2026-05-11T19:35:52+00:00" }, { "name": "symfony/filesystem", - "version": "v7.4.8", + "version": "v7.4.9", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5" + "reference": "dcd8f96bcdc0f128ec406c765cc066c6035d1be3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/58b9790d12f9670b7f53a1c1738febd3108970a5", - "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/dcd8f96bcdc0f128ec406c765cc066c6035d1be3", + "reference": "dcd8f96bcdc0f128ec406c765cc066c6035d1be3", "shasum": "" }, "require": { @@ -5958,7 +5975,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.8" + "source": "https://github.com/symfony/filesystem/tree/v7.4.9" }, "funding": [ { @@ -5978,7 +5995,7 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-04-18T13:18:21+00:00" }, { "name": "symfony/options-resolver", diff --git a/src/Config/ReporterConfig.php b/src/Config/ReporterConfig.php index e8877a4..7cbb16e 100644 --- a/src/Config/ReporterConfig.php +++ b/src/Config/ReporterConfig.php @@ -4,6 +4,7 @@ namespace WebProject\Codeception\Module\AiReporter\Config; +use function implode; use function in_array; use InvalidArgumentException; use function is_bool; @@ -54,59 +55,85 @@ private function __construct( public static function fromArray(array $raw, string $defaultOutputDir, string $projectRoot): self { /** @var self::FORMAT_* $format */ - $format = $raw['format'] ?? self::FORMAT_BOTH; - // @phpstan-ignore-next-line - if (!is_string($format) || !in_array($format, [self::FORMAT_TEXT, self::FORMAT_JSON, self::FORMAT_BOTH], true)) { - throw new InvalidArgumentException('Invalid `format`; expected one of: text, json, both.'); + $format = self::readEnum( + $raw['format'] ?? null, + 'format', + [self::FORMAT_TEXT, self::FORMAT_JSON, self::FORMAT_BOTH], + self::FORMAT_BOTH, + ); + + $output = self::readOutput($raw['output'] ?? null, $defaultOutputDir); + $resolved = self::resolvePath($output, $projectRoot); + $trimmed = rtrim($resolved, '/\\'); + + /** @var non-empty-string $outputDir */ + $outputDir = '' === $trimmed ? $resolved : $trimmed; + + return new self( + format: $format, + outputDir: $outputDir, + maxFrames: self::readPositiveInt($raw['max_frames'] ?? null, 'max_frames', self::DEFAULT_MAX_FRAMES), + includeSteps: self::readBool($raw['include_steps'] ?? null, 'include_steps', true), + includeArtifacts: self::readBool($raw['include_artifacts'] ?? null, 'include_artifacts', true), + compactPaths: self::readBool($raw['compact_paths'] ?? null, 'compact_paths', true), + ); + } + + /** + * @param list $allowed + */ + private static function readEnum(mixed $value, string $field, array $allowed, string $default): string + { + if (null === $value) { + return $default; + } + if (!is_string($value) || !in_array($value, $allowed, true)) { + throw new InvalidArgumentException(sprintf('Invalid `%s`; expected one of: %s.', $field, implode(', ', $allowed))); } - $output = $raw['output'] ?? $defaultOutputDir; - if ('' === $output) { - $output = $defaultOutputDir; + return $value; + } + + /** @param non-empty-string $default */ + private static function readOutput(mixed $value, string $default): string + { + if (null === $value || '' === $value) { + return $default; } - // @phpstan-ignore-next-line - if (!is_string($output) || '' === $output) { + if (!is_string($value)) { throw new InvalidArgumentException('Invalid `output`; expected a non-empty directory path.'); } - $outputDir = self::resolvePath($output, $projectRoot); + return $value; + } - /** @var int<1, max> $maxFrames */ - $maxFrames = $raw['max_frames'] ?? self::DEFAULT_MAX_FRAMES; - // @phpstan-ignore-next-line - if (!is_int($maxFrames) || $maxFrames < 1) { - throw new InvalidArgumentException('Invalid `max_frames`; expected a positive integer.'); + /** + * @param int<1, max> $default + * + * @return int<1, max> + */ + private static function readPositiveInt(mixed $value, string $field, int $default): int + { + if (null === $value) { + return $default; } - - $includeSteps = $raw['include_steps'] ?? true; - // @phpstan-ignore-next-line - if (!is_bool($includeSteps)) { - throw new InvalidArgumentException('Invalid `include_steps`; expected boolean.'); + if (!is_int($value) || $value < 1) { + throw new InvalidArgumentException(sprintf('Invalid `%s`; expected a positive integer.', $field)); } - $includeArtifacts = $raw['include_artifacts'] ?? true; - // @phpstan-ignore-next-line - if (!is_bool($includeArtifacts)) { - throw new InvalidArgumentException('Invalid `include_artifacts`; expected boolean.'); - } + return $value; + } - $compactPaths = $raw['compact_paths'] ?? true; - // @phpstan-ignore-next-line - if (!is_bool($compactPaths)) { - throw new InvalidArgumentException('Invalid `compact_paths`; expected boolean.'); + private static function readBool(mixed $value, string $field, bool $default): bool + { + if (null === $value) { + return $default; + } + if (!is_bool($value)) { + throw new InvalidArgumentException(sprintf('Invalid `%s`; expected boolean.', $field)); } - /** @var non-empty-string $outputDir */ - $outputDir = rtrim($outputDir, '/\\'); - - return new self( - format: $format, - outputDir: $outputDir, - maxFrames: $maxFrames, - includeSteps: $includeSteps, - includeArtifacts: $includeArtifacts, - compactPaths: $compactPaths, - ); + return $value; } public function wantsJson(): bool diff --git a/src/Extension/AiReporter.php b/src/Extension/AiReporter.php index 4636f4c..d1a42da 100644 --- a/src/Extension/AiReporter.php +++ b/src/Extension/AiReporter.php @@ -4,8 +4,8 @@ namespace WebProject\Codeception\Module\AiReporter\Extension; +use function array_map; use function array_slice; -use function array_values; use Codeception\Event\FailEvent; use Codeception\Event\PrintResultEvent; use Codeception\Event\SuiteEvent; @@ -129,8 +129,7 @@ public function _initialize(): void public function beforeSuite(SuiteEvent $event): void { - $suite = $event->getSuite(); - $this->currentSuite = null !== $suite ? $suite->getName() : ''; + $this->currentSuite = $event->getSuite()?->getName() ?? ''; } public function onFailure(FailEvent $event): void @@ -201,7 +200,7 @@ private function captureFailure(string $status, FailEvent $event): void ? $this->scenarioExtractor->extract($test, $this->runtimeConfig->maxFrames()) : []; - $hints = array_values($this->hintGenerator->generate($throwable, $trace, $scenarioSteps)); + $hints = $this->hintGenerator->generate($throwable, $trace, $scenarioSteps); $exception = [ 'class' => $throwable::class, @@ -259,42 +258,53 @@ private function printInlineContext(array $failure): void $artifacts = $failure['artifacts']; $this->writeln(' AI Context'); + $this->writeln(sprintf(' Test failed: %s', $this->consoleText->escape($failure['test']['full_name']))); $this->writeln(sprintf(' Exception: %s', $this->consoleText->escape($exception['class']))); $this->writeln(sprintf(' Message: %s', $this->consoleText->escape($this->consoleText->truncate($exception['message'])))); - if (isset($exception['comparison_diff']) && '' !== $exception['comparison_diff']) { - $this->writeln(' Diff:'); - foreach (explode("\n", $exception['comparison_diff']) as $diffLine) { - $this->writeln(sprintf(' %s', $this->consoleText->escape($diffLine))); - } + $diff = $exception['comparison_diff'] ?? ''; + $this->printSection('Diff', '' === $diff ? [] : array_map( + fn (string $line): string => $this->consoleText->escape($line), + explode("\n", $diff), + )); + + $traceLines = []; + foreach (array_slice($trace, 0, $this->runtimeConfig->maxFrames()) as $index => $frame) { + $traceLines[] = sprintf('#%d %s', $index + 1, $this->consoleText->escape($this->traceFrameProcessor->formatFrame($frame))); } + $this->printSection('Trace', $traceLines); - if ([] !== $trace) { - $this->writeln(' Trace:'); - foreach (array_slice($trace, 0, $this->runtimeConfig->maxFrames()) as $index => $frame) { - $this->writeln(sprintf(' #%d %s', $index + 1, $this->consoleText->escape($this->traceFrameProcessor->formatFrame($frame)))); - } + $stepLines = []; + foreach (array_slice($steps, 0, 2) as $step) { + $stepLines[] = sprintf('- %s', $this->consoleText->escape($step['step'])); } + $this->printSection('Scenario', $stepLines); - if ([] !== $steps) { - $this->writeln(' Scenario:'); - foreach (array_slice($steps, 0, 2) as $step) { - $this->writeln(sprintf(' - %s', $this->consoleText->escape($step['step']))); - } + $artifactLines = []; + foreach ($artifacts as $type => $path) { + $artifactLines[] = sprintf('- %s: %s', $this->consoleText->escape($type), $this->consoleText->escape($path)); } + $this->printSection('Artifacts', $artifactLines); - if ([] !== $artifacts) { - $this->writeln(' Artifacts:'); - foreach ($artifacts as $type => $path) { - $this->writeln(sprintf(' - %s: %s', $this->consoleText->escape($type), $this->consoleText->escape($path))); - } + $hintLines = []; + foreach (array_slice($hints, 0, 3) as $hint) { + $hintLines[] = sprintf('- %s', $this->consoleText->escape($hint)); } + $this->printSection('Hints', $hintLines); + } - if ([] !== $hints) { - $this->writeln(' Hints:'); - foreach (array_slice($hints, 0, 3) as $hint) { - $this->writeln(sprintf(' - %s', $this->consoleText->escape($hint))); - } + /** + * @param list $lines + */ + private function printSection(string $title, array $lines): void + { + if ([] === $lines) { + return; + } + + $this->writeln(sprintf(' %s:', $title)); + foreach ($lines as $line) { + $this->writeln(sprintf(' %s', $line)); } } @@ -359,12 +369,9 @@ private function normalizeArtifacts(array $reports): array { $normalized = []; foreach ($reports as $type => $path) { - if (is_scalar($path)) { - $normalized[(string) $type] = $this->pathNormalizer->normalize((string) $path); - continue; - } - - $normalized[(string) $type] = (string) json_encode($path, JSON_INVALID_UTF8_SUBSTITUTE); + $normalized[(string) $type] = is_scalar($path) + ? $this->pathNormalizer->normalize((string) $path) + : (string) json_encode($path, JSON_INVALID_UTF8_SUBSTITUTE); } return $normalized; diff --git a/src/Report/PathNormalizer.php b/src/Report/PathNormalizer.php index 0eb5538..1762897 100644 --- a/src/Report/PathNormalizer.php +++ b/src/Report/PathNormalizer.php @@ -14,14 +14,14 @@ final class PathNormalizer { - private string $normalizedRoot; - private string $normalizedRootLower; + private readonly string $normalizedRoot; + private readonly string $normalizedRootLower; public function __construct(string $projectRoot, private readonly bool $compactPaths) { - $normalized = str_replace('\\', '/', rtrim($projectRoot, '/\\')); - $this->normalizedRoot = $normalized . '/'; - $this->normalizedRootLower = strtolower($this->normalizedRoot); + $normalized = str_replace('\\', '/', rtrim($projectRoot, '/\\')); + $this->normalizedRoot = $normalized . '/'; + $this->normalizedRootLower = strtolower($this->normalizedRoot); } /** diff --git a/src/Util/TraceFrameProcessor.php b/src/Util/TraceFrameProcessor.php index a785c58..15a6677 100644 --- a/src/Util/TraceFrameProcessor.php +++ b/src/Util/TraceFrameProcessor.php @@ -114,12 +114,8 @@ private function isNoiseFrame(array $frame): bool $file = (string)($frame['file'] ?? ''); $call = (string)($frame['call'] ?? ''); - if ($file !== '' && !$this->isFrameworkFile($file)) { - return false; - } - - if ($file !== '' && $this->isFrameworkFile($file)) { - return true; + if ($file !== '') { + return $this->isFrameworkFile($file); } return str_starts_with($call, '[throw] PHPUnit\\Framework\\') diff --git a/tests/Support/Fixture/CapturingOutput.php b/tests/Support/Fixture/CapturingOutput.php new file mode 100644 index 0000000..9b87b01 --- /dev/null +++ b/tests/Support/Fixture/CapturingOutput.php @@ -0,0 +1,36 @@ + false, 'interactive' => false]); + } + + protected function doWrite(string $message, bool $newline): void + { + $this->buffer .= $message; + if ($newline) { + $this->buffer .= "\n"; + } + } + + public function fetch(): string + { + $out = $this->buffer; + $this->buffer = ''; + + return $out; + } +} diff --git a/tests/Support/Fixture/PathNormalizerFactory.php b/tests/Support/Fixture/PathNormalizerFactory.php new file mode 100644 index 0000000..c9fe692 --- /dev/null +++ b/tests/Support/Fixture/PathNormalizerFactory.php @@ -0,0 +1,15 @@ +setName($name); + $metadata->setFilename($filename); + $this->setMetadata($metadata); + } + + public function test(): void + { + } + + public function run(): void + { + } + + public function toString(): string + { + return $this->getName(); + } + + public function getSignature(): string + { + return $this->signature; + } +} diff --git a/tests/Unit/Config/ReporterConfigTest.php b/tests/Unit/Config/ReporterConfigTest.php index 8ee861b..4c6f5fa 100644 --- a/tests/Unit/Config/ReporterConfigTest.php +++ b/tests/Unit/Config/ReporterConfigTest.php @@ -30,22 +30,34 @@ public function testRelativeOutputIsResolvedAgainstProjectRoot(): void self::assertSame('/repo/project/tests/_output', $config->outputDir()); } - public function testInvalidFormatThrows(): void + /** + * @dataProvider provideInvalidConfigThrowsCases + * + * @param array $raw + */ + public function testInvalidConfigThrows(array $raw, string $expectedMessageFragment): void { $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid `format`'); + $this->expectExceptionMessage($expectedMessageFragment); // @phpstan-ignore-next-line - ReporterConfig::fromArray(['format' => 'xml'], '/tmp/default-output', '/repo/project'); + ReporterConfig::fromArray($raw, '/tmp/default-output', '/repo/project'); } - public function testInvalidMaxFramesThrows(): void + /** + * @return array, 1: string}> + */ + public static function provideInvalidConfigThrowsCases(): iterable { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid `max_frames`'); - - // @phpstan-ignore-next-line - ReporterConfig::fromArray(['max_frames' => 0], '/tmp/default-output', '/repo/project'); + return [ + 'invalid format' => [['format' => 'xml'], 'Invalid `format`'], + 'zero max_frames' => [['max_frames' => 0], 'Invalid `max_frames`'], + 'non-int max_frames' => [['max_frames' => '5'], 'Invalid `max_frames`'], + 'non-bool include_steps' => [['include_steps' => 'yes'], 'Invalid `include_steps`'], + 'non-bool include_artifacts'=> [['include_artifacts' => 1], 'Invalid `include_artifacts`'], + 'non-bool compact_paths' => [['compact_paths' => 'true'], 'Invalid `compact_paths`'], + 'non-string output' => [['output' => 123], 'Invalid `output`'], + ]; } public function testRelativeOutputWithWindowsProjectRootIsResolved(): void diff --git a/tests/Unit/Extension/AiReporterTest.php b/tests/Unit/Extension/AiReporterTest.php index a8a66b9..b0d5917 100644 --- a/tests/Unit/Extension/AiReporterTest.php +++ b/tests/Unit/Extension/AiReporterTest.php @@ -4,14 +4,25 @@ namespace WebProject\Codeception\Module\AiReporter\Tests\Unit\Extension; +use Codeception\Event\FailEvent; use Codeception\Event\PrintResultEvent; use Codeception\ResultAggregator; use Codeception\Test\Unit; +use function file_get_contents; +use function is_dir; use function is_file; +use PHPUnit\Framework\AssertionFailedError; +use ReflectionClass; +use ReflectionProperty; +use function rmdir; +use RuntimeException; use function sys_get_temp_dir; use function tempnam; +use function uniqid; use function unlink; use WebProject\Codeception\Module\AiReporter\Extension\AiReporter; +use WebProject\Codeception\Module\AiReporter\Tests\Support\Fixture\CapturingOutput; +use WebProject\Codeception\Module\AiReporter\Tests\Support\Fixture\StubTest; final class AiReporterTest extends Unit { @@ -38,4 +49,166 @@ public function testAfterResultDoesNotThrowWhenReportWriteFails(): void } } } + + public function testInlineContextPrintsTestFailedLineForFailure(): void + { + $reporter = $this->makeReporter(['report' => true]); + $output = $this->captureOutput($reporter); + + $reporter->onFailure(new FailEvent( + $this->makeStubTest('LoginCest:tryToLogIn'), + new AssertionFailedError('expected truthy'), + 0.01, + )); + + $text = $output->fetch(); + self::assertStringContainsString('AI Context', $text); + self::assertStringContainsString('Test failed:', $text); + self::assertStringContainsString(':tryToLogIn', $text); + self::assertStringContainsString('Exception: PHPUnit\\Framework\\AssertionFailedError', $text); + self::assertStringContainsString('Message: expected truthy', $text); + } + + public function testInlineContextPrintsTestFailedLineForError(): void + { + $reporter = $this->makeReporter(['report' => true]); + $output = $this->captureOutput($reporter); + + $reporter->onError(new FailEvent( + $this->makeStubTest('ErrorCest:explodes'), + new RuntimeException('kaboom'), + 0.0, + )); + + $text = $output->fetch(); + self::assertStringContainsString('Test failed:', $text); + self::assertStringContainsString(':explodes', $text); + self::assertStringContainsString('Message: kaboom', $text); + } + + public function testInlineContextPrintsTestFailedLineForWarning(): void + { + $reporter = $this->makeReporter(['report' => true]); + $output = $this->captureOutput($reporter); + + $reporter->onWarning(new FailEvent( + $this->makeStubTest('WarnCest:complains'), + new RuntimeException('be careful'), + 0.0, + )); + + $text = $output->fetch(); + self::assertStringContainsString('Test failed:', $text); + self::assertStringContainsString(':complains', $text); + } + + public function testInlineContextNotPrintedWhenReportOptionDisabled(): void + { + $reporter = $this->makeReporter([]); + $output = $this->captureOutput($reporter); + + $reporter->onFailure(new FailEvent( + $this->makeStubTest('a:b'), + new RuntimeException('boom'), + 0.0, + )); + + self::assertSame('', $output->fetch()); + } + + public function testInlineContextNotPrintedForNonFailureStatuses(): void + { + $reporter = $this->makeReporter(['report' => true]); + $output = $this->captureOutput($reporter); + + $event = new FailEvent( + $this->makeStubTest('a:b'), + new RuntimeException('skip me'), + 0.0, + ); + + $reporter->onSkipped($event); + $reporter->onIncomplete($event); + $reporter->onUseless($event); + + self::assertSame('', $output->fetch()); + } + + public function testAfterResultWritesJsonAndTextReportsCapturedFromFailure(): void + { + $outputDir = sys_get_temp_dir() . '/' . uniqid('ai-reporter-out-', true); + + $reporter = new AiReporter( + [ + 'format' => 'both', + 'output' => $outputDir, + ], + [], + ); + + try { + $reporter->onFailure(new FailEvent( + $this->makeStubTest('LoginCest:tryToLogIn'), + new AssertionFailedError('expected truthy'), + 0.01, + )); + + $reporter->afterResult(new PrintResultEvent(new ResultAggregator())); + + $jsonPath = $outputDir . '/ai-report.json'; + $textPath = $outputDir . '/ai-report.txt'; + + self::assertFileExists($jsonPath); + self::assertFileExists($textPath); + + $json = (string) file_get_contents($jsonPath); + self::assertStringContainsString('"full_name"', $json); + self::assertStringContainsString('PHPUnit\\\\Framework\\\\AssertionFailedError', $json); + self::assertStringContainsString('expected truthy', $json); + + $textReport = (string) file_get_contents($textPath); + self::assertStringContainsString('expected truthy', $textReport); + } finally { + foreach (['ai-report.json', 'ai-report.txt'] as $name) { + $path = $outputDir . '/' . $name; + if (is_file($path)) { + unlink($path); + } + } + if (is_dir($outputDir)) { + rmdir($outputDir); + } + } + } + + /** + * @param array $options + */ + private function makeReporter(array $options): AiReporter + { + return new AiReporter( + [ + 'format' => 'both', + 'output' => sys_get_temp_dir(), + ], + $options, + ); + } + + private function captureOutput(AiReporter $reporter): CapturingOutput + { + $buffer = new CapturingOutput(); + $prop = new ReflectionProperty($reporter, 'output'); + $prop->setValue($reporter, $buffer); + + return $buffer; + } + + private function makeStubTest(string $signature): StubTest + { + $filename = (new ReflectionClass(StubTest::class))->getFileName(); + self::assertNotFalse($filename); + + return new StubTest('stub', $filename, $signature); + } } diff --git a/tests/Unit/Report/PathNormalizerTest.php b/tests/Unit/Report/PathNormalizerTest.php index 5d692cc..514890b 100644 --- a/tests/Unit/Report/PathNormalizerTest.php +++ b/tests/Unit/Report/PathNormalizerTest.php @@ -5,27 +5,27 @@ namespace WebProject\Codeception\Module\AiReporter\Tests\Unit\Report; use Codeception\Test\Unit; -use WebProject\Codeception\Module\AiReporter\Report\PathNormalizer; +use WebProject\Codeception\Module\AiReporter\Tests\Support\Fixture\PathNormalizerFactory; final class PathNormalizerTest extends Unit { public function testCompactsProjectRelativePath(): void { - $normalizer = new PathNormalizer('/repo/project', true); + $normalizer = PathNormalizerFactory::make(); self::assertSame('src/Extension/AiReporter.php', $normalizer->normalize('/repo/project/src/Extension/AiReporter.php')); } public function testLeavesAbsolutePathWhenCompactionDisabled(): void { - $normalizer = new PathNormalizer('/repo/project', false); + $normalizer = PathNormalizerFactory::make(false); self::assertSame('/repo/project/src/Extension/AiReporter.php', $normalizer->normalize('/repo/project/src/Extension/AiReporter.php')); } public function testVendorPathDetection(): void { - $normalizer = new PathNormalizer('/repo/project', true); + $normalizer = PathNormalizerFactory::make(); self::assertTrue($normalizer->isVendorPath('/repo/project/vendor/package/file.php')); self::assertFalse($normalizer->isVendorPath('/repo/project/src/file.php')); @@ -33,7 +33,7 @@ public function testVendorPathDetection(): void public function testCompactsWindowsPathAndNormalizesSeparators(): void { - $normalizer = new PathNormalizer('C:\\repo\\project', true); + $normalizer = PathNormalizerFactory::make(true, 'C:\\repo\\project'); self::assertSame( 'src/Extension/AiReporter.php', @@ -43,7 +43,7 @@ public function testCompactsWindowsPathAndNormalizesSeparators(): void public function testVendorPathDetectionForWindowsStylePaths(): void { - $normalizer = new PathNormalizer('C:\\repo\\project', true); + $normalizer = PathNormalizerFactory::make(true, 'C:\\repo\\project'); self::assertTrue($normalizer->isVendorPath('C:\\repo\\project\\vendor\\package\\file.php')); self::assertFalse($normalizer->isVendorPath('C:\\repo\\project\\src\\file.php')); diff --git a/tests/Unit/Report/TraceNormalizerTest.php b/tests/Unit/Report/TraceNormalizerTest.php index e72e981..502c04d 100644 --- a/tests/Unit/Report/TraceNormalizerTest.php +++ b/tests/Unit/Report/TraceNormalizerTest.php @@ -5,15 +5,14 @@ namespace WebProject\Codeception\Module\AiReporter\Tests\Unit\Report; use Codeception\Test\Unit; -use WebProject\Codeception\Module\AiReporter\Report\PathNormalizer; use WebProject\Codeception\Module\AiReporter\Report\TraceNormalizer; +use WebProject\Codeception\Module\AiReporter\Tests\Support\Fixture\PathNormalizerFactory; final class TraceNormalizerTest extends Unit { public function testFiltersVendorFramesByDefaultAndLimitsSize(): void { - $pathNormalizer = new PathNormalizer('/repo/project', true); - $normalizer = new TraceNormalizer($pathNormalizer, 2); + $normalizer = new TraceNormalizer(PathNormalizerFactory::make(), 2); $frames = [ [ @@ -52,8 +51,7 @@ public function testFiltersVendorFramesByDefaultAndLimitsSize(): void public function testFallsBackToVendorFramesWhenNeeded(): void { - $pathNormalizer = new PathNormalizer('/repo/project', true); - $normalizer = new TraceNormalizer($pathNormalizer, 3); + $normalizer = new TraceNormalizer(PathNormalizerFactory::make(), 3); $frames = [ [ diff --git a/tests/Unit/Util/TraceFrameProcessorTest.php b/tests/Unit/Util/TraceFrameProcessorTest.php index 853fd3c..6ae8229 100644 --- a/tests/Unit/Util/TraceFrameProcessorTest.php +++ b/tests/Unit/Util/TraceFrameProcessorTest.php @@ -6,14 +6,14 @@ use Codeception\Test\Unit; use RuntimeException; -use WebProject\Codeception\Module\AiReporter\Report\PathNormalizer; +use WebProject\Codeception\Module\AiReporter\Tests\Support\Fixture\PathNormalizerFactory; use WebProject\Codeception\Module\AiReporter\Util\TraceFrameProcessor; final class TraceFrameProcessorTest extends Unit { public function testRemoveNoiseFramesDropsFrameworkFramesWhenProjectFrameExists(): void { - $processor = new TraceFrameProcessor(new PathNormalizer('/repo/project', true), 8); + $processor = new TraceFrameProcessor(PathNormalizerFactory::make(), 8); $trace = [ [ @@ -36,7 +36,7 @@ public function testRemoveNoiseFramesDropsFrameworkFramesWhenProjectFrameExists( public function testRemoveNoiseFramesFallsBackWhenOnlyFrameworkFramesExist(): void { - $processor = new TraceFrameProcessor(new PathNormalizer('/repo/project', true), 8); + $processor = new TraceFrameProcessor(PathNormalizerFactory::make(), 8); $trace = [ [ @@ -59,7 +59,7 @@ public function testRemoveNoiseFramesFallsBackWhenOnlyFrameworkFramesExist(): vo public function testPrepareAddsOriginFrameAndFormatsFrame(): void { - $processor = new TraceFrameProcessor(new PathNormalizer('/repo/project', true), 2); + $processor = new TraceFrameProcessor(PathNormalizerFactory::make(), 2); $throwable = new RuntimeException('boom'); $prepared = $processor->prepare($throwable, [