Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
* develop: (62 commits)
  fix unicity filter not being applied in every case
  fix error in documentation
  add documentation to explain how to use properties to test new implementations
  prove the authorized names always constitutes a valid path
  remove duplication to generate valid names
  add properties for Directory::replaceAt
  ignore fixtures and properties from coverage
  add filtering properties
  remove unreachable code
  exclude forbidden value from scenarii
  CS
  do not allow names with space characters only
  1014 file path limit only applies to macOS
  use directory properties to randomly add/remove file to/from the generated directory
  reduce the depth of generated directories to avoid hitting the filesystem path length limit
  remove restriction on file names
  disable memory limit
  throw an exception when file path to persisted is too long
  disable memory limit for coverage
  reduce the number of scenarii
  ...
  • Loading branch information
Baptouuuu committed May 10, 2020
2 parents 9793cb5 + dd720d5 commit aa94efa
Show file tree
Hide file tree
Showing 58 changed files with 3,367 additions and 109 deletions.
46 changes: 42 additions & 4 deletions .github/workflows/ci.yml
Expand Up @@ -10,6 +10,42 @@ 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: php -dmemory_limit=-1 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
env:
BLACKBOX_SET_SIZE: 10
BLACKBOX_DETAILED_PROPERTIES: 1
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
Expand All @@ -29,9 +65,11 @@ 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
run: php -dmemory_limit=-1 vendor/bin/phpunit --coverage-clover=coverage.clover
env:
BLACKBOX_SET_SIZE: 1
- uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
Expand Down Expand Up @@ -59,6 +97,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
run: vendor/bin/psalm --shepherd
73 changes: 67 additions & 6 deletions 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.

Expand Down Expand Up @@ -34,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).

Expand All @@ -55,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.
3 changes: 3 additions & 0 deletions codecov.yml
@@ -0,0 +1,3 @@
ignore_paths:
- fixtures/*
- properties/*
16 changes: 14 additions & 2 deletions composer.json
Expand Up @@ -25,7 +25,9 @@
},
"autoload": {
"psr-4": {
"Innmind\\Filesystem\\": "src/"
"Innmind\\Filesystem\\": "src/",
"Fixtures\\Innmind\\Filesystem\\": "fixtures/",
"Properties\\Innmind\\Filesystem\\": "properties/"
}
},
"autoload-dev": {
Expand All @@ -35,6 +37,16 @@
},
"require-dev": {
"phpunit/phpunit": "~8.0",
"vimeo/psalm": "^3.7"
"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"
}
}
104 changes: 104 additions & 0 deletions fixtures/Directory.php
@@ -0,0 +1,104 @@
<?php
declare(strict_types = 1);

namespace Fixtures\Innmind\Filesystem;

use Innmind\Filesystem\{
Directory\Directory as Model,
File as FileInterface,
};
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;

final class Directory
{
/**
* Will generate random directory tree with a maximum depth of 3 directories
*
* @return DataSet<Model>
*/
public static function any(): DataSet
{
return self::atDepth(0, 1);
}

/**
* @return DataSet<Model>
*/
public static function maxDepth(int $depth): DataSet
{
return self::atDepth(0, $depth);
}

private static function atDepth(int $depth, int $maxDepth): DataSet
{
if ($depth === $maxDepth) {
$files = Set::of(
FileInterface::class,
new DataSet\Randomize(
File::any(),
),
DataSet\Integers::between(0, 5),
);
} else {
$files = Set::of(
FileInterface::class,
new DataSet\Either(
new DataSet\Randomize(
File::any(),
),
self::atDepth($depth + 1, $maxDepth),
),
DataSet\Integers::between(0, 5),
);
}

$directory = DataSet\Composite::immutable(
static fn($name, $files): Model => new Model(
$name,
$files,
),
Name::any(),
$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(
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
);
}
}
45 changes: 45 additions & 0 deletions fixtures/File.php
@@ -0,0 +1,45 @@
<?php
declare(strict_types = 1);

namespace Fixtures\Innmind\Filesystem;

use Innmind\Filesystem\File\File as Model;
use Innmind\BlackBox\Set;
use Innmind\Stream\{
Readable\Stream,
Stream\Position,
};
use Fixtures\Innmind\MediaType\MediaType;

final class File
{
public static function any(): Set
{
return Set\Composite::immutable(
static function($name, $content, $mediaType, $seek): Model {
$file = new Model(
$name,
$stream = Stream::ofContent($content),
$mediaType,
);

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(),
new Set\Either(
Set\Integers::between(0, 128), // 128 is the max string length by default
Set\Elements::of(null),
),
);
}
}
45 changes: 45 additions & 0 deletions fixtures/Name.php
@@ -0,0 +1,45 @@
<?php
declare(strict_types = 1);

namespace Fixtures\Innmind\Filesystem;

use Innmind\Filesystem\Name as Model;
use Innmind\BlackBox\Set;

final class Name
{
/**
* @return Set<Model>
*/
public static function any(): Set
{
return Set\Decorate::immutable(
static fn(string $name): Model => new Model($name),
self::strings(),
);
}

/**
* @return Set<string>
*/
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)
);
}
}
2 changes: 1 addition & 1 deletion phpunit.xml.dist
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>

<phpunit colors="true" bootstrap="vendor/autoload.php">
<phpunit colors="true" bootstrap="vendor/autoload.php" printerClass="Innmind\BlackBox\PHPUnit\ResultPrinterV8">
<testsuites>
<testsuite name="Filesystem test suite">
<directory>./tests</directory>
Expand Down

0 comments on commit aa94efa

Please sign in to comment.