Skip to content

Commit 0c4ebbf

Browse files
committed
Enhance coverage reporting with JaCoCo analysis in JSON format
- CoverageService now parses .exec files using JaCoCo API and produces structured JSON output (matching mcp-jacoco-reporter format) - Reports per-class: nocovered lines, partiallycovered lines, branch info - Analyzes all workspace Java projects for complete coverage picture - Add optional Import-Package for org.jacoco.core packages - Falls back to basic file path output when JaCoCo is unavailable
1 parent 8aca939 commit 0c4ebbf

4 files changed

Lines changed: 245 additions & 3 deletions

File tree

plugins/com.github.gradusnikov.eclipse.plugin.assistai.main/META-INF/MANIFEST.MF

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,7 @@ Import-Package: com.fasterxml.jackson.core;version="[2.21.0,3.0.0)",
118118
org.reactivestreams;version="[1.0.0,2.0.0)",
119119
reactor.core.publisher;version="[3.8.0,4.0.0)",
120120
reactor.core.scheduler;version="[3.8.0,4.0.0)",
121-
reactor.util.context;version="[3.8.0,4.0.0)"
121+
reactor.util.context;version="[3.8.0,4.0.0)",
122+
org.jacoco.core.analysis;resolution:=optional,
123+
org.jacoco.core.data;resolution:=optional,
124+
org.jacoco.core.tools;resolution:=optional

plugins/com.github.gradusnikov.eclipse.plugin.assistai.main/src/com/github/gradusnikov/eclipse/assistai/mcp/services/CoverageService.java

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
11
package com.github.gradusnikov.eclipse.assistai.mcp.services;
22

3+
import java.io.File;
4+
import java.io.FileInputStream;
35
import java.io.IOException;
46
import java.nio.file.Files;
57
import java.nio.file.Path;
8+
import java.util.ArrayList;
9+
import java.util.List;
610

11+
import org.eclipse.core.resources.IProject;
712
import org.eclipse.core.resources.ResourcesPlugin;
813
import org.eclipse.core.runtime.ILog;
914
import org.eclipse.core.runtime.Platform;
1015
import org.eclipse.e4.core.di.annotations.Creatable;
16+
import org.eclipse.jdt.core.IJavaProject;
17+
import org.eclipse.jdt.core.JavaCore;
18+
import org.jacoco.core.analysis.Analyzer;
19+
import org.jacoco.core.analysis.CoverageBuilder;
20+
import org.jacoco.core.analysis.IClassCoverage;
21+
import org.jacoco.core.analysis.ICounter;
22+
import org.jacoco.core.analysis.IPackageCoverage;
23+
import org.jacoco.core.data.ExecutionDataReader;
24+
import org.jacoco.core.data.ExecutionDataStore;
25+
import org.jacoco.core.data.SessionInfoStore;
1126

1227
import jakarta.inject.Inject;
1328
import jakarta.inject.Singleton;
@@ -72,15 +87,239 @@ public String findLatestCoverageFile()
7287
}
7388

