toContain('app'); expect($formatted)->toContain('INFO'); expect($formatted)->toContain('Test message'); }); it('includes timestamp in formatted output', function () { $formatter = new LineFormatter(); $record = new LogRecord( level: LogLevel::INFO, message: 'Test message', channel: new LogChannel('app') ); $formatted = $formatter($record); // Check for timestamp pattern (YYYY-MM-DD HH:MM:SS) expect($formatted)->toMatch('/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/'); }); it('includes channel in formatted output', function () { $formatter = new LineFormatter(); $record = new LogRecord( level: LogLevel::INFO, message: 'Test message', channel: new LogChannel('security') ); $formatted = $formatter($record); expect($formatted)->toContain('security'); }); it('includes level name in formatted output', function () { $formatter = new LineFormatter(); $record = new LogRecord( level: LogLevel::ERROR, message: 'Error message', channel: new LogChannel('app') ); $formatted = $formatter($record); expect($formatted)->toContain('ERROR'); }); it('includes message in formatted output', function () { $formatter = new LineFormatter(); $record = new LogRecord( level: LogLevel::INFO, message: 'Important information', channel: new LogChannel('app') ); $formatted = $formatter($record); expect($formatted)->toContain('Important information'); }); it('defaults to "app" channel when no channel specified', function () { $formatter = new LineFormatter(); $record = new LogRecord( level: LogLevel::INFO, message: 'Test message', channel: null ); $formatted = $formatter($record); expect($formatted)->toContain('app'); }); }); describe('custom format', function () { it('uses custom format string', function () { $formatter = new LineFormatter(format: '{level}: {message}'); $record = new LogRecord( level: LogLevel::WARNING, message: 'Custom format', channel: new LogChannel('app') ); $formatted = $formatter($record); expect($formatted)->toBe('WARNING: Custom format'); }); it('allows custom timestamp format', function () { $formatter = new LineFormatter(timestampFormat: 'Y-m-d'); $record = new LogRecord( level: LogLevel::INFO, message: 'Test message', channel: new LogChannel('app') ); $formatted = $formatter($record); // Should match YYYY-MM-DD format without time expect($formatted)->toMatch('/\d{4}-\d{2}-\d{2}/'); expect($formatted)->not->toMatch('/\d{2}:\d{2}:\d{2}/'); }); it('supports minimal format', function () { $formatter = new LineFormatter(format: '{message}'); $record = new LogRecord( level: LogLevel::INFO, message: 'Just the message', channel: new LogChannel('app') ); $formatted = $formatter($record); expect($formatted)->toBe('Just the message'); }); }); describe('context handling', function () { it('includes context data as JSON when present', function () { $formatter = new LineFormatter(); $record = new LogRecord( level: LogLevel::INFO, message: 'User action', channel: new LogChannel('app'), context: ['user_id' => 123, 'action' => 'login'] ); $formatted = $formatter($record); expect($formatted)->toContain('user_id'); expect($formatted)->toContain('123'); expect($formatted)->toContain('action'); expect($formatted)->toContain('login'); }); it('includes extra data as JSON when present', function () { $formatter = new LineFormatter(); $record = new LogRecord( level: LogLevel::INFO, message: 'Test message', channel: new LogChannel('app'), extra: ['request_id' => 'req-123'] ); $formatted = $formatter($record); expect($formatted)->toContain('request_id'); expect($formatted)->toContain('req-123'); }); it('merges context and extra data in JSON', function () { $formatter = new LineFormatter(); $record = new LogRecord( level: LogLevel::INFO, message: 'Test message', channel: new LogChannel('app'), context: ['key1' => 'value1'], extra: ['key2' => 'value2'] ); $formatted = $formatter($record); expect($formatted)->toContain('key1'); expect($formatted)->toContain('value1'); expect($formatted)->toContain('key2'); expect($formatted)->toContain('value2'); }); it('shows empty string when no context or extra data', function () { $formatter = new LineFormatter(format: '{message} {context}'); $record = new LogRecord( level: LogLevel::INFO, message: 'No context', channel: new LogChannel('app') ); $formatted = $formatter($record); expect($formatted)->toBe('No context '); }); }); describe('different log levels', function () { it('formats DEBUG level correctly', function () { $formatter = new LineFormatter(); $record = new LogRecord( level: LogLevel::DEBUG, message: 'Debug info', channel: new LogChannel('app') ); $formatted = $formatter($record); expect($formatted)->toContain('DEBUG'); }); it('formats NOTICE level correctly', function () { $formatter = new LineFormatter(); $record = new LogRecord( level: LogLevel::NOTICE, message: 'Notice', channel: new LogChannel('app') ); $formatted = $formatter($record); expect($formatted)->toContain('NOTICE'); }); it('formats CRITICAL level correctly', function () { $formatter = new LineFormatter(); $record = new LogRecord( level: LogLevel::CRITICAL, message: 'Critical error', channel: new LogChannel('app') ); $formatted = $formatter($record); expect($formatted)->toContain('CRITICAL'); }); it('formats EMERGENCY level correctly', function () { $formatter = new LineFormatter(); $record = new LogRecord( level: LogLevel::EMERGENCY, message: 'Emergency', channel: new LogChannel('app') ); $formatted = $formatter($record); expect($formatted)->toContain('EMERGENCY'); }); }); describe('special characters', function () { it('handles unicode characters in message', function () { $formatter = new LineFormatter(); $record = new LogRecord( level: LogLevel::INFO, message: 'Übung mit Ümläüten und 日本語', channel: new LogChannel('app') ); $formatted = $formatter($record); expect($formatted)->toContain('Übung mit Ümläüten und 日本語'); }); it('handles newlines in message', function () { $formatter = new LineFormatter(); $record = new LogRecord( level: LogLevel::INFO, message: "Line 1\nLine 2", channel: new LogChannel('app') ); $formatted = $formatter($record); expect($formatted)->toContain("Line 1\nLine 2"); }); it('handles quotes in message', function () { $formatter = new LineFormatter(); $record = new LogRecord( level: LogLevel::INFO, message: 'Message with "quotes" and \'apostrophes\'', channel: new LogChannel('app') ); $formatted = $formatter($record); expect($formatted)->toContain('Message with "quotes" and \'apostrophes\''); }); }); describe('complex scenarios', function () { it('formats record with all fields', function () { $formatter = new LineFormatter(); $record = new LogRecord( level: LogLevel::ERROR, message: 'Database error', channel: new LogChannel('database'), context: ['query' => 'SELECT * FROM users'], extra: ['request_id' => 'req-456'] ); $formatted = $formatter($record); expect($formatted)->toContain('ERROR'); expect($formatted)->toContain('Database error'); expect($formatted)->toContain('database'); expect($formatted)->toContain('query'); expect($formatted)->toContain('request_id'); }); it('is invokable', function () { $formatter = new LineFormatter(); $record = new LogRecord( level: LogLevel::INFO, message: 'Invokable test', channel: new LogChannel('app') ); // Test that formatter is invokable $formatted = $formatter($record); expect($formatted)->toBeString(); expect($formatted)->toContain('Invokable test'); }); }); });