├── .gitignore ├── tests └── ParallelScenarioExtension │ ├── ScenarioInfo │ ├── Fixtures │ │ └── ScenarioInfoExtractor │ │ │ ├── single_scenario │ │ │ ├── test.feature │ │ │ └── expected.json │ │ │ ├── not_parallel_parallel │ │ │ ├── test.feature │ │ │ └── expected.json │ │ │ ├── parallel_not_parallel │ │ │ ├── test.feature │ │ │ └── expected.json │ │ │ ├── parallel_parallel │ │ │ ├── test.feature │ │ │ └── expected.json │ │ │ ├── parallel_parallel_wait │ │ │ ├── test.feature │ │ │ └── expected.json │ │ │ ├── parallel_wait_parallel │ │ │ ├── test.feature │ │ │ └── expected.json │ │ │ ├── parallel_parallel_examples │ │ │ ├── expected.json │ │ │ └── test.feature │ │ │ ├── parallel_parallel_examples_split │ │ │ ├── test.feature │ │ │ └── expected.json │ │ │ └── parallel_wait_parallel_examples_split │ │ │ ├── test.feature │ │ │ └── expected.json │ ├── ScenarioInfoTest.php │ └── ScenarioInfoExtractorTest.php │ ├── Listener │ ├── StopOnFailureTest.php │ └── OutputPrinterTest.php │ ├── ScenarioProcess │ ├── ScenarioProcessProfileBalanceTest.php │ ├── ScenarioProcessTest.php │ └── ScenarioProcessFactoryTest.php │ ├── Feature │ └── FeatureRunnerTest.php │ └── Cli │ └── ParallelScenarioControllerTest.php ├── src └── ParallelScenarioExtension │ ├── ScenarioProcess │ ├── Option │ │ ├── ProcessOptionInterface.php │ │ ├── ProcessOption.php │ │ ├── ProcessOptionArray.php │ │ ├── ProcessOptionScalar.php │ │ ├── ProcessOptionCollection.php │ │ └── ProcessOptionOut.php │ ├── ScenarioProcessProfileBalance.php │ ├── ScenarioProcess.php │ └── ScenarioProcessFactory.php │ ├── Event │ └── ParallelScenarioEventType.php │ ├── ScenarioInfo │ ├── ScenarioInfo.php │ └── ScenarioInfoExtractor.php │ ├── Listener │ ├── StopOnFailure.php │ └── OutputPrinter.php │ ├── Feature │ ├── FeatureExtractor.php │ └── FeatureRunner.php │ ├── Cli │ └── ParallelScenarioController.php │ └── ServiceContainer │ └── ParallelScenarioExtension.php ├── phpunit.xml.dist ├── composer.json ├── README.md └── composer.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /vendor/ -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioInfo/Fixtures/ScenarioInfoExtractor/single_scenario/test.feature: -------------------------------------------------------------------------------- 1 | Feature: Test 2 | 3 | @parallel 4 | Scenario: first -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioInfo/Fixtures/ScenarioInfoExtractor/single_scenario/expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | { 4 | "line": 4, 5 | "file": "test.feature" 6 | } 7 | ] 8 | ] -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioInfo/Fixtures/ScenarioInfoExtractor/not_parallel_parallel/test.feature: -------------------------------------------------------------------------------- 1 | Feature: Test 2 | 3 | 4 | Scenario: first 5 | 6 | @parallel-scenario 7 | Scenario: second 8 | -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioInfo/Fixtures/ScenarioInfoExtractor/parallel_not_parallel/test.feature: -------------------------------------------------------------------------------- 1 | Feature: Test 2 | 3 | @parallel-scenario 4 | Scenario: first 5 | 6 | @parallel 7 | Scenario: second 8 | -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioInfo/Fixtures/ScenarioInfoExtractor/parallel_parallel/test.feature: -------------------------------------------------------------------------------- 1 | Feature: Test 2 | 3 | @parallel-scenario 4 | Scenario: first 5 | 6 | @parallel-scenario 7 | Scenario: second 8 | -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioInfo/Fixtures/ScenarioInfoExtractor/parallel_parallel_wait/test.feature: -------------------------------------------------------------------------------- 1 | Feature: Test 2 | 3 | @parallel-scenario 4 | Scenario: first 5 | 6 | @parallel-scenario @parallel-wait 7 | Scenario: second 8 | -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioInfo/Fixtures/ScenarioInfoExtractor/parallel_wait_parallel/test.feature: -------------------------------------------------------------------------------- 1 | Feature: Test 2 | 3 | @parallel-scenario @parallel-wait 4 | Scenario: first 5 | 6 | @parallel-scenario 7 | Scenario: second 8 | -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioInfo/Fixtures/ScenarioInfoExtractor/parallel_parallel/expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | { 4 | "line": 4, 5 | "file": "test.feature" 6 | }, 7 | { 8 | "line": 7, 9 | "file": "test.feature" 10 | } 11 | ] 12 | ] -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioInfo/Fixtures/ScenarioInfoExtractor/parallel_wait_parallel/expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | { 4 | "line": 4, 5 | "file": "test.feature" 6 | }, 7 | { 8 | "line": 7, 9 | "file": "test.feature" 10 | } 11 | ] 12 | ] -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioInfo/Fixtures/ScenarioInfoExtractor/parallel_parallel_examples/expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | { 4 | "line": 4, 5 | "file": "test.feature" 6 | }, 7 | { 8 | "line": 7, 9 | "file": "test.feature" 10 | } 11 | ] 12 | ] -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioInfo/Fixtures/ScenarioInfoExtractor/not_parallel_parallel/expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | { 4 | "line": 4, 5 | "file": "test.feature" 6 | } 7 | ], 8 | [ 9 | { 10 | "line": 7, 11 | "file": "test.feature" 12 | } 13 | ] 14 | ] -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioInfo/Fixtures/ScenarioInfoExtractor/parallel_not_parallel/expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | { 4 | "line": 4, 5 | "file": "test.feature" 6 | } 7 | ], 8 | [ 9 | { 10 | "line": 7, 11 | "file": "test.feature" 12 | } 13 | ] 14 | ] -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioInfo/Fixtures/ScenarioInfoExtractor/parallel_parallel_wait/expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | { 4 | "line": 4, 5 | "file": "test.feature" 6 | } 7 | ], 8 | [ 9 | { 10 | "line": 7, 11 | "file": "test.feature" 12 | } 13 | ] 14 | ] -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioInfo/Fixtures/ScenarioInfoExtractor/parallel_parallel_examples/test.feature: -------------------------------------------------------------------------------- 1 | Feature: Test 2 | 3 | @parallel-scenario 4 | Scenario: first 5 | 6 | @parallel-scenario 7 | Scenario Outline: second 8 | Examples: 9 | | parameter | 10 | | value1 | 11 | | value2 | 12 | -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioInfo/Fixtures/ScenarioInfoExtractor/parallel_parallel_examples_split/test.feature: -------------------------------------------------------------------------------- 1 | Feature: Test 2 | 3 | @parallel-scenario 4 | Scenario: first 5 | 6 | @parallel-examples 7 | Scenario Outline: second 8 | Examples: 9 | | parameter | 10 | | value1 | 11 | | value2 | 12 | -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioInfo/Fixtures/ScenarioInfoExtractor/parallel_wait_parallel_examples_split/test.feature: -------------------------------------------------------------------------------- 1 | Feature: Test 2 | 3 | @parallel-scenario 4 | Scenario: first 5 | 6 | @parallel-examples @parallel-wait 7 | Scenario Outline: second 8 | Examples: 9 | | parameter | 10 | | value1 | 11 | | value2 | 12 | -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioInfo/Fixtures/ScenarioInfoExtractor/parallel_parallel_examples_split/expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | { 4 | "line": 4, 5 | "file": "test.feature" 6 | }, 7 | { 8 | "line": 10, 9 | "file": "test.feature" 10 | }, 11 | { 12 | "line": 11, 13 | "file": "test.feature" 14 | } 15 | ] 16 | ] -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioInfo/Fixtures/ScenarioInfoExtractor/parallel_wait_parallel_examples_split/expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | { 4 | "line": 4, 5 | "file": "test.feature" 6 | } 7 | ], 8 | [ 9 | { 10 | "line": 10, 11 | "file": "test.feature" 12 | }, 13 | { 14 | "line": 11, 15 | "file": "test.feature" 16 | } 17 | ] 18 | ] -------------------------------------------------------------------------------- /src/ParallelScenarioExtension/ScenarioProcess/Option/ProcessOptionInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface ProcessOptionInterface 11 | { 12 | /** 13 | * @return string 14 | */ 15 | public function __toString(); 16 | 17 | /** 18 | * @return string 19 | */ 20 | public function getOptionName(); 21 | } 22 | -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioInfo/ScenarioInfoTest.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ScenarioInfoTest extends \PHPUnit_Framework_TestCase 11 | { 12 | /** 13 | * @see ScenarioInfo::__toString 14 | */ 15 | public function testToString() 16 | { 17 | $scenarioInfo = new ScenarioInfo('file', '80'); 18 | $this->assertEquals('file:80', (string) $scenarioInfo); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/ParallelScenarioExtension/Event/ParallelScenarioEventType.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class ParallelScenarioEventType 13 | { 14 | const FEATURE_TESTED_BEFORE = 'parallel_scenario.feature_tested.before'; 15 | const FEATURE_TESTED_AFTER = 'parallel_scenario.feature_tested.after'; 16 | 17 | const PROCESS_BEFORE_START = ParallelProcessRunnerEventType::PROCESS_START_BEFORE; 18 | const PROCESS_AFTER_STOP = ParallelProcessRunnerEventType::PROCESS_STOP_AFTER; 19 | const PROCESS_OUT = ParallelProcessRunnerEventType::PROCESS_OUT; 20 | } 21 | -------------------------------------------------------------------------------- /src/ParallelScenarioExtension/ScenarioInfo/ScenarioInfo.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ScenarioInfo 11 | { 12 | /** 13 | * @var string 14 | */ 15 | private $file; 16 | /** 17 | * @var int 18 | */ 19 | private $line; 20 | 21 | /** 22 | * ScenarioInfo constructor. 23 | * 24 | * @param string $file 25 | * @param int $line 26 | */ 27 | public function __construct($file, $line) 28 | { 29 | $this->file = $file; 30 | $this->line = $line; 31 | } 32 | 33 | /** 34 | * @return string 35 | */ 36 | public function __toString() 37 | { 38 | return sprintf('%s:%d', $this->file, $this->line); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ParallelScenarioExtension/ScenarioProcess/Option/ProcessOption.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ProcessOption implements ProcessOptionInterface 11 | { 12 | /** 13 | * @var string 14 | */ 15 | private $optionName; 16 | 17 | /** 18 | * ProcessOption constructor. 19 | * 20 | * @param string $optionName 21 | */ 22 | public function __construct($optionName) 23 | { 24 | $this->optionName = $optionName; 25 | } 26 | 27 | /** 28 | * @return string 29 | */ 30 | public function getOptionName() 31 | { 32 | return $this->optionName; 33 | } 34 | 35 | /** 36 | * @return string 37 | */ 38 | public function __toString() 39 | { 40 | return sprintf('--%s', $this->getOptionName()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tonicforhealth/behat-parallel-scenario", 3 | "description": "Behat parallel scenario", 4 | "keywords": ["bdd", "behat", "parallel", "process", "testing"], 5 | "authors": [ 6 | { 7 | "name": "oleksii.storozhylov", 8 | "email": "oleksii.storozhylov@tonicforhealth.com" 9 | } 10 | ], 11 | 12 | "autoload": { 13 | "psr-4": { "Tonic\\Behat\\": "src/" } 14 | }, 15 | 16 | "require": { 17 | "behat/behat" : "3.*", 18 | "tonicforhealth/parallel-process-runner" : "1.*" 19 | }, 20 | 21 | "require-dev": { 22 | "phpunit/phpunit": "4.1.*", 23 | "symfony/finder": "3.*" 24 | }, 25 | 26 | "repositories": [ 27 | { 28 | "type": "vcs", 29 | "url": "https://github.com/brandonroberts/Mailinator" 30 | }, 31 | { 32 | "type": "vcs", 33 | "url": "https://github.com/tonicforhealth/MinkExtension" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/ParallelScenarioExtension/ScenarioProcess/Option/ProcessOptionArray.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ProcessOptionArray extends ProcessOption 11 | { 12 | /** 13 | * @var array 14 | */ 15 | protected $optionValues = []; 16 | 17 | /** 18 | * ProcessOptionArray constructor. 19 | * 20 | * @param string $optionName 21 | * @param array $optionValues 22 | */ 23 | public function __construct($optionName, array $optionValues) 24 | { 25 | $this->optionValues = $optionValues; 26 | parent::__construct($optionName); 27 | } 28 | 29 | /** 30 | * @return string 31 | */ 32 | public function __toString() 33 | { 34 | $options = array_map(function ($optionValue) { 35 | return sprintf('%s %s', parent::__toString(), escapeshellarg($optionValue)); 36 | }, $this->optionValues); 37 | 38 | return implode(' ', $options); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ParallelScenarioExtension/ScenarioProcess/Option/ProcessOptionScalar.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ProcessOptionScalar extends ProcessOption 11 | { 12 | /** 13 | * @var string|int 14 | */ 15 | private $optionValue; 16 | 17 | /** 18 | * ProcessOptionString constructor. 19 | * 20 | * @param string $optionName 21 | * @param mixed $optionValue 22 | */ 23 | public function __construct($optionName, $optionValue) 24 | { 25 | $this->optionValue = $optionValue; 26 | parent::__construct($optionName); 27 | } 28 | 29 | /** 30 | * @return int|string 31 | */ 32 | public function getOptionValue() 33 | { 34 | return $this->optionValue; 35 | } 36 | 37 | /** 38 | * @return string 39 | */ 40 | public function __toString() 41 | { 42 | return sprintf('%s %s', parent::__toString(), escapeshellarg($this->getOptionValue())); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Install 2 | 3 | Install via composer 4 | ```bash 5 | $ composer require tonicforhealth/behat-parallel-scenario 6 | ``` 7 | # Config 8 | 9 | #### Load extenstion into config: 10 | ```yml 11 | default: 12 | extensions: 13 | Tonic\Behat\ParallelScenarioExtension: ~ 14 | ``` 15 | #### or 16 | ```yml 17 | default: 18 | extensions: 19 | Tonic\Behat\ParallelScenarioExtension: 20 | profiles: 21 | - profile_name_for_worker_1 22 | - profile_name_for_worker_2 23 | - profile_name_for_worker_3 24 | options: 25 | skip: 26 | - any-behat-option-for-skiping-in-worker 27 | ``` 28 | #### Mark scenarios with tags 29 | * run scenario in parallel 30 | 31 | ``` 32 | @parallel-scenario 33 | ``` 34 | 35 | * wait all parallel scenarios are done 36 | 37 | ``` 38 | @parallel-wait 39 | ``` 40 | * run examples in parallel 41 | 42 | ``` 43 | @parallel-examples 44 | ``` 45 | # Run 46 | ```bash 47 | $ bin/behat --parallel-process 2 48 | ``` 49 | When parameter is absent or equal to 1 then test will be run in usual mode 50 | 51 | [![Code Climate](https://codeclimate.com/github/tonicforhealth/behat-parallel-scenario/badges/gpa.svg)](https://codeclimate.com/github/tonicforhealth/behat-parallel-scenario) 52 | -------------------------------------------------------------------------------- /src/ParallelScenarioExtension/ScenarioProcess/Option/ProcessOptionCollection.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ProcessOptionCollection 11 | { 12 | /** 13 | * @var ProcessOptionInterface[] 14 | */ 15 | private $options = []; 16 | 17 | /** 18 | * @param ProcessOptionInterface $option 19 | */ 20 | public function set(ProcessOptionInterface $option) 21 | { 22 | $this->options[$option->getOptionName()] = $option; 23 | } 24 | 25 | /** 26 | * @param string $optionName 27 | * 28 | * @return null|ProcessOptionInterface 29 | */ 30 | public function get($optionName) 31 | { 32 | return array_key_exists($optionName, $this->options) ? $this->options[$optionName] : null; 33 | } 34 | 35 | /** 36 | * @return ProcessOptionInterface[] 37 | */ 38 | public function toArray() 39 | { 40 | return $this->options; 41 | } 42 | 43 | /** 44 | * @return string 45 | */ 46 | public function __toString() 47 | { 48 | return implode(' ', array_map(function (ProcessOptionInterface $option) { 49 | return (string) $option; 50 | }, $this->options)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ParallelScenarioExtension/ScenarioProcess/Option/ProcessOptionOut.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ProcessOptionOut extends ProcessOptionArray 11 | { 12 | /** 13 | * @var string 14 | */ 15 | private $outSuffix; 16 | 17 | /** 18 | * @param string $outSuffix 19 | */ 20 | public function setOutSuffix($outSuffix) 21 | { 22 | $this->outSuffix = $outSuffix; 23 | } 24 | 25 | /** 26 | * @return string 27 | */ 28 | public function __toString() 29 | { 30 | $options = array_map(function ($optionValue) { 31 | if (!$this->isStandardOutput($optionValue)) { 32 | $optionValue = sprintf('%s/%s', $optionValue, $this->outSuffix); 33 | } 34 | 35 | return sprintf('--%s %s', $this->getOptionName(), escapeshellarg($optionValue)); 36 | }, $this->optionValues); 37 | 38 | return implode(' ', $options); 39 | } 40 | 41 | /** 42 | * Checks if provided output identifier represents standard output. 43 | * 44 | * @param string $outputId 45 | * 46 | * @see \Behat\Testwork\Output\Cli\OutputController::isStandardOutput 47 | * 48 | * @return Boolean 49 | */ 50 | private function isStandardOutput($outputId) 51 | { 52 | return in_array($outputId, [ 53 | 'std', 54 | 'null', 55 | 'false', 56 | ]); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/ParallelScenarioExtension/Listener/StopOnFailure.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class StopOnFailure implements EventSubscriberInterface 17 | { 18 | /** 19 | * @var ParallelProcessRunner 20 | */ 21 | private $parallelProcessRunner; 22 | 23 | /** 24 | * StopOnFailureListener constructor. 25 | * 26 | * @param ParallelProcessRunner $processRunner 27 | */ 28 | public function __construct(ParallelProcessRunner $processRunner) 29 | { 30 | $this->parallelProcessRunner = $processRunner; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public static function getSubscribedEvents() 37 | { 38 | return [ 39 | ParallelScenarioEventType::PROCESS_AFTER_STOP => 'stopOnFailure', 40 | ]; 41 | } 42 | 43 | /** 44 | * @param ProcessEvent $event 45 | */ 46 | public function stopOnFailure(ProcessEvent $event) 47 | { 48 | /** @var ScenarioProcess $process */ 49 | $process = $event->getProcess(); 50 | if ($process->withError()) { 51 | $this->parallelProcessRunner->stop(); 52 | $this->terminate(1); 53 | } 54 | } 55 | 56 | /** 57 | * @param int $code 58 | * 59 | * @codeCoverageIgnore 60 | */ 61 | protected function terminate($code) 62 | { 63 | exit($code); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/ParallelScenarioExtension/Listener/OutputPrinter.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class OutputPrinter implements EventSubscriberInterface 17 | { 18 | /** 19 | * @var OutputInterface 20 | */ 21 | private $output; 22 | 23 | /** 24 | * @param OutputInterface $output 25 | */ 26 | public function init(OutputInterface $output) 27 | { 28 | $this->output = $output; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public static function getSubscribedEvents() 35 | { 36 | return [ 37 | ParallelScenarioEventType::PROCESS_BEFORE_START => 'beforeStart', 38 | ParallelScenarioEventType::PROCESS_AFTER_STOP => 'afterStop', 39 | ]; 40 | } 41 | 42 | /** 43 | * @param ProcessEvent $event 44 | */ 45 | public function beforeStart(ProcessEvent $event) 46 | { 47 | $this->output->writeln(sprintf('START ::: %s', $event->getProcess()->getCommandLine())); 48 | } 49 | 50 | /** 51 | * @param ProcessEvent $event 52 | */ 53 | public function afterStop(ProcessEvent $event) 54 | { 55 | /** @var ScenarioProcess $process */ 56 | $process = $event->getProcess(); 57 | if ($process->withError()) { 58 | $this->output->writeln(sprintf('%s', $process->getOutput())); 59 | $this->output->writeln(sprintf('%s', $process->getErrorOutput())); 60 | } else { 61 | $this->output->writeln(sprintf('%s', $process->getOutput())); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/ParallelScenarioExtension/Feature/FeatureExtractor.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class FeatureExtractor 17 | { 18 | /** 19 | * @var SuiteRepository 20 | */ 21 | private $suiteRepository; 22 | 23 | /** 24 | * @var SpecificationFinder 25 | */ 26 | private $specificationFinder; 27 | 28 | /** 29 | * FeatureExtractor constructor. 30 | * 31 | * @param SuiteRepository $suiteRepository 32 | * @param SpecificationFinder $specificationFinder 33 | */ 34 | public function __construct(SuiteRepository $suiteRepository, SpecificationFinder $specificationFinder) 35 | { 36 | $this->suiteRepository = $suiteRepository; 37 | $this->specificationFinder = $specificationFinder; 38 | } 39 | 40 | /** 41 | * @param string $locator 42 | * 43 | * @return FeatureNode[] 44 | */ 45 | public function extract($locator) 46 | { 47 | $features = []; 48 | 49 | $specifications = $this->findSuitesSpecifications($locator); 50 | 51 | foreach (GroupedSpecificationIterator::group($specifications) as $iterator) { 52 | foreach ($iterator as $featureNode) { 53 | $features[] = $featureNode; 54 | } 55 | } 56 | 57 | return $features; 58 | } 59 | 60 | /** 61 | * Finds specification iterators for all provided suites using locator. 62 | * 63 | * @param null|string $locator 64 | * 65 | * @return SpecificationIterator[] 66 | */ 67 | private function findSuitesSpecifications($locator) 68 | { 69 | return $this->specificationFinder->findSuitesSpecifications( 70 | $this->suiteRepository->getSuites(), 71 | $locator 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/ParallelScenarioExtension/ScenarioProcess/ScenarioProcessProfileBalance.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class ScenarioProcessProfileBalance implements EventSubscriberInterface 16 | { 17 | /** 18 | * @var array 19 | */ 20 | protected $balance = []; 21 | 22 | /** 23 | * @param array $profiles 24 | */ 25 | public function __construct(array $profiles) 26 | { 27 | $this->balance = array_fill_keys($profiles, 0); 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public static function getSubscribedEvents() 34 | { 35 | return [ 36 | ParallelScenarioEventType::PROCESS_BEFORE_START => 'increment', 37 | ParallelScenarioEventType::PROCESS_AFTER_STOP => 'decrement', 38 | ]; 39 | } 40 | 41 | /** 42 | * @param ProcessEvent $event 43 | */ 44 | public function increment(ProcessEvent $event) 45 | { 46 | if ($this->balance) { 47 | $profile = $this->getProfileNameWithMinimumBalance(); 48 | $this->balance[$profile]++; 49 | /** @var ScenarioProcess $process */ 50 | $process = $event->getProcess(); 51 | $process->setProcessOption(new ProcessOptionScalar('profile', $profile)); 52 | } 53 | } 54 | 55 | /** 56 | * @param ProcessEvent $event 57 | */ 58 | public function decrement(ProcessEvent $event) 59 | { 60 | if ($this->balance) { 61 | /** @var ScenarioProcess $process */ 62 | $process = $event->getProcess(); 63 | /** @var ProcessOptionScalar $profileOption */ 64 | $profileOption = $process->getProcessOption('profile'); 65 | $this->balance[$profileOption->getOptionValue()]--; 66 | } 67 | } 68 | 69 | /** 70 | * @return string 71 | */ 72 | protected function getProfileNameWithMinimumBalance() 73 | { 74 | return array_keys($this->balance, min($this->balance))[0]; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/Listener/StopOnFailureTest.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class StopOnFailureTest extends \PHPUnit_Framework_TestCase 16 | { 17 | /** 18 | * @see StopOnFailure::getSubscribedEvents 19 | */ 20 | public function testGetSubscribedEvents() 21 | { 22 | $this->assertEquals([ 23 | ParallelScenarioEventType::PROCESS_AFTER_STOP => 'stopOnFailure', 24 | ], StopOnFailure::getSubscribedEvents()); 25 | } 26 | 27 | /** 28 | * @see StopOnFailure::stopOnFailure 29 | */ 30 | public function testStopOnFailureWithError() 31 | { 32 | $parallelProcessRunner = $this->getMock(ParallelProcessRunner::class, ['stop']); 33 | $parallelProcessRunner->expects($this->once())->method('stop'); 34 | 35 | $process = $this->getMock(ScenarioProcess::class, ['withError'], [], '', false); 36 | $process->expects($this->once())->method('withError')->willReturn(true); 37 | 38 | $event = $this->getMock(ProcessEvent::class, null, [$process]); 39 | 40 | $listener = $this->getMock(StopOnFailure::class, ['terminate'], [$parallelProcessRunner]); 41 | $listener->expects($this->once())->method('terminate')->with(1); 42 | 43 | /** @var StopOnFailure $listener */ 44 | /** @var ProcessEvent $event */ 45 | $listener->stopOnFailure($event); 46 | } 47 | 48 | /** 49 | * @see StopOnFailure::stopOnFailure 50 | */ 51 | public function testStopOnFailureWithoutError() 52 | { 53 | $parallelProcessRunner = $this->getMock(ParallelProcessRunner::class, ['stop']); 54 | $parallelProcessRunner->expects($this->never())->method('stop'); 55 | 56 | $process = $this->getMock(ScenarioProcess::class, ['withError'], [], '', false); 57 | $process->expects($this->once())->method('withError')->willReturn(false); 58 | 59 | $event = $this->getMock(ProcessEvent::class, null, [$process]); 60 | 61 | $listener = $this->getMock(StopOnFailure::class, ['terminate'], [$parallelProcessRunner]); 62 | $listener->expects($this->never())->method('terminate'); 63 | 64 | /** @var StopOnFailure $listener */ 65 | /** @var ProcessEvent $event */ 66 | $listener->stopOnFailure($event); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioInfo/ScenarioInfoExtractorTest.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class ScenarioInfoExtractorTest extends \PHPUnit_Framework_TestCase 18 | { 19 | /** 20 | * @return array 21 | */ 22 | public function provider() 23 | { 24 | $cases = []; 25 | $parser = $this->getParser(); 26 | $finder = new Finder(); 27 | foreach ($finder->in(sprintf('%s/Fixtures/ScenarioInfoExtractor', __DIR__))->directories() as $directory) { 28 | /** @var SplFileInfo $directory */ 29 | $featureFile = sprintf('%s/test.feature', $directory->getPathname()); 30 | $expectedFile = sprintf('%s/expected.json', $directory->getPathname()); 31 | 32 | $cases[$directory->getBasename()] = [ 33 | $parser->parse(file_get_contents($featureFile), $featureFile), 34 | $this->getScenarioInfo(json_decode(file_get_contents($expectedFile), true), $directory->getPathname()), 35 | ]; 36 | } 37 | 38 | return $cases; 39 | } 40 | 41 | /** 42 | * @param FeatureNode $feature 43 | * @param array $expected 44 | * 45 | * @dataProvider provider 46 | */ 47 | public function test(FeatureNode $feature, array $expected) 48 | { 49 | $extractor = new ScenarioInfoExtractor(); 50 | $result = $extractor->extract($feature); 51 | 52 | $this->assertEquals($expected, $result); 53 | } 54 | 55 | private function getScenarioInfo(array $data, $path) 56 | { 57 | foreach ($data as &$group) { 58 | $group = array_map(function ($data) use ($path) { 59 | return new ScenarioInfo(sprintf('%s/%s', $path, $data['file']), $data['line']); 60 | }, $group); 61 | } 62 | 63 | return $data; 64 | } 65 | 66 | /** 67 | * @return Parser 68 | */ 69 | private function getParser() 70 | { 71 | $keywords = [ 72 | 'en' => [ 73 | 'and' => 'And', 74 | 'background' => 'Background', 75 | 'but' => 'But', 76 | 'examples' => 'Examples|Scenarios', 77 | 'feature' => 'Feature|Business Need|Ability', 78 | 'given' => 'Given', 79 | 'name' => 'English', 80 | 'native' => 'English', 81 | 'scenario' => 'Scenario', 82 | 'scenario_outline' => 'Scenario Outline|Scenario Template', 83 | 'then' => 'Then', 84 | 'when' => 'When', 85 | ], 86 | ]; 87 | 88 | return new Parser(new Lexer(new ArrayKeywords($keywords))); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/ParallelScenarioExtension/ScenarioInfo/ScenarioInfoExtractor.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class ScenarioInfoExtractor 15 | { 16 | const TAG_PARALLEL_SCENARIO = 'parallel-scenario'; 17 | const TAG_PARALLEL_WAIT = 'parallel-wait'; 18 | const TAG_PARALLEL_EXAMPLES = 'parallel-examples'; 19 | 20 | /** 21 | * @param FeatureNode $feature 22 | * 23 | * @return array 24 | */ 25 | public function extract(FeatureNode $feature) 26 | { 27 | $allScenarios = []; 28 | 29 | foreach ($feature->getScenarios() as $scenario) { 30 | $scenarios = []; 31 | 32 | switch (true) { 33 | case $scenario instanceof OutlineNode && $this->isParallelExamples($scenario): 34 | foreach ($scenario->getExamples() as $exampleNode) { 35 | $scenarios[] = new ScenarioInfo($feature->getFile(), $exampleNode->getLine()); 36 | } 37 | break; 38 | case $scenario instanceof OutlineNode: 39 | case $scenario instanceof ScenarioInterface: 40 | $scenarios[] = new ScenarioInfo($feature->getFile(), $scenario->getLine()); 41 | } 42 | 43 | if ($this->isParallelWait($scenario) || !$this->isParallel($scenario)) { 44 | $allScenarios[] = []; 45 | } 46 | 47 | $lastIndex = empty($allScenarios) ? 0 : count($allScenarios) - 1; 48 | 49 | if (!array_key_exists($lastIndex, $allScenarios)) { 50 | $allScenarios[$lastIndex] = []; 51 | } 52 | 53 | $allScenarios[$lastIndex] = array_merge($allScenarios[$lastIndex], $scenarios); 54 | 55 | if (!$this->isParallel($scenario)) { 56 | $allScenarios[] = []; 57 | } 58 | } 59 | 60 | return array_values(array_filter($allScenarios)); 61 | } 62 | 63 | /** 64 | * @param ScenarioInterface $scenario 65 | * 66 | * @return bool 67 | */ 68 | private function isParallel(ScenarioInterface $scenario) 69 | { 70 | return in_array(self::TAG_PARALLEL_SCENARIO, $scenario->getTags()) || $this->isParallelExamples($scenario); 71 | } 72 | 73 | /** 74 | * @param ScenarioInterface $scenario 75 | * 76 | * @return bool 77 | */ 78 | private function isParallelWait(ScenarioInterface $scenario) 79 | { 80 | return in_array(self::TAG_PARALLEL_WAIT, $scenario->getTags()); 81 | } 82 | 83 | /** 84 | * @param ScenarioInterface $scenario 85 | * 86 | * @return bool 87 | */ 88 | private function isParallelExamples(ScenarioInterface $scenario) 89 | { 90 | return $scenario instanceof OutlineNode && in_array(self::TAG_PARALLEL_EXAMPLES, $scenario->getTags()); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/ParallelScenarioExtension/Cli/ParallelScenarioController.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class ParallelScenarioController implements Controller 22 | { 23 | const OPTION_PARALLEL_PROCESS = 'parallel-process'; 24 | 25 | /** 26 | * @var FeatureRunner 27 | */ 28 | private $featureRunner; 29 | /** 30 | * @var FeatureExtractor 31 | */ 32 | private $featureExtractor; 33 | /** 34 | * @var ScenarioProcessFactory 35 | */ 36 | private $processFactory; 37 | /** 38 | * @var OutputPrinter 39 | */ 40 | private $outputPrinter; 41 | /** 42 | * @var InputDefinition 43 | */ 44 | private $inputDefinition; 45 | 46 | /** 47 | * ParallelScenarioController constructor. 48 | * 49 | * @param FeatureRunner $featureRunner 50 | * @param FeatureExtractor $featureExtractor 51 | * @param ScenarioProcessFactory $processFactory 52 | * @param OutputPrinter $outputPrinter 53 | */ 54 | public function __construct(FeatureRunner $featureRunner, FeatureExtractor $featureExtractor, ScenarioProcessFactory $processFactory, OutputPrinter $outputPrinter) 55 | { 56 | $this->featureRunner = $featureRunner; 57 | $this->featureExtractor = $featureExtractor; 58 | $this->processFactory = $processFactory; 59 | $this->outputPrinter = $outputPrinter; 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function configure(SymfonyCommand $command) 66 | { 67 | $command->addOption(self::OPTION_PARALLEL_PROCESS, null, InputOption::VALUE_OPTIONAL, 'Max parallel processes amount', 1); 68 | $this->inputDefinition = $command->getDefinition(); 69 | } 70 | 71 | /** 72 | * {@inheritdoc} 73 | */ 74 | public function execute(InputInterface $input, OutputInterface $output) 75 | { 76 | $result = null; 77 | 78 | $maxProcessesAmount = max(1, $input->getOption(self::OPTION_PARALLEL_PROCESS)); 79 | $locator = $input->getArgument('paths'); 80 | 81 | if ($maxProcessesAmount > 1) { 82 | $this->outputPrinter->init($output); 83 | $this->processFactory->init($this->inputDefinition, $input); 84 | $this->featureRunner->setMaxParallelProcess($maxProcessesAmount); 85 | 86 | $result = 0; 87 | 88 | foreach ($this->featureExtractor->extract($locator) as $featureNode) { 89 | $result = max($result, $this->featureRunner->run($featureNode)); 90 | } 91 | } 92 | 93 | return $result; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/ParallelScenarioExtension/ScenarioProcess/ScenarioProcess.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class ScenarioProcess extends Process 16 | { 17 | /** 18 | * @var ScenarioInfo 19 | */ 20 | private $scenarioInfo; 21 | /** 22 | * @var string 23 | */ 24 | private $commandLine; 25 | /** 26 | * @var ProcessOptionCollection 27 | */ 28 | private $optionCollection; 29 | 30 | /** 31 | * ScenarioProcess constructor. 32 | * 33 | * @param ScenarioInfo $scenarioInfo 34 | * @param null|string $commandline 35 | * @param null $cwd 36 | * @param array|null $env 37 | * @param null $input 38 | * @param int $timeout 39 | * @param array $options 40 | */ 41 | public function __construct(ScenarioInfo $scenarioInfo, $commandline, $cwd = null, array $env = null, $input = null, $timeout = 0, array $options = array()) 42 | { 43 | $this->scenarioInfo = $scenarioInfo; 44 | $this->commandLine = $commandline; 45 | $this->optionCollection = new ProcessOptionCollection(); 46 | parent::__construct($this->getCommandLineWithOptions(), $cwd, $env, $input, $timeout, $options); 47 | } 48 | 49 | /** 50 | * @param ProcessOptionInterface $option 51 | */ 52 | public function setProcessOption(ProcessOptionInterface $option) 53 | { 54 | $this->optionCollection->set($option); 55 | } 56 | 57 | /** 58 | * @param string $optionName 59 | * 60 | * @return null|ProcessOptionInterface 61 | */ 62 | public function getProcessOption($optionName) 63 | { 64 | return $this->optionCollection->get($optionName); 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function setCommandLine($commandline) 71 | { 72 | $this->commandLine = $commandline; 73 | } 74 | 75 | /** 76 | * @return bool 77 | */ 78 | public function withError() 79 | { 80 | return (bool) $this->getExitCode(); 81 | } 82 | 83 | /** 84 | * {@inheritdoc} 85 | */ 86 | public function start(callable $callback = null) 87 | { 88 | $this->updateCommandLine(); 89 | parent::start($callback); 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | public function getCommandLine() 96 | { 97 | $this->updateCommandLine(); 98 | 99 | return parent::getCommandLine(); 100 | } 101 | 102 | /** 103 | * {@inheritdoc} 104 | */ 105 | protected function getCommandLineWithOptions() 106 | { 107 | return implode(' ', array_filter([ 108 | $this->commandLine, 109 | (string) $this->optionCollection, 110 | ])); 111 | } 112 | 113 | protected function updateCommandLine() 114 | { 115 | parent::setCommandLine($this->getCommandLineWithOptions()); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/ParallelScenarioExtension/Feature/FeatureRunner.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class FeatureRunner 20 | { 21 | /** 22 | * @var EventDispatcherInterface 23 | */ 24 | private $eventDispatcher; 25 | /** 26 | * @var ScenarioInfoExtractor 27 | */ 28 | private $scenarioInfoExtractor; 29 | /** 30 | * @var ScenarioProcessFactory 31 | */ 32 | private $scenarioProcessFactory; 33 | /** 34 | * @var ParallelProcessRunner 35 | */ 36 | private $parallelProcessRunner; 37 | 38 | /** 39 | * FeatureRunner constructor. 40 | * 41 | * @param EventDispatcherInterface $eventDispatcher 42 | * @param ScenarioInfoExtractor $infoExtractor 43 | * @param ScenarioProcessFactory $processFactory 44 | * @param ParallelProcessRunner $processRunner 45 | */ 46 | public function __construct(EventDispatcherInterface $eventDispatcher, ScenarioInfoExtractor $infoExtractor, ScenarioProcessFactory $processFactory, ParallelProcessRunner $processRunner) 47 | { 48 | $this->eventDispatcher = $eventDispatcher; 49 | $this->scenarioInfoExtractor = $infoExtractor; 50 | $this->scenarioProcessFactory = $processFactory; 51 | $this->parallelProcessRunner = $processRunner; 52 | } 53 | 54 | /** 55 | * @param FeatureNode $featureNode 56 | * 57 | * @return int 58 | */ 59 | public function run(FeatureNode $featureNode) 60 | { 61 | $result = 0; 62 | 63 | $this->eventDispatcher->dispatch(ParallelScenarioEventType::FEATURE_TESTED_BEFORE); 64 | $scenarioGroups = $this->scenarioInfoExtractor->extract($featureNode); 65 | 66 | foreach ($scenarioGroups as $scenarios) { 67 | $result = max($result, $this->runScenarios($scenarios)); 68 | } 69 | $this->eventDispatcher->dispatch(ParallelScenarioEventType::FEATURE_TESTED_BEFORE); 70 | 71 | return $result; 72 | } 73 | 74 | /** 75 | * @param int $maxParallelProcess 76 | * 77 | * @return $this 78 | */ 79 | public function setMaxParallelProcess($maxParallelProcess) 80 | { 81 | $this->parallelProcessRunner->setMaxParallelProcess($maxParallelProcess); 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * @param ScenarioInfo[] $scenarios 88 | * 89 | * @return int 90 | */ 91 | protected function runScenarios(array $scenarios) 92 | { 93 | $result = 0; 94 | 95 | /** @var ScenarioProcess[] $processes */ 96 | $processes = array_map(function (ScenarioInfo $scenarioInfo) { 97 | return $this->scenarioProcessFactory->make($scenarioInfo); 98 | }, $scenarios); 99 | 100 | $this->parallelProcessRunner->reset()->add($processes)->run(); 101 | 102 | foreach ($processes as $process) { 103 | $result = max($result, $process->getExitCode()); 104 | } 105 | 106 | return $result; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/Listener/OutputPrinterTest.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class OutputPrinterTest extends \PHPUnit_Framework_TestCase 17 | { 18 | /** 19 | * @see OutputPrinter::getSubscribedEvents 20 | */ 21 | public function testGetSubscribedEvents() 22 | { 23 | $this->assertEquals([ 24 | ParallelScenarioEventType::PROCESS_BEFORE_START => 'beforeStart', 25 | ParallelScenarioEventType::PROCESS_AFTER_STOP => 'afterStop', 26 | ], OutputPrinter::getSubscribedEvents()); 27 | } 28 | 29 | /** 30 | * @see OutputPrinter::beforeStart 31 | */ 32 | public function testBeforeStart() 33 | { 34 | $output = $this->getMock(ConsoleOutput::class, ['writeln']); 35 | $output->expects($this->once())->method('writeln')->with('START ::: command'); 36 | 37 | $process = $this->getMock(ScenarioProcess::class, ['getCommandLine'], [], '', false); 38 | $process->expects($this->once())->method('getCommandLine')->willReturn('command'); 39 | 40 | $event = $this->getMock(ProcessEvent::class, null, [$process]); 41 | 42 | /** @var OutputInterface $output */ 43 | /** @var ProcessEvent $event */ 44 | $printer = new OutputPrinter(); 45 | $printer->init($output); 46 | 47 | $printer->beforeStart($event); 48 | } 49 | 50 | /** 51 | * @return array 52 | */ 53 | public function providerAfterStep() 54 | { 55 | return [ 56 | [ 57 | true, 58 | [ 59 | 'output', 60 | 'error.output', 61 | ], 62 | ], 63 | [ 64 | false, 65 | [ 66 | 'output', 67 | ], 68 | ], 69 | ]; 70 | } 71 | 72 | /** 73 | * @param bool $error 74 | * @param array $expected 75 | * 76 | * @see OutputPrinter::afterStop 77 | * 78 | * @dataProvider providerAfterStep 79 | */ 80 | public function testAfterStop($error, array $expected) 81 | { 82 | $output = $this->getMock(ConsoleOutput::class, ['writeln']); 83 | foreach ($expected as $index => $line) { 84 | $output->expects($this->at($index))->method('writeln')->with($line); 85 | } 86 | $output->expects($this->exactly(count($expected)))->method('writeln'); 87 | 88 | $process = $this->getMock(ScenarioProcess::class, ['withError', 'getOutput', 'getErrorOutput'], [], '', false); 89 | $process->expects($this->once())->method('withError')->willReturn($error); 90 | $process->expects($this->once())->method('getOutput')->willReturn('output'); 91 | if ($error) { 92 | $process->expects($this->once())->method('getErrorOutput')->willReturn('error.output'); 93 | } else { 94 | $process->expects($this->never())->method('getErrorOutput'); 95 | } 96 | 97 | $event = $this->getMock(ProcessEvent::class, null, [$process]); 98 | 99 | /** @var OutputInterface $output */ 100 | /** @var ProcessEvent $event */ 101 | $printer = new OutputPrinter(); 102 | $printer->init($output); 103 | 104 | $printer->afterStop($event); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioProcess/ScenarioProcessProfileBalanceTest.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class ScenarioProcessProfileBalanceTest extends \PHPUnit_Framework_TestCase 16 | { 17 | /** 18 | * @see ScenarioProcessProfileBalance::getSubscribedEvents 19 | */ 20 | public function testGetSubscribedEvents() 21 | { 22 | $this->assertEquals([ 23 | ParallelScenarioEventType::PROCESS_BEFORE_START => 'increment', 24 | ParallelScenarioEventType::PROCESS_AFTER_STOP => 'decrement', 25 | ], ScenarioProcessProfileBalance::getSubscribedEvents()); 26 | } 27 | 28 | /** 29 | * @see ScenarioProcessProfileBalance::increase 30 | * @see ScenarioProcessProfileBalance::decrease 31 | */ 32 | public function test() 33 | { 34 | $profiles = [ 35 | 'profile1', 36 | 'profile2', 37 | ]; 38 | 39 | $events = $this->getEvents(9); 40 | 41 | $balance = new ScenarioProcessProfileBalance($profiles); 42 | foreach ($events as $event) { 43 | $balance->increment($event); 44 | } 45 | 46 | $this->assertEquals([ 47 | 'profile1' => 5, 48 | 'profile2' => 4, 49 | ], $this->countProfilesFromEvents($events)); 50 | 51 | foreach ($this->filterEventsByProfile($events, 'profile1') as $event) { 52 | $balance->decrement($event); 53 | } 54 | 55 | $newEvents = $this->getEvents(5); 56 | foreach ($newEvents as $event) { 57 | $balance->increment($event); 58 | } 59 | $this->assertEquals([ 60 | 'profile1' => 5, 61 | ], $this->countProfilesFromEvents($newEvents)); 62 | } 63 | 64 | /** 65 | * @param ProcessEvent[] $events 66 | * @param string $profile 67 | * 68 | * @return ProcessEvent[] 69 | */ 70 | public function filterEventsByProfile(array $events, $profile) 71 | { 72 | return array_filter($events, function (ProcessEvent $event) use ($profile) { 73 | /** @var ScenarioProcess $process */ 74 | $process = $event->getProcess(); 75 | /** @var ProcessOptionScalar $profileOption */ 76 | $profileOption = $process->getProcessOption('profile'); 77 | 78 | return $profileOption->getOptionValue() == $profile; 79 | }); 80 | } 81 | 82 | /** 83 | * @param int $amount 84 | * 85 | * @return ProcessEvent[] 86 | */ 87 | private function getEvents($amount) 88 | { 89 | $events = []; 90 | while ($amount--) { 91 | $scenarioInfo = $this->getMock(ScenarioInfo::class, null, [], '', false); 92 | $process = $this->getMock(ScenarioProcess::class, null, [$scenarioInfo, (string) $scenarioInfo]); 93 | $event = $this->getMock(ProcessEvent::class, null, [$process]); 94 | 95 | $events[] = $event; 96 | } 97 | 98 | return $events; 99 | } 100 | 101 | /** 102 | * @param ProcessEvent[] $events 103 | * 104 | * @return array 105 | */ 106 | private function countProfilesFromEvents(array $events) 107 | { 108 | $profilesCount = []; 109 | foreach ($events as $event) { 110 | /** @var ScenarioProcess $process */ 111 | $process = $event->getProcess(); 112 | /** @var ProcessOptionScalar $profileOption */ 113 | $profileOption = $process->getProcessOption('profile'); 114 | @$profilesCount[$profileOption->getOptionValue()]++; 115 | } 116 | 117 | return $profilesCount; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioProcess/ScenarioProcessTest.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class ScenarioProcessTest extends \PHPUnit_Framework_TestCase 16 | { 17 | /** 18 | * @return array 19 | */ 20 | public function providerSetGetProcessOption() 21 | { 22 | return [ 23 | [ 24 | 'any', 25 | ], 26 | 27 | [ 28 | 'out', 29 | new ProcessOptionOut('out', ['std']), 30 | ], 31 | ]; 32 | } 33 | 34 | /** 35 | * @param string $name 36 | * @param ProcessOptionInterface|null $option 37 | * 38 | * @dataProvider providerSetGetProcessOption 39 | */ 40 | public function testSetGetProcessOption($name, ProcessOptionInterface $option = null) 41 | { 42 | $process = new ScenarioProcess(new ScenarioInfo('file', 0), ''); 43 | if ($option) { 44 | $process->setProcessOption($option); 45 | } 46 | 47 | $this->assertEquals($option, $process->getProcessOption($name)); 48 | } 49 | 50 | /** 51 | * @param string $commandLine 52 | */ 53 | public function testSetCommandLine($commandLine = 'test') 54 | { 55 | $process = new ScenarioProcess(new ScenarioInfo('file', 0), ''); 56 | $process->setCommandLine($commandLine); 57 | 58 | $this->assertEquals($commandLine, $process->getCommandLine()); 59 | } 60 | 61 | /** 62 | * @return array 63 | */ 64 | public function providerWithError() 65 | { 66 | return [ 67 | [true], 68 | [false], 69 | ]; 70 | } 71 | 72 | /** 73 | * @param bool $withError 74 | * 75 | * @dataProvider providerWithError 76 | */ 77 | public function testWithError($withError) 78 | { 79 | $process = $this->getMock(ScenarioProcess::class, ['getExitCode'], [], '', false); 80 | $process->expects($this->once())->method('getExitCode')->willReturn($withError); 81 | /** @var ScenarioProcess $process */ 82 | $this->assertEquals($withError, $process->withError()); 83 | } 84 | 85 | /** 86 | * @param string $optionName 87 | * @param string $optionValue 88 | */ 89 | public function testCommon($optionName = 'option', $optionValue = 'test') 90 | { 91 | $process = new ScenarioProcess(new ScenarioInfo('file', 0), 'cmd'); 92 | $process->setProcessOption(new ProcessOptionScalar($optionName, $optionValue)); 93 | 94 | $this->assertEquals( 95 | sprintf('cmd --%s %s', $optionName, escapeshellarg($optionValue)), 96 | $process->getCommandLine() 97 | ); 98 | 99 | $process->setProcessOption(new ProcessOptionScalar($optionName, $optionValue.$optionValue)); 100 | $this->assertEquals( 101 | sprintf('cmd --%s %s', $optionName, escapeshellarg($optionValue.$optionValue)), 102 | $process->getCommandLine() 103 | ); 104 | } 105 | 106 | /** 107 | * @return array 108 | */ 109 | public function providerUpdateCommandLineCall() 110 | { 111 | return [ 112 | ['run'], 113 | ['start'], 114 | ['getCommandLine'], 115 | ]; 116 | } 117 | 118 | /** 119 | * @param string $method 120 | * 121 | * @dataProvider providerUpdateCommandLineCall 122 | */ 123 | public function testUpdateCommandLineCall($method) 124 | { 125 | $command = sprintf('%s', PHP_BINARY); 126 | 127 | $process = $this->getMock(ScenarioProcess::class, ['updateCommandLine'], [new ScenarioInfo('file', 0), $command]); 128 | $process->expects($this->once())->method('updateCommandLine'); 129 | /** @var ScenarioProcess $process */ 130 | $process->$method(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/ParallelScenarioExtension/ScenarioProcess/ScenarioProcessFactory.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class ScenarioProcessFactory 22 | { 23 | /** 24 | * @var string 25 | */ 26 | private $behatBinaryPath; 27 | /** 28 | * @var array 29 | */ 30 | private $skipOptions = [ 31 | ParallelScenarioController::OPTION_PARALLEL_PROCESS, 32 | ]; 33 | /** 34 | * @var ProcessOptionCollection 35 | */ 36 | private $optionCollection; 37 | 38 | /** 39 | * ScenarioProcessFactory constructor. 40 | * 41 | * @param string|null $behatBinaryPath 42 | */ 43 | public function __construct($behatBinaryPath = null) 44 | { 45 | $this->behatBinaryPath = is_null($behatBinaryPath) ? reset($_SERVER['argv']) : $behatBinaryPath; 46 | } 47 | 48 | /** 49 | * @param array $options 50 | */ 51 | public function addSkipOptions(array $options) 52 | { 53 | $this->skipOptions = array_unique(array_merge($this->skipOptions, $options)); 54 | } 55 | 56 | /** 57 | * @param InputDefinition $inputDefinition 58 | * @param InputInterface $input 59 | */ 60 | public function init(InputDefinition $inputDefinition, InputInterface $input) 61 | { 62 | $options = new ProcessOptionCollection(); 63 | 64 | foreach ($inputDefinition->getOptions() as $optionName => $inputOption) { 65 | $optionValue = $input->getOption($optionName); 66 | if ($inputOption->getDefault() != $optionValue) { 67 | switch (true) { 68 | case in_array($optionName, $this->skipOptions): 69 | $option = null; 70 | break; 71 | case $inputOption->isArray() && $optionName == 'out': 72 | $option = new ProcessOptionOut($optionName, $optionValue); 73 | break; 74 | case $inputOption->isArray(): 75 | $option = new ProcessOptionArray($optionName, $optionValue); 76 | break; 77 | case $inputOption->isValueRequired(): 78 | case $inputOption->isValueOptional(): 79 | $option = new ProcessOptionScalar($optionName, $optionValue); 80 | break; 81 | default: 82 | $option = new ProcessOption($optionName); 83 | } 84 | 85 | if ($option) { 86 | $options->set($option); 87 | } 88 | } 89 | } 90 | 91 | $this->optionCollection = $options; 92 | } 93 | 94 | /** 95 | * @param ScenarioInfo $scenarioInfo 96 | * 97 | * @return Process 98 | */ 99 | public function make(ScenarioInfo $scenarioInfo) 100 | { 101 | $fileLine = (string) $scenarioInfo; 102 | 103 | $options = $this->optionCollection->toArray(); 104 | 105 | $outOption = $this->optionCollection->get('out'); 106 | if ($outOption instanceof ProcessOptionOut) { 107 | $outOption->setOutSuffix(md5($fileLine)); 108 | $options['out'] = clone $outOption; 109 | } 110 | 111 | $commandLine = sprintf('%s %s %s', PHP_BINARY, $this->behatBinaryPath, escapeshellarg($fileLine)); 112 | $process = new ScenarioProcess($scenarioInfo, $commandLine); 113 | 114 | foreach ($options as $option) { 115 | $process->setProcessOption($option); 116 | } 117 | 118 | return $process; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/ScenarioProcess/ScenarioProcessFactoryTest.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class ScenarioProcessFactoryTest extends \PHPUnit_Framework_TestCase 17 | { 18 | /** 19 | * @return array 20 | */ 21 | public function providerMake() 22 | { 23 | $inputDefinition = new InputDefinition(); 24 | $inputDefinition->addOption(new InputOption('option_optional_default', null, InputOption::VALUE_OPTIONAL, '', 'default')); 25 | $inputDefinition->addOption(new InputOption('option_array_default_array', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY)); 26 | $inputDefinition->addOption(new InputOption('option_none', null, InputOption::VALUE_NONE)); 27 | $inputDefinition->addOption(new InputOption('out', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY)); 28 | 29 | return [ 30 | 'option_optional_default_default' => [ 31 | $inputDefinition, 32 | new ArrayInput([ 33 | '--option_optional_default' => 'default', 34 | ], $inputDefinition), 35 | ], 36 | 37 | 'option_optional_default_value' => [ 38 | $inputDefinition, 39 | new ArrayInput([ 40 | '--option_optional_default' => 'value', 41 | ], $inputDefinition), 42 | '--option_optional_default \'value\'', 43 | ], 44 | 45 | 'option_array_default_array_default' => [ 46 | $inputDefinition, 47 | new ArrayInput([ 48 | '--option_array_default_array' => [], 49 | ], $inputDefinition), 50 | ], 51 | 52 | 'option_array_default_array_value' => [ 53 | $inputDefinition, 54 | new ArrayInput([ 55 | '--option_array_default_array' => ['value1', 'value2'], 56 | ], $inputDefinition), 57 | '--option_array_default_array \'value1\' --option_array_default_array \'value2\'', 58 | ], 59 | 60 | 'option_none_default' => [ 61 | $inputDefinition, 62 | new ArrayInput([], $inputDefinition), 63 | ], 64 | 65 | 'option_none_default_value' => [ 66 | $inputDefinition, 67 | new ArrayInput([ 68 | '--option_none' => 'any', 69 | ], $inputDefinition), 70 | '--option_none', 71 | ], 72 | 73 | 'out_folder_skip' => [ 74 | $inputDefinition, 75 | new ArrayInput([ 76 | '--out' => ['folder', 'std'], 77 | ], $inputDefinition), 78 | '', 79 | ['out'], 80 | ], 81 | 82 | 'out_folder' => [ 83 | $inputDefinition, 84 | new ArrayInput([ 85 | '--out' => ['folder', 'std'], 86 | ], $inputDefinition), 87 | '--out \'folder/59adaf3f0820898ecf0da97ceab30eab\' --out \'std\'', 88 | ], 89 | ]; 90 | } 91 | 92 | /** 93 | * @param InputDefinition $inputDefinition 94 | * @param InputInterface $input 95 | * @param string $expectedCommandLine 96 | * @param array $skipOptions 97 | * 98 | * @see ScenarioProcessFactory::init 99 | * @see ScenarioProcessFactory::make 100 | * 101 | * @dataProvider providerMake 102 | */ 103 | public function testMake(InputDefinition $inputDefinition, InputInterface $input, $expectedCommandLine = '', array $skipOptions = []) 104 | { 105 | $scenarioProcessFactory = new ScenarioProcessFactory('bin/behat'); 106 | $scenarioProcessFactory->addSkipOptions($skipOptions); 107 | $scenarioProcessFactory->init($inputDefinition, $input); 108 | 109 | $scenarioInfo = new ScenarioInfo('file', 1); 110 | $process = $scenarioProcessFactory->make($scenarioInfo); 111 | 112 | $this->assertEquals(trim(sprintf('%s bin/behat \'file:1\' %s', PHP_BINARY, $expectedCommandLine)), $process->getCommandLine()); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/Feature/FeatureRunnerTest.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class FeatureRunnerTest extends \PHPUnit_Framework_TestCase 19 | { 20 | /** 21 | * @return array 22 | */ 23 | public function providerSetMaxParallelProcess() 24 | { 25 | return [ 26 | [1], 27 | [2], 28 | ]; 29 | } 30 | 31 | /** 32 | * @param int $maxParallelProcess 33 | * 34 | * @dataProvider providerSetMaxParallelProcess 35 | */ 36 | public function testSetMaxParallelProcess($maxParallelProcess) 37 | { 38 | $eventDispatcher = $this->getMock(EventDispatcherInterface::class); 39 | $scenarioInfoExtractor = $this->getMock(ScenarioInfoExtractor::class); 40 | $scenarioProcessFactory = $this->getMock(ScenarioProcessFactory::class); 41 | $parallelProcessRunner = $this->getMock(ParallelProcessRunner::class, ['setMaxParallelProcess']); 42 | 43 | $parallelProcessRunner->expects($this->once())->method('setMaxParallelProcess')->with($maxParallelProcess); 44 | 45 | /** @var $eventDispatcher EventDispatcherInterface $featureRunner */ 46 | /** @var $scenarioInfoExtractor ScenarioInfoExtractor $featureRunner */ 47 | /** @var $scenarioProcessFactory ScenarioProcessFactory $featureRunner */ 48 | /** @var $parallelProcessRunner ParallelProcessRunner $featureRunner */ 49 | $featureRunner = new FeatureRunner( 50 | $eventDispatcher, 51 | $scenarioInfoExtractor, 52 | $scenarioProcessFactory, 53 | $parallelProcessRunner 54 | ); 55 | 56 | $featureRunner->setMaxParallelProcess($maxParallelProcess); 57 | } 58 | 59 | /** 60 | * @return array 61 | */ 62 | public function providerRun() 63 | { 64 | return [ 65 | 'empty' => [ 66 | [], 67 | ], 68 | '1 scenario in 1 group' => [ 69 | [ 70 | [ 71 | new ScenarioInfo('file', 1), 72 | ], 73 | ], 74 | ], 75 | '2 scenario in 1 group' => [ 76 | [ 77 | [ 78 | new ScenarioInfo('file', 1), 79 | new ScenarioInfo('file', 2), 80 | ], 81 | ], 82 | ], 83 | '2 scenario in 2 group' => [ 84 | [ 85 | [ 86 | new ScenarioInfo('file', 1), 87 | ], 88 | [ 89 | new ScenarioInfo('file', 1), 90 | ], 91 | ], 92 | ], 93 | ]; 94 | } 95 | 96 | /** 97 | * @param array $scenarioGroups 98 | * 99 | * @dataProvider providerRun 100 | */ 101 | public function testRun(array $scenarioGroups) 102 | { 103 | $eventDispatcher = $this->getMock(EventDispatcherInterface::class); 104 | $scenarioInfoExtractor = $this->getMock(ScenarioInfoExtractor::class, ['extract']); 105 | $scenarioInfoExtractor->expects($this->once())->method('extract')->willReturn($scenarioGroups); 106 | 107 | $scenarioProcessFactory = $this->getMock(ScenarioProcessFactory::class, ['make']); 108 | 109 | $index = 0; 110 | $processGroups = []; 111 | foreach ($scenarioGroups as $groupId => $scenarios) { 112 | $processGroups[$groupId] = []; 113 | foreach ($scenarios as $scenarioInfo) { 114 | $process = $this->getMock(ScenarioProcess::class, null, [$scenarioInfo, '']); 115 | $processGroups[$groupId][] = $process; 116 | 117 | $scenarioProcessFactory->expects($this->at($index))->method('make')->with($scenarioInfo)->willReturn($process); 118 | $index++; 119 | } 120 | } 121 | $scenarioProcessFactory->expects($this->exactly($index))->method('make'); 122 | 123 | $parallelProcessRunner = $this->getMock(ParallelProcessRunner::class, ['reset', 'add', 'run']); 124 | 125 | $parallelProcessRunner->expects($this->exactly(count($processGroups)))->method('reset')->willReturn($parallelProcessRunner); 126 | foreach ($processGroups as $index => $processes) { 127 | // count index with methods amount correction 128 | $parallelProcessRunner->expects($this->at($index * 3 + 1))->method('add')->with($processes)->willReturn($parallelProcessRunner); 129 | } 130 | 131 | $parallelProcessRunner->expects($this->exactly(count($processGroups)))->method('add'); 132 | $parallelProcessRunner->expects($this->exactly(count($processGroups)))->method('run')->willReturn($parallelProcessRunner); 133 | 134 | $featureNode = $this->getMock(FeatureNode::class, [], [], '', false); 135 | 136 | /** @var EventDispatcherInterface $eventDispatcher */ 137 | /** @var ScenarioInfoExtractor $scenarioInfoExtractor */ 138 | /** @var ScenarioProcessFactory $scenarioProcessFactory */ 139 | /** @var ParallelProcessRunner $parallelProcessRunner */ 140 | /** @var FeatureNode $featureNode */ 141 | $featureRunner = new FeatureRunner( 142 | $eventDispatcher, 143 | $scenarioInfoExtractor, 144 | $scenarioProcessFactory, 145 | $parallelProcessRunner 146 | ); 147 | 148 | $featureRunner->run($featureNode); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /tests/ParallelScenarioExtension/Cli/ParallelScenarioControllerTest.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class ParallelScenarioControllerTest extends \PHPUnit_Framework_TestCase 24 | { 25 | /** 26 | * @see ParallelScenarioController::configure 27 | */ 28 | public function testConfigure() 29 | { 30 | $controller = $this->getMock(ParallelScenarioController::class, null, [], '', false); 31 | 32 | $command = $this->getMock(Command::class, ['addOption'], [], '', false); 33 | $command->expects($this->once())->method('addOption') 34 | ->with('parallel-process', null, InputOption::VALUE_OPTIONAL, 'Max parallel processes amount', 1); 35 | 36 | /** @var ParallelScenarioController $controller */ 37 | /** @var Command $command */ 38 | $controller->configure($command); 39 | } 40 | 41 | /** 42 | * @return array 43 | */ 44 | public function providerExecuteMultiProcess() 45 | { 46 | return [ 47 | [ 48 | 5, 49 | 'locator', 50 | [ 51 | $this->getMock(FeatureNode::class, [], [], '', false), 52 | ], 53 | 0, 54 | ], 55 | 56 | [ 57 | 2, 58 | 'locator', 59 | [], 60 | 0, 61 | ], 62 | 63 | [ 64 | 3, 65 | 'locator', 66 | [ 67 | $this->getMock(FeatureNode::class, [], [], '', false), 68 | $this->getMock(FeatureNode::class, [], [], '', false), 69 | ], 70 | 1, 71 | ], 72 | ]; 73 | } 74 | 75 | /** 76 | * @param int $parallelProcess 77 | * @param string $paths 78 | * @param array $featureNodes 79 | * @param int $expectedResult 80 | * 81 | * @see ParallelScenarioController::execute 82 | * 83 | * @dataProvider providerExecuteMultiProcess 84 | */ 85 | public function testExecuteMultiProcess($parallelProcess, $paths, array $featureNodes, $expectedResult) 86 | { 87 | $inputDefinition = $this->getMock(InputDefinition::class); 88 | 89 | $input = $this->getMock(ArgvInput::class, ['getArgument', 'getOption']); 90 | $input->expects($this->once())->method('getOption') 91 | ->with(ParallelScenarioController::OPTION_PARALLEL_PROCESS)->willReturn($parallelProcess); 92 | $input->expects($this->once())->method('getArgument') 93 | ->with('paths')->willReturn($paths); 94 | 95 | $output = $this->getMock(ConsoleOutput::class); 96 | 97 | $featureRunner = $this->getMock(FeatureRunner::class, ['run', 'setMaxParallelProcess'], [], '', false); 98 | $featureRunner->expects($this->once())->method('setMaxParallelProcess') 99 | ->with($parallelProcess); 100 | foreach ($featureNodes as $index => $featureNode) { 101 | $featureRunner->expects($this->at($index + 1))->method('run')->with($featureNode)->willReturn($expectedResult); 102 | } 103 | $featureRunner->expects($this->exactly(count($featureNodes)))->method('run'); 104 | 105 | $featureExtractor = $this->getMock(FeatureExtractor::class, ['extract'], [], '', false); 106 | $featureExtractor->expects($this->once())->method('extract') 107 | ->with($paths)->willReturn($featureNodes); 108 | 109 | $scenarioProcessFactory = $this->getMock(ScenarioProcessFactory::class, ['init'], [], '', false); 110 | 111 | $outputPrinter = $this->getMock(OutputPrinter::class, ['init'], [], '', false); 112 | $outputPrinter->expects($this->once())->method('init') 113 | ->with($output); 114 | 115 | $command = $this->getMock(Command::class, ['getDefinition', 'addOption'], [], '', false); 116 | $command->expects($this->once())->method('getDefinition') 117 | ->willReturn($inputDefinition); 118 | 119 | $controller = $this->getMock(ParallelScenarioController::class, null, [ 120 | $featureRunner, 121 | $featureExtractor, 122 | $scenarioProcessFactory, 123 | $outputPrinter, 124 | ]); 125 | 126 | /** @var ParallelScenarioController $controller */ 127 | /** @var Command $command */ 128 | /** @var InputInterface $input */ 129 | /** @var OutputInterface $output */ 130 | $controller->configure($command); 131 | $result = $controller->execute($input, $output); 132 | 133 | $this->assertEquals($result, $expectedResult); 134 | } 135 | 136 | /** 137 | * @return array 138 | */ 139 | public function providerExecuteSingleProcess() 140 | { 141 | return [ 142 | [0], 143 | [-1], 144 | [1], 145 | [null], 146 | ]; 147 | } 148 | 149 | /** 150 | * @param mixed $parallelProcess 151 | * 152 | * @see ParallelScenarioController::execute 153 | * @dataProvider providerExecuteSingleProcess 154 | */ 155 | public function testExecuteSingleProcess($parallelProcess) 156 | { 157 | $inputDefinition = $this->getMock(InputDefinition::class); 158 | 159 | $input = $this->getMock(ArgvInput::class, ['getArgument', 'getOption']); 160 | $input->expects($this->once())->method('getOption') 161 | ->with(ParallelScenarioController::OPTION_PARALLEL_PROCESS)->willReturn($parallelProcess); 162 | $input->expects($this->once())->method('getArgument') 163 | ->with('paths'); 164 | 165 | $output = $this->getMock(ConsoleOutput::class); 166 | 167 | $featureRunner = $this->getMock(FeatureRunner::class, ['run', 'setMaxParallelProcess'], [], '', false); 168 | $featureRunner->expects($this->never())->method('setMaxParallelProcess'); 169 | $featureRunner->expects($this->never())->method('run'); 170 | 171 | $featureExtractor = $this->getMock(FeatureExtractor::class, ['extract'], [], '', false); 172 | $featureExtractor->expects($this->never())->method('extract'); 173 | 174 | $scenarioProcessFactory = $this->getMock(ScenarioProcessFactory::class, ['init'], [], '', false); 175 | 176 | $outputPrinter = $this->getMock(OutputPrinter::class, ['init'], [], '', false); 177 | $outputPrinter->expects($this->never())->method('init'); 178 | 179 | $command = $this->getMock(Command::class, ['getDefinition', 'addOption'], [], '', false); 180 | $command->expects($this->once())->method('getDefinition') 181 | ->willReturn($inputDefinition); 182 | 183 | $controller = $this->getMock(ParallelScenarioController::class, null, [ 184 | $featureRunner, 185 | $featureExtractor, 186 | $scenarioProcessFactory, 187 | $outputPrinter, 188 | ]); 189 | 190 | /** @var ParallelScenarioController $controller */ 191 | /** @var Command $command */ 192 | /** @var InputInterface $input */ 193 | /** @var OutputInterface $output */ 194 | $controller->configure($command); 195 | $result = $controller->execute($input, $output); 196 | 197 | $this->assertNull($result); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/ParallelScenarioExtension/ServiceContainer/ParallelScenarioExtension.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class ParallelScenarioExtension implements ExtensionInterface 31 | { 32 | const FEATURE_EXTRACTOR = 'parallel_scenario.feature.extractor'; 33 | const FEATURE_RUNNER = 'parallel_scenario.feature.runner'; 34 | 35 | const SCENARIO_INFO_EXTRACTOR = 'parallel_scenario.scenario.info.extractor'; 36 | 37 | const PROCESS_RUNNER = 'parallel_scenario.process.runner'; 38 | const PROCESS_FACTORY = 'parallel_scenario.process.factory'; 39 | const PROCESS_PROFILE_BALANCE = 'parallel_scenario.process.profile_balance'; 40 | const OUTPUT_PRINTER = 'parallel_scenario.output.printer'; 41 | const STOP_ON_FAILURE = 'parallel_scenario.stop_on_failure'; 42 | 43 | const CONFIG_OPTIONS = 'options'; 44 | const CONFIG_SKIP = 'skip'; 45 | const CONFIG_PROFILES = 'profiles'; 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function process(ContainerBuilder $container) 51 | { 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function getConfigKey() 58 | { 59 | return 'parallel_scenario'; 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function initialize(ExtensionManager $extensionManager) 66 | { 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | public function configure(ArrayNodeDefinition $builder) 73 | { 74 | $builder 75 | ->children() 76 | ->arrayNode(self::CONFIG_OPTIONS) 77 | ->addDefaultsIfNotSet() 78 | ->children() 79 | ->arrayNode(self::CONFIG_SKIP) 80 | ->prototype('scalar') 81 | ->end() 82 | ->defaultValue([]) 83 | ->end(); 84 | 85 | $builder 86 | ->children() 87 | ->arrayNode(self::CONFIG_PROFILES) 88 | ->prototype('scalar') 89 | ->end(); 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | public function load(ContainerBuilder $containerBuilder, array $config) 96 | { 97 | $this->loadScenarioInfoExtractor($containerBuilder); 98 | $this->loadFeatureExtractor($containerBuilder); 99 | $this->loadFeatureRunner($containerBuilder); 100 | $this->loadProcessProfileBalance($containerBuilder, $config); 101 | 102 | $this->loadProcessRunner($containerBuilder); 103 | $this->loadProcessFactory($containerBuilder, $config); 104 | $this->loadController($containerBuilder); 105 | 106 | $this->loadOutputPrinter($containerBuilder); 107 | } 108 | 109 | /** 110 | * @param ContainerBuilder $containerBuilder 111 | * @param array $config 112 | */ 113 | protected function loadProcessFactory(ContainerBuilder $containerBuilder, array $config) 114 | { 115 | $skipOptions = $config[self::CONFIG_OPTIONS][self::CONFIG_SKIP]; 116 | 117 | $definition = new Definition(ScenarioProcessFactory::class); 118 | $definition->addMethodCall('addSkipOptions', [ 119 | $skipOptions, 120 | ]); 121 | 122 | $containerBuilder->setDefinition(self::PROCESS_FACTORY, $definition); 123 | } 124 | 125 | /** 126 | * @param ContainerBuilder $containerBuilder 127 | * @param array $config 128 | */ 129 | protected function loadProcessProfileBalance(ContainerBuilder $containerBuilder, array $config) 130 | { 131 | $profiles = $config[self::CONFIG_PROFILES]; 132 | $definition = new Definition(ScenarioProcessProfileBalance::class, [ 133 | $profiles, 134 | ]); 135 | $definition->addTag(EventDispatcherExtension::SUBSCRIBER_TAG, [ 136 | 'priority' => 800, 137 | ]); 138 | 139 | $containerBuilder->setDefinition(self::PROCESS_PROFILE_BALANCE, $definition); 140 | } 141 | 142 | /** 143 | * @param ContainerBuilder $containerBuilder 144 | */ 145 | protected function loadController(ContainerBuilder $containerBuilder) 146 | { 147 | $definition = new Definition(ParallelScenarioController::class, [ 148 | new Reference(self::FEATURE_RUNNER), 149 | new Reference(self::FEATURE_EXTRACTOR), 150 | new Reference(self::PROCESS_FACTORY), 151 | new Reference(self::OUTPUT_PRINTER), 152 | ]); 153 | 154 | $definition->addTag(CliExtension::CONTROLLER_TAG, [ 155 | 'priority' => 1, 156 | ]); 157 | 158 | $containerBuilder->setDefinition(CliExtension::CONTROLLER_TAG.'.parallel-scenario', $definition); 159 | } 160 | 161 | /** 162 | * @param ContainerBuilder $containerBuilder 163 | */ 164 | protected function loadOutputPrinter(ContainerBuilder $containerBuilder) 165 | { 166 | $definition = new Definition(OutputPrinter::class); 167 | $definition->addTag(EventDispatcherExtension::SUBSCRIBER_TAG, [ 168 | 'priority' => -1, 169 | ]); 170 | 171 | $containerBuilder->setDefinition(self::OUTPUT_PRINTER, $definition); 172 | } 173 | 174 | /** 175 | * @param ContainerBuilder $containerBuilder 176 | */ 177 | protected function loadProcessRunner(ContainerBuilder $containerBuilder) 178 | { 179 | $definition = new Definition(ParallelProcessRunner::class, [ 180 | new Reference('event_dispatcher'), 181 | ]); 182 | 183 | $containerBuilder->setDefinition(self::PROCESS_RUNNER, $definition); 184 | } 185 | 186 | /** 187 | * @param ContainerBuilder $containerBuilder 188 | */ 189 | protected function loadStopOnFailure(ContainerBuilder $containerBuilder) 190 | { 191 | $definition = new Definition(StopOnFailure::class, [ 192 | new Reference(self::PROCESS_RUNNER), 193 | ]); 194 | $definition->addTag(EventDispatcherExtension::SUBSCRIBER_TAG); 195 | 196 | $containerBuilder->setDefinition(self::STOP_ON_FAILURE, $definition); 197 | } 198 | 199 | /** 200 | * @param ContainerBuilder $containerBuilder 201 | */ 202 | protected function loadFeatureExtractor(ContainerBuilder $containerBuilder) 203 | { 204 | $definition = new Definition(FeatureExtractor::class, [ 205 | new Reference(SuiteExtension::REGISTRY_ID), 206 | new Reference(SpecificationExtension::FINDER_ID), 207 | ]); 208 | 209 | $containerBuilder->setDefinition(self::FEATURE_EXTRACTOR, $definition); 210 | } 211 | 212 | /** 213 | * @param ContainerBuilder $containerBuilder 214 | */ 215 | protected function loadFeatureRunner(ContainerBuilder $containerBuilder) 216 | { 217 | $definition = new Definition(FeatureRunner::class, [ 218 | new Reference('event_dispatcher'), 219 | new Reference(self::SCENARIO_INFO_EXTRACTOR), 220 | new Reference(self::PROCESS_FACTORY), 221 | new Reference(self::PROCESS_RUNNER), 222 | ]); 223 | 224 | $containerBuilder->setDefinition(self::FEATURE_RUNNER, $definition); 225 | } 226 | 227 | /** 228 | * @param ContainerBuilder $containerBuilder 229 | */ 230 | protected function loadScenarioInfoExtractor(ContainerBuilder $containerBuilder) 231 | { 232 | $definition = new Definition(ScenarioInfoExtractor::class); 233 | 234 | $containerBuilder->setDefinition(self::SCENARIO_INFO_EXTRACTOR, $definition); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "hash": "5aaa97acee722941895766dd2f349af6", 8 | "content-hash": "c36897894196ad8c994afcecf50db51e", 9 | "packages": [ 10 | { 11 | "name": "behat/behat", 12 | "version": "v3.0.15", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/Behat/Behat.git", 16 | "reference": "b35ae3d45332d80c532af69cc36f780a9397a996" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/Behat/Behat/zipball/b35ae3d45332d80c532af69cc36f780a9397a996", 21 | "reference": "b35ae3d45332d80c532af69cc36f780a9397a996", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "behat/gherkin": "~4.3", 26 | "behat/transliterator": "~1.0", 27 | "ext-mbstring": "*", 28 | "php": ">=5.3.3", 29 | "symfony/class-loader": "~2.1", 30 | "symfony/config": "~2.3", 31 | "symfony/console": "~2.1", 32 | "symfony/dependency-injection": "~2.1", 33 | "symfony/event-dispatcher": "~2.1", 34 | "symfony/translation": "~2.3", 35 | "symfony/yaml": "~2.1" 36 | }, 37 | "require-dev": { 38 | "phpspec/prophecy-phpunit": "~1.0", 39 | "phpunit/phpunit": "~4.0", 40 | "symfony/process": "~2.1" 41 | }, 42 | "suggest": { 43 | "behat/mink-extension": "for integration with Mink testing framework", 44 | "behat/symfony2-extension": "for integration with Symfony2 web framework", 45 | "behat/yii-extension": "for integration with Yii web framework" 46 | }, 47 | "bin": [ 48 | "bin/behat" 49 | ], 50 | "type": "library", 51 | "extra": { 52 | "branch-alias": { 53 | "dev-master": "3.0.x-dev" 54 | } 55 | }, 56 | "autoload": { 57 | "psr-0": { 58 | "Behat\\Behat": "src/", 59 | "Behat\\Testwork": "src/" 60 | } 61 | }, 62 | "notification-url": "https://packagist.org/downloads/", 63 | "license": [ 64 | "MIT" 65 | ], 66 | "authors": [ 67 | { 68 | "name": "Konstantin Kudryashov", 69 | "email": "ever.zet@gmail.com", 70 | "homepage": "http://everzet.com" 71 | } 72 | ], 73 | "description": "Scenario-oriented BDD framework for PHP 5.3", 74 | "homepage": "http://behat.org/", 75 | "keywords": [ 76 | "Agile", 77 | "BDD", 78 | "ScenarioBDD", 79 | "Scrum", 80 | "StoryBDD", 81 | "User story", 82 | "business", 83 | "development", 84 | "documentation", 85 | "examples", 86 | "symfony", 87 | "testing" 88 | ], 89 | "time": "2015-02-22 14:10:33" 90 | }, 91 | { 92 | "name": "behat/gherkin", 93 | "version": "v4.4.0", 94 | "source": { 95 | "type": "git", 96 | "url": "https://github.com/Behat/Gherkin.git", 97 | "reference": "6b3f8cf3560dc4909c4cddd4f1af3e1f6e9d80af" 98 | }, 99 | "dist": { 100 | "type": "zip", 101 | "url": "https://api.github.com/repos/Behat/Gherkin/zipball/6b3f8cf3560dc4909c4cddd4f1af3e1f6e9d80af", 102 | "reference": "6b3f8cf3560dc4909c4cddd4f1af3e1f6e9d80af", 103 | "shasum": "" 104 | }, 105 | "require": { 106 | "php": ">=5.3.1" 107 | }, 108 | "require-dev": { 109 | "phpunit/phpunit": "~4.0", 110 | "symfony/yaml": "~2.1" 111 | }, 112 | "suggest": { 113 | "symfony/yaml": "If you want to parse features, represented in YAML files" 114 | }, 115 | "type": "library", 116 | "extra": { 117 | "branch-alias": { 118 | "dev-master": "4.4-dev" 119 | } 120 | }, 121 | "autoload": { 122 | "psr-0": { 123 | "Behat\\Gherkin": "src/" 124 | } 125 | }, 126 | "notification-url": "https://packagist.org/downloads/", 127 | "license": [ 128 | "MIT" 129 | ], 130 | "authors": [ 131 | { 132 | "name": "Konstantin Kudryashov", 133 | "email": "ever.zet@gmail.com", 134 | "homepage": "http://everzet.com" 135 | } 136 | ], 137 | "description": "Gherkin DSL parser for PHP 5.3", 138 | "homepage": "http://behat.org/", 139 | "keywords": [ 140 | "BDD", 141 | "Behat", 142 | "Cucumber", 143 | "DSL", 144 | "gherkin", 145 | "parser" 146 | ], 147 | "time": "2015-09-29 13:41:19" 148 | }, 149 | { 150 | "name": "behat/transliterator", 151 | "version": "v1.1.0", 152 | "source": { 153 | "type": "git", 154 | "url": "https://github.com/Behat/Transliterator.git", 155 | "reference": "868e05be3a9f25ba6424c2dd4849567f50715003" 156 | }, 157 | "dist": { 158 | "type": "zip", 159 | "url": "https://api.github.com/repos/Behat/Transliterator/zipball/868e05be3a9f25ba6424c2dd4849567f50715003", 160 | "reference": "868e05be3a9f25ba6424c2dd4849567f50715003", 161 | "shasum": "" 162 | }, 163 | "require": { 164 | "php": ">=5.3.3" 165 | }, 166 | "type": "library", 167 | "extra": { 168 | "branch-alias": { 169 | "dev-master": "1.1-dev" 170 | } 171 | }, 172 | "autoload": { 173 | "psr-0": { 174 | "Behat\\Transliterator": "src/" 175 | } 176 | }, 177 | "notification-url": "https://packagist.org/downloads/", 178 | "license": [ 179 | "Artistic-1.0" 180 | ], 181 | "description": "String transliterator", 182 | "keywords": [ 183 | "i18n", 184 | "slug", 185 | "transliterator" 186 | ], 187 | "time": "2015-09-28 16:26:35" 188 | }, 189 | { 190 | "name": "symfony/class-loader", 191 | "version": "v2.8.0", 192 | "source": { 193 | "type": "git", 194 | "url": "https://github.com/symfony/class-loader.git", 195 | "reference": "51f83451bf0ddfc696e47e4642d6cd10fcfce160" 196 | }, 197 | "dist": { 198 | "type": "zip", 199 | "url": "https://api.github.com/repos/symfony/class-loader/zipball/51f83451bf0ddfc696e47e4642d6cd10fcfce160", 200 | "reference": "51f83451bf0ddfc696e47e4642d6cd10fcfce160", 201 | "shasum": "" 202 | }, 203 | "require": { 204 | "php": ">=5.3.9" 205 | }, 206 | "require-dev": { 207 | "symfony/finder": "~2.0,>=2.0.5|~3.0.0" 208 | }, 209 | "type": "library", 210 | "extra": { 211 | "branch-alias": { 212 | "dev-master": "2.8-dev" 213 | } 214 | }, 215 | "autoload": { 216 | "psr-4": { 217 | "Symfony\\Component\\ClassLoader\\": "" 218 | }, 219 | "exclude-from-classmap": [ 220 | "/Tests/" 221 | ] 222 | }, 223 | "notification-url": "https://packagist.org/downloads/", 224 | "license": [ 225 | "MIT" 226 | ], 227 | "authors": [ 228 | { 229 | "name": "Fabien Potencier", 230 | "email": "fabien@symfony.com" 231 | }, 232 | { 233 | "name": "Symfony Community", 234 | "homepage": "https://symfony.com/contributors" 235 | } 236 | ], 237 | "description": "Symfony ClassLoader Component", 238 | "homepage": "https://symfony.com", 239 | "time": "2015-11-26 07:00:59" 240 | }, 241 | { 242 | "name": "symfony/config", 243 | "version": "v2.8.0", 244 | "source": { 245 | "type": "git", 246 | "url": "https://github.com/symfony/config.git", 247 | "reference": "f21c97aec1b5302d2dc0d17047ea8f4e4ff93aae" 248 | }, 249 | "dist": { 250 | "type": "zip", 251 | "url": "https://api.github.com/repos/symfony/config/zipball/f21c97aec1b5302d2dc0d17047ea8f4e4ff93aae", 252 | "reference": "f21c97aec1b5302d2dc0d17047ea8f4e4ff93aae", 253 | "shasum": "" 254 | }, 255 | "require": { 256 | "php": ">=5.3.9", 257 | "symfony/filesystem": "~2.3|~3.0.0" 258 | }, 259 | "type": "library", 260 | "extra": { 261 | "branch-alias": { 262 | "dev-master": "2.8-dev" 263 | } 264 | }, 265 | "autoload": { 266 | "psr-4": { 267 | "Symfony\\Component\\Config\\": "" 268 | }, 269 | "exclude-from-classmap": [ 270 | "/Tests/" 271 | ] 272 | }, 273 | "notification-url": "https://packagist.org/downloads/", 274 | "license": [ 275 | "MIT" 276 | ], 277 | "authors": [ 278 | { 279 | "name": "Fabien Potencier", 280 | "email": "fabien@symfony.com" 281 | }, 282 | { 283 | "name": "Symfony Community", 284 | "homepage": "https://symfony.com/contributors" 285 | } 286 | ], 287 | "description": "Symfony Config Component", 288 | "homepage": "https://symfony.com", 289 | "time": "2015-11-23 20:38:01" 290 | }, 291 | { 292 | "name": "symfony/console", 293 | "version": "v2.8.0", 294 | "source": { 295 | "type": "git", 296 | "url": "https://github.com/symfony/console.git", 297 | "reference": "d232bfc100dfd32b18ccbcab4bcc8f28697b7e41" 298 | }, 299 | "dist": { 300 | "type": "zip", 301 | "url": "https://api.github.com/repos/symfony/console/zipball/d232bfc100dfd32b18ccbcab4bcc8f28697b7e41", 302 | "reference": "d232bfc100dfd32b18ccbcab4bcc8f28697b7e41", 303 | "shasum": "" 304 | }, 305 | "require": { 306 | "php": ">=5.3.9", 307 | "symfony/polyfill-mbstring": "~1.0" 308 | }, 309 | "require-dev": { 310 | "psr/log": "~1.0", 311 | "symfony/event-dispatcher": "~2.1|~3.0.0", 312 | "symfony/process": "~2.1|~3.0.0" 313 | }, 314 | "suggest": { 315 | "psr/log": "For using the console logger", 316 | "symfony/event-dispatcher": "", 317 | "symfony/process": "" 318 | }, 319 | "type": "library", 320 | "extra": { 321 | "branch-alias": { 322 | "dev-master": "2.8-dev" 323 | } 324 | }, 325 | "autoload": { 326 | "psr-4": { 327 | "Symfony\\Component\\Console\\": "" 328 | }, 329 | "exclude-from-classmap": [ 330 | "/Tests/" 331 | ] 332 | }, 333 | "notification-url": "https://packagist.org/downloads/", 334 | "license": [ 335 | "MIT" 336 | ], 337 | "authors": [ 338 | { 339 | "name": "Fabien Potencier", 340 | "email": "fabien@symfony.com" 341 | }, 342 | { 343 | "name": "Symfony Community", 344 | "homepage": "https://symfony.com/contributors" 345 | } 346 | ], 347 | "description": "Symfony Console Component", 348 | "homepage": "https://symfony.com", 349 | "time": "2015-11-30 12:35:10" 350 | }, 351 | { 352 | "name": "symfony/dependency-injection", 353 | "version": "v2.8.0", 354 | "source": { 355 | "type": "git", 356 | "url": "https://github.com/symfony/dependency-injection.git", 357 | "reference": "1ac8ce1a1cff7ff9467d44bc71b0f71dfa751ba4" 358 | }, 359 | "dist": { 360 | "type": "zip", 361 | "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/1ac8ce1a1cff7ff9467d44bc71b0f71dfa751ba4", 362 | "reference": "1ac8ce1a1cff7ff9467d44bc71b0f71dfa751ba4", 363 | "shasum": "" 364 | }, 365 | "require": { 366 | "php": ">=5.3.9" 367 | }, 368 | "conflict": { 369 | "symfony/expression-language": "<2.6" 370 | }, 371 | "require-dev": { 372 | "symfony/config": "~2.2|~3.0.0", 373 | "symfony/expression-language": "~2.6|~3.0.0", 374 | "symfony/yaml": "~2.1|~3.0.0" 375 | }, 376 | "suggest": { 377 | "symfony/config": "", 378 | "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", 379 | "symfony/yaml": "" 380 | }, 381 | "type": "library", 382 | "extra": { 383 | "branch-alias": { 384 | "dev-master": "2.8-dev" 385 | } 386 | }, 387 | "autoload": { 388 | "psr-4": { 389 | "Symfony\\Component\\DependencyInjection\\": "" 390 | }, 391 | "exclude-from-classmap": [ 392 | "/Tests/" 393 | ] 394 | }, 395 | "notification-url": "https://packagist.org/downloads/", 396 | "license": [ 397 | "MIT" 398 | ], 399 | "authors": [ 400 | { 401 | "name": "Fabien Potencier", 402 | "email": "fabien@symfony.com" 403 | }, 404 | { 405 | "name": "Symfony Community", 406 | "homepage": "https://symfony.com/contributors" 407 | } 408 | ], 409 | "description": "Symfony DependencyInjection Component", 410 | "homepage": "https://symfony.com", 411 | "time": "2015-11-30 06:56:28" 412 | }, 413 | { 414 | "name": "symfony/event-dispatcher", 415 | "version": "v2.8.0", 416 | "source": { 417 | "type": "git", 418 | "url": "https://github.com/symfony/event-dispatcher.git", 419 | "reference": "a5eb815363c0388e83247e7e9853e5dbc14999cc" 420 | }, 421 | "dist": { 422 | "type": "zip", 423 | "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a5eb815363c0388e83247e7e9853e5dbc14999cc", 424 | "reference": "a5eb815363c0388e83247e7e9853e5dbc14999cc", 425 | "shasum": "" 426 | }, 427 | "require": { 428 | "php": ">=5.3.9" 429 | }, 430 | "require-dev": { 431 | "psr/log": "~1.0", 432 | "symfony/config": "~2.0,>=2.0.5|~3.0.0", 433 | "symfony/dependency-injection": "~2.6|~3.0.0", 434 | "symfony/expression-language": "~2.6|~3.0.0", 435 | "symfony/stopwatch": "~2.3|~3.0.0" 436 | }, 437 | "suggest": { 438 | "symfony/dependency-injection": "", 439 | "symfony/http-kernel": "" 440 | }, 441 | "type": "library", 442 | "extra": { 443 | "branch-alias": { 444 | "dev-master": "2.8-dev" 445 | } 446 | }, 447 | "autoload": { 448 | "psr-4": { 449 | "Symfony\\Component\\EventDispatcher\\": "" 450 | }, 451 | "exclude-from-classmap": [ 452 | "/Tests/" 453 | ] 454 | }, 455 | "notification-url": "https://packagist.org/downloads/", 456 | "license": [ 457 | "MIT" 458 | ], 459 | "authors": [ 460 | { 461 | "name": "Fabien Potencier", 462 | "email": "fabien@symfony.com" 463 | }, 464 | { 465 | "name": "Symfony Community", 466 | "homepage": "https://symfony.com/contributors" 467 | } 468 | ], 469 | "description": "Symfony EventDispatcher Component", 470 | "homepage": "https://symfony.com", 471 | "time": "2015-10-30 20:15:42" 472 | }, 473 | { 474 | "name": "symfony/filesystem", 475 | "version": "v3.0.0", 476 | "source": { 477 | "type": "git", 478 | "url": "https://github.com/symfony/filesystem.git", 479 | "reference": "692d98d813e4ef314b9c22775c86ddbeb0f44884" 480 | }, 481 | "dist": { 482 | "type": "zip", 483 | "url": "https://api.github.com/repos/symfony/filesystem/zipball/692d98d813e4ef314b9c22775c86ddbeb0f44884", 484 | "reference": "692d98d813e4ef314b9c22775c86ddbeb0f44884", 485 | "shasum": "" 486 | }, 487 | "require": { 488 | "php": ">=5.5.9" 489 | }, 490 | "type": "library", 491 | "extra": { 492 | "branch-alias": { 493 | "dev-master": "3.0-dev" 494 | } 495 | }, 496 | "autoload": { 497 | "psr-4": { 498 | "Symfony\\Component\\Filesystem\\": "" 499 | }, 500 | "exclude-from-classmap": [ 501 | "/Tests/" 502 | ] 503 | }, 504 | "notification-url": "https://packagist.org/downloads/", 505 | "license": [ 506 | "MIT" 507 | ], 508 | "authors": [ 509 | { 510 | "name": "Fabien Potencier", 511 | "email": "fabien@symfony.com" 512 | }, 513 | { 514 | "name": "Symfony Community", 515 | "homepage": "https://symfony.com/contributors" 516 | } 517 | ], 518 | "description": "Symfony Filesystem Component", 519 | "homepage": "https://symfony.com", 520 | "time": "2015-11-23 10:41:47" 521 | }, 522 | { 523 | "name": "symfony/polyfill-mbstring", 524 | "version": "v1.0.0", 525 | "source": { 526 | "type": "git", 527 | "url": "https://github.com/symfony/polyfill-mbstring.git", 528 | "reference": "0b6a8940385311a24e060ec1fe35680e17c74497" 529 | }, 530 | "dist": { 531 | "type": "zip", 532 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0b6a8940385311a24e060ec1fe35680e17c74497", 533 | "reference": "0b6a8940385311a24e060ec1fe35680e17c74497", 534 | "shasum": "" 535 | }, 536 | "require": { 537 | "php": ">=5.3.3" 538 | }, 539 | "type": "library", 540 | "extra": { 541 | "branch-alias": { 542 | "dev-master": "1.0-dev" 543 | } 544 | }, 545 | "autoload": { 546 | "psr-4": { 547 | "Symfony\\Polyfill\\Mbstring\\": "" 548 | }, 549 | "files": [ 550 | "bootstrap.php" 551 | ] 552 | }, 553 | "notification-url": "https://packagist.org/downloads/", 554 | "license": [ 555 | "MIT" 556 | ], 557 | "authors": [ 558 | { 559 | "name": "Nicolas Grekas", 560 | "email": "p@tchwork.com" 561 | }, 562 | { 563 | "name": "Symfony Community", 564 | "homepage": "https://symfony.com/contributors" 565 | } 566 | ], 567 | "description": "Symfony polyfill for the Mbstring extension", 568 | "homepage": "https://symfony.com", 569 | "keywords": [ 570 | "compatibility", 571 | "mbstring", 572 | "polyfill", 573 | "portable", 574 | "shim" 575 | ], 576 | "time": "2015-11-04 20:28:58" 577 | }, 578 | { 579 | "name": "symfony/process", 580 | "version": "v3.0.0", 581 | "source": { 582 | "type": "git", 583 | "url": "https://github.com/symfony/process.git", 584 | "reference": "01383ed02a1020759bc8ee5d975fcec04ba16fbf" 585 | }, 586 | "dist": { 587 | "type": "zip", 588 | "url": "https://api.github.com/repos/symfony/process/zipball/01383ed02a1020759bc8ee5d975fcec04ba16fbf", 589 | "reference": "01383ed02a1020759bc8ee5d975fcec04ba16fbf", 590 | "shasum": "" 591 | }, 592 | "require": { 593 | "php": ">=5.5.9" 594 | }, 595 | "type": "library", 596 | "extra": { 597 | "branch-alias": { 598 | "dev-master": "3.0-dev" 599 | } 600 | }, 601 | "autoload": { 602 | "psr-4": { 603 | "Symfony\\Component\\Process\\": "" 604 | }, 605 | "exclude-from-classmap": [ 606 | "/Tests/" 607 | ] 608 | }, 609 | "notification-url": "https://packagist.org/downloads/", 610 | "license": [ 611 | "MIT" 612 | ], 613 | "authors": [ 614 | { 615 | "name": "Fabien Potencier", 616 | "email": "fabien@symfony.com" 617 | }, 618 | { 619 | "name": "Symfony Community", 620 | "homepage": "https://symfony.com/contributors" 621 | } 622 | ], 623 | "description": "Symfony Process Component", 624 | "homepage": "https://symfony.com", 625 | "time": "2015-11-30 12:36:17" 626 | }, 627 | { 628 | "name": "symfony/translation", 629 | "version": "v2.8.0", 630 | "source": { 631 | "type": "git", 632 | "url": "https://github.com/symfony/translation.git", 633 | "reference": "6772657767649fc3b31df12705194fb4af11ef98" 634 | }, 635 | "dist": { 636 | "type": "zip", 637 | "url": "https://api.github.com/repos/symfony/translation/zipball/6772657767649fc3b31df12705194fb4af11ef98", 638 | "reference": "6772657767649fc3b31df12705194fb4af11ef98", 639 | "shasum": "" 640 | }, 641 | "require": { 642 | "php": ">=5.3.9", 643 | "symfony/polyfill-mbstring": "~1.0" 644 | }, 645 | "conflict": { 646 | "symfony/config": "<2.7" 647 | }, 648 | "require-dev": { 649 | "psr/log": "~1.0", 650 | "symfony/config": "~2.8", 651 | "symfony/intl": "~2.4|~3.0.0", 652 | "symfony/yaml": "~2.2|~3.0.0" 653 | }, 654 | "suggest": { 655 | "psr/log": "To use logging capability in translator", 656 | "symfony/config": "", 657 | "symfony/yaml": "" 658 | }, 659 | "type": "library", 660 | "extra": { 661 | "branch-alias": { 662 | "dev-master": "2.8-dev" 663 | } 664 | }, 665 | "autoload": { 666 | "psr-4": { 667 | "Symfony\\Component\\Translation\\": "" 668 | }, 669 | "exclude-from-classmap": [ 670 | "/Tests/" 671 | ] 672 | }, 673 | "notification-url": "https://packagist.org/downloads/", 674 | "license": [ 675 | "MIT" 676 | ], 677 | "authors": [ 678 | { 679 | "name": "Fabien Potencier", 680 | "email": "fabien@symfony.com" 681 | }, 682 | { 683 | "name": "Symfony Community", 684 | "homepage": "https://symfony.com/contributors" 685 | } 686 | ], 687 | "description": "Symfony Translation Component", 688 | "homepage": "https://symfony.com", 689 | "time": "2015-11-18 13:45:00" 690 | }, 691 | { 692 | "name": "symfony/yaml", 693 | "version": "v2.8.0", 694 | "source": { 695 | "type": "git", 696 | "url": "https://github.com/symfony/yaml.git", 697 | "reference": "f79824187de95064a2f5038904c4d7f0227fedb5" 698 | }, 699 | "dist": { 700 | "type": "zip", 701 | "url": "https://api.github.com/repos/symfony/yaml/zipball/f79824187de95064a2f5038904c4d7f0227fedb5", 702 | "reference": "f79824187de95064a2f5038904c4d7f0227fedb5", 703 | "shasum": "" 704 | }, 705 | "require": { 706 | "php": ">=5.3.9" 707 | }, 708 | "type": "library", 709 | "extra": { 710 | "branch-alias": { 711 | "dev-master": "2.8-dev" 712 | } 713 | }, 714 | "autoload": { 715 | "psr-4": { 716 | "Symfony\\Component\\Yaml\\": "" 717 | }, 718 | "exclude-from-classmap": [ 719 | "/Tests/" 720 | ] 721 | }, 722 | "notification-url": "https://packagist.org/downloads/", 723 | "license": [ 724 | "MIT" 725 | ], 726 | "authors": [ 727 | { 728 | "name": "Fabien Potencier", 729 | "email": "fabien@symfony.com" 730 | }, 731 | { 732 | "name": "Symfony Community", 733 | "homepage": "https://symfony.com/contributors" 734 | } 735 | ], 736 | "description": "Symfony Yaml Component", 737 | "homepage": "https://symfony.com", 738 | "time": "2015-11-30 12:35:10" 739 | }, 740 | { 741 | "name": "tonicforhealth/parallel-process-runner", 742 | "version": "v1.0.3", 743 | "source": { 744 | "type": "git", 745 | "url": "https://github.com/tonicforhealth/parallel-process-runner.git", 746 | "reference": "9adce2e7f2f48b102a6983ddc7cb1bc46c96db85" 747 | }, 748 | "dist": { 749 | "type": "zip", 750 | "url": "https://api.github.com/repos/tonicforhealth/parallel-process-runner/zipball/9adce2e7f2f48b102a6983ddc7cb1bc46c96db85", 751 | "reference": "9adce2e7f2f48b102a6983ddc7cb1bc46c96db85", 752 | "shasum": "" 753 | }, 754 | "require": { 755 | "symfony/event-dispatcher": ">2.7 <= 3", 756 | "symfony/process": ">2.7 <= 3" 757 | }, 758 | "require-dev": { 759 | "phpunit/phpunit": "4.1.*" 760 | }, 761 | "type": "library", 762 | "autoload": { 763 | "psr-4": { 764 | "Tonic\\": "src" 765 | } 766 | }, 767 | "notification-url": "https://packagist.org/downloads/", 768 | "authors": [ 769 | { 770 | "name": "oleksii.storozhylov", 771 | "email": "oleksii.storozhylov@tonicforhealth.com" 772 | } 773 | ], 774 | "time": "2015-12-11 15:47:41" 775 | } 776 | ], 777 | "packages-dev": [ 778 | { 779 | "name": "phpunit/php-code-coverage", 780 | "version": "2.2.4", 781 | "source": { 782 | "type": "git", 783 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 784 | "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" 785 | }, 786 | "dist": { 787 | "type": "zip", 788 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", 789 | "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", 790 | "shasum": "" 791 | }, 792 | "require": { 793 | "php": ">=5.3.3", 794 | "phpunit/php-file-iterator": "~1.3", 795 | "phpunit/php-text-template": "~1.2", 796 | "phpunit/php-token-stream": "~1.3", 797 | "sebastian/environment": "^1.3.2", 798 | "sebastian/version": "~1.0" 799 | }, 800 | "require-dev": { 801 | "ext-xdebug": ">=2.1.4", 802 | "phpunit/phpunit": "~4" 803 | }, 804 | "suggest": { 805 | "ext-dom": "*", 806 | "ext-xdebug": ">=2.2.1", 807 | "ext-xmlwriter": "*" 808 | }, 809 | "type": "library", 810 | "extra": { 811 | "branch-alias": { 812 | "dev-master": "2.2.x-dev" 813 | } 814 | }, 815 | "autoload": { 816 | "classmap": [ 817 | "src/" 818 | ] 819 | }, 820 | "notification-url": "https://packagist.org/downloads/", 821 | "license": [ 822 | "BSD-3-Clause" 823 | ], 824 | "authors": [ 825 | { 826 | "name": "Sebastian Bergmann", 827 | "email": "sb@sebastian-bergmann.de", 828 | "role": "lead" 829 | } 830 | ], 831 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 832 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 833 | "keywords": [ 834 | "coverage", 835 | "testing", 836 | "xunit" 837 | ], 838 | "time": "2015-10-06 15:47:00" 839 | }, 840 | { 841 | "name": "phpunit/php-file-iterator", 842 | "version": "1.3.4", 843 | "source": { 844 | "type": "git", 845 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 846 | "reference": "acd690379117b042d1c8af1fafd61bde001bf6bb" 847 | }, 848 | "dist": { 849 | "type": "zip", 850 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/acd690379117b042d1c8af1fafd61bde001bf6bb", 851 | "reference": "acd690379117b042d1c8af1fafd61bde001bf6bb", 852 | "shasum": "" 853 | }, 854 | "require": { 855 | "php": ">=5.3.3" 856 | }, 857 | "type": "library", 858 | "autoload": { 859 | "classmap": [ 860 | "File/" 861 | ] 862 | }, 863 | "notification-url": "https://packagist.org/downloads/", 864 | "include-path": [ 865 | "" 866 | ], 867 | "license": [ 868 | "BSD-3-Clause" 869 | ], 870 | "authors": [ 871 | { 872 | "name": "Sebastian Bergmann", 873 | "email": "sb@sebastian-bergmann.de", 874 | "role": "lead" 875 | } 876 | ], 877 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 878 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 879 | "keywords": [ 880 | "filesystem", 881 | "iterator" 882 | ], 883 | "time": "2013-10-10 15:34:57" 884 | }, 885 | { 886 | "name": "phpunit/php-text-template", 887 | "version": "1.2.1", 888 | "source": { 889 | "type": "git", 890 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 891 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" 892 | }, 893 | "dist": { 894 | "type": "zip", 895 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 896 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 897 | "shasum": "" 898 | }, 899 | "require": { 900 | "php": ">=5.3.3" 901 | }, 902 | "type": "library", 903 | "autoload": { 904 | "classmap": [ 905 | "src/" 906 | ] 907 | }, 908 | "notification-url": "https://packagist.org/downloads/", 909 | "license": [ 910 | "BSD-3-Clause" 911 | ], 912 | "authors": [ 913 | { 914 | "name": "Sebastian Bergmann", 915 | "email": "sebastian@phpunit.de", 916 | "role": "lead" 917 | } 918 | ], 919 | "description": "Simple template engine.", 920 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 921 | "keywords": [ 922 | "template" 923 | ], 924 | "time": "2015-06-21 13:50:34" 925 | }, 926 | { 927 | "name": "phpunit/php-timer", 928 | "version": "1.0.7", 929 | "source": { 930 | "type": "git", 931 | "url": "https://github.com/sebastianbergmann/php-timer.git", 932 | "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b" 933 | }, 934 | "dist": { 935 | "type": "zip", 936 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3e82f4e9fc92665fafd9157568e4dcb01d014e5b", 937 | "reference": "3e82f4e9fc92665fafd9157568e4dcb01d014e5b", 938 | "shasum": "" 939 | }, 940 | "require": { 941 | "php": ">=5.3.3" 942 | }, 943 | "type": "library", 944 | "autoload": { 945 | "classmap": [ 946 | "src/" 947 | ] 948 | }, 949 | "notification-url": "https://packagist.org/downloads/", 950 | "license": [ 951 | "BSD-3-Clause" 952 | ], 953 | "authors": [ 954 | { 955 | "name": "Sebastian Bergmann", 956 | "email": "sb@sebastian-bergmann.de", 957 | "role": "lead" 958 | } 959 | ], 960 | "description": "Utility class for timing", 961 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 962 | "keywords": [ 963 | "timer" 964 | ], 965 | "time": "2015-06-21 08:01:12" 966 | }, 967 | { 968 | "name": "phpunit/php-token-stream", 969 | "version": "1.4.8", 970 | "source": { 971 | "type": "git", 972 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 973 | "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da" 974 | }, 975 | "dist": { 976 | "type": "zip", 977 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", 978 | "reference": "3144ae21711fb6cac0b1ab4cbe63b75ce3d4e8da", 979 | "shasum": "" 980 | }, 981 | "require": { 982 | "ext-tokenizer": "*", 983 | "php": ">=5.3.3" 984 | }, 985 | "require-dev": { 986 | "phpunit/phpunit": "~4.2" 987 | }, 988 | "type": "library", 989 | "extra": { 990 | "branch-alias": { 991 | "dev-master": "1.4-dev" 992 | } 993 | }, 994 | "autoload": { 995 | "classmap": [ 996 | "src/" 997 | ] 998 | }, 999 | "notification-url": "https://packagist.org/downloads/", 1000 | "license": [ 1001 | "BSD-3-Clause" 1002 | ], 1003 | "authors": [ 1004 | { 1005 | "name": "Sebastian Bergmann", 1006 | "email": "sebastian@phpunit.de" 1007 | } 1008 | ], 1009 | "description": "Wrapper around PHP's tokenizer extension.", 1010 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 1011 | "keywords": [ 1012 | "tokenizer" 1013 | ], 1014 | "time": "2015-09-15 10:49:45" 1015 | }, 1016 | { 1017 | "name": "phpunit/phpunit", 1018 | "version": "4.1.6", 1019 | "source": { 1020 | "type": "git", 1021 | "url": "https://github.com/sebastianbergmann/phpunit.git", 1022 | "reference": "241116219bb7e3b8111a36ffd8f37546888738d6" 1023 | }, 1024 | "dist": { 1025 | "type": "zip", 1026 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/241116219bb7e3b8111a36ffd8f37546888738d6", 1027 | "reference": "241116219bb7e3b8111a36ffd8f37546888738d6", 1028 | "shasum": "" 1029 | }, 1030 | "require": { 1031 | "ext-dom": "*", 1032 | "ext-json": "*", 1033 | "ext-pcre": "*", 1034 | "ext-reflection": "*", 1035 | "ext-spl": "*", 1036 | "php": ">=5.3.3", 1037 | "phpunit/php-code-coverage": "~2.0", 1038 | "phpunit/php-file-iterator": "~1.3.1", 1039 | "phpunit/php-text-template": "~1.2", 1040 | "phpunit/php-timer": "~1.0.2", 1041 | "phpunit/phpunit-mock-objects": "2.1.5", 1042 | "sebastian/comparator": "~1.0", 1043 | "sebastian/diff": "~1.1", 1044 | "sebastian/environment": "~1.0", 1045 | "sebastian/exporter": "~1.0", 1046 | "sebastian/version": "~1.0", 1047 | "symfony/yaml": "~2.0" 1048 | }, 1049 | "suggest": { 1050 | "phpunit/php-invoker": "~1.1" 1051 | }, 1052 | "bin": [ 1053 | "phpunit" 1054 | ], 1055 | "type": "library", 1056 | "extra": { 1057 | "branch-alias": { 1058 | "dev-master": "4.1.x-dev" 1059 | } 1060 | }, 1061 | "autoload": { 1062 | "classmap": [ 1063 | "src/" 1064 | ] 1065 | }, 1066 | "notification-url": "https://packagist.org/downloads/", 1067 | "include-path": [ 1068 | "", 1069 | "../../symfony/yaml/" 1070 | ], 1071 | "license": [ 1072 | "BSD-3-Clause" 1073 | ], 1074 | "authors": [ 1075 | { 1076 | "name": "Sebastian Bergmann", 1077 | "email": "sebastian@phpunit.de", 1078 | "role": "lead" 1079 | } 1080 | ], 1081 | "description": "The PHP Unit Testing framework.", 1082 | "homepage": "http://www.phpunit.de/", 1083 | "keywords": [ 1084 | "phpunit", 1085 | "testing", 1086 | "xunit" 1087 | ], 1088 | "time": "2014-08-17 08:07:02" 1089 | }, 1090 | { 1091 | "name": "phpunit/phpunit-mock-objects", 1092 | "version": "2.1.5", 1093 | "source": { 1094 | "type": "git", 1095 | "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", 1096 | "reference": "7878b9c41edb3afab92b85edf5f0981014a2713a" 1097 | }, 1098 | "dist": { 1099 | "type": "zip", 1100 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/7878b9c41edb3afab92b85edf5f0981014a2713a", 1101 | "reference": "7878b9c41edb3afab92b85edf5f0981014a2713a", 1102 | "shasum": "" 1103 | }, 1104 | "require": { 1105 | "php": ">=5.3.3", 1106 | "phpunit/php-text-template": "~1.2" 1107 | }, 1108 | "require-dev": { 1109 | "phpunit/phpunit": "~4.1" 1110 | }, 1111 | "suggest": { 1112 | "ext-soap": "*" 1113 | }, 1114 | "type": "library", 1115 | "extra": { 1116 | "branch-alias": { 1117 | "dev-master": "2.1.x-dev" 1118 | } 1119 | }, 1120 | "autoload": { 1121 | "classmap": [ 1122 | "src/" 1123 | ] 1124 | }, 1125 | "notification-url": "https://packagist.org/downloads/", 1126 | "include-path": [ 1127 | "" 1128 | ], 1129 | "license": [ 1130 | "BSD-3-Clause" 1131 | ], 1132 | "authors": [ 1133 | { 1134 | "name": "Sebastian Bergmann", 1135 | "email": "sb@sebastian-bergmann.de", 1136 | "role": "lead" 1137 | } 1138 | ], 1139 | "description": "Mock Object library for PHPUnit", 1140 | "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", 1141 | "keywords": [ 1142 | "mock", 1143 | "xunit" 1144 | ], 1145 | "time": "2014-06-12 07:22:15" 1146 | }, 1147 | { 1148 | "name": "sebastian/comparator", 1149 | "version": "1.2.0", 1150 | "source": { 1151 | "type": "git", 1152 | "url": "https://github.com/sebastianbergmann/comparator.git", 1153 | "reference": "937efb279bd37a375bcadf584dec0726f84dbf22" 1154 | }, 1155 | "dist": { 1156 | "type": "zip", 1157 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/937efb279bd37a375bcadf584dec0726f84dbf22", 1158 | "reference": "937efb279bd37a375bcadf584dec0726f84dbf22", 1159 | "shasum": "" 1160 | }, 1161 | "require": { 1162 | "php": ">=5.3.3", 1163 | "sebastian/diff": "~1.2", 1164 | "sebastian/exporter": "~1.2" 1165 | }, 1166 | "require-dev": { 1167 | "phpunit/phpunit": "~4.4" 1168 | }, 1169 | "type": "library", 1170 | "extra": { 1171 | "branch-alias": { 1172 | "dev-master": "1.2.x-dev" 1173 | } 1174 | }, 1175 | "autoload": { 1176 | "classmap": [ 1177 | "src/" 1178 | ] 1179 | }, 1180 | "notification-url": "https://packagist.org/downloads/", 1181 | "license": [ 1182 | "BSD-3-Clause" 1183 | ], 1184 | "authors": [ 1185 | { 1186 | "name": "Jeff Welch", 1187 | "email": "whatthejeff@gmail.com" 1188 | }, 1189 | { 1190 | "name": "Volker Dusch", 1191 | "email": "github@wallbash.com" 1192 | }, 1193 | { 1194 | "name": "Bernhard Schussek", 1195 | "email": "bschussek@2bepublished.at" 1196 | }, 1197 | { 1198 | "name": "Sebastian Bergmann", 1199 | "email": "sebastian@phpunit.de" 1200 | } 1201 | ], 1202 | "description": "Provides the functionality to compare PHP values for equality", 1203 | "homepage": "http://www.github.com/sebastianbergmann/comparator", 1204 | "keywords": [ 1205 | "comparator", 1206 | "compare", 1207 | "equality" 1208 | ], 1209 | "time": "2015-07-26 15:48:44" 1210 | }, 1211 | { 1212 | "name": "sebastian/diff", 1213 | "version": "1.4.1", 1214 | "source": { 1215 | "type": "git", 1216 | "url": "https://github.com/sebastianbergmann/diff.git", 1217 | "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" 1218 | }, 1219 | "dist": { 1220 | "type": "zip", 1221 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", 1222 | "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", 1223 | "shasum": "" 1224 | }, 1225 | "require": { 1226 | "php": ">=5.3.3" 1227 | }, 1228 | "require-dev": { 1229 | "phpunit/phpunit": "~4.8" 1230 | }, 1231 | "type": "library", 1232 | "extra": { 1233 | "branch-alias": { 1234 | "dev-master": "1.4-dev" 1235 | } 1236 | }, 1237 | "autoload": { 1238 | "classmap": [ 1239 | "src/" 1240 | ] 1241 | }, 1242 | "notification-url": "https://packagist.org/downloads/", 1243 | "license": [ 1244 | "BSD-3-Clause" 1245 | ], 1246 | "authors": [ 1247 | { 1248 | "name": "Kore Nordmann", 1249 | "email": "mail@kore-nordmann.de" 1250 | }, 1251 | { 1252 | "name": "Sebastian Bergmann", 1253 | "email": "sebastian@phpunit.de" 1254 | } 1255 | ], 1256 | "description": "Diff implementation", 1257 | "homepage": "https://github.com/sebastianbergmann/diff", 1258 | "keywords": [ 1259 | "diff" 1260 | ], 1261 | "time": "2015-12-08 07:14:41" 1262 | }, 1263 | { 1264 | "name": "sebastian/environment", 1265 | "version": "1.3.3", 1266 | "source": { 1267 | "type": "git", 1268 | "url": "https://github.com/sebastianbergmann/environment.git", 1269 | "reference": "6e7133793a8e5a5714a551a8324337374be209df" 1270 | }, 1271 | "dist": { 1272 | "type": "zip", 1273 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6e7133793a8e5a5714a551a8324337374be209df", 1274 | "reference": "6e7133793a8e5a5714a551a8324337374be209df", 1275 | "shasum": "" 1276 | }, 1277 | "require": { 1278 | "php": ">=5.3.3" 1279 | }, 1280 | "require-dev": { 1281 | "phpunit/phpunit": "~4.4" 1282 | }, 1283 | "type": "library", 1284 | "extra": { 1285 | "branch-alias": { 1286 | "dev-master": "1.3.x-dev" 1287 | } 1288 | }, 1289 | "autoload": { 1290 | "classmap": [ 1291 | "src/" 1292 | ] 1293 | }, 1294 | "notification-url": "https://packagist.org/downloads/", 1295 | "license": [ 1296 | "BSD-3-Clause" 1297 | ], 1298 | "authors": [ 1299 | { 1300 | "name": "Sebastian Bergmann", 1301 | "email": "sebastian@phpunit.de" 1302 | } 1303 | ], 1304 | "description": "Provides functionality to handle HHVM/PHP environments", 1305 | "homepage": "http://www.github.com/sebastianbergmann/environment", 1306 | "keywords": [ 1307 | "Xdebug", 1308 | "environment", 1309 | "hhvm" 1310 | ], 1311 | "time": "2015-12-02 08:37:27" 1312 | }, 1313 | { 1314 | "name": "sebastian/exporter", 1315 | "version": "1.2.1", 1316 | "source": { 1317 | "type": "git", 1318 | "url": "https://github.com/sebastianbergmann/exporter.git", 1319 | "reference": "7ae5513327cb536431847bcc0c10edba2701064e" 1320 | }, 1321 | "dist": { 1322 | "type": "zip", 1323 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/7ae5513327cb536431847bcc0c10edba2701064e", 1324 | "reference": "7ae5513327cb536431847bcc0c10edba2701064e", 1325 | "shasum": "" 1326 | }, 1327 | "require": { 1328 | "php": ">=5.3.3", 1329 | "sebastian/recursion-context": "~1.0" 1330 | }, 1331 | "require-dev": { 1332 | "phpunit/phpunit": "~4.4" 1333 | }, 1334 | "type": "library", 1335 | "extra": { 1336 | "branch-alias": { 1337 | "dev-master": "1.2.x-dev" 1338 | } 1339 | }, 1340 | "autoload": { 1341 | "classmap": [ 1342 | "src/" 1343 | ] 1344 | }, 1345 | "notification-url": "https://packagist.org/downloads/", 1346 | "license": [ 1347 | "BSD-3-Clause" 1348 | ], 1349 | "authors": [ 1350 | { 1351 | "name": "Jeff Welch", 1352 | "email": "whatthejeff@gmail.com" 1353 | }, 1354 | { 1355 | "name": "Volker Dusch", 1356 | "email": "github@wallbash.com" 1357 | }, 1358 | { 1359 | "name": "Bernhard Schussek", 1360 | "email": "bschussek@2bepublished.at" 1361 | }, 1362 | { 1363 | "name": "Sebastian Bergmann", 1364 | "email": "sebastian@phpunit.de" 1365 | }, 1366 | { 1367 | "name": "Adam Harvey", 1368 | "email": "aharvey@php.net" 1369 | } 1370 | ], 1371 | "description": "Provides the functionality to export PHP variables for visualization", 1372 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 1373 | "keywords": [ 1374 | "export", 1375 | "exporter" 1376 | ], 1377 | "time": "2015-06-21 07:55:53" 1378 | }, 1379 | { 1380 | "name": "sebastian/recursion-context", 1381 | "version": "1.0.2", 1382 | "source": { 1383 | "type": "git", 1384 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1385 | "reference": "913401df809e99e4f47b27cdd781f4a258d58791" 1386 | }, 1387 | "dist": { 1388 | "type": "zip", 1389 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791", 1390 | "reference": "913401df809e99e4f47b27cdd781f4a258d58791", 1391 | "shasum": "" 1392 | }, 1393 | "require": { 1394 | "php": ">=5.3.3" 1395 | }, 1396 | "require-dev": { 1397 | "phpunit/phpunit": "~4.4" 1398 | }, 1399 | "type": "library", 1400 | "extra": { 1401 | "branch-alias": { 1402 | "dev-master": "1.0.x-dev" 1403 | } 1404 | }, 1405 | "autoload": { 1406 | "classmap": [ 1407 | "src/" 1408 | ] 1409 | }, 1410 | "notification-url": "https://packagist.org/downloads/", 1411 | "license": [ 1412 | "BSD-3-Clause" 1413 | ], 1414 | "authors": [ 1415 | { 1416 | "name": "Jeff Welch", 1417 | "email": "whatthejeff@gmail.com" 1418 | }, 1419 | { 1420 | "name": "Sebastian Bergmann", 1421 | "email": "sebastian@phpunit.de" 1422 | }, 1423 | { 1424 | "name": "Adam Harvey", 1425 | "email": "aharvey@php.net" 1426 | } 1427 | ], 1428 | "description": "Provides functionality to recursively process PHP variables", 1429 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 1430 | "time": "2015-11-11 19:50:13" 1431 | }, 1432 | { 1433 | "name": "sebastian/version", 1434 | "version": "1.0.6", 1435 | "source": { 1436 | "type": "git", 1437 | "url": "https://github.com/sebastianbergmann/version.git", 1438 | "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" 1439 | }, 1440 | "dist": { 1441 | "type": "zip", 1442 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", 1443 | "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", 1444 | "shasum": "" 1445 | }, 1446 | "type": "library", 1447 | "autoload": { 1448 | "classmap": [ 1449 | "src/" 1450 | ] 1451 | }, 1452 | "notification-url": "https://packagist.org/downloads/", 1453 | "license": [ 1454 | "BSD-3-Clause" 1455 | ], 1456 | "authors": [ 1457 | { 1458 | "name": "Sebastian Bergmann", 1459 | "email": "sebastian@phpunit.de", 1460 | "role": "lead" 1461 | } 1462 | ], 1463 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1464 | "homepage": "https://github.com/sebastianbergmann/version", 1465 | "time": "2015-06-21 13:59:46" 1466 | }, 1467 | { 1468 | "name": "symfony/finder", 1469 | "version": "v3.0.0", 1470 | "source": { 1471 | "type": "git", 1472 | "url": "https://github.com/symfony/finder.git", 1473 | "reference": "3577eb98dba90721d1a0a3edfc6956ab8b1aecee" 1474 | }, 1475 | "dist": { 1476 | "type": "zip", 1477 | "url": "https://api.github.com/repos/symfony/finder/zipball/3577eb98dba90721d1a0a3edfc6956ab8b1aecee", 1478 | "reference": "3577eb98dba90721d1a0a3edfc6956ab8b1aecee", 1479 | "shasum": "" 1480 | }, 1481 | "require": { 1482 | "php": ">=5.5.9" 1483 | }, 1484 | "type": "library", 1485 | "extra": { 1486 | "branch-alias": { 1487 | "dev-master": "3.0-dev" 1488 | } 1489 | }, 1490 | "autoload": { 1491 | "psr-4": { 1492 | "Symfony\\Component\\Finder\\": "" 1493 | }, 1494 | "exclude-from-classmap": [ 1495 | "/Tests/" 1496 | ] 1497 | }, 1498 | "notification-url": "https://packagist.org/downloads/", 1499 | "license": [ 1500 | "MIT" 1501 | ], 1502 | "authors": [ 1503 | { 1504 | "name": "Fabien Potencier", 1505 | "email": "fabien@symfony.com" 1506 | }, 1507 | { 1508 | "name": "Symfony Community", 1509 | "homepage": "https://symfony.com/contributors" 1510 | } 1511 | ], 1512 | "description": "Symfony Finder Component", 1513 | "homepage": "https://symfony.com", 1514 | "time": "2015-10-30 23:35:59" 1515 | } 1516 | ], 1517 | "aliases": [], 1518 | "minimum-stability": "stable", 1519 | "stability-flags": [], 1520 | "prefer-stable": false, 1521 | "prefer-lowest": false, 1522 | "platform": [], 1523 | "platform-dev": [] 1524 | } 1525 | --------------------------------------------------------------------------------