├── .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 | [](http://travis-ci.org/jwage/phpchunkit)
4 | [](https://scrutinizer-ci.com/g/jwage/phpchunkit/?branch=master)
5 | [](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 | 
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) !== '') {
48 | $contents = preg_replace('{^.+?<\?}s', '', $contents, 1, $replacements);
49 | if ($replacements === 0) {
50 | return array();
51 | }
52 | }
53 | // strip non-php blocks in the file
54 | $contents = preg_replace('{\?>.+<\?}s', '?>', $contents);
55 | // strip trailing non-php code if needed
56 | $pos = strrpos($contents, '?>');
57 | if (false !== $pos && false === strpos(substr($contents, $pos), '')) {
58 | $contents = substr($contents, 0, $pos);
59 | }
60 |
61 | preg_match_all('{
62 | (?:
63 | \b(?])(?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 |
--------------------------------------------------------------------------------