├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── composer.json
├── phpunit.xml.dist
├── src
└── Humbug
│ └── Phpunit
│ ├── Filter
│ ├── FilterInterface.php
│ └── TestSuite
│ │ ├── AbstractFilter.php
│ │ ├── FastestFirstFilter.php
│ │ └── IncludeOnlyFilter.php
│ ├── Listener
│ ├── FilterListener.php
│ └── TimeCollectorListener.php
│ ├── Logger
│ └── JsonLogger.php
│ └── Writer
│ └── JsonWriter.php
└── tests
└── Humbug
└── Test
└── Phpunit
├── Filter
└── TestSuite
│ ├── FastestFirstFilterTest.php
│ └── IncludeOnlyFilterTest.php
├── Listener
├── FilterListenerTest.php
└── TimeCollectorListenerTest.php
├── Logger
└── JsonLoggerTest.php
└── Writer
└── JsonWriterTest.php
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | composer.lock
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 | sudo: false
3 |
4 | php:
5 | - '7.0'
6 | - '7.1'
7 | - nightly
8 | - hhvm
9 |
10 | matrix:
11 | fast_finish: true
12 | include:
13 | - php: '7.0'
14 | env: COMPOSER_FLAGS="--prefer-lowest"
15 | - php: '7.1'
16 | env: COMPOSER_FLAGS="--prefer-lowest"
17 | allow_failures:
18 | - php: nightly
19 | - php: hhvm
20 |
21 | cache:
22 | directories:
23 | - $HOME/.composer/cache
24 |
25 | before_install:
26 | - composer self-update
27 |
28 | before_script:
29 | - travis_retry composer install --no-interaction --prefer-dist
30 |
31 | script:
32 | - vendor/bin/phpunit --coverage-text
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015, Pádraic Brady
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | * Neither the name of Pádraic Brady, Humbug, nor the names of its
15 | contributors may be used to endorse or promote products derived from
16 | this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Humbug PHPUnit Extensions
2 | =========================
3 |
4 | A collection of extensions intended to allow for:
5 | * Timing of test and testsuite execution
6 | * Filter/Reorder test suites using custom filters
7 |
8 |
9 | Installation
10 | ============
11 |
12 | ```json
13 | require: {
14 | "padraic/phpunit-extensions": "~1.0@dev"
15 | }
16 | ```
17 | Configuration
18 | =============
19 |
20 | Time Collector Listener
21 | -----------------------
22 |
23 | The Time Collector Listener logs timing data on tests and test suites for use
24 | by the time dependent filters. You can enable it using the following phpunit.xml
25 | snippet showing the listeners XML block.
26 |
27 | ```xml
28 |
29 |
30 |
31 |
36 |
37 |
38 |
39 | ```
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "padraic/phpunit-extensions",
3 | "description": "Collection of Listeners to log and manipulate test run order",
4 | "type": "library",
5 | "keywords": ["phpunit", "listener", "extension", "humbug", "tests", "testing"],
6 | "license": "BSD-3-Clause",
7 | "authors": [
8 | {
9 | "name": "Padraic Brady",
10 | "email": "padraic.brady@gmail.com",
11 | "homepage": "http://blog.astrumfutura.com"
12 | }
13 | ],
14 | "require": {
15 | "php": "^7.0",
16 | "phpunit/phpunit": "^6.0"
17 | },
18 | "autoload": {
19 | "psr-0": { "Humbug\\": "src/" }
20 | },
21 | "extra": {
22 | "branch-alias": {
23 | "dev-master": "2.0-dev"
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
16 |
17 |
18 | tests
19 |
20 |
21 |
22 |
23 |
24 | ./src
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/Humbug/Phpunit/Filter/FilterInterface.php:
--------------------------------------------------------------------------------
1 | log = $log;
24 | }
25 |
26 | public function filter(array $array)
27 | {
28 | $times = $this->loadTimes();
29 | @usort($array, function (PHPUnitTestSuite $a, PHPUnitTestSuite $b) use ($times) {
30 | $na = $a->getName();
31 | $nb = $b->getName();
32 | if (!isset($times['suites'][$na]) || !isset($times['suites'][$nb])) {
33 | throw new RuntimeException(
34 | 'FastestFirstFilter has encountered an unlogged test suite which cannot be sorted'
35 | );
36 | }
37 | if ($times['suites'][$na] == $times['suites'][$nb]) {
38 | return 0;
39 | }
40 | if ($times['suites'][$na] < $times['suites'][$nb]) {
41 | return -1;
42 | }
43 |
44 | return 1;
45 | });
46 |
47 | return $array;
48 | }
49 |
50 | private function loadTimes()
51 | {
52 | if (!file_exists($this->log)) {
53 | throw new Exception(sprintf(
54 | 'Log file for collected times does not exist: %s. '
55 | .'Use the Humbug\Phpunit\Listener\TimeCollectorListener listener prior '
56 | .'to using the FastestFirstFilter filter at least once',
57 | $this->log
58 | ));
59 | }
60 |
61 | return json_decode(file_get_contents($this->log), true);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Humbug/Phpunit/Filter/TestSuite/IncludeOnlyFilter.php:
--------------------------------------------------------------------------------
1 | exclusivelyInclude = func_get_args();
21 | }
22 |
23 | public function filter(array $array)
24 | {
25 | // clearly will need to revise once multi-root suites are supported
26 | $return = [];
27 | foreach ($array as $suite) {
28 | if (in_array($suite->getName(), $this->exclusivelyInclude)) {
29 | $return[] = $suite;
30 | }
31 | }
32 |
33 | return $return;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Humbug/Phpunit/Listener/FilterListener.php:
--------------------------------------------------------------------------------
1 | rootSuiteNestingLevel = $rootSuiteNestingLevel;
36 | $args = func_get_args();
37 | array_shift($args);
38 | if (empty($args)) {
39 | throw new Exception(sprintf(
40 | 'No %s objects assigned to FilterListener',
41 | FilterInterface::class
42 | ));
43 | }
44 | foreach ($args as $filter) {
45 | $this->addFilter($filter);
46 | }
47 | }
48 |
49 | public function startTestSuite(TestSuite $suite)
50 | {
51 | $this->suiteLevel++;
52 | $this->currentSuiteName = $suite->getName();
53 | if ($this->suiteLevel == (1 + $this->rootSuiteNestingLevel)) {
54 | $this->rootSuiteName = $suite->getName();
55 | $suites = $suite->tests();
56 | $filtered = $this->filterSuites($suites);
57 | $suite->setTests($filtered);
58 | }
59 | }
60 |
61 | public function endTestSuite(TestSuite $suite)
62 | {
63 | $this->suiteLevel--;
64 | }
65 |
66 | public function getSuiteLevel()
67 | {
68 | return $this->suiteLevel;
69 | }
70 |
71 | protected function filterSuites(array $suites)
72 | {
73 | $filtered = $suites;
74 | foreach ($this->suiteFilters as $filter) {
75 | $filtered = $filter->filter($filtered);
76 | }
77 |
78 | return $filtered;
79 | }
80 |
81 | protected function addFilter(FilterInterface $filter)
82 | {
83 | $this->suiteFilters[] = $filter;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/Humbug/Phpunit/Listener/TimeCollectorListener.php:
--------------------------------------------------------------------------------
1 | logger = $logger;
35 | $this->rootSuiteNestingLevel = $rootSuiteNestingLevel;
36 | }
37 |
38 | public function __destruct()
39 | {
40 | $this->rootSuiteNestingLevel = null;
41 | $this->logger = null;
42 | }
43 |
44 | public function startTestSuite(TestSuite $suite)
45 | {
46 | $this->suiteLevel++;
47 | if (!isset($this->rootSuiteName)) {
48 | $this->rootSuiteName = $suite->getName();
49 | }
50 | $this->currentSuiteName = $suite->getName();
51 | }
52 |
53 | /**
54 | * Logs the end of the test.
55 | *
56 | * This method hints Test for its first parameter but then uses getName(), which does not exist on that interface.
57 | * getName() exists on TestCase though, which inherits from Test. So here we assume that $test is always an instance
58 | * of TestCase rather than test.
59 | *
60 | * @param Test $test
61 | * @param float $time
62 | */
63 | public function endTest(Test $test, $time)
64 | {
65 | $this->currentSuiteTime += $time;
66 | $this->logger->logTest(
67 | $this->currentSuiteName,
68 | $test->getName(),
69 | $time
70 | );
71 | }
72 |
73 | public function endTestSuite(TestSuite $suite)
74 | {
75 | /*
76 | * Only log Level 2 test suites, i.e. your actual test classes. Level 1
77 | * is the parent root suite(s) defined in the XML config and Level 3 are
78 | * those hosting data provider tests.
79 | */
80 | if ($this->suiteLevel !== (2 + $this->rootSuiteNestingLevel)) {
81 | $this->suiteLevel--;
82 |
83 | return;
84 | }
85 | $this->suiteLevel--;
86 | $this->logger->logTestSuite(
87 | $suite->getName(),
88 | $this->currentSuiteTime
89 | );
90 | $this->currentSuiteTime = 0;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/Humbug/Phpunit/Logger/JsonLogger.php:
--------------------------------------------------------------------------------
1 | writer = $writer;
35 | }
36 |
37 | public function __destruct()
38 | {
39 | $this->write();
40 | }
41 |
42 | public function logTestSuite($title, $time)
43 | {
44 | $this->suites[$title] = $time;
45 | }
46 |
47 | public function logTest($suite, $title, $time)
48 | {
49 | if (!isset($this->tests[$suite])) {
50 | $this->tests[$suite] = [];
51 | }
52 | $this->tests[$suite][] = [
53 | 'title' => $title,
54 | 'time' => $time,
55 | ];
56 | }
57 |
58 | public function write()
59 | {
60 | $this->writer->write([
61 | 'suites' => $this->suites,
62 | 'tests' => $this->tests,
63 | ]);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Humbug/Phpunit/Writer/JsonWriter.php:
--------------------------------------------------------------------------------
1 | target = $target;
24 | }
25 |
26 | /**
27 | * @param mixed $data
28 | */
29 | public function write($data)
30 | {
31 | file_put_contents($this->target, json_encode($data, JSON_PRETTY_PRINT));
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tests/Humbug/Test/Phpunit/Filter/TestSuite/FastestFirstFilterTest.php:
--------------------------------------------------------------------------------
1 | timeFile = $tmp.'/times.json';
25 | file_put_contents($this->timeFile, '{"suites":{"Suite1":"2","Suite2":"3","Suite3":"1"}}');
26 | }
27 |
28 | protected function tearDown()
29 | {
30 | @unlink($this->timeFile);
31 | }
32 |
33 | public function testShouldFilterSuitesAndReturnFastestFirst()
34 | {
35 | $filter = new FastestFirstFilter($this->timeFile);
36 |
37 | $suite1 = $this->createMock(PHPUnitTestSuite::class);
38 | $suite1->method('getName')->willReturn('Suite1');
39 |
40 | $suite2 = $this->createMock(PHPUnitTestSuite::class);
41 | $suite2->method('getName')->willReturn('Suite2');
42 |
43 | $suite3 = $this->createMock(PHPUnitTestSuite::class);
44 | $suite3->method('getName')->willReturn('Suite3');
45 |
46 | $return = $filter->filter([$suite1, $suite2, $suite3]);
47 |
48 | $this->assertSame([$suite3, $suite1, $suite2], $return);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tests/Humbug/Test/Phpunit/Filter/TestSuite/IncludeOnlyFilterTest.php:
--------------------------------------------------------------------------------
1 | createMock(TestSuite::class);
22 | $suite1->method('getName')->willReturn('Suite1');
23 | $suite2 = $this->createMock(TestSuite::class);
24 | $suite2->method('getName')->willReturn('Suite2');
25 | $suite3 = $this->createMock(TestSuite::class);
26 | $suite3->method('getName')->willReturn('Suite3');
27 |
28 | $filter = new IncludeOnlyFilter('Suite1', 'Suite3');
29 |
30 | $return = $filter->filter([$suite1, $suite2, $suite3]);
31 |
32 | $this->assertSame([$suite1, $suite3], $return);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tests/Humbug/Test/Phpunit/Listener/FilterListenerTest.php:
--------------------------------------------------------------------------------
1 | filter = $this->createMock(FilterInterface::class);
31 | $this->suite = $this->createMock(TestSuite::class);
32 | $this->subSuite1 = $this->createMock(TestSuite::class);
33 | $this->subSuite2 = $this->createMock(TestSuite::class);
34 |
35 | $this->suite->expects($this->atLeast(1))->method('getName')->willReturn('Suite1');
36 | $this->suite->expects($this->once())->method('tests')->willReturn([$this->subSuite1, $this->subSuite2]);
37 |
38 | $this->filter->expects($this->once())->method('filter')
39 | ->with([$this->subSuite1, $this->subSuite2])
40 | ->willReturn([$this->subSuite2, $this->subSuite1]);
41 |
42 | /*
43 | * The setTests method name is deceptive, it essentially accepts an array of
44 | * (sub-)TestSuite objects nested into current TestSuite.
45 | */
46 | $this->suite->expects($this->once())->method('setTests')->with([$this->subSuite2, $this->subSuite1]);
47 | }
48 |
49 | public function testShouldFilterSubSuites()
50 | {
51 | $listener = new FilterListener(0, $this->filter);
52 | $listener->startTestSuite($this->suite);
53 |
54 | /*
55 | * Asset that nesting was reset to root suite
56 | */
57 | $this->assertSame(1, $listener->getSuiteLevel());
58 |
59 | $listener->endTestSuite($this->suite);
60 |
61 | $this->assertSame(0, $listener->getSuiteLevel());
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/tests/Humbug/Test/Phpunit/Listener/TimeCollectorListenerTest.php:
--------------------------------------------------------------------------------
1 | writer = $this->createMock(JsonWriter::class);
44 | $this->writer->expects($this->once())->method('write'); // on destruction
45 |
46 | $this->test1 = $this->createMock(TestCase::class);
47 | $this->test1->expects($this->once())->method('getName')->willReturn('Test1');
48 |
49 | $this->test2 = $this->createMock(TestCase::class);
50 | $this->test2->expects($this->once())->method('getName')->willReturn('Test2');
51 |
52 | $this->suite = $this->createMock(TestSuite::class);
53 | $this->suite->method('getName')->willReturn('Suite1');
54 | }
55 |
56 | public function testShouldCollectNamesAndTimesForLogging()
57 | {
58 | $listener = new TimeCollectorListener(new JsonLogger($this->writer));
59 | $listener->startTestSuite($this->suite);
60 | $listener->endTest($this->test1, 1.0);
61 | $listener->endTest($this->test2, 2.0);
62 | $listener->endTestSuite($this->suite);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/tests/Humbug/Test/Phpunit/Logger/JsonLoggerTest.php:
--------------------------------------------------------------------------------
1 | writer = $this->createMock(JsonWriter::class);
28 | }
29 |
30 | public function testShouldWriteLogsDuringDestruct()
31 | {
32 | $this->writer->expects($this->once())->method('write');
33 |
34 | new JsonLogger($this->writer);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/tests/Humbug/Test/Phpunit/Writer/JsonWriterTest.php:
--------------------------------------------------------------------------------
1 | expectException(InvalidArgumentException::class);
14 |
15 | new JsonWriter($wrongArgument);
16 | }
17 |
18 | public function jsonWriterWrongArgumentsProvider()
19 | {
20 | return [
21 | [''],
22 | [[]],
23 | [null],
24 | [new stdClass()],
25 | ];
26 | }
27 | }
28 |
--------------------------------------------------------------------------------