Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 22 additions & 14 deletions src/Command/AnalyseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
);
}

// CI runners read stdout and stderr through separate pipes and interleave them
// non-deterministically, so the output following human-readable stdout output goes to stdout too
$summaryOutput = ($generateBaselineFile !== null || $errorFormat === 'table') && (new CiDetector())->isCiDetected()
? $inceptionResult->getStdOutput()
: $errorOutput;

if ($generateBaselineFile !== null) {
$this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput(), $analysisResult->getProcessedFiles());
if (count($internalErrorsTuples) > 0) {
Expand All @@ -505,10 +511,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
count($internalErrors) === 1 ? 'An internal error' : 'Internal errors',
));

return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime, $analysisResult->getWorkerCount());
return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime, $analysisResult->getWorkerCount(), $summaryOutput);
}

return $this->generateBaseline($generateBaselineFile, $inceptionResult, $analysisResult, $output, $allowEmptyBaseline, $baselineExtension, $failWithoutResultCache);
return $this->generateBaseline($generateBaselineFile, $inceptionResult, $analysisResult, $output, $allowEmptyBaseline, $baselineExtension, $failWithoutResultCache, $summaryOutput);
}

/** @var ErrorFormatter $errorFormatter */
Expand All @@ -532,18 +538,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$exitCode = $errorFormatter->formatErrors($analysisResult, $inceptionResult->getStdOutput());

$this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput(), $analysisResult->getProcessedFiles());
$this->runDiagnoseExtensions($container, $summaryOutput, $analysisResult->getProcessedFiles());

$errorOutput->writeLineFormatted('⚠️ Result is incomplete because of severe errors. ⚠️');
$errorOutput->writeLineFormatted(' Fix these errors first and then re-run PHPStan');
$errorOutput->writeLineFormatted(' to get all reported errors.');
$errorOutput->writeLineFormatted('');
$summaryOutput->writeLineFormatted('⚠️ Result is incomplete because of severe errors. ⚠️');
$summaryOutput->writeLineFormatted(' Fix these errors first and then re-run PHPStan');
$summaryOutput->writeLineFormatted(' to get all reported errors.');
$summaryOutput->writeLineFormatted('');

return $inceptionResult->handleReturn(
$exitCode,
$analysisResult->getPeakMemoryUsageBytes(),
$this->analysisStartTime,
$analysisResult->getWorkerCount(),
$summaryOutput,
);
}

Expand Down Expand Up @@ -640,15 +647,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$exitCode = 2;
}

$this->reportMissingResultCacheInCi($errorOutput, $analysisResult, $onlyFiles);
$this->reportMissingResultCacheInCi($summaryOutput, $analysisResult, $onlyFiles);

$this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput(), $analysisResult->getProcessedFiles());
$this->runDiagnoseExtensions($container, $summaryOutput, $analysisResult->getProcessedFiles());

return $inceptionResult->handleReturn(
$exitCode,
$analysisResult->getPeakMemoryUsageBytes(),
$this->analysisStartTime,
$analysisResult->getWorkerCount(),
$summaryOutput,
);
}

Expand Down Expand Up @@ -757,13 +765,13 @@ private function getMessageFromInternalError(FileHelper $fileHelper, InternalErr
return $message;
}

private function generateBaseline(string $generateBaselineFile, InceptionResult $inceptionResult, AnalysisResult $analysisResult, OutputInterface $output, bool $allowEmptyBaseline, string $baselineExtension, bool $failWithoutResultCache): int
private function generateBaseline(string $generateBaselineFile, InceptionResult $inceptionResult, AnalysisResult $analysisResult, OutputInterface $output, bool $allowEmptyBaseline, string $baselineExtension, bool $failWithoutResultCache, Output $summaryOutput): int
{
if (!$allowEmptyBaseline && !$analysisResult->hasErrors()) {
$inceptionResult->getStdOutput()->getStyle()->error('No errors were found during the analysis. Baseline could not be generated.');
$inceptionResult->getStdOutput()->writeLineFormatted('To allow generating empty baselines, pass <fg=cyan>--allow-empty-baseline</> option.');

return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime, $analysisResult->getWorkerCount());
return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime, $analysisResult->getWorkerCount(), $summaryOutput);
}

