mirror of
https://github.com/shufflingpixels/php-io.git
synced 2026-06-16 05:04:59 +02:00
Initial commit
This commit is contained in:
commit
c677cfccd4
23 changed files with 5553 additions and 0 deletions
1
tests/Pest.php
Normal file
1
tests/Pest.php
Normal file
|
|
@ -0,0 +1 @@
|
|||
<?php
|
||||
81
tests/Unit/BinaryReaderTest.php
Normal file
81
tests/Unit/BinaryReaderTest.php
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
use Shufflingpixels\IO\BinaryReader;
|
||||
use Shufflingpixels\IO\Buffer;
|
||||
use Shufflingpixels\IO\SeekMode;
|
||||
use Shufflingpixels\IO\StreamInterface;
|
||||
|
||||
it('creates reader from stream and string helpers', function () {
|
||||
$streamReader = BinaryReader::stream(new Buffer('abc'));
|
||||
$stringReader = BinaryReader::string('xyz');
|
||||
|
||||
expect($streamReader)->toBeInstanceOf(BinaryReader::class)
|
||||
->and($stringReader->read(3))->toBe('xyz');
|
||||
});
|
||||
|
||||
it('proxies length tell eof and seek', function () {
|
||||
$reader = BinaryReader::string('abcd');
|
||||
|
||||
expect($reader->length())->toBe(4)
|
||||
->and($reader->tell())->toBe(0)
|
||||
->and($reader->eof())->toBeFalse();
|
||||
|
||||
$reader->seek(2);
|
||||
expect($reader->tell())->toBe(2)
|
||||
->and($reader->read(1))->toBe('c');
|
||||
|
||||
$reader->seek(-1, SeekMode::END);
|
||||
expect($reader->read(1))->toBe('d')
|
||||
->and($reader->eof())->toBeTrue();
|
||||
});
|
||||
|
||||
it('throws for negative read length', function () {
|
||||
$reader = BinaryReader::string('abc');
|
||||
|
||||
expect(fn () => $reader->read(-1))->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
|
||||
it('throws when stream returns fewer bytes than requested', function () {
|
||||
$stream = new class implements StreamInterface {
|
||||
public function close(): void {}
|
||||
public function eof(): bool { return false; }
|
||||
public function length(): int { return 0; }
|
||||
public function isSeekable(): bool { return false; }
|
||||
public function seek(int $position, Shufflingpixels\IO\SeekMode $mode = Shufflingpixels\IO\SeekMode::SET): void {}
|
||||
public function tell(): int { return 0; }
|
||||
public function isReadable(): bool { return true; }
|
||||
public function read(int $length): string { return 'x'; }
|
||||
public function isWriteable(): bool { return false; }
|
||||
public function write($data): int { return 0; }
|
||||
};
|
||||
|
||||
$reader = BinaryReader::stream($stream);
|
||||
|
||||
expect(fn () => $reader->read(2))->toThrow(RuntimeException::class, 'Not enough bytes');
|
||||
});
|
||||
|
||||
it('reads 8 bit integers', function () {
|
||||
$reader = BinaryReader::string("\x7f\x80\xff");
|
||||
|
||||
expect($reader->readUInt8())->toBe(127)
|
||||
->and($reader->readInt8())->toBe(-128)
|
||||
->and($reader->readInt8())->toBe(-1);
|
||||
});
|
||||
|
||||
it('reads 16 bit integers in little and big endian', function () {
|
||||
$reader = BinaryReader::string(pack('v', 0x1234) . pack('n', 0x5678) . pack('v', 0x8000) . pack('n', 0xffff));
|
||||
|
||||
expect($reader->readUInt16LE())->toBe(0x1234)
|
||||
->and($reader->readUInt16BE())->toBe(0x5678)
|
||||
->and($reader->readInt16LE())->toBe(-32768)
|
||||
->and($reader->readInt16BE())->toBe(-1);
|
||||
});
|
||||
|
||||
it('reads 32 bit integers in little and big endian', function () {
|
||||
$reader = BinaryReader::string(pack('V', 0x12345678) . pack('N', 0x10203040) . pack('V', 0x80000000) . pack('N', 0xffffffff));
|
||||
|
||||
expect($reader->readUInt32LE())->toBe(0x12345678)
|
||||
->and($reader->readUInt32BE())->toBe(0x10203040)
|
||||
->and($reader->readInt32LE())->toBe(-2147483648)
|
||||
->and($reader->readInt32BE())->toBe(-1);
|
||||
});
|
||||
72
tests/Unit/BufferTest.php
Normal file
72
tests/Unit/BufferTest.php
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
use Shufflingpixels\IO\Buffer;
|
||||
use Shufflingpixels\IO\Exception\EndOfStreamException;
|
||||
use Shufflingpixels\IO\SeekMode;
|
||||
|
||||
it('reads, seeks and tracks position', function () {
|
||||
$buffer = new Buffer('abcdef');
|
||||
|
||||
expect($buffer->length())->toBe(6)
|
||||
->and($buffer->tell())->toBe(0)
|
||||
->and($buffer->remaining())->toBe(6)
|
||||
->and($buffer->eof())->toBeFalse();
|
||||
|
||||
expect($buffer->read(2))->toBe('ab')
|
||||
->and($buffer->tell())->toBe(2)
|
||||
->and($buffer->remaining())->toBe(4);
|
||||
|
||||
$buffer->seek(-1, SeekMode::END);
|
||||
|
||||
expect($buffer->tell())->toBe(5)
|
||||
->and($buffer->read(1))->toBe('f')
|
||||
->and($buffer->eof())->toBeTrue();
|
||||
});
|
||||
|
||||
it('supports relative seek modes', function () {
|
||||
$buffer = new Buffer('abcdef');
|
||||
|
||||
$buffer->seek(3);
|
||||
$buffer->seek(-2, SeekMode::CUR);
|
||||
|
||||
expect($buffer->tell())->toBe(1)
|
||||
->and($buffer->read(2))->toBe('bc');
|
||||
});
|
||||
|
||||
it('throws for out of bounds seek', function () {
|
||||
$buffer = new Buffer('abc');
|
||||
|
||||
expect(fn () => $buffer->seek(4))->toThrow(OutOfBoundsException::class);
|
||||
});
|
||||
|
||||
it('throws for negative read length', function () {
|
||||
$buffer = new Buffer('abc');
|
||||
|
||||
expect(fn () => $buffer->read(-1))->toThrow(InvalidArgumentException::class);
|
||||
});
|
||||
|
||||
it('throws when reading beyond end of stream', function () {
|
||||
$buffer = new Buffer('abc');
|
||||
|
||||
expect(fn () => $buffer->read(4))->toThrow(EndOfStreamException::class);
|
||||
});
|
||||
|
||||
it('writes at current position and updates contents', function () {
|
||||
$buffer = new Buffer('abcdef');
|
||||
$buffer->seek(2);
|
||||
|
||||
expect($buffer->write('XY'))->toBe(2)
|
||||
->and($buffer->tell())->toBe(4);
|
||||
|
||||
$buffer->seek(0);
|
||||
|
||||
expect($buffer->read(6))->toBe('abXYef');
|
||||
});
|
||||
|
||||
it('writes nothing for empty payload', function () {
|
||||
$buffer = new Buffer('abc');
|
||||
|
||||
expect($buffer->write(''))->toBe(0)
|
||||
->and($buffer->tell())->toBe(0)
|
||||
->and($buffer->length())->toBe(3);
|
||||
});
|
||||
9
tests/Unit/ExceptionTest.php
Normal file
9
tests/Unit/ExceptionTest.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
use Shufflingpixels\IO\Exception\EndOfStreamException;
|
||||
use Shufflingpixels\IO\Exception\IOException;
|
||||
|
||||
it('keeps the exception hierarchy stable', function () {
|
||||
expect(new IOException('io'))->toBeInstanceOf(Exception::class)
|
||||
->and(new EndOfStreamException('eos'))->toBeInstanceOf(IOException::class);
|
||||
});
|
||||
19
tests/Unit/FileModeTest.php
Normal file
19
tests/Unit/FileModeTest.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
use Shufflingpixels\IO\FileMode;
|
||||
|
||||
it('defines expected fopen mode values', function () {
|
||||
expect(FileMode::READ->value)->toBe('r')
|
||||
->and(FileMode::WRITE->value)->toBe('w')
|
||||
->and(FileMode::RW->value)->toBe('r+');
|
||||
});
|
||||
|
||||
it('reports read and write capabilities for each mode', function () {
|
||||
expect(FileMode::READ->readable())->toBeTrue()
|
||||
->and(FileMode::READ->writeable())->toBeFalse()
|
||||
->and(FileMode::WRITE->readable())->toBeFalse()
|
||||
->and(FileMode::WRITE->writeable())->toBeTrue()
|
||||
->and(FileMode::RW->readable())->toBeTrue()
|
||||
->and(FileMode::RW->writeable())->toBeTrue()
|
||||
->and(FileMode::READ->seekable())->toBeTrue();
|
||||
});
|
||||
60
tests/Unit/FileTest.php
Normal file
60
tests/Unit/FileTest.php
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
use Shufflingpixels\IO\Exception\IOException;
|
||||
use Shufflingpixels\IO\File;
|
||||
use Shufflingpixels\IO\FileMode;
|
||||
|
||||
it('opens readable files and reads contents', function () {
|
||||
$path = tempnam(sys_get_temp_dir(), 'php-io-');
|
||||
file_put_contents($path, 'hello');
|
||||
|
||||
$file = File::open($path, FileMode::READ);
|
||||
|
||||
expect($file->isReadable())->toBeTrue()
|
||||
->and($file->isWriteable())->toBeFalse()
|
||||
->and($file->length())->toBe(5)
|
||||
->and($file->read(5))->toBe('hello');
|
||||
|
||||
$file->close();
|
||||
unlink($path);
|
||||
});
|
||||
|
||||
it('opens read-write files and persists writes', function () {
|
||||
$path = tempnam(sys_get_temp_dir(), 'php-io-');
|
||||
file_put_contents($path, 'abc');
|
||||
|
||||
$file = File::open($path, FileMode::RW);
|
||||
$file->seek(0);
|
||||
|
||||
expect($file->isReadable())->toBeTrue()
|
||||
->and($file->isWriteable())->toBeTrue()
|
||||
->and($file->write('X'))->toBe(1);
|
||||
|
||||
$file->seek(0);
|
||||
expect($file->read(3))->toBe('Xbc');
|
||||
|
||||
$file->close();
|
||||
unlink($path);
|
||||
});
|
||||
|
||||
it('opens write mode files and truncates existing contents', function () {
|
||||
$path = tempnam(sys_get_temp_dir(), 'php-io-');
|
||||
file_put_contents($path, 'abcdef');
|
||||
|
||||
$file = File::open($path, FileMode::WRITE);
|
||||
|
||||
expect($file->isReadable())->toBeFalse()
|
||||
->and($file->isWriteable())->toBeTrue()
|
||||
->and($file->length())->toBe(0)
|
||||
->and($file->write('xy'))->toBe(2);
|
||||
|
||||
$file->close();
|
||||
expect(file_get_contents($path))->toBe('xy');
|
||||
unlink($path);
|
||||
});
|
||||
|
||||
it('throws an io exception when opening a missing path', function () {
|
||||
$path = sys_get_temp_dir() . '/php-io-missing-dir/' . uniqid('', true) . '.txt';
|
||||
|
||||
expect(fn () => File::open($path, FileMode::READ))->toThrow(IOException::class);
|
||||
});
|
||||
68
tests/Unit/ResourceTest.php
Normal file
68
tests/Unit/ResourceTest.php
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
use Shufflingpixels\IO\Exception\IOException;
|
||||
use Shufflingpixels\IO\Resource;
|
||||
|
||||
it('reads, writes, seeks and reports stream state', function () {
|
||||
$handle = fopen('php://temp', 'r+');
|
||||
fwrite($handle, 'hello');
|
||||
rewind($handle);
|
||||
|
||||
$stream = new class($handle, true, true, true) extends Resource {
|
||||
public function __construct($resource, bool $seekable, bool $readable, bool $writeable)
|
||||
{
|
||||
parent::__construct($resource, $seekable, $readable, $writeable);
|
||||
}
|
||||
};
|
||||
|
||||
expect($stream->length())->toBe(5)
|
||||
->and($stream->tell())->toBe(0)
|
||||
->and($stream->isSeekable())->toBeTrue()
|
||||
->and($stream->isReadable())->toBeTrue()
|
||||
->and($stream->isWriteable())->toBeTrue()
|
||||
->and($stream->read(2))->toBe('he');
|
||||
|
||||
$stream->seek(0);
|
||||
expect($stream->write('H'))->toBe(1);
|
||||
|
||||
$stream->seek(0);
|
||||
expect($stream->read(5))->toBe('Hello')
|
||||
->and($stream->length())->toBe(5)
|
||||
->and($stream->eof())->toBeFalse();
|
||||
|
||||
$stream->read(1);
|
||||
expect($stream->eof())->toBeTrue();
|
||||
|
||||
$stream->close();
|
||||
});
|
||||
|
||||
it('throws when seeking or getting length on non-seekable stream', function () {
|
||||
$handle = fopen('php://temp', 'r+');
|
||||
|
||||
$stream = new class($handle, false, true, false) extends Resource {
|
||||
public function __construct($resource, bool $seekable, bool $readable, bool $writeable)
|
||||
{
|
||||
parent::__construct($resource, $seekable, $readable, $writeable);
|
||||
}
|
||||
};
|
||||
|
||||
expect(fn () => $stream->length())->toThrow(IOException::class)
|
||||
->and(fn () => $stream->seek(0))->toThrow(IOException::class);
|
||||
|
||||
$stream->close();
|
||||
});
|
||||
|
||||
it('throws when writing to a non-writeable stream', function () {
|
||||
$handle = fopen('php://temp', 'r+');
|
||||
|
||||
$stream = new class($handle, true, true, false) extends Resource {
|
||||
public function __construct($resource, bool $seekable, bool $readable, bool $writeable)
|
||||
{
|
||||
parent::__construct($resource, $seekable, $readable, $writeable);
|
||||
}
|
||||
};
|
||||
|
||||
expect(fn () => $stream->write('x'))->toThrow(IOException::class, 'not writeable');
|
||||
|
||||
$stream->close();
|
||||
});
|
||||
9
tests/Unit/SeekModeTest.php
Normal file
9
tests/Unit/SeekModeTest.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
use Shufflingpixels\IO\SeekMode;
|
||||
|
||||
it('maps to native seek constants', function () {
|
||||
expect(SeekMode::SET->value)->toBe(SEEK_SET)
|
||||
->and(SeekMode::CUR->value)->toBe(SEEK_CUR)
|
||||
->and(SeekMode::END->value)->toBe(SEEK_END);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue