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. Il posto deve risultare libero e potersi prendere; * il puntatore lo sovrascrive l'operazione nuova (corrente() non lo * cancella: girando fuori dal mutex poteva togliere quello di un'altra). */ 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($this->stato->occupa('op-nuova', BackupOperationState::TIPO_BACKUP, 'In coda')); $this->assertSame('op-nuova', $this->stato->corrente()); } /** Un corrente() fuori dal mutex non deve togliere il puntatore appena scritto. */ public function test_corrente_non_cancella_il_puntatore(): void { Cache::put(self::CHIAVE_CORRENTE, 'op-svanita', 3600); $this->stato->corrente(); $this->assertSame('op-svanita', Cache::get(self::CHIAVE_CORRENTE)); } 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($this->stato->occupa('op-2', BackupOperationState::TIPO_BACKUP, 'In coda')); } /** * 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.'); } }