test(aot): add Symfony framework compatibility tests

- Added clock timezone coalesce assignment and clone constructor test
- Added CSS selector unicode escape preg_replace_callback test
- Added dotenv escaped dollars protection test
- Added filesystem dynamic call error handler test
- Added finder gitignore preg_replace_callback test
- Added finder directory normalization with array_merge unpack test
- Added intl environment variable filtering with static cache test
- Added LDAP collection iterator caching test
- Added process environment union and command line string conversion test
- Added semaphore negated coalesce assignment test
- Added serializer context group merging with unpack test
- Added validator constraint serialization with private key handling test
- Added word count trimming and filtering test
- Added weblink header parser with repeated attributes test
pull/15/head
韩天峰 2 months ago
parent 75bf9f4113
commit addbf897f9
  1. 56
      tests/aot/symfony/clock-timezone-coalesce-clone.phpt
  2. 32
      tests/aot/symfony/cssselector-unicode-escape-callback.phpt
  3. 31
      tests/aot/symfony/dotenv-protect-escaped-dollars.phpt
  4. 57
      tests/aot/symfony/filesystem-box-dynamic-call-error-handler.phpt
  5. 24
      tests/aot/symfony/finder-gitignore-preg-replace-callback.phpt
  6. 52
      tests/aot/symfony/finder-normalize-dir-map-merge-unpack.phpt
  7. 48
      tests/aot/symfony/intl-env-filter-static-cache.phpt
  8. 64
      tests/aot/symfony/ldap-collection-iterator-cache.phpt
  9. 61
      tests/aot/symfony/process-env-union-commandline-strval.phpt
  10. 33
      tests/aot/symfony/semaphore-negated-coalesce-assign.phpt
  11. 57
      tests/aot/symfony/serializer-context-group-merge-unpack.phpt
  12. 43
      tests/aot/symfony/validator-constraint-serialize-private-keys.phpt
  13. 21
      tests/aot/symfony/validator-word-count-trim-filter.phpt
  14. 63
      tests/aot/symfony/weblink-header-parser-repeat-attrs.phpt

@ -0,0 +1,56 @@
--TEST--
Symfony Clock style timezone coalesce assignment and clone in constructor
--XFAIL--
Known AOT bug: DateTimeZone assigned through clone during constructor can become an uninitialized internal object.
--FILE--
<?php
class SymfonyNativeClockCase
{
private DateTimeZone $timezone;
public function __construct(DateTimeZone|string|null $timezone = null)
{
$this->timezone = is_string($timezone ??= date_default_timezone_get()) ? $this->withTimeZone($timezone)->timezone : $timezone;
}
public function withTimeZone(DateTimeZone|string $timezone): static
{
if (is_string($timezone)) {
$timezone = new DateTimeZone($timezone);
}
$clone = clone $this;
$clone->timezone = $timezone;
return $clone;
}
public function name(): string
{
return $this->timezone->getName();
}
}
function main(): void
{
$previous = date_default_timezone_get();
date_default_timezone_set('UTC');
$clock = new SymfonyNativeClockCase();
var_dump($clock->name());
$tokyo = $clock->withTimeZone('Asia/Tokyo');
var_dump($clock->name());
var_dump($tokyo->name());
$custom = new SymfonyNativeClockCase(new DateTimeZone('Europe/Paris'));
var_dump($custom->name());
date_default_timezone_set($previous);
}
?>
--EXPECT--
string(3) "UTC"
string(3) "UTC"
string(10) "Asia/Tokyo"
string(12) "Europe/Paris"

