Skip to content

Commit 8e8338e

Browse files
authored
Merge pull request #103 from Servoy/coverage
added coverage into the test runs (optional, also as an optional depedency)
2 parents fa452e1 + d5629d0 commit 8e8338e

13 files changed

Lines changed: 998 additions & 190 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ bin/
44
**/bin/
55
*.class
66
**/target/
7+
/opencode.json

opencode.json

Whitespace-only changes.

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/servers/EclipseIntegrationsMcpServer.java

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -216,40 +216,48 @@ public String getConsoleOutput(
216216
@Tool(name = "runAllTests", description = "Runs all JUnit tests in a specified project and returns the results. Use findTestClasses first if unsure which project contains tests. The projectName must be the test project (e.g. 'my.app.tests'), not the main source project.", type = "object")
217217
public String runAllTests(
218218
@ToolParam(name = "projectName", description = "The exact Eclipse project name containing the test classes (use listProjects to find it)", required = true) String projectName,
219-
@ToolParam(name = "timeout", description = "Maximum time in seconds to wait for test completion (default: 60)", required = false) String timeout)
219+
@ToolParam(name = "timeout", description = "Maximum time in seconds to wait for test completion (default: 60)", required = false) String timeout,
220+
@ToolParam(name = "withCoverage", description = "If 'true', runs tests with code coverage (requires EclEmma/JaCoCo installed). Coverage data file path is included in results. Default: false", required = false) String withCoverage)
220221
{
221-
return unitTestService.runAllTests(projectName, Optional.ofNullable(timeout).map(Integer::parseInt).orElse(60));
222+
boolean coverage = Optional.ofNullable(withCoverage).map(Boolean::parseBoolean).orElse(false);
223+
return unitTestService.runAllTests(projectName, Optional.ofNullable(timeout).map(Integer::parseInt).orElse(60), coverage);
222224
}
223225

224226
@Tool(name = "runPackageTests", description = "Runs all JUnit tests in a specific package and returns the results.", type = "object")
225227
public String runPackageTests(
226228
@ToolParam(name = "projectName", description = "The exact Eclipse project name containing the test classes (use listProjects to find it)", required = true) String projectName,
227229
@ToolParam(name = "packageName", description = "The fully qualified package name (e.g. 'com.example.service')", required = true) String packageName,
228-
@ToolParam(name = "timeout", description = "Maximum time in seconds to wait for test completion (default: 60)", required = false) String timeout)
230+
@ToolParam(name = "timeout", description = "Maximum time in seconds to wait for test completion (default: 60)", required = false) String timeout,
231+
@ToolParam(name = "withCoverage", description = "If 'true', runs tests with code coverage (requires EclEmma/JaCoCo installed). Default: false", required = false) String withCoverage)
229232
{
233+
boolean coverage = Optional.ofNullable(withCoverage).map(Boolean::parseBoolean).orElse(false);
230234
return unitTestService.runPackageTests(projectName, packageName,
231-
Optional.ofNullable(timeout).map(Integer::parseInt).orElse(60));
235+
Optional.ofNullable(timeout).map(Integer::parseInt).orElse(60), coverage);
232236
}
233237

234238
@Tool(name = "runClassTests", description = "Runs all JUnit tests in a specific test class and returns the results.", type = "object")
235239
public String runClassTests(
236240
@ToolParam(name = "projectName", description = "The exact Eclipse project name containing the test class (use listProjects to find it)", required = true) String projectName,
237241
@ToolParam(name = "className", description = "The fully qualified class name including package (e.g. 'com.example.MyServiceTest')", required = true) String className,
238-
@ToolParam(name = "timeout", description = "Maximum time in seconds to wait for test completion (default: 60)", required = false) String timeout)
242+
@ToolParam(name = "timeout", description = "Maximum time in seconds to wait for test completion (default: 60)", required = false) String timeout,
243+
@ToolParam(name = "withCoverage", description = "If 'true', runs tests with code coverage (requires EclEmma/JaCoCo installed). Default: false", required = false) String withCoverage)
239244
{
245+
boolean coverage = Optional.ofNullable(withCoverage).map(Boolean::parseBoolean).orElse(false);
240246
return unitTestService.runClassTests(projectName, className,
241-
Optional.ofNullable(timeout).map(Integer::parseInt).orElse(60));
247+
Optional.ofNullable(timeout).map(Integer::parseInt).orElse(60), coverage);
242248
}
243249

