Merge branch '4.4' into 5.0

* 4.4: (23 commits)
  [Filesystem] Handle paths on different drives
  [WebProfiler] Do not add src-elem CSP directives if they do not exist
  [Yaml] fix parse error when unindented collections contain a comment
  Execute docker dependent tests with github actions
  Update exception.html.php
  [3.4][Inflector] Improve testSingularize() argument name
  [Inflector] Fix testPluralize() arguments names
  [PhpUnitBridge] fix PHP 5.3 compat again
  Skip validation when email is an empty object
  fix sr_Latn translation
  [Validator] fix lazy property usage.
  Fix annotation
  [Debug][ErrorHandler] cleanup phpunit.xml.dist files
  [Translation] Fix for translation:update command updating ICU messages
  [PhpUnitBridge] fix compat with PHP 5.3
  bumped Symfony version to 4.4.9
  updated VERSION for 4.4.8
  updated CHANGELOG for 4.4.8
  provide a useful message when extension types don't match
  [Cache] Fixed not supported Redis eviction policies
  ...
This commit is contained in:
Nicolas Grekas 2020-05-04 16:05:24 +02:00
commit ae226ee34b
65 changed files with 546 additions and 126 deletions

101
.github/workflows/tests.yml vendored Normal file
View File

@ -0,0 +1,101 @@
name: Tests
on:
push:
pull_request:
jobs:
integration:
name: Integration
runs-on: ubuntu-latest
strategy:
matrix:
php: ['7.1', '7.4']
services:
redis:
image: redis:6.0.0
ports:
- 6379:6379
redis-cluster:
image: grokzen/redis-cluster:5.0.4
ports:
- 7000:7000
- 7001:7001
- 7002:7002
- 7003:7003
- 7004:7004
- 7005:7005
- 7006:7006
- 7007:7007
env:
STANDALONE: true
memcached:
image: memcached:1.6.5
ports:
- 11211:11211
rabbitmq:
image: rabbitmq:3.8.3
ports:
- 5672:5672
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
coverage: "none"
extensions: "memcached,redis,xsl"
ini-values: "memory_limit=-1"
php-version: "${{ matrix.php }}"
tools: flex
- name: Configure composer
run: |
([ -d ~/.composer ] || mkdir ~/.composer) && cp .github/composer-config.json ~/.composer/config.json
SYMFONY_VERSION=$(cat composer.json | grep '^ *\"dev-master\". *\"[1-9]' | grep -o '[0-9.]*')
echo "::set-env name=SYMFONY_VERSION::$SYMFONY_VERSION"
echo "::set-env name=COMPOSER_ROOT_VERSION::$SYMFONY_VERSION.x-dev"
- name: Determine composer cache directory
id: composer-cache
run: echo "::set-output name=directory::$(composer config cache-dir)"
- name: Cache composer dependencies
uses: actions/cache@v1
with:
path: ${{ steps.composer-cache.outputs.directory }}
key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ matrix.php }}-composer-
- name: Install dependencies
run: |
echo "::group::composer update"
composer update --no-progress --no-suggest --ansi
echo "::endgroup::"
echo "::group::install phpunit"
./phpunit install
echo "::endgroup::"
- name: Run tests
run: ./phpunit --verbose --group integration
env:
SYMFONY_DEPRECATIONS_HELPER: 'max[indirect]=7'
REDIS_HOST: localhost
REDIS_CLUSTER_HOSTS: 'localhost:7000 localhost:7001 localhost:7002 localhost:7003 localhost:7004 localhost:7005'
MESSENGER_REDIS_DSN: redis://127.0.0.1:7006/messages
MESSENGER_AMQP_DSN: amqp://localhost/%2f/messages
MEMCACHED_HOST: localhost
- name: Run HTTP push tests
if: matrix.php == '7.4'
run: |
[ -d .phpunit ] && mv .phpunit .phpunit.bak
wget -q https://github.com/symfony/binary-utils/releases/download/v0.1/vulcain_0.1.3_Linux_x86_64.tar.gz -O - | tar xz && mv vulcain /usr/local/bin
docker run --rm -e COMPOSER_ROOT_VERSION -e SYMFONY_VERSION -v $(pwd):/app -v $(which composer):/usr/local/bin/composer -v /usr/local/bin/vulcain:/usr/local/bin/vulcain -w /app php:7.4-alpine ./phpunit --verbose src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php --filter testHttp2Push
sudo rm -rf .phpunit
[ -d .phpunit.bak ] && mv .phpunit.bak .phpunit

View File

@ -13,14 +13,11 @@ addons:
- slapd
- zookeeperd
- libzookeeper-mt-dev
- rabbitmq-server
env:
global:
- MIN_PHP=7.2.5
- SYMFONY_PROCESS_PHP_TEST_BINARY=~/.phpenv/shims/php
- MESSENGER_AMQP_DSN=amqp://localhost/%2f/messages
- MESSENGER_REDIS_DSN=redis://127.0.0.1:7006/messages
- SYMFONY_PHPUNIT_DISABLE_RESULT_CACHE=1
matrix:
@ -39,13 +36,6 @@ cache:
- php-$MIN_PHP
- ~/php-ext
services:
- memcached
- mongodb
- redis-server
- rabbitmq
- docker
before_install:
- |
# Enable Sury ppa
@ -56,12 +46,6 @@ before_install:
sudo apt update
sudo apt install -y librabbitmq-dev libsodium-dev
- |
# Start Redis cluster
docker pull grokzen/redis-cluster:5.0.4
docker run -d -p 7000:7000 -p 7001:7001 -p 7002:7002 -p 7003:7003 -p 7004:7004 -p 7005:7005 -p 7006:7006 -p 7007:7007 -e "STANDALONE=true" --name redis-cluster grokzen/redis-cluster:5.0.4
export REDIS_CLUSTER_HOSTS='localhost:7000 localhost:7001 localhost:7002 localhost:7003 localhost:7004 localhost:7005'
- |
# General configuration
set -e
@ -141,12 +125,6 @@ before_install:
(cd php-$MIN_PHP && ./configure --enable-sigchild --enable-pcntl && make -j2)
fi
- |
# Install vulcain
wget https://github.com/symfony/binary-utils/releases/download/v0.1/vulcain_0.1.3_Linux_x86_64.tar.gz -O - | tar xz
sudo mv vulcain /usr/local/bin
docker pull php:7.3-alpine
- |
# php.ini configuration
for PHP in $TRAVIS_PHP_VERSION $php_extra; do
@ -268,15 +246,6 @@ install:
export PHP=$1
phpenv global $PHP
if [[ !$deps && $PHP = 7.2 ]]; then
phpenv global $PHP
tfold 'composer update' $COMPOSER_UP
[ -d .phpunit ] && mv .phpunit .phpunit.bak
tfold src/Symfony/Component/HttpClient.h2push "docker run -it --rm -v $(pwd):/app -v $(phpenv which composer):/usr/local/bin/composer -v /usr/local/bin/vulcain:/usr/local/bin/vulcain -w /app php:7.3-alpine ./phpunit src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php --filter testHttp2Push"
sudo rm .phpunit -rf
[ -d .phpunit.bak ] && mv .phpunit.bak .phpunit
fi
if [[ $PHP != 7.4* && $PHP != $TRAVIS_PHP_VERSION && $TRAVIS_PULL_REQUEST != false ]]; then
echo -e "\\n\\e[33;1mIntermediate PHP version $PHP is skipped for pull requests.\\e[0m"
return

