Jelajahi Sumber

Merge pull request #904 from jhdxr/feature/tests

Add test
walkor 2 tahun lalu
induk
melakukan
d707ff9b88

+ 47 - 0
.github/workflows/test.yml

@@ -0,0 +1,47 @@
+name: tests
+
+on:
+  push:
+    branches:
+      - master
+      - feature/tests
+  pull_request:
+  schedule:
+    - cron: '0 0 * * *'
+
+jobs:
+  linux_tests:
+    runs-on: ubuntu-22.04
+
+    strategy:
+      fail-fast: true
+      matrix:
+        php: [8.1, 8.2]
+        stability: [prefer-lowest, prefer-stable]
+
+    name: PHP ${{ matrix.php }} - ${{ matrix.stability }}
+
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v3
+
+      - name: Setup PHP
+        uses: shivammathur/setup-php@v2
+        with:
+          php-version: ${{ matrix.php }}
+          extensions: json
+          ini-values: error_reporting=E_ALL
+          tools: composer:v2
+          coverage: xdebug
+
+      - name: Install dependencies
+        uses: nick-fields/retry@v2
+        with:
+          timeout_minutes: 5
+          max_attempts: 5
+          command: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress
+#          command: composer install --prefer-dist --no-interaction --no-progress
+
+      - name: Execute tests
+        run: vendor/bin/pest --coverage
+

+ 9 - 0
composer.json

@@ -38,5 +38,14 @@
     "minimum-stability": "dev",
     "conflict": {
         "ext-swow": "<v1.0.0"
+    },
+    "require-dev": {
+        "pestphp/pest": "2.x-dev",
+        "mockery/mockery": "2.0.x-dev"
+    },
+    "config": {
+        "allow-plugins": {
+            "pestphp/pest-plugin": true
+        }
     }
 }

+ 18 - 0
phpunit.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd"
+         bootstrap="vendor/autoload.php"
+         colors="true"
+>
+  <testsuites>
+    <testsuite name="Test Suite">
+      <directory suffix="Test.php">./tests</directory>
+    </testsuite>
+  </testsuites>
+  <coverage/>
+  <source>
+    <include>
+      <directory suffix=".php">./src</directory>
+    </include>
+  </source>
+</phpunit>

+ 6 - 0
tests/Feature/ExampleTest.php

@@ -0,0 +1,6 @@
+<?php
+
+test('example', function () {
+    expect(true)->toBeTrue();
+//    expect('3')->toBe(3);
+});

+ 31 - 0
tests/Feature/UdpConnectionTest.php

@@ -0,0 +1,31 @@
+<?php
+//example from manual
+use Workerman\Connection\AsyncUdpConnection;
+use Workerman\Timer;
+use Workerman\Worker;
+
+it('tests udp connection', function () {
+    /** @noinspection PhpObjectFieldsAreOnlyWrittenInspection */
+    $server = new Worker('udp://0.0.0.0:9292');
+    $server->onMessage = function ($connection, $data) {
+        expect($data)->toBe('hello');
+        $connection->send('xiami');
+    };
+    $server->onWorkerStart = function () {
+        //client
+        Timer::add(1, function () {
+            $client = new AsyncUdpConnection('udp://127.0.0.1:1234');
+            $client->onConnect = function ($client) {
+                $client->send('hello');
+            };
+            $client->onMessage = function ($client, $data) {
+                expect($data)->toBe('xiami');
+                //terminal this test
+                terminate_current_test();
+            };
+            $client->connect();
+        }, null, false);
+    };
+    Worker::runAll();
+})->skipOnWindows() //require posix, multiple workers
+->skip(message: 'this test needs to run isolated process while pest not support doing so yet');

+ 65 - 0
tests/Pest.php