$streamOutput = $this->createStreamOutput();
Expand Down Expand Up @@ -791,15 +799,15 @@ private function generateBaseline(string $generateBaselineFile, InceptionResult
} catch (DirectoryCreatorException $e) {
$inceptionResult->getStdOutput()->writeLineFormatted($e->getMessage());

return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime, $analysisResult->getWorkerCount());
return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime, $analysisResult->getWorkerCount(), $summaryOutput);
}

try {
FileWriter::write($generateBaselineFile, $baselineContents);
} catch (CouldNotWriteFileException $e) {
$inceptionResult->getStdOutput()->writeLineFormatted($e->getMessage());

return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime, $analysisResult->getWorkerCount());
return $inceptionResult->handleReturn(1, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime, $analysisResult->getWorkerCount(), $summaryOutput);
}

$errorsCount = 0;
Expand Down Expand Up @@ -839,7 +847,7 @@ private function generateBaseline(string $generateBaselineFile, InceptionResult
$exitCode = 2;
}

return $inceptionResult->handleReturn($exitCode, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime, $analysisResult->getWorkerCount());
return $inceptionResult->handleReturn($exitCode, $analysisResult->getPeakMemoryUsageBytes(), $this->analysisStartTime, $analysisResult->getWorkerCount(), $summaryOutput);
}