244250
@Tool(name = "runTestMethod", description = "Runs a single JUnit test method and returns the results.", type = "object")
245251
public String runTestMethod(
246252
@ToolParam(name = "projectName", description = "The exact Eclipse project name containing the test class (use listProjects to find it)", required = true) String projectName,
247253
@ToolParam(name = "className", description = "The fully qualified class name including package (e.g. 'com.example.MyServiceTest')", required = true) String className,
248254
@ToolParam(name = "methodName", description = "The test method name without parentheses (e.g. 'testCreate')", required = true) String methodName,
249-
@ToolParam(name = "timeout", description = "Maximum time in seconds to wait for test completion (default: 60)", required = false) String timeout)
255+
@ToolParam(name = "timeout", description = "Maximum time in seconds to wait for test completion (default: 60)", required = false) String timeout,
256+
@ToolParam(name = "withCoverage", description = "If 'true', runs tests with code coverage (requires EclEmma/JaCoCo installed). Default: false", required = false) String withCoverage)
250257
{
258+
boolean coverage = Optional.ofNullable(withCoverage).map(Boolean::parseBoolean).orElse(false);
251259
return unitTestService.runTestMethod(projectName, className, methodName,
252-
Optional.ofNullable(timeout).map(Integer::parseInt).orElse(60));
260+
Optional.ofNullable(timeout).map(Integer::parseInt).orElse(60), coverage);
253261
}
254262

255263
@Tool(name = "findTestClasses", description = "Finds all test classes in a project. Use this before runAllTests or runClassTests to discover the correct project name and fully qualified class names.", type = "object")

plugins/com.github.gradusnikov.eclipse.plugin.assistai.main/src/com/github/gradusnikov/eclipse/assistai/mcp/servers/PDEMcpServer.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,12 @@ public String reloadTarget()
4848
type = "object")
4949
public String runJUnitPluginTests(
5050
@ToolParam(name = "projectName", description = "The exact Eclipse project name containing the plug-in test classes") String projectName,
51-
@ToolParam(name = "timeout", description = "Maximum time in seconds to wait for test completion (default: 60)", required = false) String timeout)
51+
@ToolParam(name = "timeout", description = "Maximum time in seconds to wait for test completion (default: 60)", required = false) String timeout,
52+
@ToolParam(name = "withCoverage", description = "If 'true', runs tests with code coverage (requires EclEmma/JaCoCo installed). Default: false", required = false) String withCoverage)
5253
{
5354
int timeoutSeconds = Optional.ofNullable(timeout).map(Integer::parseInt).orElse(60);
54-
return pdeService.runJUnitPluginTests(projectName, timeoutSeconds);
55+
boolean coverage = Optional.ofNullable(withCoverage).map(Boolean::parseBoolean).orElse(false);
56+
return pdeService.runJUnitPluginTests(projectName, timeoutSeconds, coverage);
5557
}
5658

