createMock(LogHandler::class); $fallback = $this->createMock(LogHandler::class); $record = $this->createLogRecord(); $primary->expects($this->once()) ->method('handle') ->with($record); $fallback->expects($this->never()) ->method('handle'); $handler = new ResilientLogHandler($primary, $fallback); $handler->handle($record); } public function test_uses_fallback_when_primary_fails(): void { $primary = $this->createMock(LogHandler::class); $fallback = $this->createMock(LogHandler::class); $record = $this->createLogRecord(); $primary->method('handle') ->willThrowException(new \RuntimeException('Primary failed')); $fallback->expects($this->atLeastOnce()) ->method('handle'); $handler = new ResilientLogHandler($primary, $fallback); $handler->handle($record); // Should not throw exception $this->assertTrue(true); } public function test_never_throws_exception(): void { $primary = $this->createMock(LogHandler::class); $fallback = $this->createMock(LogHandler::class); $primary->method('handle') ->willThrowException(new \RuntimeException('Primary failed')); $fallback->method('handle') ->willThrowException(new \RuntimeException('Fallback failed')); $handler = new ResilientLogHandler($primary, $fallback); // Should never throw - uses error_log as last resort $handler->handle($this->createLogRecord()); $this->assertTrue(true); } public function test_circuit_breaker_opens_after_failures(): void { $primary = $this->createMock(LogHandler::class); $fallback = $this->createMock(LogHandler::class); $circuitBreaker = new CircuitBreaker( new CircuitBreakerConfig( name: 'test', failureThreshold: 3, successThreshold: 1, timeout: 60.0 ) ); $primary->method('handle') ->willThrowException(new \RuntimeException('Failed')); $handler = new ResilientLogHandler($primary, $fallback, $circuitBreaker); // Trigger failures to open circuit for ($i = 0; $i < 5; $i++) { $handler->handle($this->createLogRecord()); } $this->assertTrue($circuitBreaker->isOpen()); $this->assertFalse($handler->isHealthy()); } private function createLogRecord(string $message = 'test'): LogRecord { return new LogRecord( level: LogLevel::INFO, message: $message, channel: 'test', context: LogContext::empty(), timestamp: new \DateTimeImmutable() ); } }