- 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.
795 lines
32 KiB
PHP
795 lines
32 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Core\ValueObjects\Percentage;
|
|
use App\Framework\Queue\ValueObjects\JobId;
|
|
use App\Framework\Queue\ValueObjects\JobProgress;
|
|
|
|
describe('JobProgress Value Object', function () {
|
|
|
|
describe('Basic Construction and Validation', function () {
|
|
it('can create job progress with percentage and message', function () {
|
|
$percentage = Percentage::fromValue(50.0);
|
|
$message = 'Processing half way complete';
|
|
$metadata = ['step' => 'validation', 'items_processed' => 500];
|
|
|
|
$progress = JobProgress::withPercentage($percentage, $message, $metadata);
|
|
|
|
expect($progress->percentage)->toBe($percentage);
|
|
expect($progress->message)->toBe($message);
|
|
expect($progress->metadata)->toBe($metadata);
|
|
});
|
|
|
|
it('rejects empty progress messages', function () {
|
|
expect(fn () => JobProgress::withPercentage(Percentage::zero(), ''))
|
|
->toThrow(\InvalidArgumentException::class, 'Progress message cannot be empty');
|
|
|
|
expect(fn () => JobProgress::withPercentage(Percentage::zero(), ' '))
|
|
->toThrow(\InvalidArgumentException::class, 'Progress message cannot be empty');
|
|
});
|
|
|
|
it('is readonly and immutable', function () {
|
|
$progress = JobProgress::starting('Test job starting');
|
|
|
|
$reflection = new ReflectionClass($progress);
|
|
expect($reflection->isReadOnly())->toBeTrue();
|
|
|
|
// All properties should be readonly
|
|
foreach (['percentage', 'message', 'metadata'] as $prop) {
|
|
$property = $reflection->getProperty($prop);
|
|
expect($property->isReadOnly())->toBeTrue("Property {$prop} should be readonly");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Factory Methods', function () {
|
|
it('creates starting progress', function () {
|
|
$progress = JobProgress::starting();
|
|
|
|
expect($progress->percentage->getValue())->toBe(0.0);
|
|
expect($progress->message)->toBe('Job starting...');
|
|
expect($progress->isStarting())->toBeTrue();
|
|
expect($progress->isCompleted())->toBeFalse();
|
|
expect($progress->isFailed())->toBeFalse();
|
|
});
|
|
|
|
it('creates starting progress with custom message', function () {
|
|
$message = 'Email job initializing...';
|
|
$progress = JobProgress::starting($message);
|
|
|
|
expect($progress->percentage->getValue())->toBe(0.0);
|
|
expect($progress->message)->toBe($message);
|
|
expect($progress->isStarting())->toBeTrue();
|
|
});
|
|
|
|
it('creates completed progress', function () {
|
|
$progress = JobProgress::completed();
|
|
|
|
expect($progress->percentage->getValue())->toBe(100.0);
|
|
expect($progress->message)->toBe('Job completed successfully');
|
|
expect($progress->isCompleted())->toBeTrue();
|
|
expect($progress->isStarting())->toBeFalse();
|
|
expect($progress->isFailed())->toBeFalse();
|
|
});
|
|
|
|
it('creates completed progress with custom message', function () {
|
|
$message = 'All emails sent successfully';
|
|
$progress = JobProgress::completed($message);
|
|
|
|
expect($progress->percentage->getValue())->toBe(100.0);
|
|
expect($progress->message)->toBe($message);
|
|
expect($progress->isCompleted())->toBeTrue();
|
|
});
|
|
|
|
it('creates failed progress', function () {
|
|
$progress = JobProgress::failed();
|
|
|
|
expect($progress->percentage->getValue())->toBe(0.0);
|
|
expect($progress->message)->toBe('Job failed');
|
|
expect($progress->isFailed())->toBeTrue();
|
|
expect($progress->isCompleted())->toBeFalse();
|
|
expect($progress->isStarting())->toBeFalse();
|
|
expect($progress->metadata['status'])->toBe('failed');
|
|
});
|
|
|
|
it('creates failed progress with custom message', function () {
|
|
$message = 'Email service unavailable';
|
|
$progress = JobProgress::failed($message);
|
|
|
|
expect($progress->message)->toBe($message);
|
|
expect($progress->isFailed())->toBeTrue();
|
|
});
|
|
|
|
it('creates progress from ratio', function () {
|
|
$progress = JobProgress::fromRatio(25, 100, 'Processing items', ['current_item' => 25]);
|
|
|
|
expect($progress->percentage->getValue())->toBe(25.0);
|
|
expect($progress->message)->toBe('Processing items');
|
|
expect($progress->metadata['current_item'])->toBe(25);
|
|
});
|
|
|
|
it('handles edge cases in fromRatio', function () {
|
|
// Zero total
|
|
$progress = JobProgress::fromRatio(0, 0, 'No items to process');
|
|
expect($progress->percentage->getValue())->toBe(0.0);
|
|
|
|
// All items processed
|
|
$progress = JobProgress::fromRatio(100, 100, 'All items processed');
|
|
expect($progress->percentage->getValue())->toBe(100.0);
|
|
expect($progress->isCompleted())->toBeTrue();
|
|
});
|
|
});
|
|
|
|
describe('Status Check Methods', function () {
|
|
it('correctly identifies completed status', function () {
|
|
$completed = JobProgress::completed();
|
|
$partial = JobProgress::withPercentage(Percentage::fromValue(50.0), 'Half done');
|
|
$starting = JobProgress::starting();
|
|
|
|
expect($completed->isCompleted())->toBeTrue();
|
|
expect($partial->isCompleted())->toBeFalse();
|
|
expect($starting->isCompleted())->toBeFalse();
|
|
});
|
|
|
|
it('correctly identifies failed status', function () {
|
|
$failed = JobProgress::failed();
|
|
$completed = JobProgress::completed();
|
|
$starting = JobProgress::starting();
|
|
|
|
expect($failed->isFailed())->toBeTrue();
|
|
expect($completed->isFailed())->toBeFalse();
|
|
expect($starting->isFailed())->toBeFalse();
|
|
});
|
|
|
|
it('correctly identifies starting status', function () {
|
|
$starting = JobProgress::starting();
|
|
$partial = JobProgress::withPercentage(Percentage::fromValue(10.0), 'Just started');
|
|
$failed = JobProgress::failed();
|
|
$completed = JobProgress::completed();
|
|
|
|
expect($starting->isStarting())->toBeTrue();
|
|
expect($partial->isStarting())->toBeFalse();
|
|
expect($failed->isStarting())->toBeFalse(); // Failed is not starting
|
|
expect($completed->isStarting())->toBeFalse();
|
|
});
|
|
|
|
it('handles edge cases in status detection', function () {
|
|
// Zero percentage but not starting due to metadata
|
|
$zeroButNotStarting = JobProgress::withPercentage(
|
|
Percentage::zero(),
|
|
'Waiting for dependencies',
|
|
['status' => 'waiting']
|
|
);
|
|
expect($zeroButNotStarting->isStarting())->toBeTrue(); // Still starting since not failed
|
|
|
|
// Custom failed status
|
|
$customFailed = JobProgress::withPercentage(
|
|
Percentage::fromValue(50.0),
|
|
'Failed during processing',
|
|
['status' => 'failed']
|
|
);
|
|
expect($customFailed->isFailed())->toBeTrue();
|
|
expect($customFailed->isCompleted())->toBeFalse();
|
|
});
|
|
});
|
|
|
|
describe('Immutable Transformations', function () {
|
|
beforeEach(function () {
|
|
$this->originalProgress = JobProgress::withPercentage(
|
|
Percentage::fromValue(25.0),
|
|
'Quarter complete',
|
|
['step' => 'validation']
|
|
);
|
|
});
|
|
|
|
it('withMetadata() creates new instance with merged metadata', function () {
|
|
$newMetadata = ['items_processed' => 250, 'errors' => 0];
|
|
$updated = $this->originalProgress->withMetadata($newMetadata);
|
|
|
|
expect($updated)->not->toBe($this->originalProgress);
|
|
expect($updated->percentage)->toBe($this->originalProgress->percentage);
|
|
expect($updated->message)->toBe($this->originalProgress->message);
|
|
expect($updated->metadata['step'])->toBe('validation'); // Original metadata preserved
|
|
expect($updated->metadata['items_processed'])->toBe(250); // New metadata added
|
|
expect($updated->metadata['errors'])->toBe(0);
|
|
|
|
// Original should be unchanged
|
|
expect($this->originalProgress->metadata)->toBe(['step' => 'validation']);
|
|
});
|
|
|
|
it('withMetadata() overwrites conflicting keys', function () {
|
|
$newMetadata = ['step' => 'processing']; // Conflicts with existing key
|
|
$updated = $this->originalProgress->withMetadata($newMetadata);
|
|
|
|
expect($updated->metadata['step'])->toBe('processing'); // New value wins
|
|
});
|
|
|
|
it('withUpdatedProgress() creates new instance with updated progress', function () {
|
|
$newPercentage = Percentage::fromValue(75.0);
|
|
$newMessage = 'Three quarters complete';
|
|
$updated = $this->originalProgress->withUpdatedProgress($newPercentage, $newMessage);
|
|
|
|
expect($updated)->not->toBe($this->originalProgress);
|
|
expect($updated->percentage)->toBe($newPercentage);
|
|
expect($updated->message)->toBe($newMessage);
|
|
expect($updated->metadata)->toBe($this->originalProgress->metadata); // Metadata preserved
|
|
|
|
// Original should be unchanged
|
|
expect($this->originalProgress->percentage->getValue())->toBe(25.0);
|
|
expect($this->originalProgress->message)->toBe('Quarter complete');
|
|
});
|
|
|
|
it('can chain transformations', function () {
|
|
$final = $this->originalProgress
|
|
->withUpdatedProgress(Percentage::fromValue(50.0), 'Half complete')
|
|
->withMetadata(['processed_items' => 500]);
|
|
|
|
expect($final->percentage->getValue())->toBe(50.0);
|
|
expect($final->message)->toBe('Half complete');
|
|
expect($final->metadata['step'])->toBe('validation'); // Original preserved
|
|
expect($final->metadata['processed_items'])->toBe(500); // New added
|
|
|
|
// Original should be completely unchanged
|
|
expect($this->originalProgress->percentage->getValue())->toBe(25.0);
|
|
expect($this->originalProgress->message)->toBe('Quarter complete');
|
|
expect($this->originalProgress->metadata)->toBe(['step' => 'validation']);
|
|
});
|
|
});
|
|
|
|
describe('Array Conversion', function () {
|
|
it('toArray() provides comprehensive progress information', function () {
|
|
$progress = JobProgress::withPercentage(
|
|
Percentage::fromValue(75.5),
|
|
'Processing emails',
|
|
['batch_id' => 123, 'errors' => 2]
|
|
);
|
|
|
|
$array = $progress->toArray();
|
|
|
|
expect($array)->toHaveKey('percentage');
|
|
expect($array)->toHaveKey('percentage_formatted');
|
|
expect($array)->toHaveKey('message');
|
|
expect($array)->toHaveKey('metadata');
|
|
expect($array)->toHaveKey('is_completed');
|
|
expect($array)->toHaveKey('is_failed');
|
|
expect($array)->toHaveKey('is_starting');
|
|
|
|
expect($array['percentage'])->toBe(75.5);
|
|
expect($array['percentage_formatted'])->toBe('75.5%');
|
|
expect($array['message'])->toBe('Processing emails');
|
|
expect($array['metadata'])->toBe(['batch_id' => 123, 'errors' => 2]);
|
|
expect($array['is_completed'])->toBeFalse();
|
|
expect($array['is_failed'])->toBeFalse();
|
|
expect($array['is_starting'])->toBeFalse();
|
|
});
|
|
|
|
it('toArray() handles different progress states', function () {
|
|
$states = [
|
|
'starting' => JobProgress::starting(),
|
|
'completed' => JobProgress::completed(),
|
|
'failed' => JobProgress::failed(),
|
|
];
|
|
|
|
foreach ($states as $stateName => $progress) {
|
|
$array = $progress->toArray();
|
|
|
|
expect($array)->toBeArray();
|
|
expect($array)->toHaveKey('is_completed');
|
|
expect($array)->toHaveKey('is_failed');
|
|
expect($array)->toHaveKey('is_starting');
|
|
|
|
switch ($stateName) {
|
|
case 'starting':
|
|
expect($array['is_starting'])->toBeTrue();
|
|
expect($array['is_completed'])->toBeFalse();
|
|
expect($array['is_failed'])->toBeFalse();
|
|
|
|
break;
|
|
case 'completed':
|
|
expect($array['is_completed'])->toBeTrue();
|
|
expect($array['is_starting'])->toBeFalse();
|
|
expect($array['is_failed'])->toBeFalse();
|
|
|
|
break;
|
|
case 'failed':
|
|
expect($array['is_failed'])->toBeTrue();
|
|
expect($array['is_starting'])->toBeFalse();
|
|
expect($array['is_completed'])->toBeFalse();
|
|
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Job Progress Tracking System Mock', function () {
|
|
|
|
beforeEach(function () {
|
|
// Create a mock progress tracker for testing
|
|
$this->progressTracker = new class () {
|
|
private array $progressEntries = [];
|
|
|
|
public function updateProgress(string $jobId, JobProgress $progress, ?string $stepName = null): void
|
|
{
|
|
$this->progressEntries[$jobId][] = [
|
|
'progress' => $progress,
|
|
'step_name' => $stepName,
|
|
'timestamp' => time(),
|
|
'id' => uniqid(),
|
|
];
|
|
}
|
|
|
|
public function getCurrentProgress(string $jobId): ?JobProgress
|
|
{
|
|
if (! isset($this->progressEntries[$jobId]) || empty($this->progressEntries[$jobId])) {
|
|
return null;
|
|
}
|
|
|
|
$entries = $this->progressEntries[$jobId];
|
|
|
|
return end($entries)['progress'];
|
|
}
|
|
|
|
public function getProgressHistory(string $jobId): array
|
|
{
|
|
return $this->progressEntries[$jobId] ?? [];
|
|
}
|
|
|
|
public function markJobCompleted(string $jobId, string $message = 'Job completed successfully'): void
|
|
{
|
|
$this->updateProgress($jobId, JobProgress::completed($message));
|
|
}
|
|
|
|
public function markJobFailed(string $jobId, string $message = 'Job failed', ?\Throwable $exception = null): void
|
|
{
|
|
$metadata = [];
|
|
if ($exception) {
|
|
$metadata['exception_type'] = get_class($exception);
|
|
$metadata['exception_message'] = $exception->getMessage();
|
|
}
|
|
|
|
$progress = JobProgress::failed($message)->withMetadata($metadata);
|
|
$this->updateProgress($jobId, $progress);
|
|
}
|
|
|
|
public function getProgressForJobs(array $jobIds): array
|
|
{
|
|
$result = [];
|
|
foreach ($jobIds as $jobId) {
|
|
$current = $this->getCurrentProgress($jobId);
|
|
if ($current !== null) {
|
|
$result[$jobId] = $current;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function getJobsAboveProgress(float $minPercentage): array
|
|
{
|
|
$result = [];
|
|
foreach ($this->progressEntries as $jobId => $entries) {
|
|
$current = end($entries)['progress'];
|
|
if ($current->percentage->getValue() >= $minPercentage) {
|
|
$result[] = ['job_id' => $jobId, 'progress' => $current];
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
};
|
|
});
|
|
|
|
describe('Progress Tracking Operations', function () {
|
|
it('can track job progress updates', function () {
|
|
$jobId = JobId::generate()->toString();
|
|
|
|
// Track job progression
|
|
$this->progressTracker->updateProgress($jobId, JobProgress::starting('Job initialized'));
|
|
$this->progressTracker->updateProgress($jobId, JobProgress::fromRatio(25, 100, 'Processing batch 1'));
|
|
$this->progressTracker->updateProgress($jobId, JobProgress::fromRatio(50, 100, 'Processing batch 2'));
|
|
$this->progressTracker->updateProgress($jobId, JobProgress::fromRatio(75, 100, 'Processing batch 3'));
|
|
$this->progressTracker->markJobCompleted($jobId, 'All batches processed');
|
|
|
|
$history = $this->progressTracker->getProgressHistory($jobId);
|
|
expect(count($history))->toBe(5);
|
|
|
|
$current = $this->progressTracker->getCurrentProgress($jobId);
|
|
expect($current->isCompleted())->toBeTrue();
|
|
expect($current->message)->toBe('All batches processed');
|
|
});
|
|
|
|
it('can track job with steps', function () {
|
|
$jobId = JobId::generate()->toString();
|
|
|
|
$steps = [
|
|
'validation' => 'Validating input data',
|
|
'processing' => 'Processing records',
|
|
'notification' => 'Sending notifications',
|
|
'cleanup' => 'Cleaning up temporary files',
|
|
];
|
|
|
|
foreach ($steps as $stepName => $message) {
|
|
$this->progressTracker->updateProgress(
|
|
$jobId,
|
|
JobProgress::fromRatio(array_search($stepName, array_keys($steps)) + 1, count($steps), $message),
|
|
$stepName
|
|
);
|
|
}
|
|
|
|
$history = $this->progressTracker->getProgressHistory($jobId);
|
|
expect(count($history))->toBe(4);
|
|
|
|
// Check step names are tracked
|
|
$stepNames = array_map(fn ($entry) => $entry['step_name'], $history);
|
|
expect($stepNames)->toBe(['validation', 'processing', 'notification', 'cleanup']);
|
|
});
|
|
|
|
it('can mark jobs as failed with exception details', function () {
|
|
$jobId = JobId::generate()->toString();
|
|
|
|
$this->progressTracker->updateProgress($jobId, JobProgress::starting('Starting email job'));
|
|
$this->progressTracker->updateProgress($jobId, JobProgress::fromRatio(10, 100, 'Connecting to email service'));
|
|
|
|
// Simulate failure with exception
|
|
$exception = new \RuntimeException('Email service unavailable');
|
|
$this->progressTracker->markJobFailed($jobId, 'Failed to connect to email service', $exception);
|
|
|
|
$current = $this->progressTracker->getCurrentProgress($jobId);
|
|
expect($current->isFailed())->toBeTrue();
|
|
expect($current->message)->toBe('Failed to connect to email service');
|
|
expect($current->metadata['exception_type'])->toBe('RuntimeException');
|
|
expect($current->metadata['exception_message'])->toBe('Email service unavailable');
|
|
});
|
|
|
|
it('handles jobs with no progress', function () {
|
|
$nonExistentJobId = JobId::generate()->toString();
|
|
|
|
$current = $this->progressTracker->getCurrentProgress($nonExistentJobId);
|
|
expect($current)->toBeNull();
|
|
|
|
$history = $this->progressTracker->getProgressHistory($nonExistentJobId);
|
|
expect($history)->toBe([]);
|
|
});
|
|
});
|
|
|
|
describe('Bulk Progress Operations', function () {
|
|
it('can get progress for multiple jobs', function () {
|
|
$jobIds = [
|
|
JobId::generate()->toString(),
|
|
JobId::generate()->toString(),
|
|
JobId::generate()->toString(),
|
|
];
|
|
|
|
// Add progress for some jobs
|
|
$this->progressTracker->updateProgress($jobIds[0], JobProgress::fromRatio(25, 100, 'Job 1 progress'));
|
|
$this->progressTracker->updateProgress($jobIds[1], JobProgress::fromRatio(75, 100, 'Job 2 progress'));
|
|
// Job 3 has no progress
|
|
|
|
$bulkProgress = $this->progressTracker->getProgressForJobs($jobIds);
|
|
|
|
expect(count($bulkProgress))->toBe(2);
|
|
expect($bulkProgress[$jobIds[0]]->percentage->getValue())->toBe(25.0);
|
|
expect($bulkProgress[$jobIds[1]]->percentage->getValue())->toBe(75.0);
|
|
expect(isset($bulkProgress[$jobIds[2]]))->toBeFalse();
|
|
});
|
|
|
|
it('can find jobs above certain progress threshold', function () {
|
|
$jobs = [
|
|
JobId::generate()->toString() => 10.0,
|
|
JobId::generate()->toString() => 50.0,
|
|
JobId::generate()->toString() => 80.0,
|
|
JobId::generate()->toString() => 95.0,
|
|
];
|
|
|
|
foreach ($jobs as $jobId => $progress) {
|
|
$this->progressTracker->updateProgress(
|
|
$jobId,
|
|
JobProgress::fromRatio((int)$progress, 100, "Progress at {$progress}%")
|
|
);
|
|
}
|
|
|
|
$jobsAbove60 = $this->progressTracker->getJobsAboveProgress(60.0);
|
|
expect(count($jobsAbove60))->toBe(2); // 80% and 95%
|
|
|
|
$jobsAbove90 = $this->progressTracker->getJobsAboveProgress(90.0);
|
|
expect(count($jobsAbove90))->toBe(1); // Only 95%
|
|
|
|
$jobsAbove100 = $this->progressTracker->getJobsAboveProgress(100.0);
|
|
expect(count($jobsAbove100))->toBe(0); // None at 100%
|
|
});
|
|
|
|
it('handles empty job lists gracefully', function () {
|
|
$emptyResult = $this->progressTracker->getProgressForJobs([]);
|
|
expect($emptyResult)->toBe([]);
|
|
|
|
$noJobs = $this->progressTracker->getJobsAboveProgress(50.0);
|
|
expect($noJobs)->toBe([]);
|
|
});
|
|
});
|
|
|
|
describe('Progress Tracking Edge Cases', function () {
|
|
it('handles rapid progress updates', function () {
|
|
$jobId = JobId::generate()->toString();
|
|
|
|
// Simulate rapid updates
|
|
for ($i = 0; $i <= 100; $i += 10) {
|
|
$this->progressTracker->updateProgress(
|
|
$jobId,
|
|
JobProgress::fromRatio($i, 100, "Progress at {$i}%")
|
|
);
|
|
}
|
|
|
|
$history = $this->progressTracker->getProgressHistory($jobId);
|
|
expect(count($history))->toBe(11); // 0, 10, 20, ..., 100
|
|
|
|
$current = $this->progressTracker->getCurrentProgress($jobId);
|
|
expect($current->isCompleted())->toBeTrue();
|
|
});
|
|
|
|
it('maintains progress order', function () {
|
|
$jobId = JobId::generate()->toString();
|
|
|
|
$progressUpdates = [
|
|
['percentage' => 0, 'message' => 'Starting'],
|
|
['percentage' => 25, 'message' => 'Quarter done'],
|
|
['percentage' => 50, 'message' => 'Half done'],
|
|
['percentage' => 75, 'message' => 'Three quarters done'],
|
|
['percentage' => 100, 'message' => 'Completed'],
|
|
];
|
|
|
|
foreach ($progressUpdates as $update) {
|
|
$this->progressTracker->updateProgress(
|
|
$jobId,
|
|
JobProgress::fromRatio($update['percentage'], 100, $update['message'])
|
|
);
|
|
// Small delay to ensure different timestamps
|
|
usleep(1000);
|
|
}
|
|
|
|
$history = $this->progressTracker->getProgressHistory($jobId);
|
|
$messages = array_map(fn ($entry) => $entry['progress']->message, $history);
|
|
|
|
expect($messages)->toBe([
|
|
'Starting',
|
|
'Quarter done',
|
|
'Half done',
|
|
'Three quarters done',
|
|
'Completed',
|
|
]);
|
|
});
|
|
|
|
it('handles concurrent job tracking', function () {
|
|
$jobIds = [
|
|
'job_a' => JobId::generate()->toString(),
|
|
'job_b' => JobId::generate()->toString(),
|
|
'job_c' => JobId::generate()->toString(),
|
|
];
|
|
|
|
// Simulate concurrent progress updates
|
|
foreach ($jobIds as $label => $jobId) {
|
|
$this->progressTracker->updateProgress($jobId, JobProgress::starting("Starting {$label}"));
|
|
}
|
|
|
|
foreach ($jobIds as $label => $jobId) {
|
|
$this->progressTracker->updateProgress($jobId, JobProgress::fromRatio(50, 100, "{$label} half done"));
|
|
}
|
|
|
|
// Complete jobs at different times
|
|
$this->progressTracker->markJobCompleted($jobIds['job_a'], 'Job A completed');
|
|
$this->progressTracker->markJobFailed($jobIds['job_b'], 'Job B failed');
|
|
$this->progressTracker->markJobCompleted($jobIds['job_c'], 'Job C completed');
|
|
|
|
// Verify independent tracking
|
|
$progressA = $this->progressTracker->getCurrentProgress($jobIds['job_a']);
|
|
$progressB = $this->progressTracker->getCurrentProgress($jobIds['job_b']);
|
|
$progressC = $this->progressTracker->getCurrentProgress($jobIds['job_c']);
|
|
|
|
expect($progressA->isCompleted())->toBeTrue();
|
|
expect($progressB->isFailed())->toBeTrue();
|
|
expect($progressC->isCompleted())->toBeTrue();
|
|
|
|
// Each job should have its own history
|
|
expect(count($this->progressTracker->getProgressHistory($jobIds['job_a'])))->toBe(3);
|
|
expect(count($this->progressTracker->getProgressHistory($jobIds['job_b'])))->toBe(3);
|
|
expect(count($this->progressTracker->getProgressHistory($jobIds['job_c'])))->toBe(3);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Job Progress Integration Scenarios', function () {
|
|
|
|
beforeEach(function () {
|
|
$this->emailJob = new class () {
|
|
public function __construct(
|
|
public array $recipients = ['test@example.com'],
|
|
public string $subject = 'Test Email',
|
|
public string $template = 'newsletter'
|
|
) {
|
|
}
|
|
|
|
public function getRecipientCount(): int
|
|
{
|
|
return count($this->recipients);
|
|
}
|
|
};
|
|
|
|
$this->reportJob = new class () {
|
|
public function __construct(
|
|
public string $reportType = 'sales',
|
|
public array $criteria = ['period' => 'monthly'],
|
|
public int $totalSteps = 5
|
|
) {
|
|
}
|
|
|
|
public function getSteps(): array
|
|
{
|
|
return [
|
|
'data_collection' => 'Collecting data from database',
|
|
'data_processing' => 'Processing and aggregating data',
|
|
'chart_generation' => 'Generating charts and graphs',
|
|
'pdf_creation' => 'Creating PDF document',
|
|
'distribution' => 'Distributing report to stakeholders',
|
|
];
|
|
}
|
|
};
|
|
});
|
|
|
|
it('demonstrates email job progress tracking', function () {
|
|
$jobId = JobId::generate()->toString();
|
|
$progressTracker = new class () {
|
|
private array $progressEntries = [];
|
|
|
|
public function updateProgress(string $jobId, JobProgress $progress, ?string $stepName = null): void
|
|
{
|
|
$this->progressEntries[$jobId][] = ['progress' => $progress, 'step_name' => $stepName];
|
|
}
|
|
|
|
public function getCurrentProgress(string $jobId): ?JobProgress
|
|
{
|
|
if (! isset($this->progressEntries[$jobId])) {
|
|
return null;
|
|
}
|
|
|
|
return end($this->progressEntries[$jobId])['progress'];
|
|
}
|
|
};
|
|
|
|
$totalRecipients = count($this->emailJob->recipients);
|
|
|
|
// Start email job
|
|
$progressTracker->updateProgress(
|
|
$jobId,
|
|
JobProgress::starting('Initializing email job'),
|
|
'initialization'
|
|
);
|
|
|
|
// Template preparation
|
|
$progressTracker->updateProgress(
|
|
$jobId,
|
|
JobProgress::fromRatio(1, 4, 'Preparing email template'),
|
|
'template_preparation'
|
|
);
|
|
|
|
// Recipient validation
|
|
$progressTracker->updateProgress(
|
|
$jobId,
|
|
JobProgress::fromRatio(2, 4, 'Validating recipient addresses'),
|
|
'recipient_validation'
|
|
);
|
|
|
|
// Email sending
|
|
$progressTracker->updateProgress(
|
|
$jobId,
|
|
JobProgress::fromRatio(3, 4, 'Sending emails'),
|
|
'email_sending'
|
|
);
|
|
|
|
// Completion
|
|
$progressTracker->updateProgress(
|
|
$jobId,
|
|
JobProgress::completed('All emails sent successfully'),
|
|
'completion'
|
|
);
|
|
|
|
$finalProgress = $progressTracker->getCurrentProgress($jobId);
|
|
expect($finalProgress->isCompleted())->toBeTrue();
|
|
expect($finalProgress->message)->toBe('All emails sent successfully');
|
|
});
|
|
|
|
it('demonstrates report generation progress tracking', function () {
|
|
$jobId = JobId::generate()->toString();
|
|
$progressTracker = new class () {
|
|
private array $progressEntries = [];
|
|
|
|
public function updateProgress(string $jobId, JobProgress $progress, ?string $stepName = null): void
|
|
{
|
|
$this->progressEntries[$jobId][] = ['progress' => $progress, 'step_name' => $stepName];
|
|
}
|
|
|
|
public function getProgressHistory(string $jobId): array
|
|
{
|
|
return $this->progressEntries[$jobId] ?? [];
|
|
}
|
|
};
|
|
|
|
$steps = $this->reportJob->getSteps();
|
|
$totalSteps = count($steps);
|
|
$currentStep = 0;
|
|
|
|
foreach ($steps as $stepName => $description) {
|
|
$currentStep++;
|
|
$progressTracker->updateProgress(
|
|
$jobId,
|
|
JobProgress::fromRatio($currentStep, $totalSteps, $description),
|
|
$stepName
|
|
);
|
|
}
|
|
|
|
$history = $progressTracker->getProgressHistory($jobId);
|
|
expect(count($history))->toBe($totalSteps);
|
|
|
|
// Verify step progression
|
|
$stepNames = array_map(fn ($entry) => $entry['step_name'], $history);
|
|
expect($stepNames)->toBe(array_keys($steps));
|
|
|
|
// Verify progress percentages
|
|
$percentages = array_map(fn ($entry) => $entry['progress']->percentage->getValue(), $history);
|
|
expect($percentages)->toBe([20.0, 40.0, 60.0, 80.0, 100.0]);
|
|
});
|
|
|
|
it('demonstrates error handling with progress tracking', function () {
|
|
$jobId = JobId::generate()->toString();
|
|
$progressTracker = new class () {
|
|
private array $progressEntries = [];
|
|
|
|
public function updateProgress(string $jobId, JobProgress $progress, ?string $stepName = null): void
|
|
{
|
|
$this->progressEntries[$jobId][] = ['progress' => $progress, 'step_name' => $stepName];
|
|
}
|
|
|
|
public function getCurrentProgress(string $jobId): ?JobProgress
|
|
{
|
|
if (! isset($this->progressEntries[$jobId])) {
|
|
return null;
|
|
}
|
|
|
|
return end($this->progressEntries[$jobId])['progress'];
|
|
}
|
|
};
|
|
|
|
// Start processing
|
|
$progressTracker->updateProgress(
|
|
$jobId,
|
|
JobProgress::starting('Starting data processing job'),
|
|
'initialization'
|
|
);
|
|
|
|
$progressTracker->updateProgress(
|
|
$jobId,
|
|
JobProgress::fromRatio(1, 3, 'Loading data from database'),
|
|
'data_loading'
|
|
);
|
|
|
|
// Simulate error during processing
|
|
$exception = new \RuntimeException('Database connection lost');
|
|
$failedProgress = JobProgress::failed('Processing failed due to database error')
|
|
->withMetadata([
|
|
'exception_type' => get_class($exception),
|
|
'exception_message' => $exception->getMessage(),
|
|
'failed_at_step' => 'data_processing',
|
|
'items_processed' => 150,
|
|
'total_items' => 500,
|
|
]);
|
|
|
|
$progressTracker->updateProgress($jobId, $failedProgress, 'data_processing');
|
|
|
|
$currentProgress = $progressTracker->getCurrentProgress($jobId);
|
|
expect($currentProgress->isFailed())->toBeTrue();
|
|
expect($currentProgress->metadata['items_processed'])->toBe(150);
|
|
expect($currentProgress->metadata['exception_type'])->toBe('RuntimeException');
|
|
});
|
|
});
|