diff --git a/CHANGELOG.md b/CHANGELOG.md index 976a0c06..eb6f6501 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Add the `ForResponseGroups` target which resolves properties from the serialization groups of the `#[Serialize]` attribute by @HypeMC in https://github.com/sofascore/purgatory-bundle/pull/154 +- Add the `entity_change_purging` option and a PHPUnit extension with the `#[WithEntityChangePurging]` attribute to + enable purging on entity changes only for specific tests by @HypeMC + in https://github.com/sofascore/purgatory-bundle/pull/155 ## [1.5.0] - 2026-08-25 diff --git a/README.md b/README.md index c9c9ce91..2573fff8 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ If your project doesn't use [Symfony Flex](https://github.com/symfony/flex), con # Examples: # - /^_profiler/ # - /^_wdt/ + # Whether entity changes trigger purge requests, can be disabled in the test environment and enabled per test using the PHPUnit extension. + entity_change_purging: true doctrine_middleware: enabled: true diff --git a/config/schema/purgatory.xsd b/config/schema/purgatory.xsd index 18e21a54..7192822d 100644 --- a/config/schema/purgatory.xsd +++ b/config/schema/purgatory.xsd @@ -40,6 +40,7 @@ + diff --git a/config/services.php b/config/services.php index 67e32af7..6dd21ceb 100644 --- a/config/services.php +++ b/config/services.php @@ -19,6 +19,7 @@ use Sofascore\PurgatoryBundle\Command\DebugCommand; use Sofascore\PurgatoryBundle\Doctrine\DBAL\Middleware; use Sofascore\PurgatoryBundle\Listener\EntityChangeListener; +use Sofascore\PurgatoryBundle\Listener\EntityChangePurgeSwitcher; use Sofascore\PurgatoryBundle\Purger\AsyncPurger; use Sofascore\PurgatoryBundle\Purger\InMemoryPurger; use Sofascore\PurgatoryBundle\Purger\Messenger\PurgeMessageHandler; @@ -166,11 +167,14 @@ ->tag('purgatory.route_provider') ->arg(3, service('sofascore.purgatory.property_accessor')) + ->set('sofascore.purgatory.entity_change_purge_switcher', EntityChangePurgeSwitcher::class) + ->set('sofascore.purgatory.entity_change_listener', EntityChangeListener::class) ->args([ tagged_iterator('purgatory.route_provider'), service('router'), service('sofascore.purgatory.purger'), + service('sofascore.purgatory.entity_change_purge_switcher'), ]) ->set('sofascore.purgatory.purger.void', VoidPurger::class) diff --git a/docs/README.md b/docs/README.md index c8acf574..d838a3ff 100644 --- a/docs/README.md +++ b/docs/README.md @@ -535,6 +535,53 @@ class PurgeTest extends KernelTestCase } ``` +### Enabling Purging Only for Specific Tests + +Generating purge requests on every flush can noticeably slow down a large test suite, even though most tests never +assert on them. To disable it by default, set the `entity_change_purging` option to `false` in the test environment: + +```yaml +# config/packages/purgatory.yaml +when@test: + purgatory: + purger: in-memory + entity_change_purging: false +``` + +Then register the bundle's PHPUnit extension, which requires PHPUnit 10 or higher: + +```xml + + + + +``` + +Purging can now be enabled only where it is needed with the [`#[WithEntityChangePurging]`][6] attribute. When placed on +a test class, purging is enabled for all of its tests, from `setUpBeforeClass()` until after `tearDownAfterClass()`. +When placed on a test method, purging is enabled for that test only, from before `setUp()` until after `tearDown()`. +In both cases the configured default is restored afterwards, or as soon as a test errors or is skipped: + +```php +use Sofascore\PurgatoryBundle\PHPUnit\WithEntityChangePurging; +use Sofascore\PurgatoryBundle\Test\InteractsWithPurgatory; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; + +class PurgeTest extends KernelTestCase +{ + use InteractsWithPurgatory; + + #[WithEntityChangePurging] + public function testPurgePost() + { + // ... + } +} +``` + +The switch can also be flipped manually with the static `enable()`, `disable()` and `reset()` methods of the +[`EntityChangePurgeSwitcher`][7] class, e.g. to skip purging while loading fixtures. + ## Debugging The bundle includes integration with the [Symfony Profiler](https://symfony.com/doc/current/profiler.html) to help you @@ -563,3 +610,5 @@ This command provides insights into which routes and parameters are associated w [3]: https://github.com/sofascore/purgatory-bundle/blob/1.x/src/Listener/Enum/Action.php [4]: https://github.com/sofascore/purgatory-bundle/blob/1.x/src/Test/InteractsWithPurgatory.php [5]: https://github.com/symfony/symfony/blob/8.1/src/Symfony/Component/HttpKernel/Attribute/Serialize.php +[6]: https://github.com/sofascore/purgatory-bundle/blob/1.x/src/PHPUnit/WithEntityChangePurging.php +[7]: https://github.com/sofascore/purgatory-bundle/blob/1.x/src/Listener/EntityChangePurgeSwitcher.php diff --git a/phpunit.dist.xml b/phpunit.dist.xml index adc06783..a2feee79 100644 --- a/phpunit.dist.xml +++ b/phpunit.dist.xml @@ -14,6 +14,10 @@ failOnRisky="true" failOnWarning="true" > + + + + tests diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 57db1d9c..3d70310a 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -33,6 +33,10 @@ public function getConfigTreeBuilder(): TreeBuilder ->scalarPrototype()->end() ->defaultValue([]) ->end() + ->booleanNode('entity_change_purging') + ->info('Whether entity changes trigger purge requests, can be disabled in the test environment and enabled per test using the PHPUnit extension.') + ->defaultTrue() + ->end() ->arrayNode('doctrine_middleware') ->canBeDisabled() ->children() diff --git a/src/DependencyInjection/PurgatoryExtension.php b/src/DependencyInjection/PurgatoryExtension.php index 25fb13df..cefedb08 100644 --- a/src/DependencyInjection/PurgatoryExtension.php +++ b/src/DependencyInjection/PurgatoryExtension.php @@ -132,6 +132,9 @@ static function (ChildDefinition $definition, AsExpressionLanguageFunction $attr $container->getDefinition('sofascore.purgatory.route_metadata_provider.attribute') ->setArgument(2, $mergedConfig['route_ignore_patterns']); + $container->getDefinition('sofascore.purgatory.entity_change_purge_switcher') + ->setArgument(0, $mergedConfig['entity_change_purging']); + /** @var array $doctrineEventListenerPriorities */ $doctrineEventListenerPriorities = $mergedConfig['doctrine_event_listener_priorities']; diff --git a/src/Listener/EntityChangeListener.php b/src/Listener/EntityChangeListener.php index d05ca1c8..8568e77f 100644 --- a/src/Listener/EntityChangeListener.php +++ b/src/Listener/EntityChangeListener.php @@ -28,6 +28,7 @@ public function __construct( private readonly iterable $routeProviders, private readonly UrlGeneratorInterface $urlGenerator, private readonly PurgerInterface $purger, + private readonly EntityChangePurgeSwitcher $entityChangePurgeSwitcher = new EntityChangePurgeSwitcher(), ) { } @@ -59,6 +60,12 @@ public function process(): void return; } + if (!$this->entityChangePurgeSwitcher->isEnabled()) { + $this->reset(); + + return; + } + $purgeRequests = array_values($this->queuedPurgeRequests); $this->reset(); $this->purger->purge($purgeRequests); @@ -74,6 +81,10 @@ public function reset(): void */ private function handleChanges(LifecycleEventArgs $eventArgs, Action $action): void { + if (!$this->entityChangePurgeSwitcher->isEnabled()) { + return; + } + $entity = $eventArgs->getObject(); $entityChangeSet = $eventArgs->getObjectManager()->getUnitOfWork()->getEntityChangeSet($entity); diff --git a/src/Listener/EntityChangePurgeSwitcher.php b/src/Listener/EntityChangePurgeSwitcher.php new file mode 100644 index 00000000..ed97a453 --- /dev/null +++ b/src/Listener/EntityChangePurgeSwitcher.php @@ -0,0 +1,44 @@ +enabled; + } + + public static function enable(): void + { + self::$override = true; + } + + public static function disable(): void + { + self::$override = false; + } + + /** + * Restores the configured default. + */ + public static function reset(): void + { + self::$override = null; + } +} diff --git a/src/PHPUnit/Metadata/AttributeReader.php b/src/PHPUnit/Metadata/AttributeReader.php new file mode 100644 index 00000000..29bb717e --- /dev/null +++ b/src/PHPUnit/Metadata/AttributeReader.php @@ -0,0 +1,61 @@ + + */ + private array $cache = []; + + /** + * Also looks at the parent classes. + * + * @param class-string $className + */ + public function forClass(string $className): ?WithEntityChangePurging + { + if (\array_key_exists($className, $this->cache)) { + return $this->cache[$className]; + } + + $attribute = null; + for ($class = new \ReflectionClass($className); false !== $class; $class = $class->getParentClass()) { + if (null !== $attribute = $this->readAttribute($class)) { + break; + } + } + + return $this->cache[$className] = $attribute; + } + + /** + * @param class-string $className + */ + public function forMethod(string $className, string $methodName): ?WithEntityChangePurging + { + $key = $className.'::'.$methodName; + + if (\array_key_exists($key, $this->cache)) { + return $this->cache[$key]; + } + + return $this->cache[$key] = $this->readAttribute(new \ReflectionMethod($className, $methodName)); + } + + /** + * @param \ReflectionClass|\ReflectionMethod $reflection + */ + private function readAttribute(\ReflectionClass|\ReflectionMethod $reflection): ?WithEntityChangePurging + { + return ($reflection->getAttributes(WithEntityChangePurging::class)[0] ?? null)?->newInstance(); + } +} diff --git a/src/PHPUnit/PurgatoryExtension.php b/src/PHPUnit/PurgatoryExtension.php new file mode 100644 index 00000000..ad08f5cf --- /dev/null +++ b/src/PHPUnit/PurgatoryExtension.php @@ -0,0 +1,189 @@ +registerSubscriber(new class($reader) implements TestSuiteStartedSubscriber { + public function __construct( + private readonly AttributeReader $reader, + ) { + } + + public function notify(TestSuiteStarted $event): void + { + PurgatoryExtension::enableForTestSuite($event->testSuite(), $this->reader); + } + }); + + $facade->registerSubscriber(new class($reader) implements TestSuiteFinishedSubscriber { + public function __construct( + private readonly AttributeReader $reader, + ) { + } + + public function notify(TestSuiteFinished $event): void + { + PurgatoryExtension::resetForTestSuite($event->testSuite(), $this->reader); + } + }); + + $facade->registerSubscriber(new class($reader) implements PreparationStartedSubscriber { + public function __construct( + private readonly AttributeReader $reader, + ) { + } + + public function notify(PreparationStarted $event): void + { + PurgatoryExtension::enableForTest($event->test(), $this->reader); + } + }); + + $facade->registerSubscriber(new class($reader) implements FinishedSubscriber { + public function __construct( + private readonly AttributeReader $reader, + ) { + } + + public function notify(Finished $event): void + { + PurgatoryExtension::resetForTest($event->test(), $this->reader); + } + }); + + // the "Finished" event is not emitted when a test errors or is skipped before it is prepared + $facade->registerSubscriber(new class($reader) implements ErroredSubscriber { + public function __construct( + private readonly AttributeReader $reader, + ) { + } + + public function notify(Errored $event): void + { + PurgatoryExtension::resetForTest($event->test(), $this->reader); + } + }); + + $facade->registerSubscriber(new class($reader) implements SkippedSubscriber { + public function __construct( + private readonly AttributeReader $reader, + ) { + } + + public function notify(Skipped $event): void + { + PurgatoryExtension::resetForTest($event->test(), $this->reader); + } + }); + + if (interface_exists(BeforeTestMethodErroredSubscriber::class)) { + $facade->registerSubscriber(new class($reader) implements BeforeTestMethodErroredSubscriber { + public function __construct( + private readonly AttributeReader $reader, + ) { + } + + public function notify(BeforeTestMethodErrored $event): void + { + PurgatoryExtension::resetForTestClass($event->testClassName(), $this->reader); + } + }); + } + } + + /** + * @internal + */ + public static function enableForTestSuite(TestSuite $testSuite, AttributeReader $reader): void + { + if ($testSuite instanceof TestSuiteForTestClass && null !== $reader->forClass($testSuite->className())) { + EntityChangePurgeSwitcher::enable(); + } + } + + /** + * @internal + */ + public static function resetForTestSuite(TestSuite $testSuite, AttributeReader $reader): void + { + if ($testSuite instanceof TestSuiteForTestClass && null !== $reader->forClass($testSuite->className())) { + EntityChangePurgeSwitcher::reset(); + } + } + + /** + * @internal + */ + public static function enableForTest(Test $test, AttributeReader $reader): void + { + if ($test instanceof TestMethod && null !== $reader->forMethod($test->className(), $test->methodName())) { + EntityChangePurgeSwitcher::enable(); + } + } + + /** + * @internal + */ + public static function resetForTest(Test $test, AttributeReader $reader): void + { + if (!$test instanceof TestMethod || null === $reader->forMethod($test->className(), $test->methodName())) { + return; + } + + self::resetForTestClass($test->className(), $reader); + } + + /** + * Restores the configured default unless the attribute is set on the class, + * in which case purging stays enabled until the test suite is finished. + * + * @internal + * + * @param class-string $className + */ + public static function resetForTestClass(string $className, AttributeReader $reader): void + { + if (null === $reader->forClass($className)) { + EntityChangePurgeSwitcher::reset(); + } + } +} diff --git a/src/PHPUnit/WithEntityChangePurging.php b/src/PHPUnit/WithEntityChangePurging.php new file mode 100644 index 00000000..e1ebd431 --- /dev/null +++ b/src/PHPUnit/WithEntityChangePurging.php @@ -0,0 +1,15 @@ +isEnabled()); + } + + public static function tearDownAfterClass(): void + { + // purging is still enabled after the last test of the class has run + self::assertTrue((new EntityChangePurgeSwitcher(false))->isEnabled()); + + parent::tearDownAfterClass(); + } + + protected function setUp(): void + { + self::initializeApplication(['test_case' => 'EntityChangeListener', 'config' => 'entity_change_purging_disabled.yaml']); + + $this->entityManager = self::getContainer()->get('doctrine.orm.entity_manager'); + } + + protected function tearDown(): void + { + unset($this->entityManager); + + parent::tearDown(); + } + + public function testUrlsArePurged(): void + { + $name = $this->persistDummy(); + + self::assertUrlIsPurged('http://localhost/'.$name); + self::assertUrlIsPurged('http://example.test/foo'); + } + + public function testUrlsArePurgedForEveryTest(): void + { + $name = $this->persistDummy(); + + self::assertUrlIsPurged('http://localhost/'.$name); + self::assertUrlIsPurged('http://example.test/foo'); + } + + private function persistDummy(): string + { + $dummy = new Dummy($name = 'name_'.time()); + + $this->entityManager->persist($dummy); + $this->entityManager->flush(); + + return $name; + } +} diff --git a/tests/Application/EntityChangePurgingTest.php b/tests/Application/EntityChangePurgingTest.php new file mode 100644 index 00000000..406f2514 --- /dev/null +++ b/tests/Application/EntityChangePurgingTest.php @@ -0,0 +1,80 @@ + false, + 'testUrlsArePurgedWhenEnabledForTest' => true, + 'testConfiguredDefaultIsRestoredAfterTest' => false, + ]; + + private EntityManagerInterface $entityManager; + + protected function setUp(): void + { + // the extension must have applied the attribute before the test is prepared + self::assertSame(self::EXPECTED_STATE[$this->name()], (new EntityChangePurgeSwitcher(false))->isEnabled()); + + self::initializeApplication(['test_case' => 'EntityChangeListener', 'config' => 'entity_change_purging_disabled.yaml']); + + $this->entityManager = self::getContainer()->get('doctrine.orm.entity_manager'); + } + + protected function tearDown(): void + { + // the attribute must still apply while the test is being torn down + self::assertSame(self::EXPECTED_STATE[$this->name()], (new EntityChangePurgeSwitcher(false))->isEnabled()); + + unset($this->entityManager); + + parent::tearDown(); + } + + public function testUrlsAreNotPurgedWhenDisabled(): void + { + $this->persistDummy(); + + self::assertNoUrlsArePurged(); + } + + #[WithEntityChangePurging] + public function testUrlsArePurgedWhenEnabledForTest(): void + { + $name = $this->persistDummy(); + + self::assertUrlIsPurged('http://localhost/'.$name); + self::assertUrlIsPurged('http://example.test/foo'); + } + + #[Depends('testUrlsArePurgedWhenEnabledForTest')] + public function testConfiguredDefaultIsRestoredAfterTest(): void + { + $this->persistDummy(); + + self::assertNoUrlsArePurged(); + } + + private function persistDummy(): string + { + $dummy = new Dummy($name = 'name_'.time()); + + $this->entityManager->persist($dummy); + $this->entityManager->flush(); + + return $name; + } +} diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index dc916654..6dee5f4e 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -27,6 +27,7 @@ public function testDefaultConfig(): void self::assertSame([ 'mapping_paths' => [], 'route_ignore_patterns' => [], + 'entity_change_purging' => true, 'doctrine_middleware' => [ 'enabled' => true, 'priority' => null, @@ -151,6 +152,7 @@ public static function provideXMLCases(): iterable 'all.xml', [ 'profiler_integration' => false, + 'entity_change_purging' => false, 'doctrine_middleware' => [ 'priority' => 5, 'enabled' => true, @@ -199,6 +201,7 @@ public static function provideXMLCases(): iterable ], 'mapping_paths' => [], 'route_ignore_patterns' => [], + 'entity_change_purging' => true, 'purger' => [ 'name' => null, 'hosts' => [], diff --git a/tests/DependencyInjection/Fixtures/xml/all.xml b/tests/DependencyInjection/Fixtures/xml/all.xml index ac1b66a8..3c38db3d 100644 --- a/tests/DependencyInjection/Fixtures/xml/all.xml +++ b/tests/DependencyInjection/Fixtures/xml/all.xml @@ -5,7 +5,7 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd http://sofascore.com/schema/dic/purgatory https://sofascore.com/schema/dic/purgatory/purgatory.xsd"> - + %kernel.project_dir%/one.yaml %kernel.project_dir%/two.yaml diff --git a/tests/DependencyInjection/PurgatoryExtensionTest.php b/tests/DependencyInjection/PurgatoryExtensionTest.php index 047f26b1..6822ac27 100644 --- a/tests/DependencyInjection/PurgatoryExtensionTest.php +++ b/tests/DependencyInjection/PurgatoryExtensionTest.php @@ -340,6 +340,23 @@ public function testRouteIgnorePatternsIsSet(): void self::assertSame('/^_profiler/', $ignoredPatterns[0]); } + #[TestWith([true])] + #[TestWith([false])] + public function testEntityChangePurgingIsSet(bool $enabled): void + { + $container = new ContainerBuilder(); + $container->setParameter('kernel.project_dir', __DIR__); + + $extension = new PurgatoryExtension(); + $extension->load([ + 'purgatory' => [ + 'entity_change_purging' => $enabled, + ], + ], $container); + + self::assertSame($enabled, $container->getDefinition('sofascore.purgatory.entity_change_purge_switcher')->getArgument(0)); + } + #[TestWith([[], [[]]])] #[TestWith([['doctrine_middleware' => ['priority' => 10]], [['priority' => 10]]])] public function testDoctrineMiddlewareTagIsSet(array $middlewarePriority, array $expectedTag): void diff --git a/tests/Functional/EntityChangeListener/config/entity_change_purging_disabled.yaml b/tests/Functional/EntityChangeListener/config/entity_change_purging_disabled.yaml new file mode 100644 index 00000000..8cdce614 --- /dev/null +++ b/tests/Functional/EntityChangeListener/config/entity_change_purging_disabled.yaml @@ -0,0 +1,2 @@ +purgatory: + entity_change_purging: false diff --git a/tests/Listener/EntityChangeListenerTest.php b/tests/Listener/EntityChangeListenerTest.php index 89bacd55..de830d4d 100644 --- a/tests/Listener/EntityChangeListenerTest.php +++ b/tests/Listener/EntityChangeListenerTest.php @@ -5,9 +5,14 @@ namespace Sofascore\PurgatoryBundle\Tests\Listener; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Event\PostPersistEventArgs; +use Doctrine\ORM\UnitOfWork; use PHPUnit\Framework\Attributes\CoversClass; use Sofascore\PurgatoryBundle\Listener\EntityChangeListener; +use Sofascore\PurgatoryBundle\Listener\EntityChangePurgeSwitcher; use Sofascore\PurgatoryBundle\Purger\PurgerInterface; +use Sofascore\PurgatoryBundle\RouteProvider\PurgeRoute; +use Sofascore\PurgatoryBundle\RouteProvider\RouteProviderInterface; use Sofascore\PurgatoryBundle\Test\InteractsWithPurgatory; use Sofascore\PurgatoryBundle\Tests\Functional\AbstractKernelTestCase; use Sofascore\PurgatoryBundle\Tests\Functional\EntityChangeListener\Entity\Dummy; @@ -84,6 +89,13 @@ public function testUrlsAreNotPurgedOnFlushWhenInTransaction(): void self::assertUrlIsPurged('http://example.test/foo'); } + protected function tearDown(): void + { + EntityChangePurgeSwitcher::reset(); + + parent::tearDown(); + } + public function testProcessWithNoPurgeRequests(): void { $urlGenerator = self::createStub(UrlGeneratorInterface::class); @@ -94,4 +106,54 @@ public function testProcessWithNoPurgeRequests(): void $entityChangeListener->process(); } + + public function testNothingIsQueuedWhenPurgingIsDisabled(): void + { + $routeProvider = $this->createMock(RouteProviderInterface::class); + $routeProvider->expects(self::never())->method('supports'); + $purger = $this->createMock(PurgerInterface::class); + $purger->expects(self::never())->method('purge'); + $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager->expects(self::never())->method('getUnitOfWork'); + + $entityChangeListener = new EntityChangeListener( + [$routeProvider], + self::createStub(UrlGeneratorInterface::class), + $purger, + new EntityChangePurgeSwitcher(false), + ); + + $entityChangeListener->postPersist(new PostPersistEventArgs(new \stdClass(), $entityManager)); + $entityChangeListener->process(); + } + + public function testQueuedPurgeRequestsAreDroppedWhenPurgingIsDisabledBeforeProcessing(): void + { + $routeProvider = self::createStub(RouteProviderInterface::class); + $routeProvider->method('supports')->willReturn(true); + $routeProvider->method('provideRoutesFor')->willReturn([new PurgeRoute('route_foo', [])]); + $urlGenerator = self::createStub(UrlGeneratorInterface::class); + $urlGenerator->method('generate')->willReturn('http://localhost/foo'); + $purger = $this->createMock(PurgerInterface::class); + $purger->expects(self::never())->method('purge'); + $unitOfWork = self::createStub(UnitOfWork::class); + $unitOfWork->method('getEntityChangeSet')->willReturn([]); + $entityManager = self::createStub(EntityManagerInterface::class); + $entityManager->method('getUnitOfWork')->willReturn($unitOfWork); + + $entityChangeListener = new EntityChangeListener( + [$routeProvider], + $urlGenerator, + $purger, + new EntityChangePurgeSwitcher(true), + ); + + $entityChangeListener->postPersist(new PostPersistEventArgs(new \stdClass(), $entityManager)); + + EntityChangePurgeSwitcher::disable(); + $entityChangeListener->process(); + + EntityChangePurgeSwitcher::reset(); + $entityChangeListener->process(); + } } diff --git a/tests/Listener/EntityChangePurgeSwitcherTest.php b/tests/Listener/EntityChangePurgeSwitcherTest.php new file mode 100644 index 00000000..c3c57b11 --- /dev/null +++ b/tests/Listener/EntityChangePurgeSwitcherTest.php @@ -0,0 +1,48 @@ +isEnabled()); + self::assertTrue((new EntityChangePurgeSwitcher(true))->isEnabled()); + self::assertFalse((new EntityChangePurgeSwitcher(false))->isEnabled()); + } + + public function testOverride(): void + { + $enabledByDefault = new EntityChangePurgeSwitcher(true); + $disabledByDefault = new EntityChangePurgeSwitcher(false); + + EntityChangePurgeSwitcher::enable(); + + self::assertTrue($enabledByDefault->isEnabled()); + self::assertTrue($disabledByDefault->isEnabled()); + + EntityChangePurgeSwitcher::disable(); + + self::assertFalse($enabledByDefault->isEnabled()); + self::assertFalse($disabledByDefault->isEnabled()); + + EntityChangePurgeSwitcher::reset(); + + self::assertTrue($enabledByDefault->isEnabled()); + self::assertFalse($disabledByDefault->isEnabled()); + } +} diff --git a/tests/PHPUnit/Fixtures/WithClassAttributeDummy.php b/tests/PHPUnit/Fixtures/WithClassAttributeDummy.php new file mode 100644 index 00000000..3bb2ffa3 --- /dev/null +++ b/tests/PHPUnit/Fixtures/WithClassAttributeDummy.php @@ -0,0 +1,20 @@ +forClass(WithClassAttributeDummy::class); + + self::assertInstanceOf(WithEntityChangePurging::class, $attribute); + self::assertIsCached($attribute, $reader, WithClassAttributeDummy::class); + } + + public function testForClassWithAttributeOnParentClass(): void + { + $reader = new AttributeReader(); + + $attribute = $reader->forClass(WithInheritedClassAttributeDummy::class); + + self::assertInstanceOf(WithEntityChangePurging::class, $attribute); + self::assertIsCached($attribute, $reader, WithInheritedClassAttributeDummy::class); + } + + public function testForClassWithoutAttribute(): void + { + $reader = new AttributeReader(); + + self::assertNull($reader->forClass(WithMethodAttributeDummy::class)); + self::assertIsCached(null, $reader, WithMethodAttributeDummy::class); + } + + public function testForMethod(): void + { + $reader = new AttributeReader(); + + $attribute = $reader->forMethod(WithMethodAttributeDummy::class, 'testWithAttribute'); + + self::assertInstanceOf(WithEntityChangePurging::class, $attribute); + self::assertIsCached($attribute, $reader, WithMethodAttributeDummy::class.'::testWithAttribute'); + } + + public function testForMethodWithoutAttribute(): void + { + $reader = new AttributeReader(); + + self::assertNull($reader->forMethod(WithMethodAttributeDummy::class, 'testWithoutAttribute')); + self::assertIsCached(null, $reader, WithMethodAttributeDummy::class.'::testWithoutAttribute'); + } + + public function testForMethodDoesNotLookAtTheClass(): void + { + $reader = new AttributeReader(); + + self::assertNull($reader->forMethod(WithClassAttributeDummy::class, 'testWithoutAttribute')); + } + + private static function assertIsCached(?WithEntityChangePurging $expected, AttributeReader $reader, string $key): void + { + /** @var array $cache */ + $cache = (new \ReflectionProperty(AttributeReader::class, 'cache'))->getValue($reader); + + self::assertArrayHasKey($key, $cache); + self::assertSame($expected, $cache[$key]); + } +} diff --git a/tests/PHPUnit/PurgatoryExtensionTest.php b/tests/PHPUnit/PurgatoryExtensionTest.php new file mode 100644 index 00000000..d075582f --- /dev/null +++ b/tests/PHPUnit/PurgatoryExtensionTest.php @@ -0,0 +1,177 @@ +switcher = new EntityChangePurgeSwitcher(false); + } + + protected function tearDown(): void + { + EntityChangePurgeSwitcher::reset(); + + unset($this->switcher); + } + + public function testEnableForTestSuiteWithAttributeOnClass(): void + { + PurgatoryExtension::enableForTestSuite(self::testSuite(WithClassAttributeDummy::class), new AttributeReader()); + + self::assertTrue($this->switcher->isEnabled()); + } + + public function testEnableForTestSuiteWithoutAttributeOnClass(): void + { + PurgatoryExtension::enableForTestSuite(self::testSuite(WithMethodAttributeDummy::class), new AttributeReader()); + + self::assertFalse($this->switcher->isEnabled()); + } + + public function testEnableForTestSuiteIgnoresSuitesNotForTestClass(): void + { + PurgatoryExtension::enableForTestSuite(new TestSuiteWithName('suite', 0, TestCollection::fromArray([])), new AttributeReader()); + + self::assertFalse($this->switcher->isEnabled()); + } + + public function testResetForTestSuiteWithAttributeOnClass(): void + { + EntityChangePurgeSwitcher::enable(); + + PurgatoryExtension::resetForTestSuite(self::testSuite(WithClassAttributeDummy::class), new AttributeReader()); + + self::assertFalse($this->switcher->isEnabled()); + } + + public function testResetForTestSuiteWithoutAttributeOnClass(): void + { + EntityChangePurgeSwitcher::enable(); + + PurgatoryExtension::resetForTestSuite(self::testSuite(WithMethodAttributeDummy::class), new AttributeReader()); + + self::assertTrue($this->switcher->isEnabled()); + } + + public function testEnableForTestWithAttributeOnMethod(): void + { + PurgatoryExtension::enableForTest(self::testMethod(WithMethodAttributeDummy::class, 'testWithAttribute'), new AttributeReader()); + + self::assertTrue($this->switcher->isEnabled()); + } + + public function testEnableForTestWithoutAttributeOnMethod(): void + { + PurgatoryExtension::enableForTest(self::testMethod(WithMethodAttributeDummy::class, 'testWithoutAttribute'), new AttributeReader()); + + self::assertFalse($this->switcher->isEnabled()); + } + + public function testEnableForTestIgnoresNonMethodTests(): void + { + PurgatoryExtension::enableForTest(new Phpt(__FILE__), new AttributeReader()); + + self::assertFalse($this->switcher->isEnabled()); + } + + public function testResetForTestWithAttributeOnMethod(): void + { + EntityChangePurgeSwitcher::enable(); + + PurgatoryExtension::resetForTest(self::testMethod(WithMethodAttributeDummy::class, 'testWithAttribute'), new AttributeReader()); + + self::assertFalse($this->switcher->isEnabled()); + } + + public function testResetForTestWithAttributeOnMethodAndClass(): void + { + EntityChangePurgeSwitcher::enable(); + + PurgatoryExtension::resetForTest(self::testMethod(WithClassAttributeDummy::class, 'testWithAttribute'), new AttributeReader()); + + self::assertTrue($this->switcher->isEnabled()); + } + + public function testResetForTestWithoutAttributeOnMethod(): void + { + EntityChangePurgeSwitcher::enable(); + + PurgatoryExtension::resetForTest(self::testMethod(WithMethodAttributeDummy::class, 'testWithoutAttribute'), new AttributeReader()); + + self::assertTrue($this->switcher->isEnabled()); + } + + public function testResetForTestIgnoresNonMethodTests(): void + { + EntityChangePurgeSwitcher::enable(); + + PurgatoryExtension::resetForTest(new Phpt(__FILE__), new AttributeReader()); + + self::assertTrue($this->switcher->isEnabled()); + } + + public function testResetForTestClassWithoutAttributeOnClass(): void + { + EntityChangePurgeSwitcher::enable(); + + PurgatoryExtension::resetForTestClass(WithMethodAttributeDummy::class, new AttributeReader()); + + self::assertFalse($this->switcher->isEnabled()); + } + + public function testResetForTestClassWithAttributeOnClass(): void + { + EntityChangePurgeSwitcher::enable(); + + PurgatoryExtension::resetForTestClass(WithClassAttributeDummy::class, new AttributeReader()); + + self::assertTrue($this->switcher->isEnabled()); + } + + /** + * @param class-string $className + */ + private static function testSuite(string $className): TestSuiteForTestClass + { + return new TestSuiteForTestClass($className, 1, TestCollection::fromArray([]), __FILE__, __LINE__); + } + + /** + * @param class-string $className + */ + private static function testMethod(string $className, string $methodName): TestMethod + { + return new TestMethod( + $className, + $methodName, + __FILE__, + __LINE__, + new TestDox($className, $methodName, $methodName), + MetadataCollection::fromArray([]), + TestDataCollection::fromArray([]), + ); + } +}