├── .gitignore ├── .travis.yml ├── LICENSE ├── README.markdown ├── bin ├── config │ ├── databases.ini │ └── databases_test.ini └── phpchunkit ├── box.json ├── composer.json ├── docs ├── README.markdown ├── phar.markdown └── phpchunkit.png ├── phpchunkit.phar ├── phpchunkit.xml.dist ├── phpunit.xml.dist ├── src ├── ChunkRepository.php ├── ChunkResults.php ├── ChunkRunner.php ├── ChunkedTests.php ├── Command │ ├── BuildSandbox.php │ ├── CommandInterface.php │ ├── CreateDatabases.php │ ├── Generate.php │ ├── Run.php │ ├── Setup.php │ └── TestWatcher.php ├── Configuration.php ├── Container.php ├── DatabaseSandbox.php ├── Events.php ├── FileClassesHelper.php ├── GenerateTestClass.php ├── ListenerInterface.php ├── PHPChunkit.php ├── PHPChunkitApplication.php ├── Processes.php ├── TestChunker.php ├── TestCounter.php ├── TestFinder.php └── TestRunner.php └── tests ├── BaseTest.php ├── ChunkedTestsTest.php ├── Command ├── BuildSandboxTest.php ├── CreateDatabasesTest.php ├── RunTest.php ├── SetupTest.php └── TestWatcherTest.php ├── ConfigurationTest.php ├── ContainerTest.php ├── DatabaseSandboxTest.php ├── FileClassesHelperTest.php ├── FunctionalTest1Test.php ├── FunctionalTest2Test.php ├── FunctionalTest3Test.php ├── FunctionalTest4Test.php ├── FunctionalTest5Test.php ├── FunctionalTest6Test.php ├── FunctionalTest7Test.php ├── FunctionalTest8Test.php ├── GenerateTestClassTest.php ├── Listener ├── DatabasesCreate.php ├── SandboxCleanup.php └── SandboxPrepare.php ├── PHPChunkitApplicationTest.php ├── PHPChunkitTest.php ├── TestChunkerTest.php ├── TestCounterTest.php ├── TestFinderTest.php ├── TestRunnerTest.php ├── bootstrap.php └── phpchunkit_bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | cache: 6 | directories: 7 | - $HOME/.composer/cache 8 | 9 | php: 10 | - 7.0 11 | - 7.1 12 | - nightly 13 | 14 | env: 15 | - PHPUNIT=5.0.* COMPOSER_OPTS="--prefer-lowest" 16 | - PHPUNIT=5.1.* 17 | - PHPUNIT=5.2.* 18 | - PHPUNIT=5.3.* 19 | - PHPUNIT=5.4.* 20 | - PHPUNIT=5.5.* 21 | - PHPUNIT=5.6.* 22 | - PHPUNIT=5.7.* 23 | - PHPUNIT=6.0.* 24 | - PHPUNIT=6.1.* 25 | - PHPUNIT=6.2.* 26 | - PHPUNIT=6.3.* 27 | - PHPUNIT=6.4.* 28 | - PHPUNIT=6.5.* 29 | 30 | before_script: 31 | - phpenv config-rm xdebug.ini || true 32 | - composer self-update 33 | - composer require phpunit/phpunit:${PHPUNIT} ${COMPOSER_OPTS} 34 | - git config --global user.name travis-ci 35 | - git config --global user.email travis@example.com 36 | 37 | script: 38 | - ./bin/phpchunkit --exclude-group=functional --parallel=2 --num-chunks=4 --verbose --debug 39 | - ./bin/phpchunkit --group=functional --sandbox --create-dbs --parallel=2 --num-chunks=4 --verbose --debug 40 | 41 | git: 42 | depth: 1 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Jonathan H. Wage 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # PHPChunkit 2 | 3 | [![Build Status](https://secure.travis-ci.org/jwage/phpchunkit.png?branch=master)](http://travis-ci.org/jwage/phpchunkit) 4 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/jwage/phpchunkit/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/jwage/phpchunkit/?branch=master) 5 | [![Code Coverage](https://scrutinizer-ci.com/g/jwage/phpchunkit/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/jwage/phpchunkit/?branch=master) 6 | 7 | PHPChunkit is a library that sits on top of PHPUnit and adds additional 8 | functionality to make it easier to work with large unit and functional 9 | test suites. The primary feature is test chunking and database sandboxing 10 | which gives you the ability to run your tests in parallel chunks on the 11 | same server or across multiple servers. 12 | 13 | In order to run functional tests in parallel on the same server, you need to 14 | have a concept of database sandboxing. You are responsible for implementing 15 | the sandbox preparation, database creation, and sandbox cleanup. PHPChunkit 16 | provides a framework for you to hook in to so you can prepare your application 17 | environment sandbox. 18 | 19 | ## Parallel Execution Example 20 | 21 | Imagine you have 100 tests and each test takes 1 second. When the tests are 22 | ran serially, it will take 100 seconds to complete. But if you split the 100 23 | tests in to 10 even chunks and run the chunks in parallel, it will in theory 24 | take only 10 seconds to complete. 25 | 26 | Now imagine you have a two node Jenkins cluster. You can spread the run of each 27 | chunk across the 2 servers with 5 parallel jobs running on each server: 28 | 29 | ### Jenkins Server #1 with 5 job workers 30 | 31 | phpchunkit --num-chunks=10 --chunk=1 --sandbox --create-dbs 32 | phpchunkit --num-chunks=10 --chunk=2 --sandbox --create-dbs 33 | phpchunkit --num-chunks=10 --chunk=3 --sandbox --create-dbs 34 | phpchunkit --num-chunks=10 --chunk=4 --sandbox --create-dbs 35 | phpchunkit --num-chunks=10 --chunk=5 --sandbox --create-dbs 36 | 37 | ### Jenkins Server #2 with 5 job workers 38 | 39 | phpchunkit --num-chunks=10 --chunk=6 --sandbox --create-dbs 40 | phpchunkit --num-chunks=10 --chunk=7 --sandbox --create-dbs 41 | phpchunkit --num-chunks=10 --chunk=8 --sandbox --create-dbs 42 | phpchunkit --num-chunks=10 --chunk=9 --sandbox --create-dbs 43 | phpchunkit --num-chunks=10 --chunk=10 --sandbox --create-dbs 44 | 45 | ## Screenshot 46 | 47 | ![PHPChunkit Screenshot](https://raw.githubusercontent.com/jwage/PHPChunkit/master/docs/phpchunkit.png?1) 48 | 49 | ## Installation 50 | 51 | Install in your project with composer: 52 | 53 | composer require jwage/phpchunkit 54 | ./vendor/bin/phpchunkit 55 | 56 | Install globally with composer: 57 | 58 | composer global require jwage/phpchunkit 59 | ln -s /home/youruser/.composer/vendor/bin/phpchunkit /usr/local/bin/phpchunkit 60 | cd /path/to/your/project 61 | phpchunkit 62 | 63 | Install Phar: 64 | 65 | wget https://github.com/jwage/phpchunkit/raw/master/phpchunkit.phar 66 | chmod +x phpchunkit.phar 67 | sudo mv phpchunkit.phar /usr/local/bin/phpchunkit 68 | cd /path/to/your/project 69 | phpchunkit 70 | 71 | ## Setup 72 | 73 | As mentioned above in the introduction, you are responsible for implementing 74 | the sandbox preparation, database creation and sandbox cleanup processes 75 | by adding [EventDispatcher](http://symfony.com/doc/current/components/event_dispatcher.html) 76 | listeners. You can listen for the following events: 77 | 78 | - `sandbox.prepare` - Use the `Events::SANDBOX_PREPARE` constant. 79 | - `databases.create` - Use the `Events::DATABASES_CREATE` constant. 80 | - `sandbox.cleanup` - Use the `Events::SANDBOX_CLEANUP` constant. 81 | 82 | Take a look at the listeners implemented in this projects test suite for an example: 83 | 84 | - [SandboxPrepare.php](https://github.com/jwage/phpchunkit/blob/master/tests/Listener/SandboxPrepare.php) 85 | - [DatabasesCreate.php](https://github.com/jwage/phpchunkit/blob/master/tests/Listener/DatabasesCreate.php) 86 | - [SandboxCleanup.php](https://github.com/jwage/phpchunkit/blob/master/tests/Listener/SandboxCleanup.php) 87 | 88 | ### Configuration 89 | 90 | Here is an example `phpchunkit.xml` file. Place this in the root of your project: 91 | 92 | ```xml 93 | 94 | 95 | 103 | 104 | ./src 105 | ./tests 106 | 107 | 108 | 109 | testdb1 110 | testdb2 111 | 112 | 113 | 114 | 115 | PHPChunkit\Test\Listener\SandboxPrepare 116 | 117 | 118 | 119 | PHPChunkit\Test\Listener\SandboxCleanup 120 | 121 | 122 | 123 | PHPChunkit\Test\Listener\DatabasesCreate 124 | 125 | 126 | 127 | 128 | ``` 129 | 130 | The `tests/phpchunkit_bootstrap.php` file is loaded after the XML is loaded 131 | and gives you the ability to do more advanced things with the [Configuration](https://github.com/jwage/phpchunkit/blob/master/src/Configuration.php). 132 | 133 | Here is an example: 134 | 135 | ```php 136 | getRootDir(); 145 | 146 | $configuration = $configuration 147 | ->setWatchDirectories([ 148 | sprintf('%s/src', $rootDir), 149 | sprintf('%s/tests', $rootDir) 150 | ]) 151 | ->setTestsDirectory(sprintf('%s/tests', $rootDir)) 152 | ->setPhpunitPath(sprintf('%s/vendor/bin/phpunit', $rootDir)) 153 | ->setDatabaseNames(['testdb1', 'testdb2']) 154 | ->setMemoryLimit('256M') 155 | ->setNumChunks(2) 156 | ; 157 | 158 | $eventDispatcher = $configuration->getEventDispatcher(); 159 | 160 | $eventDispatcher->addListener(Events::SANDBOX_PREPARE, function() { 161 | // prepare the sandbox 162 | }); 163 | 164 | $eventDispatcher->addListener(Events::SANDBOX_CLEANUP, function() { 165 | // cleanup the sandbox 166 | }); 167 | 168 | $eventDispatcher->addListener(Events::DATABASES_CREATE, function() { 169 | // create databases 170 | }); 171 | ``` 172 | 173 | ## Available Commands 174 | 175 | Run all tests: 176 | 177 | phpchunkit 178 | 179 | Run just unit tests: 180 | 181 | phpchunkit --exclude-group=functional 182 | 183 | Run 4 chunks of tests across 2 parallel processes: 184 | 185 | phpchunkit --exclude-group=functional --num-chunks=4 --parallel=2 186 | 187 | Run all functional tests: 188 | 189 | phpchunkit --group=functional 190 | 191 | Run a specific chunk of functional tests: 192 | 193 | phpchunkit --num-chunks=5 --chunk=1 194 | 195 | Run test paths that match a filter: 196 | 197 | phpchunkit --filter=BuildSandbox 198 | 199 | Run a specific file: 200 | 201 | phpchunkit --file=tests/Command/BuildSandboxTest.php 202 | 203 | Run tests that contain the given content: 204 | 205 | phpchunkit --contains="SOME_CONSTANT_NAME" 206 | 207 | Run tests that do not contain the given content: 208 | 209 | phpchunkit --group=functional --not-contains="SOME_CONSTANT_NAME" 210 | 211 | Run tests for changed files: 212 | 213 | > Note: This relies on git to know which files have changed. 214 | 215 | phpchunkit --changed 216 | 217 | Watch your code for changes and run tests: 218 | 219 | phpchunkit watch 220 | 221 | Create databases: 222 | 223 | phpchunkit create-dbs 224 | 225 | Generate a test skeleton from a class: 226 | 227 | phpchunkit generate "MyProject\ClassName" 228 | 229 | Save the generated test to a file: 230 | 231 | phpchunkit generate "MyProject\ClassName" --file=tests/MyProject/Test/ClassNameTest.php 232 | 233 | Pass through options to PHPUnit when running tests: 234 | 235 | phpchunkit --phpunit-opt="--coverage-html /path/to/save/coverage" 236 | 237 | List all the available options: 238 | 239 | phpchunkit --help 240 | 241 | Help information for setting up PHPChunkit: 242 | 243 | phpchunkit setup 244 | 245 | ## Demo Project 246 | 247 | Take a look at [jwage/phpchunkit-demo](https://github.com/jwage/phpchunkit-demo) to see how it can be integrated in to an existing PHPUnit project. 248 | 249 | ## IRC 250 | 251 | Please join irc.freenode.net/phpchunkit to ask questions. 252 | -------------------------------------------------------------------------------- /bin/config/databases.ini: -------------------------------------------------------------------------------- 1 | database.testdb1.name = testdb1 2 | database.testdb2.name = testdb2 3 | -------------------------------------------------------------------------------- /bin/config/databases_test.ini: -------------------------------------------------------------------------------- 1 | database.testdb1.name = testdb1_test 2 | database.testdb2.name = testdb1_test 3 | -------------------------------------------------------------------------------- /bin/phpchunkit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | findAutoloaderPath(); 52 | 53 | require_once $autoloaderPath; 54 | 55 | $exitCode =(new PHPChunkit(getcwd(), new Container())) 56 | ->getContainer()['phpchunkit.application'] 57 | ->run( 58 | new ArgvInput(), 59 | new ConsoleOutput() 60 | ) 61 | ; 62 | 63 | exit($exitCode); 64 | -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "alias": "phpchunkit.phar", 3 | "chmod": "0755", 4 | "directories": ["src"], 5 | "files": [ 6 | "LICENSE", 7 | "bin/phpchunkit" 8 | ], 9 | "finder": [ 10 | { 11 | "name": "*.php", 12 | "exclude": ["Tests"], 13 | "in": "vendor" 14 | } 15 | ], 16 | "git-version": "dev-master", 17 | "main": "bin/phpchunkit", 18 | "output": "phpchunkit.phar", 19 | "stub": true 20 | } 21 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jwage/phpchunkit", 3 | "type": "library", 4 | "description": "PHPUnit Test Runner", 5 | "keywords": ["tests", "phpunit"], 6 | "homepage": "http://github.com/jwage/phpchunkit", 7 | "license": "MIT", 8 | "authors": [ 9 | {"name": "Jonathan H. Wage", "email": "jonwage@gmail.com"}, 10 | {"name": "Kris Wallsmith", "email": "kris.wallsmith@gmail.com"} 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "PHPChunkit\\": "src/", 15 | "PHPChunkit\\Test\\": "tests/" 16 | } 17 | }, 18 | "require": { 19 | "php": ">=7.0.0", 20 | "phpunit/phpunit": ">=5.0", 21 | "phpunit/phpunit-mock-objects": "^3.1|>=4.0", 22 | "sebastian/comparator": ">=1.2.3", 23 | "symfony/console": "^2.7|^3.0", 24 | "symfony/process": "^2.7|^3.0", 25 | "symfony/stopwatch": "^2.7|^3.0", 26 | "symfony/finder": "^2.7|^3.0", 27 | "symfony/event-dispatcher": "^2.7|^3.0", 28 | "phpunit/php-token-stream": ">=1.4", 29 | "pimple/pimple": "^3.0", 30 | "doctrine/inflector": "^1.1", 31 | "twig/twig": "^1.27" 32 | }, 33 | "require-dev": { 34 | "ext-pdo": "*", 35 | "ext-pdo_mysql": "*", 36 | "kherge/box": "^2.7" 37 | }, 38 | "bin": [ 39 | "bin/phpchunkit" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /docs/README.markdown: -------------------------------------------------------------------------------- 1 | # PHPChunkit Documentation 2 | 3 | - [Phar Build Process](phar.markdown) 4 | -------------------------------------------------------------------------------- /docs/phar.markdown: -------------------------------------------------------------------------------- 1 | # Phar 2 | 3 | ./vendor/bin/box build 4 | -------------------------------------------------------------------------------- /docs/phpchunkit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwage/phpchunkit/7a64a8d3bb5e392913fd4c949e16d461fcc97c38/docs/phpchunkit.png -------------------------------------------------------------------------------- /phpchunkit.phar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwage/phpchunkit/7a64a8d3bb5e392913fd4c949e16d461fcc97c38/phpchunkit.phar -------------------------------------------------------------------------------- /phpchunkit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | ./src 13 | ./tests 14 | 15 | 16 | 17 | testdb1 18 | testdb2 19 | 20 | 21 | 22 | 23 | PHPChunkit\Test\Listener\SandboxPrepare 24 | 25 | 26 | 27 | PHPChunkit\Test\Listener\SandboxCleanup 28 | 29 | 30 | 31 | PHPChunkit\Test\Listener\DatabasesCreate 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./tests/ 7 | 8 | 9 | 10 | 11 | 12 | ./src/ 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/ChunkRepository.php: -------------------------------------------------------------------------------- 1 | testFinder = $testFinder; 29 | $this->testChunker = $testChunker; 30 | $this->configuration = $configuration; 31 | } 32 | 33 | public function getChunkedTests(InputInterface $input) : ChunkedTests 34 | { 35 | $chunk = (int) $input->getOption('chunk'); 36 | 37 | $testFiles = $this->findTestFiles($input); 38 | 39 | $chunkedTests = (new ChunkedTests()) 40 | ->setNumChunks($this->getNumChunks($input)) 41 | ->setChunk($chunk) 42 | ; 43 | 44 | if (empty($testFiles)) { 45 | return $chunkedTests; 46 | } 47 | 48 | $this->testChunker->chunkTestFiles($chunkedTests, $testFiles); 49 | 50 | return $chunkedTests; 51 | } 52 | 53 | private function findTestFiles(InputInterface $input) 54 | { 55 | $files = $input->getOption('file'); 56 | 57 | if (!empty($files)) { 58 | return $files; 59 | } 60 | 61 | $groups = $input->getOption('group'); 62 | $excludeGroups = $input->getOption('exclude-group'); 63 | $changed = $input->getOption('changed'); 64 | $filters = $input->getOption('filter'); 65 | $contains = $input->getOption('contains'); 66 | $notContains = $input->getOption('not-contains'); 67 | 68 | $this->testFinder 69 | ->inGroups($groups) 70 | ->notInGroups($excludeGroups) 71 | ->changed($changed) 72 | ; 73 | 74 | foreach ($filters as $filter) { 75 | $this->testFinder->filter($filter); 76 | } 77 | 78 | foreach ($contains as $contain) { 79 | $this->testFinder->contains($contain); 80 | } 81 | 82 | foreach ($notContains as $notContain) { 83 | $this->testFinder->notContains($notContain); 84 | } 85 | 86 | return $this->testFinder->getFiles(); 87 | } 88 | 89 | 90 | private function getNumChunks(InputInterface $input) : int 91 | { 92 | return (int) $input->getOption('num-chunks') 93 | ?: $this->configuration->getNumChunks() ?: 1; 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/ChunkResults.php: -------------------------------------------------------------------------------- 1 | numAssertions += $num; 37 | } 38 | 39 | public function getNumAssertions() : int 40 | { 41 | return $this->numAssertions; 42 | } 43 | 44 | public function incrementNumFailures(int $num = 1) 45 | { 46 | $this->numFailures += $num; 47 | } 48 | 49 | public function getNumFailures() : int 50 | { 51 | return $this->numFailures; 52 | } 53 | 54 | public function incrementNumChunkFailures(int $num = 1) 55 | { 56 | $this->numChunkFailures += $num; 57 | } 58 | 59 | public function getNumChunkFailures() : int 60 | { 61 | return $this->numChunkFailures; 62 | } 63 | 64 | public function incrementTotalTestsRan(int $num = 1) 65 | { 66 | $this->totalTestsRan += $num; 67 | } 68 | 69 | public function getTotalTestsRan() : int 70 | { 71 | return $this->totalTestsRan; 72 | } 73 | 74 | public function addCode(int $code) 75 | { 76 | $this->codes[] = $code; 77 | } 78 | 79 | public function getCodes() : array 80 | { 81 | return $this->codes; 82 | } 83 | 84 | public function hasFailed() : bool 85 | { 86 | return array_sum($this->codes) ? true : false; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/ChunkRunner.php: -------------------------------------------------------------------------------- 1 | chunkedTests = $chunkedTests; 76 | $this->chunkResults = $chunkResults; 77 | $this->testRunner = $testRunner; 78 | $this->processes = $processes; 79 | $this->input = $input; 80 | $this->output = $output; 81 | $this->verbose = $verbose; 82 | $this->parallel = $parallel; 83 | $this->showProgressBar = !$this->verbose && !$this->parallel; 84 | } 85 | 86 | /** 87 | * @return null|integer 88 | */ 89 | public function runChunks() 90 | { 91 | $chunks = $this->chunkedTests->getChunks(); 92 | 93 | foreach ($chunks as $i => $chunk) { 94 | // drop and recreate dbs before running this chunk of tests 95 | if ($this->input->getOption('create-dbs')) { 96 | $this->testRunner->runTestCommand('create-dbs', [ 97 | '--quiet' => true, 98 | ]); 99 | } 100 | 101 | $chunkNum = $i + 1; 102 | 103 | $code = $this->runChunk($chunkNum, $chunk); 104 | 105 | if ($code > 0) { 106 | return $code; 107 | } 108 | } 109 | 110 | if ($this->parallel) { 111 | $this->processes->wait(); 112 | } 113 | } 114 | 115 | private function runChunk(int $chunkNum, array $chunk) 116 | { 117 | $numTests = $this->countNumTestsInChunk($chunk); 118 | 119 | $this->chunkResults->incrementTotalTestsRan($numTests); 120 | 121 | $process = $this->getChunkProcess($chunk); 122 | $this->processes->addProcess($process); 123 | 124 | $callback = $this->createProgressCallback($numTests); 125 | 126 | if ($this->parallel) { 127 | return $this->runChunkProcessParallel( 128 | $chunkNum, $process, $callback 129 | ); 130 | } 131 | 132 | return $this->runChunkProcessSerial( 133 | $chunkNum, $process, $callback 134 | ); 135 | } 136 | 137 | private function getChunkProcess(array $chunk) : Process 138 | { 139 | $files = $this->buildFilesFromChunk($chunk); 140 | 141 | $config = $this->testRunner->generatePhpunitXml($files); 142 | 143 | $command = sprintf('-c %s', $config); 144 | 145 | return $this->testRunner->getPhpunitProcess($command); 146 | } 147 | 148 | private function createProgressCallback(int $numTests) : Closure 149 | { 150 | if ($this->showProgressBar) { 151 | $this->progressBar = $this->createChunkProgressBar($numTests); 152 | 153 | return $this->createProgressBarCallback($this->progressBar); 154 | } 155 | 156 | if ($this->verbose) { 157 | return function($type, $out) { 158 | $this->extractDataFromPhpunitOutput($out); 159 | 160 | $this->output->write($out); 161 | }; 162 | } 163 | 164 | return function($type, $out) { 165 | $this->extractDataFromPhpunitOutput($out); 166 | }; 167 | } 168 | 169 | private function createProgressBarCallback(ProgressBar $progressBar) 170 | { 171 | return function(string $type, string $buffer) use ($progressBar) { 172 | $this->extractDataFromPhpunitOutput($buffer); 173 | 174 | if ($progressBar) { 175 | if (in_array($buffer, ['F', 'E'])) { 176 | $progressBar->setBarCharacter('='); 177 | } 178 | 179 | if (in_array($buffer, ['F', 'E', 'S', '.'])) { 180 | $progressBar->advance(); 181 | } 182 | } 183 | }; 184 | } 185 | 186 | private function runChunkProcessParallel( 187 | int $chunkNum, 188 | Process $process, 189 | Closure $callback) 190 | { 191 | $this->output->writeln(sprintf('Starting chunk #%s', $chunkNum)); 192 | 193 | $process->start($callback); 194 | 195 | $this->processes->wait(); 196 | } 197 | 198 | private function runChunkProcessSerial( 199 | int $chunkNum, 200 | Process $process, 201 | Closure $callback) 202 | { 203 | if ($this->verbose) { 204 | $this->output->writeln(''); 205 | $this->output->writeln(sprintf('Running chunk #%s', $chunkNum)); 206 | } 207 | 208 | $this->chunkResults->addCode($code = $process->run($callback)); 209 | 210 | if ($code > 0) { 211 | $this->chunkResults->incrementNumChunkFailures(); 212 | 213 | if ($this->verbose) { 214 | $this->output->writeln(sprintf('Chunk #%s FAILED', $chunkNum)); 215 | } 216 | 217 | if ($this->input->getOption('stop')) { 218 | $this->output->writeln(''); 219 | $this->output->writeln($process->getOutput()); 220 | 221 | return $code; 222 | } 223 | } 224 | 225 | if (!$this->verbose) { 226 | $this->progressBar->finish(); 227 | $this->output->writeln(''); 228 | } 229 | 230 | if ($code > 0) { 231 | $this->output->writeln(''); 232 | 233 | if (!$this->verbose) { 234 | $this->output->writeln($process->getOutput()); 235 | } 236 | } 237 | } 238 | 239 | private function countNumTestsInChunk(array $chunk) : int 240 | { 241 | return array_sum(array_map(function(array $chunkFile) { 242 | return $chunkFile['numTests']; 243 | }, $chunk)); 244 | } 245 | 246 | private function buildFilesFromChunk(array $chunk) : array 247 | { 248 | return array_map(function(array $chunkFile) { 249 | return $chunkFile['file']; 250 | }, $chunk); 251 | } 252 | 253 | private function extractDataFromPhpunitOutput(string $outputBuffer) : int 254 | { 255 | preg_match_all('/([0-9]+) assertions/', $outputBuffer, $matches); 256 | 257 | if (isset($matches[1][0])) { 258 | $this->chunkResults->incrementNumAssertions((int) $matches[1][0]); 259 | } 260 | 261 | preg_match_all('/Assertions: ([0-9]+)/', $outputBuffer, $matches); 262 | 263 | if (isset($matches[1][0])) { 264 | $this->chunkResults->incrementNumAssertions((int) $matches[1][0]); 265 | } 266 | 267 | preg_match_all('/Failures: ([0-9]+)/', $outputBuffer, $matches); 268 | 269 | if (isset($matches[1][0])) { 270 | $this->chunkResults->incrementNumFailures((int) $matches[1][0]); 271 | } 272 | 273 | return 0; 274 | } 275 | 276 | private function createChunkProgressBar(int $numTests) : ProgressBar 277 | { 278 | $progressBar = new ProgressBar($this->output, $numTests); 279 | $progressBar->setBarCharacter('='); 280 | $progressBar->setProgressCharacter("\xF0\x9F\x8C\xAD"); 281 | 282 | return $progressBar; 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/ChunkedTests.php: -------------------------------------------------------------------------------- 1 | chunk; 40 | } 41 | 42 | public function setChunk(int $chunk) : self 43 | { 44 | $this->chunk = $chunk; 45 | 46 | return $this; 47 | } 48 | 49 | public function getNumChunks() : int 50 | { 51 | return $this->numChunks; 52 | } 53 | 54 | public function setNumChunks(int $numChunks) : self 55 | { 56 | $this->numChunks = $numChunks; 57 | 58 | return $this; 59 | } 60 | 61 | public function getTestsPerChunk() : int 62 | { 63 | return $this->testsPerChunk; 64 | } 65 | 66 | public function setTestsPerChunk(int $testsPerChunk) : self 67 | { 68 | $this->testsPerChunk = $testsPerChunk; 69 | 70 | return $this; 71 | } 72 | 73 | public function getChunks() : array 74 | { 75 | return $this->chunks; 76 | } 77 | 78 | public function setChunks(array $chunks) : self 79 | { 80 | $this->chunks = $chunks; 81 | 82 | return $this; 83 | } 84 | 85 | public function getTotalTests() : int 86 | { 87 | return $this->totalTests; 88 | } 89 | 90 | public function setTotalTests(int $totalTests) : self 91 | { 92 | $this->totalTests = $totalTests; 93 | 94 | return $this; 95 | } 96 | 97 | public function hasTests() : bool 98 | { 99 | return $this->totalTests ? true : false; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Command/BuildSandbox.php: -------------------------------------------------------------------------------- 1 | testRunner = $testRunner; 35 | $this->eventDispatcher = $eventDispatcher; 36 | } 37 | 38 | public function getName() : string 39 | { 40 | return self::NAME; 41 | } 42 | 43 | public function configure(Command $command) 44 | { 45 | $command 46 | ->setDescription('Build a sandbox for a test run.') 47 | ->addOption('create-dbs', null, InputOption::VALUE_NONE, 'Create the test databases after building the sandbox.') 48 | ; 49 | } 50 | 51 | public function execute(InputInterface $input, OutputInterface $output) 52 | { 53 | $this->eventDispatcher->dispatch(Events::SANDBOX_PREPARE); 54 | 55 | if ($input->getOption('create-dbs')) { 56 | $this->testRunner->runTestCommand('create-dbs', [ 57 | '--sandbox' => true, 58 | ]); 59 | } 60 | 61 | register_shutdown_function(function() use ($output) { 62 | $output->writeln('Cleaning up sandbox...'); 63 | 64 | $this->eventDispatcher->dispatch(Events::SANDBOX_CLEANUP); 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Command/CommandInterface.php: -------------------------------------------------------------------------------- 1 | eventDispatcher = $eventDispatcher; 29 | } 30 | 31 | public function getName() : string 32 | { 33 | return self::NAME; 34 | } 35 | 36 | public function configure(Command $command) 37 | { 38 | $command 39 | ->setDescription('Create the test databases.') 40 | ->addOption('sandbox', null, InputOption::VALUE_NONE, 'Prepare sandbox before creating databases.') 41 | ; 42 | } 43 | 44 | public function execute(InputInterface $input, OutputInterface $output) 45 | { 46 | $this->eventDispatcher->dispatch(Events::DATABASES_CREATE); 47 | 48 | if (!$input->getOption('quiet')) { 49 | $output->writeln('Done creating databases!'); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Command/Generate.php: -------------------------------------------------------------------------------- 1 | generateTestClass = $generateTestClass; 28 | } 29 | 30 | public function getName() : string 31 | { 32 | return self::NAME; 33 | } 34 | 35 | public function configure(Command $command) 36 | { 37 | $command 38 | ->setDescription('Generate a test skeleton from a class.') 39 | ->addArgument('class', InputArgument::REQUIRED, 'Class to generate test for.') 40 | ->addOption('file', null, InputOption::VALUE_REQUIRED, 'File path to write test to.') 41 | ; 42 | } 43 | 44 | public function execute(InputInterface $input, OutputInterface $output) 45 | { 46 | $class = $input->getArgument('class'); 47 | 48 | $code = $this->generateTestClass->generate($class); 49 | 50 | if ($file = $input->getOption('file')) { 51 | if (file_exists($file)) { 52 | throw new \InvalidArgumentException(sprintf('%s already exists.', $file)); 53 | } 54 | 55 | $output->writeln(sprintf('Writing test to %s', $file)); 56 | 57 | file_put_contents($file, $code); 58 | } else { 59 | $output->write($code); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Command/Run.php: -------------------------------------------------------------------------------- 1 | testRunner = $testRunner; 116 | $this->configuration = $configuration; 117 | $this->testChunker = $testChunker; 118 | $this->testFinder = $testFinder; 119 | } 120 | 121 | public function getName() : string 122 | { 123 | return self::NAME; 124 | } 125 | 126 | public function configure(Command $command) 127 | { 128 | $command 129 | ->setDescription('Run tests.') 130 | ->addOption('debug', null, InputOption::VALUE_NONE, 'Run tests in debug mode.') 131 | ->addOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for PHP.') 132 | ->addOption('stop', null, InputOption::VALUE_NONE, 'Stop on failure or error.') 133 | ->addOption('failed', null, InputOption::VALUE_NONE, 'Track tests that have failed.') 134 | ->addOption('create-dbs', null, InputOption::VALUE_NONE, 'Create the test databases before running tests.') 135 | ->addOption('sandbox', null, InputOption::VALUE_NONE, 'Configure unique names.') 136 | ->addOption('chunk', null, InputOption::VALUE_REQUIRED, 'Run a specific chunk of tests.') 137 | ->addOption('num-chunks', null, InputOption::VALUE_REQUIRED, 'The number of chunks to run tests in.') 138 | ->addOption('group', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Run all tests in these groups.') 139 | ->addOption('exclude-group', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Run all tests excluding these groups.') 140 | ->addOption('changed', null, InputOption::VALUE_NONE, 'Run changed tests.') 141 | ->addOption('parallel', null, InputOption::VALUE_REQUIRED, 'Run test chunks in parallel.') 142 | ->addOption('filter', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Filter tests by path/file name and run them.') 143 | ->addOption('contains', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Run tests that match the given content.') 144 | ->addOption('not-contains', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Run tests that do not match the given content.') 145 | ->addOption('file', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Run test file.') 146 | ->addOption('phpunit-opt', null, InputOption::VALUE_REQUIRED, 'Pass through phpunit options.') 147 | ; 148 | } 149 | 150 | public function execute(InputInterface $input, OutputInterface $output) 151 | { 152 | $this->initialize($input, $output); 153 | 154 | $this->chunkedTests = $this->chunkRepository->getChunkedTests( 155 | $this->input 156 | ); 157 | 158 | $this->chunkRunner = new ChunkRunner( 159 | $this->chunkedTests, 160 | $this->chunkResults, 161 | $this->testRunner, 162 | $this->processes, 163 | $this->input, 164 | $this->output, 165 | $this->verbose, 166 | $this->parallel 167 | ); 168 | 169 | if (!$this->chunkedTests->hasTests()) { 170 | $this->output->writeln('No tests found to run.'); 171 | 172 | return; 173 | } 174 | 175 | $this->outputHeader(); 176 | 177 | $this->setupSandbox(); 178 | 179 | $this->chunkRunner->runChunks(); 180 | 181 | $this->outputFooter(); 182 | 183 | return $this->chunkResults->hasFailed() ? 1 : 0; 184 | } 185 | 186 | private function initialize(InputInterface $input, OutputInterface $output) 187 | { 188 | $this->stopwatch = new Stopwatch(); 189 | $this->stopwatch->start('Tests'); 190 | 191 | $this->input = $input; 192 | $this->output = $output; 193 | $this->verbose = $output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE; 194 | $this->numParallelProcesses = (int) $this->input->getOption('parallel'); 195 | $this->parallel = $this->numParallelProcesses > 1; 196 | $this->stop = (bool) $this->input->getOption('stop'); 197 | 198 | $this->chunkRepository = new ChunkRepository( 199 | $this->testFinder, 200 | $this->testChunker, 201 | $this->configuration 202 | ); 203 | $this->chunkResults = new ChunkResults(); 204 | $this->processes = new Processes( 205 | $this->chunkResults, 206 | $this->output, 207 | $this->numParallelProcesses, 208 | $this->verbose, 209 | $this->stop 210 | ); 211 | } 212 | 213 | private function outputHeader() 214 | { 215 | $chunks = $this->chunkedTests->getChunks(); 216 | $testsPerChunk = $this->chunkedTests->getTestsPerChunk(); 217 | $totalTests = $this->chunkedTests->getTotalTests(); 218 | $numChunks = $this->chunkedTests->getNumChunks(); 219 | 220 | $this->output->writeln(sprintf('Total Tests: %s', $totalTests)); 221 | $this->output->writeln(sprintf('Number of Chunks Configured: %s', $numChunks)); 222 | $this->output->writeln(sprintf('Number of Chunks Produced: %s', count($chunks))); 223 | $this->output->writeln(sprintf('Tests Per Chunk: ~%s', $testsPerChunk)); 224 | 225 | if ($chunk = $this->chunkedTests->getChunk()) { 226 | $this->output->writeln(sprintf('Chunk: %s', $chunk)); 227 | } 228 | 229 | $this->output->writeln('-----------'); 230 | $this->output->writeln(''); 231 | } 232 | 233 | private function setupSandbox() 234 | { 235 | if ($this->input->getOption('sandbox')) { 236 | $this->testRunner->runTestCommand('sandbox'); 237 | } 238 | } 239 | 240 | private function outputFooter() 241 | { 242 | $chunks = $this->chunkedTests->getChunks(); 243 | 244 | $failed = $this->chunkResults->hasFailed(); 245 | 246 | $event = $this->stopwatch->stop('Tests'); 247 | 248 | $this->output->writeln(''); 249 | $this->output->writeln(sprintf('Time: %s seconds, Memory: %s', 250 | round($event->getDuration() / 1000, 2), 251 | $this->formatBytes($event->getMemory()) 252 | )); 253 | 254 | $this->output->writeln(''); 255 | $this->output->writeln(sprintf('%s (%s chunks, %s tests, %s assertions, %s failures%s)', 256 | $failed ? 'FAILED' : 'PASSED', 257 | count($chunks), 258 | $this->chunkResults->getTotalTestsRan(), 259 | $this->chunkResults->getNumAssertions(), 260 | $this->chunkResults->getNumFailures(), 261 | $failed ? sprintf(', Failed chunks: %s', $this->chunkResults->getNumChunkFailures()) : '' 262 | )); 263 | } 264 | 265 | private function formatBytes(int $size, int $precision = 2) : string 266 | { 267 | if (!$size) { 268 | return 0; 269 | } 270 | 271 | $base = log($size, 1024); 272 | $suffixes = ['', 'KB', 'MB', 'GB', 'TB']; 273 | 274 | return round(pow(1024, $base - floor($base)), $precision).$suffixes[floor($base)]; 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/Command/Setup.php: -------------------------------------------------------------------------------- 1 | setDescription('Help with setting up PHPChunkit.'); 33 | } 34 | 35 | public function execute(InputInterface $input, OutputInterface $output) 36 | { 37 | $io = new SymfonyStyle($input, $output); 38 | 39 | $io->title(sprintf('%s (%s)', Container::NAME, Container::VERSION)); 40 | 41 | $io->text('PHPChunkit - An advanced PHP test runner built on top of PHPUnit.'); 42 | 43 | $io->section('Setup PHPChunkit to get started!'); 44 | 45 | $io->text('Place the XML below in phpchunkit.xml.dist in the root of your project.'); 46 | 47 | $io->text(''); 48 | 49 | $io->text(explode("\n", << 51 | 52 | 60 | 61 | ./src 62 | ./tests 63 | 64 | 65 | 66 | CONFIG 67 | )); 68 | 69 | $io->text('Place the PHP below in tests/phpchunkit_bootstrap.php in the root of your project to do more advanced configuration'); 70 | 71 | 72 | $io->text(explode("\n", << 74 | 80 | CONFIG 81 | )); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Command/TestWatcher.php: -------------------------------------------------------------------------------- 1 | testRunner = $testRunner; 45 | $this->configuration = $configuration; 46 | $this->fileClassesHelper = $fileClassesHelper; 47 | } 48 | 49 | public function getName() : string 50 | { 51 | return self::NAME; 52 | } 53 | 54 | public function configure(Command $command) 55 | { 56 | $command 57 | ->setDescription('Watch for changes to files and run the associated tests.') 58 | ->addOption('debug', null, InputOption::VALUE_NONE, 'Run tests in debug mode.') 59 | ->addOption('memory-limit', null, InputOption::VALUE_REQUIRED, 'Memory limit for PHP.') 60 | ->addOption('stop', null, InputOption::VALUE_NONE, 'Stop on failure or error.') 61 | ->addOption('failed', null, InputOption::VALUE_REQUIRED, 'Track tests that have failed.', true) 62 | ; 63 | } 64 | 65 | public function execute(InputInterface $input, OutputInterface $output) 66 | { 67 | if (empty($this->configuration->getWatchDirectories())) { 68 | throw new \InvalidArgumentException( 69 | 'In order to use the watch feature you must configure watch directories.' 70 | ); 71 | } 72 | 73 | $output->writeln('Watching for changes to your code.'); 74 | 75 | $lastTime = time(); 76 | 77 | while ($this->while()) { 78 | $this->sleep(); 79 | 80 | $finder = $this->createFinder(); 81 | 82 | foreach ($finder as $file) { 83 | $lastTime = $this->checkFile($file, $lastTime); 84 | } 85 | } 86 | } 87 | 88 | protected function sleep() 89 | { 90 | usleep(300000); 91 | } 92 | 93 | protected function while () : bool 94 | { 95 | return true; 96 | } 97 | 98 | private function checkFile(SplFileInfo $file, int $lastTime) : int 99 | { 100 | $fileLastModified = $file->getMTime(); 101 | 102 | if ($fileLastModified > $lastTime) { 103 | 104 | $lastTime = $fileLastModified; 105 | 106 | if (!$this->isTestFile($file)) { 107 | // TODO figure out a better way 108 | // We have to wait a litte bit to look at the contents of the 109 | // file because it might be empty because of the save operation. 110 | usleep(10000); 111 | 112 | $files = $this->findAssociatedTestFiles($file); 113 | 114 | if (empty($files)) { 115 | return $lastTime; 116 | } 117 | } else { 118 | $files = [$file->getPathName()]; 119 | } 120 | 121 | $this->testRunner->runTestFiles($files); 122 | } 123 | 124 | return $lastTime; 125 | } 126 | 127 | private function createFinder() : Finder 128 | { 129 | return Finder::create() 130 | ->files() 131 | ->name('*.php') 132 | ->in($this->configuration->getWatchDirectories()) 133 | ; 134 | } 135 | 136 | private function isTestFile(SplFileInfo $file) : bool 137 | { 138 | return strpos($file->getPathName(), 'Test.php') !== false; 139 | } 140 | 141 | private function findAssociatedTestFiles(SplFileInfo $file) : array 142 | { 143 | $classes = $this->getClassesInsideFile($file->getPathName()); 144 | 145 | $testFiles = []; 146 | 147 | foreach ($classes as $className) { 148 | 149 | $reflectionClass = new \ReflectionClass($className); 150 | 151 | $docComment = $reflectionClass->getDocComment(); 152 | 153 | if ($docComment !== false) { 154 | preg_match_all('/@testClass\s(.*)/', $docComment, $testClasses); 155 | 156 | if (isset($testClasses[1]) && $testClasses[1]) { 157 | foreach ($testClasses[1] as $className) { 158 | $testFiles[] = (new \ReflectionClass($className))->getFileName(); 159 | } 160 | } 161 | } 162 | } 163 | 164 | return $testFiles; 165 | } 166 | 167 | private function getClassesInsideFile(string $file) : array 168 | { 169 | return $this->fileClassesHelper->getFileClasses($file); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/Configuration.php: -------------------------------------------------------------------------------- 1 | attributes(); 70 | 71 | $xmlMappings = [ 72 | 'root-dir' => [ 73 | 'type' => 'string', 74 | 'setter' => 'setRootDir' 75 | ], 76 | 'bootstrap' => [ 77 | 'type' => 'string', 78 | 'setter' => 'setBootstrapPath' 79 | ], 80 | 'tests-dir' => [ 81 | 'type' => 'string', 82 | 'setter' => 'setTestsDirectory' 83 | ], 84 | 'phpunit-path' => [ 85 | 'type' => 'string', 86 | 'setter' => 'setPhpunitPath' 87 | ], 88 | 'memory-limit' => [ 89 | 'type' => 'string', 90 | 'setter' => 'setMemoryLimit' 91 | ], 92 | 'num-chunks' => [ 93 | 'type' => 'integer', 94 | 'setter' => 'setNumChunks' 95 | ], 96 | 'watch-directories' => [ 97 | 'type' => 'array', 98 | 'setter' => 'setWatchDirectories', 99 | 'xmlName' => 'watch-directory', 100 | ], 101 | 'database-names' => [ 102 | 'type' => 'array', 103 | 'setter' => 'setDatabaseNames', 104 | 'xmlName' => 'database-name', 105 | ], 106 | ]; 107 | 108 | foreach ($xmlMappings as $name => $mapping) { 109 | $value = null; 110 | 111 | if ($mapping['type'] === 'array') { 112 | $value = (array) $xml->{$name}->{$mapping['xmlName']}; 113 | } elseif (isset($attributes[$name])) { 114 | $value = $attributes[$name]; 115 | 116 | settype($value, $mapping['type']); 117 | } 118 | 119 | if ($value !== null) { 120 | $configuration->{$mapping['setter']}($value); 121 | } 122 | } 123 | 124 | $events = (array) $xml->{'events'}; 125 | $listeners = $events['listener'] ?? null; 126 | 127 | if ($listeners) { 128 | foreach ($listeners as $listener) { 129 | $configuration->addListener( 130 | (string) $listener->attributes()['event'], 131 | (string) $listener->class 132 | ); 133 | } 134 | } 135 | 136 | return $configuration; 137 | } 138 | 139 | public function addListener( 140 | string $eventName, 141 | string $className, 142 | int $priority = 0) : ListenerInterface 143 | { 144 | $listener = new $className($this); 145 | 146 | if (!$listener instanceof ListenerInterface) { 147 | throw new InvalidArgumentException( 148 | sprintf('%s does not implement %s', $className, ListenerInterface::class) 149 | ); 150 | } 151 | 152 | $this->getEventDispatcher()->addListener( 153 | $eventName, [$listener, 'execute'], $priority 154 | ); 155 | 156 | return $listener; 157 | } 158 | 159 | public function setRootDir(string $rootDir) : self 160 | { 161 | return $this->setPath('rootDir', $rootDir); 162 | } 163 | 164 | public function getRootDir() : string 165 | { 166 | return $this->rootDir; 167 | } 168 | 169 | public function setWatchDirectories(array $watchDirectories) : self 170 | { 171 | foreach ($watchDirectories as $key => $watchDirectory) { 172 | if (!is_dir($watchDirectory)) { 173 | throw new \InvalidArgumentException( 174 | sprintf('Watch directory "%s" does not exist.', $watchDirectory) 175 | ); 176 | } 177 | 178 | $watchDirectories[$key] = realpath($watchDirectory); 179 | } 180 | 181 | $this->watchDirectories = $watchDirectories; 182 | 183 | return $this; 184 | } 185 | 186 | public function getWatchDirectories() : array 187 | { 188 | return $this->watchDirectories; 189 | } 190 | 191 | public function setTestsDirectory(string $testsDirectory) : self 192 | { 193 | return $this->setPath('testsDirectory', $testsDirectory); 194 | } 195 | 196 | public function getTestsDirectory() : string 197 | { 198 | return $this->testsDirectory; 199 | } 200 | 201 | public function setBootstrapPath(string $bootstrapPath) : self 202 | { 203 | return $this->setPath('bootstrapPath', $bootstrapPath); 204 | } 205 | 206 | public function getBootstrapPath() : string 207 | { 208 | return $this->bootstrapPath; 209 | } 210 | 211 | public function setPhpunitPath(string $phpunitPath) : self 212 | { 213 | return $this->setPath('phpunitPath', $phpunitPath); 214 | } 215 | 216 | public function getPhpunitPath() : string 217 | { 218 | return $this->phpunitPath; 219 | } 220 | 221 | public function setDatabaseSandbox(DatabaseSandbox $databaseSandbox) : self 222 | { 223 | $this->databaseSandbox = $databaseSandbox; 224 | 225 | return $this; 226 | } 227 | 228 | public function getDatabaseSandbox() : DatabaseSandbox 229 | { 230 | if ($this->databaseSandbox === null) { 231 | $this->databaseSandbox = new DatabaseSandbox(); 232 | } 233 | 234 | return $this->databaseSandbox; 235 | } 236 | 237 | public function setDatabaseNames(array $databaseNames) : self 238 | { 239 | $this->getDatabaseSandbox()->setDatabaseNames($databaseNames); 240 | 241 | return $this; 242 | } 243 | 244 | public function setSandboxEnabled(bool $sandboxEnabled) : self 245 | { 246 | $this->getDatabaseSandbox()->setSandboxEnabled($sandboxEnabled); 247 | 248 | return $this; 249 | } 250 | 251 | public function setMemoryLimit(string $memoryLimit) : self 252 | { 253 | $this->memoryLimit = $memoryLimit; 254 | 255 | return $this; 256 | } 257 | 258 | public function getMemoryLimit() : string 259 | { 260 | return $this->memoryLimit; 261 | } 262 | 263 | public function setNumChunks(int $numChunks) : self 264 | { 265 | $this->numChunks = $numChunks; 266 | 267 | return $this; 268 | } 269 | 270 | public function getNumChunks() : int 271 | { 272 | return $this->numChunks; 273 | } 274 | 275 | public function setEventDispatcher(EventDispatcher $eventDispatcher) : self 276 | { 277 | $this->eventDispatcher = $eventDispatcher; 278 | 279 | return $this; 280 | } 281 | 282 | public function getEventDispatcher() : EventDispatcher 283 | { 284 | if ($this->eventDispatcher === null) { 285 | $this->eventDispatcher = new EventDispatcher(); 286 | } 287 | 288 | return $this->eventDispatcher; 289 | } 290 | 291 | public function isSetup() 292 | { 293 | $setup = true; 294 | 295 | if (!$this->rootDir) { 296 | $setup = false; 297 | } 298 | 299 | if (!$this->testsDirectory) { 300 | $setup = false; 301 | } 302 | 303 | return $setup; 304 | } 305 | 306 | private function setPath(string $name, string $path) : self 307 | { 308 | if (!file_exists($path)) { 309 | throw new \InvalidArgumentException( 310 | sprintf('%s "%s" does not exist.', $name, $path) 311 | ); 312 | } 313 | 314 | $this->$name = realpath($path); 315 | 316 | return $this; 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/Container.php: -------------------------------------------------------------------------------- 1 | getConfiguration(); 21 | }; 22 | 23 | $this['phpchunkit.symfony_application'] = function() { 24 | return new Application(self::NAME, self::VERSION); 25 | }; 26 | 27 | $this['phpchunkit.application'] = function() { 28 | return new PHPChunkitApplication($this); 29 | }; 30 | 31 | $this['phpchunkit.database_sandbox'] = function() { 32 | return $this['phpchunkit.configuration']->getDatabaseSandbox(); 33 | }; 34 | 35 | $this['phpchunkit.event_dispatcher'] = function() { 36 | return $this['phpchunkit.configuration']->getEventDispatcher(); 37 | }; 38 | 39 | $this['phpchunkit.test_chunker'] = function() { 40 | return new TestChunker($this['phpchunkit.test_counter']); 41 | }; 42 | 43 | $this['phpchunkit.test_runner'] = function() { 44 | return new TestRunner( 45 | $this['phpchunkit.symfony_application'], 46 | $this['phpchunkit.application.input'], 47 | $this['phpchunkit.application.output'], 48 | $this['phpchunkit.configuration'] 49 | ); 50 | }; 51 | 52 | $this['phpchunkit.test_counter'] = function() { 53 | return new TestCounter( 54 | $this['phpchunkit.file_classes_helper'] 55 | ); 56 | }; 57 | 58 | $this['phpchunkit.test_finder'] = function() { 59 | return new TestFinder( 60 | $this['phpchunkit.configuration']->getTestsDirectory() 61 | ); 62 | }; 63 | 64 | $this['phpchunkit.command.setup'] = function() { 65 | return new Command\Setup(); 66 | }; 67 | 68 | $this['phpchunkit.command.test_watcher'] = function() { 69 | return new Command\TestWatcher( 70 | $this['phpchunkit.test_runner'], 71 | $this['phpchunkit.configuration'], 72 | $this['phpchunkit.file_classes_helper'] 73 | ); 74 | }; 75 | 76 | $this['phpchunkit.command.run'] = function() { 77 | return new Command\Run( 78 | $this['phpchunkit.test_runner'], 79 | $this['phpchunkit.configuration'], 80 | $this['phpchunkit.test_chunker'], 81 | $this['phpchunkit.test_finder'] 82 | ); 83 | }; 84 | 85 | $this['phpchunkit.command.create_databases'] = function() { 86 | return new Command\CreateDatabases($this['phpchunkit.event_dispatcher']); 87 | }; 88 | 89 | $this['phpchunkit.command.build_sandbox'] = function() { 90 | return new Command\BuildSandbox( 91 | $this['phpchunkit.test_runner'], 92 | $this['phpchunkit.event_dispatcher'] 93 | ); 94 | }; 95 | 96 | $this['phpchunkit.command.generate_test'] = function() { 97 | return new Command\Generate(new GenerateTestClass()); 98 | }; 99 | 100 | $this['phpchunkit.file_classes_helper'] = function() { 101 | return new FileClassesHelper(); 102 | }; 103 | } 104 | 105 | private function getConfiguration() : Configuration 106 | { 107 | $configuration = $this->loadConfiguration(); 108 | 109 | $this->loadPHPChunkitBootstrap($configuration); 110 | 111 | // try to guess watch directories 112 | if (!$configuration->getWatchDirectories()) { 113 | $paths = [ 114 | sprintf('%s/src', $configuration->getRootDir()), 115 | sprintf('%s/lib', $configuration->getRootDir()), 116 | sprintf('%s/tests', $configuration->getRootDir()), 117 | ]; 118 | 119 | $watchDirectories = []; 120 | foreach ($paths as $path) { 121 | if (is_dir($path)) { 122 | $watchDirectories[] = $path; 123 | } 124 | } 125 | 126 | $configuration->setWatchDirectories($watchDirectories); 127 | } 128 | 129 | // try to guess tests directory 130 | if (!$configuration->getTestsDirectory()) { 131 | $paths = [ 132 | sprintf('%s/tests', $configuration->getRootDir()), 133 | sprintf('%s/src', $configuration->getRootDir()), 134 | sprintf('%s/lib', $configuration->getRootDir()), 135 | ]; 136 | 137 | foreach ($paths as $path) { 138 | if (is_dir($path)) { 139 | $configuration->setTestsDirectory($path); 140 | break; 141 | } 142 | } 143 | } 144 | 145 | return $configuration; 146 | } 147 | 148 | private function loadConfiguration() : Configuration 149 | { 150 | $xmlPath = $this->findPHPChunkitXmlPath(); 151 | 152 | $configuration = $xmlPath 153 | ? Configuration::createFromXmlFile($xmlPath) 154 | : new Configuration() 155 | ; 156 | 157 | $configuration->setSandboxEnabled($this->isSandboxEnabled()); 158 | 159 | if (!$configuration->getRootDir()) { 160 | $configuration->setRootDir($this['phpchunkit.root_dir']); 161 | } 162 | 163 | return $configuration; 164 | } 165 | 166 | private function isSandboxEnabled() : bool 167 | { 168 | return array_filter($_SERVER['argv'], function($arg) { 169 | return strpos($arg, 'sandbox') !== false; 170 | }) ? true : false; 171 | } 172 | 173 | /** 174 | * @return null|string 175 | */ 176 | private function findPHPChunkitXmlPath() 177 | { 178 | if (file_exists($distXmlPath = $this['phpchunkit.root_dir'].'/phpchunkit.xml.dist')) { 179 | return $distXmlPath; 180 | } 181 | 182 | if (file_exists($defaultXmlPath = $this['phpchunkit.root_dir'].'/phpchunkit.xml')) { 183 | return $defaultXmlPath; 184 | } 185 | } 186 | 187 | /** 188 | * @throws InvalidArgumentException 189 | */ 190 | private function loadPHPChunkitBootstrap(Configuration $configuration) 191 | { 192 | if ($bootstrapPath = $configuration->getBootstrapPath()) { 193 | if (!file_exists($bootstrapPath)) { 194 | throw new \InvalidArgumentException( 195 | sprintf('Bootstrap path "%s" does not exist.', $bootstrapPath) 196 | ); 197 | } 198 | 199 | require_once $bootstrapPath; 200 | } else { 201 | $autoloaderPath = sprintf('%s/vendor/autoload.php', $configuration->getRootDir()); 202 | 203 | if (file_exists($autoloaderPath)) { 204 | require_once $autoloaderPath; 205 | } 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/DatabaseSandbox.php: -------------------------------------------------------------------------------- 1 | sandboxEnabled = $sandboxEnabled; 32 | $this->databaseNames = $databaseNames; 33 | } 34 | 35 | public function getSandboxEnabled() : bool 36 | { 37 | return $this->sandboxEnabled; 38 | } 39 | 40 | public function setSandboxEnabled(bool $sandboxEnabled) 41 | { 42 | $this->sandboxEnabled = $sandboxEnabled; 43 | } 44 | 45 | public function getDatabaseNames() : array 46 | { 47 | return $this->databaseNames; 48 | } 49 | 50 | public function setDatabaseNames(array $databaseNames) 51 | { 52 | $this->databaseNames = $databaseNames; 53 | } 54 | 55 | public function getTestDatabaseNames() : array 56 | { 57 | $databaseNames = []; 58 | 59 | foreach ($this->databaseNames as $databaseName) { 60 | $databaseNames[$databaseName] = sprintf(self::SANDBOXED_DATABASE_NAME_PATTERN, 61 | $databaseName, 'test' 62 | ); 63 | } 64 | 65 | return $databaseNames; 66 | } 67 | 68 | public function getSandboxedDatabaseNames() : array 69 | { 70 | $this->initialize(); 71 | 72 | return $this->sandboxDatabaseNames; 73 | } 74 | 75 | protected function generateUniqueId() : string 76 | { 77 | return uniqid(); 78 | } 79 | 80 | private function initialize() 81 | { 82 | if (empty($this->sandboxDatabaseNames)) { 83 | $this->sandboxDatabaseNames = $this->generateDatabaseNames(); 84 | } 85 | } 86 | 87 | private function generateDatabaseNames() : array 88 | { 89 | $databaseNames = []; 90 | 91 | foreach ($this->databaseNames as $databaseName) { 92 | if ($this->sandboxEnabled) { 93 | $databaseNames[$databaseName] = sprintf(self::SANDBOXED_DATABASE_NAME_PATTERN, 94 | $databaseName, $this->generateUniqueId() 95 | ); 96 | } else { 97 | $databaseNames[$databaseName] = sprintf(self::SANDBOXED_DATABASE_NAME_PATTERN, 98 | $databaseName, 'test' 99 | ); 100 | } 101 | } 102 | 103 | return $databaseNames; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Events.php: -------------------------------------------------------------------------------- 1 | =')) { 27 | $extraTypes .= '|enum'; 28 | } 29 | 30 | // Use @ here instead of Silencer to actively suppress 'unhelpful' output 31 | // @link https://github.com/composer/composer/pull/4886 32 | $contents = @php_strip_whitespace($path); 33 | if (!$contents) { 34 | return []; 35 | } 36 | 37 | // return early if there is no chance of matching anything in this file 38 | if (!preg_match('{\b(?:class|interface'.$extraTypes.')\s}i', $contents)) { 39 | return array(); 40 | } 41 | 42 | // strip heredocs/nowdocs 43 | $contents = preg_replace('{<<<\s*(\'?)(\w+)\\1(?:\r\n|\n|\r)(?:.*?)(?:\r\n|\n|\r)\\2(?=\r\n|\n|\r|;)}s', 'null', $contents); 44 | // strip strings 45 | $contents = preg_replace('{"[^"\\\\]*+(\\\\.[^"\\\\]*+)*+"|\'[^\'\\\\]*+(\\\\.[^\'\\\\]*+)*+\'}s', 'null', $contents); 46 | // strip leading non-php code if needed 47 | if (substr($contents, 0, 2) !== '.+<\?}s', '?>'); 57 | if (false !== $pos && false === strpos(substr($contents, $pos), '])(?Pclass|interface'.$extraTypes.') \s++ (?P[a-zA-Z_\x7f-\xff:][a-zA-Z0-9_\x7f-\xff:\-]*+) 64 | | \b(?])(?Pnamespace) (?P\s++[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\s*+\\\\\s*+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+)? \s*+ [\{;] 65 | ) 66 | }ix', $contents, $matches); 67 | 68 | $classes = array(); 69 | $namespace = ''; 70 | 71 | for ($i = 0, $len = count($matches['type']); $i < $len; $i++) { 72 | if (!empty($matches['ns'][$i])) { 73 | $namespace = str_replace(array(' ', "\t", "\r", "\n"), '', $matches['nsname'][$i]).'\\'; 74 | } else { 75 | $name = $matches['name'][$i]; 76 | // skip anon classes extending/implementing 77 | if ($name === 'extends' || $name === 'implements') { 78 | continue; 79 | } 80 | if ($name[0] === ':') { 81 | // This is an XHP class, https://github.com/facebook/xhp 82 | $name = 'xhp'.substr(str_replace(array('-', ':'), array('_', '__'), $name), 1); 83 | } elseif ($matches['type'][$i] === 'enum') { 84 | // In Hack, something like: 85 | // enum Foo: int { HERP = '123'; } 86 | // The regex above captures the colon, which isn't part of 87 | // the class name. 88 | $name = rtrim($name, ':'); 89 | } 90 | $classes[] = ltrim($namespace.$name, '\\'); 91 | } 92 | } 93 | 94 | return $classes; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/GenerateTestClass.php: -------------------------------------------------------------------------------- 1 | container = new Container(); 52 | \$this->command = new {{ classShortName }}(); 53 | \$this->command->setContainer(\$this->container); 54 | } 55 | 56 | public function testExecute() 57 | { 58 | } 59 | } 60 | 61 | EOF; 62 | 63 | /** 64 | * @var ReflectionClass 65 | */ 66 | private $reflectionClass; 67 | 68 | /** 69 | * @var string 70 | */ 71 | private $classShortName; 72 | 73 | /** 74 | * @var string 75 | */ 76 | private $classCamelCaseName; 77 | 78 | /** 79 | * @var string 80 | */ 81 | private $testNamespace; 82 | 83 | /** 84 | * @var string 85 | */ 86 | private $testClassShortName; 87 | 88 | /** 89 | * @var string 90 | */ 91 | private $useStatementsCode; 92 | 93 | /** 94 | * @var string 95 | */ 96 | private $testPropertiesCode; 97 | 98 | /** 99 | * @var string 100 | */ 101 | private $setUpCode; 102 | 103 | /** 104 | * @var string 105 | */ 106 | private $testMethodsCode; 107 | 108 | public function generate(string $className) : string 109 | { 110 | $this->reflectionClass = new ReflectionClass($className); 111 | 112 | $this->classShortName = $this->reflectionClass->getShortName(); 113 | $this->classCamelCaseName = Inflector::camelize($this->classShortName); 114 | $this->testNamespace = preg_replace('/(.*)Bundle/', '$0\Tests', $this->reflectionClass->getNamespaceName()); 115 | $this->testClassShortName = $this->classShortName.'Test'; 116 | 117 | $this->useStatementsCode = $this->generateUseStatements(); 118 | $this->testPropertiesCode = $this->generateClassProperties(); 119 | $this->setUpCode = $this->generateSetUp(); 120 | $this->testMethodsCode = $this->generateTestMethods(); 121 | 122 | $twig = new \Twig_Environment(new \Twig_Loader_String(), [ 123 | 'autoescape' => false, 124 | ]); 125 | 126 | $template = self::CLASS_TEMPLATE; 127 | 128 | return $twig->render($template, [ 129 | 'classShortName' => $this->classShortName, 130 | 'classCamelCaseName' => $this->classCamelCaseName, 131 | 'namespace' => $this->testNamespace, 132 | 'shortName' => $this->testClassShortName, 133 | 'methods' => $this->testMethodsCode, 134 | 'properties' => $this->testPropertiesCode, 135 | 'useStatements' => $this->useStatementsCode, 136 | 'setUpCode' => $this->setUpCode, 137 | ]); 138 | } 139 | 140 | private function generateClassProperties() : string 141 | { 142 | $testPropertiesCode = []; 143 | 144 | if ($parameters = $this->getConstructorParameters()) { 145 | foreach ($parameters as $key => $parameter) { 146 | $isLast = $key === count($parameters) - 1; 147 | 148 | if ($parameterClass = $parameter->getClass()) { 149 | $testPropertiesCode[] = ' /**'; 150 | $testPropertiesCode[] = ' * @var '.$parameterClass->getShortName(); 151 | $testPropertiesCode[] = ' */'; 152 | $testPropertiesCode[] = ' private $'.$parameter->name.';'; 153 | 154 | if (!$isLast) { 155 | $testPropertiesCode[] = ''; 156 | } 157 | } else { 158 | $testPropertiesCode[] = ' /**'; 159 | $testPropertiesCode[] = ' * @var TODO'; 160 | $testPropertiesCode[] = ' */'; 161 | $testPropertiesCode[] = ' private $'.$parameter->name.';'; 162 | 163 | if (!$isLast) { 164 | $testPropertiesCode[] = ''; 165 | } 166 | } 167 | } 168 | } 169 | 170 | if (!empty($parameters)) { 171 | $testPropertiesCode[] = ''; 172 | } 173 | 174 | $testPropertiesCode[] = ' /**'; 175 | $testPropertiesCode[] = ' * @var '.$this->classShortName; 176 | $testPropertiesCode[] = ' */'; 177 | $testPropertiesCode[] = ' private $'.$this->classCamelCaseName.';'; 178 | 179 | return implode("\n", $testPropertiesCode); 180 | } 181 | 182 | private function generateSetUp() : string 183 | { 184 | $classShortName = $this->reflectionClass->getShortName(); 185 | $classCamelCaseName = Inflector::camelize($classShortName); 186 | 187 | $setUpCode = []; 188 | $setUpCode[] = ' protected function setUp()'; 189 | $setUpCode[] = ' {'; 190 | 191 | if ($parameters = $this->getConstructorParameters()) { 192 | foreach ($parameters as $parameter) { 193 | if ($parameterClass = $parameter->getClass()) { 194 | $setUpCode[] = sprintf(' $this->%s = $this->%s(%s::class);', 195 | $parameter->name, 196 | $this->getPHPUnitMockMethod(), 197 | $parameterClass->getShortName() 198 | ); 199 | } else { 200 | $setUpCode[] = sprintf(" \$this->%s = ''; // TODO", 201 | $parameter->name 202 | ); 203 | } 204 | } 205 | 206 | $setUpCode[] = ''; 207 | $setUpCode[] = sprintf(' $this->%s = new %s(', $classCamelCaseName, $classShortName); 208 | 209 | // arguments for class being tested 210 | $setUpCodeArguments = []; 211 | foreach ($parameters as $parameter) { 212 | $setUpCodeArguments[] = sprintf(' $this->%s', $parameter->name); 213 | } 214 | $setUpCode[] = implode(",\n", $setUpCodeArguments); 215 | 216 | $setUpCode[] = ' );'; 217 | } else { 218 | $setUpCode[] = sprintf(' $this->%s = new %s();', $classCamelCaseName, $classShortName); 219 | } 220 | 221 | $setUpCode[] = ' }'; 222 | 223 | return implode("\n", $setUpCode); 224 | } 225 | 226 | private function getConstructorParameters() : array 227 | { 228 | $constructor = $this->reflectionClass->getConstructor(); 229 | 230 | if ($constructor) { 231 | return $constructor->getParameters(); 232 | } 233 | 234 | return []; 235 | } 236 | 237 | private function generateTestMethods() : string 238 | { 239 | $testMethodsCode = []; 240 | 241 | foreach ($this->reflectionClass->getMethods() as $method) { 242 | if (!$this->isMethodTestable($method)) { 243 | continue; 244 | } 245 | 246 | $testMethodsCode[] = sprintf(' public function test%s()', ucfirst($method->name)); 247 | $testMethodsCode[] = ' {'; 248 | $testMethodsCode[] = $this->generateTestMethodBody($method); 249 | $testMethodsCode[] = ' }'; 250 | $testMethodsCode[] = ''; 251 | } 252 | 253 | return ' '.trim(implode("\n", $testMethodsCode)); 254 | } 255 | 256 | private function generateTestMethodBody(ReflectionMethod $method) : string 257 | { 258 | $parameters = $method->getParameters(); 259 | 260 | $testMethodBodyCode = []; 261 | 262 | if (!empty($parameters)) { 263 | foreach ($parameters as $parameter) { 264 | if ($parameterClass = $parameter->getClass()) { 265 | $testMethodBodyCode[] = sprintf( 266 | ' $%s = $this->%s(%s::class);', 267 | $parameter->name, 268 | $this->getPHPUnitMockMethod(), 269 | $parameterClass->getShortName() 270 | ); 271 | } else { 272 | $testMethodBodyCode[] = sprintf(" \$%s = '';", $parameter->name); 273 | } 274 | } 275 | 276 | $testMethodBodyCode[] = ''; 277 | $testMethodBodyCode[] = sprintf(' $this->%s->%s(', $this->classCamelCaseName, $method->name); 278 | 279 | $testMethodParameters = []; 280 | foreach ($parameters as $parameter) { 281 | $testMethodParameters[] = sprintf('$%s', $parameter->name); 282 | } 283 | 284 | $testMethodBodyCode[] = ' '.implode(",\n ", $testMethodParameters); 285 | $testMethodBodyCode[] = ' );'; 286 | } else { 287 | $testMethodBodyCode[] = sprintf(' $this->%s->%s();', $this->classCamelCaseName, $method->name); 288 | } 289 | 290 | return implode("\n", $testMethodBodyCode); 291 | } 292 | 293 | private function generateUseStatements() : string 294 | { 295 | $dependencies = []; 296 | $dependencies[] = $this->reflectionClass->name; 297 | $dependencies[] = TestCase::class; 298 | 299 | if ($parameters = $this->getConstructorParameters()) { 300 | foreach ($parameters as $parameter) { 301 | if (!$parameterClass = $parameter->getClass()) { 302 | continue; 303 | } 304 | 305 | $dependencies[] = $parameterClass->getName(); 306 | } 307 | } 308 | 309 | foreach ($this->reflectionClass->getMethods() as $method) { 310 | if (!$this->isMethodTestable($method)) { 311 | continue; 312 | } 313 | 314 | foreach ($method->getParameters() as $parameter) { 315 | if (!$parameterClass = $parameter->getClass()) { 316 | continue; 317 | } 318 | 319 | $dependencies[] = $parameterClass->getName(); 320 | } 321 | } 322 | 323 | sort($dependencies); 324 | 325 | $dependencies = array_unique($dependencies); 326 | 327 | $useStatementsCode = array_map(function($dependency) { 328 | return sprintf('use %s;', $dependency); 329 | }, $dependencies); 330 | 331 | return implode("\n", $useStatementsCode); 332 | } 333 | 334 | private function isMethodTestable(ReflectionMethod $method) : bool 335 | { 336 | if ($this->reflectionClass->name !== $method->class) { 337 | return false; 338 | } 339 | 340 | return substr($method->name, 0, 2) !== '__' && $method->isPublic(); 341 | } 342 | 343 | /** 344 | * @return string 345 | */ 346 | private function getPHPUnitMockMethod() 347 | { 348 | foreach (['createMock', 'getMock'] as $method) { 349 | try { 350 | new \ReflectionMethod(TestCase::class, $method); 351 | return $method; 352 | } catch (\ReflectionException $e) { 353 | } 354 | } 355 | 356 | throw new \RuntimeException('Unable to detect PHPUnit version'); 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /src/ListenerInterface.php: -------------------------------------------------------------------------------- 1 | container = $container ?: new Container(); 20 | $this->container['phpchunkit'] = $this; 21 | $this->container['phpchunkit.root_dir'] = $rootDir; 22 | $this->container->initialize(); 23 | } 24 | 25 | public function getContainer() : Container 26 | { 27 | return $this->container; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/PHPChunkitApplication.php: -------------------------------------------------------------------------------- 1 | container = $container; 44 | $this->symfonyApplication = $this->container['phpchunkit.symfony_application']; 45 | $this->symfonyApplication->setAutoExit(false); 46 | } 47 | 48 | public function run(InputInterface $input, OutputInterface $output) : int 49 | { 50 | $this->container['phpchunkit.application.input'] = $input; 51 | $this->container['phpchunkit.application.output'] = $output; 52 | 53 | foreach (self::$commands as $serviceName) { 54 | $this->registerCommand($serviceName); 55 | } 56 | 57 | return $this->runSymfonyApplication($input, $output); 58 | } 59 | 60 | public function registerCommand(string $serviceName) 61 | { 62 | $service = $this->container[$serviceName]; 63 | 64 | $symfonyCommand = $this->register($service->getName()); 65 | 66 | $service->configure($symfonyCommand); 67 | 68 | $symfonyCommand->setCode(function($input, $output) use ($service) { 69 | if (!$service instanceof Command\Setup) { 70 | $configuration = $this->container['phpchunkit.configuration']; 71 | 72 | if (!$configuration->isSetup()) { 73 | return $this->symfonyApplication 74 | ->find('setup')->run($input, $output); 75 | } 76 | } 77 | 78 | return call_user_func_array([$service, 'execute'], [$input, $output]); 79 | }); 80 | } 81 | 82 | protected function runSymfonyApplication( 83 | InputInterface $input, 84 | OutputInterface $output) : int 85 | { 86 | return $this->symfonyApplication->run($input, $output); 87 | } 88 | 89 | private function register(string $name) : SymfonyCommand 90 | { 91 | return $this->symfonyApplication->register($name); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Processes.php: -------------------------------------------------------------------------------- 1 | chunkResults = $chunkResults; 50 | $this->output = $output; 51 | $this->numParallelProcesses = $numParallelProcesses; 52 | $this->verbose = $verbose; 53 | $this->stop = $stop; 54 | } 55 | 56 | public function addProcess(Process $process) 57 | { 58 | $this->processes[] = $process; 59 | } 60 | 61 | public function wait() 62 | { 63 | if (count($this->processes) < $this->numParallelProcesses) { 64 | return; 65 | } 66 | 67 | while (count($this->processes)) { 68 | foreach ($this->processes as $i => $process) { 69 | $chunkNum = $i + 1; 70 | 71 | if ($process->isRunning()) { 72 | continue; 73 | } 74 | 75 | unset($this->processes[$i]); 76 | 77 | $this->chunkResults->addCode($code = $process->getExitCode()); 78 | 79 | if ($code > 0) { 80 | $this->chunkResults->incrementNumChunkFailures(); 81 | 82 | $this->output->writeln(sprintf('Chunk #%s FAILED', $chunkNum)); 83 | 84 | $this->output->writeln(''); 85 | $this->output->write($process->getOutput()); 86 | 87 | if ($this->stop) { 88 | return $code; 89 | } 90 | } else { 91 | $this->output->writeln(sprintf('Chunk #%s PASSED', $chunkNum)); 92 | 93 | if ($this->verbose) { 94 | $this->output->writeln(''); 95 | $this->output->write($process->getOutput()); 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/TestChunker.php: -------------------------------------------------------------------------------- 1 | testCounter = $testCounter; 20 | } 21 | 22 | public function chunkTestFiles(ChunkedTests $chunkedTests, array $testFiles) 23 | { 24 | $chunk = $chunkedTests->getChunk(); 25 | $numChunks = $chunkedTests->getNumChunks(); 26 | 27 | $fileTestCounts = $this->getTestFileCounts($testFiles); 28 | 29 | $totalTests = array_sum($fileTestCounts); 30 | $testsPerChunk = (int) round($totalTests / $numChunks, 0); 31 | 32 | $chunks = [[]]; 33 | 34 | $numTestsInChunk = 0; 35 | foreach ($testFiles as $file) { 36 | $numTestsInFile = $fileTestCounts[$file]; 37 | 38 | $chunkFile = [ 39 | 'file' => $file, 40 | 'numTests' => $numTestsInFile, 41 | ]; 42 | 43 | // start a new chunk 44 | if ($numTestsInChunk >= $testsPerChunk) { 45 | $chunks[] = [$chunkFile]; 46 | $numTestsInChunk = $numTestsInFile; 47 | 48 | // add file to current chunk 49 | } else { 50 | $chunks[count($chunks) - 1][] = $chunkFile; 51 | $numTestsInChunk += $numTestsInFile; 52 | } 53 | } 54 | 55 | if ($chunk) { 56 | $chunkOffset = $chunk - 1; 57 | 58 | if (isset($chunks[$chunkOffset]) && $chunks[$chunkOffset]) { 59 | $chunks = [$chunkOffset => $chunks[$chunkOffset]]; 60 | } else { 61 | $chunks = []; 62 | } 63 | } 64 | 65 | $chunkedTests->setChunks($chunks); 66 | $chunkedTests->setTotalTests($totalTests); 67 | $chunkedTests->setTestsPerChunk($testsPerChunk); 68 | } 69 | 70 | private function getTestFileCounts(array $testFiles) : array 71 | { 72 | $fileTestCounts = []; 73 | 74 | foreach ($testFiles as $file) { 75 | $fileTestCounts[$file] = $this->testCounter->countNumTestsInFile($file); 76 | } 77 | 78 | return $fileTestCounts; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/TestCounter.php: -------------------------------------------------------------------------------- 1 | fileClassesHelper = $fileClassesHelper; 32 | $this->cachePath = sprintf('%s/testcounter.cache', sys_get_temp_dir()); 33 | 34 | $this->loadCache(); 35 | } 36 | 37 | public function __destruct() 38 | { 39 | $this->writeCache(); 40 | } 41 | 42 | public function countNumTestsInFile(string $file) : int 43 | { 44 | $cacheKey = $file.@filemtime($file); 45 | 46 | if (isset($this->cache[$cacheKey])) { 47 | return $this->cache[$cacheKey]; 48 | } 49 | 50 | $numTestsInFile = 0; 51 | 52 | $classes = $this->fileClassesHelper->getFileClasses($file); 53 | 54 | if (empty($classes)) { 55 | $this->cache[$cacheKey] = $numTestsInFile; 56 | 57 | return $numTestsInFile; 58 | } 59 | 60 | $className = $classes[0]; 61 | 62 | require_once $file; 63 | 64 | foreach ($classes as $className) { 65 | $numTestsInFile += $this->countNumTestsInClass($className); 66 | } 67 | 68 | $this->cache[$cacheKey] = $numTestsInFile; 69 | 70 | return $numTestsInFile; 71 | } 72 | 73 | public function clearCache() 74 | { 75 | if (file_exists($this->cachePath)) { 76 | unlink($this->cachePath); 77 | } 78 | 79 | $this->cache = []; 80 | } 81 | 82 | protected function loadCache() 83 | { 84 | if (file_exists($this->cachePath)) { 85 | $this->cache = include($this->cachePath); 86 | } 87 | } 88 | 89 | protected function writeCache() 90 | { 91 | file_put_contents($this->cachePath, 'cache, true).';'); 92 | } 93 | 94 | private function countNumTestsInClass(string $className) : int 95 | { 96 | $reflectionClass = new ReflectionClass($className); 97 | 98 | if ($reflectionClass->isAbstract()) { 99 | return 0; 100 | } 101 | 102 | $numTests = 0; 103 | 104 | $methods = $reflectionClass->getMethods(); 105 | 106 | foreach ($methods as $method) { 107 | if (strpos($method->name, 'test') === 0) { 108 | $docComment = $method->getDocComment(); 109 | 110 | if ($docComment !== false) { 111 | preg_match_all('/@dataProvider\s([a-zA-Z0-9_]+)/', $docComment, $dataProvider); 112 | 113 | if (isset($dataProvider[1][0])) { 114 | $providerMethod = $dataProvider[1][0]; 115 | 116 | $test = new $className(); 117 | 118 | $numTests = $numTests + count($test->$providerMethod()); 119 | 120 | continue; 121 | } 122 | } 123 | 124 | $numTests++; 125 | } else { 126 | $docComment = $method->getDocComment(); 127 | 128 | if ($docComment !== false) { 129 | preg_match_all('/@test/', $docComment, $tests); 130 | 131 | if ($tests[0]) { 132 | $numTests = $numTests + count($tests[0]); 133 | } 134 | } 135 | } 136 | } 137 | 138 | return $numTests; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/TestFinder.php: -------------------------------------------------------------------------------- 1 | testsDirectory = $testsDirectory; 32 | } 33 | 34 | public function changed(bool $changed = true) : self 35 | { 36 | $this->changed = $changed; 37 | 38 | return $this; 39 | } 40 | 41 | public function filter(string $filter = null) : self 42 | { 43 | $this->getFinder()->path($filter); 44 | 45 | return $this; 46 | } 47 | 48 | public function contains(string $contains = null) : self 49 | { 50 | $this->getFinder()->contains($contains); 51 | 52 | return $this; 53 | } 54 | 55 | public function notContains(string $notContains = null) : self 56 | { 57 | $this->getFinder()->notContains($notContains); 58 | 59 | return $this; 60 | } 61 | 62 | public function inGroup(string $group = null) : self 63 | { 64 | $this->getFinder()->contains(sprintf('@group %s', $group)); 65 | 66 | return $this; 67 | } 68 | 69 | public function inGroups(array $groups = []) : self 70 | { 71 | foreach ($groups as $group) { 72 | $this->inGroup($group); 73 | } 74 | 75 | return $this; 76 | } 77 | 78 | public function notInGroup(string $group = null) : self 79 | { 80 | $this->getFinder()->notContains(sprintf('@group %s', $group)); 81 | 82 | return $this; 83 | } 84 | 85 | public function notInGroups(array $groups = []) : self 86 | { 87 | foreach ($groups as $group) { 88 | $this->notInGroup($group); 89 | } 90 | 91 | return $this; 92 | } 93 | 94 | public function findTestFilesByFilter(string $filter) : array 95 | { 96 | $this->filter($filter); 97 | 98 | return $this->buildFilesArrayFromFinder(); 99 | } 100 | 101 | public function findTestFilesInGroups(array $groups) : array 102 | { 103 | $this->inGroups($groups); 104 | 105 | return $this->buildFilesArrayFromFinder(); 106 | } 107 | 108 | public function findTestFilesExcludingGroups(array $excludeGroups) : array 109 | { 110 | $this->notInGroups($excludeGroups); 111 | 112 | return $this->buildFilesArrayFromFinder(); 113 | } 114 | 115 | public function findAllTestFiles() : array 116 | { 117 | return $this->buildFilesArrayFromFinder(); 118 | } 119 | 120 | public function findChangedTestFiles() : array 121 | { 122 | $command = "git status --porcelain | grep -e '^\(.*\)Test.php$' | cut -c 3-"; 123 | 124 | return $this->buildFilesArrayFromFindCommand($command); 125 | } 126 | 127 | public function getFiles() : array 128 | { 129 | if ($this->changed) { 130 | return $this->findChangedTestFiles(); 131 | } 132 | 133 | return $this->buildFilesArrayFromFinder(); 134 | } 135 | 136 | private function getFinder() : Finder 137 | { 138 | if ($this->finder === null) { 139 | $this->finder = Finder::create() 140 | ->files() 141 | ->name('*Test.php') 142 | ->in($this->testsDirectory) 143 | ->sortByName(); 144 | } 145 | 146 | return $this->finder; 147 | } 148 | 149 | private function buildFilesArrayFromFinder() : array 150 | { 151 | return array_values(array_map(function($file) { 152 | return $file->getPathName(); 153 | }, iterator_to_array($this->getFinder()))); 154 | } 155 | 156 | private function buildFilesArrayFromFindCommand(string $command) : array 157 | { 158 | $output = trim(shell_exec($command)); 159 | 160 | return $output ? array_map('trim', explode("\n", $output)) : []; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/TestRunner.php: -------------------------------------------------------------------------------- 1 | app = $app; 47 | $this->input = $input; 48 | $this->output = $output; 49 | $this->configuration = $configuration; 50 | } 51 | 52 | public function getRootDir() : string 53 | { 54 | return $this->configuration->getRootDir(); 55 | } 56 | 57 | public function generatePhpunitXml(array &$files) : string 58 | { 59 | // load the config into memory 60 | $phpunitXmlDistPath = is_file($file = $this->getRootDir().'/phpunit.xml') 61 | ? $file 62 | : $this->getRootDir().'/phpunit.xml.dist' 63 | ; 64 | 65 | $src = file_get_contents($phpunitXmlDistPath); 66 | $xml = simplexml_load_string(str_replace('./', $this->getRootDir().'/', $src)); 67 | 68 | // temp config file 69 | $config = tempnam($this->getRootDir(), 'phpunitxml'); 70 | 71 | register_shutdown_function(function() use ($config) { 72 | unlink($config); 73 | }); 74 | 75 | unset($xml->testsuites[0]->testsuite); 76 | $suite = $xml->testsuites[0]->addChild('testsuite'); 77 | 78 | if (!empty($files)) { 79 | $files = array_unique($files); 80 | 81 | foreach ($files as $file) { 82 | $path = strpos($file, '/') === 0 83 | ? $file 84 | : $this->getRootDir().'/'.$file; 85 | 86 | $suite->addChild('file', $path); 87 | } 88 | 89 | file_put_contents($config, $xml->asXml()); 90 | 91 | return $config; 92 | } 93 | 94 | return ''; 95 | } 96 | 97 | public function runTestFiles(array $files, array $env = []) : int 98 | { 99 | $config = $this->generatePhpunitXml($files); 100 | 101 | if ($config !== null) { 102 | $this->output->writeln(''); 103 | 104 | foreach ($files as $file) { 105 | $this->output->writeln(sprintf(' - Executing %s', $file)); 106 | } 107 | 108 | $this->output->writeln(''); 109 | 110 | return $this->runPhpunit(sprintf('-c %s', escapeshellarg($config)), $env); 111 | } else { 112 | $this->output->writeln('No tests to run.'); 113 | 114 | return 1; 115 | } 116 | } 117 | 118 | public function runPhpunit(string $command, array $env = [], \Closure $callback = null) : int 119 | { 120 | $command = sprintf('%s %s %s', 121 | $this->configuration->getPhpunitPath(), 122 | $command, 123 | $this->flags() 124 | ); 125 | 126 | return $this->run($command, false, $env, $callback); 127 | } 128 | 129 | public function getPhpunitProcess(string $command, array $env = []) : Process 130 | { 131 | $command = sprintf('%s %s %s', 132 | $this->configuration->getPhpunitPath(), 133 | $command, 134 | $this->flags() 135 | ); 136 | 137 | return $this->getProcess($command, $env); 138 | } 139 | 140 | /** 141 | * @throws RuntimeException 142 | */ 143 | public function run(string $command, bool $throw = true, array $env = [], \Closure $callback = null) : int 144 | { 145 | $process = $this->getProcess($command, $env); 146 | 147 | if ($callback === null) { 148 | $callback = function($output) { 149 | echo $output; 150 | }; 151 | } 152 | 153 | $process->run(function($type, $output) use ($callback) { 154 | $callback($output); 155 | }); 156 | 157 | if ($process->getExitCode() > 0 && $throw) { 158 | throw new RuntimeException('The command did not exit successfully.'); 159 | } 160 | 161 | return $process->getExitCode(); 162 | } 163 | 164 | public function getProcess(string $command, array $env = []) : Process 165 | { 166 | foreach ($env as $key => $value) { 167 | $command = sprintf('export %s=%s && %s', $key, $value, $command); 168 | } 169 | 170 | if ($this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { 171 | $this->output->writeln('+ '.$command.''); 172 | } 173 | 174 | return $this->createProcess($command); 175 | } 176 | 177 | public function runTestCommand(string $command, array $input = []) : int 178 | { 179 | $input['command'] = $command; 180 | 181 | return $this->app->find($command) 182 | ->run(new ArrayInput($input), $this->output); 183 | } 184 | 185 | private function flags() : string 186 | { 187 | $memoryLimit = $this->input->getOption('memory-limit') 188 | ?: $this->configuration->getMemoryLimit(); 189 | 190 | $flags = '-d memory_limit='.escapeshellarg($memoryLimit); 191 | 192 | if ($this->input->getOption('stop')) { 193 | $flags .= ' --stop-on-failure --stop-on-error'; 194 | } 195 | 196 | if ($this->output->getVerbosity() > Output::VERBOSITY_NORMAL) { 197 | $flags .= ' --verbose'; 198 | } 199 | 200 | if ($this->input->getOption('debug')) { 201 | $flags .= ' --debug'; 202 | } 203 | 204 | if ($this->output->isDecorated()) { 205 | $flags .= ' --colors'; 206 | } 207 | 208 | if ($this->input->hasOption('phpunit-opt') 209 | && $phpunitOptions = $this->input->getOption('phpunit-opt')) { 210 | $flags .= ' '.$phpunitOptions; 211 | } 212 | 213 | return $flags; 214 | } 215 | 216 | protected function createProcess(string $command) : Process 217 | { 218 | return new Process($command, null, null, null, null); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /tests/BaseTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder($className) 14 | ->setMethods($mockedMethods); 15 | 16 | if ($constructorArgs) { 17 | $builder->setConstructorArgs($constructorArgs); 18 | } else { 19 | $builder->disableOriginalConstructor(); 20 | } 21 | 22 | return $builder->getMock(); 23 | } 24 | 25 | protected function getRootDir() : string 26 | { 27 | return realpath(__DIR__.'/../'); 28 | } 29 | 30 | protected function getTestsDirectory() : string 31 | { 32 | return realpath(__DIR__.'/../tests'); 33 | } 34 | 35 | /** 36 | * PHPUnit 5.x compat, see createMock vs getMock 37 | * 38 | * @param string $originalClassName 39 | * @return \PHPUnit_Framework_MockObject_MockObject 40 | */ 41 | protected function createMock($originalClassName) 42 | { 43 | $builder = $this->getMockBuilder($originalClassName) 44 | ->disableOriginalConstructor() 45 | ->disableOriginalClone() 46 | ->disableArgumentCloning(); 47 | 48 | if (method_exists($builder, 'disallowMockingUnknownTypes')) { 49 | $builder->disallowMockingUnknownTypes(); 50 | } 51 | 52 | return $builder->getMock(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/ChunkedTestsTest.php: -------------------------------------------------------------------------------- 1 | chunkedTests = new ChunkedTests(); 17 | } 18 | 19 | public function testSetGetChunk() 20 | { 21 | $this->assertNull($this->chunkedTests->getChunk()); 22 | 23 | $this->assertSame($this->chunkedTests, $this->chunkedTests->setChunk(1)); 24 | 25 | $this->assertEquals(1, $this->chunkedTests->getChunk()); 26 | } 27 | 28 | public function testSetGetNumChunks() 29 | { 30 | $this->assertEquals(1, $this->chunkedTests->getNumChunks()); 31 | 32 | $this->assertSame($this->chunkedTests, $this->chunkedTests->setNumChunks(2)); 33 | 34 | $this->assertEquals(2, $this->chunkedTests->getNumChunks()); 35 | } 36 | 37 | public function testSetGetTestsPerChunk() 38 | { 39 | $this->assertEquals(0, $this->chunkedTests->getTestsPerChunk()); 40 | 41 | $this->assertSame($this->chunkedTests, $this->chunkedTests->setTestsPerChunk(1)); 42 | 43 | $this->assertEquals(1, $this->chunkedTests->getTestsPerChunk()); 44 | } 45 | 46 | public function testSetGetChunks() 47 | { 48 | $this->assertEmpty($this->chunkedTests->getChunks()); 49 | 50 | $this->assertSame($this->chunkedTests, $this->chunkedTests->setChunks(['test'])); 51 | 52 | $this->assertEquals(['test'], $this->chunkedTests->getChunks()); 53 | } 54 | 55 | public function testSetGetTotalTests() 56 | { 57 | $this->assertEquals(0, $this->chunkedTests->getTotalTests()); 58 | 59 | $this->assertSame($this->chunkedTests, $this->chunkedTests->setTotalTests(1)); 60 | 61 | $this->assertEquals(1, $this->chunkedTests->getTotalTests()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Command/BuildSandboxTest.php: -------------------------------------------------------------------------------- 1 | testRunner = $this->createMock(TestRunner::class); 34 | $this->eventDispatcher = $this->createMock(EventDispatcher::class); 35 | 36 | $this->buildSandbox = new BuildSandbox( 37 | $this->testRunner, 38 | $this->eventDispatcher 39 | ); 40 | } 41 | 42 | public function testExecute() 43 | { 44 | $input = $this->createMock(InputInterface::class); 45 | $output = $this->createMock(OutputInterface::class); 46 | 47 | $this->eventDispatcher->expects($this->at(0)) 48 | ->method('dispatch') 49 | ->with(Events::SANDBOX_PREPARE); 50 | 51 | $input->expects($this->once()) 52 | ->method('getOption') 53 | ->with('create-dbs') 54 | ->willReturn(true); 55 | 56 | $this->testRunner->expects($this->once()) 57 | ->method('runTestCommand') 58 | ->with('create-dbs', [ 59 | '--sandbox' => true, 60 | ]); 61 | 62 | $this->buildSandbox->execute( 63 | $input, 64 | $output 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/Command/CreateDatabasesTest.php: -------------------------------------------------------------------------------- 1 | eventDispatcher = $this->createMock(EventDispatcher::class); 29 | 30 | $this->createDatabases = new CreateDatabases( 31 | $this->eventDispatcher 32 | ); 33 | } 34 | 35 | public function testExecute() 36 | { 37 | $input = $this->createMock(InputInterface::class); 38 | $output = $this->createMock(OutputInterface::class); 39 | 40 | $this->eventDispatcher->expects($this->once()) 41 | ->method('dispatch') 42 | ->with(Events::DATABASES_CREATE); 43 | 44 | $this->createDatabases->execute($input, $output); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Command/RunTest.php: -------------------------------------------------------------------------------- 1 | testRunner = $this->createMock(TestRunner::class); 36 | $this->configuration = (new Configuration()) 37 | ->setTestsDirectory($this->getTestsDirectory()) 38 | ; 39 | $this->testChunker = $this->createMock(TestChunker::class); 40 | $this->testFinder = $this->createMock(TestFinder::class); 41 | 42 | $this->run = new Run( 43 | $this->testRunner, 44 | $this->configuration, 45 | $this->testChunker, 46 | $this->testFinder 47 | ); 48 | } 49 | 50 | public function testExecute() 51 | { 52 | $input = $this->createMock(InputInterface::class); 53 | $output = $this->createMock(OutputInterface::class); 54 | 55 | $this->testFinder->expects($this->once()) 56 | ->method('getFiles') 57 | ->willReturn([__FILE__]); 58 | 59 | $input->expects($this->at(0)) 60 | ->method('getOption') 61 | ->with('parallel') 62 | ->willReturn(false); 63 | 64 | $input->expects($this->at(1)) 65 | ->method('getOption') 66 | ->with('stop') 67 | ->willReturn(false); 68 | 69 | $input->expects($this->at(2)) 70 | ->method('getOption') 71 | ->with('chunk') 72 | ->willReturn(null); 73 | 74 | $input->expects($this->at(3)) 75 | ->method('getOption') 76 | ->with('file') 77 | ->willReturn([]); 78 | 79 | $input->expects($this->at(4)) 80 | ->method('getOption') 81 | ->with('group') 82 | ->willReturn([]); 83 | 84 | $input->expects($this->at(5)) 85 | ->method('getOption') 86 | ->with('exclude-group') 87 | ->willReturn([]); 88 | 89 | $input->expects($this->at(6)) 90 | ->method('getOption') 91 | ->with('changed') 92 | ->willReturn(false); 93 | 94 | $input->expects($this->at(7)) 95 | ->method('getOption') 96 | ->with('filter') 97 | ->willReturn([]); 98 | 99 | $input->expects($this->at(8)) 100 | ->method('getOption') 101 | ->with('contains') 102 | ->willReturn([]); 103 | 104 | $input->expects($this->at(9)) 105 | ->method('getOption') 106 | ->with('not-contains') 107 | ->willReturn([]); 108 | 109 | $input->expects($this->at(10)) 110 | ->method('getOption') 111 | ->with('num-chunks') 112 | ->willReturn(14); 113 | 114 | $input->expects($this->at(11)) 115 | ->method('getOption') 116 | ->with('sandbox') 117 | ->willReturn(true); 118 | 119 | $this->testChunker->expects($this->once()) 120 | ->method('chunkTestFiles') 121 | ->will($this->returnCallback(function(ChunkedTests $chunkedTests) { 122 | $chunkedTests->setTotalTests(1); 123 | })); 124 | 125 | $this->testRunner->expects($this->once()) 126 | ->method('runTestCommand') 127 | ->with('sandbox'); 128 | 129 | $this->testRunner->expects($this->any()) 130 | ->method('generatePhpunitXml') 131 | ->willReturn('test.xml'); 132 | 133 | $this->testRunner->expects($this->any()) 134 | ->method('runPhpunit') 135 | ->with('-c test.xml') 136 | ->willReturn(0); 137 | 138 | $this->assertEquals(0, $this->run->execute($input, $output)); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /tests/Command/SetupTest.php: -------------------------------------------------------------------------------- 1 | setup = new Setup(); 22 | } 23 | 24 | public function testGetName() 25 | { 26 | $this->assertEquals('setup', $this->setup->getName()); 27 | } 28 | 29 | public function testConfigure() 30 | { 31 | $command = $this->createMock(Command::class); 32 | 33 | $command->expects($this->once()) 34 | ->method('setDescription') 35 | ->with('Help with setting up PHPChunkit.'); 36 | 37 | $this->setup->configure($command); 38 | } 39 | 40 | public function testExecute() 41 | { 42 | $input = $this->createMock(InputInterface::class); 43 | $output = $this->createMock(OutputInterface::class); 44 | $formatter = $this->createMock(OutputFormatterInterface::class); 45 | 46 | $output->expects($this->any()) 47 | ->method('getFormatter') 48 | ->willReturn($formatter); 49 | 50 | $this->setup->execute($input, $output); 51 | 52 | $this->assertTrue(true); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Command/TestWatcherTest.php: -------------------------------------------------------------------------------- 1 | testRunner = $this->createMock(TestRunner::class); 33 | $this->configuration = (new Configuration()) 34 | ->setWatchDirectories([realpath(__DIR__.'/../..')]) 35 | ; 36 | $this->fileClassesHelper = $this->createMock(FileClassesHelper::class); 37 | 38 | $this->testWatcher = new TestWatcherStub( 39 | $this->testRunner, 40 | $this->configuration, 41 | $this->fileClassesHelper 42 | ); 43 | } 44 | 45 | public function testExecute() 46 | { 47 | $input = $this->createMock(InputInterface::class); 48 | $output = $this->createMock(OutputInterface::class); 49 | 50 | $this->testWatcher->execute($input, $output); 51 | 52 | $this->assertEquals(1, $this->testWatcher->getCount()); 53 | } 54 | } 55 | 56 | class TestWatcherStub extends TestWatcher 57 | { 58 | /** @var int */ 59 | private $count = 0; 60 | 61 | protected function sleep() 62 | { 63 | } 64 | 65 | public function getCount() : int 66 | { 67 | return $this->count; 68 | } 69 | 70 | /** 71 | * @return bool 72 | */ 73 | protected function while() : bool 74 | { 75 | $this->count++; 76 | 77 | return $this->count < 1; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/ConfigurationTest.php: -------------------------------------------------------------------------------- 1 | configuration = new Configuration(); 19 | } 20 | 21 | public function testCreateFromXmlFile() 22 | { 23 | $configuration = Configuration::createFromXmlFile( 24 | sprintf('%s/phpchunkit.xml.dist', $this->getRootDir()) 25 | ); 26 | 27 | $this->assertEquals($this->getRootDir(), $configuration->getRootDir()); 28 | $this->assertEquals(sprintf('%s/tests', $this->getRootDir()), $configuration->getTestsDirectory()); 29 | 30 | $this->assertEquals([ 31 | sprintf('%s/src', $this->getRootDir()), 32 | sprintf('%s/tests', $this->getRootDir()) 33 | ], $configuration->getWatchDirectories()); 34 | 35 | $this->assertEquals( 36 | sprintf('%s/vendor/phpunit/phpunit/phpunit', $this->getRootDir()), 37 | $configuration->getPhpunitPath() 38 | ); 39 | 40 | $this->assertEquals( 41 | sprintf('%s/tests/phpchunkit_bootstrap.php', $this->getRootDir()), 42 | $configuration->getBootstrapPath() 43 | ); 44 | 45 | $this->assertEquals('512M', $configuration->getMemoryLimit()); 46 | $this->assertEquals('1', $configuration->getNumChunks()); 47 | $this->assertEquals(['testdb1', 'testdb2'], $configuration->getDatabaseSandbox()->getDatabaseNames()); 48 | $this->assertCount(3, $configuration->getEventDispatcher()->getListeners()); 49 | } 50 | 51 | /** 52 | * @expectedException InvalidArgumentException 53 | * @expectedExceptionMessage XML file count not be found at path "invalid" 54 | */ 55 | public function testCreateFromXmlFileThrowsInvalidArgumentException() 56 | { 57 | Configuration::createFromXmlFile('invalid'); 58 | } 59 | 60 | public function testSetGetRootDir() 61 | { 62 | $this->assertEquals('', $this->configuration->getRootDir()); 63 | 64 | $this->configuration->setRootDir(__DIR__); 65 | 66 | $this->assertEquals(__DIR__, $this->configuration->getRootDir()); 67 | } 68 | 69 | /** 70 | * @expectedException InvalidArgumentException 71 | * @expectedExceptionMessage rootDir "unknown" does not exist. 72 | */ 73 | public function testSetRootDirThrowsInvalidArgumentException() 74 | { 75 | $this->configuration->setRootDir('unknown'); 76 | } 77 | 78 | public function testSetGetWatchDirectories() 79 | { 80 | $this->assertEmpty($this->configuration->getWatchDirectories()); 81 | 82 | $this->configuration->setWatchDirectories([__DIR__]); 83 | 84 | $this->assertEquals([__DIR__], $this->configuration->getWatchDirectories()); 85 | } 86 | 87 | /** 88 | * @expectedException InvalidArgumentException 89 | * @expectedExceptionMessage Watch directory "unknown" does not exist. 90 | */ 91 | public function testSetWatchDirectoriesThrowsInvalidArgumentException() 92 | { 93 | $this->configuration->setWatchDirectories(['unknown']); 94 | } 95 | 96 | public function testSetGetTestsDirectory() 97 | { 98 | $this->assertEquals('', $this->configuration->getTestsDirectory()); 99 | 100 | $this->configuration->setTestsDirectory(__DIR__); 101 | 102 | $this->assertEquals(__DIR__, $this->configuration->getTestsDirectory()); 103 | } 104 | 105 | /** 106 | * @expectedException InvalidArgumentException 107 | * @expectedExceptionMessage testsDirectory "unknown" does not exist. 108 | */ 109 | public function testSetGetTestsDirectoryThrowsInvalidArgumentException() 110 | { 111 | $this->configuration->setTestsDirectory('unknown'); 112 | } 113 | 114 | public function testSetGetBootstrapPath() 115 | { 116 | $this->assertEquals('', $this->configuration->getBootstrapPath()); 117 | 118 | $this->configuration->setBootstrapPath(__FILE__); 119 | 120 | $this->assertEquals(__FILE__, $this->configuration->getBootstrapPath()); 121 | } 122 | 123 | /** 124 | * @expectedException InvalidArgumentException 125 | * @expectedExceptionMessage bootstrapPath "unknown" does not exist. 126 | */ 127 | public function testSetGetBootstrapPathThrowsInvalidArgumentException() 128 | { 129 | $this->configuration->setBootstrapPath('unknown'); 130 | } 131 | 132 | public function testSetGetPhpunitPath() 133 | { 134 | $this->assertEquals('vendor/bin/phpunit', $this->configuration->getPhpunitPath()); 135 | 136 | $this->configuration->setPhpunitPath(__FILE__); 137 | 138 | $this->assertEquals(__FILE__, $this->configuration->getPhpunitPath()); 139 | } 140 | 141 | /** 142 | * @expectedException InvalidArgumentException 143 | * @expectedExceptionMessage phpunitPath "unknown" does not exist. 144 | */ 145 | public function testSetGetPhpunitPathThrowsInvalidArgumentException() 146 | { 147 | $this->configuration->setPhpunitPath('unknown'); 148 | } 149 | 150 | public function testSetGetDatabaseSandbox() 151 | { 152 | $this->assertInstanceOf(DatabaseSandbox::class, $this->configuration->getDatabaseSandbox()); 153 | 154 | $databaseSandbox = new DatabaseSandbox(); 155 | 156 | $this->configuration->setDatabaseSandbox($databaseSandbox); 157 | 158 | $this->assertSame($databaseSandbox, $this->configuration->getDatabaseSandbox()); 159 | } 160 | 161 | public function testSetSandboxEnabled() 162 | { 163 | $this->assertFalse($this->configuration->getDatabaseSandbox()->getSandboxEnabled()); 164 | 165 | $this->configuration->setSandboxEnabled(true); 166 | 167 | $this->assertTrue($this->configuration->getDatabaseSandbox()->getSandboxEnabled()); 168 | 169 | $this->configuration->setSandboxEnabled(false); 170 | 171 | $this->assertFalse($this->configuration->getDatabaseSandbox()->getSandboxEnabled()); 172 | } 173 | 174 | /** 175 | * @expectedException InvalidArgumentException 176 | * @expectedExceptionMessage PHPChunkit\Test\ConfigurationTest does not implement PHPChunkit\ListenerInterface 177 | */ 178 | public function testAddListenerThrowsInvalidArgumentException() 179 | { 180 | $this->configuration->addListener('test', self::class); 181 | } 182 | 183 | public function testIsSetup() 184 | { 185 | $this->assertFalse($this->configuration->isSetup()); 186 | 187 | $this->configuration->setRootDir(__DIR__); 188 | $this->configuration->setTestsDirectory(__DIR__); 189 | 190 | $this->assertTrue($this->configuration->isSetup()); 191 | } 192 | 193 | public function testSetDatabaseNames() 194 | { 195 | $databaseNames = ['testdb1', 'testdb2']; 196 | 197 | $this->configuration->setDatabaseNames($databaseNames); 198 | 199 | $this->assertEquals( 200 | $databaseNames, 201 | $this->configuration->getDatabaseSandbox()->getDatabaseNames() 202 | ); 203 | } 204 | 205 | public function testSetGetEventDispatcher() 206 | { 207 | $this->assertInstanceOf(EventDispatcher::class, $this->configuration->getEventDispatcher()); 208 | 209 | $eventDispatcher = new EventDispatcher(); 210 | 211 | $this->configuration->setEventDispatcher($eventDispatcher); 212 | 213 | $this->assertSame($eventDispatcher, $this->configuration->getEventDispatcher()); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /tests/ContainerTest.php: -------------------------------------------------------------------------------- 1 | getRootDir(); 13 | $container->initialize(); 14 | 15 | $configuration = $container['phpchunkit.configuration']; 16 | 17 | $this->assertEquals($this->getRootDir(), $configuration->getRootDir()); 18 | $this->assertTrue($configuration->isSetup()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/DatabaseSandboxTest.php: -------------------------------------------------------------------------------- 1 | databaseSandbox = new DatabaseSandboxStub(true, ['mydb']); 17 | } 18 | 19 | public function testGetTestDatabaseNames() 20 | { 21 | $databaseNames = ['mydb' => 'mydb_test']; 22 | 23 | $this->assertEquals($databaseNames, $this->databaseSandbox->getTestDatabaseNames()); 24 | } 25 | 26 | public function testGetSandboxedDatabaseNames() 27 | { 28 | $databaseNames = ['mydb' => 'mydb_uniqueid']; 29 | 30 | $this->assertEquals($databaseNames, $this->databaseSandbox->getSandboxedDatabaseNames()); 31 | } 32 | } 33 | 34 | class DatabaseSandboxStub extends DatabaseSandbox 35 | { 36 | protected function generateUniqueId() : string 37 | { 38 | return 'uniqueid'; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/FileClassesHelperTest.php: -------------------------------------------------------------------------------- 1 | fileClassesHelper = new FileClassesHelper(); 17 | } 18 | 19 | public function testGetFileClasses() 20 | { 21 | $this->assertEquals([ 22 | self::class, 23 | TestClass::class 24 | ], $this->fileClassesHelper->getFileClasses(__FILE__)); 25 | } 26 | 27 | public function testGetFileClassesNoClasses() 28 | { 29 | $this->assertEmpty($this->fileClassesHelper->getFileClasses('unknown')); 30 | } 31 | } 32 | 33 | class TestClass 34 | { 35 | } 36 | -------------------------------------------------------------------------------- /tests/FunctionalTest1Test.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 16 | 17 | $databases = parse_ini_file(realpath(__DIR__.'/../bin/config/databases_test.ini')); 18 | 19 | try { 20 | foreach ($databases as $database) { 21 | $pdo = new PDO(sprintf('mysql:host=localhost;dbname=%s', $database), 'root', null); 22 | } 23 | } catch (PDOException $e) { 24 | if ($e->getMessage() === "SQLSTATE[HY000] [1049] Unknown database 'testdb1_test'") { 25 | $this->markTestSkipped('Database is not setup. Run ./bin/phpchunkit create-dbs'); 26 | } 27 | } 28 | } 29 | 30 | public function testTest2() 31 | { 32 | $this->assertTrue(true); 33 | } 34 | 35 | public function testTest3() 36 | { 37 | $this->assertTrue(true); 38 | } 39 | 40 | public function testTest4() 41 | { 42 | $this->assertTrue(true); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/FunctionalTest2Test.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 13 | } 14 | 15 | public function testTest2() 16 | { 17 | $this->assertTrue(true); 18 | } 19 | 20 | public function testTest3() 21 | { 22 | $this->assertTrue(true); 23 | } 24 | 25 | public function testTest4() 26 | { 27 | $this->assertTrue(true); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/FunctionalTest3Test.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 13 | } 14 | 15 | public function testTest2() 16 | { 17 | $this->assertTrue(true); 18 | } 19 | 20 | public function testTest3() 21 | { 22 | $this->assertTrue(true); 23 | } 24 | 25 | public function testTest4() 26 | { 27 | $this->assertTrue(true); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/FunctionalTest4Test.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 13 | } 14 | 15 | public function testTest2() 16 | { 17 | $this->assertTrue(true); 18 | } 19 | 20 | public function testTest3() 21 | { 22 | $this->assertTrue(true); 23 | } 24 | 25 | public function testTest4() 26 | { 27 | $this->assertTrue(true); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/FunctionalTest5Test.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 13 | } 14 | 15 | public function testTest2() 16 | { 17 | $this->assertTrue(true); 18 | } 19 | 20 | public function testTest3() 21 | { 22 | $this->assertTrue(true); 23 | } 24 | 25 | public function testTest4() 26 | { 27 | $this->assertTrue(true); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/FunctionalTest6Test.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 13 | } 14 | 15 | public function testTest2() 16 | { 17 | $this->assertTrue(true); 18 | } 19 | 20 | public function testTest3() 21 | { 22 | $this->assertTrue(true); 23 | } 24 | 25 | public function testTest4() 26 | { 27 | $this->assertTrue(true); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/FunctionalTest7Test.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 13 | } 14 | 15 | public function testTest2() 16 | { 17 | $this->assertTrue(true); 18 | } 19 | 20 | public function testTest3() 21 | { 22 | $this->assertTrue(true); 23 | } 24 | 25 | public function testTest4() 26 | { 27 | $this->assertTrue(true); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/FunctionalTest8Test.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 13 | } 14 | 15 | public function testTest2() 16 | { 17 | $this->assertTrue(true); 18 | } 19 | 20 | public function testTest3() 21 | { 22 | $this->assertTrue(true); 23 | } 24 | 25 | public function testTest4() 26 | { 27 | $this->assertTrue(true); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/GenerateTestClassTest.php: -------------------------------------------------------------------------------- 1 | testDependency1 = \$this->createMock(TestDependency1::class); 57 | \$this->testDependency2 = \$this->createMock(TestDependency2::class); 58 | \$this->value1 = ''; // TODO 59 | \$this->value2 = ''; // TODO 60 | \$this->value3 = ''; // TODO 61 | 62 | \$this->testAdvancedClass = new TestAdvancedClass( 63 | \$this->testDependency1, 64 | \$this->testDependency2, 65 | \$this->value1, 66 | \$this->value2, 67 | \$this->value3 68 | ); 69 | } 70 | 71 | public function testGetSomething1() 72 | { 73 | \$user = \$this->createMock(User::class); 74 | \$test1 = \$this->createMock(TestDependency1::class); 75 | \$test2 = \$this->createMock(TestDependency2::class); 76 | \$test3 = ''; 77 | 78 | \$this->testAdvancedClass->getSomething1( 79 | \$user, 80 | \$test1, 81 | \$test2, 82 | \$test3 83 | ); 84 | } 85 | 86 | public function testGetSomething2() 87 | { 88 | \$seller = \$this->createMock(Seller::class); 89 | \$test1 = \$this->createMock(TestDependency1::class); 90 | \$test2 = \$this->createMock(TestDependency2::class); 91 | \$test3 = ''; 92 | 93 | \$this->testAdvancedClass->getSomething2( 94 | \$seller, 95 | \$test1, 96 | \$test2, 97 | \$test3 98 | ); 99 | } 100 | } 101 | 102 | EOF; 103 | 104 | const EXPECTED_SIMPLE_CLASS = <<testSimpleClass = new TestSimpleClass(); 122 | } 123 | 124 | public function testGetSomething1() 125 | { 126 | \$this->testSimpleClass->getSomething1(); 127 | } 128 | 129 | public function testGetSomething2() 130 | { 131 | \$this->testSimpleClass->getSomething2(); 132 | } 133 | } 134 | 135 | EOF; 136 | 137 | 138 | /** 139 | * @var GenerateTestClass 140 | */ 141 | private $generateTestClass; 142 | 143 | protected function setUp() 144 | { 145 | $this->generateTestClass = new GenerateTestClass(); 146 | } 147 | 148 | public function testGenerateAdvancedClass() 149 | { 150 | try { 151 | new \ReflectionMethod(TestCase::class, 'createMock'); 152 | } catch (\ReflectionException $e) { 153 | $this->markTestSkipped('PHPUnit >= 5.4 is required.'); 154 | } 155 | 156 | $this->checkGeneratedTestClass(self::EXPECTED_ADVANCED_CLASS, TestAdvancedClass::class); 157 | 158 | $test = new \PHPChunkit\Test\TestAdvancedClassTest(); 159 | $test->setUp(); 160 | $test->testGetSomething1(); 161 | $test->testGetSomething2(); 162 | } 163 | 164 | public function testGenerateAdvancedClassPHPUnitCompat() 165 | { 166 | try { 167 | new \ReflectionMethod(TestCase::class, 'createMock'); 168 | $this->markTestSkipped('PHPUnit < 5.4 is required.'); 169 | } catch (\ReflectionException $e) { 170 | } 171 | 172 | $this->checkGeneratedTestClass( 173 | str_replace('createMock', 'getMock', self::EXPECTED_ADVANCED_CLASS), 174 | TestAdvancedClass::class 175 | ); 176 | 177 | $test = new \PHPChunkit\Test\TestAdvancedClassTest(); 178 | $test->setUp(); 179 | $test->testGetSomething1(); 180 | $test->testGetSomething2(); 181 | } 182 | 183 | public function testGenerateSimpleClass() 184 | { 185 | $this->checkGeneratedTestClass(self::EXPECTED_SIMPLE_CLASS, TestSimpleClass::class); 186 | 187 | $test = new \PHPChunkit\Test\TestSimpleClassTest(); 188 | $test->setUp(); 189 | $test->testGetSomething1(); 190 | $test->testGetSomething2(); 191 | } 192 | 193 | /** 194 | * @param string $expected 195 | * @param string $className 196 | */ 197 | private function checkGeneratedTestClass($expected, $className) 198 | { 199 | $code = $this->generateTestClass->generate($className); 200 | 201 | $this->assertEquals($expected, $code); 202 | 203 | // make sure it can be executed 204 | eval(str_replace('configuration = $configuration; 19 | } 20 | 21 | public function execute() 22 | { 23 | $pdo = new PDO('mysql:host=localhost;', 'root', null); 24 | 25 | $configDir = sprintf('%s/bin/config', $this->configuration->getRootDir()); 26 | $configFilePath = sprintf('%s/databases_test.ini', $configDir); 27 | $databases = parse_ini_file($configFilePath); 28 | 29 | foreach ($databases as $databaseName) { 30 | $pdo->exec(sprintf('CREATE DATABASE %s', $databaseName)); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Listener/SandboxCleanup.php: -------------------------------------------------------------------------------- 1 | configuration = $configuration; 19 | } 20 | 21 | public function execute() 22 | { 23 | $pdo = new PDO('mysql:host=localhost;', 'root', null); 24 | 25 | $configDir = sprintf('%s/bin/config', $this->configuration->getRootDir()); 26 | $configFilePath = sprintf('%s/databases_test.ini', $configDir); 27 | $configFileBackupPath = sprintf('%s/databases_test.ini.bak', $configDir); 28 | $databases = parse_ini_file($configFilePath); 29 | 30 | foreach ($databases as $databaseName) { 31 | $pdo->exec(sprintf('DROP DATABASE IF EXISTS %s', $databaseName)); 32 | } 33 | 34 | rename($configFileBackupPath, $configFilePath); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Listener/SandboxPrepare.php: -------------------------------------------------------------------------------- 1 | configuration = $configuration; 18 | } 19 | 20 | public function execute() 21 | { 22 | $configDir = sprintf('%s/bin/config', $this->configuration->getRootDir()); 23 | $configFilePath = sprintf('%s/databases_test.ini', $configDir); 24 | $configFileBackupPath = sprintf('%s/databases_test.ini.bak', $configDir); 25 | 26 | copy($configFilePath, $configFileBackupPath); 27 | 28 | $configContent = file_get_contents($configFilePath); 29 | 30 | $databaseSandbox = $this->configuration->getDatabaseSandbox(); 31 | 32 | $modifiedConfigContent = str_replace( 33 | $databaseSandbox->getTestDatabaseNames(), 34 | $databaseSandbox->getSandboxedDatabaseNames(), 35 | $configContent 36 | ); 37 | 38 | file_put_contents($configFilePath, $modifiedConfigContent); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/PHPChunkitApplicationTest.php: -------------------------------------------------------------------------------- 1 | createMock(Application::class); 19 | 20 | $command = $this->createMock(Command::class); 21 | 22 | $command->expects($this->any()) 23 | ->method('setCode'); 24 | 25 | $symfonyApplication->expects($this->any()) 26 | ->method('register') 27 | ->willReturn($command); 28 | 29 | $container = new Container(); 30 | $container['phpchunkit.root_dir'] = $this->getRootDir(); 31 | $container['phpchunkit.configuration'] = $this->createMock(Configuration::class); 32 | $container['phpchunkit.symfony_application'] = $symfonyApplication; 33 | $container['phpchunkit.command.setup'] = $this->createMock(CommandInterface::class); 34 | $container['phpchunkit.command.test_watcher'] = $this->createMock(CommandInterface::class); 35 | $container['phpchunkit.command.run'] = $this->createMock(CommandInterface::class); 36 | $container['phpchunkit.command.build_sandbox'] = $this->createMock(CommandInterface::class); 37 | $container['phpchunkit.command.create_databases'] = $this->createMock(CommandInterface::class); 38 | $container['phpchunkit.command.generate_test'] = $this->createMock(CommandInterface::class); 39 | 40 | $phpChunkitApplication = new PHPChunkitApplicationStub($container); 41 | 42 | $input = $this->createMock(InputInterface::class); 43 | $output = $this->createMock(OutputInterface::class); 44 | 45 | $phpChunkitApplication->run($input, $output); 46 | 47 | $this->assertTrue($phpChunkitApplication->ran); 48 | } 49 | } 50 | 51 | class PHPChunkitApplicationStub extends PHPChunkitApplication 52 | { 53 | public $ran = false; 54 | 55 | protected function runSymfonyApplication( 56 | InputInterface $input, 57 | OutputInterface $output) : int 58 | { 59 | $this->ran = true; 60 | 61 | return 0; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/PHPChunkitTest.php: -------------------------------------------------------------------------------- 1 | phpChunkit = new PHPChunkit($this->getRootDir()); 21 | } 22 | 23 | public function testGetContainer() 24 | { 25 | $container = $this->phpChunkit->getContainer(); 26 | 27 | $this->assertInstanceOf(Container::class, $container); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/TestChunkerTest.php: -------------------------------------------------------------------------------- 1 | testsDirectory = $this->getTestsDirectory(); 31 | $this->testCounter = $this->createMock(TestCounter::class); 32 | $this->testChunker = new TestChunker($this->testCounter); 33 | } 34 | 35 | public function testChunkFunctionalTests() 36 | { 37 | $chunkFunctionalTests = (new ChunkedTests()) 38 | ->setNumChunks(4) 39 | ; 40 | 41 | $this->testCounter->expects($this->any()) 42 | ->method('countNumTestsInFile') 43 | ->willReturn(4); 44 | 45 | $testFiles = (new TestFinder($this->testsDirectory)) 46 | ->findTestFilesInGroups(['functional']); 47 | 48 | $this->testChunker->chunkTestFiles($chunkFunctionalTests, $testFiles); 49 | 50 | $expectedChunks = [ 51 | // chunk 1 52 | [ 53 | [ 54 | 'file' => sprintf('%s/FunctionalTest1Test.php', $this->testsDirectory), 55 | 'numTests' => 4, 56 | ], 57 | [ 58 | 'file' => sprintf('%s/FunctionalTest2Test.php', $this->testsDirectory), 59 | 'numTests' => 4, 60 | ] 61 | ], 62 | 63 | // chunk 2 64 | [ 65 | [ 66 | 'file' => sprintf('%s/FunctionalTest3Test.php', $this->testsDirectory), 67 | 'numTests' => 4, 68 | ], 69 | [ 70 | 'file' => sprintf('%s/FunctionalTest4Test.php', $this->testsDirectory), 71 | 'numTests' => 4, 72 | ] 73 | ], 74 | 75 | // chunk 3 76 | [ 77 | [ 78 | 'file' => sprintf('%s/FunctionalTest5Test.php', $this->testsDirectory), 79 | 'numTests' => 4, 80 | ], 81 | [ 82 | 'file' => sprintf('%s/FunctionalTest6Test.php', $this->testsDirectory), 83 | 'numTests' => 4, 84 | ] 85 | ], 86 | 87 | // chunk 4 88 | [ 89 | [ 90 | 'file' => sprintf('%s/FunctionalTest7Test.php', $this->testsDirectory), 91 | 'numTests' => 4, 92 | ], 93 | [ 94 | 'file' => sprintf('%s/FunctionalTest8Test.php', $this->testsDirectory), 95 | 'numTests' => 4, 96 | ] 97 | ] 98 | ]; 99 | 100 | $this->assertEquals($expectedChunks, $chunkFunctionalTests->getChunks()); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tests/TestCounterTest.php: -------------------------------------------------------------------------------- 1 | fileClassesHelper = $this->createMock(FileClassesHelper::class); 24 | $this->testCounter = new TestCounterStub($this->fileClassesHelper); 25 | $this->testCounter->clearCache(); 26 | } 27 | 28 | public function testCountNumTestsInFileCache() 29 | { 30 | $testCounter = new TestCounter($this->fileClassesHelper); 31 | $testCounter->clearCache(); 32 | 33 | $this->fileClassesHelper->expects($this->exactly(1)) 34 | ->method('getFileClasses') 35 | ->with(__FILE__) 36 | ->willReturn([ 37 | TestCounterTest::class, 38 | AbstractTest::class 39 | ]); 40 | 41 | $this->assertEquals(9, $testCounter->countNumTestsInFile(__FILE__)); 42 | $this->assertEquals(9, $testCounter->countNumTestsInFile(__FILE__)); 43 | } 44 | 45 | public function testCountNumTestsInFile() 46 | { 47 | $this->fileClassesHelper->expects($this->once()) 48 | ->method('getFileClasses') 49 | ->with(__FILE__) 50 | ->willReturn([ 51 | TestCounterTest::class, 52 | AbstractTest::class 53 | ]); 54 | 55 | $this->assertEquals(9, $this->testCounter->countNumTestsInFile(__FILE__)); 56 | } 57 | 58 | public function testCount1() 59 | { 60 | $this->assertTrue(true); 61 | } 62 | 63 | public function testCount2() 64 | { 65 | $this->assertTrue(true); 66 | } 67 | 68 | public function testCount3() 69 | { 70 | $this->assertTrue(true); 71 | } 72 | 73 | /** 74 | * @test 75 | */ 76 | public function methodWithoutTestPrefix() 77 | { 78 | $this->assertTrue(true); 79 | } 80 | 81 | /** 82 | * @dataProvider getTestWithDataProviderData 83 | */ 84 | public function testWithDataProvider() 85 | { 86 | $this->assertTrue(true); 87 | } 88 | 89 | public function getTestWithDataProviderData() 90 | { 91 | return [ 92 | [], 93 | [], 94 | [], 95 | ]; 96 | } 97 | 98 | public function nonTestPublicMethod() 99 | { 100 | $this->assertTrue(true); 101 | } 102 | 103 | protected function nonTestProtectedMethod() 104 | { 105 | $this->assertTrue(true); 106 | } 107 | 108 | private function nonTestPrivateMethod() 109 | { 110 | $this->assertTrue(true); 111 | } 112 | } 113 | 114 | class TestCounterStub extends TestCounter 115 | { 116 | protected function loadCache() 117 | { 118 | } 119 | 120 | protected function writeCache() 121 | { 122 | } 123 | } 124 | 125 | abstract class AbstractTest extends BaseTest 126 | { 127 | public function testSomething() 128 | { 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /tests/TestFinderTest.php: -------------------------------------------------------------------------------- 1 | testsDirectory = $this->getTestsDirectory(); 22 | 23 | $this->testFinder = new TestFinder($this->testsDirectory); 24 | } 25 | 26 | public function testFindTestFilesInGroups() 27 | { 28 | $functionalTestFiles = $this->testFinder->findTestFilesInGroups(['functional']); 29 | 30 | $this->assertEquals([ 31 | sprintf('%s/FunctionalTest1Test.php', $this->testsDirectory), 32 | sprintf('%s/FunctionalTest2Test.php', $this->testsDirectory), 33 | sprintf('%s/FunctionalTest3Test.php', $this->testsDirectory), 34 | sprintf('%s/FunctionalTest4Test.php', $this->testsDirectory), 35 | sprintf('%s/FunctionalTest5Test.php', $this->testsDirectory), 36 | sprintf('%s/FunctionalTest6Test.php', $this->testsDirectory), 37 | sprintf('%s/FunctionalTest7Test.php', $this->testsDirectory), 38 | sprintf('%s/FunctionalTest8Test.php', $this->testsDirectory), 39 | ], $functionalTestFiles); 40 | } 41 | 42 | public function testFindTestFilesInGroupsUnknownGroup() 43 | { 44 | $this->assertEmpty($this->testFinder->findTestFilesInGroups(['unknown'])); 45 | } 46 | 47 | public function testFindTestFilesExcludingGroups() 48 | { 49 | $testFiles = $this->testFinder->findTestFilesExcludingGroups(['functional']); 50 | 51 | $this->assertFalse(in_array(sprintf('%s/FunctionalTest1Test.php', $this->testsDirectory), $testFiles)); 52 | $this->assertTrue(in_array(sprintf('%s/DatabaseSandboxTest.php', $this->testsDirectory), $testFiles)); 53 | } 54 | 55 | public function testFindAllTestFiles() 56 | { 57 | $testFiles = $this->testFinder->findAllTestFiles(); 58 | 59 | $this->assertTrue(in_array(sprintf('%s/FunctionalTest1Test.php', $this->testsDirectory), $testFiles)); 60 | $this->assertTrue(in_array(sprintf('%s/DatabaseSandboxTest.php', $this->testsDirectory), $testFiles)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/TestRunnerTest.php: -------------------------------------------------------------------------------- 1 | app = $this->createMock(Application::class); 45 | $this->input = $this->createMock(InputInterface::class); 46 | $this->output = $this->createMock(OutputInterface::class); 47 | $this->process = $this->createMock(Process::class); 48 | $this->configuration = (new Configuration()) 49 | ->setRootDir(realpath(__DIR__.'/..')) 50 | ->setPhpunitPath(realpath(__DIR__.'/../vendor/bin/phpunit')) 51 | ; 52 | 53 | $this->testRunner = new TestRunnerStub( 54 | $this->app, 55 | $this->input, 56 | $this->output, 57 | $this->configuration 58 | ); 59 | $this->testRunner->process = $this->process; 60 | } 61 | 62 | public function testGeneratePhpunitXml() 63 | { 64 | $files = [ 65 | 'tests/Command/AllTest.php', 66 | 'tests/TestCounterTest.php', 67 | ]; 68 | 69 | $path = $this->testRunner->generatePhpunitXml($files); 70 | 71 | $xmlSource = file_get_contents($path); 72 | 73 | $xml = simplexml_load_string($xmlSource); 74 | 75 | $suite = $xml->testsuites[0]->testsuite; 76 | $suiteFiles = (array) $suite->file; 77 | 78 | $expectedFiles = [ 79 | $this->configuration->getRootDir().'/tests/Command/AllTest.php', 80 | $this->configuration->getRootDir().'/tests/TestCounterTest.php', 81 | ]; 82 | 83 | $this->assertEquals($expectedFiles, $suiteFiles); 84 | } 85 | 86 | public function testRunTestFiles() 87 | { 88 | $files = [ 89 | 'tests/AllTest.php', 90 | 'src/All.php', 91 | ]; 92 | 93 | $testRunner = $this->buildPartialMock( 94 | TestRunnerStub::class, 95 | [ 96 | 'generatePhpunitXml', 97 | 'runPhpunit', 98 | ], 99 | [ 100 | $this->app, 101 | $this->input, 102 | $this->output, 103 | $this->configuration, 104 | ] 105 | ); 106 | 107 | $testRunner->expects($this->once()) 108 | ->method('generatePhpunitXml') 109 | ->will($this->returnValue('/path/to/phpunit.xml')); 110 | 111 | $testRunner->expects($this->once()) 112 | ->method('runPhpunit') 113 | ->with("-c '/path/to/phpunit.xml'") 114 | ->will($this->returnValue(0)); 115 | 116 | $this->assertEquals(0, $testRunner->runTestFiles($files)); 117 | } 118 | 119 | public function testRunPhpunit() 120 | { 121 | $testRunner = $this->buildPartialMock( 122 | TestRunnerStub::class, 123 | [ 124 | 'run', 125 | ], 126 | [ 127 | $this->app, 128 | $this->input, 129 | $this->output, 130 | $this->configuration, 131 | ] 132 | ); 133 | 134 | $testRunner->expects($this->once()) 135 | ->method('run') 136 | ->with(sprintf("%s --exclude-group=functional -d memory_limit='256M'", $this->configuration->getPhpunitPath())) 137 | ->will($this->returnValue(0)); 138 | 139 | $this->assertEquals(0, $testRunner->runPhpunit('--exclude-group=functional')); 140 | } 141 | 142 | public function testRun() 143 | { 144 | $this->process->expects($this->any()) 145 | ->method('getExitCode') 146 | ->willReturn(0); 147 | 148 | $this->testRunner->passthruResponse = 0; 149 | $this->assertEquals(0, $this->testRunner->run('ls -la')); 150 | } 151 | 152 | /** 153 | * @expectedException RuntimeException 154 | * @expectedExceptionMessage The command did not exit successfully. 155 | */ 156 | public function testRunThrow() 157 | { 158 | $this->process->expects($this->once()) 159 | ->method('getExitCode') 160 | ->willReturn(1); 161 | 162 | $this->assertEquals(0, $this->testRunner->run('ls -la')); 163 | } 164 | 165 | public function testRunTestCommand() 166 | { 167 | $command = $this->createMock(Command::class); 168 | 169 | $this->app->expects($this->once()) 170 | ->method('find') 171 | ->with('test') 172 | ->willReturn($command); 173 | 174 | $command->expects($this->once()) 175 | ->method('run') 176 | ->with(new ArrayInput(['command' => 'test', 'test' => true])) 177 | ->willReturn(0); 178 | 179 | $this->assertEquals(0, $this->testRunner->runTestCommand('test', ['test' => true])); 180 | } 181 | } 182 | 183 | class TestRunnerStub extends TestRunner 184 | { 185 | /** 186 | * @var Process 187 | */ 188 | public $process; 189 | 190 | /** 191 | * @param string $command 192 | * 193 | * @return Process 194 | */ 195 | protected function createProcess(string $command) : Process 196 | { 197 | return $this->process; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | getRootDir(); 10 | 11 | $configuration = $configuration 12 | ->setWatchDirectories([ 13 | sprintf('%s/src', $rootDir), 14 | sprintf('%s/tests', $rootDir) 15 | ]) 16 | ->setTestsDirectory(sprintf('%s/tests', $rootDir)) 17 | ->setPhpunitPath(sprintf('%s/vendor/bin/phpunit', $rootDir)) 18 | ->setDatabaseNames(['testdb1', 'testdb2']) 19 | ; 20 | 21 | $eventDispatcher = $configuration->getEventDispatcher(); 22 | 23 | $eventDispatcher->addListener(Events::SANDBOX_PREPARE, function() { 24 | // prepare the sandbox 25 | }); 26 | 27 | $eventDispatcher->addListener(Events::SANDBOX_CLEANUP, function() { 28 | // cleanup the sandbox 29 | }); 30 | 31 | $eventDispatcher->addListener(Events::DATABASES_CREATE, function() { 32 | // create databases 33 | }); 34 | --------------------------------------------------------------------------------