203 lines
7.9 KiB
PHP
203 lines
7.9 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit;
|
|
|
|
use App\Services\BackupOperationState;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Il "posto unico": backup, ripristino e azzeramento lavorano tutti sullo
|
|
* stesso database e sullo stesso disco, quindi due in parallelo non si
|
|
* rallentano soltanto, si sovrascrivono. Qui si verifica che il posto venga
|
|
* occupato, rilasciato e — soprattutto — che non resti occupato per sempre.
|
|
*
|
|
* Gira su cache 'array' (phpunit.xml): nessun Redis in mezzo.
|
|
*/
|
|
class BackupOperationStateTest extends TestCase
|
|
{
|
|
private const CHIAVE_CORRENTE = 'backup-op:corrente';
|
|
|
|
private BackupOperationState $stato;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->stato = new BackupOperationState();
|
|
}
|
|
|
|
// ─── Il posto unico ─────────────────────────────────────────
|
|
|
|
public function test_senza_operazioni_il_posto_e_libero(): void
|
|
{
|
|
$this->assertNull($this->stato->corrente());
|
|
}
|
|
|
|
public function test_avviare_un_operazione_occupa_il_posto(): void
|
|
{
|
|
$this->stato->initialize('op-1', BackupOperationState::TIPO_BACKUP, 'Preparazione', 10);
|
|
|
|
$this->assertSame('op-1', $this->stato->corrente());
|
|
}
|
|
|
|
public function test_il_completamento_libera_il_posto(): void
|
|
{
|
|
$this->stato->initialize('op-1', BackupOperationState::TIPO_BACKUP, 'Preparazione', 10);
|
|
$this->stato->markCompleted('op-1', ['file' => 'backup.zip']);
|
|
|
|
$this->assertNull($this->stato->corrente());
|
|
$this->assertSame('completed', $this->stato->get('op-1')['status']);
|
|
}
|
|
|
|
public function test_il_fallimento_libera_il_posto(): void
|
|
{
|
|
$this->stato->initialize('op-1', BackupOperationState::TIPO_RESTORE, 'Estrazione', 10);
|
|
$this->stato->markFailed('op-1', 'disco pieno');
|
|
|
|
$this->assertNull($this->stato->corrente());
|
|
|
|
$finale = $this->stato->get('op-1');
|
|
$this->assertSame('failed', $finale['status']);
|
|
$this->assertSame('disco pieno', $finale['error']);
|
|
$this->assertStringContainsString('ERRORE: disco pieno', end($finale['logs']));
|
|
}
|
|
|
|
/**
|
|
* Il caso che il TTL da solo non copre: lo stato dell'operazione e' scaduto
|
|
* ma il puntatore no. Senza questa pulizia il posto resterebbe occupato da
|
|
* un'operazione che non esiste piu'.
|
|
*/
|
|
public function test_un_puntatore_orfano_non_tiene_occupato_il_posto(): void
|
|
{
|
|
Cache::put(self::CHIAVE_CORRENTE, 'op-svanita', 3600);
|
|
|
|
$this->assertNull($this->stato->corrente());
|
|
$this->assertNull(Cache::get(self::CHIAVE_CORRENTE), 'Il puntatore orfano va anche rimosso.');
|
|
}
|
|
|
|
public function test_un_puntatore_a_un_operazione_gia_chiusa_non_tiene_occupato_il_posto(): void
|
|
{
|
|
$this->stato->initialize('op-1', BackupOperationState::TIPO_RESET, 'Backup preventivo', 5);
|
|
|
|
// Chiusura "a mano", come se il worker fosse morto dopo aver scritto lo
|
|
// stato ma prima di rilasciare il posto.
|
|
$this->stato->update('op-1', function (array $s) {
|
|
$s['status'] = 'completed';
|
|
|
|
return $s;
|
|
});
|
|
Cache::put(self::CHIAVE_CORRENTE, 'op-1', 3600);
|
|
|
|
$this->assertNull($this->stato->corrente());
|
|
$this->assertNull(Cache::get(self::CHIAVE_CORRENTE));
|
|
}
|
|
|
|
/**
|
|
* Senza il confronto sull'id, la chiusura tardiva di un'operazione vecchia
|
|
* sbloccherebbe quella appena avviata da qualcun altro — cioe' proprio il
|
|
* parallelismo che il posto unico esiste per impedire.
|
|
*/
|
|
public function test_un_operazione_vecchia_che_si_chiude_non_libera_il_posto_della_nuova(): void
|
|
{
|
|
$this->stato->initialize('op-vecchia', BackupOperationState::TIPO_BACKUP, 'Preparazione', 3);
|
|
$this->stato->initialize('op-nuova', BackupOperationState::TIPO_RESTORE, 'Estrazione', 3);
|
|
|
|
$this->stato->markCompleted('op-vecchia');
|
|
|
|
$this->assertSame('op-nuova', $this->stato->corrente());
|
|
}
|
|
|
|
/**
|
|
* L'azzeramento si ferma in attesa del download e riparte da un secondo
|
|
* clic: in quella pausa il posto deve restare occupato, altrimenti un'altra
|
|
* operazione potrebbe infilarsi fra il backup preventivo e la cancellazione.
|
|
*/
|
|
public function test_l_attesa_del_download_tiene_il_posto_occupato(): void
|
|
{
|
|
$this->stato->initialize('op-1', BackupOperationState::TIPO_RESET, 'Backup preventivo', 4);
|
|
$this->stato->markAttesaDownload('op-1', ['file' => 'backup.zip']);
|
|
|
|
$this->assertSame('op-1', $this->stato->corrente());
|
|
|
|
$corrente = $this->stato->get('op-1');
|
|
$this->assertSame(BackupOperationState::FASE_ATTESA_DOWNLOAD, $corrente['fase']);
|
|
$this->assertSame('running', $corrente['status']);
|
|
$this->assertSame($corrente['total'], $corrente['processed'], 'La barra deve risultare piena.');
|
|
}
|
|
|
|
// ─── Avanzamento ────────────────────────────────────────────
|
|
|
|
public function test_il_cambio_di_fase_riazzera_l_avanzamento_sul_nuovo_totale(): void
|
|
{
|
|
$this->stato->initialize('op-1', BackupOperationState::TIPO_RESTORE, 'Estrazione', 100);
|
|
$this->stato->avanza('op-1', 80);
|
|
|
|
$this->stato->fase('op-1', 'Scrittura file', 7);
|
|
|
|
$corrente = $this->stato->get('op-1');
|
|
$this->assertSame('Scrittura file', $corrente['fase']);
|
|
$this->assertSame(7, $corrente['total']);
|
|
$this->assertSame(0, $corrente['processed']);
|
|
}
|
|
|
|
public function test_il_completamento_porta_la_barra_al_totale(): void
|
|
{
|
|
$this->stato->initialize('op-1', BackupOperationState::TIPO_BACKUP, 'Compressione', 12);
|
|
$this->stato->avanza('op-1', 9);
|
|
|
|
$this->stato->markCompleted('op-1');
|
|
|
|
$this->assertSame(12, $this->stato->get('op-1')['processed']);
|
|
}
|
|
|
|
public function test_il_risultato_del_completamento_non_cancella_quello_gia_registrato(): void
|
|
{
|
|
$this->stato->initialize('op-1', BackupOperationState::TIPO_RESET, 'Backup preventivo', 1);
|
|
$this->stato->markAttesaDownload('op-1', ['file' => 'backup.zip']);
|
|
|
|
$this->stato->markCompleted('op-1', ['tabelle' => 9]);
|
|
|
|
$risultato = $this->stato->get('op-1')['result'];
|
|
$this->assertSame('backup.zip', $risultato['file']);
|
|
$this->assertSame(9, $risultato['tabelle']);
|
|
}
|
|
|
|
// ─── Log ────────────────────────────────────────────────────
|
|
|
|
public function test_la_coda_dei_log_tiene_solo_le_ultime_righe(): void
|
|
{
|
|
$this->stato->initialize('op-1', BackupOperationState::TIPO_BACKUP, 'Preparazione', 0);
|
|
|
|
for ($i = 1; $i <= 75; $i++) {
|
|
$this->stato->log('op-1', "riga {$i}");
|
|
}
|
|
|
|
$logs = $this->stato->get('op-1')['logs'];
|
|
|
|
// Lo stato viaggia per intero a ogni poll: una coda illimitata
|
|
// gonfierebbe ogni risposta di un'operazione lunga.
|
|
$this->assertCount(60, $logs);
|
|
$this->assertStringContainsString('riga 75', end($logs));
|
|
$this->assertStringContainsString('riga 16', $logs[0]);
|
|
}
|
|
|
|
// ─── Bordi ──────────────────────────────────────────────────
|
|
|
|
public function test_aggiornare_un_operazione_inesistente_non_la_crea(): void
|
|
{
|
|
$this->assertNull($this->stato->update('mai-esistita', fn (array $s) => $s));
|
|
$this->assertNull($this->stato->get('mai-esistita'));
|
|
}
|
|
|
|
public function test_forget_rimuove_lo_stato(): void
|
|
{
|
|
$this->stato->initialize('op-1', BackupOperationState::TIPO_BACKUP, 'Preparazione', 1);
|
|
|
|
$this->stato->forget('op-1');
|
|
|
|
$this->assertNull($this->stato->get('op-1'));
|
|
$this->assertNull($this->stato->corrente(), 'Senza stato il posto risulta libero.');
|
|
}
|
|
}
|