*/ public array $d = []; public function incr($k) { return $this->d[$k] = (int) ($this->d[$k] ?? 0) + 1; } public function get($k) { return $this->d[$k] ?? null; } public function del($k) { unset($this->d[$k]); } public function expire($k, $s) {} public function hincrby($k, $c, $n) { $this->d[$k][$c] = ($this->d[$k][$c] ?? 0) + $n; } public function hgetall($k) { return $this->d[$k] ?? []; } }); }); /** * Session d'élève active avec ses VM, créées il y a $ilYa secondes. * * @param array $instances nom => uuid * @return array */ function sessionActive(array $instances, int $ilYa = 600): array { $session = sessionEnAttente(LabSession::ACTIF); $machines = []; foreach ($instances as $nom => $uuid) { $m = $session->machines()->create(['nom' => $nom, 'instance_id' => $uuid]); $m->forceFill(['created_at' => now()->subSeconds($ilYa)])->save(); $machines[$nom] = $m; } return $machines; } /** * @param array $etats uuid => état */ function simuler(string $classe, Closure $attentes): void { app()->instance($classe, Mockery::mock($classe, $attentes)); } function surveillance(array $etats, array $pretes = [], ?Closure $nova = null, ?Closure $stock = null): SurveillanceVm { simuler(ClientCorrecteur::class, fn ($c) => $c->shouldReceive('sante')->andReturnUsing( fn (string $id) => ($etats[$id] ?? ['repond' => true, 'etat' => 'running']) + ['erreur' => null] )); simuler(NovaClient::class, $nova ?? fn ($n) => $n->shouldNotReceive('redemarrerForce', 'demarrer')); simuler(StockChaud::class, function ($s) use ($pretes, $stock) { $s->shouldReceive('pretes')->andReturn($pretes); $stock ? $stock($s) : $s->shouldNotReceive('retirer'); }); return app(SurveillanceVm::class); } $muette = ['repond' => false, 'etat' => 'running']; test('une VM d\'élève muette 3 minutes de suite est redémarrée de force, une seule fois', function () use ($muette) { $m = sessionActive(['pc-a' => 'uuid-a', 'pc-b' => 'uuid-b', 'routeur' => 'uuid-r']); $s = surveillance(['uuid-a' => $muette], nova: fn ($n) => $n->shouldReceive('redemarrerForce')->once()->with('uuid-a')); expect($s->surveiller()['relancees'])->toBe(0) ->and($s->surveiller()['relancees'])->toBe(0) ->and($s->surveiller()['relancees'])->toBe(1); expect($m['pc-a']->refresh()->sante)->toBe(LabSessionMachine::RELANCEE) ->and($m['pc-b']->refresh()->sante)->toBeNull(); }); test('une VM éteinte par l\'élève est redémarrée, pas réinitialisée', function () { sessionActive(['pc-a' => 'uuid-a', 'pc-b' => 'uuid-b', 'routeur' => 'uuid-r']); $s = surveillance(['uuid-a' => ['repond' => false, 'etat' => 'shut off']], nova: fn ($n) => $n->shouldReceive('demarrer')->once()->with('uuid-a')); foreach (range(1, 3) as $_) { $s->surveiller(); } }); test('après deux relances dans l\'heure, la VM est déclarée irréparable et le prof le voit', function () use ($muette) { $m = sessionActive(['pc-a' => 'uuid-a', 'pc-b' => 'uuid-b', 'routeur' => 'uuid-r']); $s = surveillance(['uuid-a' => $muette], nova: fn ($n) => $n->shouldReceive('redemarrerForce')->twice()); foreach (range(1, 9) as $_) { $r = $s->surveiller(); } expect($r['irreparables'])->toBe(1) ->and($m['pc-a']->refresh()->sante)->toBe(LabSessionMachine::IRREPARABLE) ->and(Redis::hgetall('sante:compteurs'))->toBe(['relances_session' => 2, 'abandons' => 1]); $session = $m['pc-a']->labSession; $prof = $session->seance->classe->prof; $this->withSession(['jeton_session_web' => $prof->demarreNouvelleSession()]) ->actingAs($prof, 'web') ->get(route('seances.attente', $session->seance)) ->assertSee('pc-a ne répond plus'); }); test('toutes les VM muettes à la fois : panne de l\'hôte, aucune relance', function () use ($muette) { sessionActive(['pc-a' => 'uuid-a', 'pc-b' => 'uuid-b', 'routeur' => 'uuid-r']); $s = surveillance(['uuid-a' => $muette, 'uuid-b' => $muette, 'uuid-r' => $muette]); foreach (range(1, 4) as $_) { $r = $s->surveiller(); } expect($r['panne_generale'])->toBeTrue()->and($r['relancees'])->toBe(0); }); test('une VM qui vient d\'être créée n\'est pas sondée (elle démarre)', function () use ($muette) { sessionActive(['pc-a' => 'uuid-a'], ilYa: 60); $s = surveillance(['uuid-a' => $muette]); foreach (range(1, 4) as $_) { $r = $s->surveiller(); } expect($r['sondees'])->toBe(0); }); test('une stack du stock dont une VM est muette 2 minutes est retirée du stock', function () use ($muette) { $pretes = [ ['lab' => '08-deux-lan-routeur', 'nom' => 'stock-a-e1', 'id' => 'h1', 'instances' => ['pc-a' => 's1', 'routeur' => 's2', 'pc-b' => 's3'], 'cree_a' => time() - 600, 'brut' => 'A'], ['lab' => '08-deux-lan-routeur', 'nom' => 'stock-b-e1', 'id' => 'h2', 'instances' => ['pc-a' => 's4', 'routeur' => 's5', 'pc-b' => 's6'], 'cree_a' => time() - 600, 'brut' => 'B'], ]; $s = surveillance(['s2' => $muette], $pretes, stock: fn ($st) => $st->shouldReceive('retirer')->once()->with('08-deux-lan-routeur', 'A')->andReturn(true)); expect($s->surveiller()['retirees_stock'])->toBe(0) ->and($s->surveiller()['retirees_stock'])->toBe(1); }); test('correcteur injoignable : aucune action', function () { sessionActive(['pc-a' => 'uuid-a']); $this->mock(ClientCorrecteur::class, fn ($c) => $c->shouldReceive('sante')->andThrow(new RuntimeException('Correcteur injoignable'))); $this->mock(NovaClient::class, fn ($n) => $n->shouldNotReceive('redemarrerForce', 'demarrer')); $this->mock(StockChaud::class, fn ($s) => $s->shouldReceive('pretes')->andReturn([])); expect(app(SurveillanceVm::class)->surveiller()['correcteur_injoignable'])->toBeTrue(); });