JA
games/xencenter-xenadmin-xcp-jumphp / tests / PoolsTest.php · commit fb1b7c9
<?phpnamespace Tests;use App\Form\ConfirmForm;use App\Form\HostAddressesForm;use App\Form\MainForm;use App\Form\NewPoolForm;use App\Model\Connection;use App\Model\ServerProfile;use App\View\Commands;use App\View\TreeBuilder;use App\Xapi\FakeXapi;use App\Xapi\Rrd;use App\Xapi\XapiClient;use App\Xapi\XapiError;use JumPHP\Kernel\Engine;use JumPHP\Testing\GameTestCase;/** Pools (New Pool, Add Server, Remove Server, new master), Connection Addresses and the Dashboard, played on the demo servers. */final class PoolsTest extends GameTestCase{    private MainForm $form;    /** The window with no server saved (the session is one for the whole process: emptied first), no demo server known. */    private function fresh(array $params = []): MainForm    {        FakeXapi::forget();        Engine::get()->storage->delete('servers.json');        $this->game->start(MainForm::class);        /** @var MainForm $form */        $form = $this->game->scene();        $form->session->reset();        $this->game->start(MainForm::class, $params);        $this->form = $form;        return $form;    }    private function connectDemo(): Connection    {        $form = $this->fresh(['connectDemo' => true]);        $this->game->run(10);        $c = $form->session->connections[0];        self::assertTrue($c->isConnected(), 'the demo pool connects: ' . $c->error);        return $c;    }    /** Another demo server (a standalone host), connected. */    private function addServer(string $address = 'xcp-demo-2.local'): Connection    {        $c = $this->form->session->add(ServerProfile::demoServer($address));        $this->form->connect($c);        $this->until(fn () => $c->isConnected(), 60);        return $c;    }    private function until(callable $condition, int $frames = 600): void    {        for ($i = 0; $i < $frames && !$condition(); $i++) {            $this->game->run(1);        }        self::assertTrue((bool)$condition(), 'still waiting after ' . $frames . ' frames');    }    private function ref(Connection $c, string $class, string $name): string    {        $ref = $c->cache->byName($class, $name);        self::assertNotNull($ref, "no $class $name");        return $ref;    }    /** @param list<array<string, mixed>|string> $items @return array<string, bool> text => enabled */    private static function enabled(array $items): array    {        return array_column(array_filter($items, 'is_array'), 'enabled', 'text');    }    public function testAnotherDemoAddressIsAStandaloneHostOfItsOwn(): void    {        FakeXapi::forget();        $pool = new FakeXapi();        $alone = new FakeXapi('xcp-demo-2.local');        self::assertCount(2, $pool->db['host']);        self::assertCount(1, $alone->db['host']);        self::assertSame('xcp-ng-03', array_values($alone->db['host'])[0]['name_label']);        self::assertSame('', array_values($alone->db['pool'])[0]['name_label'], 'a host alone: its pool has no name');        self::assertSame([], array_intersect_key($pool->db['vm'], $alone->db['vm']), 'their refs never meet');        self::assertSame('xcp-ng-04', array_values((new FakeXapi('xcp-demo-3.local'))->db['host'])[0]['name_label']);    }    public function testANewPoolIsMadeOfTwoStandaloneServers(): void    {        $this->connectDemo();        $a = $this->addServer('xcp-demo-2.local');        $b = $this->addServer('xcp-demo-3.local');        self::assertFalse($a->cache->isRealPool());        $menu = self::enabled(Commands::menu('Pool', $a, (string)$a->cache->master(), 'host', ['mode' => 'infrastructure', 'templates' => false, 'networks' => false]));        self::assertTrue($menu['New Pool…']);        $this->form->command('pool.new');        $this->game->run(1);        $dialog = $this->game->scene();        self::assertInstanceOf(NewPoolForm::class, $dialog);        self::assertSame(['xcp-ng-03 (xcp-demo-2.local)', 'xcp-ng-04 (xcp-demo-3.local)'], $dialog->master->items, 'the connected servers in no pool');        self::assertSame([$b->profile->key()], array_keys($dialog->boxes), 'the master is not offered as a member');        $dialog->name->text = 'Lab pool';        $dialog->boxes[$b->profile->key()]->checked = true;        $this->game->run(2);        $this->game->shot('new-pool');        $dialog->click($dialog->ok);        $this->game->run(1);        self::assertSame($this->form, $this->game->scene());        $this->until(fn () => count($this->form->session->connections) === 2 && count($a->cache->hosts()) === 2);        self::assertSame('Lab pool', $a->name(), 'pool.set_name_label on the master');        self::assertTrue($a->cache->isRealPool());        self::assertNotNull($a->cache->byName('vm', 'staging-web'), "the joined host's VMs are in the pool");        self::assertNotContains($b, $this->form->session->connections, "the joined server's own connection is left");        // the join was called on the joining host, which now answers as a pool member        $joining = FakeXapi::open('xcp-demo-3.local');        self::assertTrue($joining->isSlave());        self::assertContains('Async.pool.join', $joining->calls);        $login = (new XapiClient($joining))->login(FakeXapi::USER, FakeXapi::PASSWORD);        $this->until(function () use ($joining, $login): bool {            $joining->poll(0.1);            return $login->isDone();        }, 30);        self::assertInstanceOf(XapiError::class, $login->error());        self::assertSame(['HOST_IS_SLAVE', 'xcp-demo-2.local'], $login->error()->failure);        $this->game->run(20);        self::assertContains($this->ref($a, 'host', 'xcp-ng-04'), $this->form->tree->visibleIds());        $this->game->shot('lab-pool');    }    public function testAServerJoinsTheDemoPoolThenLeavesIt(): void    {        $pool = $this->connectDemo();        $this->addServer();        $this->form->navigate((string)$pool->cache->poolRef());        $this->game->run(2);        $this->form->command('pool.addServer');        $this->game->run(1);        $dialog = $this->game->scene();        self::assertInstanceOf(NewPoolForm::class, $dialog);        self::assertStringContainsString("pool 'Demo pool'", $dialog->heading->text);        self::assertCount(1, $dialog->members(), 'the standalone server is ticked');        $this->game->shot('add-server');        $dialog->click($dialog->ok);        $this->until(fn () => count($pool->cache->hosts()) === 3 && count($this->form->session->connections) === 1);        $host = $this->ref($pool, 'host', 'xcp-ng-03');        $vm = $this->ref($pool, 'vm', 'staging-web');        $vifs = $pool->cache->vifsOf($vm);        self::assertCount(1, $vifs);        $eth0 = (string)$pool->cache->field((string)$vifs[0]['vif']['network'], 'name_label', '');        self::assertSame('Pool-wide network associated with eth0', $eth0, "its VM's network card is on the pool's eth0 network");        $nfs = $this->ref($pool, 'sr', 'NFS storage');        self::assertArrayHasKey($nfs, $pool->cache->srsOf($host), "the pool's shared storage is plugged on it");        $this->game->run(20);        self::assertContains($host, $this->form->tree->visibleIds());        // now it leaves: XenCenter's strong warning first        $this->form->navigate($host);        $this->game->run(2);        self::assertTrue(self::enabled(Commands::hostItems($pool, $host))['Remove Server from Pool']);        $this->form->command('host.removeFromPool');        $this->game->run(1);        $confirm = $this->game->scene();        self::assertInstanceOf(ConfirmForm::class, $confirm);        self::assertStringContainsString('local storage is destroyed', $confirm->message->text);        self::assertSame('Remove', $confirm->ok->text);        self::assertSame('danger', $confirm->ok->style);        $this->game->shot('remove-from-pool');        $confirm->click($confirm->ok);        $this->until(fn () => count($pool->cache->hosts()) === 2);        $this->game->run(20);        self::assertNull($pool->cache->byName('vm', 'staging-web'), 'its VMs on local storage went with it');        self::assertNotContains($host, $this->form->tree->visibleIds());        self::assertFalse(FakeXapi::open('xcp-demo-2.local')->isSlave(), 'it is a standalone server again');    }    public function testTheMasterCannotLeaveAndAnotherServerBecomesMaster(): void    {        $pool = $this->connectDemo();        $master = (string)$pool->cache->master();        $other = $this->ref($pool, 'host', 'xcp-ng-02');        $onMaster = self::enabled(Commands::hostItems($pool, $master));        self::assertFalse($onMaster['Remove Server from Pool']);        self::assertFalse($onMaster['Make this server the pool master']);        $items = self::enabled(Commands::hostItems($pool, $other));        self::assertTrue($items['Make this server the pool master']);        self::assertFalse($items['Remove Server from Pool'], 'VMs run on it');        $this->form->navigate($other);        $this->game->run(2);        $this->form->command('host.designateMaster');        $this->game->run(1);        $confirm = $this->game->scene();        self::assertInstanceOf(ConfirmForm::class, $confirm);        $confirm->click($confirm->ok);        $this->until(fn () => $pool->cache->master() === $other);        self::assertContains('Async.pool.designate_new_master', $pool->fake()?->calls ?? []);    }    public function testAServerWithRunningVmsCannotJoinAPool(): void    {        $pool = $this->connectDemo();        $alone = $this->addServer();        $host = (string)$alone->cache->master();        self::assertTrue(self::enabled(Commands::hostItems($alone, $host))["Add to Pool 'Demo pool'"]);        $vm = $this->ref($alone, 'vm', 'staging-web');        $this->form->command('vm.start', $vm, true);        $this->until(fn () => $alone->cache->field($vm, 'power_state') === 'Running');        self::assertFalse(self::enabled(Commands::hostItems($alone, $host))["Add to Pool 'Demo pool'"], 'XenCenter offers it no more');        $join = $alone->run('Join', 'pool.join', ['xcp-demo.local', FakeXapi::USER, FakeXapi::PASSWORD]);        $this->until(fn () => $join->isDone());        $failure = $join->error();        self::assertSame('JOINING_HOST_CANNOT_HAVE_RUNNING_OR_SUSPENDED_VMS', $failure instanceof XapiError ? $failure->code() : '');        self::assertCount(2, $pool->cache->hosts());    }    public function testConnectionAddressesAreSavedAndUsedFirstForConsolesAndFigures(): void    {        $pool = $this->connectDemo();        $other = $this->ref($pool, 'host', 'xcp-ng-02');        $uuid = (string)$pool->cache->field($other, 'uuid');        $this->form->navigate((string)$pool->cache->poolRef());        $this->game->run(2);        $this->form->command('pool.addresses');        $this->game->run(1);        $dialog = $this->game->scene();        self::assertInstanceOf(HostAddressesForm::class, $dialog);        self::assertCount(2, $dialog->fields);        self::assertSame('192.168.1.12', $dialog->fields[$uuid]->placeholder, "empty: XAPI's address (no naming to copy from an address)");        $dialog->fields[$uuid]->text = '10.8.0.12';        $this->game->run(2);        $this->game->shot('addresses');        $dialog->click($dialog->ok);        $this->game->run(1);        self::assertSame(['10.8.0.12' => true], array_fill_keys(array_values($pool->profile->hostAddresses), true));        $saved = Engine::get()->storage->readJson('servers.json', []);        self::assertSame('10.8.0.12', $saved[0]['hostAddresses'][$uuid] ?? null, 'saved in var/servers.json');        self::assertSame(['10.8.0.12'], array_values(ServerProfile::fromArray($saved[0])->hostAddresses));        self::assertSame('10.8.0.12', $pool->consoleTargets($this->ref($pool, 'vm', 'db-replica'))[0], 'the console of a VM on that host: there first');        self::assertSame('10.8.0.12', \App\Feature\Pools\HostAddresses::reachable($pool, $other));        $this->form->navigate($this->ref($pool, 'vm', 'db-replica'));        $this->form->tabs->selectTab('Performance');        $this->until(fn () => ($pool->hostLinks()->addresses()[$other] ?? null) === '10.8.0.12', 60);    }    public function testFiguresOfAVmOnAnotherHostComeFromThatHost(): void    {        $pool = $this->connectDemo();        $vm = $this->ref($pool, 'vm', 'db-replica');        $uuid = (string)$pool->cache->field($vm, 'uuid');        // the master only has its own figures (and its VMs')        $fake = $pool->fake();        self::assertNotNull($fake);        $reply = $fake->request('GET', '/rrd_updates?session_id=' . $pool->client?->session . '&start=0&host=true');        $this->until(fn () => $reply->isDone(), 30);        self::assertSame([], Rrd::parse($reply->result()->body)->names('vm', $uuid));        $this->form->navigate($vm);        $this->form->tabs->selectTab('Performance');        $this->until(fn () => isset($this->form->rrd[0]) && $this->form->rrd[0]->names('vm', $uuid) !== [], 120);        $this->game->run(4);        self::assertNotSame([], $this->form->chart0->series[0]['points'] ?? [], 'its CPU graph has points');        self::assertSame('192.168.1.12', $pool->hostLinks()->addresses()[$this->ref($pool, 'host', 'xcp-ng-02')]);        $master = (string)$pool->cache->field((string)$pool->cache->master(), 'uuid');        self::assertNotSame([], $this->form->rrd[0]->names('host', $master), "the master's figures stay in the merge");        $this->game->shot('perf-other-host');        $pool->disconnect(false);        self::assertSame([], $pool->hostLinks()->addresses(), 'the roads close with the connection');    }    public function testTheDashboardShowsACardPerConnectionAndAClickSelectsIt(): void    {        $pool = $this->connectDemo();        $alone = $this->addServer();        $off = $this->form->session->add(ServerProfile::demoServer('xcp-demo-3.local'));        $this->form->navigate(TreeBuilder::ROOT);        $this->game->run(2);        self::assertSame('Dashboard', $this->form->tabs->value(), 'the first tab of XenCenter');        self::assertTrue($this->form->dashboard->visible);        $cards = $this->form->dashboard->cards;        self::assertCount(3, $cards);        self::assertSame(['Demo pool', 'xcp-ng-03'], [$cards[0]['name'], $cards[1]['name']]);        self::assertSame([2, 1], [$cards[0]['hosts'], $cards[1]['hosts']]);        self::assertSame(Connection::DISCONNECTED, $cards[2]['state']);        self::assertGreaterThan(0, $cards[0]['alerts']);        $this->until(fn () => ($this->form->dashboard->cards[0]['cpu'] ?? null) !== null, 400);         // the figures come every 5 s        self::assertGreaterThan(0.0, $this->form->dashboard->cards[0]['memory']);        $this->game->run(2);        $this->game->shot('dashboard');        $t = $this->form->theme;        [$x, $y, $w, $h] = $this->form->dashboard->cardRect($t, 1);        $this->form->clickAt($x + $w / 2, $y + $h / 3);        $this->game->run(2);        self::assertSame((string)$alone->cache->master(), $this->form->selected, 'a click on a card selects that server');        $this->form->navigate(TreeBuilder::ROOT);        $this->game->run(2);        [$bx, $by, $bw, $bh] = $this->form->dashboard->connectRect($t, 2);        $this->form->clickAt($bx + $bw / 2, $by + $bh / 2);        $this->until(fn () => $off->isConnected(), 60);        self::assertTrue($pool->isConnected());    }}