├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── manifest.xml └── src ├── Attribute └── MaximumDuration.php ├── Collector ├── Collector.php └── DefaultCollector.php ├── Comparator └── DurationComparator.php ├── Console └── Color.php ├── Count.php ├── Duration.php ├── Exception ├── InvalidCount.php ├── InvalidMaximumCount.php ├── InvalidMilliseconds.php ├── InvalidNanoseconds.php ├── InvalidPhaseIdentifier.php ├── InvalidSeconds.php ├── InvalidStart.php ├── InvalidTestDescription.php ├── InvalidTestIdentifier.php ├── PhaseNotStarted.php └── SlowTestListIsEmpty.php ├── Extension.php ├── Formatter ├── DefaultDurationFormatter.php └── DurationFormatter.php ├── MaximumCount.php ├── MaximumDuration.php ├── Phase.php ├── PhaseIdentifier.php ├── PhaseStart.php ├── Reporter ├── DefaultReporter.php └── Reporter.php ├── SlowTest.php ├── SlowTestList.php ├── Subscriber ├── Test │ ├── FinishedSubscriber.php │ └── PreparationStartedSubscriber.php └── TestRunner │ └── ExecutionFinishedSubscriber.php ├── TestDescription.php ├── TestIdentifier.php ├── Time.php ├── TimeKeeper.php └── Version ├── Major.php └── Series.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## Unreleased 8 | 9 | For a full diff see [`2.19.1...main`][2.19.1...main]. 10 | 11 | ## [`2.19.1`][2.19.1] 12 | 13 | For a full diff see [`2.19.0...2.19.1`][2.19.0...2.19.1]. 14 | 15 | ### Fixed 16 | 17 | - Fixed discovery of `@maximumDuration` annotation when using data providers ([#675]), by [@morgan-atproperties] 18 | 19 | ## [`2.19.0`][2.19.0] 20 | 21 | For a full diff see [`2.18.0...2.19.0`][2.18.0...2.19.0]. 22 | 23 | ### Changed 24 | 25 | - Started formatting durations similar to `phpunit/php-timer:^4.0.0`, but always showing minutes ([#664]), by [@localheinz] 26 | 27 | ## [`2.18.0`][2.18.0] 28 | 29 | For a full diff see [`2.17.0...2.18.0`][2.17.0...2.18.0]. 30 | 31 | ### Added 32 | 33 | - Added support for `phpunit/phpunit:^12.0.0` ([#651]), by [@localheinz] 34 | 35 | ## [`2.17.0`][2.17.0] 36 | 37 | For a full diff see [`2.16.1...2.17.0`][2.16.1...2.17.0]. 38 | 39 | ### Added 40 | 41 | - Added support for PHP 8.4 ([#635]), by [@localheinz] 42 | 43 | ## [`2.16.1`][2.16.1] 44 | 45 | For a full diff see [`2.16.0...2.16.1`][2.16.0...2.16.1]. 46 | 47 | ### Fixed 48 | 49 | - Explicitly included `vendor/composer/installed.php` and `vendor/composer/InstalledVersions.php` when building PHAR ([#621]), by [@dantleech] 50 | 51 | ## [`2.16.0`][2.16.0] 52 | 53 | For a full diff see [`2.15.1...2.16.0`][2.15.1...2.16.0]. 54 | 55 | ### Changed 56 | 57 | - Allowed installation on PHP 8.4 ([#604]), by [@localheinz] 58 | 59 | ## [`2.15.1`][2.15.1] 60 | 61 | For a full diff see [`2.15.0...2.15.1`][2.15.0...2.15.1]. 62 | 63 | ### Fixed 64 | 65 | - Explicitly included `src/` directory when building PHAR ([#598]), by [@localheinz] 66 | 67 | ## [`2.15.0`][2.15.0] 68 | 69 | For a full diff see [`2.14.0...2.15.0`][2.14.0...2.15.0]. 70 | 71 | ### Changed 72 | 73 | - Started showing data provider details in list of slow tests ([#559]), by [@mvorisek] 74 | 75 | ## [`2.14.0`][2.14.0] 76 | 77 | For a full diff see [`2.13.0...2.14.0`][2.13.0...2.14.0]. 78 | 79 | ### Changed 80 | 81 | - Added support for `phpunit/phpunit:^6.5.0` ([#533]), by [@localheinz] 82 | - Added support for PHP 7.0 ([#534]), by [@localheinz] 83 | 84 | ## [`2.13.0`][2.13.0] 85 | 86 | For a full diff see [`2.12.0...2.13.0`][2.12.0...2.13.0]. 87 | 88 | ### Changed 89 | 90 | - Added support for PHP 7.1 ([#532]), by [@localheinz] 91 | 92 | ## [`2.12.0`][2.12.0] 93 | 94 | For a full diff see [`2.11.0...2.12.0`][2.11.0...2.12.0]. 95 | 96 | ### Changed 97 | 98 | - Added support for PHP 7.2 ([#531]), by [@localheinz] 99 | 100 | ## [`2.11.0`][2.11.0] 101 | 102 | For a full diff see [`2.10.0...2.11.0`][2.10.0...2.11.0]. 103 | 104 | ### Changed 105 | 106 | - Added support for PHP 7.3 ([#476]), by [@localheinz] 107 | 108 | ## [`2.10.0`][2.10.0] 109 | 110 | For a full diff see [`2.9.0...2.10.0`][2.9.0...2.10.0]. 111 | 112 | ### Changed 113 | 114 | - Added support for `phpunit/phpunit:^11.0.0` ([#485]), by [@localheinz] 115 | - Added support for using `phpunit-slow-test-detector.phar` with `phpunit/phpunit:^9.0.0` ([#491]), by [@localheinz] 116 | - Added support for using `phpunit-slow-test-detector.phar` with `phpunit/phpunit:^8.5.19` ([#494]), by [@localheinz] 117 | - Added support for using `phpunit-slow-test-detector.phar` with `phpunit/phpunit:^7.5.0` ([#495]), by [@localheinz] 118 | 119 | ## [`2.9.0`][2.9.0] 120 | 121 | For a full diff see [`2.8.0...2.9.0`][2.8.0...2.9.0]. 122 | 123 | ### Changed 124 | 125 | - Consistently included test setup and teardown in duration measurement ([#380]), by [@localheinz] and [@mvorisek] 126 | 127 | ### Fixed 128 | 129 | - Required at least `phpunit/phpunit:^7.5.0` ([#448]), by [@localheinz] 130 | 131 | ## [`2.8.0`][2.8.0] 132 | 133 | For a full diff see [`2.7.0...2.8.0`][2.7.0...2.8.0]. 134 | 135 | ### Added 136 | 137 | - Added support for `phpunit/phpunit:^7.2.0` ([#447]), by [@localheinz] 138 | 139 | ## [`2.7.0`][2.7.0] 140 | 141 | For a full diff see [`2.6.0...2.7.0`][2.6.0...2.7.0]. 142 | 143 | ### Changed 144 | 145 | - Widened version constraints to allow installation with `phpunit/phpunit:^8.5.19`, `phpunit/phpunit:^9.0.0`, and `phpunit/phpunit:^10.0.0` ([#396]), by [@localheinz] 146 | 147 | ## [`2.6.0`][2.6.0] 148 | 149 | For a full diff see [`2.5.0...2.6.0`][2.5.0...2.6.0]. 150 | 151 | ### Added 152 | 153 | - Added support for `phpunit/phpunit:^8.5.36` ([#394]), by [@localheinz] 154 | 155 | ## [`2.5.0`][2.5.0] 156 | 157 | For a full diff see [`2.4.0...2.5.0`][2.4.0...2.5.0]. 158 | 159 | ### Added 160 | 161 | - Added `Attribute\MaximumDuration` to allow configuration of maximum duration with attributes on test method level ([#367]), by [@HypeMC] 162 | - Added support for PHP 8.0 ([#375]), by [@localheinz] and [@mvorisek] 163 | - Added support for PHP 7.4 ([#390]), by [@localheinz] and [@mvorisek] 164 | 165 | ### Changed 166 | 167 | - Improved detection of PHPUnit version ([#393]), by [@localheinz] and [@mvorisek] 168 | 169 | ## [`2.4.0`][2.4.0] 170 | 171 | For a full diff see [`2.3.2...2.4.0`][2.3.2...2.4.0]. 172 | 173 | ### Added 174 | 175 | - Added support for `phpunit/phpunit:^9.6.0` ([#341]), by [@localheinz] 176 | 177 | ### Changed 178 | 179 | - Extracted `Duration` ([#351]), by [@localheinz] 180 | - Merged `MaximumDuration` into `Duration` ([#352]), by [@localheinz] 181 | - Renamed `MaximumCount` to `Count` ([#353]), by [@localheinz] 182 | - Extracted `Time` ([#354]), by [@localheinz] 183 | - Extracted `TestIdentifier` ([#355]), by [@localheinz] 184 | - Required `phpunit/phpunit:^10.4.2` ([#357]), by [@localheinz] 185 | 186 | ### Fixed 187 | 188 | - Marked `DefaultDurationFormatter` as internal ([#350]), by [@localheinz] 189 | 190 | ## [`2.3.2`][2.3.2] 191 | 192 | For a full diff see [`2.3.1...2.3.2`][2.3.1...2.3.2]. 193 | 194 | ### Fixed 195 | 196 | - Adjusted version in `manifest.xml` ([#343]), by [@localheinz] 197 | 198 | ## [`2.3.1`][2.3.1] 199 | 200 | For a full diff see [`2.3.0...2.3.1`][2.3.0...2.3.1]. 201 | 202 | ### Fixed 203 | 204 | - Prevented inclusion of `phpunit/phpunit` in PHAR ([#342]), by [@localheinz] 205 | 206 | ## [`2.3.0`][2.3.0] 207 | 208 | For a full diff see [`2.2.0...2.3.0`][2.2.0...2.3.0]. 209 | 210 | ### Changed 211 | 212 | - Added support for installing extension as a PHAR ([#273]), by [@localheinz] 213 | - Added support for PHP 8.3 ([#340]), by [@localheinz] 214 | 215 | ## [`2.2.0`][2.2.0] 216 | 217 | For a full diff see [`2.1.1...2.2.0`][2.1.1...2.2.0]. 218 | 219 | ### Changed 220 | 221 | - Suggested and required `phpunit/phpunit` as a development dependency to allow usage with `phpunit/phpunit` when installed as PHAR ([#272]), by [@localheinz] 222 | 223 | ## [`2.1.1`][2.1.1] 224 | 225 | For a full diff see [`2.1.0...2.1.1`][2.1.0...2.1.1]. 226 | 227 | ### Fixed 228 | 229 | - Stopped registering extension when running `phpunit` with the `--no-output` option ([#243]), by [@localheinz] 230 | 231 | ## [`2.1.0`][2.1.0] 232 | 233 | For a full diff see [`2.0.0...2.1.0`][2.0.0...2.1.0]. 234 | 235 | ### Changed 236 | 237 | - Started rendering slow tests as ordered list ([#224]), by [@localheinz] 238 | 239 | ## [`2.0.0`][2.0.0] 240 | 241 | For a full diff see [`1.0.0...2.0.0`][1.0.0...2.0.0]. 242 | 243 | ### Changed 244 | 245 | - Allowed configuring the maximum duration via `maximum-duration` parameter ([#212]), by [@localheinz] 246 | - Allowed configuring the maximum count via `maximum-count` parameter ([#217]), by [@localheinz] 247 | - Marked classes and interfaces as internal ([#219]), by [@localheinz] 248 | - Brought duration formatting in line with `phpunit/php-timer` ([#220]), by [@localheinz] 249 | - Allowed configuring the maximum duration via `@maximumDuration` annotation ([#222]), by [@localheinz] 250 | 251 | ### Fixed 252 | 253 | - Removed possibility to configure maximum count of reported tests using the `MAXIMUM_NUMBER` environment variable ([#211]), by [@localheinz] 254 | - Increased default maximum count from `3` to `10` and default maximum duration from `125` to `500` milliseconds ([#218]), by [@localheinz] 255 | - Fixed resolving maximum duration from `@slowThreshold` annotation ([#221]), by [@localheinz] 256 | 257 | ## [`1.0.0`][1.0.0] 258 | 259 | For a full diff see [`7afa59c...1.0.0`][7afa59c...1.0.0]. 260 | 261 | ### Added 262 | 263 | - Added `SlowTest` ([#6]), by [@localheinz] 264 | - Added `SlowTestCollector` ([#8]), by [@localheinz] 265 | - Added `Subscriber\TestPreparedSubscriber` ([#12]), by [@localheinz] 266 | - Added `Subscriber\TestPassedSubscriber` ([#13]), by [@localheinz] 267 | - Added `Formatter\ToMillisecondsDurationFormatter` ([#17]), by [@localheinz] 268 | - Added `Comparator\DurationComparator` ([#18]), by [@localheinz] 269 | - Added `SlowTestReporter` ([#19]), by [@localheinz] 270 | - Extracted `TimeKeeper` ([#22]), by [@localheinz] 271 | - Extracted `Collector` ([#23]), by [@localheinz] 272 | - Added `Subscriber\TestSuiteFinishedSubscriber` ([#34]), by [@localheinz] 273 | - Added `MaximumDuration` ([#46]), by [@localheinz] 274 | - Added `MaximumCount` ([#47]), by [@localheinz] 275 | - Allowed configuring the maximum duration for a test with a `@slowThreshold` annotation ([#49]), by [@localheinz] 276 | 277 | ### Changed 278 | 279 | - Renamed `SlowTestReporter` to `Reporter\Reporter` ([#20]), by [@localheinz] 280 | - Renamed `Reporter\Reporter` to `Reporter\DefaultReporter` and extracted `Reporter\Reporter` interface ([#21]), by [@localheinz] 281 | - Renamed `Collector` to `Collector\DefaultCollector` and extracted `Collector\Collector` interface ([#24]), by [@localheinz] 282 | - Used `TimeKeeper` instead of `SlowTestCollector` in `Subscriber\TestPreparedSubscriber` ([#25]), by [@localheinz] 283 | - Used `TimeKeeper` and `Collector\Collector` instead of `SlowTestCollector` in `Subscriber\TestPassedSubscriber` ([#26]), by [@localheinz] 284 | - Composed maximum duration into `SlowTest` ([#37]), by [@localheinz] 285 | - Rendered maximum duration in report created by `DefaultReporter` ([#38]), by [@localheinz] 286 | 287 | ### Removed 288 | 289 | - Removed `SlowTestCollector` ([#36]), by [@localheinz] 290 | 291 | [1.0.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/1.0.0 292 | [2.0.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.0.0 293 | [2.1.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.1.0 294 | [2.1.1]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.1.1 295 | [2.2.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.2.0 296 | [2.3.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.3.0 297 | [2.3.1]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.3.1 298 | [2.3.2]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.3.2 299 | [2.4.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.4.0 300 | [2.5.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.5.0 301 | [2.6.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.6.0 302 | [2.7.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.7.0 303 | [2.8.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.8.0 304 | [2.9.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.9.0 305 | [2.10.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.10.0 306 | [2.11.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.11.0 307 | [2.12.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.12.0 308 | [2.13.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.13.0 309 | [2.14.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.14.0 310 | [2.15.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.15.0 311 | [2.15.1]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.15.1 312 | [2.16.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.16.0 313 | [2.16.1]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.16.1 314 | [2.17.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.17.0 315 | [2.18.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.18.0 316 | [2.19.0]: https://github.com/ergebnis/phpunit-slow-test-detector/releases/tag/2.19.0 317 | 318 | [7afa59c...1.0.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/7afa59c...1.0.0 319 | [1.0.0...2.0.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/1.0.0...2.0.0 320 | [2.0.0...2.1.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.0.0...2.1.0 321 | [2.1.0...2.1.1]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.1.0...2.1.1 322 | [2.1.1...2.2.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.1.1...2.2.0 323 | [2.2.0...2.3.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.2.0...2.3.0 324 | [2.3.0...2.3.1]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.3.0...2.3.1 325 | [2.3.1...2.3.2]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.3.1...2.3.2 326 | [2.3.2...2.4.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.3.2...2.4.0 327 | [2.4.0...2.5.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.4.0...2.5.0 328 | [2.5.0...2.6.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.5.0...2.6.0 329 | [2.6.0...2.7.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.6.0...2.7.0 330 | [2.7.0...2.8.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.7.0...2.8.0 331 | [2.8.0...2.9.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.8.0...2.9.0 332 | [2.9.0...2.10.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.9.0...2.10.0 333 | [2.10.0...2.11.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.10.0...2.11.0 334 | [2.11.0...2.12.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.11.0...2.12.0 335 | [2.12.0...2.13.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.12.0...2.13.0 336 | [2.13.0...2.14.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.13.0...2.14.0 337 | [2.14.0...2.15.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.14.0...2.15.0 338 | [2.15.0...2.15.1]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.15.0...2.15.1 339 | [2.15.1...2.16.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.15.1...2.16.0 340 | [2.16.0...2.16.1]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.16.0...2.16.1 341 | [2.16.1...2.17.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.16.1...2.17.0 342 | [2.17.0...2.18.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.17.0...2.18.0 343 | [2.18.0...2.19.0]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.18.0...2.19.0 344 | [2.19.0...2.19.1]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.19.0...2.19.1 345 | [2.19.1...main]: https://github.com/ergebnis/phpunit-slow-test-detector/compare/2.19.1...main 346 | 347 | [#6]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/6 348 | [#8]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/8 349 | [#12]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/12 350 | [#13]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/13 351 | [#17]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/17 352 | [#18]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/18 353 | [#19]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/19 354 | [#20]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/20 355 | [#21]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/21 356 | [#22]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/22 357 | [#23]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/23 358 | [#24]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/24 359 | [#25]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/25 360 | [#26]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/26 361 | [#34]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/34 362 | [#36]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/36 363 | [#37]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/37 364 | [#38]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/38 365 | [#46]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/46 366 | [#47]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/47 367 | [#49]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/49 368 | [#211]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/211 369 | [#212]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/212 370 | [#217]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/217 371 | [#218]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/218 372 | [#219]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/219 373 | [#220]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/220 374 | [#221]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/221 375 | [#222]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/222 376 | [#224]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/224 377 | [#243]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/243 378 | [#272]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/272 379 | [#273]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/273 380 | [#340]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/340 381 | [#341]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/341 382 | [#342]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/342 383 | [#343]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/343 384 | [#350]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/350 385 | [#351]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/351 386 | [#352]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/352 387 | [#353]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/353 388 | [#354]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/354 389 | [#355]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/355 390 | [#357]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/357 391 | [#367]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/367 392 | [#375]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/375 393 | [#390]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/390 394 | [#393]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/393 395 | [#394]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/394 396 | [#396]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/396 397 | [#447]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/447 398 | [#448]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/448 399 | [#476]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/476 400 | [#485]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/485 401 | [#491]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/491 402 | [#494]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/494 403 | [#495]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/495 404 | [#531]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/531 405 | [#532]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/532 406 | [#533]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/533 407 | [#534]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/534 408 | [#559]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/559 409 | [#598]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/598 410 | [#604]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/604 411 | [#635]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/635 412 | [#651]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/651 413 | [#664]: https://github.com/ergebnis/phpunit-slow-test-detector/pull/664 414 | 415 | [@dantleech]: https://github.com/dantleech 416 | [@HypeMC]: https://github.com/HypeMC 417 | [@localheinz]: https://github.com/localheinz 418 | [@morgan-atproperties]: https://github.com/morgan-atproperties 419 | [@mvorisek]: https://github.com/mvorisek 420 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2021-2025 Andreas Möller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the _Software_), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED **AS IS**, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # phpunit-slow-test-detector 2 | 3 | [![Integrate](https://github.com/ergebnis/phpunit-slow-test-detector/workflows/Integrate/badge.svg)](https://github.com/ergebnis/phpunit-slow-test-detector/actions) 4 | [![Merge](https://github.com/ergebnis/phpunit-slow-test-detector/workflows/Merge/badge.svg)](https://github.com/ergebnis/phpunit-slow-test-detector/actions) 5 | [![Release](https://github.com/ergebnis/phpunit-slow-test-detector/workflows/Release/badge.svg)](https://github.com/ergebnis/phpunit-slow-test-detector/actions) 6 | [![Renew](https://github.com/ergebnis/phpunit-slow-test-detector/workflows/Renew/badge.svg)](https://github.com/ergebnis/phpunit-slow-test-detector/actions) 7 | 8 | [![Code Coverage](https://codecov.io/gh/ergebnis/phpunit-slow-test-detector/branch/main/graph/badge.svg)](https://codecov.io/gh/ergebnis/phpunit-slow-test-detector) 9 | 10 | [![Latest Stable Version](https://poser.pugx.org/ergebnis/phpunit-slow-test-detector/v/stable)](https://packagist.org/packages/ergebnis/phpunit-slow-test-detector) 11 | [![Total Downloads](https://poser.pugx.org/ergebnis/phpunit-slow-test-detector/downloads)](https://packagist.org/packages/ergebnis/phpunit-slow-test-detector) 12 | [![Monthly Downloads](http://poser.pugx.org/ergebnis/phpunit-slow-test-detector/d/monthly)](https://packagist.org/packages/ergebnis/phpunit-slow-test-detector) 13 | 14 | This project provides a [`composer`](https://getcomposer.org) package and a [Phar archive](https://www.php.net/manual/en/book.phar.php) with an extension for detecting slow tests in [`phpunit/phpunit`](https://github.com/sebastianbergmann/phpunit). 15 | 16 | The extension is compatible with the following versions of `phpunit/phpunit`: 17 | 18 | - [`phpunit/phpunit:^6.5.0`](https://github.com/sebastianbergmann/phpunit/tree/6.5.0) 19 | - [`phpunit/phpunit:^7.5.0`](https://github.com/sebastianbergmann/phpunit/tree/7.5.0) 20 | - [`phpunit/phpunit:^8.5.19`](https://github.com/sebastianbergmann/phpunit/tree/8.5.19) 21 | - [`phpunit/phpunit:^9.0.0`](https://github.com/sebastianbergmann/phpunit/tree/9.0.0) 22 | - [`phpunit/phpunit:^10.0.0`](https://github.com/sebastianbergmann/phpunit/tree/10.0.0) 23 | - [`phpunit/phpunit:^11.0.0`](https://github.com/sebastianbergmann/phpunit/tree/11.0.0) 24 | - [`phpunit/phpunit:^12.0.0`](https://github.com/sebastianbergmann/phpunit/tree/12.0.0) 25 | 26 | ## Installation 27 | 28 | ### Installation with `composer` 29 | 30 | Run 31 | 32 | ```sh 33 | composer require --dev ergebnis/phpunit-slow-test-detector 34 | ``` 35 | 36 | to install `ergebnis/phpunit-slow-test-detector` as a `composer` package. 37 | 38 | ### Installation as Phar 39 | 40 | Download `phpunit-slow-test-detector.phar` from the [latest release](https://github.com/ergebnis/phpunit-slow-test-detector/releases/latest). 41 | 42 | ## Usage 43 | 44 | ### Bootstrapping the extension 45 | 46 | Before the extension can detect slow tests in `phpunit/phpunit`, you need to bootstrap it. The bootstrapping mechanism depends on the version of `phpunit/phpunit` you are using. 47 | 48 | ### Bootstrapping the extension as a `composer` package 49 | 50 | To bootstrap the extension as a `composer` package when using 51 | 52 | - `phpunit/phpunit:^6.5.0` 53 | 54 | adjust your `phpunit.xml` configuration file and configure the 55 | 56 | - [`listeners` element](https://phpunit.de/manual/6.5/en/appendixes.configuration.html#appendixes.configuration.test-listeners) on [`phpunit/phpunit:^6.5.0`](https://phpunit.de/manual/6.5/en/) 57 | 58 | ```diff 59 | 64 | + 65 | + 66 | + 67 | 68 | 69 | test/Unit/ 70 | 71 | 72 | 73 | ``` 74 | 75 | To bootstrap the extension as a `composer` package when using 76 | 77 | - `phpunit/phpunit:^7.5.0` 78 | - `phpunit/phpunit:^8.5.19` 79 | - `phpunit/phpunit:^9.0.0` 80 | 81 | adjust your `phpunit.xml` configuration file and configure the 82 | 83 | - [`extensions` element](https://docs.phpunit.de/en/7.5/configuration.html#the-extensions-element) on [`phpunit/phpunit:^7.5.0`](https://docs.phpunit.de/en/7.5/) 84 | - [`extensions` element](https://docs.phpunit.de/en/8.5/configuration.html#the-extensions-element) on [`phpunit/phpunit:^8.5.19`](https://docs.phpunit.de/en/8.5/) 85 | - [`extensions` element](https://docs.phpunit.de/en/9.6/configuration.html#the-extensions-element) on [`phpunit/phpunit:^9.0.0`](https://docs.phpunit.de/en/9.6/) 86 | 87 | ```diff 88 | 93 | + 94 | + 95 | + 96 | 97 | 98 | test/Unit/ 99 | 100 | 101 | 102 | ``` 103 | 104 | To bootstrap the extension as a `composer` package when using 105 | 106 | - `phpunit/phpunit:^10.0.0` 107 | - `phpunit/phpunit:^11.0.0` 108 | - `phpunit/phpunit:^12.0.0` 109 | 110 | adjust your `phpunit.xml` configuration file and configure the 111 | 112 | - [`extensions` element](https://docs.phpunit.de/en/10.5/configuration.html#the-extensions-element) on [`phpunit/phpunit:^10.0.0`](https://docs.phpunit.de/en/10.5/) 113 | - [`extensions` element](https://docs.phpunit.de/en/11.0/configuration.html#the-extensions-element) on [`phpunit/phpunit:^11.0.0`](https://docs.phpunit.de/en/11.0/) 114 | - [`extensions` element](https://docs.phpunit.de/en/12.0/configuration.html#the-extensions-element) on [`phpunit/phpunit:^12.0.0`](https://docs.phpunit.de/en/12.0/) 115 | 116 | ```diff 117 | 122 | + 123 | + 124 | + 125 | 126 | 127 | test/Unit/ 128 | 129 | 130 | 131 | ``` 132 | 133 | ### Bootstrapping the extension as a PHAR 134 | 135 | To bootstrap the extension as a PHAR when using 136 | 137 | - `phpunit/phpunit:^7.5.0` 138 | - `phpunit/phpunit:^8.5.19` 139 | - `phpunit/phpunit:^9.0.0` 140 | 141 | adjust your `phpunit.xml` configuration file and configure the 142 | 143 | - [`extensionsDirectory` attribute](https://docs.phpunit.de/en/7.5/configuration.html#the-extensionsdirectory-attribute) and the [`extensions` element](https://docs.phpunit.de/en/7.5/configuration.html#the-extensions-element) on [`phpunit/phpunit:^7.5.0`](https://docs.phpunit.de/en/7.5/) 144 | - [`extensionsDirectory` attribute](https://docs.phpunit.de/en/8.5/configuration.html#the-extensionsdirectory-attribute) and the [`extensions` element](https://docs.phpunit.de/en/8.5/configuration.html#the-extensions-element) on [`phpunit/phpunit:^8.5.19`](https://docs.phpunit.de/en/8.5/) 145 | - [`extensionsDirectory` attribute](https://docs.phpunit.de/en/9.6/configuration.html#the-extensionsdirectory-attribute) and the [`extensions` element](https://docs.phpunit.de/en/9.6/configuration.html#the-extensions-element) on [`phpunit/phpunit:^9.0.0`](https://docs.phpunit.de/en/9.5/) 146 | 147 | ```diff 148 | 154 | + 155 | + 156 | + 157 | 158 | 159 | test/Unit/ 160 | 161 | 162 | 163 | ``` 164 | 165 | To bootstrap the extension as a PHAR when using 166 | 167 | - `phpunit/phpunit:^10.0.0` 168 | - `phpunit/phpunit:^11.0.0` 169 | - `phpunit/phpunit:^12.0.0` 170 | 171 | adjust your `phpunit.xml` configuration file and configure the 172 | 173 | - [`extensionsDirectory` attribute](https://docs.phpunit.de/en/10.5/configuration.html#the-extensionsdirectory-attribute) and the [`extensions` element](https://docs.phpunit.de/en/10.5/configuration.html#the-extensions-element) on [`phpunit/phpunit:^10.0.0`](https://docs.phpunit.de/en/10.5/) 174 | - [`extensionsDirectory` attribute](https://docs.phpunit.de/en/11.0/configuration.html#the-extensionsdirectory-attribute) and the [`extensions` element](https://docs.phpunit.de/en/11.0/configuration.html#the-extensions-element) on [`phpunit/phpunit:^11.0.0`](https://docs.phpunit.de/en/11.0/) 175 | - [`extensionsDirectory` attribute](https://docs.phpunit.de/en/12.0/configuration.html#the-extensionsdirectory-attribute) and the [`extensions` element](https://docs.phpunit.de/en/12.0/configuration.html#the-extensions-element) on [`phpunit/phpunit:^12.0.0`](https://docs.phpunit.de/en/12.0/) 176 | 177 | ```diff 178 | 184 | + 185 | + 186 | + 187 | 188 | 189 | test/Unit/ 190 | 191 | 192 | 193 | ``` 194 | 195 | ### Configuring the extension 196 | 197 | You can configure the extension with the following options in your `phpunit.xml` configuration file: 198 | 199 | - `maximum-count`, an `int`, the maximum count of slow test that should be reported, defaults to `10` 200 | - `maximum-duration`, an `int`, the maximum duration in milliseconds for a test before the extension considers it as a slow test, defaults to `500` 201 | 202 | The configuration mechanism depends on the version of `phpunit/phpunit` you are using. 203 | 204 | ### Configuring the extension 205 | 206 | To configure the extension when using 207 | 208 | - `phpunit/phpunit:^6.5.0` 209 | 210 | adjust your `phpunit.xml` configuration file and configure the 211 | 212 | - [`arguments` element](https://phpunit.de/manual/6.5/en/appendixes.configuration.html#appendixes.configuration.test-listeners) on [`phpunit/phpunit:^6.5.0`](https://phpunit.de/manual/6.5/en/) 213 | 214 | The following example configures the maximum count of slow tests to three, and the maximum duration for all tests to 250 milliseconds: 215 | 216 | ```diff 217 | 222 | 223 | - 224 | + 225 | + 226 | + 227 | + 228 | + 3 229 | + 230 | + 231 | + 250 232 | + 233 | + 234 | + 235 | + 236 | 237 | 238 | 239 | test/Unit/ 240 | 241 | 242 | 243 | ``` 244 | 245 | To configure the extension when using 246 | 247 | - `phpunit/phpunit:^7.5.0` 248 | - `phpunit/phpunit:^8.5.19` 249 | - `phpunit/phpunit:^9.0.0` 250 | 251 | adjust your `phpunit.xml` configuration file and configure the 252 | 253 | - [`arguments` element](https://docs.phpunit.de/en/7.5/configuration.html#the-arguments-element) on [`phpunit/phpunit:^7.5.0`](https://docs.phpunit.de/en/7.5/) 254 | - [`arguments` element](https://docs.phpunit.de/en/8.5/configuration.html#the-arguments-element) on [`phpunit/phpunit:^8.5.19`](https://docs.phpunit.de/en/8.5/) 255 | - [`arguments` element](https://docs.phpunit.de/en/9.6/configuration.html#the-arguments-element) on [`phpunit/phpunit:^9.0.0`](https://docs.phpunit.de/en/9.6/) 256 | 257 | The following example configures the maximum count of slow tests to three, and the maximum duration for all tests to 250 milliseconds: 258 | 259 | ```diff 260 | 265 | 266 | - 267 | + 268 | + 269 | + 270 | + 271 | + 3 272 | + 273 | + 274 | + 250 275 | + 276 | + 277 | + 278 | + 279 | 280 | 281 | 282 | test/Unit/ 283 | 284 | 285 | 286 | ``` 287 | 288 | To configure the extension when using 289 | 290 | - `phpunit/phpunit:^10.0.0` 291 | - `phpunit/phpunit:^11.0.0` 292 | - `phpunit/phpunit:^12.0.0` 293 | 294 | adjust your `phpunit.xml` configuration file and configure one or more 295 | 296 | - [`parameter` elements](https://docs.phpunit.de/en/10.5/configuration.html#the-parameter-element) on [`phpunit/phpunit:^10.0.0`](https://docs.phpunit.de/en/10.5/) 297 | - [`parameter` elements](https://docs.phpunit.de/en/11.0/configuration.html#the-parameter-element) on [`phpunit/phpunit:^11.0.0`](https://docs.phpunit.de/en/11.0/) 298 | - [`parameter` elements](https://docs.phpunit.de/en/12.0/configuration.html#the-parameter-element) on [`phpunit/phpunit:^12.0.0`](https://docs.phpunit.de/en/12.0/) 299 | 300 | The following example configures the maximum count of slow tests to three, and the maximum duration for all tests to 250 milliseconds: 301 | 302 | ```diff 303 | 308 | 309 | - 310 | + 311 | + 312 | + 313 | + 314 | 315 | 316 | 317 | test/Unit/ 318 | 319 | 320 | 321 | ``` 322 | 323 | ### Configuring the maximum duration per test case 324 | 325 | You can configure the maximum duration for a single test case with 326 | 327 | - an `Attribute\MaximumDuration` attribute when using 328 | - `phpunit/phpunit:^10.0.0` 329 | - `phpunit/phpunit:^11.0.0` 330 | - `phpunit/phpunit:^12.0.0` 331 | - a `@maximumDuration` annotation in the DocBlock when using 332 | - `phpunit/phpunit:^6.5.0` 333 | - `phpunit/phpunit:^7.5.0` 334 | - `phpunit/phpunit:^8.5.19` 335 | - `phpunit/phpunit:^9.0.0` 336 | - a `@slowThreshold` annotation in the DocBlock when using 337 | - `phpunit/phpunit:^6.5.0` 338 | - `phpunit/phpunit:^7.5.0` 339 | - `phpunit/phpunit:^8.5.19` 340 | - `phpunit/phpunit:^9.0.0` 341 | 342 | The following example configures the maximum durations for single test cases to 5,000 ms, 4,000 ms, and 3,000 ms: 343 | 344 | ```php 345 | [!NOTE] 379 | > 380 | > Support for the `@slowThreshold` annotation exists only to help you move from [`johnkary/phpunit-speedtrap`](https://github.com/johnkary/phpunit-speedtrap). It will be deprecated and removed in the near future. 381 | 382 | ### Running tests 383 | 384 | When you have bootstrapped the extension, you can run your tests as usually: 385 | 386 | ```sh 387 | vendor/bin/phpunit 388 | ``` 389 | 390 | When the extension has detected slow tests, it will report them: 391 | 392 | ```console 393 | PHPUnit 10.0.0 by Sebastian Bergmann and contributors. 394 | 395 | Runtime: PHP 8.1.0 396 | Configuration: test/EndToEnd/Default/phpunit.xml 397 | Random Seed: 1676103726 398 | 399 | ............. 13 / 13 (100%) 400 | 401 | Detected 11 tests where the duration exceeded the maximum duration. 402 | 403 | 1. 00:01.604 (00:00.500) Ergebnis\PHPUnit\SlowTestDetector\Test\EndToEnd\Default\SleeperTest::testSleeperSleepsLongerThanDefaultMaximumDurationWithDataProvider#9 404 | 2. 00:01.505 (00:00.500) Ergebnis\PHPUnit\SlowTestDetector\Test\EndToEnd\Default\SleeperTest::testSleeperSleepsLongerThanDefaultMaximumDurationWithDataProvider#8 405 | 3. 00:01.403 (00:00.500) Ergebnis\PHPUnit\SlowTestDetector\Test\EndToEnd\Default\SleeperTest::testSleeperSleepsLongerThanDefaultMaximumDurationWithDataProvider#7 406 | 4. 00:01.303 (00:00.500) Ergebnis\PHPUnit\SlowTestDetector\Test\EndToEnd\Default\SleeperTest::testSleeperSleepsLongerThanDefaultMaximumDurationWithDataProvider#6 407 | 5. 00:01.205 (00:00.500) Ergebnis\PHPUnit\SlowTestDetector\Test\EndToEnd\Default\SleeperTest::testSleeperSleepsLongerThanDefaultMaximumDurationWithDataProvider#5 408 | 6. 00:01.103 (00:00.500) Ergebnis\PHPUnit\SlowTestDetector\Test\EndToEnd\Default\SleeperTest::testSleeperSleepsLongerThanDefaultMaximumDurationWithDataProvider#4 409 | 7. 00:01.005 (00:00.500) Ergebnis\PHPUnit\SlowTestDetector\Test\EndToEnd\Default\SleeperTest::testSleeperSleepsLongerThanDefaultMaximumDurationWithDataProvider#3 410 | 8. 00:00.905 (00:00.500) Ergebnis\PHPUnit\SlowTestDetector\Test\EndToEnd\Default\SleeperTest::testSleeperSleepsLongerThanDefaultMaximumDurationWithDataProvider#2 411 | 9. 00:00.805 (00:00.500) Ergebnis\PHPUnit\SlowTestDetector\Test\EndToEnd\Default\SleeperTest::testSleeperSleepsLongerThanDefaultMaximumDurationWithDataProvider#1 412 | 10. 00:00.705 (00:00.500) Ergebnis\PHPUnit\SlowTestDetector\Test\EndToEnd\Default\SleeperTest::testSleeperSleepsLongerThanDefaultMaximumDurationWithDataProvider#0 413 | 414 | There is 1 additional slow test that is not listed here. 415 | 416 | Time: 00:12.601, Memory: 8.00 MB 417 | 418 | OK (13 tests, 13 assertions) 419 | ``` 420 | 421 | ### Understanding measured test durations 422 | 423 | #### Understanding measured test durations when using the hooks event system 424 | 425 | When using 426 | 427 | - `phpunit/phpunit:^6.5.0` 428 | - `phpunit/phpunit:^7.5.0` 429 | - `phpunit/phpunit:^8.5.19` 430 | - `phpunit/phpunit:^9.0.0` 431 | 432 | the extension uses the hooks event system of `phpunit/phpunit`, and measures the duration that is passed to the [`PHPUnit\Runner\AfterTestHook`](https://github.com/sebastianbergmann/phpunit/blob/7.5.0/src/Runner/Hook/AfterTestHook.php#L12-L21). This is the [duration of invoking `PHPUnit\Framework\TestCase::runBare()` and more](https://github.com/sebastianbergmann/phpunit/blob/8.5.19/src/Framework/TestResult.php#L671-L754). 433 | 434 | > [!NOTE] 435 | > Because of this behavior, the measured test durations can and will vary depending on the order in which `phpunit/phpunit` executes tests. 436 | 437 | #### Understanding measured test durations when using the new event system 438 | 439 | When using 440 | 441 | - `phpunit/phpunit:^10.0.0` 442 | - `phpunit/phpunit:^11.0.0` 443 | - `phpunit/phpunit:^12.0.0` 444 | 445 | the extension uses the new event system of `phpunit/phpunit`, and measures the duration between the points in time when the [`PHPUnit\Event\Test\PreparationStarted`](https://github.com/sebastianbergmann/phpunit/blob/10.0.0/src/Event/Events/Test/Lifecycle/PreparationStarted.php#L22-L50) and [`PHPUnit\Event\Test\Finished`](https://github.com/sebastianbergmann/phpunit/blob/10.0.0/src/Event/Events/Test/Lifecycle/Finished.php#L22-L57) are emitted. 446 | 447 | ## Changelog 448 | 449 | The maintainers of this project record notable changes to this project in a [changelog](CHANGELOG.md). 450 | 451 | ## Contributing 452 | 453 | The maintainers of this project suggest following the [contribution guide](.github/CONTRIBUTING.md). 454 | 455 | ## Code of Conduct 456 | 457 | The maintainers of this project ask contributors to follow the [code of conduct](.github/CODE_OF_CONDUCT.md). 458 | 459 | ## General Support Policy 460 | 461 | The maintainers of this project provide limited support. 462 | 463 | You can support the maintenance of this project by [sponsoring @localheinz](https://github.com/sponsors/localheinz) or [requesting an invoice for services related to this project](mailto:am@localheinz.com?subject=ergebnis/phpunit-slow-test-detector:%20Requesting%20invoice%20for%20services). 464 | 465 | ## PHP Version Support Policy 466 | 467 | This project supports PHP versions with [active and security support](https://www.php.net/supported-versions.php). 468 | 469 | The maintainers of this project add support for a PHP version following its initial release and drop support for a PHP version when it has reached the end of security support. 470 | 471 | ## Security Policy 472 | 473 | This project has a [security policy](.github/SECURITY.md). 474 | 475 | ## License 476 | 477 | This project uses the [MIT license](LICENSE.md). 478 | 479 | ## Credits 480 | 481 | This package is inspired by [`johnkary/phpunit-speedtrap`](https://github.com/johnkary/phpunit-speedtrap), originally licensed under MIT by [John Kary](https://github.com/johnkary). 482 | 483 | ## Social 484 | 485 | Follow [@localheinz](https://twitter.com/intent/follow?screen_name=localheinz) and [@ergebnis](https://twitter.com/intent/follow?screen_name=ergebnis) on Twitter. 486 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ergebnis/phpunit-slow-test-detector", 3 | "description": "Provides facilities for detecting slow tests in phpunit/phpunit.", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "phpunit", 8 | "slow", 9 | "test", 10 | "detector", 11 | "extension" 12 | ], 13 | "authors": [ 14 | { 15 | "name": "Andreas Möller", 16 | "email": "am@localheinz.com", 17 | "homepage": "https://localheinz.com" 18 | } 19 | ], 20 | "homepage": "https://github.com/ergebnis/phpunit-slow-test-detector", 21 | "support": { 22 | "issues": "https://github.com/ergebnis/phpunit-slow-test-detector/issues", 23 | "source": "https://github.com/ergebnis/phpunit-slow-test-detector", 24 | "security": "https://github.com/ergebnis/phpunit-slow-test-detector/blob/main/.github/SECURITY.md" 25 | }, 26 | "require": { 27 | "php": "~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", 28 | "phpunit/phpunit": "^6.5.0 || ^7.5.0 || ^8.5.19 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0" 29 | }, 30 | "require-dev": { 31 | "ergebnis/composer-normalize": "^2.47.0", 32 | "ergebnis/license": "^2.6.0", 33 | "ergebnis/php-cs-fixer-config": "^6.46.0", 34 | "fakerphp/faker": "~1.20.0", 35 | "phpstan/extension-installer": "^1.4.3", 36 | "phpstan/phpstan": "^1.12.11", 37 | "phpstan/phpstan-deprecation-rules": "^1.2.1", 38 | "phpstan/phpstan-phpunit": "^1.4.1", 39 | "phpstan/phpstan-strict-rules": "^1.6.1", 40 | "psr/container": "~1.0.0", 41 | "rector/rector": "^1.2.10" 42 | }, 43 | "minimum-stability": "dev", 44 | "prefer-stable": true, 45 | "autoload": { 46 | "psr-4": { 47 | "Ergebnis\\PHPUnit\\SlowTestDetector\\": "src/" 48 | } 49 | }, 50 | "autoload-dev": { 51 | "psr-4": { 52 | "Ergebnis\\PHPUnit\\SlowTestDetector\\Test\\": "test/" 53 | } 54 | }, 55 | "config": { 56 | "allow-plugins": { 57 | "ergebnis/composer-normalize": true, 58 | "phpstan/extension-installer": true 59 | }, 60 | "audit": { 61 | "abandoned": "report" 62 | }, 63 | "platform": { 64 | "php": "7.4.33" 65 | }, 66 | "preferred-install": "dist", 67 | "sort-packages": true 68 | }, 69 | "extra": { 70 | "branch-alias": { 71 | "dev-main": "2.16-dev" 72 | }, 73 | "composer-normalize": { 74 | "indent-size": 2, 75 | "indent-style": "space" 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /manifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Attribute/MaximumDuration.php: -------------------------------------------------------------------------------- 1 | = $milliseconds) { 32 | throw Exception\InvalidMilliseconds::notGreaterThanZero($milliseconds); 33 | } 34 | 35 | $this->milliseconds = $milliseconds; 36 | } 37 | 38 | public function milliseconds(): int 39 | { 40 | return $this->milliseconds; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Collector/Collector.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | private $slowTests = []; 28 | 29 | public function collectSlowTest(SlowTest $slowTest) 30 | { 31 | $key = $slowTest->testIdentifier()->toString(); 32 | 33 | if (\array_key_exists($key, $this->slowTests)) { 34 | $previousSlowTest = $this->slowTests[$key]; 35 | 36 | if (!$slowTest->duration()->isGreaterThan($previousSlowTest->duration())) { 37 | return; 38 | } 39 | 40 | $this->slowTests[$key] = $slowTest; 41 | 42 | return; 43 | } 44 | 45 | $this->slowTests[$key] = $slowTest; 46 | } 47 | 48 | public function slowTestList(): SlowTestList 49 | { 50 | return SlowTestList::create(...\array_values($this->slowTests)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Comparator/DurationComparator.php: -------------------------------------------------------------------------------- 1 | isLessThan($two)) { 28 | return -1; 29 | } 30 | 31 | if ($one->isGreaterThan($two)) { 32 | return 1; 33 | } 34 | 35 | return 0; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Console/Color.php: -------------------------------------------------------------------------------- 1 | value = $value; 29 | } 30 | 31 | /** 32 | * @throws Exception\InvalidCount 33 | */ 34 | public static function fromInt(int $value): self 35 | { 36 | if (0 > $value) { 37 | throw Exception\InvalidCount::notGreaterThanOrEqualToZero($value); 38 | } 39 | 40 | return new self($value); 41 | } 42 | 43 | public function equals(self $other): bool 44 | { 45 | return $this->value === $other->value; 46 | } 47 | 48 | public function toInt(): int 49 | { 50 | return $this->value; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Duration.php: -------------------------------------------------------------------------------- 1 | seconds = $seconds; 36 | $this->nanoseconds = $nanoseconds; 37 | } 38 | 39 | /** 40 | * @throws Exception\InvalidNanoseconds 41 | * @throws Exception\InvalidSeconds 42 | */ 43 | public static function fromSecondsAndNanoseconds( 44 | int $seconds, 45 | int $nanoseconds 46 | ): self { 47 | if (0 > $seconds) { 48 | throw Exception\InvalidSeconds::notGreaterThanOrEqualToZero($seconds); 49 | } 50 | 51 | if (0 > $nanoseconds) { 52 | throw Exception\InvalidNanoseconds::notGreaterThanOrEqualToZero($nanoseconds); 53 | } 54 | 55 | $maxNanoseconds = 999999999; 56 | 57 | if ($maxNanoseconds < $nanoseconds) { 58 | throw Exception\InvalidNanoseconds::notLessThanOrEqualTo( 59 | $nanoseconds, 60 | $maxNanoseconds 61 | ); 62 | } 63 | 64 | return new self( 65 | $seconds, 66 | $nanoseconds 67 | ); 68 | } 69 | 70 | /** 71 | * @throws Exception\InvalidMilliseconds 72 | */ 73 | public static function fromMilliseconds(int $milliseconds): self 74 | { 75 | if (0 > $milliseconds) { 76 | throw Exception\InvalidMilliseconds::notGreaterThanZero($milliseconds); 77 | } 78 | 79 | $seconds = \intdiv( 80 | $milliseconds, 81 | 1000 82 | ); 83 | 84 | $nanoseconds = ($milliseconds - $seconds * 1000) * 1000000; 85 | 86 | return new self( 87 | $seconds, 88 | $nanoseconds 89 | ); 90 | } 91 | 92 | public function seconds(): int 93 | { 94 | return $this->seconds; 95 | } 96 | 97 | public function nanoseconds(): int 98 | { 99 | return $this->nanoseconds; 100 | } 101 | 102 | public function add(self $other): self 103 | { 104 | $seconds = $this->seconds + $other->seconds; 105 | $nanoseconds = $this->nanoseconds + $other->nanoseconds; 106 | 107 | if (999999999 < $nanoseconds) { 108 | return new self( 109 | $seconds + 1, 110 | $nanoseconds - 1000000000 111 | ); 112 | } 113 | 114 | return new self( 115 | $seconds, 116 | $nanoseconds 117 | ); 118 | } 119 | 120 | public function isLessThan(self $other): bool 121 | { 122 | if ($this->seconds < $other->seconds) { 123 | return true; 124 | } 125 | 126 | if ($this->seconds === $other->seconds) { 127 | return $this->nanoseconds < $other->nanoseconds; 128 | } 129 | 130 | return false; 131 | } 132 | 133 | public function isGreaterThan(self $other): bool 134 | { 135 | if ($this->seconds > $other->seconds) { 136 | return true; 137 | } 138 | 139 | if ($this->seconds === $other->seconds) { 140 | return $this->nanoseconds > $other->nanoseconds; 141 | } 142 | 143 | return false; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Exception/InvalidCount.php: -------------------------------------------------------------------------------- 1 | toString() 28 | )); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Exception/SlowTestListIsEmpty.php: -------------------------------------------------------------------------------- 1 | major()->equals(Version\Major::fromInt(6))) { 32 | final class Extension implements Framework\TestListener 33 | { 34 | /** 35 | * @var int 36 | */ 37 | private $suites = 0; 38 | 39 | /** 40 | * @var MaximumDuration 41 | */ 42 | private $maximumDuration; 43 | 44 | /** 45 | * @var Collector\Collector 46 | */ 47 | private $collector; 48 | 49 | /** 50 | * @var Reporter\Reporter 51 | */ 52 | private $reporter; 53 | 54 | public function __construct(array $options = []) 55 | { 56 | $maximumCount = MaximumCount::default(); 57 | 58 | if (\array_key_exists('maximum-count', $options)) { 59 | $maximumCount = MaximumCount::fromCount(Count::fromInt((int) $options['maximum-count'])); 60 | } 61 | 62 | $maximumDuration = MaximumDuration::default(); 63 | 64 | if (\array_key_exists('maximum-duration', $options)) { 65 | $maximumDuration = MaximumDuration::fromDuration(Duration::fromMilliseconds((int) $options['maximum-duration'])); 66 | } 67 | 68 | $this->maximumDuration = $maximumDuration; 69 | $this->collector = new Collector\DefaultCollector(); 70 | $this->reporter = new Reporter\DefaultReporter( 71 | new Formatter\DefaultDurationFormatter(), 72 | $maximumCount 73 | ); 74 | } 75 | 76 | public function addError( 77 | Framework\Test $test, 78 | \Exception $e, 79 | $time 80 | ) { 81 | } 82 | 83 | public function addWarning( 84 | Framework\Test $test, 85 | Framework\Warning $e, 86 | $time 87 | ) { 88 | } 89 | 90 | public function addFailure( 91 | Framework\Test $test, 92 | Framework\AssertionFailedError $e, 93 | $time 94 | ) { 95 | } 96 | 97 | public function addIncompleteTest( 98 | Framework\Test $test, 99 | \Exception $e, 100 | $time 101 | ) { 102 | } 103 | 104 | public function addRiskyTest( 105 | Framework\Test $test, 106 | \Exception $e, 107 | $time 108 | ) { 109 | } 110 | 111 | public function addSkippedTest( 112 | Framework\Test $test, 113 | \Exception $e, 114 | $time 115 | ) { 116 | } 117 | 118 | public function startTestSuite(Framework\TestSuite $suite) 119 | { 120 | ++$this->suites; 121 | } 122 | 123 | public function endTestSuite(Framework\TestSuite $suite) 124 | { 125 | --$this->suites; 126 | 127 | if (0 < $this->suites) { 128 | return; 129 | } 130 | 131 | $slowTestList = $this->collector->slowTestList(); 132 | 133 | if ($slowTestList->isEmpty()) { 134 | return; 135 | } 136 | 137 | $report = $this->reporter->report($slowTestList); 138 | 139 | if ('' === $report) { 140 | return; 141 | } 142 | 143 | echo <<resolveMaximumDuration($test); 167 | 168 | if (!$duration->isGreaterThan($maximumDuration->toDuration())) { 169 | return; 170 | } 171 | 172 | $slowTest = SlowTest::create( 173 | TestIdentifier::fromString(\sprintf( 174 | '%s::%s', 175 | \get_class($test), 176 | $test->getName() 177 | )), 178 | TestDescription::fromString(\sprintf( 179 | '%s::%s', 180 | \get_class($test), 181 | $test->getName() 182 | )), 183 | $duration, 184 | $maximumDuration 185 | ); 186 | 187 | $this->collector->collectSlowTest($slowTest); 188 | } 189 | 190 | private function resolveMaximumDuration(Framework\Test $test): MaximumDuration 191 | { 192 | $annotations = [ 193 | 'maximumDuration', 194 | 'slowThreshold', 195 | ]; 196 | 197 | $symbolAnnotations = Util\Test::parseTestMethodAnnotations( 198 | \get_class($test), 199 | $test->getName(false) 200 | ); 201 | 202 | foreach ($annotations as $annotation) { 203 | if (!\is_array($symbolAnnotations['method'])) { 204 | continue; 205 | } 206 | 207 | if (!\array_key_exists($annotation, $symbolAnnotations['method'])) { 208 | continue; 209 | } 210 | 211 | if (!\is_array($symbolAnnotations['method'][$annotation])) { 212 | continue; 213 | } 214 | 215 | $maximumDuration = \reset($symbolAnnotations['method'][$annotation]); 216 | 217 | if (1 !== \preg_match('/^\d+$/', $maximumDuration)) { 218 | continue; 219 | } 220 | 221 | return MaximumDuration::fromDuration(Duration::fromMilliseconds((int) $maximumDuration)); 222 | } 223 | 224 | return $this->maximumDuration; 225 | } 226 | } 227 | 228 | return; 229 | } 230 | 231 | if ($phpUnitVersionSeries->major()->isOneOf(Version\Major::fromInt(7), Version\Major::fromInt(8), Version\Major::fromInt(9))) { 232 | /** 233 | * @internal 234 | */ 235 | final class Extension implements 236 | Runner\AfterLastTestHook, 237 | Runner\AfterSuccessfulTestHook, 238 | Runner\AfterTestHook, 239 | Runner\BeforeFirstTestHook 240 | { 241 | /** 242 | * @var int 243 | */ 244 | private $suites = 0; 245 | 246 | /** 247 | * @var Duration 248 | */ 249 | private $maximumDuration; 250 | 251 | /** 252 | * @var Collector\Collector 253 | */ 254 | private $collector; 255 | 256 | /** 257 | * @var Reporter\Reporter 258 | */ 259 | private $reporter; 260 | 261 | public function __construct(array $options = []) 262 | { 263 | $maximumCount = MaximumCount::default(); 264 | 265 | if (\array_key_exists('maximum-count', $options)) { 266 | $maximumCount = MaximumCount::fromCount(Count::fromInt((int) $options['maximum-count'])); 267 | } 268 | 269 | $maximumDuration = MaximumDuration::default(); 270 | 271 | if (\array_key_exists('maximum-duration', $options)) { 272 | $maximumDuration = MaximumDuration::fromDuration(Duration::fromMilliseconds((int) $options['maximum-duration'])); 273 | } 274 | 275 | $this->maximumDuration = $maximumDuration; 276 | $this->collector = new Collector\DefaultCollector(); 277 | $this->reporter = new Reporter\DefaultReporter( 278 | new Formatter\DefaultDurationFormatter(), 279 | $maximumCount 280 | ); 281 | } 282 | 283 | public function executeBeforeFirstTest(): void 284 | { 285 | ++$this->suites; 286 | } 287 | 288 | /** 289 | * @see https://github.com/sebastianbergmann/phpunit/pull/3392#issuecomment-1868311482 290 | * @see https://github.com/sebastianbergmann/phpunit/blob/7.5.0/src/TextUI/TestRunner.php#L227-L239 291 | * @see https://github.com/sebastianbergmann/phpunit/pull/3762 292 | */ 293 | public function executeAfterSuccessfulTest( 294 | string $test, 295 | float $time 296 | ): void { 297 | // intentionally left blank 298 | } 299 | 300 | public function executeAfterTest( 301 | string $test, 302 | float $time 303 | ): void { 304 | $seconds = (int) \floor($time); 305 | $nanoseconds = (int) (($time - $seconds) * 1000000000); 306 | 307 | $duration = Duration::fromSecondsAndNanoseconds( 308 | $seconds, 309 | $nanoseconds 310 | ); 311 | 312 | $maximumDuration = $this->resolveMaximumDuration($test); 313 | 314 | if (!$duration->isGreaterThan($maximumDuration->toDuration())) { 315 | return; 316 | } 317 | 318 | $slowTest = SlowTest::create( 319 | TestIdentifier::fromString($test), 320 | TestDescription::fromString($test), 321 | $duration, 322 | $maximumDuration 323 | ); 324 | 325 | $this->collector->collectSlowTest($slowTest); 326 | } 327 | 328 | public function executeAfterLastTest(): void 329 | { 330 | --$this->suites; 331 | 332 | if (0 < $this->suites) { 333 | return; 334 | } 335 | 336 | $slowTestList = $this->collector->slowTestList(); 337 | 338 | if ($slowTestList->isEmpty()) { 339 | return; 340 | } 341 | 342 | $report = $this->reporter->report($slowTestList); 343 | 344 | if ('' === $report) { 345 | return; 346 | } 347 | 348 | echo <<maximumDuration; 412 | } 413 | } 414 | 415 | return; 416 | } 417 | 418 | if ($phpUnitVersionSeries->major()->isOneOf(Version\Major::fromInt(10), Version\Major::fromInt(11), Version\Major::fromInt(12))) { 419 | /** 420 | * @internal 421 | */ 422 | final class Extension implements Runner\Extension\Extension 423 | { 424 | public function bootstrap( 425 | TextUI\Configuration\Configuration $configuration, 426 | Runner\Extension\Facade $facade, 427 | Runner\Extension\ParameterCollection $parameters 428 | ): void { 429 | if ($configuration->noOutput()) { 430 | return; 431 | } 432 | 433 | $maximumCount = MaximumCount::default(); 434 | 435 | if ($parameters->has('maximum-count')) { 436 | $maximumCount = MaximumCount::fromCount(Count::fromInt((int) $parameters->get('maximum-count'))); 437 | } 438 | 439 | $maximumDuration = MaximumDuration::default(); 440 | 441 | if ($parameters->has('maximum-duration')) { 442 | $maximumDuration = MaximumDuration::fromDuration(Duration::fromMilliseconds((int) $parameters->get('maximum-duration'))); 443 | } 444 | 445 | $timeKeeper = new TimeKeeper(); 446 | $collector = new Collector\DefaultCollector(); 447 | $reporter = new Reporter\DefaultReporter( 448 | new Formatter\DefaultDurationFormatter(), 449 | $maximumCount 450 | ); 451 | 452 | $facade->registerSubscribers( 453 | new Subscriber\Test\PreparationStartedSubscriber($timeKeeper), 454 | new Subscriber\Test\FinishedSubscriber( 455 | $maximumDuration, 456 | $timeKeeper, 457 | $collector, 458 | Version\Series::fromString(Runner\Version::series()) 459 | ), 460 | new Subscriber\TestRunner\ExecutionFinishedSubscriber( 461 | $collector, 462 | $reporter 463 | ) 464 | ); 465 | } 466 | } 467 | 468 | return; 469 | } 470 | 471 | throw new \RuntimeException(\sprintf( 472 | 'Unable to select extension for PHPUnit version with version series "%s".', 473 | Runner\Version::series() 474 | )); 475 | -------------------------------------------------------------------------------- /src/Formatter/DefaultDurationFormatter.php: -------------------------------------------------------------------------------- 1 | seconds() * 1000 + $duration->nanoseconds() / 1000000; 31 | 32 | $hours = (int) \floor($durationInMilliseconds / 60 / 60 / 1000); 33 | $hoursInMilliseconds = $hours * 60 * 60 * 1000; 34 | 35 | $minutes = ((int) \floor($durationInMilliseconds / 60 / 1000)) % 60; 36 | $minutesInMilliseconds = $minutes * 60 * 1000; 37 | 38 | $seconds = (int) \floor(($durationInMilliseconds - $hoursInMilliseconds - $minutesInMilliseconds) / 1000); 39 | $secondsInMilliseconds = $seconds * 1000; 40 | 41 | $milliseconds = (int) ($durationInMilliseconds - $hoursInMilliseconds - $minutesInMilliseconds - $secondsInMilliseconds); 42 | 43 | if (0 < $hours) { 44 | return \sprintf( 45 | '%02d:%02d:%02d.%03d', 46 | $hours, 47 | $minutes, 48 | $seconds, 49 | $milliseconds 50 | ); 51 | } 52 | 53 | return \sprintf( 54 | '%02d:%02d.%03d', 55 | $minutes, 56 | $seconds, 57 | $milliseconds 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Formatter/DurationFormatter.php: -------------------------------------------------------------------------------- 1 | count = $count; 26 | } 27 | 28 | /** 29 | * @throws Exception\InvalidMaximumCount 30 | */ 31 | public static function fromCount(Count $count): self 32 | { 33 | if ($count->toInt() <= 0) { 34 | throw Exception\InvalidMaximumCount::notGreaterThanZero($count->toInt()); 35 | } 36 | 37 | return new self($count); 38 | } 39 | 40 | public static function default(): self 41 | { 42 | return new self(Count::fromInt(10)); 43 | } 44 | 45 | public function toCount(): Count 46 | { 47 | return $this->count; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/MaximumDuration.php: -------------------------------------------------------------------------------- 1 | duration = $duration; 26 | } 27 | 28 | public static function fromDuration(Duration $duration): self 29 | { 30 | return new self($duration); 31 | } 32 | 33 | public static function default(): self 34 | { 35 | return new self(Duration::fromMilliseconds(500)); 36 | } 37 | 38 | public function toDuration(): Duration 39 | { 40 | return $this->duration; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Phase.php: -------------------------------------------------------------------------------- 1 | phaseIdentifier = $phaseIdentifier; 48 | $this->startTime = $startTime; 49 | $this->stopTime = $stopTime; 50 | $this->duration = $duration; 51 | } 52 | 53 | public static function create( 54 | PhaseIdentifier $phaseIdentifier, 55 | Time $startTime, 56 | Time $stopTime 57 | ): self { 58 | return new self( 59 | $phaseIdentifier, 60 | $startTime, 61 | $stopTime, 62 | $stopTime->duration($startTime) 63 | ); 64 | } 65 | 66 | public function phaseIdentifier(): PhaseIdentifier 67 | { 68 | return $this->phaseIdentifier; 69 | } 70 | 71 | public function startTime(): Time 72 | { 73 | return $this->startTime; 74 | } 75 | 76 | public function stopTime(): Time 77 | { 78 | return $this->stopTime; 79 | } 80 | 81 | public function duration(): Duration 82 | { 83 | return $this->duration; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/PhaseIdentifier.php: -------------------------------------------------------------------------------- 1 | value = $value; 29 | } 30 | 31 | /** 32 | * @throws Exception\InvalidPhaseIdentifier 33 | */ 34 | public static function fromString(string $value): self 35 | { 36 | if ('' === \trim($value)) { 37 | throw Exception\InvalidPhaseIdentifier::blankOrEmpty(); 38 | } 39 | 40 | return new self($value); 41 | } 42 | 43 | public function toString(): string 44 | { 45 | return $this->value; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/PhaseStart.php: -------------------------------------------------------------------------------- 1 | phaseIdentifier = $phaseIdentifier; 36 | $this->startTime = $startTime; 37 | } 38 | 39 | public static function create( 40 | PhaseIdentifier $phaseIdentifier, 41 | Time $startTime 42 | ): self { 43 | return new self( 44 | $phaseIdentifier, 45 | $startTime 46 | ); 47 | } 48 | 49 | public function phaseIdentifier(): PhaseIdentifier 50 | { 51 | return $this->phaseIdentifier; 52 | } 53 | 54 | public function startTime(): Time 55 | { 56 | return $this->startTime; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Reporter/DefaultReporter.php: -------------------------------------------------------------------------------- 1 | durationFormatter = $durationFormatter; 41 | $this->maximumCount = $maximumCount; 42 | } 43 | 44 | public function report(SlowTestList $slowTestList): string 45 | { 46 | if ($slowTestList->isEmpty()) { 47 | return ''; 48 | } 49 | 50 | return \implode("\n", \iterator_to_array($this->lines($slowTestList))); 51 | } 52 | 53 | /** 54 | * @return \Generator 55 | */ 56 | private function lines(SlowTestList $slowTestList): \Generator 57 | { 58 | $slowTestCount = $slowTestList->count(); 59 | 60 | if ($slowTestCount->equals(Count::fromInt(1))) { 61 | yield 'Detected 1 test where the duration exceeded the maximum duration.'; 62 | } else { 63 | yield \sprintf( 64 | 'Detected %d tests where the duration exceeded the maximum duration.', 65 | $slowTestCount->toInt() 66 | ); 67 | } 68 | 69 | yield ''; 70 | 71 | $slowTestListThatWillBeReported = $slowTestList 72 | ->sortByDurationDescending() 73 | ->limitTo($this->maximumCount); 74 | 75 | $slowTestWithLongestDuration = $slowTestListThatWillBeReported->first(); 76 | 77 | $slowTestWithLongestMaximumDuration = $slowTestListThatWillBeReported->sortByMaximumDurationDescending()->first(); 78 | 79 | $numberWidth = \strlen((string) $slowTestListThatWillBeReported->count()->toInt()); 80 | $durationWidth = \strlen($this->durationFormatter->format($slowTestWithLongestDuration->duration())); 81 | $maximumDurationWidth = \strlen($this->durationFormatter->format($slowTestWithLongestMaximumDuration->maximumDuration()->toDuration())); 82 | 83 | $template = \sprintf( 84 | '%%%dd. %%%ds (%%%ds) %%s', 85 | $numberWidth, 86 | $durationWidth, 87 | $maximumDurationWidth 88 | ); 89 | 90 | $number = 1; 91 | 92 | foreach ($slowTestListThatWillBeReported->toArray() as $slowTest) { 93 | yield \sprintf( 94 | $template, 95 | (string) $number, 96 | $this->durationFormatter->format($slowTest->duration()), 97 | $this->durationFormatter->format($slowTest->maximumDuration()->toDuration()), 98 | $slowTest->testDescription()->toString() 99 | ); 100 | 101 | ++$number; 102 | } 103 | 104 | $additionalSlowTestCount = Count::fromInt(\max( 105 | 0, 106 | $slowTestList->count()->toInt() - $this->maximumCount->toCount()->toInt() 107 | )); 108 | 109 | if ($additionalSlowTestCount->equals(Count::fromInt(0))) { 110 | return; 111 | } 112 | 113 | yield ''; 114 | 115 | if ($additionalSlowTestCount->equals(Count::fromInt(1))) { 116 | yield 'There is 1 additional slow test that is not listed here.'; 117 | } else { 118 | yield \sprintf( 119 | 'There are %d additional slow tests that are not listed here.', 120 | $additionalSlowTestCount->toInt() 121 | ); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Reporter/Reporter.php: -------------------------------------------------------------------------------- 1 | testIdentifier = $testIdentifier; 48 | $this->testDescription = $testDescription; 49 | $this->duration = $duration; 50 | $this->maximumDuration = $maximumDuration; 51 | } 52 | 53 | public static function create( 54 | TestIdentifier $testIdentifier, 55 | TestDescription $testDescription, 56 | Duration $duration, 57 | MaximumDuration $maximumDuration 58 | ): self { 59 | return new self( 60 | $testIdentifier, 61 | $testDescription, 62 | $duration, 63 | $maximumDuration 64 | ); 65 | } 66 | 67 | public function testIdentifier(): TestIdentifier 68 | { 69 | return $this->testIdentifier; 70 | } 71 | 72 | public function testDescription(): TestDescription 73 | { 74 | return $this->testDescription; 75 | } 76 | 77 | public function duration(): Duration 78 | { 79 | return $this->duration; 80 | } 81 | 82 | public function maximumDuration(): MaximumDuration 83 | { 84 | return $this->maximumDuration; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/SlowTestList.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | private $slowTests; 27 | 28 | private function __construct(SlowTest ...$slowTests) 29 | { 30 | $this->slowTests = $slowTests; 31 | } 32 | 33 | public static function create(SlowTest ...$slowTests): self 34 | { 35 | return new self(...$slowTests); 36 | } 37 | 38 | public function count(): Count 39 | { 40 | return Count::fromInt(\count($this->slowTests)); 41 | } 42 | 43 | /** 44 | * @throws Exception\SlowTestListIsEmpty 45 | */ 46 | public function first(): SlowTest 47 | { 48 | if ([] === $this->slowTests) { 49 | throw Exception\SlowTestListIsEmpty::create(); 50 | } 51 | 52 | return \reset($this->slowTests); 53 | } 54 | 55 | public function isEmpty(): bool 56 | { 57 | return [] === $this->slowTests; 58 | } 59 | 60 | public function limitTo(MaximumCount $maximumCount): self 61 | { 62 | return self::create(...\array_slice( 63 | $this->slowTests, 64 | 0, 65 | $maximumCount->toCount()->toInt() 66 | )); 67 | } 68 | 69 | public function sortByDurationDescending(): self 70 | { 71 | $durationComparator = new DurationComparator(); 72 | 73 | $slowTests = $this->slowTests; 74 | 75 | \usort($slowTests, static function (SlowTest $one, SlowTest $two) use ($durationComparator): int { 76 | return $durationComparator->compare( 77 | $two->duration(), 78 | $one->duration() 79 | ); 80 | }); 81 | 82 | return self::create(...$slowTests); 83 | } 84 | 85 | public function sortByMaximumDurationDescending(): self 86 | { 87 | $durationComparator = new DurationComparator(); 88 | 89 | $slowTests = $this->slowTests; 90 | 91 | \usort($slowTests, static function (SlowTest $one, SlowTest $two) use ($durationComparator): int { 92 | return $durationComparator->compare( 93 | $two->maximumDuration()->toDuration(), 94 | $one->maximumDuration()->toDuration() 95 | ); 96 | }); 97 | 98 | return self::create(...$slowTests); 99 | } 100 | 101 | /** 102 | * @return list 103 | */ 104 | public function toArray(): array 105 | { 106 | return $this->slowTests; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Subscriber/Test/FinishedSubscriber.php: -------------------------------------------------------------------------------- 1 | maximumDuration = $maximumDuration; 63 | $this->timeKeeper = $timeKeeper; 64 | $this->collector = $collector; 65 | $this->versionSeries = $versionSeries; 66 | } 67 | 68 | /** 69 | * @see https://github.com/sebastianbergmann/phpunit/blob/10.0.0/src/Framework/TestRunner.php#L198 70 | * @see https://github.com/sebastianbergmann/phpunit/blob/10.0.0/src/Framework/TestRunner.php#L238 71 | */ 72 | public function notify(Event\Test\Finished $event): void 73 | { 74 | $phaseIdentifier = PhaseIdentifier::fromString($event->test()->id()); 75 | 76 | $time = $event->telemetryInfo()->time(); 77 | 78 | $phase = $this->timeKeeper->stop( 79 | $phaseIdentifier, 80 | Time::fromSecondsAndNanoseconds( 81 | $time->seconds(), 82 | $time->nanoseconds() 83 | ) 84 | ); 85 | 86 | $duration = $phase->duration(); 87 | 88 | $maximumDuration = $this->resolveMaximumDuration($event->test()); 89 | 90 | if (!$duration->isGreaterThan($maximumDuration->toDuration())) { 91 | return; 92 | } 93 | 94 | $slowTest = SlowTest::create( 95 | TestIdentifier::fromString($event->test()->id()), 96 | self::descriptionFromTest($event->test()), 97 | $duration, 98 | $maximumDuration 99 | ); 100 | 101 | $this->collector->collectSlowTest($slowTest); 102 | } 103 | 104 | /** 105 | * @see https://github.com/sebastianbergmann/phpunit/blob/11.1.3/src/TextUI/Output/Default/ResultPrinter.php#L511-L521 106 | */ 107 | private static function descriptionFromTest(Event\Code\Test $test): TestDescription 108 | { 109 | if (!$test->isTestMethod()) { 110 | return TestDescription::fromString($test->name()); 111 | } 112 | 113 | /** @var Event\Code\TestMethod $test */ 114 | if (!$test->testData()->hasDataFromDataProvider()) { 115 | return TestDescription::fromString($test->nameWithClass()); 116 | } 117 | 118 | $dataProvider = $test->testData()->dataFromDataProvider(); 119 | 120 | /** 121 | * @see https://github.com/sebastianbergmann/phpunit/commit/5d049893b8 122 | */ 123 | if (!\method_exists($dataProvider, 'dataAsStringForResultOutput')) { 124 | $dataAsStringForResultOutput = null; 125 | 126 | foreach (\debug_backtrace() as $frame) { 127 | if (!isset($frame['object'])) { 128 | continue; 129 | } 130 | 131 | $object = $frame['object']; 132 | 133 | if (!$object instanceof Framework\TestCase) { 134 | continue; 135 | } 136 | 137 | $dataAsStringForResultOutput = $object->dataSetAsStringWithData(); 138 | } 139 | 140 | return TestDescription::fromString(\sprintf( 141 | '%s::%s%s', 142 | $test->className(), 143 | $test->methodName(), 144 | $dataAsStringForResultOutput 145 | )); 146 | } 147 | 148 | return TestDescription::fromString(\sprintf( 149 | '%s::%s%s', 150 | $test->className(), 151 | $test->methodName(), 152 | $test->testData()->dataFromDataProvider()->dataAsStringForResultOutput() 153 | )); 154 | } 155 | 156 | private function resolveMaximumDuration(Event\Code\Test $test): MaximumDuration 157 | { 158 | $maximumDurationFromAttribute = self::resolveMaximumDurationFromAttribute($test); 159 | 160 | if ($maximumDurationFromAttribute instanceof MaximumDuration) { 161 | return $maximumDurationFromAttribute; 162 | } 163 | 164 | if ($this->versionSeries->major()->isLessThan(Version\Major::fromInt(12))) { 165 | $maximumDurationFromAnnotation = self::resolveMaximumDurationFromAnnotation($test); 166 | 167 | if ($maximumDurationFromAnnotation instanceof MaximumDuration) { 168 | return $maximumDurationFromAnnotation; 169 | } 170 | } 171 | 172 | return $this->maximumDuration; 173 | } 174 | 175 | private static function resolveMaximumDurationFromAttribute(Event\Code\Test $test): ?MaximumDuration 176 | { 177 | /** @var Event\Code\TestMethod $test */ 178 | $methodReflection = new \ReflectionMethod( 179 | $test->className(), 180 | $test->methodName() 181 | ); 182 | 183 | $attributeReflections = $methodReflection->getAttributes(Attribute\MaximumDuration::class); 184 | 185 | if ([] !== $attributeReflections) { 186 | $attributeReflection = \reset($attributeReflections); 187 | 188 | $attribute = $attributeReflection->newInstance(); 189 | 190 | return MaximumDuration::fromDuration(Duration::fromMilliseconds($attribute->milliseconds())); 191 | } 192 | 193 | return null; 194 | } 195 | 196 | private static function resolveMaximumDurationFromAnnotation(Event\Code\Test $test): ?MaximumDuration 197 | { 198 | $annotations = [ 199 | 'maximumDuration', 200 | 'slowThreshold', 201 | ]; 202 | 203 | /** @var Event\Code\TestMethod $test */ 204 | $docBlock = Metadata\Annotation\Parser\Registry::getInstance()->forMethod( 205 | $test->className(), 206 | $test->methodName() 207 | ); 208 | 209 | $symbolAnnotations = $docBlock->symbolAnnotations(); 210 | 211 | foreach ($annotations as $annotation) { 212 | if (!\array_key_exists($annotation, $symbolAnnotations)) { 213 | continue; 214 | } 215 | 216 | if (!\is_array($symbolAnnotations[$annotation])) { 217 | continue; 218 | } 219 | 220 | $maximumDuration = \reset($symbolAnnotations[$annotation]); 221 | 222 | if (1 !== \preg_match('/^\d+$/', $maximumDuration)) { 223 | continue; 224 | } 225 | 226 | return MaximumDuration::fromDuration(Duration::fromMilliseconds((int) $maximumDuration)); 227 | } 228 | 229 | return null; 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/Subscriber/Test/PreparationStartedSubscriber.php: -------------------------------------------------------------------------------- 1 | timeKeeper = $timeKeeper; 34 | } 35 | 36 | /** 37 | * @see https://github.com/sebastianbergmann/phpunit/blob/10.0.0/src/Framework/TestCase.php#L585-L587 38 | */ 39 | public function notify(Event\Test\PreparationStarted $event): void 40 | { 41 | $time = $event->telemetryInfo()->time(); 42 | 43 | $this->timeKeeper->start( 44 | PhaseIdentifier::fromString($event->test()->id()), 45 | Time::fromSecondsAndNanoseconds( 46 | $time->seconds(), 47 | $time->nanoseconds() 48 | ) 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Subscriber/TestRunner/ExecutionFinishedSubscriber.php: -------------------------------------------------------------------------------- 1 | collector = $collector; 40 | $this->reporter = $reporter; 41 | } 42 | 43 | /** 44 | * @see https://github.com/sebastianbergmann/phpunit/blob/10.0.0/src/TextUI/TestRunner.php#L65 45 | */ 46 | public function notify(Event\TestRunner\ExecutionFinished $event): void 47 | { 48 | $slowTestList = $this->collector->slowTestList(); 49 | 50 | if ($slowTestList->isEmpty()) { 51 | return; 52 | } 53 | 54 | $report = $this->reporter->report($slowTestList); 55 | 56 | if ('' === $report) { 57 | return; 58 | } 59 | 60 | echo <<value = $value; 29 | } 30 | 31 | /** 32 | * @throws Exception\InvalidTestDescription 33 | */ 34 | public static function fromString(string $value): self 35 | { 36 | if ('' === \trim($value)) { 37 | throw Exception\InvalidTestDescription::blankOrEmpty(); 38 | } 39 | 40 | return new self($value); 41 | } 42 | 43 | public function toString(): string 44 | { 45 | return $this->value; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/TestIdentifier.php: -------------------------------------------------------------------------------- 1 | value = $value; 29 | } 30 | 31 | /** 32 | * @throws Exception\InvalidTestIdentifier 33 | */ 34 | public static function fromString(string $value): self 35 | { 36 | if ('' === \trim($value)) { 37 | throw Exception\InvalidTestIdentifier::blankOrEmpty(); 38 | } 39 | 40 | return new self($value); 41 | } 42 | 43 | public function toString(): string 44 | { 45 | return $this->value; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Time.php: -------------------------------------------------------------------------------- 1 | seconds = $seconds; 36 | $this->nanoseconds = $nanoseconds; 37 | } 38 | 39 | /** 40 | * @throws Exception\InvalidNanoseconds 41 | * @throws Exception\InvalidSeconds 42 | */ 43 | public static function fromSecondsAndNanoseconds( 44 | int $seconds, 45 | int $nanoseconds 46 | ): self { 47 | if (0 > $seconds) { 48 | throw Exception\InvalidSeconds::notGreaterThanOrEqualToZero($seconds); 49 | } 50 | 51 | if (0 > $nanoseconds) { 52 | throw Exception\InvalidNanoseconds::notGreaterThanOrEqualToZero($nanoseconds); 53 | } 54 | 55 | $maxNanoseconds = 999999999; 56 | 57 | if ($maxNanoseconds < $nanoseconds) { 58 | throw Exception\InvalidNanoseconds::notLessThanOrEqualTo( 59 | $nanoseconds, 60 | $maxNanoseconds 61 | ); 62 | } 63 | 64 | return new self( 65 | $seconds, 66 | $nanoseconds 67 | ); 68 | } 69 | 70 | public function seconds(): int 71 | { 72 | return $this->seconds; 73 | } 74 | 75 | public function nanoseconds(): int 76 | { 77 | return $this->nanoseconds; 78 | } 79 | 80 | /** 81 | * @throws Exception\InvalidStart 82 | */ 83 | public function duration(self $start): Duration 84 | { 85 | $seconds = $this->seconds - $start->seconds; 86 | $nanoseconds = $this->nanoseconds - $start->nanoseconds; 87 | 88 | if (0 > $nanoseconds) { 89 | --$seconds; 90 | 91 | $nanoseconds += 1000000000; 92 | } 93 | 94 | if (0 > $seconds) { 95 | throw Exception\InvalidStart::notLessThanOrEqualToEnd(); 96 | } 97 | 98 | return Duration::fromSecondsAndNanoseconds( 99 | $seconds, 100 | $nanoseconds 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/TimeKeeper.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | private $phaseStarts = []; 25 | 26 | public function start( 27 | PhaseIdentifier $phaseIdentifier, 28 | Time $startTime 29 | ): void { 30 | $key = $phaseIdentifier->toString(); 31 | 32 | $this->phaseStarts[$key] = PhaseStart::create( 33 | $phaseIdentifier, 34 | $startTime 35 | ); 36 | } 37 | 38 | /** 39 | * @throws Exception\PhaseNotStarted 40 | */ 41 | public function stop( 42 | PhaseIdentifier $phaseIdentifier, 43 | Time $stopTime 44 | ): Phase { 45 | $key = $phaseIdentifier->toString(); 46 | 47 | if (!\array_key_exists($key, $this->phaseStarts)) { 48 | throw Exception\PhaseNotStarted::fromPhaseIdentifier($phaseIdentifier); 49 | } 50 | 51 | $phaseStart = $this->phaseStarts[$key]; 52 | 53 | unset($this->phaseStarts[$key]); 54 | 55 | return Phase::create( 56 | $phaseIdentifier, 57 | $phaseStart->startTime(), 58 | $stopTime 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Version/Major.php: -------------------------------------------------------------------------------- 1 | value = $value; 29 | } 30 | 31 | /** 32 | * @throws \InvalidArgumentException 33 | */ 34 | public static function fromInt(int $value): self 35 | { 36 | if (0 > $value) { 37 | throw new \InvalidArgumentException(\sprintf( 38 | 'Value "%d" does not appear to be a valid value for a major version.', 39 | $value 40 | )); 41 | } 42 | 43 | return new self($value); 44 | } 45 | 46 | public function toInt(): int 47 | { 48 | return $this->value; 49 | } 50 | 51 | public function equals(self $other): bool 52 | { 53 | return $this->value === $other->value; 54 | } 55 | 56 | public function isLessThan(self $other): bool 57 | { 58 | return $this->value < $other->value; 59 | } 60 | 61 | public function isOneOf(self ...$others): bool 62 | { 63 | foreach ($others as $other) { 64 | if ($this->value === $other->value) { 65 | return true; 66 | } 67 | } 68 | 69 | return false; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Version/Series.php: -------------------------------------------------------------------------------- 1 | major = $major; 29 | } 30 | 31 | public static function create(Major $major): self 32 | { 33 | return new self($major); 34 | } 35 | 36 | /** 37 | * @throws \InvalidArgumentException 38 | */ 39 | public static function fromString(string $value): self 40 | { 41 | if (0 === \preg_match('/^(?P(0|[1-9]\d*))\.(?P(0|[1-9]\d*))?$/', $value, $matches)) { 42 | throw new \InvalidArgumentException(\sprintf( 43 | 'Value "%s" does not appear to be a valid value for a semantic version.', 44 | $value 45 | )); 46 | } 47 | 48 | $major = Major::fromInt((int) $matches['major']); 49 | 50 | return self::create($major); 51 | } 52 | 53 | public function major(): Major 54 | { 55 | return $this->major; 56 | } 57 | } 58 | --------------------------------------------------------------------------------