[feat] scoped datasets

This commit is contained in:
Fabio Ivona
2022-09-19 17:45:27 +02:00
parent 12618ff8b3
commit cbee6e76b0
16 changed files with 267 additions and 40 deletions

View File

@ -9,6 +9,7 @@ use function Pest\testDirectory;
use Pest\TestSuite; use Pest\TestSuite;
use RecursiveDirectoryIterator; use RecursiveDirectoryIterator;
use RecursiveIteratorIterator; use RecursiveIteratorIterator;
use SebastianBergmann\FileIterator\Facade as PhpUnitFileIterator;
/** /**
* @internal * @internal
@ -21,8 +22,6 @@ final class BootFiles
* @var array<int, string> * @var array<int, string>
*/ */
private const STRUCTURE = [ private const STRUCTURE = [
'Datasets',
'Datasets.php',
'Expectations', 'Expectations',
'Expectations.php', 'Expectations.php',
'Helpers', 'Helpers',
@ -56,6 +55,8 @@ final class BootFiles
$this->load($filename); $this->load($filename);
} }
} }
$this->bootDatasets($testsPath);
} }
/** /**
@ -73,4 +74,26 @@ final class BootFiles
include_once $filename; include_once $filename;
} }
private function bootDatasets(string $testsPath): void
{
$files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php');
foreach ($files as $fullPath) {
$filename = Str::afterLast($fullPath, DIRECTORY_SEPARATOR);
if ($filename === 'Datasets.php') {
$this->load($fullPath);
continue;
}
$directoryFullPath = Str::beforeLast($fullPath, DIRECTORY_SEPARATOR);
$directory = Str::afterLast($directoryFullPath, DIRECTORY_SEPARATOR);
if ($directory === 'Datasets') {
$this->load($fullPath);
}
}
}
} }

View File

@ -17,8 +17,8 @@ final class DatasetAlreadyExist extends InvalidArgumentException implements Exce
/** /**
* Creates a new Exception instance. * Creates a new Exception instance.
*/ */
public function __construct(string $name) public function __construct(string $name, string $scope)
{ {
parent::__construct(sprintf('A dataset with the name `%s` already exist.', $name)); parent::__construct(sprintf('A dataset with the name `%s` already exist in scope [%s].', $name, $scope));
} }
} }

View File

@ -10,6 +10,7 @@ use Pest\PendingCalls\UsesCall;
use Pest\Repositories\DatasetsRepository; use Pest\Repositories\DatasetsRepository;
use Pest\Support\Backtrace; use Pest\Support\Backtrace;
use Pest\Support\HigherOrderTapProxy; use Pest\Support\HigherOrderTapProxy;
use Pest\Support\Str;
use Pest\TestSuite; use Pest\TestSuite;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@ -60,7 +61,17 @@ if (! function_exists('dataset')) {
*/ */
function dataset(string $name, Closure|iterable $dataset): void function dataset(string $name, Closure|iterable $dataset): void
{ {
DatasetsRepository::set($name, $dataset); $file = Backtrace::datasetsFile();
$filename = Str::afterLast($file, DIRECTORY_SEPARATOR);
$scope = Str::beforeLast($file, DIRECTORY_SEPARATOR);
if (Str::afterLast($scope, DIRECTORY_SEPARATOR) === 'Datasets') {
$scope = Str::beforeLast($scope, DIRECTORY_SEPARATOR);
} elseif ($filename !== 'Datasets.php') {
$scope = $file;
}
DatasetsRepository::set($name, $dataset, $scope);
} }
} }

View File

