How to implement coverage highlighting

Answered

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
11 comments

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

1

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

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:

INFO - #io.github.nahuel92.test.MyCoverageEngine - Calling new MyCoverageSuite(this)
2025-01-20 20:19:29,225 [  12669]   INFO - #io.github.nahuel92.test.MyCoverageSuite - Executing new MyCoverageSuite(coverageEngine)...
2025-01-20 20:19:29,225 [  12669]   INFO - #io.github.nahuel92.test.MyCoverageEngine - Calling new MyCoverageSuite(this)
2025-01-20 20:19:29,225 [  12669]   INFO - #io.github.nahuel92.test.MyCoverageSuite - Executing new MyCoverageSuite(coverageEngine)...
2025-01-20 20:19:29,225 [  12669]   INFO - #io.github.nahuel92.test.MyCoverageEngine - Calling new MyCoverageSuite(this)
2025-01-20 20:19:29,225 [  12669]   INFO - #io.github.nahuel92.test.MyCoverageSuite - Executing new MyCoverageSuite(coverageEngine)...
2025-01-20 20:19:29,225 [  12669]   INFO - #io.github.nahuel92.test.MyCoverageEngine - Calling new MyCoverageSuite(this)
2025-01-20 20:19:29,225 [  12669]   INFO - #io.github.nahuel92.test.MyCoverageSuite - Executing new MyCoverageSuite(coverageEngine)...
2025-01-20 20:19:29,225 [  12669]   INFO - #io.github.nahuel92.test.MyCoverageEngine - Calling new MyCoverageSuite(this)
2025-01-20 20:19:29,225 [  12669]   INFO - #io.github.nahuel92.test.MyCoverageSuite - Executing new MyCoverageSuite(coverageEngine)...
2025-01-20 20:19:29,225 [  12669]   INFO - #io.github.nahuel92.test.MyCoverageEngine - Calling new MyCoverageSuite(this)
2025-01-20 20:19:29,225 [  12669]   INFO - #io.github.nahuel92.test.MyCoverageSuite - Executing new MyCoverageSuite(coverageEngine)...
2025-01-20 20:19:29,226 [  12670]   INFO - #io.github.nahuel92.test.MyCoverageEngine - Calling new MyCoverageSuite(this)
2025-01-20 20:19:29,226 [  12670]   INFO - #io.github.nahuel92.test.MyCoverageSuite - Executing new MyCoverageSuite(coverageEngine)...
2025-01-20 20:19:29,226 [  12670]   INFO - #io.github.nahuel92.test.MyCoverageEngine - Calling new MyCoverageSuite(this)
2025-01-20 20:19:29,226 [  12670]   INFO - #io.github.nahuel92.test.MyCoverageSuite - Executing new MyCoverageSuite(coverageEngine)...
2025-01-20 20:19:29,226 [  12670]   INFO - #io.github.nahuel92.test.MyCoverageEngine - Calling new MyCoverageSuite(this)
2025-01-20 20:19:29,226 [  12670]   INFO - #io.github.nahuel92.test.MyCoverageSuite - Executing new MyCoverageSuite(coverageEngine)...
2025-01-20 20:19:29,226 [  12670]   INFO - #io.github.nahuel92.test.MyCoverageEngine - Calling new MyCoverageSuite(this)
2025-01-20 20:19:29,226 [  12670]   INFO - #io.github.nahuel92.test.MyCoverageSuite - Executing new MyCoverageSuite(coverageEngine)...
2025-01-20 20:19:29,226 [  12670]   INFO - #io.github.nahuel92.test.MyCoverageEngine - Calling new MyCoverageSuite(this)
2025-01-20 20:19:29,226 [  12670]   INFO - #io.github.nahuel92.test.MyCoverageSuite - Executing new MyCoverageSuite(coverageEngine)...
2025-01-20 20:19:29,226 [  12670]   INFO - #io.github.nahuel92.test.MyCoverageEngine - Calling new MyCoverageSuite(long)...
2025-01-20 20:19:29,226 [  12670]   INFO - #io.github.nahuel92.test.MyCoverageSuite - Executing new MyCoverageSuite(long)...