@ -0,0 +1,32 @@
--TEST--
Symfony CssSelector pattern: unicode escape preg_replace_callback with bit operations
--FILE--
<?php
function cssUnicodeUnescape(string $value): string
{
return preg_replace_callback('/\\\\([0-9a-fA-F]{1,6})\s?/', static function ($match) {
$c = hexdec($match[1]);
if (0x80 > $c %= 0x200000) {
return chr($c);
}
if (0x800 > $c) {
return chr(0xC0 | $c >> 6).chr(0x80 | $c & 0x3F);
}
if (0x10000 > $c) {
return chr(0xE0 | $c >> 12).chr(0x80 | $c >> 6 & 0x3F).chr(0x80 | $c & 0x3F);
}
return '';
}, $value);
}
function main(): void
{
var_dump(cssUnicodeUnescape('\\41 \\26'));
var_dump(bin2hex(cssUnicodeUnescape('\\20ac')));
}
?>
--EXPECT--
string(2) "A&"
string(6) "e282ac"

@ -0,0 +1,31 @@
--TEST--
Symfony Dotenv pattern: preg_replace_callback preserves escaped dollars by backslash parity
--FILE--
<?php
function protectEscapedDollars(string $value): string
{
if (!str_contains($value, '$')) {
return $value;
}
return preg_replace_callback('/\\\\+\$/', static function ($m) {
$bs = substr($m[0], 0, -1);
if (1 === strlen($bs) % 2) {
return substr($bs, 0, -1)."\x00";
}
return $m[0];
}, $value);
}
function main(): void
{
var_dump(bin2hex(protectEscapedDollars('A\\$B')));
var_dump(protectEscapedDollars('A\\\\$B'));
var_dump(protectEscapedDollars('plain'));
}
?>
--EXPECT--
string(6) "410042"
string(5) "A\\$B"
string(5) "plain"

@ -0,0 +1,57 @@
--TEST--
Symfony Filesystem style dynamic function call boxed by first-class error handler
--FILE--
<?php
class SymfonyFilesystemBoxCase
{
private static ?array $lastError = null;
private static function assertFunctionExists(string $func): void
{
if (!function_exists($func)) {
throw new RuntimeException(sprintf('Unable to perform filesystem operation because the "%s()" function has been disabled.', $func));
}
}
private static function handleError(int $type, string $message): bool
{
self::$lastError = [$type, $message];
return true;
}
public static function box(string $func, mixed ...$args): mixed
{
self::assertFunctionExists($func);
self::$lastError = null;
set_error_handler(self::handleError(...));
try {
return $func(...$args);
} finally {
restore_error_handler();
}
}
public static function getLastError(): ?array
{
return self::$lastError;
}
}
function main(): void
{
var_dump(SymfonyFilesystemBoxCase::box('strtoupper', 'abc'));
var_dump(SymfonyFilesystemBoxCase::getLastError());
try {
SymfonyFilesystemBoxCase::box('definitely_missing_function');
} catch (RuntimeException $e) {
echo $e->getMessage(), "\n";
}
}
?>
--EXPECT--
string(3) "ABC"
NULL
Unable to perform filesystem operation because the "definitely_missing_function()" function has been disabled.

@ -0,0 +1,24 @@
--TEST--
Symfony Finder Gitignore pattern: preg_replace_callback with static arrow function
--FILE--
<?php
function normalizeGitignoreCharacterClass(string $regex): string
{
return preg_replace_callback(
'~\\\\\[((?:\\\\!)?)([^\[\]]*)\\\\\]~',
static fn (array $matches): string => '['.('' !== $matches[1] ? '^' : '').str_replace('\\-', '-', $matches[2]).']',
$regex
);
}
function main(): void
{
var_dump(normalizeGitignoreCharacterClass('\\[a\\-z\\]'));
var_dump(normalizeGitignoreCharacterClass('\\[\\!0\\-9\\]'));
var_dump(normalizeGitignoreCharacterClass('plain'));
}
?>
--EXPECT--
string(5) "[a-z]"
string(6) "[^0-9]"
string(5) "plain"

