Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements Autobahn tests against the WebSocket spec #22

Merged
merged 21 commits into from
Nov 27, 2023
57 changes: 57 additions & 0 deletions .github/workflows/spec-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: spec tests

on:
push:
branches:
- main
- '*.x'
pull_request:

jobs:
test:
runs-on: ubuntu-latest

strategy:
fail-fast: true
matrix:
php: [8.2, 8.3]
laravel: [10]

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip
ini-values: error_reporting=E_ALL
tools: composer:v2
coverage: none

- name: Install dependencies
run: |
composer require "illuminate/contracts=^${{ matrix.laravel }}" --no-update
composer update --prefer-dist --no-interaction --no-progress

- name: Pull Autobahn Docker image
run: docker pull crossbario/autobahn-testsuite

- name: Start WebSocket server
working-directory: tests/Specification
run: php spec-server.php &

- name: Run specification tests
working-directory: tests/Specification
run: |
docker run --rm \
-v $PWD:/mnt/autobahn \
-v $PWD/reports:/mnt/autobahn/reports \
--add-host host.docker.internal:host-gateway \
crossbario/autobahn-testsuite \
wstest -m fuzzingclient -s /mnt/autobahn/client-spec.json

- name: Analyze test results
working-directory: tests/Specification
run: php spec-analyze.php
18 changes: 13 additions & 5 deletions src/WebSockets/WsConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Evenement\EventEmitter;
use Laravel\Reverb\Http\Connection;
use Ratchet\RFC6455\Messaging\CloseFrameChecker;
use Ratchet\RFC6455\Messaging\DataInterface;
use Ratchet\RFC6455\Messaging\Frame;
use Ratchet\RFC6455\Messaging\FrameInterface;
use Ratchet\RFC6455\Messaging\MessageBuffer;
Expand Down Expand Up @@ -56,10 +57,12 @@ public function openBuffer(): void
/**
* Send a message to the connection.
*/
public function send(string $message): void
public function send(mixed $message): void
{
$this->connection->send(
(new Frame($message))->getContents()
$message instanceof DataInterface ?
$message->getContents() :
(new Frame($message))->getContents()
);
}

Expand All @@ -69,8 +72,9 @@ public function send(string $message): void
public function control(FrameInterface $message): void
{
match ($message->getOpcode()) {
Frame::OP_PING => $this->send(new Frame('pong', opcode: Frame::OP_PONG)),
Frame::OP_CLOSE => $this->close(),
Frame::OP_PING => $this->send(new Frame($message->getPayload(), opcode: Frame::OP_PONG)),
Frame::OP_PONG => fn () => null,
Frame::OP_CLOSE => $this->close($message),
};
}

Expand All @@ -93,8 +97,12 @@ public function onClose(callable $callback): void
/**
* Close the connection.
*/
public function close(): void
public function close(FrameInterface $frame = null): void
{
if ($frame) {
$this->send($frame);
}

$this->connection->close();
}

Expand Down
8 changes: 0 additions & 8 deletions tests/Feature/Reverb/ServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
use Laravel\Reverb\Jobs\PingInactiveConnections;
use Laravel\Reverb\Jobs\PruneStaleConnections;
use Laravel\Reverb\Tests\ReverbTestCase;
use Ratchet\RFC6455\Messaging\Frame;
use React\Promise\Deferred;

use function Ratchet\Client\connect;
Expand Down Expand Up @@ -315,13 +314,6 @@
$this->connect();
});

it('can receive a pong control frame', function () {
$frame = new Frame('ping', true, Frame::OP_PING);
$response = $this->sendRaw($frame);

expect($response)->toBe('pong');
});

it('clears application state between requests', function () {
$this->subscribe('test-channel');

Expand Down
18 changes: 0 additions & 18 deletions tests/ReverbTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,24 +182,6 @@ public function send(array $message, WebSocket $connection = null): string
return await($promise->promise());
}

/**
* Send a message to the connected client.
*/
public function sendRaw(mixed $message, WebSocket $connection = null): string
{
$promise = new Deferred;

$connection = $connection ?: $this->connect();

$connection->on('message', function ($message) use ($promise) {
$promise->resolve((string) $message);
});

$connection->send($message);

return await($promise->promise());
}

/**
* Disconnect the connected client.
*/
Expand Down
11 changes: 11 additions & 0 deletions tests/Specification/client-spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"options": { "failByDrop": false },
"outdir": "/mnt/autobahn/reports",
"servers": [{ "agent": "Reverb", "url": "ws://host.docker.internal:8080", "options": { "version": 18 } }],
"cases": ["*"],
"exclude-cases": [
"12.*",
"13.*"
],
"exclude-agent-cases": {}
}
33 changes: 33 additions & 0 deletions tests/Specification/spec-analyze.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Ratchet\RFC6455\Test;

use Illuminate\Support\Arr;

require __DIR__.'/../../vendor/autoload.php';

$hasFailures = false;

if (! file_exists($file = __DIR__.'/reports/index.json')) {
echo 'No test results found.'.PHP_EOL;

exit(1);
}

$results = file_get_contents($file);
$results = Arr::first(json_decode($results, true));

foreach ($results as $name => $result) {
if ($result['behavior'] === 'INFORMATIONAL') {
continue;
}

if (in_array($result['behavior'], ['OK', 'NON-STRICT'])) {
echo '✅ Test case '.$name.' passed.'.PHP_EOL;
} else {
$hasFailures = true;
echo '❌ Test case '.$name.' failed.'.PHP_EOL;
}
}

exit($hasFailures ? 1 : 0);
40 changes: 40 additions & 0 deletions tests/Specification/spec-server.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

use Laravel\Reverb\Http\Route;
use Laravel\Reverb\Http\Router;
use Laravel\Reverb\Http\Server;
use Laravel\Reverb\WebSockets\WsConnection;
use Psr\Http\Message\RequestInterface;
use React\EventLoop\Loop;
use React\Socket\SocketServer;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;

require __DIR__.'/../../vendor/autoload.php';

$loop = Loop::get();
$socket = new SocketServer('0.0.0.0:8080', [], $loop);
$router = new Router(new UrlMatcher(routes(), new RequestContext));

$server = new Server($socket, $router, $loop);

echo "Server running at 0.0.0.0:8080\n";

$server->start();

function routes()
{
$routes = new RouteCollection;
$routes->add(
'sockets',
Route::get('/', function (RequestInterface $request, WsConnection $connection) {
$connection->onMessage(function ($message) use ($connection) {
$connection->send($message);
});
$connection->openBuffer();
})
);

return $routes;
}