@@ -0,0 +1,65 @@
+<?php
+
+/*
+|--------------------------------------------------------------------------
+| Test Case
+|--------------------------------------------------------------------------
+|
+| The closure you provide to your test functions is always bound to a specific PHPUnit test
+| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
+| need to change it using the "uses()" function to bind a different classes or traits.
+|
+*/
+
+// uses(Tests\TestCase::class)->in('Feature');
+
+/*
+|--------------------------------------------------------------------------
+| Expectations
+|--------------------------------------------------------------------------
+|
+| When you're writing tests, you often need to check that values meet certain conditions. The
+| "expect()" function gives you access to a set of "expectations" methods that you can use
+| to assert different things. Of course, you may extend the Expectation API at any time.
+|
+*/
+
+use Workerman\Connection\TcpConnection;
+
+expect()->extend('toBeOne', function () {
+    return $this->toBe(1);
+});
+
+/*
+|--------------------------------------------------------------------------
+| Functions
+|--------------------------------------------------------------------------
+|
+| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
+| project that you don't want to repeat in every file. Here you can also expose helpers as
+| global functions to help you to reduce the number of lines of code in your test files.
+|
+*/
+
+function something()
+{
+    // ..
+}
+
+function testWithConnectionClose(Closure $closure, string $dataContains = null, $connectionClass = TcpConnection::class): void
+{
+    $tcpConnection = Mockery::spy($connectionClass);
+    $closure($tcpConnection);
+    if ($dataContains) {
+        $tcpConnection->shouldHaveReceived('close', function ($actual) use ($dataContains) {
+            return str_contains($actual, $dataContains);
+        });
+    } else {
+        $tcpConnection->shouldHaveReceived('close');
+    }
+}
+
+function terminate_current_test()
+{
+    posix_kill(posix_getppid(), SIGINT);
+}

+ 10 - 0
tests/TestCase.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace Tests;
+
+use PHPUnit\Framework\TestCase as BaseTestCase;
+
+abstract class TestCase extends BaseTestCase
+{
+    //
+}

+ 44 - 0
tests/Unit/Connection/UdpConnectionTest.php

@@ -0,0 +1,44 @@
+<?php
+
+use Workerman\Connection\UdpConnection;
+use Symfony\Component\Process\PhpProcess;
+
+$remoteAddress = '[::1]:12345';
+$process = new PhpProcess(<<<PHP
+<?php
+\$socketServer = stream_socket_server("udp://$remoteAddress", \$errno, \$errstr, STREAM_SERVER_BIND);
+do{
+    \$data = stream_socket_recvfrom(\$socketServer, 3);
+}while(\$data !== false && \$data !== 'bye');
+PHP
+);
+$process->start();
+
+it('tests ' . UdpConnection::class, function () use ($remoteAddress) {
+
+    $socketClient = stream_socket_client("udp://$remoteAddress");
+    $udpConnection = new UdpConnection($socketClient, $remoteAddress);
+    $udpConnection->protocol = \Workerman\Protocols\Text::class;
+    expect($udpConnection->send('foo'))->toBeTrue();
+
+    expect($udpConnection->getRemoteIp())->toBe('::1');
+    expect($udpConnection->getRemotePort())->toBe(12345);
+    expect($udpConnection->getRemoteAddress())->toBe($remoteAddress);
+    expect($udpConnection->getLocalIp())->toBeIn(['::1', '[::1]', '127.0.0.1']);
+    expect($udpConnection->getLocalPort())->toBeInt();
+
+    expect(json_encode($udpConnection))->toBeJson()
+        ->toContain('transport')
+        ->toContain('getRemoteIp')
+        ->toContain('remotePort')
+        ->toContain('getRemoteAddress')
+        ->toContain('getLocalIp')
+        ->toContain('getLocalPort')
+        ->toContain('isIpV4')
+        ->toContain('isIpV6');
+
+    $udpConnection->close('bye');
+    if (is_resource($socketClient)) {
+        fclose($socketClient);
+    }
+});

+ 20 - 0
tests/Unit/Protocols/FrameTest.php

@@ -0,0 +1,20 @@
+<?php
+
+use Workerman\Protocols\Frame;
+
+it('tests ::input', function () {
+    expect(Frame::input('foo'))->toBe(0);
+    expect(Frame::input("\0\0\0*foobar"))
+        ->toBe(42);
+});
+
+it('tests ::decode', function () {
+    $buffer = pack('N', 5) . 'jhdxr';
+    expect(Frame::decode($buffer))
+        ->toBe('jhdxr');
+});
+
+it('tests ::encode', function () {
+    expect(Frame::encode('jhdxr'))
+        ->toBe(pack('N', 9) . 'jhdxr');
+});

+ 57 - 0
tests/Unit/Protocols/Http/ResponseTest.php