@ -0,0 +1,52 @@
--TEST--
Symfony Finder pattern: object method first-class callable with array_merge unpack
--FILE--
<?php
class SymfonyFinderDirNormalizer
{
private array $dirs = [];
public function in(array|string $dirs): self
{
$resolvedDirs = [];
foreach ((array) $dirs as $dir) {
$glob = str_contains($dir, '*') ? ['src/', 'tests/'] : [$dir];
$resolvedDirs[] = array_map($this->normalizeDir(...), $glob);
}
$this->dirs = array_merge($this->dirs, ...$resolvedDirs);
return $this;
}
private function normalizeDir(string $dir): string
{
return rtrim(str_replace('\\', '/', $dir), '/').'/';
}
public function getDirs(): array
{
return $this->dirs;
}
}
function main(): void
{
$finder = (new SymfonyFinderDirNormalizer())
->in('var/cache')
->in(['app/*', 'vendor/package']);
var_dump($finder->getDirs());
}
?>
--EXPECT--
array(4) {
[0]=>
string(10) "var/cache/"
[1]=>
string(4) "src/"
[2]=>
string(6) "tests/"
[3]=>
string(15) "vendor/package/"
}

@ -0,0 +1,48 @@
--TEST--
Symfony Intl pattern: env fallback with filter_var and static coalesce cache
--FILE--
<?php
class SymfonyIntlEnvFlag
{
private static ?bool $withUserAssigned = null;
public static function reset(): void
{
self::$withUserAssigned = null;
}
public static function withUserAssigned(): bool
{
return self::$withUserAssigned ??= filter_var(
$_ENV['SYMFONY_INTL_WITH_USER_ASSIGNED'] ?? $_SERVER['SYMFONY_INTL_WITH_USER_ASSIGNED'] ?? getenv('SYMFONY_INTL_WITH_USER_ASSIGNED'),
FILTER_VALIDATE_BOOLEAN
);
}
}
function main(): void
{
unset($_ENV['SYMFONY_INTL_WITH_USER_ASSIGNED'], $_SERVER['SYMFONY_INTL_WITH_USER_ASSIGNED']);
putenv('SYMFONY_INTL_WITH_USER_ASSIGNED=1');
var_dump(SymfonyIntlEnvFlag::withUserAssigned());
putenv('SYMFONY_INTL_WITH_USER_ASSIGNED=0');
var_dump(SymfonyIntlEnvFlag::withUserAssigned());
SymfonyIntlEnvFlag::reset();
$_SERVER['SYMFONY_INTL_WITH_USER_ASSIGNED'] = 'false';
var_dump(SymfonyIntlEnvFlag::withUserAssigned());
SymfonyIntlEnvFlag::reset();
$_ENV['SYMFONY_INTL_WITH_USER_ASSIGNED'] = 'true';
var_dump(SymfonyIntlEnvFlag::withUserAssigned());
unset($_ENV['SYMFONY_INTL_WITH_USER_ASSIGNED'], $_SERVER['SYMFONY_INTL_WITH_USER_ASSIGNED']);
putenv('SYMFONY_INTL_WITH_USER_ASSIGNED');
}
?>
--EXPECT--
bool(true)
bool(true)
bool(false)
bool(true)

@ -0,0 +1,64 @@
--TEST--
Symfony Ldap pattern: iterator_to_array without keys cached by coalesce assignment
--FILE--
<?php
class SymfonyLdapEntryCollection implements IteratorAggregate
{
private ?array $entries = null;
public int $iterations = 0;
public function __construct(private array $source)
{
}
public function getIterator(): Traversable
{
++$this->iterations;
return new ArrayIterator($this->source);
}
public function toArray(): array
{
return $this->entries ??= iterator_to_array($this->getIterator(), false);
}
}
function main(): void
{
$collection = new SymfonyLdapEntryCollection([
'uid' => ['alice'],
'mail' => ['alice@example.com'],
]);
var_dump($collection->toArray());
var_dump($collection->toArray());
var_dump($collection->iterations);
}
?>
--EXPECT--
array(2) {
[0]=>
array(1) {
[0]=>
string(5) "alice"
}
[1]=>
array(1) {
[0]=>
string(17) "alice@example.com"
}
}
array(2) {
[0]=>
array(1) {
[0]=>
string(5) "alice"
}
[1]=>
array(1) {
[0]=>
string(17) "alice@example.com"
}
}
int(1)

