*/ private static array $annotations = [ Annotations\Depends::class, Annotations\Groups::class, ]; /** * The FQN of the Test Case class. * * @var class-string */ public string $class = TestCase::class; /** * The list of class methods. * * @var array */ public array $methods = []; /** * The list of class traits. * * @var array */ public array $traits = [ Concerns\Testable::class, Concerns\Expectable::class, ]; /** * Creates a new Factory instance. */ public function __construct( public string $filename ) { $this->bootHigherOrderable(); } public function make(): void { $methodsUsingOnly = $this->methodsUsingOnly(); $methods = array_values(array_filter( $this->methods, fn ($method) => count($methodsUsingOnly) === 0 || in_array($method, $methodsUsingOnly, true) )); if (count($methods) > 0) { $this->evaluate($this->filename, $methods); } } /** * Returns all the "only" methods. * * @return array */ public function methodsUsingOnly(): array { if (Environment::name() === Environment::CI) { return []; } return array_values(array_filter($this->methods, static fn ($method): bool => $method->only)); } /** * Creates a Test Case class using a runtime evaluate. * * @param array $methods */ public function evaluate(string $filename, array $methods): string { if ('\\' === DIRECTORY_SEPARATOR) { // In case Windows, strtolower drive name, like in UsesCall. $filename = (string) preg_replace_callback('~^(?P[a-z]+:\\\)~i', static fn ($match): string => strtolower($match['drive']), $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'); $relativePath = str_replace(DIRECTORY_SEPARATOR, '\\', $relativePath); // 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(fn (string $quote): string => sprintf('\\%s', $quote), ['\'', '"']), '', $relativePath); // Limit to A-Z, a-z, 0-9, '_', '-'. $relativePath = (string) preg_replace('/[^A-Za-z0-9\\\\]/', '', $relativePath); $classFQN = 'P\\' . $relativePath; if (class_exists($classFQN)) { return $classFQN; } $hasPrintableTestCaseClassFQN = sprintf('\%s', HasPrintableTestCaseName::class); $traitsCode = sprintf('use %s;', implode(', ', array_map( static fn ($trait): string => sprintf('\%s', $trait), $this->traits)) ); $partsFQN = explode('\\', $classFQN); $className = array_pop($partsFQN); $namespace = implode('\\', $partsFQN); $baseClass = sprintf('\%s', $this->class); if ('' === trim($className)) { $className = 'InvalidTestName' . Str::random(); $classFQN .= $className; } $methodsCode = implode('', array_map(static function (TestCaseMethodFactory $method): string { if ($method->description === null) { throw ShouldNotHappen::fromMessage('The test description may not be empty.'); } $methodName = Str::evaluable($method->description); $datasetsCode = ''; $annotations = ['@test']; foreach (self::$annotations as $annotation) { /** @phpstan-ignore-next-line */ $annotations = (new $annotation())->__invoke($method, $annotations); } if (count($method->datasets) > 0) { $dataProviderName = $methodName . '_dataset'; $annotations[] = "@dataProvider $dataProviderName"; Datasets::with($method->filename, $methodName, $method->datasets); $datasetsCode = << sprintf("\n * %s", $annotation), $annotations, )); return <<__runTest( \$this->__test, ...func_get_args(), ); } $datasetsCode EOF; }, $methods)); try { eval(" namespace $namespace; use Pest\Datasets as __PestDatasets; use Pest\TestSuite as __PestTestSuite; final class $className extends $baseClass implements $hasPrintableTestCaseClassFQN { $traitsCode private static \$__filename = '$filename'; $methodsCode } "); } catch (ParseError $caught) { throw new RuntimeException(sprintf('Unable to create test case for test file at %s', $filename), 1, $caught); } return $classFQN; } /** * Adds the given Method to the Test Case. */ public function addMethod(TestCaseMethodFactory $method): void { if ($method->description === null) { throw ShouldNotHappen::fromMessage('The test description may not be empty.'); } if (array_key_exists($method->description, $this->methods)) { throw new TestAlreadyExist($method->filename, $method->description); } if (!$method->receivesArguments()) { if ($method->closure === null) { throw ShouldNotHappen::fromMessage('The test closure may not be empty.'); } $arguments = Reflection::getFunctionArguments($method->closure); if (count($arguments) > 0) { throw new DatasetMissing($method->filename, $method->description, $arguments); } } $this->methods[$method->description] = $method; } /** * Gets a Method by the given name. */ public function getMethod(string $methodName): TestCaseMethodFactory { foreach ($this->methods as $method) { if ($method->description === null) { throw ShouldNotHappen::fromMessage('The test description may not be empty.'); } if (Str::evaluable($method->description) === $methodName) { return $method; } } throw ShouldNotHappen::fromMessage(sprintf('Method %s not found.', $methodName)); } }