+ * @author Nicolas Grekas
+ */
+class FlockStore implements StoreInterface
+{
+ private $lockPath;
+
+ /**
+ * @param string $lockPath the directory to store the lock
+ *
+ * @throws LockStorageException If the lock directory could not be created or is not writable
+ */
+ public function __construct($lockPath)
+ {
+ if (!is_dir($lockPath) || !is_writable($lockPath)) {
+ throw new InvalidArgumentException(sprintf('The directory "%s" is not writable.', $lockPath));
+ }
+
+ $this->lockPath = $lockPath;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(Key $key)
+ {
+ $this->lock($key, false);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function waitAndSave(Key $key)
+ {
+ $this->lock($key, true);
+ }
+
+ private function lock(Key $key, $blocking)
+ {
+ // The lock is maybe already acquired.
+ if ($key->hasState(__CLASS__)) {
+ return;
+ }
+
+ $fileName = sprintf('%s/sf.%s.%s.lock',
+ $this->lockPath,
+ preg_replace('/[^a-z0-9\._-]+/i', '-', $key),
+ hash('sha256', $key)
+ );
+
+ // Silence error reporting
+ set_error_handler(function () {
+ });
+ if (!$handle = fopen($fileName, 'r')) {
+ if ($handle = fopen($fileName, 'x')) {
+ chmod($fileName, 0444);
+ } elseif (!$handle = fopen($fileName, 'r')) {
+ usleep(100); // Give some time for chmod() to complete
+ $handle = fopen($fileName, 'r');
+ }
+ }
+ restore_error_handler();
+
+ if (!$handle) {
+ $error = error_get_last();
+ throw new LockStorageException($error['message'], 0, null);
+ }
+
+ // On Windows, even if PHP doc says the contrary, LOCK_NB works, see
+ // https://bugs.php.net/54129
+ if (!flock($handle, LOCK_EX | ($blocking ? 0 : LOCK_NB))) {
+ fclose($handle);
+ throw new LockConflictedException();
+ }
+
+ $key->setState(__CLASS__, $handle);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function putOffExpiration(Key $key, $ttl)
+ {
+ // do nothing, the flock locks forever.
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete(Key $key)
+ {
+ // The lock is maybe not acquired.
+ if (!$key->hasState(__CLASS__)) {
+ return;
+ }
+
+ $handle = $key->getState(__CLASS__);
+
+ flock($handle, LOCK_UN | LOCK_NB);
+ fclose($handle);
+
+ $key->removeState(__CLASS__);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function exists(Key $key)
+ {
+ return $key->hasState(__CLASS__);
+ }
+}
diff --git a/src/Symfony/Component/Lock/Store/MemcachedStore.php b/src/Symfony/Component/Lock/Store/MemcachedStore.php
new file mode 100644
index 0000000000..a1e31ee633
--- /dev/null
+++ b/src/Symfony/Component/Lock/Store/MemcachedStore.php
@@ -0,0 +1,179 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Store;
+
+use Symfony\Component\Lock\Exception\InvalidArgumentException;
+use Symfony\Component\Lock\Exception\LockConflictedException;
+use Symfony\Component\Lock\Key;
+use Symfony\Component\Lock\StoreInterface;
+
+/**
+ * MemcachedStore is a StoreInterface implementation using Memcached as store engine.
+ *
+ * @author Jérémy Derussé
+ */
+class MemcachedStore implements StoreInterface
+{
+ private $memcached;
+ private $initialTtl;
+ /** @var bool */
+ private $useExtendedReturn;
+
+ public static function isSupported()
+ {
+ return extension_loaded('memcached');
+ }
+
+ /**
+ * @param \Memcached $memcached
+ * @param int $initialTtl the expiration delay of locks in seconds
+ */
+ public function __construct(\Memcached $memcached, $initialTtl = 300)
+ {
+ if (!static::isSupported()) {
+ throw new InvalidArgumentException('Memcached extension is required');
+ }
+
+ if ($initialTtl < 1) {
+ throw new InvalidArgumentException(sprintf('%s() expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
+ }
+
+ $this->memcached = $memcached;
+ $this->initialTtl = $initialTtl;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(Key $key)
+ {
+ $token = $this->getToken($key);
+
+ if ($this->memcached->add((string) $key, $token, (int) ceil($this->initialTtl))) {
+ return;
+ }
+
+ // the lock is already acquire. It could be us. Let's try to put off.
+ $this->putOffExpiration($key, $this->initialTtl);
+ }
+
+ public function waitAndSave(Key $key)
+ {
+ throw new InvalidArgumentException(sprintf('The store "%s" does not supports blocking locks.', get_class($this)));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function putOffExpiration(Key $key, $ttl)
+ {
+ if ($ttl < 1) {
+ throw new InvalidArgumentException(sprintf('%s() expects a TTL greater or equals to 1. Got %s.', __METHOD__, $ttl));
+ }
+
+ // Interface defines a float value but Store required an integer.
+ $ttl = (int) ceil($ttl);
+
+ $token = $this->getToken($key);
+
+ list($value, $cas) = $this->getValueAndCas($key);
+
+ // Could happens when we ask a putOff after a timeout but in luck nobody steal the lock
+ if (\Memcached::RES_NOTFOUND === $this->memcached->getResultCode()) {
+ if ($this->memcached->add((string) $key, $token, $ttl)) {
+ return;
+ }
+
+ // no luck, with concurrency, someone else acquire the lock
+ throw new LockConflictedException();
+ }
+
+ // Someone else steal the lock
+ if ($value !== $token) {
+ throw new LockConflictedException();
+ }
+
+ if (!$this->memcached->cas($cas, (string) $key, $token, $ttl)) {
+ throw new LockConflictedException();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete(Key $key)
+ {
+ $token = $this->getToken($key);
+
+ list($value, $cas) = $this->getValueAndCas($key);
+
+ if ($value !== $token) {
+ // we are not the owner of the lock. Nothing to do.
+ return;
+ }
+
+ // To avoid concurrency in deletion, the trick is to extends the TTL then deleting the key
+ if (!$this->memcached->cas($cas, (string) $key, $token, 2)) {
+ // Someone steal our lock. It does not belongs to us anymore. Nothing to do.
+ return;
+ }
+
+ // Now, we are the owner of the lock for 2 more seconds, we can delete it.
+ $this->memcached->delete((string) $key);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function exists(Key $key)
+ {
+ return $this->memcached->get((string) $key) === $this->getToken($key);
+ }
+
+ /**
+ * Retrieve an unique token for the given key.
+ *
+ * @param Key $key
+ *
+ * @return string
+ */
+ private function getToken(Key $key)
+ {
+ if (!$key->hasState(__CLASS__)) {
+ $token = base64_encode(random_bytes(32));
+ $key->setState(__CLASS__, $token);
+ }
+
+ return $key->getState(__CLASS__);
+ }
+
+ private function getValueAndCas(Key $key)
+ {
+ if (null === $this->useExtendedReturn) {
+ $this->useExtendedReturn = version_compare(phpversion('memcached'), '2.9.9', '>');
+ }
+
+ if ($this->useExtendedReturn) {
+ $extendedReturn = $this->memcached->get((string) $key, null, \Memcached::GET_EXTENDED);
+ if ($extendedReturn === \Memcached::GET_ERROR_RETURN_VALUE) {
+ return array($extendedReturn, 0.0);
+ }
+
+ return array($extendedReturn['value'], $extendedReturn['cas']);
+ }
+
+ $cas = 0.0;
+ $value = $this->memcached->get((string) $key, null, $cas);
+
+ return array($value, $cas);
+ }
+}
diff --git a/src/Symfony/Component/Lock/Store/RedisStore.php b/src/Symfony/Component/Lock/Store/RedisStore.php
new file mode 100644
index 0000000000..b9ea2a5fb8
--- /dev/null
+++ b/src/Symfony/Component/Lock/Store/RedisStore.php
@@ -0,0 +1,156 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Store;
+
+use Symfony\Component\Lock\Exception\InvalidArgumentException;
+use Symfony\Component\Lock\Exception\LockConflictedException;
+use Symfony\Component\Lock\Key;
+use Symfony\Component\Lock\StoreInterface;
+
+/**
+ * RedisStore is a StoreInterface implementation using Redis as store engine.
+ *
+ * @author Jérémy Derussé
+ */
+class RedisStore implements StoreInterface
+{
+ private $redis;
+ private $initialTtl;
+
+ /**
+ * @param \Redis|\RedisArray|\RedisCluster|\Predis\Client $redisClient
+ * @param float $initialTtl the expiration delay of locks in seconds
+ */
+ public function __construct($redisClient, $initialTtl = 300.0)
+ {
+ if (!$redisClient instanceof \Redis && !$redisClient instanceof \RedisArray && !$redisClient instanceof \RedisCluster && !$redisClient instanceof \Predis\Client) {
+ throw new InvalidArgumentException(sprintf('%s() expects parameter 1 to be Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, is_object($redisClient) ? get_class($redisClient) : gettype($redisClient)));
+ }
+
+ if ($initialTtl <= 0) {
+ throw new InvalidArgumentException(sprintf('%s() expects a strictly positive TTL. Got %d.', __METHOD__, $initialTtl));
+ }
+
+ $this->redis = $redisClient;
+ $this->initialTtl = $initialTtl;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(Key $key)
+ {
+ $script = '
+ if redis.call("GET", KEYS[1]) == ARGV[1] then
+ return redis.call("PEXPIRE", KEYS[1], ARGV[2])
+ else
+ return redis.call("set", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
+ end
+ ';
+
+ $expire = (int) ceil($this->initialTtl * 1000);
+ if (!$this->evaluate($script, (string) $key, array($this->getToken($key), $expire))) {
+ throw new LockConflictedException();
+ }
+ }
+
+ public function waitAndSave(Key $key)
+ {
+ throw new InvalidArgumentException(sprintf('The store "%s" does not supports blocking locks.', get_class($this)));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function putOffExpiration(Key $key, $ttl)
+ {
+ $script = '
+ if redis.call("GET", KEYS[1]) == ARGV[1] then
+ return redis.call("PEXPIRE", KEYS[1], ARGV[2])
+ else
+ return 0
+ end
+ ';
+
+ $expire = (int) ceil($ttl * 1000);
+ if (!$this->evaluate($script, (string) $key, array($this->getToken($key), $expire))) {
+ throw new LockConflictedException();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete(Key $key)
+ {
+ $script = '
+ if redis.call("GET", KEYS[1]) == ARGV[1] then
+ return redis.call("DEL", KEYS[1])
+ else
+ return 0
+ end
+ ';
+
+ $this->evaluate($script, (string) $key, array($this->getToken($key)));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function exists(Key $key)
+ {
+ return $this->redis->get((string) $key) === $this->getToken($key);
+ }
+
+ /**
+ * Evaluates a script in the corresponding redis client.
+ *
+ * @param string $script
+ * @param string $resource
+ * @param array $args
+ *
+ * @return mixed
+ */
+ private function evaluate($script, $resource, array $args)
+ {
+ if ($this->redis instanceof \Redis || $this->redis instanceof \RedisCluster) {
+ return $this->redis->eval($script, array_merge(array($resource), $args), 1);
+ }
+
+ if ($this->redis instanceof \RedisArray) {
+ return $this->redis->_instance($this->redis->_target($resource))->eval($script, array_merge(array($resource), $args), 1);
+ }
+
+ if ($this->redis instanceof \Predis\Client) {
+ return call_user_func_array(array($this->redis, 'eval'), array_merge(array($script, 1, $resource), $args));
+ }
+
+ throw new InvalidArgumentException(sprintf('%s() expects been initialized with a Redis, RedisArray, RedisCluster or Predis\Client, %s given', __METHOD__, is_object($this->redis) ? get_class($this->redis) : gettype($this->redis)));
+ }
+
+ /**
+ * Retrieves an unique token for the given key.
+ *
+ * @param Key $key
+ *
+ * @return string
+ */
+ private function getToken(Key $key)
+ {
+ if (!$key->hasState(__CLASS__)) {
+ $token = base64_encode(random_bytes(32));
+ $key->setState(__CLASS__, $token);
+ }
+
+ return $key->getState(__CLASS__);
+ }
+}
diff --git a/src/Symfony/Component/Lock/Store/RetryTillSaveStore.php b/src/Symfony/Component/Lock/Store/RetryTillSaveStore.php
new file mode 100644
index 0000000000..dfc3b26668
--- /dev/null
+++ b/src/Symfony/Component/Lock/Store/RetryTillSaveStore.php
@@ -0,0 +1,102 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Store;
+
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerAwareTrait;
+use Psr\Log\NullLogger;
+use Symfony\Component\Lock\Exception\LockConflictedException;
+use Symfony\Component\Lock\Key;
+use Symfony\Component\Lock\StoreInterface;
+
+/**
+ * RetryTillSaveStore is a StoreInterface implementation which decorate a non blocking StoreInterface to provide a
+ * blocking storage.
+ *
+ * @author Jérémy Derussé
+ */
+class RetryTillSaveStore implements StoreInterface, LoggerAwareInterface
+{
+ use LoggerAwareTrait;
+
+ private $decorated;
+ private $retrySleep;
+ private $retryCount;
+
+ /**
+ * @param StoreInterface $decorated The decorated StoreInterface
+ * @param int $retrySleep Duration in ms between 2 retry
+ * @param int $retryCount Maximum amount of retry
+ */
+ public function __construct(StoreInterface $decorated, $retrySleep = 100, $retryCount = PHP_INT_MAX)
+ {
+ $this->decorated = $decorated;
+ $this->retrySleep = $retrySleep;
+ $this->retryCount = $retryCount;
+
+ $this->logger = new NullLogger();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(Key $key)
+ {
+ $this->decorated->save($key);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function waitAndSave(Key $key)
+ {
+ $retry = 0;
+ $sleepRandomness = (int) ($this->retrySleep / 10);
+ do {
+ try {
+ $this->decorated->save($key);
+
+ return;
+ } catch (LockConflictedException $e) {
+ usleep(($this->retrySleep + random_int(-$sleepRandomness, $sleepRandomness)) * 1000);
+ }
+ } while (++$retry < $this->retryCount);
+
+ $this->logger->warning('Failed to store the "{resource}" lock. Abort after {retry} retry.', array('resource' => $key, 'retry' => $retry));
+
+ throw new LockConflictedException();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function putOffExpiration(Key $key, $ttl)
+ {
+ $this->decorated->putOffExpiration($key, $ttl);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete(Key $key)
+ {
+ $this->decorated->delete($key);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function exists(Key $key)
+ {
+ return $this->decorated->exists($key);
+ }
+}
diff --git a/src/Symfony/Component/Lock/Store/SemaphoreStore.php b/src/Symfony/Component/Lock/Store/SemaphoreStore.php
new file mode 100644
index 0000000000..641f307db9
--- /dev/null
+++ b/src/Symfony/Component/Lock/Store/SemaphoreStore.php
@@ -0,0 +1,103 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Store;
+
+use Symfony\Component\Lock\Exception\InvalidArgumentException;
+use Symfony\Component\Lock\Exception\LockConflictedException;
+use Symfony\Component\Lock\Exception\NotSupportedException;
+use Symfony\Component\Lock\Key;
+use Symfony\Component\Lock\StoreInterface;
+
+/**
+ * SemaphoreStore is a StoreInterface implementation using Semaphore as store engine.
+ *
+ * @author Jérémy Derussé
+ */
+class SemaphoreStore implements StoreInterface
+{
+ public static function isSupported()
+ {
+ return extension_loaded('sysvsem');
+ }
+
+ public function __construct()
+ {
+ if (!static::isSupported()) {
+ throw new InvalidArgumentException('Semaphore extension (sysvsem) is required');
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(Key $key)
+ {
+ $this->lock($key, false);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function waitAndSave(Key $key)
+ {
+ $this->lock($key, true);
+ }
+
+ private function lock(Key $key, $blocking)
+ {
+ if ($key->hasState(__CLASS__)) {
+ return;
+ }
+
+ $resource = sem_get(crc32($key));
+ $acquired = sem_acquire($resource, !$blocking);
+
+ if (!$acquired) {
+ throw new LockConflictedException();
+ }
+
+ $key->setState(__CLASS__, $resource);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete(Key $key)
+ {
+ // The lock is maybe not acquired.
+ if (!$key->hasState(__CLASS__)) {
+ return;
+ }
+
+ $resource = $key->getState(__CLASS__);
+
+ sem_release($resource);
+
+ $key->removeState(__CLASS__);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function putOffExpiration(Key $key, $ttl)
+ {
+ // do nothing, the flock locks forever.
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function exists(Key $key)
+ {
+ return $key->hasState(__CLASS__);
+ }
+}
diff --git a/src/Symfony/Component/Lock/StoreInterface.php b/src/Symfony/Component/Lock/StoreInterface.php
new file mode 100644
index 0000000000..428786b4c8
--- /dev/null
+++ b/src/Symfony/Component/Lock/StoreInterface.php
@@ -0,0 +1,73 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock;
+
+use Symfony\Component\Lock\Exception\LockConflictedException;
+use Symfony\Component\Lock\Exception\NotSupportedException;
+
+/**
+ * StoreInterface defines an interface to manipulate a lock store.
+ *
+ * @author Jérémy Derussé
+ */
+interface StoreInterface
+{
+ /**
+ * Stores the resource if it's not locked by someone else.
+ *
+ * @param Key $key key to lock
+ *
+ * @throws LockConflictedException
+ */
+ public function save(Key $key);
+
+ /**
+ * Waits a key becomes free, then stores the resource.
+ *
+ * If the store does not support this feature it should throw a NotSupportedException.
+ *
+ * @param Key $key key to lock
+ *
+ * @throws LockConflictedException
+ * @throws NotSupportedException
+ */
+ public function waitAndSave(Key $key);
+
+ /**
+ * Extends the ttl of a resource.
+ *
+ * If the store does not support this feature it should throw a NotSupportedException.
+ *
+ * @param Key $key key to lock
+ * @param float $ttl amount of second to keep the lock in the store
+ *
+ * @throws LockConflictedException
+ * @throws NotSupportedException
+ */
+ public function putOffExpiration(Key $key, $ttl);
+
+ /**
+ * Removes a resource from the storage.
+ *
+ * @param Key $key key to remove
+ */
+ public function delete(Key $key);
+
+ /**
+ * Returns whether or not the resource exists in the storage.
+ *
+ * @param Key $key key to remove
+ *
+ * @return bool
+ */
+ public function exists(Key $key);
+}
diff --git a/src/Symfony/Component/Lock/Strategy/ConsensusStrategy.php b/src/Symfony/Component/Lock/Strategy/ConsensusStrategy.php
new file mode 100644
index 0000000000..047820a409
--- /dev/null
+++ b/src/Symfony/Component/Lock/Strategy/ConsensusStrategy.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Strategy;
+
+/**
+ * ConsensusStrategy is a StrategyInterface implementation where strictly more than 50% items should be successful.
+ *
+ * @author Jérémy Derussé
+ */
+class ConsensusStrategy implements StrategyInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function isMet($numberOfSuccess, $numberOfItems)
+ {
+ return $numberOfSuccess > ($numberOfItems / 2);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function canBeMet($numberOfFailure, $numberOfItems)
+ {
+ return $numberOfFailure < ($numberOfItems / 2);
+ }
+}
diff --git a/src/Symfony/Component/Lock/Strategy/StrategyInterface.php b/src/Symfony/Component/Lock/Strategy/StrategyInterface.php
new file mode 100644
index 0000000000..beaa7280a2
--- /dev/null
+++ b/src/Symfony/Component/Lock/Strategy/StrategyInterface.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Strategy;
+
+/**
+ * StrategyInterface defines an interface to indicate when a quorum is met and can be met.
+ *
+ * @author Jérémy Derussé
+ */
+interface StrategyInterface
+{
+ /**
+ * Returns whether or not the quorum is met.
+ *
+ * @param int $numberOfSuccess
+ * @param int $numberOfItems
+ *
+ * @return bool
+ */
+ public function isMet($numberOfSuccess, $numberOfItems);
+
+ /**
+ * Returns whether or not the quorum *could* be met.
+ *
+ * This method does not mean the quorum *would* be met for sure, but can be useful to stop a process early when you
+ * known there is no chance to meet the quorum.
+ *
+ * @param int $numberOfFailure
+ * @param int $numberOfItems
+ *
+ * @return bool
+ */
+ public function canBeMet($numberOfFailure, $numberOfItems);
+}
diff --git a/src/Symfony/Component/Lock/Strategy/UnanimousStrategy.php b/src/Symfony/Component/Lock/Strategy/UnanimousStrategy.php
new file mode 100644
index 0000000000..12d57e526e
--- /dev/null
+++ b/src/Symfony/Component/Lock/Strategy/UnanimousStrategy.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Strategy;
+
+/**
+ * UnanimousStrategy is a StrategyInterface implementation where 100% of elements should be successful.
+ *
+ * @author Jérémy Derussé
+ */
+class UnanimousStrategy implements StrategyInterface
+{
+ /**
+ * {@inheritdoc}
+ */
+ public function isMet($numberOfSuccess, $numberOfItems)
+ {
+ return $numberOfSuccess === $numberOfItems;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function canBeMet($numberOfFailure, $numberOfItems)
+ {
+ return $numberOfFailure === 0;
+ }
+}
diff --git a/src/Symfony/Component/Lock/Tests/FactoryTest.php b/src/Symfony/Component/Lock/Tests/FactoryTest.php
new file mode 100644
index 0000000000..d67949098c
--- /dev/null
+++ b/src/Symfony/Component/Lock/Tests/FactoryTest.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Lock\Factory;
+use Symfony\Component\Lock\LockInterface;
+use Symfony\Component\Lock\StoreInterface;
+
+/**
+ * @author Jérémy Derussé
+ */
+class FactoryTest extends TestCase
+{
+ public function testCreateLock()
+ {
+ $store = $this->getMockBuilder(StoreInterface::class)->getMock();
+ $logger = $this->getMockBuilder(LoggerInterface::class)->getMock();
+ $factory = new Factory($store);
+ $factory->setLogger($logger);
+
+ $lock = $factory->createLock('foo');
+
+ $this->assertInstanceOf(LockInterface::class, $lock);
+ }
+}
diff --git a/src/Symfony/Component/Lock/Tests/LockTest.php b/src/Symfony/Component/Lock/Tests/LockTest.php
new file mode 100644
index 0000000000..58dbdc5820
--- /dev/null
+++ b/src/Symfony/Component/Lock/Tests/LockTest.php
@@ -0,0 +1,156 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Tests;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Lock\Exception\LockConflictedException;
+use Symfony\Component\Lock\Key;
+use Symfony\Component\Lock\Lock;
+use Symfony\Component\Lock\StoreInterface;
+
+/**
+ * @author Jérémy Derussé
+ */
+class LockTest extends TestCase
+{
+ public function testAcquireNoBlocking()
+ {
+ $key = new Key(uniqid(__METHOD__, true));
+ $store = $this->getMockBuilder(StoreInterface::class)->getMock();
+ $lock = new Lock($key, $store);
+
+ $store
+ ->expects($this->once())
+ ->method('save');
+
+ $this->assertTrue($lock->acquire(false));
+ }
+
+ public function testAcquireReturnsFalse()
+ {
+ $key = new Key(uniqid(__METHOD__, true));
+ $store = $this->getMockBuilder(StoreInterface::class)->getMock();
+ $lock = new Lock($key, $store);
+
+ $store
+ ->expects($this->once())
+ ->method('save')
+ ->willThrowException(new LockConflictedException());
+
+ $this->assertFalse($lock->acquire(false));
+ }
+
+ public function testAcquireBlocking()
+ {
+ $key = new Key(uniqid(__METHOD__, true));
+ $store = $this->getMockBuilder(StoreInterface::class)->getMock();
+ $lock = new Lock($key, $store);
+
+ $store
+ ->expects($this->never())
+ ->method('save');
+ $store
+ ->expects($this->once())
+ ->method('waitAndSave');
+
+ $this->assertTrue($lock->acquire(true));
+ }
+
+ public function testAcquireSetsTtl()
+ {
+ $key = new Key(uniqid(__METHOD__, true));
+ $store = $this->getMockBuilder(StoreInterface::class)->getMock();
+ $lock = new Lock($key, $store, 10);
+
+ $store
+ ->expects($this->once())
+ ->method('save');
+ $store
+ ->expects($this->once())
+ ->method('putOffExpiration')
+ ->with($key, 10);
+
+ $lock->acquire();
+ }
+
+ public function testRefresh()
+ {
+ $key = new Key(uniqid(__METHOD__, true));
+ $store = $this->getMockBuilder(StoreInterface::class)->getMock();
+ $lock = new Lock($key, $store, 10);
+
+ $store
+ ->expects($this->once())
+ ->method('putOffExpiration')
+ ->with($key, 10);
+
+ $lock->refresh();
+ }
+
+ public function testIsAquired()
+ {
+ $key = new Key(uniqid(__METHOD__, true));
+ $store = $this->getMockBuilder(StoreInterface::class)->getMock();
+ $lock = new Lock($key, $store, 10);
+
+ $store
+ ->expects($this->once())
+ ->method('exists')
+ ->with($key)
+ ->willReturn(true);
+
+ $this->assertTrue($lock->isAcquired());
+ }
+
+ public function testRelease()
+ {
+ $key = new Key(uniqid(__METHOD__, true));
+ $store = $this->getMockBuilder(StoreInterface::class)->getMock();
+ $lock = new Lock($key, $store, 10);
+
+ $store
+ ->expects($this->once())
+ ->method('delete')
+ ->with($key);
+
+ $store
+ ->expects($this->once())
+ ->method('exists')
+ ->with($key)
+ ->willReturn(false);
+
+ $lock->release();
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Lock\Exception\LockReleasingException
+ */
+ public function testReleaseThrowsExceptionIfNotWellDeleted()
+ {
+ $key = new Key(uniqid(__METHOD__, true));
+ $store = $this->getMockBuilder(StoreInterface::class)->getMock();
+ $lock = new Lock($key, $store, 10);
+
+ $store
+ ->expects($this->once())
+ ->method('delete')
+ ->with($key);
+
+ $store
+ ->expects($this->once())
+ ->method('exists')
+ ->with($key)
+ ->willReturn(true);
+
+ $lock->release();
+ }
+}
diff --git a/src/Symfony/Component/Lock/Tests/Store/AbstractRedisStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/AbstractRedisStoreTest.php
new file mode 100644
index 0000000000..4b9c81bd8e
--- /dev/null
+++ b/src/Symfony/Component/Lock/Tests/Store/AbstractRedisStoreTest.php
@@ -0,0 +1,45 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Tests\Store;
+
+use Symfony\Component\Lock\Store\RedisStore;
+
+/**
+ * @author Jérémy Derussé
+ */
+abstract class AbstractRedisStoreTest extends AbstractStoreTest
+{
+ use ExpiringStoreTestTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getClockDelay()
+ {
+ return 250000;
+ }
+
+ /**
+ * Return a RedisConnection.
+ *
+ * @return \Redis|\RedisArray|\RedisCluster|\Predis\Client
+ */
+ abstract protected function getRedisConnection();
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStore()
+ {
+ return new RedisStore($this->getRedisConnection());
+ }
+}
diff --git a/src/Symfony/Component/Lock/Tests/Store/AbstractStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/AbstractStoreTest.php
new file mode 100644
index 0000000000..c0d758744c
--- /dev/null
+++ b/src/Symfony/Component/Lock/Tests/Store/AbstractStoreTest.php
@@ -0,0 +1,112 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Tests\Store;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Lock\Exception\LockConflictedException;
+use Symfony\Component\Lock\Key;
+use Symfony\Component\Lock\StoreInterface;
+
+/**
+ * @author Jérémy Derussé
+ */
+abstract class AbstractStoreTest extends TestCase
+{
+ /**
+ * @return StoreInterface;
+ */
+ abstract protected function getStore();
+
+ public function testSave()
+ {
+ $store = $this->getStore();
+
+ $key = new Key(uniqid(__METHOD__, true));
+
+ $this->assertFalse($store->exists($key));
+ $store->save($key);
+ $this->assertTrue($store->exists($key));
+ $store->delete($key);
+ $this->assertFalse($store->exists($key));
+ }
+
+ public function testSaveWithDifferentResources()
+ {
+ $store = $this->getStore();
+
+ $key1 = new Key(uniqid(__METHOD__, true));
+ $key2 = new Key(uniqid(__METHOD__, true));
+
+ $store->save($key1);
+ $this->assertTrue($store->exists($key1));
+ $this->assertFalse($store->exists($key2));
+ $store->save($key2);
+
+ $this->assertTrue($store->exists($key1));
+ $this->assertTrue($store->exists($key2));
+
+ $store->delete($key1);
+ $this->assertFalse($store->exists($key1));
+ $store->delete($key2);
+ $this->assertFalse($store->exists($key2));
+ }
+
+ public function testSaveWithDifferentKeysOnSameResources()
+ {
+ $store = $this->getStore();
+
+ $resource = uniqid(__METHOD__, true);
+ $key1 = new Key($resource);
+ $key2 = new Key($resource);
+
+ $store->save($key1);
+ $this->assertTrue($store->exists($key1));
+ $this->assertFalse($store->exists($key2));
+
+ try {
+ $store->save($key2);
+ throw new \Exception('The store shouldn\'t save the second key');
+ } catch (LockConflictedException $e) {
+ }
+
+ // The failure of previous attempt should not impact the state of current locks
+ $this->assertTrue($store->exists($key1));
+ $this->assertFalse($store->exists($key2));
+
+ $store->delete($key1);
+ $this->assertFalse($store->exists($key1));
+ $this->assertFalse($store->exists($key2));
+
+ $store->save($key2);
+ $this->assertFalse($store->exists($key1));
+ $this->assertTrue($store->exists($key2));
+
+ $store->delete($key2);
+ $this->assertFalse($store->exists($key1));
+ $this->assertFalse($store->exists($key2));
+ }
+
+ public function testSaveTwice()
+ {
+ $store = $this->getStore();
+
+ $resource = uniqid(__METHOD__, true);
+ $key = new Key($resource);
+
+ $store->save($key);
+ $store->save($key);
+ // just asserts it don't throw an exception
+ $this->addToAssertionCount(1);
+
+ $store->delete($key);
+ }
+}
diff --git a/src/Symfony/Component/Lock/Tests/Store/BlockingStoreTestTrait.php b/src/Symfony/Component/Lock/Tests/Store/BlockingStoreTestTrait.php
new file mode 100644
index 0000000000..34cc768155
--- /dev/null
+++ b/src/Symfony/Component/Lock/Tests/Store/BlockingStoreTestTrait.php
@@ -0,0 +1,75 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Tests\Store;
+
+use Symfony\Component\Lock\Exception\LockConflictedException;
+use Symfony\Component\Lock\Key;
+use Symfony\Component\Lock\StoreInterface;
+
+/**
+ * @author Jérémy Derussé
+ */
+trait BlockingStoreTestTrait
+{
+ /**
+ * @see AbstractStoreTest::getStore()
+ */
+ abstract protected function getStore();
+
+ /**
+ * Tests blocking locks thanks to pcntl.
+ *
+ * This test is time sensible: the $clockDelay could be adjust.
+ *
+ * @requires extension pcntl
+ */
+ public function testBlockingLocks()
+ {
+ // Amount a microsecond used to order async actions
+ $clockDelay = 50000;
+
+ /** @var StoreInterface $store */
+ $store = $this->getStore();
+ $key = new Key(uniqid(__METHOD__, true));
+
+ if ($childPID1 = pcntl_fork()) {
+ // give time to fork to start
+ usleep(2 * $clockDelay);
+
+ try {
+ // This call should failed given the lock should already by acquired by the child #1
+ $store->save($key);
+ $this->fail('The store saves a locked key.');
+ } catch (LockConflictedException $e) {
+ }
+
+ // This call should be blocked by the child #1
+ $store->waitAndSave($key);
+ $this->assertTrue($store->exists($key));
+ $store->delete($key);
+
+ // Now, assert the child process worked well
+ pcntl_waitpid($childPID1, $status1);
+ $this->assertSame(0, pcntl_wexitstatus($status1), 'The child process couldn\'t lock the resource');
+ } else {
+ try {
+ $store->save($key);
+ // Wait 3 ClockDelay to let parent process to finish
+ usleep(3 * $clockDelay);
+ $store->delete($key);
+ exit(0);
+ } catch (\Exception $e) {
+ exit(1);
+ }
+ }
+ }
+}
diff --git a/src/Symfony/Component/Lock/Tests/Store/CombinedStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/CombinedStoreTest.php
new file mode 100644
index 0000000000..debe06183d
--- /dev/null
+++ b/src/Symfony/Component/Lock/Tests/Store/CombinedStoreTest.php
@@ -0,0 +1,356 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Tests\Store;
+
+use Symfony\Component\Lock\Exception\LockConflictedException;
+use Symfony\Component\Lock\Key;
+use Symfony\Component\Lock\Strategy\UnanimousStrategy;
+use Symfony\Component\Lock\Strategy\StrategyInterface;
+use Symfony\Component\Lock\Store\CombinedStore;
+use Symfony\Component\Lock\Store\RedisStore;
+use Symfony\Component\Lock\StoreInterface;
+
+/**
+ * @author Jérémy Derussé
+ */
+class CombinedStoreTest extends AbstractStoreTest
+{
+ use ExpiringStoreTestTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getClockDelay()
+ {
+ return 250000;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStore()
+ {
+ $redis = new \Predis\Client('tcp://'.getenv('REDIS_HOST').':6379');
+ try {
+ $redis->connect();
+ } catch (\Exception $e) {
+ self::markTestSkipped($e->getMessage());
+ }
+
+ return new CombinedStore(array(new RedisStore($redis)), new UnanimousStrategy());
+ }
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject */
+ private $strategy;
+ /** @var \PHPUnit_Framework_MockObject_MockObject */
+ private $store1;
+ /** @var \PHPUnit_Framework_MockObject_MockObject */
+ private $store2;
+ /** @var CombinedStore */
+ private $store;
+
+ public function setup()
+ {
+ $this->strategy = $this->getMockBuilder(StrategyInterface::class)->getMock();
+ $this->store1 = $this->getMockBuilder(StoreInterface::class)->getMock();
+ $this->store2 = $this->getMockBuilder(StoreInterface::class)->getMock();
+
+ $this->store = new CombinedStore(array($this->store1, $this->store2), $this->strategy);
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Lock\Exception\LockConflictedException
+ */
+ public function testSaveThrowsExceptionOnFailure()
+ {
+ $key = new Key(uniqid(__METHOD__, true));
+
+ $this->store1
+ ->expects($this->once())
+ ->method('save')
+ ->with($key)
+ ->willThrowException(new LockConflictedException());
+ $this->store2
+ ->expects($this->once())
+ ->method('save')
+ ->with($key)
+ ->willThrowException(new LockConflictedException());
+
+ $this->strategy
+ ->expects($this->any())
+ ->method('canBeMet')
+ ->willReturn(true);
+ $this->strategy
+ ->expects($this->any())
+ ->method('isMet')
+ ->willReturn(false);
+
+ $this->store->save($key);
+ }
+
+ public function testSaveCleanupOnFailure()
+ {
+ $key = new Key(uniqid(__METHOD__, true));
+
+ $this->store1
+ ->expects($this->once())
+ ->method('save')
+ ->with($key)
+ ->willThrowException(new LockConflictedException());
+ $this->store2
+ ->expects($this->once())
+ ->method('save')
+ ->with($key)
+ ->willThrowException(new LockConflictedException());
+
+ $this->store1
+ ->expects($this->once())
+ ->method('delete');
+ $this->store2
+ ->expects($this->once())
+ ->method('delete');
+
+ $this->strategy
+ ->expects($this->any())
+ ->method('canBeMet')
+ ->willReturn(true);
+ $this->strategy
+ ->expects($this->any())
+ ->method('isMet')
+ ->willReturn(false);
+
+ try {
+ $this->store->save($key);
+ } catch (LockConflictedException $e) {
+ // Catch the exception given this is not what we want to assert in this tests
+ }
+ }
+
+ public function testSaveAbortWhenStrategyCantBeMet()
+ {
+ $key = new Key(uniqid(__METHOD__, true));
+
+ $this->store1
+ ->expects($this->once())
+ ->method('save')
+ ->with($key)
+ ->willThrowException(new LockConflictedException());
+ $this->store2
+ ->expects($this->never())
+ ->method('save');
+
+ $this->strategy
+ ->expects($this->once())
+ ->method('canBeMet')
+ ->willReturn(false);
+ $this->strategy
+ ->expects($this->any())
+ ->method('isMet')
+ ->willReturn(false);
+
+ try {
+ $this->store->save($key);
+ } catch (LockConflictedException $e) {
+ // Catch the exception given this is not what we want to assert in this tests
+ }
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Lock\Exception\LockConflictedException
+ */
+ public function testputOffExpirationThrowsExceptionOnFailure()
+ {
+ $key = new Key(uniqid(__METHOD__, true));
+ $ttl = random_int(1, 10);
+
+ $this->store1
+ ->expects($this->once())
+ ->method('putOffExpiration')
+ ->with($key, $ttl)
+ ->willThrowException(new LockConflictedException());
+ $this->store2
+ ->expects($this->once())
+ ->method('putOffExpiration')
+ ->with($key, $ttl)
+ ->willThrowException(new LockConflictedException());
+
+ $this->strategy
+ ->expects($this->any())
+ ->method('canBeMet')
+ ->willReturn(true);
+ $this->strategy
+ ->expects($this->any())
+ ->method('isMet')
+ ->willReturn(false);
+
+ $this->store->putOffExpiration($key, $ttl);
+ }
+
+ public function testputOffExpirationCleanupOnFailure()
+ {
+ $key = new Key(uniqid(__METHOD__, true));
+ $ttl = random_int(1, 10);
+
+ $this->store1
+ ->expects($this->once())
+ ->method('putOffExpiration')
+ ->with($key, $ttl)
+ ->willThrowException(new LockConflictedException());
+ $this->store2
+ ->expects($this->once())
+ ->method('putOffExpiration')
+ ->with($key, $ttl)
+ ->willThrowException(new LockConflictedException());
+
+ $this->store1
+ ->expects($this->once())
+ ->method('delete');
+ $this->store2
+ ->expects($this->once())
+ ->method('delete');
+
+ $this->strategy
+ ->expects($this->any())
+ ->method('canBeMet')
+ ->willReturn(true);
+ $this->strategy
+ ->expects($this->any())
+ ->method('isMet')
+ ->willReturn(false);
+
+ try {
+ $this->store->putOffExpiration($key, $ttl);
+ } catch (LockConflictedException $e) {
+ // Catch the exception given this is not what we want to assert in this tests
+ }
+ }
+
+ public function testputOffExpirationAbortWhenStrategyCantBeMet()
+ {
+ $key = new Key(uniqid(__METHOD__, true));
+ $ttl = random_int(1, 10);
+
+ $this->store1
+ ->expects($this->once())
+ ->method('putOffExpiration')
+ ->with($key, $ttl)
+ ->willThrowException(new LockConflictedException());
+ $this->store2
+ ->expects($this->never())
+ ->method('putOffExpiration');
+
+ $this->strategy
+ ->expects($this->once())
+ ->method('canBeMet')
+ ->willReturn(false);
+ $this->strategy
+ ->expects($this->any())
+ ->method('isMet')
+ ->willReturn(false);
+
+ try {
+ $this->store->putOffExpiration($key, $ttl);
+ } catch (LockConflictedException $e) {
+ // Catch the exception given this is not what we want to assert in this tests
+ }
+ }
+
+ public function testPutOffExpirationIgnoreNonExpiringStorage()
+ {
+ $store1 = $this->getMockBuilder(StoreInterface::class)->getMock();
+ $store2 = $this->getMockBuilder(StoreInterface::class)->getMock();
+
+ $store = new CombinedStore(array($store1, $store2), $this->strategy);
+
+ $key = new Key(uniqid(__METHOD__, true));
+ $ttl = random_int(1, 10);
+
+ $this->strategy
+ ->expects($this->any())
+ ->method('canBeMet')
+ ->willReturn(true);
+ $this->strategy
+ ->expects($this->once())
+ ->method('isMet')
+ ->with(2, 2)
+ ->willReturn(true);
+
+ $store->putOffExpiration($key, $ttl);
+ }
+
+ public function testExistsDontAskToEveryBody()
+ {
+ $key = new Key(uniqid(__METHOD__, true));
+
+ $this->store1
+ ->expects($this->any())
+ ->method('exists')
+ ->with($key)
+ ->willReturn(false);
+ $this->store2
+ ->expects($this->never())
+ ->method('exists');
+
+ $this->strategy
+ ->expects($this->any())
+ ->method('canBeMet')
+ ->willReturn(true);
+ $this->strategy
+ ->expects($this->once())
+ ->method('isMet')
+ ->willReturn(true);
+
+ $this->assertTrue($this->store->exists($key));
+ }
+
+ public function testExistsAbortWhenStrategyCantBeMet()
+ {
+ $key = new Key(uniqid(__METHOD__, true));
+
+ $this->store1
+ ->expects($this->any())
+ ->method('exists')
+ ->with($key)
+ ->willReturn(false);
+ $this->store2
+ ->expects($this->never())
+ ->method('exists');
+
+ $this->strategy
+ ->expects($this->once())
+ ->method('canBeMet')
+ ->willReturn(false);
+ $this->strategy
+ ->expects($this->once())
+ ->method('isMet')
+ ->willReturn(false);
+
+ $this->assertFalse($this->store->exists($key));
+ }
+
+ public function testDeleteDontStopOnFailure()
+ {
+ $key = new Key(uniqid(__METHOD__, true));
+
+ $this->store1
+ ->expects($this->once())
+ ->method('delete')
+ ->with($key)
+ ->willThrowException(new \Exception());
+ $this->store2
+ ->expects($this->once())
+ ->method('delete')
+ ->with($key);
+
+ $this->store->delete($key);
+ }
+}
diff --git a/src/Symfony/Component/Lock/Tests/Store/ExpiringStoreTestTrait.php b/src/Symfony/Component/Lock/Tests/Store/ExpiringStoreTestTrait.php
new file mode 100644
index 0000000000..0280aa6173
--- /dev/null
+++ b/src/Symfony/Component/Lock/Tests/Store/ExpiringStoreTestTrait.php
@@ -0,0 +1,78 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Tests\Store;
+
+use Symfony\Component\Lock\Key;
+use Symfony\Component\Lock\StoreInterface;
+
+/**
+ * @author Jérémy Derussé
+ */
+trait ExpiringStoreTestTrait
+{
+ /**
+ * Amount a microsecond used to order async actions.
+ *
+ * @return int
+ */
+ abstract protected function getClockDelay();
+
+ /**
+ * @see AbstractStoreTest::getStore()
+ */
+ abstract protected function getStore();
+
+ /**
+ * Tests the store automatically delete the key when it expire.
+ *
+ * This test is time sensible: the $clockDelay could be adjust.
+ */
+ public function testExpiration()
+ {
+ $key = new Key(uniqid(__METHOD__, true));
+ $clockDelay = $this->getClockDelay();
+
+ /** @var StoreInterface $store */
+ $store = $this->getStore();
+
+ $store->save($key);
+ $store->putOffExpiration($key, $clockDelay / 1000000);
+ $this->assertTrue($store->exists($key));
+
+ usleep(2 * $clockDelay);
+ $this->assertFalse($store->exists($key));
+ }
+
+ /**
+ * Tests the refresh can push the limits to the expiration.
+ *
+ * This test is time sensible: the $clockDelay could be adjust.
+ */
+ public function testRefreshLock()
+ {
+ // Amount a microsecond used to order async actions
+ $clockDelay = $this->getClockDelay();
+
+ // Amount a microsecond used to order async actions
+ $key = new Key(uniqid(__METHOD__, true));
+
+ /** @var StoreInterface $store */
+ $store = $this->getStore();
+
+ $store->save($key);
+ $store->putOffExpiration($key, 1.0 * $clockDelay / 1000000);
+ $this->assertTrue($store->exists($key));
+
+ usleep(2.1 * $clockDelay);
+ $this->assertFalse($store->exists($key));
+ }
+}
diff --git a/src/Symfony/Component/Lock/Tests/Store/FlockStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/FlockStoreTest.php
new file mode 100644
index 0000000000..17c59c440a
--- /dev/null
+++ b/src/Symfony/Component/Lock/Tests/Store/FlockStoreTest.php
@@ -0,0 +1,77 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Tests\Store;
+
+use Symfony\Component\Lock\Key;
+use Symfony\Component\Lock\Store\FlockStore;
+
+/**
+ * @author Jérémy Derussé
+ */
+class FlockStoreTest extends AbstractStoreTest
+{
+ use BlockingStoreTestTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getStore()
+ {
+ return new FlockStore(sys_get_temp_dir());
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Lock\Exception\InvalidArgumentException
+ * @expectedExceptionMessage The directory "/a/b/c/d/e" is not writable.
+ */
+ public function testConstructWhenRepositoryDoesNotExist()
+ {
+ if (!getenv('USER') || 'root' === getenv('USER')) {
+ $this->markTestSkipped('This test will fail if run under superuser');
+ }
+
+ new FlockStore('/a/b/c/d/e');
+ }
+
+ /**
+ * @expectedException \Symfony\Component\Lock\Exception\InvalidArgumentException
+ * @expectedExceptionMessage The directory "/" is not writable.
+ */
+ public function testConstructWhenRepositoryIsNotWriteable()
+ {
+ if (!getenv('USER') || 'root' === getenv('USER')) {
+ $this->markTestSkipped('This test will fail if run under superuser');
+ }
+
+ new FlockStore('/');
+ }
+
+ public function testSaveSanitizeName()
+ {
+ $store = $this->getStore();
+
+ $key = new Key('');
+
+ $file = sprintf(
+ '%s/sf.-php-echo-hello-word-.4b3d9d0d27ddef3a78a64685dda3a963e478659a9e5240feaf7b4173a8f28d5f.lock',
+ sys_get_temp_dir()
+ );
+ // ensure the file does not exist before the store
+ @unlink($file);
+
+ $store->save($key);
+
+ $this->assertFileExists($file);
+
+ $store->delete($key);
+ }
+}
diff --git a/src/Symfony/Component/Lock/Tests/Store/MemcachedStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/MemcachedStoreTest.php
new file mode 100644
index 0000000000..72615baae2
--- /dev/null
+++ b/src/Symfony/Component/Lock/Tests/Store/MemcachedStoreTest.php
@@ -0,0 +1,52 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Tests\Store;
+
+use Symfony\Component\Lock\Store\MemcachedStore;
+
+/**
+ * @author Jérémy Derussé
+ *
+ * @requires extension memcached
+ */
+class MemcachedStoreTest extends AbstractStoreTest
+{
+ use ExpiringStoreTestTrait;
+
+ public static function setupBeforeClass()
+ {
+ $memcached = new \Memcached();
+ $memcached->addServer(getenv('MEMCACHED_HOST'), 11211);
+ if (false === $memcached->getStats()) {
+ self::markTestSkipped('Unable to connect to the memcache host');
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getClockDelay()
+ {
+ return 1000000;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStore()
+ {
+ $memcached = new \Memcached();
+ $memcached->addServer(getenv('MEMCACHED_HOST'), 11211);
+
+ return new MemcachedStore($memcached);
+ }
+}
diff --git a/src/Symfony/Component/Lock/Tests/Store/PredisStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/PredisStoreTest.php
new file mode 100644
index 0000000000..621affecb5
--- /dev/null
+++ b/src/Symfony/Component/Lock/Tests/Store/PredisStoreTest.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Tests\Store;
+
+/**
+ * @author Jérémy Derussé
+ */
+class PredisStoreTest extends AbstractRedisStoreTest
+{
+ public static function setupBeforeClass()
+ {
+ $redis = new \Predis\Client('tcp://'.getenv('REDIS_HOST').':6379');
+ try {
+ $redis->connect();
+ } catch (\Exception $e) {
+ self::markTestSkipped($e->getMessage());
+ }
+ }
+
+ protected function getRedisConnection()
+ {
+ $redis = new \Predis\Client('tcp://'.getenv('REDIS_HOST').':6379');
+ $redis->connect();
+
+ return $redis;
+ }
+}
diff --git a/src/Symfony/Component/Lock/Tests/Store/RedisArrayStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/RedisArrayStoreTest.php
new file mode 100644
index 0000000000..180da4618d
--- /dev/null
+++ b/src/Symfony/Component/Lock/Tests/Store/RedisArrayStoreTest.php
@@ -0,0 +1,38 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Tests\Store;
+
+/**
+ * @author Jérémy Derussé
+ *
+ * @requires extension redis
+ */
+class RedisArrayStoreTest extends AbstractRedisStoreTest
+{
+ public static function setupBeforeClass()
+ {
+ 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']);
+ }
+ }
+
+ protected function getRedisConnection()
+ {
+ $redis = new \RedisArray(array(getenv('REDIS_HOST')));
+
+ return $redis;
+ }
+}
diff --git a/src/Symfony/Component/Lock/Tests/Store/RedisStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/RedisStoreTest.php
new file mode 100644
index 0000000000..6c7d244107
--- /dev/null
+++ b/src/Symfony/Component/Lock/Tests/Store/RedisStoreTest.php
@@ -0,0 +1,36 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Tests\Store;
+
+/**
+ * @author Jérémy Derussé
+ *
+ * @requires extension redis
+ */
+class RedisStoreTest extends AbstractRedisStoreTest
+{
+ public static function setupBeforeClass()
+ {
+ if (!@((new \Redis())->connect(getenv('REDIS_HOST')))) {
+ $e = error_get_last();
+ self::markTestSkipped($e['message']);
+ }
+ }
+
+ protected function getRedisConnection()
+ {
+ $redis = new \Redis();
+ $redis->connect(getenv('REDIS_HOST'));
+
+ return $redis;
+ }
+}
diff --git a/src/Symfony/Component/Lock/Tests/Store/RetryTillSaveStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/RetryTillSaveStoreTest.php
new file mode 100644
index 0000000000..febd48f279
--- /dev/null
+++ b/src/Symfony/Component/Lock/Tests/Store/RetryTillSaveStoreTest.php
@@ -0,0 +1,35 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Tests\Store;
+
+use Symfony\Component\Lock\Store\RedisStore;
+use Symfony\Component\Lock\Store\RetryTillSaveStore;
+
+/**
+ * @author Jérémy Derussé
+ */
+class RetryTillSaveStoreTest extends AbstractStoreTest
+{
+ use BlockingStoreTestTrait;
+
+ public function getStore()
+ {
+ $redis = new \Predis\Client('tcp://'.getenv('REDIS_HOST').':6379');
+ try {
+ $redis->connect();
+ } catch (\Exception $e) {
+ self::markTestSkipped($e->getMessage());
+ }
+
+ return new RetryTillSaveStore(new RedisStore($redis), 100, 100);
+ }
+}
diff --git a/src/Symfony/Component/Lock/Tests/Store/SemaphoreStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/SemaphoreStoreTest.php
new file mode 100644
index 0000000000..eeb95d5810
--- /dev/null
+++ b/src/Symfony/Component/Lock/Tests/Store/SemaphoreStoreTest.php
@@ -0,0 +1,32 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Tests\Store;
+
+use Symfony\Component\Lock\Store\SemaphoreStore;
+
+/**
+ * @author Jérémy Derussé
+ *
+ * @requires extension sysvsem
+ */
+class SemaphoreStoreTest extends AbstractStoreTest
+{
+ use BlockingStoreTestTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getStore()
+ {
+ return new SemaphoreStore();
+ }
+}
diff --git a/src/Symfony/Component/Lock/Tests/Strategy/ConsensusStrategyTest.php b/src/Symfony/Component/Lock/Tests/Strategy/ConsensusStrategyTest.php
new file mode 100644
index 0000000000..09215f9a94
--- /dev/null
+++ b/src/Symfony/Component/Lock/Tests/Strategy/ConsensusStrategyTest.php
@@ -0,0 +1,89 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Tests\Strategy;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Lock\Strategy\ConsensusStrategy;
+
+/**
+ * @author Jérémy Derussé
+ */
+class ConsensusStrategyTest extends TestCase
+{
+ /** @var ConsensusStrategy */
+ private $strategy;
+
+ public function setup()
+ {
+ $this->strategy = new ConsensusStrategy();
+ }
+
+ public function provideMetResults()
+ {
+ // success, failure, total, isMet
+ yield array(3, 0, 3, true);
+ yield array(2, 1, 3, true);
+ yield array(2, 0, 3, true);
+ yield array(1, 2, 3, false);
+ yield array(1, 1, 3, false);
+ yield array(1, 0, 3, false);
+ yield array(0, 3, 3, false);
+ yield array(0, 2, 3, false);
+ yield array(0, 1, 3, false);
+ yield array(0, 0, 3, false);
+
+ yield array(2, 0, 2, true);
+ yield array(1, 1, 2, false);
+ yield array(1, 0, 2, false);
+ yield array(0, 2, 2, false);
+ yield array(0, 1, 2, false);
+ yield array(0, 0, 2, false);
+ }
+
+ public function provideIndeterminate()
+ {
+ // success, failure, total, canBeMet
+ yield array(3, 0, 3, true);
+ yield array(2, 1, 3, true);
+ yield array(2, 0, 3, true);
+ yield array(1, 2, 3, false);
+ yield array(1, 1, 3, true);
+ yield array(1, 0, 3, true);
+ yield array(0, 3, 3, false);
+ yield array(0, 2, 3, false);
+ yield array(0, 1, 3, true);
+ yield array(0, 0, 3, true);
+
+ yield array(2, 0, 2, true);
+ yield array(1, 1, 2, false);
+ yield array(1, 0, 2, true);
+ yield array(0, 2, 2, false);
+ yield array(0, 1, 2, false);
+ yield array(0, 0, 2, true);
+ }
+
+ /**
+ * @dataProvider provideMetResults
+ */
+ public function testMet($success, $failure, $total, $isMet)
+ {
+ $this->assertSame($isMet, $this->strategy->isMet($success, $total));
+ }
+
+ /**
+ * @dataProvider provideIndeterminate
+ */
+ public function canBeMet($success, $failure, $total, $isMet)
+ {
+ $this->assertSame($isMet, $this->strategy->canBeMet($failure, $total));
+ }
+}
diff --git a/src/Symfony/Component/Lock/Tests/Strategy/UnanimousStrategyTest.php b/src/Symfony/Component/Lock/Tests/Strategy/UnanimousStrategyTest.php
new file mode 100644
index 0000000000..76ea68a41e
--- /dev/null
+++ b/src/Symfony/Component/Lock/Tests/Strategy/UnanimousStrategyTest.php
@@ -0,0 +1,89 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Lock\Tests\Strategy;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Lock\Strategy\UnanimousStrategy;
+
+/**
+ * @author Jérémy Derussé
+ */
+class UnanimousStrategyTest extends TestCase
+{
+ /** @var UnanimousStrategy */
+ private $strategy;
+
+ public function setup()
+ {
+ $this->strategy = new UnanimousStrategy();
+ }
+
+ public function provideMetResults()
+ {
+ // success, failure, total, isMet
+ yield array(3, 0, 3, true);
+ yield array(2, 1, 3, false);
+ yield array(2, 0, 3, false);
+ yield array(1, 2, 3, false);
+ yield array(1, 1, 3, false);
+ yield array(1, 0, 3, false);
+ yield array(0, 3, 3, false);
+ yield array(0, 2, 3, false);
+ yield array(0, 1, 3, false);
+ yield array(0, 0, 3, false);
+
+ yield array(2, 0, 2, true);
+ yield array(1, 1, 2, false);
+ yield array(1, 0, 2, false);
+ yield array(0, 2, 2, false);
+ yield array(0, 1, 2, false);
+ yield array(0, 0, 2, false);
+ }
+
+ public function provideIndeterminate()
+ {
+ // success, failure, total, canBeMet
+ yield array(3, 0, 3, true);
+ yield array(2, 1, 3, false);
+ yield array(2, 0, 3, true);
+ yield array(1, 2, 3, false);
+ yield array(1, 1, 3, false);
+ yield array(1, 0, 3, true);
+ yield array(0, 3, 3, false);
+ yield array(0, 2, 3, false);
+ yield array(0, 1, 3, false);
+ yield array(0, 0, 3, true);
+
+ yield array(2, 0, 2, true);
+ yield array(1, 1, 2, false);
+ yield array(1, 0, 2, true);
+ yield array(0, 2, 2, false);
+ yield array(0, 1, 2, false);
+ yield array(0, 0, 2, true);
+ }
+
+ /**
+ * @dataProvider provideMetResults
+ */
+ public function testMet($success, $failure, $total, $isMet)
+ {
+ $this->assertSame($isMet, $this->strategy->isMet($success, $total));
+ }
+
+ /**
+ * @dataProvider provideIndeterminate
+ */
+ public function canBeMet($success, $failure, $total, $isMet)
+ {
+ $this->assertSame($isMet, $this->strategy->canBeMet($failure, $total));
+ }
+}
diff --git a/src/Symfony/Component/Lock/composer.json b/src/Symfony/Component/Lock/composer.json
new file mode 100644
index 0000000000..a4f947de4d
--- /dev/null
+++ b/src/Symfony/Component/Lock/composer.json
@@ -0,0 +1,38 @@
+{
+ "name": "symfony/lock",
+ "type": "library",
+ "description": "Symfony Lock Component",
+ "keywords": ["locking", "redlock", "mutex", "semaphore", "flock", "cas"],
+ "homepage": "https://symfony.com",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Jérémy Derussé",
+ "email": "jeremy@derusse.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "require": {
+ "php": "^7.1.3",
+ "symfony/polyfill-php70": "~1.0",
+ "psr/log": "~1.0"
+ },
+ "require-dev": {
+ "predis/predis": "~1.0"
+ },
+ "autoload": {
+ "psr-4": { "Symfony\\Component\\Lock\\": "" },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "minimum-stability": "dev",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0-dev"
+ }
+ }
+}
diff --git a/src/Symfony/Component/Lock/phpunit.xml.dist b/src/Symfony/Component/Lock/phpunit.xml.dist
new file mode 100644
index 0000000000..be3ca21576
--- /dev/null
+++ b/src/Symfony/Component/Lock/phpunit.xml.dist
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+ ./Tests/
+
+
+
+
+
+ ./
+
+ ./Tests
+ ./vendor
+
+
+
+
diff --git a/src/Symfony/Component/Process/Process.php b/src/Symfony/Component/Process/Process.php
index a709f4a61d..fb12d393ab 100644
--- a/src/Symfony/Component/Process/Process.php
+++ b/src/Symfony/Component/Process/Process.php
@@ -320,7 +320,7 @@ class Process implements \IteratorAggregate
}
if ('\\' === DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) {
$this->options['bypass_shell'] = true;
- $commandline = $this->prepareWindowsCommandLine($commandline, $envBackup);
+ $commandline = $this->prepareWindowsCommandLine($commandline, $envBackup, $env);
} elseif (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) {
// last exit code is output on the fourth pipe and caught to work around --enable-sigchild
$descriptors[3] = array('pipe', 'w');
@@ -1627,7 +1627,7 @@ class Process implements \IteratorAggregate
return true;
}
- private function prepareWindowsCommandLine($cmd, array &$envBackup)
+ private function prepareWindowsCommandLine($cmd, array &$envBackup, array &$env = null)
{
$uid = uniqid('', true);
$varCount = 0;
@@ -1640,7 +1640,7 @@ class Process implements \IteratorAggregate
[^"%!^]*+
)++
)"/x',
- function ($m) use (&$envBackup, &$varCache, &$varCount, $uid) {
+ function ($m) use (&$envBackup, &$env, &$varCache, &$varCount, $uid) {
if (isset($varCache[$m[0]])) {
return $varCache[$m[0]];
}
@@ -1652,10 +1652,15 @@ class Process implements \IteratorAggregate
}
$value = str_replace(array('!LF!', '"^!"', '"^%"', '"^^"', '""'), array("\n", '!', '%', '^', '"'), $value);
- $value = preg_replace('/(\\\\*)"/', '$1$1\\"', $value);
-
+ $value = '"'.preg_replace('/(\\\\*)"/', '$1$1\\"', $value).'"';
$var = $uid.++$varCount;
- putenv("$var=\"$value\"");
+
+ if (null === $env) {
+ putenv("$var=$value");
+ } else {
+ $env[$var] = $value;
+ }
+
$envBackup[$var] = false;
return $varCache[$m[0]] = '!'.$var.'!';
diff --git a/src/Symfony/Component/Process/Tests/ProcessTest.php b/src/Symfony/Component/Process/Tests/ProcessTest.php
index 642df9c4a3..ade11138e3 100644
--- a/src/Symfony/Component/Process/Tests/ProcessTest.php
+++ b/src/Symfony/Component/Process/Tests/ProcessTest.php
@@ -1465,6 +1465,19 @@ class ProcessTest extends TestCase
$this->assertSame($arg, $p->getOutput());
}
+ /**
+ * @dataProvider provideEscapeArgument
+ * @group legacy
+ */
+ public function testEscapeArgumentWhenInheritEnvDisabled($arg)
+ {
+ $p = new Process(array(self::$phpBin, '-r', 'echo $argv[1];', $arg), null, array('BAR' => 'BAZ'));
+ $p->inheritEnvironmentVariables(false);
+ $p->run();
+
+ $this->assertSame($arg, $p->getOutput());
+ }
+
public function provideEscapeArgument()
{
yield array('a"b%c%');
diff --git a/src/Symfony/Component/Routing/Tests/Fixtures/validpattern.xml b/src/Symfony/Component/Routing/Tests/Fixtures/validpattern.xml
index e8d07350b7..dbc72e46dd 100644
--- a/src/Symfony/Component/Routing/Tests/Fixtures/validpattern.xml
+++ b/src/Symfony/Component/Routing/Tests/Fixtures/validpattern.xml
@@ -11,14 +11,5 @@
context.getMethod() == "GET"
-
- MyBundle:Blog:show
- GET|POST|put|OpTiOnS
- hTTps
- \w+
- RouteCompiler
- context.getMethod() == "GET"
-
-
diff --git a/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php
index e8e24fde58..d24ec79a79 100644
--- a/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php
+++ b/src/Symfony/Component/Routing/Tests/Loader/XmlFileLoaderTest.php
@@ -70,7 +70,7 @@ class XmlFileLoaderTest extends TestCase
$routeCollection = $loader->load('validresource.xml');
$routes = $routeCollection->all();
- $this->assertCount(3, $routes, 'Two routes are loaded');
+ $this->assertCount(2, $routes, 'Two routes are loaded');
$this->assertContainsOnly('Symfony\Component\Routing\Route', $routes);
foreach ($routes as $route) {
diff --git a/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php b/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php
index 8b82c51d11..cf94b1af93 100644
--- a/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php
+++ b/src/Symfony/Component/VarDumper/Dumper/HtmlDumper.php
@@ -655,17 +655,17 @@ pre.sf-dump code {
border: 1px solid #ffa500;
border-radius: 3px;
}
-.sf-dump-search-hidden {
+pre.sf-dump .sf-dump-search-hidden {
display: none;
}
-.sf-dump-search-wrapper {
+pre.sf-dump .sf-dump-search-wrapper {
float: right;
font-size: 0;
white-space: nowrap;
max-width: 100%;
text-align: right;
}
-.sf-dump-search-wrapper > * {
+pre.sf-dump .sf-dump-search-wrapper > * {
vertical-align: top;
box-sizing: border-box;
height: 21px;
@@ -675,7 +675,7 @@ pre.sf-dump code {
color: #757575;
border: 1px solid #BBB;
}
-.sf-dump-search-wrapper > input.sf-dump-search-input {
+pre.sf-dump .sf-dump-search-wrapper > input.sf-dump-search-input {
padding: 3px;
height: 21px;
font-size: 12px;
@@ -685,25 +685,25 @@ pre.sf-dump code {
border-bottom-left-radius: 3px;
color: #000;
}
-.sf-dump-search-wrapper > .sf-dump-search-input-next,
-.sf-dump-search-wrapper > .sf-dump-search-input-previous {
+pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-input-next,
+pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-input-previous {
background: #F2F2F2;
outline: none;
border-left: none;
font-size: 0;
line-height: 0;
}
-.sf-dump-search-wrapper > .sf-dump-search-input-next {
+pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-input-next {
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
}
-.sf-dump-search-wrapper > .sf-dump-search-input-next > svg,
-.sf-dump-search-wrapper > .sf-dump-search-input-previous > svg {
+pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-input-next > svg,
+pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-input-previous > svg {
pointer-events: none;
width: 12px;
height: 12px;
}
-.sf-dump-search-wrapper > .sf-dump-search-count {
+pre.sf-dump .sf-dump-search-wrapper > .sf-dump-search-count {
display: inline-block;
padding: 0 5px;
margin: 0;
diff --git a/src/Symfony/Component/Yaml/Parser.php b/src/Symfony/Component/Yaml/Parser.php
index 3134d90747..6b4a574d2d 100644
--- a/src/Symfony/Component/Yaml/Parser.php
+++ b/src/Symfony/Component/Yaml/Parser.php
@@ -360,7 +360,11 @@ class Parser
foreach ($this->lines as $line) {
try {
- $parsedLine = Inline::parse($line, $flags, $this->refs);
+ if (isset($line[0]) && ('"' === $line[0] || "'" === $line[0])) {
+ $parsedLine = $line;
+ } else {
+ $parsedLine = Inline::parse($line, $flags, $this->refs);
+ }
if (!is_string($parsedLine)) {
$parseError = true;
diff --git a/src/Symfony/Component/Yaml/Tests/ParserTest.php b/src/Symfony/Component/Yaml/Tests/ParserTest.php
index f64d7589d8..c8f4a7d40e 100644
--- a/src/Symfony/Component/Yaml/Tests/ParserTest.php
+++ b/src/Symfony/Component/Yaml/Tests/ParserTest.php
@@ -1561,8 +1561,18 @@ EOT;
$this->assertEquals("foo bar\nbaz", $this->parser->parse("foo\nbar\n\nbaz"));
}
- public function testParseMultiLineMappingValue()
+ /**
+ * @dataProvider multiLineDataProvider
+ */
+ public function testParseMultiLineMappingValue($yaml, $expected, $parseError)
{
+ $this->assertEquals($expected, $this->parser->parse($yaml));
+ }
+
+ public function multiLineDataProvider()
+ {
+ $tests = array();
+
$yaml = <<<'EOF'
foo:
- bar:
@@ -1579,7 +1589,43 @@ EOF;
),
);
- $this->assertEquals($expected, $this->parser->parse($yaml));
+ $tests[] = array($yaml, $expected, false);
+
+ $yaml = <<<'EOF'
+bar
+"foo"
+EOF;
+ $expected = 'bar "foo"';
+
+ $tests[] = array($yaml, $expected, false);
+
+ $yaml = <<<'EOF'
+bar
+"foo
+EOF;
+ $expected = 'bar "foo';
+
+ $tests[] = array($yaml, $expected, false);
+
+ $yaml = <<<'EOF'
+bar
+
+'foo'
+EOF;
+ $expected = "bar\n'foo'";
+
+ $tests[] = array($yaml, $expected, false);
+
+ $yaml = <<<'EOF'
+bar
+
+foo'
+EOF;
+ $expected = "bar\nfoo'";
+
+ $tests[] = array($yaml, $expected, false);
+
+ return $tests;
}
public function testTaggedInlineMapping()