stateGenerator = new StateGenerator; } /** * Gets the test case method name. */ public function getTestCaseMethodName(Test $test): string { if (! $test instanceof TestMethod) { throw ShouldNotHappen::fromMessage('Not an instance of TestMethod'); } return $test->testDox()->prettifiedMethodName(); } /** * Gets the test case location. */ public function getTestCaseLocation(Test $test): string { if (! $test instanceof TestMethod) { throw ShouldNotHappen::fromMessage('Not an instance of TestMethod'); } $path = $test->testDox()->prettifiedClassName(); $relativePath = $this->toRelativePath($path); // TODO: Get the description without the dataset. $description = $test->testDox()->prettifiedMethodName(); return "$relativePath::$description"; } /** * Gets the exception message. */ public function getExceptionMessage(Throwable $throwable): string { if (is_a($throwable->className(), FrameworkException::class, true)) { return $throwable->message(); } $buffer = $throwable->className(); $throwableMessage = $throwable->message(); if ($throwableMessage !== '') { $buffer .= ": $throwableMessage"; } return $buffer; } /** * Gets the exception details. */ public function getExceptionDetails(Throwable $throwable): string { $buffer = $this->getStackTrace($throwable); while ($throwable->hasPrevious()) { $throwable = $throwable->previous(); $buffer .= sprintf( "\nCaused by\n%s\n%s", $throwable->description(), $this->getStackTrace($throwable) ); } return $buffer; } /** * Gets the stack trace. */ public function getStackTrace(Throwable $throwable): string { $stackTrace = $throwable->stackTrace(); // Split stacktrace per frame. $frames = explode("\n", $stackTrace); // Remove empty lines $frames = array_filter($frames); // clean the paths of each frame. $frames = array_map( $this->toRelativePath(...), $frames ); // Format stacktrace as `at ` $frames = array_map( fn (string $frame): string => "at $frame", $frames ); return implode("\n", $frames); } /** * Gets the test suite name. */ public function getTestSuiteName(TestSuite $testSuite): string { if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) { $firstTest = $this->getFirstTest($testSuite); if ($firstTest instanceof TestMethod) { return $this->getTestMethodNameWithoutDatasetSuffix($firstTest); } } $name = $testSuite->name(); if (! str_starts_with($name, self::PREFIX)) { return $name; } return Str::after($name, self::PREFIX); } /** * Gets the trimmed test class name. */ public function getTrimmedTestClassName(TestMethod $test): string { return Str::after($test->className(), self::PREFIX); } /** * Gets the test suite location. */ public function getTestSuiteLocation(TestSuite $testSuite): ?string { $firstTest = $this->getFirstTest($testSuite); if (! $firstTest instanceof TestMethod) { return null; } $path = $firstTest->testDox()->prettifiedClassName(); $classRelativePath = $this->toRelativePath($path); if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) { $methodName = $this->getTestMethodNameWithoutDatasetSuffix($firstTest); return "$classRelativePath::$methodName"; } return $classRelativePath; } /** * Gets the prettified test method name without dataset-related suffix. */ private function getTestMethodNameWithoutDatasetSuffix(TestMethod $testMethod): string { return Str::beforeLast($testMethod->testDox()->prettifiedMethodName(), ' with data set '); } /** * Gets the first test from the test suite. */ private function getFirstTest(TestSuite $testSuite): ?TestMethod { $tests = $testSuite->tests()->asArray(); // TODO: figure out how to get the file path without a test being there. if ($tests === []) { return null; } $firstTest = $tests[0]; if (! $firstTest instanceof TestMethod) { throw ShouldNotHappen::fromMessage('Not an instance of TestMethod'); } return $firstTest; } /** * Gets the test suite size. */ public function getTestSuiteSize(TestSuite $testSuite): int { return $testSuite->count(); } /** * Transforms the given path in relative path. */ private function toRelativePath(string $path): string { // Remove cwd from the path. return str_replace("$this->rootPath".DIRECTORY_SEPARATOR, '', $path); } /** * Get the test result. */ public function getStateFromResult(PhpUnitTestResult $result): State { $events = [ ...$result->testErroredEvents(), ...$result->testFailedEvents(), ...$result->testSkippedEvents(), ...array_merge(...array_values($result->testConsideredRiskyEvents())), ...$result->testMarkedIncompleteEvents(), ]; $notPassedTests = []; foreach ($events as $event) { if ($event instanceof AfterLastTestMethodErrored) { // PHPUnit's collector does not count these towards `numberOfTestsRun`... continue; } if ($event instanceof AfterLastTestMethodFailed) { // PHPUnit's collector does not count these towards `numberOfTestsRun`... continue; } if ($event instanceof BeforeFirstTestMethodErrored || $event instanceof BeforeFirstTestMethodFailed) { $notPassedTests[] = $event->testClassName(); continue; } $notPassedTests[] = $this->getTestCaseLocation($event->test()); } $numberOfPassedTests = $result->numberOfTestsRun() - count(array_unique($notPassedTests)) - $result->numberOfTestSkippedByTestSuiteSkippedEvents(); return $this->stateGenerator->fromPhpUnitTestResult($numberOfPassedTests, $result); } }