New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Breakfast: rerun defects first #3147
Conversation
Codecov Report
@@ Coverage Diff @@
## master #3147 +/- ##
===========================================
+ Coverage 81.5% 82.1% +0.59%
- Complexity 3412 3506 +94
===========================================
Files 137 140 +3
Lines 9014 9218 +204
===========================================
+ Hits 7347 7568 +221
+ Misses 1667 1650 -17
Continue to review full report at Codecov.
|
4f40a19
to
476c1ea
Compare
|
Thanks for the quick review, again!
|
src/Util/TestResultCache.php
Outdated
/** | ||
* @var string | ||
*/ | ||
public const DEFAULT_RESULT_CACHE_FILENAME = 'result_cache.json'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if I have my project, eg ~/php-cs-fixer
folder, and I would run phpunit with cache enabled, it would create ~/php-cs-fixer/result_cache.json
? if so, please let us make it phpunit.cache.json
or sth like this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no opinion on the filename of the default value. As a developer I want the following:
- Simple usage that just works in a basic code, test, repeat cycle. A file in the working directory works well.
- PHPUnit is part of automation: use the environment variable to do your devops and have it store the cache(s) for users/configs/build-unique-ID/git-hashes/etc however en whereever you want.
The default implementation of TestResultCache
is there so the simplest use cases work out-of-the-box. The current cache is just a simple MVP. I would prefer NOT to use an external dependency like SQLite like PHPUnit Clever and Smart did, but I am starting to see why they went that way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, here I'm even not talking about using SQLite or anything like that. (while Indeed, i can see value of it - yet in caching of PHP CS Fixer we don't use it as well - too fancy for our simple purpose).
what I'm saying here @epdenouden is that if I will run phpunit in my project, using cache and out-of-the-box approach (so I don't configure anything extra), phpunit will leave result_cache.json
file there. if phpunit will leave sth, it shall indicate it's his leftover, putting it to the name part.
like
-result_cache
+phpunit_cache
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@keradus quick reply: that was a good suggestion, thx! Have pushed a change for that.
I mixed two related topics regarding the caching, making my comment confusion. Adding in persistence cleanly has been more of a chore than I expected. Will spend more time tonight on a cleaner approach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
new name looks shiny, thanks for the change!
then, I will try to review changes tomorrow ;)
src/Util/TestResultCache.php
Outdated
return; | ||
} | ||
|
||
$this->defects = $cacheData['defects']; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we have no clue that decoded data contain such key...
and even if - what is the content of it.
it's optimistic approach here.
cache file could be treated as-is, one is not supposed to modify it manually.
maybe serialize
and then unserialize with unserialize(..., ['allowed_classes' => CacheData::class])
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the earliest versions of Breakfast were just that: patches to the code that quickly (un)serialized a results file to store the state between runs. I'll try some other options.
src/Util/TestResultCache.php
Outdated
|
||
private function formatJSONOutputStruct(): string | ||
{ | ||
return \json_encode([ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
json_encode
can fail as well (eg on non utf-8 input)
src/Util/TestResultCache.php
Outdated
/** | ||
* @var array | ||
*/ | ||
private $times = []; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is the content of those collections? array of.... ints? is key meaningful or numeric?
how single defect looks like ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, all good points regarding the results cache. Started refactoring that part to make it more robust and cleaner.
phpunit.xsd
Outdated
@@ -227,6 +233,7 @@ | |||
<xs:attribute name="printerClass" type="xs:string" default="PHPUnit\TextUI\ResultPrinter"/> | |||
<xs:attribute name="printerFile" type="xs:anyURI"/> | |||
<xs:attribute name="processIsolation" type="xs:boolean" default="false"/> | |||
<xs:attribute name="stopOnDefect" type="xs:boolean" default="false"/> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would be nice to document somewhere what is a defect
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you. I have added a description to the help message.
|
||
private function updateRunState(\PHPUnit\Framework\Test $test, string $state): void | ||
{ | ||
$testName = $this->fullTestName($test); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
too many spaces
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
src/Runner/TestSuiteSorter.php
Outdated
]; | ||
|
||
/** | ||
* @var array |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
array of ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added description
src/Runner/TestSuiteSorter.php
Outdated
/** | ||
* @var array | ||
*/ | ||
private $defectSortWeight = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed, was a precalculated array before.
src/Runner/TestSuiteSorter.php
Outdated
} | ||
} | ||
|
||
private function sort(TestSuite $suite, int $order, bool $resolveDependencies): void | ||
public function setCache(TestResultCache $cache): void |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this property shall not be exposed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, the method is only used in the test. Will clean it up :)
use PHPUnit\Framework\TestCase; | ||
use PHPUnit\Framework\TestSuite; | ||
|
||
class TestSuiteSorterEmptyTestCaseTest extends TestCase |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there is no TestSuiteSorterEmptyTestCase
class, how we can have test for it ?
test here shall be part of TestSuiteSorterTest
test class, it's one concrete, edge scenario to be tested, not completely other class
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored the fixtures in TestSuiteSorterTest
and added this test in. Cleaner, thanks for the comment!
It started off as a quick test where I didn't want the fixtures from TestSuiteSorterTest
. The name is indeed confusing, it was supposed to mean "TestSuiteSorter
(tested with a) EmptyTestCase
".
59d043a
to
b46e416
Compare
tests/Runner/TestSuiteSorterTest.php
Outdated
$sorter = new TestSuiteSorter(); | ||
$sorter->setCache($cache); | ||
$_sorter = new \ReflectionClass($sorter); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_? leading _?
what about $sorterReflection
?
also, having a need to access class' internals in test indicate bad class structure. maybe Cache shall be injected to Sorter via constructor ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The $_var as a temporary version of $var as I quickly needed one, like the $_suite in TestRunner. Will pick something else for now, then look at the use of Reflection.
I can imagine $sorter = new TestSuiteSorter($cache)
but to me that feels like juggling the hot potato of persisting the cache between runs. When added to the constructor it becomes the problem of the TestRunner
which creates the TestSuiteSorter
.
Thank you for your detailed comments! I will spend more time experimenting with the code and structure later today.
src/Runner/TestSuiteSorter.php
Outdated
@@ -51,7 +51,7 @@ | |||
]; | |||
|
|||
/** | |||
* @var array | |||
* @var array[string]int Associative array of (string => DEFECT_SORT_WEIGHT) elements |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
array[string]int
is not proper type according to phpdoc
array<string, int>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Where did you find this syntax? I saw this recommended a few times, but have found no definitive answer in the manual https://docs.phpdoc.org/guides/types.html
I found some discussions regarding proposals but most of them seem to have gone to sleep, for example: phpDocumentor/phpDocumentor#650
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
psr-5 always-proposal, sadly. but it's widely suported by IDEs and SCAs
0139690
to
cbeb934
Compare
src/Util/TestResultCache.php
Outdated
return; | ||
} | ||
|
||
$this->loadFromJSON($json); | ||
$cache = \unserialize($cacheData); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please provide 2nd argument with allowed class deserialization.
now, you are exposing injection point for security hole (for PHPUnit it's not big deal, but still...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes thx :) had added that in the meantime. Since you're here, is it very picky about namespacing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done! :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self::class
takes FQCN into consideration, so all good ;)
tests/Util/TestResultCacheTest.php
Outdated
public function testCacheFilenameViaEnv() | ||
{ | ||
$_ENV['PHPUNIT_RESULT_CACHE'] = '/some/cache'; | ||
$cache = new TestResultCache; | ||
|
||
$this->assertEquals('/some/cache', $cache->getResultCacheFilename()); | ||
unset($_ENV['PHPUNIT_RESULT_CACHE']); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if assertEquals
would fail,
this line would never be executed
private $runState = []; | ||
|
||
/** | ||
* @var TestResultCache |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
new ResultCacheListener()
boom ! this property is null
, not TestResultCache
;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added some phpdoc ...which I then even had to sort 🤓
src/TextUI/Command.php
Outdated
--defects-first-order Run previously unsuccessful tests first | ||
--random-order Run tests in random order | ||
--order-by=<order> Run tests in 'default', 'random' or 'reverse' order | ||
--order-by=defects Run previously failed tests first |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure about this docs here. it's single option, it shall have single doc entry
, in which we explain what is defects
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, looks like depends
is missing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good points, yes. Wanted to give it a quick try using it myself for a while first. If this is something that @sebastianbergmann also likes I will make it production-ready.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It barely fits on one line now, but everybody reads the manual... right? :)
|
||
private function handleOrderByOption(string $value): void | ||
{ | ||
foreach (\explode(',', $value) as $order) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if I would put as value foobar
, it shall fail. now, it's ignored
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. I didn't see other --options
throw nice error messages yesterday. Will have a look at how to do this nicely. Silent failures are a big no-no here, I have typed defecst
waaaaay too often
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a simple error message with failure exit code.
@keradus The
The help message is now:
|
src/TextUI/Command.php
Outdated
@@ -1347,6 +1346,8 @@ private function handleOrderByOption(string $value): void | |||
$this->arguments['resolveDependencies'] = true; | |||
|
|||
break; | |||
default: | |||
$this->exitWithErrorMessage("unrecognized order-by option -- $order"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why double --
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed. Was a leftover from where I found this in GetOpt
.
phpunit.xsd
Outdated
@@ -171,6 +171,12 @@ | |||
<xs:enumeration value="random"/> | |||
</xs:restriction> | |||
</xs:simpleType> | |||
<xs:simpleType name="executionOrderDefectsType"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this shall be removed now, and executionOrderType
shall be extended to contain defects
and depends
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed. I have added a list of the common scenarios that make sense. Couldn't find a nice way to define the list in the XSD to help the tooling with validation and autocomplete.
src/Framework/TestResult.php
Outdated
@@ -1011,6 +1016,14 @@ public function stopOnSkipped(bool $flag): void | |||
$this->stopOnSkipped = $flag; | |||
} | |||
|
|||
/** | |||
* Enables or disables the stopping for any defect: skipped, error, failure, warning, incomplete or risky. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this description is not up to date now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
'testFour' => ['state' => BaseTestRunner::STATUS_FAILURE, 'time' => 1], | ||
'testFive' => ['state' => BaseTestRunner::STATUS_FAILURE, 'time' => 1], | ||
], | ||
['testFive', 'testOne', 'testTwo', 'testThree', 'testFour']], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👎
testFour
and testFive
failed, so I want to have them as soon as possible, with respect to their dependencies.
I would expect that order, instead:
['testFive', 'testThree', 'testFour', 'testOne', 'testTwo']
or
['testThree', 'testFour', 'testFive', 'testOne', 'testTwo']
as in the end, we claimed to run defects first, not last (vide testFour)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
testThree
has dependencies ontestOne
andtestTwo
testFour
has a dependency ontestThree
what happens:
- after sorting for defects the order is:
4
(failed),5
(failed),1
,2
,3
- running it like this would skip
4
because it needs to run3
first - same story for
3
: it needs both1
and2
- the dependency resolver fixes this quietly for us:
5
can stay up front (=defects FIRST) but4
goes behind3
which goes behind1,2
(=defects ASAP ;-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ha, ok, your first 2 points were not part of description (while dependencies of testThree for TestFour was)
then, all good 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good but not good enough :) I will document the use of MultiDependencyTest
in the dataproviders to avoid some of the confusion.
src/TextUI/TestRunner.php
Outdated
$arguments['processIsolation'] = $arguments['processIsolation'] ?? false; | ||
$arguments['processUncoveredFilesFromWhitelist'] = $arguments['processUncoveredFilesFromWhitelist'] ?? false; | ||
$arguments['randomOrderSeed'] = $arguments['randomOrderSeed'] ?? \time(); | ||
$arguments['registerMockObjectsFromTestArgumentsRecursively']= $arguments['registerMockObjectsFromTestArgumentsRecursively'] ?? false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
a space is missing before =
after solving that, alignment will be back to original one again
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried something like that in the past, let's see if your suggestion works nicer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks damn good piece of work here !
@sebastianbergmann Hi Sebastian! @keradus and I think this pull request is done. For new ideas and improvements I will open new PRs in the near future. :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
double checked after squash, looks same as my local copy before squashing
771abc2
to
5b16423
Compare
Thanks! |
🎁 thanks for the merge! :) |
🎆 |
Jis sugeneruojamas, kad būtų greitesnis testavimo-koregavimo ciklas. Įjungtas standatiškai nuo PHPUnit 8 Dokumentacija: * sebastianbergmann/phpunit#3147 * https://phpunit.de/announcements/phpunit-8.html
Jis sugeneruojamas, kad būtų greitesnis testavimo-koregavimo ciklas. Įjungtas standatiškai nuo PHPUnit 8 Dokumentacija: * sebastianbergmann/phpunit#3147 * https://phpunit.de/announcements/phpunit-8.html
Use case
Rerun defective unit tests first to speed up the red-green-refactor cycle!
Test results and timings are shared between tests runs using a cache. This allows for sorting defective tests to the front of the execution order using an additional sorter. This new sort option is compatible with existing test ordering options.
This project is a followup to my previous work on reordering test execution. The code is available in my Breakfast development branch.
Summary of changes
TestSuiteSorter
gains the ability to sort suites and tests by priority of defects and execution timeResultCacheExtension
to gather result and timing information during test runsTestResultCache
for sharing of result state between runs--cache-result
and configuration attributecacheResult='true'
--order-by=defects
and configuration attributeexecutionOrder='defects'
--cache-result-file
and configuration attributecacheResultFile
How to test
To activate storing the results of a run use
--cache-result
:For this example we switch the cache on for every run by adding the attribute
cacheResult="true"
to the<phpunit>
element inphpunit.xml
.Break
TestCaseTest
by running it backwards:Look at the cache file. By default this is
.phpunit.result.cache
in the PHPUnit working directory.Break
TestCaseTest
again with the sorting feature turning on:The skipped tests are still there but are run immediately now, but still fail. This is easily avoided by asking the sorter to keep dependencies in mind:
Design considerations
Like the New Order feature the aim is for a robust system with sane defaults that silently does its work and is easily maintained by other developers:
cacheResult="true"
to your configuration and it silently does its work.