|string> */ public array $datasets = []; /** * The FQN of the Test Case class. * * @var class-string */ public string $class = TestCase::class; /** * An array of FQN of the Test Case traits. * * @var array */ public array $traits = [ Concerns\Testable::class, Concerns\Expectable::class, ]; /** * The higher order messages for the factory that are proxyable. */ public HigherOrderMessageCollection $factoryProxies; /** * The higher order messages that are proxyable. */ public HigherOrderMessageCollection $proxies; /** * The higher order messages that are chainable. */ public HigherOrderMessageCollection $chains; /** * Creates a new Factory instance. */ public function __construct( public string $filename, public ?string $description, Closure $closure = null) { $this->test = $closure ?? fn () => Assert::getCount() > 0 ?: self::markTestIncomplete(); $this->factoryProxies = new HigherOrderMessageCollection(); $this->proxies = new HigherOrderMessageCollection(); $this->chains = new HigherOrderMessageCollection(); } /** * Makes the Test Case classes. * * @return array */ public function make(): array { if ($this->description === null) { throw ShouldNotHappen::fromMessage('Description can not be empty.'); } $chains = $this->chains; $proxies = $this->proxies; $factoryTest = $this->test; $testClosure = function () use ($chains, $proxies, $factoryTest): mixed { $proxies->proxy($this); $chains->chain($this); /* @phpstan-ignore-next-line */ return call_user_func(Closure::bind($factoryTest, $this, $this::class), ...func_get_args()); }; $className = $this->makeClassFromFilename($this->filename); $createTest = function ($description, $data) use ($className, $testClosure) { $testCase = new $className($testClosure, $description, $data); $this->factoryProxies->proxy($testCase); return $testCase; }; $datasets = Datasets::resolve($this->description, $this->datasets); return array_map($createTest, array_keys($datasets), $datasets); } /** * Makes a Fully Qualified Class Name from the given filename. */ public function makeClassFromFilename(string $filename): string { if ('\\' === DIRECTORY_SEPARATOR) { // In case Windows, strtolower drive name, like in UsesCall. $filename = (string) preg_replace_callback('~^(?P[a-z]+:\\\)~i', 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(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; } try { eval(" namespace $namespace; 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; } /** * Determine if the test case will receive argument input from Pest, or not. */ public function __receivesArguments(): bool { return count($this->datasets) > 0 || $this->factoryProxies->count('addDependencies') > 0; } }