/** * Descomplicar® Crescimento Digital * https://descomplicar.pt */ testConfig = $testConfig; $this->pdo = new \PDO( "mysql:host={$testConfig['database']['hostname']};dbname={$testConfig['database']['database']}", $testConfig['database']['username'], $testConfig['database']['password'], [\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION] ); // This will fail initially until QueueProcessor is implemented $this->queueProcessor = new \DeskMoloni\QueueProcessor($testConfig); // Clean test data TestHelpers::clearTestData(); } /** * Test queue processing performance requirements * Requirement: Process 50 tasks in under 30 seconds */ public function testQueueProcessingPerformance(): void { $taskCount = 50; $maxExecutionTime = 30; // seconds // Create test tasks $tasks = $this->createTestTasks($taskCount); $this->insertTasksIntoQueue($tasks); // Measure processing time $startTime = microtime(true); $result = $this->queueProcessor->processBatch($taskCount); $endTime = microtime(true); $executionTime = $endTime - $startTime; // Performance assertions $this->assertLessThan($maxExecutionTime, $executionTime, "Queue should process {$taskCount} tasks in under {$maxExecutionTime} seconds"); $this->assertIsArray($result); $this->assertArrayHasKey('processed_count', $result); $this->assertArrayHasKey('successful_count', $result); $this->assertArrayHasKey('failed_count', $result); $this->assertArrayHasKey('average_task_time', $result); $this->assertEquals($taskCount, $result['processed_count']); $this->assertGreaterThan(0, $result['successful_count']); $this->assertLessThan(1000, $result['average_task_time'], 'Average task time should be under 1 second'); // Verify tasks were processed $stmt = $this->pdo->query("SELECT COUNT(*) as count FROM tbl_desk_moloni_sync_queue WHERE status = 'completed'"); $completedCount = $stmt->fetch(); $this->assertGreaterThan(0, $completedCount['count'], 'Some tasks should be completed'); } /** * Test concurrent queue processing */ public function testConcurrentQueueProcessing(): void { $taskCount = 100; $workerCount = 4; // Create test tasks $tasks = $this->createTestTasks($taskCount); $this->insertTasksIntoQueue($tasks); $startTime = microtime(true); // Simulate concurrent workers $workers = []; for ($i = 0; $i < $workerCount; $i++) { $workers[$i] = $this->queueProcessor->createWorker("worker_{$i}"); } // Process tasks concurrently (simulated) $results = []; foreach ($workers as $workerId => $worker) { $results[$workerId] = $worker->processBatch($taskCount / $workerCount); } $endTime = microtime(true); $executionTime = $endTime - $startTime; // Should be faster than sequential processing $this->assertLessThan(20, $executionTime, 'Concurrent processing should be faster'); // Verify no task conflicts $stmt = $this->pdo->query("SELECT COUNT(*) as count FROM tbl_desk_moloni_sync_queue WHERE status = 'processing'"); $processingCount = $stmt->fetch(); $this->assertEquals(0, $processingCount['count'], 'No tasks should be stuck in processing state'); // Verify all tasks processed exactly once $stmt = $this->pdo->query("SELECT COUNT(*) as count FROM tbl_desk_moloni_sync_queue WHERE status IN ('completed', 'failed')"); $processedCount = $stmt->fetch(); $this->assertEquals($taskCount, $processedCount['count'], 'All tasks should be processed exactly once'); } /** * Test API rate limiting performance * Requirement: Respect Moloni rate limits without excessive delays */ public function testApiRateLimitingPerformance(): void { $rateLimiter = new \DeskMoloni\ApiRateLimiter($this->testConfig); $requestCount = 50; $maxTotalTime = 60; // seconds - should not take too long due to rate limiting $startTime = microtime(true); $successfulRequests = 0; $rateLimitedRequests = 0; for ($i = 0; $i < $requestCount; $i++) { $requestStart = microtime(true); $allowed = $rateLimiter->allowRequest('test_endpoint'); if ($allowed) { $successfulRequests++; // Simulate API call usleep(100000); // 100ms } else { $rateLimitedRequests++; // Test wait time calculation $waitTime = $rateLimiter->getWaitTime('test_endpoint'); $this->assertIsFloat($waitTime); $this->assertGreaterThanOrEqual(0, $waitTime); $this->assertLessThan(60, $waitTime, 'Wait time should not exceed 60 seconds'); } $requestTime = microtime(true) - $requestStart; $this->assertLessThan(5, $requestTime, 'Individual request processing should be under 5 seconds'); } $endTime = microtime(true); $totalTime = $endTime - $startTime; $this->assertLessThan($maxTotalTime, $totalTime, "Rate limited requests should complete in under {$maxTotalTime} seconds"); $this->assertGreaterThan(0, $successfulRequests, 'Some requests should be allowed'); // Verify rate limiting data $stmt = $this->pdo->prepare("SELECT * FROM tbl_desk_moloni_rate_limits WHERE api_endpoint = ?"); $stmt->execute(['test_endpoint']); $rateLimitData = $stmt->fetch(); $this->assertNotFalse($rateLimitData, 'Rate limit data should be recorded'); $this->assertGreaterThan(0, $rateLimitData['calls_made']); $this->assertLessThanOrEqual($rateLimitData['limit_per_window'], $rateLimitData['calls_made']); } /** * Test memory usage during bulk operations */ public function testMemoryUsageDuringBulkOperations(): void { $initialMemory = memory_get_usage(true); $maxAllowedMemory = 128 * 1024 * 1024; // 128MB // Create large batch of tasks $largeBatchSize = 1000; $tasks = $this->createTestTasks($largeBatchSize); $memoryAfterCreation = memory_get_usage(true); $creationMemoryIncrease = $memoryAfterCreation - $initialMemory; $this->assertLessThan($maxAllowedMemory / 4, $creationMemoryIncrease, 'Task creation should not use excessive memory'); // Insert tasks $this->insertTasksIntoQueue($tasks); $memoryAfterInsert = memory_get_usage(true); $insertMemoryIncrease = $memoryAfterInsert - $memoryAfterCreation; $this->assertLessThan($maxAllowedMemory / 4, $insertMemoryIncrease, 'Task insertion should not use excessive memory'); // Process tasks in chunks to test memory management $chunkSize = 100; $chunksProcessed = 0; while ($chunksProcessed * $chunkSize < $largeBatchSize) { $chunkStartMemory = memory_get_usage(true); $result = $this->queueProcessor->processBatch($chunkSize); $chunkEndMemory = memory_get_usage(true); $chunkMemoryIncrease = $chunkEndMemory - $chunkStartMemory; $this->assertLessThan($maxAllowedMemory / 8, $chunkMemoryIncrease, "Chunk processing should not leak memory (chunk {$chunksProcessed})"); $chunksProcessed++; // Force garbage collection gc_collect_cycles(); } $finalMemory = memory_get_usage(true); $totalMemoryIncrease = $finalMemory - $initialMemory; $this->assertLessThan($maxAllowedMemory, $totalMemoryIncrease, 'Total memory usage should stay within limits'); } /** * Test database query performance */ public function testDatabaseQueryPerformance(): void { // Create test data for performance testing $testDataCount = 10000; $this->createLargeTestDataset($testDataCount); // Test queue selection performance $startTime = microtime(true); $stmt = $this->pdo->query(" SELECT * FROM tbl_desk_moloni_sync_queue WHERE status = 'pending' ORDER BY priority ASC, scheduled_at ASC LIMIT 100 "); $tasks = $stmt->fetchAll(); $queryTime = microtime(true) - $startTime; $this->assertLessThan(0.5, $queryTime, 'Queue selection query should complete in under 500ms'); $this->assertLessThanOrEqual(100, count($tasks)); // Test mapping lookup performance $startTime = microtime(true); $stmt = $this->pdo->query(" SELECT m.*, l.execution_time_ms FROM tbl_desk_moloni_mapping m LEFT JOIN tbl_desk_moloni_sync_log l ON l.perfex_id = m.perfex_id AND l.entity_type = m.entity_type WHERE m.entity_type = 'client' ORDER BY m.last_sync_at DESC LIMIT 100 "); $mappings = $stmt->fetchAll(); $queryTime = microtime(true) - $startTime; $this->assertLessThan(0.3, $queryTime, 'Mapping lookup query should complete in under 300ms'); // Test log aggregation performance $startTime = microtime(true); $stmt = $this->pdo->query(" SELECT entity_type, status, COUNT(*) as count, AVG(execution_time_ms) as avg_time, MAX(created_at) as latest FROM tbl_desk_moloni_sync_log WHERE created_at > DATE_SUB(NOW(), INTERVAL 24 HOUR) GROUP BY entity_type, status "); $stats = $stmt->fetchAll(); $queryTime = microtime(true) - $startTime; $this->assertLessThan(1.0, $queryTime, 'Log aggregation query should complete in under 1 second'); $this->assertIsArray($stats); } /** * Test Redis cache performance */ public function testRedisCachePerformance(): void { if (!isset($GLOBALS['test_redis'])) { $this->markTestSkipped('Redis not available for testing'); } $redis = $GLOBALS['test_redis']; $cacheManager = new \DeskMoloni\CacheManager($redis); $testDataSize = 1000; $testData = []; // Generate test data for ($i = 0; $i < $testDataSize; $i++) { $testData["test_key_{$i}"] = [ 'id' => $i, 'name' => "Test Item {$i}", 'data' => str_repeat('x', 100) // 100 byte payload ]; } // Test write performance $startTime = microtime(true); foreach ($testData as $key => $data) { $cacheManager->set($key, $data, 300); // 5 minute TTL } $writeTime = microtime(true) - $startTime; $writeOpsPerSecond = $testDataSize / $writeTime; $this->assertGreaterThan(500, $writeOpsPerSecond, 'Cache should handle at least 500 writes per second'); // Test read performance $startTime = microtime(true); foreach (array_keys($testData) as $key) { $cached = $cacheManager->get($key); $this->assertNotNull($cached, "Cached data should exist for key {$key}"); } $readTime = microtime(true) - $startTime; $readOpsPerSecond = $testDataSize / $readTime; $this->assertGreaterThan(1000, $readOpsPerSecond, 'Cache should handle at least 1000 reads per second'); // Test batch operations $batchKeys = array_slice(array_keys($testData), 0, 100); $startTime = microtime(true); $batchResult = $cacheManager->multiGet($batchKeys); $batchTime = microtime(true) - $startTime; $this->assertLessThan(0.1, $batchTime, 'Batch get should complete in under 100ms'); $this->assertCount(100, $batchResult); } /** * Test sync operation performance benchmarks */ public function testSyncOperationPerformanceBenchmarks(): void { $syncService = new \DeskMoloni\ClientSyncService(); // Benchmark single client sync $testClient = TestHelpers::createTestClient([ 'userid' => 99999, 'company' => 'Performance Test Company', 'vat' => '999999999' ]); $syncTimes = []; $iterations = 10; for ($i = 0; $i < $iterations; $i++) { $startTime = microtime(true); $result = $syncService->syncPerfexToMoloni($testClient); $syncTime = microtime(true) - $startTime; $syncTimes[] = $syncTime; // Clean up for next iteration if ($result['success'] ?? false) { $this->pdo->exec("DELETE FROM tbl_desk_moloni_mapping WHERE perfex_id = 99999"); $this->pdo->exec("DELETE FROM tbl_desk_moloni_sync_log WHERE perfex_id = 99999"); } } $avgSyncTime = array_sum($syncTimes) / count($syncTimes); $maxSyncTime = max($syncTimes); $minSyncTime = min($syncTimes); $this->assertLessThan(5.0, $avgSyncTime, 'Average sync time should be under 5 seconds'); $this->assertLessThan(10.0, $maxSyncTime, 'Maximum sync time should be under 10 seconds'); $this->assertGreaterThan(0.1, $minSyncTime, 'Minimum sync time should be realistic (over 100ms)'); // Calculate performance metrics $standardDeviation = sqrt(array_sum(array_map(function($x) use ($avgSyncTime) { return pow($x - $avgSyncTime, 2); }, $syncTimes)) / count($syncTimes)); $this->assertLessThan($avgSyncTime * 0.5, $standardDeviation, 'Sync times should be consistent (low standard deviation)'); } private function createTestTasks(int $count): array { $tasks = []; for ($i = 1; $i <= $count; $i++) { $tasks[] = [ 'task_type' => 'sync_client', 'entity_type' => 'client', 'entity_id' => 1000 + $i, 'priority' => rand(1, 9), 'payload' => json_encode([ 'client_data' => [ 'id' => 1000 + $i, 'name' => "Test Client {$i}", 'email' => "test{$i}@example.com" ] ]), 'scheduled_at' => date('Y-m-d H:i:s') ]; } return $tasks; } private function insertTasksIntoQueue(array $tasks): void { $stmt = $this->pdo->prepare(" INSERT INTO tbl_desk_moloni_sync_queue (task_type, entity_type, entity_id, priority, payload, scheduled_at) VALUES (?, ?, ?, ?, ?, ?) "); foreach ($tasks as $task) { $stmt->execute([ $task['task_type'], $task['entity_type'], $task['entity_id'], $task['priority'], $task['payload'], $task['scheduled_at'] ]); } } private function createLargeTestDataset(int $count): void { // Create test mappings $stmt = $this->pdo->prepare(" INSERT INTO tbl_desk_moloni_mapping (entity_type, perfex_id, moloni_id, sync_direction, last_sync_at) VALUES (?, ?, ?, ?, ?) "); for ($i = 1; $i <= $count / 4; $i++) { $stmt->execute([ 'client', 10000 + $i, 20000 + $i, 'bidirectional', date('Y-m-d H:i:s', strtotime("-{$i} minutes")) ]); } // Create test logs $stmt = $this->pdo->prepare(" INSERT INTO tbl_desk_moloni_sync_log (operation_type, entity_type, perfex_id, moloni_id, direction, status, execution_time_ms, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?) "); for ($i = 1; $i <= $count; $i++) { $stmt->execute([ 'create', 'client', 10000 + ($i % ($count / 4)), 20000 + ($i % ($count / 4)), 'perfex_to_moloni', rand(0, 10) < 9 ? 'success' : 'error', // 90% success rate rand(100, 5000), // Random execution time date('Y-m-d H:i:s', strtotime("-{$i} seconds")) ]); } } protected function tearDown(): void { // Clean up performance test data $this->pdo->exec("DELETE FROM tbl_desk_moloni_sync_queue WHERE entity_id >= 1000"); $this->pdo->exec("DELETE FROM tbl_desk_moloni_mapping WHERE perfex_id >= 10000"); $this->pdo->exec("DELETE FROM tbl_desk_moloni_sync_log WHERE perfex_id >= 10000"); $this->pdo->exec("DELETE FROM tbl_desk_moloni_rate_limits WHERE api_endpoint = 'test_endpoint'"); if (isset($GLOBALS['test_redis'])) { $GLOBALS['test_redis']->flushdb(); } } }