├── 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 | [](https://github.com/ergebnis/phpunit-slow-test-detector/actions)
4 | [](https://github.com/ergebnis/phpunit-slow-test-detector/actions)
5 | [](https://github.com/ergebnis/phpunit-slow-test-detector/actions)
6 | [](https://github.com/ergebnis/phpunit-slow-test-detector/actions)
7 |
8 | [](https://codecov.io/gh/ergebnis/phpunit-slow-test-detector)
9 |
10 | [](https://packagist.org/packages/ergebnis/phpunit-slow-test-detector)
11 | [](https://packagist.org/packages/ergebnis/phpunit-slow-test-detector)
12 | [](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 |
--------------------------------------------------------------------------------