82 lines
2.9 KiB
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];
|
||
|
|
}
|
||
|
|
}
|