Following is the full code I have so far (except the RunConfiguration code):

Plugin.xml

<idea-plugin>
	<depends>com.intellij.modules.java</depends>
    <depends>org.jetbrains.idea.maven</depends>
    <depends>com.intellij.gradle</depends>
    <depends>Coverage</depends>
    
    <extensions defaultExtensionNs="com.intellij">
    	<coverageRunner implementation="io.github.nahuel92.test.runner.MyCoverageRunner" order="last"/>
        <coverageEngine implementation="io.github.nahuel92.test.MyCoverageEngine" order="last"/>
    </extensions>
    
</idea-plugin>

MyRunConfiguration

public class MyRunConfiguration extends ModuleBasedConfiguration<JavaRunConfigurationModule, MyRunConfiguration> implements Disposable {
	@Override
    @Nullable
    public RunProfileState getState(
    	@NotNull Executor executor,
	    @NotNull ExecutionEnvironment environment) {
        var javaCommandLineState = 
        new JavaCommandLineState(environment) {
            private ConsoleView consoleView;

            @Override
            protected JavaParameters createJavaParameters() {
                return JavaParametersCreator.create(
                        getConfigurationModule(),
                        getProject(), 
                        // my custom params object goes here
                );
            }

            @Override
            @NotNull
            protected OSProcessHandler startProcess() throws ExecutionException {
                var osProcessHandler = super.startProcess();
                
                osProcessHandler.addProcessListener(
                	new ProcessAdapter() {
                	@Override
                	public void processTerminated(@NotNull ProcessEvent event) {
                	// ... Code that runs a process that outputs a file with coverage data
                	var path = Path.of("my-file");
                	File file = getMyCustomReportFile("my-coverageFile.xml");
                	var coverageDataManager = CoverageDataManager.getInstance(getProject());
                	
                	coverageDataManager.addExternalCoverageSuite(
                		file,
                		new MyCoverageRunner()
                	);
                	
                	coverageDataManager.triggerPresentationUpdate();
                	}
                	}
                );
                return osProcessHandler;
            }

            @Override
            @NotNull
            public ExecutionResult execute(@NotNull Executor executor,
                                           @NotNull ProgramRunner<?> runner) throws ExecutionException {
                var processHandler = startProcess();
                var console = createConsole(executor);
                if (console != null) {
                    console.attachToProcess(processHandler);
                }
                this.consoleView = console;
                return new DefaultExecutionResult(
                        console,
                        processHandler,
                        createActions(console, processHandler, executor)
                );
            }
        };

        javaCommandLineState.setConsoleBuilder(
                TextConsoleBuilderFactory.getInstance().createBuilder(getProject())
        );
        return javaCommandLineState;
    }
}

MyCustomRunner

public class MyCoverageRunner extends CoverageRunner {
    private static final Logger LOG = Logger.getInstance(MyCoverageRunner.class);

    @Override
    @Nullable
    public ProjectData loadCoverageData(@NotNull File sessionDataFile,
                                        @Nullable CoverageSuite baseCoverageSuite) {
        throw new UnsupportedOperationException("Should not be called");
    }

    public XMLProjectData loadCoverageData(File xmlFile) {
        LOG.info("Executing loadCoverageData(xmlFile)...");
        try {
            return new XMLCoverageReport().read(new FileInputStream(xmlFile));
        } catch (IOException e){
            LOG.error(e);
        }
        return null;
    }

    @Override
    @NotNull
    @NonNls
    public String getPresentableName() {
        return "Any presentable name";
    }

    @Override
    @NotNull
    @NonNls
    public String getId() {
        return "io.github.nahuel92.test.MyCoverageEngine";
    }