@ -36,13 +36,15 @@ final class DatasetsRepository
* *
* @param Closure|iterable<int|string, mixed> $data * @param Closure|iterable<int|string, mixed> $data
*/ */
public static function set(string $name, Closure|iterable $data): void public static function set(string $name, Closure|iterable $data, string $scope): void
{ {
if (array_key_exists($name, self::$datasets)) { $datasetKey = "$scope>>>$name";
throw new DatasetAlreadyExist($name);
if (array_key_exists("$datasetKey", self::$datasets)) {
throw new DatasetAlreadyExist($name, $scope);
} }
self::$datasets[$name] = $data; self::$datasets[$datasetKey] = $data;
} }
/** /**
@ -52,7 +54,7 @@ final class DatasetsRepository
*/ */
public static function with(string $filename, string $description, array $with): void public static function with(string $filename, string $description, array $with): void
{ {
self::$withs[$filename.'>>>'.$description] = $with; self::$withs["$filename>>>$description"] = $with;
} }
public static function has(string $filename, string $description): bool public static function has(string $filename, string $description): bool
@ -67,9 +69,10 @@ final class DatasetsRepository
*/ */
public static function get(string $filename, string $description) public static function get(string $filename, string $description)
{ {
// dump("requesting file: " . $filename);
$dataset = self::$withs[$filename.'>>>'.$description]; $dataset = self::$withs[$filename.'>>>'.$description];
$dataset = self::resolve($description, $dataset); $dataset = self::resolve($dataset, $filename);
if ($dataset === null) { if ($dataset === null) {
throw ShouldNotHappen::fromMessage('Dataset [%s] not resolvable.'); throw ShouldNotHappen::fromMessage('Dataset [%s] not resolvable.');
@ -84,14 +87,14 @@ final class DatasetsRepository
* @param array<Closure|iterable<int|string, mixed>|string> $dataset * @param array<Closure|iterable<int|string, mixed>|string> $dataset
* @return array<string, mixed>|null * @return array<string, mixed>|null
*/ */
public static function resolve(string $description, array $dataset): array|null public static function resolve(array $dataset, string $currentTestFile): array|null
{ {
/* @phpstan-ignore-next-line */ /* @phpstan-ignore-next-line */
if (empty($dataset)) { if (empty($dataset)) {
return null; return null;
} }
$dataset = self::processDatasets($dataset); $dataset = self::processDatasets($dataset, $currentTestFile);
$datasetCombinations = self::getDatasetsCombinations($dataset); $datasetCombinations = self::getDatasetsCombinations($dataset);
@ -132,11 +135,45 @@ final class DatasetsRepository
return $namedData; return $namedData;
} }
/**
* @return Closure|iterable<int|string, mixed>
*/
private static function getScopedDataset(string $name, string $currentTestFile)
{
$matchingDatasets = array_filter(self::$datasets, function (string $key) use ($name, $currentTestFile) {
[$datasetScope, $datasetName] = explode('>>>', $key);
if ($name !== $datasetName) {
return false;
}
if (! str_starts_with($currentTestFile, $datasetScope)) {
return false;
}
return true;
}, ARRAY_FILTER_USE_KEY);
$closestScopeDatasetKey = array_reduce(array_keys($matchingDatasets), function ($keyA, $keyB) {
if ($keyA === null) {
return $keyB;
}
return strlen($keyA) > strlen($keyB) ? $keyA : $keyB;
});
if ($closestScopeDatasetKey === null) {
throw new DatasetDoesNotExist($name);
}
return $matchingDatasets[$closestScopeDatasetKey];
}
/** /**
* @param array<Closure|iterable<int|string, mixed>|string> $datasets * @param array<Closure|iterable<int|string, mixed>|string> $datasets
* @return array<array<mixed>> * @return array<array<mixed>>
*/ */
private static function processDatasets(array $datasets): array private static function processDatasets(array $datasets, string $currentTestFile): array
{ {
$processedDatasets = []; $processedDatasets = [];
@ -144,11 +181,7 @@ final class DatasetsRepository
$processedDataset = []; $processedDataset = [];
if (is_string($data)) { if (is_string($data)) {
if (! array_key_exists($data, self::$datasets)) { $datasets[$index] = self::getScopedDataset($data, $currentTestFile);
throw new DatasetDoesNotExist($data);
}
$datasets[$index] = self::$datasets[$data];
} }
if (is_callable($datasets[$index])) { if (is_callable($datasets[$index])) {

View File

@ -42,6 +42,30 @@ final class Backtrace
return $current[self::FILE]; return $current[self::FILE];
} }
/**
* Returns the current datasets file.
*/
public static function datasetsFile(): string
{
$current = null;
foreach (debug_backtrace(self::BACKTRACE_OPTIONS) as $trace) {
assert(array_key_exists(self::FILE, $trace));
if (Str::endsWith($trace['file'], 'Bootstrappers/BootFiles.php') || Str::endsWith($trace[self::FILE], 'overrides/Runner/TestSuiteLoader.php')) {
break;
}
$current = $trace;
}
if ($current === null) {
throw ShouldNotHappen::fromMessage('Dataset file not found.');
}
return $current[self::FILE];
}
/** /**
* Returns the filename that called the current function/method. * Returns the filename that called the current function/method.
*/ */

View File

@ -59,6 +59,24 @@ final class Str
return (string) preg_replace('/[^A-Z_a-z0-9\\\\]/', '', $code); return (string) preg_replace('/[^A-Z_a-z0-9\\\\]/', '', $code);
} }
/**
* Return the remainder of a string after the last occurrence of a given value.
*/
public static function afterLast(string $subject, string $search): string
{
if ($search === '') {
return $subject;
}
$position = strrpos($subject, $search);
if ($position === false) {
return $subject;
}
return substr($subject, $position + strlen($search));
}
/** /**
* Get the portion of a string before the last occurrence of a given value. * Get the portion of a string before the last occurrence of a given value.
*/ */

View File

@ -29,7 +29,7 @@
✓ it does not append CoversNothing to other methods ✓ it does not append CoversNothing to other methods
✓ it throws exception if no class nor method has been found ✓ it throws exception if no class nor method has been found
PASS Tests\Features\Datasets PASS Tests\Features\DatasetsTests
✓ it throws exception if dataset does not exist ✓ it throws exception if dataset does not exist
✓ it throws exception if dataset already exist ✓ it throws exception if dataset already exist
✓ it sets closures ✓ it sets closures
@ -116,6 +116,7 @@
✓ it will not resolve a closure if it is type hinted as a callable with (Closure Object (...)) #2 ✓ it will not resolve a closure if it is type hinted as a callable with (Closure Object (...)) #2
✓ it can correctly resolve a bound dataset that returns an array with (Closure Object (...)) ✓ it can correctly resolve a bound dataset that returns an array with (Closure Object (...))
✓ it can correctly resolve a bound dataset that returns an array but wants to be spread with (Closure Object (...)) ✓ it can correctly resolve a bound dataset that returns an array but wants to be spread with (Closure Object (...))
↓ forbids to define tests in Datasets dirs and Datasets.php files
PASS Tests\Features\Depends PASS Tests\Features\Depends
✓ first ✓ first
@ -663,6 +664,47 @@
✓ get 'foo' → get 'bar' → expect true → toBeTrue ✓ get 'foo' → get 'bar' → expect true → toBeTrue
✓ get 'foo' → expect true → toBeTrue ✓ get 'foo' → expect true → toBeTrue
PASS Tests\Features\ScopedDatasets\Directory\NestedDirectory1\TestFileInNestedDirectoryWithDatasetsFile
✓ uses dataset with (1)
✓ uses dataset with (2)
✓ uses dataset with (3)
✓ uses dataset with (4)
✓ uses dataset with (5)
✓ uses dataset with ('ScopedDatasets/NestedDirector...ts.php')
✓ the right dataset is taken
PASS Tests\Features\ScopedDatasets\Directory\NestedDirectory2\TestFileInNestedDirectory
✓ uses dataset with (1)
✓ uses dataset with (2)
✓ uses dataset with (3)
✓ uses dataset with (4)
✓ uses dataset with (5)
✓ uses dataset with ('ScopedDatasets/Datasets/Scoped.php')
✓ the right dataset is taken
PASS Tests\Features\ScopedDatasets\Directory\TestFileWithLocallyDefinedDataset
✓ uses dataset with (1)
✓ uses dataset with (2)
✓ uses dataset with (3)
✓ uses dataset with (4)
✓ uses dataset with (5)
✓ uses dataset with ('ScopedDatasets/ScopedDatasets.php')
✓ the right dataset is taken
PASS Tests\Features\ScopedDatasets\Directory\TestFileWithScopedDataset
✓ uses dataset with (1)
✓ uses dataset with (2)
✓ uses dataset with (3)
✓ uses dataset with (4)
✓ uses dataset with (5)
✓ uses dataset with ('ScopedDatasets/Datasets/Scoped.php')
✓ the right dataset is taken
PASS Tests\Features\ScopedDatasets\TestFileOutOfScope
✓ uses dataset with (1)
✓ uses dataset with (2)
✓ the right dataset is taken
WARN Tests\Features\Skip WARN Tests\Features\Skip
✓ it do not skips ✓ it do not skips
- it skips with truthy → 1 - it skips with truthy → 1
@ -761,7 +803,7 @@
PASS Tests\Unit\Console\Help PASS Tests\Unit\Console\Help
✓ it outputs the help information when --help is used ✓ it outputs the help information when --help is used
PASS Tests\Unit\Datasets PASS Tests\Unit\DatasetsTests
✓ it show only the names of named datasets in their description ✓ it show only the names of named datasets in their description
✓ it show the actual dataset of non-named datasets in their description ✓ it show the actual dataset of non-named datasets in their description
✓ it show only the names of multiple named datasets in their description ✓ it show only the names of multiple named datasets in their description
@ -818,4 +860,4 @@
PASS Tests\Visual\Version PASS Tests\Visual\Version
✓ visual snapshot of help command output ✓ visual snapshot of help command output
Tests: 4 incomplete, 1 todo, 18 skipped, 562 passed (1460 assertions) Tests: 4 incomplete, 2 todos, 18 skipped, 593 passed (1503 assertions)

View File

@ -13,28 +13,28 @@ it('throws exception if dataset does not exist', function () {
$this->expectException(DatasetDoesNotExist::class); $this->expectException(DatasetDoesNotExist::class);
$this->expectExceptionMessage("A dataset with the name `first` does not exist. You can create it using `dataset('first', ['a', 'b']);`."); $this->expectExceptionMessage("A dataset with the name `first` does not exist. You can create it using `dataset('first', ['a', 'b']);`.");
DatasetsRepository::resolve('foo', ['first']); DatasetsRepository::resolve(['first'], __FILE__);
}); });
it('throws exception if dataset already exist', function () { it('throws exception if dataset already exist', function () {
DatasetsRepository::set('second', [[]]); DatasetsRepository::set('second', [[]], __DIR__);
$this->expectException(DatasetAlreadyExist::class); $this->expectException(DatasetAlreadyExist::class);
$this->expectExceptionMessage('A dataset with the name `second` already exist.'); $this->expectExceptionMessage('A dataset with the name `second` already exist in scope ['.__DIR__.'].');
DatasetsRepository::set('second', [[]]); DatasetsRepository::set('second', [[]], __DIR__);
}); });
it('sets closures', function () { it('sets closures', function () {
DatasetsRepository::set('foo', function () { DatasetsRepository::set('foo', function () {
yield [1]; yield [1];
}); }, __DIR__);
expect(DatasetsRepository::resolve('foo', ['foo']))->toBe(['(1)' => [1]]); expect(DatasetsRepository::resolve(['foo'], __FILE__))->toBe(['(1)' => [1]]);
}); });
it('sets arrays', function () { it('sets arrays', function () {
DatasetsRepository::set('bar', [[2]]); DatasetsRepository::set('bar', [[2]], __DIR__);
expect(DatasetsRepository::resolve('bar', ['bar']))->toBe(['(2)' => [2]]); expect(DatasetsRepository::resolve(['bar'], __FILE__))->toBe(['(2)' => [2]]);
}); });
it('gets bound to test case object', function ($value) { it('gets bound to test case object', function ($value) {
@ -304,3 +304,5 @@ it('can correctly resolve a bound dataset that returns an array but wants to be
return ['foo', 'bar', 'baz']; return ['foo', 'bar', 'baz'];
}, },
]); ]);
todo('forbids to define tests in Datasets dirs and Datasets.php files');

View File

@ -0,0 +1,5 @@
<?php
dataset('numbers.array', [
1, 2, 3, 4, 5, 'ScopedDatasets/Datasets/Scoped.php',
]);

View File

@ -0,0 +1,5 @@
<?php
dataset('numbers.array', [
1, 2, 3, 4, 5, 'ScopedDatasets/NestedDirectory1/Datasets.php',
]);

View File

@ -0,0 +1,12 @@
<?php
$state = new stdClass();
$state->text = '';
test('uses dataset', function ($value) use ($state) {
$state->text .= $value;
expect(true)->toBe(true);
})->with('numbers.array');
test('the right dataset is taken', function () use ($state) {
expect($state->text)->toBe('12345ScopedDatasets/NestedDirectory1/Datasets.php');
});

View File

@ -0,0 +1,12 @@
<?php
$state = new stdClass();
$state->text = '';
test('uses dataset', function ($value) use ($state) {
$state->text .= $value;
expect(true)->toBe(true);
})->with('numbers.array');
test('the right dataset is taken', function () use ($state) {
expect($state->text)->toBe('12345ScopedDatasets/Datasets/Scoped.php');
});

View File

@ -0,0 +1,16 @@
<?php
dataset('numbers.array', [
1, 2, 3, 4, 5, 'ScopedDatasets/ScopedDatasets.php',
]);
$state = new stdClass();
$state->text = '';
test('uses dataset', function ($value) use ($state) {
$state->text .= $value;
expect(true)->toBe(true);
})->with('numbers.array');
test('the right dataset is taken', function () use ($state) {
expect($state->text)->toBe('12345ScopedDatasets/ScopedDatasets.php');
});

View File

@ -0,0 +1,12 @@
<?php
$state = new stdClass();
$state->text = '';
test('uses dataset', function ($value) use ($state) {
$state->text .= $value;
expect(true)->toBe(true);
})->with('numbers.array');
test('the right dataset is taken', function () use ($state) {
expect($state->text)->toBe('12345ScopedDatasets/Datasets/Scoped.php');
});

View File

@ -0,0 +1,12 @@
<?php
$state = new stdClass();
$state->text = '';
test('uses dataset', function ($value) use ($state) {
$state->text .= $value;
expect(true)->toBe(true);
})->with('numbers.array');
test('the right dataset is taken', function () use ($state) {
expect($state->text)->toBe('12');
});

View File

@ -3,31 +3,31 @@
use Pest\Repositories\DatasetsRepository; use Pest\Repositories\DatasetsRepository;
it('show only the names of named datasets in their description', function () { it('show only the names of named datasets in their description', function () {
$descriptions = array_keys(DatasetsRepository::resolve('test description', [ $descriptions = array_keys(DatasetsRepository::resolve([
[ [
'one' => [1], 'one' => [1],
'two' => [[2]], 'two' => [[2]],
], ],
])); ], __FILE__));
expect($descriptions[0])->toBe('data set "one"') expect($descriptions[0])->toBe('data set "one"')
->and($descriptions[1])->toBe('data set "two"'); ->and($descriptions[1])->toBe('data set "two"');
}); });
it('show the actual dataset of non-named datasets in their description', function () { it('show the actual dataset of non-named datasets in their description', function () {
$descriptions = array_keys(DatasetsRepository::resolve('test description', [ $descriptions = array_keys(DatasetsRepository::resolve([
[ [
[1], [1],
[[2]], [[2]],
], ],
])); ], __FILE__));
expect($descriptions[0])->toBe('(1)'); expect($descriptions[0])->toBe('(1)');
expect($descriptions[1])->toBe('(array(2))'); expect($descriptions[1])->toBe('(array(2))');
}); });
it('show only the names of multiple named datasets in their description', function () { it('show only the names of multiple named datasets in their description', function () {
$descriptions = array_keys(DatasetsRepository::resolve('test description', [ $descriptions = array_keys(DatasetsRepository::resolve([
[ [
'one' => [1], 'one' => [1],
'two' => [[2]], 'two' => [[2]],
@ -36,7 +36,7 @@ it('show only the names of multiple named datasets in their description', functi
'three' => [3], 'three' => [3],
'four' => [[4]], 'four' => [[4]],
], ],
])); ], __FILE__));
expect($descriptions[0])->toBe('data set "one" / data set "three"'); expect($descriptions[0])->toBe('data set "one" / data set "three"');
expect($descriptions[1])->toBe('data set "one" / data set "four"'); expect($descriptions[1])->toBe('data set "one" / data set "four"');
@ -45,7 +45,7 @@ it('show only the names of multiple named datasets in their description', functi
}); });
it('show the actual dataset of multiple non-named datasets in their description', function () { it('show the actual dataset of multiple non-named datasets in their description', function () {
$descriptions = array_keys(DatasetsRepository::resolve('test description', [ $descriptions = array_keys(DatasetsRepository::resolve([
[ [
[1], [1],
[[2]], [[2]],
@ -54,7 +54,7 @@ it('show the actual dataset of multiple non-named datasets in their description'
[3], [3],
[[4]], [[4]],
], ],
])); ], __FILE__));
expect($descriptions[0])->toBe('(1) / (3)'); expect($descriptions[0])->toBe('(1) / (3)');
expect($descriptions[1])->toBe('(1) / (array(4))'); expect($descriptions[1])->toBe('(1) / (array(4))');
@ -63,7 +63,7 @@ it('show the actual dataset of multiple non-named datasets in their description'
}); });
it('show the correct description for mixed named and not-named datasets', function () { it('show the correct description for mixed named and not-named datasets', function () {
$descriptions = array_keys(DatasetsRepository::resolve('test description', [ $descriptions = array_keys(DatasetsRepository::resolve([
[ [
'one' => [1], 'one' => [1],
[[2]], [[2]],
@ -72,7 +72,7 @@ it('show the correct description for mixed named and not-named datasets', functi
[3], [3],
'four' => [[4]], 'four' => [[4]],
], ],
])); ], __FILE__));
expect($descriptions[0])->toBe('data set "one" / (3)'); expect($descriptions[0])->toBe('data set "one" / (3)');
expect($descriptions[1])->toBe('data set "one" / data set "four"'); expect($descriptions[1])->toBe('data set "one" / data set "four"');