From 1e8367b6386c284bf2b64489529ec6b9c0a504e1 Mon Sep 17 00:00:00 2001 From: Rowan Tommins Date: Thu, 27 Feb 2020 22:19:34 +0000 Subject: [PATCH] Initial working set of functional tests based on real streams The idea is to build up a library of tests which describe the behaviour of real file streams under PHP. Regardless of the internal code structure, this can then be used to check that the functionality of the virtual file system is as close as possible. --- .../functional/BaseFunctionalTestCase.php | 69 ++++++ tests/phpunit/functional/FeofTestCase.php | 221 ++++++++++++++++++ tests/phpunit/functional/FtellTestCase.php | 123 ++++++++++ tests/phpunit/functional/README.md | 23 ++ .../SimultaneousStreamsTestCase.php | 156 +++++++++++++ .../functional/testthetests.phpunit.xml | 42 ++++ 6 files changed, 634 insertions(+) create mode 100644 tests/phpunit/functional/BaseFunctionalTestCase.php create mode 100644 tests/phpunit/functional/FeofTestCase.php create mode 100644 tests/phpunit/functional/FtellTestCase.php create mode 100644 tests/phpunit/functional/README.md create mode 100644 tests/phpunit/functional/SimultaneousStreamsTestCase.php create mode 100644 tests/phpunit/functional/testthetests.phpunit.xml diff --git a/tests/phpunit/functional/BaseFunctionalTestCase.php b/tests/phpunit/functional/BaseFunctionalTestCase.php new file mode 100644 index 00000000..641b9189 --- /dev/null +++ b/tests/phpunit/functional/BaseFunctionalTestCase.php @@ -0,0 +1,69 @@ +realFileName = tempnam(sys_get_temp_dir(), 'vfsstream_test_'); + } + else { + $root = vfsStream::setup(); + $this->vfsFile = vfsStream::newFile('test'); + $this->vfsFile->at($root); + } + } + + protected function getMockFileName(): string + { + if ( isset($_ENV['TEST_THE_TESTS_WITH_REAL_FILES']) ) { + return $this->realFileName; + } + else { + return $this->vfsFile->url(); + } + } + + protected function setMockFileContent(string $content): void + { + if ( isset($_ENV['TEST_THE_TESTS_WITH_REAL_FILES']) ) { + file_put_contents($this->realFileName, $content); + } + else { + $this->vfsFile->setContent($content); + } + } + + /** + * The virtual file system will clean itself up, but real files need real deletion + */ + protected function tearDown(): void + { + if ( isset($_ENV['TEST_THE_TESTS_WITH_REAL_FILES']) ) { + unlink($this->realFileName); + } + } +} \ No newline at end of file diff --git a/tests/phpunit/functional/FeofTestCase.php b/tests/phpunit/functional/FeofTestCase.php new file mode 100644 index 00000000..ee228a1a --- /dev/null +++ b/tests/phpunit/functional/FeofTestCase.php @@ -0,0 +1,221 @@ +setMockFileContent("Line 1\n"); + + $stream = fopen($this->getMockFileName(), 'rb'); + + assertFalse(feof($stream)); + } + + /** + * @test + */ + public function feofIsFalseWhenEmptyFileOpened(): void + { + $this->setMockFileContent(''); + + $stream = fopen($this->getMockFileName(), 'rb'); + + assertFalse(feof($stream)); + } + + /** + * @test + */ + public function feofIsTrueAfterEmptyFileRead(): void + { + $this->setMockFileContent(''); + + $stream = fopen($this->getMockFileName(), 'rb'); + + fgets($stream); + + assertTrue(feof($stream)); + } + + /** + * @test + */ + public function feofIsFalseWhenStreamRewound(): void + { + $this->setMockFileContent("Line 1\n"); + + $stream = fopen($this->getMockFileName(), 'rb'); + + fgets($stream); + rewind($stream); + assertFalse(feof($stream)); + } + + /** + * @test + */ + public function feofIsFalseWhenSeekingToStart(): void + { + $fp = fopen($this->getMockFileName(), 'rb'); + fseek($fp, 0, SEEK_SET); + assertFalse(feof($fp)); + fclose($fp); + } + + /** + * @test + */ + public function feofIsFalseWhenSeekingToEnd(): void + { + $fp = fopen($this->getMockFileName(), 'rb'); + fseek($fp, 0, SEEK_END); + assertFalse(feof($fp)); + fclose($fp); + } + + /** + * @test + */ + public function feofIsFalseWhenSeekingPastEnd(): void + { + $fp = fopen($this->getMockFileName(), 'rb'); + fseek($fp, 10, SEEK_END); + assertFalse(feof($fp)); + fclose($fp); + } + + /** + * @test + */ + public function feofIsFalseWhenEmptyStreamRewound(): void + { + $this->setMockFileContent(''); + + $stream = fopen($this->getMockFileName(), 'rb'); + + fgets($stream); + rewind($stream); + assertFalse(feof($stream)); + } + + /** + * @test + */ + public function feofIsFalseAfterReadingLastLine(): void + { + $this->setMockFileContent("Line 1\n"); + + $stream = fopen($this->getMockFileName(), 'rb'); + + assertThat(fgets($stream), equals("Line 1\n")); + assertFalse(feof($stream)); + } + + /** + * @test + */ + public function feofIsTrueAfterReadingBeyondLastLine(): void + { + $this->setMockFileContent("Line 1\n"); + + $stream = fopen($this->getMockFileName(), 'rb'); + + fgets($stream); + fgets($stream); + + assertTrue(feof($stream)); + } + + /** + * @test + */ + public function readLessThanSizeDoesNotReachEof(): void + { + $this->setMockFileContent('123456789'); + $stream = fopen($this->getMockFileName(), 'rb'); + fread($stream, 3); + assertFalse(feof($stream)); + } + + /** + * @test + */ + public function readSizeDoesNotReachEof(): void + { + $this->setMockFileContent('123456789'); + $stream = fopen($this->getMockFileName(), 'rb'); + fread($stream, 9); + assertFalse(feof($stream)); + } + + /** + * @test + */ + public function readSizeReachesEofOnNextRead(): void + { + $this->setMockFileContent('123456789'); + $stream = fopen($this->getMockFileName(), 'rb'); + fread($stream, 9); + fread($stream, 1); + assertTrue(feof($stream)); + } + + /** + * @test + */ + public function readMoreThanSizeReachesEof(): void + { + $this->setMockFileContent('123456789'); + $stream = fopen($this->getMockFileName(), 'rb'); + fread($stream, 10); + assertTrue(feof($stream)); + } + + /** + * @test + */ + public function readMoreThanSizeReachesEofOnNextRead(): void + { + $this->setMockFileContent('123456789'); + $stream = fopen($this->getMockFileName(), 'rb'); + fread($stream, 10); + fread($stream, 1); + assertTrue(feof($stream)); + } + + /** + * @test + */ + public function streamIsNotEofAfterTruncate(): void + { + $this->setMockFileContent('test'); + $stream = fopen($this->getMockFileName(), 'rb+'); + fread($stream, 4); + ftruncate($stream, 0); + assertFalse(feof($stream)); + } +} \ No newline at end of file diff --git a/tests/phpunit/functional/FtellTestCase.php b/tests/phpunit/functional/FtellTestCase.php new file mode 100644 index 00000000..91c92448 --- /dev/null +++ b/tests/phpunit/functional/FtellTestCase.php @@ -0,0 +1,123 @@ +setMockFileContent(''); + $stream = fopen($this->getMockFileName(), 'rb'); + assertThat(ftell($stream), equals(0)); + } + + /** + * @test + */ + public function nonEmptyFileStartsAtZero(): void + { + $this->setMockFileContent('abcdef'); + $stream = fopen($this->getMockFileName(), 'rb'); + assertThat(ftell($stream), equals(0)); + } + + /** + * @test + */ + public function fileStartsAtZeroIfOpenedWithTruncation(): void + { + $this->setMockFileContent('abcdef'); + $stream = fopen($this->getMockFileName(), 'wb'); + assertThat(ftell($stream), equals(0)); + } + + public function filePointsAtZeroAfterTruncation(): void + { + $this->setMockFileContent('abcdef'); + $stream = fopen($this->getMockFileName(), 'rb+'); + ftruncate($stream, 0); + assertThat(ftell($stream), equals(0)); + } + + public function filePointsAtEndAfterPartialTruncation(): void + { + $this->setMockFileContent('abc'); + $stream = fopen($this->getMockFileName(), 'ab+'); + ftruncate($stream, 3); + assertThat(ftell($stream), equals(3)); + } + + public function readSingleByteProgressesPointer(): void + { + $this->setMockFileContent('abc'); + $stream = fopen($this->getMockFileName(), 'rb'); + fgetc($stream); + assertThat(ftell($stream), equals(1)); + } + + public function readSingleByteTwiceProgressesPointerTwice(): void + { + $this->setMockFileContent('abc'); + $stream = fopen($this->getMockFileName(), 'rb'); + fgetc($stream); + fgetc($stream); + assertThat(ftell($stream), equals(2)); + } + + public function readAllBytesDoesntPointPastEndOfFile(): void + { + $this->setMockFileContent('abc'); + $stream = fopen($this->getMockFileName(), 'rb'); + fgetc($stream); + fgetc($stream); + fgetc($stream); + fgetc($stream); + assertThat(ftell($stream), equals(3)); + } + + public function readSeveralBytesProgressesPointer(): void + { + $this->setMockFileContent('abcdef'); + $stream = fopen($this->getMockFileName(), 'rb'); + fread($stream, 3); + assertThat(ftell($stream), equals(3)); + } + + /** + * @test + */ + public function readMoreThanSizeDoesntPointPastEndOfFile(): void + { + $this->setMockFileContent('123456789'); + $stream = fopen($this->getMockFileName(), 'rb'); + fread($stream, 10); + assertThat(ftell($stream), equals(9)); + } + + public function readLineProgressesPointer(): void + { + $this->setMockFileContent("123\n456"); + $stream = fopen($this->getMockFileName(), 'rb'); + fgets($stream); + assertThat(ftell($stream), equals(3)); + } +} \ No newline at end of file diff --git a/tests/phpunit/functional/README.md b/tests/phpunit/functional/README.md new file mode 100644 index 00000000..056fb791 --- /dev/null +++ b/tests/phpunit/functional/README.md @@ -0,0 +1,23 @@ +## Purpose + +The tests in this directory are intended to test that the behaviour of the stream wrapper +matches the behaviour of normal PHP file streams. + +The individual tests should remain completely independent of the implementation details of +the wrapper. The only library-specific logic should be in the `BaseFunctionalTestCase` class. + +## Testing the Tests + +In order to verify that the tests match the behaviour of real PHP file streams, an additional +test mode is included which runs the assertions against a real file created in the system temp +directory. + +To run these, point PHPUnit at the config file in this directory: + + vendor/bin/phpunit -c tests/phpunit/functional/testthetests.phpunit.xml + +## Adding New Types of Test + +In order for the above to work, all the setup and fixture manipulation should be maintained in +`BaseFunctionalTestCase`. Each method includes two implementations: one using `vfsStream` APIs, +and one using the real file system. \ No newline at end of file diff --git a/tests/phpunit/functional/SimultaneousStreamsTestCase.php b/tests/phpunit/functional/SimultaneousStreamsTestCase.php new file mode 100644 index 00000000..6f589070 --- /dev/null +++ b/tests/phpunit/functional/SimultaneousStreamsTestCase.php @@ -0,0 +1,156 @@ +setMockFileContent($content); + + $fp1 = fopen($this->getMockFileName(), 'rb'); + $fp2 = fopen($this->getMockFileName(), 'rb'); + + assertThat(fread($fp1, 4096), equals($content)); + assertThat(fread($fp2, 4096), equals($content)); + + fclose($fp1); + fclose($fp2); + } + + /** + * @test + */ + public function canReadTwoStreamsAlternately(): void + { + $this->setMockFileContent("Line 1\nLine 2\n"); + + $stream1 = fopen($this->getMockFileName(), 'rb'); + $stream2 = fopen($this->getMockFileName(), 'rb'); + + assertThat(fgets($stream1), equals("Line 1\n"), 'First line from first stream'); + assertThat(fgets($stream2), equals("Line 1\n"), 'First line from second stream'); + + assertThat(fgets($stream1), equals("Line 2\n"), 'Second line from first stream'); + assertThat(fgets($stream2), equals("Line 2\n"), 'Second line from second stream'); + } + + /** + * @test + */ + public function canReadTwoStreamsSequentially(): void + { + $this->setMockFileContent("Line 1\nLine 2\n"); + + $stream1 = fopen($this->getMockFileName(), 'rb'); + $stream2 = fopen($this->getMockFileName(), 'rb'); + + assertThat(fgets($stream1), equals("Line 1\n"), 'First line from first stream'); + assertThat(fgets($stream1), equals("Line 2\n"), 'Second line from first stream'); + + assertThat(fgets($stream2), equals("Line 1\n"), 'First line from second stream'); + assertThat(fgets($stream2), equals("Line 2\n"), 'Second line from second stream'); + } + + /** + * @test + */ + public function multipleWritesOnSameFileHaveDifferentPointers(): void + { + $contentA = uniqid('a'); + $contentB = uniqid('b'); + $url = $this->getMockFileName(); + + $fp1 = fopen($url, 'wb'); + $fp2 = fopen($url, 'wb'); + + fwrite($fp1, $contentA . $contentA); + fwrite($fp2, $contentB); + + fclose($fp1); + fclose($fp2); + + assertThat(file_get_contents($url), equals($contentB . $contentA)); + } + + /** + * @test + */ + public function readsAndWritesOnSameFileHaveDifferentPointers(): void + { + $contentA = uniqid('a'); + $contentB = uniqid('b'); + $url = $this->getMockFileName(); + + $fp1 = fopen($url, 'wb'); + $fp2 = fopen($url, 'rb'); + + fwrite($fp1, $contentA); + $contentBeforeWrite = fread($fp2, strlen($contentA)); + + fwrite($fp1, $contentB); + $contentAfterWrite = fread($fp2, strlen($contentB)); + + fclose($fp1); + fclose($fp2); + + assertThat($contentBeforeWrite, equals($contentA)); + assertThat($contentAfterWrite, equals($contentB)); + } + + /** + * @test + */ + public function canReadLinesWrittenBeforeReading(): void + { + $this->setMockFileContent("Line 1\nLine 2\n"); + + $writeStream = fopen($this->getMockFileName(), 'wb'); + $readStream = fopen($this->getMockFileName(), 'rb'); + + fputs($writeStream, "Line 1\n"); + fputs($writeStream, "Line 2\n"); + + assertThat(fgets($readStream), equals("Line 1\n"), 'Should be able to read first line'); + assertThat(fgets($readStream), equals("Line 2\n"), 'Should be able to read second line'); + } + + /** + * @test + */ + public function canReadLinesWrittenAfterFirstRead(): void + { + $this->setMockFileContent("Line 1\nLine 2\n"); + + $writeStream = fopen($this->getMockFileName(), 'wb'); + $readStream = fopen($this->getMockFileName(), 'rb'); + + fputs($writeStream, "Line 1\n"); + fputs($writeStream, "Line 2\n"); + assertThat(fgets($readStream), equals("Line 1\n"), 'Should be able to read first line written before first read'); + + fputs($writeStream, "Line 3\n"); + assertThat(fgets($readStream), equals("Line 2\n"), 'Should be able to read second line written before first read'); + assertThat(fgets($readStream), equals("Line 3\n"), 'Should be able to read line written after first read'); + } +} diff --git a/tests/phpunit/functional/testthetests.phpunit.xml b/tests/phpunit/functional/testthetests.phpunit.xml new file mode 100644 index 00000000..a0295541 --- /dev/null +++ b/tests/phpunit/functional/testthetests.phpunit.xml @@ -0,0 +1,42 @@ + + + + + . + + + + + + ../../../src + + + + + + + + + + + + + + + +