Skip to content

Conversation

donnysim
Copy link
Contributor

Fix for #40554 . By introducing a separate global handler we can avoid keeping every HandleExceptions with its own Application instance in memory and it can be cleaned up after each test instead of after all tests are done.

Couldn't think of any other way to deal with it without introducing a separate class.

@donnysim donnysim changed the title fix HandleExceptions memory leak in tests [8.x] fix HandleExceptions memory leak in tests Jan 24, 2022
@taylorotwell
Copy link
Member

taylorotwell commented Jan 24, 2022

When you say that currently it is not cleaned up until all tests are done - do you mean it is cleaned up after each test class is finished or not until the entire suite is done running?

@donnysim
Copy link
Contributor Author

Test suite. The previous handlers will be kept as long as the PHP process is alive and increase with each test, or we could say, every application instantiation that executes the HandleException@bootstrap. This change makes sure that PHP doesn't hold it in the error, exception or in shutdown handlers stack, preventing them from being GC.

@donnysim
Copy link
Contributor Author

Failing PHP 8.1 tests are not related to my change but can fix them too if need be.

@driesvints
Copy link
Member

@donnysim I think these failures are on your branch since they're not happening on 8.x so can you rebase maybe?

@driesvints driesvints linked an issue Jan 25, 2022 that may be closed by this pull request
@donnysim
Copy link
Contributor Author

donnysim commented Jan 25, 2022

@driesvints don't really get it, the 8.x branch does have this issue in tests even locally running with 8.1 https://github.com/laravel/framework/blob/8.x/tests/Support/SupportJsTest.php#L56

Testing started at 5:40 PM ...
PHPUnit 9.5.13 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.1.1
Configuration: \phpunit.xml.dist


Fatal error: During inheritance of JsonSerializable: Uncaught Return type of JsonSerializable@anonymous::jsonSerialize() should either be compatible with JsonSerializable::jsonSerialize(): mixed, or the #[\ReturnTypeWillChange] attribute should be used to temporarily suppress the notice

\tests\Support\SupportJsTest.php:56
\tests\Support\SupportJsTest.php:50
 in \tests\Support\SupportJsTest.php on line 50

Process finished with exit code 255

It doesn't matter if I run it with or without the change, I still get this error. Kind of confused why it doesn't occur in other pull requests.

@X-Coder264
Copy link
Contributor

X-Coder264 commented Jan 25, 2022

I think something was wrong with the error/deprecation handler prior to this PR and that this PR exposed that bug. The CI pipeline should be failing on the 8.x branch too (which currently does not happen) as there's no return type hint nor the return type will change annotation on that jsonSerialize method. You can add the array return typehint to those methods to fix this deprecation.

Maybe it has something to do with the order in which the tests are executed on the CI.

@donnysim
Copy link
Contributor Author

@X-Coder264 that would make sense, all other jsonSerialize definitions have return types defined except the ones that fail here.

@driesvints
Copy link
Member

@X-Coder264 I don't understand. Surely those tests would fail on 8.x right now as well? This shouldn't happen..

@X-Coder264
Copy link
Contributor

X-Coder264 commented Jan 25, 2022

@driesvints Yeah, they should fail on 8.x but they don't which is weird. After fixing the return type new deprecation exceptions popped up on the latest pipeline:

There were 3 errors:

1) Illuminate\Tests\Integration\Auth\ForgotPasswordTest::it_can_send_forgot_password_email
strncmp(): Passing null to parameter #1 ($string1) of type string is deprecated

/home/runner/work/framework/framework/src/Illuminate/Support/Str.php:818
/home/runner/work/framework/framework/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php:87
/home/runner/work/framework/framework/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php:72
/home/runner/work/framework/framework/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php:49
/home/runner/work/framework/framework/src/Illuminate/Support/Facades/Facade.php:261
/home/runner/work/framework/framework/tests/Integration/Auth/ForgotPasswordTest.php:45

2) Illuminate\Tests\Integration\Auth\ForgotPasswordTest::it_can_send_forgot_password_email_via_create_url_using
strncmp(): Passing null to parameter #1 ($string1) of type string is deprecated

/home/runner/work/framework/framework/src/Illuminate/Support/Str.php:818
/home/runner/work/framework/framework/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php:87
/home/runner/work/framework/framework/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php:72
/home/runner/work/framework/framework/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php:49
/home/runner/work/framework/framework/src/Illuminate/Support/Facades/Facade.php:261
/home/runner/work/framework/framework/tests/Integration/Auth/ForgotPasswordTest.php:73

3) Illuminate\Tests\Integration\Auth\ForgotPasswordTest::it_can_send_forgot_password_email_via_to_mail_using
strncmp(): Passing null to parameter #1 ($string1) of type string is deprecated

/home/runner/work/framework/framework/src/Illuminate/Support/Str.php:818
/home/runner/work/framework/framework/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php:87
/home/runner/work/framework/framework/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php:72
/home/runner/work/framework/framework/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php:49
/home/runner/work/framework/framework/src/Illuminate/Support/Facades/Facade.php:261
/home/runner/work/framework/framework/tests/Integration/Auth/ForgotPasswordTest.php:105

@donnysim
Copy link
Contributor Author

@X-Coder264 I don't understand. Surely those tests would fail on 8.x right now as well? This shouldn't happen..

