feat(Deployment): Integrate Ansible deployment via PHP deployment pipeline
- Create AnsibleDeployStage using framework's Process module for secure command execution - Integrate AnsibleDeployStage into DeploymentPipelineCommands for production deployments - Add force_deploy flag support in Ansible playbook to override stale locks - Use PHP deployment module as orchestrator (php console.php deploy:production) - Fix ErrorAggregationInitializer to use Environment class instead of $_ENV superglobal Architecture: - BuildStage → AnsibleDeployStage → HealthCheckStage for production - Process module provides timeout, error handling, and output capture - Ansible playbook supports rollback via rollback-git-based.yml - Zero-downtime deployments with health checks
This commit is contained in:
270
tests/Unit/Framework/Worker/ScheduleDiscoveryServiceTest.php
Normal file
270
tests/Unit/Framework/Worker/ScheduleDiscoveryServiceTest.php
Normal file
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Discovery\DiscoveryRegistry;
|
||||
use App\Framework\Scheduler\Services\SchedulerService;
|
||||
use App\Framework\Worker\Every;
|
||||
use App\Framework\Worker\Schedule;
|
||||
use App\Framework\Worker\ScheduleDiscoveryService;
|
||||
|
||||
// Test job classes with Schedule attribute
|
||||
#[Schedule(at: new Every(minutes: 5))]
|
||||
final class TestScheduledJob
|
||||
{
|
||||
public function handle(): array
|
||||
{
|
||||
return ['status' => 'success', 'executed_at' => time()];
|
||||
}
|
||||
}
|
||||
|
||||
#[Schedule(at: new Every(hours: 1))]
|
||||
final class HourlyTestJob
|
||||
{
|
||||
public function __invoke(): string
|
||||
{
|
||||
return 'hourly job executed';
|
||||
}
|
||||
}
|
||||
|
||||
#[Schedule(at: new Every(days: 1))]
|
||||
final class DailyTestJob
|
||||
{
|
||||
// No handle() or __invoke() - should throw exception
|
||||
}
|
||||
|
||||
describe('ScheduleDiscoveryService', function () {
|
||||
beforeEach(function () {
|
||||
// Create mock DiscoveryRegistry
|
||||
$this->discoveryRegistry = Mockery::mock(DiscoveryRegistry::class);
|
||||
|
||||
// Create mock SchedulerService
|
||||
$this->schedulerService = Mockery::mock(SchedulerService::class);
|
||||
|
||||
$this->scheduleDiscovery = new ScheduleDiscoveryService(
|
||||
$this->discoveryRegistry,
|
||||
$this->schedulerService
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Mockery::close();
|
||||
});
|
||||
|
||||
it('discovers and registers scheduled jobs', function () {
|
||||
// Mock discovery registry to return test job classes
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([
|
||||
TestScheduledJob::class,
|
||||
HourlyTestJob::class
|
||||
]);
|
||||
|
||||
// Expect scheduler to be called for each job
|
||||
$this->schedulerService
|
||||
->shouldReceive('schedule')
|
||||
->twice()
|
||||
->withArgs(function ($taskId, $schedule, $task) {
|
||||
// Verify task ID is kebab-case
|
||||
expect($taskId)->toMatch('/^[a-z0-9-]+$/');
|
||||
|
||||
// Verify schedule is IntervalSchedule
|
||||
expect($schedule)->toBeInstanceOf(\App\Framework\Scheduler\Schedules\IntervalSchedule::class);
|
||||
|
||||
// Verify task is callable
|
||||
expect($task)->toBeCallable();
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
$registered = $this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
expect($registered)->toBe(2);
|
||||
});
|
||||
|
||||
it('converts Every to IntervalSchedule correctly', function () {
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([TestScheduledJob::class]);
|
||||
|
||||
$this->schedulerService
|
||||
->shouldReceive('schedule')
|
||||
->once()
|
||||
->withArgs(function ($taskId, $schedule, $task) {
|
||||
// TestScheduledJob has Every(minutes: 5) = 300 seconds
|
||||
// IntervalSchedule should use this duration
|
||||
expect($schedule)->toBeInstanceOf(\App\Framework\Scheduler\Schedules\IntervalSchedule::class);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
$this->scheduleDiscovery->discoverAndRegister();
|
||||
});
|
||||
|
||||
it('generates kebab-case task IDs from class names', function () {
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([TestScheduledJob::class, HourlyTestJob::class]);
|
||||
|
||||
$capturedTaskIds = [];
|
||||
|
||||
$this->schedulerService
|
||||
->shouldReceive('schedule')
|
||||
->twice()
|
||||
->withArgs(function ($taskId) use (&$capturedTaskIds) {
|
||||
$capturedTaskIds[] = $taskId;
|
||||
return true;
|
||||
});
|
||||
|
||||
$this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
expect($capturedTaskIds)->toContain('test-scheduled-job');
|
||||
expect($capturedTaskIds)->toContain('hourly-test-job');
|
||||
});
|
||||
|
||||
it('executes jobs with handle() method', function () {
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([TestScheduledJob::class]);
|
||||
|
||||
$capturedTask = null;
|
||||
|
||||
$this->schedulerService
|
||||
->shouldReceive('schedule')
|
||||
->once()
|
||||
->withArgs(function ($taskId, $schedule, $task) use (&$capturedTask) {
|
||||
$capturedTask = $task;
|
||||
return true;
|
||||
});
|
||||
|
||||
$this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
// Execute the captured task
|
||||
$result = $capturedTask();
|
||||
|
||||
expect($result)->toBeArray();
|
||||
expect($result['status'])->toBe('success');
|
||||
expect($result)->toHaveKey('executed_at');
|
||||
});
|
||||
|
||||
it('executes callable jobs', function () {
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([HourlyTestJob::class]);
|
||||
|
||||
$capturedTask = null;
|
||||
|
||||
$this->schedulerService
|
||||
->shouldReceive('schedule')
|
||||
->once()
|
||||
->withArgs(function ($taskId, $schedule, $task) use (&$capturedTask) {
|
||||
$capturedTask = $task;
|
||||
return true;
|
||||
});
|
||||
|
||||
$this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
// Execute the captured task
|
||||
$result = $capturedTask();
|
||||
|
||||
expect($result)->toBe('hourly job executed');
|
||||
});
|
||||
|
||||
it('throws exception for jobs without handle() or __invoke()', function () {
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([DailyTestJob::class]);
|
||||
|
||||
$capturedTask = null;
|
||||
|
||||
$this->schedulerService
|
||||
->shouldReceive('schedule')
|
||||
->once()
|
||||
->withArgs(function ($taskId, $schedule, $task) use (&$capturedTask) {
|
||||
$capturedTask = $task;
|
||||
return true;
|
||||
});
|
||||
|
||||
$this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
// Executing the task should throw exception
|
||||
expect(fn() => $capturedTask())->toThrow(
|
||||
\RuntimeException::class,
|
||||
'must have handle() method or be callable'
|
||||
);
|
||||
});
|
||||
|
||||
it('handles multiple Schedule attributes on same class', function () {
|
||||
// Create a test class with multiple schedules (IS_REPEATABLE)
|
||||
$testClass = new class {
|
||||
#[Schedule(at: new Every(minutes: 5))]
|
||||
#[Schedule(at: new Every(hours: 1))]
|
||||
public function handle(): string
|
||||
{
|
||||
return 'multi-schedule job';
|
||||
}
|
||||
};
|
||||
|
||||
$className = $testClass::class;
|
||||
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([$className]);
|
||||
|
||||
// Should register twice (one for each Schedule attribute)
|
||||
$this->schedulerService
|
||||
->shouldReceive('schedule')
|
||||
->twice();
|
||||
|
||||
$registered = $this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
expect($registered)->toBe(2);
|
||||
});
|
||||
|
||||
it('returns 0 when no scheduled jobs found', function () {
|
||||
$this->discoveryRegistry
|
||||
->shouldReceive('getClassesWithAttribute')
|
||||
->with(Schedule::class)
|
||||
->once()
|
||||
->andReturn([]);
|
||||
|
||||
$this->schedulerService
|
||||
->shouldReceive('schedule')
|
||||
->never();
|
||||
|
||||
$registered = $this->scheduleDiscovery->discoverAndRegister();
|
||||
|
||||
expect($registered)->toBe(0);
|
||||
});
|
||||
|
||||
it('delegates getScheduledTasks to SchedulerService', function () {
|
||||
$expectedTasks = [
|
||||
['taskId' => 'test-task-1'],
|
||||
['taskId' => 'test-task-2']
|
||||
];
|
||||
|
||||
$this->schedulerService
|
||||
->shouldReceive('getScheduledTasks')
|
||||
->once()
|
||||
->andReturn($expectedTasks);
|
||||
|
||||
$tasks = $this->scheduleDiscovery->getScheduledTasks();
|
||||
|
||||
expect($tasks)->toBe($expectedTasks);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user