From bff1cf7fb5e195485c9d0ac210f2a0af9bf7f8fc Mon Sep 17 00:00:00 2001 From: Louis Fortunier Date: Fri, 18 Sep 2026 16:53:02 +0200 Subject: [PATCH 1/2] Add command phpunit-deprecated --- CHANGELOG.md | 5 + make/test.mk | 6 +- phpunit.xml.dist | 1 + recipes.json | 3 +- smartbooster.standard-bundle.1.7.json | 32 ++ src/Phpunit/DeprecationLoggerExtension.php | 65 ++++ src/Phpunit/DeprecationLoggerSubscriber.php | 379 ++++++++++++++++++++ 7 files changed, 489 insertions(+), 2 deletions(-) create mode 100644 smartbooster.standard-bundle.1.7.json create mode 100644 src/Phpunit/DeprecationLoggerExtension.php create mode 100644 src/Phpunit/DeprecationLoggerSubscriber.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 0358a8f..0a14140 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ CHANGELOG for 1.x =================== +## v1.7.0 - (2026-09-18) +### Added +- PHPUnit extension `DeprecationLoggerExtension` and `DeprecationLoggerSubscriber` to intercept and log unhandled deprecations during test execution +- `phpunit-deprecated` make target to run the test suite and export formatted deprecation reports to a log file. + ## v1.6.0 - (2026-09-14) ### TODO When updating Manual steps `composer recipes:install smartbooster/standard-bundle --reset --force` cannot handle by itself when a project upgrades to `1.6` : diff --git a/make/test.mk b/make/test.mk index 4dce30b..644dcce 100644 --- a/make/test.mk +++ b/make/test.mk @@ -1,10 +1,14 @@ ## ## Tests ## ----- -.PHONY: phpunit coverage-text coverage-html coverage jest +.PHONY: phpunit phpunit-deprecated coverage-text coverage-html coverage jest phpunit: ## Launch all tests vendor/bin/simple-phpunit +phpunit-deprecated: + rm -rf var/cache/test # must be done for get all deprecated + DEPRECATIONS_LOG_FILE="var/log/deprecations_test.log" vendor/bin/simple-phpunit + coverage-text: ## Launch all tests with code coverage text XDEBUG_MODE=coverage vendor/bin/simple-phpunit --coverage-text diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f004356..d192bf9 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -45,6 +45,7 @@ + diff --git a/recipes.json b/recipes.json index fd60449..de3f045 100644 --- a/recipes.json +++ b/recipes.json @@ -14,7 +14,8 @@ "1.4.1", "1.4.2", "1.5", - "1.6" + "1.6", + "1.7" ] }, "branch": "main", diff --git a/smartbooster.standard-bundle.1.7.json b/smartbooster.standard-bundle.1.7.json new file mode 100644 index 0000000..811e116 --- /dev/null +++ b/smartbooster.standard-bundle.1.7.json @@ -0,0 +1,32 @@ +{ + "manifests": { + "smartbooster/standard-bundle": { + "manifest": { + "bundles": { + "DAMA\\DoctrineTestBundle\\DAMADoctrineTestBundle": ["test"], + "Liip\\TestFixturesBundle\\LiipTestFixturesBundle": ["test"], + "Smart\\StandardBundle\\SmartStandardBundle": ["dev", "test"] + }, + "copy-from-package": { + "config/packages/dama_doctrine_test_bundle.yaml": "%CONFIG_DIR%/packages/test/dama_doctrine_test_bundle.yaml", + "config/packages/liip_test_fixtures.yaml": "%CONFIG_DIR%/packages/test/liip_test_fixtures.yaml", + "make/dev.mk": "make/dev.mk", + "make/qualimetry.mk": "make/qualimetry.mk", + "make/security.mk": "make/security.mk", + "make/test.mk": "make/test.mk", + "dast_scan/build-plan.sh": "dast_scan/build-plan.sh", + "dast_scan/scripts/": "dast_scan/scripts/", + "tests/bootstrap.php": "tests/bootstrap.php", + ".php-cs-fixer.dist.php": ".php-cs-fixer.dist.php", + "phpstan.neon": "phpstan.neon", + "phpstan-baseline.neon": "phpstan-baseline.neon", + "psalm.xml": "psalm.xml", + "psalm-taint-stubs.php": "psalm-taint-stubs.php", + "psalm-taint-baseline.xml": "psalm-taint-baseline.xml", + "phpunit.xml.dist": "phpunit.xml.dist" + } + }, + "ref": "fe844be745ab5a4df693fc47c32f03f6c8e6bab8" + } + } +} diff --git a/src/Phpunit/DeprecationLoggerExtension.php b/src/Phpunit/DeprecationLoggerExtension.php new file mode 100644 index 0000000..47afd5c --- /dev/null +++ b/src/Phpunit/DeprecationLoggerExtension.php @@ -0,0 +1,65 @@ +resolveLogFile($parameters); + + if (null === $logFile) { + return; + } + + $logDir = \dirname($logFile); + if (!is_dir($logDir)) { + @mkdir($logDir, 0o777, true); + } + + $facade->registerSubscriber(new DeprecationLoggerSubscriber($logFile)); + } + + private function resolveLogFile(ParameterCollection $parameters): ?string + { + $path = $_SERVER['DEPRECATIONS_LOG_FILE'] + ?? $_ENV['DEPRECATIONS_LOG_FILE'] + ?? getenv('DEPRECATIONS_LOG_FILE') + ?: null; + + if (!$path || !is_string($path)) { + return null; + } + + if ($this->isAbsolutePath($path)) { + return $path; + } + + $projectRoot = getcwd() ?: \dirname(__DIR__, 3); + + return $projectRoot . DIRECTORY_SEPARATOR . $path; + } + + private function isAbsolutePath(string $path): bool + { + if ('' === $path) { + return false; + } + + if ('/' === $path[0] || '\\' === $path[0]) { + return true; + } + + return (bool) preg_match('/^[A-Za-z]:[\\\\\\/]/', $path); + } +} diff --git a/src/Phpunit/DeprecationLoggerSubscriber.php b/src/Phpunit/DeprecationLoggerSubscriber.php new file mode 100644 index 0000000..825da2a --- /dev/null +++ b/src/Phpunit/DeprecationLoggerSubscriber.php @@ -0,0 +1,379 @@ + + * }>> + */ + private array $deprecations = []; + + /** + * @var array> + */ + private array $testsByType = []; + private bool $printed = false; + + public function __construct(?string $logFile) + { + $this->logFile = $logFile; + + if (null !== $this->logFile) { + register_shutdown_function(function (): void { // phpcs:ignore + $this->writeSummaryToLog(); + }); + + // Catch @trigger_error() + set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline): bool { + if (E_USER_DEPRECATED === $errno || E_DEPRECATED === $errno) { + $this->recordSuppressedDeprecation('user', $errfile, $errline, $errstr); + + return true; // Avoid console display + } + + return false; + }); + } + } + + public function notify(object $event): void + { + if (null === $this->logFile) { + return; + } + + if ($event instanceof TestDeprecationTriggered) { + $this->recordDeprecation( + 'user', + $event->file(), + $event->line(), + $event->message(), + $event->test() + ); + + return; + } + + if ($event instanceof TestPhpDeprecationTriggered) { + $this->recordDeprecation( + 'php', + $event->file(), + $event->line(), + $event->message(), + $event->test() + ); + + return; + } + + if ($event instanceof TestPhpunitDeprecationTriggered) { + $this->recordTestOnlyDeprecation( + 'phpunit', + $event->message(), + $event->test() + ); + + return; + } + + if ($event instanceof TestRunnerDeprecationTriggered) { + $this->recordRunnerDeprecation('runner', $event->message()); + } + } + + private function recordDeprecation(string $type, string $file, int $line, string $message, Test $test): void + { + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + + /** @var string $file */ + /** @var int $line */ + [$file, $line] = $this->resolveRealFileAndLine($file, $line, $trace); + + $key = hash('sha256', $type . "\n" . $file . "\n" . $line . "\n" . $message); + + if (!isset($this->deprecations[$type][$key])) { + $this->deprecations[$type][$key] = [ + 'file' => $file, + 'line' => $line, + 'message' => $message, + 'count' => 0, + 'tests' => [], + ]; + } + + ++$this->deprecations[$type][$key]['count']; + + $testId = $test->id(); + $testFile = $test->file(); + $testLine = null; + if ($test instanceof TestMethod) { + $testLine = $test->line(); + } + + $this->deprecations[$type][$key]['tests'][$testId] = [ + 'file' => $testFile, + 'line' => $testLine, + ]; + $this->testsByType[$type][$testId] = true; + } + + private function recordSuppressedDeprecation(string $type, string $file, int $line, string $message): void + { + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + + /** @var string $file */ + /** @var int $line */ + [$file, $line] = $this->resolveRealFileAndLine($file, $line, $trace); + + $key = hash('sha256', $type . "\n" . $file . "\n" . $line . "\n" . $message); + + if (!isset($this->deprecations[$type][$key])) { + $this->deprecations[$type][$key] = [ + 'file' => $file, + 'line' => $line, + 'message' => $message, + 'count' => 0, + 'tests' => [], + ]; + } + + ++$this->deprecations[$type][$key]['count']; + + /** @var string $testId */ + /** @var string $testFile */ + /** @var int $testLine */ + [$testId, $testFile, $testLine] = $this->findTestFromTrace($trace); + + $this->deprecations[$type][$key]['tests'][$testId] = [ + 'file' => $testFile, + 'line' => $testLine, + ]; + $this->testsByType[$type][$testId] = true; + } + + private function recordTestOnlyDeprecation(string $type, string $message, Test $test): void + { + $key = hash('sha256', $type . "\n" . $message); + + if (!isset($this->deprecations[$type][$key])) { + $this->deprecations[$type][$key] = [ + 'file' => '[unknown]', + 'line' => 0, + 'message' => $message, + 'count' => 0, + 'tests' => [], + ]; + } + + ++$this->deprecations[$type][$key]['count']; + + $testId = $test->id(); + $testFile = $test->file(); + $testLine = null; + if ($test instanceof TestMethod) { + $testLine = $test->line(); + } + + $this->deprecations[$type][$key]['tests'][$testId] = [ + 'file' => $testFile, + 'line' => $testLine, + ]; + $this->testsByType[$type][$testId] = true; + } + + private function recordRunnerDeprecation(string $type, string $message): void + { + $key = hash('sha256', $type . "\n" . $message); + + if (!isset($this->deprecations[$type][$key])) { + $this->deprecations[$type][$key] = [ + 'file' => '[test-runner]', + 'line' => 0, + 'message' => $message, + 'count' => 0, + 'tests' => [], + ]; + } + + ++$this->deprecations[$type][$key]['count']; + } + + /** + * @param list $trace + * + * @return array{0: string, 1: int} + */ + private function resolveRealFileAndLine(string $file, int $line, array $trace): array + { + $wrappers = [ + 'vendor/doctrine/deprecations', + 'vendor/symfony/deprecation-contracts', + ]; + + foreach ($wrappers as $wrapper) { + if (str_contains($file, $wrapper)) { + foreach ($trace as $frame) { + if ( + isset($frame['file']) + && !str_contains($frame['file'], 'vendor/doctrine/deprecations') + && !str_contains($frame['file'], 'vendor/symfony/deprecation-contracts') + && __FILE__ !== $frame['file'] + ) { + return [$frame['file'], $frame['line'] ?? $line]; + } + } + break; + } + } + + return [$file, $line]; + } + + /** + * @param list $trace + * + * @return array{0: string, 1: string|null, 2: int|null} + */ + private function findTestFromTrace(array $trace): array + { + $testId = '[kernel / boot]'; + $testFile = null; + $testLine = null; + + foreach ($trace as $frame) { + if (isset($frame['class']) && is_subclass_of($frame['class'], TestCase::class)) { + $testId = $frame['class'] . '::' . ($frame['function'] ?? ''); + $testFile = $frame['file'] ?? null; + $testLine = $frame['line'] ?? null; + break; + } + } + + return [$testId, $testFile, $testLine]; + } + + private function writeSummaryToLog(): void + { + if ($this->printed || null === $this->logFile) { + return; + } + + $types = [ + 'php' => 'PHP', + 'user' => 'user', + 'phpunit' => 'phpunit', + 'runner' => 'test runner', + ]; + + $output = ''; + + foreach ($types as $type => $label) { + if (empty($this->deprecations[$type])) { + continue; + } + + $totalEvents = 0; + foreach ($this->deprecations[$type] as $data) { + $totalEvents += $data['count']; + } + + $testCount = isset($this->testsByType[$type]) ? count($this->testsByType[$type]) : 0; + + if ('' === $output) { + $output .= $this->buildHeader(); + } else { + $output .= "\n"; + } + + $output .= sprintf( + "%d tests triggered %d %s deprecations:\n\n", + $testCount, + $totalEvents, + $label + ); + + $index = 1; + foreach ($this->deprecations[$type] as $data) { + $output .= sprintf( + "%d) %s:%d\n%s\n\nTriggered by:\n\n", + $index, + $data['file'], + $data['line'], + $data['message'] + ); + + foreach ($data['tests'] as $testId => $testInfo) { + $output .= '* ' . $testId . "\n"; + if (!empty($testInfo['file'])) { + $output .= ' ' . $testInfo['file']; + if (null !== $testInfo['line']) { + $output .= ':' . $testInfo['line']; + } + $output .= "\n"; + } + } + + if (empty($data['tests'])) { + $output .= "* test runner\n"; + } + + $output .= "\n"; + ++$index; + } + } + + if ('' === $output) { + $output = $this->buildHeader() . "No deprecations.\n"; + } + + $this->printed = true; + @file_put_contents($this->logFile, $output, LOCK_EX); + } + + private function buildHeader(): string + { + $memoryText = $this->formatBytes(memory_get_peak_usage(true)); + + return sprintf("Memory: %s\n\n", $memoryText); + } + + private function formatBytes(int $bytes): string + { + $mb = $bytes / 1024 / 1024; + + return sprintf('%.2f MB', $mb); + } +} From 964048b7044f0e1e0c4b3b4147e088e9e6423ca6 Mon Sep 17 00:00:00 2001 From: Mathieu Ducrot Date: Mon, 21 Sep 2026 10:46:19 +0200 Subject: [PATCH 2/2] Add `docs/front_code_validation.md` to changelog and fix 1.7 ref recipe --- CHANGELOG.md | 3 ++- smartbooster.standard-bundle.1.7.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a14140..ce8618f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,10 @@ CHANGELOG for 1.x =================== -## v1.7.0 - (2026-09-18) +## v1.7.0 - (2026-09-21) ### Added - PHPUnit extension `DeprecationLoggerExtension` and `DeprecationLoggerSubscriber` to intercept and log unhandled deprecations during test execution - `phpunit-deprecated` make target to run the test suite and export formatted deprecation reports to a log file. +- `docs/front_code_validation.md` standard added and mention on README.md ## v1.6.0 - (2026-09-14) ### TODO When updating diff --git a/smartbooster.standard-bundle.1.7.json b/smartbooster.standard-bundle.1.7.json index 811e116..a392240 100644 --- a/smartbooster.standard-bundle.1.7.json +++ b/smartbooster.standard-bundle.1.7.json @@ -26,7 +26,7 @@ "phpunit.xml.dist": "phpunit.xml.dist" } }, - "ref": "fe844be745ab5a4df693fc47c32f03f6c8e6bab8" + "ref": "3c1c7b7b1b0dd63e1fc6b77bf2549bbd778fb4ac" } } }