If I clone the 8.x branch and run those tests with PHP 8.1, it fails at the same place and more.

if you just run the file

<?php

declare(strict_types=1);

$data = new class() implements JsonSerializable
{
    public $foo = 'not hello';

    public $bar = 'not world';

    public function toJson($options = 0)
    {
        return json_encode(['foo' => 'hello', 'bar' => 'world'], $options);
    }

    public function jsonSerialize()
    {
        return ['foo' => 'not hello', 'bar' => 'not world'];
    }

    public function toArray()
    {
        return ['foo' => 'not hello', 'bar' => 'not world'];
    }
};

as it is in the tests with PHP 8.1 it throws a deprecation warning, there's just no way it should pass in the CI.

@donnysim
Copy link
Contributor Author

donnysim commented Jan 25, 2022

@driesvints there does seem to be a test ordering problem, didn't find which yet but if some tests run before it, then the deprecation is hidden, so something is tinkering with the exceptions and not restoring them (testing without this pull change).

@donnysim
Copy link
Contributor Author

donnysim commented Jan 25, 2022

@driesvints it has something to do with using Orchestra\Testbench\TestCase, as soon as this is used, deprecation warnings are gone,

@donnysim
Copy link
Contributor Author

@nunomaduro it seems like this 52e2376 commit introduced a bug where the deprecation errors are swallowed because no LogManager is registered when used with Orchestra\Testbench\TestCase. When running plain PHPUnit\Framework\TestCase the application is not instantiated and HandleExceptions never registered so a pure PHP deprecation is thrown. Also, because of the nature where the handler is never unregistered, it will always swallow the deprecation exception for further tests. Even with current pull request, the last registered HandleExceptions will be used until process exits.

@driesvints
Copy link
Member

Odd because I sent in a PR recently to explicitly surface deprecation errors in CI: #40324 so I'm not sure why these were being repressed. I pinged @nunomaduro so hopefully he can shed some light here.

@driesvints
Copy link
Member

Placing this in draft until @nunomaduro has had a chance to reply and we've resolved all issues.

@driesvints driesvints marked this pull request as draft January 25, 2022 19:06
@donnysim
Copy link
Contributor Author

donnysim commented Jan 25, 2022

The main problem is that the handler just exits silently - null/void handler result is held as user handled event. If it was supposed to fallback to the default PHP exception handling in case of the deprecation error it should return false or throw an error itself:

if (! class_exists(LogManager::class)) {
    return false;
}

try {
    $logger = $this->app->make(LogManager::class);
} catch (Exception $e) {
    return false;
}

otherwise no matter the error_reporting level, it will never reach it. What I'm afraid is that this change might become a breaking change where suddenly deprecated errors are thrown in user applications, depending if the LogManager exists. But yeah, lets wait, maybe he has some insights too.

@X-Coder264
Copy link
Contributor

X-Coder264 commented Jan 25, 2022

@driesvints @nunomaduro I took a quick look at the state on the 8.x branch. There are two issues here that need to be addressed, one concerning integration tests where the framework is booted and another which affects unit tests (which do not boot the framework).

After #39219 all deprecations for integration tests were silenced (both for user apps as well as for Laravel's own test suite).

withoutDeprecationHandling was added in #39261 but it was never applied to all existing integration tests in Laravel's own test suite. If those tests do not explicitly call withoutDeprecationHandling the new Laravel error handler will only log those errors, but since no exception is thrown the tests do not fail. The entire InteractsWithDeprecationHandling trait should be first added to the Orchestra\Testbench\TestCase class in order to be able to call that method.

So that solves the issue of why some deprecations were left unnoticed in integration tests (you can confirm this is the cause of the problem by calling withoutDeprecationHandling on 8.x branch code in the Illuminate\Tests\Integration\Auth\ForgotPasswordTest::it_can_send_forgot_password_email test and seeing that the test is red as opposed to being green before adding that method call).

Now onto why some deprecations were not noticed in unit tests (like the one in SupportJsTest which was fixed in this PR) even though there's no "framework" involved in those tests so it should be just pure PHPUnit stuff - the issue here is the order of execution.

PHPUnit is properly configured to convert deprecations to exceptions which happens in the PHPUnit ErrorHandler which is registered on the test run, but PHPUnit does not register its error handler if a previous error handler was already configured -> https://github.com/sebastianbergmann/phpunit/blob/9.5.13/src/Util/ErrorHandler.php#L131-L146

This means that if an integration test runs before an unit test it sets the Laravel error handler and then when the unit test runs PHPUnit does not register its own error handler and because Laravel's error handler does not throw an exception anymore the test turns out green instead of red.

The fix here would be to always clear the error handler that gets set during the framework boot (by calling restore_error_handler, which should also solve the memory leak) after the request is handled.

@X-Coder264
Copy link
Contributor

Alternate fix which should resolve all problems -> #40639

@driesvints @nunomaduro Any feedback on that PR is greatly appreciated.

@donnysim Can you also please confirm that it solves your memory leak problem? Thank you.

@donnysim
Copy link
Contributor Author

@X-Coder264 @driesvints with one small change the new pull is looking good. Closing this in favor of #40639.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Memory leak in tests because HandleExceptions never cleans up
4 participants