timestamp = new DateTimeImmutable('2024-01-15 10:30:45.123456', new DateTimeZone('Europe/Berlin')); }); describe('constructor', function () { it('accepts default configuration', function () { $formatter = new DevelopmentFormatter(); expect($formatter instanceof DevelopmentFormatter)->toBeTrue(); }); it('accepts color output option', function () { $formatter = new DevelopmentFormatter(colorOutput: false); expect($formatter instanceof DevelopmentFormatter)->toBeTrue(); }); it('accepts include stack trace option', function () { $formatter = new DevelopmentFormatter(includeStackTrace: false); expect($formatter instanceof DevelopmentFormatter)->toBeTrue(); }); it('accepts both options', function () { $formatter = new DevelopmentFormatter( includeStackTrace: true, colorOutput: true ); expect($formatter instanceof DevelopmentFormatter)->toBeTrue(); }); }); describe('basic formatting', function () { it('returns formatted string', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord( message: 'Test message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect($result)->toBeString(); }); it('includes timestamp with microseconds', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord( message: 'Test message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect(str_contains($result, '10:30:45.'))->toBeTrue(); }); it('includes log level', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord( message: 'Test message', context: LogContext::empty(), level: LogLevel::WARNING, timestamp: $this->timestamp ); $result = $formatter($record); expect(str_contains($result, 'WARNING'))->toBeTrue(); }); it('includes message', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord( message: 'Important test message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect(str_contains($result, 'Important test message'))->toBeTrue(); }); it('includes channel when provided', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord( message: 'Test message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp, channel: 'security' ); $result = $formatter($record); expect(str_contains($result, 'security'))->toBeTrue(); }); }); describe('color output', function () { it('includes ANSI color codes when enabled', function () { $formatter = new DevelopmentFormatter(colorOutput: true); $record = new LogRecord( message: 'Test message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); // Should contain ANSI escape sequences expect(str_contains($result, "\033["))->toBeTrue(); }); it('excludes ANSI color codes when disabled', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord( message: 'Test message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); // Should not contain ANSI escape sequences expect(str_contains($result, "\033["))->toBeFalse(); }); it('uses different colors for different log levels', function () { $formatter = new DevelopmentFormatter(colorOutput: true); $infoRecord = new LogRecord( message: 'Info', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $errorRecord = new LogRecord( message: 'Error', context: LogContext::empty(), level: LogLevel::ERROR, timestamp: $this->timestamp ); $infoResult = $formatter($infoRecord); $errorResult = $formatter($errorRecord); // Different log levels should have different color codes expect($infoResult !== $errorResult)->toBeTrue(); }); }); describe('context formatting', function () { it('formats context data with indentation', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord( message: 'Test message', context: LogContext::withData([ 'user_id' => 123, 'action' => 'login' ]), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect(str_contains($result, 'user_id'))->toBeTrue(); expect(str_contains($result, '123'))->toBeTrue(); expect(str_contains($result, 'action'))->toBeTrue(); expect(str_contains($result, 'login'))->toBeTrue(); }); it('handles nested context arrays', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord( message: 'Test message', context: LogContext::withData([ 'user' => [ 'id' => 123, 'name' => 'John Doe' ] ]), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); // Arrays are formatted as Array[n], names won't appear expect(str_contains($result, 'user'))->toBeTrue(); expect(str_contains($result, 'Array'))->toBeTrue(); }); it('handles empty context gracefully', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord( message: 'Test message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect($result)->toBeString(); expect(str_contains($result, 'Test message'))->toBeTrue(); }); }); describe('extras formatting', function () { it('formats extra data when present', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = (new LogRecord( message: 'Test message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ))->addExtra('request_id', 'req-123') ->addExtra('session_id', 'sess-456'); $result = $formatter($record); expect(str_contains($result, 'request_id'))->toBeTrue(); expect(str_contains($result, 'req-123'))->toBeTrue(); expect(str_contains($result, 'session_id'))->toBeTrue(); expect(str_contains($result, 'sess-456'))->toBeTrue(); }); it('handles empty extras gracefully', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord( message: 'Test message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect($result)->toBeString(); }); }); describe('exception formatting', function () { it('formats exception from extras', function () { $formatter = new DevelopmentFormatter(includeStackTrace: false, colorOutput: false); $record = (new LogRecord( message: 'Exception occurred', context: LogContext::empty(), level: LogLevel::ERROR, timestamp: $this->timestamp ))->addExtra('exception_class', 'RuntimeException') ->addExtra('exception_message', 'Test exception message') ->addExtra('exception_file', '/path/to/file.php') ->addExtra('exception_line', 42); $result = $formatter($record); expect(str_contains($result, 'RuntimeException'))->toBeTrue(); expect(str_contains($result, '/path/to/file.php'))->toBeTrue(); }); it('includes stack trace when enabled', function () { $formatter = new DevelopmentFormatter(includeStackTrace: true, colorOutput: false); $record = (new LogRecord( message: 'Exception occurred', context: LogContext::empty(), level: LogLevel::ERROR, timestamp: $this->timestamp ))->addExtra('exception_class', 'RuntimeException') ->addExtra('exception_file', '/path/to/file.php') ->addExtra('exception_line', 42) ->addExtra('stack_trace_short', '#0 /path/to/caller.php(10)'); $result = $formatter($record); expect(str_contains($result, 'RuntimeException'))->toBeTrue(); expect(str_contains($result, '#0'))->toBeTrue(); }); it('excludes stack trace when disabled', function () { $formatter = new DevelopmentFormatter(includeStackTrace: false, colorOutput: false); $record = (new LogRecord( message: 'Exception occurred', context: LogContext::empty(), level: LogLevel::ERROR, timestamp: $this->timestamp ))->addExtra('exception_class', 'RuntimeException') ->addExtra('exception_file', '/path/to/file.php') ->addExtra('exception_line', 42) ->addExtra('stack_trace_short', '#0 /path/to/caller.php(10)'); $result = $formatter($record); expect(str_contains($result, 'RuntimeException'))->toBeTrue(); // Stack trace should not be included when disabled $hasStackTrace = str_contains($result, 'Trace:'); expect($hasStackTrace)->toBeFalse(); }); }); describe('log levels', function () { it('formats DEBUG level', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord( message: 'Debug message', context: LogContext::empty(), level: LogLevel::DEBUG, timestamp: $this->timestamp ); $result = $formatter($record); expect(str_contains($result, 'DEBUG'))->toBeTrue(); }); it('formats INFO level', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord( message: 'Info message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect(str_contains($result, 'INFO'))->toBeTrue(); }); it('formats WARNING level', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord( message: 'Warning message', context: LogContext::empty(), level: LogLevel::WARNING, timestamp: $this->timestamp ); $result = $formatter($record); expect(str_contains($result, 'WARNING'))->toBeTrue(); }); it('formats ERROR level', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord( message: 'Error message', context: LogContext::empty(), level: LogLevel::ERROR, timestamp: $this->timestamp ); $result = $formatter($record); expect(str_contains($result, 'ERROR'))->toBeTrue(); }); it('formats CRITICAL level', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord( message: 'Critical message', context: LogContext::empty(), level: LogLevel::CRITICAL, timestamp: $this->timestamp ); $result = $formatter($record); expect(str_contains($result, 'CRITICAL'))->toBeTrue(); }); it('formats EMERGENCY level', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord( message: 'Emergency message', context: LogContext::empty(), level: LogLevel::EMERGENCY, timestamp: $this->timestamp ); $result = $formatter($record); expect(str_contains($result, 'EMERGENCY'))->toBeTrue(); }); }); describe('readonly behavior', function () { it('is a readonly class', function () { $reflection = new ReflectionClass(DevelopmentFormatter::class); expect($reflection->isReadOnly())->toBeTrue(); }); }); describe('edge cases', function () { it('handles empty message', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord( message: '', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect($result)->toBeString(); }); it('handles very long message', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $longMessage = str_repeat('x', 1000); $record = new LogRecord( message: $longMessage, context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect(str_contains($result, 'xxx'))->toBeTrue(); }); it('handles unicode characters', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord( message: 'Unicode: 你好 мир 🌍', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect(str_contains($result, '你好'))->toBeTrue(); expect(str_contains($result, 'мир'))->toBeTrue(); expect(str_contains($result, '🌍'))->toBeTrue(); }); it('handles null channel', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord( message: 'Test message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp, channel: null ); $result = $formatter($record); expect($result)->toBeString(); expect(str_contains($result, 'Test message'))->toBeTrue(); }); it('handles special characters in message', function () { $formatter = new DevelopmentFormatter(colorOutput: false); $record = new LogRecord( message: 'Message with "quotes" and \'apostrophes\' and \backslashes', context: LogContext::empty(), level: LogLevel::INFO, timestamp: $this->timestamp ); $result = $formatter($record); expect(str_contains($result, 'quotes'))->toBeTrue(); }); }); });