feat(Production): Complete production deployment infrastructure

- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
This commit is contained in:
2025-10-25 19:18:37 +02:00
parent caa85db796
commit fc3d7e6357
83016 changed files with 378904 additions and 20919 deletions

View File

@@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\FileCache;
use App\Framework\Cache\Warming\CacheWarmingService;
use App\Framework\Cache\Warming\Strategies\CriticalPathWarmingStrategy;
use App\Framework\Cache\Warming\ScheduledWarmupJob;
use App\Framework\Core\CompiledRoutes;
use App\Framework\Config\Environment;
use App\Framework\Logging\Logger;
use App\Framework\Core\ValueObjects\Duration;
describe('Cache Warming Integration', function () {
beforeEach(function () {
// Use real FileCache for integration test
$this->cacheDir = sys_get_temp_dir() . '/cache_warming_test_' . uniqid();
mkdir($this->cacheDir, 0777, true);
$this->cache = new FileCache($this->cacheDir);
$this->logger = Mockery::mock(Logger::class);
$this->logger->shouldReceive('info')->andReturnNull();
$this->logger->shouldReceive('debug')->andReturnNull();
$this->logger->shouldReceive('error')->andReturnNull();
$this->compiledRoutes = Mockery::mock(CompiledRoutes::class);
$this->compiledRoutes->shouldReceive('getStaticRoutes')->andReturn([
'/home' => 'HomeController',
'/about' => 'AboutController',
]);
$this->compiledRoutes->shouldReceive('getDynamicRoutes')->andReturn([
'/users/{id}' => 'UserController',
]);
$this->environment = Mockery::mock(Environment::class);
});
afterEach(function () {
// Cleanup test cache directory
if (is_dir($this->cacheDir)) {
array_map('unlink', glob($this->cacheDir . '/*'));
rmdir($this->cacheDir);
}
Mockery::close();
});
it('warms cache end-to-end', function () {
$strategy = new CriticalPathWarmingStrategy(
cache: $this->cache,
compiledRoutes: $this->compiledRoutes,
environment: $this->environment
);
$service = new CacheWarmingService(
strategies: [$strategy],
logger: $this->logger
);
// Execute warmup
$metrics = $service->warmAll();
expect($metrics->totalStrategiesExecuted)->toBe(1);
expect($metrics->totalItemsWarmed)->toBeGreaterThan(0);
expect($metrics->isSuccess())->toBeTrue();
// Verify cache was populated
$routesKey = CacheKey::fromString('routes_static');
$cachedRoutes = $this->cache->get($routesKey);
expect($cachedRoutes)->not->toBeNull();
expect($cachedRoutes->value)->toBeArray();
});
it('integrates with ScheduledWarmupJob', function () {
$strategy = new CriticalPathWarmingStrategy(
cache: $this->cache,
compiledRoutes: $this->compiledRoutes,
environment: $this->environment
);
$service = new CacheWarmingService(
strategies: [$strategy],
logger: $this->logger
);
$scheduledJob = new ScheduledWarmupJob(
warmingService: $service,
logger: $this->logger
);
// Execute scheduled warmup
$result = $scheduledJob->warmCriticalPaths();
expect($result)->toBeArray();
expect($result['total_strategies_executed'])->toBe(1);
expect($result['total_items_warmed'])->toBeGreaterThan(0);
});
it('handles priority-based warmup', function () {
$strategy = new CriticalPathWarmingStrategy(
cache: $this->cache,
compiledRoutes: $this->compiledRoutes,
environment: $this->environment
);
$service = new CacheWarmingService(
strategies: [$strategy],
logger: $this->logger
);
// Warm only high priority
$metrics = $service->warmByPriority(500); // HIGH priority threshold
expect($metrics->totalStrategiesExecuted)->toBe(1); // Critical > High
expect($metrics->totalItemsWarmed)->toBeGreaterThan(0);
});
it('supports forced warmup', function () {
$strategy = new CriticalPathWarmingStrategy(
cache: $this->cache,
compiledRoutes: $this->compiledRoutes,
environment: $this->environment
);
$service = new CacheWarmingService(
strategies: [$strategy],
logger: $this->logger
);
// First warmup
$service->warmAll();
// Second warmup (forced) should re-warm even if cache exists
$metrics = $service->warmAll(force: true);
expect($metrics->totalStrategiesExecuted)->toBe(1);
expect($metrics->totalItemsWarmed)->toBeGreaterThan(0);
});
it('provides accurate metrics', function () {
$strategy = new CriticalPathWarmingStrategy(
cache: $this->cache,
compiledRoutes: $this->compiledRoutes,
environment: $this->environment
);
$service = new CacheWarmingService(
strategies: [$strategy],
logger: $this->logger
);
$startTime = microtime(true);
$metrics = $service->warmAll();
$actualDuration = microtime(true) - $startTime;
expect($metrics->totalDurationSeconds)->toBeGreaterThan(0);
expect($metrics->totalDurationSeconds)->toBeLessThan($actualDuration + 1); // Allow 1s margin
expect($metrics->totalMemoryUsedBytes)->toBeGreaterThan(0);
expect($metrics->getOverallSuccessRate())->toBeGreaterThan(0.5);
});
it('handles multiple strategies correctly', function () {
$strategy1 = new CriticalPathWarmingStrategy(
cache: $this->cache,
compiledRoutes: $this->compiledRoutes,
environment: $this->environment
);
// Create a second mock strategy
$strategy2 = Mockery::mock(\App\Framework\Cache\Warming\WarmupStrategy::class);
$strategy2->shouldReceive('getName')->andReturn('test_strategy');
$strategy2->shouldReceive('getPriority')->andReturn(100);
$strategy2->shouldReceive('shouldRun')->andReturn(true);
$strategy2->shouldReceive('getEstimatedDuration')->andReturn(1);
$strategy2->shouldReceive('warmup')->andReturn(
new \App\Framework\Cache\Warming\ValueObjects\WarmupResult(
strategyName: 'test_strategy',
itemsWarmed: 5,
itemsFailed: 0,
durationSeconds: 0.5,
memoryUsedBytes: 512
)
);
$service = new CacheWarmingService(
strategies: [$strategy1, $strategy2],
logger: $this->logger
);
$metrics = $service->warmAll();
expect($metrics->totalStrategiesExecuted)->toBe(2);
expect($metrics->strategyResults)->toHaveCount(2);
});
});

