laravel-websockets/tests/Server/ChildFatalErrorGuardTest.php

82 lines
2.9 KiB
PHP

<?php
declare(strict_types=1);
namespace BlaxSoftware\LaravelWebSockets\Test\Server;
use BlaxSoftware\LaravelWebSockets\Ipc\SocketPairIpc;
use PHPUnit\Framework\TestCase;
/**
* Regression guard for #1462: a message child that died of a fatal error
* (out of memory, E_ERROR, ...) sent no `:error` frame and the client hung.
*
* Each case runs Fixtures/fatal_child.php in its own PHP process, which forks
* a child that really dies of the fatal error.
*/
class ChildFatalErrorGuardTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
if (! function_exists('pcntl_fork') || ! SocketPairIpc::isSupported()) {
$this->markTestSkipped('pcntl and sockets are required');
}
}
public function test_a_child_that_runs_out_of_memory_still_sends_the_error_frame()
{
$result = $this->runChild('oom');
$this->assertSame(255, $result['exit_code'], 'PHP exits with 255 after a fatal error.');
$this->assertCount(1, $result['frames']);
$frame = json_decode($result['frames'][0], true);
$this->assertSame('demo.crash:error', $frame['event']);
$this->assertSame('Internal server error', $frame['data']['message']);
$this->assertTrue($frame['data']['meta']['fatal']);
$this->assertTrue($result['replied'], 'The parent recognises the frame as the final reply.');
$this->assertStringContainsString('app handler saw: Allowed memory size', $result['stderr'], 'The earlier app handler still ran first.');
}
public function test_a_disarmed_guard_stays_silent()
{
$result = $this->runChild('oom-disarmed');
$this->assertSame(255, $result['exit_code']);
$this->assertSame([], $result['frames']);
$this->assertFalse($result['replied']);
}
public function test_a_normal_exit_sends_nothing()
{
$this->assertSame([], $this->runChild('clean-exit')['frames']);
$this->assertSame([], $this->runChild('warning-then-exit')['frames'], 'A non-fatal last error is not a crash.');
}
/**
* @return array{frames: string[], exit_code: ?int, replied: bool, stderr: string}
*/
private function runChild(string $mode): array
{
$process = proc_open(
[PHP_BINARY, '-d', 'display_errors=stderr', '-d', 'memory_limit=128M', __DIR__ . '/Fixtures/fatal_child.php', $mode],
[1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes
);
$this->assertIsResource($process);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($process);
$result = json_decode((string) $stdout, true);
$this->assertIsArray($result, "Driver output was not JSON.\nstdout: {$stdout}\nstderr: {$stderr}");
return $result + ['stderr' => $stderr];
}
}