From c95854d13a825d55ccd597c64910d1e6afc2ca3c Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Fri, 1 May 2020 09:52:19 +0200 Subject: [PATCH 01/62] do not show install progress --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06739d4..f5c3ea4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- - name: Install Dependencies - run: composer install + run: composer install --no-progress - name: PHPUnit run: vendor/bin/phpunit --coverage-clover=coverage.clover - uses: codecov/codecov-action@v1 @@ -59,6 +59,6 @@ jobs: key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- - name: Install Dependencies - run: composer install + run: composer install --no-progress - name: Psalm run: vendor/bin/psalm From d10e7843a815eacdccebbbe6ef2e115694ccc94a Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Fri, 1 May 2020 09:52:38 +0200 Subject: [PATCH 02/62] publish type coverage --- .github/workflows/ci.yml | 2 +- README.md | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5c3ea4..20b6407 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,4 +61,4 @@ jobs: - name: Install Dependencies run: composer install --no-progress - name: Psalm - run: vendor/bin/psalm + run: vendor/bin/psalm --shepherd diff --git a/README.md b/README.md index 8d736aa..e1f3f53 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ # Filesystem -| `develop` | -|-----------| -| [![codecov](https://codecov.io/gh/Innmind/Filesystem/branch/develop/graph/badge.svg)](https://codecov.io/gh/Innmind/Filesystem) | -| [![Build Status](https://github.com/Innmind/Filesystem/workflows/CI/badge.svg)](https://github.com/Innmind/Filesystem/actions?query=workflow%3ACI) | +[![Build Status](https://github.com/Innmind/Filesystem/workflows/CI/badge.svg)](https://github.com/Innmind/Filesystem/actions?query=workflow%3ACI) +[![codecov](https://codecov.io/gh/Innmind/Filesystem/branch/develop/graph/badge.svg)](https://codecov.io/gh/Innmind/Filesystem) +[![Type Coverage](https://shepherd.dev/github/Innmind/Filesystem/coverage.svg)](https://shepherd.dev/github/Innmind/Filesystem) Filesystem abstraction layer, the goal is to provide a model where you design how you put your files into directories without worrying where it will be persisted. From f21c904cc0c2c1925a2d1c847344308ffdb508f0 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Fri, 1 May 2020 09:55:44 +0200 Subject: [PATCH 03/62] update psalm config --- psalm.xml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/psalm.xml b/psalm.xml index 408036f..617f63d 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,6 +1,7 @@ - - - - From f3836af369729187808d4e1da7cec434f6844294 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Fri, 1 May 2020 10:08:22 +0200 Subject: [PATCH 04/62] add name properties --- composer.json | 3 ++- tests/NameTest.php | 62 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a163c86..d505b2b 100644 --- a/composer.json +++ b/composer.json @@ -35,6 +35,7 @@ }, "require-dev": { "phpunit/phpunit": "~8.0", - "vimeo/psalm": "^3.7" + "vimeo/psalm": "^3.7", + "innmind/black-box": "^4.5" } } diff --git a/tests/NameTest.php b/tests/NameTest.php index fc42f20..7aeebb4 100644 --- a/tests/NameTest.php +++ b/tests/NameTest.php @@ -8,9 +8,15 @@ Exception\DomainException, }; use PHPUnit\Framework\TestCase; +use Innmind\BlackBox\{ + PHPUnit\BlackBox, + Set, +}; class NameTest extends TestCase { + use BlackBox; + public function testInterface() { $n = new Name('foo'); @@ -31,4 +37,60 @@ public function testEquals() $this->assertTrue((new Name('foo'))->equals(new Name('foo'))); $this->assertFalse((new Name('foo'))->equals(new Name('bar'))); } + + public function testAcceptsAnyValueNotContainingASlash() + { + $this + ->forAll( + Set\Strings::any()->filter(fn($s) => \strpos($s, '/') === false), + ) + ->then(function($value) { + $name = new Name($value); + + $this->assertSame($value, $name->toString()); + }); + } + + public function testNameContainingASlashIsNotAccepted() + { + $this + ->forAll( + Set\Strings::any(), + Set\Strings::any(), + ) + ->then(function($a, $b) { + $this->expectException(DomainException::class); + + new Name("$a/$b"); + }); + } + + public function testNameEqualsItself() + { + $this + ->forAll(Set\Strings::any()->filter(fn($s) => \strpos($s, '/') === false)) + ->then(function($value) { + $name1 = new Name($value); + $name2 = new Name($value); + + $this->assertTrue($name1->equals($name1)); + $this->assertTrue($name1->equals($name2)); + }); + } + + public function testNameDoesntEqualDifferentName() + { + $this + ->forAll( + Set\Strings::any()->filter(fn($s) => \strpos($s, '/') === false), + Set\Strings::any()->filter(fn($s) => \strpos($s, '/') === false), + ) + ->then(function($a, $b) { + $name1 = new Name($a); + $name2 = new Name($b); + + $this->assertFalse($name1->equals($name2)); + $this->assertFalse($name2->equals($name1)); + }); + } } From b8fbd95ca4cc9eb2992f6ad1977f9588f8304108 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Fri, 1 May 2020 10:26:48 +0200 Subject: [PATCH 05/62] add file properties --- composer.json | 3 +- fixtures/Name.php | 21 +++++++++ tests/File/FileTest.php | 96 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 fixtures/Name.php diff --git a/composer.json b/composer.json index d505b2b..e379a99 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,8 @@ }, "autoload": { "psr-4": { - "Innmind\\Filesystem\\": "src/" + "Innmind\\Filesystem\\": "src/", + "Fixtures\\Innmind\\Filesystem\\": "fixtures/" } }, "autoload-dev": { diff --git a/fixtures/Name.php b/fixtures/Name.php new file mode 100644 index 0000000..c6c2435 --- /dev/null +++ b/fixtures/Name.php @@ -0,0 +1,21 @@ + + */ + public static function any(): Set + { + return Set\Decorate::immutable( + static fn(string $name): Model => new Model($name), + Set\Strings::any()->filter(fn($s) => \strpos($s, '/') === false), + ); + } +} diff --git a/tests/File/FileTest.php b/tests/File/FileTest.php index 766879d..8ca2c26 100644 --- a/tests/File/FileTest.php +++ b/tests/File/FileTest.php @@ -11,9 +11,17 @@ use Innmind\Stream\Readable\Stream; use Innmind\MediaType\MediaType; use PHPUnit\Framework\TestCase; +use Innmind\BlackBox\{ + PHPUnit\BlackBox, + Set, +}; +use Fixtures\Innmind\Filesystem\Name as FName; +use Fixtures\Innmind\MediaType\MediaType as FMediaType; class FileTest extends TestCase { + use BlackBox; + public function testInterface() { $f = new File($name = new Name('foo'), $c = Stream::ofContent('bar')); @@ -56,4 +64,92 @@ public function testMediaType() $this->assertSame($mt, $f->mediaType()); } + + public function testContentIsNeverAltered() + { + $this + ->forAll( + FName::any(), + Set\Strings::any(), + FMediaType::any(), + ) + ->then(function($name, $content, $mediaType) { + $file = new File( + $name, + $stream = Stream::ofContent($content), + $mediaType, + ); + + $this->assertSame($name, $file->name()); + $this->assertSame($stream, $file->content()); + $this->assertSame($mediaType, $file->mediaType()); + }); + } + + public function testByDefaultTheMediaTypeIsOctetStream() + { + $this + ->forAll( + FName::any(), + Set\Strings::any(), + ) + ->then(function($name, $content) { + $file = new File( + $name, + Stream::ofContent($content), + ); + + $this->assertSame( + 'application/octet-stream', + $file->mediaType()->toString(), + ); + }); + } + + public function testNamedConstructorNeverAltersTheContent() + { + $this + ->forAll( + FName::any(), + Set\Strings::any(), + FMediaType::any(), + ) + ->then(function($name, $content, $mediaType) { + $file = File::named( + $name->toString(), + $stream = Stream::ofContent($content), + $mediaType, + ); + + $this->assertTrue($file->name()->equals($name)); + $this->assertSame($stream, $file->content()); + $this->assertSame($mediaType, $file->mediaType()); + }); + } + + public function testWithContentIsPure() + { + $this + ->forAll( + FName::any(), + Set\Strings::any(), + Set\Strings::any(), + FMediaType::any(), + ) + ->then(function($name, $content, $content2, $mediaType) { + $file1 = new File( + $name, + $stream = Stream::ofContent($content), + $mediaType, + ); + $file2 = $file1->withContent(Stream::ofContent($content2)); + + $this->assertSame($name, $file1->name()); + $this->assertSame($stream, $file1->content()); + $this->assertSame($mediaType, $file1->mediaType()); + $this->assertSame($name, $file2->name()); + $this->assertSame($content2, $file2->content()->toString()); + $this->assertSame($mediaType, $file2->mediaType()); + }); + } } From 6b5281d5145b62f7331c73a6ffb6a4268710ee44 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Fri, 1 May 2020 11:14:17 +0200 Subject: [PATCH 06/62] empty names are not allowed --- fixtures/Name.php | 4 +++- src/Name.php | 4 ++++ tests/NameTest.php | 26 ++++++++++++++++++++++---- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/fixtures/Name.php b/fixtures/Name.php index c6c2435..bb44a2d 100644 --- a/fixtures/Name.php +++ b/fixtures/Name.php @@ -15,7 +15,9 @@ public static function any(): Set { return Set\Decorate::immutable( static fn(string $name): Model => new Model($name), - Set\Strings::any()->filter(fn($s) => \strpos($s, '/') === false), + Set\Strings::any() + ->filter(static fn($s) => \strpos($s, '/') === false) + ->filter(static fn($s) => $s !== ''), ); } } diff --git a/src/Name.php b/src/Name.php index 4acc3ff..e5fe373 100644 --- a/src/Name.php +++ b/src/Name.php @@ -16,6 +16,10 @@ public function __construct(string $value) throw new DomainException("A file name can't contain a slash, $value given"); } + if (Str::of($value)->empty()) { + throw new DomainException('A file name can\'t be empty'); + } + $this->value = $value; } diff --git a/tests/NameTest.php b/tests/NameTest.php index 7aeebb4..a9e508d 100644 --- a/tests/NameTest.php +++ b/tests/NameTest.php @@ -38,11 +38,21 @@ public function testEquals() $this->assertFalse((new Name('foo'))->equals(new Name('bar'))); } + public function testEmptyNameIsNotAllowed() + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage('A file name can\'t be empty'); + + new Name(''); + } + public function testAcceptsAnyValueNotContainingASlash() { $this ->forAll( - Set\Strings::any()->filter(fn($s) => \strpos($s, '/') === false), + Set\Strings::any() + ->filter(fn($s) => \strpos($s, '/') === false) + ->filter(fn($s) => $s !== ''), ) ->then(function($value) { $name = new Name($value); @@ -68,7 +78,11 @@ public function testNameContainingASlashIsNotAccepted() public function testNameEqualsItself() { $this - ->forAll(Set\Strings::any()->filter(fn($s) => \strpos($s, '/') === false)) + ->forAll( + Set\Strings::any() + ->filter(fn($s) => \strpos($s, '/') === false) + ->filter(fn($s) => $s !== ''), + ) ->then(function($value) { $name1 = new Name($value); $name2 = new Name($value); @@ -82,8 +96,12 @@ public function testNameDoesntEqualDifferentName() { $this ->forAll( - Set\Strings::any()->filter(fn($s) => \strpos($s, '/') === false), - Set\Strings::any()->filter(fn($s) => \strpos($s, '/') === false), + Set\Strings::any() + ->filter(fn($s) => \strpos($s, '/') === false) + ->filter(fn($s) => $s !== ''), + Set\Strings::any() + ->filter(fn($s) => \strpos($s, '/') === false) + ->filter(fn($s) => $s !== ''), ) ->then(function($a, $b) { $name1 = new Name($a); From 54f673908e7c0cc79b9ca81a0d78bb0e37c7c681 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Fri, 1 May 2020 11:14:29 +0200 Subject: [PATCH 07/62] change printer class --- phpunit.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6cfd7bd..0cbc76b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,6 +1,6 @@ - + ./tests From b5af26958f2ff96587be4b6e9976fcd936c877e4 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Fri, 1 May 2020 13:01:23 +0200 Subject: [PATCH 08/62] add directory properties --- composer.json | 3 +- fixtures/File.php | 26 ++++++ properties/Directory.php | 31 +++++++ .../AccessingUnknownFileThrowsAnException.php | 39 +++++++++ properties/Directory/AddDirectory.php | 54 +++++++++++++ properties/Directory/AddFile.php | 56 +++++++++++++ .../AllFilesInTheDirectoryAreAccessible.php | 32 ++++++++ ...AlwaysReturnTrueForFilesInTheDirectory.php | 29 +++++++ .../ContentHoldsTheNamesOfTheFiles.php | 33 ++++++++ .../Directory/MediaTypeIsAlwaysTheSame.php | 30 +++++++ properties/Directory/RemoveDirectory.php | 64 +++++++++++++++ properties/Directory/RemoveFile.php | 51 ++++++++++++ .../RemovingAnUnknownFileHasNoEffect.php | 33 ++++++++ src/Directory/Directory.php | 13 +++ tests/Directory/DirectoryTest.php | 80 +++++++++++++++++++ 15 files changed, 573 insertions(+), 1 deletion(-) create mode 100644 fixtures/File.php create mode 100644 properties/Directory.php create mode 100644 properties/Directory/AccessingUnknownFileThrowsAnException.php create mode 100644 properties/Directory/AddDirectory.php create mode 100644 properties/Directory/AddFile.php create mode 100644 properties/Directory/AllFilesInTheDirectoryAreAccessible.php create mode 100644 properties/Directory/ContainsMethodAlwaysReturnTrueForFilesInTheDirectory.php create mode 100644 properties/Directory/ContentHoldsTheNamesOfTheFiles.php create mode 100644 properties/Directory/MediaTypeIsAlwaysTheSame.php create mode 100644 properties/Directory/RemoveDirectory.php create mode 100644 properties/Directory/RemoveFile.php create mode 100644 properties/Directory/RemovingAnUnknownFileHasNoEffect.php diff --git a/composer.json b/composer.json index e379a99..3dbab0b 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,8 @@ "autoload": { "psr-4": { "Innmind\\Filesystem\\": "src/", - "Fixtures\\Innmind\\Filesystem\\": "fixtures/" + "Fixtures\\Innmind\\Filesystem\\": "fixtures/", + "Properties\\Innmind\\Filesystem\\": "properties/" } }, "autoload-dev": { diff --git a/fixtures/File.php b/fixtures/File.php new file mode 100644 index 0000000..ff7a355 --- /dev/null +++ b/fixtures/File.php @@ -0,0 +1,26 @@ + new Model( + $name, + Stream::ofContent($content), + $mediaType, + ), + Name::any(), + Set\Strings::any(), + MediaType::any(), + ); + } +} diff --git a/properties/Directory.php b/properties/Directory.php new file mode 100644 index 0000000..9e29c0d --- /dev/null +++ b/properties/Directory.php @@ -0,0 +1,31 @@ + + */ + public static function properties(): Set + { + return Set\Properties::of( + new Directory\MediaTypeIsAlwaysTheSame, + new Directory\ContainsMethodAlwaysReturnTrueForFilesInTheDirectory, + new Directory\AllFilesInTheDirectoryAreAccessible, + new Directory\AccessingUnknownFileThrowsAnException, + new Directory\RemovingAnUnknownFileHasNoEffect, + new Directory\RemoveFile, + new Directory\RemoveDirectory, + new Directory\ContentHoldsTheNamesOfTheFiles, + new Directory\AddFile, + new Directory\AddDirectory, + ); + } +} diff --git a/properties/Directory/AccessingUnknownFileThrowsAnException.php b/properties/Directory/AccessingUnknownFileThrowsAnException.php new file mode 100644 index 0000000..964bc48 --- /dev/null +++ b/properties/Directory/AccessingUnknownFileThrowsAnException.php @@ -0,0 +1,39 @@ +contains(new Name(self::UNKNOWN)); + } + + public function ensureHeldBy(object $directory): object + { + try { + $directory->get(new Name(self::UNKNOWN)); + + Assert::fail('It should throw an exception'); + } catch (FileNotFound $e) { + Assert::assertTrue(true); + } + + return $directory; + } +} diff --git a/properties/Directory/AddDirectory.php b/properties/Directory/AddDirectory.php new file mode 100644 index 0000000..0bc8bd7 --- /dev/null +++ b/properties/Directory/AddDirectory.php @@ -0,0 +1,54 @@ +contains(new Name(self::NAME)); + } + + public function ensureHeldBy(object $directory): object + { + $file = new Directory( + new Name(self::NAME), + ); + + Assert::assertFalse($directory->contains($file->name())); + $newDirectory = $directory->add($file); + Assert::assertNotSame($directory, $newDirectory); + Assert::assertFalse($directory->contains($file->name())); + Assert::assertTrue($newDirectory->contains($file->name())); + Assert::assertGreaterThan( + $directory->modifications()->size(), + $newDirectory->modifications()->size(), + ); + Assert::assertInstanceOf( + FileWasAdded::class, + $newDirectory->modifications()->last(), + ); + Assert::assertSame( + $file, + $newDirectory->modifications()->last()->file(), + ); + + return $newDirectory; + } +} diff --git a/properties/Directory/AddFile.php b/properties/Directory/AddFile.php new file mode 100644 index 0000000..6d31067 --- /dev/null +++ b/properties/Directory/AddFile.php @@ -0,0 +1,56 @@ +contains(new Name(self::NAME)); + } + + public function ensureHeldBy(object $directory): object + { + $file = new File( + new Name(self::NAME), + new NullStream, + ); + + Assert::assertFalse($directory->contains($file->name())); + $newDirectory = $directory->add($file); + Assert::assertNotSame($directory, $newDirectory); + Assert::assertFalse($directory->contains($file->name())); + Assert::assertTrue($newDirectory->contains($file->name())); + Assert::assertGreaterThan( + $directory->modifications()->size(), + $newDirectory->modifications()->size(), + ); + Assert::assertInstanceOf( + FileWasAdded::class, + $newDirectory->modifications()->last(), + ); + Assert::assertSame( + $file, + $newDirectory->modifications()->last()->file(), + ); + + return $newDirectory; + } +} diff --git a/properties/Directory/AllFilesInTheDirectoryAreAccessible.php b/properties/Directory/AllFilesInTheDirectoryAreAccessible.php new file mode 100644 index 0000000..8eb3e08 --- /dev/null +++ b/properties/Directory/AllFilesInTheDirectoryAreAccessible.php @@ -0,0 +1,32 @@ +foreach(static function($file) use ($directory) { + Assert::assertSame( + $file, + $directory->get($file->name()), + ); + }); + + return $directory; + } +} diff --git a/properties/Directory/ContainsMethodAlwaysReturnTrueForFilesInTheDirectory.php b/properties/Directory/ContainsMethodAlwaysReturnTrueForFilesInTheDirectory.php new file mode 100644 index 0000000..ec86ffb --- /dev/null +++ b/properties/Directory/ContainsMethodAlwaysReturnTrueForFilesInTheDirectory.php @@ -0,0 +1,29 @@ +foreach(static function($file) use ($directory) { + Assert::assertTrue($directory->contains($file->name())); + }); + + return $directory; + } +} diff --git a/properties/Directory/ContentHoldsTheNamesOfTheFiles.php b/properties/Directory/ContentHoldsTheNamesOfTheFiles.php new file mode 100644 index 0000000..24b6f7e --- /dev/null +++ b/properties/Directory/ContentHoldsTheNamesOfTheFiles.php @@ -0,0 +1,33 @@ +content()->toString(); + $directory->foreach(function($file) use ($content) { + Assert::assertStringContainsString( + $file->name()->toString(), + $content, + ); + }); + + return $directory; + } +} diff --git a/properties/Directory/MediaTypeIsAlwaysTheSame.php b/properties/Directory/MediaTypeIsAlwaysTheSame.php new file mode 100644 index 0000000..8e740dd --- /dev/null +++ b/properties/Directory/MediaTypeIsAlwaysTheSame.php @@ -0,0 +1,30 @@ +mediaType()->toString(), + ); + + return $directory; + } +} diff --git a/properties/Directory/RemoveDirectory.php b/properties/Directory/RemoveDirectory.php new file mode 100644 index 0000000..5a7cea0 --- /dev/null +++ b/properties/Directory/RemoveDirectory.php @@ -0,0 +1,64 @@ +reduce( + false, + fn($found, $file) => $found || $file instanceof Directory, + ); + } + + public function ensureHeldBy(object $directory): object + { + $file = $directory->reduce( + null, + function($found, $file) { + if ($found) { + return $found; + } + + if ($file instanceof Directory) { + return $file; + } + + return null; + }, + ); + + $newDirectory = $directory->remove($file->name()); + Assert::assertFalse($newDirectory->contains($file->name())); + Assert::assertTrue($directory->contains($file->name())); + Assert::assertGreaterThan( + $directory->modifications()->size(), + $newDirectory->modifications()->size(), + ); + Assert::assertInstanceOf( + FileWasRemoved::class, + $newDirectory->modifications()->last(), + ); + Assert::assertSame( + $file->name(), + $newDirectory->modifications()->last()->file(), + ); + + return $newDirectory; + } +} diff --git a/properties/Directory/RemoveFile.php b/properties/Directory/RemoveFile.php new file mode 100644 index 0000000..dcf0e4a --- /dev/null +++ b/properties/Directory/RemoveFile.php @@ -0,0 +1,51 @@ +reduce( + false, + fn() => true, + ); + } + + public function ensureHeldBy(object $directory): object + { + $file = $directory->reduce( + null, + fn($found, $file) => $found ?? $file, + ); + + $newDirectory = $directory->remove($file->name()); + Assert::assertFalse($newDirectory->contains($file->name())); + Assert::assertTrue($directory->contains($file->name())); + Assert::assertGreaterThan( + $directory->modifications()->size(), + $newDirectory->modifications()->size(), + ); + Assert::assertInstanceOf( + FileWasRemoved::class, + $newDirectory->modifications()->last(), + ); + Assert::assertSame( + $file->name(), + $newDirectory->modifications()->last()->file(), + ); + + return $newDirectory; + } +} diff --git a/properties/Directory/RemovingAnUnknownFileHasNoEffect.php b/properties/Directory/RemovingAnUnknownFileHasNoEffect.php new file mode 100644 index 0000000..077ee0a --- /dev/null +++ b/properties/Directory/RemovingAnUnknownFileHasNoEffect.php @@ -0,0 +1,33 @@ +contains(new Name(self::UNKNOWN)); + } + + public function ensureHeldBy(object $directory): object + { + Assert::assertSame( + $directory, + $directory->remove(new Name(self::UNKNOWN)), + ); + + return $directory; + } +} diff --git a/src/Directory/Directory.php b/src/Directory/Directory.php index 33f3955..d20eceb 100644 --- a/src/Directory/Directory.php +++ b/src/Directory/Directory.php @@ -46,6 +46,19 @@ public function __construct(Name $name, Set $files = null) assertSet(File::class, $files, 2); + $files->reduce( + Set::strings(), + static function(Set $names, File $file): Set { + $name = $file->name()->toString(); + + if ($names->contains($name)) { + throw new LogicException("Same file '$name' found multiple times"); + } + + return ($names)($name); + }, + ); + $this->name = $name; $this->files = $files; $this->mediaType = new MediaType( diff --git a/tests/Directory/DirectoryTest.php b/tests/Directory/DirectoryTest.php index 46e4b1f..51b7368 100644 --- a/tests/Directory/DirectoryTest.php +++ b/tests/Directory/DirectoryTest.php @@ -18,9 +18,21 @@ use Innmind\Immutable\Set; use function Innmind\Immutable\unwrap; use PHPUnit\Framework\TestCase; +use Innmind\BlackBox\{ + PHPUnit\BlackBox, + Set as DataSet, +}; +use Fixtures\Innmind\Filesystem\{ + Name as FName, + File as FFile, +}; +use Fixtures\Innmind\Immutable\Set as FSet; +use Properties\Innmind\Filesystem\Directory as PDirectory; class DirectoryTest extends TestCase { + use BlackBox; + public function testInterface() { $d = new Directory(new Name('foo')); @@ -279,4 +291,72 @@ public function testFilter() $this->assertSame('foo', unwrap($set)[0]->name()->toString()); $this->assertSame('foobar', unwrap($set)[1]->name()->toString()); } + + public function testEmptyDirectoryHoldProperties() + { + $this + ->forAll( + PDirectory::properties(), + FName::any(), + ) + ->then(function($properties, $name) { + $directory = new Directory($name); + + $properties->ensureHeldBy($directory); + }); + } + + public function testDirectoryWithSomeFilesHoldProperties() + { + $this + ->forAll( + PDirectory::properties(), + FName::any(), + FSet::of( + File::class, + new DataSet\Randomize( + FFile::any(), + ), + ), + ) + ->filter(function($properties, $name, $files) { + if ($files->empty()) { + return true; + } + + // do not accept duplicated files + return $files + ->groupBy(fn($file) => $file->name()->toString()) + ->size() === $files->size(); + }) + ->then(function($properties, $name, $files) { + $directory = new Directory($name, $files); + + $properties->ensureHeldBy($directory); + }); + } + + public function testDirectoryLoadedWithDifferentFilesWithTheSameNameThrows() + { + $this + ->forAll( + FName::any(), + FName::any(), + DataSet\Strings::any(), + DataSet\Strings::any(), + ) + ->then(function($directory, $file, $content1, $content2) { + $this->expectException(LogicException::class); + $this->expectExceptionMessage("Same file '{$file->toString()}' found multiple times"); + + new Directory( + $directory, + Set::of( + File::class, + File\File::named($file->toString(), Stream::ofContent($content1)), + File\File::named($file->toString(), Stream::ofContent($content2)), + ), + ); + }); + } } From 1372c8bcb24aac4e7bc4c42d92413279ca9af066 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 2 May 2020 11:04:49 +0200 Subject: [PATCH 09/62] split standard testing to coverage to reduce CI time --- .github/workflows/ci.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20b6407..24ce28b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,34 @@ jobs: os: [ubuntu-latest, macOS-latest] php-version: ['7.4'] name: 'PHPUnit - PHP/${{ matrix.php-version }} - OS/${{ matrix.os }}' + steps: + - name: Checkout + uses: actions/checkout@v1 + - name: Setup PHP + uses: shivammathur/setup-php@v1 + with: + php-version: ${{ matrix.php-version }} + extensions: mbstring, intl + - name: Get Composer Cache Directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + - name: Cache dependencies + uses: actions/cache@v1 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + - name: Install Dependencies + run: composer install --no-progress + - name: PHPUnit + run: vendor/bin/phpunit + coverage: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macOS-latest] + php-version: ['7.4'] + name: 'Coverage - PHP/${{ matrix.php-version }} - OS/${{ matrix.os }}' steps: - name: Checkout uses: actions/checkout@v1 @@ -32,6 +60,8 @@ jobs: run: composer install --no-progress - name: PHPUnit run: vendor/bin/phpunit --coverage-clover=coverage.clover + env: + BLACKBOX_SET_SIZE: 1 - uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} From ff1455edc1e00ea34d8cb2f4e4b11d688b568a11 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 2 May 2020 12:39:11 +0200 Subject: [PATCH 10/62] add adapter properties --- properties/Adapter.php | 30 +++++++ .../AccessingUnknownFileThrowsAnException.php | 39 +++++++++ .../AddDirectoryFromAnotherAdapter.php | 64 +++++++++++++++ ...rectoryFromAnotherAdapterWithFileAdded.php | 81 +++++++++++++++++++ ...ctoryFromAnotherAdapterWithFileRemoved.php | 74 +++++++++++++++++ properties/Adapter/AddEmptyDirectory.php | 52 ++++++++++++ properties/Adapter/AddFile.php | 48 +++++++++++ .../Adapter/AllRootFilesAreAccessible.php | 39 +++++++++ properties/Adapter/RemoveFile.php | 42 ++++++++++ properties/Adapter/RemoveUnknownFile.php | 31 +++++++ tests/Adapter/InMemoryTest.php | 15 ++++ 11 files changed, 515 insertions(+) create mode 100644 properties/Adapter.php create mode 100644 properties/Adapter/AccessingUnknownFileThrowsAnException.php create mode 100644 properties/Adapter/AddDirectoryFromAnotherAdapter.php create mode 100644 properties/Adapter/AddDirectoryFromAnotherAdapterWithFileAdded.php create mode 100644 properties/Adapter/AddDirectoryFromAnotherAdapterWithFileRemoved.php create mode 100644 properties/Adapter/AddEmptyDirectory.php create mode 100644 properties/Adapter/AddFile.php create mode 100644 properties/Adapter/AllRootFilesAreAccessible.php create mode 100644 properties/Adapter/RemoveFile.php create mode 100644 properties/Adapter/RemoveUnknownFile.php diff --git a/properties/Adapter.php b/properties/Adapter.php new file mode 100644 index 0000000..c48899f --- /dev/null +++ b/properties/Adapter.php @@ -0,0 +1,30 @@ + + */ + public static function properties(): Set + { + return Set\Properties::of( + new Adapter\AddFile, + new Adapter\AddEmptyDirectory, + new Adapter\AddDirectoryFromAnotherAdapter, + new Adapter\AddDirectoryFromAnotherAdapterWithFileAdded, + new Adapter\AddDirectoryFromAnotherAdapterWithFileRemoved, + new Adapter\RemoveUnknownFile, + new Adapter\RemoveFile, + new Adapter\AllRootFilesAreAccessible, + new Adapter\AccessingUnknownFileThrowsAnException, + ); + } +} diff --git a/properties/Adapter/AccessingUnknownFileThrowsAnException.php b/properties/Adapter/AccessingUnknownFileThrowsAnException.php new file mode 100644 index 0000000..aa9ca00 --- /dev/null +++ b/properties/Adapter/AccessingUnknownFileThrowsAnException.php @@ -0,0 +1,39 @@ +contains(new Name(self::NAME)); + } + + public function ensureHeldBy(object $adapter): object + { + try { + $adapter->get(new Name(self::NAME)); + + Assert::fail('It should throw an exception'); + } catch (FileNotFound $e) { + Assert::assertTrue(true); + } + + return $adapter; + } +} diff --git a/properties/Adapter/AddDirectoryFromAnotherAdapter.php b/properties/Adapter/AddDirectoryFromAnotherAdapter.php new file mode 100644 index 0000000..11cb4e9 --- /dev/null +++ b/properties/Adapter/AddDirectoryFromAnotherAdapter.php @@ -0,0 +1,64 @@ +contains(new Name(self::NAME)); + } + + public function ensureHeldBy(object $adapter): object + { + // directories loaded from other adapters have files injecting at + // construct time (so there is no modifications()) + $directory = new Directory( + new Name(self::NAME), + Set::of( + File::class, + new File\File( + new Name('file from other adapter'), + Stream::ofContent('foobar'), + ), + ), + ); + + Assert::assertFalse($adapter->contains($directory->name())); + Assert::assertNull($adapter->add($directory)); + Assert::assertTrue($adapter->contains($directory->name())); + Assert::assertTrue( + $adapter + ->get($directory->name()) + ->contains(new Name('file from other adapter')), + ); + Assert::assertSame( + 'foobar', + $adapter + ->get($directory->name()) + ->get(new Name('file from other adapter')) + ->content() + ->toString(), + ); + + return $adapter; + } +} diff --git a/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileAdded.php b/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileAdded.php new file mode 100644 index 0000000..01e9e1f --- /dev/null +++ b/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileAdded.php @@ -0,0 +1,81 @@ +contains(new Name(self::NAME)); + } + + public function ensureHeldBy(object $adapter): object + { + // directories loaded from other adapters have files injecting at + // construct time (so there is no modifications()) + $directory = new Directory( + new Name(self::NAME), + Set::of( + File::class, + new File\File( + new Name('file from other adapter'), + Stream::ofContent('foobar'), + ), + ), + ); + $directory = $directory->add(new File\File( + new Name('file added afterward'), + Stream::ofContent('baz'), + )); + + Assert::assertFalse($adapter->contains($directory->name())); + Assert::assertNull($adapter->add($directory)); + Assert::assertTrue($adapter->contains($directory->name())); + Assert::assertTrue( + $adapter + ->get($directory->name()) + ->contains(new Name('file from other adapter')), + ); + Assert::assertTrue( + $adapter + ->get($directory->name()) + ->contains(new Name('file added afterward')), + ); + Assert::assertSame( + 'foobar', + $adapter + ->get($directory->name()) + ->get(new Name('file from other adapter')) + ->content() + ->toString(), + ); + Assert::assertSame( + 'baz', + $adapter + ->get($directory->name()) + ->get(new Name('file added afterward')) + ->content() + ->toString(), + ); + + return $adapter; + } +} diff --git a/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileRemoved.php b/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileRemoved.php new file mode 100644 index 0000000..16878f2 --- /dev/null +++ b/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileRemoved.php @@ -0,0 +1,74 @@ +contains(new Name(self::NAME)); + } + + public function ensureHeldBy(object $adapter): object + { + // directories loaded from other adapters have files injecting at + // construct time (so there is no modifications()) + $directory = new Directory( + new Name(self::NAME), + Set::of( + File::class, + new File\File( + new Name('file removed afterward'), + Stream::ofContent('baz'), + ), + new File\File( + new Name('file from other adapter'), + Stream::ofContent('foobar'), + ), + ), + ); + $directory = $directory->remove(new Name('file removed afterward')); + + Assert::assertFalse($adapter->contains($directory->name())); + Assert::assertNull($adapter->add($directory)); + Assert::assertTrue($adapter->contains($directory->name())); + Assert::assertTrue( + $adapter + ->get($directory->name()) + ->contains(new Name('file from other adapter')), + ); + Assert::assertFalse( + $adapter + ->get($directory->name()) + ->contains(new Name('file removed afterward')), + ); + Assert::assertSame( + 'foobar', + $adapter + ->get($directory->name()) + ->get(new Name('file from other adapter')) + ->content() + ->toString(), + ); + + return $adapter; + } +} diff --git a/properties/Adapter/AddEmptyDirectory.php b/properties/Adapter/AddEmptyDirectory.php new file mode 100644 index 0000000..6d9415c --- /dev/null +++ b/properties/Adapter/AddEmptyDirectory.php @@ -0,0 +1,52 @@ +contains(new Name(self::NAME)); + } + + public function ensureHeldBy(object $adapter): object + { + $directory = new Directory( + new Name(self::NAME), + ); + + Assert::assertFalse($adapter->contains($directory->name())); + Assert::assertNull($adapter->add($directory)); + Assert::assertTrue($adapter->contains($directory->name())); + Assert::assertSame( + [], + $adapter + ->get($directory->name()) + ->reduce( + [], + static function(array $files, $file): array { + $files[] = $file; + + return $files; + }, + ), + ); + + return $adapter; + } +} diff --git a/properties/Adapter/AddFile.php b/properties/Adapter/AddFile.php new file mode 100644 index 0000000..11164cf --- /dev/null +++ b/properties/Adapter/AddFile.php @@ -0,0 +1,48 @@ +contains(new Name(self::NAME)); + } + + public function ensureHeldBy(object $adapter): object + { + $file = new File( + new Name(self::NAME), + Stream::ofContent('foo'), + ); + + Assert::assertFalse($adapter->contains($file->name())); + Assert::assertNull($adapter->add($file)); + Assert::assertTrue($adapter->contains($file->name())); + Assert::assertSame( + 'foo', + $adapter + ->get($file->name()) + ->content() + ->toString(), + ); + + return $adapter; + } +} diff --git a/properties/Adapter/AllRootFilesAreAccessible.php b/properties/Adapter/AllRootFilesAreAccessible.php new file mode 100644 index 0000000..13affc9 --- /dev/null +++ b/properties/Adapter/AllRootFilesAreAccessible.php @@ -0,0 +1,39 @@ +all() + ->foreach(function($file) use ($adapter) { + Assert::assertTrue($adapter->contains($file->name())); + Assert::assertSame( + $file->content()->toString(), + $adapter + ->get($file->name()) + ->content() + ->toString(), + ); + }); + + return $adapter; + } +} diff --git a/properties/Adapter/RemoveFile.php b/properties/Adapter/RemoveFile.php new file mode 100644 index 0000000..4c5ca0b --- /dev/null +++ b/properties/Adapter/RemoveFile.php @@ -0,0 +1,42 @@ +add($file)); + Assert::assertTrue($adapter->contains($file->name())); + Assert::assertNull($adapter->remove($file->name())); + Assert::assertFalse($adapter->contains($file->name())); + + return $adapter; + } +} diff --git a/properties/Adapter/RemoveUnknownFile.php b/properties/Adapter/RemoveUnknownFile.php new file mode 100644 index 0000000..2e77900 --- /dev/null +++ b/properties/Adapter/RemoveUnknownFile.php @@ -0,0 +1,31 @@ +remove(new Name(self::NAME))); + Assert::assertFalse($adapter->contains(new Name(self::NAME))); + + return $adapter; + } +} diff --git a/tests/Adapter/InMemoryTest.php b/tests/Adapter/InMemoryTest.php index bf4b1e8..c78d541 100644 --- a/tests/Adapter/InMemoryTest.php +++ b/tests/Adapter/InMemoryTest.php @@ -16,9 +16,15 @@ use Innmind\Immutable\Set; use function Innmind\Immutable\unwrap; use PHPUnit\Framework\TestCase; +use Innmind\BlackBox\{ + PHPUnit\BlackBox, +}; +use Properties\Innmind\Filesystem\Adapter as PAdapter; class InMemoryTest extends TestCase { + use BlackBox; + public function testInterface() { $a = new InMemory; @@ -67,4 +73,13 @@ public function testAll() unwrap($all), ); } + + public function testHoldProperties() + { + $this + ->forAll(PAdapter::properties()) + ->then(function($properties) { + $properties->ensureHeldBy(new InMemory); + }); + } } From fba5fe25f3772a0e8d1d58dfe29a91be8df5a41c Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 2 May 2020 15:00:25 +0200 Subject: [PATCH 11/62] randomize directory properties --- composer.json | 2 +- properties/Directory.php | 23 ++++++++++---- .../AccessingUnknownFileThrowsAnException.php | 13 +++++--- properties/Directory/AddDirectory.php | 13 +++++--- properties/Directory/AddFile.php | 30 +++++++++---------- .../RemovingAnUnknownFileHasNoEffect.php | 13 +++++--- tests/Directory/DirectoryTest.php | 4 +-- 7 files changed, 62 insertions(+), 36 deletions(-) diff --git a/composer.json b/composer.json index 3dbab0b..4c791b4 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,6 @@ "require-dev": { "phpunit/phpunit": "~8.0", "vimeo/psalm": "^3.7", - "innmind/black-box": "^4.5" + "innmind/black-box": "^4.6" } } diff --git a/properties/Directory.php b/properties/Directory.php index 9e29c0d..0e84057 100644 --- a/properties/Directory.php +++ b/properties/Directory.php @@ -6,6 +6,11 @@ use Innmind\BlackBox\{ Set, Property, + PHPUnit\Seeder, +}; +use Fixtures\Innmind\Filesystem\{ + Name, + File, }; final class Directory @@ -13,19 +18,27 @@ final class Directory /** * @return Set */ - public static function properties(): Set + public static function properties(Seeder $seed): Set { return Set\Properties::of( new Directory\MediaTypeIsAlwaysTheSame, new Directory\ContainsMethodAlwaysReturnTrueForFilesInTheDirectory, new Directory\AllFilesInTheDirectoryAreAccessible, - new Directory\AccessingUnknownFileThrowsAnException, - new Directory\RemovingAnUnknownFileHasNoEffect, + new Directory\AccessingUnknownFileThrowsAnException( + $seed(Name::any()), + ), + new Directory\RemovingAnUnknownFileHasNoEffect( + $seed(Name::any()), + ), new Directory\RemoveFile, new Directory\RemoveDirectory, new Directory\ContentHoldsTheNamesOfTheFiles, - new Directory\AddFile, - new Directory\AddDirectory, + new Directory\AddFile( + $seed(File::any()), + ), + new Directory\AddDirectory( + $seed(Name::any()), + ), ); } } diff --git a/properties/Directory/AccessingUnknownFileThrowsAnException.php b/properties/Directory/AccessingUnknownFileThrowsAnException.php index 964bc48..18ead0e 100644 --- a/properties/Directory/AccessingUnknownFileThrowsAnException.php +++ b/properties/Directory/AccessingUnknownFileThrowsAnException.php @@ -12,22 +12,27 @@ final class AccessingUnknownFileThrowsAnException implements Property { - private const UNKNOWN = 'some unknown file name'; + private Name $name; + + public function __construct(Name $name) + { + $this->name = $name; + } public function name(): string { - return 'Accessing an unknown file throw an exception'; + return "Accessing unknown file '{$this->name->toString()}' must throw an exception"; } public function applicableTo(object $directory): bool { - return !$directory->contains(new Name(self::UNKNOWN)); + return !$directory->contains($this->name); } public function ensureHeldBy(object $directory): object { try { - $directory->get(new Name(self::UNKNOWN)); + $directory->get($this->name); Assert::fail('It should throw an exception'); } catch (FileNotFound $e) { diff --git a/properties/Directory/AddDirectory.php b/properties/Directory/AddDirectory.php index 0bc8bd7..9f6222c 100644 --- a/properties/Directory/AddDirectory.php +++ b/properties/Directory/AddDirectory.php @@ -13,22 +13,27 @@ final class AddDirectory implements Property { - private const NAME = 'Some new directory'; + private Name $name; + + public function __construct(Name $name) + { + $this->name = $name; + } public function name(): string { - return 'Add directory'; + return "Add directory '{$this->name->toString()}'"; } public function applicableTo(object $directory): bool { - return !$directory->contains(new Name(self::NAME)); + return !$directory->contains($this->name); } public function ensureHeldBy(object $directory): object { $file = new Directory( - new Name(self::NAME), + $this->name, ); Assert::assertFalse($directory->contains($file->name())); diff --git a/properties/Directory/AddFile.php b/properties/Directory/AddFile.php index 6d31067..78f8241 100644 --- a/properties/Directory/AddFile.php +++ b/properties/Directory/AddFile.php @@ -4,9 +4,7 @@ namespace Properties\Innmind\Filesystem\Directory; use Innmind\Filesystem\{ - File\File, - Name, - Stream\NullStream, + File, Event\FileWasAdded, }; use Innmind\BlackBox\Property; @@ -14,30 +12,30 @@ final class AddFile implements Property { - private const NAME = 'Some new file'; + private File $file; + + public function __construct(File $file) + { + $this->file = $file; + } public function name(): string { - return 'Add file'; + return "Add file '{$this->file->name()->toString()}'"; } public function applicableTo(object $directory): bool { - return !$directory->contains(new Name(self::NAME)); + return !$directory->contains($this->file->name()); } public function ensureHeldBy(object $directory): object { - $file = new File( - new Name(self::NAME), - new NullStream, - ); - - Assert::assertFalse($directory->contains($file->name())); - $newDirectory = $directory->add($file); + Assert::assertFalse($directory->contains($this->file->name())); + $newDirectory = $directory->add($this->file); Assert::assertNotSame($directory, $newDirectory); - Assert::assertFalse($directory->contains($file->name())); - Assert::assertTrue($newDirectory->contains($file->name())); + Assert::assertFalse($directory->contains($this->file->name())); + Assert::assertTrue($newDirectory->contains($this->file->name())); Assert::assertGreaterThan( $directory->modifications()->size(), $newDirectory->modifications()->size(), @@ -47,7 +45,7 @@ public function ensureHeldBy(object $directory): object $newDirectory->modifications()->last(), ); Assert::assertSame( - $file, + $this->file, $newDirectory->modifications()->last()->file(), ); diff --git a/properties/Directory/RemovingAnUnknownFileHasNoEffect.php b/properties/Directory/RemovingAnUnknownFileHasNoEffect.php index 077ee0a..b553ec9 100644 --- a/properties/Directory/RemovingAnUnknownFileHasNoEffect.php +++ b/properties/Directory/RemovingAnUnknownFileHasNoEffect.php @@ -9,23 +9,28 @@ final class RemovingAnUnknownFileHasNoEffect implements Property { - private const UNKNOWN = 'some unknown file name'; + private Name $name; + + public function __construct(Name $name) + { + $this->name = $name; + } public function name(): string { - return 'Removing an unknown file has no effect'; + return "Removing unknown file '{$this->name->toString()}' has no effect"; } public function applicableTo(object $directory): bool { - return !$directory->contains(new Name(self::UNKNOWN)); + return !$directory->contains($this->name); } public function ensureHeldBy(object $directory): object { Assert::assertSame( $directory, - $directory->remove(new Name(self::UNKNOWN)), + $directory->remove($this->name), ); return $directory; diff --git a/tests/Directory/DirectoryTest.php b/tests/Directory/DirectoryTest.php index 51b7368..1d2f284 100644 --- a/tests/Directory/DirectoryTest.php +++ b/tests/Directory/DirectoryTest.php @@ -296,7 +296,7 @@ public function testEmptyDirectoryHoldProperties() { $this ->forAll( - PDirectory::properties(), + PDirectory::properties($this->seeder()), FName::any(), ) ->then(function($properties, $name) { @@ -310,7 +310,7 @@ public function testDirectoryWithSomeFilesHoldProperties() { $this ->forAll( - PDirectory::properties(), + PDirectory::properties($this->seeder()), FName::any(), FSet::of( File::class, From 9905acef628a95cf053f81026057bd119acd1fc5 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 2 May 2020 15:38:15 +0200 Subject: [PATCH 12/62] randomize adapter properties --- properties/Adapter.php | 44 +++++++++++++++---- .../AccessingUnknownFileThrowsAnException.php | 13 ++++-- .../AddDirectoryFromAnotherAdapter.php | 26 ++++++----- ...rectoryFromAnotherAdapterWithFileAdded.php | 39 ++++++++-------- ...ctoryFromAnotherAdapterWithFileRemoved.php | 37 +++++++++------- properties/Adapter/AddEmptyDirectory.php | 13 ++++-- properties/Adapter/AddFile.php | 32 ++++++-------- properties/Adapter/RemoveFile.php | 28 +++++------- properties/Adapter/RemoveUnknownFile.php | 13 ++++-- tests/Adapter/InMemoryTest.php | 2 +- 10 files changed, 145 insertions(+), 102 deletions(-) diff --git a/properties/Adapter.php b/properties/Adapter.php index c48899f..59b3cd0 100644 --- a/properties/Adapter.php +++ b/properties/Adapter.php @@ -6,6 +6,11 @@ use Innmind\BlackBox\{ Set, Property, + PHPUnit\Seeder, +}; +use Fixtures\Innmind\Filesystem\{ + File, + Name, }; final class Adapter @@ -13,18 +18,39 @@ final class Adapter /** * @return Set */ - public static function properties(): Set + public static function properties(Seeder $seed): Set { return Set\Properties::of( - new Adapter\AddFile, - new Adapter\AddEmptyDirectory, - new Adapter\AddDirectoryFromAnotherAdapter, - new Adapter\AddDirectoryFromAnotherAdapterWithFileAdded, - new Adapter\AddDirectoryFromAnotherAdapterWithFileRemoved, - new Adapter\RemoveUnknownFile, - new Adapter\RemoveFile, + new Adapter\AddFile( + $seed(File::any()), + ), + new Adapter\AddEmptyDirectory( + $seed(Name::any()), + ), + new Adapter\AddDirectoryFromAnotherAdapter( + $seed(Name::any()), + $seed(File::any()), + ), + new Adapter\AddDirectoryFromAnotherAdapterWithFileAdded( + $seed(Name::any()), + $seed(File::any()), + $seed(File::any()), + ), + new Adapter\AddDirectoryFromAnotherAdapterWithFileRemoved( + $seed(Name::any()), + $seed(File::any()), + $seed(File::any()), + ), + new Adapter\RemoveUnknownFile( + $seed(Name::any()), + ), + new Adapter\RemoveFile( + $seed(File::any()), + ), new Adapter\AllRootFilesAreAccessible, - new Adapter\AccessingUnknownFileThrowsAnException, + new Adapter\AccessingUnknownFileThrowsAnException( + $seed(Name::any()), + ), ); } } diff --git a/properties/Adapter/AccessingUnknownFileThrowsAnException.php b/properties/Adapter/AccessingUnknownFileThrowsAnException.php index aa9ca00..53e7fad 100644 --- a/properties/Adapter/AccessingUnknownFileThrowsAnException.php +++ b/properties/Adapter/AccessingUnknownFileThrowsAnException.php @@ -12,22 +12,27 @@ final class AccessingUnknownFileThrowsAnException implements Property { - private const NAME = 'Unknown file'; + private Name $name; + + public function __construct(Name $name) + { + $this->name = $name; + } public function name(): string { - return 'Accessing an unknown file throws an exception'; + return "Accessing unknown file '{$this->name->toString()}' must throw an exception"; } public function applicableTo(object $adapter): bool { - return !$adapter->contains(new Name(self::NAME)); + return !$adapter->contains($this->name); } public function ensureHeldBy(object $adapter): object { try { - $adapter->get(new Name(self::NAME)); + $adapter->get($this->name); Assert::fail('It should throw an exception'); } catch (FileNotFound $e) { diff --git a/properties/Adapter/AddDirectoryFromAnotherAdapter.php b/properties/Adapter/AddDirectoryFromAnotherAdapter.php index 11cb4e9..e4025b5 100644 --- a/properties/Adapter/AddDirectoryFromAnotherAdapter.php +++ b/properties/Adapter/AddDirectoryFromAnotherAdapter.php @@ -15,16 +15,23 @@ final class AddDirectoryFromAnotherAdapter implements Property { - private const NAME = 'Some directory from another adapter'; + private Name $name; + private File $file; + + public function __construct(Name $name, File $file) + { + $this->name = $name; + $this->file = $file; + } public function name(): string { - return 'Add directory loaded from another adapter'; + return "Add directory '{$this->name->toString()}' loaded from another adapter"; } public function applicableTo(object $adapter): bool { - return !$adapter->contains(new Name(self::NAME)); + return !$adapter->contains($this->name); } public function ensureHeldBy(object $adapter): object @@ -32,13 +39,10 @@ public function ensureHeldBy(object $adapter): object // directories loaded from other adapters have files injecting at // construct time (so there is no modifications()) $directory = new Directory( - new Name(self::NAME), + $this->name, Set::of( File::class, - new File\File( - new Name('file from other adapter'), - Stream::ofContent('foobar'), - ), + $this->file, ), ); @@ -48,13 +52,13 @@ public function ensureHeldBy(object $adapter): object Assert::assertTrue( $adapter ->get($directory->name()) - ->contains(new Name('file from other adapter')), + ->contains($this->file->name()), ); Assert::assertSame( - 'foobar', + $this->file->content()->toString(), $adapter ->get($directory->name()) - ->get(new Name('file from other adapter')) + ->get($this->file->name()) ->content() ->toString(), ); diff --git a/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileAdded.php b/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileAdded.php index 01e9e1f..f49214f 100644 --- a/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileAdded.php +++ b/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileAdded.php @@ -15,16 +15,25 @@ final class AddDirectoryFromAnotherAdapterWithFileAdded implements Property { - private const NAME = 'Some directory from another adapter with file added'; + private Name $name; + private File $file; + private File $added; + + public function __construct(Name $name, File $file, File $added) + { + $this->name = $name; + $this->file = $file; + $this->added = $added; + } public function name(): string { - return 'Add directory loaded from another adapter with file added'; + return "Add directory '{$this->name->toString()}' loaded from another adapter with file '{$this->added->name()->toString()}' added"; } public function applicableTo(object $adapter): bool { - return !$adapter->contains(new Name(self::NAME)); + return !$adapter->contains($this->name); } public function ensureHeldBy(object $adapter): object @@ -32,19 +41,13 @@ public function ensureHeldBy(object $adapter): object // directories loaded from other adapters have files injecting at // construct time (so there is no modifications()) $directory = new Directory( - new Name(self::NAME), + $this->name, Set::of( File::class, - new File\File( - new Name('file from other adapter'), - Stream::ofContent('foobar'), - ), + $this->file, ), ); - $directory = $directory->add(new File\File( - new Name('file added afterward'), - Stream::ofContent('baz'), - )); + $directory = $directory->add($this->added); Assert::assertFalse($adapter->contains($directory->name())); Assert::assertNull($adapter->add($directory)); @@ -52,26 +55,26 @@ public function ensureHeldBy(object $adapter): object Assert::assertTrue( $adapter ->get($directory->name()) - ->contains(new Name('file from other adapter')), + ->contains($this->file->name()), ); Assert::assertTrue( $adapter ->get($directory->name()) - ->contains(new Name('file added afterward')), + ->contains($this->added->name()), ); Assert::assertSame( - 'foobar', + $this->file->content()->toString(), $adapter ->get($directory->name()) - ->get(new Name('file from other adapter')) + ->get($this->file->name()) ->content() ->toString(), ); Assert::assertSame( - 'baz', + $this->added->content()->toString(), $adapter ->get($directory->name()) - ->get(new Name('file added afterward')) + ->get($this->added->name()) ->content() ->toString(), ); diff --git a/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileRemoved.php b/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileRemoved.php index 16878f2..ef67e71 100644 --- a/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileRemoved.php +++ b/properties/Adapter/AddDirectoryFromAnotherAdapterWithFileRemoved.php @@ -15,16 +15,25 @@ final class AddDirectoryFromAnotherAdapterWithFileRemoved implements Property { - private const NAME = 'Some directory from another adapter with file removed'; + private Name $name; + private File $file; + private File $removed; + + public function __construct(Name $name, File $file, File $removed) + { + $this->name = $name; + $this->file = $file; + $this->removed = $removed; + } public function name(): string { - return 'Add directory loaded from another adapter with file removed'; + return "Add directory '{$this->name->toString()}' loaded from another adapter with file '{$this->removed->name()->toString()}' removed"; } public function applicableTo(object $adapter): bool { - return !$adapter->contains(new Name(self::NAME)); + return !$adapter->contains($this->name); } public function ensureHeldBy(object $adapter): object @@ -32,20 +41,14 @@ public function ensureHeldBy(object $adapter): object // directories loaded from other adapters have files injecting at // construct time (so there is no modifications()) $directory = new Directory( - new Name(self::NAME), + $this->name, Set::of( File::class, - new File\File( - new Name('file removed afterward'), - Stream::ofContent('baz'), - ), - new File\File( - new Name('file from other adapter'), - Stream::ofContent('foobar'), - ), + $this->removed, + $this->file, ), ); - $directory = $directory->remove(new Name('file removed afterward')); + $directory = $directory->remove($this->removed->name()); Assert::assertFalse($adapter->contains($directory->name())); Assert::assertNull($adapter->add($directory)); @@ -53,18 +56,18 @@ public function ensureHeldBy(object $adapter): object Assert::assertTrue( $adapter ->get($directory->name()) - ->contains(new Name('file from other adapter')), + ->contains($this->file->name()), ); Assert::assertFalse( $adapter ->get($directory->name()) - ->contains(new Name('file removed afterward')), + ->contains($this->removed->name()), ); Assert::assertSame( - 'foobar', + $this->file->content()->toString(), $adapter ->get($directory->name()) - ->get(new Name('file from other adapter')) + ->get($this->file->name()) ->content() ->toString(), ); diff --git a/properties/Adapter/AddEmptyDirectory.php b/properties/Adapter/AddEmptyDirectory.php index 6d9415c..f463773 100644 --- a/properties/Adapter/AddEmptyDirectory.php +++ b/properties/Adapter/AddEmptyDirectory.php @@ -12,22 +12,27 @@ final class AddEmptyDirectory implements Property { - private const NAME = 'Some empty directory'; + private Name $name; + + public function __construct(Name $name) + { + $this->name = $name; + } public function name(): string { - return 'Add empty directory'; + return "Add empty directory '{$this->name->toString()}'"; } public function applicableTo(object $adapter): bool { - return !$adapter->contains(new Name(self::NAME)); + return !$adapter->contains($this->name); } public function ensureHeldBy(object $adapter): object { $directory = new Directory( - new Name(self::NAME), + $this->name, ); Assert::assertFalse($adapter->contains($directory->name())); diff --git a/properties/Adapter/AddFile.php b/properties/Adapter/AddFile.php index 11164cf..58f8723 100644 --- a/properties/Adapter/AddFile.php +++ b/properties/Adapter/AddFile.php @@ -3,42 +3,38 @@ namespace Properties\Innmind\Filesystem\Adapter; -use Innmind\Filesystem\{ - File\File, - Name, -}; -use Innmind\Stream\Readable\Stream; +use Innmind\Filesystem\File; use Innmind\BlackBox\Property; use PHPUnit\Framework\Assert; final class AddFile implements Property { - private const NAME = 'Some new file'; + private File $file; + + public function __construct(File $file) + { + $this->file = $file; + } public function name(): string { - return 'Add file'; + return "Add file '{$this->file->name()->toString()}'"; } public function applicableTo(object $adapter): bool { - return !$adapter->contains(new Name(self::NAME)); + return !$adapter->contains($this->file->name()); } public function ensureHeldBy(object $adapter): object { - $file = new File( - new Name(self::NAME), - Stream::ofContent('foo'), - ); - - Assert::assertFalse($adapter->contains($file->name())); - Assert::assertNull($adapter->add($file)); - Assert::assertTrue($adapter->contains($file->name())); + Assert::assertFalse($adapter->contains($this->file->name())); + Assert::assertNull($adapter->add($this->file)); + Assert::assertTrue($adapter->contains($this->file->name())); Assert::assertSame( - 'foo', + $this->file->content()->toString(), $adapter - ->get($file->name()) + ->get($this->file->name()) ->content() ->toString(), ); diff --git a/properties/Adapter/RemoveFile.php b/properties/Adapter/RemoveFile.php index 4c5ca0b..b9151d7 100644 --- a/properties/Adapter/RemoveFile.php +++ b/properties/Adapter/RemoveFile.php @@ -3,21 +3,22 @@ namespace Properties\Innmind\Filesystem\Adapter; -use Innmind\Filesystem\{ - File\File, - Name, -}; -use Innmind\Stream\Readable\Stream; +use Innmind\Filesystem\File; use Innmind\BlackBox\Property; use PHPUnit\Framework\Assert; final class RemoveFile implements Property { - private const NAME = 'Some file to be removed'; + private File $file; + + public function __construct(File $file) + { + $this->file = $file; + } public function name(): string { - return 'Remove file'; + return "Remove file '{$this->file->name()->toString()}'"; } public function applicableTo(object $adapter): bool @@ -27,15 +28,10 @@ public function applicableTo(object $adapter): bool public function ensureHeldBy(object $adapter): object { - $file = new File( - new Name(self::NAME), - Stream::ofContent('foo'), - ); - - Assert::assertNull($adapter->add($file)); - Assert::assertTrue($adapter->contains($file->name())); - Assert::assertNull($adapter->remove($file->name())); - Assert::assertFalse($adapter->contains($file->name())); + Assert::assertNull($adapter->add($this->file)); + Assert::assertTrue($adapter->contains($this->file->name())); + Assert::assertNull($adapter->remove($this->file->name())); + Assert::assertFalse($adapter->contains($this->file->name())); return $adapter; } diff --git a/properties/Adapter/RemoveUnknownFile.php b/properties/Adapter/RemoveUnknownFile.php index 2e77900..becd5eb 100644 --- a/properties/Adapter/RemoveUnknownFile.php +++ b/properties/Adapter/RemoveUnknownFile.php @@ -9,11 +9,16 @@ final class RemoveUnknownFile implements Property { - private const NAME = 'Unknown file'; + private Name $name; + + public function __construct(Name $name) + { + $this->name = $name; + } public function name(): string { - return 'Remove unknown file'; + return "Remove unknown file '{$this->name->toString()}'"; } public function applicableTo(object $adapter): bool @@ -23,8 +28,8 @@ public function applicableTo(object $adapter): bool public function ensureHeldBy(object $adapter): object { - Assert::assertNull($adapter->remove(new Name(self::NAME))); - Assert::assertFalse($adapter->contains(new Name(self::NAME))); + Assert::assertNull($adapter->remove($this->name)); + Assert::assertFalse($adapter->contains($this->name)); return $adapter; } diff --git a/tests/Adapter/InMemoryTest.php b/tests/Adapter/InMemoryTest.php index c78d541..97c5bf4 100644 --- a/tests/Adapter/InMemoryTest.php +++ b/tests/Adapter/InMemoryTest.php @@ -77,7 +77,7 @@ public function testAll() public function testHoldProperties() { $this - ->forAll(PAdapter::properties()) + ->forAll(PAdapter::properties($this->seeder())) ->then(function($properties) { $properties->ensureHeldBy(new InMemory); }); From 9f2f1df50058fa32e0563952e6472bc8a048266a Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 2 May 2020 16:48:31 +0200 Subject: [PATCH 13/62] add property that any random directoy structure is persisted correctly --- fixtures/Directory.php | 102 ++++++++++++++++++++++++++++ properties/Adapter.php | 4 ++ properties/Adapter/AddDirectory.php | 67 ++++++++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 fixtures/Directory.php create mode 100644 properties/Adapter/AddDirectory.php diff --git a/fixtures/Directory.php b/fixtures/Directory.php new file mode 100644 index 0000000..5265236 --- /dev/null +++ b/fixtures/Directory.php @@ -0,0 +1,102 @@ + + */ + public static function any(): DataSet + { + return self::atDepth(0); + } + + private static function atDepth(int $depth): DataSet + { + if ($depth === 3) { + $files = Set::of( + FileInterface::class, + new DataSet\Randomize( + File::any(), + ), + DataSet\Integers::between(0, 10), + ); + $toAdd = DataSet\Sequence::of( + new DataSet\Randomize( + File::any(), + ), + DataSet\Integers::between(0, 10), + ); + } else { + $files = Set::of( + FileInterface::class, + new DataSet\Either( + new DataSet\Randomize( + File::any(), + ), + self::atDepth($depth + 1), + ), + DataSet\Integers::between(0, 10), + )->filter(static function($files): bool { + if ($files->empty()) { + return true; + } + + // do not accept duplicated files + return $files + ->groupBy(static fn($file) => $file->name()->toString()) + ->size() === $files->size(); + }); + $toAdd = DataSet\Sequence::of( + new DataSet\Either( + new DataSet\Randomize( + File::any(), + ), + self::atDepth($depth + 1), + ), + DataSet\Integers::between(0, 10), + ); + } + + return DataSet\Composite::immutable( + static function($name, $files, $toAdd, $numberToRemove): Model { + $directory = new Model( + $name, + $files, + ); + + foreach ($toAdd as $file) { + $directory = $directory->add($file); + } + + $files = \array_merge( + unwrap($files), + $toAdd, + ); + $toRemove = \array_slice($files, 0, $numberToRemove); + + foreach ($toRemove as $file) { + $directory = $directory->remove($file->name()); + } + + return $directory; + }, + Name::any(), + $files, + $toAdd, + DataSet\Integers::between(0, 20), + ); + } +} diff --git a/properties/Adapter.php b/properties/Adapter.php index 59b3cd0..77e7132 100644 --- a/properties/Adapter.php +++ b/properties/Adapter.php @@ -11,6 +11,7 @@ use Fixtures\Innmind\Filesystem\{ File, Name, + Directory, }; final class Adapter @@ -51,6 +52,9 @@ public static function properties(Seeder $seed): Set new Adapter\AccessingUnknownFileThrowsAnException( $seed(Name::any()), ), + new Adapter\AddDirectory( + $seed(Directory::any()), + ), ); } } diff --git a/properties/Adapter/AddDirectory.php b/properties/Adapter/AddDirectory.php new file mode 100644 index 0000000..15942d1 --- /dev/null +++ b/properties/Adapter/AddDirectory.php @@ -0,0 +1,67 @@ +directory = $directory; + } + + public function name(): string + { + return "Add directory '{$this->directory->name()->toString()}'"; + } + + public function applicableTo(object $adapter): bool + { + return !$adapter->contains($this->directory->name()); + } + + public function ensureHeldBy(object $adapter): object + { + Assert::assertFalse($adapter->contains($this->directory->name())); + Assert::assertNull($adapter->add($this->directory)); + Assert::assertTrue($adapter->contains($this->directory->name())); + $this->assertSame( + $this->directory, + $adapter->get($this->directory->name()), + ); + + return $adapter; + } + + private function assertSame(File $source, File $target): void + { + Assert::assertSame( + $source->name()->toString(), + $target->name()->toString(), + ); + Assert::assertSame( + $source->content()->toString(), + $target->content()->toString(), + ); + + if ($target instanceof Directory) { + $target->foreach(function($file) use ($source) { + Assert::assertTrue($source->contains($file->name())); + + $this->assertSame( + $source->get($file->name()), + $file, + ); + }); + } + } +} From e0566a8ec95aa840a5afb0e3d9ccc4abb5ad6316 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 2 May 2020 17:01:33 +0200 Subject: [PATCH 14/62] randomly seek stream to simulate user reading files --- fixtures/File.php | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/fixtures/File.php b/fixtures/File.php index ff7a355..53a87da 100644 --- a/fixtures/File.php +++ b/fixtures/File.php @@ -5,7 +5,10 @@ use Innmind\Filesystem\File\File as Model; use Innmind\BlackBox\Set; -use Innmind\Stream\Readable\Stream; +use Innmind\Stream\{ + Readable\Stream, + Stream\Position, +}; use Fixtures\Innmind\MediaType\MediaType; final class File @@ -13,14 +16,24 @@ final class File public static function any(): Set { return Set\Composite::immutable( - static fn($name, $content, $mediaType): Model => new Model( - $name, - Stream::ofContent($content), - $mediaType, - ), + static function($name, $content, $mediaType, $seek): Model { + // as the generated seeked position may be higher than the actual + // content size + $seek = \min(\strlen($content), $seek); + + $file = new Model( + $name, + $stream = Stream::ofContent($content), + $mediaType, + ); + $stream->seek(new Position($seek)); + + return $file; + }, Name::any(), Set\Strings::any(), MediaType::any(), + Set\Integers::between(0, 128), // 128 is the max string length by default ); } } From 6a04de92cf588f9cdb2bd92e0e328816880737b5 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 2 May 2020 17:05:15 +0200 Subject: [PATCH 15/62] test lazy adapter hold all properties --- tests/Adapter/LazyTest.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Adapter/LazyTest.php b/tests/Adapter/LazyTest.php index 0548515..7deb230 100644 --- a/tests/Adapter/LazyTest.php +++ b/tests/Adapter/LazyTest.php @@ -17,9 +17,13 @@ use Innmind\Immutable\Set; use function Innmind\Immutable\unwrap; use PHPUnit\Framework\TestCase; +use Innmind\BlackBox\PHPUnit\BlackBox; +use Properties\Innmind\Filesystem\Adapter; class LazyTest extends TestCase { + use BlackBox; + public function testInterface() { $l = new Lazy($a = new InMemory); @@ -110,4 +114,13 @@ public function testGetFileAddedButNotYetPersisted() $this->assertNull($filesystem->add($file)); $this->assertSame($file, $filesystem->get($file->name())); } + + public function testHoldProperties() + { + $this + ->forAll(Adapter::properties($this->seeder())) + ->then(function($properties) { + $properties->ensureHeldBy(new Lazy(new InMemory)); + }); + } } From 5788371ab0bfb374d08d0010bd8f08d6da3e5092 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 2 May 2020 17:08:29 +0200 Subject: [PATCH 16/62] test cache opened files adapter hold all properties --- tests/Adapter/CacheOpenedFilesTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/Adapter/CacheOpenedFilesTest.php b/tests/Adapter/CacheOpenedFilesTest.php index a1ac7d1..8640d8d 100644 --- a/tests/Adapter/CacheOpenedFilesTest.php +++ b/tests/Adapter/CacheOpenedFilesTest.php @@ -5,6 +5,7 @@ use Innmind\Filesystem\{ Adapter\CacheOpenedFiles, + Adapter\InMemory, Adapter, File, Name, @@ -12,9 +13,13 @@ use Innmind\Stream\Readable; use Innmind\Immutable\Set; use PHPUnit\Framework\TestCase; +use Innmind\BlackBox\PHPUnit\BlackBox; +use Properties\Innmind\Filesystem\Adapter as PAdapter; class CacheOpenedFilesTest extends TestCase { + use BlackBox; + public function testInterface() { $this->assertInstanceOf( @@ -139,4 +144,13 @@ public function testAll() $this->assertSame($expected, $filesystem->all()); $this->assertSame($file, $filesystem->get(new Name('foo'))); } + + public function testHoldProperties() + { + $this + ->forAll(PAdapter::properties($this->seeder())) + ->then(function($properties) { + $properties->ensureHeldBy(new CacheOpenedFiles(new InMemory)); + }); + } } From 72aa505a9c7867a76db5b66b9acf855cd76a70ff Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 2 May 2020 17:18:50 +0200 Subject: [PATCH 17/62] make stream seeking optional to simulate unopened files --- fixtures/File.php | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/fixtures/File.php b/fixtures/File.php index 53a87da..3a8473b 100644 --- a/fixtures/File.php +++ b/fixtures/File.php @@ -17,23 +17,29 @@ public static function any(): Set { return Set\Composite::immutable( static function($name, $content, $mediaType, $seek): Model { - // as the generated seeked position may be higher than the actual - // content size - $seek = \min(\strlen($content), $seek); - $file = new Model( $name, $stream = Stream::ofContent($content), $mediaType, ); - $stream->seek(new Position($seek)); + + if (\is_int($seek)) { + // as the generated seeked position may be higher than the + // actual content size + $seek = \min(\strlen($content), $seek); + + $stream->seek(new Position($seek)); + } return $file; }, Name::any(), Set\Strings::any(), MediaType::any(), - Set\Integers::between(0, 128), // 128 is the max string length by default + new Set\Either( + Set\Integers::between(0, 128), // 128 is the max string length by default + Set\Elements::of(null), + ), ); } } From 77b5b025801e0a6fa25c70356a89b50bf569a731 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 3 May 2020 12:27:45 +0200 Subject: [PATCH 18/62] fix filesystem adapter not persisting correctly directories loaded from another adapter --- fixtures/Directory.php | 18 +- properties/Directory.php | 4 + .../AddFileMustUnwrapSourceDecorator.php | 39 +++++ .../RemoveFileMustUnwrapSourceDecorator.php | 41 +++++ src/Adapter/Filesystem.php | 83 ++++----- src/Directory/Directory.php | 3 +- src/Directory/Source.php | 106 ++++++++++++ src/File/Source.php | 52 ++++++ src/Source.php | 17 ++ tests/Adapter/FilesystemTest.php | 21 ++- tests/Directory/SourceTest.php | 124 +++++++++++++ tests/File/SourceTest.php | 163 ++++++++++++++++++ 12 files changed, 611 insertions(+), 60 deletions(-) create mode 100644 properties/Directory/AddFileMustUnwrapSourceDecorator.php create mode 100644 properties/Directory/RemoveFileMustUnwrapSourceDecorator.php create mode 100644 src/Directory/Source.php create mode 100644 src/File/Source.php create mode 100644 src/Source.php create mode 100644 tests/Directory/SourceTest.php create mode 100644 tests/File/SourceTest.php diff --git a/fixtures/Directory.php b/fixtures/Directory.php index 5265236..0aa28f8 100644 --- a/fixtures/Directory.php +++ b/fixtures/Directory.php @@ -20,12 +20,20 @@ final class Directory */ public static function any(): DataSet { - return self::atDepth(0); + return self::atDepth(0, 3); } - private static function atDepth(int $depth): DataSet + /** + * @return DataSet + */ + public static function maxDepth(int $depth): DataSet + { + return self::atDepth(0, $depth); + } + + private static function atDepth(int $depth, int $maxDepth): DataSet { - if ($depth === 3) { + if ($depth === $maxDepth) { $files = Set::of( FileInterface::class, new DataSet\Randomize( @@ -46,7 +54,7 @@ private static function atDepth(int $depth): DataSet new DataSet\Randomize( File::any(), ), - self::atDepth($depth + 1), + self::atDepth($depth + 1, $maxDepth), ), DataSet\Integers::between(0, 10), )->filter(static function($files): bool { @@ -64,7 +72,7 @@ private static function atDepth(int $depth): DataSet new DataSet\Randomize( File::any(), ), - self::atDepth($depth + 1), + self::atDepth($depth + 1, $maxDepth), ), DataSet\Integers::between(0, 10), ); diff --git a/properties/Directory.php b/properties/Directory.php index 0e84057..2c97a29 100644 --- a/properties/Directory.php +++ b/properties/Directory.php @@ -31,11 +31,15 @@ public static function properties(Seeder $seed): Set $seed(Name::any()), ), new Directory\RemoveFile, + new Directory\RemoveFileMustUnwrapSourceDecorator, new Directory\RemoveDirectory, new Directory\ContentHoldsTheNamesOfTheFiles, new Directory\AddFile( $seed(File::any()), ), + new Directory\AddFileMustUnwrapSourceDecorator( + $seed(File::any()), + ), new Directory\AddDirectory( $seed(Name::any()), ), diff --git a/properties/Directory/AddFileMustUnwrapSourceDecorator.php b/properties/Directory/AddFileMustUnwrapSourceDecorator.php new file mode 100644 index 0000000..bce0a68 --- /dev/null +++ b/properties/Directory/AddFileMustUnwrapSourceDecorator.php @@ -0,0 +1,39 @@ +file = $file; + } + + public function name(): string + { + return "Add file '{$this->file->name()->toString()}' must unwrap source decorator"; + } + + public function applicableTo(object $directory): bool + { + return $directory instanceof Source; + } + + public function ensureHeldBy(object $directory): object + { + $newDirectory = $directory->add($this->file); + Assert::assertNotInstanceOf(Source::class, $newDirectory); + + return $newDirectory; + } +} diff --git a/properties/Directory/RemoveFileMustUnwrapSourceDecorator.php b/properties/Directory/RemoveFileMustUnwrapSourceDecorator.php new file mode 100644 index 0000000..45840d5 --- /dev/null +++ b/properties/Directory/RemoveFileMustUnwrapSourceDecorator.php @@ -0,0 +1,41 @@ +reduce( + false, + fn() => true, + ); + } + + public function ensureHeldBy(object $directory): object + { + $file = $directory->reduce( + null, + fn($found, $file) => $found ?? $file, + ); + + $newDirectory = $directory->remove($file->name()); + Assert::assertNotInstanceOf(Source::class, $newDirectory); + + return $newDirectory; + } +} diff --git a/src/Adapter/Filesystem.php b/src/Adapter/Filesystem.php index 68a56be..24f5ab8 100644 --- a/src/Adapter/Filesystem.php +++ b/src/Adapter/Filesystem.php @@ -9,9 +9,9 @@ Name, Directory, Stream\LazyStream, + Stream\Source, Exception\FileNotFound, Exception\PathDoesntRepresentADirectory, - Event\FileWasAdded, Event\FileWasRemoved, }; use Innmind\MediaType\{ @@ -19,10 +19,7 @@ Exception\InvalidMediaTypeString, }; use Innmind\Url\Path; -use Innmind\Immutable\{ - Map, - Set, -}; +use Innmind\Immutable\Set; use Symfony\Component\{ Filesystem\Filesystem as FS, Finder\Finder, @@ -34,8 +31,6 @@ final class Filesystem implements Adapter private const INVALID_FILES = ['.', '..']; private Path $path; private FS $filesystem; - private Map $files; - private Set $handledEvents; public function __construct(Path $path) { @@ -45,8 +40,6 @@ public function __construct(Path $path) $this->path = $path; $this->filesystem = new FS; - $this->files = Map::of('string', File::class); - $this->handledEvents = Set::objects(); if (!$this->filesystem->exists($this->path->toString())) { $this->filesystem->mkdir($this->path->toString()); @@ -117,48 +110,28 @@ public function all(): Set */ private function createFileAt(Path $path, File $file): void { - if ($file instanceof Directory) { - $folder = $path->resolve(Path::of($file->name()->toString().'/')); + $name = $file->name()->toString(); - if ( - $this->files->contains($folder->toString()) && - $this->files->get($folder->toString()) === $file - ) { - return; - } - - $this->filesystem->mkdir($folder->toString()); - $file - ->modifications() - ->foreach(function(object $event) use ($folder) { - if ($this->handledEvents->contains($event)) { - return; - } - - switch (true) { - case $event instanceof FileWasRemoved: - $this - ->filesystem - ->remove($folder->toString().$event->file()->toString()); - break; - case $event instanceof FileWasAdded: - $this->createFileAt($folder, $event->file()); - break; - } + if ($file instanceof Directory) { + $name .= '/'; + } - $this->handledEvents = ($this->handledEvents)($event); - }); - $this->files = ($this->files)($folder->toString(), $file); + $path = $path->resolve(Path::of($name)); + if ($file instanceof Source && !$file->shouldPersistAt($this, $path)) { return; } - $path = $path->resolve(Path::of($file->name()->toString())); + if ($file instanceof Directory) { + $this->filesystem->mkdir($path->toString()); + $file->foreach(fn(File $file) => $this->createFileAt($path, $file)); + $file + ->modifications() + ->filter(static fn(object $event): bool => $event instanceof FileWasRemoved) + ->foreach(fn(FileWasRemoved $event) => $this->filesystem->remove( + $path->toString().$event->file()->toString(), + )); - if ( - $this->files->contains($path->toString()) && - $this->files->get($path->toString()) === $file - ) { return; } @@ -169,8 +142,6 @@ private function createFileAt(Path $path, File $file): void while (!$stream->end()) { \fwrite($handle, $stream->read(8192)->toString()); } - - $this->files = ($this->files)($path->toString(), $file); } /** @@ -196,7 +167,11 @@ private function open(Path $folder, Name $file): File \closedir($handle); })($folder->resolve(Path::of($file->toString().'/')))); - $object = new Directory\Directory($file, $files); + $object = new Directory\Source( + new Directory\Directory($file, $files), + $this, + $path, + ); } else { try { $mediaType = MediaType::of(\mime_content_type($path->toString())); @@ -204,15 +179,17 @@ private function open(Path $folder, Name $file): File $mediaType = MediaType::null(); } - $object = new File\File( - $file, - new LazyStream($path), - $mediaType, + $object = new File\Source( + new File\File( + $file, + new LazyStream($path), + $mediaType, + ), + $this, + $path, ); } - $this->files = ($this->files)($path->toString(), $object); - return $object; } } diff --git a/src/Directory/Directory.php b/src/Directory/Directory.php index d20eceb..22c678b 100644 --- a/src/Directory/Directory.php +++ b/src/Directory/Directory.php @@ -93,7 +93,8 @@ public function content(): Readable /** @var Set $names */ $names = $this ->files - ->toSetOf('string', fn($file): \Generator => yield $file->name()->toString()); + ->toSetOf('string', fn($file): \Generator => yield $file->name()->toString()) + ->sort(static fn(string $a, string $b): int => $a <=> $b); $this->content = Readable\Stream::ofContent( join("\n", $names)->toString(), ); diff --git a/src/Directory/Source.php b/src/Directory/Source.php new file mode 100644 index 0000000..832a8e2 --- /dev/null +++ b/src/Directory/Source.php @@ -0,0 +1,106 @@ +directory = $directory; + $this->openedBy = $openedBy; + $this->path = $path; + } + + public function shouldPersistAt(Adapter $target, Path $path): bool + { + return $this->openedBy !== $target || + !$this->path->equals($path); + } + + public function name(): Name + { + return $this->directory->name(); + } + + public function content(): Readable + { + return $this->directory->content(); + } + + public function mediaType(): MediaType + { + return $this->directory->mediaType(); + } + + public function add(File $file): Directory + { + return $this->directory->add($file); + } + + public function get(Name $name): File + { + return $this->directory->get($name); + } + + public function contains(Name $name): bool + { + return $this->directory->contains($name); + } + + public function remove(Name $name): Directory + { + if (!$this->contains($name)) { + return $this; + } + + return $this->directory->remove($name); + } + + public function replaceAt(Path $path, File $file): Directory + { + return $this->directory->replaceAt($path, $file); + } + + public function foreach(callable $function): void + { + $this->directory->foreach($function); + } + + public function filter(callable $predicate): Set + { + return $this->directory->filter($predicate); + } + + public function reduce($carry, callable $reducer) + { + return $this->directory->reduce($carry, $reducer); + } + + public function modifications(): Sequence + { + return $this->directory->modifications(); + } +} diff --git a/src/File/Source.php b/src/File/Source.php new file mode 100644 index 0000000..91724ce --- /dev/null +++ b/src/File/Source.php @@ -0,0 +1,52 @@ +file = $file; + $this->openedBy = $openedBy; + $this->path = $path; + } + + public function shouldPersistAt(Adapter $target, Path $path): bool + { + return $this->openedBy !== $target || + !$this->path->equals($path); + } + + public function name(): Name + { + return $this->file->name(); + } + + public function content(): Readable + { + return $this->file->content(); + } + + public function mediaType(): MediaType + { + return $this->file->mediaType(); + } +} diff --git a/src/Source.php b/src/Source.php new file mode 100644 index 0000000..3a33bec --- /dev/null +++ b/src/Source.php @@ -0,0 +1,17 @@ +assertTrue($all->contains('baz')); $this->assertSame('foo', $adapter->get(new Name('foo'))->content()->toString()); $this->assertSame('bar', $adapter->get(new Name('bar'))->content()->toString()); - $this->assertInstanceOf(Directory::class, $adapter->get(new Name('baz'))); + $this->assertInstanceOf(DirectoryInterface::class, $adapter->get(new Name('baz'))); $adapter->remove(new Name('foo')); $adapter->remove(new Name('bar')); $adapter->remove(new Name('baz')); @@ -210,4 +215,18 @@ public function testAddingTheSameFileTwiceDoesNothing() $this->assertNull($adapter->add($file)); $this->assertNull($adapter->add($file)); } + + public function testHoldProperties() + { + $this + ->forAll(PAdapter::properties($this->seeder())) + ->then(function($properties) { + $path = \sys_get_temp_dir().'/innmind/filesystem/'; + (new FS)->remove($path); + + $properties->ensureHeldBy(new Filesystem(Path::of($path))); + + (new FS)->remove($path); + }); + } } diff --git a/tests/Directory/SourceTest.php b/tests/Directory/SourceTest.php new file mode 100644 index 0000000..dc9d305 --- /dev/null +++ b/tests/Directory/SourceTest.php @@ -0,0 +1,124 @@ +assertInstanceOf( + SourceInterface::class, + new Source( + $this->createMock(DirectoryInterface::class), + $this->createMock(Adapter::class), + $this->seeder()(Path::any()), + ), + ); + $this->assertInstanceOf( + DirectoryInterface::class, + new Source( + $this->createMock(DirectoryInterface::class), + $this->createMock(Adapter::class), + $this->seeder()(Path::any()), + ), + ); + } + + public function testAnyFileSourceFromAnotherShouldBePersisted() + { + $this + ->forAll(Path::any()) + ->then(function($path) { + $source = new Source( + $this->createMock(DirectoryInterface::class), + $this->createMock(Adapter::class), + $path, + ); + + $this->assertTrue($source->shouldPersistAt( + $this->createMock(Adapter::class), + $path, + )); + }); + } + + public function testFileOpenedInADifferentPathThanTheTargetPathShouldBePersisted() + { + $this + ->forAll( + Path::any(), + Path::any(), + ) + ->filter(function($openedAt, $writeAt) { + return !$openedAt->equals($writeAt); + }) + ->then(function($openedAt, $writeAt) { + $source = new Source( + $this->createMock(DirectoryInterface::class), + $adapter = $this->createMock(Adapter::class), + $openedAt, + ); + + $this->assertTrue($source->shouldPersistAt( + $adapter, + $writeAt, + )); + }); + } + + public function testShouldNotPersistAFileWhereItWasOpenedInTheSameAdapter() + { + $this + ->forAll(Path::any()) + ->then(function($path) { + $source = new Source( + $this->createMock(DirectoryInterface::class), + $adapter = $this->createMock(Adapter::class), + $path, + ); + + $this->assertFalse($source->shouldPersistAt( + $adapter, + $path, + )); + }); + } + + public function testHoldProperties() + { + $this + ->forAll( + PDirectory::properties($this->seeder()), + Directory::maxDepth(1), + Path::any(), + ) + ->then(function($properties, $inner, $path) { + $source = new Source( + $inner, + $this->createMock(Adapter::class), + $path, + ); + + $properties->ensureHeldBy($source); + }); + } +} diff --git a/tests/File/SourceTest.php b/tests/File/SourceTest.php new file mode 100644 index 0000000..d620283 --- /dev/null +++ b/tests/File/SourceTest.php @@ -0,0 +1,163 @@ +assertInstanceOf( + SourceInterface::class, + new Source( + $this->createMock(File::class), + $this->createMock(Adapter::class), + $this->seeder()(Path::any()), + ), + ); + } + + public function testAnyFileSourceFromAnotherShouldBePersisted() + { + $this + ->forAll(Path::any()) + ->then(function($path) { + $source = new Source( + $this->createMock(File::class), + $this->createMock(Adapter::class), + $path, + ); + + $this->assertTrue($source->shouldPersistAt( + $this->createMock(Adapter::class), + $path, + )); + }); + } + + public function testFileOpenedInADifferentPathThanTheTargetPathShouldBePersisted() + { + $this + ->forAll( + Path::any(), + Path::any(), + ) + ->filter(function($openedAt, $writeAt) { + return !$openedAt->equals($writeAt); + }) + ->then(function($openedAt, $writeAt) { + $source = new Source( + $this->createMock(File::class), + $adapter = $this->createMock(Adapter::class), + $openedAt, + ); + + $this->assertTrue($source->shouldPersistAt( + $adapter, + $writeAt, + )); + }); + } + + public function testShouldNotPersistAFileWhereItWasOpenedInTheSameAdapter() + { + $this + ->forAll(Path::any()) + ->then(function($path) { + $source = new Source( + $this->createMock(File::class), + $adapter = $this->createMock(Adapter::class), + $path, + ); + + $this->assertFalse($source->shouldPersistAt( + $adapter, + $path, + )); + }); + } + + public function testName() + { + $this + ->forAll( + Name::any(), + Path::any(), + ) + ->then(function($name, $path) { + $source = new Source( + $inner = $this->createMock(File::class), + $this->createMock(Adapter::class), + $path, + ); + $inner + ->expects($this->once()) + ->method('name') + ->willReturn($name); + + $this->assertSame($name, $source->name()); + }); + } + + public function testContent() + { + $this + ->forAll( + Set\Strings::any(), + Path::any(), + ) + ->then(function($content, $path) { + $source = new Source( + $inner = $this->createMock(File::class), + $this->createMock(Adapter::class), + $path, + ); + $inner + ->expects($this->once()) + ->method('content') + ->willReturn($expected = Stream::ofContent($content)); + + $this->assertSame($expected, $source->content()); + }); + } + + public function testMediaType() + { + $this + ->forAll( + MediaType::any(), + Path::any(), + ) + ->then(function($mediaType, $path) { + $source = new Source( + $inner = $this->createMock(File::class), + $this->createMock(Adapter::class), + $path, + ); + $inner + ->expects($this->once()) + ->method('mediaType') + ->willReturn($mediaType); + + $this->assertSame($mediaType, $source->mediaType()); + }); + } +} From ca5f4b170f3a9746028fb493d8db95defee378ae Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 3 May 2020 12:32:39 +0200 Subject: [PATCH 19/62] CS --- src/Adapter/Filesystem.php | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/Adapter/Filesystem.php b/src/Adapter/Filesystem.php index 24f5ab8..db38fb7 100644 --- a/src/Adapter/Filesystem.php +++ b/src/Adapter/Filesystem.php @@ -167,29 +167,27 @@ private function open(Path $folder, Name $file): File \closedir($handle); })($folder->resolve(Path::of($file->toString().'/')))); - $object = new Directory\Source( + return new Directory\Source( new Directory\Directory($file, $files), $this, $path, ); - } else { - try { - $mediaType = MediaType::of(\mime_content_type($path->toString())); - } catch (InvalidMediaTypeString $e) { - $mediaType = MediaType::null(); - } - - $object = new File\Source( - new File\File( - $file, - new LazyStream($path), - $mediaType, - ), - $this, - $path, - ); } - return $object; + try { + $mediaType = MediaType::of(\mime_content_type($path->toString())); + } catch (InvalidMediaTypeString $e) { + $mediaType = MediaType::null(); + } + + return new File\Source( + new File\File( + $file, + new LazyStream($path), + $mediaType, + ), + $this, + $path, + ); } } From 9e60c8fdb15197acc98242aaa7e0bcd865f8d189 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 3 May 2020 14:49:40 +0200 Subject: [PATCH 20/62] prevent unwanted removals --- properties/Adapter.php | 8 +++ ...dRemoveAddModificationsStillAddTheFile.php | 51 +++++++++++++++++++ ...AddRemoveModificationsDoesntAddTheFile.php | 51 +++++++++++++++++++ src/Adapter/Filesystem.php | 34 ++++++++++--- 4 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 properties/Adapter/AddRemoveAddModificationsStillAddTheFile.php create mode 100644 properties/Adapter/RemoveAddRemoveModificationsDoesntAddTheFile.php diff --git a/properties/Adapter.php b/properties/Adapter.php index 77e7132..16a9900 100644 --- a/properties/Adapter.php +++ b/properties/Adapter.php @@ -55,6 +55,14 @@ public static function properties(Seeder $seed): Set new Adapter\AddDirectory( $seed(Directory::any()), ), + new Adapter\AddRemoveAddModificationsStillAddTheFile( + $seed(Directory::any()), + $seed(File::any()), + ), + new Adapter\RemoveAddRemoveModificationsDoesntAddTheFile( + $seed(Directory::any()), + $seed(File::any()), + ), ); } } diff --git a/properties/Adapter/AddRemoveAddModificationsStillAddTheFile.php b/properties/Adapter/AddRemoveAddModificationsStillAddTheFile.php new file mode 100644 index 0000000..d64e564 --- /dev/null +++ b/properties/Adapter/AddRemoveAddModificationsStillAddTheFile.php @@ -0,0 +1,51 @@ +directory = $directory; + $this->file = $file; + } + + public function name(): string + { + return "Add/remove/add still add the file '{$this->file->name()->toString()}'"; + } + + public function applicableTo(object $adapter): bool + { + return !$adapter->contains($this->directory->name()); + } + + public function ensureHeldBy(object $adapter): object + { + $adapter->add( + $this + ->directory + ->add($this->file) + ->remove($this->file->name()) + ->add($this->file), + ); + Assert::assertTrue( + $adapter + ->get($this->directory->name()) + ->contains($this->file->name()), + ); + + return $adapter; + } +} diff --git a/properties/Adapter/RemoveAddRemoveModificationsDoesntAddTheFile.php b/properties/Adapter/RemoveAddRemoveModificationsDoesntAddTheFile.php new file mode 100644 index 0000000..8e96ae7 --- /dev/null +++ b/properties/Adapter/RemoveAddRemoveModificationsDoesntAddTheFile.php @@ -0,0 +1,51 @@ +directory = $directory; + $this->file = $file; + } + + public function name(): string + { + return "Add/remove/add still add the file '{$this->file->name()->toString()}'"; + } + + public function applicableTo(object $adapter): bool + { + return !$adapter->contains($this->directory->name()); + } + + public function ensureHeldBy(object $adapter): object + { + $adapter->add( + $this + ->directory + ->remove($this->file->name()) + ->add($this->file) + ->remove($this->file->name()), + ); + Assert::assertFalse( + $adapter + ->get($this->directory->name()) + ->contains($this->file->name()), + ); + + return $adapter; + } +} diff --git a/src/Adapter/Filesystem.php b/src/Adapter/Filesystem.php index db38fb7..b906d78 100644 --- a/src/Adapter/Filesystem.php +++ b/src/Adapter/Filesystem.php @@ -13,6 +13,7 @@ Exception\FileNotFound, Exception\PathDoesntRepresentADirectory, Event\FileWasRemoved, + Event\FileWasAdded, }; use Innmind\MediaType\{ MediaType, @@ -124,13 +125,34 @@ private function createFileAt(Path $path, File $file): void if ($file instanceof Directory) { $this->filesystem->mkdir($path->toString()); - $file->foreach(fn(File $file) => $this->createFileAt($path, $file)); - $file + $alreadyAdded = $file ->modifications() - ->filter(static fn(object $event): bool => $event instanceof FileWasRemoved) - ->foreach(fn(FileWasRemoved $event) => $this->filesystem->remove( - $path->toString().$event->file()->toString(), - )); + ->reduce( + [], + function(array $added, object $event) use ($path): array { + switch (\get_class($event)) { + case FileWasRemoved::class: + $this->filesystem->remove( + $path->toString().$event->file()->toString(), + ); + break; + + case FileWasAdded::class: + $this->createFileAt($path, $event->file()); + $added[] = $event->file()->name()->toString(); + break; + } + + return $added; + }, + ); + $file->foreach(function(File $file) use ($path, $alreadyAdded): void { + if (\in_array($file->name()->toString(), $alreadyAdded, true)) { + return; + } + + $this->createFileAt($path, $file); + }); return; } From 8fbd436cf49e304d233f4f1a990d21008df572a3 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 3 May 2020 14:59:32 +0200 Subject: [PATCH 21/62] dots cannot be used as file names --- fixtures/Name.php | 2 +- src/Name.php | 5 +++++ tests/NameTest.php | 20 ++++++++++++++++---- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/fixtures/Name.php b/fixtures/Name.php index bb44a2d..77e3ab1 100644 --- a/fixtures/Name.php +++ b/fixtures/Name.php @@ -17,7 +17,7 @@ public static function any(): Set static fn(string $name): Model => new Model($name), Set\Strings::any() ->filter(static fn($s) => \strpos($s, '/') === false) - ->filter(static fn($s) => $s !== ''), + ->filter(static fn($s) => $s !== '' && $s !== '.' && $s !== '..'), ); } } diff --git a/src/Name.php b/src/Name.php index e5fe373..a657758 100644 --- a/src/Name.php +++ b/src/Name.php @@ -20,6 +20,11 @@ public function __construct(string $value) throw new DomainException('A file name can\'t be empty'); } + if ($value === '.' || $value === '..') { + // as they are special links on unix filesystems + throw new DomainException("'.' and '..' can't be used"); + } + $this->value = $value; } diff --git a/tests/NameTest.php b/tests/NameTest.php index a9e508d..9b4d15c 100644 --- a/tests/NameTest.php +++ b/tests/NameTest.php @@ -52,7 +52,7 @@ public function testAcceptsAnyValueNotContainingASlash() ->forAll( Set\Strings::any() ->filter(fn($s) => \strpos($s, '/') === false) - ->filter(fn($s) => $s !== ''), + ->filter(fn($s) => $s !== '' && $s !== '.' && $s !== '..'), ) ->then(function($value) { $name = new Name($value); @@ -81,7 +81,7 @@ public function testNameEqualsItself() ->forAll( Set\Strings::any() ->filter(fn($s) => \strpos($s, '/') === false) - ->filter(fn($s) => $s !== ''), + ->filter(fn($s) => $s !== '' && $s !== '.' && $s !== '..'), ) ->then(function($value) { $name1 = new Name($value); @@ -98,10 +98,10 @@ public function testNameDoesntEqualDifferentName() ->forAll( Set\Strings::any() ->filter(fn($s) => \strpos($s, '/') === false) - ->filter(fn($s) => $s !== ''), + ->filter(fn($s) => $s !== '' && $s !== '.' && $s !== '..'), Set\Strings::any() ->filter(fn($s) => \strpos($s, '/') === false) - ->filter(fn($s) => $s !== ''), + ->filter(fn($s) => $s !== '' && $s !== '.' && $s !== '..'), ) ->then(function($a, $b) { $name1 = new Name($a); @@ -111,4 +111,16 @@ public function testNameDoesntEqualDifferentName() $this->assertFalse($name2->equals($name1)); }); } + + public function testDotFoldersAreNotAccepted() + { + $this + ->forAll(Set\Elements::of('.', '..')) + ->then(function($name) { + $this->expectException(DomainException::class); + $this->expectExceptionMessage("'.' and '..' can't be used"); + + new Name($name); + }); + } } From 81b46b762688a711992119fbef8267cb88b6e264 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 3 May 2020 15:01:18 +0200 Subject: [PATCH 22/62] fix use statement --- src/Adapter/Filesystem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Adapter/Filesystem.php b/src/Adapter/Filesystem.php index b906d78..24f994d 100644 --- a/src/Adapter/Filesystem.php +++ b/src/Adapter/Filesystem.php @@ -9,7 +9,7 @@ Name, Directory, Stream\LazyStream, - Stream\Source, + Source, Exception\FileNotFound, Exception\PathDoesntRepresentADirectory, Event\FileWasRemoved, From af44e0ffa544474dc0b40727310b1b0edfb29b65 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 3 May 2020 15:27:29 +0200 Subject: [PATCH 23/62] use abstraction to verify the file is written correctly --- src/Adapter/Filesystem.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Adapter/Filesystem.php b/src/Adapter/Filesystem.php index 24f994d..e9d7b03 100644 --- a/src/Adapter/Filesystem.php +++ b/src/Adapter/Filesystem.php @@ -15,6 +15,7 @@ Event\FileWasRemoved, Event\FileWasAdded, }; +use Innmind\Stream\Writable\Stream; use Innmind\MediaType\{ MediaType, Exception\InvalidMediaTypeString, @@ -159,10 +160,12 @@ function(array $added, object $event) use ($path): array { $stream = $file->content(); $stream->rewind(); - $handle = \fopen($path->toString(), 'w'); + $handle = new Stream(\fopen($path->toString(), 'w')); while (!$stream->end()) { - \fwrite($handle, $stream->read(8192)->toString()); + $handle->write( + $stream->read(8192)->toEncoding('ASCII'), + ); } } From 73a0ad386493e190008b336e425f0d8a6a94c46b Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 3 May 2020 17:27:56 +0200 Subject: [PATCH 24/62] add adapter property --- properties/Adapter.php | 1 + .../Adapter/ReAddingFilesHasNoSideEffect.php | 40 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 properties/Adapter/ReAddingFilesHasNoSideEffect.php diff --git a/properties/Adapter.php b/properties/Adapter.php index 16a9900..edc3a21 100644 --- a/properties/Adapter.php +++ b/properties/Adapter.php @@ -63,6 +63,7 @@ public static function properties(Seeder $seed): Set $seed(Directory::any()), $seed(File::any()), ), + new Adapter\ReAddingFilesHasNoSideEffect, ); } } diff --git a/properties/Adapter/ReAddingFilesHasNoSideEffect.php b/properties/Adapter/ReAddingFilesHasNoSideEffect.php new file mode 100644 index 0000000..96cd0aa --- /dev/null +++ b/properties/Adapter/ReAddingFilesHasNoSideEffect.php @@ -0,0 +1,40 @@ +all() + ->foreach(function($file) use ($adapter) { + $adapter->add($file); + Assert::assertTrue($adapter->contains($file->name())); + Assert::assertSame( + $file->content()->toString(), + $adapter + ->get($file->name()) + ->content() + ->toString(), + ); + }); + + return $adapter; + } +} From be2fef9f7167c7a1ab17df36aca62be210b90262 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 3 May 2020 17:53:06 +0200 Subject: [PATCH 25/62] directory content is now sorted --- tests/Directory/DirectoryTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Directory/DirectoryTest.php b/tests/Directory/DirectoryTest.php index 1d2f284..9998a66 100644 --- a/tests/Directory/DirectoryTest.php +++ b/tests/Directory/DirectoryTest.php @@ -144,7 +144,7 @@ public function testGenerator() ); $this->assertSame( - 'foo' . "\n" . 'bar' . "\n" . 'foobar' . "\n" . 'sub', + 'bar' . "\n" . 'foo' . "\n" . 'foobar' . "\n" . 'sub', $d->content()->toString() ); } From 677270170316e6ae85e748e20b519574617210c6 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 3 May 2020 17:57:59 +0200 Subject: [PATCH 26/62] use parameterized properties --- composer.json | 2 +- properties/Adapter.php | 82 +++++++++++++++----------- properties/Directory.php | 66 +++++++++++++-------- tests/Adapter/CacheOpenedFilesTest.php | 2 +- tests/Adapter/FilesystemTest.php | 2 +- tests/Adapter/InMemoryTest.php | 2 +- tests/Adapter/LazyTest.php | 2 +- tests/Directory/DirectoryTest.php | 4 +- tests/Directory/SourceTest.php | 2 +- 9 files changed, 98 insertions(+), 66 deletions(-) diff --git a/composer.json b/composer.json index 4c791b4..4dbf490 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,6 @@ "require-dev": { "phpunit/phpunit": "~8.0", "vimeo/psalm": "^3.7", - "innmind/black-box": "^4.6" + "innmind/black-box": "^4.7" } } diff --git a/properties/Adapter.php b/properties/Adapter.php index edc3a21..806c279 100644 --- a/properties/Adapter.php +++ b/properties/Adapter.php @@ -6,7 +6,6 @@ use Innmind\BlackBox\{ Set, Property, - PHPUnit\Seeder, }; use Fixtures\Innmind\Filesystem\{ File, @@ -19,51 +18,66 @@ final class Adapter /** * @return Set */ - public static function properties(Seeder $seed): Set + public static function properties(): Set { - return Set\Properties::of( - new Adapter\AddFile( - $seed(File::any()), + return Set\Properties::any( + Set\Property::of( + Adapter\AddFile::class, + File::any(), ), - new Adapter\AddEmptyDirectory( - $seed(Name::any()), + Set\Property::of( + Adapter\AddEmptyDirectory::class, + Name::any(), ), - new Adapter\AddDirectoryFromAnotherAdapter( - $seed(Name::any()), - $seed(File::any()), + Set\Property::of( + Adapter\AddDirectoryFromAnotherAdapter::class, + Name::any(), + File::any(), ), - new Adapter\AddDirectoryFromAnotherAdapterWithFileAdded( - $seed(Name::any()), - $seed(File::any()), - $seed(File::any()), + Set\Property::of( + Adapter\AddDirectoryFromAnotherAdapterWithFileAdded::class, + Name::any(), + File::any(), + File::any(), ), - new Adapter\AddDirectoryFromAnotherAdapterWithFileRemoved( - $seed(Name::any()), - $seed(File::any()), - $seed(File::any()), + Set\Property::of( + Adapter\AddDirectoryFromAnotherAdapterWithFileRemoved::class, + Name::any(), + File::any(), + File::any(), ), - new Adapter\RemoveUnknownFile( - $seed(Name::any()), + Set\Property::of( + Adapter\RemoveUnknownFile::class, + Name::any(), ), - new Adapter\RemoveFile( - $seed(File::any()), + Set\Property::of( + Adapter\RemoveFile::class, + File::any(), ), - new Adapter\AllRootFilesAreAccessible, - new Adapter\AccessingUnknownFileThrowsAnException( - $seed(Name::any()), + Set\Property::of( + Adapter\AllRootFilesAreAccessible::class, ), - new Adapter\AddDirectory( - $seed(Directory::any()), + Set\Property::of( + Adapter\AccessingUnknownFileThrowsAnException::class, + Name::any(), ), - new Adapter\AddRemoveAddModificationsStillAddTheFile( - $seed(Directory::any()), - $seed(File::any()), + Set\Property::of( + Adapter\AddDirectory::class, + Directory::any(), ), - new Adapter\RemoveAddRemoveModificationsDoesntAddTheFile( - $seed(Directory::any()), - $seed(File::any()), + Set\Property::of( + Adapter\AddRemoveAddModificationsStillAddTheFile::class, + Directory::any(), + File::any(), + ), + Set\Property::of( + Adapter\RemoveAddRemoveModificationsDoesntAddTheFile::class, + Directory::any(), + File::any(), + ), + Set\Property::of( + Adapter\ReAddingFilesHasNoSideEffect::class, ), - new Adapter\ReAddingFilesHasNoSideEffect, ); } } diff --git a/properties/Directory.php b/properties/Directory.php index 2c97a29..0904012 100644 --- a/properties/Directory.php +++ b/properties/Directory.php @@ -6,7 +6,6 @@ use Innmind\BlackBox\{ Set, Property, - PHPUnit\Seeder, }; use Fixtures\Innmind\Filesystem\{ Name, @@ -18,30 +17,49 @@ final class Directory /** * @return Set */ - public static function properties(Seeder $seed): Set + public static function properties(): Set { - return Set\Properties::of( - new Directory\MediaTypeIsAlwaysTheSame, - new Directory\ContainsMethodAlwaysReturnTrueForFilesInTheDirectory, - new Directory\AllFilesInTheDirectoryAreAccessible, - new Directory\AccessingUnknownFileThrowsAnException( - $seed(Name::any()), - ), - new Directory\RemovingAnUnknownFileHasNoEffect( - $seed(Name::any()), - ), - new Directory\RemoveFile, - new Directory\RemoveFileMustUnwrapSourceDecorator, - new Directory\RemoveDirectory, - new Directory\ContentHoldsTheNamesOfTheFiles, - new Directory\AddFile( - $seed(File::any()), - ), - new Directory\AddFileMustUnwrapSourceDecorator( - $seed(File::any()), - ), - new Directory\AddDirectory( - $seed(Name::any()), + return Set\Properties::any( + Set\Property::of( + Directory\MediaTypeIsAlwaysTheSame::class, + ), + Set\Property::of( + Directory\ContainsMethodAlwaysReturnTrueForFilesInTheDirectory::class, + ), + Set\Property::of( + Directory\AllFilesInTheDirectoryAreAccessible::class, + ), + Set\Property::of( + Directory\AccessingUnknownFileThrowsAnException::class, + Name::any(), + ), + Set\Property::of( + Directory\RemovingAnUnknownFileHasNoEffect::class, + Name::any(), + ), + Set\Property::of( + Directory\RemoveFile::class, + ), + Set\Property::of( + Directory\RemoveFileMustUnwrapSourceDecorator::class, + ), + Set\Property::of( + Directory\RemoveDirectory::class, + ), + Set\Property::of( + Directory\ContentHoldsTheNamesOfTheFiles::class, + ), + Set\Property::of( + Directory\AddFile::class, + File::any(), + ), + Set\Property::of( + Directory\AddFileMustUnwrapSourceDecorator::class, + File::any(), + ), + Set\Property::of( + Directory\AddDirectory::class, + Name::any(), ), ); } diff --git a/tests/Adapter/CacheOpenedFilesTest.php b/tests/Adapter/CacheOpenedFilesTest.php index 8640d8d..cd2f872 100644 --- a/tests/Adapter/CacheOpenedFilesTest.php +++ b/tests/Adapter/CacheOpenedFilesTest.php @@ -148,7 +148,7 @@ public function testAll() public function testHoldProperties() { $this - ->forAll(PAdapter::properties($this->seeder())) + ->forAll(PAdapter::properties()) ->then(function($properties) { $properties->ensureHeldBy(new CacheOpenedFiles(new InMemory)); }); diff --git a/tests/Adapter/FilesystemTest.php b/tests/Adapter/FilesystemTest.php index d81dda1..c5ec857 100644 --- a/tests/Adapter/FilesystemTest.php +++ b/tests/Adapter/FilesystemTest.php @@ -219,7 +219,7 @@ public function testAddingTheSameFileTwiceDoesNothing() public function testHoldProperties() { $this - ->forAll(PAdapter::properties($this->seeder())) + ->forAll(PAdapter::properties()) ->then(function($properties) { $path = \sys_get_temp_dir().'/innmind/filesystem/'; (new FS)->remove($path); diff --git a/tests/Adapter/InMemoryTest.php b/tests/Adapter/InMemoryTest.php index 97c5bf4..c78d541 100644 --- a/tests/Adapter/InMemoryTest.php +++ b/tests/Adapter/InMemoryTest.php @@ -77,7 +77,7 @@ public function testAll() public function testHoldProperties() { $this - ->forAll(PAdapter::properties($this->seeder())) + ->forAll(PAdapter::properties()) ->then(function($properties) { $properties->ensureHeldBy(new InMemory); }); diff --git a/tests/Adapter/LazyTest.php b/tests/Adapter/LazyTest.php index 7deb230..7d5af45 100644 --- a/tests/Adapter/LazyTest.php +++ b/tests/Adapter/LazyTest.php @@ -118,7 +118,7 @@ public function testGetFileAddedButNotYetPersisted() public function testHoldProperties() { $this - ->forAll(Adapter::properties($this->seeder())) + ->forAll(Adapter::properties()) ->then(function($properties) { $properties->ensureHeldBy(new Lazy(new InMemory)); }); diff --git a/tests/Directory/DirectoryTest.php b/tests/Directory/DirectoryTest.php index 9998a66..540f78d 100644 --- a/tests/Directory/DirectoryTest.php +++ b/tests/Directory/DirectoryTest.php @@ -296,7 +296,7 @@ public function testEmptyDirectoryHoldProperties() { $this ->forAll( - PDirectory::properties($this->seeder()), + PDirectory::properties(), FName::any(), ) ->then(function($properties, $name) { @@ -310,7 +310,7 @@ public function testDirectoryWithSomeFilesHoldProperties() { $this ->forAll( - PDirectory::properties($this->seeder()), + PDirectory::properties(), FName::any(), FSet::of( File::class, diff --git a/tests/Directory/SourceTest.php b/tests/Directory/SourceTest.php index dc9d305..99269a9 100644 --- a/tests/Directory/SourceTest.php +++ b/tests/Directory/SourceTest.php @@ -107,7 +107,7 @@ public function testHoldProperties() { $this ->forAll( - PDirectory::properties($this->seeder()), + PDirectory::properties(), Directory::maxDepth(1), Path::any(), ) From 41840a73cc5ff4c00dc1d83cb32860cab9383594 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 3 May 2020 18:02:54 +0200 Subject: [PATCH 27/62] rename Source::shouldBePersistedAt to Source::sourcedAt to ease understanding --- src/Adapter/Filesystem.php | 3 ++- src/Directory/Source.php | 6 +++--- src/File/Source.php | 6 +++--- src/Source.php | 2 +- tests/Directory/SourceTest.php | 6 +++--- tests/File/SourceTest.php | 6 +++--- 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/Adapter/Filesystem.php b/src/Adapter/Filesystem.php index e9d7b03..2774971 100644 --- a/src/Adapter/Filesystem.php +++ b/src/Adapter/Filesystem.php @@ -120,7 +120,8 @@ private function createFileAt(Path $path, File $file): void $path = $path->resolve(Path::of($name)); - if ($file instanceof Source && !$file->shouldPersistAt($this, $path)) { + if ($file instanceof Source && $file->sourcedAt($this, $path)) { + // no need to persist untouched file where it was loaded from return; } diff --git a/src/Directory/Source.php b/src/Directory/Source.php index 832a8e2..7ca6fbe 100644 --- a/src/Directory/Source.php +++ b/src/Directory/Source.php @@ -34,10 +34,10 @@ public function __construct( $this->path = $path; } - public function shouldPersistAt(Adapter $target, Path $path): bool + public function sourcedAt(Adapter $adapter, Path $path): bool { - return $this->openedBy !== $target || - !$this->path->equals($path); + return $this->openedBy === $adapter && + $this->path->equals($path); } public function name(): Name diff --git a/src/File/Source.php b/src/File/Source.php index 91724ce..65e598c 100644 --- a/src/File/Source.php +++ b/src/File/Source.php @@ -29,10 +29,10 @@ public function __construct( $this->path = $path; } - public function shouldPersistAt(Adapter $target, Path $path): bool + public function sourcedAt(Adapter $adapter, Path $path): bool { - return $this->openedBy !== $target || - !$this->path->equals($path); + return $this->openedBy === $adapter && + $this->path->equals($path); } public function name(): Name diff --git a/src/Source.php b/src/Source.php index 3a33bec..2f163aa 100644 --- a/src/Source.php +++ b/src/Source.php @@ -13,5 +13,5 @@ */ interface Source extends File { - public function shouldPersistAt(Adapter $target, Path $path): bool; + public function sourcedAt(Adapter $adapter, Path $path): bool; } diff --git a/tests/Directory/SourceTest.php b/tests/Directory/SourceTest.php index 99269a9..29b1d7a 100644 --- a/tests/Directory/SourceTest.php +++ b/tests/Directory/SourceTest.php @@ -54,7 +54,7 @@ public function testAnyFileSourceFromAnotherShouldBePersisted() $path, ); - $this->assertTrue($source->shouldPersistAt( + $this->assertFalse($source->sourcedAt( $this->createMock(Adapter::class), $path, )); @@ -78,7 +78,7 @@ public function testFileOpenedInADifferentPathThanTheTargetPathShouldBePersisted $openedAt, ); - $this->assertTrue($source->shouldPersistAt( + $this->assertFalse($source->sourcedAt( $adapter, $writeAt, )); @@ -96,7 +96,7 @@ public function testShouldNotPersistAFileWhereItWasOpenedInTheSameAdapter() $path, ); - $this->assertFalse($source->shouldPersistAt( + $this->assertTrue($source->sourcedAt( $adapter, $path, )); diff --git a/tests/File/SourceTest.php b/tests/File/SourceTest.php index d620283..ba399ec 100644 --- a/tests/File/SourceTest.php +++ b/tests/File/SourceTest.php @@ -46,7 +46,7 @@ public function testAnyFileSourceFromAnotherShouldBePersisted() $path, ); - $this->assertTrue($source->shouldPersistAt( + $this->assertFalse($source->sourcedAt( $this->createMock(Adapter::class), $path, )); @@ -70,7 +70,7 @@ public function testFileOpenedInADifferentPathThanTheTargetPathShouldBePersisted $openedAt, ); - $this->assertTrue($source->shouldPersistAt( + $this->assertFalse($source->sourcedAt( $adapter, $writeAt, )); @@ -88,7 +88,7 @@ public function testShouldNotPersistAFileWhereItWasOpenedInTheSameAdapter() $path, ); - $this->assertFalse($source->shouldPersistAt( + $this->assertTrue($source->sourcedAt( $adapter, $path, )); From a40362896f6b28ad791fb03f1760eb9fb9670037 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 3 May 2020 18:05:13 +0200 Subject: [PATCH 28/62] reduce the size of generated directories --- fixtures/Directory.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/fixtures/Directory.php b/fixtures/Directory.php index 0aa28f8..879185c 100644 --- a/fixtures/Directory.php +++ b/fixtures/Directory.php @@ -20,7 +20,7 @@ final class Directory */ public static function any(): DataSet { - return self::atDepth(0, 3); + return self::atDepth(0, 2); } /** @@ -39,13 +39,13 @@ private static function atDepth(int $depth, int $maxDepth): DataSet new DataSet\Randomize( File::any(), ), - DataSet\Integers::between(0, 10), + DataSet\Integers::between(0, 5), ); $toAdd = DataSet\Sequence::of( new DataSet\Randomize( File::any(), ), - DataSet\Integers::between(0, 10), + DataSet\Integers::between(0, 5), ); } else { $files = Set::of( @@ -56,7 +56,7 @@ private static function atDepth(int $depth, int $maxDepth): DataSet ), self::atDepth($depth + 1, $maxDepth), ), - DataSet\Integers::between(0, 10), + DataSet\Integers::between(0, 5), )->filter(static function($files): bool { if ($files->empty()) { return true; @@ -74,7 +74,7 @@ private static function atDepth(int $depth, int $maxDepth): DataSet ), self::atDepth($depth + 1, $maxDepth), ), - DataSet\Integers::between(0, 10), + DataSet\Integers::between(0, 5), ); } @@ -104,7 +104,7 @@ static function($name, $files, $toAdd, $numberToRemove): Model { Name::any(), $files, $toAdd, - DataSet\Integers::between(0, 20), + DataSet\Integers::between(0, 10), ); } } From 436d21c205e734d97f2c2cab7dbe51b404ae51ed Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Mon, 4 May 2020 08:42:07 +0200 Subject: [PATCH 29/62] reduce the number of scenarii to run --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24ce28b..08503e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,8 @@ jobs: run: composer install --no-progress - name: PHPUnit run: vendor/bin/phpunit + env: + BLACKBOX_SET_SIZE: 1 coverage: runs-on: ${{ matrix.os }} strategy: From c913d54291f9420bc06bccf2de9362e293a95015 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Fri, 8 May 2020 08:43:35 +0200 Subject: [PATCH 30/62] specify blackbox fixtures are provided --- composer.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/composer.json b/composer.json index 4dbf490..0a47d42 100644 --- a/composer.json +++ b/composer.json @@ -39,5 +39,14 @@ "phpunit/phpunit": "~8.0", "vimeo/psalm": "^3.7", "innmind/black-box": "^4.7" + }, + "conflict": { + "innmind/black-box": "<4.7|~5.0" + }, + "suggest": { + "innmind/black-box": "For property based testing" + }, + "provide": { + "innmind/black-box": "4.7.0" } } From fdf7b9b2169bfb7fc5b6bbe99297f7caa354770c Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Fri, 8 May 2020 08:44:12 +0200 Subject: [PATCH 31/62] remove unreachable test case --- tests/Adapter/FilesystemTest.php | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/tests/Adapter/FilesystemTest.php b/tests/Adapter/FilesystemTest.php index c5ec857..d52defe 100644 --- a/tests/Adapter/FilesystemTest.php +++ b/tests/Adapter/FilesystemTest.php @@ -180,30 +180,6 @@ public function testAll() $adapter->remove(new Name('baz')); } - public function testDotPseudoFilesAreNotListedInDirectory() - { - @mkdir('/tmp/sub'); - @mkdir('/tmp/sub/test'); - $adapter = new Filesystem(Path::of('/tmp/sub/')); - - $this->assertFalse($adapter->get(new Name('test'))->contains(new Name('.'))); - $this->assertFalse($adapter->get(new Name('test'))->contains(new Name('..'))); - $this->assertFalse($adapter->contains(new Name('.'))); - $this->assertFalse($adapter->contains(new Name('..'))); - $this->assertFalse( - $adapter - ->all() - ->reduce( - false, - fn($found, $file) => $found || $file->name()->equals(new Name('.')) || $file->name()->equals(new Name('..')), - ), - ); - - $this->expectException(FileNotFound::class); - - $adapter->get(new Name('..')); - } - public function testAddingTheSameFileTwiceDoesNothing() { $adapter = new Filesystem(Path::of('/tmp/')); From dbcb78e479d6e3124c70501cca66b611fbe80989 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Fri, 8 May 2020 11:29:48 +0200 Subject: [PATCH 32/62] fix file name allowing invalid characters --- fixtures/Name.php | 25 ++++++++-- src/Name.php | 36 ++++++++++++-- tests/NameTest.php | 118 +++++++++++++++++++++++++++++++++++++++------ 3 files changed, 158 insertions(+), 21 deletions(-) diff --git a/fixtures/Name.php b/fixtures/Name.php index 77e3ab1..ed6fbf6 100644 --- a/fixtures/Name.php +++ b/fixtures/Name.php @@ -15,9 +15,28 @@ public static function any(): Set { return Set\Decorate::immutable( static fn(string $name): Model => new Model($name), - Set\Strings::any() - ->filter(static fn($s) => \strpos($s, '/') === false) - ->filter(static fn($s) => $s !== '' && $s !== '.' && $s !== '..'), + Set\Composite::immutable( + static fn(string $first, array $chrs): string => $first.\implode('', $chrs), + Set\Decorate::immutable( + static fn(int $chr): string => \chr($chr), + Set\Elements::of( + ...range(1, 8), + ...range(14, 31), + ...range(33, 46), + ...range(48, 127), + ), + ), + Set\Sequence::of( + Set\Decorate::immutable( + static fn(int $chr): string => \chr($chr), + Set\Elements::of( + ...range(1, 46), + ...range(48, 127), + ), + ), + Set\Integers::between(0, 254), + ), + )->filter(static fn(string $name): bool => $name !== '.' && $name !== '..'), ); } } diff --git a/src/Name.php b/src/Name.php index a657758..bc7e3f0 100644 --- a/src/Name.php +++ b/src/Name.php @@ -12,14 +12,21 @@ final class Name public function __construct(string $value) { - if (Str::of($value)->matches('|/|')) { - throw new DomainException("A file name can't contain a slash, $value given"); - } - if (Str::of($value)->empty()) { throw new DomainException('A file name can\'t be empty'); } + if (Str::of($value, 'ASCII')->length() > 255) { + throw new DomainException($value); + } + + if (Str::of($value)->contains('/')) { + throw new DomainException("A file name can't contain a slash, $value given"); + } + + $this->assertContainsOnlyValidCharacters($value); + $this->assertFirstCharacterValid($value); + if ($value === '.' || $value === '..') { // as they are special links on unix filesystems throw new DomainException("'.' and '..' can't be used"); @@ -37,4 +44,25 @@ public function toString(): string { return $this->value; } + + private function assertContainsOnlyValidCharacters(string $value): void + { + $value = Str::of($value); + $invalid = [0, ...range(128, 255)]; + + foreach ($invalid as $ord) { + if ($value->contains(\chr($ord))) { + throw new DomainException($value->toString()); + } + } + } + + private function assertFirstCharacterValid(string $value): void + { + $index = \ord(Str::of($value, 'ASCII')->take(1)->toString()); + + if (\in_array($index, [32, ...range(9, 13)], true)) { + throw new DomainException($value); + } + } } diff --git a/tests/NameTest.php b/tests/NameTest.php index 9b4d15c..d667572 100644 --- a/tests/NameTest.php +++ b/tests/NameTest.php @@ -50,9 +50,7 @@ public function testAcceptsAnyValueNotContainingASlash() { $this ->forAll( - Set\Strings::any() - ->filter(fn($s) => \strpos($s, '/') === false) - ->filter(fn($s) => $s !== '' && $s !== '.' && $s !== '..'), + $this->valid(), ) ->then(function($value) { $name = new Name($value); @@ -65,8 +63,8 @@ public function testNameContainingASlashIsNotAccepted() { $this ->forAll( - Set\Strings::any(), - Set\Strings::any(), + $this->valid(), + $this->valid(), ) ->then(function($a, $b) { $this->expectException(DomainException::class); @@ -79,9 +77,7 @@ public function testNameEqualsItself() { $this ->forAll( - Set\Strings::any() - ->filter(fn($s) => \strpos($s, '/') === false) - ->filter(fn($s) => $s !== '' && $s !== '.' && $s !== '..'), + $this->valid(), ) ->then(function($value) { $name1 = new Name($value); @@ -96,12 +92,8 @@ public function testNameDoesntEqualDifferentName() { $this ->forAll( - Set\Strings::any() - ->filter(fn($s) => \strpos($s, '/') === false) - ->filter(fn($s) => $s !== '' && $s !== '.' && $s !== '..'), - Set\Strings::any() - ->filter(fn($s) => \strpos($s, '/') === false) - ->filter(fn($s) => $s !== '' && $s !== '.' && $s !== '..'), + $this->valid(), + $this->valid(), ) ->then(function($a, $b) { $name1 = new Name($a); @@ -123,4 +115,102 @@ public function testDotFoldersAreNotAccepted() new Name($name); }); } + + public function testNamesContainingOnlyOneCharacterOutsideOfAllowedRangeAreNotAccepted() + { + $this + ->forAll(Set\Elements::of( + 0, + 32, + 47, + ...range(9, 13), + ...range(128, 255), + )) + ->then(function($invalid) { + $this->expectException(DomainException::class); + + new Name(\chr($invalid)); + }); + } + + public function testNamesContainingCharOrdAbove127IsNotAccepted() + { + $this + ->forAll(Set\Elements::of( + ...range(128, 255), + )) + ->then(function($invalid) { + $this->expectException(DomainException::class); + + new Name('a'.\chr($invalid).'a'); + }); + } + + public function testChr0IsNotAccepted() + { + $this->expectException(DomainException::class); + + new Name('a'.\chr(0).'a'); + } + + public function testNamesLongerThan255AreNotAccepted() + { + $this + ->forAll( + Set\Composite::immutable( + static fn(string $first, array $chrs): string => $first.\implode('', $chrs), + Set\Decorate::immutable( + static fn(int $chr): string => \chr($chr), + Set\Elements::of( + ...range(1, 8), + ...range(14, 31), + ...range(33, 46), + ...range(48, 127), + ), + ), + Set\Sequence::of( + Set\Decorate::immutable( + static fn(int $chr): string => \chr($chr), + Set\Elements::of( + ...range(1, 46), + // chr(47) alias '/' not accepted + ...range(48, 127), + ), + ), + Set\Integers::between(255, 1024), // upper limit at 1024 to avoid out of memory + ), + )->filter(static fn(string $name): bool => $name !== '.' && $name !== '..') + ) + ->then(function($name) { + $this->expectException(DomainException::class); + + new Name($name); + }); + } + + private function valid(): Set + { + return Set\Composite::immutable( + static fn(string $first, array $chrs): string => $first.\implode('', $chrs), + Set\Decorate::immutable( + static fn(int $chr): string => \chr($chr), + Set\Elements::of( + ...range(1, 8), + ...range(14, 31), + ...range(33, 46), + ...range(48, 127), + ), + ), + Set\Sequence::of( + Set\Decorate::immutable( + static fn(int $chr): string => \chr($chr), + Set\Elements::of( + ...range(1, 46), + ...range(48, 127), + ), + ), + Set\Integers::between(0, 254), + ), + )->filter(static fn(string $name): bool => $name !== '.' && $name !== '..'); + } } From a26043fe0742612bc487b61f252a70ef43a0288c Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Fri, 8 May 2020 18:19:22 +0200 Subject: [PATCH 33/62] avoid persisting files being removed right after --- src/Adapter/Filesystem.php | 42 +++++++++++++------------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/src/Adapter/Filesystem.php b/src/Adapter/Filesystem.php index 2774971..00deed0 100644 --- a/src/Adapter/Filesystem.php +++ b/src/Adapter/Filesystem.php @@ -13,7 +13,6 @@ Exception\FileNotFound, Exception\PathDoesntRepresentADirectory, Event\FileWasRemoved, - Event\FileWasAdded, }; use Innmind\Stream\Writable\Stream; use Innmind\MediaType\{ @@ -127,34 +126,21 @@ private function createFileAt(Path $path, File $file): void if ($file instanceof Directory) { $this->filesystem->mkdir($path->toString()); - $alreadyAdded = $file - ->modifications() - ->reduce( - [], - function(array $added, object $event) use ($path): array { - switch (\get_class($event)) { - case FileWasRemoved::class: - $this->filesystem->remove( - $path->toString().$event->file()->toString(), - ); - break; - - case FileWasAdded::class: - $this->createFileAt($path, $event->file()); - $added[] = $event->file()->name()->toString(); - break; - } - - return $added; - }, - ); - $file->foreach(function(File $file) use ($path, $alreadyAdded): void { - if (\in_array($file->name()->toString(), $alreadyAdded, true)) { - return; - } + $persisted = $file->reduce( + Set::strings(), + function(Set $persisted, File $file) use ($path): Set { + $this->createFileAt($path, $file); - $this->createFileAt($path, $file); - }); + return ($persisted)($file->name()->toString()); + }, + ); + $file + ->modifications() + ->filter(static fn(object $event): bool => $event instanceof FileWasRemoved) + ->filter(static fn(FileWasRemoved $event): bool => !$persisted->contains($event->file()->toString())) + ->foreach(fn(FileWasRemoved $event) => $this->filesystem->remove( + $path->toString().$event->file()->toString(), + )); return; } From 9373281d84350f6f9fe7f055b8e85ce43c92aefc Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Fri, 8 May 2020 18:33:51 +0200 Subject: [PATCH 34/62] increase the number of runs --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08503e4..ac92da8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: - name: PHPUnit run: vendor/bin/phpunit env: - BLACKBOX_SET_SIZE: 1 + BLACKBOX_SET_SIZE: 10 coverage: runs-on: ${{ matrix.os }} strategy: From 2b78b6cd5b11ef701b7df6db41ed4533fc6c150b Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Fri, 8 May 2020 19:15:59 +0200 Subject: [PATCH 35/62] display detailed properties --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac92da8..ca350e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,7 @@ jobs: run: vendor/bin/phpunit env: BLACKBOX_SET_SIZE: 10 + BLACKBOX_DETAILED_PROPERTIES: 1 coverage: runs-on: ${{ matrix.os }} strategy: From 0dc8fd9196ab22e4ae7eaa64cdb9797719634ced Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Fri, 8 May 2020 19:16:29 +0200 Subject: [PATCH 36/62] disable memory limit --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca350e4..6cdfe01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: - name: Install Dependencies run: composer install --no-progress - name: PHPUnit - run: vendor/bin/phpunit + run: php -dmemory_limit=-1 vendor/bin/phpunit env: BLACKBOX_SET_SIZE: 10 BLACKBOX_DETAILED_PROPERTIES: 1 From 388a031fc610d8fc259651247b1a90481a741011 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Fri, 8 May 2020 19:19:52 +0200 Subject: [PATCH 37/62] ignore psalm errors --- src/Adapter/Filesystem.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Adapter/Filesystem.php b/src/Adapter/Filesystem.php index 00deed0..9819aa9 100644 --- a/src/Adapter/Filesystem.php +++ b/src/Adapter/Filesystem.php @@ -134,6 +134,10 @@ function(Set $persisted, File $file) use ($path): Set { return ($persisted)($file->name()->toString()); }, ); + /** + * @psalm-suppress ArgumentTypeCoercion + * @psalm-suppress MissingClosureReturnType + */ $file ->modifications() ->filter(static fn(object $event): bool => $event instanceof FileWasRemoved) From 997f4c13d583233544135fa7081a467fe89d250a Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 9 May 2020 12:11:00 +0200 Subject: [PATCH 38/62] prevent using characters causing errors with fopen --- fixtures/Name.php | 11 ++++++++--- src/Name.php | 4 ++-- tests/NameTest.php | 37 +++++++++++++++++++++++++++++++------ 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/fixtures/Name.php b/fixtures/Name.php index ed6fbf6..960f98c 100644 --- a/fixtures/Name.php +++ b/fixtures/Name.php @@ -20,17 +20,22 @@ public static function any(): Set Set\Decorate::immutable( static fn(int $chr): string => \chr($chr), Set\Elements::of( + 33, ...range(1, 8), ...range(14, 31), - ...range(33, 46), - ...range(48, 127), + ...range(35, 38), + ...range(40, 46), + ...range(48, 122), + ...range(126, 127), ), ), Set\Sequence::of( Set\Decorate::immutable( static fn(int $chr): string => \chr($chr), Set\Elements::of( - ...range(1, 46), + ...range(1, 33), + ...range(35, 38), + ...range(40, 46), ...range(48, 127), ), ), diff --git a/src/Name.php b/src/Name.php index bc7e3f0..188b346 100644 --- a/src/Name.php +++ b/src/Name.php @@ -48,7 +48,7 @@ public function toString(): string private function assertContainsOnlyValidCharacters(string $value): void { $value = Str::of($value); - $invalid = [0, ...range(128, 255)]; + $invalid = [0, 34, 39, ...range(128, 255)]; foreach ($invalid as $ord) { if ($value->contains(\chr($ord))) { @@ -61,7 +61,7 @@ private function assertFirstCharacterValid(string $value): void { $index = \ord(Str::of($value, 'ASCII')->take(1)->toString()); - if (\in_array($index, [32, ...range(9, 13)], true)) { + if (\in_array($index, [32, ...range(9, 13), ...range(123, 125)], true)) { throw new DomainException($value); } } diff --git a/tests/NameTest.php b/tests/NameTest.php index d667572..0897e42 100644 --- a/tests/NameTest.php +++ b/tests/NameTest.php @@ -124,6 +124,7 @@ public function testNamesContainingOnlyOneCharacterOutsideOfAllowedRangeAreNotAc 32, 47, ...range(9, 13), + ...range(123, 125), ...range(128, 255), )) ->then(function($invalid) { @@ -153,6 +154,20 @@ public function testChr0IsNotAccepted() new Name('a'.\chr(0).'a'); } + public function testSingleQuoteIsNotAccepted() + { + $this->expectException(DomainException::class); + + new Name("a'a"); + } + + public function testDoubleQuoteIsNotAccepted() + { + $this->expectException(DomainException::class); + + new Name('a"a'); + } + public function testNamesLongerThan255AreNotAccepted() { $this @@ -162,17 +177,22 @@ public function testNamesLongerThan255AreNotAccepted() Set\Decorate::immutable( static fn(int $chr): string => \chr($chr), Set\Elements::of( + 33, ...range(1, 8), ...range(14, 31), - ...range(33, 46), - ...range(48, 127), + ...range(35, 38), + ...range(40, 46), + ...range(48, 122), + ...range(126, 127), ), ), Set\Sequence::of( Set\Decorate::immutable( static fn(int $chr): string => \chr($chr), Set\Elements::of( - ...range(1, 46), + ...range(1, 33), + ...range(35, 38), + ...range(40, 46), // chr(47) alias '/' not accepted ...range(48, 127), ), @@ -195,17 +215,22 @@ private function valid(): Set Set\Decorate::immutable( static fn(int $chr): string => \chr($chr), Set\Elements::of( + 33, ...range(1, 8), ...range(14, 31), - ...range(33, 46), - ...range(48, 127), + ...range(35, 38), + ...range(40, 46), + ...range(48, 122), + ...range(126, 127), ), ), Set\Sequence::of( Set\Decorate::immutable( static fn(int $chr): string => \chr($chr), Set\Elements::of( - ...range(1, 46), + ...range(1, 33), + ...range(35, 38), + ...range(40, 46), ...range(48, 127), ), ), From 653e0171122410b0ae70770b4c638511609ca899 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 9 May 2020 12:48:00 +0200 Subject: [PATCH 39/62] reduce the number of generated files to speed up tests --- tests/Directory/DirectoryTest.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/Directory/DirectoryTest.php b/tests/Directory/DirectoryTest.php index 540f78d..b9b7203 100644 --- a/tests/Directory/DirectoryTest.php +++ b/tests/Directory/DirectoryTest.php @@ -317,13 +317,10 @@ public function testDirectoryWithSomeFilesHoldProperties() new DataSet\Randomize( FFile::any(), ), + DataSet\Integers::between(1, 5), // only to speed up tests ), ) ->filter(function($properties, $name, $files) { - if ($files->empty()) { - return true; - } - // do not accept duplicated files return $files ->groupBy(fn($file) => $file->name()->toString()) From fd99d7426a3f266444d32d5e1ef634afa5b7b654 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 9 May 2020 12:48:39 +0200 Subject: [PATCH 40/62] test properties individually to better identify implementation errors --- properties/Adapter.php | 12 ++++- properties/Directory.php | 12 ++++- tests/Adapter/CacheOpenedFilesTest.php | 25 +++++++++++ tests/Adapter/FilesystemTest.php | 29 ++++++++++++ tests/Adapter/InMemoryTest.php | 23 ++++++++++ tests/Adapter/LazyTest.php | 25 +++++++++++ tests/Directory/DirectoryTest.php | 62 ++++++++++++++++++++++++++ 7 files changed, 184 insertions(+), 4 deletions(-) diff --git a/properties/Adapter.php b/properties/Adapter.php index 806c279..137ef4c 100644 --- a/properties/Adapter.php +++ b/properties/Adapter.php @@ -20,7 +20,15 @@ final class Adapter */ public static function properties(): Set { - return Set\Properties::any( + return Set\Properties::any(...self::list()); + } + + /** + * @return list + */ + public static function list(): array + { + return [ Set\Property::of( Adapter\AddFile::class, File::any(), @@ -78,6 +86,6 @@ public static function properties(): Set Set\Property::of( Adapter\ReAddingFilesHasNoSideEffect::class, ), - ); + ]; } } diff --git a/properties/Directory.php b/properties/Directory.php index 0904012..fcc4202 100644 --- a/properties/Directory.php +++ b/properties/Directory.php @@ -19,7 +19,15 @@ final class Directory */ public static function properties(): Set { - return Set\Properties::any( + return Set\Properties::any(...self::list()); + } + + /** + * @return list> + */ + public static function list(): array + { + return [ Set\Property::of( Directory\MediaTypeIsAlwaysTheSame::class, ), @@ -61,6 +69,6 @@ public static function properties(): Set Directory\AddDirectory::class, Name::any(), ), - ); + ]; } } diff --git a/tests/Adapter/CacheOpenedFilesTest.php b/tests/Adapter/CacheOpenedFilesTest.php index cd2f872..8d48fc5 100644 --- a/tests/Adapter/CacheOpenedFilesTest.php +++ b/tests/Adapter/CacheOpenedFilesTest.php @@ -145,6 +145,24 @@ public function testAll() $this->assertSame($file, $filesystem->get(new Name('foo'))); } + /** + * @dataProvider properties + */ + public function testHoldProperty($property) + { + $this + ->forAll($property) + ->then(function($property) { + $adapter = new CacheOpenedFiles(new InMemory); + + if (!$property->applicableTo($adapter)) { + $this->markTestSkipped(); + } + + $property->ensureHeldBy($adapter); + }); + } + public function testHoldProperties() { $this @@ -153,4 +171,11 @@ public function testHoldProperties() $properties->ensureHeldBy(new CacheOpenedFiles(new InMemory)); }); } + + public function properties(): iterable + { + foreach (PAdapter::list() as $property) { + yield [$property]; + } + } } diff --git a/tests/Adapter/FilesystemTest.php b/tests/Adapter/FilesystemTest.php index d52defe..a61a67d 100644 --- a/tests/Adapter/FilesystemTest.php +++ b/tests/Adapter/FilesystemTest.php @@ -192,6 +192,28 @@ public function testAddingTheSameFileTwiceDoesNothing() $this->assertNull($adapter->add($file)); } + /** + * @dataProvider properties + */ + public function testHoldProperty($property) + { + $this + ->forAll($property) + ->then(function($property) { + $path = \sys_get_temp_dir().'/innmind/filesystem/'; + (new FS)->remove($path); + $adapter = new Filesystem(Path::of($path)); + + if (!$property->applicableTo($adapter)) { + $this->markTestSkipped(); + } + + $property->ensureHeldBy($adapter); + + (new FS)->remove($path); + }); + } + public function testHoldProperties() { $this @@ -205,4 +227,11 @@ public function testHoldProperties() (new FS)->remove($path); }); } + + public function properties(): iterable + { + foreach (PAdapter::list() as $property) { + yield [$property]; + } + } } diff --git a/tests/Adapter/InMemoryTest.php b/tests/Adapter/InMemoryTest.php index c78d541..b1fc5ca 100644 --- a/tests/Adapter/InMemoryTest.php +++ b/tests/Adapter/InMemoryTest.php @@ -74,6 +74,22 @@ public function testAll() ); } + /** + * @dataProvider properties + */ + public function testHoldProperty($property) + { + $this + ->forAll($property) + ->then(function($property) { + if (!$property->applicableTo(new InMemory)) { + $this->markTestSkipped(); + } + + $property->ensureHeldBy(new InMemory); + }); + } + public function testHoldProperties() { $this @@ -82,4 +98,11 @@ public function testHoldProperties() $properties->ensureHeldBy(new InMemory); }); } + + public function properties(): iterable + { + foreach (PAdapter::list() as $property) { + yield [$property]; + } + } } diff --git a/tests/Adapter/LazyTest.php b/tests/Adapter/LazyTest.php index 7d5af45..cc0a5c5 100644 --- a/tests/Adapter/LazyTest.php +++ b/tests/Adapter/LazyTest.php @@ -115,6 +115,24 @@ public function testGetFileAddedButNotYetPersisted() $this->assertSame($file, $filesystem->get($file->name())); } + /** + * @dataProvider properties + */ + public function testHoldProperty($property) + { + $this + ->forAll($property) + ->then(function($property) { + $adapter = new Lazy(new InMemory); + + if (!$property->applicableTo($adapter)) { + $this->markTestSkipped(); + } + + $property->ensureHeldBy($adapter); + }); + } + public function testHoldProperties() { $this @@ -123,4 +141,11 @@ public function testHoldProperties() $properties->ensureHeldBy(new Lazy(new InMemory)); }); } + + public function properties(): iterable + { + foreach (Adapter::list() as $property) { + yield [$property]; + } + } } diff --git a/tests/Directory/DirectoryTest.php b/tests/Directory/DirectoryTest.php index b9b7203..f79f15c 100644 --- a/tests/Directory/DirectoryTest.php +++ b/tests/Directory/DirectoryTest.php @@ -292,6 +292,61 @@ public function testFilter() $this->assertSame('foobar', unwrap($set)[1]->name()->toString()); } + /** + * @dataProvider properties + */ + public function testEmptyDirectoryHoldProperty($property) + { + $this + ->forAll( + $property, + FName::any(), + ) + ->then(function($property, $name) { + $directory = new Directory($name); + + if (!$property->applicableTo($directory)) { + $this->markTestSkipped(); + } + + $property->ensureHeldBy($directory); + }); + } + + /** + * @dataProvider properties + */ + public function testDirectoryWithSomeFilesHoldProperty($property) + { + $this + ->forAll( + $property, + FName::any(), + FSet::of( + File::class, + new DataSet\Randomize( + FFile::any(), + ), + DataSet\Integers::between(1, 5), // only to speed up tests + ), + ) + ->filter(function($property, $name, $files) { + // do not accept duplicated files + return $files + ->groupBy(fn($file) => $file->name()->toString()) + ->size() === $files->size(); + }) + ->then(function($property, $name, $files) { + $directory = new Directory($name, $files); + + if (!$property->applicableTo($directory)) { + $this->markTestSkipped(); + } + + $property->ensureHeldBy($directory); + }); + } + public function testEmptyDirectoryHoldProperties() { $this @@ -356,4 +411,11 @@ public function testDirectoryLoadedWithDifferentFilesWithTheSameNameThrows() ); }); } + + public function properties(): iterable + { + foreach (PDirectory::list() as $property) { + yield [$property]; + } + } } From 356200c2240b2bd42652ed75119e60fb68171f46 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 9 May 2020 13:29:45 +0200 Subject: [PATCH 41/62] use dedicated group to test only sequence of properties --- .github/workflows/ci.yml | 6 +++++- tests/Adapter/CacheOpenedFilesTest.php | 3 +++ tests/Adapter/FilesystemTest.php | 3 +++ tests/Adapter/InMemoryTest.php | 3 +++ tests/Adapter/LazyTest.php | 3 +++ tests/Directory/DirectoryTest.php | 6 ++++++ tests/Directory/SourceTest.php | 3 +++ 7 files changed, 26 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6cdfe01..eb72a3f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,11 @@ jobs: - name: Install Dependencies run: composer install --no-progress - name: PHPUnit - run: php -dmemory_limit=-1 vendor/bin/phpunit + run: vendor/bin/phpunit --exclude-group properties + env: + BLACKBOX_DETAILED_PROPERTIES: 1 + - name: Properties + run: php -dmemory_limit=-1 vendor/bin/phpunit --group properties env: BLACKBOX_SET_SIZE: 10 BLACKBOX_DETAILED_PROPERTIES: 1 diff --git a/tests/Adapter/CacheOpenedFilesTest.php b/tests/Adapter/CacheOpenedFilesTest.php index 8d48fc5..30fbd2d 100644 --- a/tests/Adapter/CacheOpenedFilesTest.php +++ b/tests/Adapter/CacheOpenedFilesTest.php @@ -163,6 +163,9 @@ public function testHoldProperty($property) }); } + /** + * @group properties + */ public function testHoldProperties() { $this diff --git a/tests/Adapter/FilesystemTest.php b/tests/Adapter/FilesystemTest.php index a61a67d..7d85b1e 100644 --- a/tests/Adapter/FilesystemTest.php +++ b/tests/Adapter/FilesystemTest.php @@ -214,6 +214,9 @@ public function testHoldProperty($property) }); } + /** + * @group properties + */ public function testHoldProperties() { $this diff --git a/tests/Adapter/InMemoryTest.php b/tests/Adapter/InMemoryTest.php index b1fc5ca..4a75262 100644 --- a/tests/Adapter/InMemoryTest.php +++ b/tests/Adapter/InMemoryTest.php @@ -90,6 +90,9 @@ public function testHoldProperty($property) }); } + /** + * @group properties + */ public function testHoldProperties() { $this diff --git a/tests/Adapter/LazyTest.php b/tests/Adapter/LazyTest.php index cc0a5c5..39f70c2 100644 --- a/tests/Adapter/LazyTest.php +++ b/tests/Adapter/LazyTest.php @@ -133,6 +133,9 @@ public function testHoldProperty($property) }); } + /** + * @group properties + */ public function testHoldProperties() { $this diff --git a/tests/Directory/DirectoryTest.php b/tests/Directory/DirectoryTest.php index f79f15c..84fa415 100644 --- a/tests/Directory/DirectoryTest.php +++ b/tests/Directory/DirectoryTest.php @@ -347,6 +347,9 @@ public function testDirectoryWithSomeFilesHoldProperty($property) }); } + /** + * @group properties + */ public function testEmptyDirectoryHoldProperties() { $this @@ -361,6 +364,9 @@ public function testEmptyDirectoryHoldProperties() }); } + /** + * @group properties + */ public function testDirectoryWithSomeFilesHoldProperties() { $this diff --git a/tests/Directory/SourceTest.php b/tests/Directory/SourceTest.php index 29b1d7a..0e184e6 100644 --- a/tests/Directory/SourceTest.php +++ b/tests/Directory/SourceTest.php @@ -103,6 +103,9 @@ public function testShouldNotPersistAFileWhereItWasOpenedInTheSameAdapter() }); } + /** + * @group properties + */ public function testHoldProperties() { $this From 921d5f62ab1268955b60656e43f879ff8298dc32 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 9 May 2020 14:31:16 +0200 Subject: [PATCH 42/62] use sets instead of loading all values in memory --- fixtures/Name.php | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/fixtures/Name.php b/fixtures/Name.php index 960f98c..e96c8c8 100644 --- a/fixtures/Name.php +++ b/fixtures/Name.php @@ -19,24 +19,24 @@ public static function any(): Set static fn(string $first, array $chrs): string => $first.\implode('', $chrs), Set\Decorate::immutable( static fn(int $chr): string => \chr($chr), - Set\Elements::of( - 33, - ...range(1, 8), - ...range(14, 31), - ...range(35, 38), - ...range(40, 46), - ...range(48, 122), - ...range(126, 127), + new Set\Either( + Set\Elements::of(33, 126, 127), + Set\Integers::between(1, 8), + Set\Integers::between(14, 31), + Set\Integers::between(35, 38), + Set\Integers::between(40, 46), + Set\Integers::between(48, 122), + Set\Integers::between(48, 122), ), ), Set\Sequence::of( Set\Decorate::immutable( static fn(int $chr): string => \chr($chr), - Set\Elements::of( - ...range(1, 33), - ...range(35, 38), - ...range(40, 46), - ...range(48, 127), + new Set\Either( + Set\Integers::between(1, 33), + Set\Integers::between(35, 38), + Set\Integers::between(40, 46), + Set\Integers::between(48, 127), ), ), Set\Integers::between(0, 254), From bbfb8c21450dd068ab982554e0f990ea0b966698 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 9 May 2020 16:14:20 +0200 Subject: [PATCH 43/62] reduce the number of scenarii --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb72a3f..be5701c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,7 @@ jobs: - name: PHPUnit run: vendor/bin/phpunit --exclude-group properties env: + BLACKBOX_SET_SIZE: 20 BLACKBOX_DETAILED_PROPERTIES: 1 - name: Properties run: php -dmemory_limit=-1 vendor/bin/phpunit --group properties From 00bf9113b6db3b649c27d2e44f998c36719b529c Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 9 May 2020 16:14:42 +0200 Subject: [PATCH 44/62] disable memory limit for coverage --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index be5701c..acb02c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,7 @@ jobs: - name: Install Dependencies run: composer install --no-progress - name: PHPUnit - run: vendor/bin/phpunit --coverage-clover=coverage.clover + run: php -dmemory_limit=-1 vendor/bin/phpunit --coverage-clover=coverage.clover env: BLACKBOX_SET_SIZE: 1 - uses: codecov/codecov-action@v1 From eae17da0725ccb149e6d6235fb6631ee0cabdfa6 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 9 May 2020 16:30:29 +0200 Subject: [PATCH 45/62] throw an exception when file path to persisted is too long --- src/Adapter/Filesystem.php | 23 ++++++++++++++++++++- src/Exception/PathTooLong.php | 8 ++++++++ tests/Adapter/FilesystemTest.php | 34 ++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/Exception/PathTooLong.php diff --git a/src/Adapter/Filesystem.php b/src/Adapter/Filesystem.php index 9819aa9..1f4b988 100644 --- a/src/Adapter/Filesystem.php +++ b/src/Adapter/Filesystem.php @@ -12,6 +12,8 @@ Source, Exception\FileNotFound, Exception\PathDoesntRepresentADirectory, + Exception\PathTooLong, + Exception\RuntimeException, Event\FileWasRemoved, }; use Innmind\Stream\Writable\Stream; @@ -20,9 +22,13 @@ Exception\InvalidMediaTypeString, }; use Innmind\Url\Path; -use Innmind\Immutable\Set; +use Innmind\Immutable\{ + Set, + Str, +}; use Symfony\Component\{ Filesystem\Filesystem as FS, + Filesystem\Exception\IOException, Finder\Finder, Finder\SplFileInfo, }; @@ -151,6 +157,21 @@ function(Set $persisted, File $file) use ($path): Set { $stream = $file->content(); $stream->rewind(); + + try { + $this->filesystem->touch($path->toString()); + } catch (IOException $e) { + if (Str::of($path->toString(), 'ASCII')->length() > 1014) { + throw new PathTooLong($path->toString(), 0, $e); + } + + throw new RuntimeException( + $e->getMessage(), + (int) $e->getCode(), + $e, + ); + } + $handle = new Stream(\fopen($path->toString(), 'w')); while (!$stream->end()) { diff --git a/src/Exception/PathTooLong.php b/src/Exception/PathTooLong.php new file mode 100644 index 0000000..148d37b --- /dev/null +++ b/src/Exception/PathTooLong.php @@ -0,0 +1,8 @@ +remove($path); + + $filesystem = new Filesystem(Path::of($path)); + + $this->expectException(PathTooLong::class); + + $filesystem->add(new Directory( + new Name(str_repeat('a', 255)), + Set::of( + FileInterface::class, + new Directory( + new Name(str_repeat('a', 255)), + Set::of( + FileInterface::class, + new Directory( + new Name(str_repeat('a', 255)), + Set::of( + FileInterface::class, + new File( + new Name(str_repeat('a', 255)), + Stream::ofContent('foo') + ) + ) + ) + ) + ) + ) + )); + } + public function properties(): iterable { foreach (PAdapter::list() as $property) { From d43f0af73e2bae30bbbf40f2091b44e52c3030bb Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 9 May 2020 16:39:51 +0200 Subject: [PATCH 46/62] disable memory limit --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index acb02c6..cc63f82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: - name: Install Dependencies run: composer install --no-progress - name: PHPUnit - run: vendor/bin/phpunit --exclude-group properties + run: php -dmemory_limit=-1 vendor/bin/phpunit --exclude-group properties env: BLACKBOX_SET_SIZE: 20 BLACKBOX_DETAILED_PROPERTIES: 1 From c70c219e2a2400220f76bf7ab608449962bbe488 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 9 May 2020 17:15:49 +0200 Subject: [PATCH 47/62] remove restriction on file names as it was probably mistaken to max file path --- fixtures/Name.php | 22 ++----- src/Name.php | 12 +--- tests/Adapter/FilesystemTest.php | 99 +++++++++++++++++++++++++++++++- tests/NameTest.php | 74 +++--------------------- 4 files changed, 110 insertions(+), 97 deletions(-) diff --git a/fixtures/Name.php b/fixtures/Name.php index e96c8c8..ca26652 100644 --- a/fixtures/Name.php +++ b/fixtures/Name.php @@ -15,31 +15,17 @@ public static function any(): Set { return Set\Decorate::immutable( static fn(string $name): Model => new Model($name), - Set\Composite::immutable( - static fn(string $first, array $chrs): string => $first.\implode('', $chrs), - Set\Decorate::immutable( - static fn(int $chr): string => \chr($chr), - new Set\Either( - Set\Elements::of(33, 126, 127), - Set\Integers::between(1, 8), - Set\Integers::between(14, 31), - Set\Integers::between(35, 38), - Set\Integers::between(40, 46), - Set\Integers::between(48, 122), - Set\Integers::between(48, 122), - ), - ), + Set\Decorate::immutable( + static fn(array $chrs): string => \implode('', $chrs), Set\Sequence::of( Set\Decorate::immutable( static fn(int $chr): string => \chr($chr), new Set\Either( - Set\Integers::between(1, 33), - Set\Integers::between(35, 38), - Set\Integers::between(40, 46), + Set\Integers::between(1, 46), Set\Integers::between(48, 127), ), ), - Set\Integers::between(0, 254), + Set\Integers::between(1, 255), ), )->filter(static fn(string $name): bool => $name !== '.' && $name !== '..'), ); diff --git a/src/Name.php b/src/Name.php index 188b346..14f2e45 100644 --- a/src/Name.php +++ b/src/Name.php @@ -25,7 +25,6 @@ public function __construct(string $value) } $this->assertContainsOnlyValidCharacters($value); - $this->assertFirstCharacterValid($value); if ($value === '.' || $value === '..') { // as they are special links on unix filesystems @@ -48,7 +47,7 @@ public function toString(): string private function assertContainsOnlyValidCharacters(string $value): void { $value = Str::of($value); - $invalid = [0, 34, 39, ...range(128, 255)]; + $invalid = [0, ...range(128, 255)]; foreach ($invalid as $ord) { if ($value->contains(\chr($ord))) { @@ -56,13 +55,4 @@ private function assertContainsOnlyValidCharacters(string $value): void } } } - - private function assertFirstCharacterValid(string $value): void - { - $index = \ord(Str::of($value, 'ASCII')->take(1)->toString()); - - if (\in_array($index, [32, ...range(9, 13), ...range(123, 125)], true)) { - throw new DomainException($value); - } - } } diff --git a/tests/Adapter/FilesystemTest.php b/tests/Adapter/FilesystemTest.php index 81ef1f7..acef97c 100644 --- a/tests/Adapter/FilesystemTest.php +++ b/tests/Adapter/FilesystemTest.php @@ -21,7 +21,11 @@ use Innmind\Immutable\Set; use Symfony\Component\Filesystem\Filesystem as FS; use PHPUnit\Framework\TestCase; -use Innmind\BlackBox\PHPUnit\BlackBox; +use Innmind\BlackBox\{ + PHPUnit\BlackBox, + Set as DataSet, +}; +use Fixtures\Innmind\Filesystem\Name as FName; use Properties\Innmind\Filesystem\Adapter as PAdapter; class FilesystemTest extends TestCase @@ -265,6 +269,99 @@ public function testPathTooLongThrowAnException() )); } + public function testPersistedNameCanStartWithAnyAsciiCharacter() + { + $this + ->forAll( + new DataSet\Either( + DataSet\Integers::between(1, 46), + DataSet\Integers::between(48, 127), + ), + DataSet\Strings::any(), + ) + ->then(function($ord, $content) { + $path = \sys_get_temp_dir().'/innmind/filesystem/'; + (new FS)->remove($path); + + $filesystem = new Filesystem(Path::of($path)); + + $this->assertNull($filesystem->add(new Directory( + new Name(chr($ord).'a'), + Set::of( + FileInterface::class, + new File( + new Name('a'), + Stream::ofContent($content), + ), + ), + ))); + + (new FS)->remove($path); + }); + } + + public function testPersistedNameCanContainWithAnyAsciiCharacter() + { + $this + ->forAll( + new DataSet\Either( + DataSet\Integers::between(1, 46), + DataSet\Integers::between(48, 127), + ), + DataSet\Strings::any(), + ) + ->then(function($ord, $content) { + $path = \sys_get_temp_dir().'/innmind/filesystem/'; + (new FS)->remove($path); + + $filesystem = new Filesystem(Path::of($path)); + + $this->assertNull($filesystem->add(new Directory( + new Name('a'.chr($ord).'a'), + Set::of( + FileInterface::class, + new File( + new Name('a'), + Stream::ofContent($content), + ), + ), + ))); + + (new FS)->remove($path); + }); + } + + public function testPersistedNameCanContainOnlyOneAsciiCharacter() + { + $this + ->forAll( + new DataSet\Either( + DataSet\Integers::between(1, 45), + DataSet\Integers::between(48, 127), + ), + DataSet\Strings::any(), + ) + ->then(function($ord, $content) { + $path = \sys_get_temp_dir().'/innmind/filesystem/'; + (new FS)->remove($path); + + $filesystem = new Filesystem(Path::of($path)); + + $this->assertNull($filesystem->add(new Directory( + new Name(chr($ord)), + Set::of( + FileInterface::class, + new File( + new Name('a'), + Stream::ofContent($content), + ), + ), + ))); + + (new FS)->remove($path); + }); + } + public function properties(): iterable { foreach (PAdapter::list() as $property) { diff --git a/tests/NameTest.php b/tests/NameTest.php index 0897e42..da93c22 100644 --- a/tests/NameTest.php +++ b/tests/NameTest.php @@ -116,24 +116,6 @@ public function testDotFoldersAreNotAccepted() }); } - public function testNamesContainingOnlyOneCharacterOutsideOfAllowedRangeAreNotAccepted() - { - $this - ->forAll(Set\Elements::of( - 0, - 32, - 47, - ...range(9, 13), - ...range(123, 125), - ...range(128, 255), - )) - ->then(function($invalid) { - $this->expectException(DomainException::class); - - new Name(\chr($invalid)); - }); - } - public function testNamesContainingCharOrdAbove127IsNotAccepted() { $this @@ -154,45 +136,17 @@ public function testChr0IsNotAccepted() new Name('a'.\chr(0).'a'); } - public function testSingleQuoteIsNotAccepted() - { - $this->expectException(DomainException::class); - - new Name("a'a"); - } - - public function testDoubleQuoteIsNotAccepted() - { - $this->expectException(DomainException::class); - - new Name('a"a'); - } - public function testNamesLongerThan255AreNotAccepted() { $this ->forAll( - Set\Composite::immutable( - static fn(string $first, array $chrs): string => $first.\implode('', $chrs), - Set\Decorate::immutable( - static fn(int $chr): string => \chr($chr), - Set\Elements::of( - 33, - ...range(1, 8), - ...range(14, 31), - ...range(35, 38), - ...range(40, 46), - ...range(48, 122), - ...range(126, 127), - ), - ), + Set\Decorate::immutable( + static fn(array $chrs): string => \implode('', $chrs), Set\Sequence::of( Set\Decorate::immutable( static fn(int $chr): string => \chr($chr), Set\Elements::of( - ...range(1, 33), - ...range(35, 38), - ...range(40, 46), + ...range(1, 46), // chr(47) alias '/' not accepted ...range(48, 127), ), @@ -210,31 +164,17 @@ public function testNamesLongerThan255AreNotAccepted() private function valid(): Set { - return Set\Composite::immutable( - static fn(string $first, array $chrs): string => $first.\implode('', $chrs), - Set\Decorate::immutable( - static fn(int $chr): string => \chr($chr), - Set\Elements::of( - 33, - ...range(1, 8), - ...range(14, 31), - ...range(35, 38), - ...range(40, 46), - ...range(48, 122), - ...range(126, 127), - ), - ), + return Set\Decorate::immutable( + static fn(array $chrs): string => \implode('', $chrs), Set\Sequence::of( Set\Decorate::immutable( static fn(int $chr): string => \chr($chr), Set\Elements::of( - ...range(1, 33), - ...range(35, 38), - ...range(40, 46), + ...range(1, 46), ...range(48, 127), ), ), - Set\Integers::between(0, 254), + Set\Integers::between(1, 255), ), )->filter(static fn(string $name): bool => $name !== '.' && $name !== '..'); } From 5cced12b6fc516949ca38d5a7af96b2668f3faf0 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 9 May 2020 17:24:39 +0200 Subject: [PATCH 48/62] reduce the depth of generated directories to avoid hitting the filesystem path length limit --- fixtures/Directory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fixtures/Directory.php b/fixtures/Directory.php index 879185c..8209b0d 100644 --- a/fixtures/Directory.php +++ b/fixtures/Directory.php @@ -20,7 +20,7 @@ final class Directory */ public static function any(): DataSet { - return self::atDepth(0, 2); + return self::atDepth(0, 1); } /** From 6ebfe2610b48888a7553160fe176f170fbd87acb Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 9 May 2020 17:46:54 +0200 Subject: [PATCH 49/62] use directory properties to randomly add/remove file to/from the generated directory --- fixtures/Directory.php | 76 +++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/fixtures/Directory.php b/fixtures/Directory.php index 8209b0d..dd90469 100644 --- a/fixtures/Directory.php +++ b/fixtures/Directory.php @@ -7,7 +7,11 @@ Directory\Directory as Model, File as FileInterface, }; -use Innmind\BlackBox\Set as DataSet; +use Properties\Innmind\Filesystem\Directory as Properties; +use Innmind\BlackBox\{ + Set as DataSet, + Properties as Ensure, +}; use Fixtures\Innmind\Immutable\Set; use function Innmind\Immutable\unwrap; @@ -41,12 +45,6 @@ private static function atDepth(int $depth, int $maxDepth): DataSet ), DataSet\Integers::between(0, 5), ); - $toAdd = DataSet\Sequence::of( - new DataSet\Randomize( - File::any(), - ), - DataSet\Integers::between(0, 5), - ); } else { $files = Set::of( FileInterface::class, @@ -67,44 +65,40 @@ private static function atDepth(int $depth, int $maxDepth): DataSet ->groupBy(static fn($file) => $file->name()->toString()) ->size() === $files->size(); }); - $toAdd = DataSet\Sequence::of( - new DataSet\Either( - new DataSet\Randomize( - File::any(), - ), - self::atDepth($depth + 1, $maxDepth), - ), - DataSet\Integers::between(0, 5), - ); } - return DataSet\Composite::immutable( - static function($name, $files, $toAdd, $numberToRemove): Model { - $directory = new Model( - $name, - $files, - ); - - foreach ($toAdd as $file) { - $directory = $directory->add($file); - } - - $files = \array_merge( - unwrap($files), - $toAdd, - ); - $toRemove = \array_slice($files, 0, $numberToRemove); - - foreach ($toRemove as $file) { - $directory = $directory->remove($file->name()); - } - - return $directory; - }, + $directory = DataSet\Composite::immutable( + static fn($name, $files): Model => new Model( + $name, + $files, + ), Name::any(), $files, - $toAdd, - DataSet\Integers::between(0, 10), + ); + + $modified = DataSet\Composite::immutable( + static fn($directory, $properties): Model => $properties->ensureHeldBy($directory), + $directory, + DataSet\Decorate::immutable( + static fn(array $properties): Ensure => new Ensure(...$properties), + DataSet\Sequence::of( + new DataSet\Either( + DataSet\Property::of( + Properties\RemoveFile::class, + ), + DataSet\Property::of( + Properties\AddFile::class, + File::any(), + ), + ), + DataSet\Integers::between(1, 10), + ), + ), + ); + + return new DataSet\Either( + $directory, + $modified ); } } From 44db603aa7eba570ef415f2edb23b3941642d230 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 9 May 2020 20:12:16 +0200 Subject: [PATCH 50/62] 1014 file path limit only applies to macOS --- src/Adapter/Filesystem.php | 2 +- tests/Adapter/FilesystemTest.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Adapter/Filesystem.php b/src/Adapter/Filesystem.php index 1f4b988..d7087e7 100644 --- a/src/Adapter/Filesystem.php +++ b/src/Adapter/Filesystem.php @@ -161,7 +161,7 @@ function(Set $persisted, File $file) use ($path): Set { try { $this->filesystem->touch($path->toString()); } catch (IOException $e) { - if (Str::of($path->toString(), 'ASCII')->length() > 1014) { + if (\PHP_OS === 'Darwin' && Str::of($path->toString(), 'ASCII')->length() > 1014) { throw new PathTooLong($path->toString(), 0, $e); } diff --git a/tests/Adapter/FilesystemTest.php b/tests/Adapter/FilesystemTest.php index acef97c..a558045 100644 --- a/tests/Adapter/FilesystemTest.php +++ b/tests/Adapter/FilesystemTest.php @@ -238,6 +238,10 @@ public function testHoldProperties() public function testPathTooLongThrowAnException() { + if (\PHP_OS !== 'Darwin') { + $this->markTestSkipped(); + } + $path = \sys_get_temp_dir().'/innmind/filesystem/'; (new FS)->remove($path); From f2670e7b462f9ef95c4fc971f2a929b722fb57c2 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 9 May 2020 21:17:29 +0200 Subject: [PATCH 51/62] do not allow names with space characters only --- fixtures/Name.php | 6 +++++- src/Name.php | 5 +++++ tests/NameTest.php | 23 ++++++++++++++++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/fixtures/Name.php b/fixtures/Name.php index ca26652..e60c945 100644 --- a/fixtures/Name.php +++ b/fixtures/Name.php @@ -27,7 +27,11 @@ public static function any(): Set ), Set\Integers::between(1, 255), ), - )->filter(static fn(string $name): bool => $name !== '.' && $name !== '..'), + )->filter( + static fn(string $name): bool => $name !== '.' && + $name !== '..' && + !\preg_match('~\s+~', $name) + ), ); } } diff --git a/src/Name.php b/src/Name.php index 14f2e45..5928142 100644 --- a/src/Name.php +++ b/src/Name.php @@ -24,6 +24,11 @@ public function __construct(string $value) throw new DomainException("A file name can't contain a slash, $value given"); } + // name with only _spaces_ are not accepted as it is not as valid path + if (Str::of($value)->matches('~^\s+$~')) { + throw new DomainException($value); + } + $this->assertContainsOnlyValidCharacters($value); if ($value === '.' || $value === '..') { diff --git a/tests/NameTest.php b/tests/NameTest.php index da93c22..5995508 100644 --- a/tests/NameTest.php +++ b/tests/NameTest.php @@ -162,6 +162,24 @@ public function testNamesLongerThan255AreNotAccepted() }); } + public function testNameWithOnlyWhiteSpacesIsNotAccepted() + { + $this + ->forAll(Set\Elements::of( + 32, + ...range(9, 13), + )) + ->then(function($ord) { + try { + new Name(chr($ord)); + + $this->fail('it should throw'); + } catch (DomainException $e) { + $this->assertTrue(true); + } + }); + } + private function valid(): Set { return Set\Decorate::immutable( @@ -176,6 +194,9 @@ private function valid(): Set ), Set\Integers::between(1, 255), ), - )->filter(static fn(string $name): bool => $name !== '.' && $name !== '..'); + )->filter(static fn(string $name): bool => $name !== '.' && + $name !== '..' && + !\preg_match('~^\s+$~', $name) + ); } } From d39ac727ea6bf2ad2cfdca46cb58be01ee6a752c Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 9 May 2020 21:17:41 +0200 Subject: [PATCH 52/62] CS --- src/Name.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Name.php b/src/Name.php index 5928142..ff6b122 100644 --- a/src/Name.php +++ b/src/Name.php @@ -52,7 +52,7 @@ public function toString(): string private function assertContainsOnlyValidCharacters(string $value): void { $value = Str::of($value); - $invalid = [0, ...range(128, 255)]; + $invalid = [0, ...\range(128, 255)]; foreach ($invalid as $ord) { if ($value->contains(\chr($ord))) { From 56114029430d2c28c8b924c8c4b1e083612e5751 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sat, 9 May 2020 21:32:56 +0200 Subject: [PATCH 53/62] exclude forbidden value from scenarii --- tests/Adapter/FilesystemTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Adapter/FilesystemTest.php b/tests/Adapter/FilesystemTest.php index a558045..8e25a08 100644 --- a/tests/Adapter/FilesystemTest.php +++ b/tests/Adapter/FilesystemTest.php @@ -340,7 +340,9 @@ public function testPersistedNameCanContainOnlyOneAsciiCharacter() $this ->forAll( new DataSet\Either( - DataSet\Integers::between(1, 45), + DataSet\Integers::between(1, 8), + DataSet\Integers::between(14, 31), + DataSet\Integers::between(33, 45), DataSet\Integers::between(48, 127), ), DataSet\Strings::any(), From 668fe80e6ef8eae319c343a4b6912a4a0520073d Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 10 May 2020 10:32:36 +0200 Subject: [PATCH 54/62] remove unreachable code --- src/Adapter/Filesystem.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Adapter/Filesystem.php b/src/Adapter/Filesystem.php index d7087e7..f7f0e3f 100644 --- a/src/Adapter/Filesystem.php +++ b/src/Adapter/Filesystem.php @@ -78,10 +78,6 @@ public function get(Name $file): File */ public function contains(Name $file): bool { - if (\in_array($file->toString(), self::INVALID_FILES, true)) { - return false; - } - return $this->filesystem->exists($this->path->toString().'/'.$file->toString()); } From 604fee2de57cafe3e73f585a03b2fc55fb2fb66d Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 10 May 2020 11:12:09 +0200 Subject: [PATCH 55/62] add filtering properties --- properties/Directory.php | 7 ++++ .../FilteringDoesntAffectTheDirectory.php | 31 ++++++++++++++ .../FilteringRetunsTheExpectedElements.php | 42 +++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 properties/Directory/FilteringDoesntAffectTheDirectory.php create mode 100644 properties/Directory/FilteringRetunsTheExpectedElements.php diff --git a/properties/Directory.php b/properties/Directory.php index fcc4202..9ade347 100644 --- a/properties/Directory.php +++ b/properties/Directory.php @@ -69,6 +69,13 @@ public static function list(): array Directory\AddDirectory::class, Name::any(), ), + Set\Property::of( + Directory\FilteringDoesntAffectTheDirectory::class, + ), + Set\Property::of( + Directory\FilteringRetunsTheExpectedElements::class, + File::any(), + ), ]; } } diff --git a/properties/Directory/FilteringDoesntAffectTheDirectory.php b/properties/Directory/FilteringDoesntAffectTheDirectory.php new file mode 100644 index 0000000..7414507 --- /dev/null +++ b/properties/Directory/FilteringDoesntAffectTheDirectory.php @@ -0,0 +1,31 @@ +filter(static fn(): bool => true); + $set = $directory->filter(static fn(): bool => false); + + Assert::assertTrue($set->empty()); + $files->foreach(static fn($file) => Assert::assertTrue($directory->contains($file->name()))); + + return $directory; + } +} diff --git a/properties/Directory/FilteringRetunsTheExpectedElements.php b/properties/Directory/FilteringRetunsTheExpectedElements.php new file mode 100644 index 0000000..48bb158 --- /dev/null +++ b/properties/Directory/FilteringRetunsTheExpectedElements.php @@ -0,0 +1,42 @@ +file = $file; + } + + public function name(): string + { + return 'Filtering returns the expected elements'; + } + + public function applicableTo(object $directory): bool + { + return true; + } + + public function ensureHeldBy(object $directory): object + { + $shouldBeEmpty = $directory->filter(fn($file): bool => $file === $this->file); + $shouldContainsOurFile = $directory + ->add($this->file) + ->filter(fn($file): bool => $file === $this->file); + + Assert::assertCount(0, $shouldBeEmpty); + Assert::assertCount(1, $shouldContainsOurFile); + Assert::assertTrue($shouldContainsOurFile->contains($this->file)); + + return $directory; + } +} From 829c1a1014eb1d4cb821a2e8e80ad96a1b50328e Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 10 May 2020 11:14:56 +0200 Subject: [PATCH 56/62] ignore fixtures and properties from coverage --- codecov.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..efab03f --- /dev/null +++ b/codecov.yml @@ -0,0 +1,3 @@ +ignore_paths: + - fixtures/* + - properties/* From 6d6e56d0108348410ab6459bf7237609ab3dbfa0 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 10 May 2020 12:00:55 +0200 Subject: [PATCH 57/62] add properties for Directory::replaceAt --- properties/Directory.php | 20 +++++ .../Directory/ReplaceFileInSubDirectory.php | 89 +++++++++++++++++++ ...ngFileAtEmptyPathIsSameAsAddingTheFile.php | 58 ++++++++++++ ...PathTargetingAFileMustThrowAnException.php | 53 +++++++++++ ...gFileAtUnknownPathMustThrowAnException.php | 51 +++++++++++ src/Directory/Directory.php | 2 +- 6 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 properties/Directory/ReplaceFileInSubDirectory.php create mode 100644 properties/Directory/ReplacingFileAtEmptyPathIsSameAsAddingTheFile.php create mode 100644 properties/Directory/ReplacingFileAtPathTargetingAFileMustThrowAnException.php create mode 100644 properties/Directory/ReplacingFileAtUnknownPathMustThrowAnException.php diff --git a/properties/Directory.php b/properties/Directory.php index 9ade347..d14e217 100644 --- a/properties/Directory.php +++ b/properties/Directory.php @@ -76,6 +76,26 @@ public static function list(): array Directory\FilteringRetunsTheExpectedElements::class, File::any(), ), + Set\Property::of( + Directory\ReplacingFileAtEmptyPathIsSameAsAddingTheFile::class, + File::any(), + ), + Set\Property::of( + Directory\ReplacingFileAtUnknownPathMustThrowAnException::class, + Name::any(), + File::any(), + ), + Set\Property::of( + Directory\ReplacingFileAtPathTargetingAFileMustThrowAnException::class, + File::any(), + File::any(), + ), + Set\Property::of( + Directory\ReplaceFileInSubDirectory::class, + Name::any(), + Name::any(), + File::any(), + ), ]; } } diff --git a/properties/Directory/ReplaceFileInSubDirectory.php b/properties/Directory/ReplaceFileInSubDirectory.php new file mode 100644 index 0000000..c0b298b --- /dev/null +++ b/properties/Directory/ReplaceFileInSubDirectory.php @@ -0,0 +1,89 @@ +level1 = $level1; + $this->level2 = $level2; + $this->file = $file; + } + + public function name(): string + { + return 'Replace file in sub directory'; + } + + public function applicableTo(object $directory): bool + { + return !$directory->contains($this->level1); + } + + public function ensureHeldBy(object $directory): object + { + $directory = $directory->add(new Directory( + $this->level1, + Set::of( + File::class, + new Directory($this->level2), + ), + )); + $newDirectory = $directory->replaceAt( + Path::of('/'.$this->level1->toString().'/'.$this->level2->toString().'/'), + $this->file, + ); + + Assert::assertNotSame($directory, $newDirectory); + Assert::assertFalse( + $directory + ->get($this->level1) + ->get($this->level2) + ->contains($this->file->name()) + ); + Assert::assertTrue( + $newDirectory + ->get($this->level1) + ->get($this->level2) + ->contains($this->file->name()) + ); + Assert::assertInstanceOf( + FileWasAdded::class, + $newDirectory->modifications()->last(), + ); + Assert::assertInstanceOf( + FileWasAdded::class, + $newDirectory + ->get($this->level1) + ->modifications() + ->last(), + ); + Assert::assertInstanceOf( + FileWasAdded::class, + $newDirectory + ->get($this->level1) + ->get($this->level2) + ->modifications() + ->last(), + ); + + return $newDirectory; + } +} diff --git a/properties/Directory/ReplacingFileAtEmptyPathIsSameAsAddingTheFile.php b/properties/Directory/ReplacingFileAtEmptyPathIsSameAsAddingTheFile.php new file mode 100644 index 0000000..75ea2b2 --- /dev/null +++ b/properties/Directory/ReplacingFileAtEmptyPathIsSameAsAddingTheFile.php @@ -0,0 +1,58 @@ +file = $file; + } + + public function name(): string + { + return 'Replacing file at empty path is same as adding the file'; + } + + public function applicableTo(object $directory): bool + { + return !$directory->contains($this->file->name()); + } + + public function ensureHeldBy(object $directory): object + { + $files = $directory->filter(static fn(): bool => true); + $newDirectory = $directory->replaceAt(Path::of('/'), $this->file); + + Assert::assertNotSame($directory, $newDirectory); + Assert::assertFalse($directory->contains($this->file->name())); + Assert::assertTrue($newDirectory->contains($this->file->name())); + $files->foreach(fn($file) => Assert::assertTrue($directory->contains($file->name()))); + $files->foreach(fn($file) => Assert::assertTrue($newDirectory->contains($file->name()))); + Assert::assertGreaterThan( + $directory->modifications()->size(), + $newDirectory->modifications()->size(), + ); + Assert::assertInstanceOf( + FileWasAdded::class, + $newDirectory->modifications()->last(), + ); + Assert::assertSame( + $this->file, + $newDirectory->modifications()->last()->file(), + ); + + return $newDirectory; + } +} diff --git a/properties/Directory/ReplacingFileAtPathTargetingAFileMustThrowAnException.php b/properties/Directory/ReplacingFileAtPathTargetingAFileMustThrowAnException.php new file mode 100644 index 0000000..d6db728 --- /dev/null +++ b/properties/Directory/ReplacingFileAtPathTargetingAFileMustThrowAnException.php @@ -0,0 +1,53 @@ +target = $target; + $this->file = $file; + } + + public function name(): string + { + return 'Replacing file at path targeting a file must throw an exception'; + } + + public function applicableTo(object $directory): bool + { + return !$directory->contains($this->target->name()); + } + + public function ensureHeldBy(object $directory): object + { + try { + $directory + ->add($this->target) + ->replaceAt( + Path::of($this->target->name()->toString()), + $this->file, + ); + + Assert::fail('it should throw'); + } catch (LogicException $e) { + Assert::assertTrue(true); + } + + return $directory; + } +} diff --git a/properties/Directory/ReplacingFileAtUnknownPathMustThrowAnException.php b/properties/Directory/ReplacingFileAtUnknownPathMustThrowAnException.php new file mode 100644 index 0000000..b4bb259 --- /dev/null +++ b/properties/Directory/ReplacingFileAtUnknownPathMustThrowAnException.php @@ -0,0 +1,51 @@ +unknown = $unknown; + $this->file = $file; + } + + public function name(): string + { + return 'Replacing file at unknown path must throw an exception'; + } + + public function applicableTo(object $directory): bool + { + return !$directory->contains($this->unknown); + } + + public function ensureHeldBy(object $directory): object + { + try { + $directory->replaceAt( + Path::of($this->unknown->toString()), + $this->file, + ); + + Assert::fail('it should throw'); + } catch (FileNotFound $e) { + Assert::assertTrue(true); + } + + return $directory; + } +} diff --git a/src/Directory/Directory.php b/src/Directory/Directory.php index 22c678b..e78e273 100644 --- a/src/Directory/Directory.php +++ b/src/Directory/Directory.php @@ -182,7 +182,7 @@ public function remove(Name $name): DirectoryInterface */ public function replaceAt(Path $path, File $file): DirectoryInterface { - $normalizedPath = Str::of($path->toString())->leftTrim('/'); + $normalizedPath = Str::of($path->toString())->trim('/'); $pieces = $normalizedPath->split('/'); if ($normalizedPath->empty()) { From 7b71e09add4a4889064bd64e1ecfd91435f1f88e Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 10 May 2020 12:03:35 +0200 Subject: [PATCH 58/62] remove duplication to generate valid names --- fixtures/Name.php | 36 ++++++++++++++++++++++-------------- tests/NameTest.php | 33 +++++++-------------------------- 2 files changed, 29 insertions(+), 40 deletions(-) diff --git a/fixtures/Name.php b/fixtures/Name.php index e60c945..602f99e 100644 --- a/fixtures/Name.php +++ b/fixtures/Name.php @@ -15,23 +15,31 @@ public static function any(): Set { return Set\Decorate::immutable( static fn(string $name): Model => new Model($name), - Set\Decorate::immutable( - static fn(array $chrs): string => \implode('', $chrs), - Set\Sequence::of( - Set\Decorate::immutable( - static fn(int $chr): string => \chr($chr), - new Set\Either( - Set\Integers::between(1, 46), - Set\Integers::between(48, 127), - ), + self::strings(), + ); + } + + /** + * @return Set + */ + public static function strings(): Set + { + return Set\Decorate::immutable( + static fn(array $chrs): string => \implode('', $chrs), + Set\Sequence::of( + Set\Decorate::immutable( + static fn(int $chr): string => \chr($chr), + new Set\Either( + Set\Integers::between(1, 46), + Set\Integers::between(48, 127), ), - Set\Integers::between(1, 255), ), - )->filter( - static fn(string $name): bool => $name !== '.' && - $name !== '..' && - !\preg_match('~\s+~', $name) + Set\Integers::between(1, 255), ), + )->filter( + static fn(string $name): bool => $name !== '.' && + $name !== '..' && + !\preg_match('~\s+~', $name) ); } } diff --git a/tests/NameTest.php b/tests/NameTest.php index 5995508..5cac137 100644 --- a/tests/NameTest.php +++ b/tests/NameTest.php @@ -7,6 +7,7 @@ Name, Exception\DomainException, }; +use Fixtures\Innmind\Filesystem\Name as Fixture; use PHPUnit\Framework\TestCase; use Innmind\BlackBox\{ PHPUnit\BlackBox, @@ -50,7 +51,7 @@ public function testAcceptsAnyValueNotContainingASlash() { $this ->forAll( - $this->valid(), + Fixture::strings(), ) ->then(function($value) { $name = new Name($value); @@ -63,8 +64,8 @@ public function testNameContainingASlashIsNotAccepted() { $this ->forAll( - $this->valid(), - $this->valid(), + Fixture::strings(), + Fixture::strings(), ) ->then(function($a, $b) { $this->expectException(DomainException::class); @@ -77,7 +78,7 @@ public function testNameEqualsItself() { $this ->forAll( - $this->valid(), + Fixture::strings(), ) ->then(function($value) { $name1 = new Name($value); @@ -92,8 +93,8 @@ public function testNameDoesntEqualDifferentName() { $this ->forAll( - $this->valid(), - $this->valid(), + Fixture::strings(), + Fixture::strings(), ) ->then(function($a, $b) { $name1 = new Name($a); @@ -179,24 +180,4 @@ public function testNameWithOnlyWhiteSpacesIsNotAccepted() } }); } - - private function valid(): Set - { - return Set\Decorate::immutable( - static fn(array $chrs): string => \implode('', $chrs), - Set\Sequence::of( - Set\Decorate::immutable( - static fn(int $chr): string => \chr($chr), - Set\Elements::of( - ...range(1, 46), - ...range(48, 127), - ), - ), - Set\Integers::between(1, 255), - ), - )->filter(static fn(string $name): bool => $name !== '.' && - $name !== '..' && - !\preg_match('~^\s+$~', $name) - ); - } } From b44c3b94c47d0015b0c13d5fcf5106b3503831dc Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 10 May 2020 12:07:59 +0200 Subject: [PATCH 59/62] prove the authorized names always constitutes a valid path --- tests/NameTest.php | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/NameTest.php b/tests/NameTest.php index 5cac137..d58ecfa 100644 --- a/tests/NameTest.php +++ b/tests/NameTest.php @@ -7,12 +7,13 @@ Name, Exception\DomainException, }; -use Fixtures\Innmind\Filesystem\Name as Fixture; +use Innmind\Url\Path; use PHPUnit\Framework\TestCase; use Innmind\BlackBox\{ PHPUnit\BlackBox, Set, }; +use Fixtures\Innmind\Filesystem\Name as Fixture; class NameTest extends TestCase { @@ -180,4 +181,25 @@ public function testNameWithOnlyWhiteSpacesIsNotAccepted() } }); } + + public function testAnySequenceOfNamesConstitutesAValidPath() + { + $this + ->forAll(Set\Sequence::of( + Fixture::any(), + Set\Integers::between(1, 10), // enough to prove the behaviour + )) + ->then(function($names) { + $strings = \array_map( + fn($name) => $name->toString(), + $names, + ); + $path = '/'.\implode('/', $strings); + + $this->assertInstanceOf( + Path::class, + Path::of($path), + ); + }); + } } From d3da1878436cdcb67074aca4a846c90d07931ac9 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 10 May 2020 12:40:35 +0200 Subject: [PATCH 60/62] add documentation to explain how to use properties to test new implementations --- README.md | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/README.md b/README.md index e1f3f53..5e7b4ff 100644 --- a/README.md +++ b/README.md @@ -54,3 +54,65 @@ $adapter = new Lazy(new Filesystem(Path::of('/var/www/web'))); $adapter->add($directory); // nothing is written to disk $adapter->persist(); // every new files are persisted, and removals occur at this time as well ``` + +## Properties + +This library allows you to extend its behaviour by creating new implementations of the exposed interfaces ([`File`](src/File.php), [`Directory`](src/Directory.php) and [`Adapter`](src/Adapter.php)). The interfaces are strict enough to guide you through the expected behaviour but the type system can't express all of them, leaving the door open to inconsistencies between implementations. That's why the library expose a set of properties (as declared by [`innmind/black-box`](https://packagist.org/packages/innmind/black-box)) to help you make sure your implementations fulfill the expected behaviours. + +You can test properties on your adapter as follow (with PHPUnit): + +```php +use Properties\Innmind\Filesystem\Adapter; +use Innmind\BlackBox\PHPUnit\BlackBox; +use PHPUnit\Framework\TestCase; + +class MyAdapterTest extends TestCase +{ + use BlackBox; + + /** + * This test will make sure each property is held by your adapter + * + * @dataProvider properties + */ + public function testHoldProperty($property) + { + $this + ->forAll($property) + ->then(function($property) { + $adapter = /* instanciate you implementation here */; + + if (!$property->applicableTo($adapter)) { + $this->markTestSkipped(); + } + + $property->ensureHeldBy($adapter); + }); + } + + /** + * This test will try to prove your adapter hold any sequence of property + * + * This is useful to find bugs due to state mismanage + */ + public function testHoldProperties() + { + $this + ->forAll(Adapter::properties()) + ->then(function($properties) { + $properties->ensureHeldBy(/* instanciate you implementation here */); + }); + } + + public function properties(): iterable + { + foreach (Adapter::list() as $property) { + yield [$property]; + } + } +} +``` + +You can use the same logic to test `Directory` implementations with `Properties\Innmind\Filesystem\Directory`. + +**Note**: there is no properties for the `File` interface as it doesn't expose any behaviour. From 89de659e2d205fe4a745f04df206eef24d22c777 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 10 May 2020 12:47:04 +0200 Subject: [PATCH 61/62] fix error in documentation --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5e7b4ff..f8c8ca8 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,11 @@ $directory = Directory::named('uploads')->add( Stream::open($_FILES['my_upload']['tmp_name']) ) ); -$adapter = new Filesystem(Path::of('/var/www/web')); +$adapter = new Filesystem(Path::of('/var/www/web/')); $adapter->add($directory); ``` -This example show you how you can create a new directory `uploads` in the folder `/var/www/web` of your filesystem and create the uploaded file into it. +This example show you how you can create a new directory `uploads` in the folder `/var/www/web/` of your filesystem and create the uploaded file into it. **Note**: For performance reasons the filesystem adapter only persist to disk the files that have changed (achievable via the immutable nature of file objects). From dd720d5ca44e90f81b6fec7ea9afe8ee4c7c9368 Mon Sep 17 00:00:00 2001 From: Baptiste Langlade Date: Sun, 10 May 2020 14:40:07 +0200 Subject: [PATCH 62/62] fix unicity filter not being applied in every case --- fixtures/Directory.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/fixtures/Directory.php b/fixtures/Directory.php index dd90469..0618f11 100644 --- a/fixtures/Directory.php +++ b/fixtures/Directory.php @@ -55,16 +55,7 @@ private static function atDepth(int $depth, int $maxDepth): DataSet self::atDepth($depth + 1, $maxDepth), ), DataSet\Integers::between(0, 5), - )->filter(static function($files): bool { - if ($files->empty()) { - return true; - } - - // do not accept duplicated files - return $files - ->groupBy(static fn($file) => $file->name()->toString()) - ->size() === $files->size(); - }); + ); } $directory = DataSet\Composite::immutable( @@ -73,7 +64,16 @@ private static function atDepth(int $depth, int $maxDepth): DataSet $files, ), Name::any(), - $files, + $files->filter(static function($files): bool { + if ($files->empty()) { + return true; + } + + // do not accept duplicated files + return $files + ->groupBy(static fn($file) => $file->name()->toString()) + ->size() === $files->size(); + }), ); $modified = DataSet\Composite::immutable(