diff --git a/.azure-pipelines/vscode-java-test-nightly.yml b/.azure-pipelines/vscode-java-test-nightly.yml index b6d4d210..0883cfa0 100644 --- a/.azure-pipelines/vscode-java-test-nightly.yml +++ b/.azure-pipelines/vscode-java-test-nightly.yml @@ -67,12 +67,6 @@ extends: command: custom verbose: false customCommand: run build-plugin - - task: Npm@1 - displayName: npm run compile - inputs: - command: custom - verbose: false - customCommand: run compile - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@2 displayName: ESRP CodeSigning inputs: diff --git a/.azure-pipelines/vscode-java-test-rc.yml b/.azure-pipelines/vscode-java-test-rc.yml index 9c5e811e..cdcbe1d1 100644 --- a/.azure-pipelines/vscode-java-test-rc.yml +++ b/.azure-pipelines/vscode-java-test-rc.yml @@ -62,12 +62,6 @@ extends: command: custom verbose: false customCommand: run build-plugin - - task: Npm@1 - displayName: npm run compile - inputs: - command: custom - verbose: false - customCommand: run compile - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@2 displayName: ESRP CodeSigning inputs: diff --git a/.gitignore b/.gitignore index 1e62f707..afc3a180 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,4 @@ node_modules resources/templates/css/** resources/templates/js/** resources/templates/fonts/** -dist -**/vscode.d.ts -**/vscode.proposed.d.ts \ No newline at end of file +dist \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 647d5799..02210eba 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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", } \ No newline at end of file diff --git a/.vscodeignore b/.vscodeignore index e64e5a26..1ef8a2ad 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -21,6 +21,7 @@ extension.bundle.ts javaConfig.json .github/** .azure-pipelines/** +vscode.d.ts # Ignore output of code sign server/*.md diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 20b9dedf..7458b648 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -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 ========================================= @@ -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) diff --git a/java-extension/com.microsoft.java.test.plugin.test/projects/coverage-test/.classpath b/java-extension/com.microsoft.java.test.plugin.test/projects/coverage-test/.classpath new file mode 100644 index 00000000..df66b20f --- /dev/null +++ b/java-extension/com.microsoft.java.test.plugin.test/projects/coverage-test/.classpath @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java-extension/com.microsoft.java.test.plugin.test/projects/coverage-test/.project b/java-extension/com.microsoft.java.test.plugin.test/projects/coverage-test/.project new file mode 100644 index 00000000..ef32fdce --- /dev/null +++ b/java-extension/com.microsoft.java.test.plugin.test/projects/coverage-test/.project @@ -0,0 +1,34 @@ + + + coverage-test + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + + + 1709104275722 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + + diff --git a/java-extension/com.microsoft.java.test.plugin.test/projects/coverage-test/jacoco.exec b/java-extension/com.microsoft.java.test.plugin.test/projects/coverage-test/jacoco.exec new file mode 100644 index 00000000..24af8ac9 Binary files /dev/null and b/java-extension/com.microsoft.java.test.plugin.test/projects/coverage-test/jacoco.exec differ diff --git a/java-extension/com.microsoft.java.test.plugin.test/projects/coverage-test/pom.xml b/java-extension/com.microsoft.java.test.plugin.test/projects/coverage-test/pom.xml new file mode 100644 index 00000000..39fb6783 --- /dev/null +++ b/java-extension/com.microsoft.java.test.plugin.test/projects/coverage-test/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + com.example + coverage-test + 1.0-SNAPSHOT + + + UTF-8 + 17 + ${maven.compiler.source} + + + + + + org.junit + junit-bom + 5.7.1 + pom + import + + + + + + + org.junit.jupiter + junit-jupiter + test + + + + + + + maven-compiler-plugin + 3.8.1 + + + maven-surefire-plugin + 2.22.2 + + + + + diff --git a/java-extension/com.microsoft.java.test.plugin.test/projects/coverage-test/src/main/java/com/example/project/Sample.java b/java-extension/com.microsoft.java.test.plugin.test/projects/coverage-test/src/main/java/com/example/project/Sample.java new file mode 100644 index 00000000..8424ead9 --- /dev/null +++ b/java-extension/com.microsoft.java.test.plugin.test/projects/coverage-test/src/main/java/com/example/project/Sample.java @@ -0,0 +1,54 @@ +package com.example.project; + +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +public class Sample { + + void sample(boolean x) { + Foo foo = getFoo(); + if (x) { + foo = new Foo(); // <-- BOTH THIS LINE + } + Bar bar = new Bar(); + Baz baz = new Baz(); + baz.from(bar::bar) + .as(Sample::toArray) // <-- AND THIS LINE + .to(foo::foo); + } + + static String[] toArray(List s) { + return s.toArray(String[]::new); + } + + static Foo getFoo() { + return new Foo(); + } + + static class Foo { + void foo(String... foo) { + } + } + + static class Bar { + List bar() { + return List.of("bar"); + } + } + + static class Baz { + Baz from(Supplier> from) { + return this; + } + + Baz as(Function, String[]> as) { + return this; + } + + Baz to(Consumer to) { + return this; + } + } +} diff --git a/java-extension/com.microsoft.java.test.plugin.test/projects/coverage-test/src/test/java/com/example/project/SampleTests.java b/java-extension/com.microsoft.java.test.plugin.test/projects/coverage-test/src/test/java/com/example/project/SampleTests.java new file mode 100644 index 00000000..09c53088 --- /dev/null +++ b/java-extension/com.microsoft.java.test.plugin.test/projects/coverage-test/src/test/java/com/example/project/SampleTests.java @@ -0,0 +1,13 @@ +package com.example.project; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SampleTests { + + @Test + void test() { + assertTrue(true); + } +} diff --git a/java-extension/com.microsoft.java.test.plugin.test/src/com/microsoft/java/test/plugin/coverage/CoverageHandlerTest.java b/java-extension/com.microsoft.java.test.plugin.test/src/com/microsoft/java/test/plugin/coverage/CoverageHandlerTest.java new file mode 100644 index 00000000..00b063e6 --- /dev/null +++ b/java-extension/com.microsoft.java.test.plugin.test/src/com/microsoft/java/test/plugin/coverage/CoverageHandlerTest.java @@ -0,0 +1,53 @@ +/******************************************************************************* +* Copyright (c) 2024 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.AbstractProjectsManagerBasedTest; +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 org.eclipse.core.resources.IProject; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.ls.core.internal.ProjectUtils; +import org.eclipse.jdt.ls.core.internal.managers.ProjectsManager; +import org.junit.Test; + +import java.io.File; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertTrue; + +public class CoverageHandlerTest extends AbstractProjectsManagerBasedTest { + + @Test + public void testGetCoverageDetail() throws Exception { + importProjects(Collections.singleton("coverage-test")); + final IJavaProject javaProject = ProjectUtils.getJavaProject("coverage-test"); + final String basePath = new File("projects/coverage-test").getAbsolutePath(); + final CoverageHandler coverageHandler = new CoverageHandler(javaProject, basePath); + final List coverageDetail = coverageHandler.getCoverageDetail(new NullProgressMonitor()); + for (final SourceFileCoverage fileCoverage : coverageDetail) { + for (final LineCoverage lineCoverage : fileCoverage.getLineCoverages()) { + assertTrue(lineCoverage.getLineNumber() > 0); + } + + for (final MethodCoverage methodCoverage : fileCoverage.getMethodCoverages()) { + assertTrue(methodCoverage.getLineNumber() > 0); + } + } + } + +} diff --git a/java-extension/com.microsoft.java.test.plugin/META-INF/MANIFEST.MF b/java-extension/com.microsoft.java.test.plugin/META-INF/MANIFEST.MF index bce8d61c..491a9225 100644 --- a/java-extension/com.microsoft.java.test.plugin/META-INF/MANIFEST.MF +++ b/java-extension/com.microsoft.java.test.plugin/META-INF/MANIFEST.MF @@ -39,7 +39,11 @@ 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" + com.microsoft.java.test.plugin.model;x-friends:="com.microsoft.java.test.plugin.test", + com.microsoft.java.test.plugin.coverage;x-friends:="com.microsoft.java.test.plugin.test", + com.microsoft.java.test.plugin.coverage.model;x-friends:="com.microsoft.java.test.plugin.test" Bundle-ClassPath: . diff --git a/java-extension/com.microsoft.java.test.plugin/plugin.xml b/java-extension/com.microsoft.java.test.plugin/plugin.xml index 7ec8a6ef..246edb61 100644 --- a/java-extension/com.microsoft.java.test.plugin/plugin.xml +++ b/java-extension/com.microsoft.java.test.plugin/plugin.xml @@ -13,6 +13,7 @@ + diff --git a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/coverage/CoverageHandler.java b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/coverage/CoverageHandler.java new file mode 100644 index 00000000..aefb55de --- /dev/null +++ b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/coverage/CoverageHandler.java @@ -0,0 +1,270 @@ +/******************************************************************************* +* 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.apache.commons.lang3.StringUtils; +import org.eclipse.core.resources.IProject; +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.core.Signature; +import org.eclipse.jdt.internal.core.ClasspathEntry; +import org.eclipse.jdt.ls.core.internal.ProjectUtils; +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.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +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 getCoverageDetail(IProgressMonitor monitor) throws JavaModelException, IOException { + if (ProjectsManager.DEFAULT_PROJECT_NAME.equals(javaProject.getProject().getName())) { + return Collections.emptyList(); + } + final List coverage = new LinkedList<>(); + final List javaProjects = new LinkedList<>(); + javaProjects.addAll(getAllJavaProjects(javaProject)); + final Map> outputToSourcePaths = getOutputToSourcePathsMapping(javaProjects); + + final File executionDataFile = reportBasePath.resolve(JACOCO_EXEC).toFile(); + final ExecFileLoader execFileLoader = new ExecFileLoader(); + execFileLoader.load(executionDataFile); + for (final Map.Entry> entry : outputToSourcePaths.entrySet()) { + final CoverageBuilder coverageBuilder = new CoverageBuilder(); + final Analyzer analyzer = new Analyzer( + execFileLoader.getExecutionDataStore(), coverageBuilder); + final File outputDirectory = entry.getKey().toFile(); + if (!outputDirectory.exists()) { + continue; + } + analyzer.analyzeAll(outputDirectory); + final Map> 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 lineCoverages = getLineCoverages(sourceFileCoverage); + final String sourcePath = sourceFileCoverage.getPackageName() + "/" + + sourceFileCoverage.getName(); + final List methodCoverages = getMethodCoverages( + classCoverageBySourceFilePath.get(sourcePath), sourceFileCoverage); + coverage.add(new SourceFileCoverage(uri.toString(), lineCoverages, methodCoverages)); + } + } + return coverage; + } + + private Map> getOutputToSourcePathsMapping(List javaProjects) + throws JavaModelException { + final Map> outputToSourcePaths = new HashMap<>(); + for (final IJavaProject javaProject : javaProjects) { + for (final IClasspathEntry entry : javaProject.getRawClasspath()) { + if (entry.getEntryKind() != ClasspathEntry.CPE_SOURCE || + ProjectTestUtils.isTestEntry(entry)) { + continue; + } + + final IProject project = javaProject.getProject(); + final IPath sourceRelativePath = entry.getPath().makeRelativeTo(project.getFullPath()); + final IPath realSourcePath = project.getFolder(sourceRelativePath).getLocation(); + + IPath outputLocation = entry.getOutputLocation(); + if (outputLocation == null) { + outputLocation = javaProject.getOutputLocation(); + } + final IPath outputRelativePath = outputLocation.makeRelativeTo(javaProject.getProject().getFullPath()); + final IPath realOutputPath = project.getFolder(outputRelativePath).getLocation(); + + outputToSourcePaths.computeIfAbsent(realOutputPath, k -> new LinkedList<>()) + .add(realSourcePath); + } + } + return outputToSourcePaths; + } + + private Map> groupClassCoverageBySourceFilePath( + final Collection classCoverages) { + final Map> 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 null. + */ + private File getSourceFile(List 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 File sourceFile = sourceRoot.append(sourceRelativePath).toFile(); + if (sourceFile.exists()) { + return sourceFile; + } + } + return null; + } + + private List getLineCoverages(final ISourceFileCoverage sourceFileCoverage) { + final List lineCoverages = new LinkedList<>(); + final int last = sourceFileCoverage.getLastLine(); + final int first = sourceFileCoverage.getFirstLine(); + if (first == ISourceNode.UNKNOWN_LINE || last == ISourceNode.UNKNOWN_LINE) { + return lineCoverages; + } + for (int nr = first; nr <= last; nr++) { + final ILine line = sourceFileCoverage.getLine(nr); + if (line.getStatus() != ICounter.EMPTY) { + final List 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 getMethodCoverages(final Collection classCoverages, + final ISourceFileCoverage sourceFileCoverage) { + if (classCoverages == null || classCoverages.isEmpty()) { + return Collections.emptyList(); + } + final List methodCoverages = new LinkedList<>(); + for (final IClassCoverage classCoverage : classCoverages) { + for (final IMethodCoverage methodCoverage : classCoverage.getMethods()) { + if (methodCoverage.getFirstLine() == ISourceNode.UNKNOWN_LINE) { + continue; + } + methodCoverages.add(new MethodCoverage( + methodCoverage.getFirstLine(), + methodCoverage.getMethodCounter().getCoveredCount() > 0 ? 1 : 0, + getMethodName(methodCoverage) + )); + } + } + return methodCoverages; + } + + private String getMethodName(IMethodCoverage methodCoverage) { + final String methodName = methodCoverage.getName(); + if ("".equals(methodName) || "".equals(methodName)) { + return methodName; + } + final String signature = methodCoverage.getDesc(); + if (StringUtils.isBlank(signature)) { + return methodName; + } + + try { + final String[] parameterTypes = Signature.getParameterTypes(signature); + final List parameterNames = new LinkedList<>(); + if (parameterTypes.length > 0) { + for (final String parameterType : parameterTypes) { + final String simpleName = Signature.getSignatureSimpleName(parameterType.replace("/", ".")); + parameterNames.add(simpleName); + } + } + return String.format("%s(%s)", methodName, String.join(", ", parameterNames)); + } catch (IllegalArgumentException e) { + return methodName; + } + } + + /** + * Returns a collection containing all Java projects that are required by the given {@link IJavaProject} + * and the input java project as well. + */ + private static Collection getAllJavaProjects(IJavaProject javaProject) + throws JavaModelException { + final Set result = new LinkedHashSet<>(); + final List toScan = new LinkedList<>(); + toScan.add(javaProject); + while (!toScan.isEmpty()) { + final IJavaProject currentProject = toScan.remove(0); + if (result.contains(currentProject)) { + continue; + } + result.add(currentProject); + for (final String projectName : currentProject.getRequiredProjectNames()) { + final IJavaProject requiredProject = ProjectUtils.getJavaProject(projectName); + if (requiredProject != null) { + toScan.add(requiredProject); + } + } + } + return result; + } +} diff --git a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/coverage/model/BranchCoverage.java b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/coverage/model/BranchCoverage.java new file mode 100644 index 00000000..f0a6ca61 --- /dev/null +++ b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/coverage/model/BranchCoverage.java @@ -0,0 +1,28 @@ +/******************************************************************************* +* 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; + } + + public int getHit() { + return hit; + } + + public void setHit(int hit) { + this.hit = hit; + } +} diff --git a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/coverage/model/LineCoverage.java b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/coverage/model/LineCoverage.java new file mode 100644 index 00000000..fce6f361 --- /dev/null +++ b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/coverage/model/LineCoverage.java @@ -0,0 +1,51 @@ +/******************************************************************************* +* 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 branchCoverages; + + public LineCoverage(int lineNumber, int hit, List branchCoverages) { + this.lineNumber = lineNumber; + this.hit = hit; + this.branchCoverages = branchCoverages; + } + + public int getLineNumber() { + return lineNumber; + } + + public void setLineNumber(int lineNumber) { + this.lineNumber = lineNumber; + } + + public int getHit() { + return hit; + } + + public void setHit(int hit) { + this.hit = hit; + } + + public List getBranchCoverages() { + return branchCoverages; + } + + public void setBranchCoverages(List branchCoverages) { + this.branchCoverages = branchCoverages; + } + +} diff --git a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/coverage/model/MethodCoverage.java b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/coverage/model/MethodCoverage.java new file mode 100644 index 00000000..9686e561 --- /dev/null +++ b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/coverage/model/MethodCoverage.java @@ -0,0 +1,49 @@ +/******************************************************************************* +* 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; + } + + public int getLineNumber() { + return lineNumber; + } + + public void setLineNumber(int lineNumber) { + this.lineNumber = lineNumber; + } + + public int getHit() { + return hit; + } + + public void setHit(int hit) { + this.hit = hit; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/coverage/model/SourceFileCoverage.java b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/coverage/model/SourceFileCoverage.java new file mode 100644 index 00000000..a9dae3ca --- /dev/null +++ b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/coverage/model/SourceFileCoverage.java @@ -0,0 +1,51 @@ +/******************************************************************************* +* 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 lineCoverages; + List methodCoverages; + + public SourceFileCoverage(String uriString, List lineCoverages, + List methodCoverages) { + this.uriString = uriString; + this.lineCoverages = lineCoverages; + this.methodCoverages = methodCoverages; + } + + public String getUriString() { + return uriString; + } + + public void setUriString(String uriString) { + this.uriString = uriString; + } + + public List getLineCoverages() { + return lineCoverages; + } + + public void setLineCoverages(List lineCoverages) { + this.lineCoverages = lineCoverages; + } + + public List getMethodCoverages() { + return methodCoverages; + } + + public void setMethodCoverages(List methodCoverages) { + this.methodCoverages = methodCoverages; + } +} diff --git a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/handler/TestDelegateCommandHandler.java b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/handler/TestDelegateCommandHandler.java index 4be392c1..3b726cef 100644 --- a/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/handler/TestDelegateCommandHandler.java +++ b/java-extension/com.microsoft.java.test.plugin/src/main/java/com/microsoft/java/test/plugin/handler/TestDelegateCommandHandler.java @@ -11,7 +11,9 @@ package com.microsoft.java.test.plugin.handler; +import com.microsoft.java.test.plugin.coverage.CoverageHandler; import com.microsoft.java.test.plugin.launchers.JUnitLaunchUtils; +import com.microsoft.java.test.plugin.util.JUnitPlugin; import com.microsoft.java.test.plugin.util.ProjectTestUtils; import com.microsoft.java.test.plugin.util.TestGenerationUtils; import com.microsoft.java.test.plugin.util.TestNavigationUtils; @@ -19,7 +21,9 @@ import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.ls.core.internal.IDelegateCommandHandler; +import org.eclipse.jdt.ls.core.internal.ProjectUtils; import java.util.List; @@ -36,6 +40,7 @@ public class TestDelegateCommandHandler implements IDelegateCommandHandler { private static final String RESOLVE_PATH = "vscode.java.test.resolvePath"; private static final String FIND_TEST_LOCATION = "vscode.java.test.findTestLocation"; private static final String NAVIGATE_TO_TEST_OR_TARGET = "vscode.java.test.navigateToTestOrTarget"; + private static final String GET_COVERAGE_DETAIL = "vscode.java.test.jacoco.getCoverageDetail"; @Override public Object executeCommand(String commandId, List arguments, IProgressMonitor monitor) throws Exception { @@ -64,6 +69,20 @@ public Object executeCommand(String commandId, List arguments, IProgress return TestSearchUtils.findTestLocation(arguments, monitor); case NAVIGATE_TO_TEST_OR_TARGET: return TestNavigationUtils.findTestOrTarget(arguments, monitor); + case GET_COVERAGE_DETAIL: + if (arguments == null || arguments.size() < 2) { + throw new IllegalArgumentException( + "The arguments for command 'vscode.java.test.jacoco.getCoverageDetail' is invalid."); + } + final String projectName = (String) arguments.get(0); + final IJavaProject javaProject = ProjectUtils.getJavaProject(projectName); + if (javaProject == null) { + JUnitPlugin.logError("Cannot find the project: " + projectName + " for coverage generation."); + return null; + } + final String reportBasePath = (String) arguments.get(1); + final CoverageHandler coverageHandler = new CoverageHandler(javaProject, reportBasePath); + return coverageHandler.getCoverageDetail(monitor); default: throw new UnsupportedOperationException( String.format("Java test plugin doesn't support the command '%s'.", commandId)); diff --git a/java-extension/com.microsoft.java.test.target/com.microsoft.java.test.tp.target b/java-extension/com.microsoft.java.test.target/com.microsoft.java.test.tp.target index daf4f146..d2b60879 100644 --- a/java-extension/com.microsoft.java.test.target/com.microsoft.java.test.tp.target +++ b/java-extension/com.microsoft.java.test.target/com.microsoft.java.test.tp.target @@ -1,10 +1,10 @@ - + - + @@ -41,7 +41,16 @@ - + + + + + + org.jacoco + org.jacoco.core + 0.8.11 + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 280d5135..e6c0a5c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "@types/mocha": "^9.1.1", "@types/node": "^16.18.13", "@types/sinon": "^10.0.13", - "@types/vscode": "1.69.0", + "@types/vscode": "1.88.0", "@typescript-eslint/eslint-plugin": "^5.60.1", "@typescript-eslint/parser": "^5.60.1", "@vscode/test-electron": "^2.3.8", @@ -39,7 +39,7 @@ "webpack-cli": "^4.10.0" }, "engines": { - "vscode": "^1.69.0" + "vscode": "^1.88.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -444,9 +444,9 @@ "dev": true }, "node_modules/@types/vscode": { - "version": "1.69.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.69.0.tgz", - "integrity": "sha512-RlzDAnGqUoo9wS6d4tthNyAdZLxOIddLiX3djMoWk29jFfSA1yJbIwr0epBYqqYarWB6s2Z+4VaZCQ80Jaa3kA==", + "version": "1.88.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.88.0.tgz", + "integrity": "sha512-rWY+Bs6j/f1lvr8jqZTyp5arRMfovdxolcqGi+//+cPDOh8SBvzXH90e7BiSXct5HJ9HGW6jATchbRTpTJpEkw==", "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { diff --git a/package.json b/package.json index 6ca7dadf..41ac643d 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ ], "aiKey": "90c182a8-8dab-45d4-bfb8-1353eb55aa7f", "engines": { - "vscode": "^1.69.0" + "vscode": "^1.88.0" }, "categories": [ "Testing" @@ -71,6 +71,7 @@ "./server/org.eclipse.jdt.junit4.runtime_1.3.0.v20220609-1843.jar", "./server/org.eclipse.jdt.junit5.runtime_1.1.100.v20220907-0450.jar", "./server/org.opentest4j_1.2.0.jar", + "./server/org.jacoco.core_0.8.11.202310140853.jar", "./server/com.microsoft.java.test.plugin-0.40.1.jar" ], "viewsWelcome": [ @@ -325,6 +326,18 @@ "type": "string", "markdownDescription": "%configuration.java.test.config.when.description%", "default": "" + }, + "coverage": { + "type": "object", + "description": "%configuration.java.test.config.coverage.description%", + "default": {}, + "properties": { + "appendResult": { + "type": "boolean", + "description": "%configuration.java.test.config.coverage.appendResult.description%", + "default": true + } + } } }, "description": "%configuration.java.test.config.description%", @@ -468,6 +481,18 @@ "type": "string", "markdownDescription": "%configuration.java.test.config.when.description%", "default": "" + }, + "coverage": { + "type": "object", + "description": "%configuration.java.test.config.coverage.description%", + "default": {}, + "properties": { + "appendResult": { + "type": "boolean", + "description": "%configuration.java.test.config.coverage.appendResult.description%", + "default": true + } + } } } }, @@ -496,7 +521,7 @@ "@types/mocha": "^9.1.1", "@types/node": "^16.18.13", "@types/sinon": "^10.0.13", - "@types/vscode": "1.69.0", + "@types/vscode": "1.88.0", "@typescript-eslint/eslint-plugin": "^5.60.1", "@typescript-eslint/parser": "^5.60.1", "@vscode/test-electron": "^2.3.8", diff --git a/package.nls.json b/package.nls.json index c2bd8c25..3bd0593f 100644 --- a/package.nls.json +++ b/package.nls.json @@ -33,6 +33,8 @@ "configuration.java.test.config.filters.tags.description": "Specify the tags to be included or excluded. \n\nTags having `!` as the prefix will be **excluded**. \n\nNote: This setting **only** takes effect when `testKind` is set to `junit`.", "configuration.java.test.config.when.description": "Specify the when clause for matching tests by to determine if the configuration should be run with.\n\nNote: `testItem =~ //` is the only supported clause currently, where `testItem` is the fully-qualified name of a test class or method. For example:\n- `testItem =~ /^com\\.company\\.package\\.test/` - a package with the name \"com.company.package.test\".\n- `testItem =~ /(?<=\\.)Test/` - a class with a name containing \"Test\".\n\nWhen launching a test that satisfies a single configuration's when clause, it will be run with that configuration. If multiple configurations are satisfied, the user will be prompted to pick which configuration to run with.\n\nWhen launching multiple tests (e.g. for a class or package), a configuration's when clause must be satisfied for **all** tests to be considered.\n\nConfigurations that do not define a when clause will match all tests.", "configuration.java.test.config.javaExec.description": "The path to java executable to use. For example: `C:\\Program Files\\jdk\\bin\\java.exe`. If unset project JDK's java executable is used.", + "configuration.java.test.config.coverage.description": "The configurations for test coverage.", + "configuration.java.test.config.coverage.appendResult.description": "Whether the coverage result is appended.", "contributes.viewsWelcome.inLightWeightMode": "No test cases are listed because the Java Language Server is currently running in [LightWeight Mode](https://aka.ms/vscode-java-lightweight). To show test cases, click on the button to switch to Standard Mode.\n[Switch to Standard Mode](command:java.server.mode.switch?%5B%22Standard%22,true%5D)", "contributes.viewsWelcome.enableTests": "Click below button to configure a test framework for your project.\n[Enable Java Tests](command:_java.test.enableTests)" } diff --git a/package.nls.zh.json b/package.nls.zh.json index 2f5c153b..44302760 100644 --- a/package.nls.zh.json +++ b/package.nls.zh.json @@ -33,6 +33,8 @@ "configuration.java.test.config.filters.tags.description": "指定要包含或排除的标记。\n\n带有`!`前缀的标记将会被**排除**。 \n\n注意:该选项**仅**会在 `testKind` 设置为 `junit` 时生效。", "configuration.java.test.config.when.description": "指定测试配置项的启用条件。\n\n注:目前仅支持`testItem =~ /<正则表达式>/`,其中,`testItem` 是测试类或方法的完全限定名。例如:\n- `testItem =~ /^com\\.company\\.package\\.test/` - 匹配包名包含`com.company.package.test`的测试。\n- `testItem =~ /(?<=\\.)Test/` - 匹配类名包含`Test`的测试。\n\n如果在执行测试时,仅有一个配置满足匹配条件,该配置将会被启用。如果多个配置项均满足,用户需要选择其中一个。\n\n当一次执行多个测试用例时(例如执行整个测试类或者包),那么配置项的匹配条件必须满足**全部**测试项才会视为匹配通过。\n\n该选项为空时,则视为匹配全部测试项。", "configuration.java.test.config.javaExec.description": "指定 Java 可执行文件。例如:`C:\\Program Files\\jdk\\bin\\java.exe`。未指定时将使用项目 JDK 执行测试。", + "configuration.java.test.config.coverage.description": "测试覆盖配置项。", + "configuration.java.test.config.coverage.appendResult.description": "是否追加测试覆盖结果。", "contributes.viewsWelcome.inLightWeightMode": "由于 Java 语言服务正运行在 [LightWeight 模式](https://aka.ms/vscode-java-lightweight)下,因此测试用例将不会展示在该视图中。如果您需要展示测试用例,可以点击下方按钮将 Java 语言服务切换至 Standard 模式。\n[切换至 Standard 模式](command:java.server.mode.switch?%5B%22Standard%22,true%5D)", "contributes.viewsWelcome.enableTests": "点击下方按钮为你的项目添加一个测试框架\n[启用 Java 测试](command:_java.test.enableTests)" } diff --git a/scripts/buildJdtlsExt.js b/scripts/buildJdtlsExt.js index 93466180..e9ba5cae 100644 --- a/scripts/buildJdtlsExt.js +++ b/scripts/buildJdtlsExt.js @@ -26,6 +26,7 @@ const bundleList = [ 'junit-platform-suite-commons', 'junit-platform-suite-engine', 'org.apiguardian.api', + 'org.jacoco.core' ]; cp.execSync(`${mvnw()} clean verify`, { cwd: serverDir, stdio: [0, 1, 2] }); copy(path.join(serverDir, 'com.microsoft.java.test.plugin/target'), path.resolve('server'), (file) => path.extname(file) === '.jar'); @@ -34,6 +35,7 @@ copy(path.join(serverDir, 'com.microsoft.java.test.plugin.site/target/repository return bundleList.some(bundleName => file.startsWith(bundleName)); }); updateVersion(); +downloadJacocoAgent(); function copy(sourceFolder, targetFolder, fileFilter) { const jars = fse.readdirSync(sourceFolder).filter(file => fileFilter(file)); @@ -73,6 +75,18 @@ function findNewRequiredJar(fileName) { return f; } +function downloadJacocoAgent() { + const version = "0.8.11"; + const jacocoAgentUrl = `https://repo1.maven.org/maven2/org/jacoco/org.jacoco.agent/${version}/org.jacoco.agent-${version}-runtime.jar`; + const jacocoAgentPath = path.resolve('server', 'jacocoagent.jar'); + if (!fs.existsSync(jacocoAgentPath)) { + cp.execSync(`curl -L ${jacocoAgentUrl} -o ${jacocoAgentPath}`); + } + if (!fs.existsSync(jacocoAgentPath)) { + throw new Error('Failed to download jacoco agent.'); + } +} + function isWin() { return /^win/.test(process.platform); } diff --git a/src/constants.ts b/src/constants.ts index ea2924f8..2023c384 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -16,6 +16,7 @@ export namespace JavaTestRunnerDelegateCommands { export const FIND_TEST_TYPES_AND_METHODS: string = 'vscode.java.test.findTestTypesAndMethods'; export const RESOLVE_PATH: string = 'vscode.java.test.resolvePath'; export const NAVIGATE_TO_TEST_OR_TARGET: string = 'vscode.java.test.navigateToTestOrTarget'; + export const GET_COVERAGE_DETAIL: string = 'vscode.java.test.jacoco.getCoverageDetail'; } export namespace JavaTestRunnerCommands { diff --git a/src/controller/testController.ts b/src/controller/testController.ts index 8c12ae69..07d3ea97 100644 --- a/src/controller/testController.ts +++ b/src/controller/testController.ts @@ -3,7 +3,7 @@ import * as _ from 'lodash'; import * as path from 'path'; -import { CancellationToken, DebugConfiguration, Disposable, FileSystemWatcher, RelativePattern, TestController, TestItem, TestRun, TestRunProfileKind, TestRunRequest, tests, TestTag, Uri, window, workspace, WorkspaceFolder } from 'vscode'; +import { CancellationToken, DebugConfiguration, Disposable, FileCoverage, FileCoverageDetail, FileSystemWatcher, RelativePattern, TestController, TestItem, TestRun, TestRunProfileKind, TestRunRequest, tests, TestTag, Uri, window, workspace, WorkspaceFolder } from 'vscode'; import { instrumentOperation, sendError, sendInfo } from 'vscode-extension-telemetry-wrapper'; import { refreshExplorer } from '../commands/testExplorerCommands'; import { IProgressReporter } from '../debugger.api'; @@ -18,6 +18,7 @@ import { loadRunConfig } from '../utils/configUtils'; import { resolveLaunchConfigurationForRunner } from '../utils/launchUtils'; import { dataCache, ITestItemData } from './testItemDataCache'; import { findDirectTestChildrenForClass, findTestPackagesAndTypes, findTestTypesAndMethods, loadJavaProjects, resolvePath, synchronizeItemsRecursively, updateItemForDocumentWithDebounce } from './utils'; +import { JavaTestCoverageProvider } from '../provider/JavaTestCoverageProvider'; export let testController: TestController | undefined; export const watchers: Disposable[] = []; @@ -33,6 +34,7 @@ export function createTestController(): void { testController.createRunProfile('Run Tests', TestRunProfileKind.Run, runHandler, true, runnableTag); testController.createRunProfile('Debug Tests', TestRunProfileKind.Debug, runHandler, true, runnableTag); + testController.createRunProfile('Run Tests with Coverage', TestRunProfileKind.Coverage, runHandler, true, runnableTag); testController.refreshHandler = () => { refreshExplorer(); @@ -139,6 +141,7 @@ async function runHandler(request: TestRunRequest, token: CancellationToken): Pr export const runTests: (request: TestRunRequest, option: IRunOption) => any = instrumentOperation('java.test.runTests', async (operationId: string, request: TestRunRequest, option: IRunOption) => { sendInfo(operationId, { isDebug: `${option.isDebug}`, + profile: request.profile?.label ?? 'UNKNOWN', }); const testItems: TestItem[] = await new Promise(async (resolve: (result: TestItem[]) => void): Promise => { @@ -164,6 +167,14 @@ export const runTests: (request: TestRunRequest, option: IRunOption) => any = in } const run: TestRun = testController!.createTestRun(request); + let coverageProvider: JavaTestCoverageProvider | undefined; + if (request.profile?.kind === TestRunProfileKind.Coverage) { + coverageProvider = new JavaTestCoverageProvider(); + request.profile.loadDetailedCoverage = (_testRun: TestRun, fileCoverage: FileCoverage, _token: CancellationToken): Promise => { + return Promise.resolve(coverageProvider!.getCoverageDetails(fileCoverage.uri)); + }; + } + try { await new Promise(async (resolve: () => void): Promise => { const token: CancellationToken = option.token ?? run.token; @@ -214,6 +225,7 @@ export const runTests: (request: TestRunRequest, option: IRunOption) => any = in testItems: items, testRun: run, workspaceFolder, + profile: request.profile, }; const runner: BaseRunner | undefined = getRunnerByContext(testContext); if (!runner) { @@ -227,13 +239,16 @@ export const runTests: (request: TestRunRequest, option: IRunOption) => any = in delegatedToDebugger = true; trackTestFrameworkVersion(testContext.kind, resolvedConfiguration.classPaths, resolvedConfiguration.modulePaths); await runner.run(resolvedConfiguration, token, option.progressReporter); - } catch(error) { + } catch (error) { window.showErrorMessage(error.message || 'Failed to run tests.'); option.progressReporter?.done(); } finally { await runner.tearDown(); } } + if (request.profile?.kind === TestRunProfileKind.Coverage) { + await coverageProvider!.provideFileCoverage(run, projectName); + } } } return resolve(); diff --git a/src/provider/JavaTestCoverageProvider.ts b/src/provider/JavaTestCoverageProvider.ts new file mode 100644 index 00000000..6f10671b --- /dev/null +++ b/src/provider/JavaTestCoverageProvider.ts @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { BranchCoverage, DeclarationCoverage, FileCoverage, FileCoverageDetail, Position, StatementCoverage, TestRun, Uri } from 'vscode'; +import { getJacocoReportBasePath } from '../utils/coverageUtils'; +import { executeJavaLanguageServerCommand } from '../utils/commandUtils'; +import { JavaTestRunnerDelegateCommands } from '../constants'; + +export class JavaTestCoverageProvider { + + private coverageDetails: Map = new Map(); + + public async provideFileCoverage(run: TestRun, projectName: string): Promise { + const sourceFileCoverages: ISourceFileCoverage[] = await executeJavaLanguageServerCommand(JavaTestRunnerDelegateCommands.GET_COVERAGE_DETAIL, + projectName, getJacocoReportBasePath(projectName)) || []; + for (const sourceFileCoverage of sourceFileCoverages) { + const uri: Uri = Uri.parse(sourceFileCoverage.uriString); + const detailedCoverage: FileCoverageDetail[] = []; + for (const lineCoverage of sourceFileCoverage.lineCoverages) { + const branchCoverages: BranchCoverage[] = []; + for (const branchCoverage of lineCoverage.branchCoverages) { + branchCoverages.push(new BranchCoverage(branchCoverage.hit, new Position(lineCoverage.lineNumber - 1, 0))); + } + const statementCoverage: StatementCoverage = new StatementCoverage(lineCoverage.hit, + new Position(lineCoverage.lineNumber - 1, 0), branchCoverages); + detailedCoverage.push(statementCoverage); + } + for (const methodCoverage of sourceFileCoverage.methodCoverages) { + const functionCoverage: DeclarationCoverage = new DeclarationCoverage(methodCoverage.name, methodCoverage.hit, + new Position(methodCoverage.lineNumber - 1, 0)); + detailedCoverage.push(functionCoverage); + } + run.addCoverage(FileCoverage.fromDetails(uri, detailedCoverage)); + this.coverageDetails.set(uri, detailedCoverage); + } + } + + public getCoverageDetails(uri: Uri): FileCoverageDetail[] { + return this.coverageDetails.get(uri) || []; + } +} + +interface ISourceFileCoverage { + uriString: string; + lineCoverages: ILineCoverage[]; + methodCoverages: IMethodCoverages[]; +} + +interface ILineCoverage { + lineNumber: number; + hit: number; + branchCoverages: IBranchCoverage[]; +} + +interface IBranchCoverage { + hit: number; +} + +interface IMethodCoverages { + lineNumber: number; + hit: number; + name: string; +} diff --git a/src/runConfigs.ts b/src/runConfigs.ts index 62e7820c..9d3fbbab 100644 --- a/src/runConfigs.ts +++ b/src/runConfigs.ts @@ -101,6 +101,19 @@ export interface IExecutionConfig { * @since 0.37.0 */ tags?: string[] + }; + + /** + * The coverage configuration. + * @since 0.41.0 + */ + coverage?: { + /** + * Whether the coverage result is appended. For Jacoco, it means the execution data + * is appended to the existing data file if it already exists. + * @since 0.41.0 + */ + appendResult?: boolean; } /** diff --git a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts index f0d71c1e..9df4b882 100644 --- a/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts +++ b/src/runners/junitRunner/JUnitRunnerResultAnalyzer.ts @@ -101,7 +101,6 @@ export class JUnitRunnerResultAnalyzer extends RunnerResultAnalyzer { } else if (data.startsWith(MessageId.TraceStart)) { this.traces = new MarkdownString(); this.traces.isTrusted = true; - this.traces.supportHtml = true; this.recordingType = RecordingType.StackTrace; } else if (data.startsWith(MessageId.TraceEnd)) { if (!this.tracingItem) { diff --git a/src/runners/testngRunner/TestNGRunnerResultAnalyzer.ts b/src/runners/testngRunner/TestNGRunnerResultAnalyzer.ts index 1e83d542..dcc0a8ee 100644 --- a/src/runners/testngRunner/TestNGRunnerResultAnalyzer.ts +++ b/src/runners/testngRunner/TestNGRunnerResultAnalyzer.ts @@ -77,7 +77,6 @@ export class TestNGRunnerResultAnalyzer extends RunnerResultAnalyzer { if (outputData.attributes.trace) { const markdownTrace: MarkdownString = new MarkdownString(); markdownTrace.isTrusted = true; - markdownTrace.supportHtml = true; for (const line of outputData.attributes.trace.split(/\r?\n/)) { this.processStackTrace(line, markdownTrace, this.currentItem, this.projectName); diff --git a/src/types.ts b/src/types.ts index e04d243a..f453411d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -import { Range, TestItem, TestRun } from 'vscode'; +import { Range, TestItem, TestRun, TestRunProfile } from 'vscode'; import * as vscode from 'vscode'; export interface IJavaTestItem { @@ -56,6 +56,7 @@ export interface IRunTestContext { testItems: TestItem[]; testRun: TestRun; workspaceFolder: vscode.WorkspaceFolder; + profile?: TestRunProfile; } export enum ProjectType { diff --git a/src/utils/coverageUtils.ts b/src/utils/coverageUtils.ts new file mode 100644 index 00000000..579ba9d6 --- /dev/null +++ b/src/utils/coverageUtils.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { extensionContext } from '../extension'; +import * as path from 'path'; + +export function getJacocoAgentPath(): string { + return extensionContext.asAbsolutePath('server/jacocoagent.jar'); +} + +export function getJacocoReportBasePath(projectName: string): string { + return path.join(extensionContext.storageUri!.fsPath, projectName, 'coverage'); +} + +export function getJacocoDataFilePath(projectName: string): string { + return path.join(getJacocoReportBasePath(projectName), 'jacoco.exec'); +} diff --git a/src/utils/launchUtils.ts b/src/utils/launchUtils.ts index 75b0533c..47f3be59 100644 --- a/src/utils/launchUtils.ts +++ b/src/utils/launchUtils.ts @@ -2,16 +2,17 @@ // Licensed under the MIT license. import * as path from 'path'; -import { DebugConfiguration, TestItem } from 'vscode'; +import * as os from 'os'; +import { DebugConfiguration, TestItem, TestRunProfileKind } from 'vscode'; import { sendError, sendInfo } from 'vscode-extension-telemetry-wrapper'; import { JavaTestRunnerDelegateCommands } from '../constants'; import { dataCache } from '../controller/testItemDataCache'; import { extensionContext } from '../extension'; import { IExecutionConfig } from '../runConfigs'; -import { BaseRunner } from '../runners/baseRunner/BaseRunner'; -import { IJUnitLaunchArguments, Response } from '../runners/baseRunner/BaseRunner'; +import { BaseRunner, IJUnitLaunchArguments, Response } from '../runners/baseRunner/BaseRunner'; import { IRunTestContext, TestKind, TestLevel } from '../types'; import { executeJavaLanguageServerCommand } from './commandUtils'; +import { getJacocoAgentPath, getJacocoDataFilePath } from './coverageUtils'; export async function resolveLaunchConfigurationForRunner(runner: BaseRunner, testContext: IRunTestContext, config?: IExecutionConfig): Promise { const launchArguments: IJUnitLaunchArguments = await getLaunchArguments(testContext); @@ -22,8 +23,9 @@ export async function resolveLaunchConfigurationForRunner(runner: BaseRunner, te launchArguments.vmArguments.push(...config.vmargs.filter(Boolean)); } + let debugConfiguration: DebugConfiguration; if (testContext.kind === TestKind.TestNG) { - return { + debugConfiguration = { name: `Launch Java Tests - ${testContext.testItems[0].label}`, type: 'java', request: 'launch', @@ -49,36 +51,49 @@ export async function resolveLaunchConfigurationForRunner(runner: BaseRunner, te postDebugTask: config?.postDebugTask, javaExec: config?.javaExec, }; + } else { + debugConfiguration = { + name: `Launch Java Tests - ${testContext.testItems[0].label}`, + type: 'java', + request: 'launch', + mainClass: launchArguments.mainClass, + projectName: launchArguments.projectName, + cwd: config && config.workingDirectory ? config.workingDirectory : launchArguments.workingDirectory, + classPaths: [ + ...config?.classPaths || [], + ...launchArguments.classpath || [], + ], + modulePaths: [ + ...config?.modulePaths || [], + ...launchArguments.modulepath || [], + ], + args: [ + ...launchArguments.programArguments, + ...(testContext.kind === TestKind.JUnit5 ? parseTags(config) : []) + ], + vmArgs: launchArguments.vmArguments, + env: config?.env, + envFile: config?.envFile, + noDebug: !testContext.isDebug, + sourcePaths: config?.sourcePaths, + preLaunchTask: config?.preLaunchTask, + postDebugTask: config?.postDebugTask, + javaExec: config?.javaExec, + }; + } + + if (testContext.profile?.kind === TestRunProfileKind.Coverage) { + let agentArg: string = `-javaagent:${getJacocoAgentPath()}=destfile=${getJacocoDataFilePath(launchArguments.projectName)}`; + if (config?.coverage?.appendResult === false) { + agentArg += ',append=false'; + } + if (os.platform() === 'win32') { + agentArg = `"${agentArg}"`; + } + (debugConfiguration.vmArgs as string[]).push(agentArg); } - return { - name: `Launch Java Tests - ${testContext.testItems[0].label}`, - type: 'java', - request: 'launch', - mainClass: launchArguments.mainClass, - projectName: launchArguments.projectName, - cwd: config && config.workingDirectory ? config.workingDirectory : launchArguments.workingDirectory, - classPaths: [ - ...config?.classPaths || [], - ...launchArguments.classpath || [], - ], - modulePaths: [ - ...config?.modulePaths || [], - ...launchArguments.modulepath || [], - ], - args: [ - ...launchArguments.programArguments, - ...(testContext.kind === TestKind.JUnit5 ? parseTags(config) : []) - ], - vmArgs: launchArguments.vmArguments, - env: config?.env, - envFile: config?.envFile, - noDebug: !testContext.isDebug, - sourcePaths: config?.sourcePaths, - preLaunchTask: config?.preLaunchTask, - postDebugTask: config?.postDebugTask, - javaExec: config?.javaExec, - }; + return debugConfiguration; } async function getLaunchArguments(testContext: IRunTestContext): Promise {