    @Override
    @NotNull
    @NonNls
    public String getDataFileExtension() {
        return "xml";
    }

    @Override
    public boolean acceptsCoverageEngine(@NotNull CoverageEngine coverageEngine) {
        return coverageEngine instanceof MyCoverageEngine;
    }
}

MyCoverageEngine

public class MyCoverageEngine extends CoverageEngine {
    private static final Logger LOG = Logger.getInstance(MyCoverageEngine.class);

    @Override
    @Nullable
    public CoverageSuite createCoverageSuite(
    @NotNull CoverageRunner runner,
    @NotNull String name,
    @NotNull CoverageFileProvider fileProvider,
    @Nullable String[] filters,
    long lastCoverageTimeStamp,
    @Nullable String suiteToMerge,
    boolean coverageByTestEnabled,
    boolean branchCoverage,
    boolean trackTestFolders,
    Project project) {
        if (!(runner instanceof MyCoverageRunner r)) {
            return null;
        }
        LOG.info("Calling new MyCoverageSuite(long)...");
        return new MyCoverageSuite(name, project, r, fileProvider, lastCoverageTimeStamp, this);
    }

    @Override
    @Nullable
    public CoverageSuite createCoverageSuite(@NotNull CoverageRunner covRunner,
    @NotNull String name,
    @NotNull CoverageFileProvider coverageDataFileProvider,
    @NotNull CoverageEnabledConfiguration config) {
        throw new UnsupportedOperationException("Should not be called");
    }

    @Override
    @Nullable
    public MyCoverageSuite createEmptyCoverageSuite(@NotNull CoverageRunner coverageRunner) {
        if (!(coverageRunner instanceof MyCoverageRunner)) {
            return null;
        }
        LOG.info("Calling new MyCoverageSuite(this)");
        return new MyCoverageSuite(this);
    }

    @Override
    public boolean coverageEditorHighlightingApplicableTo(@NotNull PsiFile psiFile) {
        return psiFile instanceof PsiClassOwner;
    }

    @Override
    public boolean acceptedByFilters(@NotNull PsiFile psiFile, @NotNull CoverageSuitesBundle suite) {
        LOG.info("Executing acceptedByFilters...");
        Map.Entry<String, String> entry = packageAndFileName();
        if (entry == null) {
            return false;
        }

        LOG.info("Looping through suites...");
        for (CoverageSuite xmlSuite : suite.getSuites()) {
            if (!(xmlSuite instanceof MyCoverageSuite mySuite)) {
                continue;
            }
            LOG.info("Calling mySuite.getFileInfo()...");
            if (mySuite.getFileInfo(entry.getKey(), entry.getValue()) != null) {
                LOG.info("mySuite.getFileInfo() finished");
                return true;
            }
        }
        return false;
    }

    @Override
    public JavaCoverageViewExtension createCoverageViewExtension(Project project, CoverageSuitesBundle suiteBundle) {
        return new JavaCoverageViewExtension(getCoverageAnnotator(project), project, suiteBundle) {
            @Override
            protected boolean isBranchInfoAvailable(CoverageRunner coverageRunner, boolean branchCoverage) {
                LOG.info("Executing isBranchInfoAvailable()...");
                return true;
            }
        };
    }


    @Override
    @NlsActions.ActionText
    public String getPresentableText() {
        return "Test";
    }

    @Override
    public boolean isApplicableTo(@NotNull RunConfigurationBase<?> conf) {
        return false;
    }

    @Override
    @NotNull
    public Set<String> getQualifiedNames(@NotNull PsiFile sourceFile) {
        throw new UnsupportedOperationException("Should not be called");
    }

    @Override
    @NotNull
    public CoverageEnabledConfiguration createCoverageEnabledConfiguration(@NotNull RunConfigurationBase<?> conf) {
        throw new UnsupportedOperationException("Should not be called");
    }

    @Override
    @NotNull
    public MyCoverageAnnotator getCoverageAnnotator(Project project) {
        LOG.info("Executing getCoverageAnnotator...");
        return MyCoverageAnnotator.getInstance(project);
    }

