Browse Source

Merge pull request #8 from EvilFreelancer/streamrefactor

Stream refactoring
tags/0.9 0.9
Coder 7 years ago
committed by GitHub
parent
commit
bb3e52a90a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      phpunit.xml
  2. 59
      src/APIConnector.php
  3. 196
      src/APILengthCoDec.php
  4. 131
      src/Client.php
  5. 14
      src/Exceptions/StreamException.php
  6. 65
      src/Helpers/BinaryStringHelper.php
  7. 46
      src/Interfaces/StreamInterface.php
  8. 21
      src/SocketTrait.php
  9. 110
      src/Streams/ResourceStream.php
  10. 97
      src/Streams/StringStream.php
  11. 98
      tests/APIConnectorTest.php
  12. 106
      tests/APILengthCoDecTest.php
  13. 50
      tests/ClientTest.php
  14. 49
      tests/Helpers/BinaryStringHelperTest.php
  15. 263
      tests/Streams/ResourceStreamTest.php
  16. 160
      tests/Streams/StringStreamTest.php

7
phpunit.xml

@ -16,4 +16,11 @@
<directory suffix=".php">./tests/</directory> <directory suffix=".php">./tests/</directory>
</testsuite> </testsuite>
</testsuites> </testsuites>
<php>
<env name="ROS_HOST" value="127.0.0.1"/>
<env name="ROS_USER" value="admin"/>
<env name="ROS_PASS" value="admin"/>
<env name="ROS_PORT_MODERN" value="18728"/>
<env name="ROS_PORT_LEGACY" value="28728"/>
</php>
</phpunit> </phpunit>

59
src/APIConnector.php

@ -0,0 +1,59 @@
<?php
namespace RouterOS;
use RouterOS\Interfaces\StreamInterface;
/**
* Class APIConnector
*
* Implement middle level dialog with router by masking word dialog implementation to client class
*
* @package RouterOS
* @since 0.9
*/
class APIConnector
{
/**
* @var StreamInterface $stream The stream used to communicate with the router
*/
protected $stream;
/**
* Constructor
*
* @param StreamInterface $stream
*/
public function __construct(StreamInterface $stream)
{
$this->stream = $stream;
}
/**
* Reads a WORD from the stream
*
* WORDs are part of SENTENCE. Each WORD has to be encoded in certain way - length of the WORD followed by WORD content.
* Length of the WORD should be given as count of bytes that are going to be sent
*
* @return string The word content, en empty string for end of SENTENCE
*/
public function readWord(): string
{
// Get length of next word
$length = APILengthCoDec::decodeLength($this->stream);
return ($length > 0) ? $this->stream->read($length) : '';
}
/**
* Write word to stream
*
* @param string $word
* @return int return number of written bytes
*/
public function writeWord(string $word): int
{
$encodedLength = APILengthCoDec::encodeLength(strlen($word));
return $this->stream->write($encodedLength . $word);
}
}

196
src/APILengthCoDec.php