@ -0,0 +1,61 @@
--TEST--
Symfony Process style env array union and commandline array_map(strval(...))
--FILE--
<?php
class SymfonyProcessCommandCase
{
private static array $executables = [];
public function __construct(
private array|string $commandline,
private array $env = [],
) {
}
public function normalize(array $runtimeEnv, array $defaultEnv): array
{
$env = $runtimeEnv;
if ($this->env) {
$env += '\\' === DIRECTORY_SEPARATOR ? array_diff_ukey($this->env, $env, 'strcasecmp') : $this->env;
}
$env += '\\' === DIRECTORY_SEPARATOR ? array_diff_ukey($defaultEnv, $env, 'strcasecmp') : $defaultEnv;
if (is_array($commandline = $this->commandline)) {
$commandline = array_values(array_map(strval(...), $commandline));
}
if ('\\' === DIRECTORY_SEPARATOR && isset($commandline[0][0]) && strlen($commandline[0]) === strcspn($commandline[0], ':/\\')) {
$commandline[0] = (self::$executables[$commandline[0]] ??= $commandline[0]) ?? $commandline[0];
}
return [$env, $commandline];
}
}
function main(): void
{
$process = new SymfonyProcessCommandCase(['php', '-r', 123], ['APP_ENV' => 'local', 'NEW_VAR' => 'yes']);
[$env, $commandline] = $process->normalize(['APP_ENV' => 'runtime'], ['PATH' => '/usr/bin', 'APP_ENV' => 'default']);
var_dump($env);
var_dump($commandline);
}
?>
--EXPECT--
array(3) {
["APP_ENV"]=>
string(7) "runtime"
["NEW_VAR"]=>
string(3) "yes"
["PATH"]=>
string(8) "/usr/bin"
}
array(3) {
[0]=>
string(3) "php"
[1]=>
string(2) "-r"
[2]=>
string(3) "123"
}

@ -0,0 +1,33 @@
--TEST--
Symfony Semaphore pattern: negated coalesce assignment in condition
--FILE--
<?php
class SymfonySemaphoreLike
{
public function __construct(private ?float $ttlInSecond = null)
{
}
public function refresh(?float $ttlInSecond = null): string
{
if (!$ttlInSecond ??= $this->ttlInSecond) {
return 'missing';
}
return 'ttl='.$ttlInSecond;
}
}
function main(): void
{
var_dump((new SymfonySemaphoreLike())->refresh());
var_dump((new SymfonySemaphoreLike(2.5))->refresh());
var_dump((new SymfonySemaphoreLike(2.5))->refresh(1.25));
var_dump((new SymfonySemaphoreLike(2.5))->refresh(0.0));
}
?>
--EXPECT--
string(7) "missing"
string(7) "ttl=2.5"
string(8) "ttl=1.25"
string(7) "missing"

@ -0,0 +1,57 @@
--TEST--
Symfony Serializer pattern: group contexts merged with unpack
--FILE--
<?php
class SymfonyAttributeContextMetadata
{
private array $normalizationContexts = [];
public function setNormalizationContextForGroups(array $context, array $groups = []): void
{
if (!$groups) {
$this->normalizationContexts['*'] = $context;
}
foreach ($groups as $group) {
$this->normalizationContexts[$group] = $context;
}
}
public function getNormalizationContextForGroups(array $groups): array
{
$contexts = [];
foreach ($groups as $group) {
$contexts[] = $this->normalizationContexts[$group] ?? [];
}
return array_merge($this->normalizationContexts['*'] ?? [], ...$contexts);
}
}
function main(): void
{
$metadata = new SymfonyAttributeContextMetadata();
$metadata->setNormalizationContextForGroups(['skip_null_values' => true, 'groups' => ['default']]);
$metadata->setNormalizationContextForGroups(['groups' => ['admin'], 'max_depth' => 2], ['admin']);
$metadata->setNormalizationContextForGroups(['ignored_attributes' => ['secret']], ['public']);
var_dump($metadata->getNormalizationContextForGroups(['missing', 'admin', 'public']));
}
?>
--EXPECT--
array(4) {
["skip_null_values"]=>
bool(true)
["groups"]=>
array(1) {
[0]=>
string(5) "admin"
}
["max_depth"]=>
int(2)
["ignored_attributes"]=>
array(1) {
[0]=>
string(6) "secret"
}
}