    private Map.Entry<String, String> packageAndFileName() {
        if (!(this instanceof PsiClassOwner s)) {
            return null;
        }
        var packageName = s.getPackageName();
        return Map.entry(packageName, s.getName());
    }
}

MyCoverageSuite

public class MyCoverageSuite extends JavaCoverageSuite {
    private static final Logger LOG = Logger.getInstance(MyCoverageSuite.class);
    private XMLProjectData data;

    public MyCoverageSuite(@NotNull MyCoverageEngine coverageEngine) {
        super(coverageEngine);
        LOG.info("Executing new MyCoverageSuite(coverageEngine)...");
    }

    public MyCoverageSuite(@NotNull String name,
                           Project project,
                           @NotNull MyCoverageRunner runner,
                           @NotNull CoverageFileProvider fileProvider,
                           long lastCoverageTimeStamp,
                           MyCoverageEngine myCoverageEngine) {
        super(
                name,
                fileProvider,
                new String[]{},
                new String[]{},
                lastCoverageTimeStamp,
                false,
                true,
                false,
                runner,
                myCoverageEngine,
                project
        );
        LOG.info("Executing new MyCoverageSuite(long)...");
    }

    XMLProjectData getReportData() {
        LOG
        .info("Executing getReportData()...");
        if (data != null) {
            return data;
        }
        var file = new File(getCoverageDataFileName());
        if (!file.exists()) {
            return null;
        }
        LOG.info("Calling loadCoverageData()...");
        data = ((MyCoverageRunner) getRunner()).loadCoverageData(file);
        LOG.info("loadCoverageData() finished");
        return data;
    }

    public XMLProjectData.FileInfo getFileInfo(String packageName, String fileName) {
        LOG.info("Executing getFileInfo()...");
        String path = getPath(packageName, fileName);
        XMLProjectData reportData = getReportData();
        return reportData == null ? null : reportData.getFile(path);
    }

    @Override
    public ProjectData getCoverageData(CoverageDataManager coverageDataManager) {
        throw new UnsupportedOperationException("Should not be called");
    }

    @Override
    public void setCoverageData(ProjectData projectData) {
        throw new UnsupportedOperationException("Should not be called");
    }

    static String getPath(String packageName, String fileName) {
        if (packageName.isEmpty()) {
            return fileName;
        }
        return "${AnalysisUtils.fqnToInternalName(packageName)}/$fileName";
    }
}

MyCoverageAnnotator

@Service(Service.Level.PROJECT)
public class MyCoverageAnnotator extends JavaCoverageAnnotator {
    private static final Logger LOG = Logger.getInstance(MyCoverageAnnotator.class);
    private final Project project;

    MyCoverageAnnotator(Project project) {
        super(project);
        this.project = project;
    }

    public static MyCoverageAnnotator getInstance(Project project) {
        return project.getService(MyCoverageAnnotator.class);
    }

    @Override
    public Runnable createRenewRequest(@NotNull CoverageSuitesBundle suite,
                                       @NotNull CoverageDataManager dataManager) {
        LOG.info("Executing createRenewRequest()...");
        return () -> {
            annotate(suite, dataManager, new JavaCoverageInfoCollector(this));
            myStructure = new CoverageClassStructure(project, this, suite);
            Disposer.register(this, myStructure);
            dataManager.triggerPresentationUpdate();
        };
    }