View File

@ -7,6 +7,56 @@ in 4.4 minor versions.
To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash
To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v4.4.0...v4.4.1
* 4.4.8 (2020-04-28)
* bug #36536 [Cache] Allow invalidateTags calls to be traced by data collector (l-vo)
* bug #36566 [PhpUnitBridge] Use COMPOSER_BINARY env var if available (fancyweb)
* bug #36560 [YAML] escape DEL(\x7f) (sdkawata)
* bug #36539 [PhpUnitBridge] fix compatibility with phpunit 9 (garak)
* bug #36555 [Cache] skip APCu in chains when the backend is disabled (nicolas-grekas)
* bug #36523 [Form] apply automatically step=1 for datetime-local input (ottaviano)
* bug #36519 [FrameworkBundle] debug:autowiring: Fix wrong display when using class_alias (weaverryan)
* bug #36454 [DependencyInjection][ServiceSubscriber] Support late aliases (fancyweb)
* bug #36498 [Security/Core] fix escape for username in LdapBindAuthenticationProvider.php (stoccc)
* bug #36506 [FrameworkBundle] Fix session.attribute_bag service definition (fancyweb)
* bug #36500 [Routing][PrefixTrait] Add the _locale requirement (fancyweb)
* bug #36457 [Cache] CacheItem with tag is never a hit after expired (alexander-schranz, nicolas-grekas)
* bug #36490 [HttpFoundation] workaround PHP bug in the session module (nicolas-grekas)
* bug #36483 [SecurityBundle] fix accepting env vars in remember-me configurations (zek)
* bug #36343 [Form] Fixed handling groups sequence validation (HeahDude)
* bug #36460 [Cache] Avoid memory leak in TraceableAdapter::reset() (lyrixx)
* bug #36467 Mailer from sender fixes (fabpot)
* bug #36408 [PhpUnitBridge] add PolyfillTestCaseTrait::expectExceptionMessageMatches to provide FC with recent phpunit versions (soyuka)
* bug #36447 Remove return type for Twig function workflow_metadata() (gisostallenberg)
* bug #36449 [Messenger] Make sure redis transports are initialized correctly (Seldaek)
* bug #36411 [Form] RepeatedType should always have inner types mapped (biozshock)
* bug #36441 [DI] fix loading defaults when using the PHP-DSL (nicolas-grekas)
* bug #36434 [HttpKernel] silence E_NOTICE triggered since PHP 7.4 (xabbuh)
* bug #36365 [Validator] Fixed default group for nested composite constraints (HeahDude)
* bug #36422 [HttpClient] fix HTTP/2 support on non-SSL connections - CurlHttpClient only (nicolas-grekas)
* bug #36417 Force ping after transport exception (oesteve)
* bug #35591 [Validator] do not merge constraints within interfaces (greedyivan)
* bug #36377 [HttpClient] Fix scoped client without query option configuration (X-Coder264)
* bug #36387 [DI] fix detecting short service syntax in yaml (nicolas-grekas)
* bug #36392 [DI] add missing property declarations in InlineServiceConfigurator (nicolas-grekas)
* bug #36400 Allowing empty secrets to be set (weaverryan)
* bug #36380 [Process] Fixed input/output error on PHP 7.4 (mbardelmeijer)
* bug #36376 [Workflow] Use a strict comparison when retrieving raw marking in MarkingStore (lyrixx)
* bug #36375 [Workflow] Use a strict comparison when retrieving raw marking in MarkingStore (lyrixx)
* bug #36305 [PropertyInfo][ReflectionExtractor] Check the array mutator prefixes last when the property is singular (fancyweb)
* bug #35656 [HttpFoundation] Fixed session migration with custom cookie lifetime (Guite)
* bug #36342 [HttpKernel][FrameworkBundle] fix compat with Debug component (nicolas-grekas)
* bug #36315 [WebProfilerBundle] Support for Content Security Policy style-src-elem and script-src-elem in WebProfiler (ampaze)
* bug #36286 [Validator] Allow URL-encoded special characters in basic auth part of URLs (cweiske)
* bug #36335 [Security] Track session usage whenever a new token is set (wouterj)
* bug #36332 [Serializer] Fix unitialized properties (from PHP 7.4.2) when serializing context for the cache key (alanpoulain)
* bug #36337 [MonologBridge] Fix $level type (fancyweb)
* bug #36223 [Security][Http][SwitchUserListener] Ignore all non existent username protection errors (fancyweb)
* bug #36239 [HttpKernel][LoggerDataCollector] Prevent keys collisions in the sanitized logs processing (fancyweb)
* bug #36245 [Validator] Fixed calling getters before resolving groups (HeahDude)
* bug #36265 Fix the reporting of deprecations in twig:lint (stof)
* bug #36283 [Security] forward multiple attributes voting flag (xabbuh)
* 4.4.7 (2020-03-30)
* security #cve-2020-5255 [HttpFoundation] Do not set the default Content-Type based on the Accept header (yceruto)

View File

