107 lines
5.2 KiB
PHP
107 lines
5.2 KiB
PHP
<?php
|
|
|
|
use App\Services\ReplenishmentProcessLock;
|
|
use App\Services\ReplenishmentQueue;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Str;
|
|
|
|
beforeEach(function () {
|
|
$this->freezeTime();
|
|
$this->originalStorage = app()->storagePath();
|
|
$this->heartbeatStorage = $this->originalStorage.'/framework/testing/heartbeat-'.Str::uuid();
|
|
File::ensureDirectoryExists($this->heartbeatStorage.'/framework');
|
|
app()->useStoragePath($this->heartbeatStorage);
|
|
});
|
|
|
|
afterEach(function () {
|
|
app()->useStoragePath($this->originalStorage);
|
|
File::deleteDirectory($this->heartbeatStorage);
|
|
});
|
|
|
|
test('native replenishment command records success or pending recovery without identities', function (int $failed, int $code, string $result) {
|
|
$queue = $this->mock(ReplenishmentQueue::class);
|
|
$queue->shouldReceive('ready')->once()->andReturnTrue();
|
|
$queue->shouldReceive('discover')->once();
|
|
$queue->shouldReceive('drain')->with(12)->once()->andReturn(['processed' => 2, 'failed' => $failed]);
|
|
$this->artisan('sgc:replenishment-tick', ['--limit' => 12])->assertExitCode($code);
|
|
$data = json_decode(File::get(storage_path('framework/replenishment-command-status.json')), true, 32, JSON_THROW_ON_ERROR);
|
|
expect($data)->toHaveCount(6)->and($data['result'])->toBe($result)->and($data['exit_code'])->toBe($code)
|
|
->and($data['finished_at'])->toBe(now()->toIso8601String());
|
|
})->with([[0, 0, 'passed'], [1, 1, 'retry_pending']]);
|
|
|
|
test('interrupted discovery replaces a previous success with recovery pending and preserves the original error', function () {
|
|
File::put(storage_path('framework/replenishment-command-status.json'), '{"result":"passed"}');
|
|
$queue = $this->mock(ReplenishmentQueue::class);
|
|
$queue->shouldReceive('ready')->once()->andReturnTrue();
|
|
$queue->shouldReceive('discover')->once()->andThrow(new RuntimeException('Synthetic discovery interruption'));
|
|
$queue->shouldNotReceive('drain');
|
|
try {
|
|
$this->artisan('sgc:replenishment-tick')->run();
|
|
test()->fail('Expected discovery failure');
|
|
} catch (RuntimeException $error) {
|
|
expect($error->getMessage())->toBe('Synthetic discovery interruption');
|
|
}
|
|
$data = json_decode(File::get(storage_path('framework/replenishment-command-status.json')), true, 32, JSON_THROW_ON_ERROR);
|
|
expect($data['result'])->toBe('retry_pending')->and($data['exit_code'])->toBe(1);
|
|
expect(File::get(storage_path('framework/replenishment-command-status.json')))->not->toContain('Synthetic discovery interruption');
|
|
});
|
|
|
|
test('invalid limits do not publish a successful scheduler observation', function () {
|
|
$queue = $this->mock(ReplenishmentQueue::class);
|
|
$queue->shouldReceive('ready')->once()->andReturnTrue();
|
|
$queue->shouldNotReceive('discover');
|
|
$queue->shouldNotReceive('drain');
|
|
$this->artisan('sgc:replenishment-tick', ['--limit' => 501])->assertExitCode(2);
|
|
expect(File::exists(storage_path('framework/replenishment-command-status.json')))->toBeFalse();
|
|
});
|
|
|
|
test('overlapping native command neither processes work nor replaces the last completed observation', function () {
|
|
$held = new ReplenishmentProcessLock;
|
|
expect($held->acquire())->toBeTrue();
|
|
$path = storage_path('framework/replenishment-command-status.json');
|
|
File::put($path, '{"result":"retry_pending","finished_at":"previous"}');
|
|
$before = File::get($path);
|
|
$queue = $this->mock(ReplenishmentQueue::class);
|
|
$queue->shouldReceive('ready')->once()->andReturnTrue();
|
|
$queue->shouldNotReceive('discover');
|
|
$queue->shouldNotReceive('drain');
|
|
try {
|
|
$this->artisan('sgc:replenishment-tick')->expectsOutput('Verificação periódica já em execução; aguarde a próxima passagem.')->assertExitCode(1);
|
|
expect(File::get($path))->toBe($before);
|
|
} finally {
|
|
$held->release();
|
|
}
|
|
$next = new ReplenishmentProcessLock;
|
|
expect($next->acquire())->toBeTrue();
|
|
$next->release();
|
|
});
|
|
|
|
test('exception and successful runs both release native process exclusion', function () {
|
|
$queue = $this->mock(ReplenishmentQueue::class);
|
|
$queue->shouldReceive('ready')->twice()->andReturnTrue();
|
|
$queue->shouldReceive('discover')->once()->andThrow(new RuntimeException('Synthetic interruption'));
|
|
try {
|
|
$this->artisan('sgc:replenishment-tick')->run();
|
|
test()->fail('Expected interruption');
|
|
} catch (RuntimeException $error) {
|
|
expect($error->getMessage())->toBe('Synthetic interruption');
|
|
}
|
|
$queue->shouldReceive('discover')->once();
|
|
$queue->shouldReceive('drain')->once()->andReturn(['processed' => 0, 'failed' => 0]);
|
|
$this->artisan('sgc:replenishment-tick')->assertExitCode(0);
|
|
$next = new ReplenishmentProcessLock;
|
|
expect($next->acquire())->toBeTrue();
|
|
$next->release();
|
|
});
|
|
|
|
test('destroyed worker lock can be acquired without deleting its control file', function () {
|
|
$first = new ReplenishmentProcessLock;
|
|
expect($first->acquire())->toBeTrue();
|
|
$contender = new ReplenishmentProcessLock;
|
|
expect($contender->acquire())->toBeFalse();
|
|
unset($first);
|
|
expect(File::exists(storage_path('framework/replenishment-command.lock')))->toBeTrue();
|
|
expect($contender->acquire())->toBeTrue();
|
|
$contender->release();
|
|
});
|