From 01e941123f11c21513d27af913243ff78849f4d8 Mon Sep 17 00:00:00 2001 From: Vova Stolyarchuk <41348702+cb-vova@users.noreply.github.com> Date: Tue, 15 Jul 2025 17:30:34 +0300 Subject: [PATCH 1/3] ci: add coverage badge --- .github/workflows/default.yaml | 48 +++++++++++++++++++++++++++++++--- README.md | 2 ++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/.github/workflows/default.yaml b/.github/workflows/default.yaml index 430984c..d225825 100644 --- a/.github/workflows/default.yaml +++ b/.github/workflows/default.yaml @@ -9,14 +9,16 @@ on: jobs: build: runs-on: ubuntu-latest + permissions: + contents: write strategy: fail-fast: false matrix: - php-versions: ['8.2', '8.3', '8.4'] + php-versions: [ '8.2', '8.3', '8.4' ] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup PHP with PECL extension uses: shivammathur/setup-php@v2 @@ -26,7 +28,7 @@ jobs: - name: Cache Composer packages id: composer-cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: vendor key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} @@ -43,4 +45,42 @@ jobs: run: vendor/bin/phpstan analyse - name: Execute PHPUnit tests - run: vendor/bin/phpunit + id: phpunit + run: | + if [[ "${{ matrix.php-versions }}" == "8.3" && "${{ github.ref }}" == "refs/heads/main" && "${{ github.event_name }}" == "push" ]]; then + vendor/bin/phpunit --coverage-clover coverage.xml + echo "generate-coverage-badge=true" >> $GITHUB_OUTPUT + else + echo "generate-coverage-badge=false" >> $GITHUB_OUTPUT + vendor/bin/phpunit + fi + + - name: Generate coverage badge + uses: timkrase/phpunit-coverage-badge@v1.2.1 + if: steps.phpunit.outputs.generate-coverage-badge == 'true' + with: + coverage_badge_path: ./output/coverage.svg + push_badge: false + report: coverage.xml + + - name: Check badge file exists + if: steps.phpunit.outputs.generate-coverage-badge == 'true' + id: check_badge + run: | + if [ -f "./output/coverage.svg" ]; then + echo "Coverage badge file found" + echo "badge_exists=true" >> $GITHUB_OUTPUT + else + echo "badge_exists=false" >> $GITHUB_OUTPUT + echo "Coverage badge file not found, skipping push to image-data branch" + fi + + - name: Deploy badge to image-data branch + if: steps.phpunit.outputs.generate-coverage-badge == 'true' && steps.check_badge.outputs.badge_exists == 'true' + uses: peaceiris/actions-gh-pages@v4 + with: + publish_dir: ./output + publish_branch: image-data + github_token: ${{ secrets.GITHUB_TOKEN }} + user_name: 'github-actions[bot]' + user_email: 'github-actions[bot]@users.noreply.github.com' diff --git a/README.md b/README.md index 0b6575b..1daa771 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # PHP-VCR integration for PHPUnit +![Coverage](https://raw.githubusercontent.com/angelov/phpunit-php-vcr/image-data/coverage.svg) + A library that allows you to easily use the PHP-VCR library in your PHPUnit tests. ## Requirements From 2dd6ad7cd203219b10894697451654b1ec639cf6 Mon Sep 17 00:00:00 2001 From: Volodymyr Stolyarchuk Date: Tue, 15 Jul 2025 15:01:32 +0300 Subject: [PATCH 2/3] feat: separate cassettes per dataProvider cases --- README.md | 168 +++++++++++++++++- src/Subscribers/AttributeResolverTrait.php | 41 +++-- src/Subscribers/StartRecording.php | 30 +++- src/UseCassette.php | 9 +- src/Values/TestCaseParameters.php | 16 ++ src/Values/TestMethodInfo.php | 30 ++++ ...SeparateCassettesInSeparateFoldersTest.php | 58 ++++++ ...ithSeparateCassettesInSingleFolderTest.php | 54 ++++++ tests/AttributeDeclaredOnMethodsTest.php | 54 ++++++ tests/WithoutVcrTest.php | 18 ++ tests/fixtures/on_methods_without_extension | 24 +++ .../example-com | 24 +++ .../example-org | 24 +++ ...er_and_separated_cassettes-example-com.yml | 24 +++ ...er_and_separated_cassettes-example-org.yml | 24 +++ .../0.yml | 24 +++ .../1.yml | 24 +++ .../on_class.yml | 24 +++ .../on_class/0.yml | 24 +++ .../on_class/1.yml | 24 +++ .../on_class/example-com.yml | 24 +++ .../on_class/example-org.yml | 24 +++ .../on_class-0.yml | 24 +++ .../on_class-1.yml | 24 +++ .../on_class-example-com.yml | 24 +++ .../on_class-example-org.yml | 24 +++ .../on_class.yml | 24 +++ 27 files changed, 860 insertions(+), 26 deletions(-) create mode 100644 src/Values/TestCaseParameters.php create mode 100644 src/Values/TestMethodInfo.php create mode 100644 tests/AttributeDeclaredOnClassWithSeparateCassettesInSeparateFoldersTest.php create mode 100644 tests/AttributeDeclaredOnClassWithSeparateCassettesInSingleFolderTest.php create mode 100644 tests/WithoutVcrTest.php create mode 100644 tests/fixtures/on_methods_without_extension create mode 100644 tests/fixtures/on_methods_without_extension_with_data_provider/example-com create mode 100644 tests/fixtures/on_methods_without_extension_with_data_provider/example-org create mode 100644 tests/fixtures/with_data_provider_and_separated_cassettes-example-com.yml create mode 100644 tests/fixtures/with_data_provider_and_separated_cassettes-example-org.yml create mode 100644 tests/fixtures/with_data_provider_and_separated_cassettes_in_directory/0.yml create mode 100644 tests/fixtures/with_data_provider_and_separated_cassettes_in_directory/1.yml create mode 100644 tests/fixtures/with_separate_cassettes_in_separated_folders/on_class.yml create mode 100644 tests/fixtures/with_separate_cassettes_in_separated_folders/on_class/0.yml create mode 100644 tests/fixtures/with_separate_cassettes_in_separated_folders/on_class/1.yml create mode 100644 tests/fixtures/with_separate_cassettes_in_separated_folders/on_class/example-com.yml create mode 100644 tests/fixtures/with_separate_cassettes_in_separated_folders/on_class/example-org.yml create mode 100644 tests/fixtures/with_separate_cassettes_in_single_folder/on_class-0.yml create mode 100644 tests/fixtures/with_separate_cassettes_in_single_folder/on_class-1.yml create mode 100644 tests/fixtures/with_separate_cassettes_in_single_folder/on_class-example-com.yml create mode 100644 tests/fixtures/with_separate_cassettes_in_single_folder/on_class-example-org.yml create mode 100644 tests/fixtures/with_separate_cassettes_in_single_folder/on_class.yml diff --git a/README.md b/README.md index 1daa771..587e905 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,8 @@ Then, add the extension to your PHPUnit configuration file. ## Usage The library provides an `UseCassette` attribute that can be declared on test classes or specific test methods. The -attribute expects one string argument - the name of the cassette. +attribute accepts a cassette name and optional parameters for advanced functionality like separate cassettes per +data provider case. When running the tests, the library will automatically turn the recorder on and off, and insert the cassettes when needed. @@ -56,7 +57,7 @@ responses in the given cassette. { #[Test] public function example(): void { ... } - + #[Test] public function another(): void { ... } } @@ -104,4 +105,165 @@ used for that method. In this example, the responses from the requests made in t #[UseCassette("example_2.yml")] public function recorded(): void { ... } } - ``` \ No newline at end of file + ``` + +## DataProvider Support + +The library supports PHPUnit's `DataProvider` functionality with additional options for managing cassettes when using data providers. + +### Basic DataProvider Usage + +When using a data provider with the basic `UseCassette` attribute, all test cases from the data provider will share the same cassette file: + +```php +use Angelov\PHPUnitPHPVcr\UseCassette; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +class ExampleTest extends TestCase +{ + #[Test] + #[UseCassette("shared_cassette.yml")] + #[DataProvider("urls")] + public function testWithDataProvider(string $url): void + { + $content = file_get_contents($url); + // All test cases will use the same cassette file + } + + public static function urls(): iterable + { + yield ["https://example.com"]; + yield ["https://example.org"]; + } +} +``` + +### Separate Cassettes Per DataProvider Case + +For more granular control, you can create separate cassette files for each data provider case using the `separateCassettePerCase` parameter: + +```php +use Angelov\PHPUnitPHPVcr\UseCassette; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +class ExampleTest extends TestCase +{ + #[Test] + #[UseCassette(name: "separate_cassettes.yml", separateCassettePerCase: true)] + #[DataProvider("urls")] + public function testWithSeparateCassettes(string $url): void + { + $content = file_get_contents($url); + // Each test case will have its own cassette file: + // - separate_cassettes-0.yml + // - separate_cassettes-1.yml + } + + public static function urls(): iterable + { + yield ["https://example.com"]; + yield ["https://example.org"]; + } +} +``` + +### Named DataProvider Cases + +When using named data provider cases, the cassette files will use the case names: + +```php +use Angelov\PHPUnitPHPVcr\UseCassette; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +class ExampleTest extends TestCase +{ + #[Test] + #[UseCassette(name: "named_cassettes.yml", separateCassettePerCase: true)] + #[DataProvider("namedUrls")] + public function testWithNamedCassettes(string $url): void + { + $content = file_get_contents($url); + // Each test case will have its own cassette file: + // - named_cassettes-example-com.yml + // - named_cassettes-example-org.yml + } + + public static function namedUrls(): iterable + { + yield 'example.com' => ["https://example.com"]; + yield 'example.org' => ["https://example.org"]; + } +} +``` + +### Grouping Cassettes in Directories + +To organize separate cassette files in directories, use the `groupCaseFilesInDirectory` parameter: + +```php +use Angelov\PHPUnitPHPVcr\UseCassette; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +class ExampleTest extends TestCase +{ + #[Test] + #[UseCassette( + name: "organized_cassettes.yml", + separateCassettePerCase: true, + groupCaseFilesInDirectory: true + )] + #[DataProvider("urls")] + public function testWithOrganizedCassettes(string $url): void + { + $content = file_get_contents($url); + // Cassette files will be organized in a directory: + // - organized_cassettes/0.yml + // - organized_cassettes/1.yml + } + + public static function urls(): iterable + { + yield ["https://example.com"]; + yield ["https://example.org"]; + } +} +``` + +### Class-Level DataProvider Support + +The dataProvider functionality also works when the `UseCassette` attribute is declared at the class level: + +```php +use Angelov\PHPUnitPHPVcr\UseCassette; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; + +#[UseCassette(name: "class_level.yml", separateCassettePerCase: true)] +class ExampleTest extends TestCase +{ + #[Test] + #[DataProvider("urls")] + public function testMethod(string $url): void + { + $content = file_get_contents($url); + // Each test case will have separate cassettes: + // - class_level-0.yml + // - class_level-1.yml + } + + public static function urls(): iterable + { + yield ["https://example.com"]; + yield ["https://example.org"]; + } +} +``` diff --git a/src/Subscribers/AttributeResolverTrait.php b/src/Subscribers/AttributeResolverTrait.php index 556f7a4..92099b6 100644 --- a/src/Subscribers/AttributeResolverTrait.php +++ b/src/Subscribers/AttributeResolverTrait.php @@ -5,6 +5,8 @@ namespace Angelov\PHPUnitPHPVcr\Subscribers; use Angelov\PHPUnitPHPVcr\UseCassette; +use Angelov\PHPUnitPHPVcr\Values\TestCaseParameters; +use Angelov\PHPUnitPHPVcr\Values\TestMethodInfo; use Exception; use ReflectionMethod; @@ -12,24 +14,19 @@ trait AttributeResolverTrait { private function needsRecording(string $test): bool { - return $this->getAttribute($test) !== null; + return $this->getTestCaseCassetteParameters($test) !== null; } - private function getCassetteName(string $test): ?string + private function getTestCaseCassetteParameters(string $test): ?TestCaseParameters { - return $this->getAttribute($test)?->name; - } - - private function getAttribute(string $test): ?UseCassette - { - $test = $this->parseMethod($test); + $testMethodDetails = $this->parseMethod($test); try { if (PHP_VERSION_ID < 80300) { - $method = new ReflectionMethod($test); + $method = new ReflectionMethod($testMethodDetails->method); } else { // @phpstan-ignore-next-line - $method = ReflectionMethod::createFromMethodName($test); + $method = ReflectionMethod::createFromMethodName($testMethodDetails->method); } } catch (Exception) { return null; @@ -37,27 +34,35 @@ private function getAttribute(string $test): ?UseCassette $attributes = $method->getAttributes(UseCassette::class); - if ($attributes) { - return $attributes[0]->newInstance(); + $cassette = $attributes ? $attributes[0]->newInstance() : $this->getAttributeFromClass($testMethodDetails); + if ($cassette === null) { + return null; } - return $this->getAttributeFromClass($test); + return new TestCaseParameters( + cassette: $cassette, + case: $testMethodDetails->dataProvider, + ); } - private function parseMethod(string $test): string + private function parseMethod(string $test): TestMethodInfo { $test = explode(" ", $test)[0]; - return explode("#", $test)[0]; + $methoDetails = explode("#", $test); + return new TestMethodInfo( + method: $methoDetails[0], + dataProvider: $methoDetails[1] ?? null + ); } - private function getAttributeFromClass(string $test): ?UseCassette + private function getAttributeFromClass(TestMethodInfo $test): ?UseCassette { if (PHP_VERSION_ID < 80300) { - $method = new ReflectionMethod($test); + $method = new ReflectionMethod($test->method); } else { // @phpstan-ignore-next-line - $method = ReflectionMethod::createFromMethodName($test); + $method = ReflectionMethod::createFromMethodName($test->method); } $class = $method->getDeclaringClass(); $attributes = $class->getAttributes(UseCassette::class); diff --git a/src/Subscribers/StartRecording.php b/src/Subscribers/StartRecording.php index 049341f..d8f2f13 100644 --- a/src/Subscribers/StartRecording.php +++ b/src/Subscribers/StartRecording.php @@ -4,6 +4,8 @@ namespace Angelov\PHPUnitPHPVcr\Subscribers; +use Angelov\PHPUnitPHPVcr\UseCassette; +use Angelov\PHPUnitPHPVcr\Values\TestCaseParameters; use PHPUnit\Event\Test\Prepared; use PHPUnit\Event\Test\PreparedSubscriber; use VCR\VCR; @@ -20,10 +22,34 @@ public function notify(Prepared $event): void return; } - $cassetteName = $this->getCassetteName($test); - assert($cassetteName !== null); + $testCaseCassetteParameters = $this->getTestCaseCassetteParameters($test); + assert($testCaseCassetteParameters instanceof TestCaseParameters); + + if ($testCaseCassetteParameters->case !== null) { + $cassetteName = $this->makeCassetteNameForCase( + case: $testCaseCassetteParameters->case, + cassette: $testCaseCassetteParameters->cassette, + ); + } else { + $cassetteName = $testCaseCassetteParameters->cassette->name; + } VCR::turnOn(); VCR::insertCassette($cassetteName); } + + private function makeCassetteNameForCase(string $case, UseCassette $cassette): string + { + if (!$cassette->separateCassettePerCase) { + return $cassette->name; + } + $cassetteNameParts = explode('.', $cassette->name); + $cassetteSuffix = $cassette->groupCaseFilesInDirectory ? '/' . $case : '-' . $case; + if (count($cassetteNameParts) === 1) { + //cassette name does not contain a dot, so we can use it as is + return $cassette->name . $cassetteSuffix; + } + $ext = array_pop($cassetteNameParts); + return implode('.', $cassetteNameParts) . $cassetteSuffix . '.' . $ext; + } } diff --git a/src/UseCassette.php b/src/UseCassette.php index 0474b70..59e904a 100644 --- a/src/UseCassette.php +++ b/src/UseCassette.php @@ -7,9 +7,12 @@ use Attribute; #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] -class UseCassette +readonly class UseCassette { - public function __construct(public readonly string $name) - { + public function __construct( + public string $name, + public bool $separateCassettePerCase = false, + public bool $groupCaseFilesInDirectory = false + ) { } } diff --git a/src/Values/TestCaseParameters.php b/src/Values/TestCaseParameters.php new file mode 100644 index 0000000..5b43e2a --- /dev/null +++ b/src/Values/TestCaseParameters.php @@ -0,0 +1,16 @@ +dataProvider = $this->normaliseDataProvider($dataProvider); + } + + private function normaliseDataProvider(?string $dataProvider): ?string + { + if ($dataProvider === null) { + return null; + } + + $replaced = (string)preg_replace('/-+/', '-', (string)preg_replace('/\W+/', '-', $dataProvider)); + if ($replaced === '') { + throw new \InvalidArgumentException('Invalid data provider name: ' . $dataProvider); + } + return trim(strtolower($replaced)); + } +} diff --git a/tests/AttributeDeclaredOnClassWithSeparateCassettesInSeparateFoldersTest.php b/tests/AttributeDeclaredOnClassWithSeparateCassettesInSeparateFoldersTest.php new file mode 100644 index 0000000..9f23c7f --- /dev/null +++ b/tests/AttributeDeclaredOnClassWithSeparateCassettesInSeparateFoldersTest.php @@ -0,0 +1,58 @@ +assertSame("Example body for \"https://example.com\"", $content); + } + + #[Test] + #[DataProvider("urls")] + public function it_uses_vcr_on_methods_with_data_provider(string $url): void + { + $content = file_get_contents($url); + + $this->assertSame(sprintf("Example body for \"%s\"", $url), $content); + } + + #[Test] + #[DataProvider("namedUrls")] + public function it_uses_vcr_on_methods_with_data_provider_named_use_cases(string $url): void + { + $content = file_get_contents($url); + + $this->assertSame(sprintf("Example body for \"%s\"", $url), $content); + } + + /** @return iterable> */ + public static function urls(): iterable + { + yield ["https://example.com"]; + yield ["https://example.org"]; + } + + /** @return iterable> */ + public static function namedUrls(): iterable + { + yield 'example.com' => ["https://example.com"]; + yield 'example.org' => ["https://example.org"]; + } +} diff --git a/tests/AttributeDeclaredOnClassWithSeparateCassettesInSingleFolderTest.php b/tests/AttributeDeclaredOnClassWithSeparateCassettesInSingleFolderTest.php new file mode 100644 index 0000000..26026c8 --- /dev/null +++ b/tests/AttributeDeclaredOnClassWithSeparateCassettesInSingleFolderTest.php @@ -0,0 +1,54 @@ +assertSame("Example body for \"https://example.com\"", $content); + } + + #[Test] + #[DataProvider("urls")] + public function it_uses_vcr_on_methods_with_data_provider(string $url): void + { + $content = file_get_contents($url); + + $this->assertSame(sprintf("Example body for \"%s\"", $url), $content); + } + + #[Test] + #[DataProvider("namedUrls")] + public function it_uses_vcr_on_methods_with_data_provider_named_use_cases(string $url): void + { + $content = file_get_contents($url); + + $this->assertSame(sprintf("Example body for \"%s\"", $url), $content); + } + + /** @return iterable> */ + public static function urls(): iterable + { + yield ["https://example.com"]; + yield ["https://example.org"]; + } + + /** @return iterable> */ + public static function namedUrls(): iterable + { + yield 'example.com' => ["https://example.com"]; + yield 'example.org' => ["https://example.org"]; + } +} diff --git a/tests/AttributeDeclaredOnMethodsTest.php b/tests/AttributeDeclaredOnMethodsTest.php index 12673a2..7268ff6 100644 --- a/tests/AttributeDeclaredOnMethodsTest.php +++ b/tests/AttributeDeclaredOnMethodsTest.php @@ -20,6 +20,29 @@ public function it_uses_vcr_on_methods_with_attribute(): void $this->assertSame("Example body.", $content); } + #[Test] + #[UseCassette("on_methods_without_extension")] + public function it_uses_vcr_on_methods_with_attribute_cassette_without_extension(): void + { + $content = file_get_contents("https://example.com"); + + $this->assertSame("Example body.", $content); + } + + #[Test] + #[UseCassette( + name: "on_methods_without_extension_with_data_provider", + separateCassettePerCase: true, + groupCaseFilesInDirectory:true, + )] + #[DataProvider("namedUrls")] + public function it_uses_vcr_on_methods_with_attribute_cassette_and_data_provider_without_extension(string $url): void + { + $content = file_get_contents($url); + + $this->assertSame(sprintf("Example body for \"%s\"", $url), $content); + } + #[Test] #[UseCassette("with_data_provider.yml")] #[DataProvider("urls")] @@ -30,10 +53,41 @@ public function it_uses_vcr_on_methods_with_data_provider(string $url): void $this->assertSame(sprintf("Example body for \"%s\"", $url), $content); } + #[Test] + #[UseCassette(name: "with_data_provider_and_separated_cassettes.yml", separateCassettePerCase: true)] + #[DataProvider("namedUrls")] + public function it_uses_vcr_on_methods_with_data_provider_and_separate_cassette_per_case(string $url): void + { + $content = file_get_contents($url); + + $this->assertSame(sprintf("Example body for \"%s\"", $url), $content); + } + + #[Test] + #[UseCassette( + name: "with_data_provider_and_separated_cassettes_in_directory.yml", + separateCassettePerCase: true, + groupCaseFilesInDirectory: true, + )] + #[DataProvider("urls")] + public function it_uses_vcr_on_methods_with_data_provider_and_separate_cassette_per_case_in_directories(string $url): void + { + $content = file_get_contents($url); + + $this->assertSame(sprintf("Example body for \"%s\"", $url), $content); + } + /** @return iterable> */ public static function urls(): iterable { yield ["https://example.com"]; yield ["https://example.org"]; } + + /** @return iterable> */ + public static function namedUrls(): iterable + { + yield 'example.com' => ["https://example.com"]; + yield 'example.org' => ["https://example.org"]; + } } diff --git a/tests/WithoutVcrTest.php b/tests/WithoutVcrTest.php new file mode 100644 index 0000000..34a1af0 --- /dev/null +++ b/tests/WithoutVcrTest.php @@ -0,0 +1,18 @@ +assertTrue(true); + } +} diff --git a/tests/fixtures/on_methods_without_extension b/tests/fixtures/on_methods_without_extension new file mode 100644 index 0000000..c2ca2dc --- /dev/null +++ b/tests/fixtures/on_methods_without_extension @@ -0,0 +1,24 @@ +- + request: + method: GET + url: 'https://example.com' + headers: + Host: example.com + response: + status: + code: 200 + message: '' + headers: + age: '454479' + cache-control: max-age=604800 + content-type: 'text/html; charset=UTF-8' + date: 'Sun, 12 Mar 2023 10:07:20 GMT' + etag: '"3147526947+ident"' + expires: 'Sun, 19 Mar 2023 10:07:20 GMT' + last-modified: 'Thu, 17 Oct 2019 07:18:26 GMT' + server: 'ECS (dcb/7EA3)' + vary: Accept-Encoding + x-cache: HIT + content-length: '1256' + body: "Example body." + index: 0 diff --git a/tests/fixtures/on_methods_without_extension_with_data_provider/example-com b/tests/fixtures/on_methods_without_extension_with_data_provider/example-com new file mode 100644 index 0000000..9447be7 --- /dev/null +++ b/tests/fixtures/on_methods_without_extension_with_data_provider/example-com @@ -0,0 +1,24 @@ +- + request: + method: GET + url: 'https://example.com' + headers: + Host: example.com + response: + status: + code: 200 + message: '' + headers: + age: '455285' + cache-control: max-age=604800 + content-type: 'text/html; charset=UTF-8' + date: 'Sun, 12 Mar 2023 10:20:46 GMT' + etag: '"3147526947+ident"' + expires: 'Sun, 19 Mar 2023 10:20:46 GMT' + last-modified: 'Thu, 17 Oct 2019 07:18:26 GMT' + server: 'ECS (dcb/7EA3)' + vary: Accept-Encoding + x-cache: HIT + content-length: '1256' + body: "Example body for \"https://example.com\"" + index: 0 diff --git a/tests/fixtures/on_methods_without_extension_with_data_provider/example-org b/tests/fixtures/on_methods_without_extension_with_data_provider/example-org new file mode 100644 index 0000000..748aa65 --- /dev/null +++ b/tests/fixtures/on_methods_without_extension_with_data_provider/example-org @@ -0,0 +1,24 @@ +- + request: + method: GET + url: 'https://example.org' + headers: + Host: example.org + response: + status: + code: 200 + message: '' + headers: + age: '331432' + cache-control: max-age=604800 + content-type: 'text/html; charset=UTF-8' + date: 'Sun, 12 Mar 2023 10:17:02 GMT' + etag: '"3147526947+ident"' + expires: 'Sun, 19 Mar 2023 10:17:02 GMT' + last-modified: 'Thu, 17 Oct 2019 07:18:26 GMT' + server: 'ECS (dcb/7F83)' + vary: Accept-Encoding + x-cache: HIT + content-length: '1256' + body: "Example body for \"https://example.org\"" + index: 0 diff --git a/tests/fixtures/with_data_provider_and_separated_cassettes-example-com.yml b/tests/fixtures/with_data_provider_and_separated_cassettes-example-com.yml new file mode 100644 index 0000000..9447be7 --- /dev/null +++ b/tests/fixtures/with_data_provider_and_separated_cassettes-example-com.yml @@ -0,0 +1,24 @@ +- + request: + method: GET + url: 'https://example.com' + headers: + Host: example.com + response: + status: + code: 200 + message: '' + headers: + age: '455285' + cache-control: max-age=604800 + content-type: 'text/html; charset=UTF-8' + date: 'Sun, 12 Mar 2023 10:20:46 GMT' + etag: '"3147526947+ident"' + expires: 'Sun, 19 Mar 2023 10:20:46 GMT' + last-modified: 'Thu, 17 Oct 2019 07:18:26 GMT' + server: 'ECS (dcb/7EA3)' + vary: Accept-Encoding + x-cache: HIT + content-length: '1256' + body: "Example body for \"https://example.com\"" + index: 0 diff --git a/tests/fixtures/with_data_provider_and_separated_cassettes-example-org.yml b/tests/fixtures/with_data_provider_and_separated_cassettes-example-org.yml new file mode 100644 index 0000000..748aa65 --- /dev/null +++ b/tests/fixtures/with_data_provider_and_separated_cassettes-example-org.yml @@ -0,0 +1,24 @@ +- + request: + method: GET + url: 'https://example.org' + headers: + Host: example.org + response: + status: + code: 200 + message: '' + headers: + age: '331432' + cache-control: max-age=604800 + content-type: 'text/html; charset=UTF-8' + date: 'Sun, 12 Mar 2023 10:17:02 GMT' + etag: '"3147526947+ident"' + expires: 'Sun, 19 Mar 2023 10:17:02 GMT' + last-modified: 'Thu, 17 Oct 2019 07:18:26 GMT' + server: 'ECS (dcb/7F83)' + vary: Accept-Encoding + x-cache: HIT + content-length: '1256' + body: "Example body for \"https://example.org\"" + index: 0 diff --git a/tests/fixtures/with_data_provider_and_separated_cassettes_in_directory/0.yml b/tests/fixtures/with_data_provider_and_separated_cassettes_in_directory/0.yml new file mode 100644 index 0000000..9447be7 --- /dev/null +++ b/tests/fixtures/with_data_provider_and_separated_cassettes_in_directory/0.yml @@ -0,0 +1,24 @@ +- + request: + method: GET + url: 'https://example.com' + headers: + Host: example.com + response: + status: + code: 200 + message: '' + headers: + age: '455285' + cache-control: max-age=604800 + content-type: 'text/html; charset=UTF-8' + date: 'Sun, 12 Mar 2023 10:20:46 GMT' + etag: '"3147526947+ident"' + expires: 'Sun, 19 Mar 2023 10:20:46 GMT' + last-modified: 'Thu, 17 Oct 2019 07:18:26 GMT' + server: 'ECS (dcb/7EA3)' + vary: Accept-Encoding + x-cache: HIT + content-length: '1256' + body: "Example body for \"https://example.com\"" + index: 0 diff --git a/tests/fixtures/with_data_provider_and_separated_cassettes_in_directory/1.yml b/tests/fixtures/with_data_provider_and_separated_cassettes_in_directory/1.yml new file mode 100644 index 0000000..748aa65 --- /dev/null +++ b/tests/fixtures/with_data_provider_and_separated_cassettes_in_directory/1.yml @@ -0,0 +1,24 @@ +- + request: + method: GET + url: 'https://example.org' + headers: + Host: example.org + response: + status: + code: 200 + message: '' + headers: + age: '331432' + cache-control: max-age=604800 + content-type: 'text/html; charset=UTF-8' + date: 'Sun, 12 Mar 2023 10:17:02 GMT' + etag: '"3147526947+ident"' + expires: 'Sun, 19 Mar 2023 10:17:02 GMT' + last-modified: 'Thu, 17 Oct 2019 07:18:26 GMT' + server: 'ECS (dcb/7F83)' + vary: Accept-Encoding + x-cache: HIT + content-length: '1256' + body: "Example body for \"https://example.org\"" + index: 0 diff --git a/tests/fixtures/with_separate_cassettes_in_separated_folders/on_class.yml b/tests/fixtures/with_separate_cassettes_in_separated_folders/on_class.yml new file mode 100644 index 0000000..8b46c6e --- /dev/null +++ b/tests/fixtures/with_separate_cassettes_in_separated_folders/on_class.yml @@ -0,0 +1,24 @@ +- + request: + method: GET + url: 'https://example.com' + headers: + Host: example.com + response: + status: + code: 200 + message: '' + headers: + age: '455285' + cache-control: max-age=604800 + content-type: 'text/html; charset=UTF-8' + date: 'Sun, 12 Mar 2023 10:20:46 GMT' + etag: '"3147526947+ident"' + expires: 'Sun, 19 Mar 2023 10:20:46 GMT' + last-modified: 'Thu, 17 Oct 2019 07:18:26 GMT' + server: 'ECS (dcb/7EA3)' + vary: Accept-Encoding + x-cache: HIT + content-length: '1256' + body: "Example body for \"https://example.com\"" + index: 0 diff --git a/tests/fixtures/with_separate_cassettes_in_separated_folders/on_class/0.yml b/tests/fixtures/with_separate_cassettes_in_separated_folders/on_class/0.yml new file mode 100644 index 0000000..8b46c6e --- /dev/null +++ b/tests/fixtures/with_separate_cassettes_in_separated_folders/on_class/0.yml @@ -0,0 +1,24 @@ +- + request: + method: GET + url: 'https://example.com' + headers: + Host: example.com + response: + status: + code: 200 + message: '' + headers: + age: '455285' + cache-control: max-age=604800 + content-type: 'text/html; charset=UTF-8' + date: 'Sun, 12 Mar 2023 10:20:46 GMT' + etag: '"3147526947+ident"' + expires: 'Sun, 19 Mar 2023 10:20:46 GMT' + last-modified: 'Thu, 17 Oct 2019 07:18:26 GMT' + server: 'ECS (dcb/7EA3)' + vary: Accept-Encoding + x-cache: HIT + content-length: '1256' + body: "Example body for \"https://example.com\"" + index: 0 diff --git a/tests/fixtures/with_separate_cassettes_in_separated_folders/on_class/1.yml b/tests/fixtures/with_separate_cassettes_in_separated_folders/on_class/1.yml new file mode 100644 index 0000000..748aa65 --- /dev/null +++ b/tests/fixtures/with_separate_cassettes_in_separated_folders/on_class/1.yml @@ -0,0 +1,24 @@ +- + request: + method: GET + url: 'https://example.org' + headers: + Host: example.org + response: + status: + code: 200 + message: '' + headers: + age: '331432' + cache-control: max-age=604800 + content-type: 'text/html; charset=UTF-8' + date: 'Sun, 12 Mar 2023 10:17:02 GMT' + etag: '"3147526947+ident"' + expires: 'Sun, 19 Mar 2023 10:17:02 GMT' + last-modified: 'Thu, 17 Oct 2019 07:18:26 GMT' + server: 'ECS (dcb/7F83)' + vary: Accept-Encoding + x-cache: HIT + content-length: '1256' + body: "Example body for \"https://example.org\"" + index: 0 diff --git a/tests/fixtures/with_separate_cassettes_in_separated_folders/on_class/example-com.yml b/tests/fixtures/with_separate_cassettes_in_separated_folders/on_class/example-com.yml new file mode 100644 index 0000000..9447be7 --- /dev/null +++ b/tests/fixtures/with_separate_cassettes_in_separated_folders/on_class/example-com.yml @@ -0,0 +1,24 @@ +- + request: + method: GET + url: 'https://example.com' + headers: + Host: example.com + response: + status: + code: 200 + message: '' + headers: + age: '455285' + cache-control: max-age=604800 + content-type: 'text/html; charset=UTF-8' + date: 'Sun, 12 Mar 2023 10:20:46 GMT' + etag: '"3147526947+ident"' + expires: 'Sun, 19 Mar 2023 10:20:46 GMT' + last-modified: 'Thu, 17 Oct 2019 07:18:26 GMT' + server: 'ECS (dcb/7EA3)' + vary: Accept-Encoding + x-cache: HIT + content-length: '1256' + body: "Example body for \"https://example.com\"" + index: 0 diff --git a/tests/fixtures/with_separate_cassettes_in_separated_folders/on_class/example-org.yml b/tests/fixtures/with_separate_cassettes_in_separated_folders/on_class/example-org.yml new file mode 100644 index 0000000..748aa65 --- /dev/null +++ b/tests/fixtures/with_separate_cassettes_in_separated_folders/on_class/example-org.yml @@ -0,0 +1,24 @@ +- + request: + method: GET + url: 'https://example.org' + headers: + Host: example.org + response: + status: + code: 200 + message: '' + headers: + age: '331432' + cache-control: max-age=604800 + content-type: 'text/html; charset=UTF-8' + date: 'Sun, 12 Mar 2023 10:17:02 GMT' + etag: '"3147526947+ident"' + expires: 'Sun, 19 Mar 2023 10:17:02 GMT' + last-modified: 'Thu, 17 Oct 2019 07:18:26 GMT' + server: 'ECS (dcb/7F83)' + vary: Accept-Encoding + x-cache: HIT + content-length: '1256' + body: "Example body for \"https://example.org\"" + index: 0 diff --git a/tests/fixtures/with_separate_cassettes_in_single_folder/on_class-0.yml b/tests/fixtures/with_separate_cassettes_in_single_folder/on_class-0.yml new file mode 100644 index 0000000..9447be7 --- /dev/null +++ b/tests/fixtures/with_separate_cassettes_in_single_folder/on_class-0.yml @@ -0,0 +1,24 @@ +- + request: + method: GET + url: 'https://example.com' + headers: + Host: example.com + response: + status: + code: 200 + message: '' + headers: + age: '455285' + cache-control: max-age=604800 + content-type: 'text/html; charset=UTF-8' + date: 'Sun, 12 Mar 2023 10:20:46 GMT' + etag: '"3147526947+ident"' + expires: 'Sun, 19 Mar 2023 10:20:46 GMT' + last-modified: 'Thu, 17 Oct 2019 07:18:26 GMT' + server: 'ECS (dcb/7EA3)' + vary: Accept-Encoding + x-cache: HIT + content-length: '1256' + body: "Example body for \"https://example.com\"" + index: 0 diff --git a/tests/fixtures/with_separate_cassettes_in_single_folder/on_class-1.yml b/tests/fixtures/with_separate_cassettes_in_single_folder/on_class-1.yml new file mode 100644 index 0000000..748aa65 --- /dev/null +++ b/tests/fixtures/with_separate_cassettes_in_single_folder/on_class-1.yml @@ -0,0 +1,24 @@ +- + request: + method: GET + url: 'https://example.org' + headers: + Host: example.org + response: + status: + code: 200 + message: '' + headers: + age: '331432' + cache-control: max-age=604800 + content-type: 'text/html; charset=UTF-8' + date: 'Sun, 12 Mar 2023 10:17:02 GMT' + etag: '"3147526947+ident"' + expires: 'Sun, 19 Mar 2023 10:17:02 GMT' + last-modified: 'Thu, 17 Oct 2019 07:18:26 GMT' + server: 'ECS (dcb/7F83)' + vary: Accept-Encoding + x-cache: HIT + content-length: '1256' + body: "Example body for \"https://example.org\"" + index: 0 diff --git a/tests/fixtures/with_separate_cassettes_in_single_folder/on_class-example-com.yml b/tests/fixtures/with_separate_cassettes_in_single_folder/on_class-example-com.yml new file mode 100644 index 0000000..9447be7 --- /dev/null +++ b/tests/fixtures/with_separate_cassettes_in_single_folder/on_class-example-com.yml @@ -0,0 +1,24 @@ +- + request: + method: GET + url: 'https://example.com' + headers: + Host: example.com + response: + status: + code: 200 + message: '' + headers: + age: '455285' + cache-control: max-age=604800 + content-type: 'text/html; charset=UTF-8' + date: 'Sun, 12 Mar 2023 10:20:46 GMT' + etag: '"3147526947+ident"' + expires: 'Sun, 19 Mar 2023 10:20:46 GMT' + last-modified: 'Thu, 17 Oct 2019 07:18:26 GMT' + server: 'ECS (dcb/7EA3)' + vary: Accept-Encoding + x-cache: HIT + content-length: '1256' + body: "Example body for \"https://example.com\"" + index: 0 diff --git a/tests/fixtures/with_separate_cassettes_in_single_folder/on_class-example-org.yml b/tests/fixtures/with_separate_cassettes_in_single_folder/on_class-example-org.yml new file mode 100644 index 0000000..748aa65 --- /dev/null +++ b/tests/fixtures/with_separate_cassettes_in_single_folder/on_class-example-org.yml @@ -0,0 +1,24 @@ +- + request: + method: GET + url: 'https://example.org' + headers: + Host: example.org + response: + status: + code: 200 + message: '' + headers: + age: '331432' + cache-control: max-age=604800 + content-type: 'text/html; charset=UTF-8' + date: 'Sun, 12 Mar 2023 10:17:02 GMT' + etag: '"3147526947+ident"' + expires: 'Sun, 19 Mar 2023 10:17:02 GMT' + last-modified: 'Thu, 17 Oct 2019 07:18:26 GMT' + server: 'ECS (dcb/7F83)' + vary: Accept-Encoding + x-cache: HIT + content-length: '1256' + body: "Example body for \"https://example.org\"" + index: 0 diff --git a/tests/fixtures/with_separate_cassettes_in_single_folder/on_class.yml b/tests/fixtures/with_separate_cassettes_in_single_folder/on_class.yml new file mode 100644 index 0000000..9447be7 --- /dev/null +++ b/tests/fixtures/with_separate_cassettes_in_single_folder/on_class.yml @@ -0,0 +1,24 @@ +- + request: + method: GET + url: 'https://example.com' + headers: + Host: example.com + response: + status: + code: 200 + message: '' + headers: + age: '455285' + cache-control: max-age=604800 + content-type: 'text/html; charset=UTF-8' + date: 'Sun, 12 Mar 2023 10:20:46 GMT' + etag: '"3147526947+ident"' + expires: 'Sun, 19 Mar 2023 10:20:46 GMT' + last-modified: 'Thu, 17 Oct 2019 07:18:26 GMT' + server: 'ECS (dcb/7EA3)' + vary: Accept-Encoding + x-cache: HIT + content-length: '1256' + body: "Example body for \"https://example.com\"" + index: 0 From e57c21c3534d9d6a5009c784c041760c56a7b9ac Mon Sep 17 00:00:00 2001 From: Volodymyr Stolyarchuk Date: Tue, 15 Jul 2025 19:49:50 +0300 Subject: [PATCH 3/3] fix: remove explode by space --- src/Subscribers/AttributeResolverTrait.php | 2 -- tests/AttributeDeclaredOnMethodsTest.php | 1 + .../test-case-with-spaces | 24 +++++++++++++++++++ ...arated_cassettes-test-case-with-spaces.yml | 24 +++++++++++++++++++ 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/on_methods_without_extension_with_data_provider/test-case-with-spaces create mode 100644 tests/fixtures/with_data_provider_and_separated_cassettes-test-case-with-spaces.yml diff --git a/src/Subscribers/AttributeResolverTrait.php b/src/Subscribers/AttributeResolverTrait.php index 92099b6..c513392 100644 --- a/src/Subscribers/AttributeResolverTrait.php +++ b/src/Subscribers/AttributeResolverTrait.php @@ -47,8 +47,6 @@ private function getTestCaseCassetteParameters(string $test): ?TestCaseParameter private function parseMethod(string $test): TestMethodInfo { - $test = explode(" ", $test)[0]; - $methoDetails = explode("#", $test); return new TestMethodInfo( method: $methoDetails[0], diff --git a/tests/AttributeDeclaredOnMethodsTest.php b/tests/AttributeDeclaredOnMethodsTest.php index 7268ff6..fd8f4f1 100644 --- a/tests/AttributeDeclaredOnMethodsTest.php +++ b/tests/AttributeDeclaredOnMethodsTest.php @@ -89,5 +89,6 @@ public static function namedUrls(): iterable { yield 'example.com' => ["https://example.com"]; yield 'example.org' => ["https://example.org"]; + yield 'test case with spaces' => ["https://example.org"]; } } diff --git a/tests/fixtures/on_methods_without_extension_with_data_provider/test-case-with-spaces b/tests/fixtures/on_methods_without_extension_with_data_provider/test-case-with-spaces new file mode 100644 index 0000000..748aa65 --- /dev/null +++ b/tests/fixtures/on_methods_without_extension_with_data_provider/test-case-with-spaces @@ -0,0 +1,24 @@ +- + request: + method: GET + url: 'https://example.org' + headers: + Host: example.org + response: + status: + code: 200 + message: '' + headers: + age: '331432' + cache-control: max-age=604800 + content-type: 'text/html; charset=UTF-8' + date: 'Sun, 12 Mar 2023 10:17:02 GMT' + etag: '"3147526947+ident"' + expires: 'Sun, 19 Mar 2023 10:17:02 GMT' + last-modified: 'Thu, 17 Oct 2019 07:18:26 GMT' + server: 'ECS (dcb/7F83)' + vary: Accept-Encoding + x-cache: HIT + content-length: '1256' + body: "Example body for \"https://example.org\"" + index: 0 diff --git a/tests/fixtures/with_data_provider_and_separated_cassettes-test-case-with-spaces.yml b/tests/fixtures/with_data_provider_and_separated_cassettes-test-case-with-spaces.yml new file mode 100644 index 0000000..92f524a --- /dev/null +++ b/tests/fixtures/with_data_provider_and_separated_cassettes-test-case-with-spaces.yml @@ -0,0 +1,24 @@ +- + request: + method: GET + url: 'https://example.org' + headers: + Host: example.org + response: + status: + code: 200 + message: '' + headers: + age: '331432' + cache-control: max-age=604800 + content-type: 'text/html; charset=UTF-8' + date: 'Sun, 12 Mar 2023 10:17:02 GMT' + etag: '"3147526947+ident"' + expires: 'Sun, 19 Mar 2023 10:17:02 GMT' + last-modified: 'Thu, 17 Oct 2019 07:18:26 GMT' + server: 'ECS (dcb/7F83)' + vary: Accept-Encoding + x-cache: HIT + content-length: '1256' + body: "Example body for \"https://example.org\"" + index: 0