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
@Depends file will be run if not ran yet + Full namespace bug when using @depends [FEATURE&BUG-FIX] #3449
@Depends file will be run if not ran yet + Full namespace bug when using @depends [FEATURE&BUG-FIX] #3449
Conversation
Codecov Report
@@ Coverage Diff @@
## master #3449 +/- ##
============================================
+ Coverage 82.15% 82.26% +0.11%
- Complexity 3580 3601 +21
============================================
Files 143 143
Lines 9398 9457 +59
============================================
+ Hits 7721 7780 +59
Misses 1677 1677
Continue to review full report at Codecov.
|
@Niko9911 Oh nice, more test execution order features :-) I will have a look for sure |
There is also issue when returning value from depended-upon test. It will always return Meanwhile, please approve ;) |
I think it would be best if this could be merged into 7.5? |
src/Framework/TestCase.php
Outdated
$failedKeys = []; | ||
|
||
foreach ($failed as $failure) { | ||
$pos = \strpos($failure->getTestName(), ' with data set'); |
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 substring with [data set]
is dependent on how test names are send to the loggers. This could change in the future, for example with #3439
You can retrieve the TestCase
via $failure->failedTest()
and explore if it has a data set using $test->dataName()
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, this is the way how it is used elsewhere also.
At that point only thing what I'm interested about is \some\namespace\class::someFunction
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 is used like this elsewhere indeed! Which keeps giving me headaches as I am trying to refactor it out. 😆 For the test execution reordering and TestDox original-ordering, I cooked up this one:
phpunit/src/Runner/TestSuiteSorter.php
Lines 77 to 94 in b8f38d3
public static function getTestSorterUID(Test $test): string | |
{ | |
if ($test instanceof PhptTestCase) { | |
return $test->getName(); | |
} | |
if ($test instanceof TestCase) { | |
$testName = $test->getName(true); | |
if (\strpos($testName, '::') === false) { | |
$testName = \get_class($test) . '::' . $testName; | |
} | |
return $testName; | |
} | |
return $test->getName(); | |
} |
The with data set
substring is a user interface element that should be independent of the inner workings of the test runner. Here's an upcoming change that might interfere with your code: 06d8c4b
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.
Okey, I'm still not sure how you're gonna replace that "with data set". You would need also refactor these
phpunit/src/Framework/TestCase.php
Lines 1685 to 1780 in b8f38d3
private function handleDependencies(): bool | |
{ | |
if (!empty($this->dependencies) && !$this->inIsolation) { | |
$className = \get_class($this); | |
$passed = $this->result->passed(); | |
$passedKeys = \array_keys($passed); | |
$numKeys = \count($passedKeys); | |
for ($i = 0; $i < $numKeys; $i++) { | |
$pos = \strpos($passedKeys[$i], ' with data set'); | |
if ($pos !== false) { | |
$passedKeys[$i] = \substr($passedKeys[$i], 0, $pos); | |
} | |
} | |
$passedKeys = \array_flip(\array_unique($passedKeys)); | |
foreach ($this->dependencies as $dependency) { | |
$deepClone = false; | |
$shallowClone = false; | |
if (\strpos($dependency, 'clone ') === 0) { | |
$deepClone = true; | |
$dependency = \substr($dependency, \strlen('clone ')); | |
} elseif (\strpos($dependency, '!clone ') === 0) { | |
$deepClone = false; | |
$dependency = \substr($dependency, \strlen('!clone ')); | |
} | |
if (\strpos($dependency, 'shallowClone ') === 0) { | |
$shallowClone = true; | |
$dependency = \substr($dependency, \strlen('shallowClone ')); | |
} elseif (\strpos($dependency, '!shallowClone ') === 0) { | |
$shallowClone = false; | |
$dependency = \substr($dependency, \strlen('!shallowClone ')); | |
} | |
if (\strpos($dependency, '::') === false) { | |
$dependency = $className . '::' . $dependency; | |
} | |
if (!isset($passedKeys[$dependency])) { | |
$this->status = BaseTestRunner::STATUS_SKIPPED; | |
$this->result->startTest($this); | |
$this->result->addError( | |
$this, | |
new SkippedTestError( | |
\sprintf( | |
'This test depends on "%s" to pass.', | |
$dependency | |
) | |
), | |
0 | |
); | |
$this->result->endTest($this, 0); | |
return false; | |
} | |
if (isset($passed[$dependency])) { | |
if ($passed[$dependency]['size'] != \PHPUnit\Util\Test::UNKNOWN && | |
$this->getSize() != \PHPUnit\Util\Test::UNKNOWN && | |
$passed[$dependency]['size'] > $this->getSize()) { | |
$this->result->addError( | |
$this, | |
new SkippedTestError( | |
'This test depends on a test that is larger than itself.' | |
), | |
0 | |
); | |
return false; | |
} | |
if ($deepClone) { | |
$deepCopy = new DeepCopy; | |
$deepCopy->skipUncloneable(false); | |
$this->dependencyInput[$dependency] = $deepCopy->copy($passed[$dependency]['result']); | |
} elseif ($shallowClone) { | |
$this->dependencyInput[$dependency] = clone $passed[$dependency]['result']; | |
} else { | |
$this->dependencyInput[$dependency] = $passed[$dependency]['result']; | |
} | |
} else { | |
$this->dependencyInput[$dependency] = null; | |
} | |
} | |
} | |
return true; | |
} |
phpunit/src/Framework/TestCase.php
Lines 1097 to 1116 in b8f38d3
public function getDataSetAsString(bool $includeData = true): string | |
{ | |
$buffer = ''; | |
if (!empty($this->data)) { | |
if (\is_int($this->dataName)) { | |
$buffer .= \sprintf(' with data set #%d', $this->dataName); | |
} else { | |
$buffer .= \sprintf(' with data set "%s"', $this->dataName); | |
} | |
$exporter = new Exporter; | |
if ($includeData) { | |
$buffer .= \sprintf(' (%s)', $exporter->shortenedRecursiveExport($this->data)); | |
} | |
} | |
return $buffer; | |
} |
I'm still not sure how these should be refactored.
Also I'm not sure how I can access dependencies of test inside of TestSuiteSorter.
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's enough to refactor for everyone! Don't be shy, just grab a few classes and have at it. 🛠
You can get the name of a TestCase
with:
$testCase->getName($withDataSet)
which gives you the name with(out) a prettified data set$testCase->dataName()
returns the name of the data set you see ingetName()
$testCase->getDependencies()
returns a list of dependencies, if any
Please explore the surrounding code a bit more and write tests before you make such large changes.
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 understand and I would like to fix things. However I want remind about few facts.
I'm using heavily my own time to fix & learn this stuff. It's not as simple as it looks for guy who have no experience with PHPUnit core. I will look into this after I have created some tests.
Would need this feature ASAP into PHPUnit...
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 get it, no worries! Like many other tools and frameworks PHPUnit is maintained by volunteer contributors. We are paid in emoji and sleepless nights.
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.
@epdenouden Tests are now Implemented 🎉
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.
@epdenouden Btw, cannot really remove this $pos = \strpos($failure->getTestName(), ' with data set');
Because if taking look into src/Framework/TestFailure.php
there will be
/**
* Constructs a TestFailure with the given test and exception.
*
* @param Throwable $t
*/
public function __construct(Test $failedTest, $t)
{
if ($failedTest instanceof SelfDescribing) {
$this->testName = $failedTest->toString();
} else {
$this->testName = \get_class($failedTest);
}
if (!$failedTest instanceof TestCase || !$failedTest->isInIsolation()) {
$this->failedTest = $failedTest;
}
$this->thrownException = $t;
}
Same with other stuff. Meaning this needs to be refactored and refactoring this means refactoring also skipped, passed, etc... Would change little bit too much logic to be in this PR.
src/Framework/TestCase.php
Outdated
) { | ||
$dependencyClass = \explode('::', $dependency, 2)[0]; | ||
(new TestSuite($dependencyClass, 'Dependency_' . \ucfirst($dependencyClass))) | ||
->run($this->result); |
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 is scary bit of code. It fires off a lot of logic that will have unforeseen consequences, I'm just not sure which at the moment.
If I am reading this correctly, this code does the following:
- if the
$dependency
has not been passed yet, do - check if
$dependency
hasn't been failed or skipped - get the name of the class in which the dependency resides by splitting the FQCN
- yolo
(new TestSuite($theDependencyWeNeed))->run($currentResult)
I get what you are trying to achieve: auto-run dependencies without any hassle on the user side. If this logic were to be accepted, it needs more safeguards and refactoring:
- what are the side effects of instantiating a
TestSuite
here in this scope? - what happens if this
TestSuite
has already been instantiated, but: not run? or not run completely? failed during eithersetUpBeforeClass
orsetUp
? - even if this works,
handleDependencies()
shouldn't just->run()
the missing dependency and merge it into the current result. Leave that responsibility to the Runner and find a cleaner way to pass around the execution plan.
Tests, tests and more tests. Preferably a few end-to-end ones :-)
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 is scary bit of code. It fires off a lot of logic
I know... Kinda the way to deal with it, when I cannot access the list of tests to execute. Another solution would be scanning all files which depends on which one and reorder them. However, I tried it without success.
If I am reading this correctly, this code does the following:
Yup.
what are the side effects of instantiating a TestSuite here in this scope?
what happens if this TestSuite has already been instantiated, but: not run? or not run completely? failed during either setUpBeforeClass or setUp
Good question. I will check what happens.
Tests, tests and more tests. Preferably a few end-to-end ones :-)
Not sure how to implement those at all...
even if this works, handleDependencies() shouldn't just ->run() the missing dependency and merge it into the current result. Leave that responsibility to the Runner and find a cleaner way to pass around the execution plan.
More specific implementation how to do it?
I could just run it like this and remove the result from wrong place like shown in the image?
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.
FYI, this issue showing in the image has been fixed..
@Niko9911 I will reply more tomorrow, logging off for the day. |
@epdenouden NP. Thanks for helping. I will try resolve some dangerous bugs meanwhile when using stuff chained.... |
Okey, I have done everything I could. I tested them with my project. Tests could be nice feature to add here, but I have no Idea how to create them.... Plz Review 😃 |
Seems like 1021 test makes sure this will not fail... Ah crap, I'll fix it... |
Tests are now passing. 😏 |
@Niko9911 Quick glance: what test has been added that tests the functionality you added? Did you add a test that failed first and now works? Changes in the core runner requires strong testing, especially to guard against edge cases. |
@epdenouden There is no tests created for this functionality. I will try create some today. I will be posting tests today/tomorrow into this PR. Could you review the code meanwhile. (The code is already tested in our project using many edge cases so it should work. But as we all know, that is not enough and I will try reproduce those cases as tests and implement those tests into this PR.) |
@Niko9911 Keep in mind that PHPUnit is a tool used by many different projects and environments. If it works in your project that's... a good start. :-) By the way, which project is it and it is publicly available somewhere? I am still very curious what the actual use case is. Depending on "[...] a test that is declared in another test case class" (as @sebastianbergmann says in#2647) makes me curious what other mechanism for sharing dependencies could solve your problem. |
@epdenouden Sorry, I had to blur even the project names in previous images. It's private and under strict NDA. Would like share code, but as I think you understand, it's out of my hands if I can share it or not. The idea is to check if x tests was success fully working, so test y can be tested. For example, if Domain tests fail, commandHandler or Infra tests depending on domain test will be marked skipped, until domain tests will pass. |
@sebastianbergmann Could it be possible to implement this into 7.5.x? |
I kinda found another bug 💤 if depending upon other class and it is not executed yet, it works, but if it's in same class as your test function, it will fail as the function is not executed yet. Reordering functions in class works. I'm gonna fix this also, but it's more dangerous... (High possibility for infinite loop) |
Then make sure you have a stand-alone and self-contained test for this scenario in your proposed code.
That makes sense. You are looking for what is basically the The current solution you are proposing doesn't just "skip if other tests haven't run successfully yet", it actively changes the state of the test runner while this is running. 😨 If you want a solution based on test reordering you will always end up with hacky solutions like this, unless you want to refactor more internals. |
This won't fix due the basic logic behind the PHPUnit. I will mark tests as skipped with following msg:
True, Thats why I have to propose hacky solution, but it is working. Also refactoring internals would mean basically refactoring ½ test framework, which would mean it's better to start from scratch and decide what copy from "old" framework to new.
But would make much more sense to implement this in level on TestFramework.
What you mean? Could you elaborate? If this is not possible to implement (merge) into the PHPUnit, could it be possible to make TestCase expendable in this manner? |
Travis.... ComeON! (Random error) |
Will reply more later, but for now: I don't agree with just shoving in a hacky solution without proper tests. It can be done properly. You'd have to read up on the functionality and limitations of test reordering. I disagree fundamentally that it makes sense to implement a quickfix in a generalized testing framework, instead of in an extended TestCase, setUp() or even a Packagist package that patches PHPUnit's files. The test runner core is like a watch or a clock. I do not understand, at all, how you can just propose creating and running a TestSuite there in the middle of a dependency check. |
I'm working on tests... I just cannot use those I used in our project. Hold on... Tests are coming...
To not refactor everything. If you agree with this feature, you could implement it better as you can see, my knowledge isn't enough to any deeper...
How does one implement this into @sebastianbergmann I'm highly waiting for you opinion about this, if this is worth to spent time? |
From my perspective this PR is ready to be merged. |
$dependencyClass = \explode('::', $dependency, 2)[0]; | ||
$dependencyKey = self::DEPENDENCY_PREFIX . \ucfirst($dependencyClass); | ||
$oldErrorsCount = self::$DEPENDENCY_TASK_RESULTS->errorCount(); | ||
(new TestSuite($dependencyClass, $dependencyKey))->run(self::$DEPENDENCY_TASK_RESULTS); |
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.
Before you put any more work into this, wait for a reply for @sebastianbergmann
The design of this feature still worries me. Creating a new TestSuite
for the missing dependenc(y|ies) and run()
it while running the main loop, is the least optimal way of solving this. If you want to run @depends
before the dependant test, make sure the tests are in the optimal order before you start running.
- what protection is there against instantiating (and running) the same
TestSuite
twice? - ...or more than twice? are results from the
@depends
test merged back into the 'main'$testResult
- how does all this work out when sent to the various loggers? Some of them, like JUNit, log the
TestSuite
hierarchy as thestart*/add*/end*
events are fired
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 protection is there against instantiating (and running) the same TestSuite twice
It will be ran twice, if it is not in correct order.
...or more than twice? are results from the @Depends test merged back into the 'main' $testResult
Not possible to run it over twice. The result will be put into dependency-only testResult in case it's required more than one time before it is ran in correct order.
how does all this work out when sent to the various loggers? Some of them, like JUNit, log the TestSuite hierarchy as the start*/add*/end* events are fired
I'll test this. Should not affect that.
private const DEPENDENCY_PREFIX = 'Dependency_'; | ||
|
||
/** @var TestResult \PHPUnit\Framework\TestResult */ | ||
private static $DEPENDENCY_TASK_RESULTS; |
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 data structure is only needed for the edge case of automatically running dependencies located in another class. Does this need to be a class-wide variable?
Also: why static
, it belongs to an actual TestCase
instance?
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's static for optimization reasons.
Meaning, if multiple functions require this same dependency, the results will be used from this class property instead of running dependency again and again, maybe even 100 times, before it will run the test.
$_SERVER['argv'][4] = '--order-by=no-depends,reverse'; | ||
$_SERVER['argv'][5] = 'MultiDependencyTest'; | ||
$_SERVER['argv'][6] = __DIR__ . '/../_files/MultiDependencyTest.php'; | ||
$_SERVER['argv'][4] = '--reverse-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.
Please use --order-by=
CLI flag instead of legacy --reverse-order
and --ignore-dependencies
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!
Will fix this.
@Niko9911 I can have a more detailed look this weekend. The current design of creating+running a new My vote would be for this change being an external script, like Symfony's PHPUnit bridge. Or implement it like the (Context: after two rounds of clean up of the TestDox logger, I am still finding edge cases and yesterday a bug that throws a PHP error.) |
There is new tests for this logic. As I use this now heavily with edge cases 7 days/week currently, I can quickly identify and fix these issues. I've been implementing as much tests as I can. @sebastianbergmann Would really require your opinion about this! Is this possible to be merged into PHPUnit & What is your opinion? |
@sebastianbergmann @epdenouden Any news? |
Not from me, no |
@Niko9911 Not sure how busy @sebastianbergmann is at the moment, it's Xmas season and all that. As for the cross-class I will be adding full support for 'interdependent units of work' in the near future. However, a clean and maintainable solution is required there, as that refactoring relates to solving |
@epdenouden So you basically say I should drop this current proposed implementation and go with TestSuiteSorter::resolveDependencies? @sebastianbergmann I'm not gonna stop mentioning you before you reply. Just to keep reminding you about this PR. 😜 |
@Niko9911 TL;DR: I agree inter-class Mind you, this isn't my personal project. I just enjoy working on it and being useful around the yard. Not sure what to tell you, as you have mentioned before you do not mind a hacky temporary solution. If that is what you need right now and it solves your problems, my advice would be to make your change a private patch for PHPUnit and publish it on Packagist. I mentioned the Your proposed change will run missing dependencies on the spot, while the existing executio reordering does so before the run. Also it doesn't force-run missing dependencies. I am not sure yours does even. Anyway. I have a few other loose ends to tie up, I'll wait and see what @sebastianbergmann thinks. |
Replaced by #3519. |
This functionality has been inplemented as a part of #3936 |
#2647
As @sebastianbergmann said:
This PR will now ensure depended-upon test is executed before the test(s) that depend(s) on it.
Some stuff to note:
This test depends on "%s" to pass.
This test depends on "%s" to pass.
Fix:
In case of you're writing
@depends \Some\Project\Tests\Unit\Domain\ProductTest::test__toString
this will fail with messageThis test depends on "%s" to pass.
. Why? Due the\
at beginning of the string... I fixed this with following code:PS. I had issues creating tests for these changes. Can somebody show example or create tests?
PPS. Would it be possible to get this feature into 7.5 or 7.6 etc..?