diff --git a/src/Framework/Attributes/DataProvider.php b/src/Framework/Attributes/DataProvider.php index fc4a822f64..71363ee653 100644 --- a/src/Framework/Attributes/DataProvider.php +++ b/src/Framework/Attributes/DataProvider.php @@ -24,14 +24,16 @@ */ private string $methodName; private bool $validateArgumentCount; + private bool $skipWhenEmpty; /** * @param non-empty-string $methodName */ - public function __construct(string $methodName, bool $validateArgumentCount = true) + public function __construct(string $methodName, bool $validateArgumentCount = true, bool $skipWhenEmpty = false) { $this->methodName = $methodName; $this->validateArgumentCount = $validateArgumentCount; + $this->skipWhenEmpty = $skipWhenEmpty; } /** @@ -46,4 +48,9 @@ public function validateArgumentCount(): bool { return $this->validateArgumentCount; } + + public function skipWhenEmpty(): bool + { + return $this->skipWhenEmpty; + } } diff --git a/src/Framework/Attributes/DataProviderExternal.php b/src/Framework/Attributes/DataProviderExternal.php index e1f11ab131..01cb44c4bf 100644 --- a/src/Framework/Attributes/DataProviderExternal.php +++ b/src/Framework/Attributes/DataProviderExternal.php @@ -29,16 +29,18 @@ */ private string $methodName; private bool $validateArgumentCount; + private bool $skipWhenEmpty; /** * @param class-string $className * @param non-empty-string $methodName */ - public function __construct(string $className, string $methodName, bool $validateArgumentCount = true) + public function __construct(string $className, string $methodName, bool $validateArgumentCount = true, bool $skipWhenEmpty = false) { $this->className = $className; $this->methodName = $methodName; $this->validateArgumentCount = $validateArgumentCount; + $this->skipWhenEmpty = $skipWhenEmpty; } /** @@ -61,4 +63,9 @@ public function validateArgumentCount(): bool { return $this->validateArgumentCount; } + + public function skipWhenEmpty(): bool + { + return $this->skipWhenEmpty; + } } diff --git a/src/Framework/TestBuilder.php b/src/Framework/TestBuilder.php index 3eb4b1a02d..be2734be0c 100644 --- a/src/Framework/TestBuilder.php +++ b/src/Framework/TestBuilder.php @@ -55,7 +55,7 @@ public function build(ReflectionClass $theClass, string $methodName, array $grou } } - if ($data !== null) { + if ($data !== null && $data !== []) { return $this->buildDataProviderTestSuite( $methodName, $className, @@ -69,6 +69,12 @@ public function build(ReflectionClass $theClass, string $methodName, array $grou $test = new $className($methodName); + if ($data === []) { + $test->setEmptyDataProviderSkipMessage( + 'The data provider for this test provided no data, which is explicitly permitted', + ); + } + $this->configureTestCase( $test, $this->shouldTestMethodBeRunInSeparateProcess($className, $methodName), diff --git a/src/Framework/TestCase.php b/src/Framework/TestCase.php index 51c288880c..1e04f83a9c 100644 --- a/src/Framework/TestCase.php +++ b/src/Framework/TestCase.php @@ -229,8 +229,9 @@ abstract class TestCase extends Assert implements Reorderable, SelfDescribing, T /** * @var false|resource */ - private mixed $errorLogCapture = false; - private false|string $previousErrorLogTarget = false; + private mixed $errorLogCapture = false; + private false|string $previousErrorLogTarget = false; + private ?string $emptyDataProviderSkipMessage = null; /** * @param non-empty-string $name @@ -500,6 +501,10 @@ final public function runBare(): void $this->checkRequirements(); $hasMetRequirements = true; + if ($this->emptyDataProviderSkipMessage !== null) { + $this->markTestSkipped($this->emptyDataProviderSkipMessage); + } + if ($this->inIsolation) { // @codeCoverageIgnoreStart $this->invokeBeforeClassHookMethods($hookMethods, $emitter); @@ -788,6 +793,14 @@ final public function setInIsolation(bool $inIsolation): void $this->inIsolation = $inIsolation; } + /** + * @internal This method is not covered by the backward compatibility promise for PHPUnit + */ + final public function setEmptyDataProviderSkipMessage(string $message): void + { + $this->emptyDataProviderSkipMessage = $message; + } + /** * @internal This method is not covered by the backward compatibility promise for PHPUnit * diff --git a/src/Metadata/Api/DataProvider.php b/src/Metadata/Api/DataProvider.php index da3eeecb62..0a69baa5f5 100644 --- a/src/Metadata/Api/DataProvider.php +++ b/src/Metadata/Api/DataProvider.php @@ -86,6 +86,7 @@ private function dataProvidedByMethods(string $testClassName, ReflectionMethod $ $methodsCalled = []; $result = []; + $skipWhenEmpty = false; $testMethodNumberOfParameters = $testMethod->getNumberOfParameters(); $testMethodIsNonVariadic = !$testMethod->isVariadic(); @@ -96,6 +97,10 @@ private function dataProvidedByMethods(string $testClassName, ReflectionMethod $ $dataProviderMethod = new Event\Code\ClassMethod($_dataProvider->className(), $_dataProvider->methodName()); $validateArgumentCount = $testMethodIsNonVariadic && $_dataProvider->validateArgumentCount(); + if ($_dataProvider->skipWhenEmpty()) { + $skipWhenEmpty = true; + } + Event\Facade::emitter()->dataProviderMethodCalled( $testMethodValueObject, $dataProviderMethod, @@ -331,6 +336,10 @@ private function dataProvidedByMethods(string $testClassName, ReflectionMethod $ ); if ($result === []) { + if ($skipWhenEmpty) { + return []; + } + throw new InvalidDataProviderException( 'Empty data set provided by data provider', ); diff --git a/src/Metadata/DataProvider.php b/src/Metadata/DataProvider.php index e3a31d78d1..77c8c3b1df 100644 --- a/src/Metadata/DataProvider.php +++ b/src/Metadata/DataProvider.php @@ -26,18 +26,20 @@ */ private string $methodName; private bool $validateArgumentCount; + private bool $skipWhenEmpty; /** * @param class-string $className * @param non-empty-string $methodName */ - protected function __construct(Level $level, string $className, string $methodName, bool $validateArgumentCount) + protected function __construct(Level $level, string $className, string $methodName, bool $validateArgumentCount, bool $skipWhenEmpty) { parent::__construct($level); $this->className = $className; $this->methodName = $methodName; $this->validateArgumentCount = $validateArgumentCount; + $this->skipWhenEmpty = $skipWhenEmpty; } public function isDataProvider(): true @@ -65,4 +67,9 @@ public function validateArgumentCount(): bool { return $this->validateArgumentCount; } + + public function skipWhenEmpty(): bool + { + return $this->skipWhenEmpty; + } } diff --git a/src/Metadata/Metadata.php b/src/Metadata/Metadata.php index 6bf65eaab6..86b5ff6923 100644 --- a/src/Metadata/Metadata.php +++ b/src/Metadata/Metadata.php @@ -143,9 +143,9 @@ public static function coversNothingOnMethod(): CoversNothing * @param class-string $className * @param non-empty-string $methodName */ - public static function dataProvider(string $className, string $methodName, bool $validateArgumentCount): DataProvider + public static function dataProvider(string $className, string $methodName, bool $validateArgumentCount, bool $skipWhenEmpty): DataProvider { - return new DataProvider(Level::METHOD_LEVEL, $className, $methodName, $validateArgumentCount); + return new DataProvider(Level::METHOD_LEVEL, $className, $methodName, $validateArgumentCount, $skipWhenEmpty); } public static function dataProviderClosure(Closure $callable, bool $validateArgumentCount): DataProviderClosure diff --git a/src/Metadata/Parser/AttributeParser.php b/src/Metadata/Parser/AttributeParser.php index 5603f9d54e..af5c938009 100644 --- a/src/Metadata/Parser/AttributeParser.php +++ b/src/Metadata/Parser/AttributeParser.php @@ -608,14 +608,14 @@ public function forMethod(string $className, string $methodName): MetadataCollec case DataProvider::class: assert($attributeInstance instanceof DataProvider); - $result[] = Metadata::dataProvider($className, $attributeInstance->methodName(), $attributeInstance->validateArgumentCount()); + $result[] = Metadata::dataProvider($className, $attributeInstance->methodName(), $attributeInstance->validateArgumentCount(), $attributeInstance->skipWhenEmpty()); break; case DataProviderExternal::class: assert($attributeInstance instanceof DataProviderExternal); - $result[] = Metadata::dataProvider($attributeInstance->className(), $attributeInstance->methodName(), $attributeInstance->validateArgumentCount()); + $result[] = Metadata::dataProvider($attributeInstance->className(), $attributeInstance->methodName(), $attributeInstance->validateArgumentCount(), $attributeInstance->skipWhenEmpty()); break; diff --git a/tests/end-to-end/event/_files/EmptyDataProviderSkipWhenEmptyTest.php b/tests/end-to-end/event/_files/EmptyDataProviderSkipWhenEmptyTest.php new file mode 100644 index 0000000000..be0f9213bc --- /dev/null +++ b/tests/end-to-end/event/_files/EmptyDataProviderSkipWhenEmptyTest.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\TestFixture\Event; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; + +final class EmptyDataProviderSkipWhenEmptyTest extends TestCase +{ + public static function providerMethod(): array + { + return []; + } + + #[DataProvider('providerMethod', skipWhenEmpty: true)] + public function testCase(): void + { + } +} diff --git a/tests/end-to-end/event/data-provider-empty-skip-when-empty.phpt b/tests/end-to-end/event/data-provider-empty-skip-when-empty.phpt new file mode 100644 index 0000000000..46f449a1ce --- /dev/null +++ b/tests/end-to-end/event/data-provider-empty-skip-when-empty.phpt @@ -0,0 +1,31 @@ +--TEST-- +The right events are emitted in the right order for a test that uses an empty data provider with skipWhenEmpty +--FILE-- +run($_SERVER['argv']); +--EXPECTF-- +PHPUnit Started (PHPUnit %s using %s) +Test Runner Configured +Event Facade Sealed +Data Provider Method Called (PHPUnit\TestFixture\Event\EmptyDataProviderSkipWhenEmptyTest::providerMethod for test method PHPUnit\TestFixture\Event\EmptyDataProviderSkipWhenEmptyTest::testCase) +Data Provider Method Finished for PHPUnit\TestFixture\Event\EmptyDataProviderSkipWhenEmptyTest::testCase: +- PHPUnit\TestFixture\Event\EmptyDataProviderSkipWhenEmptyTest::providerMethod +Test Suite Loaded (1 test) +Test Runner Started +Test Suite Sorted +Test Runner Execution Started (1 test) +Test Suite Started (PHPUnit\TestFixture\Event\EmptyDataProviderSkipWhenEmptyTest, 1 test) +Test Preparation Started (PHPUnit\TestFixture\Event\EmptyDataProviderSkipWhenEmptyTest::testCase) +Test Skipped (PHPUnit\TestFixture\Event\EmptyDataProviderSkipWhenEmptyTest::testCase) +The data provider for this test provided no data, which is explicitly permitted +Test Suite Finished (PHPUnit\TestFixture\Event\EmptyDataProviderSkipWhenEmptyTest, 1 test) +Test Runner Execution Finished +Test Runner Finished +PHPUnit Finished (Shell Exit Code: 0) diff --git a/tests/unit/Metadata/MetadataCollectionTest.php b/tests/unit/Metadata/MetadataCollectionTest.php index c4d18a757b..540cbeb33a 100644 --- a/tests/unit/Metadata/MetadataCollectionTest.php +++ b/tests/unit/Metadata/MetadataCollectionTest.php @@ -602,7 +602,7 @@ private function collectionWithOneOfEach(): MetadataCollection Metadata::coversFunction(''), Metadata::coversMethod('', ''), Metadata::coversNothingOnClass(), - Metadata::dataProvider('', '', true), + Metadata::dataProvider('', '', true, false), Metadata::dataProviderClosure($closure, true), Metadata::dependsOnClass('', false, false), Metadata::dependsOnMethod('', '', false, false), diff --git a/tests/unit/Metadata/MetadataTest.php b/tests/unit/Metadata/MetadataTest.php index dc3165f594..b39471dc2d 100644 --- a/tests/unit/Metadata/MetadataTest.php +++ b/tests/unit/Metadata/MetadataTest.php @@ -1210,7 +1210,7 @@ public function testCanBeCoversTrait(): void public function testCanBeDataProvider(): void { - $metadata = Metadata::dataProvider(self::class, 'method', true); + $metadata = Metadata::dataProvider(self::class, 'method', true, false); $this->assertFalse($metadata->isAfter()); $this->assertFalse($metadata->isAfterClass());