Skip to content

Instantly share code, notes, and snippets.

@Ocramius
Created March 31, 2022 17:16
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Ocramius/d5f797cd30f64b2214cf620aff5ea3d7 to your computer and use it in GitHub Desktop.
Save Ocramius/d5f797cd30f64b2214cf620aff5ea3d7 to your computer and use it in GitHub Desktop.
<?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()
);
}
}
@Seldaek
Copy link

Seldaek commented Apr 6, 2022

Thanks! Minor patch to add missing generic types and support prophecy properties.

@@ -69,7 +69,10 @@ final class ClearAllNonPhpunitProperties
         self::unsetNonPHPUnitObjectProperties($instance);
     }

-    /** @return non-empty-list<ReflectionClass<object>> */
+    /**
+     * @param ReflectionClass<TestCase> $thisClass
+     * @return non-empty-list<ReflectionClass<TestCase>>
+     */
     private static function getClasses(ReflectionClass $thisClass): array
     {
         $parentClass = $thisClass->getParentClass();
@@ -83,11 +86,12 @@ final class ClearAllNonPhpunitProperties

     private static function isNotPhpunitInternalProperty(ReflectionProperty $property): bool
     {
-        return strpos($property->getDeclaringClass()->getName(), 'PHPUnit\\') !== 0;
+        return !str_starts_with($property->getDeclaringClass()->getName(), 'PHPUnit\\')
+            && !in_array($property->getName(), ['prophet', 'prophecyAssertionsCounted'], true);
     }

     /**
-     * @param non-empty-list<ReflectionClass> $classes
+     * @param non-empty-list<ReflectionClass<TestCase>> $classes
      *
      * @return ReflectionProperty[]
      */

@Ocramius
Copy link
Author

Ocramius commented Apr 6, 2022

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 👍

@Seldaek
Copy link

Seldaek commented Apr 6, 2022

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