Files
michaelschiemer/tests/Framework/Queue/JobProgressTrackingTest.php
Michael Schiemer 5050c7d73a docs: consolidate documentation into organized structure
- Move 12 markdown files from root to docs/ subdirectories
- Organize documentation by category:
  • docs/troubleshooting/ (1 file)  - Technical troubleshooting guides
  • docs/deployment/      (4 files) - Deployment and security documentation
  • docs/guides/          (3 files) - Feature-specific guides
  • docs/planning/        (4 files) - Planning and improvement proposals

Root directory cleanup:
- Reduced from 16 to 4 markdown files in root
- Only essential project files remain:
  • CLAUDE.md (AI instructions)
  • README.md (Main project readme)
  • CLEANUP_PLAN.md (Current cleanup plan)
  • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements)

This improves:
 Documentation discoverability
 Logical organization by purpose
 Clean root directory
 Better maintainability
2025-10-05 11:05:04 +02:00

760 lines
32 KiB
PHP

<?php
declare(strict_types=1);
use App\Framework\Queue\ValueObjects\JobProgress;
use App\Framework\Queue\ValueObjects\ProgressStep;
use App\Framework\Queue\ValueObjects\JobId;
use App\Framework\Core\ValueObjects\Percentage;
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');
});
});