@ -0,0 +1,196 @@
<?php
namespace RouterOS;
use RouterOS\Interfaces\StreamInterface;
use RouterOS\Helpers\BinaryStringHelper;
/**
* class APILengthCoDec
*
* Coder / Decoder for length field in mikrotik API communication protocol
*
* @package RouterOS
* @since 0.9
*/
class APILengthCoDec
{
/**
* Encode string to length of string
*
* @param int|float $length
* @return string
*/
public static function encodeLength($length): string
{
// Encode the length :
// - if length <= 0x7F (binary : 01111111 => 7 bits set to 1)
// - encode length with one byte
// - set the byte to length value, as length maximal value is 7 bits set to 1, the most significant bit is always 0
// - end
// - length <= 0x3FFF (binary : 00111111 11111111 => 14 bits set to 1)
// - encode length with two bytes
// - set length value to 0x8000 (=> 10000000 00000000)
// - add length : as length maximumal value is 14 bits to 1, this does not modify the 2 most significance bits (10)
// - end
// => minimal encoded value is 10000000 10000000
// - length <= 0x1FFFFF (binary : 00011111 11111111 11111111 => 21 bits set to 1)
// - encode length with three bytes
// - set length value to 0xC00000 (binary : 11000000 00000000 00000000)
// - add length : as length maximal vlaue is 21 bits to 1, this does not modify the 3 most significance bits (110)
// - end
// => minimal encoded value is 11000000 01000000 00000000
// - length <= 0x0FFFFFFF (binary : 00001111 11111111 11111111 11111111 => 28 bits set to 1)
// - encode length with four bytes
// - set length value to 0xE0000000 (binary : 11100000 00000000 00000000 00000000)
// - add length : as length maximal vlaue is 28 bits to 1, this does not modify the 4 most significance bits (1110)
// - end
// => minimal encoded value is 11100000 00100000 00000000 00000000
// - length <= 0x7FFFFFFFFF (binary : 00000111 11111111 11111111 11111111 11111111 => 35 bits set to 1)
// - encode length with five bytes
// - set length value to 0xF000000000 (binary : 11110000 00000000 00000000 00000000 00000000)
// - add length : as length maximal vlaue is 35 bits to 1, this does not modify the 5 most significance bits (11110)
// - end
// - length > 0x7FFFFFFFFF : not supported
if ($length < 0) {
throw new \DomainException("Length of word could not to be negative ($length)");
}
if ($length <= 0x7F) {
return BinaryStringHelper::IntegerToNBOBinaryString($length);
}
if ($length <= 0x3FFF) {
return BinaryStringHelper::IntegerToNBOBinaryString(0x8000 + $length);
}
if ($length <= 0x1FFFFF) {
return BinaryStringHelper::IntegerToNBOBinaryString(0xC00000 + $length);
}
if ($length <= 0x0FFFFFFF) {
return BinaryStringHelper::IntegerToNBOBinaryString(0xE0000000 + $length);
}
// https://wiki.mikrotik.com/wiki/Manual:API#API_words
// If len >= 0x10000000 then 0xF0 and len as four bytes
return BinaryStringHelper::IntegerToNBOBinaryString(0xF000000000 + $length);
}
// Decode length of data when reading :
// The 5 firsts bits of the first byte specify how the length is encoded.
// The position of the first 0 value bit, starting from the most significant postion.
// - 0xxxxxxx => The 7 remainings bits of the first byte is the length :
// => min value of length is 0x00
// => max value of length is 0x7F (127 bytes)
// - 10xxxxxx => The 6 remainings bits of the first byte plus the next byte represent the lenght
// NOTE : the next byte MUST be at least 0x80 !!
// => min value of length is 0x80
// => max value of length is 0x3FFF (16,383 bytes, near 16 KB)
// - 110xxxxx => The 5 remainings bits of th first byte and the two next bytes represent the length
// => max value of length is 0x1FFFFF (2,097,151 bytes, near 2 MB)
// - 1110xxxx => The 4 remainings bits of the first byte and the three next bytes represent the length
// => max value of length is 0xFFFFFFF (268,435,455 bytes, near 270 MB)
// - 11110xxx => The 3 remainings bits of the first byte and the four next bytes represent the length
// => max value of length is 0x7FFFFFFF (2,147,483,647 byes, 2GB)
// - 11111xxx => This byte is not a length-encoded word but a control byte.
// => Extracted from Mikrotik API doc :
// it is a reserved control byte.
// After receiving unknown control byte API client cannot proceed, because it cannot know how to interpret following bytes
// Currently control bytes are not used
public static function decodeLength(StreamInterface $stream): int
{
// if (false === is_resource($stream)) {
// throw new \InvalidArgumentException(
// sprintf(
// 'Argument must be a stream resource type. %s given.',
// gettype($stream)
// )
// );
// }
// Read first byte
$firstByte = ord($stream->read(1));
// If first byte is not set, length is the value of the byte
if (0 === ($firstByte & 0x80)) {
return $firstByte;
}
// if 10xxxxxx, length is 2 bytes encoded
if (0x80 === ($firstByte & 0xC0)) {
// Set 2 most significands bits to 0
$result = $firstByte & 0x3F;
// shift left 8 bits to have 2 bytes
$result <<= 8;
// read next byte and use it as least significant
$result |= ord($stream->read(1));
return $result;
}
// if 110xxxxx, length is 3 bytes encoded
if (0xC0 === ($firstByte & 0xE0)) {
// Set 3 most significands bits to 0
$result = $firstByte & 0x1F;
// shift left 16 bits to have 3 bytes
$result <<= 16;
// read next 2 bytes as value and use it as least significant position
$result |= (ord($stream->read(1)) << 8);
$result |= ord($stream->read(1));
return $result;
}
// if 1110xxxx, length is 4 bytes encoded
if (0xE0 === ($firstByte & 0xF0)) {
// Set 4 most significance bits to 0
$result = $firstByte & 0x0F;
// shift left 24 bits to have 4 bytes
$result <<= 24;
// read next 3 bytes as value and use it as least significant position
$result |= (ord($stream->read(1)) << 16);
$result |= (ord($stream->read(1)) << 8);
$result |= ord($stream->read(1));
return $result;
}
// if 11110xxx, length is 5 bytes encoded
if (0xF0 === ($firstByte & 0xF8)) {
// Not possible on 32 bits systems
if (PHP_INT_SIZE < 8) {
// Cannot be done on 32 bits systems
// PHP5 windows versions of php, even on 64 bits systems was impacted
// see : https://stackoverflow.com/questions/27865340/php-int-size-returns-4-but-my-operating-system-is-64-bit
// How can we test it ?
// @codeCoverageIgnoreStart
throw new \OverflowException("Your system is using 32 bits integers, cannot decode this value ($firstByte) on this system");
// @codeCoverageIgnoreEnd
}
// Set 5 most significance bits to 0
$result = $firstByte & 0x07;
// shift left 232 bits to have 5 bytes
$result <<= 32;
// read next 4 bytes as value and use it as least significant position
$result |= (ord($stream->read(1)) << 24);
$result |= (ord($stream->read(1)) << 16);
$result |= (ord($stream->read(1)) << 8);
$result |= ord($stream->read(1));
return $result;
}
// Now the only solution is 5 most significance bits are set to 1 (11111xxx)
// This is a control word, not implemented by Mikrotik for the moment
throw new \UnexpectedValueException('Control Word found');
}
}

131
src/Client.php

