Give the PHP time limit back after every test (#89)

A streaming controller's set_time_limit() outlived its test in the single PHPUnit process and killed slower runs part-way. A PHPUnit extension resets it after every test.

Co-authored-by: pmdevelopment <4947485+pmdevelopment@users.noreply.github.com>
This commit is contained in:
Svenandpmdevelopment authored and GitHub committed 2026-10-15 14:49:28 +02:00
1 parent 280f58b7b5
commit 55a6d67a48
3 files changed
+82

No files matched your search

+3
View File
@@ -53,5 +53,8 @@
</source> </source>
<extensions> <extensions>
<!-- The streaming controllers call set_time_limit(), and in one
PHPUnit process that limit outlives their test. See the class. -->
<bootstrap class="App\Tests\Support\PhpUnit\ResetTimeLimitExtension"/>
</extensions> </extensions>
</phpunit> </phpunit>
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Tests\Support\PhpUnit;
use PHPUnit\Event\Test\Finished;
use PHPUnit\Event\Test\FinishedSubscriber;
use PHPUnit\Runner\Extension\Extension;
use PHPUnit\Runner\Extension\Facade;
use PHPUnit\Runner\Extension\ParameterCollection;
use PHPUnit\TextUI\Configuration\Configuration;
/**
* Gives the time limit back after every test.
*
* Four controllers stream for longer than PHP's default allows and call
* set_time_limit() to say so — the JMAP event source, the thread summary, the
* compose assistant and the AI warm-up. In a web request that is right: the
* limit dies with the request. In the suite it does not, because every test
* runs in the one PHPUnit process, and a limit set by a controller test is
* still counting for every test after it. On a fast machine the rest of the
* suite finished inside it; on a slower one the run died part-way with
* "Maximum execution time of 300 seconds exceeded", in whichever unrelated
* test happened to be running — CalendarPullerTest one time, GraphFolderMoveTest
* the next — and passed again when that test was run on its own.
*
* In the suite rather than in the controllers: the four calls are correct, and
* a seam in production code to hide them from tests would be four places to
* get wrong for the sake of one place that is. ResetTimeLimitExtensionTest
* holds it.
*/
final class ResetTimeLimitExtension implements Extension
{
public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void
{
$facade->registerSubscriber(new class implements FinishedSubscriber {
public function notify(Finished $event): void
{
// 0 is the CLI default: no limit. set_time_limit() also
// restarts the clock, which is what a limit somebody else set
// needs.
set_time_limit(0);
}
});
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Tests\Support\PhpUnit;
use PHPUnit\Framework\Attributes\Depends;
use PHPUnit\Framework\TestCase;
/**
* A time limit one test sets is gone by the next one.
*
* Two tests in a fixed order, because the claim is about what one test leaves
* behind for the next: the first does what a streaming controller does, the
* second looks. Without ResetTimeLimitExtension registered in phpunit.dist.xml
* the second sees the first's 300 seconds and fails.
*/
final class ResetTimeLimitExtensionTest extends TestCase
{
public function testAControllerTestSetsALimitTheWayTheStreamingEndpointsDo(): void
{
set_time_limit(300);
self::assertSame('300', ini_get('max_execution_time'));
}
#[Depends('testAControllerTestSetsALimitTheWayTheStreamingEndpointsDo')]
public function testTheNextTestRunsWithoutIt(): void
{
self::assertSame('0', ini_get('max_execution_time'), 'a time limit set by one test is still counting in the next');
}
}