How to implement coverage highlighting
Hi there!
I'm developing a custom plugin to integrate mutation testing into IntelliJ.
I was able to make it work and I can show the results externally via a web browser. But I'm interested in using IntelliJ's built-in code coverage highlighting capabilites to achieve a better user experience.
However, IntelliJ's documentation is scarce, and there isn't an example I could use to achieve this and, to make things more complicated, IntelliJ errors when I try to download the Platform SDK sources (so I have to guess what different pieces of code do, and the SDK seems to be uncommented).
I attempted to use RunLineMarkerProvider, but the caveat is that it paints lines all the time, when I need it to run as a result of a process output (for which I use ModuleBasedConfiguration<JavaRunConfigurationModule, MyConfig>), I don't have a chance to feed it with the coverage data when the process I run collects it.
Googling and asking ChatGPT, I heard about using RangeHighlighter, but then the downstream dependencies that class has seem to be quite hard to create and wire. I also found this post:
Which makes me feel better, as I'm not the only one struggling with, but I'm expecting to reuse the built-in feature by passing my coverage data (i.e., which files/lines to paint of what color), not to reimplement it from scratch (which, I believe, is what the solution was for the post author).
Can you help me please?
Thanks
Please sign in to leave a comment.
Dear Nahuel,
Unfortunately, the implementation of coverage highlighting is not so flexible as you expected. The implementation is highly tied to the coverage needs, so it is not just ‘which files/lines to paint of what color’.
What is the format of data you want to highlight?
Depending on your situation, I see two options.
First is to use RangeHighlighter-s directly and reimplement highlighting for your needs.
The second option is to convert your data into a form of a coverage report that is expected by the coverage engine. For example, you can see how xml reports loading is implemented in IDEA and do the same for your data. See `com.intellij.coverage.xml.XMLReportRunner` https://github.com/JetBrains/intellij-community/blob/master/plugins/coverage/src/com/intellij/coverage/xml/XMLReportRunner.kt
Hi Maksim Zuev, thanks for your quick answer!!
Regarding RangeHighlighter, I tried to do so but I failed to “hook it” into IntelliJ's SDK. Put it differently, my Highlighter was never called. At this point, I decided to ask the community if I could simply rely on IntelliJ's built-in mechanism to save myself time and effort.
On the other hand, following you will find the XML output of the process I run to get coverage (in case it helps, it the PIT mutation test CLI XML output):
I built a parser for this XML as part of my plugin's logic, and I'm trying to find a way to input it to the built-in mechanism used by IntelliJ to highlight code coverage. I believe that would really save me a lot of effort.
I will play around the class you shared, althought I'm using Java 21 instead of Kotlin. That shouldn't be an issue anyway, but I brought it up in case it matters.
Edit after original comment: Following your clue, I came up with this code:
The only caveat is that I don't have access to ProjectData from my plugin, and I wonder if it's an internal API or if it's available to custom plugins to use. This seems promising to be honest, but I can't test it because I'm not sure how to make that class available on my project (I tried a couple of things that ChatGPT mentioned without any luck).
Also, my IntelliJ errors when I try to download source code for the SDK, so I did a search on GitHub and I haven't seen the @Internal annotation, but I'm not sure if this is considered internal API and I shouldn't use it (the plugin guide suggests sticking to public API only).
Let me know if you need further clarification. Thanks again!!
Please try adding `Coverage` plugin to the list of dependencies. `ProjectData` should be available. Looks like you are in the right direction, but I think you should also implement `MyCoverageRunner`, `MyCoverageAnnotator` the same way `XMLReportEngine` does it
For reference:
Where:
Now I'm currently working on implementing:
Hopefully, I will have good news soon.
Thanks again!
Hi there!
I'm back after some time. I couldn't make it work completely and I'm not sure why. I reimplemented practically all those classes but I can't get the `getReportData()` method to be invoked.
I'm not sure what I'm missing, but I only see the following log lines logged in idea.log when I run the plugin:
Following is the full code I have so far (except the RunConfiguration code):
Plugin.xml
MyRunConfiguration
MyCustomRunner
MyCoverageEngine
MyCoverageSuite
MyCoverageAnnotator
A lot of the code is duplicated from what IntelliJ comes with for Coverage. It's not important because I can't even get this to execute beyond the constructor of MyCoverageSuite class. I added it here just to add as much context as possible.
Any help is appreciated.
Thank you!
Dear Nahuel,
You are doing a great job so far. I think the only missing part is calling the coverage suite opening after you created a suite.
Also,
coverageDataManager.triggerPresentationUpdate()
is unnecessary as it will be called automatically by the platform code.Hi Maksim, hope you're doing well :)
Yes, you rock it! Now I see my custom logic being called. And even though the logic itself is not finished yet, now I can start debugging it and making the required changes.
This is how the code looks after all your awesome suggestions:
However, as I mentioned in the multiline comment above, I noticed that
coverageDataManager.getSuites()
returns several instances ofMyCoverageSuite
:Which kind of makes sense, if you consider what the logs show:
I would like to know if this is normal, or if I need to change something to ensure that only one instance of
MyCoverageSuite
is returned by the aforementioned method.Thank you very much Maksim!
H!
addExternalCoverageSuite
registers a suite in a storage so that you can later open it from the UI: https://www.jetbrains.com/help/idea/code-coverage.html#select_coverage_suitesIf you don't want to have several entries for your suites, you should call
com.intellij.coverage.CoverageDataManager#removeCoverageSuite
orcom.intellij.coverage.CoverageDataManager#unregisterCoverageSuite
when your suite is closed/on the opening of the new oneHi Maksim!!
Thanks a lot, I thought I did something wrong and I was afraid that it could cause any unwanted side-effect on the IDE. For now I'll leave it as is.
I have a last couple of questions before having everything I need to finish my implementation:
Let's say I would like to reuse the existing
XMLReportRunner
and all related classes (as you suggested back in November).My understanding is that I could map my current XML file to what is understood by those built-in classes for it to work, and then call it somehow (probably using the same code I already have in my config).
As I haven't found the definition (or an example) of the XML format expected by those classes, I thought I could just run any class with coverage and then debug what's fed into the aforementioned XML classes to get an idea on how this mapping could be.
Do you think this could be a good approach to understand the XML model used by Intellij? Or I may miss anything relevant by just looking at one sample report that could backfire later?
And, most importantly, if this is a good-to-explore option, do you happen to know if the XML format varies significatively from one IDE version to another?
My idea is to evaluate if it's worth to heavily rely on the built-in classes to save me some work (and to reuse a battle-tested solution, mainly). But if the XML format is unstable, or has significant changes from version to version, then I'm happy to stick with the current approach.
Again, thank you very much Maksim, you have no idea how much you have helped me.
Hi! XMLReportRunner expects JaCoCo xml report as an input
https://www.eclemma.org/jacoco/trunk/doc/index.html
So, you can try mapping your data to this format, or use your parsing method
Awesome, now I have everything I need to implement what I need for my plugin.
Thanks a lot Maksim for your help and patience!!