diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6bdb8844..9e8b6885 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,20 +1,22 @@ + failOnWarning="true" + failOnDeprecation="true" + displayDetailsOnTestsThatTriggerDeprecations="true"> tests - + src diff --git a/rector.php b/rector.php index a601b303..68fbff99 100644 --- a/rector.php +++ b/rector.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Rector\Config\RectorConfig; +use Rector\DeadCode\Rector\If_\RemoveAlwaysTrueIfConditionRector; use Rector\Symfony\Set\SymfonySetList; use Rector\ValueObject\PhpVersion; @@ -20,4 +21,8 @@ ->withSets([ SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION, ]) + ->withSkip([ + // Conditions on the Symfony version: always true with the installed version only + RemoveAlwaysTrueIfConditionRector::class => [__DIR__.'/tests/App/TestKernel.php'], + ]) ; diff --git a/tests/App/TestKernel.php b/tests/App/TestKernel.php new file mode 100644 index 00000000..bdda2eee --- /dev/null +++ b/tests/App/TestKernel.php @@ -0,0 +1,131 @@ +getRunDir().'/cache/'.$this->environment; + } + + public function getLogDir(): string + { + return $this->getRunDir().'/logs'; + } + + /** + * Codes of the tasks of the bundle (cleverage.task tags of config/services/task.yaml). + * + * @return list + */ + public static function getTaskCodes(): array + { + /** @var array{services: array}|string|null>} $config */ + $config = Yaml::parseFile(\dirname(__DIR__, 2).'/config/services/task.yaml', Yaml::PARSE_CUSTOM_TAGS); + $codes = []; + foreach ($config['services'] as $service) { + foreach (\is_array($service) ? $service['tags'] ?? [] : [] as $tag) { + if (\is_array($tag) && 'cleverage.task' === ($tag['name'] ?? null) && isset($tag['code'])) { + $codes[] = $tag['code']; + } + } + } + + return $codes; + } + + protected function configureContainer(ContainerConfigurator $container): void + { + $container->extension('framework', [ + 'secret' => 'test', + 'test' => true, + 'http_method_override' => false, + 'handle_all_throwables' => true, + 'php_errors' => ['log' => true], + 'validation' => ['email_validation_mode' => 'html5'], + ]); + if (Kernel::VERSION_ID >= 70300) { + // Not setting it is deprecated since Symfony 7.3: its default value changes in 8.0 + $container->extension('framework', ['property_info' => ['with_constructor_extractor' => true]]); + } + + $tasks = []; + foreach (self::getTaskCodes() as $code) { + $tasks[$code] = ['task' => $code]; + } + $container->extension('clever_age_process', [ + 'configurations' => [ + 'test.every_task' => ['tasks' => $tasks], + 'test.run' => [ + 'end_point' => 'format', + 'tasks' => [ + 'constant' => [ + 'task' => 'constant_output', + 'options' => ['output' => 'world'], + 'outputs' => ['format'], + ], + 'format' => [ + 'task' => 'transformer', + 'options' => ['transformers' => ['sprintf' => ['format' => 'Hello %s']]], + ], + ], + ], + ], + ]); + + $services = $container->services(); + // The default logger of FrameworkBundle may write the process logs to the output + $services->set('logger', NullLogger::class); + // Exposed to the tests + $services->alias('test.task_registry', TaskRegistry::class)->public(); + $services->alias('test.transformer_registry', TransformerRegistry::class)->public(); + $services->alias('test.process_configuration_registry', ProcessConfigurationRegistry::class)->public(); + $services->alias('test.process_manager', ProcessManager::class)->public(); + } + + private function getRunDir(): string + { + return sys_get_temp_dir().'/process_bundle_tests_'.getmypid(); + } +} diff --git a/tests/DependencyInjection/Compiler/RegistryCompilerPassTest.php b/tests/DependencyInjection/Compiler/RegistryCompilerPassTest.php index de3314dd..97b2a4fd 100644 --- a/tests/DependencyInjection/Compiler/RegistryCompilerPassTest.php +++ b/tests/DependencyInjection/Compiler/RegistryCompilerPassTest.php @@ -14,6 +14,7 @@ namespace CleverAge\ProcessBundle\Tests\DependencyInjection\Compiler; use CleverAge\ProcessBundle\DependencyInjection\Compiler\RegistryCompilerPass; +use CleverAge\ProcessBundle\Tests\IgnoreDeprecationsTrait; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -21,6 +22,8 @@ #[\PHPUnit\Framework\Attributes\CoversClass(RegistryCompilerPass::class)] class RegistryCompilerPassTest extends TestCase { + use IgnoreDeprecationsTrait; + public function testIsDeprecated(): void { $deprecations = []; @@ -44,7 +47,7 @@ public function testTaggedServicesAreAddedToTheRegistry(): void $container->register('registry', \ArrayObject::class); $container->register('service', \stdClass::class)->addTag('tag'); - (new RegistryCompilerPass('registry', 'tag', 'append'))->process($container); + self::ignoreDeprecations(static fn () => new RegistryCompilerPass('registry', 'tag', 'append'))->process($container); self::assertEquals([['append', [new Reference('service')]]], $container->getDefinition('registry')->getMethodCalls()); } @@ -54,7 +57,7 @@ public function testMissingRegistryIsIgnored(): void $container = new ContainerBuilder(); $container->register('service', \stdClass::class)->addTag('tag'); - (new RegistryCompilerPass('registry', 'tag', 'append'))->process($container); + self::ignoreDeprecations(static fn () => new RegistryCompilerPass('registry', 'tag', 'append'))->process($container); self::assertFalse($container->has('registry')); } @@ -64,6 +67,6 @@ public function testUndefinedConfigurationThrows(): void $this->expectException(\LogicException::class); $this->expectExceptionMessage('The registry, tag and method of the RegistryCompilerPass must be defined'); - (new RegistryCompilerPass())->process(new ContainerBuilder()); + self::ignoreDeprecations(static fn () => new RegistryCompilerPass())->process(new ContainerBuilder()); } } diff --git a/tests/DependencyInjection/Compiler/TaskLocatorCompilerPassTest.php b/tests/DependencyInjection/Compiler/TaskLocatorCompilerPassTest.php index 431ab3c3..75825e9c 100644 --- a/tests/DependencyInjection/Compiler/TaskLocatorCompilerPassTest.php +++ b/tests/DependencyInjection/Compiler/TaskLocatorCompilerPassTest.php @@ -17,6 +17,7 @@ use CleverAge\ProcessBundle\DependencyInjection\Compiler\TaskLocatorCompilerPass; use CleverAge\ProcessBundle\DependencyInjection\Configuration; use CleverAge\ProcessBundle\Task\DummyTask; +use CleverAge\ProcessBundle\Tests\IgnoreDeprecationsTrait; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; @@ -32,10 +33,13 @@ #[UsesClass(Configuration::class)] class TaskLocatorCompilerPassTest extends TestCase { + use IgnoreDeprecationsTrait; + public function testLocatorContainsTheServicesReferencedByTheProcesses(): void { $container = new ContainerBuilder(); - (new CleverAgeProcessExtension())->load([[ + // Deprecated "service" option + self::ignoreDeprecations(static fn () => (new CleverAgeProcessExtension())->load([[ 'configurations' => [ 'test.first' => ['tasks' => [ 'dummy' => ['service' => '@'.DummyTask::class], @@ -46,7 +50,7 @@ public function testLocatorContainsTheServicesReferencedByTheProcesses(): void 'unknown' => ['service' => '@app.unknown'], ]], ], - ]], $container); + ]], $container)); // Private services can be fetched from the locator $container->setDefinition('app.private_task', (new Definition(DummyTask::class))->setPublic(false)); diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index 8d3ead2b..a5baf3cd 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -15,6 +15,7 @@ use CleverAge\ProcessBundle\DependencyInjection\CleverAgeProcessExtension; use CleverAge\ProcessBundle\DependencyInjection\Configuration; +use CleverAge\ProcessBundle\Tests\IgnoreDeprecationsTrait; use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\Definition\Processor; @@ -24,6 +25,8 @@ #[\PHPUnit\Framework\Attributes\CoversClass(CleverAgeProcessExtension::class)] class ConfigurationTest extends TestCase { + use IgnoreDeprecationsTrait; + public function testLogLevelsDefaults(): void { $config = $this->process(['configurations' => ['test.process' => ['tasks' => []]]]); @@ -56,10 +59,11 @@ public function testLogLevelsCanBeConfigured(): void public function testTaskIsDefinedByAServiceOrACode(): void { - $config = $this->process(['configurations' => ['test.process' => ['tasks' => [ + // Deprecated "service" option, see testServiceIsDeprecated() + $config = self::ignoreDeprecations(fn (): array => $this->process(['configurations' => ['test.process' => ['tasks' => [ 'by_service' => ['service' => '@app.task'], 'by_code' => ['task' => 'transformer'], - ]]]]); + ]]]])); $tasks = $config['configurations']['test.process']['tasks']; self::assertSame(['@app.task', null], [$tasks['by_service']['service'], $tasks['by_service']['task']]); @@ -107,7 +111,8 @@ public function testTaskMustDefineEitherAServiceOrACode(array $task): void $this->expectException(InvalidConfigurationException::class); $this->expectExceptionMessage('A task must define either its code ("task: ") or, deprecated, its "service", not both'); - $this->process(['configurations' => ['test.process' => ['tasks' => ['invalid' => $task]]]]); + // Deprecated "service" option, see testServiceIsDeprecated() + self::ignoreDeprecations(fn (): array => $this->process(['configurations' => ['test.process' => ['tasks' => ['invalid' => $task]]]])); } public function testInvalidDefaultLogLevelIsRejected(): void diff --git a/tests/Functional/KernelTest.php b/tests/Functional/KernelTest.php new file mode 100644 index 00000000..8e216b3d --- /dev/null +++ b/tests/Functional/KernelTest.php @@ -0,0 +1,95 @@ +exceptionHandler = $this->getExceptionHandler(); + } + + protected function tearDown(): void + { + parent::tearDown(); + // The ErrorHandler registered when the kernel boots is not removed by the first Symfony 6.4 versions + if ($this->getExceptionHandler() !== $this->exceptionHandler) { + restore_exception_handler(); + } + } + + public function testEveryTaskIsResolvedByItsCode(): void + { + $processConfigurationRegistry = self::getContainer()->get('test.process_configuration_registry'); + self::assertInstanceOf(ProcessConfigurationRegistry::class, $processConfigurationRegistry); + $taskRegistry = self::getContainer()->get('test.task_registry'); + self::assertInstanceOf(TaskRegistry::class, $taskRegistry); + + $processConfiguration = $processConfigurationRegistry->getProcessConfiguration('test.every_task'); + $codes = TestKernel::getTaskCodes(); + self::assertNotEmpty($codes); + foreach ($codes as $code) { + self::assertInstanceOf(TaskInterface::class, $taskRegistry->getTask($processConfiguration->getTaskConfiguration($code)), $code); + } + } + + public function testEveryTransformerIsResolved(): void + { + $transformerRegistry = self::getContainer()->get('test.transformer_registry'); + self::assertInstanceOf(TransformerRegistry::class, $transformerRegistry); + + $transformers = $transformerRegistry->getTransformers(); + self::assertNotEmpty($transformers); + self::assertContainsOnlyInstancesOf(TransformerInterface::class, $transformers); + self::assertInstanceOf(TransformerInterface::class, $transformerRegistry->getTransformer('sprintf')); + } + + public function testProcessIsExecuted(): void + { + $processManager = self::getContainer()->get('test.process_manager'); + self::assertInstanceOf(ProcessManager::class, $processManager); + + self::assertSame('Hello world', $processManager->execute('test.run')); + } + + private function getExceptionHandler(): mixed + { + $handler = set_exception_handler(null); + restore_exception_handler(); + + return $handler; + } + + protected static function getKernelClass(): string + { + return TestKernel::class; + } +} diff --git a/tests/IgnoreDeprecationsTrait.php b/tests/IgnoreDeprecationsTrait.php new file mode 100644 index 00000000..b17c519b --- /dev/null +++ b/tests/IgnoreDeprecationsTrait.php @@ -0,0 +1,38 @@ + true, \E_USER_DEPRECATED); + try { + return $callback(); + } finally { + restore_error_handler(); + } + } +} diff --git a/tests/Transformer/CallbackTransformerTest.php b/tests/Transformer/CallbackTransformerTest.php index 3f88d162..6b17d323 100644 --- a/tests/Transformer/CallbackTransformerTest.php +++ b/tests/Transformer/CallbackTransformerTest.php @@ -13,6 +13,7 @@ namespace CleverAge\ProcessBundle\Tests\Transformer; +use CleverAge\ProcessBundle\Tests\IgnoreDeprecationsTrait; use CleverAge\ProcessBundle\Transformer\CallbackTransformer; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -23,6 +24,8 @@ #[\PHPUnit\Framework\Attributes\CoversClass(CallbackTransformer::class)] class CallbackTransformerTest extends TestCase { + use IgnoreDeprecationsTrait; + public static function doCallback(mixed ...$args): string { return implode('-', $args); @@ -82,10 +85,10 @@ public function testTransform(array $options, mixed $value, mixed $expected): vo public function testAdditionalParametersAreUsedAsRightParameters(): void { $transformer = new CallbackTransformer(); - $options = $this->resolveOptions($transformer, [ + $options = self::ignoreDeprecations(fn (): array => $this->resolveOptions($transformer, [ 'callback' => [self::class, 'doCallback'], 'additional_parameters' => [7, 8], - ]); + ])); self::assertSame('6-7-8', $transformer->transform('6', $options)); } @@ -121,11 +124,11 @@ public function testAdditionalParametersAreDeprecated(): void public function testAdditionalParametersAreIgnoredWhenRightParametersAreSet(): void { $transformer = new CallbackTransformer(); - $options = $this->resolveOptions($transformer, [ + $options = self::ignoreDeprecations(fn (): array => $this->resolveOptions($transformer, [ 'callback' => [self::class, 'doCallback'], 'right_parameters' => [4], 'additional_parameters' => [7, 8], - ]); + ])); self::assertSame('6-4', $transformer->transform('6', $options)); } diff --git a/tests/Transformer/Serialization/DenormalizeTransformerTest.php b/tests/Transformer/Serialization/DenormalizeTransformerTest.php index e05faa53..baab1728 100644 --- a/tests/Transformer/Serialization/DenormalizeTransformerTest.php +++ b/tests/Transformer/Serialization/DenormalizeTransformerTest.php @@ -49,7 +49,7 @@ public function testCollectionClassIsSupported(): void $transformer = $this->createTransformer(); $options = $this->resolveOptions($transformer, ['class' => \DateTimeImmutable::class.'[]']); - $result = $transformer->transform(['a' => '2024-01-02', 'b' => '2024-02-03'], $options); + $result = $transformer->transform(['a' => '2024-01-02T00:00:00+00:00', 'b' => '2024-02-03T00:00:00+00:00'], $options); self::assertIsArray($result); self::assertSame(['a', 'b'], array_keys($result)); diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 00000000..ede03ad4 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,19 @@ +