diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php index f3fdf031..4d628e5a 100644 --- a/src/Factories/TestCaseFactory.php +++ b/src/Factories/TestCaseFactory.php @@ -5,14 +5,17 @@ declare(strict_types=1); namespace Pest\Factories; use Closure; +use ParseError; use Pest\Concerns; use Pest\Contracts\HasPrintableTestCaseName; use Pest\Datasets; use Pest\Exceptions\ShouldNotHappen; use Pest\Support\HigherOrderMessageCollection; use Pest\Support\NullClosure; +use Pest\Support\Str; use Pest\TestSuite; use PHPUnit\Framework\TestCase; +use RuntimeException; /** * @internal @@ -168,7 +171,7 @@ final class TestCaseFactory }, $filename); } - $filename = (string) realpath($filename); + $filename = str_replace('\\\\', '\\', addslashes((string) realpath($filename))); $rootPath = TestSuite::getInstance()->rootPath; $relativePath = str_replace($rootPath . DIRECTORY_SEPARATOR, '', $filename); $relativePath = dirname(ucfirst($relativePath)) . DIRECTORY_SEPARATOR . basename($relativePath, '.php'); @@ -176,8 +179,12 @@ final class TestCaseFactory // Strip out any %-encoded octets. $relativePath = (string) preg_replace('|%[a-fA-F0-9][a-fA-F0-9]|', '', $relativePath); + // Remove escaped quote sequences (maintain namespace) + $relativePath = str_replace(array_map(function (string $quote): string { + return sprintf('\\%s', $quote); + }, ['\'', '"']), '', $relativePath); // Limit to A-Z, a-z, 0-9, '_', '-'. - $relativePath = (string) preg_replace('/[^A-Za-z0-9.\\\]/', '', $relativePath); + $relativePath = (string) preg_replace('/[^A-Za-z0-9\\\\]/', '', $relativePath); $classFQN = 'P\\' . $relativePath; if (class_exists($classFQN)) { @@ -194,15 +201,24 @@ final class TestCaseFactory $namespace = implode('\\', $partsFQN); $baseClass = sprintf('\%s', $this->class); - eval(" - namespace $namespace; + if ('' === trim($className)) { + $className = 'InvalidTestName' . Str::random(); + $classFQN .= $className; + } - final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN { - $traitsCode + try { + eval(" + namespace $namespace; - private static \$__filename = '$filename'; - } - "); + final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN { + $traitsCode + + private static \$__filename = '$filename'; + } + "); + } catch (ParseError $caught) { + throw new RuntimeException(sprintf('Unable to create test case for test file at %s', $filename), 1, $caught); + } return $classFQN; } diff --git a/src/Support/Str.php b/src/Support/Str.php index aa135647..7a07a105 100644 --- a/src/Support/Str.php +++ b/src/Support/Str.php @@ -9,6 +9,25 @@ namespace Pest\Support; */ final class Str { + /** + * Pool of alpha-numeric characters for generating (unsafe) random strings + * from. + */ + private const POOL = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + + /** + * Create a (unsecure & non-cryptographically safe) random alpha-numeric + * string value. + * + * @param int $length the length of the resulting randomized string + * + * @see https://github.com/laravel/framework/blob/4.2/src/Illuminate/Support/Str.php#L240-L242 + */ + public static function random(int $length = 16): string + { + return substr(str_shuffle(str_repeat(self::POOL, 5)), 0, $length); + } + /** * Checks if the given `$target` starts with the given `$search`. */ diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt index 36136f81..f4db6877 100644 --- a/tests/.snapshots/success.txt +++ b/tests/.snapshots/success.txt @@ -103,6 +103,27 @@ PASS Tests\Fixtures\ExampleTest ✓ it example 2 + PASS Tests\PHPUnit\CustomAffixes\InvalidTestName + ✓ it runs file names like `@#$%^&()-_=+.php` + + PASS Tests\PHPUnit\CustomAffixes\ATestWithSpaces + ✓ it runs file names like `A Test With Spaces.php` + + PASS Tests\PHPUnit\CustomAffixes\AdditionalFileExtensionspec + ✓ it runs file names like `AdditionalFileExtension.spec.php` + + PASS Tests\PHPUnit\CustomAffixes\ManyExtensionsclasstest + ✓ it runs file names like `ManyExtensions.class.test.php` + + PASS Tests\PHPUnit\CustomAffixes\TestCaseWithQuotes + ✓ it runs file names like `Test 'Case' With Quotes.php` + + PASS Tests\PHPUnit\CustomAffixes\kebabcasespec + ✓ it runs file names like `kebab-case-spec.php` + + PASS Tests\PHPUnit\CustomAffixes\snakecasespec + ✓ it runs file names like `snake_case_spec.php` + PASS Tests\PHPUnit\CustomTestCase\UsesPerDirectory ✓ closure was bound to CustomTestCase @@ -188,5 +209,5 @@ ✓ it is a test ✓ it uses correct parent class - Tests: 7 skipped, 108 passed + Tests: 7 skipped, 115 passed \ No newline at end of file diff --git a/tests/PHPUnit/CustomAffixes/@#$%^&()-_=+.php b/tests/PHPUnit/CustomAffixes/@#$%^&()-_=+.php new file mode 100644 index 00000000..c829124d --- /dev/null +++ b/tests/PHPUnit/CustomAffixes/@#$%^&()-_=+.php @@ -0,0 +1,10 @@ +assertTrue(true); diff --git a/tests/PHPUnit/CustomAffixes/A Test With Spaces.php b/tests/PHPUnit/CustomAffixes/A Test With Spaces.php new file mode 100644 index 00000000..28497a1e --- /dev/null +++ b/tests/PHPUnit/CustomAffixes/A Test With Spaces.php @@ -0,0 +1,3 @@ +assertTrue(true); diff --git a/tests/PHPUnit/CustomAffixes/AdditionalFileExtension.spec.php b/tests/PHPUnit/CustomAffixes/AdditionalFileExtension.spec.php new file mode 100644 index 00000000..28497a1e --- /dev/null +++ b/tests/PHPUnit/CustomAffixes/AdditionalFileExtension.spec.php @@ -0,0 +1,3 @@ +assertTrue(true); diff --git a/tests/PHPUnit/CustomAffixes/ManyExtensions.class.test.php b/tests/PHPUnit/CustomAffixes/ManyExtensions.class.test.php new file mode 100644 index 00000000..28497a1e --- /dev/null +++ b/tests/PHPUnit/CustomAffixes/ManyExtensions.class.test.php @@ -0,0 +1,3 @@ +assertTrue(true); diff --git a/tests/PHPUnit/CustomAffixes/Test 'Case' With Quotes.php b/tests/PHPUnit/CustomAffixes/Test 'Case' With Quotes.php new file mode 100644 index 00000000..28497a1e --- /dev/null +++ b/tests/PHPUnit/CustomAffixes/Test 'Case' With Quotes.php @@ -0,0 +1,3 @@ +assertTrue(true); diff --git a/tests/PHPUnit/CustomAffixes/kebab-case-spec.php b/tests/PHPUnit/CustomAffixes/kebab-case-spec.php new file mode 100644 index 00000000..28497a1e --- /dev/null +++ b/tests/PHPUnit/CustomAffixes/kebab-case-spec.php @@ -0,0 +1,3 @@ +assertTrue(true); diff --git a/tests/PHPUnit/CustomAffixes/snake_case_spec.php b/tests/PHPUnit/CustomAffixes/snake_case_spec.php new file mode 100644 index 00000000..28497a1e --- /dev/null +++ b/tests/PHPUnit/CustomAffixes/snake_case_spec.php @@ -0,0 +1,3 @@ +assertTrue(true); diff --git a/tests/Visual/Success.php b/tests/Visual/Success.php index 1323da5b..8c5e1d6f 100644 --- a/tests/Visual/Success.php +++ b/tests/Visual/Success.php @@ -13,7 +13,13 @@ test('visual snapshot of test suite on success', function () { $process->run(); - return preg_replace('#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $process->getOutput()); + return preg_replace([ + '#\\x1b[[][^A-Za-z]*[A-Za-z]#', + '/(Tests\\\PHPUnit\\\CustomAffixes\\\InvalidTestName)([A-Za-z0-9]*)/', + ], [ + '', + '$1', + ], $process->getOutput()); }; if (getenv('REBUILD_SNAPSHOTS')) {