@ -318,7 +318,14 @@ EOF
{
$filteredCatalogue = new MessageCatalogue($catalogue->getLocale());
if ($messages = $catalogue->all($domain)) {
// extract intl-icu messages only
$intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX;
if ($intlMessages = $catalogue->all($intlDomain)) {
$filteredCatalogue->add($intlMessages, $intlDomain);
}
// extract all messages and subtract intl-icu messages
if ($messages = array_diff($catalogue->all($domain), $intlMessages)) {
$filteredCatalogue->add($messages, $domain);
}
foreach ($catalogue->getResources() as $resource) {

View File

@ -26,9 +26,12 @@ class CachePoolsTest extends AbstractWebTestCase
/**
* @requires extension redis
* @group integration
*/
public function testRedisCachePools()
{
$this->skipIfRedisUnavailable();
try {
$this->doTestCachePools(['root_config' => 'redis_config.yml', 'environment' => 'redis_cache'], RedisAdapter::class);
} catch (\PHPUnit\Framework\Error\Warning $e) {
@ -51,9 +54,12 @@ class CachePoolsTest extends AbstractWebTestCase
/**
* @requires extension redis
* @group integration
*/
public function testRedisCustomCachePools()
{
$this->skipIfRedisUnavailable();
try {
$this->doTestCachePools(['root_config' => 'redis_custom_config.yml', 'environment' => 'custom_redis_cache'], RedisAdapter::class);
} catch (\PHPUnit\Framework\Error\Warning $e) {
@ -121,4 +127,13 @@ class CachePoolsTest extends AbstractWebTestCase
{
return parent::createKernel(['test_case' => 'CachePools'] + $options);
}
private function skipIfRedisUnavailable()
{
try {
(new \Redis())->connect(getenv('REDIS_HOST'));
} catch (\Exception $e) {
self::markTestSkipped($e->getMessage());
}
}
}

View File

@ -129,12 +129,11 @@ class ContentSecurityPolicyHandler
continue;
}
if (!isset($headers[$header][$type])) {
if (isset($headers[$header]['default-src'])) {
$headers[$header][$type] = $headers[$header]['default-src'];
} else {
// If there is no script-src/style-src and no default-src, no additional rules required.
if (null === $fallback = $this->getDirectiveFallback($directives, $type)) {
continue;
}
$headers[$header][$type] = $fallback;
}
$ruleIsSet = true;
if (!\in_array('\'unsafe-inline\'', $headers[$header][$type], true)) {
@ -199,9 +198,7 @@ class ContentSecurityPolicyHandler
{
if (isset($directivesSet[$type])) {
$directives = $directivesSet[$type];
} elseif (isset($directivesSet['default-src'])) {
$directives = $directivesSet['default-src'];
} else {
} elseif (null === $directives = $this->getDirectiveFallback($directivesSet, $type)) {
return false;
}
@ -225,6 +222,16 @@ class ContentSecurityPolicyHandler
return false;
}
private function getDirectiveFallback(array $directiveSet, $type)
{
if (\in_array($type, ['script-src-elem', 'style-src-elem'], true) || !isset($directiveSet['default-src'])) {
// Let the browser fallback on it's own
return null;
}
return $directiveSet['default-src'];
}
/**
* Retrieves the Content-Security-Policy headers (either X-Content-Security-Policy or Content-Security-Policy) from
* a response.

View File

@ -131,7 +131,14 @@ class ContentSecurityPolicyHandlerTest extends TestCase
['csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce],
$this->createRequest(),
$this->createResponse(['Content-Security-Policy' => 'default-src \'self\' domain.com; script-src \'self\' \'unsafe-inline\'', 'Content-Security-Policy-Report-Only' => 'default-src \'self\' domain-report-only.com; script-src \'self\' \'unsafe-inline\'']),
['Content-Security-Policy' => 'default-src \'self\' domain.com; script-src \'self\' \'unsafe-inline\'; script-src-elem \'self\' domain.com \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src \'self\' domain.com \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src-elem \'self\' domain.com \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'Content-Security-Policy-Report-Only' => 'default-src \'self\' domain-report-only.com; script-src \'self\' \'unsafe-inline\'; script-src-elem \'self\' domain-report-only.com \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src \'self\' domain-report-only.com \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src-elem \'self\' domain-report-only.com \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'X-Content-Security-Policy' => null],
['Content-Security-Policy' => 'default-src \'self\' domain.com; script-src \'self\' \'unsafe-inline\'; style-src \'self\' domain.com \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'Content-Security-Policy-Report-Only' => 'default-src \'self\' domain-report-only.com; script-src \'self\' \'unsafe-inline\'; style-src \'self\' domain-report-only.com \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'X-Content-Security-Policy' => null],
],
[
$nonce,
['csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce],
$this->createRequest(),
$this->createResponse(['Content-Security-Policy' => 'default-src \'self\' domain.com; script-src \'self\' \'unsafe-inline\'; script-src-elem \'self\'; style-src \'self\' \'unsafe-inline\'; style-src-elem \'self\'', 'Content-Security-Policy-Report-Only' => 'default-src \'self\' domain-report-only.com; script-src \'self\' \'unsafe-inline\'; script-src-elem \'self\'; style-src \'self\' \'unsafe-inline\'; style-src-elem \'self\'']),
['Content-Security-Policy' => 'default-src \'self\' domain.com; script-src \'self\' \'unsafe-inline\'; script-src-elem \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src \'self\' \'unsafe-inline\'; style-src-elem \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'Content-Security-Policy-Report-Only' => 'default-src \'self\' domain-report-only.com; script-src \'self\' \'unsafe-inline\'; script-src-elem \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src \'self\' \'unsafe-inline\'; style-src-elem \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'X-Content-Security-Policy' => null],
],
[
$nonce,

View File

@ -14,8 +14,8 @@ namespace Symfony\Component\Cache\Adapter;
use Predis\Connection\Aggregate\ClusterInterface;
use Predis\Connection\Aggregate\PredisCluster;
use Predis\Response\Status;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Exception\LogicException;
use Symfony\Component\Cache\Marshaller\DeflateMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Marshaller\TagAwareMarshaller;
@ -95,9 +95,7 @@ class RedisTagAwareAdapter extends AbstractTagAwareAdapter
{
$eviction = $this->getRedisEvictionPolicy();
if ('noeviction' !== $eviction && 0 !== strpos($eviction, 'volatile-')) {
CacheItem::log($this->logger, sprintf('Redis maxmemory-policy setting "%s" is *not* supported by RedisTagAwareAdapter, use "noeviction" or "volatile-*" eviction policies', $eviction));
return false;
throw new LogicException(sprintf('Redis maxmemory-policy setting "%s" is *not* supported by RedisTagAwareAdapter, use "noeviction" or "volatile-*" eviction policies.', $eviction));
}
// serialize values

View File

@ -34,9 +34,10 @@ abstract class AbstractRedisAdapterTest extends AdapterTestCase
if (!\extension_loaded('redis')) {
self::markTestSkipped('Extension redis required.');
}
if (!@((new \Redis())->connect(getenv('REDIS_HOST')))) {
$e = error_get_last();
self::markTestSkipped($e['message']);
try {
(new \Redis())->connect(getenv('REDIS_HOST'));
} catch (\Exception $e) {
self::markTestSkipped($e->getMessage());
}
}

View File

@ -15,6 +15,9 @@ use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Adapter\MemcachedAdapter;
/**
* @group integration
*/
class MemcachedAdapterTest extends AdapterTestCase
{
protected $skippedTests = [

View File

@ -14,6 +14,9 @@ namespace Symfony\Component\Cache\Tests\Adapter;
use Predis\Connection\StreamConnection;
use Symfony\Component\Cache\Adapter\RedisAdapter;
/**
* @group integration
*/
class PredisAdapterTest extends AbstractRedisAdapterTest
{
public static function setUpBeforeClass(): void

View File

@ -11,6 +11,9 @@
namespace Symfony\Component\Cache\Tests\Adapter;
/**
* @group integration
*/
class PredisClusterAdapterTest extends AbstractRedisAdapterTest
{
public static function setUpBeforeClass(): void

View File

@ -13,6 +13,9 @@ namespace Symfony\Component\Cache\Tests\Adapter;
use Symfony\Component\Cache\Adapter\RedisAdapter;
/**
* @group integration
*/
class PredisRedisClusterAdapterTest extends AbstractRedisAdapterTest
{
public static function setUpBeforeClass(): void

View File

@ -15,6 +15,9 @@ use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter;
use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait;
/**
* @group integration
*/
class PredisTagAwareAdapterTest extends PredisAdapterTest
{
use TagAwareTestTrait;

View File

@ -15,6 +15,9 @@ use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter;
use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait;
/**
* @group integration
*/
class PredisTagAwareClusterAdapterTest extends PredisClusterAdapterTest
{
use TagAwareTestTrait;

View File

@ -14,6 +14,9 @@ namespace Symfony\Component\Cache\Tests\Adapter;
use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Adapter\RedisAdapter;
/**
* @group integration
*/
class RedisAdapterSentinelTest extends AbstractRedisAdapterTest
{
public static function setUpBeforeClass(): void

View File

@ -16,6 +16,9 @@ use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Adapter\RedisAdapter;
use Symfony\Component\Cache\Traits\RedisProxy;
/**
* @group integration
*/
class RedisAdapterTest extends AbstractRedisAdapterTest
{
public static function setUpBeforeClass(): void

View File

@ -11,6 +11,9 @@
namespace Symfony\Component\Cache\Tests\Adapter;
/**
* @group integration
*/
class RedisArrayAdapterTest extends AbstractRedisAdapterTest
{
public static function setUpBeforeClass(): void

View File

@ -16,6 +16,9 @@ use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Adapter\RedisAdapter;
use Symfony\Component\Cache\Traits\RedisClusterProxy;
/**
* @group integration
*/
class RedisClusterAdapterTest extends AbstractRedisAdapterTest
{
public static function setUpBeforeClass(): void

View File

@ -16,6 +16,9 @@ use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter;
use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait;
use Symfony\Component\Cache\Traits\RedisProxy;
/**
* @group integration
*/
class RedisTagAwareAdapterTest extends RedisAdapterTest
{
use TagAwareTestTrait;

View File

@ -15,6 +15,9 @@ use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter;
use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait;
/**
* @group integration
*/
class RedisTagAwareArrayAdapterTest extends RedisArrayAdapterTest
{
use TagAwareTestTrait;

View File

@ -16,6 +16,9 @@ use Symfony\Component\Cache\Adapter\RedisTagAwareAdapter;
use Symfony\Component\Cache\Tests\Traits\TagAwareTestTrait;
use Symfony\Component\Cache\Traits\RedisClusterProxy;
/**
* @group integration
*/
class RedisTagAwareClusterAdapterTest extends RedisClusterAdapterTest
{
use TagAwareTestTrait;

View File

@ -435,7 +435,7 @@ class QuestionHelper extends Helper
if (false !== $shell = $this->getShell()) {
$readCmd = 'csh' === $shell ? 'set mypassword = $<' : 'read -r mypassword';
$command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword'", $shell, $readCmd);
$command = sprintf("/usr/bin/env %s -c 'stty -echo; %s; stty echo; echo \$mypassword' 2> /dev/null", $shell, $readCmd);
$sCommand = shell_exec($command);
$value = $trimmable ? rtrim($sCommand) : $sCommand;
$output->writeln('');
@ -459,6 +459,11 @@ class QuestionHelper extends Helper
{
$error = null;
$attempts = $question->getMaxAttempts();
if (null === $attempts && !$this->isTty()) {
$attempts = 1;
}
while (null === $attempts || $attempts--) {
if (null !== $error) {
$this->writeError($output, $error);
@ -501,4 +506,19 @@ class QuestionHelper extends Helper
return self::$shell;
}
private function isTty(): bool
{
$inputStream = !$this->inputStream && \defined('STDIN') ? STDIN : $this->inputStream;
if (\function_exists('stream_isatty')) {
return stream_isatty($inputStream);
}
if (!\function_exists('posix_isatty')) {
return posix_isatty($inputStream);
}
return true;
}
}

View File

@ -726,6 +726,23 @@ class QuestionHelperTest extends AbstractQuestionHelperTest
$dialog->ask($this->createStreamableInputInterfaceMock($this->getInputStream('')), $this->createOutputInterface(), $question);
}
public function testAskThrowsExceptionFromValidatorEarlyWhenTtyIsMissing()
{
$this->expectException('Exception');
$this->expectExceptionMessage('Bar, not Foo');
$output = $this->getMockBuilder('\Symfony\Component\Console\Output\OutputInterface')->getMock();
$output->expects($this->once())->method('writeln');
(new QuestionHelper())->ask(
$this->createStreamableInputInterfaceMock($this->getInputStream('Foo'), true),
$output,
(new Question('Q?'))->setHidden(true)->setValidator(function ($input) {
throw new \Exception("Bar, not $input");
})
);
}
public function testEmptyChoices()
{
$this->expectException('LogicException');

View File

@ -682,7 +682,7 @@ class YamlFileLoader extends FileLoader
try {
$configuration = $this->yamlParser->parseFile($file, Yaml::PARSE_CONSTANT | Yaml::PARSE_CUSTOM_TAGS);
} catch (ParseException $e) {
throw new InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML: '.$e->getMessage(), $file), 0, $e);
throw new InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML', $file).': '.$e->getMessage(), 0, $e);
}
return $this->validate($configuration, $file);

View File

@ -32,7 +32,7 @@
$exceptionAsArray = $exception->toArray();
$exceptionWithUserCode = [];
$exceptionAsArrayCount = count($exceptionAsArray);
$last = count($exceptionAsArray) - 1;
$last = $exceptionAsArrayCount - 1;
foreach ($exceptionAsArray as $i => $e) {
foreach ($e['trace'] as $trace) {
if ($trace['file'] && false === mb_strpos($trace['file'], '/vendor/') && false === mb_strpos($trace['file'], '/var/cache/') && $i < $last) {

View File

@ -14,10 +14,7 @@
<testsuites>
<testsuite name="Symfony ErrorHandler Component Test Suite">
<directory>./Tests/</directory>
</testsuite>
<testsuite name="Symfony ErrorHandler Extension Test Suite">
<directory suffix=".phpt">./Resources/ext/tests/</directory>
<directory suffix=".phpt">./Tests/</directory>
</testsuite>
</testsuites>

View File

@ -434,28 +434,19 @@ class Filesystem
$startPath = str_replace('\\', '/', $startPath);
}
$stripDriveLetter = function ($path) {
if (\strlen($path) > 2 && ':' === $path[1] && '/' === $path[2] && ctype_alpha($path[0])) {
return substr($path, 2);
}
return $path;
$splitDriveLetter = function ($path) {
return (\strlen($path) > 2 && ':' === $path[1] && '/' === $path[2] && ctype_alpha($path[0]))
? [substr($path, 2), strtoupper($path[0])]
: [$path, null];
};
$endPath = $stripDriveLetter($endPath);
$startPath = $stripDriveLetter($startPath);
// Split the paths into arrays
$startPathArr = explode('/', trim($startPath, '/'));
$endPathArr = explode('/', trim($endPath, '/'));
$normalizePathArray = function ($pathSegments) {
$splitPath = function ($path) {
$result = [];
foreach ($pathSegments as $segment) {
foreach (explode('/', trim($path, '/')) as $segment) {
if ('..' === $segment) {
array_pop($result);
} elseif ('.' !== $segment) {
} elseif ('.' !== $segment && '' !== $segment) {
$result[] = $segment;
}
}
@ -463,8 +454,16 @@ class Filesystem
return $result;
};
$startPathArr = $normalizePathArray($startPathArr);
$endPathArr = $normalizePathArray($endPathArr);
list($endPath, $endDriveLetter) = $splitDriveLetter($endPath);
list($startPath, $startDriveLetter) = $splitDriveLetter($startPath);
$startPathArr = $splitPath($startPath);
$endPathArr = $splitPath($endPath);
if ($endDriveLetter && $startDriveLetter && $endDriveLetter != $startDriveLetter) {
// End path is on another drive, so no relative path exists
return $endDriveLetter.':/'.($endPathArr ? implode('/', $endPathArr).'/' : '');
}
// Find for which directory the common path stops
$index = 0;

View File

@ -1151,10 +1151,14 @@ class FilesystemTest extends FilesystemTestCase
['/../aa/bb/cc', '/aa/dd/..', 'bb/cc/'],
['/../../aa/../bb/cc', '/aa/dd/..', '../bb/cc/'],
['C:/aa/bb/cc', 'C:/aa/dd/..', 'bb/cc/'],
['C:/aa/bb/cc', 'c:/aa/dd/..', 'bb/cc/'],
['c:/aa/../bb/cc', 'c:/aa/dd/..', '../bb/cc/'],
['C:/aa/bb/../../cc', 'C:/aa/../dd/..', 'cc/'],
['C:/../aa/bb/cc', 'C:/aa/dd/..', 'bb/cc/'],
['C:/../../aa/../bb/cc', 'C:/aa/dd/..', '../bb/cc/'],
['D:/', 'C:/aa/../bb/cc', 'D:/'],
['D:/aa/bb', 'C:/aa', 'D:/aa/bb/'],
['D:/../../aa/../bb/cc', 'C:/aa/dd/..', 'D:/bb/cc/'],
];
if ('\\' === \DIRECTORY_SEPARATOR) {

View File

@ -62,7 +62,7 @@ class DependencyInjectionExtension implements FormExtensionInterface
$extensions = [];
if (isset($this->typeExtensionServices[$name])) {
foreach ($this->typeExtensionServices[$name] as $serviceId => $extension) {
foreach ($this->typeExtensionServices[$name] as $extension) {
$extensions[] = $extension;
$extendedTypes = [];
@ -72,7 +72,7 @@ class DependencyInjectionExtension implements FormExtensionInterface
// validate the result of getExtendedTypes() to ensure it is consistent with the service definition
if (!\in_array($name, $extendedTypes, true)) {
throw new InvalidArgumentException(sprintf('The extended type specified for the service "%s" does not match the actual extended type. Expected "%s", given "%s".', $serviceId, $name, implode(', ', $extendedTypes)));
throw new InvalidArgumentException(sprintf('The extended type "%s" specified for the type extension class "%s" does not match any of the actual extended types (["%s"]).', $name, \get_class($extension), implode('", "', $extendedTypes)));
}
}
}

View File

@ -958,7 +958,7 @@ class Form implements \IteratorAggregate, FormInterface, ClearableErrorsInterfac
*
* @return FormInterface The child form
*
* @throws \OutOfBoundsException if the named child does not exist
* @throws OutOfBoundsException if the named child does not exist
*/
public function offsetGet($name)
{

View File

@ -60,7 +60,7 @@ interface FormInterface extends \ArrayAccess, \Traversable, \Countable
*
* @return self
*
* @throws \OutOfBoundsException if the named child does not exist
* @throws Exception\OutOfBoundsException if the named child does not exist
*/
public function get(string $name);

View File

@ -44,6 +44,8 @@ class DependencyInjectionExtensionTest extends TestCase
public function testThrowExceptionForInvalidExtendedType()
{
$this->expectException('Symfony\Component\Form\Exception\InvalidArgumentException');
$this->expectExceptionMessage(sprintf('The extended type "unmatched" specified for the type extension class "%s" does not match any of the actual extended types (["test"]).', TestTypeExtension::class));
$extensions = [
'unmatched' => new \ArrayIterator([new TestTypeExtension()]),
];

View File

@ -44,6 +44,11 @@ abstract class AbstractRedisSessionHandlerTestCase extends TestCase
if (!\extension_loaded('redis')) {
self::markTestSkipped('Extension redis required.');
}
try {
(new \Redis())->connect(getenv('REDIS_HOST'));
} catch (\Exception $e) {
self::markTestSkipped($e->getMessage());
}
$host = getenv('REDIS_HOST') ?: 'localhost';

View File

@ -13,6 +13,9 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler;
use Predis\Client;
/**
* @group integration
*/
class PredisClusterSessionHandlerTest extends AbstractRedisSessionHandlerTestCase
{
/**

View File

@ -13,6 +13,9 @@ namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler;
use Predis\Client;
/**
* @group integration
*/
class PredisSessionHandlerTest extends AbstractRedisSessionHandlerTestCase
{
/**

View File

@ -11,6 +11,9 @@
namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler;
/**
* @group integration
*/
class RedisArraySessionHandlerTest extends AbstractRedisSessionHandlerTestCase
{
/**

View File

@ -11,6 +11,9 @@
namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler;
/**
* @group integration
*/
class RedisClusterSessionHandlerTest extends AbstractRedisSessionHandlerTestCase
{
public static function setUpBeforeClass(): void

View File

@ -11,6 +11,9 @@
namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler;
/**
* @group integration
*/
class RedisSessionHandlerTest extends AbstractRedisSessionHandlerTestCase
{
/**

View File

@ -294,30 +294,30 @@ class InflectorTest extends TestCase
/**
* @dataProvider singularizeProvider
*/
public function testSingularize($plural, $singular)
public function testSingularize($plural, $expectedSingular)
{
$single = Inflector::singularize($plural);
if (\is_string($singular) && \is_array($single)) {
$this->fail("--- Expected\n`string`: ".$singular."\n+++ Actual\n`array`: ".implode(', ', $single));
} elseif (\is_array($singular) && \is_string($single)) {
$this->fail("--- Expected\n`array`: ".implode(', ', $singular)."\n+++ Actual\n`string`: ".$single);
$singular = Inflector::singularize($plural);
if (\is_string($expectedSingular) && \is_array($singular)) {
$this->fail("--- Expected\n`string`: ".$expectedSingular."\n+++ Actual\n`array`: ".implode(', ', $singular));
} elseif (\is_array($expectedSingular) && \is_string($singular)) {
$this->fail("--- Expected\n`array`: ".implode(', ', $expectedSingular)."\n+++ Actual\n`string`: ".$singular);
}
$this->assertEquals($singular, $single);
$this->assertEquals($expectedSingular, $singular);
}
/**
* @dataProvider pluralizeProvider
*/
public function testPluralize($plural, $singular)
public function testPluralize($singular, $expectedPlural)
{
$single = Inflector::pluralize($plural);
if (\is_string($singular) && \is_array($single)) {
$this->fail("--- Expected\n`string`: ".$singular."\n+++ Actual\n`array`: ".implode(', ', $single));
} elseif (\is_array($singular) && \is_string($single)) {
$this->fail("--- Expected\n`array`: ".implode(', ', $singular)."\n+++ Actual\n`string`: ".$single);
$plural = Inflector::pluralize($singular);
if (\is_string($expectedPlural) && \is_array($plural)) {
$this->fail("--- Expected\n`string`: ".$expectedPlural."\n+++ Actual\n`array`: ".implode(', ', $plural));
} elseif (\is_array($expectedPlural) && \is_string($plural)) {
$this->fail("--- Expected\n`array`: ".implode(', ', $expectedPlural)."\n+++ Actual\n`string`: ".$plural);
}
$this->assertEquals($singular, $single);
$this->assertEquals($expectedPlural, $plural);
}
}

View File

@ -19,6 +19,7 @@ use Symfony\Component\Lock\Store\MemcachedStore;
* @author Jérémy Derussé <jeremy@derusse.com>
*
* @requires extension memcached
* @group integration
*/
class MemcachedStoreTest extends AbstractStoreTest
{

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Lock\Tests\Store;
/**
* @author Jérémy Derussé <jeremy@derusse.com>
* @group integration
*/
class PredisStoreTest extends AbstractRedisStoreTest
{

View File

@ -15,6 +15,7 @@ namespace Symfony\Component\Lock\Tests\Store;
* @author Jérémy Derussé <jeremy@derusse.com>
*
* @requires extension redis
* @group integration
*/
class RedisArrayStoreTest extends AbstractRedisStoreTest
{
@ -23,9 +24,10 @@ class RedisArrayStoreTest extends AbstractRedisStoreTest
if (!class_exists('RedisArray')) {
self::markTestSkipped('The RedisArray class is required.');
}
if (!@((new \Redis())->connect(getenv('REDIS_HOST')))) {
$e = error_get_last();
self::markTestSkipped($e['message']);
try {
(new \Redis())->connect(getenv('REDIS_HOST'));
} catch (\Exception $e) {
self::markTestSkipped($e->getMessage());
}
}

View File

@ -15,6 +15,7 @@ namespace Symfony\Component\Lock\Tests\Store;
* @author Jérémy Derussé <jeremy@derusse.com>
*
* @requires extension redis
* @group integration
*/
class RedisClusterStoreTest extends AbstractRedisStoreTest
{

View File

@ -17,14 +17,16 @@ use Symfony\Component\Lock\Store\RedisStore;
* @author Jérémy Derussé <jeremy@derusse.com>
*
* @requires extension redis
* @group integration
*/
class RedisStoreTest extends AbstractRedisStoreTest
{
public static function setUpBeforeClass(): void
{
if (!@((new \Redis())->connect(getenv('REDIS_HOST')))) {
$e = error_get_last();
self::markTestSkipped($e['message']);
try {
(new \Redis())->connect(getenv('REDIS_HOST'));
} catch (\Exception $e) {
self::markTestSkipped($e->getMessage());
}
}

View File

@ -33,6 +33,7 @@ use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
/**
* @requires extension amqp
* @group integration
*/
class AmqpExtIntegrationTest extends TestCase
{

View File

@ -17,14 +17,14 @@ use Symfony\Component\Messenger\Transport\RedisExt\Connection;
/**
* @requires extension redis >= 4.3.0
* @group integration
*/
class ConnectionTest extends TestCase
{
public static function setUpBeforeClass(): void
{
$redis = Connection::fromDsn('redis://localhost/queue');
try {
$redis = Connection::fromDsn('redis://localhost/queue');
$redis->get();
} catch (TransportException $e) {
if (0 === strpos($e->getMessage(), 'ERR unknown command \'X')) {
@ -32,6 +32,8 @@ class ConnectionTest extends TestCase
}
throw $e;
} catch (\RedisException $e) {
self::markTestSkipped($e->getMessage());
}
}

View File

@ -18,6 +18,7 @@ use Symfony\Component\Messenger\Transport\RedisExt\Connection;
/**
* @requires extension redis
* @group time-sensitive
* @group integration
*/
class RedisExtIntegrationTest extends TestCase
{
@ -30,10 +31,14 @@ class RedisExtIntegrationTest extends TestCase
$this->markTestSkipped('The "MESSENGER_REDIS_DSN" environment variable is required.');
}
$this->redis = new \Redis();
$this->connection = Connection::fromDsn(getenv('MESSENGER_REDIS_DSN'), [], $this->redis);
$this->connection->cleanup();
$this->connection->setup();
try {
$this->redis = new \Redis();
$this->connection = Connection::fromDsn(getenv('MESSENGER_REDIS_DSN'), [], $this->redis);
$this->connection->cleanup();
$this->connection->setup();
} catch (\Exception $e) {
self::markTestSkipped($e->getMessage());
}
}
public function testConnectionSendAndGet()

View File

@ -31,12 +31,26 @@ class RedisTransportFactoryTest extends TestCase
$this->assertFalse($factory->supports('invalid-dsn', []));
}
/**
* @group integration
*/
public function testCreateTransport()
{
$this->skipIfRedisUnavailable();
$factory = new RedisTransportFactory();
$serializer = $this->getMockBuilder(SerializerInterface::class)->getMock();
$expectedTransport = new RedisTransport(Connection::fromDsn('redis://localhost', ['foo' => 'bar']), $serializer);
$expectedTransport = new RedisTransport(Connection::fromDsn('redis://'.getenv('REDIS_HOST'), ['foo' => 'bar']), $serializer);
$this->assertEquals($expectedTransport, $factory->createTransport('redis://localhost', ['foo' => 'bar'], $serializer));
$this->assertEquals($expectedTransport, $factory->createTransport('redis://'.getenv('REDIS_HOST'), ['foo' => 'bar'], $serializer));
}
private function skipIfRedisUnavailable()
{
try {
(new \Redis())->connect(getenv('REDIS_HOST'));
} catch (\Exception $e) {
self::markTestSkipped($e->getMessage());
}
}
}

View File

@ -62,7 +62,7 @@ class YamlFileLoader extends FileLoader
try {
$parsedConfig = $this->yamlParser->parseFile($path, Yaml::PARSE_CONSTANT);
} catch (ParseException $e) {
throw new \InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML.', $path), 0, $e);
throw new \InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML', $path).': '.$e->getMessage(), 0, $e);
}
$collection = new RouteCollection();

View File

@ -151,6 +151,10 @@ class Serializer implements SerializerInterface, ContextAwareNormalizerInterface
}
if (\is_array($data) || $data instanceof \Traversable) {
if ($data instanceof \Countable && 0 === $data->count()) {
return $data;
}
$normalized = [];
foreach ($data as $key => $val) {
$normalized[$key] = $this->normalize($val, $format, $context);

View File

@ -25,6 +25,7 @@ use Symfony\Component\Serializer\Mapping\ClassMetadataInterface;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\CustomNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
@ -488,6 +489,26 @@ class SerializerTest extends TestCase
(new Serializer())->normalize(tmpfile());
}
public function testNormalizePreserveEmptyArrayObject()
{
$serializer = new Serializer(
[
new PropertyNormalizer(),
new ObjectNormalizer(),
new ArrayDenormalizer(),
],
[
'json' => new JsonEncoder(),
]
);
$object = [];
$object['foo'] = new \ArrayObject();
$object['bar'] = new \ArrayObject(['notempty']);
$object['baz'] = new \ArrayObject(['nested' => new \ArrayObject()]);
$this->assertEquals('{"foo":{},"bar":["notempty"],"baz":{"nested":{}}}', $serializer->serialize($object, 'json', [AbstractObjectNormalizer::PRESERVE_EMPTY_OBJECTS => true]));
}
private function serializerWithClassDiscriminator()
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));

View File

@ -42,7 +42,7 @@ class YamlFileLoader extends FileLoader
try {
$messages = $this->yamlParser->parseFile($resource, Yaml::PARSE_CONSTANT);
} catch (ParseException $e) {
throw new InvalidResourceException(sprintf('Error parsing YAML, invalid file "%s".', $resource), 0, $e);
throw new InvalidResourceException(sprintf('The file "%s" does not contain valid YAML', $resource).': '.$e->getMessage(), 0, $e);
}
if (null !== $messages && !\is_array($messages)) {

View File

@ -68,6 +68,11 @@ class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterf
public function all(string $domain = null)
{
if (null !== $domain) {
// skip messages merge if intl-icu requested explicitly
if (false !== strpos($domain, self::INTL_DOMAIN_SUFFIX)) {
return $this->messages[$domain] ?? [];
}
return ($this->messages[$domain.self::INTL_DOMAIN_SUFFIX] ?? []) + ($this->messages[$domain] ?? []);
}

View File

@ -56,6 +56,30 @@ class MessageCatalogueTest extends TestCase
$this->assertEquals($messages, $catalogue->all());
}
public function testAllIntICU()
{
$messages = [
'domain1+intl-icu' => ['foo' => 'bar'],
'domain2+intl-icu' => ['bar' => 'foo'],
'domain2' => ['biz' => 'biz'],
];
$catalogue = new MessageCatalogue('en', $messages);
// separated domains
$this->assertSame(['foo' => 'bar'], $catalogue->all('domain1+intl-icu'));
$this->assertSame(['bar' => 'foo'], $catalogue->all('domain2+intl-icu'));
// merged, intl-icu ignored
$this->assertSame(['bar' => 'foo', 'biz' => 'biz'], $catalogue->all('domain2'));
// intl-icu ignored
$messagesExpected = [
'domain1' => ['foo' => 'bar'],
'domain2' => ['bar' => 'foo', 'biz' => 'biz'],
];
$this->assertSame($messagesExpected, $catalogue->all());
}
public function testHas()
{
$catalogue = new MessageCatalogue('en', ['domain1' => ['foo' => 'foo'], 'domain2+intl-icu' => ['bar' => 'bar']]);

View File

@ -60,6 +60,9 @@ class EmailValidator extends ConstraintValidator
}
$value = (string) $value;
if ('' === $value) {
return;
}
if (null !== $constraint->normalizer) {
$value = ($constraint->normalizer)($value);

View File

@ -114,7 +114,7 @@ class YamlFileLoader extends FileLoader
try {
$classes = $this->yamlParser->parseFile($path, Yaml::PARSE_CONSTANT);
} catch (ParseException $e) {
throw new \InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML.', $path), 0, $e);
throw new \InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML', $path).': '.$e->getMessage(), 0, $e);
}
// empty file

View File

@ -352,7 +352,7 @@
</trans-unit>
<trans-unit id="91">
<source>This value should be either negative or zero.</source>
<target>Ova vrednost bi trebala biti pozitivna ili nula.</target>
<target>Ova vrednost bi trebala biti negativna ili nula.</target>
</trans-unit>
<trans-unit id="92">
<source>This value is not a valid timezone.</source>

View File

@ -46,6 +46,13 @@ class EmailValidatorTest extends ConstraintValidatorTestCase
$this->assertNoViolation();
}
public function testObjectEmptyStringIsValid()
{
$this->validator->validate(new EmptyEmailObject(), new Email());
$this->assertNoViolation();
}
public function testExpectsStringCompatibleType()
{
$this->expectException('Symfony\Component\Validator\Exception\UnexpectedValueException');
@ -300,3 +307,11 @@ class EmailValidatorTest extends ConstraintValidatorTestCase
];
}
}
class EmptyEmailObject
{
public function __toString()
{
return '';
}
}

View File

@ -53,6 +53,11 @@ class Entity extends EntityParent implements EntityInterfaceB
$this->internal = $internal;
}
public function getFirstName()
{
return $this->firstName;
}
public function getInternal()
{
return $this->internal.' from getter';
@ -141,4 +146,9 @@ class Entity extends EntityParent implements EntityInterfaceB
{
$this->childB = $childB;
}
public function getReference()
{
return $this->reference;
}
}

View File

@ -32,6 +32,8 @@ abstract class AbstractValidatorTest extends TestCase
const REFERENCE_CLASS = 'Symfony\Component\Validator\Tests\Fixtures\Reference';
const LAZY_PROPERTY = 'Symfony\Component\Validator\Validator\LazyProperty';
/**
* @var FakeMetadataFactory
*/
@ -54,6 +56,7 @@ abstract class AbstractValidatorTest extends TestCase
$this->referenceMetadata = new ClassMetadata(self::REFERENCE_CLASS);
$this->metadataFactory->addMetadata($this->metadata);
$this->metadataFactory->addMetadata($this->referenceMetadata);
$this->metadataFactory->addMetadata(new ClassMetadata(self::LAZY_PROPERTY));
}
protected function tearDown(): void
@ -510,7 +513,10 @@ abstract class AbstractValidatorTest extends TestCase
$this->validate($entity);
}
public function testArrayReference()
/**
* @dataProvider getConstraintMethods
*/
public function testArrayReference($constraintMethod)
{
$entity = new Entity();
$entity->reference = ['key' => new Reference()];
@ -528,7 +534,7 @@ abstract class AbstractValidatorTest extends TestCase
$context->addViolation('Message %param%', ['%param%' => 'value']);
};
$this->metadata->addPropertyConstraint('reference', new Valid());
$this->metadata->$constraintMethod('reference', new Valid());
$this->referenceMetadata->addConstraint(new Callback([
'callback' => $callback,
'groups' => 'Group',
@ -548,8 +554,10 @@ abstract class AbstractValidatorTest extends TestCase
$this->assertNull($violations[0]->getCode());
}
// https://github.com/symfony/symfony/issues/6246
public function testRecursiveArrayReference()
/**
* @dataProvider getConstraintMethods
*/
public function testRecursiveArrayReference($constraintMethod)
{
$entity = new Entity();
$entity->reference = [2 => ['key' => new Reference()]];
@ -567,7 +575,7 @@ abstract class AbstractValidatorTest extends TestCase
$context->addViolation('Message %param%', ['%param%' => 'value']);
};
$this->metadata->addPropertyConstraint('reference', new Valid());
$this->metadata->$constraintMethod('reference', new Valid());
$this->referenceMetadata->addConstraint(new Callback([
'callback' => $callback,
'groups' => 'Group',
@ -611,7 +619,10 @@ abstract class AbstractValidatorTest extends TestCase
$this->assertCount(0, $violations);
}
public function testArrayTraversalCannotBeDisabled()
/**
* @dataProvider getConstraintMethods
*/
public function testArrayTraversalCannotBeDisabled($constraintMethod)
{
$entity = new Entity();
$entity->reference = ['key' => new Reference()];
@ -620,7 +631,7 @@ abstract class AbstractValidatorTest extends TestCase
$context->addViolation('Message %param%', ['%param%' => 'value']);
};
$this->metadata->addPropertyConstraint('reference', new Valid([
$this->metadata->$constraintMethod('reference', new Valid([
'traverse' => false,
]));
$this->referenceMetadata->addConstraint(new Callback($callback));
@ -631,7 +642,10 @@ abstract class AbstractValidatorTest extends TestCase
$this->assertCount(1, $violations);
}
public function testRecursiveArrayTraversalCannotBeDisabled()
/**
* @dataProvider getConstraintMethods
*/
public function testRecursiveArrayTraversalCannotBeDisabled($constraintMethod)
{
$entity = new Entity();
$entity->reference = [2 => ['key' => new Reference()]];
@ -640,9 +654,10 @@ abstract class AbstractValidatorTest extends TestCase
$context->addViolation('Message %param%', ['%param%' => 'value']);
};
$this->metadata->addPropertyConstraint('reference', new Valid([
$this->metadata->$constraintMethod('reference', new Valid([
'traverse' => false,
]));
$this->referenceMetadata->addConstraint(new Callback($callback));
$violations = $this->validate($entity);
@ -651,12 +666,15 @@ abstract class AbstractValidatorTest extends TestCase
$this->assertCount(1, $violations);
}
public function testIgnoreScalarsDuringArrayTraversal()
/**
* @dataProvider getConstraintMethods
*/
public function testIgnoreScalarsDuringArrayTraversal($constraintMethod)
{
$entity = new Entity();
$entity->reference = ['string', 1234];
$this->metadata->addPropertyConstraint('reference', new Valid());
$this->metadata->$constraintMethod('reference', new Valid());
$violations = $this->validate($entity);
@ -664,12 +682,15 @@ abstract class AbstractValidatorTest extends TestCase
$this->assertCount(0, $violations);
}
public function testIgnoreNullDuringArrayTraversal()
/**
* @dataProvider getConstraintMethods
*/
public function testIgnoreNullDuringArrayTraversal($constraintMethod)
{
$entity = new Entity();
$entity->reference = [null];
$this->metadata->addPropertyConstraint('reference', new Valid());
$this->metadata->$constraintMethod('reference', new Valid());
$violations = $this->validate($entity);
@ -1218,6 +1239,14 @@ abstract class AbstractValidatorTest extends TestCase
}
}
public function getConstraintMethods()
{
return [
['addPropertyConstraint'],
['addGetterConstraint'],
];
}
public function getTestReplaceDefaultGroup()
{
return [

View File

@ -627,6 +627,10 @@ class RecursiveContextualValidator implements ContextualValidatorInterface
// See validateClassNode()
$cascadedGroups = null !== $cascadedGroups && \count($cascadedGroups) > 0 ? $cascadedGroups : $groups;
if ($value instanceof LazyProperty) {
$value = $value->getPropertyValue();
}
if (\is_array($value)) {
// Arrays are always traversed, independent of the specified
// traversal strategy

View File

@ -17,6 +17,7 @@ use Symfony\Component\VarDumper\Test\VarDumperTestTrait;
/**
* @author Nicolas Grekas <p@tchwork.com>
* @requires extension redis
* @group integration
*/
class RedisCasterTest extends TestCase
{
@ -37,16 +38,18 @@ EODUMP;
public function testConnected()
{
$redisHost = getenv('REDIS_HOST');
$redis = new \Redis();
if (!@$redis->connect('127.0.0.1')) {
$e = error_get_last();
self::markTestSkipped($e['message']);
try {
$redis->connect($redisHost);
} catch (\Exception $e) {
self::markTestSkipped($e->getMessage());
}
$xCast = <<<'EODUMP'
$xCast = <<<EODUMP
Redis {%A
isConnected: true
host: "127.0.0.1"
host: "$redisHost"
port: 6379
auth: null
mode: ATOMIC

View File

@ -618,8 +618,14 @@ class Parser
}
$isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
$isItComment = $this->isCurrentLineComment();
while ($this->moveToNextLine()) {
if ($isItComment && !$isItUnindentedCollection) {
$isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
$isItComment = $this->isCurrentLineComment();
}
$indent = $this->getCurrentLineIndentation();
if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {

View File

@ -74,3 +74,17 @@ yaml: |
'foo #': baz
php: |
['foo #' => 'baz']
---
test: Comment before first item in unindented collection
brief: >
Comment directly before unindented collection is allowed
yaml: |
collection1:
# comment
- a
- b
collection2:
- a
- b
php: |
['collection1' => ['a', 'b'], 'collection2' => ['a', 'b']]