@@ -0,0 +1,57 @@
+<?php
+
+use Workerman\Protocols\Http\Response;
+
+it('test some simple case', function () {
+    $response = new Response(201, ['X-foo' => 'bar'], 'hello, xiami');
+
+    expect($response->getStatusCode())->toBe(201);
+    expect($response->getHeaders())->toBe(['X-foo' => 'bar']);
+    expect($response->rawBody())->toBe('hello, xiami');
+
+    //headers
+    $response->header('abc', '123');
+    $response->withHeader('X-foo', 'baz');
+    $response->withHeaders(['def' => '456']);
+    expect((string)$response)
+        ->toContain('X-foo: baz')
+        ->toContain('abc: 123')
+        ->toContain('def: 456');
+    $response->withoutHeader('def');
+    expect((string)$response)->not->toContain('def: 456');
+    expect($response->getHeader('abc'))
+        ->toBe('123');
+
+    $response->withStatus(202, 'some reason');
+    expect($response->getReasonPhrase())->toBe('some reason');
+
+    $response->withProtocolVersion('1.0');
+    $response->withBody('hello, world');
+    expect((string)$response)
+        ->toContain('HTTP/1.0')
+        ->toContain('hello, world')
+        ->toContain('Content-Type: ')
+        ->toContain('Content-Length: 12')
+        ->not()->toContain('Transfer-Encoding: ');
+
+
+    //cookie
+    $response->cookie('foo', 'bar', httpOnly: true, domain: 'xia.moe');
+    expect((string)$response)
+        ->toContain('Set-Cookie: foo=bar; Domain=xia.moe; HttpOnly');
+});
+
+it('tests file', function (){
+    //todo may have to redo the simple test,
+    // as the implementation of headers is a different function for files.
+    // or actually maybe the Response is the one should be rewritten to reuse?
+    $response = new Response();
+    $tmpFile = tempnam(sys_get_temp_dir(), 'test');
+    rename($tmpFile, $tmpFile .'.jpg');
+    $tmpFile .= '.jpg';
+    file_put_contents($tmpFile, 'hello, xiami');
+    $response->withFile($tmpFile, 0, 12);
+    expect((string)$response)
+        ->toContain('Content-Type: image/jpeg')
+        ->toContain('Last-Modified: ');
+});

+ 15 - 0
tests/Unit/Protocols/Http/ServerSentEventsTest.php

@@ -0,0 +1,15 @@
+<?php
+
+use Workerman\Protocols\Http\ServerSentEvents;
+
+it('tests ' . ServerSentEvents::class, function () {
+    $data = [
+        'event' => 'ping',
+        'data' => 'some thing',
+        'id' => 1000,
+        'retry' => 5000,
+    ];
+    $sse = new ServerSentEvents($data);
+    $expected = "event: {$data['event']}\ndata: {$data['data']}\n\nid: {$data['id']}\nretry: {$data['retry']}\n";
+    expect((string)$sse)->toBe($expected);
+});

+ 113 - 0
tests/Unit/Protocols/HttpTest.php

