Created
March 31, 2022 17:16
-
-
Save Ocramius/d5f797cd30f64b2214cf620aff5ea3d7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Tests\Integration; | |
use PHPUnit\Framework\TestCase; | |
use ReflectionClass; | |
use ReflectionProperty; | |
use Webmozart\Assert\Assert; | |
use function array_filter; | |
use function array_key_exists; | |
use function array_map; | |
use function array_merge; | |
use function array_walk; | |
use function gc_collect_cycles; | |
use function get_class; | |
use function strpos; | |
final class ClearAllNonPhpunitProperties | |
{ | |
/** @psalm-var array<class-string, array<ReflectionProperty>> */ | |
private static array $propertiesToBeUnsetCache = []; | |
/** | |
* Removes all state of the given {@see TestCase} by doing an `unset()` on all properties that | |
* were not declared by the parent {@see TestCase} class. | |
* | |
* PHPUnit "leaks by design", because it keeps {@see TestCase} instances in memory until the full suite | |
* is through, at which time it traverses all {@see TestCase} instances again to generate a test report. | |
* PHPUnit's design is simplistic and works, but leads to memory leaks, and sometimes clutters | |
* connections to external services which would otherwise be terminated (thanks to garbage collection). | |
*/ | |
public static function unsetNonPHPUnitObjectProperties(TestCase $instance): void | |
{ | |
$className = get_class($instance); | |
if (array_key_exists($className, self::$propertiesToBeUnsetCache)) { | |
array_walk( | |
self::$propertiesToBeUnsetCache[$className], | |
static function (ReflectionProperty $property) use ($instance): void { | |
$unset = (static function (TestCase $instance) use ($property): void { | |
unset($instance->{$property->getName()}); | |
}) | |
->bindTo(null, $property->getDeclaringClass()->getName()); | |
Assert::notFalse($unset); | |
$unset($instance); | |
} | |
); | |
gc_collect_cycles(); | |
return; | |
} | |
self::$propertiesToBeUnsetCache[$className] = array_filter( | |
self::getAllPropertiesForClasses(self::getClasses(new ReflectionClass($className))), | |
[self::class, 'isNotPhpunitInternalProperty'] | |
); | |
// Recursion - cache is not empty, so this will short-circuit | |
self::unsetNonPHPUnitObjectProperties($instance); | |
} | |
/** @return non-empty-list<ReflectionClass<object>> */ | |
private static function getClasses(ReflectionClass $thisClass): array | |
{ | |
$parentClass = $thisClass->getParentClass(); | |
if ($parentClass instanceof ReflectionClass) { | |
return array_merge([$thisClass], self::getClasses($parentClass)); | |
} | |
return [$thisClass]; | |
} | |
private static function isNotPhpunitInternalProperty(ReflectionProperty $property): bool | |
{ | |
return strpos($property->getDeclaringClass()->getName(), 'PHPUnit\\') !== 0; | |
} | |
/** | |
* @param non-empty-list<ReflectionClass> $classes | |
* | |
* @return ReflectionProperty[] | |
*/ | |
private static function getAllPropertiesForClasses(array $classes): array | |
{ | |
return array_filter( | |
array_merge( | |
...array_map(static function (ReflectionClass $class): array { | |
return $class->getProperties(); | |
}, $classes) | |
), | |
static fn (ReflectionProperty $property): bool => ! $property->isStatic() | |
); | |
} | |
} |
Considering that prophecy is gone-gone-gone in all my projects, IMO not to be done :P
I'd totally add it on an upstream patch to phpunit itself though 👍
Yeah we still have some leftover Prophecy bits here so I ran into problems, just sharing if it helps anyone else passing by here :)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks! Minor patch to add missing generic types and support prophecy properties.