test(aot): 新增 Symfony 模式测试及 PHPT 产物清理脚本

pull/14/head
韩天峰 2 months ago
parent f22499bdf9
commit b1dcaaa37e
  1. 140
      bin/clean-phpt-artifacts.php
  2. 43
      tests/aot/symfony/array-push-unpack-static-recursion.phpt
  3. 37
      tests/aot/symfony/conditional-first-class-callable-property.phpt
  4. 40
      tests/aot/symfony/dynamic-static-property-multidim-isset.phpt
  5. 39
      tests/aot/symfony/env-array-diff-ukey-union.phpt
  6. 41
      tests/aot/symfony/late-static-new-static-return.phpt
  7. 33
      tests/aot/symfony/new-expression-constructor-default.phpt
  8. 36
      tests/aot/symfony/nullsafe-attribute-newinstance-chain.phpt
  9. 40
      tests/aot/symfony/process-string-offset-commandline.phpt
  10. 39
      tests/aot/symfony/static-property-array-spread-merge.phpt

@ -0,0 +1,140 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
const DEFAULT_TEST_DIR = 'tests';
$root = dirname(__DIR__);
$artifactSuffixes = [
'.php',
'.exp',
'.log',
'.out',
'.diff',
'.sh',
'.mem',
'.clean.php',
'.skip.php',
];
$binarySuffix = PHP_OS_FAMILY === 'Windows' ? '.exe' : '';
$dryRun = false;
$paths = [];
foreach (array_slice($argv, 1) as $arg) {
if ($arg === '--dry-run' || $arg === '-n') {
$dryRun = true;
continue;
}
if ($arg === '--help' || $arg === '-h') {
printUsage($argv[0]);
exit(0);
}
$paths[] = $arg;
}
if (!$paths) {
$paths[] = DEFAULT_TEST_DIR;
}
$phptFiles = [];
foreach ($paths as $path) {
$absolute = resolvePath($root, $path);
if (is_file($absolute)) {
if (str_ends_with($absolute, '.phpt')) {
$phptFiles[$absolute] = true;
}
continue;
}
if (!is_dir($absolute)) {
fwrite(STDERR, "Path not found: {$path}\n");
exit(1);
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($absolute, FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
$filePath = $file->getPathname();
if (str_ends_with($filePath, '.phpt')) {
$phptFiles[$filePath] = true;
}
}
}
$artifacts = [];
foreach (array_keys($phptFiles) as $phptFile) {
$base = substr($phptFile, 0, -5);
foreach ($artifactSuffixes as $suffix) {
$artifact = $base . $suffix;
if (is_file($artifact)) {
$artifacts[$artifact] = true;
}
}
$binary = $root . DIRECTORY_SEPARATOR . str_replace('-', '_', basename($base)) . $binarySuffix;
if (is_file($binary) && is_executable($binary)) {
$artifacts[$binary] = true;
}
}
$artifacts = array_keys($artifacts);
sort($artifacts);
$deleted = 0;
foreach ($artifacts as $artifact) {
$displayPath = relativePath($root, $artifact);
if ($dryRun) {
echo $displayPath, PHP_EOL;
continue;
}
if (!unlink($artifact)) {
fwrite(STDERR, "Failed to remove: {$displayPath}\n");
exit(1);
}
++$deleted;
}
if ($dryRun) {
printf("Found %d PHPT artifact file(s).\n", count($artifacts));
} else {
printf("Removed %d PHPT artifact file(s).\n", $deleted);
}
function printUsage(string $script): void
{
echo <<<USAGE
Usage:
php {$script} [--dry-run] [path ...]
Examples:
php {$script}
php {$script} tests/aot/symfony
php {$script} --dry-run tests/aot/basic/001.phpt
Only files generated from an existing .phpt are removed. This includes adjacent
.php/.log/.out/.diff files and the AOT test binary generated in the project root.
USAGE;
}
function resolvePath(string $root, string $path): string
{
if ($path === '') {
return $root;
}
if ($path[0] === '/') {
return $path;
}
return $root . DIRECTORY_SEPARATOR . $path;
}
function relativePath(string $root, string $path): string
{
$prefix = rtrim($root, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
if (str_starts_with($path, $prefix)) {
return substr($path, strlen($prefix));
}
return $path;
}

@ -0,0 +1,43 @@
--TEST--
Symfony pattern: array_push with unpacked static method result
--FILE--
<?php
class FunctionCallParser
{
public static function parse(string $expr): array
{
$calls = [];
foreach (explode(',', $expr) as $part) {
$part = trim($part);
if ($part === '') {
continue;
}
$calls[] = strtoupper($part);
if (str_contains($part, '+')) {
array_push($calls, ...self::parse(str_replace('+', ',', $part)));
}
}
return $calls;
}
}
function main(): void
{
var_dump(FunctionCallParser::parse('foo,bar+baz'));
}
?>
--EXPECT--
array(4) {
[0]=>
string(3) "FOO"
[1]=>
string(7) "BAR+BAZ"
[2]=>
string(3) "BAR"
[3]=>
string(3) "BAZ"
}

@ -0,0 +1,37 @@
--TEST--
Symfony pattern: conditional first-class callable stored in property
--FILE--
<?php
class DeferredCallback
{
private mixed $callback = null;
public function set(?callable $callback): void
{
$this->callback = null !== $callback ? $callback(...) : null;
}
public function run(string $value): ?string
{
if (null === $callback = $this->callback) {
return null;
}
return $callback($value);
}
}
function main(): void
{
$callback = new DeferredCallback();
$callback->set(static fn (string $value): string => strtoupper($value));
var_dump($callback->run('symfony'));
$callback->set(null);
var_dump($callback->run('symfony'));
}
?>
--EXPECT--
string(7) "SYMFONY"
NULL

@ -0,0 +1,40 @@
--TEST--
Symfony ErrorHandler pattern: dynamic static property name with multidimensional isset
--FILE--
<?php
class FinalMemberRegistry
{
private static array $finalConstants = [
'ParentClass' => [
'STATUS' => 'ParentClass::STATUS',
],
];
private static array $finalProperties = [
'ParentClass' => [
'name' => 'ParentClass::$name',
],
];
public static function lookup(string $type, string $class, string $name): ?string
{
if (isset(self::${$type}[$class][$name])) {
return self::${$type}[$class][$name];
}
return null;
}
}
function main(): void
{
var_dump(FinalMemberRegistry::lookup('finalConstants', 'ParentClass', 'STATUS'));
var_dump(FinalMemberRegistry::lookup('finalProperties', 'ParentClass', 'name'));
var_dump(FinalMemberRegistry::lookup('finalProperties', 'ParentClass', 'missing'));
}
?>
--EXPECT--
string(19) "ParentClass::STATUS"
string(18) "ParentClass::$name"
NULL

@ -0,0 +1,39 @@
--TEST--
Symfony Process pattern: environment union with array_diff_ukey callback
--FILE--
<?php
function mergeEnv(array $env, array $defaults, bool $windows): array
{
$result = ['PATH' => '/bin'];
$result += $windows ? array_diff_ukey($env, $result, 'strcasecmp') : $env;
$result += $windows ? array_diff_ukey($defaults, $result, 'strcasecmp') : $defaults;
return $result;
}
function main(): void
{
var_dump(mergeEnv(['Path' => '/usr/bin', 'APP_ENV' => 'dev'], ['HOME' => '/tmp'], true));
var_dump(mergeEnv(['Path' => '/usr/bin', 'APP_ENV' => 'dev'], ['HOME' => '/tmp'], false));
}
?>
--EXPECT--
array(3) {
["PATH"]=>
string(4) "/bin"
["APP_ENV"]=>
string(3) "dev"
["HOME"]=>
string(4) "/tmp"
}
array(4) {
["PATH"]=>
string(4) "/bin"
["Path"]=>
string(8) "/usr/bin"
["APP_ENV"]=>
string(3) "dev"
["HOME"]=>
string(4) "/tmp"
}

@ -0,0 +1,41 @@
--TEST--
Symfony pattern: late static binding with new static and static return type
--FILE--
<?php
class BaseFactory
{
public string $name = 'base';
public static function create(string $name): static
{
$instance = new static();
$instance->name = $name;
return $instance;
}
}
class ChildFactory extends BaseFactory
{
public string $extra = 'child';
}
function main(): void
{
$base = BaseFactory::create('root');
$child = ChildFactory::create('leaf');
var_dump($base::class);
var_dump($base->name);
var_dump($child::class);
var_dump($child->name);
var_dump($child->extra);
}
?>
--EXPECT--
string(11) "BaseFactory"
string(4) "root"
string(12) "ChildFactory"
string(4) "leaf"
string(5) "child"

@ -0,0 +1,33 @@
--TEST--
Symfony AssetMapper pattern: new expression as constructor default
--FILE--
<?php
class Compressor
{
public function compress(string $value): string
{
return 'gz:'.$value;
}
}
class GzipAsset
{
public function __construct(
private readonly Compressor $compressor = new Compressor(),
) {
}
public function encode(string $value): string
{
return $this->compressor->compress($value);
}
}
function main(): void
{
var_dump((new GzipAsset())->encode('asset'));
}
?>
--EXPECT--
string(8) "gz:asset"

@ -0,0 +1,36 @@
--TEST--
Symfony pattern: nullsafe attribute newInstance chain
--FILE--
<?php
#[Attribute(Attribute::TARGET_CLASS)]
class TaggedItem
{
public function __construct(public int $priority = 0)
{
}
}
#[TaggedItem(priority: 20)]
class TaggedService
{
}
class UntaggedService
{
}
function priorityOf(string $class): ?int
{
return ((new ReflectionClass($class))->getAttributes(TaggedItem::class)[0] ?? null)?->newInstance()->priority;
}
function main(): void
{
var_dump(priorityOf(TaggedService::class));
var_dump(priorityOf(UntaggedService::class));
}
?>
--EXPECT--
int(20)
NULL

@ -0,0 +1,40 @@
--TEST--
Symfony Process pattern: nested isset on commandline string offset
--FILE--
<?php
function normalizeCommand(array $commandline): array
{
if (isset($commandline[0][0]) && strlen($commandline[0]) === strcspn($commandline[0], ':/\\')) {
$commandline[0] = 'resolved:'.$commandline[0];
}
return $commandline;
}
function main(): void
{
var_dump(normalizeCommand(['php', '-v']));
var_dump(normalizeCommand(['/usr/bin/php', '-v']));
var_dump(normalizeCommand(['', '-v']));
}
?>
--EXPECT--
array(2) {
[0]=>
string(12) "resolved:php"
[1]=>
string(2) "-v"
}
array(2) {
[0]=>
string(12) "/usr/bin/php"
[1]=>
string(2) "-v"
}
array(2) {
[0]=>
string(0) ""
[1]=>
string(2) "-v"
}

@ -0,0 +1,39 @@
--TEST--
Symfony pattern: static property array spread merge assignment
--FILE--
<?php
class CasterRegistry
{
private static array $casters = [
'default' => 'base',
];
public static function add(array $casters): void
{
self::$casters = [...self::$casters, ...$casters];
}
public static function all(): array
{
return self::$casters;
}
}
function main(): void
{
CasterRegistry::add(['a' => 'A']);
CasterRegistry::add(['default' => 'override', 'b' => 'B']);
var_dump(CasterRegistry::all());
}
?>
--EXPECT--
array(3) {
["default"]=>
string(8) "override"
["a"]=>
string(1) "A"
["b"]=>
string(1) "B"
}
Loading…
Cancel
Save