Skip to content
Closed
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
15 changes: 14 additions & 1 deletion src/Analyser/ResultCache/ResultCacheManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
use function array_unique;
use function array_values;
use function class_exists;
use function clearstatcache;
use function count;
use function error_get_last;
use function explode;
Expand Down Expand Up @@ -336,10 +337,22 @@ public function restore(array $allAnalysedFiles, bool $debug, bool $onlyFiles, ?
}

if (!is_array($data)) {
// readCacheFile() returns null both for a file it could not read back and for one it could
// not open at all, and the second is not a corruption: the file was there for the is_file()
// check above and gone by the time it was opened. A concurrent clear-result-cache, a temp
// directory being swept, a CI cache artifact expiring mid-run. Calling that corrupt sends
// someone looking for a broken disk. Ask before the unlink below, which removes the file
// in both cases, and clear the stat cache first: PHP may still answer from the is_file()
// check above.
clearstatcache(true, $cacheFilePath);
$reason = is_file($cacheFilePath)
? 'Result cache not used because the cache file is corrupted.'
: 'Result cache not used because the cache file disappeared while it was being read.';

@unlink($cacheFilePath);

return $this->fullAnalysis(
'Result cache not used because the cache file is corrupted.',
$reason,
$allAnalysedFiles,
$this->getMeta($allAnalysedFiles, $projectConfigArray),
$currentFileHashes,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser\ResultCache;

use Override;
use PHPStan\Command\Output;
use PHPStan\File\FileWriter;
use PHPStan\Testing\PHPStanTestCase;
use function dirname;
use function is_dir;
use function is_file;
use function mkdir;
use function unlink;

class ResultCacheManagerCorruptedFileTest extends PHPStanTestCase
{

/**
* A real file on purpose: deleting it clears PHP's stat cache, so a check that runs after the
* unlink sees the file as gone. A stream wrapper cannot reproduce that.
*/
public function testCorruptedCacheFileIsReportedAsCorrupted(): void
{
$cacheFilePath = self::getContainer()->getParameter('resultCachePath');
if (!is_dir(dirname($cacheFilePath))) {
mkdir(dirname($cacheFilePath), 0777, true);
}
FileWriter::write($cacheFilePath, "garbage\n");

$manager = self::getContainer()->getByType(ResultCacheManagerFactory::class)->create([]);
$resultCache = $manager->restore([], false, false, null, $this->createStub(Output::class));

$this->assertTrue($resultCache->isFullAnalysis());
$this->assertSame('Result cache not used because the cache file is corrupted.', $resultCache->getFullAnalysisReason());
$this->assertFalse(is_file($cacheFilePath));
}

#[Override]
protected function tearDown(): void
{
$cacheFilePath = self::getContainer()->getParameter('resultCachePath');
if (is_file($cacheFilePath)) {
unlink($cacheFilePath);
}

parent::tearDown();
}

public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/data/result-cache-corrupted.neon',
];
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser\ResultCache;

use Override;
use PHPStan\Command\Output;
use PHPStan\Testing\PHPStanTestCase;
use function stream_wrapper_register;
use function stream_wrapper_unregister;

class ResultCacheManagerVanishedFileTest extends PHPStanTestCase
{

protected function setUp(): void
{
parent::setUp();

ResultCacheStreamWrapper::$deleted = false;
stream_wrapper_register(ResultCacheStreamWrapper::SCHEME, ResultCacheStreamWrapper::class);
}

#[Override]
protected function tearDown(): void
{
stream_wrapper_unregister(ResultCacheStreamWrapper::SCHEME);

parent::tearDown();
}

public function testCacheFileThatVanishedBeforeOpeningIsNotReportedAsCorrupted(): void
{
$manager = self::getContainer()->getByType(ResultCacheManagerFactory::class)->create([]);
$resultCache = $manager->restore([], false, false, null, $this->createStub(Output::class));

$this->assertTrue($resultCache->isFullAnalysis());
$this->assertSame('Result cache not used because the cache file disappeared while it was being read.', $resultCache->getFullAnalysisReason());
}

public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/data/result-cache-vanished.neon',
];
}

}
48 changes: 48 additions & 0 deletions tests/PHPStan/Analyser/ResultCache/ResultCacheStreamWrapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser\ResultCache;

use AllowDynamicProperties;

/**
* A result cache file that passes the is_file() check and then refuses to open, the way a file does
* when something deletes it between the two. A real file cannot be made to do that without a race.
*
* PHP assigns the stream context to $context on every instance it creates, which is a dynamic
* property nothing here reads.
*
* phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
*/
#[AllowDynamicProperties]
final class ResultCacheStreamWrapper
{

public const SCHEME = 'phpstan-result-cache-test';

public static bool $deleted = false;

/**
* @return array<string, int>|false
*/
public function url_stat(string $path, int $flags)
{
if (self::$deleted) {
return false;
}

return ['mode' => 0100644, 'size' => 8];
}

public function stream_open(string $path, string $mode, int $options, ?string &$openedPath): bool
{
self::$deleted = true;

return false;
}

public function unlink(string $path): bool
{
return true;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
parameters:
resultCachePath: %tmpDir%/result-cache-manager-restore-test/resultCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
parameters:
resultCachePath: phpstan-result-cache-test://resultCache.php
Loading