- 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
493 lines
22 KiB
PHP
493 lines
22 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\DI\Container;
|
|
use App\Framework\DI\DefaultContainer;
|
|
use App\Framework\Queue\Queue;
|
|
use App\Framework\Queue\QueueInitializer;
|
|
use App\Framework\Queue\QueueDependencyInitializer;
|
|
|
|
// Queue service interfaces
|
|
use App\Framework\Queue\Interfaces\DistributedLockInterface;
|
|
use App\Framework\Queue\Contracts\JobProgressTrackerInterface;
|
|
use App\Framework\Queue\Contracts\JobDependencyManagerInterface;
|
|
use App\Framework\Queue\Contracts\DeadLetterQueueInterface;
|
|
use App\Framework\Queue\Contracts\JobChainManagerInterface;
|
|
use App\Framework\Queue\Services\JobMetricsManagerInterface;
|
|
|
|
// Concrete implementations
|
|
use App\Framework\Queue\Services\DatabaseDistributedLock;
|
|
use App\Framework\Queue\Services\DatabaseJobProgressTracker;
|
|
use App\Framework\Queue\Services\DatabaseJobDependencyManager;
|
|
use App\Framework\Queue\Services\DatabaseDeadLetterQueue;
|
|
use App\Framework\Queue\Services\DatabaseJobChainManager;
|
|
use App\Framework\Queue\Services\JobMetricsManager;
|
|
|
|
// Additional services
|
|
use App\Framework\Queue\Services\WorkerRegistry;
|
|
use App\Framework\Queue\Services\JobDistributionService;
|
|
use App\Framework\Queue\Services\WorkerHealthCheckService;
|
|
use App\Framework\Queue\Services\FailoverRecoveryService;
|
|
use App\Framework\Queue\Services\DependencyResolutionEngine;
|
|
|
|
// Framework dependencies
|
|
use App\Framework\Database\EntityManagerInterface;
|
|
use App\Framework\Logging\Logger;
|
|
|
|
describe('Queue Service Registration', function () {
|
|
|
|
beforeEach(function () {
|
|
$this->container = new DefaultContainer();
|
|
|
|
// Mock essential framework dependencies
|
|
$this->mockEntityManager = new class implements EntityManagerInterface {
|
|
public function persist(object $entity): void {}
|
|
public function find(string $className, mixed $id): ?object { return null; }
|
|
public function flush(): void {}
|
|
public function remove(object $entity): void {}
|
|
public function clear(): void {}
|
|
public function detach(object $entity): void {}
|
|
public function contains(object $entity): bool { return false; }
|
|
public function refresh(object $entity): void {}
|
|
public function createQueryBuilder(): object { return new stdClass(); }
|
|
public function getRepository(string $className): object { return new stdClass(); }
|
|
public function beginTransaction(): void {}
|
|
public function commit(): void {}
|
|
public function rollback(): void {}
|
|
public function isTransactionActive(): bool { return false; }
|
|
};
|
|
|
|
$this->mockLogger = new class implements Logger {
|
|
public function emergency(string $message, array $context = []): void {}
|
|
public function alert(string $message, array $context = []): void {}
|
|
public function critical(string $message, array $context = []): void {}
|
|
public function error(string $message, array $context = []): void {}
|
|
public function warning(string $message, array $context = []): void {}
|
|
public function notice(string $message, array $context = []): void {}
|
|
public function info(string $message, array $context = []): void {}
|
|
public function debug(string $message, array $context = []): void {}
|
|
public function log(string $level, string $message, array $context = []): void {}
|
|
};
|
|
|
|
// Register mocked dependencies
|
|
$this->container->instance(EntityManagerInterface::class, $this->mockEntityManager);
|
|
$this->container->instance(Logger::class, $this->mockLogger);
|
|
});
|
|
|
|
describe('Core Queue Service', function () {
|
|
it('registers Queue service correctly', function () {
|
|
// This test verifies that the QueueInitializer properly registers a Queue
|
|
// Note: This will fallback to FileQueue since Redis is not available in tests
|
|
|
|
$queueInitializer = new QueueInitializer(
|
|
pathProvider: new class {
|
|
public function resolvePath(string $path): string {
|
|
return '/home/michael/dev/michaelschiemer/tests/tmp/queue/';
|
|
}
|
|
}
|
|
);
|
|
|
|
$queue = $queueInitializer($this->mockLogger);
|
|
|
|
expect($queue)->toBeInstanceOf(Queue::class);
|
|
expect($queue)->not->toBeNull();
|
|
});
|
|
|
|
it('Queue service is accessible from container after registration', function () {
|
|
// Register queue manually for testing
|
|
$this->container->singleton(Queue::class, function() {
|
|
return new \App\Framework\Queue\InMemoryQueue();
|
|
});
|
|
|
|
$queue = $this->container->get(Queue::class);
|
|
expect($queue)->toBeInstanceOf(Queue::class);
|
|
});
|
|
});
|
|
|
|
describe('Queue Dependencies Registration', function () {
|
|
beforeEach(function () {
|
|
// Initialize the queue dependency system
|
|
$this->dependencyInitializer = new QueueDependencyInitializer();
|
|
|
|
// Register a basic queue interface for the dependencies
|
|
$this->container->singleton(\App\Framework\Queue\Contracts\QueueInterface::class, function() {
|
|
return new class implements \App\Framework\Queue\Contracts\QueueInterface {
|
|
public function push(mixed $job): void {}
|
|
public function pop(): mixed { return null; }
|
|
public function size(): int { return 0; }
|
|
};
|
|
});
|
|
|
|
// Register EventDispatcher mock
|
|
$this->container->singleton(\App\Framework\Core\Events\EventDispatcherInterface::class, function() {
|
|
return new class implements \App\Framework\Core\Events\EventDispatcherInterface {
|
|
public function dispatch(object $event): void {}
|
|
public function listen(string $event, callable $listener): void {}
|
|
};
|
|
});
|
|
});
|
|
|
|
it('registers JobDependencyManagerInterface', function () {
|
|
$dependencyManager = $this->dependencyInitializer->__invoke($this->container);
|
|
|
|
expect($dependencyManager)->toBeInstanceOf(JobDependencyManagerInterface::class);
|
|
expect($dependencyManager)->toBeInstanceOf(DatabaseJobDependencyManager::class);
|
|
|
|
// Should be accessible from container
|
|
$retrieved = $this->container->get(JobDependencyManagerInterface::class);
|
|
expect($retrieved)->toBe($dependencyManager);
|
|
});
|
|
|
|
it('registers JobChainManagerInterface', function () {
|
|
$this->dependencyInitializer->__invoke($this->container);
|
|
|
|
$chainManager = $this->container->get(JobChainManagerInterface::class);
|
|
expect($chainManager)->toBeInstanceOf(JobChainManagerInterface::class);
|
|
expect($chainManager)->toBeInstanceOf(DatabaseJobChainManager::class);
|
|
});
|
|
|
|
it('registers DependencyResolutionEngine as singleton', function () {
|
|
$this->dependencyInitializer->__invoke($this->container);
|
|
|
|
$engine1 = $this->container->get(DependencyResolutionEngine::class);
|
|
$engine2 = $this->container->get(DependencyResolutionEngine::class);
|
|
|
|
expect($engine1)->toBeInstanceOf(DependencyResolutionEngine::class);
|
|
expect($engine1)->toBe($engine2); // Should be same instance (singleton)
|
|
});
|
|
|
|
it('registers JobMetricsManager as singleton', function () {
|
|
$this->dependencyInitializer->__invoke($this->container);
|
|
|
|
$metrics1 = $this->container->get(JobMetricsManager::class);
|
|
$metrics2 = $this->container->get(JobMetricsManager::class);
|
|
|
|
expect($metrics1)->toBeInstanceOf(JobMetricsManager::class);
|
|
expect($metrics1)->toBe($metrics2); // Should be same instance (singleton)
|
|
});
|
|
});
|
|
|
|
describe('Individual Service Registration', function () {
|
|
it('can register DistributedLockInterface service', function () {
|
|
$lockService = new DatabaseDistributedLock(
|
|
entityManager: $this->mockEntityManager,
|
|
logger: $this->mockLogger
|
|
);
|
|
|
|
$this->container->singleton(DistributedLockInterface::class, $lockService);
|
|
|
|
$retrieved = $this->container->get(DistributedLockInterface::class);
|
|
expect($retrieved)->toBe($lockService);
|
|
expect($retrieved)->toBeInstanceOf(DistributedLockInterface::class);
|
|
});
|
|
|
|
it('can register JobProgressTrackerInterface service', function () {
|
|
$progressTracker = new DatabaseJobProgressTracker(
|
|
entityManager: $this->mockEntityManager,
|
|
logger: $this->mockLogger
|
|
);
|
|
|
|
$this->container->singleton(JobProgressTrackerInterface::class, $progressTracker);
|
|
|
|
$retrieved = $this->container->get(JobProgressTrackerInterface::class);
|
|
expect($retrieved)->toBe($progressTracker);
|
|
expect($retrieved)->toBeInstanceOf(JobProgressTrackerInterface::class);
|
|
});
|
|
|
|
it('can register DeadLetterQueueInterface service', function () {
|
|
$deadLetterQueue = new DatabaseDeadLetterQueue(
|
|
entityManager: $this->mockEntityManager,
|
|
logger: $this->mockLogger
|
|
);
|
|
|
|
$this->container->singleton(DeadLetterQueueInterface::class, $deadLetterQueue);
|
|
|
|
$retrieved = $this->container->get(DeadLetterQueueInterface::class);
|
|
expect($retrieved)->toBe($deadLetterQueue);
|
|
expect($retrieved)->toBeInstanceOf(DeadLetterQueueInterface::class);
|
|
});
|
|
|
|
it('can register WorkerRegistry service', function () {
|
|
$workerRegistry = new WorkerRegistry(
|
|
entityManager: $this->mockEntityManager,
|
|
logger: $this->mockLogger
|
|
);
|
|
|
|
$this->container->singleton(WorkerRegistry::class, $workerRegistry);
|
|
|
|
$retrieved = $this->container->get(WorkerRegistry::class);
|
|
expect($retrieved)->toBe($workerRegistry);
|
|
expect($retrieved)->toBeInstanceOf(WorkerRegistry::class);
|
|
});
|
|
|
|
it('can register JobDistributionService', function () {
|
|
// First register dependencies
|
|
$this->container->singleton(WorkerRegistry::class, new WorkerRegistry(
|
|
$this->mockEntityManager,
|
|
$this->mockLogger
|
|
));
|
|
|
|
$this->container->singleton(\App\Framework\Queue\Contracts\QueueInterface::class, function() {
|
|
return new class implements \App\Framework\Queue\Contracts\QueueInterface {
|
|
public function push(mixed $job): void {}
|
|
public function pop(): mixed { return null; }
|
|
public function size(): int { return 0; }
|
|
};
|
|
});
|
|
|
|
$distributionService = new JobDistributionService(
|
|
workerRegistry: $this->container->get(WorkerRegistry::class),
|
|
queue: $this->container->get(\App\Framework\Queue\Contracts\QueueInterface::class),
|
|
logger: $this->mockLogger
|
|
);
|
|
|
|
$this->container->singleton(JobDistributionService::class, $distributionService);
|
|
|
|
$retrieved = $this->container->get(JobDistributionService::class);
|
|
expect($retrieved)->toBe($distributionService);
|
|
expect($retrieved)->toBeInstanceOf(JobDistributionService::class);
|
|
});
|
|
});
|
|
|
|
describe('Service Dependencies and Integration', function () {
|
|
it('services have proper dependencies injected', function () {
|
|
$this->dependencyInitializer->__invoke($this->container);
|
|
|
|
$dependencyManager = $this->container->get(JobDependencyManagerInterface::class);
|
|
$chainManager = $this->container->get(JobChainManagerInterface::class);
|
|
$resolutionEngine = $this->container->get(DependencyResolutionEngine::class);
|
|
|
|
// Verify dependencies are properly injected
|
|
expect($dependencyManager)->toBeInstanceOf(DatabaseJobDependencyManager::class);
|
|
expect($chainManager)->toBeInstanceOf(DatabaseJobChainManager::class);
|
|
expect($resolutionEngine)->toBeInstanceOf(DependencyResolutionEngine::class);
|
|
|
|
// These services should be functional (not throw errors)
|
|
expect(fn() => $dependencyManager)->not->toThrow();
|
|
expect(fn() => $chainManager)->not->toThrow();
|
|
expect(fn() => $resolutionEngine)->not->toThrow();
|
|
});
|
|
|
|
it('can resolve complex dependency graph', function () {
|
|
$this->dependencyInitializer->__invoke($this->container);
|
|
|
|
// Add additional services
|
|
$this->container->singleton(DistributedLockInterface::class, function() {
|
|
return new DatabaseDistributedLock(
|
|
$this->mockEntityManager,
|
|
$this->mockLogger
|
|
);
|
|
});
|
|
|
|
$this->container->singleton(JobProgressTrackerInterface::class, function() {
|
|
return new DatabaseJobProgressTracker(
|
|
$this->mockEntityManager,
|
|
$this->mockLogger
|
|
);
|
|
});
|
|
|
|
// All services should be resolvable
|
|
$services = [
|
|
JobDependencyManagerInterface::class,
|
|
JobChainManagerInterface::class,
|
|
DependencyResolutionEngine::class,
|
|
JobMetricsManager::class,
|
|
DistributedLockInterface::class,
|
|
JobProgressTrackerInterface::class,
|
|
];
|
|
|
|
foreach ($services as $serviceInterface) {
|
|
$service = $this->container->get($serviceInterface);
|
|
expect($service)->not->toBeNull();
|
|
expect($service)->toBeObject();
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Service Lifecycle Management', function () {
|
|
it('singleton services maintain state across requests', function () {
|
|
$this->dependencyInitializer->__invoke($this->container);
|
|
|
|
$metrics1 = $this->container->get(JobMetricsManager::class);
|
|
$metrics2 = $this->container->get(JobMetricsManager::class);
|
|
|
|
// Should be exact same instance
|
|
expect($metrics1)->toBe($metrics2);
|
|
});
|
|
|
|
it('services can be replaced for testing', function () {
|
|
$this->dependencyInitializer->__invoke($this->container);
|
|
|
|
// Get original service
|
|
$original = $this->container->get(JobMetricsManager::class);
|
|
|
|
// Create mock replacement
|
|
$mock = new class implements JobMetricsManagerInterface {
|
|
public function recordJobExecution(\App\Framework\Queue\ValueObjects\JobId $jobId, float $executionTime): void {}
|
|
public function recordJobFailure(\App\Framework\Queue\ValueObjects\JobId $jobId, string $errorMessage): void {}
|
|
public function getJobMetrics(\App\Framework\Queue\ValueObjects\JobId $jobId): ?\App\Framework\Queue\ValueObjects\JobMetrics { return null; }
|
|
public function getQueueMetrics(\App\Framework\Queue\ValueObjects\QueueName $queueName): \App\Framework\Queue\ValueObjects\QueueMetrics {
|
|
return new \App\Framework\Queue\ValueObjects\QueueMetrics(
|
|
queueName: $queueName,
|
|
totalJobs: 0,
|
|
completedJobs: 0,
|
|
failedJobs: 0,
|
|
averageExecutionTime: 0.0
|
|
);
|
|
}
|
|
public function getSystemMetrics(): array { return []; }
|
|
};
|
|
|
|
// Replace with mock
|
|
$this->container->instance(JobMetricsManagerInterface::class, $mock);
|
|
|
|
$replaced = $this->container->get(JobMetricsManagerInterface::class);
|
|
expect($replaced)->toBe($mock);
|
|
expect($replaced)->not->toBe($original);
|
|
});
|
|
|
|
it('handles missing dependencies gracefully', function () {
|
|
// Don't register EventDispatcher - this should cause failure
|
|
unset($this->container);
|
|
$this->container = new DefaultContainer();
|
|
$this->container->instance(EntityManagerInterface::class, $this->mockEntityManager);
|
|
$this->container->instance(Logger::class, $this->mockLogger);
|
|
|
|
$dependencyInitializer = new QueueDependencyInitializer();
|
|
|
|
// This should fail due to missing dependencies
|
|
expect(fn() => $dependencyInitializer->__invoke($this->container))
|
|
->toThrow();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Queue Service Integration Test', function () {
|
|
|
|
beforeEach(function () {
|
|
$this->container = new DefaultContainer();
|
|
|
|
// Register all required mocks
|
|
$this->container->instance(EntityManagerInterface::class, new class implements EntityManagerInterface {
|
|
public function persist(object $entity): void {}
|
|
public function find(string $className, mixed $id): ?object { return null; }
|
|
public function flush(): void {}
|
|
public function remove(object $entity): void {}
|
|
public function clear(): void {}
|
|
public function detach(object $entity): void {}
|
|
public function contains(object $entity): bool { return false; }
|
|
public function refresh(object $entity): void {}
|
|
public function createQueryBuilder(): object { return new stdClass(); }
|
|
public function getRepository(string $className): object { return new stdClass(); }
|
|
public function beginTransaction(): void {}
|
|
public function commit(): void {}
|
|
public function rollback(): void {}
|
|
public function isTransactionActive(): bool { return false; }
|
|
});
|
|
|
|
$this->container->instance(Logger::class, new class implements Logger {
|
|
public function emergency(string $message, array $context = []): void {}
|
|
public function alert(string $message, array $context = []): void {}
|
|
public function critical(string $message, array $context = []): void {}
|
|
public function error(string $message, array $context = []): void {}
|
|
public function warning(string $message, array $context = []): void {}
|
|
public function notice(string $message, array $context = []): void {}
|
|
public function info(string $message, array $context = []): void {}
|
|
public function debug(string $message, array $context = []): void {}
|
|
public function log(string $level, string $message, array $context = []): void {}
|
|
});
|
|
|
|
$this->container->singleton(\App\Framework\Queue\Contracts\QueueInterface::class, function() {
|
|
return new class implements \App\Framework\Queue\Contracts\QueueInterface {
|
|
public function push(mixed $job): void {}
|
|
public function pop(): mixed { return null; }
|
|
public function size(): int { return 0; }
|
|
};
|
|
});
|
|
|
|
$this->container->singleton(\App\Framework\Core\Events\EventDispatcherInterface::class, function() {
|
|
return new class implements \App\Framework\Core\Events\EventDispatcherInterface {
|
|
public function dispatch(object $event): void {}
|
|
public function listen(string $event, callable $listener): void {}
|
|
};
|
|
});
|
|
});
|
|
|
|
it('can initialize complete queue system', function () {
|
|
// Initialize all queue services
|
|
$dependencyInitializer = new QueueDependencyInitializer();
|
|
$dependencyInitializer->__invoke($this->container);
|
|
|
|
// Register additional services that would normally be auto-registered
|
|
$this->container->singleton(DistributedLockInterface::class, function($container) {
|
|
return new DatabaseDistributedLock(
|
|
$container->get(EntityManagerInterface::class),
|
|
$container->get(Logger::class)
|
|
);
|
|
});
|
|
|
|
$this->container->singleton(JobProgressTrackerInterface::class, function($container) {
|
|
return new DatabaseJobProgressTracker(
|
|
$container->get(EntityManagerInterface::class),
|
|
$container->get(Logger::class)
|
|
);
|
|
});
|
|
|
|
$this->container->singleton(DeadLetterQueueInterface::class, function($container) {
|
|
return new DatabaseDeadLetterQueue(
|
|
$container->get(EntityManagerInterface::class),
|
|
$container->get(Logger::class)
|
|
);
|
|
});
|
|
|
|
$this->container->singleton(WorkerRegistry::class, function($container) {
|
|
return new WorkerRegistry(
|
|
$container->get(EntityManagerInterface::class),
|
|
$container->get(Logger::class)
|
|
);
|
|
});
|
|
|
|
// Verify all 9 expected queue services are registered
|
|
$expectedServices = [
|
|
DistributedLockInterface::class,
|
|
WorkerRegistry::class,
|
|
JobDistributionService::class, // This might not be auto-registered
|
|
WorkerHealthCheckService::class, // This might not be auto-registered
|
|
FailoverRecoveryService::class, // This might not be auto-registered
|
|
JobProgressTrackerInterface::class,
|
|
DeadLetterQueueInterface::class,
|
|
JobMetricsManagerInterface::class,
|
|
JobDependencyManagerInterface::class,
|
|
];
|
|
|
|
$registeredCount = 0;
|
|
foreach ($expectedServices as $service) {
|
|
try {
|
|
$instance = $this->container->get($service);
|
|
if ($instance !== null) {
|
|
$registeredCount++;
|
|
}
|
|
} catch (\Exception $e) {
|
|
// Service not registered, which is expected for some
|
|
}
|
|
}
|
|
|
|
// At least the core services should be registered
|
|
expect($registeredCount)->toBeGreaterThan(4);
|
|
});
|
|
|
|
it('services can interact without errors', function () {
|
|
$dependencyInitializer = new QueueDependencyInitializer();
|
|
$dependencyInitializer->__invoke($this->container);
|
|
|
|
$dependencyManager = $this->container->get(JobDependencyManagerInterface::class);
|
|
$chainManager = $this->container->get(JobChainManagerInterface::class);
|
|
$metricsManager = $this->container->get(JobMetricsManager::class);
|
|
|
|
// Basic interaction tests (should not throw)
|
|
expect(fn() => $dependencyManager)->not->toThrow();
|
|
expect(fn() => $chainManager)->not->toThrow();
|
|
expect(fn() => $metricsManager)->not->toThrow();
|
|
});
|
|
}); |