Skip to content

Keep information which data provider provided current data set #5992

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
5 changes: 4 additions & 1 deletion src/Framework/Exception/Exception.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@
*/
class Exception extends RuntimeException implements \PHPUnit\Exception
{
public ?string $method;

/**
* @var list<array{file: string, line: int, function: string}>
*/
protected array $serializableTrace;

public function __construct(string $message = '', int|string $code = 0, ?Throwable $previous = null)
public function __construct(string $message = '', int|string $code = 0, ?Throwable $previous = null, ?string $method = null)
{
/**
* @see https://github.com/sebastianbergmann/phpunit/issues/5965
Expand All @@ -68,6 +70,7 @@ public function __construct(string $message = '', int|string $code = 0, ?Throwab
foreach (array_keys($this->serializableTrace) as $key) {
unset($this->serializableTrace[$key]['args']);
}
$this->method = $method;
}

public function __sleep(): array
Expand Down
28 changes: 16 additions & 12 deletions src/Framework/TestBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

use function array_merge;
use function assert;
use function reset;
use PHPUnit\Metadata\Api\DataProvider;
use PHPUnit\Metadata\Api\Groups;
use PHPUnit\Metadata\Api\Requirements;
Expand Down Expand Up @@ -47,7 +48,7 @@ public function build(ReflectionClass $theClass, string $methodName, array $grou
$data = (new DataProvider)->providedData($className, $methodName);
}

if ($data !== null) {
if ($data !== null && reset($data) !== null) {
return $this->buildDataProviderTestSuite(
$methodName,
$className,
Expand Down Expand Up @@ -91,20 +92,23 @@ private function buildDataProviderTestSuite(string $methodName, string $classNam
(new Groups)->groups($className, $methodName),
);

foreach ($data as $_dataName => $_data) {
$_test = new $className($methodName);
foreach ($data as $providerName => $providedData) {
foreach ($providedData as $_dataName => $_data) {
$_test = new $className($methodName);

$_test->setData($_dataName, $_data);
$_test->setData($_dataName, $_data);

$this->configureTestCase(
$_test,
$runTestInSeparateProcess,
$preserveGlobalState,
$runClassInSeparateProcess,
$backupSettings,
);
$this->configureTestCase(
$_test,
$runTestInSeparateProcess,
$preserveGlobalState,
$runClassInSeparateProcess,
$backupSettings,
);

$dataProviderTestSuite->addTest($_test, $groups);
$_test->setProviderName($providerName);
$dataProviderTestSuite->addTest($_test, $groups);
}
}

return $dataProviderTestSuite;
Expand Down
19 changes: 19 additions & 0 deletions src/Framework/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@
*/
abstract class TestCase extends Assert implements Reorderable, SelfDescribing, Test
{
/**
* @var non-empty-string
*/
protected string $providerName;
private ?bool $backupGlobals = null;

/**
Expand Down Expand Up @@ -360,6 +364,21 @@ final public function setGroups(array $groups): void
$this->groups = $groups;
}

public function getProviderName(): string
{
return $this->providerName;
}

/**
* @return $this
*/
public function setProviderName(string $providerName): self
{
$this->providerName = $providerName;

return $this;
}

/**
* @internal This method is not covered by the backward compatibility promise for PHPUnit
*/
Expand Down
3 changes: 2 additions & 1 deletion src/Framework/TestSuite.php
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,8 @@ protected function addTestMethod(ReflectionClass $class, ReflectionMethod $metho
Event\TestData\TestDataCollection::fromArray([]),
),
sprintf(
"The data provider specified for %s::%s is invalid\n%s",
"The data provider %s specified for %s::%s is invalid\n%s",
$e->method,
$className,
$methodName,
$this->throwableToString($e),
Expand Down
127 changes: 70 additions & 57 deletions src/Metadata/Api/DataProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@
use function get_debug_type;
use function is_array;
use function is_int;
use function is_iterable;
use function is_string;
use function key;
use function reset;
use function sprintf;
use PHPUnit\Event;
use PHPUnit\Event\Code\ClassMethod;
use PHPUnit\Framework\InvalidDataProviderException;
use PHPUnit\Metadata\DataProvider as DataProviderMetadata;
use PHPUnit\Metadata\MetadataCollection;
Expand Down Expand Up @@ -52,48 +56,55 @@ public function providedData(string $className, string $methodName): ?array
if ($dataProvider->isNotEmpty()) {
$data = $this->dataProvidedByMethods($className, $methodName, $dataProvider);
} else {
$data = $this->dataProvidedByMetadata($testWith);
$data = ['testWith' => $this->dataProvidedByMetadata($testWith)];
}

if ($data === []) {
if ($data === [] || reset($data) === []) {
throw new InvalidDataProviderException(
'Empty data set provided by data provider',
method: key($data),
);
}

foreach ($data as $key => $value) {
if (!is_array($value)) {
throw new InvalidDataProviderException(
sprintf(
'Data set %s is invalid, expected array but got %s',
is_int($key) ? '#' . $key : '"' . $key . '"',
get_debug_type($value),
),
);
foreach ($data as $providerMethodName => $providedData) {
foreach ($providedData as $key => $value) {
if (!is_array($value)) {
throw new InvalidDataProviderException(
sprintf(
'Data set %s is invalid, expected array but got %s',
is_int($key) ? '#' . $key : '"' . $key . '"',
get_debug_type($value),
),
method: $providerMethodName,
);
}
}
}

return $data;
}

/**
* @param class-string $className
* @param non-empty-string $methodName
* @param class-string $testClassName Name of class with test
* @param non-empty-string $testMethodName Name of method containing test
*
* @throws InvalidDataProviderException
*
* @return array<array<mixed>>
*/
private function dataProvidedByMethods(string $className, string $methodName, MetadataCollection $dataProvider): array
private function dataProvidedByMethods(string $testClassName, string $testMethodName, MetadataCollection $dataProvider): array
{
$testMethod = new Event\Code\ClassMethod($className, $methodName);
$testMethod = new ClassMethod($testClassName, $testMethodName);
$methodsCalled = [];
$result = [];
$return = [];
$caseNames = [];

foreach ($dataProvider as $_dataProvider) {
assert($_dataProvider instanceof DataProviderMetadata);

$dataProviderMethod = new Event\Code\ClassMethod($_dataProvider->className(), $_dataProvider->methodName());
$providerClassName = $_dataProvider->className();
$providerMethodName = $_dataProvider->methodName();
$dataProviderMethod = new ClassMethod($providerClassName, $providerMethodName);

Event\Facade::emitter()->dataProviderMethodCalled(
$testMethod,
Expand All @@ -103,91 +114,93 @@ private function dataProvidedByMethods(string $className, string $methodName, Me
$methodsCalled[] = $dataProviderMethod;

try {
$class = new ReflectionClass($_dataProvider->className());
$method = $class->getMethod($_dataProvider->methodName());
$class = new ReflectionClass($providerClassName);
$method = $class->getMethod($providerMethodName);

if (!$method->isPublic()) {
throw new InvalidDataProviderException(
sprintf(
'Data Provider method %s::%s() is not public',
$_dataProvider->className(),
$_dataProvider->methodName(),
),
);
$this->throwInvalid('is not public', $_dataProvider);
}

if (!$method->isStatic()) {
throw new InvalidDataProviderException(
sprintf(
'Data Provider method %s::%s() is not static',
$_dataProvider->className(),
$_dataProvider->methodName(),
),
);
$this->throwInvalid('is not static', $_dataProvider);
}

if ($method->getNumberOfParameters() > 0) {
throw new InvalidDataProviderException(
sprintf(
'Data Provider method %s::%s() expects an argument',
$_dataProvider->className(),
$_dataProvider->methodName(),
),
);
$this->throwInvalid('expects an argument', $_dataProvider);
}

$className = $_dataProvider->className();
$methodName = $_dataProvider->methodName();
$data = $className::$methodName();
$data = $providerClassName::$providerMethodName();

if (!is_iterable($data)) {
$this->throwInvalid('does not provide iterable type', $_dataProvider);
}
} catch (Throwable $e) {
Event\Facade::emitter()->dataProviderMethodFinished(
$testMethod,
...$methodsCalled,
);
$this->finishMethods($testMethod, $methodsCalled);

throw new InvalidDataProviderException(
$e->getMessage(),
$e->getCode(),
$e,
$providerMethodName,
);
}

$result = [];

foreach ($data as $key => $value) {
if (is_int($key)) {
$result[] = $value;
} elseif (is_string($key)) {
if (array_key_exists($key, $result)) {
Event\Facade::emitter()->dataProviderMethodFinished(
$testMethod,
...$methodsCalled,
);
if (isset($caseNames[$key])) {
$this->finishMethods($testMethod, $methodsCalled);

throw new InvalidDataProviderException(
sprintf(
'The key "%s" has already been defined by a previous data provider',
$key,
),
method: $providerMethodName,
);
}

$result[$key] = $value;
$caseNames[$key] = 1;
$result[$key] = $value;
} else {
throw new InvalidDataProviderException(
sprintf(
'The key must be an integer or a string, %s given',
get_debug_type($key),
),
method: $providerMethodName,
);
}
}
$return[$providerMethodName] = $result;
}
$this->finishMethods($testMethod, $methodsCalled);

return $return;
}

private function throwInvalid(string $message, DataProviderMetadata $dataProvider): never
{
throw new InvalidDataProviderException(
sprintf(
'Data Provider method %s::%s() ',
$dataProvider->className(),
$dataProvider->methodName(),
) . $message,
);
}

/**
* @param ClassMethod[] $methodsCalled
*/
private function finishMethods(ClassMethod $method, array $methodsCalled): void
{
Event\Facade::emitter()->dataProviderMethodFinished(
$testMethod,
$method,
...$methodsCalled,
);

return $result;
}

/**
Expand Down
7 changes: 4 additions & 3 deletions tests/_files/AbstractVariousIterableDataProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
*/
namespace PHPUnit\TestFixture;

use Generator;
use PHPUnit\Framework\Attributes\DataProvider;

abstract class AbstractVariousIterableDataProviderTest
{
public static function asArrayProviderInParent()
public static function asArrayProviderInParent(): array
{
return [
['J'],
Expand All @@ -22,7 +23,7 @@ public static function asArrayProviderInParent()
];
}

public static function asIteratorProviderInParent()
public static function asIteratorProviderInParent(): Generator
{
yield ['M'];

Expand All @@ -31,7 +32,7 @@ public static function asIteratorProviderInParent()
yield ['O'];
}

public static function asTraversableProviderInParent()
public static function asTraversableProviderInParent(): WrapperIteratorAggregate
{
return new WrapperIteratorAggregate([
['P'],
Expand Down
2 changes: 1 addition & 1 deletion tests/_files/DuplicateKeyDataProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public static function dataProvider(): Generator
}

#[DataProvider('dataProvider')]
public function test($arg): void
public function test(string $arg): void
{
}
}
2 changes: 1 addition & 1 deletion tests/_files/DuplicateKeyDataProvidersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static function dataProvider2(): iterable

#[DataProvider('dataProvider1')]
#[DataProvider('dataProvider2')]
public function test($value): void
public function test(int $value): void
{
$this->assertSame(2, $value);
}
Expand Down
Loading