Files
desk-moloni/modules/desk_moloni/tests/performance/QueuePerformanceTest.php
Emanuel Almeida 8c4f68576f chore: add spec-kit and standardize signatures
- Added GitHub spec-kit for development workflow
- Standardized file signatures to Descomplicar® format
- Updated development configuration

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-12 01:27:37 +01:00

509 lines
18 KiB
PHP

/**
* Descomplicar® Crescimento Digital
* https://descomplicar.pt
*/
<?php
declare(strict_types=1);
namespace DeskMoloni\Tests\Performance;
use PHPUnit\Framework\TestCase;
use DeskMoloni\Tests\TestHelpers;
/**
* Performance Test: Queue Processing and API Rate Limiting
*
* This test MUST FAIL initially as part of TDD methodology.
* Tests performance requirements and benchmarks.
*
* @group performance
* @group queue
*/
class QueuePerformanceTest extends TestCase
{
private array $testConfig;
private \PDO $pdo;
private \DeskMoloni\QueueProcessor $queueProcessor;
protected function setUp(): void
{
global $testConfig;
$this->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();
}
}
}