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:

https://intellij-support.jetbrains.com/hc/en-us/community/posts/15113678822802-Seeking-Guidance-on-Implementing-Coverage-Highlighting-in-IntelliJ-IDEA-Plugin

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

0

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
 

0

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):

<?xml version="1.0" encoding="UTF-8"?>
<mutations partial="true">
    <mutation detected='false' status='NO_COVERAGE' numberOfTestsRun='0'>
        <sourceFile>App.java</sourceFile>
        <mutatedClass>io.github.nahuel92.App</mutatedClass>
        <mutatedMethod>main</mutatedMethod>
        <methodDescription>([Ljava/lang/String;)V</methodDescription>
        <lineNumber>5</lineNumber>
        <mutator>org.pitest.mutationtest.engine.gregor.mutators.VoidMethodCallMutator</mutator>
        <indexes>
            <index>5</index>
        </indexes>
        <blocks>
            <block>0</block>
        </blocks>
        <killingTest/>
        <description>removed call to java/io/PrintStream::println</description>
    </mutation>
    
    <mutation detected='true' status='KILLED' numberOfTestsRun='1'>
        <sourceFile>App.java</sourceFile>
        <mutatedClass>io.github.nahuel92.App</mutatedClass>
        <mutatedMethod>sum</mutatedMethod>
        <methodDescription>(II)I</methodDescription>
        <lineNumber>9</lineNumber>
        <mutator>org.pitest.mutationtest.engine.gregor.mutators.returns.PrimitiveReturnsMutator</mutator>
        <indexes>
            <index>6</index>
        </indexes>
        <blocks>
            <block>0</block>
        </blocks>
        <killingTest>
            io.github.nahuel92.AppTest.[engine:junit-jupiter]/[class:io.github.nahuel92.AppTest]/[method:successOnTestingSimpleSum()]
        </killingTest>
        <description>replaced int return with 0 for io/github/nahuel92/App::sum</description>
    </mutation>
</mutations>

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:

// After I execute the command that provides the XML output, I can do:
final var path = Path.of(); // Path to the XML file
final var coverageDataManager = CoverageDataManager.getInstance(getProject());
coverageDataManager.addExternalCoverageSuite(
    path.toFile(),
    new MyCoverageEngine()
);
coverageDataManager.triggerPresentationUpdate();

// ...

public class MyCoverageEngine extends CoverageRunner {
    @Override
    // ProjectData is not available in my custom plugin. Not sure if it's internal to the IDE only and I shouldn't use it
    public com.intellij.rt.coverage.data.ProjectData loadCoverageData(@NotNull File file, @Nullable CoverageSuite coverageSuite) {
        return null; // It seems here I could do the mapping between the XML content and ProjectData
    }

    @Override
    @NotNull
    @NonNls
    public String getPresentableName() {
        return "Presentable name test"; // I'll give it a proper name later
    }

    @Override
    @NotNull
    @NonNls
    public String getId() {
        return "io.github.nahuel92.MyCoverageEngine"; // This shouldn't collision with any other id
    }

    @Override
    @NotNull
    @NonNls
    public String getDataFileExtension() {
        return "xml"; // Should it be in upper case?
    }

    @Override
    public boolean acceptsCoverageEngine(@NotNull final CoverageEngine coverageEngine) {
        return true; // Currently I'm not sure how should I filter
    }
}

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!!

0

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

0

For reference:

  • I had to configure my build.gradle.kts as follows:
dependencies {
    implementation("org.jetbrains.intellij.deps:intellij-coverage-agent:1.0.763")
    
    intellijPlatform {
        bundledPlugin("Coverage")
    }
}

Where:

  • intellij-coverage-agent contains a bunch of classes I need to implement MyCoverageAnnotator
  • Coverage contains ProjectData, LineData and ClassData, which I need to implement MyCoverageRunner

Now I'm currently working on implementing:

@Service(Service.Level.PROJECT)
public final class MyCoverageAnnotator extends JavaCoverageAnnotator

Hopefully, I will have good news soon.

Thanks again!

0

请先登录再写评论。