@ -0,0 +1,43 @@
--TEST--
Symfony Validator Constraint pattern: serialize object vars with match(true) private key normalization
--XFAIL--
AOT does not yet match private object array keys with NUL-prefixed class names in str_starts_with().
--FILE--
<?php
class SymfonyConstraintLike
{
public string $publicName = 'public';
protected string $protectedName = 'protected';
private string $privateName = 'private';
public function __serialize(): array
{
$data = [];
$class = $this::class;
foreach ((array) $this as $k => $v) {
$data[match (true) {
'' === $k || "\0" !== $k[0] => $k,
str_starts_with($k, "\0*\0") => substr($k, 3),
str_starts_with($k, "\0{$class}\0") => substr($k, 2 + strlen($class)),
default => $k,
}] = $v;
}
return $data;
}
}
function main(): void
{
var_dump((new SymfonyConstraintLike())->__serialize());
}
?>
--EXPECT--
array(3) {
["publicName"]=>
string(6) "public"
["protectedName"]=>
string(9) "protected"
["privateName"]=>
string(7) "private"
}

@ -0,0 +1,21 @@
--TEST--
Symfony Validator pattern: array_map(trim(...)) filtered by static arrow callback
--FILE--
<?php
class SymfonyWordCounter
{
public static function countWords(array $words): int
{
return count(array_filter(array_map(trim(...), $words), static fn ($word) => '' !== $word));
}
}
function main(): void
{
var_dump(SymfonyWordCounter::countWords([' one ', '', " \t ", 'two', ' three']));
var_dump(SymfonyWordCounter::countWords([]));
}
?>
--EXPECT--
int(3)
int(0)

@ -0,0 +1,63 @@
--TEST--
Symfony WebLink style header parser with match(true), rel ??= and repeated attributes
--FILE--
<?php
function symfony_parse_link_attributes(string $attributesString): array
{
$attributes = [];
$rels = null;
if (preg_match_all('/;\s*([a-zA-Z0-9_-]+)(?:=(?:"((?:\\\"|[^"])*)"|([^;,\s]+)))?/', $attributesString, $attributeMatches, PREG_SET_ORDER)) {
foreach ($attributeMatches as $pm) {
$key = $pm[1];
$value = match (true) {
($pm[2] ?? '') !== '' => stripcslashes($pm[2]),
($pm[3] ?? '') !== '' => $pm[3],
default => true,
};
if ('rel' === $key) {
$rels ??= true === $value ? [] : preg_split('/\s+/', $value, 0, PREG_SPLIT_NO_EMPTY);
} elseif (is_array($attributes[$key] ?? null)) {
$attributes[$key][] = $value;
} elseif (isset($attributes[$key])) {
$attributes[$key] = [$attributes[$key], $value];
} else {
$attributes[$key] = $value;
}
}
}
return [$rels, $attributes];
}
function main(): void
{
[$rels, $attributes] = symfony_parse_link_attributes('; rel="preload module"; rel=ignored; as=script; title="a \"quoted\" title"; hreflang=en; hreflang=fr; disabled');
var_dump($rels);
var_dump($attributes);
}
?>
--EXPECT--
array(2) {
[0]=>
string(7) "preload"
[1]=>
string(6) "module"
}
array(4) {
["as"]=>
string(6) "script"
["title"]=>
string(16) "a "quoted" title"
["hreflang"]=>
array(2) {
[0]=>
string(2) "en"
[1]=>
string(2) "fr"
}
["disabled"]=>
bool(true)
}
Loading…
Cancel
Save