testDir = sys_get_temp_dir() . '/rotating_handler_test_' . uniqid(); mkdir($this->testDir, 0777, true); $this->testLogFile = $this->testDir . '/test.log'; }); afterEach(function () { // Clean up test files $files = glob($this->testDir . '/*'); if ($files) { foreach ($files as $file) { if (is_file($file)) { unlink($file); } } } if (is_dir($this->testDir)) { rmdir($this->testDir); } }); describe('withSizeRotation()', function () { it('creates handler with size-based rotation', function () { $handler = RotatingFileHandler::withSizeRotation( $this->testLogFile, maxFileSize: Byte::fromKilobytes(1), maxFiles: 3 ); expect($handler)->toBeInstanceOf(RotatingFileHandler::class); // Write small log $record = new LogRecord( message: 'Test message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable() ); $handler->handle($record); expect(file_exists($this->testLogFile))->toBeTrue(); }); it('rotates log when size limit exceeded', function () { $handler = RotatingFileHandler::withSizeRotation( $this->testLogFile, maxFileSize: Byte::fromBytes(100), // Very small size maxFiles: 3 ); // Write multiple large messages to exceed size for ($i = 0; $i < 10; $i++) { $record = new LogRecord( message: str_repeat('X', 50), // 50 bytes per message context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable() ); $handler->handle($record); } // Check that rotation happened (should have .1 file) expect(file_exists($this->testLogFile . '.1'))->toBeTrue(); }); }); describe('daily()', function () { it('creates handler with daily rotation strategy', function () { $handler = RotatingFileHandler::daily($this->testLogFile); expect($handler)->toBeInstanceOf(RotatingFileHandler::class); $strategy = $handler->getRotationStrategy(); expect($strategy['time_based'])->toBe('daily'); expect($strategy['size_based'])->toBeTrue(); }); it('rotates log when file is from previous day', function () { // Create old log file file_put_contents($this->testLogFile, "Old log content\n"); // Set file modification time to yesterday touch($this->testLogFile, strtotime('yesterday')); $handler = RotatingFileHandler::daily($this->testLogFile); // Write new log $record = new LogRecord( message: 'New log entry', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable() ); $handler->handle($record); // File should have been rotated expect(file_exists($this->testLogFile . '.1'))->toBeTrue(); // New file should contain only new entry $content = file_get_contents($this->testLogFile); expect($content)->toContain('New log entry'); expect($content)->not->toContain('Old log content'); }); it('does not rotate log when file is from today', function () { // Create log file file_put_contents($this->testLogFile, "Today's log\n"); $handler = RotatingFileHandler::daily($this->testLogFile); // Write new log $record = new LogRecord( message: 'Another entry', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable() ); $handler->handle($record); // No rotation should have occurred expect(file_exists($this->testLogFile . '.1'))->toBeFalse(); // File should contain both entries $content = file_get_contents($this->testLogFile); expect($content)->toContain("Today's log"); expect($content)->toContain('Another entry'); }); }); describe('weekly()', function () { it('creates handler with weekly rotation strategy', function () { $handler = RotatingFileHandler::weekly($this->testLogFile); $strategy = $handler->getRotationStrategy(); expect($strategy['time_based'])->toBe('weekly'); }); it('rotates log when file is from previous week', function () { // Create old log file file_put_contents($this->testLogFile, "Old week log\n"); // Set file modification time to last week touch($this->testLogFile, strtotime('last week')); $handler = RotatingFileHandler::weekly($this->testLogFile); // Write new log $record = new LogRecord( message: 'New week entry', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable() ); $handler->handle($record); // File should have been rotated expect(file_exists($this->testLogFile . '.1'))->toBeTrue(); }); }); describe('monthly()', function () { it('creates handler with monthly rotation strategy', function () { $handler = RotatingFileHandler::monthly($this->testLogFile); $strategy = $handler->getRotationStrategy(); expect($strategy['time_based'])->toBe('monthly'); }); it('rotates log when file is from previous month', function () { // Create old log file file_put_contents($this->testLogFile, "Old month log\n"); // Set file modification time to last month touch($this->testLogFile, strtotime('last month')); $handler = RotatingFileHandler::monthly($this->testLogFile); // Write new log $record = new LogRecord( message: 'New month entry', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable() ); $handler->handle($record); // File should have been rotated expect(file_exists($this->testLogFile . '.1'))->toBeTrue(); }); }); describe('production()', function () { it('creates handler with production-optimized settings', function () { $handler = RotatingFileHandler::production($this->testLogFile); expect($handler)->toBeInstanceOf(RotatingFileHandler::class); $strategy = $handler->getRotationStrategy(); expect($strategy['time_based'])->toBe('daily'); expect($strategy['size_based'])->toBeTrue(); }); it('uses INFO as default level for production', function () { $handler = RotatingFileHandler::production($this->testLogFile); // DEBUG should not be handled $debugRecord = new LogRecord( message: 'Debug message', context: LogContext::empty(), level: LogLevel::DEBUG, timestamp: new DateTimeImmutable() ); expect($handler->isHandling($debugRecord))->toBeFalse(); // INFO should be handled $infoRecord = new LogRecord( message: 'Info message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable() ); expect($handler->isHandling($infoRecord))->toBeTrue(); }); }); describe('withMaxSize()', function () { it('allows configuring max size after creation', function () { $handler = RotatingFileHandler::daily($this->testLogFile); $handler->withMaxSize(Byte::fromBytes(50), maxFiles: 5); expect($handler)->toBeInstanceOf(RotatingFileHandler::class); }); }); describe('getRotationStrategy()', function () { it('returns rotation strategy info', function () { $handler = RotatingFileHandler::daily($this->testLogFile); $strategy = $handler->getRotationStrategy(); expect($strategy)->toHaveKeys(['time_based', 'size_based', 'last_check']); expect($strategy['time_based'])->toBe('daily'); expect($strategy['size_based'])->toBeTrue(); }); it('returns none for size-only rotation', function () { $handler = RotatingFileHandler::withSizeRotation($this->testLogFile); $strategy = $handler->getRotationStrategy(); expect($strategy['time_based'])->toBe('none'); }); }); describe('caching behavior', function () { it('caches rotation checks for performance', function () { $handler = RotatingFileHandler::daily($this->testLogFile); // First write $record1 = new LogRecord( message: 'First message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable() ); $handler->handle($record1); $strategy1 = $handler->getRotationStrategy(); $lastCheck1 = $strategy1['last_check']; // Immediate second write (within cache window) $record2 = new LogRecord( message: 'Second message', context: LogContext::empty(), level: LogLevel::INFO, timestamp: new DateTimeImmutable() ); $handler->handle($record2); $strategy2 = $handler->getRotationStrategy(); $lastCheck2 = $strategy2['last_check']; // Last check should be same (cached) expect($lastCheck1)->toBe($lastCheck2); }); }); });