Skip to content
Draft
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
128 changes: 128 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,134 @@ jobs:
echo "$OUTPUT"
../bashunit -a matches "Note: Using configuration file .+phpstan.neon." "$OUTPUT"
../bashunit -a contains 'Result cache not used because the metadata do not match: metaExtensions' "$OUTPUT"
- script: |
cd e2e/result-cache-dependency
composer install
../../bin/phpstan clear-result-cache
mkdir -p tmp
: > tmp/hash-calls.log
: > tmp/rule-pids.log
: > tmp/collected-data.log
set +e
../../bin/phpstan analyse -vv --error-format raw > tmp/analysis-output.log 2>&1 &
PHPSTAN_MAIN_PID=$!
wait "$PHPSTAN_MAIN_PID"
STATUS=$?
set -e
../bashunit -a equals "0" "$STATUS"
OUTPUT=$(cat tmp/analysis-output.log)
echo "$OUTPUT"
../bashunit -a contains 'Result cache is saved.' "$OUTPUT"
../bashunit -a equals 'hashed=0 unhashed=6' "$(cat tmp/collected-data.log)"
php assert-hash-calls.php cold "$PHPSTAN_MAIN_PID"
php assert-result-cache-dependencies.php
: > tmp/hash-calls.log
: > tmp/rule-pids.log
: > tmp/collected-data.log
set +e
../../bin/phpstan analyse -vv --error-format raw > tmp/analysis-output.log 2>&1 &
PHPSTAN_MAIN_PID=$!
wait "$PHPSTAN_MAIN_PID"
STATUS=$?
set -e
../bashunit -a equals "0" "$STATUS"
OUTPUT=$(cat tmp/analysis-output.log)
echo "$OUTPUT"
../bashunit -a contains 'Result cache restored. 0 files will be reanalysed.' "$OUTPUT"
../bashunit -a equals 'hashed=0 unhashed=6' "$(cat tmp/collected-data.log)"
php assert-hash-calls.php warm "$PHPSTAN_MAIN_PID"
php assert-result-cache-dependencies.php
# Duplicate provider keys are rejected before analysis.
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -c duplicate-provider.neon -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'Duplicate ResultCacheDependencyExtension with key' "$OUTPUT"
../bashunit -a contains 'ConfigTypeRegistry" found.' "$OUTPUT"
../bashunit -a not_contains '100%' "$OUTPUT"
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -c duplicate-provider.neon --debug --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'Duplicate ResultCacheDependencyExtension with key' "$OUTPUT"
../bashunit -a not_contains 'src/Consumer.php' "$OUTPUT"
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -c duplicate-provider.neon src/Consumer.php -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'Duplicate ResultCacheDependencyExtension with key' "$OUTPUT"
../bashunit -a not_contains '100%' "$OUTPUT"
# The same dependency key in different provider namespaces must not collide.
sed -i 's/"checkout.label": "string"/"checkout.label": "int"/' tenant-config-types.json
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'Result cache restored. 1 file will be reanalysed.' "$OUTPUT"
../bashunit -a contains 'TenantConsumer.php:7:Parameter #1 $string of function strlen expects string, int given.' "$OUTPUT"
# Changing a key shared by two consumers invalidates both files.
sed -i 's/"checkout.label": "string"/"checkout.label": "int"/' config-types.json
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'Result cache restored. 2 files will be reanalysed.' "$OUTPUT"
../bashunit -a contains 'Consumer.php:11:Parameter #1 $string of function strlen expects string, int given.' "$OUTPUT"
../bashunit -a contains 'SecondConsumer.php:7:Parameter #1 $string of function strlen expects string, int given.' "$OUTPUT"
# Changing an unused key invalidates no files.
sed -i 's/"unused": "string"/"unused": "int"/' config-types.json
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'Result cache restored. 0 files will be reanalysed.' "$OUTPUT"
../bashunit -a contains 'Consumer.php:11:Parameter #1 $string of function strlen expects string, int given.' "$OUTPUT"
# Changing a key used by one consumer invalidates only that file.
sed -i 's/"profile.name": "string"/"profile.name": "int"/' config-types.json
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'Result cache restored. 1 file will be reanalysed.' "$OUTPUT"
../bashunit -a contains 'Consumer.php:16:Parameter #1 $string of function strlen expects string, int given.' "$OUTPUT"
# A source-invalidated file does not need its obsolete dependency hashes.
sed -i 's/"profile.name": "int"/"profile.name": "throw"/' config-types.json
sed -i "s/configValue('profile.name')/configValue('replacement')/" src/Consumer.php
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'Result cache restored. 1 file will be reanalysed.' "$OUTPUT"
../bashunit -a not_contains 'boom from dependency getHash' "$OUTPUT"
../bashunit -a not_contains 'Consumer.php:16:Parameter #1 $string of function strlen expects string, int given.' "$OUTPUT"
sed -i 's/"profile.name": "throw"/"profile.name": "string"/' config-types.json
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'Result cache restored. 0 files will be reanalysed.' "$OUTPUT"
sed -i 's/"replacement": "string"/"replacement": "int"/' config-types.json
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'Result cache restored. 1 file will be reanalysed.' "$OUTPUT"
../bashunit -a contains 'Consumer.php:16:Parameter #1 $string of function strlen expects string, int given.' "$OUTPUT"
# A selector change invalidates the file before its old selected key is hashed.
sed -i 's/"database.default": "legacy"/"database.default": "modern"/' config-types.json
sed -i 's/"database.connection.legacy": "string"/"database.connection.legacy": "throw"/' config-types.json
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'Result cache restored. 1 file will be reanalysed.' "$OUTPUT"
../bashunit -a not_contains 'boom from dependency getHash' "$OUTPUT"
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'Result cache restored. 0 files will be reanalysed.' "$OUTPUT"
../bashunit -a not_contains 'boom from dependency getHash' "$OUTPUT"
sed -i 's/"database.connection.modern": "string"/"database.connection.modern": "int"/' config-types.json
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'Result cache restored. 1 file will be reanalysed.' "$OUTPUT"
../bashunit -a contains 'Consumer.php:21:Parameter #1 $string of function strlen expects string, int given.' "$OUTPUT"
# Malformed or unknown persisted records invalidate their emitting files.
php mutate-result-cache.php unknown-provider
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'Result cache restored. 2 files will be reanalysed.' "$OUTPUT"
php mutate-result-cache.php malformed-record
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'Result cache restored. 1 file will be reanalysed.' "$OUTPUT"
php mutate-result-cache.php malformed-payload
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'Result cache restored. 1 file will be reanalysed.' "$OUTPUT"
# Provider exceptions propagate instead of being treated as cache misses.
sed -i 's/"replacement": "int"/"replacement": "throw"/' config-types.json
OUTPUT=$(../bashunit -a exit_code "1" "../../bin/phpstan analyse -vv --error-format raw")
echo "$OUTPUT"
../bashunit -a contains 'boom from dependency getHash' "$OUTPUT"
../bashunit -a not_contains 'Swallowed by global exception handler' "$OUTPUT"
- script: |
cd e2e/result-cache-meta-extension-throw
composer install
Expand Down
2 changes: 2 additions & 0 deletions e2e/result-cache-dependency/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/vendor
/tmp
82 changes: 82 additions & 0 deletions e2e/result-cache-dependency/assert-hash-calls.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

