Skip to content
12 changes: 11 additions & 1 deletion src/Plugins/Parallel/Paratest/ResultPrinter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

use ParaTest\Options;
use Pest\Plugins\Parallel\Support\CompactPrinter;
use Pest\Plugins\Profile;
use Pest\Result;
use Pest\Support\StateGenerator;
use PHPUnit\TestRunner\TestResult\TestResult;
use PHPUnit\TextUI\Output\Printer;
Expand Down Expand Up @@ -130,6 +132,8 @@ public function printFeedback(
*/
public function printResults(TestResult $testResult, array $teamcityFiles, array $testdoxFiles, Duration $duration): void
{
$profile = Profile::results();

if ($this->options->needsTeamcity) {
$teamcityProgress = $this->tailMultiple($teamcityFiles);

Expand Down Expand Up @@ -166,7 +170,13 @@ public function printResults(TestResult $testResult, array $teamcityFiles, array

if (! isset($_SERVER['PEST_PARALLEL_NO_OUTPUT'])) {
$this->compactPrinter->errors($state);
$this->compactPrinter->recap($state, $testResult, $duration, $this->options);
$this->compactPrinter->recap(
$state,
$testResult,
$duration,
$this->options,
Result::ok($this->options->configuration, $testResult) ? $profile : [],
);
}
}

Expand Down
10 changes: 9 additions & 1 deletion src/Plugins/Parallel/Support/CompactPrinter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use NunoMaduro\Collision\Adapters\Phpunit\State;
use NunoMaduro\Collision\Adapters\Phpunit\Style;
use NunoMaduro\Collision\Adapters\Phpunit\TestResult;
use ParaTest\Options;
use PHPUnit\Event\Telemetry\CpuTime;
use PHPUnit\Event\Telemetry\GarbageCollectorStatus;
Expand Down Expand Up @@ -96,7 +97,10 @@ public function errors(State $state): void
$this->style->writeErrorsSummary($state);
}

public function recap(State $state, PHPUnitTestResult $testResult, Duration $duration, Options $options): void
/**
* @param list<TestResult> $profile
*/
public function recap(State $state, PHPUnitTestResult $testResult, Duration $duration, Options $options, array $profile = []): void
{
assert($this->output instanceof ConsoleOutput);

Expand Down Expand Up @@ -156,5 +160,9 @@ public function recap(State $state, PHPUnitTestResult $testResult, Duration $dur
"\n",
"\n",
]);

if ($profile !== []) {
$this->style->writeSlowTests($profile, $telemetry);
}
}
}
119 changes: 114 additions & 5 deletions src/Plugins/Profile.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,138 @@

namespace Pest\Plugins;

use const DIRECTORY_SEPARATOR;

use NunoMaduro\Collision\Adapters\Phpunit\TestResult;
use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Exceptions\InvalidOption;
use PHPUnit\Event;
use PHPUnit\Event\Telemetry\HRTime;
use PHPUnit\Event\Test\Finished;
use PHPUnit\Event\Test\FinishedSubscriber;
use PHPUnit\Event\Test\PreparationStarted;
use PHPUnit\Event\Test\PreparationStartedSubscriber;

