From 79a9a781d3494866c400ed15c19f133e35303524 Mon Sep 17 00:00:00 2001 From: Chad Sikorra Date: Sat, 25 Apr 2026 13:01:06 -0400 Subject: [PATCH] Migrate to phpunit. Bump minimum version to PHP 8.2. --- .github/workflows/analysis.yml | 4 +- .github/workflows/build.yml | 8 +- .gitignore | 3 +- composer.json | 28 ++- phpspec.cov.yml | 13 -- phpspec.yml | 4 - phpstan.neon | 4 +- phpstan.tests.neon | 5 + phpunit.xml.dist | 30 +++ src/FreeDSx/Socket/MessageQueue.php | 21 -- tests/fixture/FreeDSx/Socket/Pdu.php | 50 ---- .../Exception/ConnectionExceptionSpec.php | 27 --- .../Exception/PartialMessageExceptionSpec.php | 27 --- .../spec/FreeDSx/Socket/MessageQueueSpec.php | 62 ----- .../Socket/Queue/Asn1MessageQueueSpec.php | 102 -------- .../spec/FreeDSx/Socket/Queue/BufferSpec.php | 37 --- .../spec/FreeDSx/Socket/Queue/MessageSpec.php | 46 ---- .../spec/FreeDSx/Socket/SocketServerSpec.php | 96 -------- tests/spec/FreeDSx/Socket/SocketSpec.php | 200 ---------------- tests/unit/Pdu.php | 37 +++ tests/unit/Queue/Asn1MessageQueueTest.php | 141 +++++++++++ tests/unit/Queue/BufferTest.php | 43 ++++ tests/unit/Queue/MessageTest.php | 52 +++++ .../Socket => unit}/RequiresUnixTransport.php | 8 +- .../SocketPoolTest.php} | 42 ++-- tests/unit/SocketServerTest.php | 105 +++++++++ tests/unit/SocketTest.php | 220 ++++++++++++++++++ 27 files changed, 682 insertions(+), 733 deletions(-) delete mode 100644 phpspec.cov.yml delete mode 100644 phpspec.yml create mode 100644 phpstan.tests.neon create mode 100644 phpunit.xml.dist delete mode 100644 src/FreeDSx/Socket/MessageQueue.php delete mode 100644 tests/fixture/FreeDSx/Socket/Pdu.php delete mode 100644 tests/spec/FreeDSx/Socket/Exception/ConnectionExceptionSpec.php delete mode 100644 tests/spec/FreeDSx/Socket/Exception/PartialMessageExceptionSpec.php delete mode 100644 tests/spec/FreeDSx/Socket/MessageQueueSpec.php delete mode 100644 tests/spec/FreeDSx/Socket/Queue/Asn1MessageQueueSpec.php delete mode 100644 tests/spec/FreeDSx/Socket/Queue/BufferSpec.php delete mode 100644 tests/spec/FreeDSx/Socket/Queue/MessageSpec.php delete mode 100644 tests/spec/FreeDSx/Socket/SocketServerSpec.php delete mode 100644 tests/spec/FreeDSx/Socket/SocketSpec.php create mode 100644 tests/unit/Pdu.php create mode 100644 tests/unit/Queue/Asn1MessageQueueTest.php create mode 100644 tests/unit/Queue/BufferTest.php create mode 100644 tests/unit/Queue/MessageTest.php rename tests/{spec/FreeDSx/Socket => unit}/RequiresUnixTransport.php (68%) rename tests/{spec/FreeDSx/Socket/SocketPoolSpec.php => unit/SocketPoolTest.php} (55%) create mode 100644 tests/unit/SocketServerTest.php create mode 100644 tests/unit/SocketTest.php diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index 8ceb27f..c2b5161 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -30,7 +30,9 @@ jobs: run: composer install --no-progress --prefer-dist --optimize-autoloader - name: Run Static Analysis - run: composer run-script analyse + run: | + composer run-script analyse + composer run-script analyse-tests - name: Run Test Coverage run: composer run-script test-coverage diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3ac300a..51a5db8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ jobs: strategy: matrix: operating-system: [ubuntu-latest, windows-latest] - php-versions: ['8.1', '8.2', '8.3', '8.4', '8.5'] + php-versions: ['8.2', '8.3', '8.4', '8.5'] name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} steps: - name: Checkout @@ -16,7 +16,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} - coverage: xdebug + coverage: none - name: Get Composer Cache Directory id: composer-cache @@ -33,5 +33,5 @@ jobs: - name: Install Composer dependencies run: composer install --no-progress --prefer-dist --optimize-autoloader - - name: Run Specs - run: composer run-script test + - name: Run Tests + run: composer run-script test-unit diff --git a/.gitignore b/.gitignore index 16cbe25..b798b30 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ composer.lock composer.phar .idea/ vendor/ -bin/ \ No newline at end of file +bin/ +.phpunit.result.cache diff --git a/composer.json b/composer.json index 8a00e5f..a0d7e20 100644 --- a/composer.json +++ b/composer.json @@ -15,13 +15,14 @@ } ], "require": { - "php": ">=7.1" + "php": ">=8.2" }, "require-dev": { - "phpspec/phpspec": "^7.1|^8.0", "freedsx/asn1": ">=0.4, <1.0", - "friends-of-phpspec/phpspec-code-coverage": "^6.1|^7.0", - "phpstan/phpstan": "^2.0" + "phpunit/phpunit": "^11.5", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/extension-installer": "^1.4" }, "suggest": { "ext-openssl": "For SSL/TLS support.", @@ -32,19 +33,26 @@ }, "autoload-dev": { "psr-4": { - "fixture\\FreeDSx\\Socket\\": "tests/fixture/FreeDSx/Socket", - "spec\\FreeDSx\\Socket\\": "tests/spec/FreeDSx/Socket" + "Tests\\Unit\\FreeDSx\\Socket\\": "tests/unit" + } + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true } }, "scripts": { - "test-coverage": [ - "phpspec run --no-interaction -c phpspec.cov.yml" + "test-unit": [ + "@php -d xdebug.mode=off vendor/bin/phpunit --testsuite unit" ], - "test": [ - "phpspec run --no-interaction" + "test-coverage": [ + "@php -d xdebug.mode=coverage vendor/bin/phpunit --testsuite unit --coverage-clover=coverage.xml" ], "analyse": [ "phpstan analyse" + ], + "analyse-tests": [ + "phpstan analyse -c phpstan.tests.neon" ] } } diff --git a/phpspec.cov.yml b/phpspec.cov.yml deleted file mode 100644 index 5271b7f..0000000 --- a/phpspec.cov.yml +++ /dev/null @@ -1,13 +0,0 @@ -suites: - default: - spec_path: tests/ - src_path: src/ -formatter.name: progress -extensions: - FriendsOfPhpSpec\PhpSpec\CodeCoverage\CodeCoverageExtension: - format: - - clover - output: - clover: coverage.xml - whitelist: - - src diff --git a/phpspec.yml b/phpspec.yml deleted file mode 100644 index 0f1eed9..0000000 --- a/phpspec.yml +++ /dev/null @@ -1,4 +0,0 @@ -suites: - default: - spec_path: tests/ - src_path: src/ diff --git a/phpstan.neon b/phpstan.neon index b8fa543..876ed33 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,5 +2,5 @@ parameters: level: 6 paths: - %currentWorkingDirectory%/src - phpVersion: 70100 - treatPhpDocTypesAsCertain: false \ No newline at end of file + phpVersion: 80200 + treatPhpDocTypesAsCertain: false diff --git a/phpstan.tests.neon b/phpstan.tests.neon new file mode 100644 index 0000000..0987142 --- /dev/null +++ b/phpstan.tests.neon @@ -0,0 +1,5 @@ +parameters: + treatPhpDocTypesAsCertain: false + level: 10 + paths: + - %currentWorkingDirectory%/tests diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..63ba7d6 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + ./src + + + + + + + ./tests/unit + + + diff --git a/src/FreeDSx/Socket/MessageQueue.php b/src/FreeDSx/Socket/MessageQueue.php deleted file mode 100644 index a886233..0000000 --- a/src/FreeDSx/Socket/MessageQueue.php +++ /dev/null @@ -1,21 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace FreeDSx\Socket; - -use FreeDSx\Socket\Queue\Asn1MessageQueue; - -/** - * @deprecated Used for backwards-compatibility. Will be removed. - * @author Chad Sikorra - */ -class MessageQueue extends Asn1MessageQueue -{ -} diff --git a/tests/fixture/FreeDSx/Socket/Pdu.php b/tests/fixture/FreeDSx/Socket/Pdu.php deleted file mode 100644 index 29642db..0000000 --- a/tests/fixture/FreeDSx/Socket/Pdu.php +++ /dev/null @@ -1,50 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace fixture\FreeDSx\Socket; - -use FreeDSx\Asn1\Type\AbstractType; - -/** - * Throw away PDU class for message queue specs. - * - * @author Chad Sikorra - */ -class Pdu implements \FreeDSx\Socket\PduInterface -{ - /** - * @var AbstractType - */ - protected $type; - - /** - * @param AbstractType $type - */ - public function __construct(AbstractType $type) - { - $this->type = $type; - } - - /** - * {@inheritdoc} - */ - public function toAsn1(): AbstractType - { - return $this->type; - } - - /** - * {@inheritdoc} - */ - public static function fromAsn1(AbstractType $asn1) - { - return new self($asn1); - } -} diff --git a/tests/spec/FreeDSx/Socket/Exception/ConnectionExceptionSpec.php b/tests/spec/FreeDSx/Socket/Exception/ConnectionExceptionSpec.php deleted file mode 100644 index 1a41c99..0000000 --- a/tests/spec/FreeDSx/Socket/Exception/ConnectionExceptionSpec.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace spec\FreeDSx\Socket\Exception; - -use FreeDSx\Socket\Exception\ConnectionException; -use PhpSpec\ObjectBehavior; - -class ConnectionExceptionSpec extends ObjectBehavior -{ - function it_is_initializable() - { - $this->shouldHaveType(ConnectionException::class); - } - - function it_should_extend_exception() - { - $this->shouldBeAnInstanceOf(\Exception::class); - } -} diff --git a/tests/spec/FreeDSx/Socket/Exception/PartialMessageExceptionSpec.php b/tests/spec/FreeDSx/Socket/Exception/PartialMessageExceptionSpec.php deleted file mode 100644 index eee5e1a..0000000 --- a/tests/spec/FreeDSx/Socket/Exception/PartialMessageExceptionSpec.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace spec\FreeDSx\Socket\Exception; - -use FreeDSx\Socket\Exception\PartialMessageException; -use PhpSpec\ObjectBehavior; - -class PartialMessageExceptionSpec extends ObjectBehavior -{ - function it_is_initializable() - { - $this->shouldHaveType(PartialMessageException::class); - } - - function it_should_extend_exception() - { - $this->shouldBeAnInstanceOf(\Exception::class); - } -} diff --git a/tests/spec/FreeDSx/Socket/MessageQueueSpec.php b/tests/spec/FreeDSx/Socket/MessageQueueSpec.php deleted file mode 100644 index fa651f6..0000000 --- a/tests/spec/FreeDSx/Socket/MessageQueueSpec.php +++ /dev/null @@ -1,62 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace spec\FreeDSx\Socket; - -use fixture\FreeDSx\Socket\Pdu; -use FreeDSx\Asn1\Encoder\EncoderInterface; -use FreeDSx\Asn1\Exception\PartialPduException; -use FreeDSx\Asn1\Type\IntegerType; -use FreeDSx\Socket\Exception\ConnectionException; -use FreeDSx\Socket\MessageQueue; -use FreeDSx\Socket\Socket; -use PhpSpec\ObjectBehavior; - -class MessageQueueSpec extends ObjectBehavior -{ - function let(Socket $socket, EncoderInterface $encoder) - { - $this->beConstructedWith($socket, $encoder, Pdu::class); - } - - function it_is_initializable() - { - $this->shouldHaveType(MessageQueue::class); - } - - function it_should_return_a_single_message_on_tcp_read(Socket $socket, EncoderInterface $encoder) - { - $socket->read()->willReturn('foo'); - $socket->read(false)->willReturn(false); - $encoder->decode('foo')->shouldBeCalled()->willReturn(new IntegerType(100)); - $encoder->getLastPosition()->willReturn(2); - - $this->getMessage()->shouldBeLike(new Pdu(new IntegerType(100))); - } - - function it_should_continue_on_during_partial_PDUs(Socket $socket, EncoderInterface $encoder) - { - $socket->read()->willReturn('foo', 'bar'); - $socket->read(false)->willReturn(false); - - $encoder->decode('foo')->shouldBeCalled()->willThrow(PartialPduException::class); - $encoder->decode('foobar')->shouldBeCalled()->willReturn(new IntegerType(100)); - $encoder->getLastPosition()->willReturn(6); - - $this->getMessage()->shouldBeLike(new Pdu(new IntegerType(100))); - } - - function it_should_throw_an_exception_on_get_message_when_there_is_none(Socket $socket) - { - $socket->read()->willReturn(false); - - $this->shouldThrow(ConnectionException::class)->duringGetMessage(); - } -} diff --git a/tests/spec/FreeDSx/Socket/Queue/Asn1MessageQueueSpec.php b/tests/spec/FreeDSx/Socket/Queue/Asn1MessageQueueSpec.php deleted file mode 100644 index 5bfdb7b..0000000 --- a/tests/spec/FreeDSx/Socket/Queue/Asn1MessageQueueSpec.php +++ /dev/null @@ -1,102 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace spec\FreeDSx\Socket\Queue; - -use fixture\FreeDSx\Socket\Pdu; -use FreeDSx\Asn1\Encoder\EncoderInterface; -use FreeDSx\Asn1\Exception\PartialPduException; -use FreeDSx\Asn1\Type\IntegerType; -use FreeDSx\Socket\Exception\ConnectionException; -use FreeDSx\Socket\Queue\Asn1MessageQueue; -use FreeDSx\Socket\Socket; -use PhpSpec\ObjectBehavior; - -class Asn1MessageQueueSpec extends ObjectBehavior -{ - function it_is_initializable() - { - $this->shouldHaveType(Asn1MessageQueue::class); - } - - function let(Socket $socket, EncoderInterface $encoder) - { - $this->beConstructedWith($socket, $encoder, Pdu::class); - } - - function it_should_return_a_single_message_on_tcp_read($socket, $encoder) - { - $socket->read()->willReturn('foo'); - $socket->read(false)->willReturn(false); - $encoder->decode('foo')->shouldBeCalled()->willReturn(new IntegerType(100)); - $encoder->getLastPosition()->willReturn(3); - - $this->getMessage()->shouldBeLike(new Pdu(new IntegerType(100))); - } - - function it_should_continue_on_during_partial_PDUs($socket, $encoder) - { - $socket->read()->willReturn('foo', 'bar'); - - $encoder->decode('foo')->shouldBeCalled()->willThrow(PartialPduException::class); - $encoder->decode('foobar')->shouldBeCalled()->willReturn(new IntegerType(100)); - $encoder->getLastPosition()->willReturn(3); - - $this->getMessage()->shouldBeLike(new Pdu(new IntegerType(100))); - } - - function it_should_throw_an_exception_on_get_message_when_there_is_none($socket) - { - $socket->read()->willReturn(false); - - $this->shouldThrow(ConnectionException::class)->duringGetMessage(); - } - - function it_should_not_peek_the_socket_after_decoding_a_complete_message($socket, $encoder) - { - $socket->read()->willReturn('foo'); - $socket->read(false)->shouldNotBeCalled(); - $encoder->decode('foo')->shouldBeCalled()->willReturn(new IntegerType(100)); - $encoder->getLastPosition()->willReturn(3); - - $this->getMessage()->shouldBeLike(new Pdu(new IntegerType(100))); - } - - function it_should_yield_messages_continuously_from_the_generator($socket, $encoder) - { - $socket->read()->willReturn('foobar', 'baz'); - $encoder->decode('foobar')->willReturn(new IntegerType(1)); - $encoder->decode('bar')->willReturn(new IntegerType(2)); - $encoder->decode('baz')->willReturn(new IntegerType(3)); - $encoder->getLastPosition()->willReturn(3); - - $iter = $this->getMessages()->getWrappedObject(); - - if (!$iter instanceof \Generator) { - throw new \RuntimeException('getMessages() must return a Generator.'); - } - - $first = $iter->current(); - $iter->next(); - $second = $iter->current(); - $iter->next(); - $third = $iter->current(); - - if ($first != new Pdu(new IntegerType(1))) { - throw new \RuntimeException('First yielded message did not match.'); - } - if ($second != new Pdu(new IntegerType(2))) { - throw new \RuntimeException('Second yielded message did not match.'); - } - if ($third != new Pdu(new IntegerType(3))) { - throw new \RuntimeException('Third yielded message did not match.'); - } - } -} diff --git a/tests/spec/FreeDSx/Socket/Queue/BufferSpec.php b/tests/spec/FreeDSx/Socket/Queue/BufferSpec.php deleted file mode 100644 index 93f6ae1..0000000 --- a/tests/spec/FreeDSx/Socket/Queue/BufferSpec.php +++ /dev/null @@ -1,37 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace spec\FreeDSx\Socket\Queue; - -use FreeDSx\Socket\Queue\Buffer; -use PhpSpec\ObjectBehavior; - -class BufferSpec extends ObjectBehavior -{ - function let() - { - $this->beConstructedWith('foo', 4); - } - - function it_is_initializable() - { - $this->shouldHaveType(Buffer::class); - } - - function it_should_get_the_bytes() - { - $this->bytes()->shouldBeEqualTo('foo'); - } - - function it_should_get_where_the_buffer_ends() - { - $this->endsAt()->shouldBeEqualTo(4); - } -} diff --git a/tests/spec/FreeDSx/Socket/Queue/MessageSpec.php b/tests/spec/FreeDSx/Socket/Queue/MessageSpec.php deleted file mode 100644 index 058a6ce..0000000 --- a/tests/spec/FreeDSx/Socket/Queue/MessageSpec.php +++ /dev/null @@ -1,46 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace spec\FreeDSx\Socket\Queue; - -use fixture\FreeDSx\Socket\Pdu; -use FreeDSx\Asn1\Type\IntegerType; -use FreeDSx\Socket\Queue\Message; -use PhpSpec\ObjectBehavior; - -class MessageSpec extends ObjectBehavior -{ - function let() - { - $this->beConstructedWith(new Pdu(new IntegerType(1))); - } - - function it_is_initializable() - { - $this->shouldHaveType(Message::class); - } - - function it_should_get_the_message() - { - $this->getMessage()->shouldBeLike(new Pdu(new IntegerType(1))); - } - - function it_should_have_no_last_position_data_by_default() - { - $this->getLastPosition()->shouldBeNull(); - } - - function it_should_get_the_last_position() - { - $this->beConstructedWith(new Pdu(new IntegerType(1)), 2); - - $this->getLastPosition()->shouldBeEqualTo(2); - } -} diff --git a/tests/spec/FreeDSx/Socket/SocketServerSpec.php b/tests/spec/FreeDSx/Socket/SocketServerSpec.php deleted file mode 100644 index ef034fc..0000000 --- a/tests/spec/FreeDSx/Socket/SocketServerSpec.php +++ /dev/null @@ -1,96 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace spec\FreeDSx\Socket; - -use FreeDSx\Socket\Exception\ConnectionException; -use FreeDSx\Socket\Socket; -use FreeDSx\Socket\SocketServer; -use PhpSpec\ObjectBehavior; - -class SocketServerSpec extends ObjectBehavior -{ - use RequiresUnixTransport; - - private $testSocket = ''; - - function let() - { - $this->testSocket = sys_get_temp_dir() . '/phpspec.socket'; - $this->beConstructedThrough('bind', ['0.0.0.0', 33389]); - } - - function letGo() - { - @$this->close(); - if ($this->testSocket && file_exists($this->testSocket)) { - @unlink($this->testSocket); - } - } - - function it_is_initializable() - { - $this->shouldHaveType(SocketServer::class); - } - - function it_should_throw_a_connection_exception_if_it_cannot_listen_on_the_ip_and_port() - { - $this->beConstructedWith([]); - - $this->shouldThrow(ConnectionException::class)->during('Listen',['1.2.3.4', 389]); - } - - function it_should_return_null_if_there_is_no_client_on_accept() - { - $this->accept(0)->shouldBeNull(); - } - - function it_should_construct_a_tcp_based_socket_server() - { - $this->beConstructedThrough('bindTcp', ['0.0.0.0', 33389]); - - $this->getOptions()->shouldHaveKeyWithValue('transport', 'tcp'); - $this->isConnected()->shouldBeEqualTo(true); - $this->close(); - $this->isConnected()->shouldBeEqualTo(false); - } - - function it_should_construct_a_udp_based_socket_server() - { - $this->beConstructedThrough('bindUdp', ['0.0.0.0', 33389]); - - $this->getOptions()->shouldHaveKeyWithValue('transport', 'udp'); - } - - function it_should_construct_a_unix_based_socket_server() - { - $this->requireUnixTransport(); - - $this->beConstructedThrough('bindUnix', [$this->testSocket]); - - $this->getOptions()->shouldHaveKeyWithValue('transport', 'unix'); - $this->isConnected()->shouldBeEqualTo(true); - $this->close(); - $this->isConnected()->shouldBeEqualTo(false); - } - - function it_should_receive_data() - { - $this->beConstructedThrough('bindUdp', ['0.0.0.0', 33389]); - # This is here to force PhpSpec to construct the object. It needs to be constructed to write to it. - # Otherwise, the test would hang... - $this->getOptions(); - - $socket = Socket::udp('127.0.0.1', ['port' => 33389]); - $socket->write('foo'); - - $this->receive()->shouldBeEqualTo('foo'); - } -} diff --git a/tests/spec/FreeDSx/Socket/SocketSpec.php b/tests/spec/FreeDSx/Socket/SocketSpec.php deleted file mode 100644 index fad4a3d..0000000 --- a/tests/spec/FreeDSx/Socket/SocketSpec.php +++ /dev/null @@ -1,200 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace spec\FreeDSx\Socket; - -use FreeDSx\Socket\Socket; -use PhpSpec\ObjectBehavior; - -class SocketSpec extends ObjectBehavior -{ - use RequiresUnixTransport; - - - /** - * @var resource|null - */ - private $local; - - /** - * @var resource|null - */ - private $remote; - - /** - * @var resource|null - */ - private $unixServer; - - /** - * @var string|null - */ - private $unixPath; - - function letGo(): void - { - if (is_resource($this->remote)) { - fclose($this->remote); - } - if (is_resource($this->local)) { - fclose($this->local); - } - if (is_resource($this->unixServer)) { - fclose($this->unixServer); - } - if ($this->unixPath !== null && file_exists($this->unixPath)) { - @unlink($this->unixPath); - } - } - - private function createSocketPair(): void - { - $domain = DIRECTORY_SEPARATOR === '\\' - ? STREAM_PF_INET - : STREAM_PF_UNIX; - - [$this->local, $this->remote] = stream_socket_pair( - $domain, - STREAM_SOCK_STREAM, - STREAM_IPPROTO_IP - ); - } - - private function createUnixServer(): string - { - $this->requireUnixTransport(); - $this->unixPath = sys_get_temp_dir() . '/freedsx_socket_' . uniqid('', true) . '.sock'; - $this->unixServer = stream_socket_server('unix://' . $this->unixPath); - - return $this->unixPath; - } - - function it_is_initializable() - { - $this->shouldHaveType(Socket::class); - } - - function it_should_get_the_options_for_the_socket() - { - $this->getOptions()->shouldBeEqualTo([ - 'transport' => 'tcp', - 'port' => 389, - 'use_ssl' => false, - 'ssl_crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLS_CLIENT, - 'ssl_ciphers' => 'DEFAULT', - 'ssl_validate_cert' => true, - 'ssl_allow_self_signed' => null, - 'ssl_ca_cert' => null, - 'ssl_peer_name' => null, - 'timeout_connect' => 3, - 'timeout_read' => 15, - 'buffer_size' => 8192, - ]); - } - - function it_should_create_a_socket() - { - $this::create('www.google.com', ['port' => 80])->shouldBeAnInstanceOf(Socket::class); - } - - function it_should_create_a_unix_based_socket() - { - $path = $this->createUnixServer(); - - $this::unix($path)->shouldBeAnInstanceOf(Socket::class); - } - - function it_should_create_a_tcp_based_socket() - { - $this::tcp('www.google.com', ['port' => 80])->getOptions()->shouldHaveKeyWithValue('transport', 'tcp'); - } - - function it_should_create_a_udp_based_socket() - { - $this::udp('8.8.8.8', ['port' => 53])->getOptions()->shouldHaveKeyWithValue('transport', 'udp'); - } - - function it_should_have_a_default_buffer_size_of_65507_for_UDP() - { - $this::udp('8.8.8.8', ['port' => 53])->getOptions()->shouldHaveKeyWithValue('buffer_size', 65507); - } - - function it_should_tell_whether_or_not_it_is_connected_for_tcp() - { - $this->beConstructedThrough('tcp', ['www.google.com', ['port' => 80]]); - - $this->isConnected()->shouldBeEqualTo(true); - $this->close(); - $this->isConnected()->shouldBeEqualTo(false); - } - - function it_should_tell_whether_or_not_it_is_connected_for_udp() - { - $this->beConstructedThrough('udp', ['www.google.com', ['port' => 53]]); - - $this->isConnected()->shouldBeEqualTo(true); - $this->close(); - $this->isConnected()->shouldBeEqualTo(false); - } - - function it_should_tell_whether_it_is_connected_for_unix() - { - $path = $this->createUnixServer(); - $this->beConstructedThrough('unix', [$path]); - - $this->isConnected()->shouldBeEqualTo(true); - $this->close(); - $this->isConnected()->shouldBeEqualTo(false); - } - - function it_should_return_at_most_buffer_size_bytes_per_read() - { - $this->createSocketPair(); - fwrite($this->remote, '0123456789'); - - $this->beConstructedWith($this->local, ['buffer_size' => 4]); - - $this->read()->shouldBe('0123'); - $this->read()->shouldBe('4567'); - $this->read()->shouldBe('89'); - } - - function it_should_return_false_on_a_non_blocking_read_when_no_data_is_available() - { - $this->createSocketPair(); - - $this->beConstructedWith($this->local); - - $this->read(false)->shouldBe(false); - } - - function it_should_return_false_on_a_blocking_read_when_the_peer_has_closed() - { - $this->createSocketPair(); - fclose($this->remote); - - $this->beConstructedWith($this->local); - - $this->read()->shouldBe(false); - } - - function it_should_leave_the_socket_in_blocking_mode_after_a_non_blocking_read() - { - $this->createSocketPair(); - - $this->beConstructedWith($this->local); - - $this->read(false); - - if (stream_get_meta_data($this->local)['blocked'] !== true) { - throw new \RuntimeException('Socket should be in blocking mode after a non-blocking read.'); - } - } -} diff --git a/tests/unit/Pdu.php b/tests/unit/Pdu.php new file mode 100644 index 0000000..6a92078 --- /dev/null +++ b/tests/unit/Pdu.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Unit\FreeDSx\Socket; + +use FreeDSx\Asn1\Type\AbstractType; +use FreeDSx\Socket\PduInterface; + +/** + * Throw away PDU class for message queue tests. + */ +class Pdu implements PduInterface +{ + public function __construct(private readonly AbstractType $type) + { + } + + public function toAsn1(): AbstractType + { + return $this->type; + } + + public static function fromAsn1(AbstractType $asn1): self + { + return new self($asn1); + } +} diff --git a/tests/unit/Queue/Asn1MessageQueueTest.php b/tests/unit/Queue/Asn1MessageQueueTest.php new file mode 100644 index 0000000..f34be58 --- /dev/null +++ b/tests/unit/Queue/Asn1MessageQueueTest.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Unit\FreeDSx\Socket\Queue; + +use FreeDSx\Asn1\Encoder\EncoderInterface; +use FreeDSx\Asn1\Exception\PartialPduException; +use FreeDSx\Asn1\Type\IntegerType; +use FreeDSx\Socket\Exception\ConnectionException; +use FreeDSx\Socket\Queue\Asn1MessageQueue; +use FreeDSx\Socket\Socket; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Tests\Unit\FreeDSx\Socket\Pdu; + +final class Asn1MessageQueueTest extends TestCase +{ + private Socket&MockObject $socket; + + private EncoderInterface&MockObject $encoder; + + private Asn1MessageQueue $subject; + + protected function setUp(): void + { + $this->socket = $this->createMock(Socket::class); + $this->encoder = $this->createMock(EncoderInterface::class); + $this->subject = new Asn1MessageQueue( + $this->socket, + $this->encoder, + Pdu::class, + ); + } + + public function test_it_should_return_a_single_message_on_tcp_read(): void + { + $this->socket->method('read')->willReturn('foo'); + $this->encoder + ->expects(self::atLeastOnce()) + ->method('decode') + ->with('foo') + ->willReturn(new IntegerType(100)); + $this->encoder->method('getLastPosition')->willReturn(3); + + self::assertEquals( + new Pdu(new IntegerType(100)), + $this->subject->getMessage(), + ); + } + + public function test_it_should_continue_on_during_partial_PDUs(): void + { + $this->socket + ->method('read') + ->willReturnOnConsecutiveCalls('foo', 'bar'); + $this->encoder + ->expects(self::atLeast(2)) + ->method('decode') + ->willReturnCallback( + static fn (string $bytes): IntegerType => match ($bytes) { + 'foo' => throw new PartialPduException(), + 'foobar' => new IntegerType(100), + default => self::fail("Unexpected decode argument: {$bytes}"), + }, + ); + $this->encoder->method('getLastPosition')->willReturn(3); + + self::assertEquals( + new Pdu(new IntegerType(100)), + $this->subject->getMessage(), + ); + } + + public function test_it_should_throw_an_exception_on_get_message_when_there_is_none(): void + { + $this->socket->method('read')->willReturn(false); + + $this->expectException(ConnectionException::class); + + $this->subject->getMessage(); + } + + public function test_it_should_not_peek_the_socket_after_decoding_a_complete_message(): void + { + $this->socket->method('read')->willReturnCallback( + static function (bool $block = true): string { + if (!$block) { + self::fail('socket->read(false) should not be called after decoding a complete message.'); + } + return 'foo'; + }, + ); + $this->encoder + ->expects(self::atLeastOnce()) + ->method('decode') + ->with('foo') + ->willReturn(new IntegerType(100)); + $this->encoder->method('getLastPosition')->willReturn(3); + + self::assertEquals( + new Pdu(new IntegerType(100)), + $this->subject->getMessage(), + ); + } + + public function test_it_should_yield_messages_continuously_from_the_generator(): void + { + $this->socket + ->method('read') + ->willReturnOnConsecutiveCalls('foobar', 'baz'); + $this->encoder + ->method('decode') + ->willReturnCallback( + static fn (string $bytes): IntegerType => match ($bytes) { + 'foobar' => new IntegerType(1), + 'bar' => new IntegerType(2), + 'baz' => new IntegerType(3), + default => self::fail("Unexpected decode argument: {$bytes}"), + }, + ); + $this->encoder->method('getLastPosition')->willReturn(3); + + $iter = $this->subject->getMessages(); + + self::assertEquals(new Pdu(new IntegerType(1)), $iter->current()); + $iter->next(); + self::assertEquals(new Pdu(new IntegerType(2)), $iter->current()); + $iter->next(); + self::assertEquals(new Pdu(new IntegerType(3)), $iter->current()); + } +} diff --git a/tests/unit/Queue/BufferTest.php b/tests/unit/Queue/BufferTest.php new file mode 100644 index 0000000..b087238 --- /dev/null +++ b/tests/unit/Queue/BufferTest.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Unit\FreeDSx\Socket\Queue; + +use FreeDSx\Socket\Queue\Buffer; +use PHPUnit\Framework\TestCase; + +final class BufferTest extends TestCase +{ + private Buffer $subject; + + protected function setUp(): void + { + $this->subject = new Buffer('foo', 4); + } + + public function test_it_should_get_the_bytes(): void + { + self::assertSame( + 'foo', + $this->subject->bytes(), + ); + } + + public function test_it_should_get_where_the_buffer_ends(): void + { + self::assertSame( + 4, + $this->subject->endsAt(), + ); + } +} diff --git a/tests/unit/Queue/MessageTest.php b/tests/unit/Queue/MessageTest.php new file mode 100644 index 0000000..b80abb0 --- /dev/null +++ b/tests/unit/Queue/MessageTest.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Unit\FreeDSx\Socket\Queue; + +use FreeDSx\Asn1\Type\IntegerType; +use FreeDSx\Socket\Queue\Message; +use PHPUnit\Framework\TestCase; +use Tests\Unit\FreeDSx\Socket\Pdu; + +final class MessageTest extends TestCase +{ + private Message $subject; + + protected function setUp(): void + { + $this->subject = new Message(new Pdu(new IntegerType(1))); + } + + public function test_it_should_get_the_message(): void + { + self::assertEquals( + new Pdu(new IntegerType(1)), + $this->subject->getMessage(), + ); + } + + public function test_it_should_have_no_last_position_data_by_default(): void + { + self::assertNull($this->subject->getLastPosition()); + } + + public function test_it_should_get_the_last_position(): void + { + $this->subject = new Message(new Pdu(new IntegerType(1)), 2); + + self::assertSame( + 2, + $this->subject->getLastPosition(), + ); + } +} diff --git a/tests/spec/FreeDSx/Socket/RequiresUnixTransport.php b/tests/unit/RequiresUnixTransport.php similarity index 68% rename from tests/spec/FreeDSx/Socket/RequiresUnixTransport.php rename to tests/unit/RequiresUnixTransport.php index b0550d6..5febef1 100644 --- a/tests/spec/FreeDSx/Socket/RequiresUnixTransport.php +++ b/tests/unit/RequiresUnixTransport.php @@ -1,5 +1,7 @@ beConstructedWith(['servers' => ['foo', 'bar']]); - } + private ?string $unixPath = null; - function letGo(): void + protected function tearDown(): void { if (is_resource($this->unixServer)) { fclose($this->unixServer); @@ -46,22 +37,21 @@ function letGo(): void } } - function it_is_initializable() - { - $this->shouldHaveType(SocketPool::class); - } - - function it_should_respect_the_transport_type_when_connecting() + public function test_it_should_respect_the_transport_type_when_connecting(): void { $this->requireUnixTransport(); $this->unixPath = sys_get_temp_dir() . '/freedsx_socket_pool_' . uniqid('', true) . '.sock'; - $this->unixServer = stream_socket_server('unix://' . $this->unixPath); + $server = stream_socket_server('unix://' . $this->unixPath); + if ($server === false) { + self::fail('Failed to create unix socket server.'); + } + $this->unixServer = $server; - $this->beConstructedWith([ + $subject = new SocketPool([ 'servers' => [$this->unixPath], 'transport' => 'unix', ]); - $this->connect()->isConnected()->shouldBeEqualTo(true); + self::assertTrue($subject->connect()->isConnected()); } } diff --git a/tests/unit/SocketServerTest.php b/tests/unit/SocketServerTest.php new file mode 100644 index 0000000..43f6d1b --- /dev/null +++ b/tests/unit/SocketServerTest.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Unit\FreeDSx\Socket; + +use FreeDSx\Socket\Exception\ConnectionException; +use FreeDSx\Socket\Socket; +use FreeDSx\Socket\SocketServer; +use PHPUnit\Framework\TestCase; + +final class SocketServerTest extends TestCase +{ + use RequiresUnixTransport; + + private string $testSocket = ''; + + private ?SocketServer $subject = null; + + protected function setUp(): void + { + $this->testSocket = sys_get_temp_dir() . '/phpunit.socket'; + } + + protected function tearDown(): void + { + $this->subject?->close(); + if ($this->testSocket !== '' && file_exists($this->testSocket)) { + @unlink($this->testSocket); + } + } + + public function test_it_should_throw_a_connection_exception_if_it_cannot_listen_on_the_ip_and_port(): void + { + $this->subject = new SocketServer([]); + + $this->expectException(ConnectionException::class); + + $this->subject->listen('1.2.3.4', 389); + } + + public function test_it_should_return_null_if_there_is_no_client_on_accept(): void + { + $this->subject = SocketServer::bind('0.0.0.0', 33389); + + self::assertNull($this->subject->accept(0)); + } + + public function test_it_should_construct_a_tcp_based_socket_server(): void + { + $this->subject = SocketServer::bindTcp('0.0.0.0', 33389); + + self::assertSame( + 'tcp', + $this->subject->getOptions()['transport'] + ); + self::assertTrue($this->subject->isConnected()); + $this->subject->close(); + self::assertFalse($this->subject->isConnected()); + } + + public function test_it_should_construct_a_udp_based_socket_server(): void + { + $this->subject = SocketServer::bindUdp('0.0.0.0', 33389); + + self::assertSame('udp', $this->subject->getOptions()['transport']); + } + + public function test_it_should_construct_a_unix_based_socket_server(): void + { + $this->requireUnixTransport(); + + $this->subject = SocketServer::bindUnix($this->testSocket); + + self::assertSame( + 'unix', + $this->subject->getOptions()['transport'] + ); + self::assertTrue($this->subject->isConnected()); + $this->subject->close(); + self::assertFalse($this->subject->isConnected()); + } + + public function test_it_should_receive_data(): void + { + $this->subject = SocketServer::bindUdp('0.0.0.0', 33389); + + $client = Socket::udp('127.0.0.1', ['port' => 33389]); + $client->write('foo'); + + self::assertSame( + 'foo', + $this->subject->receive() + ); + } +} diff --git a/tests/unit/SocketTest.php b/tests/unit/SocketTest.php new file mode 100644 index 0000000..7b3a7c8 --- /dev/null +++ b/tests/unit/SocketTest.php @@ -0,0 +1,220 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\Unit\FreeDSx\Socket; + +use FreeDSx\Socket\Socket; +use PHPUnit\Framework\TestCase; + +final class SocketTest extends TestCase +{ + use RequiresUnixTransport; + + /** + * @var resource|null + */ + private $local; + + /** + * @var resource|null + */ + private $remote; + + /** + * @var resource|null + */ + private $unixServer; + + private ?string $unixPath = null; + + protected function tearDown(): void + { + if (is_resource($this->remote)) { + fclose($this->remote); + } + if (is_resource($this->local)) { + fclose($this->local); + } + if (is_resource($this->unixServer)) { + fclose($this->unixServer); + } + if ($this->unixPath !== null && file_exists($this->unixPath)) { + @unlink($this->unixPath); + } + } + + public function test_it_should_get_the_options_for_the_socket(): void + { + $subject = new Socket(); + + self::assertSame( + [ + 'transport' => 'tcp', + 'port' => 389, + 'use_ssl' => false, + 'ssl_crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLS_CLIENT, + 'ssl_ciphers' => 'DEFAULT', + 'ssl_validate_cert' => true, + 'ssl_allow_self_signed' => null, + 'ssl_ca_cert' => null, + 'ssl_peer_name' => null, + 'timeout_connect' => 3, + 'timeout_read' => 15, + 'buffer_size' => 8192, + ], + $subject->getOptions(), + ); + } + + public function test_it_should_create_a_socket(): void + { + $subject = Socket::create('www.google.com', ['port' => 80]); + + self::assertTrue($subject->isConnected()); + } + + public function test_it_should_create_a_unix_based_socket(): void + { + $path = $this->createUnixServer(); + + $subject = Socket::unix($path); + + self::assertSame('unix', $subject->getOptions()['transport']); + } + + public function test_it_should_create_a_tcp_based_socket(): void + { + self::assertSame( + 'tcp', + Socket::tcp('www.google.com', ['port' => 80])->getOptions()['transport'], + ); + } + + public function test_it_should_create_a_udp_based_socket(): void + { + self::assertSame( + 'udp', + Socket::udp('8.8.8.8', ['port' => 53])->getOptions()['transport'], + ); + } + + public function test_it_should_have_a_default_buffer_size_of_65507_for_UDP(): void + { + self::assertSame( + 65507, + Socket::udp('8.8.8.8', ['port' => 53])->getOptions()['buffer_size'], + ); + } + + public function test_it_should_tell_whether_or_not_it_is_connected_for_tcp(): void + { + $subject = Socket::tcp('www.google.com', ['port' => 80]); + + self::assertTrue($subject->isConnected()); + $subject->close(); + self::assertFalse($subject->isConnected()); + } + + public function test_it_should_tell_whether_or_not_it_is_connected_for_udp(): void + { + $subject = Socket::udp('www.google.com', ['port' => 53]); + + self::assertTrue($subject->isConnected()); + $subject->close(); + self::assertFalse($subject->isConnected()); + } + + public function test_it_should_tell_whether_it_is_connected_for_unix(): void + { + $path = $this->createUnixServer(); + $subject = Socket::unix($path); + + self::assertTrue($subject->isConnected()); + $subject->close(); + self::assertFalse($subject->isConnected()); + } + + public function test_it_should_return_at_most_buffer_size_bytes_per_read(): void + { + [$local, $remote] = $this->createSocketPair(); + fwrite($remote, '0123456789'); + + $subject = new Socket($local, ['buffer_size' => 4]); + + self::assertSame('0123', $subject->read()); + self::assertSame('4567', $subject->read()); + self::assertSame('89', $subject->read()); + } + + public function test_it_should_return_false_on_a_non_blocking_read_when_no_data_is_available(): void + { + [$local] = $this->createSocketPair(); + $subject = new Socket($local); + + self::assertFalse($subject->read(false)); + } + + public function test_it_should_return_false_on_a_blocking_read_when_the_peer_has_closed(): void + { + [$local, $remote] = $this->createSocketPair(); + fclose($remote); + $subject = new Socket($local); + + self::assertFalse($subject->read()); + } + + public function test_it_should_leave_the_socket_in_blocking_mode_after_a_non_blocking_read(): void + { + [$local] = $this->createSocketPair(); + $subject = new Socket($local); + + $subject->read(false); + + self::assertTrue(stream_get_meta_data($local)['blocked']); + } + + /** + * @return array{0: resource, 1: resource} + */ + private function createSocketPair(): array + { + $domain = DIRECTORY_SEPARATOR === '\\' + ? STREAM_PF_INET + : STREAM_PF_UNIX; + + $pair = stream_socket_pair( + $domain, + STREAM_SOCK_STREAM, + STREAM_IPPROTO_IP, + ); + if ($pair === false) { + self::fail('Failed to create socket pair.'); + } + [$this->local, $this->remote] = $pair; + + return [$this->local, $this->remote]; + } + + private function createUnixServer(): string + { + $this->requireUnixTransport(); + $this->unixPath = sys_get_temp_dir() . '/freedsx_socket_' . uniqid('', true) . '.sock'; + $server = stream_socket_server('unix://' . $this->unixPath); + if ($server === false) { + self::fail('Failed to create unix socket server.'); + } + $this->unixServer = $server; + + return $this->unixPath; + } +}