From addbf897f906739284c9530a4199f5dc26149f0b Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Thu, 9 Jul 2026 14:20:19 +0800 Subject: [PATCH] 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 --- .../clock-timezone-coalesce-clone.phpt | 56 ++++++++++++++++ .../cssselector-unicode-escape-callback.phpt | 32 ++++++++++ .../dotenv-protect-escaped-dollars.phpt | 31 +++++++++ ...system-box-dynamic-call-error-handler.phpt | 57 +++++++++++++++++ ...inder-gitignore-preg-replace-callback.phpt | 24 +++++++ ...finder-normalize-dir-map-merge-unpack.phpt | 52 +++++++++++++++ .../symfony/intl-env-filter-static-cache.phpt | 48 ++++++++++++++ .../ldap-collection-iterator-cache.phpt | 64 +++++++++++++++++++ .../process-env-union-commandline-strval.phpt | 61 ++++++++++++++++++ .../semaphore-negated-coalesce-assign.phpt | 33 ++++++++++ ...serializer-context-group-merge-unpack.phpt | 57 +++++++++++++++++ ...tor-constraint-serialize-private-keys.phpt | 43 +++++++++++++ .../validator-word-count-trim-filter.phpt | 21 ++++++ .../weblink-header-parser-repeat-attrs.phpt | 63 ++++++++++++++++++ 14 files changed, 642 insertions(+) create mode 100644 tests/aot/symfony/clock-timezone-coalesce-clone.phpt create mode 100644 tests/aot/symfony/cssselector-unicode-escape-callback.phpt create mode 100644 tests/aot/symfony/dotenv-protect-escaped-dollars.phpt create mode 100644 tests/aot/symfony/filesystem-box-dynamic-call-error-handler.phpt create mode 100644 tests/aot/symfony/finder-gitignore-preg-replace-callback.phpt create mode 100644 tests/aot/symfony/finder-normalize-dir-map-merge-unpack.phpt create mode 100644 tests/aot/symfony/intl-env-filter-static-cache.phpt create mode 100644 tests/aot/symfony/ldap-collection-iterator-cache.phpt create mode 100644 tests/aot/symfony/process-env-union-commandline-strval.phpt create mode 100644 tests/aot/symfony/semaphore-negated-coalesce-assign.phpt create mode 100644 tests/aot/symfony/serializer-context-group-merge-unpack.phpt create mode 100644 tests/aot/symfony/validator-constraint-serialize-private-keys.phpt create mode 100644 tests/aot/symfony/validator-word-count-trim-filter.phpt create mode 100644 tests/aot/symfony/weblink-header-parser-repeat-attrs.phpt diff --git a/tests/aot/symfony/clock-timezone-coalesce-clone.phpt b/tests/aot/symfony/clock-timezone-coalesce-clone.phpt new file mode 100644 index 00000000..17f077ce --- /dev/null +++ b/tests/aot/symfony/clock-timezone-coalesce-clone.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-- +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" diff --git a/tests/aot/symfony/cssselector-unicode-escape-callback.phpt b/tests/aot/symfony/cssselector-unicode-escape-callback.phpt new file mode 100644 index 00000000..9090a46c --- /dev/null +++ b/tests/aot/symfony/cssselector-unicode-escape-callback.phpt @@ -0,0 +1,32 @@ +--TEST-- +Symfony CssSelector pattern: unicode escape preg_replace_callback with bit operations +--FILE-- + $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" diff --git a/tests/aot/symfony/dotenv-protect-escaped-dollars.phpt b/tests/aot/symfony/dotenv-protect-escaped-dollars.phpt new file mode 100644 index 00000000..1e48a49f --- /dev/null +++ b/tests/aot/symfony/dotenv-protect-escaped-dollars.phpt @@ -0,0 +1,31 @@ +--TEST-- +Symfony Dotenv pattern: preg_replace_callback preserves escaped dollars by backslash parity +--FILE-- + +--EXPECT-- +string(6) "410042" +string(5) "A\\$B" +string(5) "plain" diff --git a/tests/aot/symfony/filesystem-box-dynamic-call-error-handler.phpt b/tests/aot/symfony/filesystem-box-dynamic-call-error-handler.phpt new file mode 100644 index 00000000..8f176c44 --- /dev/null +++ b/tests/aot/symfony/filesystem-box-dynamic-call-error-handler.phpt @@ -0,0 +1,57 @@ +--TEST-- +Symfony Filesystem style dynamic function call boxed by first-class error handler +--FILE-- +getMessage(), "\n"; + } +} +?> +--EXPECT-- +string(3) "ABC" +NULL +Unable to perform filesystem operation because the "definitely_missing_function()" function has been disabled. diff --git a/tests/aot/symfony/finder-gitignore-preg-replace-callback.phpt b/tests/aot/symfony/finder-gitignore-preg-replace-callback.phpt new file mode 100644 index 00000000..f58e3c84 --- /dev/null +++ b/tests/aot/symfony/finder-gitignore-preg-replace-callback.phpt @@ -0,0 +1,24 @@ +--TEST-- +Symfony Finder Gitignore pattern: preg_replace_callback with static arrow function +--FILE-- + '['.('' !== $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" diff --git a/tests/aot/symfony/finder-normalize-dir-map-merge-unpack.phpt b/tests/aot/symfony/finder-normalize-dir-map-merge-unpack.phpt new file mode 100644 index 00000000..83ffa186 --- /dev/null +++ b/tests/aot/symfony/finder-normalize-dir-map-merge-unpack.phpt @@ -0,0 +1,52 @@ +--TEST-- +Symfony Finder pattern: object method first-class callable with array_merge unpack +--FILE-- +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/" +} diff --git a/tests/aot/symfony/intl-env-filter-static-cache.phpt b/tests/aot/symfony/intl-env-filter-static-cache.phpt new file mode 100644 index 00000000..f93f6956 --- /dev/null +++ b/tests/aot/symfony/intl-env-filter-static-cache.phpt @@ -0,0 +1,48 @@ +--TEST-- +Symfony Intl pattern: env fallback with filter_var and static coalesce cache +--FILE-- + +--EXPECT-- +bool(true) +bool(true) +bool(false) +bool(true) diff --git a/tests/aot/symfony/ldap-collection-iterator-cache.phpt b/tests/aot/symfony/ldap-collection-iterator-cache.phpt new file mode 100644 index 00000000..46a7a578 --- /dev/null +++ b/tests/aot/symfony/ldap-collection-iterator-cache.phpt @@ -0,0 +1,64 @@ +--TEST-- +Symfony Ldap pattern: iterator_to_array without keys cached by coalesce assignment +--FILE-- +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) diff --git a/tests/aot/symfony/process-env-union-commandline-strval.phpt b/tests/aot/symfony/process-env-union-commandline-strval.phpt new file mode 100644 index 00000000..64592d03 --- /dev/null +++ b/tests/aot/symfony/process-env-union-commandline-strval.phpt @@ -0,0 +1,61 @@ +--TEST-- +Symfony Process style env array union and commandline array_map(strval(...)) +--FILE-- +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" +} diff --git a/tests/aot/symfony/semaphore-negated-coalesce-assign.phpt b/tests/aot/symfony/semaphore-negated-coalesce-assign.phpt new file mode 100644 index 00000000..375906ad --- /dev/null +++ b/tests/aot/symfony/semaphore-negated-coalesce-assign.phpt @@ -0,0 +1,33 @@ +--TEST-- +Symfony Semaphore pattern: negated coalesce assignment in condition +--FILE-- +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" diff --git a/tests/aot/symfony/serializer-context-group-merge-unpack.phpt b/tests/aot/symfony/serializer-context-group-merge-unpack.phpt new file mode 100644 index 00000000..036b32e4 --- /dev/null +++ b/tests/aot/symfony/serializer-context-group-merge-unpack.phpt @@ -0,0 +1,57 @@ +--TEST-- +Symfony Serializer pattern: group contexts merged with unpack +--FILE-- +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" + } +} diff --git a/tests/aot/symfony/validator-constraint-serialize-private-keys.phpt b/tests/aot/symfony/validator-constraint-serialize-private-keys.phpt new file mode 100644 index 00000000..7dbd4989 --- /dev/null +++ b/tests/aot/symfony/validator-constraint-serialize-private-keys.phpt @@ -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-- + $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" +} diff --git a/tests/aot/symfony/validator-word-count-trim-filter.phpt b/tests/aot/symfony/validator-word-count-trim-filter.phpt new file mode 100644 index 00000000..d249e633 --- /dev/null +++ b/tests/aot/symfony/validator-word-count-trim-filter.phpt @@ -0,0 +1,21 @@ +--TEST-- +Symfony Validator pattern: array_map(trim(...)) filtered by static arrow callback +--FILE-- + '' !== $word)); + } +} + +function main(): void +{ + var_dump(SymfonyWordCounter::countWords([' one ', '', " \t ", 'two', ' three'])); + var_dump(SymfonyWordCounter::countWords([])); +} +?> +--EXPECT-- +int(3) +int(0) diff --git a/tests/aot/symfony/weblink-header-parser-repeat-attrs.phpt b/tests/aot/symfony/weblink-header-parser-repeat-attrs.phpt new file mode 100644 index 00000000..16e538ac --- /dev/null +++ b/tests/aot/symfony/weblink-header-parser-repeat-attrs.phpt @@ -0,0 +1,63 @@ +--TEST-- +Symfony WebLink style header parser with match(true), rel ??= and repeated attributes +--FILE-- + 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) +}