@@ -0,0 +1,113 @@
+<?php
+
+use Workerman\Connection\TcpConnection;
+use Workerman\Protocols\Http;
+use Workerman\Protocols\Http\Request;
+use Workerman\Protocols\Http\Response;
+
+it('customizes request class', function () {
+    //backup old request class
+    $oldRequestClass = Http::requestClass();
+
+    //actual test
+    $class = new class {
+    };
+    Http::requestClass($class::class);
+    expect(Http::requestClass())->toBe($class::class);
+
+    //restore old request class
+    Http::requestClass($oldRequestClass);
+});
+
+it('tests ::input', function () {
+    //test 413 payload too large
+    testWithConnectionClose(function (TcpConnection $tcpConnection) {
+        expect(Http::input(str_repeat('jhdxr', 3333), $tcpConnection))
+            ->toBe(0);
+    }, '413 Payload Too Large');
+
+    //example request from ChatGPT :)
+    $buffer = "POST /path/to/resource HTTP/1.1\r\n" .
+        "Host: example.com\r\n" .
+        "Content-Type: application/json\r\n" .
+        "Content-Length: 27\r\n" .
+        "\r\n" .
+        '{"key": "value", "foo": "bar"}';
+
+    //unrecognized method
+    testWithConnectionClose(function (TcpConnection $tcpConnection) use ($buffer) {
+        expect(Http::input(str_replace('POST', 'MIAOWU', $buffer), $tcpConnection))
+            ->toBe(0);
+    }, '400 Bad Request');
+
+    //HTTP 1.1 without Host header
+    testWithConnectionClose(function (TcpConnection $tcpConnection) use ($buffer) {
+        expect(Http::input(str_replace("Host: ", 'NotHost: ', $buffer), $tcpConnection))
+            ->toBe(0);
+    }, '400 Bad Request');
+
+    //content-length exceeds connection max package size
+    testWithConnectionClose(function (TcpConnection $tcpConnection) use ($buffer) {
+        $tcpConnection->maxPackageSize = 10;
+        expect(Http::input($buffer, $tcpConnection))
+            ->toBe(0);
+    }, '413 Payload Too Large');
+});
+
+it('tests ::encode for non-object response', function () {
+    $tcpConnection = Mockery::mock(TcpConnection::class);
+    $tcpConnection->headers = [
+        'foo' => 'bar',
+        'jhdxr' => ['a', 'b'],
+    ];
+    $extHeader = "foo: bar\r\n" .
+        "jhdxr: a\r\n" .
+        "jhdxr: b\r\n";
+
+    expect(Http::encode('xiami', $tcpConnection))
+        ->toBe("HTTP/1.1 200 OK\r\n" .
+            "Server: workerman\r\n" .
+            "{$extHeader}Connection: keep-alive\r\n" .
+            "Content-Type: text/html;charset=utf-8\r\n" .
+            "Content-Length: 5\r\n\r\nxiami");
+});
+
+it('tests ::encode for ' . Response::class, function () {
+    $tcpConnection = Mockery::mock(TcpConnection::class);
+    $tcpConnection->headers = [
+        'foo' => 'bar',
+        'jhdxr' => ['a', 'b'],
+    ];
+    $extHeader = "foo: bar\r\n" .
+        "jhdxr: a\r\n" .
+        "jhdxr: b\r\n";
+
+    $response = new Response(body: 'xiami');
+
+    expect(Http::encode($response, $tcpConnection))
+        ->toBe("HTTP/1.1 200 OK\r\n" .
+            "Server: workerman\r\n" .
+            "{$extHeader}Connection: keep-alive\r\n" .
+            "Content-Type: text/html;charset=utf-8\r\n" .
+            "Content-Length: 5\r\n\r\nxiami");
+});
+
+it('tests ::decode', function () {
+    $tcpConnection = Mockery::mock(TcpConnection::class);
+
+    //example request from ChatGPT :)
+    $buffer = "POST /path/to/resource HTTP/1.1\r\n" .
+        "Host: example.com\r\n" .
+        "Content-Type: application/json\r\n" .
+        "Content-Length: 27\r\n" .
+        "\r\n" .
+        '{"key": "value", "foo": "bar"}';
+
+    $value = expect(Http::decode($buffer, $tcpConnection))
+        ->toBeInstanceOf(Request::class)
+        ->value;
+
+    //test cache
+    expect($value == Http::decode($buffer, $tcpConnection))
+        ->toBeTrue();
+});

+ 30 - 0
tests/Unit/Protocols/TextTest.php

@@ -0,0 +1,30 @@
+<?php
+
+use Workerman\Connection\ConnectionInterface;
+use Workerman\Protocols\Text;
+
+test(Text::class, function () {
+    $connection = Mockery::mock(ConnectionInterface::class);
+
+    //::input
+    //input too long
+    testWithConnectionClose(function ($connection) {
+        $connection->maxPackageSize = 5;
+        expect(Text::input('abcdefgh', $connection))
+            ->toBe(0);
+    });
+    //input without "\n"
+    expect(Text::input('jhdxr', $connection))
+        ->toBe(0);
+    //input with "\n"
+    expect(Text::input("jhdxr\n", $connection))
+        ->toBe(6);
+
+    //::encode
+    expect(Text::encode('jhdxr'))
+        ->toBe("jhdxr\n");
+
+    //::decode
+    expect(Text::decode("jhdxr\n"))
+        ->toBe('jhdxr');
+});