@ -18,32 +18,19 @@ class Client implements Interfaces\ClientInterface
use SocketTrait; use SocketTrait;
/** /**
* Socket resource
* Configuration of connection
* *
* @var resource|null
* @var \RouterOS\Config
*/ */
private $_socket;
private $_config;
/** /**
* Code of error
* API communication object
* *
* @var int
* @var \RouterOS\APIConnector
*/ */
private $_socket_err_num;
/**
* Description of socket error
*
* @var string
*/
private $_socket_err_str;
/**
* Configuration of connection
*
* @var \RouterOS\Config
*/
private $_config;
private $_connector;
/** /**
* Client constructor. * Client constructor.
@ -109,91 +96,10 @@ class Client implements Interfaces\ClientInterface
} }
/** /**
* Encode given length in RouterOS format
*
* @param string $string
* @return string Encoded length
* @throws \RouterOS\Exceptions\ClientException
*/
private function encodeLength(string $string): string
{
$length = \strlen($string);
if ($length < 128) {
$orig_length = $length;
$offset = -1;
} elseif ($length < 16384) {
$orig_length = $length | 0x8000;
$offset = -2;
} elseif ($length < 2097152) {
$orig_length = $length | 0xC00000;
$offset = -3;
} elseif ($length < 268435456) {
$orig_length = $length | 0xE0000000;
$offset = -4;
} else {
throw new ClientException("Unable to encode length of '$string'");
}
// Pack string to binary format
$result = pack('I*', $orig_length);
// Parse binary string to array
$result = str_split($result);
// Reverse array
$result = array_reverse($result);
// Extract values from offset to end of array
$result = \array_slice($result, $offset);
// Sew items into one line
$output = null;
foreach ($result as $item) {
$output .= $item;
}
return $output;
}
/**
* Read length of line
*
* @param int $byte
* @return int
*/
private function getLength(int $byte): int
{
// If the first bit is set then we need to remove the first four bits, shift left 8
// and then read another byte in.
// We repeat this for the second and third bits.
// If the fourth bit is set, we need to remove anything left in the first byte
// and then read in yet another byte.
if ($byte & 128) {
if (($byte & 192) === 128) {
$length = (($byte & 63) << 8) + \ord(fread($this->_socket, 1));
} elseif (($byte & 224) === 192) {
$length = (($byte & 31) << 8) + \ord(fread($this->_socket, 1));
$length = ($length << 8) + \ord(fread($this->_socket, 1));
} elseif (($byte & 240) === 224) {
$length = (($byte & 15) << 8) + \ord(fread($this->_socket, 1));
$length = ($length << 8) + \ord(fread($this->_socket, 1));
$length = ($length << 8) + \ord(fread($this->_socket, 1));
} else {
$length = \ord(fread($this->_socket, 1));
$length = ($length << 8) + \ord(fread($this->_socket, 1)) * 3;
$length = ($length << 8) + \ord(fread($this->_socket, 1));
$length = ($length << 8) + \ord(fread($this->_socket, 1));
}
} else {
$length = $byte;
}
return $length;
}
/**
* Send write query to RouterOS (with or without tag) * Send write query to RouterOS (with or without tag)
* *
* @param string|array|\RouterOS\Query $query * @param string|array|\RouterOS\Query $query
* @return \RouterOS\Client * @return \RouterOS\Client
* @throws \RouterOS\Exceptions\ClientException
* @throws \RouterOS\Exceptions\QueryException * @throws \RouterOS\Exceptions\QueryException
*/ */
public function write($query): Client public function write($query): Client
@ -211,12 +117,11 @@ class Client implements Interfaces\ClientInterface
// Send commands via loop to router // Send commands via loop to router
foreach ($query->getQuery() as $command) { foreach ($query->getQuery() as $command) {
$command = trim($command);
fwrite($this->_socket, $this->encodeLength($command) . $command);
$this->_connector->writeWord(trim($command));
} }
// Write zero-terminator
fwrite($this->_socket, \chr(0));
// Write zero-terminator (empty string)
$this->_connector->writeWord('');
return $this; return $this;
} }
@ -229,7 +134,7 @@ class Client implements Interfaces\ClientInterface
* Each block end with an zero byte (empty line) * Each block end with an zero byte (empty line)
* Reply ends with a complete !done or !fatal block (ended with 'empty line') * Reply ends with a complete !done or !fatal block (ended with 'empty line')
* A !fatal block precedes TCP connexion close * A !fatal block precedes TCP connexion close
*
*
* @param bool $parse * @param bool $parse
* @return array * @return array
*/ */
@ -242,12 +147,9 @@ class Client implements Interfaces\ClientInterface
// Read answer from socket in loop // Read answer from socket in loop
while (true) { while (true) {
// Read the first byte of input which gives us some or all of the length
// of the remaining reply.
$byte = fread($this->_socket, 1);
$length = $this->getLength(\ord($byte));
$word = $this->_connector->readWord();
if ($length == 0) {
if ('' === $word) {
if ($lastReply) { if ($lastReply) {
// We received a !done or !fatal message in a precedent loop // We received a !done or !fatal message in a precedent loop
// response is complete // response is complete
@ -260,12 +162,12 @@ class Client implements Interfaces\ClientInterface
} }
// Save output line to response array // Save output line to response array
$response[] = $line = stream_get_contents($this->_socket, $length);
$response[] = $word;
// If we get a !done or !fatal line in response, we are now ready to finish the read // If we get a !done or !fatal line in response, we are now ready to finish the read
// but we need to wait a 0 length message, switch the flag // but we need to wait a 0 length message, switch the flag
if ('!done' === $line || '!fatal' === $line) {
$lastReply = true;
if ('!done' === $word || '!fatal' === $word) {
$lastReply = true;
} }
} }
@ -278,7 +180,6 @@ class Client implements Interfaces\ClientInterface
* *
* @param string|array|\RouterOS\Query $query * @param string|array|\RouterOS\Query $query
* @return \RouterOS\Client * @return \RouterOS\Client
* @throws \RouterOS\Exceptions\ClientException
* @throws \RouterOS\Exceptions\QueryException * @throws \RouterOS\Exceptions\QueryException
*/ */
public function w($query): Client public function w($query): Client
@ -449,7 +350,7 @@ class Client implements Interfaces\ClientInterface
// If socket is active // If socket is active
if (null !== $this->getSocket()) { if (null !== $this->getSocket()) {
$this->_connector = new APIConnector(new Streams\ResourceStream($this->getSocket()));
// If we logged in then exit from loop // If we logged in then exit from loop
if (true === $this->login()) { if (true === $this->login()) {
$connected = true; $connected = true;

14
src/Exceptions/StreamException.php

@ -0,0 +1,14 @@
<?php
namespace RouterOS\Exceptions;
/**
* Class StreamException
*
* @package RouterOS\Exceptions
* @since 0.9
*/
class StreamException extends \Exception
{
}

65
src/Helpers/BinaryStringHelper.php

@ -0,0 +1,65 @@
<?php
namespace RouterOS\Helpers;
/**
* class BinaryStringHelper
*
* Strings and binary data manipulations
*
* @package RouterOS\Helpers
* @since 0.9
*/
class BinaryStringHelper
{
/**
* Convert an integer value in a "Network Byte Ordered" binary string (most significant value first)
*
* Reads the integer, starting from the most significant byte, one byte a time.
* Once reach a non 0 byte, construct a binary string representing this values
* ex :
* 0xFF7 => chr(0x0F).chr(0xF7)
* 0x12345678 => chr(0x12).chr(0x34).chr(0x56).chr(0x76)
* Compatible with 8, 16, 32, 64 etc.. bits systems
*
* @see https://en.wikipedia.org/wiki/Endianness
* @param int|float $value the integer value to be converted
* @return string the binary string
*/
public static function IntegerToNBOBinaryString($value): string
{
// Initialize an empty string
$buffer = '';
// Lets start from the most significant byte
for ($i = (PHP_INT_SIZE - 1); $i >= 0; $i--) {
// Prepare a mask to keep only the most significant byte of $value
$mask = 0xFF << ($i * 8);
// If the most significant byte is not 0, the final string must contain it
// If we have already started to construct the string (i.e. there are more signficant digits)
// we must set the byte, even if it is a 0.
// 0xFF00FF, for example, require to set the second byte byte with a 0 value
if (($value & $mask) || $buffer !== '') {
// Get the current byte by shifting it to least significant position and add it to the string
// 0xFF12345678 => 0xFF
$byte = $value >> (8 * $i);
$buffer .= chr($byte);
// Set the most significant byte to 0 so we can restart the process being shure
// that the value is left padded with 0
// 0xFF12345678 => 0x12345678
// -1 = 0xFFFFF.... (number of F depend of PHP_INT_SIZE )
$mask = -1 >> ((PHP_INT_SIZE - $i) * 8);
$value &= $mask;
}
}
// Special case, 0 will not fill the buffer, have to construct it manualy
if (0 === $value) {
$buffer = chr(0);
}
return $buffer;
}
}

46
src/Interfaces/StreamInterface.php

@ -0,0 +1,46 @@
<?php
namespace RouterOS\Interfaces;
/**
* Interface QueryInterface
*
* Stream abstraction
*
* @package RouterOS\Interfaces
* @since 0.9
*/
interface StreamInterface
{
/**
* Reads a stream
*
* Reads $length bytes from the stream, returns the bytes into a string
* Must be binary safe (as fread).
*
* @param int $length the numer of bytes to read
* @return string a binary string containing the readed byes
*/
public function read(int $length): string;
/**
* Writes a string to a stream
*
* Write $length bytes of string, if not mentioned, write all the string
* Must be binary safe (as fread).
* if $length is greater than string length, write all string and return number of writen bytes
* if $length os smaller than string length, remaining bytes are losts.
*
* @param string $string
* @param int $length the number of bytes to read
* @return int return number of written bytes
*/
public function write(string $string, int $length = -1): int;
/**
* Close stream connection
*
* @return void
*/
public function close();
}

21
src/SocketTrait.php

@ -7,6 +7,27 @@ use RouterOS\Exceptions\ClientException;
trait SocketTrait trait SocketTrait
{ {
/** /**
* Socket resource
*
* @var resource|null
*/
private $_socket;
/**
* Code of error
*
* @var int
*/
private $_socket_err_num;
/**
* Description of socket error
*
* @var string
*/
private $_socket_err_str;
/**
* Initiate socket session * Initiate socket session
* *
* @return void * @return void

110
src/Streams/ResourceStream.php

@ -0,0 +1,110 @@
<?php
namespace RouterOS\Streams;
use RouterOS\Interfaces\StreamInterface;
use RouterOS\Exceptions\StreamException;
/**
* class ResourceStream
*
* Stream using a resource (socket, file, pipe etc.)
*
* @package RouterOS
* @since 0.9
*/
class ResourceStream implements StreamInterface
{
protected $stream;
/**
* ResourceStream constructor.
*
* @param $stream
*/
public function __construct($stream)
{
if (!is_resource($stream)) {
throw new \InvalidArgumentException(
sprintf(
'Argument must be a valid resource type. %s given.',
gettype($stream)
)
);
}
// TODO: Should we verify the resource type?
$this->stream = $stream;
}
/**
* @param int $length
* @return string
* @throws \RouterOS\Exceptions\StreamException
* @throws \InvalidArgumentException
*/
public function read(int $length): string
{
if ($length <= 0) {
throw new \InvalidArgumentException('Cannot read zero ot negative count of bytes from a stream');
}
// TODO: Ignore errors here, but why?
$result = @fread($this->stream, $length);
if (false === $result) {
throw new StreamException("Error reading $length bytes");
}
return $result;
}
/**
* Writes a string to a stream
*
* Write $length bytes of string, if not mentioned, write all the string
* Must be binary safe (as fread).
* if $length is greater than string length, write all string and return number of writen bytes
* if $length os smaller than string length, remaining bytes are losts.
*
* @param string $string
* @param int|null $length the numer of bytes to read
* @return int the number of written bytes
* @throws \RouterOS\Exceptions\StreamException
*/
public function write(string $string, int $length = null): int
{
if (null === $length) {
$length = strlen($string);
}
// TODO: Ignore errors here, but why?
$result = @fwrite($this->stream, $string, $length);
if (false === $result) {
throw new StreamException("Error writing $length bytes");
}
return $result;
}
/**
* Close stream connection
*
* @return void
* @throws \RouterOS\Exceptions\StreamException
*/
public function close()
{
$hasBeenClosed = false;
if (null !== $this->stream) {
$hasBeenClosed = @fclose($this->stream);
$this->stream = null;
}
if (false === $hasBeenClosed) {
throw new StreamException('Error closing stream');
}
}
}

97
src/Streams/StringStream.php

@ -0,0 +1,97 @@
<?php
namespace RouterOS\Streams;
use RouterOS\Interfaces\StreamInterface;
use RouterOS\Exceptions\StreamException;
/**
* class StringStream
*
* Initialized with a string, the read method retreive it as done with fread, consuming the buffer.
* When all the string has been read, exception is thrown when try to read again.
*
* @package RouterOS\Streams
* @since 0.9
*/
class StringStream implements StreamInterface
{
/**
* @var string $buffer Stores the string to use
*/
protected $buffer;
/**
* StringStream constructor.
*
* @param string $string
*/
public function __construct(string $string)
{
$this->buffer = $string;
}
/**
* {@inheritDoc}
*
* @throws \InvalidArgumentException when length parameter is invalid
* @throws StreamException when the stream have been tatly red and read methd is called again
*/
public function read(int $length): string
{
$remaining = strlen($this->buffer);
if ($length < 0) {
throw new \InvalidArgumentException('Cannot read a negative count of bytes from a stream');
}
if (0 === $remaining) {
throw new StreamException('End of stream');
}
if ($length >= $remaining) {
// returns all
$result = $this->buffer;
// No more in the buffer
$this->buffer = '';
} else {
// acquire $length characters from the buffer
$result = substr($this->buffer, 0, $length);
// remove $length characters from the buffer
$this->buffer = substr_replace($this->buffer, '', 0, $length);
}
return $result;
}
/**
* Fake write method, do nothing except return the "writen" length
*
* @param string $string The string to write
* @param int|null $length the number of characters to write
* @return int number of "writen" bytes
* @throws \InvalidArgumentException on invalid length
*/
public function write(string $string, int $length = null): int
{
if (null === $length) {
$length = strlen($string);
}
if ($length < 0) {
throw new \InvalidArgumentException('Cannot write a negative count of bytes');
}
return min($length, strlen($string));
}
/**
* Close stream connection
*
* @return void
*/
public function close()
{
$this->buffer = '';
}
}

98
tests/APIConnectorTest.php

@ -0,0 +1,98 @@
<?php
namespace RouterOS\Tests;
use PHPUnit\Framework\TestCase;
use RouterOS\APIConnector;
use RouterOS\Streams\StringStream;
use RouterOS\Streams\ResourceStream;
use RouterOS\APILengthCoDec;
use RouterOS\Interfaces\StreamInterface;
/**
* Limit code coverage to the class RouterOS\APIStream
*
* @coversDefaultClass \RouterOS\APIConnector
*/
class APIConnectorTest extends TestCase
{
/**
* Test that constructor is OK with different kinds of resources
*
* @covers ::__construct
* @dataProvider constructProvider
*
* @param StreamInterface $stream Cannot typehint, PHP refuse it
* @param bool $closeResource shall we close the resource ?
*/
public function test_construct(StreamInterface $stream, bool $closeResource = false)
{
$apiStream = new APIConnector($stream);
$this->assertInstanceOf(APIConnector::class, $apiStream);
if ($closeResource) {
$apiStream->close();
}
}
public function constructProvider(): array
{
return [
[new ResourceStream(fopen(__FILE__, 'rb')),], // Myself, sure I exists
[new ResourceStream(fsockopen('tcp://' . getenv('ROS_HOST'), getenv('ROS_PORT_MODERN'))),], // Socket
[new ResourceStream(STDIN), false], // Try it, but do not close STDIN please !!!
[new StringStream('Hello World !!!')], // Try it, but do not close STDIN please !!!
[new StringStream('')], // Try it, but do not close STDIN please !!!
// What else ?
];
}
/**
* @covers ::readWord
* @dataProvider readWordProvider
*
* @param APIConnector $connector
* @param string $expected
*/
public function test__readWord(APIConnector $connector, string $expected)
{
$this->assertSame($expected, $connector->readWord());
}
public function readWordProvider(): array
{
$longString = '=comment=' . str_repeat('a', 10000);
$length = strlen($longString);
return [
[new APIConnector(new StringStream(chr(0))), ''],
[new APIConnector(new StringStream(chr(3) . '!re')), '!re'],
[new APIConnector(new StringStream(chr(5) . '!done')), '!done'],
[new APIConnector(new StringStream(APILengthCoDec::encodeLength($length) . $longString)), $longString],
];
}
/**
* @covers ::writeWord
* @dataProvider writeWordProvider
*
* @param APIConnector $connector
* @param string $toWrite
* @param int $expected
*/
public function test_writeWord(APIConnector $connector, string $toWrite, int $expected)
{
$this->assertEquals($expected, $connector->writeWord($toWrite));
}
public function writeWordProvider(): array
{
return [
[new APIConnector(new StringStream('Have FUN !!!')), '', 1], // length is 0, but have to write it on 1 byte, minimum
[new APIConnector(new StringStream('Have FUN !!!')), str_repeat(' ', 54), 55], // arbitrary value
[new APIConnector(new StringStream('Have FUN !!!')), str_repeat(' ', 127), 128], // maximum value for 1 byte encoding lentgth
[new APIConnector(new StringStream('Have FUN !!!')), str_repeat(' ', 128), 130], // minimum value for 2 bytes encoding lentgth
[new APIConnector(new StringStream('Have FUN !!!')), str_repeat(' ', 254), 256], // special value isn't it ?
[new APIConnector(new StringStream('Have FUN !!!')), str_repeat(' ', 255), 257], // special value isn't it ?
];
}
}

106
tests/APILengthCoDecTest.php

@ -0,0 +1,106 @@
<?php
namespace RouterOS\Tests;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Constraint\IsType;
use RouterOS\APILengthCoDec;
use RouterOS\Streams\StringStream;
use RouterOS\Helpers\BinaryStringHelper;
/**
* Limit code coverage to the class
*
* @coversDefaultClass \RouterOS\APILengthCoDec
*/
class APILengthCoDecTest extends TestCase
{
/**
* @dataProvider encodeLengthNegativeProvider
* @expectedException \DomainException
* @covers ::encodeLength
*/
public function test__encodeLengthNegative($length)
{
APILengthCoDec::encodeLength($length);
}
public function encodeLengthNegativeProvider(): array
{
return [
[-1],
[PHP_INT_MIN],
];
}
/**
* @dataProvider encodedLengthProvider
* @covers ::encodeLength
*/
public function test__encodeLength($expected, $length)
{
$this->assertEquals(BinaryStringHelper::IntegerToNBOBinaryString((int) $expected), APILengthCoDec::encodeLength($length));
}
public function encodedLengthProvider(): array
{
// [encoded length value, length value]
$result = [
[0, 0], // Low limit value for 1 byte encoded length
[0x39, 0x39], // Arbitrary median value for 1 byte encoded length
[0x7f, 0x7F], // High limit value for 1 byte encoded length
[0x8080, 0x80], // Low limit value for 2 bytes encoded length
[0x9C42, 0x1C42], // Arbitrary median value for 2 bytes encoded length
[0xBFFF, 0x3FFF], // High limit value for 2 bytes encoded length
[0xC04000, 0x4000], // Low limit value for 3 bytes
[0xCAD73B, 0xAD73B], // Arbitrary median value for 3 bytes encoded length
[0xDFFFFF, 0x1FFFFF], // High limit value for 3 bytes encoded length
[0xE0200000, 0x200000], // Low limit value for 4 bytes encoded length
[0xE5AD736B, 0x5AD736B], // Arbitrary median value for 4 bytes encoded length
[0xEFFFFFFF, 0xFFFFFFF], // High limit value for 4 bytes encoded length
];
if (PHP_INT_SIZE > 4) {
$result[] = [0xF010000000, 0x10000000]; // Low limit value for 5 bytes encoded length
$result[] = [0xF10D4EF9C3, 0x10D4EF9C3]; // Arbitrary median value for 5 bytes encoded length
$result[] = [0xF7FFFFFFFF, 0x7FFFFFFFF]; // High limit value for 5 bytes encoded length
}
return $result;
}
/**
* @dataProvider encodedLengthProvider
* @covers ::decodeLength
*/
public function test__decodeLength($encodedLength, $expected)
{
// We have to provide $encodedLength as a "bytes" stream
$stream = new StringStream(BinaryStringHelper::IntegerToNBOBinaryString($encodedLength));
$this->assertEquals($expected, APILengthCoDec::decodeLength($stream));
}
/**
* @dataProvider decodeLengthControlWordProvider
* @covers ::decodeLength
* @expectedException \UnexpectedValueException
*/
public function test_decodeLengthControlWord(string $encodedLength)
{
APILengthCoDec::decodeLength(new StringStream($encodedLength));
}
public function decodeLengthControlWordProvider(): array
{
// Control bytes: 5 most significance its sets to 1
return [
[chr(0xF8)], // minimum
[chr(0xFC)], // arbitrary value
[chr(0xFF)], // maximum
];
}
}

50
tests/ClientTest.php

@ -16,7 +16,7 @@ class ClientTest extends TestCase
{ {
try { try {
$config = new Config(); $config = new Config();
$config->set('user', 'admin')->set('pass', 'admin')->set('host', '127.0.0.1');
$config->set('user', getenv('ROS_USER'))->set('pass', getenv('ROS_PASS'))->set('host', getenv('ROS_HOST'));
$obj = new Client($config); $obj = new Client($config);
$this->assertInternalType('object', $obj); $this->assertInternalType('object', $obj);
$socket = $obj->getSocket(); $socket = $obj->getSocket();
@ -30,9 +30,9 @@ class ClientTest extends TestCase
{ {
try { try {
$config = new Config([ $config = new Config([
'user' => 'admin',
'pass' => 'admin',
'host' => '127.0.0.1'
'user' => getenv('ROS_USER'),
'pass' => getenv('ROS_PASS'),
'host' => getenv('ROS_HOST')
]); ]);
$obj = new Client($config); $obj = new Client($config);
$this->assertInternalType('object', $obj); $this->assertInternalType('object', $obj);
@ -47,9 +47,9 @@ class ClientTest extends TestCase
{ {
try { try {
$obj = new Client([ $obj = new Client([
'user' => 'admin',
'pass' => 'admin',
'host' => '127.0.0.1'
'user' => getenv('ROS_USER'),
'pass' => getenv('ROS_PASS'),
'host' => getenv('ROS_HOST')
]); ]);
$this->assertInternalType('object', $obj); $this->assertInternalType('object', $obj);
$socket = $obj->getSocket(); $socket = $obj->getSocket();
@ -64,8 +64,8 @@ class ClientTest extends TestCase
$this->expectException(ConfigException::class); $this->expectException(ConfigException::class);
$obj = new Client([ $obj = new Client([
'user' => 'admin',
'pass' => 'admin',
'user' => getenv('ROS_USER'),
'pass' => getenv('ROS_PASS'),
]); ]);
} }
@ -73,8 +73,8 @@ class ClientTest extends TestCase
{ {
try { try {
$config = new Config(); $config = new Config();
$config->set('user', 'admin')->set('pass', 'admin')
->set('host', '127.0.0.1')->set('port', 18728)->set('legacy', true);
$config->set('user', getenv('ROS_USER'))->set('pass', getenv('ROS_PASS'))
->set('host', getenv('ROS_HOST'))->set('port', (int) getenv('ROS_PORT_MODERN'))->set('legacy', true);
$obj = new Client($config); $obj = new Client($config);
$this->assertInternalType('object', $obj); $this->assertInternalType('object', $obj);
} catch (\Exception $e) { } catch (\Exception $e) {
@ -84,15 +84,15 @@ class ClientTest extends TestCase
/** /**
* Test non legacy connection on legacy router (pre 6.43) * Test non legacy connection on legacy router (pre 6.43)
*
*
* login() method recognise legacy router response and swap to legacy mode * login() method recognise legacy router response and swap to legacy mode
*/
*/
public function test__constructLegacy2() public function test__constructLegacy2()
{ {
try { try {
$config = new Config(); $config = new Config();
$config->set('user', 'admin')->set('pass', 'admin')
->set('host', '127.0.0.1')->set('port', 18728)->set('legacy', false);
$config->set('user', getenv('ROS_USER'))->set('pass', getenv('ROS_PASS'))
->set('host', getenv('ROS_HOST'))->set('port', (int) getenv('ROS_PORT_MODERN'))->set('legacy', false);
$obj = new Client($config); $obj = new Client($config);
$this->assertInternalType('object', $obj); $this->assertInternalType('object', $obj);
} catch (\Exception $e) { } catch (\Exception $e) {
@ -106,7 +106,7 @@ class ClientTest extends TestCase
$this->expectException(ClientException::class); $this->expectException(ClientException::class);
$config = (new Config())->set('attempts', 2); $config = (new Config())->set('attempts', 2);
$config->set('user', 'admin')->set('pass', 'admin2')->set('host', '127.0.0.1');
$config->set('user', getenv('ROS_USER'))->set('pass', 'admin2')->set('host', getenv('ROS_HOST'));
$obj = new Client($config); $obj = new Client($config);
} }
@ -118,14 +118,14 @@ class ClientTest extends TestCase
$this->expectException(ClientException::class); $this->expectException(ClientException::class);
$config = new Config(); $config = new Config();
$config->set('user', 'admin')->set('pass', 'admin')->set('host', '127.0.0.1')->set('port', 11111);
$config->set('user', getenv('ROS_USER'))->set('pass', getenv('ROS_PASS'))->set('host', getenv('ROS_HOST'))->set('port', 11111);
$obj = new Client($config); $obj = new Client($config);
} }
public function testWriteRead() public function testWriteRead()
{ {
$config = new Config(); $config = new Config();
$config->set('user', 'admin')->set('pass', 'admin')->set('host', '127.0.0.1');
$config->set('user', getenv('ROS_USER'))->set('pass', getenv('ROS_PASS'))->set('host', getenv('ROS_HOST'));
$obj = new Client($config); $obj = new Client($config);
$query = new Query('/ip/address/print'); $query = new Query('/ip/address/print');
@ -157,7 +157,7 @@ class ClientTest extends TestCase
public function testWriteReadString() public function testWriteReadString()
{ {
$config = new Config(); $config = new Config();
$config->set('user', 'admin')->set('pass', 'admin')->set('host', '127.0.0.1');
$config->set('user', getenv('ROS_USER'))->set('pass', getenv('ROS_PASS'))->set('host', getenv('ROS_HOST'));
$obj = new Client($config); $obj = new Client($config);
$readTrap = $obj->wr('/interface', false); $readTrap = $obj->wr('/interface', false);
@ -168,7 +168,7 @@ class ClientTest extends TestCase
public function testWriteReadArray() public function testWriteReadArray()
{ {
$config = new Config(); $config = new Config();
$config->set('user', 'admin')->set('pass', 'admin')->set('host', '127.0.0.1');
$config->set('user', getenv('ROS_USER'))->set('pass', getenv('ROS_PASS'))->set('host', getenv('ROS_HOST'));
$obj = new Client($config); $obj = new Client($config);
$readTrap = $obj->wr(['/interface'], false); $readTrap = $obj->wr(['/interface'], false);
@ -179,7 +179,7 @@ class ClientTest extends TestCase
public function testFatal() public function testFatal()
{ {
$config = new Config(); $config = new Config();
$config->set('user', 'admin')->set('pass', 'admin')->set('host', '127.0.0.1');
$config->set('user', getenv('ROS_USER'))->set('pass', getenv('ROS_PASS'))->set('host', getenv('ROS_HOST'));
$obj = new Client($config); $obj = new Client($config);
$readTrap = $obj->wr('/quit'); $readTrap = $obj->wr('/quit');
@ -192,7 +192,7 @@ class ClientTest extends TestCase
$this->expectException(QueryException::class); $this->expectException(QueryException::class);
$config = new Config(); $config = new Config();
$config->set('user', 'admin')->set('pass', 'admin')->set('host', '127.0.0.1');
$config->set('user', getenv('ROS_USER'))->set('pass', getenv('ROS_PASS'))->set('host', getenv('ROS_HOST'));
$obj = new Client($config); $obj = new Client($config);
$error = $obj->write($obj)->read(false); $error = $obj->write($obj)->read(false);
} }
@ -200,9 +200,9 @@ class ClientTest extends TestCase
public function testGetConfig() public function testGetConfig()
{ {
$obj = new Client([ $obj = new Client([
'user' => 'admin',
'pass' => 'admin',
'host' => '127.0.0.1'
'user' => getenv('ROS_USER'),
'pass' => getenv('ROS_PASS'),
'host' => getenv('ROS_HOST')
]); ]);
$config = $obj->getConfig(); $config = $obj->getConfig();

49
tests/Helpers/BinaryStringHelperTest.php

@ -0,0 +1,49 @@
<?php
namespace RouterOS\Tests\Helpers;
use PHPUnit\Framework\TestCase;
use RouterOS\Helpers\BinaryStringHelper;
/**
* Limit code coverage to the class
*
* @coversDefaultClass \RouterOS\Helpers\BinaryStringHelper
*/
class BinaryStringHelperTest extends TestCase
{
/**
* @dataProvider IntegerToNBOBinaryStringProvider
* @covers ::IntegerToNBOBinaryString
*/
public function test__IntegerToNBOBinaryString($value, $expected)
{
$this->assertEquals($expected, BinaryStringHelper::IntegerToNBOBinaryString($value));
}
public function IntegerToNBOBinaryStringProvider(): array
{
$result = [
[0, chr(0)], // lower boundary value
[0xFFFFFFFF, chr(0xFF) . chr(0xFF) . chr(0xFF) . chr(0xFF)], // 32 bits maximal value
// strange behaviour :
// TypeError: Argument 1 passed to RouterOS\Tests\Helpers\BinaryStringHelperTest::test__IntegerToNBOBinaryString() must be of the type integer, float given
// Seems that php auto convert to float 0xFFF....
//
// [0xFFFFFFFFFFFFFFFF, chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF).chr(0xFF)],
// Let's try random value
[0x390DDD99, chr(0x39) . chr(0x0D) . chr(0xDD) . chr(0x99)],
];
if (PHP_INT_SIZE > 4) {
// -1 is encoded with 0xFFFFFFF.....
// 64 bits maximal value (on a 64 bits system only)
$result[] = [-1, chr(0xFF) . chr(0xFF) . chr(0xFF) . chr(0xFF) . chr(0xFF) . chr(0xFF) . chr(0xFF) . chr(0xFF)]; // 64 bits upper boundary value
}
return $result;
}
}

263
tests/Streams/ResourceStreamTest.php

@ -0,0 +1,263 @@
<?php
namespace RouterOS\Tests\Streams;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Constraint\IsType;
use RouterOS\Streams\ResourceStream;
/**
* Limit code coverage to the class RouterOS\APIStream
*
* @coversDefaultClass \RouterOS\Streams\ResourceStream
*/
class ResourceStreamTest extends TestCase
{
/**
* Test that constructor throws an InvalidArgumentException on bad parameter type
*
* @covers ::__construct
* @expectedException \InvalidArgumentException
* @dataProvider constructNotResourceProvider
*
* @param $notResource
*/
public function test__constructNotResource($notResource)
{
new ResourceStream($notResource);
}
/**
* Data provider for test__constructNotResource
*
* returns data not of type resource
*/
public function constructNotResourceProvider(): array
{
return [
[0], // integer
[3.14], // float
['a string'], // string
[
[0, 3.14] // Array
],
[new \stdClass()], // Object
// What else ?
];
}
/**
* Test that constructor is OK with different kinds of resources
*
* @covers ::__construct
* @dataProvider constructProvider
*
* @param resource $resource Cannot typehint, PHP refuse it
* @param bool $closeResource shall we close the resource ?
*/
public function test_construct($resource, bool $closeResource = true)
{
$resourceStream = new ResourceStream($resource);
$stream = $this->getObjectAttribute($resourceStream, 'stream');
$this->assertInternalType(IsType::TYPE_RESOURCE, $stream);
if ($closeResource) {
fclose($resource);
}
}
/**
* Data provider for test__construct
*
* @return array data of type resource
*/
public function constructProvider(): array
{
return [
[fopen(__FILE__, 'rb'),], // Myself, sure I exists
[fsockopen('tcp://' . getenv('ROS_HOST'), getenv('ROS_PORT_MODERN')),], // Socket
[STDIN, false], // Try it, but do not close STDIN please !!!
// What else ?
];
}
/**
* Test that read function return expected values, and that consecutive reads return data
*
* @covers ::read
* @dataProvider readProvider
*
* @param ResourceStream $stream Cannot typehint, PHP refuse it
* @param string $expected the result we should have
* @throws \RouterOS\Exceptions\StreamException
* @throws \InvalidArgumentException
*/
public function test__read(ResourceStream $stream, string $expected)
{
$this->assertSame($expected, $stream->read(strlen($expected)));
}
public function readProvider(): array
{
$resource = fopen(__FILE__, 'rb');
$me = new ResourceStream($resource);
return [
[$me, '<'], // Read for byte
[$me, '?php'], // Read following bytes. File statrts with "<php"
];
}
/**
* Test that read invalid lengths
*
* @covers ::read
* @dataProvider readBadLengthProvider
* @expectedException \InvalidArgumentException
*
* @param ResourceStream $stream Cannot typehint, PHP refuse it
* @param int $length
* @throws \RouterOS\Exceptions\StreamException
* @throws \InvalidArgumentException
*/
public function test__readBadLength(ResourceStream $stream, int $length)
{
$stream->read($length);
}
public function readBadLengthProvider(): array
{
$resource = fopen(__FILE__, 'rb');
$me = new ResourceStream($resource);
return [
[$me, 0],
[$me, -1],
];
}
/**
* Test read to invalid resource
*
* @covers ::read
* @dataProvider readBadResourceProvider
* @expectedException \RouterOS\Exceptions\StreamException
*
* @param ResourceStream $stream Cannot typehint, PHP refuse it
* @param int $length
*/
public function test__readBadResource(ResourceStream $stream, int $length)
{
$stream->read($length);
}
public function readBadResourceProvider(): array
{
$resource = fopen(__FILE__, 'rb');
$me = new ResourceStream($resource);
fclose($resource);
return [
[$me, 1],
];
}
/**
* Test that write function returns writen length
*
* @covers ::write
* @dataProvider writeProvider
*
* @param ResourceStream $stream to test
* @param string $toWrite the writed string
* @throws \RouterOS\Exceptions\StreamException
*/
public function test__write(ResourceStream $stream, string $toWrite)
{
$this->assertEquals(strlen($toWrite), $stream->write($toWrite));
}
public function writeProvider(): array
{
$resource = fopen('/dev/null', 'wb');
$null = new ResourceStream($resource);
return [
[$null, 'yyaagagagag'], // Take that
];
}
/**
* Test write to invalid resource
*
* @covers ::write
* @dataProvider writeBadResourceProvider
* @expectedException \RouterOS\Exceptions\StreamException
*
* @param ResourceStream $stream to test
* @param string $toWrite the written string
*/
public function test__writeBadResource(ResourceStream $stream, string $toWrite)
{
$stream->write($toWrite);
}
public function writeBadResourceProvider(): array
{
$resource = fopen('/dev/null', 'wb');
$me = new ResourceStream($resource);
fclose($resource);
return [
[$me, 'sasasaas'], // Take that
];
}
/**
* Test double close resource
*
* @covers ::close
* @dataProvider doubleCloseProvider
* @expectedException \RouterOS\Exceptions\StreamException
*
* @param ResourceStream $stream to test
*/
public function test_doubleClose(ResourceStream $stream)
{
$stream->close();
$stream->close();
}
public function doubleCloseProvider(): array
{
return [
[new ResourceStream(fopen('/dev/null', 'wb')), 'sasasaas'], // Take that
];
}
/**
* Test write to closed resource
*
* @covers ::close
* @covers ::write
* @dataProvider writeClosedResourceProvider
* @expectedException \RouterOS\Exceptions\StreamException
*
* @param ResourceStream $stream to test
* @param string $toWrite the written string
*/
public function test_close(ResourceStream $stream, string $toWrite)
{
$stream->close();
$stream->write($toWrite);
}
public function writeClosedResourceProvider(): array
{
return [
[new ResourceStream(fopen('/dev/null', 'wb')), 'sasasaas'], // Take that
];
}
}

160
tests/Streams/StringStreamTest.php

@ -0,0 +1,160 @@
<?php
namespace RouterOS\Tests\Streams;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\Constraint\IsType;
use RouterOS\Streams\StringStream;
use RouterOS\Exceptions\StreamException;
/**
* Limit code coverage to the class RouterOS\APIStream
*
* @coversDefaultClass \RouterOS\Streams\StringStream
*/
class StringStreamTest extends TestCase
{
/**
* @covers ::__construct
* @dataProvider constructProvider
*
* @param string $string
*/
public function test__construct(string $string)
{
$this->assertInstanceOf(StringStream::class, new StringStream($string));
}
public function constructProvider(): array
{
return [
[chr(0)],
[''],
['1'],
['lkjl' . chr(0) . 'kjkljllkjkljljklkjkljlkjljlkjkljkljlkjjll'],
];
}
/**
* Test that write function returns the effective written bytes
*
* @covers ::write
* @dataProvider writeProvider
*
* @param string $string the string to write
* @param int|null $length the count if bytes to write
* @param int $expected the number of bytes that must be writen
*/
public function test__write(string $string, $length, int $expected)
{
$stream = new StringStream('Does not matters');
if (null === $length) {
$this->assertEquals($expected, $stream->write($string));
} else {
$this->assertEquals($expected, $stream->write($string, $length));
}
}
public function writeProvider(): array
{
return [
['', 0, 0],
['', 10, 0],
['', null, 0],
['Yabala', 0, 0],
['Yabala', 1, 1],
['Yabala', 6, 6],
['Yabala', 100, 6],
['Yabala', null, 6],
[chr(0), 0, 0],
[chr(0), 1, 1],
[chr(0), 100, 1],
[chr(0), null, 1],
];
}
/**
* @covers ::write
* @expectedException \InvalidArgumentException
*/
public function test__writeWithNegativeLength()
{
$stream = new StringStream('Does not matters');
$stream->write('PLOP', -1);
}
/**
* Test read function
*
* @throws \RouterOS\Exceptions\StreamException
*/
public function test__read()
{
$stream = new StringStream('123456789');
$this->assertEquals('', $stream->read(0));
$this->assertEquals('1', $stream->read(1));
$this->assertEquals('23', $stream->read(2));
$this->assertEquals('456', $stream->read(3));
$this->assertEquals('', $stream->read(0));
$this->assertEquals('789', $stream->read(4));
}
/**
* @expectedException \InvalidArgumentException
*
* @throws \RouterOS\Exceptions\StreamException
*/
public function test__readBadLength()
{
$stream = new StringStream('123456789');
$stream->read(-1);
}
/**
* @covers ::read
* @dataProvider readWhileEmptyProvider
* @expectedException \RouterOS\Exceptions\StreamException
*
* @param StringStream $stream
* @param int $length
* @throws \RouterOS\Exceptions\StreamException
*/
public function test__readWhileEmpty(StringStream $stream, int $length)
{
$stream->read($length);
}
/**
* @return \Generator
* @throws StreamException
*/
public function readWhileEmptyProvider()
{
$stream = new StringStream('123456789');
$stream->read(9);
yield [$stream, 1];
$stream = new StringStream('123456789');
$stream->read(5);
$stream->read(4);
yield [$stream, 1];
$stream = new StringStream('');
yield [$stream, 1];
}
/**
* @expectedException \RouterOS\Exceptions\StreamException
*/
public function testReadClosed()
{
$stream = new StringStream('123456789');
$stream->close();
$stream->read(1);
}
}
Loading…
Cancel
Save