Skip to content

Commit

Permalink
Implement the test coverage feature (#1637)
Browse files Browse the repository at this point in the history
- Jacoco is used to provide test coverage Support.

Signed-off-by: Sheng Chen <[email protected]>
  • Loading branch information
jdneo committed Dec 15, 2023
1 parent e8235d9 commit 333d442
Show file tree
Hide file tree
Showing 24 changed files with 854 additions and 47 deletions.
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@
"**/archetype-resources/**",
"**/META-INF/maven/**",
"**/test/test-projects/**"
]
],
"java.checkstyle.version": "8.18",
"java.checkstyle.configuration": "${workspaceFolder}/java-extension/build-tools/src/main/resources/checkstyle/checkstyle.xml",
}
32 changes: 26 additions & 6 deletions ThirdPartyNotices.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ This project incorporates components from the projects listed below. The origina
5. Eskibear/vscode-extension-telemetry-wrapper (https://github.com/Eskibear/vscode-extension-telemetry-wrapper)
6. google/gson (https://github.com/google/gson)
7. isaacs/node-lru-cache (https://github.com/isaacs/node-lru-cache)
8. jprichardson/node-fs-extra (https://github.com/jprichardson/node-fs-extra)
9. junit-team/junit5 (https://github.com/junit-team/junit5)
10. lodash/lodash (https://github.com/lodash/lodash)
11. microsoft/vscode-languageserver-node (https://github.com/microsoft/vscode-languageserver-node)
12. ota4j-team/opentest4j (https://github.com/ota4j-team/opentest4j)
13. sindresorhus/get-port (https://github.com/sindresorhus/get-port)
8. jacoco/jacoco (https://github.com/jacoco/jacoco)
9. jprichardson/node-fs-extra (https://github.com/jprichardson/node-fs-extra)
10. junit-team/junit5 (https://github.com/junit-team/junit5)
11. lodash/lodash (https://github.com/lodash/lodash)
12. microsoft/vscode-languageserver-node (https://github.com/microsoft/vscode-languageserver-node)
13. ota4j-team/opentest4j (https://github.com/ota4j-team/opentest4j)
14. sindresorhus/get-port (https://github.com/sindresorhus/get-port)

%% Apache Commons Lang NOTICES AND INFORMATION BEGIN HERE
=========================================
Expand Down Expand Up @@ -976,6 +977,25 @@ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
=========================================
END OF isaacs/node-lru-cache NOTICES AND INFORMATION

%% jacoco/jacoco NOTICES AND INFORMATION BEGIN HERE
=========================================
License
=======

Copyright (c) 2009, 2023 Mountainminds GmbH & Co. KG and Contributors

The JaCoCo Java Code Coverage Library and all included documentation is made
available by Mountainminds GmbH & Co. KG, Munich. Except indicated below, the
Content is provided to you under the terms and conditions of the Eclipse Public
License Version 2.0 ("EPL"). A copy of the EPL is available at
[https://www.eclipse.org/legal/epl-2.0/](https://www.eclipse.org/legal/epl-2.0/).

Please visit
[http://www.jacoco.org/jacoco/trunk/doc/license.html](http://www.jacoco.org/jacoco/trunk/doc/license.html)
for the complete license information including third party licenses and trademarks.
=========================================
END OF jacoco/jacoco NOTICES AND INFORMATION

%% jprichardson/node-fs-extra NOTICES AND INFORMATION BEGIN HERE
=========================================
(The MIT License)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ Require-Bundle: org.eclipse.jdt.core,
junit-platform-suite-engine;bundle-version="1.8.1",
org.apiguardian.api;bundle-version="1.0.0",
org.apache.commons.lang3;bundle-version="3.1.0",
com.google.gson;bundle-version="2.7.0"
com.google.gson;bundle-version="2.7.0",
org.objectweb.asm;bundle-version="9.6.0",
org.jacoco.core;bundle-version="0.8.11"
Export-Package: com.microsoft.java.test.plugin.launchers;x-friends:="com.microsoft.java.test.plugin.test",
com.microsoft.java.test.plugin.model;x-friends:="com.microsoft.java.test.plugin.test"
Bundle-ClassPath: .
1 change: 1 addition & 0 deletions java-extension/com.microsoft.java.test.plugin/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<command id="vscode.java.test.resolvePath" />
<command id="vscode.java.test.findTestLocation" />
<command id="vscode.java.test.navigateToTestOrTarget" />
<command id="vscode.java.test.jacoco.getCoverageDetail" />
</delegateCommandHandler>
</extension>
</plugin>
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*******************************************************************************
* Copyright (c) 2023 Microsoft Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Microsoft Corporation - initial API and implementation
*******************************************************************************/

package com.microsoft.java.test.plugin.coverage;

import com.microsoft.java.test.plugin.coverage.model.BranchCoverage;
import com.microsoft.java.test.plugin.coverage.model.LineCoverage;
import com.microsoft.java.test.plugin.coverage.model.MethodCoverage;
import com.microsoft.java.test.plugin.coverage.model.SourceFileCoverage;
import com.microsoft.java.test.plugin.util.JUnitPlugin;
import com.microsoft.java.test.plugin.util.ProjectTestUtils;

import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.core.ClasspathEntry;
import org.eclipse.jdt.ls.core.internal.managers.ProjectsManager;
import org.jacoco.core.analysis.Analyzer;
import org.jacoco.core.analysis.CoverageBuilder;
import org.jacoco.core.analysis.IClassCoverage;
import org.jacoco.core.analysis.ICounter;
import org.jacoco.core.analysis.ILine;
import org.jacoco.core.analysis.IMethodCoverage;
import org.jacoco.core.analysis.ISourceFileCoverage;
import org.jacoco.core.analysis.ISourceNode;
import org.jacoco.core.tools.ExecFileLoader;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

public class CoverageHandler {

private IJavaProject javaProject;
private Path reportBasePath;

/**
* The Jacoco data file name
*/
private static final String JACOCO_EXEC = "jacoco.exec";

public CoverageHandler(IJavaProject javaProject, String basePath) {
this.javaProject = javaProject;
reportBasePath = Paths.get(basePath);
}

public List<SourceFileCoverage> getCoverageDetail(IProgressMonitor monitor) throws JavaModelException, IOException {
if (ProjectsManager.DEFAULT_PROJECT_NAME.equals(javaProject.getProject().getName())) {
return Collections.emptyList();
}
final List<SourceFileCoverage> coverage = new LinkedList<>();
final Map<IPath, List<IPath>> outputToSourcePaths = getOutputToSourcePathsMapping();

final File executionDataFile = reportBasePath.resolve(JACOCO_EXEC).toFile();
final ExecFileLoader execFileLoader = new ExecFileLoader();
execFileLoader.load(executionDataFile);
for (final Map.Entry<IPath, List<IPath>> entry : outputToSourcePaths.entrySet()) {
final CoverageBuilder coverageBuilder = new CoverageBuilder();
final Analyzer analyzer = new Analyzer(
execFileLoader.getExecutionDataStore(), coverageBuilder);
final File outputDirectory = getFileForFs(javaProject, entry.getKey());
analyzer.analyzeAll(outputDirectory);
final Map<String, Collection<IClassCoverage>> classCoverageBySourceFilePath =
groupClassCoverageBySourceFilePath(coverageBuilder.getClasses());
for (final ISourceFileCoverage sourceFileCoverage : coverageBuilder.getSourceFiles()) {
if (monitor.isCanceled()) {
return Collections.emptyList();
}

if (sourceFileCoverage.getFirstLine() == ISourceNode.UNKNOWN_LINE) {
JUnitPlugin.logError("Missing debug information for file: " + sourceFileCoverage.getName());
continue; // no debug information
}
final File sourceFile = getSourceFile(entry.getValue(), sourceFileCoverage);
if (sourceFile == null) {
JUnitPlugin.logError("Cannot find file: " + sourceFileCoverage.getName());
continue;
}

final URI uri = sourceFile.toURI();
final List<LineCoverage> lineCoverages = getLineCoverages(sourceFileCoverage);
final String sourcePath = sourceFileCoverage.getPackageName() + "/" +
sourceFileCoverage.getName();
final List<MethodCoverage> methodCoverages = getMethodCoverages(
classCoverageBySourceFilePath.get(sourcePath), sourceFileCoverage);
coverage.add(new SourceFileCoverage(uri.toString(), lineCoverages, methodCoverages));
}
}
return coverage;
}

private Map<IPath, List<IPath>> getOutputToSourcePathsMapping() throws JavaModelException {
final Map<IPath, List<IPath>> outputToSourcePaths = new HashMap<>();
for (final IClasspathEntry entry : javaProject.getRawClasspath()) {
if (entry.getEntryKind() != ClasspathEntry.CPE_SOURCE ||
ProjectTestUtils.isTestEntry(entry)) {
continue;
}

final IPath sourceRelativePath = entry.getPath().makeRelativeTo(javaProject.getProject().getFullPath());
IPath outputLocation = entry.getOutputLocation();
if (outputLocation == null) {
outputLocation = javaProject.getOutputLocation();
}
final IPath outputRelativePath = outputLocation.makeRelativeTo(javaProject.getProject().getFullPath());
outputToSourcePaths.computeIfAbsent(outputRelativePath, k -> new LinkedList<>()).add(sourceRelativePath);
}
return outputToSourcePaths;
}

private Map<String, Collection<IClassCoverage>> groupClassCoverageBySourceFilePath(
final Collection<IClassCoverage> classCoverages) {
final Map<String, Collection<IClassCoverage>> result = new HashMap<>();
for (final IClassCoverage classCoverage : classCoverages) {
final String key = classCoverage.getPackageName() + "/" + classCoverage.getSourceFileName();
result.computeIfAbsent(key, k -> new LinkedList<>()).add(classCoverage);
}
return result;
}

/**
* Infer the source file for the given {@link ISourceFileCoverage}. If no file found, return <code>null</code>.
*/
private File getSourceFile(List<IPath> sourceRoots, ISourceFileCoverage sourceFileCoverage) {
final String packagePath = sourceFileCoverage.getPackageName().replace(".", "/");
final IPath sourceRelativePath = new org.eclipse.core.runtime.Path(packagePath)
.append(sourceFileCoverage.getName());
for (final IPath sourceRoot : sourceRoots) {
final IPath relativePath = sourceRoot.append(sourceRelativePath);
final File sourceFile = getFileForFs(javaProject, relativePath);
if (sourceFile.exists()) {
return sourceFile;
}
}
return null;
}

private static File getFileForFs(IJavaProject javaProject, IPath path) {
return javaProject.getProject().getLocation().append(path).toFile();
}

private List<LineCoverage> getLineCoverages(final ISourceFileCoverage sourceFileCoverage) {
final List<LineCoverage> lineCoverages = new LinkedList<>();
final int last = sourceFileCoverage.getLastLine();
for (int nr = sourceFileCoverage.getFirstLine(); nr <= last; nr++) {
final ILine line = sourceFileCoverage.getLine(nr);
if (line.getStatus() != ICounter.EMPTY) {
final List<BranchCoverage> branchCoverages = new LinkedList<>();
for (int i = 0; i < line.getBranchCounter().getTotalCount(); i++) {
branchCoverages.add(new BranchCoverage(
i < line.getBranchCounter().getCoveredCount() ? 1 : 0));
}
lineCoverages.add(new LineCoverage(
nr,
line.getInstructionCounter().getCoveredCount(),
branchCoverages
));
}
}
return lineCoverages;
}

private List<MethodCoverage> getMethodCoverages(final Collection<IClassCoverage> classCoverages,
final ISourceFileCoverage sourceFileCoverage) {
if (classCoverages == null || classCoverages.isEmpty()) {
return Collections.emptyList();
}
final List<MethodCoverage> methodCoverages = new LinkedList<>();
for (final IClassCoverage classCoverage : classCoverages) {
for (final IMethodCoverage methodCoverage : classCoverage.getMethods()) {
methodCoverages.add(new MethodCoverage(
methodCoverage.getFirstLine(),
methodCoverage.getMethodCounter().getCoveredCount() > 0 ? 1 : 0,
methodCoverage.getName()
));
}
}
return methodCoverages;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*******************************************************************************
* Copyright (c) 2023 Microsoft Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Microsoft Corporation - initial API and implementation
*******************************************************************************/

package com.microsoft.java.test.plugin.coverage.model;

public class BranchCoverage {
int hit;

public BranchCoverage(int hit) {
this.hit = hit;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*******************************************************************************
* Copyright (c) 2023 Microsoft Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Microsoft Corporation - initial API and implementation
*******************************************************************************/

package com.microsoft.java.test.plugin.coverage.model;

import java.util.List;

public class LineCoverage {
int lineNumber;
int hit;
List<BranchCoverage> branchCoverages;

public LineCoverage(int lineNumber, int hit, List<BranchCoverage> branchCoverages) {
this.lineNumber = lineNumber;
this.hit = hit;
this.branchCoverages = branchCoverages;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*******************************************************************************
* Copyright (c) 2023 Microsoft Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Microsoft Corporation - initial API and implementation
*******************************************************************************/

package com.microsoft.java.test.plugin.coverage.model;

public class MethodCoverage {
int lineNumber;
int hit;
String name;

public MethodCoverage(int lineNumber, int hit, String name) {
this.lineNumber = lineNumber;
this.hit = hit;
this.name = name;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*******************************************************************************
* Copyright (c) 2023 Microsoft Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Microsoft Corporation - initial API and implementation
*******************************************************************************/

package com.microsoft.java.test.plugin.coverage.model;

import java.util.List;

public class SourceFileCoverage {
String uriString;
List<LineCoverage> lineCoverages;
List<MethodCoverage> methodCoverages;

public SourceFileCoverage(String uriString, List<LineCoverage> lineCoverages,
List<MethodCoverage> methodCoverages) {
this.uriString = uriString;
this.lineCoverages = lineCoverages;
this.methodCoverages = methodCoverages;
}
}
Loading

0 comments on commit 333d442

Please sign in to comment.