5759
@Tool(name = "runJUnitPluginTestClass",
@@ -60,9 +62,11 @@ public String runJUnitPluginTests(
6062
public String runJUnitPluginTestClass(
6163
@ToolParam(name = "projectName", description = "The exact Eclipse project name containing the test class") String projectName,
6264
@ToolParam(name = "className", description = "The fully qualified class name (e.g., 'com.example.MyPluginTest')") String className,
63-
@ToolParam(name = "timeout", description = "Maximum time in seconds to wait for test completion (default: 60)", required = false) String timeout)
65+
@ToolParam(name = "timeout", description = "Maximum time in seconds to wait for test completion (default: 60)", required = false) String timeout,
66+
@ToolParam(name = "withCoverage", description = "If 'true', runs tests with code coverage (requires EclEmma/JaCoCo installed). Default: false", required = false) String withCoverage)
6467
{
6568
int timeoutSeconds = Optional.ofNullable(timeout).map(Integer::parseInt).orElse(60);
66-
return pdeService.runJUnitPluginTestClass(projectName, className, timeoutSeconds);
69+
boolean coverage = Optional.ofNullable(withCoverage).map(Boolean::parseBoolean).orElse(false);
70+
return pdeService.runJUnitPluginTestClass(projectName, className, timeoutSeconds, coverage);
6771
}
6872
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
package com.github.gradusnikov.eclipse.assistai.mcp.services;
2+
3+
import java.io.File;
4+
import java.io.FileInputStream;
5+
import java.io.IOException;
6+
import java.nio.file.Files;
7+
import java.nio.file.Path;
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
11+
import org.eclipse.core.resources.IProject;
12+
import org.eclipse.core.resources.ResourcesPlugin;
13+
import org.eclipse.core.runtime.ILog;
14+
import org.eclipse.core.runtime.Platform;
15+
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;
26+
27+
import jakarta.inject.Inject;
28+
import jakarta.inject.Singleton;
29+
30+
@Creatable
31+
@Singleton
32+
public class CoverageService
33+
{
34+
private static final String ECLEMMA_CORE_BUNDLE = "org.eclipse.eclemma.core";
35+
private static final String COVERAGE_LAUNCH_MODE = "coverage";
36+
37+
@Inject
38+
private ILog logger;
39+
40+
public boolean isCoverageAvailable()
41+
{
42+
return Platform.getBundle( ECLEMMA_CORE_BUNDLE ) != null;
43+
}
44+
45+
public String getCoverageLaunchMode()
46+
{
47+
return COVERAGE_LAUNCH_MODE;
48+
}
49+
50+
public String findLatestCoverageFile()
51+
{
52+
Path basePath = ResourcesPlugin.getWorkspace().getRoot().getLocation().toFile().toPath()
53+
.resolve( ".metadata" ).resolve( ".plugins" ).resolve( ECLEMMA_CORE_BUNDLE );
54+
55+
if ( !Files.exists( basePath ) )
56+
{
57+
return null;
58+
}
59+
60+
try
61+
{
62+
var execFiles = Files.walk( basePath, 2 )
63+
.filter( p -> p.toString().endsWith( ".exec" ) )
64+
.sorted( ( a, b ) -> {
65+
try
66+
{
67+
return Long.compare( Files.getLastModifiedTime( b ).toMillis(),
68+
Files.getLastModifiedTime( a ).toMillis() );
69+
}
70+
catch ( IOException e )
71+
{
72+
return 0;
73+
}
74+
} )
75+
.toList();
76+
77+
if ( !execFiles.isEmpty() )
78+
{
79+
return execFiles.get( 0 ).toString();
80+
}
81+
}
82+
catch ( IOException e )
83+
{
84+
logger.error( "Error searching for coverage files", e );
85+
}
86+
return null;
87+
}
88+
89+
public String formatCoverageInfo( String execFilePath )
90+
{
91+
return formatCoverageInfo( execFilePath, null );
92+
}
93+
94+
public String formatCoverageInfo( String execFilePath, String projectName )
95+
{
96+
if ( execFilePath == null )
97+
{
98+
return "";
99+
}
100+
101+
if ( projectName != null )
102+
{
103+
try
104+
{
105+
return analyzeCoverage( execFilePath, projectName );
106+
}
107+
catch ( Exception e )
108+
{
109+
if ( logger != null )
110+
{
111+
try { logger.error( "Error analyzing coverage data, falling back to basic info", e ); }
112+
catch ( Exception logEx ) { /* logger not fully initialized */ }
113+
}
114+
}
115+
}
116+
117+
StringBuilder sb = new StringBuilder();
118+
sb.append( "\n--- Coverage ---\n" );
119+
sb.append( "Coverage data collected. View in Eclipse via Coverage view.\n" );
120+
sb.append( "Coverage data file: " ).append( execFilePath ).append( "\n" );
121+
return sb.toString();
122+
}
123+
124+
private String analyzeCoverage( String execFilePath, String projectName ) throws Exception
125+
{
126+
ExecutionDataStore executionDataStore = new ExecutionDataStore();
127+
SessionInfoStore sessionInfoStore = new SessionInfoStore();
128+
129+
try ( FileInputStream fis = new FileInputStream( execFilePath ) )
130+
{
131+
ExecutionDataReader reader = new ExecutionDataReader( fis );
132+
reader.setExecutionDataVisitor( executionDataStore );
133+
reader.setSessionInfoVisitor( sessionInfoStore );
134+
reader.read();
135+
}
136+
137+
CoverageBuilder coverageBuilder = new CoverageBuilder();
138+
Analyzer analyzer = new Analyzer( executionDataStore, coverageBuilder );
139+
140+
IProject[] allProjects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
141+
for ( IProject project : allProjects )
142+
{
143+
if ( !project.isOpen() || !project.hasNature( JavaCore.NATURE_ID ) )
144+
{
145+
continue;
146+
}
147+
IJavaProject javaProject = JavaCore.create( project );
148+
File outputLocation = project.getLocation().append(
149+
javaProject.getOutputLocation().removeFirstSegments( 1 ) ).toFile();
150+
if ( outputLocation.exists() )
151+
{
152+
analyzer.analyzeAll( outputLocation );
153+
}
154+
}
155+
156+
return formatReport( coverageBuilder, projectName );
157+
}
158+
159+
private String formatReport( CoverageBuilder coverageBuilder, String projectName )
160+
{
161+
StringBuilder sb = new StringBuilder();
162+
sb.append( "\n--- Coverage Report ---\n" );
163+
164+
int totalLines = 0;
165+
int coveredLines = 0;
166+
int totalBranches = 0;
167+
int coveredBranches = 0;
168+
169+
StringBuilder jsonArray = new StringBuilder();
170+
jsonArray.append( "[\n" );
171+
boolean first = true;
172+
173+
for ( IPackageCoverage pkg : coverageBuilder.getBundle( projectName ).getPackages() )
174+
{
175+
String packageName = pkg.getName().replace( '/', '.' );
176+
totalLines += pkg.getLineCounter().getTotalCount();
177+
coveredLines += pkg.getLineCounter().getCoveredCount();
178+
totalBranches += pkg.getBranchCounter().getTotalCount();
179+
coveredBranches += pkg.getBranchCounter().getCoveredCount();
180+
181+
for ( IClassCoverage cls : pkg.getClasses() )
182+
{
183+
int clsLines = cls.getLineCounter().getTotalCount();
184+
if ( clsLines == 0 ) continue;
185+
186+
List<Integer> uncoveredLines = new ArrayList<>();
187+
List<Integer> partiallyCoveredLines = new ArrayList<>();
188+
List<Integer> uncoveredBranchLines = new ArrayList<>();
189+
List<Integer> partiallyCoveredBranchLines = new ArrayList<>();
190+
191+
for ( int i = cls.getFirstLine(); i <= cls.getLastLine(); i++ )
192+
{
193+
int lineStatus = cls.getLine( i ).getStatus();
194+
if ( lineStatus == ICounter.NOT_COVERED )
195+
{
196+
uncoveredLines.add( i );
197+
}
198+
else if ( lineStatus == ICounter.PARTLY_COVERED )
199+
{
200+
partiallyCoveredLines.add( i );
201+
}
202+
203+
int branchTotal = cls.getLine( i ).getBranchCounter().getTotalCount();
204+
if ( branchTotal > 0 )
205+
{
206+
int branchStatus = cls.getLine( i ).getBranchCounter().getStatus();
207+
if ( branchStatus == ICounter.NOT_COVERED )
208+
{
209+
uncoveredBranchLines.add( i );
210+
}
211+
else if ( branchStatus == ICounter.PARTLY_COVERED )
212+
{
213+
partiallyCoveredBranchLines.add( i );
214+
}
215+
}
216+
}
217+
218+
if ( uncoveredLines.isEmpty() && partiallyCoveredLines.isEmpty()
219+
&& uncoveredBranchLines.isEmpty() && partiallyCoveredBranchLines.isEmpty() )
220+
{
221+
continue;
222+
}
223+
224+
if ( !first ) jsonArray.append( ",\n" );
225+
first = false;
226+
227+
String sourceFile = cls.getSourceFileName() != null ? cls.getSourceFileName()
228+
: cls.getName().substring( cls.getName().lastIndexOf( '/' ) + 1 ) + ".java";
229+
230+
jsonArray.append( " {\n" );
231+
jsonArray.append( " \"sourcefile\": \"" ).append( sourceFile ).append( "\",\n" );
232+
jsonArray.append( " \"package\": \"" ).append( packageName ).append( "\",\n" );
233+
jsonArray.append( " \"lines\": {\n" );
234+
jsonArray.append( " \"nocovered\": " ).append( toJsonArray( uncoveredLines ) ).append( ",\n" );
235+
jsonArray.append( " \"partiallycovered\": " ).append( toJsonArray( partiallyCoveredLines ) ).append( "\n" );
236+
jsonArray.append( " },\n" );
237+
jsonArray.append( " \"branch\": {\n" );
238+
jsonArray.append( " \"nocovered\": " ).append( toJsonArray( uncoveredBranchLines ) ).append( ",\n" );
239+
jsonArray.append( " \"partiallycovered\": " ).append( toJsonArray( partiallyCoveredBranchLines ) ).append( "\n" );
240+
jsonArray.append( " }\n" );
241+
jsonArray.append( " }" );
242+
}
243+
}
244+
245+
jsonArray.append( "\n]" );
246+
247+
if ( totalLines > 0 )
248+
{
249+
int overallLinePct = (int) ( 100.0 * coveredLines / totalLines );
250+
sb.append( "Overall: " ).append( overallLinePct ).append( "% line coverage (" )
251+
.append( coveredLines ).append( "/" ).append( totalLines ).append( " lines)" );
252+
if ( totalBranches > 0 )
253+
{
254+
int overallBranchPct = (int) ( 100.0 * coveredBranches / totalBranches );
255+
sb.append( ", " ).append( overallBranchPct ).append( "% branch coverage (" )
256+
.append( coveredBranches ).append( "/" ).append( totalBranches ).append( " branches)" );
257+
}
258+
sb.append( "\n\n" );
259+
}
260+
261+
if ( first )
262+
{
263+
sb.append( "All classes fully covered.\n" );
264+
}
265+
else
266+
{
267+
sb.append( "Classes with incomplete coverage:\n" );
268+
sb.append( jsonArray );
269+
sb.append( "\n" );
270+
}
271+
272+
return sb.toString();
273+
}
274+
275+
private String toJsonArray( List<Integer> values )
276+
{
277+
if ( values.isEmpty() ) return "[]";
278+
StringBuilder sb = new StringBuilder();
279+
sb.append( "[" );
280+
for ( int i = 0; i < values.size(); i++ )
281+
{
282+
if ( i > 0 ) sb.append( ", " );
283+
sb.append( values.get( i ) );
284+
}
285+
sb.append( "]" );
286+
return sb.toString();
287+
}
288+
}

0 commit comments

Comments
 (0)