-
-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
TestCase is instanciated per test (correct), but not destroyed immediatelly (wrong, non GCable) #4705
Comments
PHPUnit has worked like this since the beginning, which is now for over 20 years. While this is certainly not desired behaviour, it is the expected behaviour, at least for the time being. I would therefore not call it a bug. Unfortunately, we are not yet in a situation where this can easily be changed. My hope is that once the dust has settled from the major undertakings of implementing support for PHP 8 attributes, introducing an event system, and cleaning up PHPUnit's configuration handling that this can be approached. |
@sebastianbergmann Since attributes and the event system have launched, could you maybe consider to evaluate this for v12? I think this is one of the more common pitfalls, and multiple 3rd parties now hint to always clear properties with some default tearDown reflection magic. Random example: https://stackoverflow.com/questions/36032168/symfony-and-phpunit-memory-leak In one of our bigger projects (200k test lines) we use explicit tearDown implementations which are slightly faster than the generic reflection trick, but since it tends to get forgotten and takes effort we’ll convert to the reflection trick too. Alternatively: I suspect current postponed destruction to have quite an impact on used resources and cloud pipeline bills worldwide. |
This is one working approach to reduce memory consumption:
My example implementation from https://github.com/TYPO3/testing-framework/pull/562/files BaseTestCase with GC in tearDownAfterClass and memory consumption info per test (click to open)abstract class BaseTestCase extends TestCase
{
public static function setUpBeforeClass(): void
{
if(getenv('MEMORY_INFO')) {
echo PHP_EOL . '# \\' . static::class . PHP_EOL;
// Force garbage collection to measure memory used by these test-cases only.
gc_collect_cycles();
$GLOBALS['memory_usage_start'] = memory_get_usage();
}
parent::setUpBeforeClass();
}
protected function tearDown(): void
{
parent::tearDown();
/*
* Unset properties from test-object
*
* The properties - introduced in the test-class' setUp method - persist memory for each test-case run until all
* test-runs have finished. So memory adds up with every test-case.
*
* All these properties should actually be unset in the test-class' tearDown method.
* Unfortunately tearDown is not implemented in every test-class or tearDown does not accurately unset all
* properties.
*
* Important: Gargbage collection must be triggered to really free up memory. As garbage collection consumes
* time this is done only once per test-set in tearDownAfterClass method.
*
* Note: Private properties cannot be cleaned up here.
*/
$reflection = new \ReflectionClass(static::class);
foreach ($reflection->getProperties(
\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED
) as $property) {
if (!$property->isStatic() && $property->class === static::class && isset($this->{$property->name})) {
unset($this->{$property->name});
if(getenv('MEMORY_INFO')) {
echo '!';
}
}
}
unset($reflection);
}
public static function tearDownAfterClass():void
{
parent::tearDownAfterClass();
/*
* In tearDown method all properties of the test-object are unset to free up memory.
* This memory is only reclaimed if neccessary, e.g. the memory of the running PHP process is running into its
* limitation and thereby an automatic garbage collection is triggered.
*
* The limit for memory is really high, therefor garbage collection is never triggered and the memory adds up.
*
* To use as little memory as possible we force a garbage collection here to reclaim free memory after a
* test-set is completed.
*/
gc_collect_cycles();
if(getenv('MEMORY_INFO')) {
$memStart = $GLOBALS['memory_usage_start'];
$memCurrent = memory_get_usage();
echo PHP_EOL . 'Memory: '
. sprintf("%0+1.2f", ($memCurrent - $memStart) / 1024 / 1024) . ' MB / '
. sprintf("%01.2f", $memCurrent / 1024 / 1024) . ' MB' . PHP_EOL;
unset($GLOBALS['memory_usage_start']);
}
}
} |
@rr-it : One of many other example cleanup scripts: https://github.com/talkinnl/dont-leak , then you only need to add a single line to your BaseTestCase. It'll also cleanup privates, and privates of parents. I think user-space remedies are covered with our suggestions, but I hope for this issue to be solved by design. :) |
@sebastianbergmann this issue is not fixed as claimed in #5861 (comment) |
@mvorisek Based on your PR (#5867 ), I didn't get yet that it really didn't work for you. And the change actually has proven to pay off for huge testsuites (my own, see my comment in the change PR, testsuite of 2 hours), Typo3 and others. HOWEVER, (ping @sebastianbergmann ), I did just run your PR with proposed test and you're right about this specific scenario: TestCases with dataproviders aren't yet destructed immediately [edited to add: ], but after the various dataprover instances are done, which is still way earlier than before. |
Please stop commenting on a closed ticket. If what @talkinnl writes (test cases that use data providers are not destructed immediately) then please open a new ticket with a minimal, self-contained, reproducing test case that shows this. |
Yes, you're right. One last time on this closed ticket, as a triaging service :) So it's not at the end as before, but after the last provided data instance. Which is already quite a bit better. I will NOT log a new issue; it hurts way less than before, and if this is really blocking you can still use a tearDown(). Curiosity got a hold of me: The issue is with TestSuiteIterator::$tests. By modifying the iterator to use array_shift instead of position logic, I could fix this. Needs more work though.. |
If you know how to fix this please propose a PR. |
Summary
TestCase is instanciated per test (correct), but not destroyed immediatelly (wrong, non GCable)
Current behavior
not destroyed immediatelly
How to reproduce
see atk4/data@849a548 and related output https://github.com/atk4/data/runs/2782214124#step:8:31 (can be reproduced by running phpunit locally)
In the output, you can see that
x
representingTestCase::__destruct
call is called after all tests are done.Expected behavior
TestCase must be destroyed immediatelly after a test finishes. It is important to allow GC to free the TestCase instance immediatelly. This should be also tested in unit tests so same issue does not happen again.
The text was updated successfully, but these errors were encountered: