Files
michaelschiemer/tests/Framework/Queue/QueueTest.php
Michael Schiemer c8b47e647d feat(Docker): Upgrade to PHP 8.5.0RC3 with native ext-uri support
BREAKING CHANGE: Requires PHP 8.5.0RC3

Changes:
- Update Docker base image from php:8.4-fpm to php:8.5.0RC3-fpm
- Enable ext-uri for native WHATWG URL parsing support
- Update composer.json PHP requirement from ^8.4 to ^8.5
- Add ext-uri as required extension in composer.json
- Move URL classes from Url.php85/ to Url/ directory (now compatible)
- Remove temporary PHP 8.4 compatibility workarounds

Benefits:
- Native URL parsing with Uri\WhatWg\Url class
- Better performance for URL operations
- Future-proof with latest PHP features
- Eliminates PHP version compatibility issues
2025-10-27 09:31:28 +01:00

367 lines
13 KiB
PHP

<?php
declare(strict_types=1);
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Queue\InMemoryQueue;
use App\Framework\Queue\ValueObjects\JobPayload;
use App\Framework\Queue\ValueObjects\QueuePriority;
// Test job classes
class SimpleTestJob
{
public function handle(): string
{
return 'test job executed';
}
}
class CounterTestJob
{
public function __construct(public int $id)
{
}
public function handle(): string
{
return "job {$this->id} executed";
}
}
class PriorityTestJob
{
public function __construct(public string $priority)
{
}
public function handle(): string
{
return "job with {$this->priority} priority executed";
}
}
describe('Queue Interface Basic Operations', function () {
beforeEach(function () {
$this->queue = new InMemoryQueue();
$this->testJob = new SimpleTestJob();
});
describe('push() operation', function () {
it('can push jobs to queue', function () {
$payload = JobPayload::create($this->testJob);
$this->queue->push($payload);
expect($this->queue->size())->toBe(1);
});
it('maintains priority order when pushing multiple jobs', function () {
$lowPriorityJob = JobPayload::create($this->testJob, QueuePriority::low());
$highPriorityJob = JobPayload::create($this->testJob, QueuePriority::high());
$criticalJob = JobPayload::create($this->testJob, QueuePriority::critical());
// Push in random order
$this->queue->push($lowPriorityJob);
$this->queue->push($criticalJob);
$this->queue->push($highPriorityJob);
expect($this->queue->size())->toBe(3);
// Peek should return critical priority first
$next = $this->queue->peek();
expect($next->priority->isCritical())->toBeTrue();
});
it('accepts jobs with different configurations', function () {
$immediateJob = JobPayload::immediate($this->testJob);
$delayedJob = JobPayload::delayed($this->testJob, Duration::fromSeconds(30));
$backgroundJob = JobPayload::background($this->testJob);
$criticalJob = JobPayload::critical($this->testJob);
$this->queue->push($immediateJob);
$this->queue->push($delayedJob);
$this->queue->push($backgroundJob);
$this->queue->push($criticalJob);
expect($this->queue->size())->toBe(4);
});
});
describe('pop() operation', function () {
it('returns null when queue is empty', function () {
expect($this->queue->pop())->toBeNull();
});
it('returns and removes highest priority job first', function () {
$lowJob = JobPayload::create($this->testJob, QueuePriority::low());
$highJob = JobPayload::create($this->testJob, QueuePriority::high());
$this->queue->push($lowJob);
$this->queue->push($highJob);
$popped = $this->queue->pop();
expect($popped->priority->isHigh())->toBeTrue();
expect($this->queue->size())->toBe(1);
$remaining = $this->queue->pop();
expect($remaining->priority->isLow())->toBeTrue();
expect($this->queue->size())->toBe(0);
});
it('processes FIFO for same priority jobs', function () {
$job1 = (object)['id' => 1];
$job2 = (object)['id' => 2];
$payload1 = JobPayload::create($job1, QueuePriority::normal());
$payload2 = JobPayload::create($job2, QueuePriority::normal());
$this->queue->push($payload1);
$this->queue->push($payload2);
$first = $this->queue->pop();
expect($first->job->id)->toBe(1);
$second = $this->queue->pop();
expect($second->job->id)->toBe(2);
});
});
describe('peek() operation', function () {
it('returns null when queue is empty', function () {
expect($this->queue->peek())->toBeNull();
});
it('returns next job without removing it', function () {
$payload = JobPayload::create($this->testJob);
$this->queue->push($payload);
$peeked = $this->queue->peek();
expect($peeked)->not->toBeNull();
expect($this->queue->size())->toBe(1);
// Should return same job when peeked again
$peekedAgain = $this->queue->peek();
expect($peekedAgain)->toBe($peeked);
});
it('shows highest priority job', function () {
$normalJob = JobPayload::create($this->testJob, QueuePriority::normal());
$criticalJob = JobPayload::create($this->testJob, QueuePriority::critical());
$this->queue->push($normalJob);
$this->queue->push($criticalJob);
$peeked = $this->queue->peek();
expect($peeked->priority->isCritical())->toBeTrue();
});
});
describe('size() operation', function () {
it('returns 0 for empty queue', function () {
expect($this->queue->size())->toBe(0);
});
it('tracks size correctly as jobs are added and removed', function () {
expect($this->queue->size())->toBe(0);
$this->queue->push(JobPayload::create($this->testJob));
expect($this->queue->size())->toBe(1);
$this->queue->push(JobPayload::create($this->testJob));
expect($this->queue->size())->toBe(2);
$this->queue->pop();
expect($this->queue->size())->toBe(1);
$this->queue->pop();
expect($this->queue->size())->toBe(0);
});
});
describe('clear() operation', function () {
it('returns 0 when clearing empty queue', function () {
expect($this->queue->clear())->toBe(0);
});
it('removes all jobs and returns count', function () {
$this->queue->push(JobPayload::create($this->testJob));
$this->queue->push(JobPayload::create($this->testJob));
$this->queue->push(JobPayload::create($this->testJob));
expect($this->queue->size())->toBe(3);
$cleared = $this->queue->clear();
expect($cleared)->toBe(3);
expect($this->queue->size())->toBe(0);
});
it('queue is usable after clearing', function () {
$this->queue->push(JobPayload::create($this->testJob));
$this->queue->clear();
// Should be able to add new jobs
$this->queue->push(JobPayload::create($this->testJob));
expect($this->queue->size())->toBe(1);
});
});
describe('getStats() operation', function () {
it('returns basic stats for empty queue', function () {
$stats = $this->queue->getStats();
expect($stats)->toHaveKey('size');
expect($stats['size'])->toBe(0);
expect($stats)->toHaveKey('priority_breakdown');
expect($stats['priority_breakdown'])->toBe([]);
});
it('provides priority breakdown for populated queue', function () {
$this->queue->push(JobPayload::create($this->testJob, QueuePriority::high()));
$this->queue->push(JobPayload::create($this->testJob, QueuePriority::high()));
$this->queue->push(JobPayload::create($this->testJob, QueuePriority::normal()));
$this->queue->push(JobPayload::create($this->testJob, QueuePriority::low()));
$stats = $this->queue->getStats();
expect($stats['size'])->toBe(4);
expect($stats['priority_breakdown']['high'])->toBe(2);
expect($stats['priority_breakdown']['normal'])->toBe(1);
expect($stats['priority_breakdown']['low'])->toBe(1);
});
it('updates stats as queue changes', function () {
$this->queue->push(JobPayload::create($this->testJob, QueuePriority::critical()));
$this->queue->push(JobPayload::create($this->testJob, QueuePriority::normal()));
$stats = $this->queue->getStats();
expect($stats['size'])->toBe(2);
expect($stats['priority_breakdown']['critical'])->toBe(1);
// Remove one job
$this->queue->pop();
$updatedStats = $this->queue->getStats();
expect($updatedStats['size'])->toBe(1);
expect($updatedStats['priority_breakdown']['critical'] ?? 0)->toBe(0);
expect($updatedStats['priority_breakdown']['normal'])->toBe(1);
});
});
});
describe('Queue Priority Processing', function () {
beforeEach(function () {
$this->queue = new InMemoryQueue();
});
it('processes jobs in correct priority order', function () {
$jobs = [];
// Create jobs with different priorities
$jobs['low'] = JobPayload::create((object)['type' => 'low'], QueuePriority::low());
$jobs['deferred'] = JobPayload::create((object)['type' => 'deferred'], QueuePriority::deferred());
$jobs['normal'] = JobPayload::create((object)['type' => 'normal'], QueuePriority::normal());
$jobs['high'] = JobPayload::create((object)['type' => 'high'], QueuePriority::high());
$jobs['critical'] = JobPayload::create((object)['type' => 'critical'], QueuePriority::critical());
// Push in random order
$this->queue->push($jobs['normal']);
$this->queue->push($jobs['deferred']);
$this->queue->push($jobs['critical']);
$this->queue->push($jobs['low']);
$this->queue->push($jobs['high']);
// Pop should return in priority order
$order = [];
while (($job = $this->queue->pop()) !== null) {
$order[] = $job->job->type;
}
expect($order)->toBe(['critical', 'high', 'normal', 'low', 'deferred']);
});
it('handles custom priority values correctly', function () {
$customHigh = JobPayload::create((object)['id' => 'custom_high'], new QueuePriority(500));
$customLow = JobPayload::create((object)['id' => 'custom_low'], new QueuePriority(-50));
$standardHigh = JobPayload::create((object)['id' => 'standard_high'], QueuePriority::high());
$this->queue->push($customLow);
$this->queue->push($standardHigh);
$this->queue->push($customHigh);
$first = $this->queue->pop();
expect($first->job->id)->toBe('custom_high'); // 500 priority
$second = $this->queue->pop();
expect($second->job->id)->toBe('standard_high'); // 100 priority
$third = $this->queue->pop();
expect($third->job->id)->toBe('custom_low'); // -50 priority
});
});
describe('Queue Edge Cases', function () {
beforeEach(function () {
$this->queue = new InMemoryQueue();
});
it('handles many operations on empty queue gracefully', function () {
expect($this->queue->pop())->toBeNull();
expect($this->queue->pop())->toBeNull();
expect($this->queue->peek())->toBeNull();
expect($this->queue->peek())->toBeNull();
expect($this->queue->size())->toBe(0);
expect($this->queue->clear())->toBe(0);
expect($this->queue->clear())->toBe(0);
});
it('maintains integrity after mixed operations', function () {
$job = (object)['data' => 'test'];
// Complex sequence of operations
$this->queue->push(JobPayload::create($job));
expect($this->queue->size())->toBe(1);
$peeked = $this->queue->peek();
expect($peeked->job->data)->toBe('test');
expect($this->queue->size())->toBe(1);
$popped = $this->queue->pop();
expect($popped->job->data)->toBe('test');
expect($this->queue->size())->toBe(0);
expect($this->queue->peek())->toBeNull();
expect($this->queue->pop())->toBeNull();
// Add more after emptying
$this->queue->push(JobPayload::create($job));
expect($this->queue->size())->toBe(1);
});
it('handles large number of jobs efficiently', function () {
$start = microtime(true);
// Add 1000 jobs
for ($i = 0; $i < 1000; $i++) {
$job = new CounterTestJob($i);
$payload = JobPayload::create($job, QueuePriority::normal());
$this->queue->push($payload);
}
expect($this->queue->size())->toBe(1000);
// Process all jobs
$processed = 0;
while ($this->queue->pop() !== null) {
$processed++;
}
expect($processed)->toBe(1000);
expect($this->queue->size())->toBe(0);
$elapsed = microtime(true) - $start;
expect($elapsed)->toBeLessThan(1.0); // Should complete within 1 second
});
});