    private void annotate(CoverageSuitesBundle suite, CoverageDataManager dataManager, CoverageInfoCollector collector) {
        var classCoverage = new HashMap<String, PackageAnnotator.ClassCoverageInfo>();
        var flattenPackageCoverage = new HashMap<String, PackageAnnotator.PackageCoverageInfo>();
        var flattenDirectoryCoverage = new HashMap<VirtualFile, PackageAnnotator.PackageCoverageInfo>();
        Module[] sourceRoots0 = dataManager.doInReadActionIfProjectOpen(
                () ->  ModuleManager.getInstance(suite.getProject()).getModules()
        );
        var sourceRoots1 = sourceRoots0 == null ? List.<Module>of() : Arrays.asList(sourceRoots0);
        var sourceRoots = sourceRoots1.stream()
                .map(JavaCoverageClassesAnnotator::getSourceRoots)
                .flatMap(Collection::stream)
                .collect(Collectors.toSet());

        for (var xmlSuite : suite.getSuites()) {
            if (!(xmlSuite instanceof MyCoverageSuite s)) {
                continue;
            }

            var xmlReport = s.getReportData();
            if (xmlReport == null) {
                continue;
            }
            for (var classInfo : xmlReport.getClasses()) {
                var currentCoverage = classCoverage.putIfAbsent(classInfo.name, new PackageAnnotator.ClassCoverageInfo());
                var thisSuiteCoverage = getCoverageForClass(classInfo);

                // apply delta (tbd)
                //var coverage = thisSuiteCoverage - currentCoverage;
                //currentCoverage.append(coverage);

                String packageName = StringUtil.getPackageName(classInfo.name);
                VirtualFile virtualFile = findFile(packageName, classInfo.fileName, sourceRoots);

                PackageAnnotator.PackageCoverageInfo tbd = flattenPackageCoverage.putIfAbsent(packageName, new PackageAnnotator.PackageCoverageInfo());
                //tbd.append(coverage);
                if (virtualFile != null) {
                    PackageAnnotator.PackageCoverageInfo tdb2 = flattenDirectoryCoverage.putIfAbsent(virtualFile, new PackageAnnotator.PackageCoverageInfo());
                    //tbd2.append(coverage);
                }
            }
        }

        // Include anonymous and internal classes to the containing class
        classCoverage
                .entrySet()
                .stream()
                .collect(Collectors.groupingBy(e -> AnalysisUtils.getSourceToplevelFQName(e.getKey())))
                .forEach((key, value) -> {
                    var coverage = new PackageAnnotator.ClassCoverageInfo();
                    value.forEach(r -> coverage.append(r.getValue()));
                    collector.addClass(key, coverage);
                });

        JavaCoverageClassesAnnotator.annotatePackages(flattenPackageCoverage, collector);
        JavaCoverageClassesAnnotator.annotateDirectories(flattenDirectoryCoverage, collector, sourceRoots);
    }

    private VirtualFile findFile(String packageName, String fileName, Collection<VirtualFile> sourceRoots) {
        if (fileName == null) return null;
        var path = MyCoverageSuite.getPath(packageName, fileName);
        for (var root : sourceRoots) {
            var file = root.findFileByRelativePath(path);
            if (file == null) {
                continue;
            }
            return file.getParent();
        }
        return null;
    }

    private PackageAnnotator.ClassCoverageInfo getCoverageForClass(XMLProjectData.ClassInfo classInfo) {
        var coverage = new PackageAnnotator.ClassCoverageInfo();
        coverage.totalBranchCount = classInfo.coveredBranches + classInfo.missedBranches;
        coverage.coveredBranchCount = classInfo.coveredBranches;
        coverage.totalMethodCount = classInfo.coveredMethods + classInfo.missedMethods;
        coverage.coveredMethodCount = classInfo.coveredMethods;
        if (coverage.totalMethodCount > 0) {
            coverage.totalClassCount = 1;
        } else {
            coverage.totalClassCount = 0;
        }
        if (classInfo.coveredMethods > 0) {
            coverage.coveredClassCount = 1;
        } else {
            coverage.coveredClassCount = 0;
        }
        coverage.totalLineCount = classInfo.coveredLines + classInfo.missedLines;
        coverage.fullyCoveredLineCount = classInfo.coveredLines;
        return coverage;
    }
}

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!

 

 

0

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. 

