diff options
author | Andrew Manning <tamanning@zoho.com> | 2016-05-01 22:29:51 -0400 |
---|---|---|
committer | Andrew Manning <tamanning@zoho.com> | 2016-05-01 22:29:51 -0400 |
commit | c2d15e6c3bd8a29bae89d184a999ddac15fcb807 (patch) | |
tree | 96cbbfa98f131d830fbfd154f82e37383bb37bd8 /library/symfony | |
parent | b1ae4d776c7c093c7f3ff6905e1a5302fbd5e3f6 (diff) | |
download | volse-hubzilla-c2d15e6c3bd8a29bae89d184a999ddac15fcb807.tar.gz volse-hubzilla-c2d15e6c3bd8a29bae89d184a999ddac15fcb807.tar.bz2 volse-hubzilla-c2d15e6c3bd8a29bae89d184a999ddac15fcb807.zip |
New plugin repo is cloned to /store/pluginrepos/REPONAME for analysis
Diffstat (limited to 'library/symfony')
52 files changed, 10218 insertions, 0 deletions
diff --git a/library/symfony/options-resolver/CHANGELOG.md b/library/symfony/options-resolver/CHANGELOG.md new file mode 100644 index 000000000..5f6d15b2c --- /dev/null +++ b/library/symfony/options-resolver/CHANGELOG.md @@ -0,0 +1,46 @@ +CHANGELOG +========= + +2.6.0 +----- + + * deprecated OptionsResolverInterface + * [BC BREAK] removed "array" type hint from OptionsResolverInterface methods + setRequired(), setAllowedValues(), addAllowedValues(), setAllowedTypes() and + addAllowedTypes() + * added OptionsResolver::setDefault() + * added OptionsResolver::hasDefault() + * added OptionsResolver::setNormalizer() + * added OptionsResolver::isRequired() + * added OptionsResolver::getRequiredOptions() + * added OptionsResolver::isMissing() + * added OptionsResolver::getMissingOptions() + * added OptionsResolver::setDefined() + * added OptionsResolver::isDefined() + * added OptionsResolver::getDefinedOptions() + * added OptionsResolver::remove() + * added OptionsResolver::clear() + * deprecated OptionsResolver::replaceDefaults() + * deprecated OptionsResolver::setOptional() in favor of setDefined() + * deprecated OptionsResolver::isKnown() in favor of isDefined() + * [BC BREAK] OptionsResolver::isRequired() returns true now if a required + option has a default value set + * [BC BREAK] merged Options into OptionsResolver and turned Options into an + interface + * deprecated Options::overload() (now in OptionsResolver) + * deprecated Options::set() (now in OptionsResolver) + * deprecated Options::get() (now in OptionsResolver) + * deprecated Options::has() (now in OptionsResolver) + * deprecated Options::replace() (now in OptionsResolver) + * [BC BREAK] Options::get() (now in OptionsResolver) can only be used within + lazy option/normalizer closures now + * [BC BREAK] removed Traversable interface from Options since using within + lazy option/normalizer closures resulted in exceptions + * [BC BREAK] removed Options::all() since using within lazy option/normalizer + closures resulted in exceptions + * [BC BREAK] OptionDefinitionException now extends LogicException instead of + RuntimeException + * [BC BREAK] normalizers are not executed anymore for unset options + * normalizers are executed after validating the options now + * [BC BREAK] an UndefinedOptionsException is now thrown instead of an + InvalidOptionsException when non-existing options are passed diff --git a/library/symfony/options-resolver/Exception/AccessException.php b/library/symfony/options-resolver/Exception/AccessException.php new file mode 100644 index 000000000..c12b68064 --- /dev/null +++ b/library/symfony/options-resolver/Exception/AccessException.php @@ -0,0 +1,22 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when trying to read an option outside of or write it inside of + * {@link \Symfony\Component\OptionsResolver\Options::resolve()}. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class AccessException extends \LogicException implements ExceptionInterface +{ +} diff --git a/library/symfony/options-resolver/Exception/ExceptionInterface.php b/library/symfony/options-resolver/Exception/ExceptionInterface.php new file mode 100644 index 000000000..b62bb51d4 --- /dev/null +++ b/library/symfony/options-resolver/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Marker interface for all exceptions thrown by the OptionsResolver component. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +interface ExceptionInterface +{ +} diff --git a/library/symfony/options-resolver/Exception/InvalidArgumentException.php b/library/symfony/options-resolver/Exception/InvalidArgumentException.php new file mode 100644 index 000000000..6d421d68b --- /dev/null +++ b/library/symfony/options-resolver/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when an argument is invalid. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/library/symfony/options-resolver/Exception/InvalidOptionsException.php b/library/symfony/options-resolver/Exception/InvalidOptionsException.php new file mode 100644 index 000000000..6fd4f125f --- /dev/null +++ b/library/symfony/options-resolver/Exception/InvalidOptionsException.php @@ -0,0 +1,23 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when the value of an option does not match its validation rules. + * + * You should make sure a valid value is passed to the option. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class InvalidOptionsException extends InvalidArgumentException +{ +} diff --git a/library/symfony/options-resolver/Exception/MissingOptionsException.php b/library/symfony/options-resolver/Exception/MissingOptionsException.php new file mode 100644 index 000000000..faa487f16 --- /dev/null +++ b/library/symfony/options-resolver/Exception/MissingOptionsException.php @@ -0,0 +1,23 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Exception thrown when a required option is missing. + * + * Add the option to the passed options array. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class MissingOptionsException extends InvalidArgumentException +{ +} diff --git a/library/symfony/options-resolver/Exception/NoSuchOptionException.php b/library/symfony/options-resolver/Exception/NoSuchOptionException.php new file mode 100644 index 000000000..4c3280f4c --- /dev/null +++ b/library/symfony/options-resolver/Exception/NoSuchOptionException.php @@ -0,0 +1,26 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when trying to read an option that has no value set. + * + * When accessing optional options from within a lazy option or normalizer you should first + * check whether the optional option is set. You can do this with `isset($options['optional'])`. + * In contrast to the {@link UndefinedOptionsException}, this is a runtime exception that can + * occur when evaluating lazy options. + * + * @author Tobias Schultze <http://tobion.de> + */ +class NoSuchOptionException extends \OutOfBoundsException implements ExceptionInterface +{ +} diff --git a/library/symfony/options-resolver/Exception/OptionDefinitionException.php b/library/symfony/options-resolver/Exception/OptionDefinitionException.php new file mode 100644 index 000000000..e8e339d44 --- /dev/null +++ b/library/symfony/options-resolver/Exception/OptionDefinitionException.php @@ -0,0 +1,21 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Thrown when two lazy options have a cyclic dependency. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class OptionDefinitionException extends \LogicException implements ExceptionInterface +{ +} diff --git a/library/symfony/options-resolver/Exception/UndefinedOptionsException.php b/library/symfony/options-resolver/Exception/UndefinedOptionsException.php new file mode 100644 index 000000000..6ca3fce47 --- /dev/null +++ b/library/symfony/options-resolver/Exception/UndefinedOptionsException.php @@ -0,0 +1,24 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Exception; + +/** + * Exception thrown when an undefined option is passed. + * + * You should remove the options in question from your code or define them + * beforehand. + * + * @author Bernhard Schussek <bschussek@gmail.com> + */ +class UndefinedOptionsException extends InvalidArgumentException +{ +} diff --git a/library/symfony/options-resolver/LICENSE b/library/symfony/options-resolver/LICENSE new file mode 100644 index 000000000..43028bc60 --- /dev/null +++ b/library/symfony/options-resolver/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2015 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/library/symfony/options-resolver/Options.php b/library/symfony/options-resolver/Options.php new file mode 100644 index 000000000..d444ec423 --- /dev/null +++ b/library/symfony/options-resolver/Options.php @@ -0,0 +1,22 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver; + +/** + * Contains resolved option values. + * + * @author Bernhard Schussek <bschussek@gmail.com> + * @author Tobias Schultze <http://tobion.de> + */ +interface Options extends \ArrayAccess, \Countable +{ +} diff --git a/library/symfony/options-resolver/OptionsResolver.php b/library/symfony/options-resolver/OptionsResolver.php new file mode 100644 index 000000000..e0578af71 --- /dev/null +++ b/library/symfony/options-resolver/OptionsResolver.php @@ -0,0 +1,1218 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver; + +use Symfony\Component\OptionsResolver\Exception\AccessException; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; +use Symfony\Component\OptionsResolver\Exception\NoSuchOptionException; +use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException; +use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; + +/** + * Validates options and merges them with default values. + * + * @author Bernhard Schussek <bschussek@gmail.com> + * @author Tobias Schultze <http://tobion.de> + */ +class OptionsResolver implements Options, OptionsResolverInterface +{ + /** + * The fully qualified name of the {@link Options} interface. + * + * @internal + */ + const OPTIONS_INTERFACE = 'Symfony\\Component\\OptionsResolver\\Options'; + + /** + * The names of all defined options. + * + * @var array + */ + private $defined = array(); + + /** + * The default option values. + * + * @var array + */ + private $defaults = array(); + + /** + * The names of required options. + * + * @var array + */ + private $required = array(); + + /** + * The resolved option values. + * + * @var array + */ + private $resolved = array(); + + /** + * A list of normalizer closures. + * + * @var \Closure[] + */ + private $normalizers = array(); + + /** + * A list of accepted values for each option. + * + * @var array + */ + private $allowedValues = array(); + + /** + * A list of accepted types for each option. + * + * @var array + */ + private $allowedTypes = array(); + + /** + * A list of closures for evaluating lazy options. + * + * @var array + */ + private $lazy = array(); + + /** + * A list of lazy options whose closure is currently being called. + * + * This list helps detecting circular dependencies between lazy options. + * + * @var array + */ + private $calling = array(); + + /** + * Whether the instance is locked for reading. + * + * Once locked, the options cannot be changed anymore. This is + * necessary in order to avoid inconsistencies during the resolving + * process. If any option is changed after being read, all evaluated + * lazy options that depend on this option would become invalid. + * + * @var bool + */ + private $locked = false; + + private static $typeAliases = array( + 'boolean' => 'bool', + 'integer' => 'int', + 'double' => 'float', + ); + + /** + * Sets the default value of a given option. + * + * If the default value should be set based on other options, you can pass + * a closure with the following signature: + * + * function (Options $options) { + * // ... + * } + * + * The closure will be evaluated when {@link resolve()} is called. The + * closure has access to the resolved values of other options through the + * passed {@link Options} instance: + * + * function (Options $options) { + * if (isset($options['port'])) { + * // ... + * } + * } + * + * If you want to access the previously set default value, add a second + * argument to the closure's signature: + * + * $options->setDefault('name', 'Default Name'); + * + * $options->setDefault('name', function (Options $options, $previousValue) { + * // 'Default Name' === $previousValue + * }); + * + * This is mostly useful if the configuration of the {@link Options} object + * is spread across different locations of your code, such as base and + * sub-classes. + * + * @param string $option The name of the option + * @param mixed $value The default value of the option + * + * @return OptionsResolver This instance + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setDefault($option, $value) + { + // Setting is not possible once resolving starts, because then lazy + // options could manipulate the state of the object, leading to + // inconsistent results. + if ($this->locked) { + throw new AccessException('Default values cannot be set from a lazy option or normalizer.'); + } + + // If an option is a closure that should be evaluated lazily, store it + // in the "lazy" property. + if ($value instanceof \Closure) { + $reflClosure = new \ReflectionFunction($value); + $params = $reflClosure->getParameters(); + + if (isset($params[0]) && null !== ($class = $params[0]->getClass()) && self::OPTIONS_INTERFACE === $class->name) { + // Initialize the option if no previous value exists + if (!isset($this->defaults[$option])) { + $this->defaults[$option] = null; + } + + // Ignore previous lazy options if the closure has no second parameter + if (!isset($this->lazy[$option]) || !isset($params[1])) { + $this->lazy[$option] = array(); + } + + // Store closure for later evaluation + $this->lazy[$option][] = $value; + $this->defined[$option] = true; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + } + + // This option is not lazy anymore + unset($this->lazy[$option]); + + // Yet undefined options can be marked as resolved, because we only need + // to resolve options with lazy closures, normalizers or validation + // rules, none of which can exist for undefined options + // If the option was resolved before, update the resolved value + if (!isset($this->defined[$option]) || array_key_exists($option, $this->resolved)) { + $this->resolved[$option] = $value; + } + + $this->defaults[$option] = $value; + $this->defined[$option] = true; + + return $this; + } + + /** + * Sets a list of default values. + * + * @param array $defaults The default values to set + * + * @return OptionsResolver This instance + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setDefaults(array $defaults) + { + foreach ($defaults as $option => $value) { + $this->setDefault($option, $value); + } + + return $this; + } + + /** + * Returns whether a default value is set for an option. + * + * Returns true if {@link setDefault()} was called for this option. + * An option is also considered set if it was set to null. + * + * @param string $option The option name + * + * @return bool Whether a default value is set + */ + public function hasDefault($option) + { + return array_key_exists($option, $this->defaults); + } + + /** + * Marks one or more options as required. + * + * @param string|string[] $optionNames One or more option names + * + * @return OptionsResolver This instance + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setRequired($optionNames) + { + if ($this->locked) { + throw new AccessException('Options cannot be made required from a lazy option or normalizer.'); + } + + foreach ((array) $optionNames as $option) { + $this->defined[$option] = true; + $this->required[$option] = true; + } + + return $this; + } + + /** + * Returns whether an option is required. + * + * An option is required if it was passed to {@link setRequired()}. + * + * @param string $option The name of the option + * + * @return bool Whether the option is required + */ + public function isRequired($option) + { + return isset($this->required[$option]); + } + + /** + * Returns the names of all required options. + * + * @return string[] The names of the required options + * + * @see isRequired() + */ + public function getRequiredOptions() + { + return array_keys($this->required); + } + + /** + * Returns whether an option is missing a default value. + * + * An option is missing if it was passed to {@link setRequired()}, but not + * to {@link setDefault()}. This option must be passed explicitly to + * {@link resolve()}, otherwise an exception will be thrown. + * + * @param string $option The name of the option + * + * @return bool Whether the option is missing + */ + public function isMissing($option) + { + return isset($this->required[$option]) && !array_key_exists($option, $this->defaults); + } + + /** + * Returns the names of all options missing a default value. + * + * @return string[] The names of the missing options + * + * @see isMissing() + */ + public function getMissingOptions() + { + return array_keys(array_diff_key($this->required, $this->defaults)); + } + + /** + * Defines a valid option name. + * + * Defines an option name without setting a default value. The option will + * be accepted when passed to {@link resolve()}. When not passed, the + * option will not be included in the resolved options. + * + * @param string|string[] $optionNames One or more option names + * + * @return OptionsResolver This instance + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function setDefined($optionNames) + { + if ($this->locked) { + throw new AccessException('Options cannot be defined from a lazy option or normalizer.'); + } + + foreach ((array) $optionNames as $option) { + $this->defined[$option] = true; + } + + return $this; + } + + /** + * Returns whether an option is defined. + * + * Returns true for any option passed to {@link setDefault()}, + * {@link setRequired()} or {@link setDefined()}. + * + * @param string $option The option name + * + * @return bool Whether the option is defined + */ + public function isDefined($option) + { + return isset($this->defined[$option]); + } + + /** + * Returns the names of all defined options. + * + * @return string[] The names of the defined options + * + * @see isDefined() + */ + public function getDefinedOptions() + { + return array_keys($this->defined); + } + + /** + * Sets the normalizer for an option. + * + * The normalizer should be a closure with the following signature: + * + * ```php + * function (Options $options, $value) { + * // ... + * } + * ``` + * + * The closure is invoked when {@link resolve()} is called. The closure + * has access to the resolved values of other options through the passed + * {@link Options} instance. + * + * The second parameter passed to the closure is the value of + * the option. + * + * The resolved option value is set to the return value of the closure. + * + * @param string $option The option name + * @param \Closure $normalizer The normalizer + * + * @return OptionsResolver This instance + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setNormalizer($option, \Closure $normalizer) + { + if ($this->locked) { + throw new AccessException('Normalizers cannot be set from a lazy option or normalizer.'); + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + $this->normalizers[$option] = $normalizer; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Sets the normalizers for an array of options. + * + * @param array $normalizers An array of closures + * + * @return OptionsResolver This instance + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + * + * @see setNormalizer() + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function setNormalizers(array $normalizers) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use setNormalizer() instead.', E_USER_DEPRECATED); + + foreach ($normalizers as $option => $normalizer) { + $this->setNormalizer($option, $normalizer); + } + + return $this; + } + + /** + * Sets allowed values for an option. + * + * Instead of passing values, you may also pass a closures with the + * following signature: + * + * function ($value) { + * // return true or false + * } + * + * The closure receives the value as argument and should return true to + * accept the value and false to reject the value. + * + * @param string $option The option name + * @param mixed $allowedValues One or more acceptable values/closures + * + * @return OptionsResolver This instance + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setAllowedValues($option, $allowedValues = null) + { + if ($this->locked) { + throw new AccessException('Allowed values cannot be set from a lazy option or normalizer.'); + } + + // BC + if (is_array($option) && null === $allowedValues) { + @trigger_error('Calling the '.__METHOD__.' method with an array of options is deprecated since version 2.6 and will be removed in 3.0. Use the new signature with a single option instead.', E_USER_DEPRECATED); + + foreach ($option as $optionName => $optionValues) { + $this->setAllowedValues($optionName, $optionValues); + } + + return $this; + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + $this->allowedValues[$option] = is_array($allowedValues) ? $allowedValues : array($allowedValues); + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Adds allowed values for an option. + * + * The values are merged with the allowed values defined previously. + * + * Instead of passing values, you may also pass a closures with the + * following signature: + * + * function ($value) { + * // return true or false + * } + * + * The closure receives the value as argument and should return true to + * accept the value and false to reject the value. + * + * @param string $option The option name + * @param mixed $allowedValues One or more acceptable values/closures + * + * @return OptionsResolver This instance + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function addAllowedValues($option, $allowedValues = null) + { + if ($this->locked) { + throw new AccessException('Allowed values cannot be added from a lazy option or normalizer.'); + } + + // BC + if (is_array($option) && null === $allowedValues) { + @trigger_error('Calling the '.__METHOD__.' method with an array of options is deprecated since version 2.6 and will be removed in 3.0. Use the new signature with a single option instead.', E_USER_DEPRECATED); + + foreach ($option as $optionName => $optionValues) { + $this->addAllowedValues($optionName, $optionValues); + } + + return $this; + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + if (!is_array($allowedValues)) { + $allowedValues = array($allowedValues); + } + + if (!isset($this->allowedValues[$option])) { + $this->allowedValues[$option] = $allowedValues; + } else { + $this->allowedValues[$option] = array_merge($this->allowedValues[$option], $allowedValues); + } + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Sets allowed types for an option. + * + * Any type for which a corresponding is_<type>() function exists is + * acceptable. Additionally, fully-qualified class or interface names may + * be passed. + * + * @param string $option The option name + * @param string|string[] $allowedTypes One or more accepted types + * + * @return OptionsResolver This instance + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function setAllowedTypes($option, $allowedTypes = null) + { + if ($this->locked) { + throw new AccessException('Allowed types cannot be set from a lazy option or normalizer.'); + } + + // BC + if (is_array($option) && null === $allowedTypes) { + @trigger_error('Calling the '.__METHOD__.' method with an array of options is deprecated since version 2.6 and will be removed in 3.0. Use the new signature with a single option instead.', E_USER_DEPRECATED); + + foreach ($option as $optionName => $optionTypes) { + $this->setAllowedTypes($optionName, $optionTypes); + } + + return $this; + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + $this->allowedTypes[$option] = (array) $allowedTypes; + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Adds allowed types for an option. + * + * The types are merged with the allowed types defined previously. + * + * Any type for which a corresponding is_<type>() function exists is + * acceptable. Additionally, fully-qualified class or interface names may + * be passed. + * + * @param string $option The option name + * @param string|string[] $allowedTypes One or more accepted types + * + * @return OptionsResolver This instance + * + * @throws UndefinedOptionsException If the option is undefined + * @throws AccessException If called from a lazy option or normalizer + */ + public function addAllowedTypes($option, $allowedTypes = null) + { + if ($this->locked) { + throw new AccessException('Allowed types cannot be added from a lazy option or normalizer.'); + } + + // BC + if (is_array($option) && null === $allowedTypes) { + @trigger_error('Calling the '.__METHOD__.' method with an array of options is deprecated since version 2.6 and will be removed in 3.0. Use the new signature with a single option instead.', E_USER_DEPRECATED); + + foreach ($option as $optionName => $optionTypes) { + $this->addAllowedTypes($optionName, $optionTypes); + } + + return $this; + } + + if (!isset($this->defined[$option])) { + throw new UndefinedOptionsException(sprintf( + 'The option "%s" does not exist. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + if (!isset($this->allowedTypes[$option])) { + $this->allowedTypes[$option] = (array) $allowedTypes; + } else { + $this->allowedTypes[$option] = array_merge($this->allowedTypes[$option], (array) $allowedTypes); + } + + // Make sure the option is processed + unset($this->resolved[$option]); + + return $this; + } + + /** + * Removes the option with the given name. + * + * Undefined options are ignored. + * + * @param string|string[] $optionNames One or more option names + * + * @return OptionsResolver This instance + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function remove($optionNames) + { + if ($this->locked) { + throw new AccessException('Options cannot be removed from a lazy option or normalizer.'); + } + + foreach ((array) $optionNames as $option) { + unset($this->defined[$option], $this->defaults[$option], $this->required[$option], $this->resolved[$option]); + unset($this->lazy[$option], $this->normalizers[$option], $this->allowedTypes[$option], $this->allowedValues[$option]); + } + + return $this; + } + + /** + * Removes all options. + * + * @return OptionsResolver This instance + * + * @throws AccessException If called from a lazy option or normalizer + */ + public function clear() + { + if ($this->locked) { + throw new AccessException('Options cannot be cleared from a lazy option or normalizer.'); + } + + $this->defined = array(); + $this->defaults = array(); + $this->required = array(); + $this->resolved = array(); + $this->lazy = array(); + $this->normalizers = array(); + $this->allowedTypes = array(); + $this->allowedValues = array(); + + return $this; + } + + /** + * Merges options with the default values stored in the container and + * validates them. + * + * Exceptions are thrown if: + * + * - Undefined options are passed; + * - Required options are missing; + * - Options have invalid types; + * - Options have invalid values. + * + * @param array $options A map of option names to values + * + * @return array The merged and validated options + * + * @throws UndefinedOptionsException If an option name is undefined + * @throws InvalidOptionsException If an option doesn't fulfill the + * specified validation rules + * @throws MissingOptionsException If a required option is missing + * @throws OptionDefinitionException If there is a cyclic dependency between + * lazy options and/or normalizers + * @throws NoSuchOptionException If a lazy option reads an unavailable option + * @throws AccessException If called from a lazy option or normalizer + */ + public function resolve(array $options = array()) + { + if ($this->locked) { + throw new AccessException('Options cannot be resolved from a lazy option or normalizer.'); + } + + // Allow this method to be called multiple times + $clone = clone $this; + + // Make sure that no unknown options are passed + $diff = array_diff_key($options, $clone->defined); + + if (count($diff) > 0) { + ksort($clone->defined); + ksort($diff); + + throw new UndefinedOptionsException(sprintf( + (count($diff) > 1 ? 'The options "%s" do not exist.' : 'The option "%s" does not exist.').' Defined options are: "%s".', + implode('", "', array_keys($diff)), + implode('", "', array_keys($clone->defined)) + )); + } + + // Override options set by the user + foreach ($options as $option => $value) { + $clone->defaults[$option] = $value; + unset($clone->resolved[$option], $clone->lazy[$option]); + } + + // Check whether any required option is missing + $diff = array_diff_key($clone->required, $clone->defaults); + + if (count($diff) > 0) { + ksort($diff); + + throw new MissingOptionsException(sprintf( + count($diff) > 1 ? 'The required options "%s" are missing.' : 'The required option "%s" is missing.', + implode('", "', array_keys($diff)) + )); + } + + // Lock the container + $clone->locked = true; + + // Now process the individual options. Use offsetGet(), which resolves + // the option itself and any options that the option depends on + foreach ($clone->defaults as $option => $_) { + $clone->offsetGet($option); + } + + return $clone->resolved; + } + + /** + * Returns the resolved value of an option. + * + * @param string $option The option name + * + * @return mixed The option value + * + * @throws AccessException If accessing this method outside of + * {@link resolve()} + * @throws NoSuchOptionException If the option is not set + * @throws InvalidOptionsException If the option doesn't fulfill the + * specified validation rules + * @throws OptionDefinitionException If there is a cyclic dependency between + * lazy options and/or normalizers + */ + public function offsetGet($option) + { + if (!$this->locked) { + throw new AccessException('Array access is only supported within closures of lazy options and normalizers.'); + } + + // Shortcut for resolved options + if (array_key_exists($option, $this->resolved)) { + return $this->resolved[$option]; + } + + // Check whether the option is set at all + if (!array_key_exists($option, $this->defaults)) { + if (!isset($this->defined[$option])) { + throw new NoSuchOptionException(sprintf( + 'The option "%s" does not exist. Defined options are: "%s".', + $option, + implode('", "', array_keys($this->defined)) + )); + } + + throw new NoSuchOptionException(sprintf( + 'The optional option "%s" has no value set. You should make sure it is set with "isset" before reading it.', + $option + )); + } + + $value = $this->defaults[$option]; + + // Resolve the option if the default value is lazily evaluated + if (isset($this->lazy[$option])) { + // If the closure is already being called, we have a cyclic + // dependency + if (isset($this->calling[$option])) { + throw new OptionDefinitionException(sprintf( + 'The options "%s" have a cyclic dependency.', + implode('", "', array_keys($this->calling)) + )); + } + + // The following section must be protected from cyclic + // calls. Set $calling for the current $option to detect a cyclic + // dependency + // BEGIN + $this->calling[$option] = true; + try { + foreach ($this->lazy[$option] as $closure) { + $value = $closure($this, $value); + } + } catch (\Exception $e) { + unset($this->calling[$option]); + throw $e; + } + unset($this->calling[$option]); + // END + } + + // Validate the type of the resolved option + if (isset($this->allowedTypes[$option])) { + $valid = false; + + foreach ($this->allowedTypes[$option] as $type) { + $type = isset(self::$typeAliases[$type]) ? self::$typeAliases[$type] : $type; + + if (function_exists($isFunction = 'is_'.$type)) { + if ($isFunction($value)) { + $valid = true; + break; + } + + continue; + } + + if ($value instanceof $type) { + $valid = true; + break; + } + } + + if (!$valid) { + throw new InvalidOptionsException(sprintf( + 'The option "%s" with value %s is expected to be of type '. + '"%s", but is of type "%s".', + $option, + $this->formatValue($value), + implode('" or "', $this->allowedTypes[$option]), + $this->formatTypeOf($value) + )); + } + } + + // Validate the value of the resolved option + if (isset($this->allowedValues[$option])) { + $success = false; + $printableAllowedValues = array(); + + foreach ($this->allowedValues[$option] as $allowedValue) { + if ($allowedValue instanceof \Closure) { + if ($allowedValue($value)) { + $success = true; + break; + } + + // Don't include closures in the exception message + continue; + } elseif ($value === $allowedValue) { + $success = true; + break; + } + + $printableAllowedValues[] = $allowedValue; + } + + if (!$success) { + $message = sprintf( + 'The option "%s" with value %s is invalid.', + $option, + $this->formatValue($value) + ); + + if (count($printableAllowedValues) > 0) { + $message .= sprintf( + ' Accepted values are: %s.', + $this->formatValues($printableAllowedValues) + ); + } + + throw new InvalidOptionsException($message); + } + } + + // Normalize the validated option + if (isset($this->normalizers[$option])) { + // If the closure is already being called, we have a cyclic + // dependency + if (isset($this->calling[$option])) { + throw new OptionDefinitionException(sprintf( + 'The options "%s" have a cyclic dependency.', + implode('", "', array_keys($this->calling)) + )); + } + + $normalizer = $this->normalizers[$option]; + + // The following section must be protected from cyclic + // calls. Set $calling for the current $option to detect a cyclic + // dependency + // BEGIN + $this->calling[$option] = true; + try { + $value = $normalizer($this, $value); + } catch (\Exception $e) { + unset($this->calling[$option]); + throw $e; + } + unset($this->calling[$option]); + // END + } + + // Mark as resolved + $this->resolved[$option] = $value; + + return $value; + } + + /** + * Returns whether a resolved option with the given name exists. + * + * @param string $option The option name + * + * @return bool Whether the option is set + * + * @throws AccessException If accessing this method outside of {@link resolve()} + * + * @see \ArrayAccess::offsetExists() + */ + public function offsetExists($option) + { + if (!$this->locked) { + throw new AccessException('Array access is only supported within closures of lazy options and normalizers.'); + } + + return array_key_exists($option, $this->defaults); + } + + /** + * Not supported. + * + * @throws AccessException + */ + public function offsetSet($option, $value) + { + throw new AccessException('Setting options via array access is not supported. Use setDefault() instead.'); + } + + /** + * Not supported. + * + * @throws AccessException + */ + public function offsetUnset($option) + { + throw new AccessException('Removing options via array access is not supported. Use remove() instead.'); + } + + /** + * Returns the number of set options. + * + * This may be only a subset of the defined options. + * + * @return int Number of options + * + * @throws AccessException If accessing this method outside of {@link resolve()} + * + * @see \Countable::count() + */ + public function count() + { + if (!$this->locked) { + throw new AccessException('Counting is only supported within closures of lazy options and normalizers.'); + } + + return count($this->defaults); + } + + /** + * Alias of {@link setDefault()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function set($option, $value) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the setDefaults() method instead.', E_USER_DEPRECATED); + + return $this->setDefault($option, $value); + } + + /** + * Shortcut for {@link clear()} and {@link setDefaults()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function replace(array $defaults) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the clear() and setDefaults() methods instead.', E_USER_DEPRECATED); + + $this->clear(); + + return $this->setDefaults($defaults); + } + + /** + * Alias of {@link setDefault()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function overload($option, $value) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the setDefault() method instead.', E_USER_DEPRECATED); + + return $this->setDefault($option, $value); + } + + /** + * Alias of {@link offsetGet()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function get($option) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the ArrayAccess syntax instead to get an option value.', E_USER_DEPRECATED); + + return $this->offsetGet($option); + } + + /** + * Alias of {@link offsetExists()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function has($option) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the ArrayAccess syntax instead to get an option value.', E_USER_DEPRECATED); + + return $this->offsetExists($option); + } + + /** + * Shortcut for {@link clear()} and {@link setDefaults()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function replaceDefaults(array $defaultValues) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the clear() and setDefaults() methods instead.', E_USER_DEPRECATED); + + $this->clear(); + + return $this->setDefaults($defaultValues); + } + + /** + * Alias of {@link setDefined()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function setOptional(array $optionNames) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the setDefined() method instead.', E_USER_DEPRECATED); + + return $this->setDefined($optionNames); + } + + /** + * Alias of {@link isDefined()}. + * + * @deprecated since version 2.6, to be removed in 3.0. + */ + public function isKnown($option) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.6 and will be removed in 3.0. Use the isDefined() method instead.', E_USER_DEPRECATED); + + return $this->isDefined($option); + } + + /** + * Returns a string representation of the type of the value. + * + * This method should be used if you pass the type of a value as + * message parameter to a constraint violation. Note that such + * parameters should usually not be included in messages aimed at + * non-technical people. + * + * @param mixed $value The value to return the type of + * + * @return string The type of the value + */ + private function formatTypeOf($value) + { + return is_object($value) ? get_class($value) : gettype($value); + } + + /** + * Returns a string representation of the value. + * + * This method returns the equivalent PHP tokens for most scalar types + * (i.e. "false" for false, "1" for 1 etc.). Strings are always wrapped + * in double quotes ("). + * + * @param mixed $value The value to format as string + * + * @return string The string representation of the passed value + */ + private function formatValue($value) + { + if (is_object($value)) { + return get_class($value); + } + + if (is_array($value)) { + return 'array'; + } + + if (is_string($value)) { + return '"'.$value.'"'; + } + + if (is_resource($value)) { + return 'resource'; + } + + if (null === $value) { + return 'null'; + } + + if (false === $value) { + return 'false'; + } + + if (true === $value) { + return 'true'; + } + + return (string) $value; + } + + /** + * Returns a string representation of a list of values. + * + * Each of the values is converted to a string using + * {@link formatValue()}. The values are then concatenated with commas. + * + * @param array $values A list of values + * + * @return string The string representation of the value list + * + * @see formatValue() + */ + private function formatValues(array $values) + { + foreach ($values as $key => $value) { + $values[$key] = $this->formatValue($value); + } + + return implode(', ', $values); + } +} diff --git a/library/symfony/options-resolver/OptionsResolverInterface.php b/library/symfony/options-resolver/OptionsResolverInterface.php new file mode 100644 index 000000000..aebc8df22 --- /dev/null +++ b/library/symfony/options-resolver/OptionsResolverInterface.php @@ -0,0 +1,212 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver; + +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; +use Symfony\Component\OptionsResolver\Exception\OptionDefinitionException; + +/** + * @author Bernhard Schussek <bschussek@gmail.com> + * + * @deprecated since version 2.6, to be removed in 3.0. Use {@link OptionsResolver} instead. + */ +interface OptionsResolverInterface +{ + /** + * Sets default option values. + * + * The options can either be values of any types or closures that + * evaluate the option value lazily. These closures must have one + * of the following signatures: + * + * <code> + * function (Options $options) + * function (Options $options, $value) + * </code> + * + * The second parameter passed to the closure is the previously + * set default value, in case you are overwriting an existing + * default value. + * + * The closures should return the lazily created option value. + * + * @param array $defaultValues A list of option names as keys and default + * values or closures as values. + * + * @return OptionsResolverInterface The resolver instance. + */ + public function setDefaults(array $defaultValues); + + /** + * Replaces default option values. + * + * Old defaults are erased, which means that closures passed here cannot + * access the previous default value. This may be useful to improve + * performance if the previous default value is calculated by an expensive + * closure. + * + * @param array $defaultValues A list of option names as keys and default + * values or closures as values. + * + * @return OptionsResolverInterface The resolver instance. + */ + public function replaceDefaults(array $defaultValues); + + /** + * Sets optional options. + * + * This method declares valid option names without setting default values for them. + * If these options are not passed to {@link resolve()} and no default has been set + * for them, they will be missing in the final options array. This can be helpful + * if you want to determine whether an option has been set or not because otherwise + * {@link resolve()} would trigger an exception for unknown options. + * + * @param array $optionNames A list of option names. + * + * @return OptionsResolverInterface The resolver instance. + */ + public function setOptional(array $optionNames); + + /** + * Sets required options. + * + * If these options are not passed to {@link resolve()} and no default has been set for + * them, an exception will be thrown. + * + * @param array $optionNames A list of option names. + * + * @return OptionsResolverInterface The resolver instance. + */ + public function setRequired($optionNames); + + /** + * Sets allowed values for a list of options. + * + * @param array $allowedValues A list of option names as keys and arrays + * with values acceptable for that option as + * values. + * + * @return OptionsResolverInterface The resolver instance. + * + * @throws InvalidOptionsException If an option has not been defined + * (see {@link isKnown()}) for which + * an allowed value is set. + */ + public function setAllowedValues($allowedValues); + + /** + * Adds allowed values for a list of options. + * + * The values are merged with the allowed values defined previously. + * + * @param array $allowedValues A list of option names as keys and arrays + * with values acceptable for that option as + * values. + * + * @return OptionsResolverInterface The resolver instance. + * + * @throws InvalidOptionsException If an option has not been defined + * (see {@link isKnown()}) for which + * an allowed value is set. + */ + public function addAllowedValues($allowedValues); + + /** + * Sets allowed types for a list of options. + * + * @param array $allowedTypes A list of option names as keys and type + * names passed as string or array as values. + * + * @return OptionsResolverInterface The resolver instance. + * + * @throws InvalidOptionsException If an option has not been defined for + * which an allowed type is set. + */ + public function setAllowedTypes($allowedTypes); + + /** + * Adds allowed types for a list of options. + * + * The types are merged with the allowed types defined previously. + * + * @param array $allowedTypes A list of option names as keys and type + * names passed as string or array as values. + * + * @return OptionsResolverInterface The resolver instance. + * + * @throws InvalidOptionsException If an option has not been defined for + * which an allowed type is set. + */ + public function addAllowedTypes($allowedTypes); + + /** + * Sets normalizers that are applied on resolved options. + * + * The normalizers should be closures with the following signature: + * + * <code> + * function (Options $options, $value) + * </code> + * + * The second parameter passed to the closure is the value of + * the option. + * + * The closure should return the normalized value. + * + * @param array $normalizers An array of closures. + * + * @return OptionsResolverInterface The resolver instance. + */ + public function setNormalizers(array $normalizers); + + /** + * Returns whether an option is known. + * + * An option is known if it has been passed to either {@link setDefaults()}, + * {@link setRequired()} or {@link setOptional()} before. + * + * @param string $option The name of the option. + * + * @return bool Whether the option is known. + */ + public function isKnown($option); + + /** + * Returns whether an option is required. + * + * An option is required if it has been passed to {@link setRequired()}, + * but not to {@link setDefaults()}. That is, the option has been declared + * as required and no default value has been set. + * + * @param string $option The name of the option. + * + * @return bool Whether the option is required. + */ + public function isRequired($option); + + /** + * Returns the combination of the default and the passed options. + * + * @param array $options The custom option values. + * + * @return array A list of options and their values. + * + * @throws InvalidOptionsException If any of the passed options has not + * been defined or does not contain an + * allowed value. + * @throws MissingOptionsException If a required option is missing. + * @throws OptionDefinitionException If a cyclic dependency is detected + * between two lazy options. + */ + public function resolve(array $options = array()); +} diff --git a/library/symfony/options-resolver/README.md b/library/symfony/options-resolver/README.md new file mode 100644 index 000000000..cd7a7405d --- /dev/null +++ b/library/symfony/options-resolver/README.md @@ -0,0 +1,20 @@ +OptionsResolver Component +========================= + +This component processes and validates option arrays. + +Documentation +------------- + +The documentation for the component can be found [online] [1]. + +Resources +--------- + +You can run the unit tests with the following command: + + $ cd path/to/Symfony/Component/OptionsResolver/ + $ composer install + $ phpunit + +[1]: https://symfony.com/doc/current/components/options_resolver.html diff --git a/library/symfony/options-resolver/Tests/LegacyOptionsResolverTest.php b/library/symfony/options-resolver/Tests/LegacyOptionsResolverTest.php new file mode 100644 index 000000000..ee89f5279 --- /dev/null +++ b/library/symfony/options-resolver/Tests/LegacyOptionsResolverTest.php @@ -0,0 +1,733 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Tests; + +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\OptionsResolver\Options; + +/** + * @group legacy + */ +class LegacyOptionsResolverTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var OptionsResolver + */ + private $resolver; + + protected function setUp() + { + $this->resolver = new OptionsResolver(); + } + + public function testResolve() + { + $this->resolver->setDefaults(array( + 'one' => '1', + 'two' => '2', + )); + + $options = array( + 'two' => '20', + ); + + $this->assertEquals(array( + 'one' => '1', + 'two' => '20', + ), $this->resolver->resolve($options)); + } + + public function testResolveNumericOptions() + { + $this->resolver->setDefaults(array( + '1' => '1', + '2' => '2', + )); + + $options = array( + '2' => '20', + ); + + $this->assertEquals(array( + '1' => '1', + '2' => '20', + ), $this->resolver->resolve($options)); + } + + public function testResolveLazy() + { + $this->resolver->setDefaults(array( + 'one' => '1', + 'two' => function (Options $options) { + return '20'; + }, + )); + + $this->assertEquals(array( + 'one' => '1', + 'two' => '20', + ), $this->resolver->resolve(array())); + } + + public function testTypeAliasesForAllowedTypes() + { + $this->resolver->setDefaults(array( + 'force' => false, + )); + + $this->resolver->setAllowedTypes(array( + 'force' => 'boolean', + )); + + $this->resolver->resolve(array( + 'force' => true, + )); + } + + public function testResolveLazyDependencyOnOptional() + { + $this->resolver->setDefaults(array( + 'one' => '1', + 'two' => function (Options $options) { + return $options['one'].'2'; + }, + )); + + $options = array( + 'one' => '10', + ); + + $this->assertEquals(array( + 'one' => '10', + 'two' => '102', + ), $this->resolver->resolve($options)); + } + + public function testResolveLazyDependencyOnMissingOptionalWithoutDefault() + { + $test = $this; + + $this->resolver->setOptional(array( + 'one', + )); + + $this->resolver->setDefaults(array( + 'two' => function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertFalse(isset($options['one'])); + + return '2'; + }, + )); + + $options = array(); + + $this->assertEquals(array( + 'two' => '2', + ), $this->resolver->resolve($options)); + } + + public function testResolveLazyDependencyOnOptionalWithoutDefault() + { + $test = $this; + + $this->resolver->setOptional(array( + 'one', + )); + + $this->resolver->setDefaults(array( + 'two' => function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertTrue(isset($options['one'])); + + return $options['one'].'2'; + }, + )); + + $options = array( + 'one' => '10', + ); + + $this->assertEquals(array( + 'one' => '10', + 'two' => '102', + ), $this->resolver->resolve($options)); + } + + public function testResolveLazyDependencyOnRequired() + { + $this->resolver->setRequired(array( + 'one', + )); + $this->resolver->setDefaults(array( + 'two' => function (Options $options) { + return $options['one'].'2'; + }, + )); + + $options = array( + 'one' => '10', + ); + + $this->assertEquals(array( + 'one' => '10', + 'two' => '102', + ), $this->resolver->resolve($options)); + } + + public function testResolveLazyReplaceDefaults() + { + $test = $this; + + $this->resolver->setDefaults(array( + 'one' => function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->fail('Previous closure should not be executed'); + }, + )); + + $this->resolver->replaceDefaults(array( + 'one' => function (Options $options, $previousValue) { + return '1'; + }, + )); + + $this->assertEquals(array( + 'one' => '1', + ), $this->resolver->resolve(array())); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + * @expectedExceptionMessage The option "foo" does not exist. Defined options are: "one", "three", "two". + */ + public function testResolveFailsIfNonExistingOption() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setRequired(array( + 'two', + )); + + $this->resolver->setOptional(array( + 'three', + )); + + $this->resolver->resolve(array( + 'foo' => 'bar', + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException + */ + public function testResolveFailsIfMissingRequiredOption() + { + $this->resolver->setRequired(array( + 'one', + )); + + $this->resolver->setDefaults(array( + 'two' => '2', + )); + + $this->resolver->resolve(array( + 'two' => '20', + )); + } + + public function testResolveSucceedsIfOptionValueAllowed() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedValues(array( + 'one' => array('1', 'one'), + )); + + $options = array( + 'one' => 'one', + ); + + $this->assertEquals(array( + 'one' => 'one', + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionValueAllowed2() + { + $this->resolver->setDefaults(array( + 'one' => '1', + 'two' => '2', + )); + + $this->resolver->setAllowedValues(array( + 'one' => '1', + 'two' => '2', + )); + $this->resolver->addAllowedValues(array( + 'one' => 'one', + 'two' => 'two', + )); + + $options = array( + 'one' => '1', + 'two' => 'two', + ); + + $this->assertEquals(array( + 'one' => '1', + 'two' => 'two', + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionalWithAllowedValuesNotSet() + { + $this->resolver->setRequired(array( + 'one', + )); + + $this->resolver->setOptional(array( + 'two', + )); + + $this->resolver->setAllowedValues(array( + 'one' => array('1', 'one'), + 'two' => array('2', 'two'), + )); + + $options = array( + 'one' => '1', + ); + + $this->assertEquals(array( + 'one' => '1', + ), $this->resolver->resolve($options)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfOptionValueNotAllowed() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedValues(array( + 'one' => array('1', 'one'), + )); + + $this->resolver->resolve(array( + 'one' => '2', + )); + } + + public function testResolveSucceedsIfOptionTypeAllowed() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => 'string', + )); + + $options = array( + 'one' => 'one', + ); + + $this->assertEquals(array( + 'one' => 'one', + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionTypeAllowedPassArray() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => array('string', 'bool'), + )); + + $options = array( + 'one' => true, + ); + + $this->assertEquals(array( + 'one' => true, + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionTypeAllowedPassObject() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => 'object', + )); + + $object = new \stdClass(); + $options = array( + 'one' => $object, + ); + + $this->assertEquals(array( + 'one' => $object, + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionTypeAllowedPassClass() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => '\stdClass', + )); + + $object = new \stdClass(); + $options = array( + 'one' => $object, + ); + + $this->assertEquals(array( + 'one' => $object, + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionTypeAllowedAddTypes() + { + $this->resolver->setDefaults(array( + 'one' => '1', + 'two' => '2', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => 'string', + 'two' => 'bool', + )); + $this->resolver->addAllowedTypes(array( + 'one' => 'float', + 'two' => 'integer', + )); + + $options = array( + 'one' => 1.23, + 'two' => false, + ); + + $this->assertEquals(array( + 'one' => 1.23, + 'two' => false, + ), $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfOptionalWithTypeAndWithoutValue() + { + $this->resolver->setOptional(array( + 'one', + 'two', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => 'string', + 'two' => 'int', + )); + + $options = array( + 'two' => 1, + ); + + $this->assertEquals(array( + 'two' => 1, + ), $this->resolver->resolve($options)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfOptionTypeNotAllowed() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => array('string', 'bool'), + )); + + $this->resolver->resolve(array( + 'one' => 1.23, + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfOptionTypeNotAllowedMultipleOptions() + { + $this->resolver->setDefaults(array( + 'one' => '1', + 'two' => '2', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => 'string', + 'two' => 'bool', + )); + + $this->resolver->resolve(array( + 'one' => 'foo', + 'two' => 1.23, + )); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfOptionTypeNotAllowedAddTypes() + { + $this->resolver->setDefaults(array( + 'one' => '1', + )); + + $this->resolver->setAllowedTypes(array( + 'one' => 'string', + )); + $this->resolver->addAllowedTypes(array( + 'one' => 'bool', + )); + + $this->resolver->resolve(array( + 'one' => 1.23, + )); + } + + public function testFluidInterface() + { + $this->resolver->setDefaults(array('one' => '1')) + ->replaceDefaults(array('one' => '2')) + ->setAllowedValues(array('one' => array('1', '2'))) + ->addAllowedValues(array('one' => array('3'))) + ->setRequired(array('two')) + ->setOptional(array('three')); + + $options = array( + 'two' => '2', + ); + + $this->assertEquals(array( + 'one' => '2', + 'two' => '2', + ), $this->resolver->resolve($options)); + } + + public function testKnownIfDefaultWasSet() + { + $this->assertFalse($this->resolver->isKnown('foo')); + + $this->resolver->setDefaults(array( + 'foo' => 'bar', + )); + + $this->assertTrue($this->resolver->isKnown('foo')); + } + + public function testKnownIfRequired() + { + $this->assertFalse($this->resolver->isKnown('foo')); + + $this->resolver->setRequired(array( + 'foo', + )); + + $this->assertTrue($this->resolver->isKnown('foo')); + } + + public function testKnownIfOptional() + { + $this->assertFalse($this->resolver->isKnown('foo')); + + $this->resolver->setOptional(array( + 'foo', + )); + + $this->assertTrue($this->resolver->isKnown('foo')); + } + + public function testRequiredIfRequired() + { + $this->assertFalse($this->resolver->isRequired('foo')); + + $this->resolver->setRequired(array( + 'foo', + )); + + $this->assertTrue($this->resolver->isRequired('foo')); + } + + public function testNormalizersTransformFinalOptions() + { + $this->resolver->setDefaults(array( + 'foo' => 'bar', + 'bam' => 'baz', + )); + $this->resolver->setNormalizers(array( + 'foo' => function (Options $options, $value) { + return $options['bam'].'['.$value.']'; + }, + )); + + $expected = array( + 'foo' => 'baz[bar]', + 'bam' => 'baz', + ); + + $this->assertEquals($expected, $this->resolver->resolve(array())); + + $expected = array( + 'foo' => 'boo[custom]', + 'bam' => 'boo', + ); + + $this->assertEquals($expected, $this->resolver->resolve(array( + 'foo' => 'custom', + 'bam' => 'boo', + ))); + } + + public function testResolveWithoutOptionSucceedsIfRequiredAndDefaultValue() + { + $this->resolver->setRequired(array( + 'foo', + )); + $this->resolver->setDefaults(array( + 'foo' => 'bar', + )); + + $this->assertEquals(array( + 'foo' => 'bar', + ), $this->resolver->resolve(array())); + } + + public function testResolveWithoutOptionSucceedsIfDefaultValueAndRequired() + { + $this->resolver->setDefaults(array( + 'foo' => 'bar', + )); + $this->resolver->setRequired(array( + 'foo', + )); + + $this->assertEquals(array( + 'foo' => 'bar', + ), $this->resolver->resolve(array())); + } + + public function testResolveSucceedsIfOptionRequiredAndValueAllowed() + { + $this->resolver->setRequired(array( + 'one', 'two', + )); + $this->resolver->setAllowedValues(array( + 'two' => array('2'), + )); + + $options = array( + 'one' => '1', + 'two' => '2', + ); + + $this->assertEquals($options, $this->resolver->resolve($options)); + } + + public function testResolveSucceedsIfValueAllowedCallbackReturnsTrue() + { + $this->resolver->setRequired(array( + 'test', + )); + $this->resolver->setAllowedValues(array( + 'test' => function ($value) { + return true; + }, + )); + + $options = array( + 'test' => true, + ); + + $this->assertEquals($options, $this->resolver->resolve($options)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfValueAllowedCallbackReturnsFalse() + { + $this->resolver->setRequired(array( + 'test', + )); + $this->resolver->setAllowedValues(array( + 'test' => function ($value) { + return false; + }, + )); + + $options = array( + 'test' => true, + ); + + $this->assertEquals($options, $this->resolver->resolve($options)); + } + + public function testClone() + { + $this->resolver->setDefaults(array('one' => '1')); + + $clone = clone $this->resolver; + + // Changes after cloning don't affect each other + $this->resolver->setDefaults(array('two' => '2')); + $clone->setDefaults(array('three' => '3')); + + $this->assertEquals(array( + 'one' => '1', + 'two' => '2', + ), $this->resolver->resolve()); + + $this->assertEquals(array( + 'one' => '1', + 'three' => '3', + ), $clone->resolve()); + } + + public function testOverloadReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->overload('foo', 'bar')); + } + + public function testOverloadCallsSet() + { + $this->resolver->overload('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } +} diff --git a/library/symfony/options-resolver/Tests/LegacyOptionsTest.php b/library/symfony/options-resolver/Tests/LegacyOptionsTest.php new file mode 100644 index 000000000..b65a09002 --- /dev/null +++ b/library/symfony/options-resolver/Tests/LegacyOptionsTest.php @@ -0,0 +1,337 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Tests; + +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @group legacy + */ +class LegacyOptionsTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var OptionsResolver + */ + private $options; + + protected function setUp() + { + $this->options = new OptionsResolver(); + } + + public function testSetLazyOption() + { + $test = $this; + + $this->options->set('foo', function (Options $options) use ($test) { + return 'dynamic'; + }); + + $this->assertEquals(array('foo' => 'dynamic'), $this->options->resolve()); + } + + public function testOverloadKeepsPreviousValue() + { + $test = $this; + + // defined by superclass + $this->options->set('foo', 'bar'); + + // defined by subclass + $this->options->overload('foo', function (Options $options, $previousValue) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals('bar', $previousValue); + + return 'dynamic'; + }); + + $this->assertEquals(array('foo' => 'dynamic'), $this->options->resolve()); + } + + public function testPreviousValueIsEvaluatedIfLazy() + { + $test = $this; + + // defined by superclass + $this->options->set('foo', function (Options $options) { + return 'bar'; + }); + + // defined by subclass + $this->options->overload('foo', function (Options $options, $previousValue) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals('bar', $previousValue); + + return 'dynamic'; + }); + + $this->assertEquals(array('foo' => 'dynamic'), $this->options->resolve()); + } + + public function testPreviousValueIsNotEvaluatedIfNoSecondArgument() + { + $test = $this; + + // defined by superclass + $this->options->set('foo', function (Options $options) use ($test) { + $test->fail('Should not be called'); + }); + + // defined by subclass, no $previousValue argument defined! + $this->options->overload('foo', function (Options $options) use ($test) { + return 'dynamic'; + }); + + $this->assertEquals(array('foo' => 'dynamic'), $this->options->resolve()); + } + + public function testLazyOptionCanAccessOtherOptions() + { + $test = $this; + + $this->options->set('foo', 'bar'); + + $this->options->set('bam', function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals('bar', $options->get('foo')); + + return 'dynamic'; + }); + + $this->assertEquals(array('foo' => 'bar', 'bam' => 'dynamic'), $this->options->resolve()); + } + + public function testLazyOptionCanAccessOtherLazyOptions() + { + $test = $this; + + $this->options->set('foo', function (Options $options) { + return 'bar'; + }); + + $this->options->set('bam', function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals('bar', $options->get('foo')); + + return 'dynamic'; + }); + + $this->assertEquals(array('foo' => 'bar', 'bam' => 'dynamic'), $this->options->resolve()); + } + + public function testNormalizer() + { + $this->options->set('foo', 'bar'); + + $this->options->setNormalizer('foo', function () { + return 'normalized'; + }); + + $this->assertEquals(array('foo' => 'normalized'), $this->options->resolve()); + } + + public function testNormalizerReceivesUnnormalizedValue() + { + $this->options->set('foo', 'bar'); + + $this->options->setNormalizer('foo', function (Options $options, $value) { + return 'normalized['.$value.']'; + }); + + $this->assertEquals(array('foo' => 'normalized[bar]'), $this->options->resolve()); + } + + public function testNormalizerCanAccessOtherOptions() + { + $test = $this; + + $this->options->set('foo', 'bar'); + $this->options->set('bam', 'baz'); + + $this->options->setNormalizer('bam', function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals('bar', $options->get('foo')); + + return 'normalized'; + }); + + $this->assertEquals(array('foo' => 'bar', 'bam' => 'normalized'), $this->options->resolve()); + } + + public function testNormalizerCanAccessOtherLazyOptions() + { + $test = $this; + + $this->options->set('foo', function (Options $options) { + return 'bar'; + }); + $this->options->set('bam', 'baz'); + + $this->options->setNormalizer('bam', function (Options $options) use ($test) { + /* @var \PHPUnit_Framework_TestCase $test */ + $test->assertEquals('bar', $options->get('foo')); + + return 'normalized'; + }); + + $this->assertEquals(array('foo' => 'bar', 'bam' => 'normalized'), $this->options->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailForCyclicDependencies() + { + $this->options->set('foo', function (Options $options) { + $options->get('bam'); + }); + + $this->options->set('bam', function (Options $options) { + $options->get('foo'); + }); + + $this->options->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailForCyclicDependenciesBetweenNormalizers() + { + $this->options->set('foo', 'bar'); + $this->options->set('bam', 'baz'); + + $this->options->setNormalizer('foo', function (Options $options) { + $options->get('bam'); + }); + + $this->options->setNormalizer('bam', function (Options $options) { + $options->get('foo'); + }); + + $this->options->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailForCyclicDependenciesBetweenNormalizerAndLazyOption() + { + $this->options->set('foo', function (Options $options) { + $options->get('bam'); + }); + $this->options->set('bam', 'baz'); + + $this->options->setNormalizer('bam', function (Options $options) { + $options->get('foo'); + }); + + $this->options->resolve(); + } + + public function testReplaceClearsAndSets() + { + $this->options->set('one', '1'); + + $this->options->replace(array( + 'two' => '2', + 'three' => function (Options $options) { + return '2' === $options['two'] ? '3' : 'foo'; + }, + )); + + $this->assertEquals(array( + 'two' => '2', + 'three' => '3', + ), $this->options->resolve()); + } + + public function testClearRemovesAllOptions() + { + $this->options->set('one', 1); + $this->options->set('two', 2); + + $this->options->clear(); + + $this->assertEmpty($this->options->resolve()); + } + + public function testOverloadCannotBeEvaluatedLazilyWithoutExpectedClosureParams() + { + $this->options->set('foo', 'bar'); + + $this->options->overload('foo', function () { + return 'test'; + }); + + $resolved = $this->options->resolve(); + $this->assertTrue(is_callable($resolved['foo'])); + } + + public function testOverloadCannotBeEvaluatedLazilyWithoutFirstParamTypeHint() + { + $this->options->set('foo', 'bar'); + + $this->options->overload('foo', function ($object) { + return 'test'; + }); + + $resolved = $this->options->resolve(); + $this->assertTrue(is_callable($resolved['foo'])); + } + + public function testRemoveOptionAndNormalizer() + { + $this->options->set('foo1', 'bar'); + $this->options->setNormalizer('foo1', function (Options $options) { + return ''; + }); + $this->options->set('foo2', 'bar'); + $this->options->setNormalizer('foo2', function (Options $options) { + return ''; + }); + + $this->options->remove('foo2'); + $this->assertEquals(array('foo1' => ''), $this->options->resolve()); + } + + public function testReplaceOptionAndNormalizer() + { + $this->options->set('foo1', 'bar'); + $this->options->setNormalizer('foo1', function (Options $options) { + return ''; + }); + $this->options->set('foo2', 'bar'); + $this->options->setNormalizer('foo2', function (Options $options) { + return ''; + }); + + $this->options->replace(array('foo1' => 'new')); + $this->assertEquals(array('foo1' => 'new'), $this->options->resolve()); + } + + public function testClearOptionAndNormalizer() + { + $this->options->set('foo1', 'bar'); + $this->options->setNormalizer('foo1', function (Options $options) { + return ''; + }); + $this->options->set('foo2', 'bar'); + $this->options->setNormalizer('foo2', function (Options $options) { + return ''; + }); + + $this->options->clear(); + $this->assertEmpty($this->options->resolve()); + } +} diff --git a/library/symfony/options-resolver/Tests/OptionsResolver2Dot6Test.php b/library/symfony/options-resolver/Tests/OptionsResolver2Dot6Test.php new file mode 100644 index 000000000..9158c5ba0 --- /dev/null +++ b/library/symfony/options-resolver/Tests/OptionsResolver2Dot6Test.php @@ -0,0 +1,1550 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\OptionsResolver\Tests; + +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class OptionsResolver2Dot6Test extends \PHPUnit_Framework_TestCase +{ + /** + * @var OptionsResolver + */ + private $resolver; + + protected function setUp() + { + $this->resolver = new OptionsResolver(); + } + + //////////////////////////////////////////////////////////////////////////// + // resolve() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + * @expectedExceptionMessage The option "foo" does not exist. Defined options are: "a", "z". + */ + public function testResolveFailsIfNonExistingOption() + { + $this->resolver->setDefault('z', '1'); + $this->resolver->setDefault('a', '2'); + + $this->resolver->resolve(array('foo' => 'bar')); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + * @expectedExceptionMessage The options "baz", "foo", "ping" do not exist. Defined options are: "a", "z". + */ + public function testResolveFailsIfMultipleNonExistingOptions() + { + $this->resolver->setDefault('z', '1'); + $this->resolver->setDefault('a', '2'); + + $this->resolver->resolve(array('ping' => 'pong', 'foo' => 'bar', 'baz' => 'bam')); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testResolveFailsFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->resolve(array()); + }); + + $this->resolver->resolve(); + } + + //////////////////////////////////////////////////////////////////////////// + // setDefault()/hasDefault() + //////////////////////////////////////////////////////////////////////////// + + public function testSetDefaultReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->setDefault('foo', 'bar')); + } + + public function testSetDefault() + { + $this->resolver->setDefault('one', '1'); + $this->resolver->setDefault('two', '20'); + + $this->assertEquals(array( + 'one' => '1', + 'two' => '20', + ), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetDefaultFromLazyOption() + { + $this->resolver->setDefault('lazy', function (Options $options) { + $options->setDefault('default', 42); + }); + + $this->resolver->resolve(); + } + + public function testHasDefault() + { + $this->assertFalse($this->resolver->hasDefault('foo')); + $this->resolver->setDefault('foo', 42); + $this->assertTrue($this->resolver->hasDefault('foo')); + } + + public function testHasDefaultWithNullValue() + { + $this->assertFalse($this->resolver->hasDefault('foo')); + $this->resolver->setDefault('foo', null); + $this->assertTrue($this->resolver->hasDefault('foo')); + } + + //////////////////////////////////////////////////////////////////////////// + // lazy setDefault() + //////////////////////////////////////////////////////////////////////////// + + public function testSetLazyReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->setDefault('foo', function (Options $options) {})); + } + + public function testSetLazyClosure() + { + $this->resolver->setDefault('foo', function (Options $options) { + return 'lazy'; + }); + + $this->assertEquals(array('foo' => 'lazy'), $this->resolver->resolve()); + } + + public function testClosureWithoutTypeHintNotInvoked() + { + $closure = function ($options) { + \PHPUnit_Framework_Assert::fail('Should not be called'); + }; + + $this->resolver->setDefault('foo', $closure); + + $this->assertSame(array('foo' => $closure), $this->resolver->resolve()); + } + + public function testClosureWithoutParametersNotInvoked() + { + $closure = function () { + \PHPUnit_Framework_Assert::fail('Should not be called'); + }; + + $this->resolver->setDefault('foo', $closure); + + $this->assertSame(array('foo' => $closure), $this->resolver->resolve()); + } + + public function testAccessPreviousDefaultValue() + { + // defined by superclass + $this->resolver->setDefault('foo', 'bar'); + + // defined by subclass + $this->resolver->setDefault('foo', function (Options $options, $previousValue) { + \PHPUnit_Framework_Assert::assertEquals('bar', $previousValue); + + return 'lazy'; + }); + + $this->assertEquals(array('foo' => 'lazy'), $this->resolver->resolve()); + } + + public function testAccessPreviousLazyDefaultValue() + { + // defined by superclass + $this->resolver->setDefault('foo', function (Options $options) { + return 'bar'; + }); + + // defined by subclass + $this->resolver->setDefault('foo', function (Options $options, $previousValue) { + \PHPUnit_Framework_Assert::assertEquals('bar', $previousValue); + + return 'lazy'; + }); + + $this->assertEquals(array('foo' => 'lazy'), $this->resolver->resolve()); + } + + public function testPreviousValueIsNotEvaluatedIfNoSecondArgument() + { + // defined by superclass + $this->resolver->setDefault('foo', function () { + \PHPUnit_Framework_Assert::fail('Should not be called'); + }); + + // defined by subclass, no $previousValue argument defined! + $this->resolver->setDefault('foo', function (Options $options) { + return 'lazy'; + }); + + $this->assertEquals(array('foo' => 'lazy'), $this->resolver->resolve()); + } + + public function testOverwrittenLazyOptionNotEvaluated() + { + $this->resolver->setDefault('foo', function (Options $options) { + \PHPUnit_Framework_Assert::fail('Should not be called'); + }); + + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testInvokeEachLazyOptionOnlyOnce() + { + $calls = 0; + + $this->resolver->setDefault('lazy1', function (Options $options) use (&$calls) { + \PHPUnit_Framework_Assert::assertSame(1, ++$calls); + + $options['lazy2']; + }); + + $this->resolver->setDefault('lazy2', function (Options $options) use (&$calls) { + \PHPUnit_Framework_Assert::assertSame(2, ++$calls); + }); + + $this->resolver->resolve(); + + $this->assertSame(2, $calls); + } + + //////////////////////////////////////////////////////////////////////////// + // setRequired()/isRequired()/getRequiredOptions() + //////////////////////////////////////////////////////////////////////////// + + public function testSetRequiredReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->setRequired('foo')); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetRequiredFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setRequired('bar'); + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException + */ + public function testResolveFailsIfRequiredOptionMissing() + { + $this->resolver->setRequired('foo'); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfRequiredOptionSet() + { + $this->resolver->setRequired('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + public function testResolveSucceedsIfRequiredOptionPassed() + { + $this->resolver->setRequired('foo'); + + $this->assertNotEmpty($this->resolver->resolve(array('foo' => 'bar'))); + } + + public function testIsRequired() + { + $this->assertFalse($this->resolver->isRequired('foo')); + $this->resolver->setRequired('foo'); + $this->assertTrue($this->resolver->isRequired('foo')); + } + + public function testRequiredIfSetBefore() + { + $this->assertFalse($this->resolver->isRequired('foo')); + + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setRequired('foo'); + + $this->assertTrue($this->resolver->isRequired('foo')); + } + + public function testStillRequiredAfterSet() + { + $this->assertFalse($this->resolver->isRequired('foo')); + + $this->resolver->setRequired('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertTrue($this->resolver->isRequired('foo')); + } + + public function testIsNotRequiredAfterRemove() + { + $this->assertFalse($this->resolver->isRequired('foo')); + $this->resolver->setRequired('foo'); + $this->resolver->remove('foo'); + $this->assertFalse($this->resolver->isRequired('foo')); + } + + public function testIsNotRequiredAfterClear() + { + $this->assertFalse($this->resolver->isRequired('foo')); + $this->resolver->setRequired('foo'); + $this->resolver->clear(); + $this->assertFalse($this->resolver->isRequired('foo')); + } + + public function testGetRequiredOptions() + { + $this->resolver->setRequired(array('foo', 'bar')); + $this->resolver->setDefault('bam', 'baz'); + $this->resolver->setDefault('foo', 'boo'); + + $this->assertSame(array('foo', 'bar'), $this->resolver->getRequiredOptions()); + } + + //////////////////////////////////////////////////////////////////////////// + // isMissing()/getMissingOptions() + //////////////////////////////////////////////////////////////////////////// + + public function testIsMissingIfNotSet() + { + $this->assertFalse($this->resolver->isMissing('foo')); + $this->resolver->setRequired('foo'); + $this->assertTrue($this->resolver->isMissing('foo')); + } + + public function testIsNotMissingIfSet() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->assertFalse($this->resolver->isMissing('foo')); + $this->resolver->setRequired('foo'); + $this->assertFalse($this->resolver->isMissing('foo')); + } + + public function testIsNotMissingAfterRemove() + { + $this->resolver->setRequired('foo'); + $this->resolver->remove('foo'); + $this->assertFalse($this->resolver->isMissing('foo')); + } + + public function testIsNotMissingAfterClear() + { + $this->resolver->setRequired('foo'); + $this->resolver->clear(); + $this->assertFalse($this->resolver->isRequired('foo')); + } + + public function testGetMissingOptions() + { + $this->resolver->setRequired(array('foo', 'bar')); + $this->resolver->setDefault('bam', 'baz'); + $this->resolver->setDefault('foo', 'boo'); + + $this->assertSame(array('bar'), $this->resolver->getMissingOptions()); + } + + //////////////////////////////////////////////////////////////////////////// + // setDefined()/isDefined()/getDefinedOptions() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetDefinedFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setDefined('bar'); + }); + + $this->resolver->resolve(); + } + + public function testDefinedOptionsNotIncludedInResolvedOptions() + { + $this->resolver->setDefined('foo'); + + $this->assertSame(array(), $this->resolver->resolve()); + } + + public function testDefinedOptionsIncludedIfDefaultSetBefore() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setDefined('foo'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testDefinedOptionsIncludedIfDefaultSetAfter() + { + $this->resolver->setDefined('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testDefinedOptionsIncludedIfPassedToResolve() + { + $this->resolver->setDefined('foo'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve(array('foo' => 'bar'))); + } + + public function testIsDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setDefined('foo'); + $this->assertTrue($this->resolver->isDefined('foo')); + } + + public function testLazyOptionsAreDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setDefault('foo', function (Options $options) {}); + $this->assertTrue($this->resolver->isDefined('foo')); + } + + public function testRequiredOptionsAreDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setRequired('foo'); + $this->assertTrue($this->resolver->isDefined('foo')); + } + + public function testSetOptionsAreDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setDefault('foo', 'bar'); + $this->assertTrue($this->resolver->isDefined('foo')); + } + + public function testGetDefinedOptions() + { + $this->resolver->setDefined(array('foo', 'bar')); + $this->resolver->setDefault('baz', 'bam'); + $this->resolver->setRequired('boo'); + + $this->assertSame(array('foo', 'bar', 'baz', 'boo'), $this->resolver->getDefinedOptions()); + } + + public function testRemovedOptionsAreNotDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setDefined('foo'); + $this->assertTrue($this->resolver->isDefined('foo')); + $this->resolver->remove('foo'); + $this->assertFalse($this->resolver->isDefined('foo')); + } + + public function testClearedOptionsAreNotDefined() + { + $this->assertFalse($this->resolver->isDefined('foo')); + $this->resolver->setDefined('foo'); + $this->assertTrue($this->resolver->isDefined('foo')); + $this->resolver->clear(); + $this->assertFalse($this->resolver->isDefined('foo')); + } + + //////////////////////////////////////////////////////////////////////////// + // setAllowedTypes() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testSetAllowedTypesFailsIfUnknownOption() + { + $this->resolver->setAllowedTypes('foo', 'string'); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetAllowedTypesFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setAllowedTypes('bar', 'string'); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + /** + * @dataProvider provideInvalidTypes + */ + public function testResolveFailsIfInvalidType($actualType, $allowedType, $exceptionMessage) + { + $this->resolver->setDefined('option'); + $this->resolver->setAllowedTypes('option', $allowedType); + $this->setExpectedException('Symfony\Component\OptionsResolver\Exception\InvalidOptionsException', $exceptionMessage); + $this->resolver->resolve(array('option' => $actualType)); + } + + public function provideInvalidTypes() + { + return array( + array(true, 'string', 'The option "option" with value true is expected to be of type "string", but is of type "boolean".'), + array(false, 'string', 'The option "option" with value false is expected to be of type "string", but is of type "boolean".'), + array(fopen(__FILE__, 'r'), 'string', 'The option "option" with value resource is expected to be of type "string", but is of type "resource".'), + array(array(), 'string', 'The option "option" with value array is expected to be of type "string", but is of type "array".'), + array(new OptionsResolver(), 'string', 'The option "option" with value Symfony\Component\OptionsResolver\OptionsResolver is expected to be of type "string", but is of type "Symfony\Component\OptionsResolver\OptionsResolver".'), + array(42, 'string', 'The option "option" with value 42 is expected to be of type "string", but is of type "integer".'), + array(null, 'string', 'The option "option" with value null is expected to be of type "string", but is of type "NULL".'), + array('bar', '\stdClass', 'The option "option" with value "bar" is expected to be of type "\stdClass", but is of type "string".'), + ); + } + + public function testResolveSucceedsIfValidType() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'string'); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value 42 is expected to be of type "string" or "bool", but is of type "integer". + */ + public function testResolveFailsIfInvalidTypeMultiple() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedTypes('foo', array('string', 'bool')); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidTypeMultiple() + { + $this->resolver->setDefault('foo', true); + $this->resolver->setAllowedTypes('foo', array('string', 'bool')); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + public function testResolveSucceedsIfInstanceOfClass() + { + $this->resolver->setDefault('foo', new \stdClass()); + $this->resolver->setAllowedTypes('foo', '\stdClass'); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // addAllowedTypes() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testAddAllowedTypesFailsIfUnknownOption() + { + $this->resolver->addAllowedTypes('foo', 'string'); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfAddAllowedTypesFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->addAllowedTypes('bar', 'string'); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidAddedType() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->addAllowedTypes('foo', 'string'); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidAddedType() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->addAllowedTypes('foo', 'string'); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidAddedTypeMultiple() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->addAllowedTypes('foo', array('string', 'bool')); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidAddedTypeMultiple() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->addAllowedTypes('foo', array('string', 'bool')); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + public function testAddAllowedTypesDoesNotOverwrite() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'string'); + $this->resolver->addAllowedTypes('foo', 'bool'); + + $this->resolver->setDefault('foo', 'bar'); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + public function testAddAllowedTypesDoesNotOverwrite2() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'string'); + $this->resolver->addAllowedTypes('foo', 'bool'); + + $this->resolver->setDefault('foo', false); + + $this->assertNotEmpty($this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // setAllowedValues() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testSetAllowedValuesFailsIfUnknownOption() + { + $this->resolver->setAllowedValues('foo', 'bar'); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetAllowedValuesFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setAllowedValues('bar', 'baz'); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value 42 is invalid. Accepted values are: "bar". + */ + public function testResolveFailsIfInvalidValue() + { + $this->resolver->setDefined('foo'); + $this->resolver->setAllowedValues('foo', 'bar'); + + $this->resolver->resolve(array('foo' => 42)); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value null is invalid. Accepted values are: "bar". + */ + public function testResolveFailsIfInvalidValueIsNull() + { + $this->resolver->setDefault('foo', null); + $this->resolver->setAllowedValues('foo', 'bar'); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidValueStrict() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', '42'); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidValue() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', 'bar'); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testResolveSucceedsIfValidValueIsNull() + { + $this->resolver->setDefault('foo', null); + $this->resolver->setAllowedValues('foo', null); + + $this->assertEquals(array('foo' => null), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + * @expectedExceptionMessage The option "foo" with value 42 is invalid. Accepted values are: "bar", false, null. + */ + public function testResolveFailsIfInvalidValueMultiple() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', array('bar', false, null)); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidValueMultiple() + { + $this->resolver->setDefault('foo', 'baz'); + $this->resolver->setAllowedValues('foo', array('bar', 'baz')); + + $this->assertEquals(array('foo' => 'baz'), $this->resolver->resolve()); + } + + public function testResolveFailsIfClosureReturnsFalse() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', function ($value) use (&$passedValue) { + $passedValue = $value; + + return false; + }); + + try { + $this->resolver->resolve(); + $this->fail('Should fail'); + } catch (InvalidOptionsException $e) { + } + + $this->assertSame(42, $passedValue); + } + + public function testResolveSucceedsIfClosureReturnsTrue() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', function ($value) use (&$passedValue) { + $passedValue = $value; + + return true; + }); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + $this->assertSame('bar', $passedValue); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfAllClosuresReturnFalse() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', array( + function () { return false; }, + function () { return false; }, + function () { return false; }, + )); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfAnyClosureReturnsTrue() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', array( + function () { return false; }, + function () { return true; }, + function () { return false; }, + )); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // addAllowedValues() + //////////////////////////////////////////////////////////////////////////// + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testAddAllowedValuesFailsIfUnknownOption() + { + $this->resolver->addAllowedValues('foo', 'bar'); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfAddAllowedValuesFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->addAllowedValues('bar', 'baz'); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidAddedValue() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->addAllowedValues('foo', 'bar'); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidAddedValue() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->addAllowedValues('foo', 'bar'); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testResolveSucceedsIfValidAddedValueIsNull() + { + $this->resolver->setDefault('foo', null); + $this->resolver->addAllowedValues('foo', null); + + $this->assertEquals(array('foo' => null), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfInvalidAddedValueMultiple() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->addAllowedValues('foo', array('bar', 'baz')); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfValidAddedValueMultiple() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->addAllowedValues('foo', array('bar', 'baz')); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testAddAllowedValuesDoesNotOverwrite() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', 'bar'); + $this->resolver->addAllowedValues('foo', 'baz'); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testAddAllowedValuesDoesNotOverwrite2() + { + $this->resolver->setDefault('foo', 'baz'); + $this->resolver->setAllowedValues('foo', 'bar'); + $this->resolver->addAllowedValues('foo', 'baz'); + + $this->assertEquals(array('foo' => 'baz'), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testResolveFailsIfAllAddedClosuresReturnFalse() + { + $this->resolver->setDefault('foo', 42); + $this->resolver->setAllowedValues('foo', function () { return false; }); + $this->resolver->addAllowedValues('foo', function () { return false; }); + + $this->resolver->resolve(); + } + + public function testResolveSucceedsIfAnyAddedClosureReturnsTrue() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', function () { return false; }); + $this->resolver->addAllowedValues('foo', function () { return true; }); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testResolveSucceedsIfAnyAddedClosureReturnsTrue2() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', function () { return true; }); + $this->resolver->addAllowedValues('foo', function () { return false; }); + + $this->assertEquals(array('foo' => 'bar'), $this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // setNormalizer() + //////////////////////////////////////////////////////////////////////////// + + public function testSetNormalizerReturnsThis() + { + $this->resolver->setDefault('foo', 'bar'); + $this->assertSame($this->resolver, $this->resolver->setNormalizer('foo', function () {})); + } + + public function testSetNormalizerClosure() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setNormalizer('foo', function () { + return 'normalized'; + }); + + $this->assertEquals(array('foo' => 'normalized'), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException + */ + public function testSetNormalizerFailsIfUnknownOption() + { + $this->resolver->setNormalizer('foo', function () {}); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetNormalizerFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setNormalizer('foo', function () {}); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + public function testNormalizerReceivesSetOption() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->setNormalizer('foo', function (Options $options, $value) { + return 'normalized['.$value.']'; + }); + + $this->assertEquals(array('foo' => 'normalized[bar]'), $this->resolver->resolve()); + } + + public function testNormalizerReceivesPassedOption() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->setNormalizer('foo', function (Options $options, $value) { + return 'normalized['.$value.']'; + }); + + $resolved = $this->resolver->resolve(array('foo' => 'baz')); + + $this->assertEquals(array('foo' => 'normalized[baz]'), $resolved); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testValidateTypeBeforeNormalization() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->setAllowedTypes('foo', 'int'); + + $this->resolver->setNormalizer('foo', function () { + \PHPUnit_Framework_Assert::fail('Should not be called.'); + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException + */ + public function testValidateValueBeforeNormalization() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->setAllowedValues('foo', 'baz'); + + $this->resolver->setNormalizer('foo', function () { + \PHPUnit_Framework_Assert::fail('Should not be called.'); + }); + + $this->resolver->resolve(); + } + + public function testNormalizerCanAccessOtherOptions() + { + $this->resolver->setDefault('default', 'bar'); + $this->resolver->setDefault('norm', 'baz'); + + $this->resolver->setNormalizer('norm', function (Options $options) { + /* @var \PHPUnit_Framework_TestCase $test */ + \PHPUnit_Framework_Assert::assertSame('bar', $options['default']); + + return 'normalized'; + }); + + $this->assertEquals(array( + 'default' => 'bar', + 'norm' => 'normalized', + ), $this->resolver->resolve()); + } + + public function testNormalizerCanAccessLazyOptions() + { + $this->resolver->setDefault('lazy', function (Options $options) { + return 'bar'; + }); + $this->resolver->setDefault('norm', 'baz'); + + $this->resolver->setNormalizer('norm', function (Options $options) { + /* @var \PHPUnit_Framework_TestCase $test */ + \PHPUnit_Framework_Assert::assertEquals('bar', $options['lazy']); + + return 'normalized'; + }); + + $this->assertEquals(array( + 'lazy' => 'bar', + 'norm' => 'normalized', + ), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailIfCyclicDependencyBetweenNormalizers() + { + $this->resolver->setDefault('norm1', 'bar'); + $this->resolver->setDefault('norm2', 'baz'); + + $this->resolver->setNormalizer('norm1', function (Options $options) { + $options['norm2']; + }); + + $this->resolver->setNormalizer('norm2', function (Options $options) { + $options['norm1']; + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailIfCyclicDependencyBetweenNormalizerAndLazyOption() + { + $this->resolver->setDefault('lazy', function (Options $options) { + $options['norm']; + }); + + $this->resolver->setDefault('norm', 'baz'); + + $this->resolver->setNormalizer('norm', function (Options $options) { + $options['lazy']; + }); + + $this->resolver->resolve(); + } + + public function testCatchedExceptionFromNormalizerDoesNotCrashOptionResolver() + { + $throw = true; + + $this->resolver->setDefaults(array('catcher' => null, 'thrower' => null)); + + $this->resolver->setNormalizer('catcher', function (Options $options) { + try { + return $options['thrower']; + } catch(\Exception $e) { + return false; + } + }); + + $this->resolver->setNormalizer('thrower', function (Options $options) use (&$throw) { + if ($throw) { + $throw = false; + throw new \UnexpectedValueException('throwing'); + } + + return true; + }); + + $this->resolver->resolve(); + } + + public function testCatchedExceptionFromLazyDoesNotCrashOptionResolver() + { + $throw = true; + + $this->resolver->setDefault('catcher', function (Options $options) { + try { + return $options['thrower']; + } catch(\Exception $e) { + return false; + } + }); + + $this->resolver->setDefault('thrower', function (Options $options) use (&$throw) { + if ($throw) { + $throw = false; + throw new \UnexpectedValueException('throwing'); + } + + return true; + }); + + $this->resolver->resolve(); + } + + public function testInvokeEachNormalizerOnlyOnce() + { + $calls = 0; + + $this->resolver->setDefault('norm1', 'bar'); + $this->resolver->setDefault('norm2', 'baz'); + + $this->resolver->setNormalizer('norm1', function ($options) use (&$calls) { + \PHPUnit_Framework_Assert::assertSame(1, ++$calls); + + $options['norm2']; + }); + $this->resolver->setNormalizer('norm2', function () use (&$calls) { + \PHPUnit_Framework_Assert::assertSame(2, ++$calls); + }); + + $this->resolver->resolve(); + + $this->assertSame(2, $calls); + } + + public function testNormalizerNotCalledForUnsetOptions() + { + $this->resolver->setDefined('norm'); + + $this->resolver->setNormalizer('norm', function () { + \PHPUnit_Framework_Assert::fail('Should not be called.'); + }); + + $this->assertEmpty($this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // setDefaults() + //////////////////////////////////////////////////////////////////////////// + + public function testSetDefaultsReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->setDefaults(array('foo', 'bar'))); + } + + public function testSetDefaults() + { + $this->resolver->setDefault('one', '1'); + $this->resolver->setDefault('two', 'bar'); + + $this->resolver->setDefaults(array( + 'two' => '2', + 'three' => '3', + )); + + $this->assertEquals(array( + 'one' => '1', + 'two' => '2', + 'three' => '3', + ), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfSetDefaultsFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->setDefaults(array('two' => '2')); + }); + + $this->resolver->resolve(); + } + + //////////////////////////////////////////////////////////////////////////// + // remove() + //////////////////////////////////////////////////////////////////////////// + + public function testRemoveReturnsThis() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame($this->resolver, $this->resolver->remove('foo')); + } + + public function testRemoveSingleOption() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setDefault('baz', 'boo'); + $this->resolver->remove('foo'); + + $this->assertSame(array('baz' => 'boo'), $this->resolver->resolve()); + } + + public function testRemoveMultipleOptions() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setDefault('baz', 'boo'); + $this->resolver->setDefault('doo', 'dam'); + + $this->resolver->remove(array('foo', 'doo')); + + $this->assertSame(array('baz' => 'boo'), $this->resolver->resolve()); + } + + public function testRemoveLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + return 'lazy'; + }); + $this->resolver->remove('foo'); + + $this->assertSame(array(), $this->resolver->resolve()); + } + + public function testRemoveNormalizer() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setNormalizer('foo', function (Options $options, $value) { + return 'normalized'; + }); + $this->resolver->remove('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testRemoveAllowedTypes() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'int'); + $this->resolver->remove('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testRemoveAllowedValues() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', array('baz', 'boo')); + $this->resolver->remove('foo'); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfRemoveFromLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->remove('bar'); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + public function testRemoveUnknownOptionIgnored() + { + $this->assertNotNull($this->resolver->remove('foo')); + } + + //////////////////////////////////////////////////////////////////////////// + // clear() + //////////////////////////////////////////////////////////////////////////// + + public function testClearReturnsThis() + { + $this->assertSame($this->resolver, $this->resolver->clear()); + } + + public function testClearRemovesAllOptions() + { + $this->resolver->setDefault('one', 1); + $this->resolver->setDefault('two', 2); + + $this->resolver->clear(); + + $this->assertEmpty($this->resolver->resolve()); + } + + public function testClearLazyOption() + { + $this->resolver->setDefault('foo', function (Options $options) { + return 'lazy'; + }); + $this->resolver->clear(); + + $this->assertSame(array(), $this->resolver->resolve()); + } + + public function testClearNormalizer() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setNormalizer('foo', function (Options $options, $value) { + return 'normalized'; + }); + $this->resolver->clear(); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testClearAllowedTypes() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedTypes('foo', 'int'); + $this->resolver->clear(); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + public function testClearAllowedValues() + { + $this->resolver->setDefault('foo', 'bar'); + $this->resolver->setAllowedValues('foo', 'baz'); + $this->resolver->clear(); + $this->resolver->setDefault('foo', 'bar'); + + $this->assertSame(array('foo' => 'bar'), $this->resolver->resolve()); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testFailIfClearFromLazyption() + { + $this->resolver->setDefault('foo', function (Options $options) { + $options->clear(); + }); + + $this->resolver->setDefault('bar', 'baz'); + + $this->resolver->resolve(); + } + + public function testClearOptionAndNormalizer() + { + $this->resolver->setDefault('foo1', 'bar'); + $this->resolver->setNormalizer('foo1', function (Options $options) { + return ''; + }); + $this->resolver->setDefault('foo2', 'bar'); + $this->resolver->setNormalizer('foo2', function (Options $options) { + return ''; + }); + + $this->resolver->clear(); + $this->assertEmpty($this->resolver->resolve()); + } + + //////////////////////////////////////////////////////////////////////////// + // ArrayAccess + //////////////////////////////////////////////////////////////////////////// + + public function testArrayAccess() + { + $this->resolver->setDefault('default1', 0); + $this->resolver->setDefault('default2', 1); + $this->resolver->setRequired('required'); + $this->resolver->setDefined('defined'); + $this->resolver->setDefault('lazy1', function (Options $options) { + return 'lazy'; + }); + + $this->resolver->setDefault('lazy2', function (Options $options) { + \PHPUnit_Framework_Assert::assertTrue(isset($options['default1'])); + \PHPUnit_Framework_Assert::assertTrue(isset($options['default2'])); + \PHPUnit_Framework_Assert::assertTrue(isset($options['required'])); + \PHPUnit_Framework_Assert::assertTrue(isset($options['lazy1'])); + \PHPUnit_Framework_Assert::assertTrue(isset($options['lazy2'])); + \PHPUnit_Framework_Assert::assertFalse(isset($options['defined'])); + + \PHPUnit_Framework_Assert::assertSame(0, $options['default1']); + \PHPUnit_Framework_Assert::assertSame(42, $options['default2']); + \PHPUnit_Framework_Assert::assertSame('value', $options['required']); + \PHPUnit_Framework_Assert::assertSame('lazy', $options['lazy1']); + + // Obviously $options['lazy'] and $options['defined'] cannot be + // accessed + }); + + $this->resolver->resolve(array('default2' => 42, 'required' => 'value')); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testArrayAccessGetFailsOutsideResolve() + { + $this->resolver->setDefault('default', 0); + + $this->resolver['default']; + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testArrayAccessExistsFailsOutsideResolve() + { + $this->resolver->setDefault('default', 0); + + isset($this->resolver['default']); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testArrayAccessSetNotSupported() + { + $this->resolver['default'] = 0; + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testArrayAccessUnsetNotSupported() + { + $this->resolver->setDefault('default', 0); + + unset($this->resolver['default']); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\NoSuchOptionException + * @expectedExceptionMessage The option "undefined" does not exist. Defined options are: "foo", "lazy". + */ + public function testFailIfGetNonExisting() + { + $this->resolver->setDefault('foo', 'bar'); + + $this->resolver->setDefault('lazy', function (Options $options) { + $options['undefined']; + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\NoSuchOptionException + * @expectedExceptionMessage The optional option "defined" has no value set. You should make sure it is set with "isset" before reading it. + */ + public function testFailIfGetDefinedButUnset() + { + $this->resolver->setDefined('defined'); + + $this->resolver->setDefault('lazy', function (Options $options) { + $options['defined']; + }); + + $this->resolver->resolve(); + } + + /** + * @expectedException \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException + */ + public function testFailIfCyclicDependency() + { + $this->resolver->setDefault('lazy1', function (Options $options) { + $options['lazy2']; + }); + + $this->resolver->setDefault('lazy2', function (Options $options) { + $options['lazy1']; + }); + + $this->resolver->resolve(); + } + + //////////////////////////////////////////////////////////////////////////// + // Countable + //////////////////////////////////////////////////////////////////////////// + + public function testCount() + { + $this->resolver->setDefault('default', 0); + $this->resolver->setRequired('required'); + $this->resolver->setDefined('defined'); + $this->resolver->setDefault('lazy1', function () {}); + + $this->resolver->setDefault('lazy2', function (Options $options) { + \PHPUnit_Framework_Assert::assertCount(4, $options); + }); + + $this->assertCount(4, $this->resolver->resolve(array('required' => 'value'))); + } + + /** + * In resolve() we count the options that are actually set (which may be + * only a subset of the defined options). Outside of resolve(), it's not + * clear what is counted. + * + * @expectedException \Symfony\Component\OptionsResolver\Exception\AccessException + */ + public function testCountFailsOutsideResolve() + { + $this->resolver->setDefault('foo', 0); + $this->resolver->setRequired('bar'); + $this->resolver->setDefined('bar'); + $this->resolver->setDefault('lazy1', function () {}); + + count($this->resolver); + } +} diff --git a/library/symfony/options-resolver/phpunit.xml.dist b/library/symfony/options-resolver/phpunit.xml.dist new file mode 100644 index 000000000..abf84614b --- /dev/null +++ b/library/symfony/options-resolver/phpunit.xml.dist @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd" + backupGlobals="false" + colors="true" + bootstrap="vendor/autoload.php" +> + <php> + <ini name="error_reporting" value="-1" /> + </php> + + <testsuites> + <testsuite name="Symfony OptionsResolver Component Test Suite"> + <directory>./Tests/</directory> + </testsuite> + </testsuites> + + <filter> + <whitelist> + <directory>./</directory> + <exclude> + <directory>./Resources</directory> + <directory>./Tests</directory> + <directory>./vendor</directory> + </exclude> + </whitelist> + </filter> +</phpunit> diff --git a/library/symfony/process/CHANGELOG.md b/library/symfony/process/CHANGELOG.md new file mode 100644 index 000000000..2f3c1beb7 --- /dev/null +++ b/library/symfony/process/CHANGELOG.md @@ -0,0 +1,40 @@ +CHANGELOG +========= + +2.5.0 +----- + + * added support for PTY mode + * added the convenience method "mustRun" + * deprecation: Process::setStdin() is deprecated in favor of Process::setInput() + * deprecation: Process::getStdin() is deprecated in favor of Process::getInput() + * deprecation: Process::setInput() and ProcessBuilder::setInput() do not accept non-scalar types + +2.4.0 +----- + + * added the ability to define an idle timeout + +2.3.0 +----- + + * added ProcessUtils::escapeArgument() to fix the bug in escapeshellarg() function on Windows + * added Process::signal() + * added Process::getPid() + * added support for a TTY mode + +2.2.0 +----- + + * added ProcessBuilder::setArguments() to reset the arguments on a builder + * added a way to retrieve the standard and error output incrementally + * added Process:restart() + +2.1.0 +----- + + * added support for non-blocking processes (start(), wait(), isRunning(), stop()) + * enhanced Windows compatibility + * added Process::getExitCodeText() that returns a string representation for + the exit code returned by the process + * added ProcessBuilder diff --git a/library/symfony/process/Exception/ExceptionInterface.php b/library/symfony/process/Exception/ExceptionInterface.php new file mode 100644 index 000000000..75c1c9e5d --- /dev/null +++ b/library/symfony/process/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +/** + * Marker Interface for the Process Component. + * + * @author Johannes M. Schmitt <schmittjoh@gmail.com> + */ +interface ExceptionInterface +{ +} diff --git a/library/symfony/process/Exception/InvalidArgumentException.php b/library/symfony/process/Exception/InvalidArgumentException.php new file mode 100644 index 000000000..926ee2118 --- /dev/null +++ b/library/symfony/process/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +/** + * InvalidArgumentException for the Process Component. + * + * @author Romain Neutron <imprec@gmail.com> + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/library/symfony/process/Exception/LogicException.php b/library/symfony/process/Exception/LogicException.php new file mode 100644 index 000000000..be3d490dd --- /dev/null +++ b/library/symfony/process/Exception/LogicException.php @@ -0,0 +1,21 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +/** + * LogicException for the Process Component. + * + * @author Romain Neutron <imprec@gmail.com> + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/library/symfony/process/Exception/ProcessFailedException.php b/library/symfony/process/Exception/ProcessFailedException.php new file mode 100644 index 000000000..328acfde5 --- /dev/null +++ b/library/symfony/process/Exception/ProcessFailedException.php @@ -0,0 +1,54 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Process; + +/** + * Exception for failed processes. + * + * @author Johannes M. Schmitt <schmittjoh@gmail.com> + */ +class ProcessFailedException extends RuntimeException +{ + private $process; + + public function __construct(Process $process) + { + if ($process->isSuccessful()) { + throw new InvalidArgumentException('Expected a failed process, but the given process was successful.'); + } + + $error = sprintf('The command "%s" failed.'."\n\nExit Code: %s(%s)\n\nWorking directory: %s", + $process->getCommandLine(), + $process->getExitCode(), + $process->getExitCodeText(), + $process->getWorkingDirectory() + ); + + if (!$process->isOutputDisabled()) { + $error .= sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", + $process->getOutput(), + $process->getErrorOutput() + ); + } + + parent::__construct($error); + + $this->process = $process; + } + + public function getProcess() + { + return $this->process; + } +} diff --git a/library/symfony/process/Exception/ProcessTimedOutException.php b/library/symfony/process/Exception/ProcessTimedOutException.php new file mode 100644 index 000000000..d45114696 --- /dev/null +++ b/library/symfony/process/Exception/ProcessTimedOutException.php @@ -0,0 +1,69 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Process; + +/** + * Exception that is thrown when a process times out. + * + * @author Johannes M. Schmitt <schmittjoh@gmail.com> + */ +class ProcessTimedOutException extends RuntimeException +{ + const TYPE_GENERAL = 1; + const TYPE_IDLE = 2; + + private $process; + private $timeoutType; + + public function __construct(Process $process, $timeoutType) + { + $this->process = $process; + $this->timeoutType = $timeoutType; + + parent::__construct(sprintf( + 'The process "%s" exceeded the timeout of %s seconds.', + $process->getCommandLine(), + $this->getExceededTimeout() + )); + } + + public function getProcess() + { + return $this->process; + } + + public function isGeneralTimeout() + { + return $this->timeoutType === self::TYPE_GENERAL; + } + + public function isIdleTimeout() + { + return $this->timeoutType === self::TYPE_IDLE; + } + + public function getExceededTimeout() + { + switch ($this->timeoutType) { + case self::TYPE_GENERAL: + return $this->process->getTimeout(); + + case self::TYPE_IDLE: + return $this->process->getIdleTimeout(); + + default: + throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType)); + } + } +} diff --git a/library/symfony/process/Exception/RuntimeException.php b/library/symfony/process/Exception/RuntimeException.php new file mode 100644 index 000000000..adead2536 --- /dev/null +++ b/library/symfony/process/Exception/RuntimeException.php @@ -0,0 +1,21 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +/** + * RuntimeException for the Process Component. + * + * @author Johannes M. Schmitt <schmittjoh@gmail.com> + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/library/symfony/process/ExecutableFinder.php b/library/symfony/process/ExecutableFinder.php new file mode 100644 index 000000000..fa11cb6e4 --- /dev/null +++ b/library/symfony/process/ExecutableFinder.php @@ -0,0 +1,90 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +/** + * Generic executable finder. + * + * @author Fabien Potencier <fabien@symfony.com> + * @author Johannes M. Schmitt <schmittjoh@gmail.com> + */ +class ExecutableFinder +{ + private $suffixes = array('.exe', '.bat', '.cmd', '.com'); + + /** + * Replaces default suffixes of executable. + * + * @param array $suffixes + */ + public function setSuffixes(array $suffixes) + { + $this->suffixes = $suffixes; + } + + /** + * Adds new possible suffix to check for executable. + * + * @param string $suffix + */ + public function addSuffix($suffix) + { + $this->suffixes[] = $suffix; + } + + /** + * Finds an executable by name. + * + * @param string $name The executable name (without the extension) + * @param string $default The default to return if no executable is found + * @param array $extraDirs Additional dirs to check into + * + * @return string The executable path or default value + */ + public function find($name, $default = null, array $extraDirs = array()) + { + if (ini_get('open_basedir')) { + $searchPath = explode(PATH_SEPARATOR, ini_get('open_basedir')); + $dirs = array(); + foreach ($searchPath as $path) { + // Silencing against https://bugs.php.net/69240 + if (@is_dir($path)) { + $dirs[] = $path; + } else { + if (basename($path) == $name && is_executable($path)) { + return $path; + } + } + } + } else { + $dirs = array_merge( + explode(PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), + $extraDirs + ); + } + + $suffixes = array(''); + if ('\\' === DIRECTORY_SEPARATOR) { + $pathExt = getenv('PATHEXT'); + $suffixes = $pathExt ? explode(PATH_SEPARATOR, $pathExt) : $this->suffixes; + } + foreach ($suffixes as $suffix) { + foreach ($dirs as $dir) { + if (is_file($file = $dir.DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === DIRECTORY_SEPARATOR || is_executable($file))) { + return $file; + } + } + } + + return $default; + } +} diff --git a/library/symfony/process/LICENSE b/library/symfony/process/LICENSE new file mode 100644 index 000000000..43028bc60 --- /dev/null +++ b/library/symfony/process/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-2015 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/library/symfony/process/PhpExecutableFinder.php b/library/symfony/process/PhpExecutableFinder.php new file mode 100644 index 000000000..fb297825f --- /dev/null +++ b/library/symfony/process/PhpExecutableFinder.php @@ -0,0 +1,90 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +/** + * An executable finder specifically designed for the PHP executable. + * + * @author Fabien Potencier <fabien@symfony.com> + * @author Johannes M. Schmitt <schmittjoh@gmail.com> + */ +class PhpExecutableFinder +{ + private $executableFinder; + + public function __construct() + { + $this->executableFinder = new ExecutableFinder(); + } + + /** + * Finds The PHP executable. + * + * @param bool $includeArgs Whether or not include command arguments + * + * @return string|false The PHP executable path or false if it cannot be found + */ + public function find($includeArgs = true) + { + $args = $this->findArguments(); + $args = $includeArgs && $args ? ' '.implode(' ', $args) : ''; + + // HHVM support + if (defined('HHVM_VERSION')) { + return (getenv('PHP_BINARY') ?: PHP_BINARY).$args; + } + + // PHP_BINARY return the current sapi executable + if (defined('PHP_BINARY') && PHP_BINARY && in_array(PHP_SAPI, array('cli', 'cli-server', 'phpdbg')) && is_file(PHP_BINARY)) { + return PHP_BINARY.$args; + } + + if ($php = getenv('PHP_PATH')) { + if (!is_executable($php)) { + return false; + } + + return $php; + } + + if ($php = getenv('PHP_PEAR_PHP_BIN')) { + if (is_executable($php)) { + return $php; + } + } + + $dirs = array(PHP_BINDIR); + if ('\\' === DIRECTORY_SEPARATOR) { + $dirs[] = 'C:\xampp\php\\'; + } + + return $this->executableFinder->find('php', false, $dirs); + } + + /** + * Finds the PHP executable arguments. + * + * @return array The PHP executable arguments + */ + public function findArguments() + { + $arguments = array(); + + if (defined('HHVM_VERSION')) { + $arguments[] = '--php'; + } elseif ('phpdbg' === PHP_SAPI) { + $arguments[] = '-qrr'; + } + + return $arguments; + } +} diff --git a/library/symfony/process/PhpProcess.php b/library/symfony/process/PhpProcess.php new file mode 100644 index 000000000..4a2a2625f --- /dev/null +++ b/library/symfony/process/PhpProcess.php @@ -0,0 +1,72 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\RuntimeException; + +/** + * PhpProcess runs a PHP script in an independent process. + * + * $p = new PhpProcess('<?php echo "foo"; ?>'); + * $p->run(); + * print $p->getOutput()."\n"; + * + * @author Fabien Potencier <fabien@symfony.com> + */ +class PhpProcess extends Process +{ + /** + * Constructor. + * + * @param string $script The PHP script to run (as a string) + * @param string|null $cwd The working directory or null to use the working dir of the current PHP process + * @param array|null $env The environment variables or null to use the same environment as the current PHP process + * @param int $timeout The timeout in seconds + * @param array $options An array of options for proc_open + */ + public function __construct($script, $cwd = null, array $env = null, $timeout = 60, array $options = array()) + { + $executableFinder = new PhpExecutableFinder(); + if (false === $php = $executableFinder->find()) { + $php = null; + } + if ('phpdbg' === PHP_SAPI) { + $file = tempnam(sys_get_temp_dir(), 'dbg'); + file_put_contents($file, $script); + register_shutdown_function('unlink', $file); + $php .= ' '.ProcessUtils::escapeArgument($file); + $script = null; + } + + parent::__construct($php, $cwd, $env, $script, $timeout, $options); + } + + /** + * Sets the path to the PHP binary to use. + */ + public function setPhpBinary($php) + { + $this->setCommandLine($php); + } + + /** + * {@inheritdoc} + */ + public function start($callback = null) + { + if (null === $this->getCommandLine()) { + throw new RuntimeException('Unable to find the PHP executable.'); + } + + parent::start($callback); + } +} diff --git a/library/symfony/process/Pipes/AbstractPipes.php b/library/symfony/process/Pipes/AbstractPipes.php new file mode 100644 index 000000000..1ca85739f --- /dev/null +++ b/library/symfony/process/Pipes/AbstractPipes.php @@ -0,0 +1,74 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +/** + * @author Romain Neutron <imprec@gmail.com> + * + * @internal + */ +abstract class AbstractPipes implements PipesInterface +{ + /** @var array */ + public $pipes = array(); + + /** @var string */ + protected $inputBuffer = ''; + /** @var resource|null */ + protected $input; + + /** @var bool */ + private $blocked = true; + + /** + * {@inheritdoc} + */ + public function close() + { + foreach ($this->pipes as $pipe) { + fclose($pipe); + } + $this->pipes = array(); + } + + /** + * Returns true if a system call has been interrupted. + * + * @return bool + */ + protected function hasSystemCallBeenInterrupted() + { + $lastError = error_get_last(); + + // stream_select returns false when the `select` system call is interrupted by an incoming signal + return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call'); + } + + /** + * Unblocks streams. + */ + protected function unblock() + { + if (!$this->blocked) { + return; + } + + foreach ($this->pipes as $pipe) { + stream_set_blocking($pipe, 0); + } + if (null !== $this->input) { + stream_set_blocking($this->input, 0); + } + + $this->blocked = false; + } +} diff --git a/library/symfony/process/Pipes/PipesInterface.php b/library/symfony/process/Pipes/PipesInterface.php new file mode 100644 index 000000000..09d3f61d6 --- /dev/null +++ b/library/symfony/process/Pipes/PipesInterface.php @@ -0,0 +1,60 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +/** + * PipesInterface manages descriptors and pipes for the use of proc_open. + * + * @author Romain Neutron <imprec@gmail.com> + * + * @internal + */ +interface PipesInterface +{ + const CHUNK_SIZE = 16384; + + /** + * Returns an array of descriptors for the use of proc_open. + * + * @return array + */ + public function getDescriptors(); + + /** + * Returns an array of filenames indexed by their related stream in case these pipes use temporary files. + * + * @return string[] + */ + public function getFiles(); + + /** + * Reads data in file handles and pipes. + * + * @param bool $blocking Whether to use blocking calls or not. + * @param bool $close Whether to close pipes if they've reached EOF. + * + * @return string[] An array of read data indexed by their fd. + */ + public function readAndWrite($blocking, $close = false); + + /** + * Returns if the current state has open file handles or pipes. + * + * @return bool + */ + public function areOpen(); + + /** + * Closes file handles and pipes. + */ + public function close(); +} diff --git a/library/symfony/process/Pipes/UnixPipes.php b/library/symfony/process/Pipes/UnixPipes.php new file mode 100644 index 000000000..f8a0d1997 --- /dev/null +++ b/library/symfony/process/Pipes/UnixPipes.php @@ -0,0 +1,214 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +use Symfony\Component\Process\Process; + +/** + * UnixPipes implementation uses unix pipes as handles. + * + * @author Romain Neutron <imprec@gmail.com> + * + * @internal + */ +class UnixPipes extends AbstractPipes +{ + /** @var bool */ + private $ttyMode; + /** @var bool */ + private $ptyMode; + /** @var bool */ + private $disableOutput; + + public function __construct($ttyMode, $ptyMode, $input, $disableOutput) + { + $this->ttyMode = (bool) $ttyMode; + $this->ptyMode = (bool) $ptyMode; + $this->disableOutput = (bool) $disableOutput; + + if (is_resource($input)) { + $this->input = $input; + } else { + $this->inputBuffer = (string) $input; + } + } + + public function __destruct() + { + $this->close(); + } + + /** + * {@inheritdoc} + */ + public function getDescriptors() + { + if ($this->disableOutput) { + $nullstream = fopen('/dev/null', 'c'); + + return array( + array('pipe', 'r'), + $nullstream, + $nullstream, + ); + } + + if ($this->ttyMode) { + return array( + array('file', '/dev/tty', 'r'), + array('file', '/dev/tty', 'w'), + array('file', '/dev/tty', 'w'), + ); + } + + if ($this->ptyMode && Process::isPtySupported()) { + return array( + array('pty'), + array('pty'), + array('pty'), + ); + } + + return array( + array('pipe', 'r'), + array('pipe', 'w'), // stdout + array('pipe', 'w'), // stderr + ); + } + + /** + * {@inheritdoc} + */ + public function getFiles() + { + return array(); + } + + /** + * {@inheritdoc} + */ + public function readAndWrite($blocking, $close = false) + { + // only stdin is left open, job has been done ! + // we can now close it + if (1 === count($this->pipes) && array(0) === array_keys($this->pipes)) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + + if (empty($this->pipes)) { + return array(); + } + + $this->unblock(); + + $read = array(); + + if (null !== $this->input) { + // if input is a resource, let's add it to stream_select argument to + // fill a buffer + $r = array_merge($this->pipes, array('input' => $this->input)); + } else { + $r = $this->pipes; + } + // discard read on stdin + unset($r[0]); + + $w = isset($this->pipes[0]) ? array($this->pipes[0]) : null; + $e = null; + + // let's have a look if something changed in streams + if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { + // if a system call has been interrupted, forget about it, let's try again + // otherwise, an error occurred, let's reset pipes + if (!$this->hasSystemCallBeenInterrupted()) { + $this->pipes = array(); + } + + return $read; + } + + // nothing has changed + if (0 === $n) { + return $read; + } + + foreach ($r as $pipe) { + // prior PHP 5.4 the array passed to stream_select is modified and + // lose key association, we have to find back the key + $type = (false !== $found = array_search($pipe, $this->pipes)) ? $found : 'input'; + $data = ''; + while ('' !== $dataread = (string) fread($pipe, self::CHUNK_SIZE)) { + $data .= $dataread; + } + + if ('' !== $data) { + if ($type === 'input') { + $this->inputBuffer .= $data; + } else { + $read[$type] = $data; + } + } + + if (false === $data || (true === $close && feof($pipe) && '' === $data)) { + if ($type === 'input') { + // no more data to read on input resource + // use an empty buffer in the next reads + $this->input = null; + } else { + fclose($this->pipes[$type]); + unset($this->pipes[$type]); + } + } + } + + if (null !== $w && 0 < count($w)) { + while (strlen($this->inputBuffer)) { + $written = fwrite($w[0], $this->inputBuffer, 2 << 18); // write 512k + if ($written > 0) { + $this->inputBuffer = (string) substr($this->inputBuffer, $written); + } else { + break; + } + } + } + + // no input to read on resource, buffer is empty and stdin still open + if ('' === $this->inputBuffer && null === $this->input && isset($this->pipes[0])) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + + return $read; + } + + /** + * {@inheritdoc} + */ + public function areOpen() + { + return (bool) $this->pipes; + } + + /** + * Creates a new UnixPipes instance. + * + * @param Process $process + * @param string|resource $input + * + * @return UnixPipes + */ + public static function create(Process $process, $input) + { + return new static($process->isTty(), $process->isPty(), $input, $process->isOutputDisabled()); + } +} diff --git a/library/symfony/process/Pipes/WindowsPipes.php b/library/symfony/process/Pipes/WindowsPipes.php new file mode 100644 index 000000000..1472f8c6c --- /dev/null +++ b/library/symfony/process/Pipes/WindowsPipes.php @@ -0,0 +1,253 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +use Symfony\Component\Process\Process; +use Symfony\Component\Process\Exception\RuntimeException; + +/** + * WindowsPipes implementation uses temporary files as handles. + * + * @see https://bugs.php.net/bug.php?id=51800 + * @see https://bugs.php.net/bug.php?id=65650 + * + * @author Romain Neutron <imprec@gmail.com> + * + * @internal + */ +class WindowsPipes extends AbstractPipes +{ + /** @var array */ + private $files = array(); + /** @var array */ + private $fileHandles = array(); + /** @var array */ + private $readBytes = array( + Process::STDOUT => 0, + Process::STDERR => 0, + ); + /** @var bool */ + private $disableOutput; + + public function __construct($disableOutput, $input) + { + $this->disableOutput = (bool) $disableOutput; + + if (!$this->disableOutput) { + // Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big. + // Workaround for this problem is to use temporary files instead of pipes on Windows platform. + // + // @see https://bugs.php.net/bug.php?id=51800 + $this->files = array( + Process::STDOUT => tempnam(sys_get_temp_dir(), 'out_sf_proc'), + Process::STDERR => tempnam(sys_get_temp_dir(), 'err_sf_proc'), + ); + foreach ($this->files as $offset => $file) { + if (false === $file || false === $this->fileHandles[$offset] = fopen($file, 'rb')) { + throw new RuntimeException('A temporary file could not be opened to write the process output to, verify that your TEMP environment variable is writable'); + } + } + } + + if (is_resource($input)) { + $this->input = $input; + } else { + $this->inputBuffer = $input; + } + } + + public function __destruct() + { + $this->close(); + $this->removeFiles(); + } + + /** + * {@inheritdoc} + */ + public function getDescriptors() + { + if ($this->disableOutput) { + $nullstream = fopen('NUL', 'c'); + + return array( + array('pipe', 'r'), + $nullstream, + $nullstream, + ); + } + + // We're not using pipe on Windows platform as it hangs (https://bugs.php.net/bug.php?id=51800) + // We're not using file handles as it can produce corrupted output https://bugs.php.net/bug.php?id=65650 + // So we redirect output within the commandline and pass the nul device to the process + return array( + array('pipe', 'r'), + array('file', 'NUL', 'w'), + array('file', 'NUL', 'w'), + ); + } + + /** + * {@inheritdoc} + */ + public function getFiles() + { + return $this->files; + } + + /** + * {@inheritdoc} + */ + public function readAndWrite($blocking, $close = false) + { + $this->write($blocking, $close); + + $read = array(); + $fh = $this->fileHandles; + foreach ($fh as $type => $fileHandle) { + if (0 !== fseek($fileHandle, $this->readBytes[$type])) { + continue; + } + $data = ''; + $dataread = null; + while (!feof($fileHandle)) { + if (false !== $dataread = fread($fileHandle, self::CHUNK_SIZE)) { + $data .= $dataread; + } + } + if (0 < $length = strlen($data)) { + $this->readBytes[$type] += $length; + $read[$type] = $data; + } + + if (false === $dataread || (true === $close && feof($fileHandle) && '' === $data)) { + fclose($this->fileHandles[$type]); + unset($this->fileHandles[$type]); + } + } + + return $read; + } + + /** + * {@inheritdoc} + */ + public function areOpen() + { + return (bool) $this->pipes && (bool) $this->fileHandles; + } + + /** + * {@inheritdoc} + */ + public function close() + { + parent::close(); + foreach ($this->fileHandles as $handle) { + fclose($handle); + } + $this->fileHandles = array(); + } + + /** + * Creates a new WindowsPipes instance. + * + * @param Process $process The process + * @param $input + * + * @return WindowsPipes + */ + public static function create(Process $process, $input) + { + return new static($process->isOutputDisabled(), $input); + } + + /** + * Removes temporary files. + */ + private function removeFiles() + { + foreach ($this->files as $filename) { + if (file_exists($filename)) { + @unlink($filename); + } + } + $this->files = array(); + } + + /** + * Writes input to stdin. + * + * @param bool $blocking + * @param bool $close + */ + private function write($blocking, $close) + { + if (empty($this->pipes)) { + return; + } + + $this->unblock(); + + $r = null !== $this->input ? array('input' => $this->input) : null; + $w = isset($this->pipes[0]) ? array($this->pipes[0]) : null; + $e = null; + + // let's have a look if something changed in streams + if (false === $n = @stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { + // if a system call has been interrupted, forget about it, let's try again + // otherwise, an error occurred, let's reset pipes + if (!$this->hasSystemCallBeenInterrupted()) { + $this->pipes = array(); + } + + return; + } + + // nothing has changed + if (0 === $n) { + return; + } + + if (null !== $w && 0 < count($r)) { + $data = ''; + while ($dataread = fread($r['input'], self::CHUNK_SIZE)) { + $data .= $dataread; + } + + $this->inputBuffer .= $data; + + if (false === $data || (true === $close && feof($r['input']) && '' === $data)) { + // no more data to read on input resource + // use an empty buffer in the next reads + $this->input = null; + } + } + + if (null !== $w && 0 < count($w)) { + while (strlen($this->inputBuffer)) { + $written = fwrite($w[0], $this->inputBuffer, 2 << 18); + if ($written > 0) { + $this->inputBuffer = (string) substr($this->inputBuffer, $written); + } else { + break; + } + } + } + + // no input to read on resource, buffer is empty and stdin still open + if ('' === $this->inputBuffer && null === $this->input && isset($this->pipes[0])) { + fclose($this->pipes[0]); + unset($this->pipes[0]); + } + } +} diff --git a/library/symfony/process/Process.php b/library/symfony/process/Process.php new file mode 100644 index 000000000..c1e732170 --- /dev/null +++ b/library/symfony/process/Process.php @@ -0,0 +1,1515 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\InvalidArgumentException; +use Symfony\Component\Process\Exception\LogicException; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Exception\ProcessTimedOutException; +use Symfony\Component\Process\Exception\RuntimeException; +use Symfony\Component\Process\Pipes\PipesInterface; +use Symfony\Component\Process\Pipes\UnixPipes; +use Symfony\Component\Process\Pipes\WindowsPipes; + +/** + * Process is a thin wrapper around proc_* functions to easily + * start independent PHP processes. + * + * @author Fabien Potencier <fabien@symfony.com> + * @author Romain Neutron <imprec@gmail.com> + */ +class Process +{ + const ERR = 'err'; + const OUT = 'out'; + + const STATUS_READY = 'ready'; + const STATUS_STARTED = 'started'; + const STATUS_TERMINATED = 'terminated'; + + const STDIN = 0; + const STDOUT = 1; + const STDERR = 2; + + // Timeout Precision in seconds. + const TIMEOUT_PRECISION = 0.2; + + private $callback; + private $commandline; + private $cwd; + private $env; + private $input; + private $starttime; + private $lastOutputTime; + private $timeout; + private $idleTimeout; + private $options; + private $exitcode; + private $fallbackExitcode; + private $processInformation; + private $outputDisabled = false; + private $stdout; + private $stderr; + private $enhanceWindowsCompatibility = true; + private $enhanceSigchildCompatibility; + private $process; + private $status = self::STATUS_READY; + private $incrementalOutputOffset = 0; + private $incrementalErrorOutputOffset = 0; + private $tty; + private $pty; + + private $useFileHandles = false; + /** @var PipesInterface */ + private $processPipes; + + private $latestSignal; + + private static $sigchild; + + /** + * Exit codes translation table. + * + * User-defined errors must use exit codes in the 64-113 range. + * + * @var array + */ + public static $exitCodes = array( + 0 => 'OK', + 1 => 'General error', + 2 => 'Misuse of shell builtins', + + 126 => 'Invoked command cannot execute', + 127 => 'Command not found', + 128 => 'Invalid exit argument', + + // signals + 129 => 'Hangup', + 130 => 'Interrupt', + 131 => 'Quit and dump core', + 132 => 'Illegal instruction', + 133 => 'Trace/breakpoint trap', + 134 => 'Process aborted', + 135 => 'Bus error: "access to undefined portion of memory object"', + 136 => 'Floating point exception: "erroneous arithmetic operation"', + 137 => 'Kill (terminate immediately)', + 138 => 'User-defined 1', + 139 => 'Segmentation violation', + 140 => 'User-defined 2', + 141 => 'Write to pipe with no one reading', + 142 => 'Signal raised by alarm', + 143 => 'Termination (request to terminate)', + // 144 - not defined + 145 => 'Child process terminated, stopped (or continued*)', + 146 => 'Continue if stopped', + 147 => 'Stop executing temporarily', + 148 => 'Terminal stop signal', + 149 => 'Background process attempting to read from tty ("in")', + 150 => 'Background process attempting to write to tty ("out")', + 151 => 'Urgent data available on socket', + 152 => 'CPU time limit exceeded', + 153 => 'File size limit exceeded', + 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"', + 155 => 'Profiling timer expired', + // 156 - not defined + 157 => 'Pollable event', + // 158 - not defined + 159 => 'Bad syscall', + ); + + /** + * Constructor. + * + * @param string $commandline The command line to run + * @param string|null $cwd The working directory or null to use the working dir of the current PHP process + * @param array|null $env The environment variables or null to use the same environment as the current PHP process + * @param string|null $input The input + * @param int|float|null $timeout The timeout in seconds or null to disable + * @param array $options An array of options for proc_open + * + * @throws RuntimeException When proc_open is not installed + */ + public function __construct($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array()) + { + if (!function_exists('proc_open')) { + throw new RuntimeException('The Process class relies on proc_open, which is not available on your PHP installation.'); + } + + $this->commandline = $commandline; + $this->cwd = $cwd; + + // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started + // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected + // @see : https://bugs.php.net/bug.php?id=51800 + // @see : https://bugs.php.net/bug.php?id=50524 + if (null === $this->cwd && (defined('ZEND_THREAD_SAFE') || '\\' === DIRECTORY_SEPARATOR)) { + $this->cwd = getcwd(); + } + if (null !== $env) { + $this->setEnv($env); + } + + $this->input = $input; + $this->setTimeout($timeout); + $this->useFileHandles = '\\' === DIRECTORY_SEPARATOR; + $this->pty = false; + $this->enhanceWindowsCompatibility = true; + $this->enhanceSigchildCompatibility = '\\' !== DIRECTORY_SEPARATOR && $this->isSigchildEnabled(); + $this->options = array_replace(array('suppress_errors' => true, 'binary_pipes' => true), $options); + } + + public function __destruct() + { + // stop() will check if we have a process running. + $this->stop(); + } + + public function __clone() + { + $this->resetProcessData(); + } + + /** + * Runs the process. + * + * The callback receives the type of output (out or err) and + * some bytes from the output in real-time. It allows to have feedback + * from the independent process during execution. + * + * The STDOUT and STDERR are also available after the process is finished + * via the getOutput() and getErrorOutput() methods. + * + * @param callable|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @return int The exit status code + * + * @throws RuntimeException When process can't be launched + * @throws RuntimeException When process stopped after receiving signal + * @throws LogicException In case a callback is provided and output has been disabled + */ + public function run($callback = null) + { + $this->start($callback); + + return $this->wait(); + } + + /** + * Runs the process. + * + * This is identical to run() except that an exception is thrown if the process + * exits with a non-zero exit code. + * + * @param callable|null $callback + * + * @return self + * + * @throws RuntimeException if PHP was compiled with --enable-sigchild and the enhanced sigchild compatibility mode is not enabled + * @throws ProcessFailedException if the process didn't terminate successfully + */ + public function mustRun($callback = null) + { + if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); + } + + if (0 !== $this->run($callback)) { + throw new ProcessFailedException($this); + } + + return $this; + } + + /** + * Starts the process and returns after writing the input to STDIN. + * + * This method blocks until all STDIN data is sent to the process then it + * returns while the process runs in the background. + * + * The termination of the process can be awaited with wait(). + * + * The callback receives the type of output (out or err) and some bytes from + * the output in real-time while writing the standard input to the process. + * It allows to have feedback from the independent process during execution. + * If there is no callback passed, the wait() method can be called + * with true as a second parameter then the callback will get all data occurred + * in (and since) the start call. + * + * @param callable|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @throws RuntimeException When process can't be launched + * @throws RuntimeException When process is already running + * @throws LogicException In case a callback is provided and output has been disabled + */ + public function start($callback = null) + { + if ($this->isRunning()) { + throw new RuntimeException('Process is already running'); + } + if ($this->outputDisabled && null !== $callback) { + throw new LogicException('Output has been disabled, enable it to allow the use of a callback.'); + } + + $this->resetProcessData(); + $this->starttime = $this->lastOutputTime = microtime(true); + $this->callback = $this->buildCallback($callback); + $descriptors = $this->getDescriptors(); + + $commandline = $this->commandline; + + if ('\\' === DIRECTORY_SEPARATOR && $this->enhanceWindowsCompatibility) { + $commandline = 'cmd /V:ON /E:ON /D /C "('.$commandline.')'; + foreach ($this->processPipes->getFiles() as $offset => $filename) { + $commandline .= ' '.$offset.'>'.ProcessUtils::escapeArgument($filename); + } + $commandline .= '"'; + + if (!isset($this->options['bypass_shell'])) { + $this->options['bypass_shell'] = true; + } + } + + $ptsWorkaround = null; + + if (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { + // Workaround for the bug, when PTS functionality is enabled. + // @see : https://bugs.php.net/69442 + $ptsWorkaround = fopen(__FILE__, 'r'); + } + + $this->process = proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $this->env, $this->options); + + if ($ptsWorkaround) { + fclose($ptsWorkaround); + } + + if (!is_resource($this->process)) { + throw new RuntimeException('Unable to launch a new process.'); + } + $this->status = self::STATUS_STARTED; + + if ($this->tty) { + return; + } + + $this->updateStatus(false); + $this->checkTimeout(); + } + + /** + * Restarts the process. + * + * Be warned that the process is cloned before being started. + * + * @param callable|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @return Process The new process + * + * @throws RuntimeException When process can't be launched + * @throws RuntimeException When process is already running + * + * @see start() + */ + public function restart($callback = null) + { + if ($this->isRunning()) { + throw new RuntimeException('Process is already running'); + } + + $process = clone $this; + $process->start($callback); + + return $process; + } + + /** + * Waits for the process to terminate. + * + * The callback receives the type of output (out or err) and some bytes + * from the output in real-time while writing the standard input to the process. + * It allows to have feedback from the independent process during execution. + * + * @param callable|null $callback A valid PHP callback + * + * @return int The exitcode of the process + * + * @throws RuntimeException When process timed out + * @throws RuntimeException When process stopped after receiving signal + * @throws LogicException When process is not yet started + */ + public function wait($callback = null) + { + $this->requireProcessIsStarted(__FUNCTION__); + + $this->updateStatus(false); + if (null !== $callback) { + $this->callback = $this->buildCallback($callback); + } + + do { + $this->checkTimeout(); + $running = '\\' === DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen(); + $close = '\\' !== DIRECTORY_SEPARATOR || !$running; + $this->readPipes(true, $close); + } while ($running); + + while ($this->isRunning()) { + usleep(1000); + } + + if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) { + throw new RuntimeException(sprintf('The process has been signaled with signal "%s".', $this->processInformation['termsig'])); + } + + return $this->exitcode; + } + + /** + * Returns the Pid (process identifier), if applicable. + * + * @return int|null The process id if running, null otherwise + * + * @throws RuntimeException In case --enable-sigchild is activated + */ + public function getPid() + { + if ($this->isSigchildEnabled()) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved.'); + } + + $this->updateStatus(false); + + return $this->isRunning() ? $this->processInformation['pid'] : null; + } + + /** + * Sends a POSIX signal to the process. + * + * @param int $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php) + * + * @return Process + * + * @throws LogicException In case the process is not running + * @throws RuntimeException In case --enable-sigchild is activated + * @throws RuntimeException In case of failure + */ + public function signal($signal) + { + $this->doSignal($signal, true); + + return $this; + } + + /** + * Disables fetching output and error output from the underlying process. + * + * @return Process + * + * @throws RuntimeException In case the process is already running + * @throws LogicException if an idle timeout is set + */ + public function disableOutput() + { + if ($this->isRunning()) { + throw new RuntimeException('Disabling output while the process is running is not possible.'); + } + if (null !== $this->idleTimeout) { + throw new LogicException('Output can not be disabled while an idle timeout is set.'); + } + + $this->outputDisabled = true; + + return $this; + } + + /** + * Enables fetching output and error output from the underlying process. + * + * @return Process + * + * @throws RuntimeException In case the process is already running + */ + public function enableOutput() + { + if ($this->isRunning()) { + throw new RuntimeException('Enabling output while the process is running is not possible.'); + } + + $this->outputDisabled = false; + + return $this; + } + + /** + * Returns true in case the output is disabled, false otherwise. + * + * @return bool + */ + public function isOutputDisabled() + { + return $this->outputDisabled; + } + + /** + * Returns the current output of the process (STDOUT). + * + * @return string The process output + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + */ + public function getOutput() + { + if ($this->outputDisabled) { + throw new LogicException('Output has been disabled.'); + } + + $this->requireProcessIsStarted(__FUNCTION__); + + $this->readPipes(false, '\\' === DIRECTORY_SEPARATOR ? !$this->processInformation['running'] : true); + + return $this->stdout; + } + + /** + * Returns the output incrementally. + * + * In comparison with the getOutput method which always return the whole + * output, this one returns the new output since the last call. + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + * + * @return string The process output since the last call + */ + public function getIncrementalOutput() + { + $this->requireProcessIsStarted(__FUNCTION__); + + $data = $this->getOutput(); + + $latest = substr($data, $this->incrementalOutputOffset); + + if (false === $latest) { + return ''; + } + + $this->incrementalOutputOffset = strlen($data); + + return $latest; + } + + /** + * Clears the process output. + * + * @return Process + */ + public function clearOutput() + { + $this->stdout = ''; + $this->incrementalOutputOffset = 0; + + return $this; + } + + /** + * Returns the current error output of the process (STDERR). + * + * @return string The process error output + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + */ + public function getErrorOutput() + { + if ($this->outputDisabled) { + throw new LogicException('Output has been disabled.'); + } + + $this->requireProcessIsStarted(__FUNCTION__); + + $this->readPipes(false, '\\' === DIRECTORY_SEPARATOR ? !$this->processInformation['running'] : true); + + return $this->stderr; + } + + /** + * Returns the errorOutput incrementally. + * + * In comparison with the getErrorOutput method which always return the + * whole error output, this one returns the new error output since the last + * call. + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + * + * @return string The process error output since the last call + */ + public function getIncrementalErrorOutput() + { + $this->requireProcessIsStarted(__FUNCTION__); + + $data = $this->getErrorOutput(); + + $latest = substr($data, $this->incrementalErrorOutputOffset); + + if (false === $latest) { + return ''; + } + + $this->incrementalErrorOutputOffset = strlen($data); + + return $latest; + } + + /** + * Clears the process output. + * + * @return Process + */ + public function clearErrorOutput() + { + $this->stderr = ''; + $this->incrementalErrorOutputOffset = 0; + + return $this; + } + + /** + * Returns the exit code returned by the process. + * + * @return null|int The exit status code, null if the Process is not terminated + * + * @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled + */ + public function getExitCode() + { + if ($this->isSigchildEnabled() && !$this->enhanceSigchildCompatibility) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'); + } + + $this->updateStatus(false); + + return $this->exitcode; + } + + /** + * Returns a string representation for the exit code returned by the process. + * + * This method relies on the Unix exit code status standardization + * and might not be relevant for other operating systems. + * + * @return null|string A string representation for the exit status code, null if the Process is not terminated. + * + * @throws RuntimeException In case --enable-sigchild is activated and the sigchild compatibility mode is disabled + * + * @see http://tldp.org/LDP/abs/html/exitcodes.html + * @see http://en.wikipedia.org/wiki/Unix_signal + */ + public function getExitCodeText() + { + if (null === $exitcode = $this->getExitCode()) { + return; + } + + return isset(self::$exitCodes[$exitcode]) ? self::$exitCodes[$exitcode] : 'Unknown error'; + } + + /** + * Checks if the process ended successfully. + * + * @return bool true if the process ended successfully, false otherwise + */ + public function isSuccessful() + { + return 0 === $this->getExitCode(); + } + + /** + * Returns true if the child process has been terminated by an uncaught signal. + * + * It always returns false on Windows. + * + * @return bool + * + * @throws RuntimeException In case --enable-sigchild is activated + * @throws LogicException In case the process is not terminated + */ + public function hasBeenSignaled() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + if ($this->isSigchildEnabled()) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); + } + + $this->updateStatus(false); + + return $this->processInformation['signaled']; + } + + /** + * Returns the number of the signal that caused the child process to terminate its execution. + * + * It is only meaningful if hasBeenSignaled() returns true. + * + * @return int + * + * @throws RuntimeException In case --enable-sigchild is activated + * @throws LogicException In case the process is not terminated + */ + public function getTermSignal() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + if ($this->isSigchildEnabled()) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved.'); + } + + $this->updateStatus(false); + + return $this->processInformation['termsig']; + } + + /** + * Returns true if the child process has been stopped by a signal. + * + * It always returns false on Windows. + * + * @return bool + * + * @throws LogicException In case the process is not terminated + */ + public function hasBeenStopped() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + $this->updateStatus(false); + + return $this->processInformation['stopped']; + } + + /** + * Returns the number of the signal that caused the child process to stop its execution. + * + * It is only meaningful if hasBeenStopped() returns true. + * + * @return int + * + * @throws LogicException In case the process is not terminated + */ + public function getStopSignal() + { + $this->requireProcessIsTerminated(__FUNCTION__); + + $this->updateStatus(false); + + return $this->processInformation['stopsig']; + } + + /** + * Checks if the process is currently running. + * + * @return bool true if the process is currently running, false otherwise + */ + public function isRunning() + { + if (self::STATUS_STARTED !== $this->status) { + return false; + } + + $this->updateStatus(false); + + return $this->processInformation['running']; + } + + /** + * Checks if the process has been started with no regard to the current state. + * + * @return bool true if status is ready, false otherwise + */ + public function isStarted() + { + return $this->status != self::STATUS_READY; + } + + /** + * Checks if the process is terminated. + * + * @return bool true if process is terminated, false otherwise + */ + public function isTerminated() + { + $this->updateStatus(false); + + return $this->status == self::STATUS_TERMINATED; + } + + /** + * Gets the process status. + * + * The status is one of: ready, started, terminated. + * + * @return string The current process status + */ + public function getStatus() + { + $this->updateStatus(false); + + return $this->status; + } + + /** + * Stops the process. + * + * @param int|float $timeout The timeout in seconds + * @param int $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9) + * + * @return int The exit-code of the process + */ + public function stop($timeout = 10, $signal = null) + { + $timeoutMicro = microtime(true) + $timeout; + if ($this->isRunning()) { + // given `SIGTERM` may not be defined and that `proc_terminate` uses the constant value and not the constant itself, we use the same here + $this->doSignal(15, false); + do { + usleep(1000); + } while ($this->isRunning() && microtime(true) < $timeoutMicro); + + if ($this->isRunning() && !$this->isSigchildEnabled()) { + // Avoid exception here: process is supposed to be running, but it might have stopped just + // after this line. In any case, let's silently discard the error, we cannot do anything. + $this->doSignal($signal ?: 9, false); + } + } + + $this->updateStatus(false); + if ($this->processInformation['running']) { + $this->close(); + } + + return $this->exitcode; + } + + /** + * Adds a line to the STDOUT stream. + * + * @param string $line The line to append + */ + public function addOutput($line) + { + $this->lastOutputTime = microtime(true); + $this->stdout .= $line; + } + + /** + * Adds a line to the STDERR stream. + * + * @param string $line The line to append + */ + public function addErrorOutput($line) + { + $this->lastOutputTime = microtime(true); + $this->stderr .= $line; + } + + /** + * Gets the command line to be executed. + * + * @return string The command to execute + */ + public function getCommandLine() + { + return $this->commandline; + } + + /** + * Sets the command line to be executed. + * + * @param string $commandline The command to execute + * + * @return self The current Process instance + */ + public function setCommandLine($commandline) + { + $this->commandline = $commandline; + + return $this; + } + + /** + * Gets the process timeout (max. runtime). + * + * @return float|null The timeout in seconds or null if it's disabled + */ + public function getTimeout() + { + return $this->timeout; + } + + /** + * Gets the process idle timeout (max. time since last output). + * + * @return float|null The timeout in seconds or null if it's disabled + */ + public function getIdleTimeout() + { + return $this->idleTimeout; + } + + /** + * Sets the process timeout (max. runtime). + * + * To disable the timeout, set this value to null. + * + * @param int|float|null $timeout The timeout in seconds + * + * @return self The current Process instance + * + * @throws InvalidArgumentException if the timeout is negative + */ + public function setTimeout($timeout) + { + $this->timeout = $this->validateTimeout($timeout); + + return $this; + } + + /** + * Sets the process idle timeout (max. time since last output). + * + * To disable the timeout, set this value to null. + * + * @param int|float|null $timeout The timeout in seconds + * + * @return self The current Process instance. + * + * @throws LogicException if the output is disabled + * @throws InvalidArgumentException if the timeout is negative + */ + public function setIdleTimeout($timeout) + { + if (null !== $timeout && $this->outputDisabled) { + throw new LogicException('Idle timeout can not be set while the output is disabled.'); + } + + $this->idleTimeout = $this->validateTimeout($timeout); + + return $this; + } + + /** + * Enables or disables the TTY mode. + * + * @param bool $tty True to enabled and false to disable + * + * @return self The current Process instance + * + * @throws RuntimeException In case the TTY mode is not supported + */ + public function setTty($tty) + { + if ('\\' === DIRECTORY_SEPARATOR && $tty) { + throw new RuntimeException('TTY mode is not supported on Windows platform.'); + } + if ($tty && (!file_exists('/dev/tty') || !is_readable('/dev/tty'))) { + throw new RuntimeException('TTY mode requires /dev/tty to be readable.'); + } + + $this->tty = (bool) $tty; + + return $this; + } + + /** + * Checks if the TTY mode is enabled. + * + * @return bool true if the TTY mode is enabled, false otherwise + */ + public function isTty() + { + return $this->tty; + } + + /** + * Sets PTY mode. + * + * @param bool $bool + * + * @return self + */ + public function setPty($bool) + { + $this->pty = (bool) $bool; + + return $this; + } + + /** + * Returns PTY state. + * + * @return bool + */ + public function isPty() + { + return $this->pty; + } + + /** + * Gets the working directory. + * + * @return string|null The current working directory or null on failure + */ + public function getWorkingDirectory() + { + if (null === $this->cwd) { + // getcwd() will return false if any one of the parent directories does not have + // the readable or search mode set, even if the current directory does + return getcwd() ?: null; + } + + return $this->cwd; + } + + /** + * Sets the current working directory. + * + * @param string $cwd The new working directory + * + * @return self The current Process instance + */ + public function setWorkingDirectory($cwd) + { + $this->cwd = $cwd; + + return $this; + } + + /** + * Gets the environment variables. + * + * @return array The current environment variables + */ + public function getEnv() + { + return $this->env; + } + + /** + * Sets the environment variables. + * + * An environment variable value should be a string. + * If it is an array, the variable is ignored. + * + * That happens in PHP when 'argv' is registered into + * the $_ENV array for instance. + * + * @param array $env The new environment variables + * + * @return self The current Process instance + */ + public function setEnv(array $env) + { + // Process can not handle env values that are arrays + $env = array_filter($env, function ($value) { + return !is_array($value); + }); + + $this->env = array(); + foreach ($env as $key => $value) { + $this->env[$key] = (string) $value; + } + + return $this; + } + + /** + * Gets the contents of STDIN. + * + * @return string|null The current contents + * + * @deprecated since version 2.5, to be removed in 3.0. + * Use setInput() instead. + * This method is deprecated in favor of getInput. + */ + public function getStdin() + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.5 and will be removed in 3.0. Use the getInput() method instead.', E_USER_DEPRECATED); + + return $this->getInput(); + } + + /** + * Gets the Process input. + * + * @return null|string The Process input + */ + public function getInput() + { + return $this->input; + } + + /** + * Sets the contents of STDIN. + * + * @param string|null $stdin The new contents + * + * @return self The current Process instance + * + * @deprecated since version 2.5, to be removed in 3.0. + * Use setInput() instead. + * + * @throws LogicException In case the process is running + * @throws InvalidArgumentException In case the argument is invalid + */ + public function setStdin($stdin) + { + @trigger_error('The '.__METHOD__.' method is deprecated since version 2.5 and will be removed in 3.0. Use the setInput() method instead.', E_USER_DEPRECATED); + + return $this->setInput($stdin); + } + + /** + * Sets the input. + * + * This content will be passed to the underlying process standard input. + * + * @param mixed $input The content + * + * @return self The current Process instance + * + * @throws LogicException In case the process is running + * + * Passing an object as an input is deprecated since version 2.5 and will be removed in 3.0. + */ + public function setInput($input) + { + if ($this->isRunning()) { + throw new LogicException('Input can not be set while the process is running.'); + } + + $this->input = ProcessUtils::validateInput(sprintf('%s::%s', __CLASS__, __FUNCTION__), $input); + + return $this; + } + + /** + * Gets the options for proc_open. + * + * @return array The current options + */ + public function getOptions() + { + return $this->options; + } + + /** + * Sets the options for proc_open. + * + * @param array $options The new options + * + * @return self The current Process instance + */ + public function setOptions(array $options) + { + $this->options = $options; + + return $this; + } + + /** + * Gets whether or not Windows compatibility is enabled. + * + * This is true by default. + * + * @return bool + */ + public function getEnhanceWindowsCompatibility() + { + return $this->enhanceWindowsCompatibility; + } + + /** + * Sets whether or not Windows compatibility is enabled. + * + * @param bool $enhance + * + * @return self The current Process instance + */ + public function setEnhanceWindowsCompatibility($enhance) + { + $this->enhanceWindowsCompatibility = (bool) $enhance; + + return $this; + } + + /** + * Returns whether sigchild compatibility mode is activated or not. + * + * @return bool + */ + public function getEnhanceSigchildCompatibility() + { + return $this->enhanceSigchildCompatibility; + } + + /** + * Activates sigchild compatibility mode. + * + * Sigchild compatibility mode is required to get the exit code and + * determine the success of a process when PHP has been compiled with + * the --enable-sigchild option + * + * @param bool $enhance + * + * @return self The current Process instance + */ + public function setEnhanceSigchildCompatibility($enhance) + { + $this->enhanceSigchildCompatibility = (bool) $enhance; + + return $this; + } + + /** + * Performs a check between the timeout definition and the time the process started. + * + * In case you run a background process (with the start method), you should + * trigger this method regularly to ensure the process timeout + * + * @throws ProcessTimedOutException In case the timeout was reached + */ + public function checkTimeout() + { + if ($this->status !== self::STATUS_STARTED) { + return; + } + + if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) { + $this->stop(0); + + throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL); + } + + if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) { + $this->stop(0); + + throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE); + } + } + + /** + * Returns whether PTY is supported on the current operating system. + * + * @return bool + */ + public static function isPtySupported() + { + static $result; + + if (null !== $result) { + return $result; + } + + if ('\\' === DIRECTORY_SEPARATOR) { + return $result = false; + } + + $proc = @proc_open('echo 1', array(array('pty'), array('pty'), array('pty')), $pipes); + if (is_resource($proc)) { + proc_close($proc); + + return $result = true; + } + + return $result = false; + } + + /** + * Creates the descriptors needed by the proc_open. + * + * @return array + */ + private function getDescriptors() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->processPipes = WindowsPipes::create($this, $this->input); + } else { + $this->processPipes = UnixPipes::create($this, $this->input); + } + $descriptors = $this->processPipes->getDescriptors($this->outputDisabled); + + if (!$this->useFileHandles && $this->enhanceSigchildCompatibility && $this->isSigchildEnabled()) { + // last exit code is output on the fourth pipe and caught to work around --enable-sigchild + $descriptors = array_merge($descriptors, array(array('pipe', 'w'))); + + $this->commandline = '('.$this->commandline.') 3>/dev/null; code=$?; echo $code >&3; exit $code'; + } + + return $descriptors; + } + + /** + * Builds up the callback used by wait(). + * + * The callbacks adds all occurred output to the specific buffer and calls + * the user callback (if present) with the received output. + * + * @param callable|null $callback The user defined PHP callback + * + * @return \Closure A PHP closure + */ + protected function buildCallback($callback) + { + $that = $this; + $out = self::OUT; + $callback = function ($type, $data) use ($that, $callback, $out) { + if ($out == $type) { + $that->addOutput($data); + } else { + $that->addErrorOutput($data); + } + + if (null !== $callback) { + call_user_func($callback, $type, $data); + } + }; + + return $callback; + } + + /** + * Updates the status of the process, reads pipes. + * + * @param bool $blocking Whether to use a blocking read call. + */ + protected function updateStatus($blocking) + { + if (self::STATUS_STARTED !== $this->status) { + return; + } + + $this->processInformation = proc_get_status($this->process); + $this->captureExitCode(); + + $this->readPipes($blocking, '\\' === DIRECTORY_SEPARATOR ? !$this->processInformation['running'] : true); + + if (!$this->processInformation['running']) { + $this->close(); + } + } + + /** + * Returns whether PHP has been compiled with the '--enable-sigchild' option or not. + * + * @return bool + */ + protected function isSigchildEnabled() + { + if (null !== self::$sigchild) { + return self::$sigchild; + } + + if (!function_exists('phpinfo')) { + return self::$sigchild = false; + } + + ob_start(); + phpinfo(INFO_GENERAL); + + return self::$sigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); + } + + /** + * Validates and returns the filtered timeout. + * + * @param int|float|null $timeout + * + * @return float|null + * + * @throws InvalidArgumentException if the given timeout is a negative number + */ + private function validateTimeout($timeout) + { + $timeout = (float) $timeout; + + if (0.0 === $timeout) { + $timeout = null; + } elseif ($timeout < 0) { + throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); + } + + return $timeout; + } + + /** + * Reads pipes, executes callback. + * + * @param bool $blocking Whether to use blocking calls or not. + * @param bool $close Whether to close file handles or not. + */ + private function readPipes($blocking, $close) + { + $result = $this->processPipes->readAndWrite($blocking, $close); + + $callback = $this->callback; + foreach ($result as $type => $data) { + if (3 == $type) { + $this->fallbackExitcode = (int) $data; + } else { + $callback($type === self::STDOUT ? self::OUT : self::ERR, $data); + } + } + } + + /** + * Captures the exitcode if mentioned in the process information. + */ + private function captureExitCode() + { + if (isset($this->processInformation['exitcode']) && -1 != $this->processInformation['exitcode']) { + $this->exitcode = $this->processInformation['exitcode']; + } + } + + /** + * Closes process resource, closes file handles, sets the exitcode. + * + * @return int The exitcode + */ + private function close() + { + $this->processPipes->close(); + if (is_resource($this->process)) { + $exitcode = proc_close($this->process); + } else { + $exitcode = -1; + } + + $this->exitcode = -1 !== $exitcode ? $exitcode : (null !== $this->exitcode ? $this->exitcode : -1); + $this->status = self::STATUS_TERMINATED; + + if (-1 === $this->exitcode && null !== $this->fallbackExitcode) { + $this->exitcode = $this->fallbackExitcode; + } elseif (-1 === $this->exitcode && $this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) { + // if process has been signaled, no exitcode but a valid termsig, apply Unix convention + $this->exitcode = 128 + $this->processInformation['termsig']; + } + + return $this->exitcode; + } + + /** + * Resets data related to the latest run of the process. + */ + private function resetProcessData() + { + $this->starttime = null; + $this->callback = null; + $this->exitcode = null; + $this->fallbackExitcode = null; + $this->processInformation = null; + $this->stdout = null; + $this->stderr = null; + $this->process = null; + $this->latestSignal = null; + $this->status = self::STATUS_READY; + $this->incrementalOutputOffset = 0; + $this->incrementalErrorOutputOffset = 0; + } + + /** + * Sends a POSIX signal to the process. + * + * @param int $signal A valid POSIX signal (see http://www.php.net/manual/en/pcntl.constants.php) + * @param bool $throwException Whether to throw exception in case signal failed + * + * @return bool True if the signal was sent successfully, false otherwise + * + * @throws LogicException In case the process is not running + * @throws RuntimeException In case --enable-sigchild is activated + * @throws RuntimeException In case of failure + */ + private function doSignal($signal, $throwException) + { + if (!$this->isRunning()) { + if ($throwException) { + throw new LogicException('Can not send signal on a non running process.'); + } + + return false; + } + + if ($this->isSigchildEnabled()) { + if ($throwException) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + } + + return false; + } + + if ('\\' === DIRECTORY_SEPARATOR) { + exec(sprintf('taskkill /F /T /PID %d 2>&1', $this->getPid()), $output, $exitCode); + if ($exitCode) { + if ($throwException) { + throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output))); + } + + return false; + } + } + + if (true !== @proc_terminate($this->process, $signal) && '\\' !== DIRECTORY_SEPARATOR) { + if ($throwException) { + throw new RuntimeException(sprintf('Error while sending signal `%s`.', $signal)); + } + + return false; + } + + $this->latestSignal = $signal; + + return true; + } + + /** + * Ensures the process is running or terminated, throws a LogicException if the process has a not started. + * + * @param string $functionName The function name that was called. + * + * @throws LogicException If the process has not run. + */ + private function requireProcessIsStarted($functionName) + { + if (!$this->isStarted()) { + throw new LogicException(sprintf('Process must be started before calling %s.', $functionName)); + } + } + + /** + * Ensures the process is terminated, throws a LogicException if the process has a status different than `terminated`. + * + * @param string $functionName The function name that was called. + * + * @throws LogicException If the process is not yet terminated. + */ + private function requireProcessIsTerminated($functionName) + { + if (!$this->isTerminated()) { + throw new LogicException(sprintf('Process must be terminated before calling %s.', $functionName)); + } + } +} diff --git a/library/symfony/process/ProcessBuilder.php b/library/symfony/process/ProcessBuilder.php new file mode 100644 index 000000000..a782fd69e --- /dev/null +++ b/library/symfony/process/ProcessBuilder.php @@ -0,0 +1,287 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\InvalidArgumentException; +use Symfony\Component\Process\Exception\LogicException; + +/** + * Process builder. + * + * @author Kris Wallsmith <kris@symfony.com> + */ +class ProcessBuilder +{ + private $arguments; + private $cwd; + private $env = array(); + private $input; + private $timeout = 60; + private $options = array(); + private $inheritEnv = true; + private $prefix = array(); + private $outputDisabled = false; + + /** + * Constructor. + * + * @param string[] $arguments An array of arguments + */ + public function __construct(array $arguments = array()) + { + $this->arguments = $arguments; + } + + /** + * Creates a process builder instance. + * + * @param string[] $arguments An array of arguments + * + * @return ProcessBuilder + */ + public static function create(array $arguments = array()) + { + return new static($arguments); + } + + /** + * Adds an unescaped argument to the command string. + * + * @param string $argument A command argument + * + * @return ProcessBuilder + */ + public function add($argument) + { + $this->arguments[] = $argument; + + return $this; + } + + /** + * Adds a prefix to the command string. + * + * The prefix is preserved when resetting arguments. + * + * @param string|array $prefix A command prefix or an array of command prefixes + * + * @return ProcessBuilder + */ + public function setPrefix($prefix) + { + $this->prefix = is_array($prefix) ? $prefix : array($prefix); + + return $this; + } + + /** + * Sets the arguments of the process. + * + * Arguments must not be escaped. + * Previous arguments are removed. + * + * @param string[] $arguments + * + * @return ProcessBuilder + */ + public function setArguments(array $arguments) + { + $this->arguments = $arguments; + + return $this; + } + + /** + * Sets the working directory. + * + * @param null|string $cwd The working directory + * + * @return ProcessBuilder + */ + public function setWorkingDirectory($cwd) + { + $this->cwd = $cwd; + + return $this; + } + + /** + * Sets whether environment variables will be inherited or not. + * + * @param bool $inheritEnv + * + * @return ProcessBuilder + */ + public function inheritEnvironmentVariables($inheritEnv = true) + { + $this->inheritEnv = $inheritEnv; + + return $this; + } + + /** + * Sets an environment variable. + * + * Setting a variable overrides its previous value. Use `null` to unset a + * defined environment variable. + * + * @param string $name The variable name + * @param null|string $value The variable value + * + * @return ProcessBuilder + */ + public function setEnv($name, $value) + { + $this->env[$name] = $value; + + return $this; + } + + /** + * Adds a set of environment variables. + * + * Already existing environment variables with the same name will be + * overridden by the new values passed to this method. Pass `null` to unset + * a variable. + * + * @param array $variables The variables + * + * @return ProcessBuilder + */ + public function addEnvironmentVariables(array $variables) + { + $this->env = array_replace($this->env, $variables); + + return $this; + } + + /** + * Sets the input of the process. + * + * @param mixed $input The input as a string + * + * @return ProcessBuilder + * + * @throws InvalidArgumentException In case the argument is invalid + * + * Passing an object as an input is deprecated since version 2.5 and will be removed in 3.0. + */ + public function setInput($input) + { + $this->input = ProcessUtils::validateInput(sprintf('%s::%s', __CLASS__, __FUNCTION__), $input); + + return $this; + } + + /** + * Sets the process timeout. + * + * To disable the timeout, set this value to null. + * + * @param float|null $timeout + * + * @return ProcessBuilder + * + * @throws InvalidArgumentException + */ + public function setTimeout($timeout) + { + if (null === $timeout) { + $this->timeout = null; + + return $this; + } + + $timeout = (float) $timeout; + + if ($timeout < 0) { + throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); + } + + $this->timeout = $timeout; + + return $this; + } + + /** + * Adds a proc_open option. + * + * @param string $name The option name + * @param string $value The option value + * + * @return ProcessBuilder + */ + public function setOption($name, $value) + { + $this->options[$name] = $value; + + return $this; + } + + /** + * Disables fetching output and error output from the underlying process. + * + * @return ProcessBuilder + */ + public function disableOutput() + { + $this->outputDisabled = true; + + return $this; + } + + /** + * Enables fetching output and error output from the underlying process. + * + * @return ProcessBuilder + */ + public function enableOutput() + { + $this->outputDisabled = false; + + return $this; + } + + /** + * Creates a Process instance and returns it. + * + * @return Process + * + * @throws LogicException In case no arguments have been provided + */ + public function getProcess() + { + if (0 === count($this->prefix) && 0 === count($this->arguments)) { + throw new LogicException('You must add() command arguments before calling getProcess().'); + } + + $options = $this->options; + + $arguments = array_merge($this->prefix, $this->arguments); + $script = implode(' ', array_map(array(__NAMESPACE__.'\\ProcessUtils', 'escapeArgument'), $arguments)); + + if ($this->inheritEnv) { + // include $_ENV for BC purposes + $env = array_replace($_ENV, $_SERVER, $this->env); + } else { + $env = $this->env; + } + + $process = new Process($script, $this->cwd, $env, $this->input, $this->timeout, $options); + + if ($this->outputDisabled) { + $process->disableOutput(); + } + + return $process; + } +} diff --git a/library/symfony/process/ProcessUtils.php b/library/symfony/process/ProcessUtils.php new file mode 100644 index 000000000..4f30b630d --- /dev/null +++ b/library/symfony/process/ProcessUtils.php @@ -0,0 +1,115 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\InvalidArgumentException; + +/** + * ProcessUtils is a bunch of utility methods. + * + * This class contains static methods only and is not meant to be instantiated. + * + * @author Martin Hasoň <martin.hason@gmail.com> + */ +class ProcessUtils +{ + /** + * This class should not be instantiated. + */ + private function __construct() + { + } + + /** + * Escapes a string to be used as a shell argument. + * + * @param string $argument The argument that will be escaped + * + * @return string The escaped argument + */ + public static function escapeArgument($argument) + { + //Fix for PHP bug #43784 escapeshellarg removes % from given string + //Fix for PHP bug #49446 escapeshellarg doesn't work on Windows + //@see https://bugs.php.net/bug.php?id=43784 + //@see https://bugs.php.net/bug.php?id=49446 + if ('\\' === DIRECTORY_SEPARATOR) { + if ('' === $argument) { + return escapeshellarg($argument); + } + + $escapedArgument = ''; + $quote = false; + foreach (preg_split('/(")/', $argument, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) as $part) { + if ('"' === $part) { + $escapedArgument .= '\\"'; + } elseif (self::isSurroundedBy($part, '%')) { + // Avoid environment variable expansion + $escapedArgument .= '^%"'.substr($part, 1, -1).'"^%'; + } else { + // escape trailing backslash + if ('\\' === substr($part, -1)) { + $part .= '\\'; + } + $quote = true; + $escapedArgument .= $part; + } + } + if ($quote) { + $escapedArgument = '"'.$escapedArgument.'"'; + } + + return $escapedArgument; + } + + return escapeshellarg($argument); + } + + /** + * Validates and normalizes a Process input. + * + * @param string $caller The name of method call that validates the input + * @param mixed $input The input to validate + * + * @return string The validated input + * + * @throws InvalidArgumentException In case the input is not valid + * + * Passing an object as an input is deprecated since version 2.5 and will be removed in 3.0. + */ + public static function validateInput($caller, $input) + { + if (null !== $input) { + if (is_resource($input)) { + return $input; + } + if (is_scalar($input)) { + return (string) $input; + } + // deprecated as of Symfony 2.5, to be removed in 3.0 + if (is_object($input) && method_exists($input, '__toString')) { + @trigger_error('Passing an object as an input is deprecated since version 2.5 and will be removed in 3.0.', E_USER_DEPRECATED); + + return (string) $input; + } + + throw new InvalidArgumentException(sprintf('%s only accepts strings or stream resources.', $caller)); + } + + return $input; + } + + private static function isSurroundedBy($arg, $char) + { + return 2 < strlen($arg) && $char === $arg[0] && $char === $arg[strlen($arg) - 1]; + } +} diff --git a/library/symfony/process/README.md b/library/symfony/process/README.md new file mode 100644 index 000000000..7222fe895 --- /dev/null +++ b/library/symfony/process/README.md @@ -0,0 +1,65 @@ +Process Component +================= + +Process executes commands in sub-processes. + +In this example, we run a simple directory listing and get the result back: + +```php +use Symfony\Component\Process\Process; +use Symfony\Component\Process\Exception\ProcessFailedException; + +$process = new Process('ls -lsa'); +$process->setTimeout(3600); +$process->run(); +if (!$process->isSuccessful()) { + throw new ProcessFailedException($process); +} + +print $process->getOutput(); +``` + +You can think that this is easy to achieve with plain PHP but it's not especially +if you want to take care of the subtle differences between the different platforms. + +You can simplify the code by using `mustRun()` instead of `run()`, which will +throw a `ProcessFailedException` automatically in case of a problem: + +```php +use Symfony\Component\Process\Process; + +$process = new Process('ls -lsa'); +$process->setTimeout(3600); +$process->mustRun(); + +print $process->getOutput(); +``` + +And if you want to be able to get some feedback in real-time, just pass an +anonymous function to the ``run()`` method and you will get the output buffer +as it becomes available: + +```php +use Symfony\Component\Process\Process; + +$process = new Process('ls -lsa'); +$process->run(function ($type, $buffer) { + if (Process::ERR === $type) { + echo 'ERR > '.$buffer; + } else { + echo 'OUT > '.$buffer; + } +}); +``` + +That's great if you want to execute a long running command (like rsync-ing files to a +remote server) and give feedback to the user in real-time. + +Resources +--------- + +You can run the unit tests with the following command: + + $ cd path/to/Symfony/Component/Process/ + $ composer install + $ phpunit diff --git a/library/symfony/process/Tests/AbstractProcessTest.php b/library/symfony/process/Tests/AbstractProcessTest.php new file mode 100644 index 000000000..fca3729be --- /dev/null +++ b/library/symfony/process/Tests/AbstractProcessTest.php @@ -0,0 +1,1196 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use Symfony\Component\Process\Exception\LogicException; +use Symfony\Component\Process\Exception\ProcessTimedOutException; +use Symfony\Component\Process\Exception\RuntimeException; +use Symfony\Component\Process\PhpExecutableFinder; +use Symfony\Component\Process\Pipes\PipesInterface; +use Symfony\Component\Process\Process; + +/** + * @author Robert Schönthal <seroscho@googlemail.com> + */ +abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase +{ + protected static $phpBin; + + public static function setUpBeforeClass() + { + $phpBin = new PhpExecutableFinder(); + self::$phpBin = 'phpdbg' === PHP_SAPI ? 'php' : $phpBin->find(); + } + + public function testThatProcessDoesNotThrowWarningDuringRun() + { + @trigger_error('Test Error', E_USER_NOTICE); + $process = $this->getProcess(self::$phpBin." -r 'sleep(3)'"); + $process->run(); + $actualError = error_get_last(); + $this->assertEquals('Test Error', $actualError['message']); + $this->assertEquals(E_USER_NOTICE, $actualError['type']); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException + */ + public function testNegativeTimeoutFromConstructor() + { + $this->getProcess('', null, null, null, -1); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException + */ + public function testNegativeTimeoutFromSetter() + { + $p = $this->getProcess(''); + $p->setTimeout(-1); + } + + public function testFloatAndNullTimeout() + { + $p = $this->getProcess(''); + + $p->setTimeout(10); + $this->assertSame(10.0, $p->getTimeout()); + + $p->setTimeout(null); + $this->assertNull($p->getTimeout()); + + $p->setTimeout(0.0); + $this->assertNull($p->getTimeout()); + } + + public function testStopWithTimeoutIsActuallyWorking() + { + if (!extension_loaded('pcntl')) { + $this->markTestSkipped('Extension pcntl is required.'); + } + + // exec is mandatory here since we send a signal to the process + // see https://github.com/symfony/symfony/issues/5030 about prepending + // command with exec + $p = $this->getProcess('exec '.self::$phpBin.' '.__DIR__.'/NonStopableProcess.php 3'); + $p->start(); + usleep(100000); + $start = microtime(true); + $p->stop(1.1, SIGKILL); + while ($p->isRunning()) { + usleep(1000); + } + $duration = microtime(true) - $start; + + $this->assertLessThan(4, $duration); + } + + public function testAllOutputIsActuallyReadOnTermination() + { + // this code will result in a maximum of 2 reads of 8192 bytes by calling + // start() and isRunning(). by the time getOutput() is called the process + // has terminated so the internal pipes array is already empty. normally + // the call to start() will not read any data as the process will not have + // generated output, but this is non-deterministic so we must count it as + // a possibility. therefore we need 2 * PipesInterface::CHUNK_SIZE plus + // another byte which will never be read. + $expectedOutputSize = PipesInterface::CHUNK_SIZE * 2 + 2; + + $code = sprintf('echo str_repeat(\'*\', %d);', $expectedOutputSize); + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code))); + + $p->start(); + // Let's wait enough time for process to finish... + // Here we don't call Process::run or Process::wait to avoid any read of pipes + usleep(500000); + + if ($p->isRunning()) { + $this->markTestSkipped('Process execution did not complete in the required time frame'); + } + + $o = $p->getOutput(); + + $this->assertEquals($expectedOutputSize, strlen($o)); + } + + public function testCallbacksAreExecutedWithStart() + { + $data = ''; + + $process = $this->getProcess('echo foo && php -r "sleep(1);" && echo foo'); + $process->start(function ($type, $buffer) use (&$data) { + $data .= $buffer; + }); + + while ($process->isRunning()) { + usleep(10000); + } + + $this->assertEquals(2, preg_match_all('/foo/', $data, $matches)); + } + + /** + * tests results from sub processes. + * + * @dataProvider responsesCodeProvider + */ + public function testProcessResponses($expected, $getter, $code) + { + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code))); + $p->run(); + + $this->assertSame($expected, $p->$getter()); + } + + /** + * tests results from sub processes. + * + * @dataProvider pipesCodeProvider + */ + public function testProcessPipes($code, $size) + { + $expected = str_repeat(str_repeat('*', 1024), $size).'!'; + $expectedLength = (1024 * $size) + 1; + + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code))); + $p->setInput($expected); + $p->run(); + + $this->assertEquals($expectedLength, strlen($p->getOutput())); + $this->assertEquals($expectedLength, strlen($p->getErrorOutput())); + } + + /** + * @dataProvider pipesCodeProvider + */ + public function testSetStreamAsInput($code, $size) + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestIncomplete('This test fails with a timeout on Windows, can someone investigate please?'); + } + $expected = str_repeat(str_repeat('*', 1024), $size).'!'; + $expectedLength = (1024 * $size) + 1; + + $stream = fopen('php://temporary', 'w+'); + fwrite($stream, $expected); + rewind($stream); + + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg($code)), null, null, null, 5); + $p->setInput($stream); + $p->run(); + + fclose($stream); + + $this->assertEquals($expectedLength, strlen($p->getOutput())); + $this->assertEquals($expectedLength, strlen($p->getErrorOutput())); + } + + public function testSetInputWhileRunningThrowsAnException() + { + $process = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $process->start(); + try { + $process->setInput('foobar'); + $process->stop(); + $this->fail('A LogicException should have been raised.'); + } catch (LogicException $e) { + $this->assertEquals('Input can not be set while the process is running.', $e->getMessage()); + } + $process->stop(); + } + + /** + * @dataProvider provideInvalidInputValues + * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException + * @expectedExceptionMessage Symfony\Component\Process\Process::setInput only accepts strings or stream resources. + */ + public function testInvalidInput($value) + { + $process = $this->getProcess(self::$phpBin.' -v'); + $process->setInput($value); + } + + public function provideInvalidInputValues() + { + return array( + array(array()), + array(new NonStringifiable()), + ); + } + + /** + * @dataProvider provideInputValues + */ + public function testValidInput($expected, $value) + { + $process = $this->getProcess(self::$phpBin.' -v'); + $process->setInput($value); + $this->assertSame($expected, $process->getInput()); + } + + public function provideInputValues() + { + return array( + array(null, null), + array('24.5', 24.5), + array('input data', 'input data'), + ); + } + + /** + * @dataProvider provideLegacyInputValues + * @group legacy + */ + public function testLegacyValidInput($expected, $value) + { + $process = $this->getProcess(self::$phpBin.' -v'); + $process->setInput($value); + $this->assertSame($expected, $process->getInput()); + } + + public function provideLegacyInputValues() + { + return array( + array('stringifiable', new Stringifiable()), + ); + } + + public function chainedCommandsOutputProvider() + { + if ('\\' === DIRECTORY_SEPARATOR) { + return array( + array("2 \r\n2\r\n", '&&', '2'), + ); + } + + return array( + array("1\n1\n", ';', '1'), + array("2\n2\n", '&&', '2'), + ); + } + + /** + * @dataProvider chainedCommandsOutputProvider + */ + public function testChainedCommandsOutput($expected, $operator, $input) + { + $process = $this->getProcess(sprintf('echo %s %s echo %s', $input, $operator, $input)); + $process->run(); + $this->assertEquals($expected, $process->getOutput()); + } + + public function testCallbackIsExecutedForOutput() + { + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('echo \'foo\';'))); + + $called = false; + $p->run(function ($type, $buffer) use (&$called) { + $called = $buffer === 'foo'; + }); + + $this->assertTrue($called, 'The callback should be executed with the output'); + } + + public function testGetErrorOutput() + { + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }'))); + + $p->run(); + $this->assertEquals(3, preg_match_all('/ERROR/', $p->getErrorOutput(), $matches)); + } + + public function testGetIncrementalErrorOutput() + { + // use a lock file to toggle between writing ("W") and reading ("R") the + // error stream + $lock = tempnam(sys_get_temp_dir(), get_class($this).'Lock'); + file_put_contents($lock, 'W'); + + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { if (\'W\' === file_get_contents('.var_export($lock, true).')) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; file_put_contents('.var_export($lock, true).', \'R\'); } usleep(100); }'))); + + $p->start(); + while ($p->isRunning()) { + if ('R' === file_get_contents($lock)) { + $this->assertLessThanOrEqual(1, preg_match_all('/ERROR/', $p->getIncrementalErrorOutput(), $matches)); + file_put_contents($lock, 'W'); + } + usleep(100); + } + + unlink($lock); + } + + public function testFlushErrorOutput() + { + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; }'))); + + $p->run(); + $p->clearErrorOutput(); + $this->assertEmpty($p->getErrorOutput()); + } + + public function testGetEmptyIncrementalErrorOutput() + { + // use a lock file to toggle between writing ("W") and reading ("R") the + // output stream + $lock = tempnam(sys_get_temp_dir(), get_class($this).'Lock'); + file_put_contents($lock, 'W'); + + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { if (\'W\' === file_get_contents('.var_export($lock, true).')) { file_put_contents(\'php://stderr\', \'ERROR\'); $n++; file_put_contents('.var_export($lock, true).', \'R\'); } usleep(100); }'))); + + $p->start(); + + $shouldWrite = false; + + while ($p->isRunning()) { + if ('R' === file_get_contents($lock)) { + if (!$shouldWrite) { + $this->assertLessThanOrEqual(1, preg_match_all('/ERROR/', $p->getIncrementalOutput(), $matches)); + $shouldWrite = true; + } else { + $this->assertSame('', $p->getIncrementalOutput()); + + file_put_contents($lock, 'W'); + $shouldWrite = false; + } + } + usleep(100); + } + + unlink($lock); + } + + public function testGetOutput() + { + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { echo \' foo \'; $n++; }'))); + + $p->run(); + $this->assertEquals(3, preg_match_all('/foo/', $p->getOutput(), $matches)); + } + + public function testGetIncrementalOutput() + { + // use a lock file to toggle between writing ("W") and reading ("R") the + // output stream + $lock = tempnam(sys_get_temp_dir(), get_class($this).'Lock'); + file_put_contents($lock, 'W'); + + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { if (\'W\' === file_get_contents('.var_export($lock, true).')) { echo \' foo \'; $n++; file_put_contents('.var_export($lock, true).', \'R\'); } usleep(100); }'))); + + $p->start(); + while ($p->isRunning()) { + if ('R' === file_get_contents($lock)) { + $this->assertLessThanOrEqual(1, preg_match_all('/foo/', $p->getIncrementalOutput(), $matches)); + file_put_contents($lock, 'W'); + } + usleep(100); + } + + unlink($lock); + } + + public function testFlushOutput() + { + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n=0;while ($n<3) {echo \' foo \';$n++;}'))); + + $p->run(); + $p->clearOutput(); + $this->assertEmpty($p->getOutput()); + } + + public function testGetEmptyIncrementalOutput() + { + // use a lock file to toggle between writing ("W") and reading ("R") the + // output stream + $lock = tempnam(sys_get_temp_dir(), get_class($this).'Lock'); + file_put_contents($lock, 'W'); + + $p = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 0; while ($n < 3) { if (\'W\' === file_get_contents('.var_export($lock, true).')) { echo \' foo \'; $n++; file_put_contents('.var_export($lock, true).', \'R\'); } usleep(100); }'))); + + $p->start(); + + $shouldWrite = false; + + while ($p->isRunning()) { + if ('R' === file_get_contents($lock)) { + if (!$shouldWrite) { + $this->assertLessThanOrEqual(1, preg_match_all('/foo/', $p->getIncrementalOutput(), $matches)); + $shouldWrite = true; + } else { + $this->assertSame('', $p->getIncrementalOutput()); + + file_put_contents($lock, 'W'); + $shouldWrite = false; + } + } + usleep(100); + } + + unlink($lock); + } + + public function testZeroAsOutput() + { + if ('\\' === DIRECTORY_SEPARATOR) { + // see http://stackoverflow.com/questions/7105433/windows-batch-echo-without-new-line + $p = $this->getProcess('echo | set /p dummyName=0'); + } else { + $p = $this->getProcess('printf 0'); + } + + $p->run(); + $this->assertSame('0', $p->getOutput()); + } + + public function testExitCodeCommandFailed() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support POSIX exit code'); + } + + // such command run in bash return an exitcode 127 + $process = $this->getProcess('nonexistingcommandIhopeneversomeonewouldnameacommandlikethis'); + $process->run(); + + $this->assertGreaterThan(0, $process->getExitCode()); + } + + public function testTTYCommand() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does have /dev/tty support'); + } + + $process = $this->getProcess('echo "foo" >> /dev/null && '.self::$phpBin.' -r "usleep(100000);"'); + $process->setTty(true); + $process->start(); + $this->assertTrue($process->isRunning()); + $process->wait(); + + $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus()); + } + + public function testTTYCommandExitCode() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does have /dev/tty support'); + } + + $process = $this->getProcess('echo "foo" >> /dev/null'); + $process->setTty(true); + $process->run(); + + $this->assertTrue($process->isSuccessful()); + } + + public function testTTYInWindowsEnvironment() + { + if ('\\' !== DIRECTORY_SEPARATOR) { + $this->markTestSkipped('This test is for Windows platform only'); + } + + $process = $this->getProcess('echo "foo" >> /dev/null'); + $process->setTty(false); + $this->setExpectedException('Symfony\Component\Process\Exception\RuntimeException', 'TTY mode is not supported on Windows platform.'); + $process->setTty(true); + } + + public function testExitCodeTextIsNullWhenExitCodeIsNull() + { + $process = $this->getProcess(''); + $this->assertNull($process->getExitCodeText()); + } + + public function testPTYCommand() + { + if (!Process::isPtySupported()) { + $this->markTestSkipped('PTY is not supported on this operating system.'); + } + + $process = $this->getProcess('echo "foo"'); + $process->setPty(true); + $process->run(); + + $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus()); + $this->assertEquals("foo\r\n", $process->getOutput()); + } + + public function testMustRun() + { + $process = $this->getProcess('echo foo'); + + $this->assertSame($process, $process->mustRun()); + $this->assertEquals('foo'.PHP_EOL, $process->getOutput()); + } + + public function testSuccessfulMustRunHasCorrectExitCode() + { + $process = $this->getProcess('echo foo')->mustRun(); + $this->assertEquals(0, $process->getExitCode()); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\ProcessFailedException + */ + public function testMustRunThrowsException() + { + $process = $this->getProcess('exit 1'); + $process->mustRun(); + } + + public function testExitCodeText() + { + $process = $this->getProcess(''); + $r = new \ReflectionObject($process); + $p = $r->getProperty('exitcode'); + $p->setAccessible(true); + + $p->setValue($process, 2); + $this->assertEquals('Misuse of shell builtins', $process->getExitCodeText()); + } + + public function testStartIsNonBlocking() + { + $process = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $start = microtime(true); + $process->start(); + $end = microtime(true); + $this->assertLessThan(0.4, $end - $start); + $process->wait(); + } + + public function testUpdateStatus() + { + $process = $this->getProcess(self::$phpBin.' -v'); + $process->run(); + $this->assertTrue(strlen($process->getOutput()) > 0); + } + + public function testGetExitCodeIsNullOnStart() + { + $process = $this->getProcess(self::$phpBin.' -r "usleep(200000);"'); + $this->assertNull($process->getExitCode()); + $process->start(); + $this->assertNull($process->getExitCode()); + $process->wait(); + $this->assertEquals(0, $process->getExitCode()); + } + + public function testGetExitCodeIsNullOnWhenStartingAgain() + { + $process = $this->getProcess(self::$phpBin.' -r "usleep(200000);"'); + $process->run(); + $this->assertEquals(0, $process->getExitCode()); + $process->start(); + $this->assertNull($process->getExitCode()); + $process->wait(); + $this->assertEquals(0, $process->getExitCode()); + } + + public function testGetExitCode() + { + $process = $this->getProcess(self::$phpBin.' -v'); + $process->run(); + $this->assertSame(0, $process->getExitCode()); + } + + public function testStatus() + { + $process = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $this->assertFalse($process->isRunning()); + $this->assertFalse($process->isStarted()); + $this->assertFalse($process->isTerminated()); + $this->assertSame(Process::STATUS_READY, $process->getStatus()); + $process->start(); + $this->assertTrue($process->isRunning()); + $this->assertTrue($process->isStarted()); + $this->assertFalse($process->isTerminated()); + $this->assertSame(Process::STATUS_STARTED, $process->getStatus()); + $process->wait(); + $this->assertFalse($process->isRunning()); + $this->assertTrue($process->isStarted()); + $this->assertTrue($process->isTerminated()); + $this->assertSame(Process::STATUS_TERMINATED, $process->getStatus()); + } + + public function testStop() + { + $process = $this->getProcess(self::$phpBin.' -r "sleep(4);"'); + $process->start(); + $this->assertTrue($process->isRunning()); + $process->stop(); + $this->assertFalse($process->isRunning()); + } + + public function testIsSuccessful() + { + $process = $this->getProcess(self::$phpBin.' -v'); + $process->run(); + $this->assertTrue($process->isSuccessful()); + } + + public function testIsSuccessfulOnlyAfterTerminated() + { + $process = $this->getProcess(self::$phpBin.' -r "sleep(1);"'); + $process->start(); + + $this->assertFalse($process->isSuccessful()); + + while ($process->isRunning()) { + usleep(300000); + } + + $this->assertTrue($process->isSuccessful()); + } + + public function testIsNotSuccessful() + { + $process = $this->getProcess(self::$phpBin.' -r "usleep(500000);throw new \Exception(\'BOUM\');"'); + $process->start(); + $this->assertTrue($process->isRunning()); + $process->wait(); + $this->assertFalse($process->isSuccessful()); + } + + public function testProcessIsNotSignaled() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support POSIX signals'); + } + + $process = $this->getProcess(self::$phpBin.' -v'); + $process->run(); + $this->assertFalse($process->hasBeenSignaled()); + } + + public function testProcessWithoutTermSignalIsNotSignaled() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support POSIX signals'); + } + + $process = $this->getProcess(self::$phpBin.' -v'); + $process->run(); + $this->assertFalse($process->hasBeenSignaled()); + } + + public function testProcessWithoutTermSignal() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support POSIX signals'); + } + + $process = $this->getProcess(self::$phpBin.' -v'); + $process->run(); + $this->assertEquals(0, $process->getTermSignal()); + } + + public function testProcessIsSignaledIfStopped() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support POSIX signals'); + } + + $process = $this->getProcess(self::$phpBin.' -r "sleep(4);"'); + $process->start(); + $process->stop(); + $this->assertTrue($process->hasBeenSignaled()); + } + + public function testProcessWithTermSignal() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Windows does not support POSIX signals'); + } + + // SIGTERM is only defined if pcntl extension is present + $termSignal = defined('SIGTERM') ? SIGTERM : 15; + + $process = $this->getProcess(self::$phpBin.' -r "sleep(4);"'); + $process->start(); + $process->stop(); + + $this->assertEquals($termSignal, $process->getTermSignal()); + } + + public function testProcessThrowsExceptionWhenExternallySignaled() + { + if (!function_exists('posix_kill')) { + $this->markTestSkipped('Function posix_kill is required.'); + } + + $termSignal = defined('SIGKILL') ? SIGKILL : 9; + + $process = $this->getProcess('exec '.self::$phpBin.' -r "while (true) {}"'); + $process->start(); + posix_kill($process->getPid(), $termSignal); + + $this->setExpectedException('Symfony\Component\Process\Exception\RuntimeException', 'The process has been signaled with signal "9".'); + $process->wait(); + } + + public function testRestart() + { + $process1 = $this->getProcess(self::$phpBin.' -r "echo getmypid();"'); + $process1->run(); + $process2 = $process1->restart(); + + $process2->wait(); // wait for output + + // Ensure that both processed finished and the output is numeric + $this->assertFalse($process1->isRunning()); + $this->assertFalse($process2->isRunning()); + $this->assertTrue(is_numeric($process1->getOutput())); + $this->assertTrue(is_numeric($process2->getOutput())); + + // Ensure that restart returned a new process by check that the output is different + $this->assertNotEquals($process1->getOutput(), $process2->getOutput()); + } + + public function testRunProcessWithTimeout() + { + $timeout = 0.5; + $process = $this->getProcess(self::$phpBin.' -r "usleep(600000);"'); + $process->setTimeout($timeout); + $start = microtime(true); + try { + $process->run(); + $this->fail('A RuntimeException should have been raised'); + } catch (RuntimeException $e) { + } + $duration = microtime(true) - $start; + + if ('\\' === DIRECTORY_SEPARATOR) { + // Windows is a bit slower as it read file handles, then allow twice the precision + $maxDuration = $timeout + 2 * Process::TIMEOUT_PRECISION; + } else { + $maxDuration = $timeout + Process::TIMEOUT_PRECISION; + } + + $this->assertLessThan($maxDuration, $duration); + } + + public function testCheckTimeoutOnNonStartedProcess() + { + $process = $this->getProcess(self::$phpBin.' -r "sleep(3);"'); + $process->checkTimeout(); + } + + public function testCheckTimeoutOnTerminatedProcess() + { + $process = $this->getProcess(self::$phpBin.' -v'); + $process->run(); + $process->checkTimeout(); + } + + public function testCheckTimeoutOnStartedProcess() + { + $timeout = 0.5; + $precision = 100000; + $process = $this->getProcess(self::$phpBin.' -r "sleep(3);"'); + $process->setTimeout($timeout); + $start = microtime(true); + + $process->start(); + + try { + while ($process->isRunning()) { + $process->checkTimeout(); + usleep($precision); + } + $this->fail('A RuntimeException should have been raised'); + } catch (RuntimeException $e) { + } + $duration = microtime(true) - $start; + + $this->assertLessThan($timeout + $precision, $duration); + $this->assertFalse($process->isSuccessful()); + } + + public function testIdleTimeout() + { + $process = $this->getProcess(self::$phpBin.' -r "sleep(3);"'); + $process->setTimeout(10); + $process->setIdleTimeout(0.5); + + try { + $process->run(); + + $this->fail('A timeout exception was expected.'); + } catch (ProcessTimedOutException $ex) { + $this->assertTrue($ex->isIdleTimeout()); + $this->assertFalse($ex->isGeneralTimeout()); + $this->assertEquals(0.5, $ex->getExceededTimeout()); + } + } + + public function testIdleTimeoutNotExceededWhenOutputIsSent() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestIncomplete('This test fails with a timeout on Windows, can someone investigate please?'); + } + $process = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 30; while ($n--) {echo "foo\n"; usleep(100000); }'))); + $process->setTimeout(2); + $process->setIdleTimeout(1); + + try { + $process->run(); + $this->fail('A timeout exception was expected.'); + } catch (ProcessTimedOutException $ex) { + $this->assertTrue($ex->isGeneralTimeout(), 'A general timeout is expected.'); + $this->assertFalse($ex->isIdleTimeout(), 'No idle timeout is expected.'); + $this->assertEquals(2, $ex->getExceededTimeout()); + } + } + + public function testStartAfterATimeout() + { + $process = $this->getProcess(sprintf('%s -r %s', self::$phpBin, escapeshellarg('$n = 1000; while ($n--) {echo \'\'; usleep(1000); }'))); + $process->setTimeout(0.1); + + try { + $process->run(); + $this->fail('A RuntimeException should have been raised.'); + } catch (RuntimeException $e) { + } + $process->start(); + usleep(1000); + $process->stop(); + } + + public function testGetPid() + { + $process = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $process->start(); + $this->assertGreaterThan(0, $process->getPid()); + $process->wait(); + } + + public function testGetPidIsNullBeforeStart() + { + $process = $this->getProcess(self::$phpBin.' -r "sleep(1);"'); + $this->assertNull($process->getPid()); + } + + public function testGetPidIsNullAfterRun() + { + $process = $this->getProcess(self::$phpBin.' -v'); + $process->run(); + $this->assertNull($process->getPid()); + } + + public function testSignal() + { + if (!extension_loaded('pcntl')) { + $this->markTestSkipped('Extension pcntl is required.'); + } + + $process = $this->getProcess('exec php -f '.__DIR__.'/SignalListener.php'); + $process->start(); + usleep(500000); + $process->signal(SIGUSR1); + + while ($process->isRunning() && false === strpos($process->getOutput(), 'Caught SIGUSR1')) { + usleep(10000); + } + + $this->assertEquals('Caught SIGUSR1', $process->getOutput()); + } + + public function testExitCodeIsAvailableAfterSignal() + { + if (!extension_loaded('pcntl')) { + $this->markTestSkipped('Extension pcntl is required.'); + } + + $process = $this->getProcess('sleep 4'); + $process->start(); + $process->signal(SIGKILL); + + while ($process->isRunning()) { + usleep(10000); + } + + $this->assertFalse($process->isRunning()); + $this->assertTrue($process->hasBeenSignaled()); + $this->assertFalse($process->isSuccessful()); + $this->assertEquals(137, $process->getExitCode()); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\LogicException + */ + public function testSignalProcessNotRunning() + { + if (!extension_loaded('pcntl')) { + $this->markTestSkipped('Extension pcntl is required.'); + } + + $process = $this->getProcess(self::$phpBin.' -v'); + $process->signal(SIGHUP); + } + + /** + * @dataProvider provideMethodsThatNeedARunningProcess + */ + public function testMethodsThatNeedARunningProcess($method) + { + $process = $this->getProcess(self::$phpBin.' -v'); + $this->setExpectedException('Symfony\Component\Process\Exception\LogicException', sprintf('Process must be started before calling %s.', $method)); + $process->{$method}(); + } + + public function provideMethodsThatNeedARunningProcess() + { + return array( + array('getOutput'), + array('getIncrementalOutput'), + array('getErrorOutput'), + array('getIncrementalErrorOutput'), + array('wait'), + ); + } + + /** + * @dataProvider provideMethodsThatNeedATerminatedProcess + */ + public function testMethodsThatNeedATerminatedProcess($method) + { + $process = $this->getProcess(self::$phpBin.' -r "sleep(1);"'); + $process->start(); + try { + $process->{$method}(); + $process->stop(0); + $this->fail('A LogicException must have been thrown'); + } catch (\Exception $e) { + $this->assertInstanceOf('Symfony\Component\Process\Exception\LogicException', $e); + $this->assertEquals(sprintf('Process must be terminated before calling %s.', $method), $e->getMessage()); + } + $process->stop(0); + } + + public function provideMethodsThatNeedATerminatedProcess() + { + return array( + array('hasBeenSignaled'), + array('getTermSignal'), + array('hasBeenStopped'), + array('getStopSignal'), + ); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + */ + public function testSignalWithWrongIntSignal() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('POSIX signals do not work on Windows'); + } + + $process = $this->getProcess(self::$phpBin.' -r "sleep(3);"'); + $process->start(); + $process->signal(-4); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + */ + public function testSignalWithWrongNonIntSignal() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('POSIX signals do not work on Windows'); + } + + $process = $this->getProcess(self::$phpBin.' -r "sleep(3);"'); + $process->start(); + $process->signal('Céphalopodes'); + } + + public function testDisableOutputDisablesTheOutput() + { + $p = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $this->assertFalse($p->isOutputDisabled()); + $p->disableOutput(); + $this->assertTrue($p->isOutputDisabled()); + $p->enableOutput(); + $this->assertFalse($p->isOutputDisabled()); + } + + public function testDisableOutputWhileRunningThrowsException() + { + $p = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $p->start(); + $this->setExpectedException('Symfony\Component\Process\Exception\RuntimeException', 'Disabling output while the process is running is not possible.'); + $p->disableOutput(); + } + + public function testEnableOutputWhileRunningThrowsException() + { + $p = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $p->disableOutput(); + $p->start(); + $this->setExpectedException('Symfony\Component\Process\Exception\RuntimeException', 'Enabling output while the process is running is not possible.'); + $p->enableOutput(); + } + + public function testEnableOrDisableOutputAfterRunDoesNotThrowException() + { + $p = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $p->disableOutput(); + $p->start(); + $p->wait(); + $p->enableOutput(); + $p->disableOutput(); + } + + public function testDisableOutputWhileIdleTimeoutIsSet() + { + $process = $this->getProcess('sleep 3'); + $process->setIdleTimeout(1); + $this->setExpectedException('Symfony\Component\Process\Exception\LogicException', 'Output can not be disabled while an idle timeout is set.'); + $process->disableOutput(); + } + + public function testSetIdleTimeoutWhileOutputIsDisabled() + { + $process = $this->getProcess('sleep 3'); + $process->disableOutput(); + $this->setExpectedException('Symfony\Component\Process\Exception\LogicException', 'Idle timeout can not be set while the output is disabled.'); + $process->setIdleTimeout(1); + } + + public function testSetNullIdleTimeoutWhileOutputIsDisabled() + { + $process = $this->getProcess('sleep 3'); + $process->disableOutput(); + $process->setIdleTimeout(null); + } + + /** + * @dataProvider provideStartMethods + */ + public function testStartWithACallbackAndDisabledOutput($startMethod, $exception, $exceptionMessage) + { + $p = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $p->disableOutput(); + $this->setExpectedException($exception, $exceptionMessage); + $p->{$startMethod}(function () {}); + } + + public function provideStartMethods() + { + return array( + array('start', 'Symfony\Component\Process\Exception\LogicException', 'Output has been disabled, enable it to allow the use of a callback.'), + array('run', 'Symfony\Component\Process\Exception\LogicException', 'Output has been disabled, enable it to allow the use of a callback.'), + array('mustRun', 'Symfony\Component\Process\Exception\LogicException', 'Output has been disabled, enable it to allow the use of a callback.'), + ); + } + + /** + * @dataProvider provideOutputFetchingMethods + */ + public function testGetOutputWhileDisabled($fetchMethod) + { + $p = $this->getProcess(self::$phpBin.' -r "usleep(500000);"'); + $p->disableOutput(); + $p->start(); + $this->setExpectedException('Symfony\Component\Process\Exception\LogicException', 'Output has been disabled.'); + $p->{$fetchMethod}(); + } + + public function provideOutputFetchingMethods() + { + return array( + array('getOutput'), + array('getIncrementalOutput'), + array('getErrorOutput'), + array('getIncrementalErrorOutput'), + ); + } + + public function responsesCodeProvider() + { + return array( + //expected output / getter / code to execute + //array(1,'getExitCode','exit(1);'), + //array(true,'isSuccessful','exit();'), + array('output', 'getOutput', 'echo \'output\';'), + ); + } + + public function pipesCodeProvider() + { + $variations = array( + 'fwrite(STDOUT, $in = file_get_contents(\'php://stdin\')); fwrite(STDERR, $in);', + 'include \''.__DIR__.'/PipeStdinInStdoutStdErrStreamSelect.php\';', + ); + + if ('\\' === DIRECTORY_SEPARATOR) { + // Avoid XL buffers on Windows because of https://bugs.php.net/bug.php?id=65650 + $sizes = array(1, 2, 4, 8); + } else { + $sizes = array(1, 16, 64, 1024, 4096); + } + + $codes = array(); + foreach ($sizes as $size) { + foreach ($variations as $code) { + $codes[] = array($code, $size); + } + } + + return $codes; + } + + /** + * provides default method names for simple getter/setter. + */ + public function methodProvider() + { + $defaults = array( + array('CommandLine'), + array('Timeout'), + array('WorkingDirectory'), + array('Env'), + array('Stdin'), + array('Input'), + array('Options'), + ); + + return $defaults; + } + + /** + * @param string $commandline + * @param null|string $cwd + * @param null|array $env + * @param null|string $input + * @param int $timeout + * @param array $options + * + * @return Process + */ + abstract protected function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array()); +} + +class Stringifiable +{ + public function __toString() + { + return 'stringifiable'; + } +} + +class NonStringifiable +{ +} diff --git a/library/symfony/process/Tests/ExecutableFinderTest.php b/library/symfony/process/Tests/ExecutableFinderTest.php new file mode 100644 index 000000000..812429e88 --- /dev/null +++ b/library/symfony/process/Tests/ExecutableFinderTest.php @@ -0,0 +1,144 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use Symfony\Component\Process\ExecutableFinder; + +/** + * @author Chris Smith <chris@cs278.org> + */ +class ExecutableFinderTest extends \PHPUnit_Framework_TestCase +{ + private $path; + + protected function tearDown() + { + if ($this->path) { + // Restore path if it was changed. + putenv('PATH='.$this->path); + } + } + + private function setPath($path) + { + $this->path = getenv('PATH'); + putenv('PATH='.$path); + } + + /** + * @requires PHP 5.4 + */ + public function testFind() + { + if (ini_get('open_basedir')) { + $this->markTestSkipped('Cannot test when open_basedir is set'); + } + + $this->setPath(dirname(PHP_BINARY)); + + $finder = new ExecutableFinder(); + $result = $finder->find($this->getPhpBinaryName()); + + $this->assertSamePath(PHP_BINARY, $result); + } + + public function testFindWithDefault() + { + if (ini_get('open_basedir')) { + $this->markTestSkipped('Cannot test when open_basedir is set'); + } + + $expected = 'defaultValue'; + + $this->setPath(''); + + $finder = new ExecutableFinder(); + $result = $finder->find('foo', $expected); + + $this->assertEquals($expected, $result); + } + + /** + * @requires PHP 5.4 + */ + public function testFindWithExtraDirs() + { + if (ini_get('open_basedir')) { + $this->markTestSkipped('Cannot test when open_basedir is set'); + } + + $this->setPath(''); + + $extraDirs = array(dirname(PHP_BINARY)); + + $finder = new ExecutableFinder(); + $result = $finder->find($this->getPhpBinaryName(), null, $extraDirs); + + $this->assertSamePath(PHP_BINARY, $result); + } + + /** + * @requires PHP 5.4 + */ + public function testFindWithOpenBaseDir() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Cannot run test on windows'); + } + + if (ini_get('open_basedir')) { + $this->markTestSkipped('Cannot test when open_basedir is set'); + } + + $this->iniSet('open_basedir', dirname(PHP_BINARY).(!defined('HHVM_VERSION') ? PATH_SEPARATOR.'/' : '')); + + $finder = new ExecutableFinder(); + $result = $finder->find($this->getPhpBinaryName()); + + $this->assertSamePath(PHP_BINARY, $result); + } + + /** + * @requires PHP 5.4 + */ + public function testFindProcessInOpenBasedir() + { + if (ini_get('open_basedir')) { + $this->markTestSkipped('Cannot test when open_basedir is set'); + } + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Cannot run test on windows'); + } + + $this->setPath(''); + $this->iniSet('open_basedir', PHP_BINARY.(!defined('HHVM_VERSION') ? PATH_SEPARATOR.'/' : '')); + + $finder = new ExecutableFinder(); + $result = $finder->find($this->getPhpBinaryName(), false); + + $this->assertSamePath(PHP_BINARY, $result); + } + + private function assertSamePath($expected, $tested) + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->assertEquals(strtolower($expected), strtolower($tested)); + } else { + $this->assertEquals($expected, $tested); + } + } + + private function getPhpBinaryName() + { + return basename(PHP_BINARY, '\\' === DIRECTORY_SEPARATOR ? '.exe' : ''); + } +} diff --git a/library/symfony/process/Tests/NonStopableProcess.php b/library/symfony/process/Tests/NonStopableProcess.php new file mode 100644 index 000000000..54510c16a --- /dev/null +++ b/library/symfony/process/Tests/NonStopableProcess.php @@ -0,0 +1,45 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Runs a PHP script that can be stopped only with a SIGKILL (9) signal for 3 seconds. + * + * @args duration Run this script with a custom duration + * + * @example `php NonStopableProcess.php 42` will run the script for 42 seconds + */ +function handleSignal($signal) +{ + switch ($signal) { + case SIGTERM: + $name = 'SIGTERM'; + break; + case SIGINT: + $name = 'SIGINT'; + break; + default: + $name = $signal.' (unknown)'; + break; + } + + echo "received signal $name\n"; +} + +declare (ticks = 1); +pcntl_signal(SIGTERM, 'handleSignal'); +pcntl_signal(SIGINT, 'handleSignal'); + +$duration = isset($argv[1]) ? (int) $argv[1] : 3; +$start = microtime(true); + +while ($duration > (microtime(true) - $start)) { + usleep(1000); +} diff --git a/library/symfony/process/Tests/PhpExecutableFinderTest.php b/library/symfony/process/Tests/PhpExecutableFinderTest.php new file mode 100644 index 000000000..87d0efe9e --- /dev/null +++ b/library/symfony/process/Tests/PhpExecutableFinderTest.php @@ -0,0 +1,119 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use Symfony\Component\Process\PhpExecutableFinder; + +/** + * @author Robert Schönthal <seroscho@googlemail.com> + */ +class PhpExecutableFinderTest extends \PHPUnit_Framework_TestCase +{ + /** + * tests find() with the env var PHP_PATH. + */ + public function testFindWithPhpPath() + { + if (defined('PHP_BINARY')) { + $this->markTestSkipped('The PHP binary is easily available as of PHP 5.4'); + } + + $f = new PhpExecutableFinder(); + + $current = $f->find(); + + //not executable PHP_PATH + putenv('PHP_PATH=/not/executable/php'); + $this->assertFalse($f->find(), '::find() returns false for not executable PHP'); + $this->assertFalse($f->find(false), '::find() returns false for not executable PHP'); + + //executable PHP_PATH + putenv('PHP_PATH='.$current); + $this->assertEquals($f->find(), $current, '::find() returns the executable PHP'); + $this->assertEquals($f->find(false), $current, '::find() returns the executable PHP'); + } + + /** + * tests find() with the constant PHP_BINARY. + * + * @requires PHP 5.4 + */ + public function testFind() + { + if (defined('HHVM_VERSION')) { + $this->markTestSkipped('Should not be executed in HHVM context.'); + } + + $f = new PhpExecutableFinder(); + + $current = PHP_BINARY; + $args = 'phpdbg' === PHP_SAPI ? ' -qrr' : ''; + + $this->assertEquals($current.$args, $f->find(), '::find() returns the executable PHP'); + $this->assertEquals($current, $f->find(false), '::find() returns the executable PHP'); + } + + /** + * tests find() with the env var / constant PHP_BINARY with HHVM. + */ + public function testFindWithHHVM() + { + if (!defined('HHVM_VERSION')) { + $this->markTestSkipped('Should be executed in HHVM context.'); + } + + $f = new PhpExecutableFinder(); + + $current = getenv('PHP_BINARY') ?: PHP_BINARY; + + $this->assertEquals($current.' --php', $f->find(), '::find() returns the executable PHP'); + $this->assertEquals($current, $f->find(false), '::find() returns the executable PHP'); + } + + /** + * tests find() with the env var PHP_PATH. + */ + public function testFindArguments() + { + $f = new PhpExecutableFinder(); + + if (defined('HHVM_VERSION')) { + $this->assertEquals($f->findArguments(), array('--php'), '::findArguments() returns HHVM arguments'); + } elseif ('phpdbg' === PHP_SAPI) { + $this->assertEquals($f->findArguments(), array('-qrr'), '::findArguments() returns phpdbg arguments'); + } else { + $this->assertEquals($f->findArguments(), array(), '::findArguments() returns no arguments'); + } + } + + /** + * tests find() with default executable. + */ + public function testFindWithSuffix() + { + if (defined('PHP_BINARY')) { + $this->markTestSkipped('The PHP binary is easily available as of PHP 5.4'); + } + + putenv('PHP_PATH='); + putenv('PHP_PEAR_PHP_BIN='); + $f = new PhpExecutableFinder(); + + $current = $f->find(); + + //TODO maybe php executable is custom or even Windows + if ('\\' === DIRECTORY_SEPARATOR) { + $this->assertTrue(is_executable($current)); + $this->assertTrue((bool) preg_match('/'.addslashes(DIRECTORY_SEPARATOR).'php\.(exe|bat|cmd|com)$/i', $current), '::find() returns the executable PHP with suffixes'); + } + } +} diff --git a/library/symfony/process/Tests/PhpProcessTest.php b/library/symfony/process/Tests/PhpProcessTest.php new file mode 100644 index 000000000..2cf79aa1a --- /dev/null +++ b/library/symfony/process/Tests/PhpProcessTest.php @@ -0,0 +1,53 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use Symfony\Component\Process\PhpExecutableFinder; +use Symfony\Component\Process\PhpProcess; + +class PhpProcessTest extends \PHPUnit_Framework_TestCase +{ + public function testNonBlockingWorks() + { + $expected = 'hello world!'; + $process = new PhpProcess(<<<PHP +<?php echo '$expected'; +PHP + ); + $process->start(); + $process->wait(); + $this->assertEquals($expected, $process->getOutput()); + } + + public function testCommandLine() + { + if ('phpdbg' === PHP_SAPI) { + $this->markTestSkipped('phpdbg SAPI is not supported by this test.'); + } + + $process = new PhpProcess(<<<PHP +<?php echo 'foobar'; +PHP + ); + + $f = new PhpExecutableFinder(); + $commandLine = $f->find(); + + $this->assertSame($commandLine, $process->getCommandLine(), '::getCommandLine() returns the command line of PHP before start'); + + $process->start(); + $this->assertSame($commandLine, $process->getCommandLine(), '::getCommandLine() returns the command line of PHP after start'); + + $process->wait(); + $this->assertSame($commandLine, $process->getCommandLine(), '::getCommandLine() returns the command line of PHP after wait'); + } +} diff --git a/library/symfony/process/Tests/PipeStdinInStdoutStdErrStreamSelect.php b/library/symfony/process/Tests/PipeStdinInStdoutStdErrStreamSelect.php new file mode 100644 index 000000000..bbd7ddfeb --- /dev/null +++ b/library/symfony/process/Tests/PipeStdinInStdoutStdErrStreamSelect.php @@ -0,0 +1,72 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +define('ERR_SELECT_FAILED', 1); +define('ERR_TIMEOUT', 2); +define('ERR_READ_FAILED', 3); +define('ERR_WRITE_FAILED', 4); + +$read = array(STDIN); +$write = array(STDOUT, STDERR); + +stream_set_blocking(STDIN, 0); +stream_set_blocking(STDOUT, 0); +stream_set_blocking(STDERR, 0); + +$out = $err = ''; +while ($read || $write) { + $r = $read; + $w = $write; + $e = null; + $n = stream_select($r, $w, $e, 5); + + if (false === $n) { + die(ERR_SELECT_FAILED); + } elseif ($n < 1) { + die(ERR_TIMEOUT); + } + + if (in_array(STDOUT, $w) && strlen($out) > 0) { + $written = fwrite(STDOUT, (binary) $out, 32768); + if (false === $written) { + die(ERR_WRITE_FAILED); + } + $out = (binary) substr($out, $written); + } + if (null === $read && '' === $out) { + $write = array_diff($write, array(STDOUT)); + } + + if (in_array(STDERR, $w) && strlen($err) > 0) { + $written = fwrite(STDERR, (binary) $err, 32768); + if (false === $written) { + die(ERR_WRITE_FAILED); + } + $err = (binary) substr($err, $written); + } + if (null === $read && '' === $err) { + $write = array_diff($write, array(STDERR)); + } + + if ($r) { + $str = fread(STDIN, 32768); + if (false !== $str) { + $out .= $str; + $err .= $str; + } + if (false === $str || feof(STDIN)) { + $read = null; + if (!feof(STDIN)) { + die(ERR_READ_FAILED); + } + } + } +} diff --git a/library/symfony/process/Tests/ProcessBuilderTest.php b/library/symfony/process/Tests/ProcessBuilderTest.php new file mode 100644 index 000000000..1b5056d1b --- /dev/null +++ b/library/symfony/process/Tests/ProcessBuilderTest.php @@ -0,0 +1,225 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use Symfony\Component\Process\ProcessBuilder; + +class ProcessBuilderTest extends \PHPUnit_Framework_TestCase +{ + public function testInheritEnvironmentVars() + { + $_ENV['MY_VAR_1'] = 'foo'; + + $proc = ProcessBuilder::create() + ->add('foo') + ->getProcess(); + + unset($_ENV['MY_VAR_1']); + + $env = $proc->getEnv(); + $this->assertArrayHasKey('MY_VAR_1', $env); + $this->assertEquals('foo', $env['MY_VAR_1']); + } + + public function testAddEnvironmentVariables() + { + $pb = new ProcessBuilder(); + $env = array( + 'foo' => 'bar', + 'foo2' => 'bar2', + ); + $proc = $pb + ->add('command') + ->setEnv('foo', 'bar2') + ->addEnvironmentVariables($env) + ->inheritEnvironmentVariables(false) + ->getProcess() + ; + + $this->assertSame($env, $proc->getEnv()); + } + + public function testProcessShouldInheritAndOverrideEnvironmentVars() + { + $_ENV['MY_VAR_1'] = 'foo'; + + $proc = ProcessBuilder::create() + ->setEnv('MY_VAR_1', 'bar') + ->add('foo') + ->getProcess(); + + unset($_ENV['MY_VAR_1']); + + $env = $proc->getEnv(); + $this->assertArrayHasKey('MY_VAR_1', $env); + $this->assertEquals('bar', $env['MY_VAR_1']); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException + */ + public function testNegativeTimeoutFromSetter() + { + $pb = new ProcessBuilder(); + $pb->setTimeout(-1); + } + + public function testNullTimeout() + { + $pb = new ProcessBuilder(); + $pb->setTimeout(10); + $pb->setTimeout(null); + + $r = new \ReflectionObject($pb); + $p = $r->getProperty('timeout'); + $p->setAccessible(true); + + $this->assertNull($p->getValue($pb)); + } + + public function testShouldSetArguments() + { + $pb = new ProcessBuilder(array('initial')); + $pb->setArguments(array('second')); + + $proc = $pb->getProcess(); + + $this->assertContains('second', $proc->getCommandLine()); + } + + public function testPrefixIsPrependedToAllGeneratedProcess() + { + $pb = new ProcessBuilder(); + $pb->setPrefix('/usr/bin/php'); + + $proc = $pb->setArguments(array('-v'))->getProcess(); + if ('\\' === DIRECTORY_SEPARATOR) { + $this->assertEquals('"/usr/bin/php" "-v"', $proc->getCommandLine()); + } else { + $this->assertEquals("'/usr/bin/php' '-v'", $proc->getCommandLine()); + } + + $proc = $pb->setArguments(array('-i'))->getProcess(); + if ('\\' === DIRECTORY_SEPARATOR) { + $this->assertEquals('"/usr/bin/php" "-i"', $proc->getCommandLine()); + } else { + $this->assertEquals("'/usr/bin/php' '-i'", $proc->getCommandLine()); + } + } + + public function testArrayPrefixesArePrependedToAllGeneratedProcess() + { + $pb = new ProcessBuilder(); + $pb->setPrefix(array('/usr/bin/php', 'composer.phar')); + + $proc = $pb->setArguments(array('-v'))->getProcess(); + if ('\\' === DIRECTORY_SEPARATOR) { + $this->assertEquals('"/usr/bin/php" "composer.phar" "-v"', $proc->getCommandLine()); + } else { + $this->assertEquals("'/usr/bin/php' 'composer.phar' '-v'", $proc->getCommandLine()); + } + + $proc = $pb->setArguments(array('-i'))->getProcess(); + if ('\\' === DIRECTORY_SEPARATOR) { + $this->assertEquals('"/usr/bin/php" "composer.phar" "-i"', $proc->getCommandLine()); + } else { + $this->assertEquals("'/usr/bin/php' 'composer.phar' '-i'", $proc->getCommandLine()); + } + } + + public function testShouldEscapeArguments() + { + $pb = new ProcessBuilder(array('%path%', 'foo " bar', '%baz%baz')); + $proc = $pb->getProcess(); + + if ('\\' === DIRECTORY_SEPARATOR) { + $this->assertSame('^%"path"^% "foo \\" bar" "%baz%baz"', $proc->getCommandLine()); + } else { + $this->assertSame("'%path%' 'foo \" bar' '%baz%baz'", $proc->getCommandLine()); + } + } + + public function testShouldEscapeArgumentsAndPrefix() + { + $pb = new ProcessBuilder(array('arg')); + $pb->setPrefix('%prefix%'); + $proc = $pb->getProcess(); + + if ('\\' === DIRECTORY_SEPARATOR) { + $this->assertSame('^%"prefix"^% "arg"', $proc->getCommandLine()); + } else { + $this->assertSame("'%prefix%' 'arg'", $proc->getCommandLine()); + } + } + + /** + * @expectedException \Symfony\Component\Process\Exception\LogicException + */ + public function testShouldThrowALogicExceptionIfNoPrefixAndNoArgument() + { + ProcessBuilder::create()->getProcess(); + } + + public function testShouldNotThrowALogicExceptionIfNoArgument() + { + $process = ProcessBuilder::create() + ->setPrefix('/usr/bin/php') + ->getProcess(); + + if ('\\' === DIRECTORY_SEPARATOR) { + $this->assertEquals('"/usr/bin/php"', $process->getCommandLine()); + } else { + $this->assertEquals("'/usr/bin/php'", $process->getCommandLine()); + } + } + + public function testShouldNotThrowALogicExceptionIfNoPrefix() + { + $process = ProcessBuilder::create(array('/usr/bin/php')) + ->getProcess(); + + if ('\\' === DIRECTORY_SEPARATOR) { + $this->assertEquals('"/usr/bin/php"', $process->getCommandLine()); + } else { + $this->assertEquals("'/usr/bin/php'", $process->getCommandLine()); + } + } + + public function testShouldReturnProcessWithDisabledOutput() + { + $process = ProcessBuilder::create(array('/usr/bin/php')) + ->disableOutput() + ->getProcess(); + + $this->assertTrue($process->isOutputDisabled()); + } + + public function testShouldReturnProcessWithEnabledOutput() + { + $process = ProcessBuilder::create(array('/usr/bin/php')) + ->disableOutput() + ->enableOutput() + ->getProcess(); + + $this->assertFalse($process->isOutputDisabled()); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\InvalidArgumentException + * @expectedExceptionMessage Symfony\Component\Process\ProcessBuilder::setInput only accepts strings or stream resources. + */ + public function testInvalidInput() + { + $builder = ProcessBuilder::create(); + $builder->setInput(array()); + } +} diff --git a/library/symfony/process/Tests/ProcessFailedExceptionTest.php b/library/symfony/process/Tests/ProcessFailedExceptionTest.php new file mode 100644 index 000000000..0d763a470 --- /dev/null +++ b/library/symfony/process/Tests/ProcessFailedExceptionTest.php @@ -0,0 +1,146 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use Symfony\Component\Process\Exception\ProcessFailedException; + +/** + * @author Sebastian Marek <proofek@gmail.com> + */ +class ProcessFailedExceptionTest extends \PHPUnit_Framework_TestCase +{ + /** + * tests ProcessFailedException throws exception if the process was successful. + */ + public function testProcessFailedExceptionThrowsException() + { + $process = $this->getMock( + 'Symfony\Component\Process\Process', + array('isSuccessful'), + array('php') + ); + $process->expects($this->once()) + ->method('isSuccessful') + ->will($this->returnValue(true)); + + $this->setExpectedException( + '\InvalidArgumentException', + 'Expected a failed process, but the given process was successful.' + ); + + new ProcessFailedException($process); + } + + /** + * tests ProcessFailedException uses information from process output + * to generate exception message. + */ + public function testProcessFailedExceptionPopulatesInformationFromProcessOutput() + { + $cmd = 'php'; + $exitCode = 1; + $exitText = 'General error'; + $output = 'Command output'; + $errorOutput = 'FATAL: Unexpected error'; + $workingDirectory = getcwd(); + + $process = $this->getMock( + 'Symfony\Component\Process\Process', + array('isSuccessful', 'getOutput', 'getErrorOutput', 'getExitCode', 'getExitCodeText', 'isOutputDisabled', 'getWorkingDirectory'), + array($cmd) + ); + $process->expects($this->once()) + ->method('isSuccessful') + ->will($this->returnValue(false)); + + $process->expects($this->once()) + ->method('getOutput') + ->will($this->returnValue($output)); + + $process->expects($this->once()) + ->method('getErrorOutput') + ->will($this->returnValue($errorOutput)); + + $process->expects($this->once()) + ->method('getExitCode') + ->will($this->returnValue($exitCode)); + + $process->expects($this->once()) + ->method('getExitCodeText') + ->will($this->returnValue($exitText)); + + $process->expects($this->once()) + ->method('isOutputDisabled') + ->will($this->returnValue(false)); + + $process->expects($this->once()) + ->method('getWorkingDirectory') + ->will($this->returnValue($workingDirectory)); + + $exception = new ProcessFailedException($process); + + $this->assertEquals( + "The command \"$cmd\" failed.\n\nExit Code: $exitCode($exitText)\n\nWorking directory: {$workingDirectory}\n\nOutput:\n================\n{$output}\n\nError Output:\n================\n{$errorOutput}", + $exception->getMessage() + ); + } + + /** + * Tests that ProcessFailedException does not extract information from + * process output if it was previously disabled. + */ + public function testDisabledOutputInFailedExceptionDoesNotPopulateOutput() + { + $cmd = 'php'; + $exitCode = 1; + $exitText = 'General error'; + $workingDirectory = getcwd(); + + $process = $this->getMock( + 'Symfony\Component\Process\Process', + array('isSuccessful', 'isOutputDisabled', 'getExitCode', 'getExitCodeText', 'getOutput', 'getErrorOutput', 'getWorkingDirectory'), + array($cmd) + ); + $process->expects($this->once()) + ->method('isSuccessful') + ->will($this->returnValue(false)); + + $process->expects($this->never()) + ->method('getOutput'); + + $process->expects($this->never()) + ->method('getErrorOutput'); + + $process->expects($this->once()) + ->method('getExitCode') + ->will($this->returnValue($exitCode)); + + $process->expects($this->once()) + ->method('getExitCodeText') + ->will($this->returnValue($exitText)); + + $process->expects($this->once()) + ->method('isOutputDisabled') + ->will($this->returnValue(true)); + + $process->expects($this->once()) + ->method('getWorkingDirectory') + ->will($this->returnValue($workingDirectory)); + + $exception = new ProcessFailedException($process); + + $this->assertEquals( + "The command \"$cmd\" failed.\n\nExit Code: $exitCode($exitText)\n\nWorking directory: {$workingDirectory}", + $exception->getMessage() + ); + } +} diff --git a/library/symfony/process/Tests/ProcessInSigchildEnvironment.php b/library/symfony/process/Tests/ProcessInSigchildEnvironment.php new file mode 100644 index 000000000..3977bcdcf --- /dev/null +++ b/library/symfony/process/Tests/ProcessInSigchildEnvironment.php @@ -0,0 +1,22 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use Symfony\Component\Process\Process; + +class ProcessInSigchildEnvironment extends Process +{ + protected function isSigchildEnabled() + { + return true; + } +} diff --git a/library/symfony/process/Tests/ProcessUtilsTest.php b/library/symfony/process/Tests/ProcessUtilsTest.php new file mode 100644 index 000000000..e6564cde5 --- /dev/null +++ b/library/symfony/process/Tests/ProcessUtilsTest.php @@ -0,0 +1,48 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use Symfony\Component\Process\ProcessUtils; + +class ProcessUtilsTest extends \PHPUnit_Framework_TestCase +{ + /** + * @dataProvider dataArguments + */ + public function testEscapeArgument($result, $argument) + { + $this->assertSame($result, ProcessUtils::escapeArgument($argument)); + } + + public function dataArguments() + { + if ('\\' === DIRECTORY_SEPARATOR) { + return array( + array('"\"php\" \"-v\""', '"php" "-v"'), + array('"foo bar"', 'foo bar'), + array('^%"path"^%', '%path%'), + array('"<|>\\" \\"\'f"', '<|>" "\'f'), + array('""', ''), + array('"with\trailingbs\\\\"', 'with\trailingbs\\'), + ); + } + + return array( + array("'\"php\" \"-v\"'", '"php" "-v"'), + array("'foo bar'", 'foo bar'), + array("'%path%'", '%path%'), + array("'<|>\" \"'\\''f'", '<|>" "\'f'), + array("''", ''), + array("'with\\trailingbs\\'", 'with\trailingbs\\'), + ); + } +} diff --git a/library/symfony/process/Tests/SigchildDisabledProcessTest.php b/library/symfony/process/Tests/SigchildDisabledProcessTest.php new file mode 100644 index 000000000..fdae5ec25 --- /dev/null +++ b/library/symfony/process/Tests/SigchildDisabledProcessTest.php @@ -0,0 +1,263 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +class SigchildDisabledProcessTest extends AbstractProcessTest +{ + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testGetExitCode() + { + parent::testGetExitCode(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testGetExitCodeIsNullOnStart() + { + parent::testGetExitCodeIsNullOnStart(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testGetExitCodeIsNullOnWhenStartingAgain() + { + parent::testGetExitCodeIsNullOnWhenStartingAgain(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testExitCodeCommandFailed() + { + parent::testExitCodeCommandFailed(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testMustRun() + { + parent::testMustRun(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testSuccessfulMustRunHasCorrectExitCode() + { + parent::testSuccessfulMustRunHasCorrectExitCode(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + */ + public function testMustRunThrowsException() + { + parent::testMustRunThrowsException(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + */ + public function testProcessIsSignaledIfStopped() + { + parent::testProcessIsSignaledIfStopped(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved. + */ + public function testProcessWithTermSignal() + { + parent::testProcessWithTermSignal(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved. + */ + public function testProcessIsNotSignaled() + { + parent::testProcessIsNotSignaled(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved. + */ + public function testProcessWithoutTermSignal() + { + parent::testProcessWithoutTermSignal(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testCheckTimeoutOnStartedProcess() + { + parent::testCheckTimeoutOnStartedProcess(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved. + */ + public function testGetPid() + { + parent::testGetPid(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved. + */ + public function testGetPidIsNullBeforeStart() + { + parent::testGetPidIsNullBeforeStart(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved. + */ + public function testGetPidIsNullAfterRun() + { + parent::testGetPidIsNullAfterRun(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testExitCodeText() + { + $process = $this->getProcess('qdfsmfkqsdfmqmsd'); + $process->run(); + + $process->getExitCodeText(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testExitCodeTextIsNullWhenExitCodeIsNull() + { + parent::testExitCodeTextIsNullWhenExitCodeIsNull(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testIsSuccessful() + { + parent::testIsSuccessful(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testIsSuccessfulOnlyAfterTerminated() + { + parent::testIsSuccessfulOnlyAfterTerminated(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testIsNotSuccessful() + { + parent::testIsNotSuccessful(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method. + */ + public function testTTYCommandExitCode() + { + parent::testTTYCommandExitCode(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. The process can not be signaled. + */ + public function testSignal() + { + parent::testSignal(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved. + */ + public function testProcessWithoutTermSignalIsNotSignaled() + { + parent::testProcessWithoutTermSignalIsNotSignaled(); + } + + public function testStopWithTimeoutIsActuallyWorking() + { + $this->markTestSkipped('Stopping with signal is not supported in sigchild environment'); + } + + public function testProcessThrowsExceptionWhenExternallySignaled() + { + $this->markTestSkipped('Retrieving Pid is not supported in sigchild environment'); + } + + public function testExitCodeIsAvailableAfterSignal() + { + $this->markTestSkipped('Signal is not supported in sigchild environment'); + } + + public function testRunProcessWithTimeout() + { + $this->markTestSkipped('Signal (required for timeout) is not supported in sigchild environment'); + } + + public function provideStartMethods() + { + return array( + array('start', 'Symfony\Component\Process\Exception\LogicException', 'Output has been disabled, enable it to allow the use of a callback.'), + array('run', 'Symfony\Component\Process\Exception\LogicException', 'Output has been disabled, enable it to allow the use of a callback.'), + array('mustRun', 'Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. You must use setEnhanceSigchildCompatibility() to use this method.'), + ); + } + + /** + * {@inheritdoc} + */ + protected function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array()) + { + $process = new ProcessInSigchildEnvironment($commandline, $cwd, $env, $input, $timeout, $options); + $process->setEnhanceSigchildCompatibility(false); + + return $process; + } +} diff --git a/library/symfony/process/Tests/SigchildEnabledProcessTest.php b/library/symfony/process/Tests/SigchildEnabledProcessTest.php new file mode 100644 index 000000000..2668a9b4b --- /dev/null +++ b/library/symfony/process/Tests/SigchildEnabledProcessTest.php @@ -0,0 +1,148 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +class SigchildEnabledProcessTest extends AbstractProcessTest +{ + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved. + */ + public function testProcessIsSignaledIfStopped() + { + parent::testProcessIsSignaledIfStopped(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved. + */ + public function testProcessWithTermSignal() + { + parent::testProcessWithTermSignal(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved. + */ + public function testProcessIsNotSignaled() + { + parent::testProcessIsNotSignaled(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved. + */ + public function testProcessWithoutTermSignal() + { + parent::testProcessWithoutTermSignal(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved. + */ + public function testGetPid() + { + parent::testGetPid(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved. + */ + public function testGetPidIsNullBeforeStart() + { + parent::testGetPidIsNullBeforeStart(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. The process identifier can not be retrieved. + */ + public function testGetPidIsNullAfterRun() + { + parent::testGetPidIsNullAfterRun(); + } + + public function testExitCodeText() + { + $process = $this->getProcess('qdfsmfkqsdfmqmsd'); + $process->run(); + + $this->assertInternalType('string', $process->getExitCodeText()); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. The process can not be signaled. + */ + public function testSignal() + { + parent::testSignal(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\RuntimeException + * @expectedExceptionMessage This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved. + */ + public function testProcessWithoutTermSignalIsNotSignaled() + { + parent::testProcessWithoutTermSignalIsNotSignaled(); + } + + public function testProcessThrowsExceptionWhenExternallySignaled() + { + $this->markTestSkipped('Retrieving Pid is not supported in sigchild environment'); + } + + public function testExitCodeIsAvailableAfterSignal() + { + $this->markTestSkipped('Signal is not supported in sigchild environment'); + } + + public function testStartAfterATimeout() + { + if ('\\' === DIRECTORY_SEPARATOR) { + $this->markTestSkipped('Restarting a timed-out process on Windows is not supported in sigchild environment'); + } + parent::testStartAfterATimeout(); + } + + public function testStopWithTimeoutIsActuallyWorking() + { + $this->markTestSkipped('Stopping with signal is not supported in sigchild environment'); + } + + public function testRunProcessWithTimeout() + { + $this->markTestSkipped('Signal (required for timeout) is not supported in sigchild environment'); + } + + public function testCheckTimeoutOnStartedProcess() + { + $this->markTestSkipped('Signal (required for timeout) is not supported in sigchild environment'); + } + + /** + * {@inheritdoc} + */ + protected function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array()) + { + $process = new ProcessInSigchildEnvironment($commandline, $cwd, $env, $input, $timeout, $options); + $process->setEnhanceSigchildCompatibility(true); + + return $process; + } +} diff --git a/library/symfony/process/Tests/SignalListener.php b/library/symfony/process/Tests/SignalListener.php new file mode 100644 index 000000000..4206550f5 --- /dev/null +++ b/library/symfony/process/Tests/SignalListener.php @@ -0,0 +1,25 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +// required for signal handling +declare (ticks = 1); + +pcntl_signal(SIGUSR1, function () {echo 'Caught SIGUSR1'; exit;}); + +$n = 0; + +// ticks require activity to work - sleep(4); does not work +while ($n < 400) { + usleep(10000); + ++$n; +} + +return; diff --git a/library/symfony/process/Tests/SimpleProcessTest.php b/library/symfony/process/Tests/SimpleProcessTest.php new file mode 100644 index 000000000..78f20eb10 --- /dev/null +++ b/library/symfony/process/Tests/SimpleProcessTest.php @@ -0,0 +1,216 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Tests; + +use Symfony\Component\Process\Process; + +class SimpleProcessTest extends AbstractProcessTest +{ + private $enabledSigchild = false; + + protected function setUp() + { + ob_start(); + phpinfo(INFO_GENERAL); + + $this->enabledSigchild = false !== strpos(ob_get_clean(), '--enable-sigchild'); + } + + public function testGetExitCode() + { + $this->skipIfPHPSigchild(); // This test use exitcode that is not available in this case + parent::testGetExitCode(); + } + + public function testExitCodeCommandFailed() + { + $this->skipIfPHPSigchild(); // This test use exitcode that is not available in this case + parent::testExitCodeCommandFailed(); + } + + public function testProcessIsSignaledIfStopped() + { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved'); + parent::testProcessIsSignaledIfStopped(); + } + + public function testProcessWithTermSignal() + { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved'); + parent::testProcessWithTermSignal(); + } + + public function testProcessIsNotSignaled() + { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved'); + parent::testProcessIsNotSignaled(); + } + + public function testProcessWithoutTermSignal() + { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved'); + parent::testProcessWithoutTermSignal(); + } + + public function testExitCodeText() + { + $this->skipIfPHPSigchild(); // This test use exitcode that is not available in this case + parent::testExitCodeText(); + } + + public function testIsSuccessful() + { + $this->skipIfPHPSigchild(); // This test use PID that is not available in this case + parent::testIsSuccessful(); + } + + public function testIsNotSuccessful() + { + $this->skipIfPHPSigchild(); // This test use PID that is not available in this case + parent::testIsNotSuccessful(); + } + + public function testGetPid() + { + $this->skipIfPHPSigchild(); // This test use PID that is not available in this case + parent::testGetPid(); + } + + public function testGetPidIsNullBeforeStart() + { + $this->skipIfPHPSigchild(); // This test use PID that is not available in this case + parent::testGetPidIsNullBeforeStart(); + } + + public function testGetPidIsNullAfterRun() + { + $this->skipIfPHPSigchild(); // This test use PID that is not available in this case + parent::testGetPidIsNullAfterRun(); + } + + public function testSignal() + { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + parent::testSignal(); + } + + public function testProcessWithoutTermSignalIsNotSignaled() + { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. Term signal can not be retrieved'); + parent::testProcessWithoutTermSignalIsNotSignaled(); + } + + public function testProcessThrowsExceptionWhenExternallySignaled() + { + $this->skipIfPHPSigchild(); // This test use PID that is not available in this case + parent::testProcessThrowsExceptionWhenExternallySignaled(); + } + + public function testExitCodeIsAvailableAfterSignal() + { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + parent::testExitCodeIsAvailableAfterSignal(); + } + + /** + * @expectedException \Symfony\Component\Process\Exception\LogicException + * @expectedExceptionMessage Can not send signal on a non running process. + */ + public function testSignalProcessNotRunning() + { + parent::testSignalProcessNotRunning(); + } + + public function testSignalWithWrongIntSignal() + { + if ($this->enabledSigchild) { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + } else { + $this->setExpectedException('Symfony\Component\Process\Exception\RuntimeException', 'Error while sending signal `-4`.'); + } + parent::testSignalWithWrongIntSignal(); + } + + public function testSignalWithWrongNonIntSignal() + { + if ($this->enabledSigchild) { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + } else { + $this->setExpectedException('Symfony\Component\Process\Exception\RuntimeException', 'Error while sending signal `Céphalopodes`.'); + } + parent::testSignalWithWrongNonIntSignal(); + } + + public function testStopTerminatesProcessCleanly() + { + $process = $this->getProcess(self::$phpBin.' -r "echo \'foo\'; sleep(1); echo \'bar\';"'); + $process->run(function () use ($process) { + $process->stop(); + }); + $this->assertTrue(true, 'A call to stop() is not expected to cause wait() to throw a RuntimeException'); + } + + public function testKillSignalTerminatesProcessCleanly() + { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + + $process = $this->getProcess(self::$phpBin.' -r "echo \'foo\'; sleep(1); echo \'bar\';"'); + $process->run(function () use ($process) { + if ($process->isRunning()) { + $process->signal(defined('SIGKILL') ? SIGKILL : 9); + } + }); + $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException'); + } + + public function testTermSignalTerminatesProcessCleanly() + { + $this->expectExceptionIfPHPSigchild('Symfony\Component\Process\Exception\RuntimeException', 'This PHP has been compiled with --enable-sigchild. The process can not be signaled.'); + + $process = $this->getProcess(self::$phpBin.' -r "echo \'foo\'; sleep(1); echo \'bar\';"'); + $process->run(function () use ($process) { + if ($process->isRunning()) { + $process->signal(defined('SIGTERM') ? SIGTERM : 15); + } + }); + $this->assertTrue(true, 'A call to signal() is not expected to cause wait() to throw a RuntimeException'); + } + + public function testStopWithTimeoutIsActuallyWorking() + { + $this->skipIfPHPSigchild(); + + parent::testStopWithTimeoutIsActuallyWorking(); + } + + /** + * {@inheritdoc} + */ + protected function getProcess($commandline, $cwd = null, array $env = null, $input = null, $timeout = 60, array $options = array()) + { + return new Process($commandline, $cwd, $env, $input, $timeout, $options); + } + + private function skipIfPHPSigchild() + { + if ($this->enabledSigchild) { + $this->markTestSkipped('Your PHP has been compiled with --enable-sigchild, this test can not be executed'); + } + } + + private function expectExceptionIfPHPSigchild($classname, $message) + { + if ($this->enabledSigchild) { + $this->setExpectedException($classname, $message); + } + } +} diff --git a/library/symfony/process/phpunit.xml.dist b/library/symfony/process/phpunit.xml.dist new file mode 100644 index 000000000..788500084 --- /dev/null +++ b/library/symfony/process/phpunit.xml.dist @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd" + backupGlobals="false" + colors="true" + bootstrap="vendor/autoload.php" +> + <php> + <ini name="error_reporting" value="-1" /> + </php> + + <testsuites> + <testsuite name="Symfony Process Component Test Suite"> + <directory>./Tests/</directory> + </testsuite> + </testsuites> + + <filter> + <whitelist> + <directory>./</directory> + <exclude> + <directory>./Tests</directory> + <directory>./vendor</directory> + </exclude> + </whitelist> + </filter> +</phpunit> |