├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src └── Alchemy │ └── BinaryDriver │ ├── AbstractBinary.php │ ├── BinaryDriverTestCase.php │ ├── BinaryInterface.php │ ├── Configuration.php │ ├── ConfigurationAwareInterface.php │ ├── ConfigurationInterface.php │ ├── Exception │ ├── ExceptionInterface.php │ ├── ExecutableNotFoundException.php │ ├── ExecutionFailureException.php │ └── InvalidArgumentException.php │ ├── Listeners │ ├── DebugListener.php │ ├── ListenerInterface.php │ └── Listeners.php │ ├── ProcessBuilderFactory.php │ ├── ProcessBuilderFactoryAwareInterface.php │ ├── ProcessBuilderFactoryInterface.php │ ├── ProcessRunner.php │ ├── ProcessRunnerAwareInterface.php │ └── ProcessRunnerInterface.php └── tests ├── Alchemy └── Tests │ └── BinaryDriver │ ├── AbstractBinaryTest.php │ ├── AbstractProcessBuilderFactoryTest.php │ ├── ConfigurationTest.php │ ├── Exceptions │ └── ExecutionFailureExceptionTest.php │ ├── LTSProcessBuilder.php │ ├── LTSProcessBuilderFactoryTest.php │ ├── Listeners │ ├── DebugListenerTest.php │ └── ListenersTest.php │ ├── NONLTSProcessBuilderFactoryTest.php │ └── ProcessRunnerTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | dist: trusty 3 | 4 | sudo: false 5 | 6 | php: 7 | - 7.0 8 | - 7.1 9 | - 7.2 10 | - 7.3 11 | 12 | matrix: 13 | include: 14 | - php: 7.0 15 | env: COMPOSER_REQUIRE="symfony/process:^2.0" 16 | - php: 7.0 17 | env: COMPOSER_REQUIRE="symfony/process:^3.0" 18 | - php: 7.2 19 | env: COMPOSER_REQUIRE="symfony/process:^5.0" 20 | - php: 7.3 21 | env: COMPOSER_REQUIRE="symfony/process:^5.0" 22 | 23 | before_script: 24 | - composer self-update 25 | - if [ -n "$COMPOSER_REQUIRE" ]; then composer require --no-update $COMPOSER_REQUIRE; fi 26 | - composer update $COMPOSER_OPTIONS 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | --------- 3 | * 1.6.0 (2015-03-02) 4 | * BC Break: bump minimum PHP versions 5 | * Allow use of evenement v2.0 (thanks @patkar for the P/R) 6 | 7 | * 1.5.0 (2013-06-21) 8 | 9 | * BC Break : ConfigurationInterface::get does not throw exceptions anymore 10 | in case the key does not exist. Second argument is a default value to return 11 | in case the key does not exist. 12 | 13 | * 1.4.1 (2013-05-23) 14 | 15 | * Add third parameter to BinaryInterface::command method to pass a listener or 16 | an array of listener that will be registered just the time of the command. 17 | 18 | * 1.4.0 (2013-05-11) 19 | 20 | * Extract process run management to ProcessRunner. 21 | * Add support for process listeners. 22 | * Provides bundled DebugListener. 23 | * Add BinaryInterface::command method. 24 | * BC break : ProcessRunnerInterface::run now takes an SplObjectStorage containing 25 | listeners as second argument. 26 | * BC break : BinaryInterface no longer implements LoggerAwareInterface 27 | as it is now supported by ProcessRunner. 28 | 29 | * 1.3.4 (2013-04-26) 30 | 31 | * Add BinaryDriver::run method. 32 | 33 | * 1.3.3 (2013-04-26) 34 | 35 | * Add BinaryDriver::createProcessMock method. 36 | 37 | * 1.3.2 (2013-04-26) 38 | 39 | * Add BinaryDriverTestCase for testing BinaryDriver implementations. 40 | 41 | * 1.3.1 (2013-04-24) 42 | 43 | * Add timeouts handling 44 | 45 | * 1.3.0 (2013-04-24) 46 | 47 | * Add BinaryInterface and AbstractBinary 48 | 49 | * 1.2.1 (2013-04-24) 50 | 51 | * Add ConfigurationAwareInterface 52 | * Add ProcessBuilderAwareInterface 53 | 54 | * 1.2.0 (2013-04-24) 55 | 56 | * Add BinaryDriver\Configuration 57 | 58 | * 1.1.0 (2013-04-24) 59 | 60 | * Add support for timeouts via `setTimeout` method 61 | 62 | * 1.0.0 (2013-04-23) 63 | 64 | * First stable version. 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BinaryDriver is released with MIT License : 2 | 3 | Copyright (c) 2013 Alchemy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Binary Driver 2 | 3 | Binary-Driver is a set of PHP tools to build binary drivers. 4 | 5 | [![Build Status](https://travis-ci.org/alchemy-fr/BinaryDriver.png?branch=master)](https://travis-ci.org/alchemy-fr/BinaryDriver) 6 | 7 | ## Why ? 8 | 9 | You may wonder *Why building a library while I can use `exec` or 10 | [symfony/process](https://github.com/symfony/Process) ?*. 11 | 12 | Here is a simple answer : 13 | 14 | - If you use `exec`, `passthru`, `system`, `proc_open` or any low level process 15 | handling in PHP, you should have a look to [symfony/process](https://github.com/symfony/Process) 16 | component that will provide an OO portable, testable and secure interface to 17 | deal with this. It seems easy at first approach, but if you look at this 18 | component [unit tests](https://github.com/symfony/Process/tree/master/Tests), 19 | you will see that handling process in a simple interface can easily become a 20 | nightmare. 21 | 22 | - If you already use symfony/process, and want to build binary drivers, you 23 | will always have the same common set of methods and objects to configure, log, 24 | debug, and generate processes. 25 | This library is a base to implement any binary driver with this common set of 26 | needs. 27 | 28 | ## AbstractBinary 29 | 30 | `AbstractBinary` provides an abstract class to build a binary driver. It implements 31 | `BinaryInterface`. 32 | 33 | Implementation example : 34 | 35 | ```php 36 | use Alchemy\BinaryDriver\AbstractBinary; 37 | 38 | class LsDriver extends AbstractBinary 39 | { 40 | public function getName() 41 | { 42 | return 'ls driver'; 43 | } 44 | } 45 | 46 | $parser = new LsParser(); 47 | 48 | $driver = Driver::load('ls'); 49 | // will return the output of `ls -a -l` 50 | $parser->parse($driver->command(array('-a', '-l'))); 51 | ``` 52 | 53 | ### Binary detection troubleshooting 54 | 55 | If you are using Nginx with PHP-fpm, executable detection may not work because of an empty `$_ENV['path']`. 56 | To avoid having an empty `PATH` environment variable, add the following line to your `fastcgi_params` 57 | config file (replace `/your/current/path/` with the output of `printenv PATH`) : 58 | 59 | ``` 60 | fastcgi_param PATH /your/current/path 61 | ``` 62 | 63 | ## Logging 64 | 65 | You can log events with a `Psr\Log\LoggerInterface` by passing it in the load 66 | method as second argument : 67 | 68 | ```php 69 | $logger = new Monolog\Logger('driver'); 70 | $driver = Driver::load('ls', $logger); 71 | ``` 72 | 73 | ## Listeners 74 | 75 | You can add custom listeners on processes. 76 | Listeners are built on top of [Evenement](https://github.com/igorw/evenement) 77 | and must implement `Alchemy\BinaryDriver\ListenerInterface`. 78 | 79 | ```php 80 | use Symfony\Component\Process\Process; 81 | 82 | class DebugListener extends EventEmitter implements ListenerInterface 83 | { 84 | public function handle($type, $data) 85 | { 86 | foreach (explode(PHP_EOL, $data) as $line) { 87 | $this->emit($type === Process::ERR ? 'error' : 'out', array($line)); 88 | } 89 | } 90 | 91 | public function forwardedEvents() 92 | { 93 | // forward 'error' events to the BinaryInterface 94 | return array('error'); 95 | } 96 | } 97 | 98 | $listener = new DebugListener(); 99 | 100 | $driver = CustomImplementation::load('php'); 101 | 102 | // adds listener 103 | $driver->listen($listener); 104 | 105 | $driver->on('error', function ($line) { 106 | echo '[ERROR] ' . $line . PHP_EOL; 107 | }); 108 | 109 | // removes listener 110 | $driver->unlisten($listener); 111 | ``` 112 | 113 | ### Bundled listeners 114 | 115 | The debug listener is a simple listener to catch `stderr` and `stdout` outputs ; 116 | read the implementation for customization. 117 | 118 | ```php 119 | use Alchemy\BinaryDriver\Listeners\DebugListener; 120 | 121 | $driver = CustomImplementation::load('php'); 122 | $driver->listen(new DebugListener()); 123 | 124 | $driver->on('debug', function ($line) { 125 | echo $line; 126 | }); 127 | ``` 128 | 129 | ## ProcessBuilderFactory 130 | 131 | ProcessBuilderFactory ease spawning processes by generating Symfony [Process] 132 | (http://symfony.com/doc/master/components/process.html) objects. 133 | 134 | ```php 135 | use Alchemy\BinaryDriver\ProcessBuilderFactory; 136 | 137 | $factory = new ProcessBuilderFactory('/usr/bin/php'); 138 | 139 | // return a Symfony\Component\Process\Process 140 | $process = $factory->create('-v'); 141 | 142 | // echoes '/usr/bin/php' '-v' 143 | echo $process->getCommandLine(); 144 | 145 | $process = $factory->create(array('-r', 'echo "Hello !";')); 146 | 147 | // echoes '/usr/bin/php' '-r' 'echo "Hello !";' 148 | echo $process->getCommandLine(); 149 | ``` 150 | 151 | ## Configuration 152 | 153 | A simple configuration object, providing an `ArrayAccess` and `IteratorAggregate` 154 | interface. 155 | 156 | ```php 157 | use Alchemy\BinaryDriver\Configuration; 158 | 159 | $conf = new Configuration(array('timeout' => 0)); 160 | 161 | echo $conf->get('timeout'); 162 | 163 | if ($conf->has('param')) { 164 | $conf->remove('param'); 165 | } 166 | 167 | $conf->set('timeout', 20); 168 | 169 | $conf->all(); 170 | ``` 171 | 172 | Same example using the `ArrayAccess` interface : 173 | 174 | ```php 175 | use Alchemy\BinaryDriver\Configuration; 176 | 177 | $conf = new Configuration(array('timeout' => 0)); 178 | 179 | echo $conf['timeout']; 180 | 181 | if (isset($conf['param'])) { 182 | unset($conf['param']); 183 | } 184 | 185 | $conf['timeout'] = 20; 186 | ``` 187 | 188 | ## License 189 | 190 | This project is released under the MIT license. 191 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alchemy/binary-driver", 3 | "type": "library", 4 | "description": "A set of tools to build binary drivers", 5 | "keywords": ["binary", "driver"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Nicolas Le Goff", 10 | "email": "legoff.n@gmail.com" 11 | }, 12 | { 13 | "name": "Romain Neutron", 14 | "email": "imprec@gmail.com", 15 | "homepage": "http://www.lickmychip.com/" 16 | }, 17 | { 18 | "name": "Phraseanet Team", 19 | "email": "info@alchemy.fr", 20 | "homepage": "http://www.phraseanet.com/" 21 | }, 22 | { 23 | "name": "Jens Hausdorf", 24 | "email": "mail@jens-hausdorf.de", 25 | "homepage": "https://jens-hausdorf.de", 26 | "role": "Maintainer" 27 | } 28 | ], 29 | "require": { 30 | "php" : ">=5.5", 31 | "evenement/evenement" : "^3.0|^2.0|^1.0", 32 | "psr/log" : "^1.0", 33 | "symfony/process" : "^2.3|^3.0|^4.0|^5.0" 34 | }, 35 | "require-dev": { 36 | "phpunit/phpunit" : "^4.0|^5.0" 37 | }, 38 | "autoload": { 39 | "psr-0": { 40 | "Alchemy": "src" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | tests 21 | 22 | 23 | 24 | 25 | vendor 26 | tests 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Alchemy/BinaryDriver/AbstractBinary.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Alchemy\BinaryDriver; 13 | 14 | use Alchemy\BinaryDriver\Exception\ExecutableNotFoundException; 15 | use Alchemy\BinaryDriver\Exception\ExecutionFailureException; 16 | use Alchemy\BinaryDriver\Listeners\Listeners; 17 | use Alchemy\BinaryDriver\Listeners\ListenerInterface; 18 | use Evenement\EventEmitter; 19 | use Psr\Log\LoggerInterface; 20 | use Psr\Log\NullLogger; 21 | use Symfony\Component\Process\ExecutableFinder; 22 | use Symfony\Component\Process\Process; 23 | 24 | abstract class AbstractBinary extends EventEmitter implements BinaryInterface 25 | { 26 | /** @var ConfigurationInterface */ 27 | protected $configuration; 28 | 29 | /** @var ProcessBuilderFactoryInterface */ 30 | protected $factory; 31 | 32 | /** @var ProcessRunner */ 33 | private $processRunner; 34 | 35 | /** @var Listeners */ 36 | private $listenersManager; 37 | 38 | public function __construct(ProcessBuilderFactoryInterface $factory, LoggerInterface $logger, ConfigurationInterface $configuration) 39 | { 40 | $this->factory = $factory; 41 | $this->configuration = $configuration; 42 | $this->processRunner = new ProcessRunner($logger, $this->getName()); 43 | $this->listenersManager = new Listeners(); 44 | $this->applyProcessConfiguration(); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function listen(ListenerInterface $listener) 51 | { 52 | $this->listenersManager->register($listener, $this); 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function unlisten(ListenerInterface $listener) 61 | { 62 | $this->listenersManager->unregister($listener, $this); 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function getConfiguration() 71 | { 72 | return $this->configuration; 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | * 78 | * @return BinaryInterface 79 | */ 80 | public function setConfiguration(ConfigurationInterface $configuration) 81 | { 82 | $this->configuration = $configuration; 83 | $this->applyProcessConfiguration(); 84 | 85 | return $this; 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function getProcessBuilderFactory() 92 | { 93 | return $this->factory; 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | * 99 | * @return BinaryInterface 100 | */ 101 | public function setProcessBuilderFactory(ProcessBuilderFactoryInterface $factory) 102 | { 103 | $this->factory = $factory; 104 | $this->applyProcessConfiguration(); 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * {@inheritdoc} 111 | */ 112 | public function getProcessRunner() 113 | { 114 | return $this->processRunner; 115 | } 116 | 117 | /** 118 | * {@inheritdoc} 119 | */ 120 | public function setProcessRunner(ProcessRunnerInterface $runner) 121 | { 122 | $this->processRunner = $runner; 123 | 124 | return $this; 125 | } 126 | 127 | /** 128 | * {@inheritdoc} 129 | */ 130 | public function command($command, $bypassErrors = false, $listeners = null) 131 | { 132 | if (!is_array($command)) { 133 | $command = array($command); 134 | } 135 | 136 | return $this->run($this->factory->create($command), $bypassErrors, $listeners); 137 | } 138 | 139 | /** 140 | * {@inheritdoc} 141 | */ 142 | public static function load($binaries, LoggerInterface $logger = null, $configuration = array()) 143 | { 144 | $finder = new ExecutableFinder(); 145 | $binary = null; 146 | $binaries = is_array($binaries) ? $binaries : array($binaries); 147 | 148 | foreach ($binaries as $candidate) { 149 | if (file_exists($candidate) && is_executable($candidate)) { 150 | $binary = $candidate; 151 | break; 152 | } 153 | if (null !== $binary = $finder->find($candidate)) { 154 | break; 155 | } 156 | } 157 | 158 | if (null === $binary) { 159 | throw new ExecutableNotFoundException(sprintf( 160 | 'Executable not found, proposed : %s', implode(', ', $binaries) 161 | )); 162 | } 163 | 164 | if (null === $logger) { 165 | $logger = new NullLogger(); 166 | } 167 | 168 | $configuration = $configuration instanceof ConfigurationInterface ? $configuration : new Configuration($configuration); 169 | 170 | return new static(new ProcessBuilderFactory($binary), $logger, $configuration); 171 | } 172 | 173 | /** 174 | * Returns the name of the driver 175 | * 176 | * @return string 177 | */ 178 | abstract public function getName(); 179 | 180 | /** 181 | * Executes a process, logs events 182 | * 183 | * @param Process $process 184 | * @param Boolean $bypassErrors Set to true to disable throwing ExecutionFailureExceptions 185 | * @param ListenerInterface|array $listeners A listener or an array of listener to register for this unique run 186 | * 187 | * @return string The Process output 188 | * 189 | * @throws ExecutionFailureException in case of process failure. 190 | */ 191 | protected function run(Process $process, $bypassErrors = false, $listeners = null) 192 | { 193 | if (null !== $listeners) { 194 | if (!is_array($listeners)) { 195 | $listeners = array($listeners); 196 | } 197 | 198 | $listenersManager = clone $this->listenersManager; 199 | 200 | foreach ($listeners as $listener) { 201 | $listenersManager->register($listener, $this); 202 | } 203 | } else { 204 | $listenersManager = $this->listenersManager; 205 | } 206 | 207 | return $this->processRunner->run($process, $listenersManager->storage, $bypassErrors); 208 | } 209 | 210 | private function applyProcessConfiguration() 211 | { 212 | if ($this->configuration->has('timeout')) { 213 | $this->factory->setTimeout($this->configuration->get('timeout')); 214 | } 215 | 216 | return $this; 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/Alchemy/BinaryDriver/BinaryDriverTestCase.php: -------------------------------------------------------------------------------- 1 | getMock('Alchemy\BinaryDriver\ProcessBuilderFactoryInterface'); 19 | } 20 | 21 | /** 22 | * @param integer $runs The number of runs expected 23 | * @param Boolean $success True if the process expects to be successfull 24 | * @param string $commandLine The commandline executed 25 | * @param string $output The process output 26 | * @param string $error The process error output 27 | * 28 | * @return Process 29 | */ 30 | public function createProcessMock($runs = 1, $success = true, $commandLine = null, $output = null, $error = null, $callback = false) 31 | { 32 | $process = $this->getMockBuilder('Symfony\Component\Process\Process') 33 | ->disableOriginalConstructor() 34 | ->getMock(); 35 | 36 | $builder = $process->expects($this->exactly($runs)) 37 | ->method('run'); 38 | 39 | if (true === $callback) { 40 | $builder->with($this->isInstanceOf('Closure')); 41 | } 42 | 43 | $process->expects($this->any()) 44 | ->method('isSuccessful') 45 | ->will($this->returnValue($success)); 46 | 47 | foreach (array( 48 | 'getOutput' => $output, 49 | 'getErrorOutput' => $error, 50 | 'getCommandLine' => $commandLine, 51 | ) as $command => $value) { 52 | $process 53 | ->expects($this->any()) 54 | ->method($command) 55 | ->will($this->returnValue($value)); 56 | } 57 | 58 | return $process; 59 | } 60 | 61 | /** 62 | * @return LoggerInterface 63 | */ 64 | public function createLoggerMock() 65 | { 66 | return $this->getMock('Psr\Log\LoggerInterface'); 67 | } 68 | 69 | /** 70 | * @return ConfigurationInterface 71 | */ 72 | public function createConfigurationMock() 73 | { 74 | return $this->getMock('Alchemy\BinaryDriver\ConfigurationInterface'); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Alchemy/BinaryDriver/BinaryInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Alchemy\BinaryDriver; 13 | 14 | use Alchemy\BinaryDriver\Exception\ExecutableNotFoundException; 15 | use Alchemy\BinaryDriver\Exception\ExecutionFailureException; 16 | use Alchemy\BinaryDriver\Listeners\ListenerInterface; 17 | use Psr\Log\LoggerInterface; 18 | use Evenement\EventEmitterInterface; 19 | 20 | interface BinaryInterface extends ConfigurationAwareInterface, ProcessBuilderFactoryAwareInterface, ProcessRunnerAwareInterface, EventEmitterInterface 21 | { 22 | /** 23 | * Adds a listener to the binary driver 24 | * 25 | * @param ListenerInterface $listener 26 | * 27 | * @return BinaryInterface 28 | */ 29 | public function listen(ListenerInterface $listener); 30 | 31 | /** 32 | * Removes a listener from the binary driver 33 | * 34 | * @param ListenerInterface $listener 35 | * 36 | * @return BinaryInterface 37 | */ 38 | public function unlisten(ListenerInterface $listener); 39 | 40 | /** 41 | * Runs a command against the driver. 42 | * 43 | * Calling this method on a `ls` driver with the command `-a` would run `ls -a`. 44 | * 45 | * @param array|string $command A command or an array of command 46 | * @param Boolean $bypassErrors If set to true, an erronous process will not throw an exception 47 | * @param ListenerInterface|array $listeners A listener or an array of listeners to register for this unique run 48 | * 49 | * @return string The command output 50 | * 51 | * @throws ExecutionFailureException in case of process failure. 52 | */ 53 | public function command($command, $bypassErrors = false, $listeners = null); 54 | 55 | /** 56 | * Loads a binary 57 | * 58 | * @param string|array $binaries A binary name or an array of binary names 59 | * @param null|LoggerInterface $logger A Logger 60 | * @param array|ConfigurationInterface $configuration The configuration 61 | * 62 | * @throws ExecutableNotFoundException In case none of the binaries were found 63 | * 64 | * @return BinaryInterface 65 | */ 66 | public static function load($binaries, LoggerInterface $logger = null, $configuration = array()); 67 | } 68 | -------------------------------------------------------------------------------- /src/Alchemy/BinaryDriver/Configuration.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Alchemy\BinaryDriver; 13 | 14 | class Configuration implements ConfigurationInterface 15 | { 16 | private $data; 17 | 18 | public function __construct(array $data = array()) 19 | { 20 | $this->data = $data; 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function getIterator() 27 | { 28 | return new \ArrayIterator($this->data); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function get($key, $default = null) 35 | { 36 | return isset($this->data[$key]) ? $this->data[$key] : $default; 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function set($key, $value) 43 | { 44 | $this->data[$key] = $value; 45 | 46 | return $this; 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function has($key) 53 | { 54 | return array_key_exists($key, $this->data); 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function remove($key) 61 | { 62 | $value = $this->get($key); 63 | unset($this->data[$key]); 64 | 65 | return $value; 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function all() 72 | { 73 | return $this->data; 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function offsetExists($offset) 80 | { 81 | return $this->has($offset); 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | */ 87 | public function offsetGet($offset) 88 | { 89 | return $this->get($offset); 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | public function offsetSet($offset, $value) 96 | { 97 | $this->set($offset, $value); 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | public function offsetUnset($offset) 104 | { 105 | $this->remove($offset); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Alchemy/BinaryDriver/ConfigurationAwareInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Alchemy\BinaryDriver; 13 | 14 | interface ConfigurationAwareInterface 15 | { 16 | /** 17 | * Returns the configuration 18 | * 19 | * @return ConfigurationInterface 20 | */ 21 | public function getConfiguration(); 22 | 23 | /** 24 | * Set the configuration 25 | * 26 | * @param ConfigurationInterface $configuration 27 | */ 28 | public function setConfiguration(ConfigurationInterface $configuration); 29 | } 30 | -------------------------------------------------------------------------------- /src/Alchemy/BinaryDriver/ConfigurationInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Alchemy\BinaryDriver; 13 | 14 | interface ConfigurationInterface extends \ArrayAccess, \IteratorAggregate 15 | { 16 | /** 17 | * Returns the value given a key from configuration 18 | * 19 | * @param string $key 20 | * @param mixed $default The default value in case the key does not exist 21 | * 22 | * @return mixed 23 | */ 24 | public function get($key, $default = null); 25 | 26 | /** 27 | * Set a value to configuration 28 | * 29 | * @param string $key The key 30 | * @param mixed $value The value corresponding to the key 31 | */ 32 | public function set($key, $value); 33 | 34 | /** 35 | * Tells if Configuration contains `$key` 36 | * 37 | * @param string $key 38 | * 39 | * @return Boolean 40 | */ 41 | public function has($key); 42 | 43 | /** 44 | * Removes a value given a key 45 | * 46 | * @param string $key 47 | * 48 | * @return mixed The previous value 49 | */ 50 | public function remove($key); 51 | 52 | /** 53 | * Returns all values set in the configuration 54 | * 55 | * @return array 56 | */ 57 | public function all(); 58 | } 59 | -------------------------------------------------------------------------------- /src/Alchemy/BinaryDriver/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Alchemy\BinaryDriver\Exception; 13 | 14 | interface ExceptionInterface 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Alchemy/BinaryDriver/Exception/ExecutableNotFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Alchemy\BinaryDriver\Exception; 13 | 14 | class ExecutableNotFoundException extends \RuntimeException implements ExceptionInterface 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Alchemy/BinaryDriver/Exception/ExecutionFailureException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Alchemy\BinaryDriver\Exception; 13 | 14 | class ExecutionFailureException extends \RuntimeException implements ExceptionInterface 15 | { 16 | /** @var string */ 17 | protected $command; 18 | 19 | /** @var string */ 20 | protected $errorOutput; 21 | 22 | public function __construct($binaryName, $command, $errorOutput = null, $code = 0, $previous = null) 23 | { 24 | $message = sprintf("%s failed to execute command %s:\n\nError Output:\n\n %s", $binaryName, $command, $errorOutput); 25 | parent::__construct($message, $code, $previous); 26 | $this->command = $command; 27 | $this->errorOutput = $errorOutput; 28 | } 29 | 30 | public function getCommand(){ 31 | return $this->command; 32 | } 33 | 34 | public function getErrorOutput(){ 35 | return $this->errorOutput; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Alchemy/BinaryDriver/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Alchemy\BinaryDriver\Exception; 13 | 14 | class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Alchemy/BinaryDriver/Listeners/DebugListener.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Alchemy\BinaryDriver\Listeners; 13 | 14 | use Evenement\EventEmitter; 15 | use Symfony\Component\Process\Process; 16 | 17 | class DebugListener extends EventEmitter implements ListenerInterface 18 | { 19 | private $prefixOut; 20 | private $prefixErr; 21 | private $eventOut; 22 | private $eventErr; 23 | 24 | public function __construct($prefixOut = '[OUT] ', $prefixErr = '[ERROR] ', $eventOut = 'debug', $eventErr = 'debug') 25 | { 26 | $this->prefixOut = $prefixOut; 27 | $this->prefixErr = $prefixErr; 28 | $this->eventOut = $eventOut; 29 | $this->eventErr = $eventErr; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function handle($type, $data) 36 | { 37 | if (Process::ERR === $type) { 38 | $this->emitLines($this->eventErr, $this->prefixErr, $data); 39 | } elseif (Process::OUT === $type) { 40 | $this->emitLines($this->eventOut, $this->prefixOut, $data); 41 | } 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function forwardedEvents() 48 | { 49 | return array_unique(array($this->eventErr, $this->eventOut)); 50 | } 51 | 52 | private function emitLines($event, $prefix, $lines) 53 | { 54 | foreach (explode("\n", $lines) as $line) { 55 | $this->emit($event, array($prefix . $line)); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Alchemy/BinaryDriver/Listeners/ListenerInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Alchemy\BinaryDriver\Listeners; 13 | 14 | use Evenement\EventEmitterInterface; 15 | 16 | interface ListenerInterface extends EventEmitterInterface 17 | { 18 | /** 19 | * Handle the output of a ProcessRunner 20 | * 21 | * @param string $type The data type, one of Process::ERR, Process::OUT constants 22 | * @param string $data The output 23 | */ 24 | public function handle($type, $data); 25 | 26 | /** 27 | * An array of events that should be forwarded to BinaryInterface 28 | * 29 | * @return array 30 | */ 31 | public function forwardedEvents(); 32 | } 33 | -------------------------------------------------------------------------------- /src/Alchemy/BinaryDriver/Listeners/Listeners.php: -------------------------------------------------------------------------------- 1 | storage = new SplObjectStorage(); 16 | } 17 | 18 | public function __clone() 19 | { 20 | $storage = $this->storage; 21 | $this->storage = new SplObjectStorage(); 22 | $this->storage->addAll($storage); 23 | } 24 | 25 | /** 26 | * Registers a listener, pass the listener events to the target. 27 | * 28 | * @param ListenerInterface $listener 29 | * @param null|EventEmitter $target 30 | * 31 | * @return ListenersInterface 32 | */ 33 | public function register(ListenerInterface $listener, EventEmitter $target = null) 34 | { 35 | $EElisteners = array(); 36 | 37 | if (null !== $target) { 38 | $EElisteners = $this->forwardEvents($listener, $target, $listener->forwardedEvents()); 39 | } 40 | 41 | $this->storage->attach($listener, $EElisteners); 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * Unregisters a listener, removes the listener events from the target. 48 | * 49 | * @param ListenerInterface $listener 50 | * 51 | * @return ListenersInterface 52 | * 53 | * @throws InvalidArgumentException In case the listener is not registered 54 | */ 55 | public function unregister(ListenerInterface $listener) 56 | { 57 | if (!isset($this->storage[$listener])) { 58 | throw new InvalidArgumentException('Listener is not registered.'); 59 | } 60 | 61 | foreach ($this->storage[$listener] as $event => $EElistener) { 62 | $listener->removeListener($event, $EElistener); 63 | } 64 | 65 | $this->storage->detach($listener); 66 | 67 | return $this; 68 | } 69 | 70 | private function forwardEvents($source, $target, array $events) 71 | { 72 | $EElisteners = array(); 73 | 74 | foreach ($events as $event) { 75 | $listener = $this->createListener($event, $target); 76 | $source->on($event, $EElisteners[$event] = $listener); 77 | } 78 | 79 | return $EElisteners; 80 | } 81 | 82 | private function createListener($event, $target) 83 | { 84 | return function () use ($event, $target) { 85 | $target->emit($event, func_get_args()); 86 | }; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Alchemy/BinaryDriver/ProcessBuilderFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Alchemy\BinaryDriver; 13 | 14 | use Alchemy\BinaryDriver\Exception\InvalidArgumentException; 15 | use Symfony\Component\Process\Process; 16 | use Symfony\Component\Process\ProcessBuilder; 17 | 18 | class ProcessBuilderFactory implements ProcessBuilderFactoryInterface 19 | { 20 | /** 21 | * The binary path 22 | * 23 | * @var String 24 | */ 25 | protected $binary; 26 | 27 | /** 28 | * The timeout for the generated processes 29 | * 30 | * @var integer|float 31 | */ 32 | private $timeout; 33 | 34 | /** 35 | * An internal ProcessBuilder. 36 | * 37 | * Note that this one is used only if Symfony ProcessBuilder has method 38 | * setPrefix (2.3) 39 | * 40 | * @var ProcessBuilder 41 | */ 42 | private $builder; 43 | 44 | /** 45 | * Tells whether Symfony LTS ProcessBuilder should be emulated or not. 46 | * 47 | * This symfony version provided a brand new ::setPrefix method. 48 | * 49 | * @var Boolean 50 | */ 51 | public static $emulateSfLTS; 52 | 53 | /** 54 | * Constructor 55 | * 56 | * @param String $binary The path to the binary 57 | * 58 | * @throws InvalidArgumentException In case binary path is invalid 59 | */ 60 | public function __construct($binary) 61 | { 62 | $this->detectEmulation(); 63 | 64 | if (!self::$emulateSfLTS) { 65 | $this->builder = new ProcessBuilder(); 66 | } 67 | 68 | $this->useBinary($binary); 69 | } 70 | 71 | /** 72 | * Covenient method for unit testing 73 | * 74 | * @return type 75 | */ 76 | public function getBuilder() 77 | { 78 | return $this->builder; 79 | } 80 | 81 | /** 82 | * Covenient method for unit testing 83 | * 84 | * @param ProcessBuilder $builder 85 | * @return ProcessBuilderFactory 86 | */ 87 | public function setBuilder(ProcessBuilder $builder) 88 | { 89 | $this->builder = $builder; 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * @inheritdoc 96 | */ 97 | public function getBinary() 98 | { 99 | return $this->binary; 100 | } 101 | 102 | /** 103 | * @inheritdoc 104 | */ 105 | public function useBinary($binary) 106 | { 107 | if (!is_executable($binary)) { 108 | throw new InvalidArgumentException(sprintf('`%s` is not an executable binary', $binary)); 109 | } 110 | 111 | $this->binary = $binary; 112 | 113 | if (!static::$emulateSfLTS) { 114 | $this->builder->setPrefix($binary); 115 | } 116 | 117 | return $this; 118 | } 119 | 120 | /** 121 | * @inheritdoc 122 | */ 123 | public function setTimeout($timeout) 124 | { 125 | $this->timeout = $timeout; 126 | 127 | if (!static::$emulateSfLTS) { 128 | $this->builder->setTimeout($this->timeout); 129 | } 130 | 131 | return $this; 132 | } 133 | 134 | /** 135 | * @inheritdoc 136 | */ 137 | public function getTimeout() 138 | { 139 | return $this->timeout; 140 | } 141 | 142 | /** 143 | * @inheritdoc 144 | */ 145 | public function create($arguments = array()) 146 | { 147 | if (null === $this->binary) { 148 | throw new InvalidArgumentException('No binary set'); 149 | } 150 | 151 | if (!is_array($arguments)) { 152 | $arguments = array($arguments); 153 | } 154 | 155 | if (static::$emulateSfLTS) { 156 | array_unshift($arguments, $this->binary); 157 | if (method_exists('Symfony\Component\Process\ProcessUtils', 'escapeArgument')) { 158 | $script = implode(' ', array_map(array('Symfony\Component\Process\ProcessUtils', 'escapeArgument'), $arguments)); 159 | } else { 160 | $script = $arguments; 161 | } 162 | 163 | $env = array_replace($_ENV, $_SERVER); 164 | $env = array_filter($env, function ($value) { 165 | return !is_array($value); 166 | }); 167 | 168 | return new Process($script, null, $env, null, $this->timeout); 169 | } else { 170 | return $this->builder 171 | ->setArguments($arguments) 172 | ->getProcess(); 173 | } 174 | } 175 | 176 | private function detectEmulation() 177 | { 178 | if (null !== static::$emulateSfLTS) { 179 | return $this; 180 | } 181 | 182 | static::$emulateSfLTS = !method_exists('Symfony\Component\Process\ProcessBuilder', 'setPrefix'); 183 | 184 | return $this; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/Alchemy/BinaryDriver/ProcessBuilderFactoryAwareInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Alchemy\BinaryDriver; 13 | 14 | interface ProcessBuilderFactoryAwareInterface 15 | { 16 | /** 17 | * Returns the current process builder factory 18 | * 19 | * @return ProcessBuilderFactoryInterface 20 | */ 21 | public function getProcessBuilderFactory(); 22 | 23 | /** 24 | * Set a process builder factory 25 | * 26 | * @param ProcessBuilderFactoryInterface $factory 27 | */ 28 | public function setProcessBuilderFactory(ProcessBuilderFactoryInterface $factory); 29 | } 30 | -------------------------------------------------------------------------------- /src/Alchemy/BinaryDriver/ProcessBuilderFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Alchemy\BinaryDriver; 13 | 14 | use Alchemy\BinaryDriver\Exception\InvalidArgumentException; 15 | use Symfony\Component\Process\Process; 16 | 17 | interface ProcessBuilderFactoryInterface 18 | { 19 | /** 20 | * Returns a new instance of Symfony Process 21 | * 22 | * @param string|array $arguments An argument or an array of arguments 23 | * 24 | * @return Process 25 | * 26 | * @throws InvalidArgumentException 27 | */ 28 | public function create($arguments = array()); 29 | 30 | /** 31 | * Returns the path to the binary that is used 32 | * 33 | * @return String 34 | */ 35 | public function getBinary(); 36 | 37 | /** 38 | * Sets the path to the binary 39 | * 40 | * @param String $binary A path to a binary 41 | * 42 | * @return ProcessBuilderFactoryInterface 43 | * 44 | * @throws InvalidArgumentException In case binary is not executable 45 | */ 46 | public function useBinary($binary); 47 | 48 | /** 49 | * Set the default timeout to apply on created processes. 50 | * 51 | * @param integer|float $timeout 52 | * 53 | * @return ProcessBuilderFactoryInterface 54 | * 55 | * @throws InvalidArgumentException In case the timeout is not valid 56 | */ 57 | public function setTimeout($timeout); 58 | 59 | /** 60 | * Returns the current timeout applied to the created processes. 61 | * 62 | * @return integer|float 63 | */ 64 | public function getTimeout(); 65 | } 66 | -------------------------------------------------------------------------------- /src/Alchemy/BinaryDriver/ProcessRunner.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Alchemy\BinaryDriver; 13 | 14 | use Alchemy\BinaryDriver\Exception\ExecutionFailureException; 15 | use Psr\Log\LoggerInterface; 16 | use SplObjectStorage; 17 | use Symfony\Component\Process\Exception\RuntimeException; 18 | use Symfony\Component\Process\Process; 19 | 20 | class ProcessRunner implements ProcessRunnerInterface 21 | { 22 | /** @var LoggerInterface */ 23 | private $logger; 24 | 25 | /** @var string */ 26 | private $name; 27 | 28 | public function __construct(LoggerInterface $logger, $name) 29 | { 30 | $this->logger = $logger; 31 | $this->name = $name; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | * 37 | * @return ProcessRunner 38 | */ 39 | public function setLogger(LoggerInterface $logger) 40 | { 41 | $this->logger = $logger; 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * @return LoggerInterface 48 | */ 49 | public function getLogger() 50 | { 51 | return $this->logger; 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function run(Process $process, SplObjectStorage $listeners, $bypassErrors) 58 | { 59 | $this->logger->info(sprintf( 60 | '%s running command %s', $this->name, $process->getCommandLine() 61 | )); 62 | 63 | try { 64 | $process->run($this->buildCallback($listeners)); 65 | } catch (RuntimeException $e) { 66 | if (!$bypassErrors) { 67 | $this->doExecutionFailure($process->getCommandLine(), $process->getErrorOutput(), $e); 68 | } 69 | } 70 | 71 | 72 | if (!$bypassErrors && !$process->isSuccessful()) { 73 | $this->doExecutionFailure($process->getCommandLine(), $process->getErrorOutput()); 74 | } elseif (!$process->isSuccessful()) { 75 | $this->logger->error($this->createErrorMessage($process->getCommandLine(), $process->getErrorOutput())); 76 | return; 77 | } else { 78 | $this->logger->info(sprintf('%s executed command successfully', $this->name)); 79 | return $process->getOutput(); 80 | } 81 | } 82 | 83 | private function buildCallback(SplObjectStorage $listeners) 84 | { 85 | return function ($type, $data) use ($listeners) { 86 | foreach ($listeners as $listener) { 87 | $listener->handle($type, $data); 88 | } 89 | }; 90 | } 91 | 92 | private function doExecutionFailure($command, $errorOutput, \Exception $e = null) 93 | { 94 | $this->logger->error($this->createErrorMessage($command, $errorOutput)); 95 | throw new ExecutionFailureException($this->name, $command, $errorOutput, 96 | $e ? $e->getCode() : 0, $e ?: null); 97 | } 98 | 99 | private function createErrorMessage($command, $errorOutput){ 100 | return sprintf('%s failed to execute command %s: %s', $this->name, $command, $errorOutput); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Alchemy/BinaryDriver/ProcessRunnerAwareInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Alchemy\BinaryDriver; 13 | 14 | interface ProcessRunnerAwareInterface 15 | { 16 | /** 17 | * Returns the current process runner 18 | * 19 | * @return ProcessRunnerInterface 20 | */ 21 | public function getProcessRunner(); 22 | 23 | /** 24 | * Sets a process runner 25 | * 26 | * @param ProcessRunnerInterface $runner 27 | */ 28 | public function setProcessRunner(ProcessRunnerInterface $runner); 29 | } 30 | -------------------------------------------------------------------------------- /src/Alchemy/BinaryDriver/ProcessRunnerInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Alchemy\BinaryDriver; 13 | 14 | use Alchemy\BinaryDriver\Exception\ExecutionFailureException; 15 | use Psr\Log\LoggerAwareInterface; 16 | use SplObjectStorage; 17 | use Symfony\Component\Process\Process; 18 | 19 | interface ProcessRunnerInterface extends LoggerAwareInterface 20 | { 21 | /** 22 | * Executes a process, logs events 23 | * 24 | * @param Process $process 25 | * @param SplObjectStorage $listeners Some listeners 26 | * @param Boolean $bypassErrors Set to true to disable throwing ExecutionFailureExceptions 27 | * 28 | * @return string The Process output 29 | * 30 | * @throws ExecutionFailureException in case of process failure. 31 | */ 32 | public function run(Process $process, SplObjectStorage $listeners, $bypassErrors); 33 | } 34 | -------------------------------------------------------------------------------- /tests/Alchemy/Tests/BinaryDriver/AbstractBinaryTest.php: -------------------------------------------------------------------------------- 1 | find('php'); 18 | 19 | if (null === $php) { 20 | $this->markTestSkipped('Unable to find a php binary'); 21 | } 22 | 23 | return $php; 24 | } 25 | 26 | public function testSimpleLoadWithBinaryPath() 27 | { 28 | $php = $this->getPhpBinary(); 29 | $imp = Implementation::load($php); 30 | $this->assertInstanceOf('Alchemy\Tests\BinaryDriver\Implementation', $imp); 31 | 32 | $this->assertEquals($php, $imp->getProcessBuilderFactory()->getBinary()); 33 | } 34 | 35 | public function testMultipleLoadWithBinaryPath() 36 | { 37 | $php = $this->getPhpBinary(); 38 | $imp = Implementation::load(array('/zz/path/to/unexisting/command', $php)); 39 | $this->assertInstanceOf('Alchemy\Tests\BinaryDriver\Implementation', $imp); 40 | 41 | $this->assertEquals($php, $imp->getProcessBuilderFactory()->getBinary()); 42 | } 43 | 44 | public function testSimpleLoadWithBinaryName() 45 | { 46 | $php = $this->getPhpBinary(); 47 | $imp = Implementation::load('php'); 48 | $this->assertInstanceOf('Alchemy\Tests\BinaryDriver\Implementation', $imp); 49 | 50 | $this->assertEquals($php, $imp->getProcessBuilderFactory()->getBinary()); 51 | } 52 | 53 | public function testMultipleLoadWithBinaryName() 54 | { 55 | $php = $this->getPhpBinary(); 56 | $imp = Implementation::load(array('bachibouzouk', 'php')); 57 | $this->assertInstanceOf('Alchemy\Tests\BinaryDriver\Implementation', $imp); 58 | 59 | $this->assertEquals($php, $imp->getProcessBuilderFactory()->getBinary()); 60 | } 61 | 62 | public function testLoadWithMultiplePathExpectingAFailure() 63 | { 64 | $this->setExpectedException(ExecutableNotFoundException::class); 65 | 66 | Implementation::load(array('bachibouzouk', 'moribon')); 67 | } 68 | 69 | public function testLoadWithUniquePathExpectingAFailure() 70 | { 71 | $this->setExpectedException(ExecutableNotFoundException::class); 72 | 73 | Implementation::load('bachibouzouk'); 74 | } 75 | 76 | public function testLoadWithCustomLogger() 77 | { 78 | $logger = $this->getMock('Psr\Log\LoggerInterface'); 79 | $imp = Implementation::load('php', $logger); 80 | 81 | $this->assertEquals($logger, $imp->getProcessRunner()->getLogger()); 82 | } 83 | 84 | public function testLoadWithCustomConfigurationAsArray() 85 | { 86 | $conf = array('timeout' => 200); 87 | $imp = Implementation::load('php', null, $conf); 88 | 89 | $this->assertEquals($conf, $imp->getConfiguration()->all()); 90 | } 91 | 92 | public function testLoadWithCustomConfigurationAsObject() 93 | { 94 | $conf = $this->getMock('Alchemy\BinaryDriver\ConfigurationInterface'); 95 | $imp = Implementation::load('php', null, $conf); 96 | 97 | $this->assertEquals($conf, $imp->getConfiguration()); 98 | } 99 | 100 | public function testProcessBuilderFactoryGetterAndSetters() 101 | { 102 | $imp = Implementation::load('php'); 103 | $factory = $this->getMock('Alchemy\BinaryDriver\ProcessBuilderFactoryInterface'); 104 | 105 | $imp->setProcessBuilderFactory($factory); 106 | $this->assertEquals($factory, $imp->getProcessBuilderFactory()); 107 | } 108 | 109 | public function testConfigurationGetterAndSetters() 110 | { 111 | $imp = Implementation::load('php'); 112 | $conf = $this->getMock('Alchemy\BinaryDriver\ConfigurationInterface'); 113 | 114 | $imp->setConfiguration($conf); 115 | $this->assertEquals($conf, $imp->getConfiguration()); 116 | } 117 | 118 | public function testTimeoutIsSetOnConstruction() 119 | { 120 | $imp = Implementation::load('php', null, array('timeout' => 42)); 121 | $this->assertEquals(42, $imp->getProcessBuilderFactory()->getTimeout()); 122 | } 123 | 124 | public function testTimeoutIsSetOnConfigurationSetting() 125 | { 126 | $imp = Implementation::load('php', null); 127 | $imp->setConfiguration(new Configuration(array('timeout' => 42))); 128 | $this->assertEquals(42, $imp->getProcessBuilderFactory()->getTimeout()); 129 | } 130 | 131 | public function testTimeoutIsSetOnProcessBuilderSetting() 132 | { 133 | $imp = Implementation::load('php', null, array('timeout' => 42)); 134 | 135 | $factory = $this->getMock('Alchemy\BinaryDriver\ProcessBuilderFactoryInterface'); 136 | $factory->expects($this->once()) 137 | ->method('setTimeout') 138 | ->with(42); 139 | 140 | $imp->setProcessBuilderFactory($factory); 141 | } 142 | 143 | public function testListenRegistersAListener() 144 | { 145 | $imp = Implementation::load('php'); 146 | 147 | $listeners = $this->getMockBuilder('Alchemy\BinaryDriver\Listeners\Listeners') 148 | ->disableOriginalConstructor() 149 | ->getMock(); 150 | 151 | $listener = $this->getMock('Alchemy\BinaryDriver\Listeners\ListenerInterface'); 152 | 153 | $listeners->expects($this->once()) 154 | ->method('register') 155 | ->with($this->equalTo($listener), $this->equalTo($imp)); 156 | 157 | $reflexion = new \ReflectionClass('Alchemy\BinaryDriver\AbstractBinary'); 158 | $prop = $reflexion->getProperty('listenersManager'); 159 | $prop->setAccessible(true); 160 | $prop->setValue($imp, $listeners); 161 | 162 | $imp->listen($listener); 163 | } 164 | 165 | /** 166 | * @dataProvider provideCommandParameters 167 | */ 168 | public function testCommandRunsAProcess($parameters, $bypassErrors, $expectedParameters, $output) 169 | { 170 | $imp = Implementation::load('php'); 171 | $factory = $this->getMock('Alchemy\BinaryDriver\ProcessBuilderFactoryInterface'); 172 | $processRunner = $this->getMock('Alchemy\BinaryDriver\ProcessRunnerInterface'); 173 | 174 | $process = $this->getMockBuilder('Symfony\Component\Process\Process') 175 | ->disableOriginalConstructor() 176 | ->getMock(); 177 | 178 | $processRunner->expects($this->once()) 179 | ->method('run') 180 | ->with($this->equalTo($process), $this->isInstanceOf('SplObjectStorage'), $this->equalTo($bypassErrors)) 181 | ->will($this->returnValue($output)); 182 | 183 | $factory->expects($this->once()) 184 | ->method('create') 185 | ->with($expectedParameters) 186 | ->will($this->returnValue($process)); 187 | 188 | $imp->setProcessBuilderFactory($factory); 189 | $imp->setProcessRunner($processRunner); 190 | 191 | $this->assertEquals($output, $imp->command($parameters, $bypassErrors)); 192 | } 193 | 194 | /** 195 | * @dataProvider provideCommandWithListenersParameters 196 | */ 197 | public function testCommandWithTemporaryListeners($parameters, $bypassErrors, $expectedParameters, $output, $count, $listeners) 198 | { 199 | $imp = Implementation::load('php'); 200 | $factory = $this->getMock('Alchemy\BinaryDriver\ProcessBuilderFactoryInterface'); 201 | $processRunner = $this->getMock('Alchemy\BinaryDriver\ProcessRunnerInterface'); 202 | 203 | $process = $this->getMockBuilder('Symfony\Component\Process\Process') 204 | ->disableOriginalConstructor() 205 | ->getMock(); 206 | 207 | $firstStorage = $secondStorage = null; 208 | 209 | $processRunner->expects($this->exactly(2)) 210 | ->method('run') 211 | ->with($this->equalTo($process), $this->isInstanceOf('SplObjectStorage'), $this->equalTo($bypassErrors)) 212 | ->will($this->returnCallback(function ($process, $storage, $errors) use ($output, &$firstStorage, &$secondStorage) { 213 | if (null === $firstStorage) { 214 | $firstStorage = $storage; 215 | } else { 216 | $secondStorage = $storage; 217 | } 218 | 219 | return $output; 220 | })); 221 | 222 | $factory->expects($this->exactly(2)) 223 | ->method('create') 224 | ->with($expectedParameters) 225 | ->will($this->returnValue($process)); 226 | 227 | $imp->setProcessBuilderFactory($factory); 228 | $imp->setProcessRunner($processRunner); 229 | 230 | $this->assertEquals($output, $imp->command($parameters, $bypassErrors, $listeners)); 231 | $this->assertCount($count, $firstStorage); 232 | $this->assertEquals($output, $imp->command($parameters, $bypassErrors)); 233 | $this->assertCount(0, $secondStorage); 234 | } 235 | 236 | public function provideCommandWithListenersParameters() 237 | { 238 | return array( 239 | array('-a', false, array('-a'), 'loubda', 2, array($this->getMockListener(), $this->getMockListener())), 240 | array('-a', false, array('-a'), 'loubda', 1, array($this->getMockListener())), 241 | array('-a', false, array('-a'), 'loubda', 1, $this->getMockListener()), 242 | array('-a', false, array('-a'), 'loubda', 0, array()), 243 | ); 244 | } 245 | 246 | public function provideCommandParameters() 247 | { 248 | return array( 249 | array('-a', false, array('-a'), 'loubda'), 250 | array('-a', true, array('-a'), 'loubda'), 251 | array('-a -b', false, array('-a -b'), 'loubda'), 252 | array(array('-a'), false, array('-a'), 'loubda'), 253 | array(array('-a'), true, array('-a'), 'loubda'), 254 | array(array('-a', '-b'), false, array('-a', '-b'), 'loubda'), 255 | ); 256 | } 257 | 258 | public function testUnlistenUnregistersAListener() 259 | { 260 | $imp = Implementation::load('php'); 261 | 262 | $listeners = $this->getMockBuilder('Alchemy\BinaryDriver\Listeners\Listeners') 263 | ->disableOriginalConstructor() 264 | ->getMock(); 265 | 266 | $listener = $this->getMock('Alchemy\BinaryDriver\Listeners\ListenerInterface'); 267 | 268 | $listeners->expects($this->once()) 269 | ->method('unregister') 270 | ->with($this->equalTo($listener), $this->equalTo($imp)); 271 | 272 | $reflexion = new \ReflectionClass('Alchemy\BinaryDriver\AbstractBinary'); 273 | $prop = $reflexion->getProperty('listenersManager'); 274 | $prop->setAccessible(true); 275 | $prop->setValue($imp, $listeners); 276 | 277 | $imp->unlisten($listener); 278 | } 279 | 280 | /** 281 | * @return \PHPUnit_Framework_MockObject_MockObject 282 | */ 283 | private function getMockListener() 284 | { 285 | $listener = $this->getMock(ListenerInterface::class); 286 | $listener->expects($this->any()) 287 | ->method('forwardedEvents') 288 | ->willReturn(array()); 289 | 290 | return $listener; 291 | } 292 | } 293 | 294 | class Implementation extends AbstractBinary 295 | { 296 | public function getName() 297 | { 298 | return 'Implementation'; 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /tests/Alchemy/Tests/BinaryDriver/AbstractProcessBuilderFactoryTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Unable to detect php binary, skipping'); 23 | } 24 | } 25 | 26 | public static function setUpBeforeClass() 27 | { 28 | $finder = new ExecutableFinder(); 29 | static::$phpBinary = $finder->find('php'); 30 | } 31 | 32 | public function testThatBinaryIsSetOnConstruction() 33 | { 34 | $factory = $this->getProcessBuilderFactory(static::$phpBinary); 35 | $this->assertEquals(static::$phpBinary, $factory->getBinary()); 36 | } 37 | 38 | public function testGetSetBinary() 39 | { 40 | $finder = new ExecutableFinder(); 41 | $phpUnit = $finder->find('phpunit'); 42 | 43 | if (null === $phpUnit) { 44 | $this->markTestSkipped('Unable to detect phpunit binary, skipping'); 45 | } 46 | 47 | $factory = $this->getProcessBuilderFactory(static::$phpBinary); 48 | $factory->useBinary($phpUnit); 49 | $this->assertEquals($phpUnit, $factory->getBinary()); 50 | } 51 | 52 | /** 53 | * @expectedException Alchemy\BinaryDriver\Exception\InvalidArgumentException 54 | */ 55 | public function testUseNonExistantBinary() 56 | { 57 | $factory = $this->getProcessBuilderFactory(static::$phpBinary); 58 | $factory->useBinary('itissureitdoesnotexist'); 59 | } 60 | 61 | public function testCreateShouldReturnAProcess() 62 | { 63 | $factory = $this->getProcessBuilderFactory(static::$phpBinary); 64 | $process = $factory->create(); 65 | 66 | $this->assertInstanceOf('Symfony\Component\Process\Process', $process); 67 | $this->assertEquals("'".static::$phpBinary."'", $process->getCommandLine()); 68 | } 69 | 70 | public function testCreateWithStringArgument() 71 | { 72 | $factory = $this->getProcessBuilderFactory(static::$phpBinary); 73 | $process = $factory->create('-v'); 74 | 75 | $this->assertInstanceOf('Symfony\Component\Process\Process', $process); 76 | $this->assertEquals("'".static::$phpBinary."' '-v'", $process->getCommandLine()); 77 | } 78 | 79 | public function testCreateWithArrayArgument() 80 | { 81 | $factory = $this->getProcessBuilderFactory(static::$phpBinary); 82 | $process = $factory->create(array('-r', 'echo "Hello !";')); 83 | 84 | $this->assertInstanceOf('Symfony\Component\Process\Process', $process); 85 | $this->assertEquals("'".static::$phpBinary."' '-r' 'echo \"Hello !\";'", $process->getCommandLine()); 86 | } 87 | 88 | public function testCreateWithTimeout() 89 | { 90 | $factory = $this->getProcessBuilderFactory(static::$phpBinary); 91 | $factory->setTimeout(200); 92 | $process = $factory->create(array('-i')); 93 | 94 | $this->assertInstanceOf('Symfony\Component\Process\Process', $process); 95 | $this->assertEquals(200, $process->getTimeout()); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/Alchemy/Tests/BinaryDriver/ConfigurationTest.php: -------------------------------------------------------------------------------- 1 | 'value')); 12 | 13 | $this->assertTrue(isset($configuration['key'])); 14 | $this->assertEquals('value', $configuration['key']); 15 | 16 | $this->assertFalse(isset($configuration['key2'])); 17 | unset($configuration['key']); 18 | $this->assertFalse(isset($configuration['key'])); 19 | 20 | $configuration['key2'] = 'value2'; 21 | $this->assertTrue(isset($configuration['key2'])); 22 | $this->assertEquals('value2', $configuration['key2']); 23 | } 24 | 25 | public function testGetOnNonExistentKeyShouldReturnDefaultValue() 26 | { 27 | $conf = new Configuration(); 28 | $this->assertEquals('booba', $conf->get('hooba', 'booba')); 29 | $this->assertEquals(null, $conf->get('hooba')); 30 | } 31 | 32 | public function testSetHasGetRemove() 33 | { 34 | $configuration = new Configuration(array('key' => 'value')); 35 | 36 | $this->assertTrue($configuration->has('key')); 37 | $this->assertEquals('value', $configuration->get('key')); 38 | 39 | $this->assertFalse($configuration->has('key2')); 40 | $configuration->remove('key'); 41 | $this->assertFalse($configuration->has('key')); 42 | 43 | $configuration->set('key2', 'value2'); 44 | $this->assertTrue($configuration->has('key2')); 45 | $this->assertEquals('value2', $configuration->get('key2')); 46 | } 47 | 48 | public function testIterator() 49 | { 50 | $data = array( 51 | 'key1' => 'value1', 52 | 'key2' => 'value2', 53 | 'key3' => 'value3', 54 | ); 55 | 56 | $captured = array(); 57 | $conf = new Configuration($data); 58 | 59 | foreach ($conf as $key => $value) { 60 | $captured[$key] = $value; 61 | } 62 | 63 | $this->assertEquals($data, $captured); 64 | } 65 | 66 | public function testAll() 67 | { 68 | $data = array( 69 | 'key1' => 'value1', 70 | 'key2' => 'value2', 71 | 'key3' => 'value3', 72 | ); 73 | 74 | $conf = new Configuration($data); 75 | $this->assertEquals($data, $conf->all()); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /tests/Alchemy/Tests/BinaryDriver/Exceptions/ExecutionFailureExceptionTest.php: -------------------------------------------------------------------------------- 1 | createLoggerMock(); 19 | $runner = $this->getProcessRunner($logger); 20 | 21 | $process = $this->createProcessMock(1, false, '--helloworld--', null, "Error Output", true); 22 | try{ 23 | $runner->run($process, new \SplObjectStorage(), false); 24 | $this->fail('An exception should have been raised'); 25 | } 26 | catch (ExecutionFailureException $e){ 27 | $this->assertEquals("--helloworld--", $e->getCommand()); 28 | $this->assertEquals("Error Output", $e->getErrorOutput()); 29 | } 30 | 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /tests/Alchemy/Tests/BinaryDriver/LTSProcessBuilder.php: -------------------------------------------------------------------------------- 1 | arguments = $arguments; 19 | parent::__construct($arguments); 20 | } 21 | 22 | public function setArguments(array $arguments) 23 | { 24 | $this->arguments = $arguments; 25 | 26 | return $this; 27 | } 28 | 29 | public function setPrefix($prefix) 30 | { 31 | $this->prefix = $prefix; 32 | 33 | return $this; 34 | } 35 | 36 | public function setTimeout($timeout) 37 | { 38 | $this->timeout = $timeout; 39 | 40 | return $this; 41 | } 42 | 43 | public function getProcess() 44 | { 45 | if (!$this->prefix && !count($this->arguments)) { 46 | throw new LogicException('You must add() command arguments before calling getProcess().'); 47 | } 48 | 49 | $args = $this->prefix ? array_merge(array($this->prefix), $this->arguments) : $this->arguments; 50 | $script = implode(' ', array_map('escapeshellarg', $args)); 51 | 52 | return new Process($script, null, null, null, $this->timeout); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Alchemy/Tests/BinaryDriver/LTSProcessBuilderFactoryTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('ProcessBuilder is not available.'); 13 | return; 14 | } 15 | 16 | parent::setUp(); 17 | } 18 | 19 | protected function getProcessBuilderFactory($binary) 20 | { 21 | $factory = new ProcessBuilderFactory($binary); 22 | $factory->setBuilder(new LTSProcessBuilder()); 23 | ProcessBuilderFactory::$emulateSfLTS = false; 24 | $factory->useBinary($binary); 25 | 26 | return $factory; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Alchemy/Tests/BinaryDriver/Listeners/DebugListenerTest.php: -------------------------------------------------------------------------------- 1 | on('debug', function ($line) use (&$lines) { 16 | $lines[] = $line; 17 | }); 18 | $listener->handle(Process::ERR, "first line\nsecond line"); 19 | $listener->handle(Process::OUT, "cool output"); 20 | $listener->handle('unknown', "lalala"); 21 | $listener->handle(Process::OUT, "another output\n"); 22 | 23 | $expected = array( 24 | '[ERROR] first line', 25 | '[ERROR] second line', 26 | '[OUT] cool output', 27 | '[OUT] another output', 28 | '[OUT] ', 29 | ); 30 | 31 | $this->assertEquals($expected, $lines); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Alchemy/Tests/BinaryDriver/Listeners/ListenersTest.php: -------------------------------------------------------------------------------- 1 | register($listener); 17 | 18 | $n = 0; 19 | $listener->on('received', function ($type, $data) use (&$n, &$capturedType, &$capturedData) { 20 | $n++; 21 | $capturedData = $data; 22 | $capturedType = $type; 23 | }); 24 | 25 | $type = 'type'; 26 | $data = 'data'; 27 | 28 | $listener->handle($type, $data); 29 | $listener->handle($type, $data); 30 | 31 | $listeners->unregister($listener); 32 | 33 | $listener->handle($type, $data); 34 | 35 | $this->assertEquals(3, $n); 36 | $this->assertEquals($type, $capturedType); 37 | $this->assertEquals($data, $capturedData); 38 | } 39 | 40 | public function testRegisterAndForwardThenUnregister() 41 | { 42 | $listener = new MockListener(); 43 | $target = new EventEmitter(); 44 | 45 | $n = 0; 46 | $target->on('received', function ($type, $data) use (&$n, &$capturedType, &$capturedData) { 47 | $n++; 48 | $capturedData = $data; 49 | $capturedType = $type; 50 | }); 51 | 52 | $m = 0; 53 | $listener->on('received', function ($type, $data) use (&$m, &$capturedType2, &$capturedData2) { 54 | $m++; 55 | $capturedData2 = $data; 56 | $capturedType2 = $type; 57 | }); 58 | 59 | $listeners = new Listeners(); 60 | $listeners->register($listener, $target); 61 | 62 | $type = 'type'; 63 | $data = 'data'; 64 | 65 | $listener->handle($type, $data); 66 | $listener->handle($type, $data); 67 | 68 | $listeners->unregister($listener, $target); 69 | 70 | $listener->handle($type, $data); 71 | 72 | $this->assertEquals(2, $n); 73 | $this->assertEquals(3, $m); 74 | $this->assertEquals($type, $capturedType); 75 | $this->assertEquals($data, $capturedData); 76 | $this->assertEquals($type, $capturedType2); 77 | $this->assertEquals($data, $capturedData2); 78 | } 79 | } 80 | 81 | class MockListener extends EventEmitter implements ListenerInterface 82 | { 83 | public function handle($type, $data) 84 | { 85 | $this->emit('received', array($type, $data)); 86 | } 87 | 88 | public function forwardedEvents() 89 | { 90 | return array('received'); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/Alchemy/Tests/BinaryDriver/NONLTSProcessBuilderFactoryTest.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Alchemy\Tests\BinaryDriver; 13 | 14 | use Alchemy\BinaryDriver\ProcessRunner; 15 | use Alchemy\BinaryDriver\BinaryDriverTestCase; 16 | use Alchemy\BinaryDriver\Exception\ExecutionFailureException; 17 | use Alchemy\BinaryDriver\Listeners\ListenerInterface; 18 | use Evenement\EventEmitter; 19 | use Symfony\Component\Process\Exception\RuntimeException as ProcessRuntimeException; 20 | 21 | class ProcessRunnerTest extends BinaryDriverTestCase 22 | { 23 | public function getProcessRunner($logger) 24 | { 25 | return new ProcessRunner($logger, 'test-runner'); 26 | } 27 | 28 | public function testRunSuccessFullProcess() 29 | { 30 | $logger = $this->createLoggerMock(); 31 | $runner = $this->getProcessRunner($logger); 32 | 33 | $process = $this->createProcessMock(1, true, '--helloworld--', "Kikoo Romain", null, true); 34 | 35 | $logger 36 | ->expects($this->never()) 37 | ->method('error'); 38 | $logger 39 | ->expects($this->exactly(2)) 40 | ->method('info'); 41 | 42 | $this->assertEquals('Kikoo Romain', $runner->run($process, new \SplObjectStorage(), false)); 43 | } 44 | 45 | public function testRunSuccessFullProcessBypassingErrors() 46 | { 47 | $logger = $this->createLoggerMock(); 48 | $runner = $this->getProcessRunner($logger); 49 | 50 | $process = $this->createProcessMock(1, true, '--helloworld--', "Kikoo Romain", null, true); 51 | 52 | $logger 53 | ->expects($this->never()) 54 | ->method('error'); 55 | $logger 56 | ->expects($this->exactly(2)) 57 | ->method('info'); 58 | 59 | $this->assertEquals('Kikoo Romain', $runner->run($process, new \SplObjectStorage(), true)); 60 | } 61 | 62 | public function testRunFailingProcess() 63 | { 64 | $logger = $this->createLoggerMock(); 65 | $runner = $this->getProcessRunner($logger); 66 | 67 | $process = $this->createProcessMock(1, false, '--helloworld--', null, null, true); 68 | 69 | $logger 70 | ->expects($this->once()) 71 | ->method('error'); 72 | $logger 73 | ->expects($this->once()) 74 | ->method('info'); 75 | 76 | try { 77 | $runner->run($process, new \SplObjectStorage(), false); 78 | $this->fail('An exception should have been raised'); 79 | } catch (ExecutionFailureException $e) { 80 | 81 | } 82 | } 83 | 84 | public function testRunFailingProcessWithException() 85 | { 86 | $logger = $this->createLoggerMock(); 87 | $runner = $this->getProcessRunner($logger); 88 | 89 | $exception = new ProcessRuntimeException('Process Failed'); 90 | $process = $this->getMockBuilder('Symfony\Component\Process\Process') 91 | ->disableOriginalConstructor() 92 | ->getMock(); 93 | $process->expects($this->once()) 94 | ->method('run') 95 | ->will($this->throwException($exception)); 96 | 97 | $logger 98 | ->expects($this->once()) 99 | ->method('error'); 100 | $logger 101 | ->expects($this->once()) 102 | ->method('info'); 103 | 104 | try { 105 | $runner->run($process, new \SplObjectStorage(), false); 106 | $this->fail('An exception should have been raised'); 107 | } catch (ExecutionFailureException $e) { 108 | $this->assertEquals($exception, $e->getPrevious()); 109 | } 110 | } 111 | 112 | public function testRunfailingProcessBypassingErrors() 113 | { 114 | $logger = $this->createLoggerMock(); 115 | $runner = $this->getProcessRunner($logger); 116 | 117 | $process = $this->createProcessMock(1, false, '--helloworld--', 'Hello output', null, true); 118 | 119 | $logger 120 | ->expects($this->once()) 121 | ->method('error'); 122 | $logger 123 | ->expects($this->once()) 124 | ->method('info'); 125 | 126 | $this->assertNull($runner->run($process, new \SplObjectStorage(), true)); 127 | } 128 | 129 | public function testRunFailingProcessWithExceptionBypassingErrors() 130 | { 131 | $logger = $this->createLoggerMock(); 132 | $runner = $this->getProcessRunner($logger); 133 | 134 | $exception = new ProcessRuntimeException('Process Failed'); 135 | $process = $this->getMockBuilder('Symfony\Component\Process\Process') 136 | ->disableOriginalConstructor() 137 | ->getMock(); 138 | $process->expects($this->once()) 139 | ->method('run') 140 | ->will($this->throwException($exception)); 141 | 142 | $logger 143 | ->expects($this->once()) 144 | ->method('error'); 145 | $logger 146 | ->expects($this->once()) 147 | ->method('info'); 148 | 149 | $this->assertNull($runner->run($process, new \SplObjectStorage(), true)); 150 | } 151 | 152 | public function testRunSuccessFullProcessWithHandlers() 153 | { 154 | $logger = $this->createLoggerMock(); 155 | $runner = $this->getProcessRunner($logger); 156 | 157 | $capturedCallback = null; 158 | 159 | $process = $this->createProcessMock(1, true, '--helloworld--', "Kikoo Romain", null, true); 160 | $process->expects($this->once()) 161 | ->method('run') 162 | ->with($this->isInstanceOf('Closure')) 163 | ->will($this->returnCallback(function ($callback) use (&$capturedCallback) { 164 | $capturedCallback = $callback; 165 | })); 166 | 167 | $logger 168 | ->expects($this->never()) 169 | ->method('error'); 170 | $logger 171 | ->expects($this->exactly(2)) 172 | ->method('info'); 173 | 174 | $listener = new TestListener(); 175 | $storage = new \SplObjectStorage(); 176 | $storage->attach($listener); 177 | 178 | $capturedType = $capturedData = null; 179 | 180 | $listener->on('received', function ($type, $data) use (&$capturedType, &$capturedData) { 181 | $capturedData = $data; 182 | $capturedType = $type; 183 | }); 184 | 185 | $this->assertEquals('Kikoo Romain', $runner->run($process, $storage, false)); 186 | 187 | $type = 'err'; 188 | $data = 'data'; 189 | 190 | $capturedCallback($type, $data); 191 | 192 | $this->assertEquals($data, $capturedData); 193 | $this->assertEquals($type, $capturedType); 194 | } 195 | } 196 | 197 | class TestListener extends EventEmitter implements ListenerInterface 198 | { 199 | public function handle($type, $data) 200 | { 201 | return $this->emit('received', array($type, $data)); 202 | } 203 | 204 | public function forwardedEvents() 205 | { 206 | return array(); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | add('Alchemy\Tests', __DIR__); 5 | --------------------------------------------------------------------------------