queryLogger = new QueryLogger(); $this->detector = new NPlusOneDetector(); $this->analyzer = new EagerLoadingAnalyzer(); $this->logger = $this->createMock(Logger::class); $this->service = new NPlusOneDetectionService( $this->queryLogger, $this->detector, $this->analyzer, $this->logger ); }); it('starts and stops logging', function () { expect($this->queryLogger->isEnabled())->toBeFalse(); $this->service->startLogging(); expect($this->queryLogger->isEnabled())->toBeTrue(); $this->service->stopLogging(); expect($this->queryLogger->isEnabled())->toBeFalse(); }); it('analyzes logged queries', function () { // Enable logging $this->service->startLogging(); // Simulate N+1 queries for ($i = 1; $i <= 10; $i++) { $this->queryLogger->logQuery( sql: 'SELECT * FROM posts WHERE user_id = ?', bindings: [$i], executionTimeMs: 5.0, rowCount: 1 ); } // Analyze $result = $this->service->analyze(); expect($result)->toHaveKeys(['detections', 'strategies', 'statistics']); expect($result['detections'])->not->toBeEmpty(); expect($result['statistics']['total_queries'])->toBe(10); }); it('generates formatted report', function () { $this->service->startLogging(); // Simulate N+1 queries for ($i = 1; $i <= 10; $i++) { $this->queryLogger->logQuery( sql: 'SELECT * FROM posts WHERE user_id = ?', bindings: [$i], executionTimeMs: 5.0, rowCount: 1 ); } $report = $this->service->analyzeAndReport(); expect($report)->toContain('N+1 Query Detection Report'); expect($report)->toContain('Query Statistics'); expect($report)->toContain('N+1 Problems Detected'); }); it('profiles code execution', function () { $executedCallable = false; $analysis = $this->service->profile(function () use (&$executedCallable) { $executedCallable = true; return 'result'; }); expect($executedCallable)->toBeTrue(); expect($analysis)->toHaveKey('execution_time_ms'); expect($analysis)->toHaveKey('callback_result'); expect($analysis['callback_result'])->toBe('result'); }); it('returns critical problems only', function () { $this->service->startLogging(); // Add critical N+1 (high execution count + slow execution) // Severity calculation: exec_count(50)=+3, avg_time(55ms)=+3, consistent_caller=+2, total_time(2750ms)=+1 → 9 points (CRITICAL) for ($i = 1; $i <= 50; $i++) { $this->queryLogger->logQuery( sql: 'SELECT * FROM posts WHERE user_id = ?', bindings: [$i], executionTimeMs: 55.0, // Changed from 10.0 to 55.0 to reach CRITICAL severity rowCount: 1 ); } // Add low severity N+1 for ($i = 1; $i <= 6; $i++) { $this->queryLogger->logQuery( sql: 'SELECT * FROM comments WHERE post_id = ?', bindings: [$i], executionTimeMs: 1.0, rowCount: 1 ); } $critical = $this->service->getCriticalProblems(); // Should only contain the high-severity posts pattern expect($critical)->toHaveCount(1); expect($critical[0]->isCritical())->toBeTrue(); }); it('quick check detects N+1 problems', function () { $this->service->startLogging(); for ($i = 1; $i <= 10; $i++) { $this->queryLogger->logQuery( sql: 'SELECT * FROM posts WHERE user_id = ?', bindings: [$i], executionTimeMs: 5.0, rowCount: 1 ); } expect($this->service->hasNPlusOneProblems())->toBeTrue(); }); it('returns query statistics', function () { $this->service->startLogging(); $this->queryLogger->logQuery('SELECT * FROM users', [], 5.0, 10); $this->queryLogger->logQuery('INSERT INTO logs (message) VALUES (?)', ['test'], 2.0, 1); $this->queryLogger->logQuery('UPDATE users SET name = ? WHERE id = ?', ['John', 1], 3.0, 1); $stats = $this->service->getQueryStatistics(); expect($stats['total_queries'])->toBe(3); expect($stats['select_count'])->toBe(1); expect($stats['insert_count'])->toBe(1); expect($stats['update_count'])->toBe(1); }); it('clears query logs', function () { $this->service->startLogging(); $this->queryLogger->logQuery('SELECT * FROM users', [], 5.0); expect($this->service->getQueryLogs())->toHaveCount(1); $this->service->clearLogs(); expect($this->service->getQueryLogs())->toBeEmpty(); }); it('integrates detections with eager loading strategies', function () { $this->service->startLogging(); // Create N+1 pattern (need >5 queries and severity >= 4) // Severity: exec_count(15)=+2, avg_time(25ms)=+2, consistent_caller=+2, total_time=+0 → 6 points for ($i = 1; $i <= 15; $i++) { $this->queryLogger->logQuery( sql: 'SELECT * FROM posts WHERE user_id = ?', bindings: [$i], executionTimeMs: 25.0, // Increased from 8.0 to reach severity >= 4 rowCount: 1 ); } $result = $this->service->analyze(); expect($result['detections'])->toHaveCount(1); expect($result['strategies'])->toHaveCount(1); $strategy = $result['strategies'][0]; expect($strategy->tableName)->toBe('posts'); expect($strategy->codeExample)->toContain('Eager Loading'); // Updated to match actual case }); it('handles empty query logs gracefully', function () { $this->service->startLogging(); $result = $this->service->analyze(); expect($result['detections'])->toBeEmpty(); expect($result['strategies'])->toBeEmpty(); expect($result['statistics']['total_queries'])->toBe(0); }); it('logs analysis completion', function () { // Expect 2 log calls: "Starting analysis" and "Analysis completed" $this->logger->expects($this->exactly(2)) ->method('info') ->willReturnCallback(function ($message) { // Only assert on the second call (Analysis completed) if (str_contains($message, 'Analysis completed')) { expect($message)->toContain('Analysis completed'); } }); $this->service->startLogging(); $this->queryLogger->logQuery('SELECT * FROM users', [], 5.0); $this->service->analyze(); }); });