/**
* @internal
*/
final class Profile implements HandlesArguments
{
use Concerns\HandleArguments;
/** @var array<string, HRTime> */
private static array $startTimes = [];

/**
* {@inheritDoc}
*/
public function handleArguments(array $arguments): array
{
if (! $this->hasArgument('--profile', $arguments)) {
$runId = Parallel::getGlobal('PROFILE_RUN_ID');

if (! isset($_SERVER['COLLISION_PRINTER_PROFILE']) && ! is_string($runId)) {
return $arguments;
}

if ($this->hasArgument('--parallel', $arguments)) {
throw new InvalidOption('The [--profile] option is not supported when running in parallel.');
if (Parallel::isWorker() && is_string($runId)) {
Event\Facade::instance()->registerSubscriber(new class implements PreparationStartedSubscriber
{
public function notify(PreparationStarted $event): void
{
Profile::started($event);
}
});

Event\Facade::instance()->registerSubscriber(new class implements FinishedSubscriber
{
public function notify(Finished $event): void
{
Profile::finished($event);
}
});

return $arguments;
}

if (Parallel::isEnabled()) {
Parallel::setGlobal('PROFILE_RUN_ID', bin2hex(random_bytes(16)));
}

return $arguments;
}

public static function started(PreparationStarted $event): void
{
self::$startTimes[$event->test()->id()] = $event->telemetryInfo()->time();
}

public static function finished(Finished $event): void
{
$test = $event->test();

if (! isset(self::$startTimes[$test->id()])) {
return;
}

$duration = $event->telemetryInfo()->time()->duration(self::$startTimes[$test->id()]);
$result = TestResult::fromPestParallelTestCase($test, TestResult::PASS);
$result->setDuration($duration->asFloat() * 1000);

unset(self::$startTimes[$test->id()]);

$runId = Parallel::getGlobal('PROFILE_RUN_ID');

if (is_string($runId)) {
file_put_contents(
self::resultPath($runId),
base64_encode(serialize($result)).PHP_EOL,
FILE_APPEND | LOCK_EX,
);
}
}

/** @return list<TestResult> */
public static function results(): array
{
$runId = Parallel::getGlobal('PROFILE_RUN_ID');

if (! is_string($runId)) {
return [];
}

$path = self::resultPath($runId);

if (! is_file($path)) {
return [];
}

$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
unlink($path);

if ($lines === false) {
return [];
}

$results = [];

foreach ($lines as $line) {
$serializedResult = base64_decode($line, true);

if ($serializedResult === false) {
continue;
}

$result = unserialize($serializedResult, ['allowed_classes' => [TestResult::class]]);

if ($result instanceof TestResult) {
$results[] = $result;
}
}

usort($results, fn (TestResult $a, TestResult $b): int => $b->duration <=> $a->duration);

return array_slice($results, 0, 10);
}

private static function resultPath(string $runId): string
{
return sys_get_temp_dir().DIRECTORY_SEPARATOR.'__pest_profile_'.$runId.'.txt';
}
}
3 changes: 2 additions & 1 deletion tests/Features/Tia.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use Pest\Plugins\Tia;
use Pest\Plugins\Tia\ChangedFiles;
use Pest\Plugins\Tia\CiDefaultBranch;
use Pest\Plugins\Tia\FileState;
use Pest\Plugins\Tia\Fingerprint;
use Pest\Plugins\Tia\Graph;
Expand All @@ -18,7 +19,7 @@

try {
$changedFiles = new ChangedFiles($projectRoot);
$branch = $changedFiles->currentBranch() ?? 'main';
$branch = $changedFiles->currentBranch() ?? CiDefaultBranch::detect() ?? 'main';
$sha = $changedFiles->currentSha();

$id = fn (string $description): string => 'P\Tests\Fixtures\Suites\TiaReplayHooks::'.Str::evaluable($description);
Expand Down
8 changes: 7 additions & 1 deletion tests/Visual/Parallel.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
test('parallel', function () use ($run): void {
$output = $run('--exclude-group=integration');
$output = implode("\n", array_slice(explode("\n", (string) $output), -10));
$profileOutput = $run('tests/Fixtures/Suites/SuccessOnly.php', '--profile');

if (getenv('REBUILD_SNAPSHOTS')) {
preg_match('/Tests:\s+(.+\(\d+ assertions\))/', $output, $matches);
Expand All @@ -36,7 +37,12 @@

expect($output)
->toContain("Tests: {$expected}")
->toContain('Parallel: 3 processes');
->and(
str_contains($output, 'Parallel: 3 processes')
&& str_contains((string) $profileOutput, 'Top 10 slowest tests:')
&& str_contains((string) $profileOutput, 'can pass with comparison')
&& str_contains((string) $profileOutput, 'can also pass'),
)->toBeTrue();
})->skipOnWindows();

test('a parallel test can extend another test with same name', function () use ($run): void {
Expand Down