7489
public String formatCoverageInfo( String execFilePath )
90+
{
91+
return formatCoverageInfo( execFilePath, null );
92+
}
93+
94+
public String formatCoverageInfo( String execFilePath, String projectName )
7595
{
7696
if ( execFilePath == null )
7797
{
7898
return "";
7999
}
100+
101+
if ( projectName != null )
102+
{
103+
try
104+
{
105+
return analyzeCoverage( execFilePath, projectName );
106+
}
107+
catch ( Exception e )
108+
{
109+
logger.error( "Error analyzing coverage data, falling back to basic info", e );
110+
}
111+
}
112+
80113
StringBuilder sb = new StringBuilder();
81114
sb.append( "\n--- Coverage ---\n" );
82115
sb.append( "Coverage data collected. View in Eclipse via Coverage view.\n" );
83116
sb.append( "Coverage data file: " ).append( execFilePath ).append( "\n" );
84117
return sb.toString();
85118
}
119+
120+
private String analyzeCoverage( String execFilePath, String projectName ) throws Exception
121+
{
122+
ExecutionDataStore executionDataStore = new ExecutionDataStore();
123+
SessionInfoStore sessionInfoStore = new SessionInfoStore();
124+
125+
try ( FileInputStream fis = new FileInputStream( execFilePath ) )
126+
{
127+
ExecutionDataReader reader = new ExecutionDataReader( fis );
128+
reader.setExecutionDataVisitor( executionDataStore );
129+
reader.setSessionInfoVisitor( sessionInfoStore );
130+
reader.read();
131+
}
132+
133+
CoverageBuilder coverageBuilder = new CoverageBuilder();
134+
Analyzer analyzer = new Analyzer( executionDataStore, coverageBuilder );
135+
136+
IProject[] allProjects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
137+
for ( IProject project : allProjects )
138+
{
139+
if ( !project.isOpen() || !project.hasNature( JavaCore.NATURE_ID ) )
140+
{
141+
continue;
142+
}
143+
IJavaProject javaProject = JavaCore.create( project );
144+
File outputLocation = project.getLocation().append(
145+
javaProject.getOutputLocation().removeFirstSegments( 1 ) ).toFile();
146+
if ( outputLocation.exists() )
147+
{
148+
analyzer.analyzeAll( outputLocation );
149+
}
150+
}
151+
152+
return formatReport( coverageBuilder, projectName );
153+
}
154+
155+
private String formatReport( CoverageBuilder coverageBuilder, String projectName )
156+
{
157+
StringBuilder sb = new StringBuilder();
158+
sb.append( "\n--- Coverage Report ---\n" );
159+
160+
int totalLines = 0;
161+
int coveredLines = 0;
162+
int totalBranches = 0;
163+
int coveredBranches = 0;
164+
165+
StringBuilder jsonArray = new StringBuilder();
166+
jsonArray.append( "[\n" );
167+
boolean first = true;
168+
169+
for ( IPackageCoverage pkg : coverageBuilder.getBundle( projectName ).getPackages() )
170+
{
171+
String packageName = pkg.getName().replace( '/', '.' );
172+
totalLines += pkg.getLineCounter().getTotalCount();
173+
coveredLines += pkg.getLineCounter().getCoveredCount();
174+
totalBranches += pkg.getBranchCounter().getTotalCount();
175+
coveredBranches += pkg.getBranchCounter().getCoveredCount();
176+
177+
for ( IClassCoverage cls : pkg.getClasses() )
178+
{
179+
int clsLines = cls.getLineCounter().getTotalCount();
180+
if ( clsLines == 0 ) continue;
181+
182+
List<Integer> uncoveredLines = new ArrayList<>();
183+
List<Integer> partiallyCoveredLines = new ArrayList<>();
184+
List<Integer> uncoveredBranchLines = new ArrayList<>();
185+
List<Integer> partiallyCoveredBranchLines = new ArrayList<>();
186+
187+
for ( int i = cls.getFirstLine(); i <= cls.getLastLine(); i++ )
188+
{
189+
int lineStatus = cls.getLine( i ).getStatus();
190+
if ( lineStatus == ICounter.NOT_COVERED )
191+
{
192+
uncoveredLines.add( i );
193+
}
194+
else if ( lineStatus == ICounter.PARTLY_COVERED )
195+
{
196+
partiallyCoveredLines.add( i );
197+
}
198+
199+
int branchTotal = cls.getLine( i ).getBranchCounter().getTotalCount();
200+
if ( branchTotal > 0 )
201+
{
202+
int branchStatus = cls.getLine( i ).getBranchCounter().getStatus();
203+
if ( branchStatus == ICounter.NOT_COVERED )
204+
{
205+
uncoveredBranchLines.add( i );
206+
}
207+
else if ( branchStatus == ICounter.PARTLY_COVERED )
208+
{
209+
partiallyCoveredBranchLines.add( i );
210+
}
211+
}
212+
}
213+
214+
if ( uncoveredLines.isEmpty() && partiallyCoveredLines.isEmpty()
215+
&& uncoveredBranchLines.isEmpty() && partiallyCoveredBranchLines.isEmpty() )
216+
{
217+
continue;
218+
}
219+
220+
if ( !first ) jsonArray.append( ",\n" );
221+
first = false;
222+
223+
String sourceFile = cls.getSourceFileName() != null ? cls.getSourceFileName()
224+
: cls.getName().substring( cls.getName().lastIndexOf( '/' ) + 1 ) + ".java";
225+
226+
jsonArray.append( " {\n" );
227+
jsonArray.append( " \"sourcefile\": \"" ).append( sourceFile ).append( "\",\n" );
228+
jsonArray.append( " \"package\": \"" ).append( packageName ).append( "\",\n" );
229+
jsonArray.append( " \"lines\": {\n" );
230+
jsonArray.append( " \"nocovered\": " ).append( toJsonArray( uncoveredLines ) ).append( ",\n" );
231+
jsonArray.append( " \"partiallycovered\": " ).append( toJsonArray( partiallyCoveredLines ) ).append( "\n" );
232+
jsonArray.append( " },\n" );
233+
jsonArray.append( " \"branch\": {\n" );
234+
jsonArray.append( " \"nocovered\": " ).append( toJsonArray( uncoveredBranchLines ) ).append( ",\n" );
235+
jsonArray.append( " \"partiallycovered\": " ).append( toJsonArray( partiallyCoveredBranchLines ) ).append( "\n" );
236+
jsonArray.append( " }\n" );
237+
jsonArray.append( " }" );
238+
}
239+
}
240+
241+
jsonArray.append( "\n]" );
242+
243+
if ( totalLines > 0 )
244+
{
245+
int overallLinePct = (int) ( 100.0 * coveredLines / totalLines );
246+
sb.append( "Overall: " ).append( overallLinePct ).append( "% line coverage (" )
247+
.append( coveredLines ).append( "/" ).append( totalLines ).append( " lines)" );
248+
if ( totalBranches > 0 )
249+
{
250+
int overallBranchPct = (int) ( 100.0 * coveredBranches / totalBranches );
251+
sb.append( ", " ).append( overallBranchPct ).append( "% branch coverage (" )
252+
.append( coveredBranches ).append( "/" ).append( totalBranches ).append( " branches)" );
253+
}
254+
sb.append( "\n\n" );
255+
}
256+
257+
if ( first )
258+
{
259+
sb.append( "All classes fully covered.\n" );
260+
}
261+
else
262+
{
263+
sb.append( "Classes with incomplete coverage:\n" );
264+
sb.append( jsonArray );
265+
sb.append( "\n" );
266+
}
267+
268+
return sb.toString();
269+
}
270+
271+
private String toJsonArray( List<Integer> values )
272+
{
273+
if ( values.isEmpty() ) return "[]";
274+
StringBuilder sb = new StringBuilder();
275+
sb.append( "[" );
276+
for ( int i = 0; i < values.size(); i++ )
277+
{
278+
if ( i > 0 ) sb.append( ", " );
279+
sb.append( values.get( i ) );
280+
}
281+
sb.append( "]" );
282+
return sb.toString();
283+
}
284+
285+
private String formatLineList( List<Integer> lines )
286+
{
287+
if ( lines.isEmpty() ) return "";
288+
if ( lines.size() > 30 )
289+
{
290+
return lines.size() + " lines";
291+
}
292+
StringBuilder sb = new StringBuilder();
293+
int start = lines.get( 0 );
294+
int end = start;
295+
296+
for ( int i = 1; i < lines.size(); i++ )
297+
{
298+
if ( lines.get( i ) == end + 1 )
299+
{
300+
end = lines.get( i );
301+
}
302+
else
303+
{
304+
appendRange( sb, start, end );
305+
sb.append( ", " );
306+
start = lines.get( i );
307+
end = start;
308+
}
309+
}
310+
appendRange( sb, start, end );
311+
return sb.toString();
312+
}
313+
314+
private void appendRange( StringBuilder sb, int start, int end )
315+
{
316+
if ( start == end )
317+
{
318+
sb.append( start );
319+
}
320+
else
321+
{
322+
sb.append( start ).append( "-" ).append( end );
323+
}
324+
}
86325
}

plugins/com.github.gradusnikov.eclipse.plugin.assistai.main/src/com/github/gradusnikov/eclipse/assistai/mcp/services/PDEService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ public void testCaseFinished( ITestCaseElement testCaseElement )
409409
if ( useCoverage )
410410
{
411411
String execFile = coverageService.findLatestCoverageFile();
412-
results += coverageService.formatCoverageInfo( execFile );
412+
results += coverageService.formatCoverageInfo( execFile, javaProject.getProject().getName() );
413413
}
414414

415415
return results;

plugins/com.github.gradusnikov.eclipse.plugin.assistai.main/src/com/github/gradusnikov/eclipse/assistai/mcp/services/UnitTestService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ public void testCaseFinished(ITestCaseElement testCaseElement) {
544544

545545
if (useCoverage) {
546546
String execFile = coverageService.findLatestCoverageFile();
547-
results += coverageService.formatCoverageInfo( execFile );
547+
results += coverageService.formatCoverageInfo( execFile, javaProject.getProject().getName() );
548548
}
549549

550550
return results;

0 commit comments

Comments
 (0)