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]; } }