View File

@@ -0,0 +1,249 @@
<?php
declare(strict_types=1);
use App\Framework\Cache\Cache;
use App\Framework\Cache\Warming\CacheWarmingService;
use App\Framework\Cache\Warming\WarmupStrategy;
use App\Framework\Cache\Warming\ValueObjects\WarmupPriority;
use App\Framework\Cache\Warming\ValueObjects\WarmupResult;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
describe('CacheWarmingService', function () {
beforeEach(function () {
$this->logger = Mockery::mock(Logger::class);
$this->logger->shouldReceive('info')->andReturnNull();
$this->logger->shouldReceive('debug')->andReturnNull();
$this->logger->shouldReceive('error')->andReturnNull();
});
afterEach(function () {
Mockery::close();
});
it('warms all strategies', function () {
$strategy1 = Mockery::mock(WarmupStrategy::class);
$strategy1->shouldReceive('getName')->andReturn('strategy1');
$strategy1->shouldReceive('getPriority')->andReturn(WarmupPriority::HIGH->value);
$strategy1->shouldReceive('shouldRun')->andReturn(true);
$strategy1->shouldReceive('getEstimatedDuration')->andReturn(1);
$strategy1->shouldReceive('warmup')->andReturn(new WarmupResult(
strategyName: 'strategy1',
itemsWarmed: 10,
itemsFailed: 0,
durationSeconds: 1.0,
memoryUsedBytes: 1024
));
$strategy2 = Mockery::mock(WarmupStrategy::class);
$strategy2->shouldReceive('getName')->andReturn('strategy2');
$strategy2->shouldReceive('getPriority')->andReturn(WarmupPriority::MEDIUM->value);
$strategy2->shouldReceive('shouldRun')->andReturn(true);
$strategy2->shouldReceive('getEstimatedDuration')->andReturn(2);
$strategy2->shouldReceive('warmup')->andReturn(new WarmupResult(
strategyName: 'strategy2',
itemsWarmed: 20,
itemsFailed: 0,
durationSeconds: 2.0,
memoryUsedBytes: 2048
));
$service = new CacheWarmingService(
strategies: [$strategy1, $strategy2],
logger: $this->logger
);
$metrics = $service->warmAll();
expect($metrics->totalStrategiesExecuted)->toBe(2);
expect($metrics->totalItemsWarmed)->toBe(30);
});
it('skips strategies when shouldRun returns false', function () {
$strategy1 = Mockery::mock(WarmupStrategy::class);
$strategy1->shouldReceive('getName')->andReturn('strategy1');
$strategy1->shouldReceive('getPriority')->andReturn(WarmupPriority::HIGH->value);
$strategy1->shouldReceive('shouldRun')->andReturn(false);
$strategy1->shouldNotReceive('warmup');
$service = new CacheWarmingService(
strategies: [$strategy1],
logger: $this->logger
);
$metrics = $service->warmAll(force: false);
expect($metrics->totalStrategiesExecuted)->toBe(0);
});
it('forces execution when force is true', function () {
$strategy1 = Mockery::mock(WarmupStrategy::class);
$strategy1->shouldReceive('getName')->andReturn('strategy1');
$strategy1->shouldReceive('getPriority')->andReturn(WarmupPriority::HIGH->value);
$strategy1->shouldReceive('shouldRun')->andReturn(false);
$strategy1->shouldReceive('getEstimatedDuration')->andReturn(1);
$strategy1->shouldReceive('warmup')->andReturn(new WarmupResult(
strategyName: 'strategy1',
itemsWarmed: 10,
itemsFailed: 0,
durationSeconds: 1.0,
memoryUsedBytes: 1024
));
$service = new CacheWarmingService(
strategies: [$strategy1],
logger: $this->logger
);
$metrics = $service->warmAll(force: true);
expect($metrics->totalStrategiesExecuted)->toBe(1);
expect($metrics->totalItemsWarmed)->toBe(10);
});
it('sorts strategies by priority', function () {
$lowPriority = Mockery::mock(WarmupStrategy::class);
$lowPriority->shouldReceive('getName')->andReturn('low');
$lowPriority->shouldReceive('getPriority')->andReturn(WarmupPriority::LOW->value);
$lowPriority->shouldReceive('shouldRun')->andReturn(true);
$lowPriority->shouldReceive('getEstimatedDuration')->andReturn(1);
$lowPriority->shouldReceive('warmup')->andReturn(new WarmupResult(
strategyName: 'low',
itemsWarmed: 5,
itemsFailed: 0,
durationSeconds: 1.0,
memoryUsedBytes: 512
));
$highPriority = Mockery::mock(WarmupStrategy::class);
$highPriority->shouldReceive('getName')->andReturn('high');
$highPriority->shouldReceive('getPriority')->andReturn(WarmupPriority::HIGH->value);
$highPriority->shouldReceive('shouldRun')->andReturn(true);
$highPriority->shouldReceive('getEstimatedDuration')->andReturn(1);
$highPriority->shouldReceive('warmup')->andReturn(new WarmupResult(
strategyName: 'high',
itemsWarmed: 10,
itemsFailed: 0,
durationSeconds: 1.0,
memoryUsedBytes: 1024
));
// Pass in wrong order - service should sort by priority
$service = new CacheWarmingService(
strategies: [$lowPriority, $highPriority],
logger: $this->logger
);
$strategies = $service->getStrategies();
expect($strategies[0]->getName())->toBe('high');
expect($strategies[1]->getName())->toBe('low');
});
it('warms specific strategy by name', function () {
$strategy1 = Mockery::mock(WarmupStrategy::class);
$strategy1->shouldReceive('getName')->andReturn('strategy1');
$strategy1->shouldReceive('getPriority')->andReturn(WarmupPriority::HIGH->value);
$strategy1->shouldReceive('getEstimatedDuration')->andReturn(1);
$strategy1->shouldReceive('warmup')->andReturn(new WarmupResult(
strategyName: 'strategy1',
itemsWarmed: 10,
itemsFailed: 0,
durationSeconds: 1.0,
memoryUsedBytes: 1024
));
$service = new CacheWarmingService(
strategies: [$strategy1],
logger: $this->logger
);
$result = $service->warmStrategy('strategy1');
expect($result->strategyName)->toBe('strategy1');
expect($result->itemsWarmed)->toBe(10);
});
it('throws when strategy not found', function () {
$service = new CacheWarmingService(
strategies: [],
logger: $this->logger
);
$service->warmStrategy('nonexistent');
})->throws(InvalidArgumentException::class, 'Strategy not found: nonexistent');
it('warms by priority threshold', function () {
$critical = Mockery::mock(WarmupStrategy::class);
$critical->shouldReceive('getName')->andReturn('critical');
$critical->shouldReceive('getPriority')->andReturn(WarmupPriority::CRITICAL->value);
$critical->shouldReceive('shouldRun')->andReturn(true);
$critical->shouldReceive('getEstimatedDuration')->andReturn(1);
$critical->shouldReceive('warmup')->andReturn(new WarmupResult(
strategyName: 'critical',
itemsWarmed: 10,
itemsFailed: 0,
durationSeconds: 1.0,
memoryUsedBytes: 1024
));
$low = Mockery::mock(WarmupStrategy::class);
$low->shouldReceive('getName')->andReturn('low');
$low->shouldReceive('getPriority')->andReturn(WarmupPriority::LOW->value);
$low->shouldNotReceive('warmup');
$service = new CacheWarmingService(
strategies: [$critical, $low],
logger: $this->logger
);
$metrics = $service->warmByPriority(WarmupPriority::HIGH->value);
// Should only warm critical (priority 1000 >= 500)
expect($metrics->totalStrategiesExecuted)->toBe(1);
});
it('calculates estimated total duration', function () {
$strategy1 = Mockery::mock(WarmupStrategy::class);
$strategy1->shouldReceive('getName')->andReturn('strategy1');
$strategy1->shouldReceive('getPriority')->andReturn(WarmupPriority::HIGH->value);
$strategy1->shouldReceive('getEstimatedDuration')->andReturn(5);
$strategy1->shouldReceive('shouldRun')->andReturn(true);
$strategy2 = Mockery::mock(WarmupStrategy::class);
$strategy2->shouldReceive('getName')->andReturn('strategy2');
$strategy2->shouldReceive('getPriority')->andReturn(WarmupPriority::MEDIUM->value);
$strategy2->shouldReceive('getEstimatedDuration')->andReturn(3);
$strategy2->shouldReceive('shouldRun')->andReturn(true);
$service = new CacheWarmingService(
strategies: [$strategy1, $strategy2],
logger: $this->logger
);
$duration = $service->getEstimatedTotalDuration();
expect($duration)->toBe(8);
});
it('handles strategy exceptions gracefully', function () {
$failingStrategy = Mockery::mock(WarmupStrategy::class);
$failingStrategy->shouldReceive('getName')->andReturn('failing');
$failingStrategy->shouldReceive('getPriority')->andReturn(WarmupPriority::HIGH->value);
$failingStrategy->shouldReceive('shouldRun')->andReturn(true);
$failingStrategy->shouldReceive('getEstimatedDuration')->andReturn(1);
$failingStrategy->shouldReceive('warmup')->andThrow(new RuntimeException('Test error'));
$service = new CacheWarmingService(
strategies: [$failingStrategy],
logger: $this->logger
);
$metrics = $service->warmAll();
// Should handle error and return failed result
expect($metrics->totalStrategiesExecuted)->toBe(1);
expect($metrics->totalItemsFailed)->toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
use App\Framework\Cache\Cache;
use App\Framework\Cache\CacheKey;
use App\Framework\Cache\Warming\Strategies\CriticalPathWarmingStrategy;
use App\Framework\Cache\Warming\ValueObjects\WarmupPriority;
use App\Framework\Core\CompiledRoutes;
use App\Framework\Config\Environment;
describe('CriticalPathWarmingStrategy', function () {
beforeEach(function () {
$this->cache = Mockery::mock(Cache::class);
$this->compiledRoutes = Mockery::mock(CompiledRoutes::class);
$this->environment = Mockery::mock(Environment::class);
});
afterEach(function () {
Mockery::close();
});
it('has correct name', function () {
$strategy = new CriticalPathWarmingStrategy(
cache: $this->cache,
compiledRoutes: $this->compiledRoutes,
environment: $this->environment
);
expect($strategy->getName())->toBe('critical_path');
});
it('has critical priority', function () {
$strategy = new CriticalPathWarmingStrategy(
cache: $this->cache,
compiledRoutes: $this->compiledRoutes,
environment: $this->environment
);
expect($strategy->getPriority())->toBe(WarmupPriority::CRITICAL->value);
});
it('always runs', function () {
$strategy = new CriticalPathWarmingStrategy(
cache: $this->cache,
compiledRoutes: $this->compiledRoutes,
environment: $this->environment
);
expect($strategy->shouldRun())->toBeTrue();
});
it('warms routes cache', function () {
$this->compiledRoutes->shouldReceive('getStaticRoutes')
->once()
->andReturn(['route1' => 'handler1']);
$this->compiledRoutes->shouldReceive('getDynamicRoutes')
->once()
->andReturn(['route2' => 'handler2']);
$this->cache->shouldReceive('set')
->atLeast(2) // routes_static + routes_dynamic + config + env
->andReturn(true);
$strategy = new CriticalPathWarmingStrategy(
cache: $this->cache,
compiledRoutes: $this->compiledRoutes,
environment: $this->environment
);
$result = $strategy->warmup();
expect($result->isSuccess())->toBeTrue();
expect($result->itemsWarmed)->toBeGreaterThan(0);
});
it('estimates reasonable duration', function () {
$strategy = new CriticalPathWarmingStrategy(
cache: $this->cache,
compiledRoutes: $this->compiledRoutes,
environment: $this->environment
);
$duration = $strategy->getEstimatedDuration();
expect($duration)->toBeGreaterThan(0);
expect($duration)->toBeLessThan(30); // Should be fast (< 30 seconds)
});
it('handles cache failures gracefully', function () {
$this->compiledRoutes->shouldReceive('getStaticRoutes')
->andReturn(['route1' => 'handler1']);
$this->compiledRoutes->shouldReceive('getDynamicRoutes')
->andReturn(['route2' => 'handler2']);
$this->cache->shouldReceive('set')
->andReturn(false); // Simulate cache failure
$strategy = new CriticalPathWarmingStrategy(
cache: $this->cache,
compiledRoutes: $this->compiledRoutes,
environment: $this->environment
);
$result = $strategy->warmup();
// Should complete even with failures
expect($result->itemsFailed)->toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
use App\Framework\Cache\Cache;
use App\Framework\Cache\Warming\Strategies\PredictiveWarmingStrategy;
use App\Framework\Cache\Warming\ValueObjects\WarmupPriority;
describe('PredictiveWarmingStrategy', function () {
beforeEach(function () {
$this->cache = Mockery::mock(Cache::class);
});
afterEach(function () {
Mockery::close();
});
it('has correct name', function () {
$strategy = new PredictiveWarmingStrategy($this->cache);
expect($strategy->getName())->toBe('predictive');
});
it('has background priority', function () {
$strategy = new PredictiveWarmingStrategy($this->cache);
expect($strategy->getPriority())->toBe(WarmupPriority::BACKGROUND->value);
});
it('should not run without sufficient access patterns', function () {
$this->cache->shouldReceive('get')
->andReturn(null); // No access patterns available
$strategy = new PredictiveWarmingStrategy($this->cache);
expect($strategy->shouldRun())->toBeFalse();
});
it('should run with sufficient access patterns', function () {
// Mock access patterns (10+ patterns)
$accessPatterns = [];
for ($i = 0; $i < 15; $i++) {
$accessPatterns["pattern_{$i}"] = [
'access_count' => 10 + $i,
'hourly_distribution' => array_fill(0, 24, 0.04),
'daily_distribution' => array_fill(0, 7, 0.14),
];
}
$this->cache->shouldReceive('get')
->andReturn($accessPatterns);
$strategy = new PredictiveWarmingStrategy($this->cache);
expect($strategy->shouldRun())->toBeTrue();
});
it('estimates reasonable duration', function () {
$strategy = new PredictiveWarmingStrategy($this->cache);
$duration = $strategy->getEstimatedDuration();
expect($duration)->toBeGreaterThan(0);
expect($duration)->toBeLessThan(300); // Should be < 5 minutes
});
it('warms predicted cache keys', function () {
// Mock access patterns with high probability
$currentHour = (int) date('G');
$currentDay = (int) date('N') - 1;
$accessPatterns = [
'key1' => [
'access_count' => 50,
'hourly_distribution' => array_fill(0, 24, 0.04),
'daily_distribution' => array_fill(0, 7, 0.14),
],
'key2' => [
'access_count' => 30,
'hourly_distribution' => array_fill(0, 24, 0.04),
'daily_distribution' => array_fill(0, 7, 0.14),
],
];
// Boost current hour/day to ensure high probability
$accessPatterns['key1']['hourly_distribution'][$currentHour] = 0.5;
$accessPatterns['key1']['daily_distribution'][$currentDay] = 0.5;
$this->cache->shouldReceive('get')
->with(Mockery::pattern('/access_patterns/'))
->andReturn($accessPatterns);
$this->cache->shouldReceive('get')
->with(Mockery::pattern('/^key[12]$/'))
->andReturn(null); // Cache miss
$this->cache->shouldReceive('set')
->atLeast(1)
->andReturn(true);
$strategy = new PredictiveWarmingStrategy($this->cache);
$result = $strategy->warmup();
expect($result->isSuccess())->toBeTrue();
});
it('skips low probability keys', function () {
$accessPatterns = [
'low_prob_key' => [
'access_count' => 2, // Below MIN_ACCESS_COUNT
'hourly_distribution' => array_fill(0, 24, 0.01),
'daily_distribution' => array_fill(0, 7, 0.01),
],
];
$this->cache->shouldReceive('get')
->with(Mockery::pattern('/access_patterns/'))
->andReturn($accessPatterns);
$this->cache->shouldNotReceive('set'); // Should not warm low probability
$strategy = new PredictiveWarmingStrategy($this->cache);
$result = $strategy->warmup();
// No items warmed due to low probability
expect($result->itemsWarmed)->toBe(0);
});
});

View File

@@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
use App\Framework\Cache\Warming\ValueObjects\WarmupMetrics;
use App\Framework\Cache\Warming\ValueObjects\WarmupResult;
describe('WarmupMetrics', function () {
it('creates from single result', function () {
$result = new WarmupResult(
strategyName: 'test_strategy',
itemsWarmed: 10,
itemsFailed: 2,
durationSeconds: 1.5,
memoryUsedBytes: 1024000
);
$metrics = WarmupMetrics::fromResults([$result]);
expect($metrics->totalStrategiesExecuted)->toBe(1);
expect($metrics->totalItemsWarmed)->toBe(10);
expect($metrics->totalItemsFailed)->toBe(2);
expect($metrics->totalDurationSeconds)->toBe(1.5);
expect($metrics->totalMemoryUsedBytes)->toBe(1024000);
expect($metrics->strategyResults)->toHaveCount(1);
});
it('aggregates multiple results', function () {
$results = [
new WarmupResult(
strategyName: 'strategy1',
itemsWarmed: 10,
itemsFailed: 1,
durationSeconds: 1.0,
memoryUsedBytes: 1024000
),
new WarmupResult(
strategyName: 'strategy2',
itemsWarmed: 20,
itemsFailed: 2,
durationSeconds: 2.0,
memoryUsedBytes: 2048000
),
];
$metrics = WarmupMetrics::fromResults($results);
expect($metrics->totalStrategiesExecuted)->toBe(2);
expect($metrics->totalItemsWarmed)->toBe(30);
expect($metrics->totalItemsFailed)->toBe(3);
expect($metrics->totalDurationSeconds)->toBe(3.0);
expect($metrics->totalMemoryUsedBytes)->toBe(3072000);
expect($metrics->strategyResults)->toHaveCount(2);
});
it('handles empty results', function () {
$metrics = WarmupMetrics::fromResults([]);
expect($metrics->totalStrategiesExecuted)->toBe(0);
expect($metrics->totalItemsWarmed)->toBe(0);
expect($metrics->totalItemsFailed)->toBe(0);
expect($metrics->totalDurationSeconds)->toBe(0.0);
expect($metrics->totalMemoryUsedBytes)->toBe(0);
expect($metrics->strategyResults)->toBeEmpty();
});
it('calculates overall success rate', function () {
$results = [
new WarmupResult(
strategyName: 'strategy1',
itemsWarmed: 8,
itemsFailed: 2,
durationSeconds: 1.0,
memoryUsedBytes: 1024
),
new WarmupResult(
strategyName: 'strategy2',
itemsWarmed: 18,
itemsFailed: 2,
durationSeconds: 1.0,
memoryUsedBytes: 1024
),
];
$metrics = WarmupMetrics::fromResults($results);
// 26 warmed, 4 failed = 30 total = 26/30 = 0.8666...
expect($metrics->getOverallSuccessRate())->toBeGreaterThan(0.86);
expect($metrics->getOverallSuccessRate())->toBeLessThan(0.87);
});
it('handles zero items for success rate', function () {
$metrics = WarmupMetrics::fromResults([]);
expect($metrics->getOverallSuccessRate())->toBe(0.0);
});
it('calculates average items per second', function () {
$results = [
new WarmupResult(
strategyName: 'strategy1',
itemsWarmed: 100,
itemsFailed: 0,
durationSeconds: 2.0,
memoryUsedBytes: 1024
),
new WarmupResult(
strategyName: 'strategy2',
itemsWarmed: 200,
itemsFailed: 0,
durationSeconds: 4.0,
memoryUsedBytes: 1024
),
];
$metrics = WarmupMetrics::fromResults($results);
// 300 total items / 6 total seconds = 50 items/second
expect($metrics->getAverageItemsPerSecond())->toBe(50.0);
});
it('handles zero duration for items per second', function () {
$result = new WarmupResult(
strategyName: 'test',
itemsWarmed: 100,
itemsFailed: 0,
durationSeconds: 0.0,
memoryUsedBytes: 1024
);
$metrics = WarmupMetrics::fromResults([$result]);
expect($metrics->getAverageItemsPerSecond())->toBe(0.0);
});
it('calculates total memory in MB', function () {
$results = [
new WarmupResult(
strategyName: 'strategy1',
itemsWarmed: 10,
itemsFailed: 0,
durationSeconds: 1.0,
memoryUsedBytes: 1048576 // 1MB
),
new WarmupResult(
strategyName: 'strategy2',
itemsWarmed: 20,
itemsFailed: 0,
durationSeconds: 1.0,
memoryUsedBytes: 2097152 // 2MB
),
];
$metrics = WarmupMetrics::fromResults($results);
expect($metrics->getTotalMemoryUsedMB())->toBe(3.0);
});
it('converts to array', function () {
$result = new WarmupResult(
strategyName: 'test_strategy',
itemsWarmed: 10,
itemsFailed: 2,
durationSeconds: 1.5,
memoryUsedBytes: 1024000
);
$metrics = WarmupMetrics::fromResults([$result]);
$array = $metrics->toArray();
expect($array)->toBeArray();
expect($array['total_strategies_executed'])->toBe(1);
expect($array['total_items_warmed'])->toBe(10);
expect($array['total_items_failed'])->toBe(2);
expect($array['total_duration_seconds'])->toBe(1.5);
expect($array['total_memory_used_bytes'])->toBe(1024000);
expect($array['strategy_results'])->toHaveCount(1);
});
it('includes per-strategy data in array', function () {
$results = [
new WarmupResult(
strategyName: 'strategy1',
itemsWarmed: 10,
itemsFailed: 1,
durationSeconds: 1.0,
memoryUsedBytes: 1024000
),
new WarmupResult(
strategyName: 'strategy2',
itemsWarmed: 20,
itemsFailed: 0,
durationSeconds: 2.0,
memoryUsedBytes: 2048000
),
];
$metrics = WarmupMetrics::fromResults($results);
$array = $metrics->toArray();
expect($array['strategy_results'])->toHaveCount(2);
expect($array['strategy_results'][0]['strategy_name'])->toBe('strategy1');
expect($array['strategy_results'][1]['strategy_name'])->toBe('strategy2');
expect($array['strategy_results'][0]['items_warmed'])->toBe(10);
expect($array['strategy_results'][1]['items_warmed'])->toBe(20);
});
});

View File

@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
use App\Framework\Cache\Warming\ValueObjects\WarmupResult;
describe('WarmupResult', function () {
it('creates result with valid data', function () {
$result = new WarmupResult(
strategyName: 'test_strategy',
itemsWarmed: 10,
itemsFailed: 2,
durationSeconds: 1.5,
memoryUsedBytes: 1024000
);
expect($result->strategyName)->toBe('test_strategy');
expect($result->itemsWarmed)->toBe(10);
expect($result->itemsFailed)->toBe(2);
expect($result->durationSeconds)->toBe(1.5);
expect($result->memoryUsedBytes)->toBe(1024000);
});
it('calculates success correctly', function () {
$successResult = new WarmupResult(
strategyName: 'test',
itemsWarmed: 10,
itemsFailed: 0,
durationSeconds: 1.0,
memoryUsedBytes: 1024
);
expect($successResult->isSuccess())->toBeTrue();
$failureResult = new WarmupResult(
strategyName: 'test',
itemsWarmed: 0,
itemsFailed: 5,
durationSeconds: 1.0,
memoryUsedBytes: 1024
);
expect($failureResult->isSuccess())->toBeFalse();
});
it('calculates success rate', function () {
$result = new WarmupResult(
strategyName: 'test',
itemsWarmed: 8,
itemsFailed: 2,
durationSeconds: 1.0,
memoryUsedBytes: 1024
);
expect($result->getSuccessRate())->toBe(0.8);
});
it('handles zero total items', function () {
$result = new WarmupResult(
strategyName: 'test',
itemsWarmed: 0,
itemsFailed: 0,
durationSeconds: 0.1,
memoryUsedBytes: 1024
);
expect($result->getSuccessRate())->toBe(0.0);
});
it('calculates items per second', function () {
$result = new WarmupResult(
strategyName: 'test',
itemsWarmed: 100,
itemsFailed: 0,
durationSeconds: 2.0,
memoryUsedBytes: 1024
);
expect($result->getItemsPerSecond())->toBe(50.0);
});
it('handles zero duration', function () {
$result = new WarmupResult(
strategyName: 'test',
itemsWarmed: 10,
itemsFailed: 0,
durationSeconds: 0.0,
memoryUsedBytes: 1024
);
expect($result->getItemsPerSecond())->toBe(0.0);
});
it('calculates memory in MB', function () {
$result = new WarmupResult(
strategyName: 'test',
itemsWarmed: 10,
itemsFailed: 0,
durationSeconds: 1.0,
memoryUsedBytes: 2048000 // 2MB
);
expect($result->getMemoryUsedMB())->toBe(2.0);
});
it('converts to array', function () {
$result = new WarmupResult(
strategyName: 'test_strategy',
itemsWarmed: 10,
itemsFailed: 2,
durationSeconds: 1.5,
memoryUsedBytes: 1024000,
errors: [['item' => 'test', 'error' => 'failed']],
metadata: ['key' => 'value']
);
$array = $result->toArray();
expect($array)->toBeArray();
expect($array['strategy_name'])->toBe('test_strategy');
expect($array['items_warmed'])->toBe(10);
expect($array['items_failed'])->toBe(2);
expect($array['duration_seconds'])->toBe(1.5);
expect($array['memory_used_bytes'])->toBe(1024000);
expect($array['errors'])->toHaveCount(1);
expect($array['metadata'])->toHaveKey('key');
});
it('throws on negative items warmed', function () {
new WarmupResult(
strategyName: 'test',
itemsWarmed: -1,
itemsFailed: 0,
durationSeconds: 1.0,
memoryUsedBytes: 1024
);
})->throws(InvalidArgumentException::class);
it('throws on negative items failed', function () {
new WarmupResult(
strategyName: 'test',
itemsWarmed: 10,
itemsFailed: -1,
durationSeconds: 1.0,
memoryUsedBytes: 1024
);
})->throws(InvalidArgumentException::class);
it('throws on negative duration', function () {
new WarmupResult(
strategyName: 'test',
itemsWarmed: 10,
itemsFailed: 0,
durationSeconds: -1.0,
memoryUsedBytes: 1024
);
})->throws(InvalidArgumentException::class);
it('throws on negative memory', function () {
new WarmupResult(
strategyName: 'test',
itemsWarmed: 10,
itemsFailed: 0,
durationSeconds: 1.0,
memoryUsedBytes: -1024
);
})->throws(InvalidArgumentException::class);
});