$phase = $argv[1] ?? null;
if ($phase !== 'cold' && $phase !== 'warm') {
throw new RuntimeException('Expected a cold or warm phase.');
}

$mainPid = filter_var($argv[2] ?? null, FILTER_VALIDATE_INT);
if (!is_int($mainPid) || $mainPid <= 0) {
throw new RuntimeException('Expected the PHPStan main-process PID.');
}

$hashCallLines = file(__DIR__ . '/tmp/hash-calls.log', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($hashCallLines === false) {
throw new RuntimeException('Could not read dependency hash calls.');
}

$hashCallPids = [];
$hashCallDependencies = [];
foreach ($hashCallLines as $line) {
if (preg_match('/^(\d+) (\S+) (.+)$/D', $line, $matches) !== 1) {
throw new RuntimeException(sprintf('Malformed dependency hash call: %s', $line));
}

$hashCallPids[] = (int) $matches[1];
$hashCallDependencies[] = sprintf('%s %s', $matches[2], $matches[3]);
}

$expectedDependencies = [
'ResultCacheE2E\\Dependency\\ConfigTypeRegistry checkout.label',
'ResultCacheE2E\\Dependency\\ConfigTypeRegistry database.connection.legacy',
'ResultCacheE2E\\Dependency\\ConfigTypeRegistry database.default',
'ResultCacheE2E\\Dependency\\ConfigTypeRegistry profile.name',
'ResultCacheE2E\\Dependency\\TenantConfigTypeRegistry checkout.label',
];
sort($hashCallDependencies);
if ($hashCallDependencies !== $expectedDependencies) {
throw new RuntimeException(sprintf(
'Expected one hash call for each unique provider and dependency key, got: %s',
implode(', ', $hashCallDependencies),
));
}

$uniqueHashCallPids = array_values(array_unique($hashCallPids));
if ($uniqueHashCallPids !== [$mainPid]) {
throw new RuntimeException(sprintf(
'Expected dependency hashes to be calculated by main process %d, got: %s',
$mainPid,
implode(', ', $uniqueHashCallPids),
));
}

$rulePidLines = file(__DIR__ . '/tmp/rule-pids.log', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($rulePidLines === false) {
throw new RuntimeException('Could not read semantic dependency rule processes.');
}
$rulePids = [];
foreach ($rulePidLines as $line) {
if (preg_match('/^\d+$/D', $line) !== 1) {
throw new RuntimeException(sprintf('Malformed semantic dependency rule process: %s', $line));
}
$rulePids[] = (int) $line;
}

if ($phase === 'cold') {
if ($rulePids === []) {
throw new RuntimeException('Expected semantic dependency rules to run during cold analysis.');
}
if (in_array($mainPid, $rulePids, true)) {
throw new RuntimeException('Expected semantic dependency rules to run in worker processes.');
}
} elseif ($rulePids !== []) {
throw new RuntimeException('Expected no semantic dependency rules during warm cache restoration.');
}

printf(
"%s cache phase calculated five unique dependency hashes in main process %d.\n",
$phase,
$mainPid,
);
30 changes: 30 additions & 0 deletions e2e/result-cache-dependency/assert-result-cache-dependencies.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

use PHPStan\Collectors\ResultCacheDependencyCollector;

$cache = require __DIR__ . '/tmp/resultCache.php';
$collectedData = $cache['collectedDataCallback']();
$recordCount = 0;
foreach ($collectedData as $file => $collectedDataForFile) {
$records = $collectedDataForFile[ResultCacheDependencyCollector::class] ?? [];
$seen = [];
foreach ($records as $record) {
$identity = $record['extensionKey'] . "\0" . $record['dependencyKey'];
if (isset($seen[$identity])) {
throw new RuntimeException(sprintf('Duplicate result-cache dependency persisted for %s.', $file));
}
$seen[$identity] = true;
if ($record['hash'] === 'extension-supplied') {
throw new RuntimeException(sprintf('Extension-supplied hash persisted for %s.', $file));
}
$recordCount++;
}
}

if ($recordCount !== 6) {
throw new RuntimeException(sprintf('Expected 6 persisted dependency records, got %d.', $recordCount));
}

echo "result cache contains six unique main-process dependency hashes.\n";
23 changes: 23 additions & 0 deletions e2e/result-cache-dependency/bootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

function configValue(string $key): mixed
{
return null;
}

function configuredConnectionValue(string $selectorKey): mixed
{
return null;
}

function tenantConfigValue(string $key): mixed
{
return null;
}

set_exception_handler(static function (\Throwable $e): void {
fwrite(STDERR, 'Swallowed by global exception handler: ' . $e->getMessage() . "\n");
exit(0);
});
5 changes: 5 additions & 0 deletions e2e/result-cache-dependency/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"autoload-dev": {
"classmap": ["extension/", "src/"]
}
}
18 changes: 18 additions & 0 deletions e2e/result-cache-dependency/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions e2e/result-cache-dependency/config-types.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"checkout.label": "string",
"profile.name": "string",
"unused": "string",
"replacement": "string",
"database.default": "legacy",
"database.connection.legacy": "string",
"database.connection.modern": "string"
}
11 changes: 11 additions & 0 deletions e2e/result-cache-dependency/duplicate-provider.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
includes:
- phpstan.neon

parameters:
tmpDir: tmp/duplicate

services:
-
class: ResultCacheE2E\Dependency\DuplicateResultCacheDependencyExtension
tags:
- phpstan.resultCacheDependencyExtension
Loading
Loading