/**
Expand Down
14 changes: 8 additions & 6 deletions src/Command/InceptionResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,36 +103,38 @@ public function getEditorModeInsteadOfFile(): ?string
/**
* @param int|null $peakMemoryUsageBytes the heaviest parallel worker's peak, 0 when the
* analysis ran in this process
* @param Output|null $summaryOutput error output by default
*/
public function handleReturn(int $exitCode, ?int $peakMemoryUsageBytes, float $analysisStartTime, int $workerCount = 0): int
public function handleReturn(int $exitCode, ?int $peakMemoryUsageBytes, float $analysisStartTime, int $workerCount = 0, ?Output $summaryOutput = null): int
{
if ($this->getErrorOutput()->isVerbose()) {
$summaryOutput ??= $this->getErrorOutput();
if ($summaryOutput->isVerbose()) {
$elapsedTime = round(microtime(true) - $analysisStartTime, 2);
if ($elapsedTime < 60) {
$elapsedTimeString = sprintf('%.2f seconds', $elapsedTime);
} else {
$elapsedTimeString = $this->formatDuration((int) $elapsedTime);
}
$this->getErrorOutput()->writeLineFormatted(sprintf(
$summaryOutput->writeLineFormatted(sprintf(
'Elapsed time: %s',
$elapsedTimeString,
));
}

if ($peakMemoryUsageBytes !== null && $this->getErrorOutput()->isVerbose()) {
if ($peakMemoryUsageBytes !== null && $summaryOutput->isVerbose()) {
// This process's peak is read here, at the very end, so it covers collecting
// the workers' results and saving the result cache - the part of a parallel
// run where the main process is at its largest. Both numbers are per process,
// which is also how memory_limit applies.
$mainProcessPeak = memory_get_peak_usage(true);
if ($peakMemoryUsageBytes === 0 || $workerCount === 0) {
$this->getErrorOutput()->writeLineFormatted(sprintf(
$summaryOutput->writeLineFormatted(sprintf(
'Peak memory: %s',
BytesHelper::bytes($mainProcessPeak),
));
} else {
$mechanism = $this->container->getByType(ForkParallelChecker::class)->isSupported() ? 'forked' : 'spawned';
$this->getErrorOutput()->writeLineFormatted(sprintf(
$summaryOutput->writeLineFormatted(sprintf(
'Peak memory: %s (main process), %s (%s)',
BytesHelper::bytes($mainProcessPeak),
BytesHelper::bytes($peakMemoryUsageBytes),
Expand Down
67 changes: 67 additions & 0 deletions tests/PHPStan/Command/AnalyseCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@
use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Tester\CommandTester;
use Throwable;
use function chdir;
use function getcwd;
use function getenv;
use function microtime;
use function putenv;
use function realpath;
use function sprintf;
use function sys_get_temp_dir;
use const DIRECTORY_SEPARATOR;
use const PHP_EOL;

Expand Down Expand Up @@ -71,6 +75,69 @@ public function testValidAutoloadFile(): void
}
}

public function testSummaryIsPrintedToStdOutputInCiWithTableFormat(): void
{
[$stdOutput, $errorOutput] = $this->runCommandInGithubActions('table');
$this->assertStringContainsString('[OK] No errors', $stdOutput);
$this->assertStringContainsString('PHPStan version:', $stdOutput);
$this->assertStringContainsString('Elapsed time:', $stdOutput);
$this->assertStringContainsString('Peak memory:', $stdOutput);
$this->assertStringNotContainsString('PHPStan version:', $errorOutput);
$this->assertStringNotContainsString('Elapsed time:', $errorOutput);
$this->assertStringNotContainsString('Peak memory:', $errorOutput);
}

public function testSummaryIsPrintedToErrorOutputInCiWithMachineReadableFormat(): void
{
[$stdOutput, $errorOutput] = $this->runCommandInGithubActions('json');
$this->assertStringNotContainsString('PHPStan version:', $stdOutput);
$this->assertStringNotContainsString('Elapsed time:', $stdOutput);
$this->assertStringNotContainsString('Peak memory:', $stdOutput);
$this->assertStringContainsString('PHPStan version:', $errorOutput);
$this->assertStringContainsString('Elapsed time:', $errorOutput);
$this->assertStringContainsString('Peak memory:', $errorOutput);
}

public function testSummaryIsPrintedToStdOutputInCiWhenGeneratingBaseline(): void
{
[$stdOutput, $errorOutput] = $this->runCommandInGithubActions('json', 1, ['--generate-baseline' => sys_get_temp_dir() . '/phpstan-summary-baseline.neon']);
$this->assertStringContainsString('To allow generating empty baselines', $stdOutput);
$this->assertStringContainsString('Elapsed time:', $stdOutput);
$this->assertStringNotContainsString('Elapsed time:', $errorOutput);
}

/**
* @param array<string, string> $parameters
* @return array{string, string}
*/
private function runCommandInGithubActions(string $errorFormat, int $expectedStatusCode = 0, array $parameters = []): array
{
$originalDir = getcwd();
if ($originalDir === false) {
throw new ShouldNotHappenException();
}
$originalGithubActions = getenv('GITHUB_ACTIONS');

chdir(__DIR__);
putenv('GITHUB_ACTIONS=true');

try {
$commandTester = new CommandTester(new AnalyseCommand([], microtime(true)));
$commandTester->execute([
'paths' => [__DIR__ . DIRECTORY_SEPARATOR . 'test'],
'--debug' => true,
'--error-format' => $errorFormat,
] + $parameters, ['verbosity' => OutputInterface::VERBOSITY_DEBUG, 'capture_stderr_separately' => true]);

$this->assertSame($expectedStatusCode, $commandTester->getStatusCode(), $commandTester->getDisplay() . $commandTester->getErrorOutput());

return [$commandTester->getDisplay(), $commandTester->getErrorOutput()];
} finally {
chdir($originalDir);
putenv($originalGithubActions === false ? 'GITHUB_ACTIONS' : 'GITHUB_ACTIONS=' . $originalGithubActions);
}
}

/**
* @return string[][]
*/
Expand Down
Loading