var coverageDataManager = CoverageDataManager.getInstance(getProject());
var suite = coverageDataManager.addExternalCoverageSuite(file,
                new MyCoverageRunner() // can be replaced with com.intellij.coverage.CoverageRunner#getInstance
);
// The missing part:
coverageDataManager.chooseSuitesBundle(new CoverageSuitesBundle(suite));

Also, coverageDataManager.triggerPresentationUpdate() is unnecessary as it will be called automatically by the platform code.

1

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:

/* Adding a breakpoint here and evaluating
CoverageDataManager#getSuites returns several MyCoverageSuite
instances (currently 15) */
var coverageDataManager = CoverageDataManager.getInstance(
	getProject()
);

var suite = coverageDataManager.addExternalCoverageSuite(
	file,
	CoverageRunner.getInstance(MyCoverageRunner.class)
);

coverageDataManager.chooseSuitesBundle(
	new CoverageSuitesBundle(suite)
);

However, as I mentioned in the multiline comment above,  I noticed that coverageDataManager.getSuites() returns several instances of MyCoverageSuite:

Which kind of makes sense, if you consider what the logs show:

2025-01-22 20:25:38,908 [  14730]   INFO - #io.github.nahuel92.runner.MyCoverageSuite - In MyCoverageSuite first constructor...
2025-01-22 20:25:38,908 [  14730]   INFO - #io.github.nahuel92.test.runner.MyCoverageSuite - In MyCoverageSuite first constructor...
2025-01-22 20:25:38,908 [  14730]   INFO - #io.github.nahuel92.test.runner.MyCoverageSuite - In MyCoverageSuite first constructor...
2025-01-22 20:25:38,909 [  14731]   INFO - #io.github.nahuel92.test.runner.MyCoverageSuite - In MyCoverageSuite first constructor...
2025-01-22 20:25:38,909 [  14731]   INFO - #io.github.nahuel92.test.runner.MyCoverageSuite - In MyCoverageSuite first constructor...
2025-01-22 20:25:38,909 [  14731]   INFO - #io.github.nahuel92.test.runner.MyCoverageSuite - In MyCoverageSuite first constructor...
2025-01-22 20:25:38,909 [  14731]   INFO - #io.github.nahuel92.test.runner.MyCoverageSuite - In MyCoverageSuite first constructor...
2025-01-22 20:25:38,909 [  14731]   INFO - #io.github.nahuel92.test.runner.MyCoverageSuite - In MyCoverageSuite first constructor...
2025-01-22 20:25:38,909 [  14731]   INFO - #io.github.nahuel92.test.runner.MyCoverageSuite - In MyCoverageSuite first constructor...
2025-01-22 20:25:38,909 [  14731]   INFO - #io.github.nahuel92.test.runner.MyCoverageSuite - In MyCoverageSuite first constructor...
2025-01-22 20:25:38,909 [  14731]   INFO - #io.github.nahuel92.test.runner.MyCoverageSuite - In MyCoverageSuite first constructor...
2025-01-22 20:25:38,909 [  14731]   INFO - #io.github.nahuel92.test.runner.MyCoverageSuite - In MyCoverageSuite first constructor...
2025-01-22 20:25:38,909 [  14731]   INFO - #io.github.nahuel92.test.runner.MyCoverageSuite - In MyCoverageSuite first constructor...
2025-01-22 20:25:38,909 [  14731]   INFO - #io.github.nahuel92.test.runner.MyCoverageSuite - In MyCoverageSuite second constructor...

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!

 

0

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_suites

If you don't want to have several entries for your suites, you should call com.intellij.coverage.CoverageDataManager#removeCoverageSuite or com.intellij.coverage.CoverageDataManager#unregisterCoverageSuite when your suite is closed/on the opening of the new one

1

Hi 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.

0

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

1

Awesome, now I have everything I need to implement what I need for my plugin.

Thanks a lot Maksim for your help and patience!!

0

Please sign in to leave a comment.