timestamp = new DateTimeImmutable('2024-01-15 10:30:45', new DateTimeZone('Europe/Berlin')); }); describe('constructor', function () { it('accepts default format and timestamp format', function () { $formatter = new LineFormatter(); expect($formatter instanceof LineFormatter)->toBeTrue(); }); it('accepts custom format', function () { $formatter = new LineFormatter( format: '{level}: {message}' ); expect($formatter instanceof LineFormatter)->toBeTrue(); }); it('accepts custom timestamp format', function () { $formatter = new LineFormatter( format: '[{timestamp}] {message}', timestampFormat: 'H:i:s' ); expect($formatter instanceof LineFormatter)->toBeTrue(); }); }); describe('default format', function () { it('formats with default format string', function () { $formatter = new LineFormatter(); $record = new LogRecord( message: 'Test message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect($result)->toBeString(); expect($result)->toContain('2024-01-15'); expect($result)->toContain('INFO'); expect($result)->toContain('Test message'); }); it('includes channel in output', function () { $formatter = new LineFormatter(); $record = new LogRecord( message: 'Test message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp, channel: 'security' ); $result = $formatter($record); expect($result)->toContain('security'); }); it('uses default channel "app" when none provided', function () { $formatter = new LineFormatter(); $record = new LogRecord( message: 'Test message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect($result)->toContain('app'); }); it('includes context as JSON', function () { $formatter = new LineFormatter(); $record = new LogRecord( message: 'Test message', context: LogContext::withData([ 'user_id' => 123, 'action' => 'login' ]), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect($result)->toContain('"user_id":123'); expect($result)->toContain('"action":"login"'); }); it('handles empty context', function () { $formatter = new LineFormatter(); $record = new LogRecord( message: 'Test message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); // Empty context should result in empty context string expect($result)->toBeString(); expect($result)->toContain('Test message'); }); }); describe('custom format', function () { it('uses custom format string', function () { $formatter = new LineFormatter( format: '{level}: {message}' ); $record = new LogRecord( message: 'Custom format', context: LogContext::empty(), level: LogLevel::WARNING, timestamp: $this->timestamp ); $result = $formatter($record); expect($result)->toBe('WARNING: Custom format'); }); it('supports timestamp-only format', function () { $formatter = new LineFormatter( format: '[{timestamp}]', timestampFormat: 'Y-m-d H:i:s' ); $record = new LogRecord( message: 'Message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect($result)->toBe('[2024-01-15 10:30:45]'); }); it('supports message-only format', function () { $formatter = new LineFormatter( format: '{message}' ); $record = new LogRecord( message: 'Simple message', context: LogContext::empty(), level: LogLevel::DEBUG, timestamp: $this->timestamp ); $result = $formatter($record); expect($result)->toBe('Simple message'); }); it('supports complex custom format', function () { $formatter = new LineFormatter( format: '[{timestamp}] {channel}.{level}: {message} {context}' ); $record = new LogRecord( message: 'Complex', context: LogContext::withData(['key' => 'value']), level: LogLevel::ERROR, timestamp: $this->timestamp, channel: 'api' ); $result = $formatter($record); expect($result)->toContain('[2024-01-15'); expect($result)->toContain('api.ERROR'); expect($result)->toContain('Complex'); expect($result)->toContain('"key":"value"'); }); }); describe('timestamp formatting', function () { it('uses default timestamp format', function () { $formatter = new LineFormatter( format: '{timestamp}' ); $record = new LogRecord( message: 'Message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); // Default format is Y-m-d H:i:s expect($result)->toBe('2024-01-15 10:30:45'); }); it('uses custom timestamp format', function () { $formatter = new LineFormatter( format: '{timestamp}', timestampFormat: 'd/m/Y H:i' ); $record = new LogRecord( message: 'Message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect($result)->toBe('15/01/2024 10:30'); }); it('supports ISO 8601 timestamp format', function () { $formatter = new LineFormatter( format: '{timestamp}', timestampFormat: 'c' ); $record = new LogRecord( message: 'Message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect($result)->toContain('2024-01-15T10:30:45'); }); it('supports microseconds in timestamp', function () { $formatter = new LineFormatter( format: '{timestamp}', timestampFormat: 'Y-m-d H:i:s.u' ); $record = new LogRecord( message: 'Message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect($result)->toContain('2024-01-15 10:30:45.'); }); }); describe('context handling', function () { it('formats context as JSON with unescaped slashes', function () { $formatter = new LineFormatter( format: '{context}' ); $record = new LogRecord( message: 'Message', context: LogContext::withData([ 'url' => 'https://example.com/path', 'file' => '/var/www/html/index.php' ]), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); // JSON_UNESCAPED_SLASHES should be used expect($result)->toContain('https://example.com/path'); expect($result)->toContain('/var/www/html/index.php'); // Slashes should not be escaped expect(str_contains($result, '\\/'))->toBeFalse(); }); it('combines context and extras', function () { $formatter = new LineFormatter( format: '{context}' ); $record = (new LogRecord( message: 'Message', context: LogContext::withData(['context_key' => 'context_value']), level: LogLevel::INFO, timestamp: $this->timestamp ))->addExtra('extra_key', 'extra_value'); $result = $formatter($record); expect($result)->toContain('"context_key":"context_value"'); expect($result)->toContain('"extra_key":"extra_value"'); }); it('handles nested context arrays', function () { $formatter = new LineFormatter( format: '{context}' ); $record = new LogRecord( message: 'Message', context: LogContext::withData([ 'user' => [ 'id' => 123, 'name' => 'John Doe', 'roles' => ['admin', 'user'] ] ]), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect($result)->toContain('"user"'); expect($result)->toContain('"id":123'); expect($result)->toContain('"name":"John Doe"'); expect($result)->toContain('["admin","user"]'); }); it('handles special characters in context', function () { $formatter = new LineFormatter( format: '{context}' ); $record = new LogRecord( message: 'Message', context: LogContext::withData([ 'text' => 'Special chars: "quotes", \'apostrophes\', \\backslashes' ]), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect($result)->toBeString(); expect($result)->toContain('Special chars'); }); }); describe('log levels', function () { it('formats DEBUG level', function () { $formatter = new LineFormatter(format: '{level}'); $record = new LogRecord( message: 'Message', context: LogContext::empty(), level: LogLevel::DEBUG, timestamp: $this->timestamp ); expect($formatter($record))->toBe('DEBUG'); }); it('formats INFO level', function () { $formatter = new LineFormatter(format: '{level}'); $record = new LogRecord( message: 'Message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); expect($formatter($record))->toBe('INFO'); }); it('formats WARNING level', function () { $formatter = new LineFormatter(format: '{level}'); $record = new LogRecord( message: 'Message', context: LogContext::empty(), level: LogLevel::WARNING, timestamp: $this->timestamp ); expect($formatter($record))->toBe('WARNING'); }); it('formats ERROR level', function () { $formatter = new LineFormatter(format: '{level}'); $record = new LogRecord( message: 'Message', context: LogContext::empty(), level: LogLevel::ERROR, timestamp: $this->timestamp ); expect($formatter($record))->toBe('ERROR'); }); it('formats CRITICAL level', function () { $formatter = new LineFormatter(format: '{level}'); $record = new LogRecord( message: 'Message', context: LogContext::empty(), level: LogLevel::CRITICAL, timestamp: $this->timestamp ); expect($formatter($record))->toBe('CRITICAL'); }); it('formats ALERT level', function () { $formatter = new LineFormatter(format: '{level}'); $record = new LogRecord( message: 'Message', context: LogContext::empty(), level: LogLevel::ALERT, timestamp: $this->timestamp ); expect($formatter($record))->toBe('ALERT'); }); it('formats EMERGENCY level', function () { $formatter = new LineFormatter(format: '{level}'); $record = new LogRecord( message: 'Message', context: LogContext::empty(), level: LogLevel::EMERGENCY, timestamp: $this->timestamp ); expect($formatter($record))->toBe('EMERGENCY'); }); }); describe('readonly behavior', function () { it('is a readonly class', function () { $reflection = new ReflectionClass(LineFormatter::class); expect($reflection->isReadOnly())->toBeTrue(); }); }); describe('edge cases', function () { it('handles empty message', function () { $formatter = new LineFormatter(format: '{message}'); $record = new LogRecord( message: '', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); expect($formatter($record))->toBe(''); }); it('handles very long message', function () { $formatter = new LineFormatter(format: '{message}'); $longMessage = str_repeat('x', 10000); $record = new LogRecord( message: $longMessage, context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect($result)->toHaveLength(10000); }); it('handles unicode characters in message', function () { $formatter = new LineFormatter(format: '{message}'); $record = new LogRecord( message: 'Unicode: 你好 мир 🌍', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect($result)->toContain('你好'); expect($result)->toContain('мир'); expect($result)->toContain('🌍'); }); it('handles null channel gracefully', function () { $formatter = new LineFormatter(format: '{channel}'); $record = new LogRecord( message: 'Message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp, channel: null ); $result = $formatter($record); expect($result)->toBe('app'); // Default channel }); it('handles format with no placeholders', function () { $formatter = new LineFormatter(format: 'static text'); $record = new LogRecord( message: 'Message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); expect($formatter($record))->toBe('static text'); }); }); });