Entrar
PT
<?phpnamespace Tests;use App\Xapi\FakeXapi;use App\Xapi\HttpChannel;use App\Xapi\HttpReply;use App\Xapi\Rrd;use App\Xapi\XapiClient;use App\Xapi\XapiError;use Evenement\EventEmitter;use PHPUnit\Framework\TestCase;/** A stream that records what is written and answers what the test says. */final class FakeStream extends EventEmitter{    public string $written = '';    public bool $closed = false;    public function write(string $bytes): bool    {        $this->written .= $bytes;        return true;    }    public function close(): void    {        $this->closed = true;    }}final class XapiTest extends TestCase{    public function testTwoRequestsShareOneKeepAliveConnectionAndReadContentLengthAndChunkedBodies(): void    {        $streams = [];        $channel = new HttpChannel(function () use (&$streams) {            return $streams[] = new FakeStream();        }, 'xcp.local');        $first = $channel->request('POST', '/jsonrpc', '{"a":1}');        $second = $channel->request('GET', '/rrd_updates?x=1');        self::assertCount(1, $streams);        $s = $streams[0];        $s->emit('open');        self::assertStringStartsWith("POST /jsonrpc HTTP/1.1\r\nHost: xcp.local\r\n", $s->written);        self::assertStringContainsString("Content-Length: 7\r\n\r\n{\"a\":1}", $s->written);        self::assertStringNotContainsString('GET', $s->written, 'one request at a time');        // the answer arrives in two pieces        $s->emit('data', ["HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhel"]);        self::assertFalse($first->isDone());        $s->emit('data', ["lo"]);        self::assertSame('hello', $first->result()->body);        self::assertStringContainsString('GET /rrd_updates?x=1', $s->written, 'the next request on the same stream');        $s->emit('data', ["HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n4\r\n<xpo\r\n3\r\nrt>\r\n0\r\n\r\n"]);        self::assertSame('<xport>', $second->result()->body);        self::assertCount(1, $streams);    }    public function testABigAnswerArrivingInSmallPiecesIsReadAtOnceNotInQuadraticTime(): void    {        // the first event.from of a pool: tens of MB, in the browser thousands of WebSocket messages        $size = 16 * 1024 * 1024;        $body = str_repeat('{"x":1},', $size / 8);        foreach (['length' => "Content-Length: $size\r\n\r\n$body",            'chunked' => "Transfer-Encoding: chunked\r\n\r\n" . implode('', array_map(fn ($c) => dechex(strlen($c)) . "\r\n$c\r\n", str_split($body, 16384))) . "0\r\n\r\n"] as $mode => $raw) {            $s = null;            $channel = new HttpChannel(function () use (&$s) {                return $s = new FakeStream();            }, 'h');            $task = $channel->request('POST', '/jsonrpc', '{}');            $s->emit('open');            $start = microtime(true);            foreach (str_split("HTTP/1.1 200 OK\r\n" . $raw, 4096) as $piece) {                $s->emit('data', [$piece]);            }            self::assertTrue($task->isDone(), $mode);            self::assertSame($size, strlen($task->result()->body), $mode);            self::assertLessThan(2.0, microtime(true) - $start, "$mode: each byte read once");        }    }    public function testTheTimeoutCountsSilenceNotTheLengthOfAnAnswerStillArriving(): void    {        $s = null;        $channel = new HttpChannel(function () use (&$s) {            return $s = new FakeStream();        }, 'h', 10.0);        $task = $channel->request('POST', '/jsonrpc', '{}');        $s->emit('open');        $s->emit('data', ["HTTP/1.1 200 OK\r\nContent-Length: 30\r\n\r\n"]);        for ($i = 0; $i < 29; $i++) {            $channel->poll(1.0);        // 29 s in all, a byte a second            $s->emit('data', ['x']);        }        self::assertFalse($task->isDone());        $s->emit('data', ['y']);        self::assertSame(30, strlen($task->result()->body));        $late = $channel->request('POST', '/jsonrpc', '{}');        $channel->poll(11.0);           // on the kept-alive stream: sent again once (it may have died idle)        $channel->poll(11.0);        self::assertInstanceOf(XapiError::class, $late->error(), 'silence still times out');    }    public function testAClosedConnectionIsOpenedAgainForTheNextRequest(): void    {        $streams = [];        $channel = new HttpChannel(function () use (&$streams) {            return $streams[] = new FakeStream();        }, 'h');        $a = $channel->request('POST', '/jsonrpc', 'x');        $streams[0]->emit('open');        $streams[0]->emit('data', ["HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 2\r\n\r\nok"]);        self::assertSame('ok', $a->result()->body);        self::assertTrue($streams[0]->closed);        $b = $channel->request('POST', '/jsonrpc', 'y');        self::assertCount(2, $streams);        $streams[1]->emit('error', ['connection refused']);        self::assertInstanceOf(XapiError::class, $b->error());        self::assertSame('connection refused', $b->error()->getMessage());    }    public function testARequestOnAKeptAliveConnectionTheServerClosedIsSentAgainOnANewOne(): void    {        $streams = [];        $channel = new HttpChannel(function () use (&$streams) {            return $streams[] = new FakeStream();        }, 'h');        $a = $channel->request('POST', '/jsonrpc', 'a');        $streams[0]->emit('open');        $streams[0]->emit('data', ["HTTP/1.1 200 OK\r\nContent-Length: 1\r\n\r\nA"]);        self::assertSame('A', $a->result()->body);        $b = $channel->request('POST', '/jsonrpc', 'b');        // written on the idle connection...        $streams[0]->emit('close', [null]);                     // ...that was already gone        self::assertFalse($b->isDone(), 'not failed: sent again');        self::assertCount(2, $streams);        $streams[1]->emit('open');        $streams[1]->emit('data', ["HTTP/1.1 200 OK\r\nContent-Length: 1\r\n\r\nB"]);        self::assertSame('B', $b->result()->body);    }    public function testAJsonRpcFailureBecomesAXapiError(): void    {        [, $error] = XapiClient::decode(new HttpReply(200, [], '{"jsonrpc":"2.0","error":{"code":1,"message":"SESSION_AUTHENTICATION_FAILED","data":["SESSION_AUTHENTICATION_FAILED","root","Authentication failure"]},"id":1}'), 'session.login_with_password');        self::assertInstanceOf(XapiError::class, $error);        self::assertSame('SESSION_AUTHENTICATION_FAILED', $error->code());        self::assertStringContainsString('Authentication failed', $error->getMessage());        [$result] = XapiClient::decode(new HttpReply(200, [], '{"jsonrpc":"2.0","result":"OpaqueRef:1","id":2}'), 'x');        self::assertSame('OpaqueRef:1', $result);    }    public function testTheDemoPoolLogsInAndAnswersRecordsAndEvents(): void    {        $fake = new FakeXapi();        $client = new XapiClient($fake);        $session = $client->login('root', 'demo');        $fake->poll(0.1);        self::assertStringStartsWith('OpaqueRef:', $session->result());        $vms = $client->call('VM.get_all_records');        $events = $client->events(['vm'], '', 1.0);        $fake->poll(0.1);        self::assertNotEmpty($vms->result());        $batch = $events->result();        self::assertSame(count($vms->result()), count($batch['events']), 'the first event.from gives every object');        self::assertSame('add', $batch['events'][0]['operation']);        $bad = (new XapiClient($fake))->login('root', 'wrong');        $fake->poll(0.1);        self::assertSame('SESSION_AUTHENTICATION_FAILED', $bad->error()->code());    }    public function testRrdUpdatesAreParsed(): void    {        $xml = '<xport><meta><start>100</start><step>5</step><end>110</end><rows>2</rows><columns>2</columns><legend><entry>AVERAGE:vm:u1:cpu0</entry>'            . '<entry>AVERAGE:vm:u1:cpu1</entry></legend></meta><data><row><t>110</t><v>0.5</v><v>0.1</v></row><row><t>105</t><v>0.3</v><v>NaN</v></row></data></xport>';        $rrd = Rrd::parse($xml);        self::assertSame([[105, 0.3], [110, 0.5]], $rrd->series('vm', 'u1', 'cpu0'));        self::assertSame(['cpu0', 'cpu1'], $rrd->names('vm', 'u1'));        self::assertEqualsWithDelta(0.3, $rrd->average('vm', 'u1', ['cpu0', 'cpu1'])[1][1], 0.001);    }    public function testTheRfbClientDoesTheHandshakeThenAppliesRawAndCopyRectUpdates(): void    {        $stream = new FakeStream();        $rfb = new \App\Vnc\RfbClient($stream, "CONNECT /console?ref=x HTTP/1.0\r\n\r\n");        $stream->emit('open');        self::assertStringStartsWith('CONNECT /console', $stream->written);        $stream->emit('data', ["HTTP/1.1 200 OK\r\n\r\nRFB 003.008\n"]);        self::assertStringEndsWith("RFB 003.008\n", $stream->written);        $stream->emit('data', ["\x01\x01" . pack('N', 0)]);        $stream->emit('data', [pack('nn', 4, 2) . str_repeat("\0", 16) . pack('N', 4) . 'test']);        self::assertSame(\App\Vnc\RfbClient::READY, $rfb->state);        self::assertSame('test', $rfb->name);        // a raw red pixel at (1, 0), then a copy of it to (3, 1)        $stream->emit('data', [pack('Cxn', 0, 2) . pack('nnnnN', 1, 0, 1, 1, 0) . "\x00\x00\xff\x00" . pack('nnnnN', 3, 1, 1, 1, 1) . pack('nn', 1, 0)]);        self::assertSame([255, 0, 0], $rfb->fb->pixel(1, 0));        self::assertSame([255, 0, 0], $rfb->fb->pixel(3, 1));        self::assertSame([0, 0, 0], $rfb->fb->pixel(0, 0));        // keys and the pointer go out as RFB messages        $stream->written = '';        $rfb->tap(0x61);        $rfb->pointer(2, 1, 1);        self::assertSame(pack('CCnN', 4, 1, 0, 0x61) . pack('CCnN', 4, 0, 0, 0x61) . pack('CCnn', 5, 1, 2, 1), $stream->written);    }    public function testAFramebufferRectangleBecomesABmpPicture(): void    {        $fb = new \App\Vnc\Framebuffer(2, 2);        $fb->put(0, 0, 2, 2, str_repeat("\x01\x02\x03\x00", 4));        $bmp = $fb->bmp(0, 0, 2, 2);        self::assertSame('BM', substr($bmp, 0, 2));        self::assertSame(54 + 16, strlen($bmp));        self::assertSame(-2, unpack('l', substr($bmp, 22, 4))[1], 'top-down rows');        self::assertSame([0, 0, 2, 2], $fb->takeDirty());        self::assertNull($fb->takeDirty());    }}