-
Notifications
You must be signed in to change notification settings - Fork 26
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
Serialize tests during coverage calculation #91
base: master
Are you sure you want to change the base?
Serialize tests during coverage calculation #91
Conversation
3c3ffba
to
963fd1e
Compare
963fd1e
to
f960b26
Compare
|
||
public TestIdentifierListener(Class<?> testClass, TestUnitExecutionListener l) { | ||
this.testClass = testClass; | ||
this.l = l; | ||
serializeExecution = !(l instanceof NullExecutionListener); |
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 not immediately clear why serialized execution is dependent on the type of the execution listener. Think some comments to explain what is going on are needed.
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
@@ -97,10 +102,15 @@ private class TestIdentifierListener implements TestExecutionListener { | |||
private final Class<?> testClass; | |||
private final TestUnitExecutionListener l; | |||
private final List<TestIdentifier> identifiers = synchronizedList(new ArrayList<>()); | |||
private final boolean serializeExecution; | |||
private final Map<UniqueId, ReentrantLock> parentCoverageSerializers = new ConcurrentHashMap<>(); |
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 a lot of locks here. I'd have imagined that we could force things to run serially using just a single lock. I'm sure that I'm wrong, but to understand what all these locks are doing I think a big comment block explaining what junit does and the rationale behind 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 imagined also a single lock, unfortunately at least for Spock it didn't work.
I added a wall of text explaining the situation and logic that hopefully makes it clearer.
I really was "a bit" silent in regards of comments here. :-D
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 also made the child locks lazily initialized atomic references now, so that the locks for childs are only created if actually needed like for data-driven Spock features.
@@ -134,6 +158,14 @@ public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult | |||
} else if (testIdentifier.isTest()) { | |||
l.executionFinished(new Description(testIdentifier.getUniqueId(), testClass), true); | |||
} | |||
|
|||
if (serializeExecution) { | |||
parentCoverageSerializers.remove(testIdentifier.getUniqueIdObject()); |
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 remove the lock from a map but don't unlock it? When is the lock released?
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 wall of text above should hopefully make it clear.
This is the lock for child tests, so it should not be locked at this point.
f960b26
to
325f0c5
Compare
@Vampire Do you have the spock version of the MCVE available anywhere? |
325f0c5
to
d01afb0
Compare
Sure, I expanded the given MCVE to add Spock: pitest-issue-760-with-spock.zip |
7b7d28e
to
310fd0b
Compare
@Vampire Just been taking another look at this. Unfortunately I think the idea of stopping parallel execution during coverage, but allowing it during mutation testing is flawed. It risks the killing test being misidentified, which could give in incorrect results when history recording is enabled. I'm also still uncomfortable about the large number of locks required to prevent parallel running. I think the current soloution of automatically disabling parallel running by automatically setting a junit configuration value is preferable. Although it doesn't work for spock, this could presumably be rectified by adding the correct setting. In theory we'd have to do this for an open ended number of test runners, but in practice there are very few in use. |
Would you mind elaborating where you see that risk? You just have to make sure you properly handle the calls in the result collector in a thread-safe way.
And I'm still unaware of what you mean. For Jupiter tests you will have exactly 1 lock, the global lock, as there are no nodes with type For Spock, there will be at most 2 locks locked at the same time. It is just implemented like it is to cover all platform engines someone could come up with sanely.
Well, our opinions greatly differ here and I still hope I can convince you. :-)
There is no setting you can set that overrides it for Spock afair. There is also no setting on the JUnit platform level I'm aware of that could disable this, because it is the test engine that decides to either use a I really do think that using the locks during coverage calculation and accepting parallel execution later on is the proper way to go. It is imho the easiest, than to always react to a new engine being brought up and then first make it add an overwrite setting and then use it and so on, when this solution here should just generally work for practically any sane implementation someone can come up with without the need to do anything further. The non-thread safe parts that could fail like the mentioned This work-around will typically be used independent of whether parallel running is currently enabled or disabled. You put it in place because you enable parallel running and even if for whatever reason you temporarily disable parallel running (or even permanently because by then you forgot you made this work-around), the work-around will still be in place. So if you now disable parallel running using the settings switch of Jupiter or one that might be introduced for Spock and all test scheduling now happens on the same thread, the test execution and thus So all according code that somehow communicates with the test execution or discovery phase needs to be made thread-safe where not already happened anyway or you get nasty multi-threading problems that will be hard to find and fix. |
Nah, that's not as trivial as I thought while writing that sentence, because you need to recognize that there is a parent locked and on which layer you are. Having looked over the implementation and what I imagined as replacement, I think the current code is fine already. Now it could happen that This logic automatically works for any depth as long as you don't have a |
You are correct, there should no longer be a problem misreporting the killing test if they run in parallel. The minion used to send the name of a test back to the parent process before executing it so the parent was guaranteed to receive it even if the test caused an infinite loop. This would not work if the tests ran in parallel, but I'd forgotten that this got ripped out because it proved too flakey. We now accept that we won't know which test triggered an infinite loop and all data is sent after execution. The "lots of locks" comment is because looking at the code statically the number of locks is unbounded (we have two maps of them). The fact that we have only 1 or 2 locks at runtime is not obvious without intimate knowledge of how spock, jupiter, cucumber and the other test runners behave at runtime. Given that tests cannot be run in parallel during the coverage stage, and the benefit of them being in parallel during the mutation analysis stage is unclear (pitest will only run one at once so the only possible parallelism is where a "unit" contains more than one test) the simplest, easiest to maintain soloution would be to disable parallel running at the level of the test framework. Unfortunately that doesn't seem to be an option. So, the two possible ways forward are
My preference is always to keep things as simple as possible (i.e 2), but if you're happy to maintain this going forward I'll go with 1 and merge it in. |
310fd0b
to
6020822
Compare
Which I hope to get changed with hcoles/pitest#1223 once the JUnit platform launcher supports aborting a run gracefully. :-) But for now at least for the hunting minion test discovery and for atomic units with mutliple tests it will speed up a bit.
❤️
Sure, no problem. |
Maybe it would make sense though to first cut a new |
Any idea when you get a chance to cut a release of pitest @hcoles? |
@Vampire 1.14.2 should be available from maven central now. |
This generically fixes hcoles/pitest#760 and pitest#73 for all platform engines, removing the Jupiter specific work-around from pitest#74 and serializing test execution during coverage calculation using locks. This way the tests can also properly run in parallel later on during mutant hunting.
6020822
to
b66b56d
Compare
Perfect, thank you very much. |
So, anything left to do here? :-) |
Depends on #90
Fixes #73
Fixes hcoles/pitest#760
This generically fixes hcoles/pitest#760 and #73 for all platform engines,
removing the Jupiter specific work-around from #74 and serializing test execution
during coverage calculation using locks. This way the tests can also properly run
in parallel later on during mutant hunting.
I tested it extensively with the MCVE from hcoles/pitest#760 and also with the same MCVE ported to Spock with parallel test execution
and got consistently the correct result of 6 killed mutants across several 100 runs. (7th mutant is immortal)