From 40bae1af2279a038afbc2ce873ac0ffb7d5249d4 Mon Sep 17 00:00:00 2001 From: Dusan Petrovic Date: Thu, 21 Nov 2024 14:24:43 +0100 Subject: [PATCH] Action for running tests in parallel --- .../micronaut/nbproject/project.properties | 2 +- .../resources/micronaut-actions-maven.xml | 14 + extide/gradle/apichanges.xml | 12 + extide/gradle/nbproject/project.properties | 2 +- .../modules/gradle/ActionProviderImpl.java | 5 +- .../gradle/actions/declarative-actions.xml | 3 + ide/projectapi/apichanges.xml | 21 ++ ide/projectapi/manifest.mf | 2 +- .../api/project/ContainedProjectFilter.java | 65 ++++ .../netbeans/spi/project/ActionProvider.java | 7 + .../org/netbeans/spi/project/NestedClass.java | 147 ++++++++ .../netbeans/spi/project/SingleMethod.java | 65 +++- java/gradle.java/apichanges.xml | 12 + java/gradle.java/nbproject/project.properties | 2 +- java/gradle.java/nbproject/project.xml | 4 +- .../gradle/java/GradleJavaTokenProvider.java | 11 +- .../gradle/java/JavaActionProvider.java | 1 + .../gradle/java/ProjectsTokenProvider.java | 95 +++++ .../modules/gradle/java/action-mapping.xml | 3 + java/gradle.test/manifest.mf | 2 +- java/gradle.test/nbproject/project.xml | 2 +- .../test/GradleTestProgressListener.java | 78 +++- .../integration/maven-actions-override.xml | 1 - .../nbproject/project.properties | 2 +- java/java.lsp.server/nbproject/project.xml | 2 +- .../debugging/launch/NbLaunchDelegate.java | 71 +++- .../launch/NbLaunchRequestHandler.java | 4 +- .../java/lsp/server/progress/ModuleInfo.java | 46 +++ .../server/progress/TestProgressHandler.java | 76 +++- .../lsp/server/protocol/TestSuiteInfo.java | 89 ++++- .../server/protocol/WorkspaceServiceImpl.java | 35 +- .../launch/NbLaunchDelegateTest.java | 18 +- .../progress/TestProgressHandlerTest.java | 9 +- java/java.lsp.server/vscode/src/extension.ts | 61 ++- java/java.lsp.server/vscode/src/protocol.ts | 2 + .../java.lsp.server/vscode/src/testAdapter.ts | 197 +++++++++- java/java.source.base/apichanges.xml | 12 + .../nbproject/project.properties | 2 +- java/java.source.base/nbproject/project.xml | 2 +- .../netbeans/api/java/source/SourceUtils.java | 14 +- java/maven.junit/manifest.mf | 2 +- java/maven.junit/nbproject/project.xml | 2 +- .../junit/JUnitOutputListenerProvider.java | 351 +++++++++++------- java/maven/apichanges.xml | 15 + .../modules/maven/event/NbEventSpy.java | 6 + .../nbproject/org-netbeans-modules-maven.sig | 29 +- java/maven/nbproject/project.properties | 2 +- java/maven/nbproject/project.xml | 4 +- .../modules/maven/ActionProviderImpl.java | 9 +- .../modules/maven/api/execute/RunConfig.java | 22 ++ .../maven/execute/AbstractOutputHandler.java | 28 +- .../modules/maven/execute/BeanRunConfig.java | 36 +- .../execute/CommandLineOutputHandler.java | 34 +- .../execute/DefaultReplaceTokenProvider.java | 29 +- .../execute/MavenCommandLineExecutor.java | 26 +- .../execute/MavenCommandLineOptions.java | 50 +++ .../modules/maven/execute/ModelRunConfig.java | 5 + .../modules/maven/execute/cmd/ExecMojo.java | 34 ++ .../maven/execute/defaultActionMappings.xml | 17 + .../execute/model/NetbeansActionMapping.java | 43 ++- .../jdom/NetbeansBuildActionJDOMWriter.java | 1 + .../xpp3/NetbeansBuildActionXpp3Reader.java | 14 + .../xpp3/NetbeansBuildActionXpp3Writer.java | 11 + .../queries/MavenArtifactsImplementation.java | 1 + .../maven/debug/DebuggerCheckerTest.java | 19 +- 65 files changed, 1679 insertions(+), 309 deletions(-) create mode 100644 ide/projectapi/src/org/netbeans/api/project/ContainedProjectFilter.java create mode 100644 ide/projectapi/src/org/netbeans/spi/project/NestedClass.java create mode 100644 java/gradle.java/src/org/netbeans/modules/gradle/java/ProjectsTokenProvider.java create mode 100644 java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/progress/ModuleInfo.java create mode 100644 java/maven/src/org/netbeans/modules/maven/execute/MavenCommandLineOptions.java diff --git a/enterprise/micronaut/nbproject/project.properties b/enterprise/micronaut/nbproject/project.properties index f35467d14f60..38d65c0a0fa8 100644 --- a/enterprise/micronaut/nbproject/project.properties +++ b/enterprise/micronaut/nbproject/project.properties @@ -19,7 +19,7 @@ javac.source=1.8 javac.compilerargs=-Xlint -Xlint:-serial release.external/spring-boot-configuration-metadata-2.4.4.jar=modules/ext/spring-boot-configuration-metadata-2.4.4.jar release.external/android-json-0.0.20131108.vaadin1.jar=modules/ext/android-json-0.0.20131108.vaadin1.jar -spec.version.base=1.16.0 +spec.version.base=1.17.0 requires.nb.javac=true test-unit-sys-prop.test.netbeans.dest.dir=${netbeans.dest.dir} test.unit.cp.extra=${tools.jar} diff --git a/enterprise/micronaut/src/org/netbeans/modules/micronaut/resources/micronaut-actions-maven.xml b/enterprise/micronaut/src/org/netbeans/modules/micronaut/resources/micronaut-actions-maven.xml index 3b9d7ce3fbff..59bff1291680 100644 --- a/enterprise/micronaut/src/org/netbeans/modules/micronaut/resources/micronaut-actions-maven.xml +++ b/enterprise/micronaut/src/org/netbeans/modules/micronaut/resources/micronaut-actions-maven.xml @@ -35,6 +35,20 @@ native-image + + test.single + + * + + + io.micronaut.maven:micronaut-maven-plugin:start-testresources-service + process-test-classes + surefire:test + + + ${packageClassName} + + diff --git a/extide/gradle/apichanges.xml b/extide/gradle/apichanges.xml index 867fd993607c..0c66b4e8b646 100644 --- a/extide/gradle/apichanges.xml +++ b/extide/gradle/apichanges.xml @@ -83,6 +83,18 @@ is the proper place. + + + Added action for running tests in parallel + + + + + + Added action for running tests in parallel with ability to specify the projects on which + the action will be applied. + + LoadOptions object replaces growing number of argumetns to project load APIs. Access to current Lookup added. diff --git a/extide/gradle/nbproject/project.properties b/extide/gradle/nbproject/project.properties index ae784ba78a58..dafc8d6be29e 100644 --- a/extide/gradle/nbproject/project.properties +++ b/extide/gradle/nbproject/project.properties @@ -25,7 +25,7 @@ javadoc.apichanges=${basedir}/apichanges.xml nbm.module.author=Laszlo Kishalmi source.reference.netbeans-gradle-tooling.jar=netbeans-gradle-tooling/src/main/groovy -spec.version.base=2.44.0 +spec.version.base=2.45.0 test-unit-sys-prop.test.netbeans.dest.dir=${netbeans.dest.dir} test-unit-sys-prop.java.awt.headless=true diff --git a/extide/gradle/src/org/netbeans/modules/gradle/ActionProviderImpl.java b/extide/gradle/src/org/netbeans/modules/gradle/ActionProviderImpl.java index 37c98e43b348..78d36ee4dd27 100644 --- a/extide/gradle/src/org/netbeans/modules/gradle/ActionProviderImpl.java +++ b/extide/gradle/src/org/netbeans/modules/gradle/ActionProviderImpl.java @@ -26,7 +26,6 @@ import org.netbeans.modules.gradle.api.execute.RunUtils; import org.netbeans.modules.gradle.actions.ActionToTaskUtils; import org.netbeans.modules.gradle.execute.GradleExecutorOptionsPanel; -import org.netbeans.modules.gradle.spi.actions.GradleActionsProvider; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -189,6 +188,7 @@ public boolean isActionEnabled(String command, Lookup context) throws IllegalArg "# {0} - artifactId", "TXT_ApplyCodeChanges=Apply Code Changes ({0})", "# {0} - artifactId", "TXT_Profile=Profile ({0})", "# {0} - artifactId", "TXT_Test=Test ({0})", + "# {0} - artifactId", "TXT_Test_Parallel=Test In Parallel ({0})", "# {0} - artifactId", "TXT_Build=Build ({0})", "# {0} - artifactId", "TXT_Delete=Delete ({0})", }) @@ -215,6 +215,9 @@ static String taskName(Project project, String action, Lookup lkp) { case ActionProvider.COMMAND_TEST: title = TXT_Test(prjLabel); break; + case ActionProvider.COMMAND_TEST_PARALLEL: + title = TXT_Test_Parallel(prjLabel); + break; case ActionProvider.COMMAND_RUN_SINGLE: title = TXT_Run(dobjName); break; diff --git a/extide/gradle/test/unit/src/org/netbeans/modules/gradle/actions/declarative-actions.xml b/extide/gradle/test/unit/src/org/netbeans/modules/gradle/actions/declarative-actions.xml index 321cf0acb93d..5b7eaa0e5ccc 100644 --- a/extide/gradle/test/unit/src/org/netbeans/modules/gradle/actions/declarative-actions.xml +++ b/extide/gradle/test/unit/src/org/netbeans/modules/gradle/actions/declarative-actions.xml @@ -49,6 +49,9 @@ + + --parallel --rerun-tasks ${taskWithProjects} + cleanTest test --tests "${selectedClass}" diff --git a/ide/projectapi/apichanges.xml b/ide/projectapi/apichanges.xml index dec21c0f8cd0..c47f195f203c 100644 --- a/ide/projectapi/apichanges.xml +++ b/ide/projectapi/apichanges.xml @@ -83,6 +83,27 @@ is the proper place. + + + Added action for running tests in parallel + + + + + +

+ ActionProvider.COMMAND_TEST_PARALLEL was introduced in order to + allow running tests in parallel. + ContainedProjectFilter was added and + it can be used to pass list of projects the project action should apply to. + NestedClass was added in order to support + nested classes. +

+
+ + + +
Added ProjectActionContext that can pass action-like environment to project queries diff --git a/ide/projectapi/manifest.mf b/ide/projectapi/manifest.mf index 7fefbde717a1..bf89633c6a95 100644 --- a/ide/projectapi/manifest.mf +++ b/ide/projectapi/manifest.mf @@ -1,6 +1,6 @@ Manifest-Version: 1.0 OpenIDE-Module: org.netbeans.modules.projectapi/1 -OpenIDE-Module-Specification-Version: 1.98 +OpenIDE-Module-Specification-Version: 1.99 OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/projectapi/Bundle.properties OpenIDE-Module-Layer: org/netbeans/modules/projectapi/layer.xml OpenIDE-Module-Needs: org.netbeans.spi.project.ProjectManagerImplementation diff --git a/ide/projectapi/src/org/netbeans/api/project/ContainedProjectFilter.java b/ide/projectapi/src/org/netbeans/api/project/ContainedProjectFilter.java new file mode 100644 index 000000000000..2680ec2c3efa --- /dev/null +++ b/ide/projectapi/src/org/netbeans/api/project/ContainedProjectFilter.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.api.project; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * Provides list of projects the project action should apply to + * + *

+ * An action that processes multiple projects might use ContainedProjectFilter + * to operate only on a specific subset of projects. + * The use of ContainedProjectFilter is optional and determined + * by the requirements of individual actions. + * Actions employing this class must document their specific filtering logic + * and behavior. + *

+ * + * @author Dusan Petrovic + * + * @since 1.99 + */ +public final class ContainedProjectFilter { + + private final List projectsToProcess; + + private ContainedProjectFilter(List projectsToProcess) { + this.projectsToProcess = projectsToProcess; + } + + /** + * Static factory method to create an instance of ContainedProjectFilter. + * + * @param projectsToProcess the list of projects to include in the filter + * @return an Optional containing ContainedProjectFilter, or Optional.empty() if the list is null or empty + */ + public static Optional of(List projectsToProcess) { + if (projectsToProcess == null || projectsToProcess.isEmpty()) { + return Optional.empty(); + } + return Optional.of(new ContainedProjectFilter(projectsToProcess)); + } + + public List getProjectsToProcess() { + return Collections.unmodifiableList(projectsToProcess); + } +} diff --git a/ide/projectapi/src/org/netbeans/spi/project/ActionProvider.java b/ide/projectapi/src/org/netbeans/spi/project/ActionProvider.java index 65098b8435c7..106a3e1f9783 100644 --- a/ide/projectapi/src/org/netbeans/spi/project/ActionProvider.java +++ b/ide/projectapi/src/org/netbeans/spi/project/ActionProvider.java @@ -83,6 +83,13 @@ public interface ActionProvider { */ String COMMAND_TEST_SINGLE = "test.single"; // NOI18N + /** + * Standard command for running tests in parallel on given projects sub-modules + * + * @since 1.99 + */ + String COMMAND_TEST_PARALLEL = "test.parallel"; // NOI18N + /** * Standard command for running the project in debugger */ diff --git a/ide/projectapi/src/org/netbeans/spi/project/NestedClass.java b/ide/projectapi/src/org/netbeans/spi/project/NestedClass.java new file mode 100644 index 000000000000..5df90f2a7104 --- /dev/null +++ b/ide/projectapi/src/org/netbeans/spi/project/NestedClass.java @@ -0,0 +1,147 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.spi.project; + +import java.util.Objects; +import org.openide.filesystems.FileObject; + +/** + * Structure representing an identification of a nested class in a file. + * + *

+ * NestedClass can be used to represent nested classes within parent class + * Example: + * If we have following structure: ParentClass (parent-of) ChildClass1 (parent-of) ChildClass2, + * for ChildClass1 className field would contain "ChildClass1" and topLevelClassName would contain "ParentClass", + * for ChildClass2 className field would contain "ChildClass1.ChildClass2" and topLevelClassName would contain "ParentClass" + *

+ * + * @author Dusan Petrovic + * + * @since 1.99 + */ +public final class NestedClass { + + private final FileObject file; + private final String className; + private final String topLevelClassName; + + /** + * Creates a new instance holding the specified identification + * of a nested class. + * + * @param className name of a class inside the file + * @param topLevelClassName top level name of a class inside the file + * @param file file to be kept in the object + * @exception java.lang.IllegalArgumentException + * if the file or class name is {@code null} + * @since 1.99 + */ + public NestedClass(String className, String topLevelClassName, FileObject file) { + super(); + if (className == null) { + throw new IllegalArgumentException("className is "); + } + if (topLevelClassName == null) { + throw new IllegalArgumentException("topLevelClassName is "); + } + if (file == null) { + throw new IllegalArgumentException("file is "); + } + this.className = className; + this.topLevelClassName = topLevelClassName; + this.file = file; + } + + /** + * Returns the file identification. + * + * @return file held by this object + * @since 1.99 + */ + public FileObject getFile() { + return file; + } + + /** + * Returns name of a nested class within a file. + * + * @return class name held by this object + * @since 1.99 + */ + public String getClassName() { + return className; + } + + /** + * Returns name of a top level class within a file. + * + * @return top level class name held by this object + * @since 1.99 + */ + public String getTopLevelClassName() { + return topLevelClassName; + } + + /** + * Returns fully qualified name. + * + * @param packageName name of the package where the class is + * + * @return fully qualified name held by this object + * @since 1.99 + */ + public String getFQN(String packageName) { + return String.join(".", packageName, topLevelClassName, className); + } + + /** + * Returns fully qualified name. + * + * @param packageName name of the package where the class is + * @param nestedClassSeparator separator for the nested classes + * + * @return fully qualified name held by this object + * @since 1.99 + */ + public String getFQN(String packageName, String nestedClassSeparator) { + return String.join(".", packageName, String.join(nestedClassSeparator, topLevelClassName, className.replace(".", nestedClassSeparator))); + } + + @Override + public int hashCode() { + int hash = 3; + hash = 41 * hash + Objects.hashCode(this.className); + hash = 41 * hash + Objects.hashCode(this.topLevelClassName); + hash = 41 * hash + Objects.hashCode(this.file); + return hash; + } + + @Override + public boolean equals(Object obj) { + if ((obj == null) || (obj.getClass() != NestedClass.class)) { + return false; + } + if (this == obj) { + return true; + } + final NestedClass other = (NestedClass) obj; + return other.file.equals(file) && other.className.equals(className) && other.topLevelClassName.equals(topLevelClassName); + } +} diff --git a/ide/projectapi/src/org/netbeans/spi/project/SingleMethod.java b/ide/projectapi/src/org/netbeans/spi/project/SingleMethod.java index e459a28ca9eb..ca2874deb02e 100644 --- a/ide/projectapi/src/org/netbeans/spi/project/SingleMethod.java +++ b/ide/projectapi/src/org/netbeans/spi/project/SingleMethod.java @@ -19,6 +19,7 @@ package org.netbeans.spi.project; +import java.util.Objects; import org.openide.filesystems.FileObject; /** @@ -29,9 +30,26 @@ */ public final class SingleMethod { - private FileObject file; - private String methodName; + private final FileObject file; + private final String methodName; + private final NestedClass nestedClass; + /** + * Creates a new instance holding the specified identification + * of a method/function in a file. + * + * @param nestedClass nested class containing the method + * @param file file to be kept in the object + * @param methodName name of a method inside the file + * + * @since 1.99 + */ + private SingleMethod(NestedClass nestedClass, FileObject file, String methodName) { + this.methodName = methodName; + this.file = file; + this.nestedClass = nestedClass; + } + /** * Creates a new instance holding the specified identification * of a method/function in a file. @@ -43,15 +61,39 @@ public final class SingleMethod { * @since 1.19 */ public SingleMethod(FileObject file, String methodName) { - super(); - if (file == null) { - throw new IllegalArgumentException("file is "); - } - if (methodName == null) { - throw new IllegalArgumentException("methodName is "); + this(null, nonNull(file, "file"), nonNull(methodName, "methodName")); + } + + /** + * Creates a new instance holding the specified identification + * of a method/function in nested class in a file. + * + * @param methodName name of a method inside the file + * @param nestedClass nested class containing the method + * + * @exception java.lang.IllegalArgumentException + * if the nested class name is {@code null} + * @since 1.99 + */ + public SingleMethod(String methodName, NestedClass nestedClass) { + this(nonNull(nestedClass, "nestedClass"), nonNull(nestedClass.getFile(), "file"), nonNull(methodName, "methodName")); + } + + private static T nonNull(T value, String paramName) { + if (value == null) { + throw new IllegalArgumentException(paramName + " is "); } - this.file = file; - this.methodName = methodName; + return value; + } + + /** + * Returns the nested class containing the method. + * + * @return nested class containing the method + * @since 1.99 + */ + public NestedClass getNestedClass() { + return nestedClass; } /** @@ -94,7 +136,7 @@ public boolean equals(Object obj) { return false; } SingleMethod other = (SingleMethod) obj; - return other.file.equals(file) && other.methodName.equals(methodName); + return other.file.equals(file) && other.methodName.equals(methodName) && Objects.equals(other.nestedClass, nestedClass); } @Override @@ -102,6 +144,7 @@ public int hashCode() { int hash = 7; hash = 29 * hash + this.file.hashCode(); hash = 29 * hash + this.methodName.hashCode(); + hash = 29 * hash + Objects.hashCode(this.nestedClass); return hash; } } diff --git a/java/gradle.java/apichanges.xml b/java/gradle.java/apichanges.xml index 55a1019a9685..fc7e3f311fdd 100644 --- a/java/gradle.java/apichanges.xml +++ b/java/gradle.java/apichanges.xml @@ -83,6 +83,18 @@ is the proper place. + + + Added action for running tests in parallel + + + + + + Added action for running tests in parallel with ability to specify the projects on which + the action will be applied. + + Support for per-language output directories diff --git a/java/gradle.java/nbproject/project.properties b/java/gradle.java/nbproject/project.properties index fb6f49f0b16b..fd605b84f415 100644 --- a/java/gradle.java/nbproject/project.properties +++ b/java/gradle.java/nbproject/project.properties @@ -25,5 +25,5 @@ javadoc.apichanges=${basedir}/apichanges.xml test-unit-sys-prop.test.netbeans.dest.dir=${netbeans.dest.dir} test-unit-sys-prop.java.awt.headless=true test.use.jdk.javac=true -spec.version.base=1.29.0 +spec.version.base=1.30.0 diff --git a/java/gradle.java/nbproject/project.xml b/java/gradle.java/nbproject/project.xml index 0c9f70e8e166..e02a778936ad 100644 --- a/java/gradle.java/nbproject/project.xml +++ b/java/gradle.java/nbproject/project.xml @@ -171,7 +171,7 @@ - 2.73 + 2.74 @@ -188,7 +188,7 @@ 1 - 1.57.1 + 1.99 diff --git a/java/gradle.java/src/org/netbeans/modules/gradle/java/GradleJavaTokenProvider.java b/java/gradle.java/src/org/netbeans/modules/gradle/java/GradleJavaTokenProvider.java index 3e2723d802fd..84435b7fbc65 100644 --- a/java/gradle.java/src/org/netbeans/modules/gradle/java/GradleJavaTokenProvider.java +++ b/java/gradle.java/src/org/netbeans/modules/gradle/java/GradleJavaTokenProvider.java @@ -34,6 +34,7 @@ import org.netbeans.api.java.source.ClasspathInfo; import org.netbeans.api.java.source.SourceUtils; import org.netbeans.api.project.Project; +import org.netbeans.spi.project.NestedClass; import org.netbeans.spi.project.SingleMethod; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; @@ -84,8 +85,9 @@ public Map createReplacements(String action, Lookup context) { private void processSelectedPackageAndClass(final Map map, Lookup context) { FileObject fo = RunUtils.extractFileObjectfromLookup(context); + NestedClass nestedClass = context.lookup(NestedClass.class); GradleJavaProject gjp = GradleJavaProject.get(project); - String className = evaluateClassName(gjp, fo); + String className = evaluateClassName(gjp, fo, nestedClass); if (className != null) { map.put(SELECTED_CLASS, className); int dot = className.lastIndexOf('.'); @@ -104,7 +106,8 @@ private void processSelectedMethod(final Map map, Lookup context FileObject fo = method != null ? method.getFile() : RunUtils.extractFileObjectfromLookup(context); if ((fo != null) && fo.isData()) { GradleJavaProject gjp = GradleJavaProject.get(project); - String className = evaluateClassName(gjp, fo); + NestedClass nestedClass = method != null ? method.getNestedClass() : context.lookup(NestedClass.class); + String className = evaluateClassName(gjp, fo, nestedClass); String selectedMethod = method != null ? className + '.' + method.getMethodName() : className; map.put(SELECTED_METHOD, selectedMethod); } @@ -133,7 +136,7 @@ private void processSourceSets(final Map map, Lookup context) { } } - private String evaluateClassName(GradleJavaProject gjp, FileObject fo) { + private String evaluateClassName(GradleJavaProject gjp, FileObject fo, NestedClass nestedClass) { String ret = null; if ((gjp != null) && (fo != null)) { File f = FileUtil.toFile(fo); @@ -145,7 +148,7 @@ private String evaluateClassName(GradleJavaProject gjp, FileObject fo) { ret = relPath.replace('/', '.'); ret = ret + '*'; } else { - ret = SourceUtils.classNameFor(ClasspathInfo.create(fo), relPath); + ret = SourceUtils.classNameFor(ClasspathInfo.create(fo), relPath, nestedClass); } } } diff --git a/java/gradle.java/src/org/netbeans/modules/gradle/java/JavaActionProvider.java b/java/gradle.java/src/org/netbeans/modules/gradle/java/JavaActionProvider.java index 2d4045b3d55b..d3dda0ff6d39 100644 --- a/java/gradle.java/src/org/netbeans/modules/gradle/java/JavaActionProvider.java +++ b/java/gradle.java/src/org/netbeans/modules/gradle/java/JavaActionProvider.java @@ -72,6 +72,7 @@ public class JavaActionProvider extends DefaultGradleActionsProvider { COMMAND_RUN, COMMAND_DEBUG, COMMAND_TEST, + COMMAND_TEST_PARALLEL, COMMAND_TEST_SINGLE, COMMAND_DEBUG_TEST_SINGLE, COMMAND_RUN_SINGLE_METHOD, diff --git a/java/gradle.java/src/org/netbeans/modules/gradle/java/ProjectsTokenProvider.java b/java/gradle.java/src/org/netbeans/modules/gradle/java/ProjectsTokenProvider.java new file mode 100644 index 000000000000..612db9ad03da --- /dev/null +++ b/java/gradle.java/src/org/netbeans/modules/gradle/java/ProjectsTokenProvider.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.gradle.java; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.netbeans.api.project.Project; +import org.netbeans.modules.gradle.api.NbGradleProject; +import org.netbeans.modules.gradle.spi.actions.ReplaceTokenProvider; +import org.netbeans.spi.project.ActionProvider; +import org.netbeans.api.project.ContainedProjectFilter; +import org.netbeans.spi.project.ProjectServiceProvider; +import org.openide.util.Lookup; + +/** + * + * @author Dusan Petrovic + */ +@ProjectServiceProvider( + service = ReplaceTokenProvider.class, + projectType = NbGradleProject.GRADLE_PROJECT_TYPE +) +public class ProjectsTokenProvider implements ReplaceTokenProvider { + + private static final String TASK_WITH_PROJECTS = "taskWithProjects"; //NOI18N + private static final Set SUPPORTED = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + TASK_WITH_PROJECTS + ))); + + @Override + public Set getSupportedTokens() { + return SUPPORTED; + } + + @Override + public Map createReplacements(String action, Lookup context) { + String taskName = getTaskForAction(action); + if (taskName == null) { + return new HashMap<>(); + } + return getProjectsWithTaskReplacement(taskName, context); + } + + private String getTaskForAction(String action) { + return switch (action) { + case ActionProvider.COMMAND_TEST_PARALLEL -> "test"; //NOI18N + default -> null; + }; + } + + private Map getProjectsWithTaskReplacement(String taskName, Lookup context) { + ContainedProjectFilter parameters = context.lookup(ContainedProjectFilter.class); + List projects = parameters == null ? null : parameters.getProjectsToProcess(); + if (projects == null || projects.isEmpty()) { + return Map.of(TASK_WITH_PROJECTS, taskName); + } + StringBuilder resultTask = new StringBuilder(); + List projectReplacements = createProjectsReplacement(projects); + for (String project : projectReplacements) { + resultTask.append(project) + .append(":") //NOI18N + .append(taskName) + .append(" ");//NOI18N + } + return Map.of(TASK_WITH_PROJECTS, resultTask.toString().trim()); + } + + private List createProjectsReplacement(List projects) { + return projects + .stream() + .map(prj -> prj.getProjectDirectory().getName()) + .toList(); + } +} diff --git a/java/gradle.java/src/org/netbeans/modules/gradle/java/action-mapping.xml b/java/gradle.java/src/org/netbeans/modules/gradle/java/action-mapping.xml index af720a333f82..97fadb9fee99 100644 --- a/java/gradle.java/src/org/netbeans/modules/gradle/java/action-mapping.xml +++ b/java/gradle.java/src/org/netbeans/modules/gradle/java/action-mapping.xml @@ -21,6 +21,9 @@ --> + + --parallel --rerun-tasks ${taskWithProjects} + "${cleanTestTaskName}" "${testTaskName}" --tests "${selectedClass}" diff --git a/java/gradle.test/manifest.mf b/java/gradle.test/manifest.mf index 1dafb59394bd..2e5175701464 100644 --- a/java/gradle.test/manifest.mf +++ b/java/gradle.test/manifest.mf @@ -2,4 +2,4 @@ Manifest-Version: 1.0 AutoUpdate-Show-In-Client: false OpenIDE-Module: org.netbeans.modules.gradle.test OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/gradle/test/Bundle.properties -OpenIDE-Module-Specification-Version: 1.23 +OpenIDE-Module-Specification-Version: 1.24 diff --git a/java/gradle.test/nbproject/project.xml b/java/gradle.test/nbproject/project.xml index 6bbce2d0cc20..efe3ef690ebb 100644 --- a/java/gradle.test/nbproject/project.xml +++ b/java/gradle.test/nbproject/project.xml @@ -90,7 +90,7 @@ - 2.4.1.2.25.8.1 + 2.74 diff --git a/java/gradle.test/src/org/netbeans/modules/gradle/test/GradleTestProgressListener.java b/java/gradle.test/src/org/netbeans/modules/gradle/test/GradleTestProgressListener.java index a3e70dc76c52..a45f9d20176d 100644 --- a/java/gradle.test/src/org/netbeans/modules/gradle/test/GradleTestProgressListener.java +++ b/java/gradle.test/src/org/netbeans/modules/gradle/test/GradleTestProgressListener.java @@ -47,6 +47,7 @@ import org.gradle.tooling.events.test.TestStartEvent; import org.gradle.tooling.events.test.TestSuccessResult; import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectUtils; import org.netbeans.modules.gsf.testrunner.api.CommonUtils; import org.netbeans.modules.gsf.testrunner.api.CoreManager; import org.netbeans.modules.gsf.testrunner.api.Report; @@ -62,13 +63,13 @@ * * @author Laszlo Kishalmi */ -@ProjectServiceProvider(service = GradleProgressListenerProvider.class, projectType = NbGradleProject.GRADLE_PLUGIN_TYPE + "/java") +@ProjectServiceProvider(service = GradleProgressListenerProvider.class, projectType = NbGradleProject.GRADLE_PROJECT_TYPE ) public final class GradleTestProgressListener implements ProgressListener, GradleProgressListenerProvider { private final Project project; - TestSession session; + private final Map sessions = new ConcurrentHashMap<>(); - Map runningTests = new ConcurrentHashMap<>(); + private Map> runningTests = new ConcurrentHashMap<>(); public GradleTestProgressListener(Project project) { this.project = project; @@ -131,17 +132,22 @@ private void processTestProgress(TestProgressEvent evt) { } private void processTestOutput(TestOutputEvent evt) { + TestSession session = sessions.get(getSessionKey(evt.getDescriptor())); + assert session != null; + if (session == null) { + throw new IllegalArgumentException("TestSession is null"); + } TestOutputDescriptor desc = evt.getDescriptor(); OperationDescriptor parent = desc.getParent(); CoreManager manager = getManager(); String msg = desc.getMessage(); if (msg != null && msg.endsWith("\n")) { msg = msg.substring(0, msg.length() - 1); - if (manager != null) { + if (manager != null && session != null) { manager.displayOutput(session, msg, desc.getDestination().equals(Destination.StdErr)); } if (parent instanceof JvmTestOperationDescriptor) { - Testcase tc = runningTests.get(getTestOpKey((JvmTestOperationDescriptor) parent)); + Testcase tc = runningTests.get(session).get(getTestOpKey((JvmTestOperationDescriptor) parent)); if (tc != null) { tc.addOutputLines(Arrays.asList(msg.split("\\R"))); } @@ -151,8 +157,12 @@ private void processTestOutput(TestOutputEvent evt) { private void sessionStart(TestStartEvent evt) { - session = new TestSession(evt.getDisplayName(), project, TestSession.SessionType.TEST); - runningTests.clear(); + String key = getSessionKey(evt.getDescriptor()); + TestSession session; + synchronized (this) { + session = sessions.computeIfAbsent(key, name -> new TestSession(name, getProject(key), TestSession.SessionType.TEST)); + runningTests.put(session, new ConcurrentHashMap<>()); + } CoreManager manager = getManager(); if (manager != null) { manager.registerNodeFactory(); @@ -161,7 +171,12 @@ private void sessionStart(TestStartEvent evt) { } private void sessionFinish(TestFinishEvent evt) { - runningTests.clear(); + TestSession session; + synchronized (this) { + session = sessions.remove(getSessionKey(evt.getDescriptor())); + assert session != null; + runningTests.remove(session); + } CoreManager manager = getManager(); if (manager != null) { manager.sessionFinished(session); @@ -172,6 +187,8 @@ private void suiteStart(TestStartEvent evt, JvmTestOperationDescriptor op) { } private void suiteFinish(TestFinishEvent evt, JvmTestOperationDescriptor op) { + TestSession session = sessions.get(getSessionKey(evt.getDescriptor())); + assert session != null; TestOperationResult result = evt.getResult(); TestSuite currentSuite = session.getCurrentSuite(); String suiteName = GradleTestSuite.suiteName(op); @@ -186,6 +203,7 @@ private void suiteFinish(TestFinishEvent evt, JvmTestOperationDescriptor op) { } private void caseStart(TestStartEvent evt, JvmTestOperationDescriptor op) { + TestSession session = sessions.get(getSessionKey(evt.getDescriptor())); assert session != null; assert op.getParent() != null; TestSuite currentSuite = session.getCurrentSuite(); @@ -198,12 +216,19 @@ private void caseStart(TestStartEvent evt, JvmTestOperationDescriptor op) { } } Testcase tc = new GradleTestcase(op, session); - runningTests.put(getTestOpKey(op), tc); - session.addTestCase(tc); + synchronized (this) { + runningTests.get(session).put(getTestOpKey(op), tc); + session.addTestCase(tc); + } } - private void caseFinish(TestFinishEvent evt, JvmTestOperationDescriptor op) { - Testcase tc = runningTests.get(getTestOpKey(op)); + private void caseFinish(TestFinishEvent evt, JvmTestOperationDescriptor op) { + Testcase tc; + synchronized (this) { + TestSession session = sessions.get(getSessionKey(evt.getDescriptor())); + assert session != null; + tc = runningTests.get(session).remove(getTestOpKey(op)); + } if (tc != null) { TestOperationResult result = evt.getResult(); long time = result.getEndTime() - result.getStartTime(); @@ -241,11 +266,38 @@ private void caseFinish(TestFinishEvent evt, JvmTestOperationDescriptor op) { } } - runningTests.remove(getTestOpKey(op)); } } + private static final String GRADLE_TEST_RUN = "Gradle Test Run :"; // NOI18N + private static String TEST = ":test"; + + private Project getProject(String key) { + if (key != null && key.startsWith(GRADLE_TEST_RUN)) { + key = key.substring(GRADLE_TEST_RUN.length()); + if (key.endsWith(TEST)) { + key = key.substring(0, key.length() - TEST.length()).trim(); + if (!key.isEmpty()) { + for (Project containedPrj : ProjectUtils.getContainedProjects(project, true)) { + if (key.equals(containedPrj.getProjectDirectory().getName())) { + return containedPrj; + } + } + } + } + } + return project; + } + + private static String getSessionKey(OperationDescriptor op) { + String id = ""; + for (OperationDescriptor descriptor = op; descriptor != null; descriptor = descriptor.getParent()) { + id = descriptor.getName(); + } + return id; + } + private static JvmTestOperationDescriptor getSuiteOpDesc(JvmTestOperationDescriptor op, String className) { for (JvmTestOperationDescriptor descriptor = op; descriptor != null; descriptor = (JvmTestOperationDescriptor) descriptor.getParent()) { if (className == null || className.equals(descriptor.getClassName())) { diff --git a/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/maven-actions-override.xml b/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/maven-actions-override.xml index 3ba325ab656d..eacae56a62c9 100644 --- a/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/maven-actions-override.xml +++ b/java/java.lsp.server/nbcode/integration/src/org/netbeans/modules/nbcode/integration/maven-actions-override.xml @@ -55,7 +55,6 @@ ${packageClassName}
- run diff --git a/java/java.lsp.server/nbproject/project.properties b/java/java.lsp.server/nbproject/project.properties index 628f2f91a172..1a84ddf386cd 100644 --- a/java/java.lsp.server/nbproject/project.properties +++ b/java/java.lsp.server/nbproject/project.properties @@ -17,7 +17,7 @@ javac.source=1.8 javac.compilerargs=-Xlint -Xlint:-serial -spec.version.base=2.10.0 +spec.version.base=2.11.0 javadoc.arch=${basedir}/arch.xml requires.nb.javac=true lsp.build.dir=vscode/nbcode diff --git a/java/java.lsp.server/nbproject/project.xml b/java/java.lsp.server/nbproject/project.xml index b2357086f913..fffc512714b3 100644 --- a/java/java.lsp.server/nbproject/project.xml +++ b/java/java.lsp.server/nbproject/project.xml @@ -519,7 +519,7 @@ 1 - 1.85 + 1.99 diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchDelegate.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchDelegate.java index 25f96af67529..01d92d5c82a3 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchDelegate.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchDelegate.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; @@ -76,6 +77,8 @@ import org.netbeans.modules.nativeimage.api.debug.StartDebugParameters; import org.netbeans.spi.project.ActionProgress; import org.netbeans.spi.project.ActionProvider; +import org.netbeans.api.project.ContainedProjectFilter; +import org.netbeans.spi.project.NestedClass; import org.netbeans.spi.project.ProjectConfiguration; import org.netbeans.spi.project.ProjectConfigurationProvider; import org.netbeans.spi.project.SingleMethod; @@ -124,16 +127,29 @@ protected void notifyFinished(DebugAdapterContext ctx, boolean success) { } public final CompletableFuture nbLaunch(FileObject toRun, boolean preferProjActions, @NullAllowed File nativeImageFile, - @NullAllowed String method, Map launchArguments, DebugAdapterContext context, - boolean debug, boolean testRun, Consumer consoleMessages) { + @NullAllowed String method, @NullAllowed String nestedClassName, Map launchArguments, DebugAdapterContext context, + boolean debug, boolean testRun, Consumer consoleMessages, + boolean testInParallel) { CompletableFuture launchFuture = new CompletableFuture<>(); NbProcessConsole ioContext = new NbProcessConsole(consoleMessages); + NestedClass nestedClass; + if (nestedClassName != null) { + int topLevelClassSeparatorIdx = nestedClassName.indexOf("."); + String topLevelClassName = nestedClassName.substring(0, topLevelClassSeparatorIdx); + String nestedName = nestedClassName.substring(topLevelClassSeparatorIdx + 1); + nestedClass = new NestedClass(nestedName, topLevelClassName, toRun); + } else { + nestedClass = null; + } SingleMethod singleMethod; if (method != null) { - singleMethod = new SingleMethod(toRun, method); + singleMethod = nestedClass != null ? + new SingleMethod(method, nestedClass) + : new SingleMethod(toRun, method); } else { singleMethod = null; } + ActionProgress progress = new ActionProgress() { private final AtomicInteger count = new AtomicInteger(0); private final AtomicBoolean finalSuccess = new AtomicBoolean(true); @@ -154,6 +170,13 @@ public void finished(boolean success) { }; if (nativeImageFile == null) { Project prj = FileOwnerQuery.getOwner(toRun); + ContainedProjectFilter projectFilter; + if (testInParallel) { + projectFilter = getProjectFilter(prj, launchArguments); + } else { + projectFilter = null; + } + class W extends Writer { @Override public void write(char[] cbuf, int off, int len) throws IOException { @@ -176,7 +199,7 @@ public void close() throws IOException { } } W writer = new W(); - CompletableFuture> commandFuture = findTargetWithPossibleRebuild(prj, preferProjActions, toRun, singleMethod, debug, testRun, ioContext); + CompletableFuture> commandFuture = findTargetWithPossibleRebuild(prj, preferProjActions, toRun, singleMethod, nestedClass, debug, testRun, ioContext, testInParallel, projectFilter); commandFuture.thenAccept((providerAndCommand) -> { ExplicitProcessParameters params = createExplicitProcessParameters(launchArguments); OperationContext ctx = OperationContext.find(Lookup.getDefault()); @@ -224,7 +247,7 @@ public void progressHandleCreated(ProgressOperationEvent e) { } Lookup lookup = new ProxyLookup( - createTargetLookup(prj, singleMethod, toRun), + createTargetLookup(prj, singleMethod, nestedClass, toRun, projectFilter), Lookups.fixed(runContext.toArray(new Object[runContext.size()])) ); // the execution Lookup is fully populated now. If the Project supports Configurations, @@ -357,6 +380,7 @@ public void propertyChange(PropertyChangeEvent evt) { private static ExplicitProcessParameters createExplicitProcessParameters(Map launchArguments) { List args = argsToStringList(launchArguments.get("args")); List vmArgs = argsToStringList(launchArguments.get("vmArgs")); + String cwd = Objects.toString(launchArguments.get("cwd"), null); Object envObj = launchArguments.get("env"); Map env = envObj != null ? (Map) envObj : Collections.emptyMap(); @@ -486,8 +510,8 @@ static List argsToStringList(Object o) { } } - private static CompletableFuture> findTargetWithPossibleRebuild(Project proj, boolean preferProjActions, FileObject toRun, SingleMethod singleMethod, boolean debug, boolean testRun, NbProcessConsole ioContext) throws IllegalArgumentException { - Pair providerAndCommand = findTarget(proj, preferProjActions, toRun, singleMethod, debug, testRun); + private static CompletableFuture> findTargetWithPossibleRebuild(Project proj, boolean preferProjActions, FileObject toRun, SingleMethod singleMethod, NestedClass nestedClass, boolean debug, boolean testRun, NbProcessConsole ioContext, boolean testInParallel, ContainedProjectFilter projectFilter) throws IllegalArgumentException { + Pair providerAndCommand = findTarget(proj, preferProjActions, toRun, singleMethod, nestedClass, debug, testRun, testInParallel, projectFilter); if (providerAndCommand != null) { return CompletableFuture.completedFuture(providerAndCommand); } @@ -503,7 +527,7 @@ protected void started() { @Override public void finished(boolean success) { if (success) { - Pair providerAndCommand = findTarget(proj, preferProjActions, toRun, singleMethod, debug, testRun); + Pair providerAndCommand = findTarget(proj, preferProjActions, toRun, singleMethod, nestedClass, debug, testRun, testInParallel, projectFilter); if (providerAndCommand != null) { afterBuild.complete(providerAndCommand); return; @@ -536,7 +560,7 @@ public void finished(boolean success) { return afterBuild; } - protected static @CheckForNull Pair findTarget(Project prj, boolean preferProjActions, FileObject toRun, SingleMethod singleMethod, boolean debug, boolean testRun) { + protected static @CheckForNull Pair findTarget(Project prj, boolean preferProjActions, FileObject toRun, SingleMethod singleMethod, NestedClass nestedClass, boolean debug, boolean testRun, boolean testInParallel, ContainedProjectFilter projectFilter) { ClassPath sourceCP = ClassPath.getClassPath(toRun, ClassPath.SOURCE); FileObject fileRoot = sourceCP != null ? sourceCP.findOwnerRoot(toRun) : null; boolean mainSource; @@ -548,11 +572,14 @@ public void finished(boolean success) { ActionProvider provider = null; String command = null; Collection actionProviders = findActionProviders(prj); - Lookup testLookup = createTargetLookup(preferProjActions ? prj : null, singleMethod, toRun); + Lookup testLookup = createTargetLookup(preferProjActions ? prj : null, singleMethod, nestedClass, toRun, projectFilter); String[] actions; - if (!mainSource && singleMethod != null) { + + if (testInParallel) { + actions = new String[] {ActionProvider.COMMAND_TEST_PARALLEL, ActionProvider.COMMAND_RUN}; + } else if (!mainSource && singleMethod != null) { actions = debug ? new String[] {SingleMethod.COMMAND_DEBUG_SINGLE_METHOD} - : new String[] {SingleMethod.COMMAND_RUN_SINGLE_METHOD}; + : new String[] {SingleMethod.COMMAND_RUN_SINGLE_METHOD}; } else { if (preferProjActions && prj != null) { actions = debug ? mainSource ? new String[] {ActionProvider.COMMAND_DEBUG} @@ -562,7 +589,7 @@ public void finished(boolean success) { if (debug && !mainSource) { // We are calling COMMAND_DEBUG_TEST_SINGLE instead of a missing COMMAND_DEBUG_TEST // This is why we need to add the file to the lookup - testLookup = createTargetLookup(null, singleMethod, toRun); + testLookup = createTargetLookup(null, singleMethod, nestedClass, toRun, projectFilter); } } else { actions = debug ? mainSource ? new String[] {ActionProvider.COMMAND_DEBUG_SINGLE} @@ -623,7 +650,7 @@ public void invokeAction(String command, Lookup context) throws IllegalArgumentE return Pair.of(provider, command); } - static Lookup createTargetLookup(Project prj, SingleMethod singleMethod, FileObject toRun) { + static Lookup createTargetLookup(Project prj, SingleMethod singleMethod, NestedClass nestedClass, FileObject toRun, ContainedProjectFilter projectFilter) { List arr = new ArrayList<>(); if (prj != null) { arr.add(Lookups.singleton(prj)); @@ -632,11 +659,27 @@ static Lookup createTargetLookup(Project prj, SingleMethod singleMethod, FileObj Lookup methodLookup = Lookups.singleton(singleMethod); arr.add(methodLookup); } + if (nestedClass != null) { + Lookup nestedClassLookup = Lookups.singleton(nestedClass); + arr.add(nestedClassLookup); + } + if (projectFilter != null) { + Lookup projectLookup = Lookups.singleton(projectFilter); + arr.add(projectLookup); + } if (toRun != null) { arr.add(toRun.getLookup()); } return new ProxyLookup(arr.toArray(new Lookup[0])); } + + static ContainedProjectFilter getProjectFilter(Project prj, Map launchArguments) { + List projectsArg = argsToStringList(launchArguments.get("projects")); + List projects = ProjectUtils.getContainedProjects(prj, false).stream() + .filter(project -> projectsArg.contains(project.getProjectDirectory().getName())) + .toList(); + return ContainedProjectFilter.of(projects).orElse(null); + } static Collection findActionProviders(Project prj) { Collection actionProviders = new ArrayList<>(); diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchRequestHandler.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchRequestHandler.java index 0b1a26d3875d..8d96ac88be89 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchRequestHandler.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchRequestHandler.java @@ -245,7 +245,9 @@ public CompletableFuture launch(Map launchArguments, Debug context.setSourcePaths((String[]) launchArguments.get("sourcePaths")); } String singleMethod = (String)launchArguments.get("methodName"); - activeLaunchHandler.nbLaunch(file, preferProjActions, nativeImageFile, singleMethod, launchArguments, context, !noDebug, testRun, new OutputListener(context)).thenRun(() -> { + String nestedClass = (String)launchArguments.get("nestedClass"); + boolean testInParallel = (Boolean) launchArguments.getOrDefault("testInParallel", Boolean.FALSE); + activeLaunchHandler.nbLaunch(file, preferProjActions, nativeImageFile, singleMethod, nestedClass, launchArguments, context, !noDebug, testRun, new OutputListener(context), testInParallel).thenRun(() -> { activeLaunchHandler.postLaunch(launchArguments, context); resultFuture.complete(null); }).exceptionally(e -> { diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/progress/ModuleInfo.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/progress/ModuleInfo.java new file mode 100644 index 000000000000..ac423a4dd199 --- /dev/null +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/progress/ModuleInfo.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.java.lsp.server.progress; + +import java.util.List; + +/** + * + * @author Dusan Petrovic + * + * @since 2.11.0 + */ +public final class ModuleInfo { + + private final String moduleName; + private final List testRoots; + + public ModuleInfo(String moduleName, List testRoots) { + this.moduleName = moduleName; + this.testRoots = testRoots; + } + + public String getModuleName() { + return moduleName; + } + + public List getTestRoots() { + return testRoots; + } +} diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/progress/TestProgressHandler.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/progress/TestProgressHandler.java index 1330c0f366f6..deb6d8b54e80 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/progress/TestProgressHandler.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/progress/TestProgressHandler.java @@ -18,15 +18,25 @@ */ package org.netbeans.modules.java.lsp.server.progress; +import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; import org.eclipse.lsp4j.debug.OutputEventArguments; import org.eclipse.lsp4j.debug.services.IDebugProtocolClient; import org.netbeans.api.extexecution.print.LineConvertors; +import org.netbeans.api.java.project.JavaProjectConstants; +import org.netbeans.api.java.queries.UnitTestForSourceQuery; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectUtils; +import org.netbeans.api.project.SourceGroup; import org.netbeans.modules.gsf.testrunner.api.Report; import org.netbeans.modules.gsf.testrunner.api.Status; import org.netbeans.modules.gsf.testrunner.api.TestSession; @@ -38,13 +48,15 @@ import org.netbeans.modules.java.lsp.server.protocol.TestProgressParams; import org.netbeans.modules.java.lsp.server.protocol.TestSuiteInfo; import org.openide.filesystems.FileObject; +import org.openide.filesystems.URLMapper; /** * * @author Dusan Balek */ -public final class TestProgressHandler implements TestResultDisplayHandler.Spi { - +public final class TestProgressHandler implements TestResultDisplayHandler.Spi { + private static final Logger LOG = Logger.getLogger(TestProgressHandler.class.getName()); + private final NbCodeLanguageClient lspClient; private final IDebugProtocolClient debugClient; private final String uri; @@ -56,31 +68,41 @@ public TestProgressHandler(NbCodeLanguageClient lspClient, IDebugProtocolClient } @Override - public TestProgressHandler create(TestSession session) { - return this; + public ModuleInfo create(TestSession session) { + return getModuleInfo(session); } @Override - public void displayOutput(TestProgressHandler token, String text, boolean error) { + public void displayOutput(ModuleInfo token, String text, boolean error) { if (text != null) { OutputEventArguments output = new OutputEventArguments(); output.setOutput(text.trim() + "\n"); debugClient.output(output); } } + + private String firstModulePath(ModuleInfo token) { + List paths = token.getTestRoots(); + if (paths == null || paths.isEmpty()) { + return null; + } else if (paths.size() > 1) { + LOG.log(Level.WARNING, "Mutliple test roots are not yet supported for module {0}", token.getModuleName()); + } + return paths.iterator().next(); + } @Override - public void displaySuiteRunning(TestProgressHandler token, String suiteName) { - lspClient.notifyTestProgress(new TestProgressParams(uri, new TestSuiteInfo(suiteName, TestSuiteInfo.State.Started))); + public void displaySuiteRunning(ModuleInfo token, String suiteName) { + lspClient.notifyTestProgress(new TestProgressParams(uri, new TestSuiteInfo(suiteName, TestSuiteInfo.State.Started).setModuleName(token.getModuleName()).setModulePath(firstModulePath(token)))); } @Override - public void displaySuiteRunning(TestProgressHandler token, TestSuite suite) { - lspClient.notifyTestProgress(new TestProgressParams(uri, new TestSuiteInfo(suite.getName(), TestSuiteInfo.State.Started))); + public void displaySuiteRunning(ModuleInfo token, TestSuite suite) { + lspClient.notifyTestProgress(new TestProgressParams(uri, new TestSuiteInfo(suite.getName(), TestSuiteInfo.State.Started).setModuleName(token.getModuleName()).setModulePath(firstModulePath(token)))); } @Override - public void displayReport(TestProgressHandler token, Report report) { + public void displayReport(ModuleInfo token, Report report) { Map fileLocations = new HashMap<>(); Map testCases = new LinkedHashMap<>(); String className = report.getSuiteClassName(); @@ -108,20 +130,21 @@ public void displayReport(TestProgressHandler token, Report report) { } String state = statusToState(report.getStatus()); FileObject fo = fileLocations.size() == 1 ? fileLocations.values().iterator().next() : null; - lspClient.notifyTestProgress(new TestProgressParams(uri, new TestSuiteInfo(report.getSuiteClassName(), + + lspClient.notifyTestProgress(new TestProgressParams(uri, new TestSuiteInfo(report.getSuiteClassName(), token.getModuleName(), firstModulePath(token), fo != null ? Utils.toUri(fo) : null, null, state, new ArrayList<>(testCases.values())))); } @Override - public void displayMessage(TestProgressHandler token, String message) { + public void displayMessage(ModuleInfo token, String message) { } @Override - public void displayMessageSessionFinished(TestProgressHandler token, String message) { + public void displayMessageSessionFinished(ModuleInfo token, String message) { } @Override - public int getTotalTests(TestProgressHandler token) { + public int getTotalTests(ModuleInfo token) { return 0; } @@ -144,4 +167,29 @@ private String statusToState(Status status) { throw new IllegalStateException("Unexpected testsuite status: " + status); } } + + private static ModuleInfo getModuleInfo(TestSession session) { + Project project = session.getProject(); + String moduleName = project != null ? ProjectUtils.getInformation(project).getDisplayName() : null; + List testPaths = getModuleTestPaths(project); + return new ModuleInfo(moduleName, testPaths); + } + + private static List getModuleTestPaths(Project project) { + if (project == null) { + return null; + } + SourceGroup[] sourceGroups = ProjectUtils.getSources(project).getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA); + Set paths = new LinkedHashSet<>(); + for (SourceGroup sourceGroup : sourceGroups) { + URL[] urls = UnitTestForSourceQuery.findUnitTests(sourceGroup.getRootFolder()); + for (URL u : urls) { + FileObject f = URLMapper.findFileObject(u); + if (f != null) { + paths.add(f.getPath()); + } + } + } + return paths.isEmpty() ? null : new ArrayList<>(paths); + } } diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TestSuiteInfo.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TestSuiteInfo.java index 6258caf36d08..2907ca89ee62 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TestSuiteInfo.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TestSuiteInfo.java @@ -39,6 +39,16 @@ public final class TestSuiteInfo { @NonNull private String name; + /** + * The optional module name to be displayed by the Test Explorer. + */ + private String moduleName; + + /** + * The optional module path used by the Test Explorer. + */ + private String modulePath; + /** * The file containing this suite (if known). */ @@ -70,8 +80,10 @@ public TestSuiteInfo(@NonNull final String name, @NonNull final String state) { this.state = Preconditions.checkNotNull(state, "state"); } - public TestSuiteInfo(@NonNull final String name, final String file, final Range range, @NonNull final String state, final List tests) { + public TestSuiteInfo(@NonNull final String name, final String moduleName, final String modulePath, final String file, final Range range, @NonNull final String state, final List tests) { this(name, state); + this.moduleName = moduleName; + this.modulePath = modulePath; this.file = file; this.range = range; this.tests = tests; @@ -89,8 +101,41 @@ public String getName() { /** * The test suite name to be displayed by the Test Explorer. */ - public void setSuiteName(@NonNull final String name) { + public TestSuiteInfo setSuiteName(@NonNull final String name) { this.name = Preconditions.checkNotNull(name, "name"); + return this; + } + + /** + * The optional module name to be displayed by the Test Explorer. + */ + @Pure + public String getModuleName() { + return moduleName; + } + + /** + * The optional module name to be displayed by the Test Explorer. + */ + public TestSuiteInfo setModuleName(final String moduleName) { + this.moduleName = moduleName; + return this; + } + + /** + * The optional module path used by the Test Explorer. + */ + @Pure + public String getModulePath() { + return modulePath; + } + + /** + * The optional module path used by the Test Explorer. + */ + public TestSuiteInfo setModulePath(final String modulePath) { + this.modulePath = modulePath; + return this; } /** @@ -104,8 +149,9 @@ public String getFile() { /** * The file containing this suite (if known). */ - public void setFile(final String file) { + public TestSuiteInfo setFile(final String file) { this.file = file; + return this; } /** @@ -119,8 +165,9 @@ public Range getRange() { /** * The range within the specified file where the suite definition is located (if known). */ - public void setRange(final Range range) { + public TestSuiteInfo setRange(final Range range) { this.range = range; + return this; } /** @@ -137,8 +184,9 @@ public String getState() { * The state of the tests suite. Can be one of the following values: * "loaded" | "started" | "passed" | "failed" | "skipped" | "errored" */ - public void setState(@NonNull final String state) { + public TestSuiteInfo setState(@NonNull final String state) { this.state = Preconditions.checkNotNull(state, "state"); + return this; } /** @@ -152,8 +200,9 @@ public List getTests() { /** * The test cases of the test suite. */ - public void setTests(List tests) { + public TestSuiteInfo setTests(List tests) { this.tests = tests; + return this; } @Override @@ -161,6 +210,8 @@ public void setTests(List tests) { public String toString() { ToStringBuilder b = new ToStringBuilder(this); b.add("name", name); + b.add("moduleName", moduleName); + b.add("modulePath", modulePath); b.add("file", file); b.add("range", range); b.add("state", state); @@ -173,6 +224,8 @@ public String toString() { public int hashCode() { int hash = 7; hash = 67 * hash + Objects.hashCode(this.name); + hash = 67 * hash + Objects.hashCode(this.moduleName); + hash = 67 * hash + Objects.hashCode(this.modulePath); hash = 67 * hash + Objects.hashCode(this.file); hash = 67 * hash + Objects.hashCode(this.range); hash = 67 * hash + Objects.hashCode(this.state); @@ -196,6 +249,12 @@ public boolean equals(Object obj) { if (!Objects.equals(this.name, other.name)) { return false; } + if (!Objects.equals(this.moduleName, other.moduleName)) { + return false; + } + if (!Objects.equals(this.modulePath, other.modulePath)) { + return false; + } if (!Objects.equals(this.file, other.file)) { return false; } @@ -279,8 +338,9 @@ public String getId() { /** * The test case ID. */ - public void setId(@NonNull final String id) { + public TestCaseInfo setId(@NonNull final String id) { this.id = Preconditions.checkNotNull(id, "id"); + return this; } /** @@ -295,8 +355,9 @@ public String getName() { /** * The name to be displayed by the Test Explorer for this test case. */ - public void setName(@NonNull final String name) { + public TestCaseInfo setName(@NonNull final String name) { this.name = Preconditions.checkNotNull(name, "name"); + return this; } /** @@ -310,8 +371,9 @@ public String getFile() { /** * The file containing this test case (if known). */ - public void setFile(final String file) { + public TestCaseInfo setFile(final String file) { this.file = file; + return this; } /** @@ -325,8 +387,9 @@ public Range getRange() { /** * The range within the specified file where the test case definition is located (if known). */ - public void setRange(final Range range) { + public TestCaseInfo setRange(final Range range) { this.range = range; + return this; } /** @@ -343,8 +406,9 @@ public String getState() { * The state of the test case. Can be one of the following values: * "loaded" | "started" | "passed" | "failed" | "skipped" | "errored" */ - public void setState(@NonNull final String state) { + public TestCaseInfo setState(@NonNull final String state) { this.state = Preconditions.checkNotNull(state, "state"); + return this; } /** @@ -358,8 +422,9 @@ public List getStackTrace() { /** * Stack trace for a test failure. */ - public void setStackTrace(final List stackTrace) { + public TestCaseInfo setStackTrace(final List stackTrace) { this.stackTrace = stackTrace; + return this; } @Override diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java index 6e0755bfb6e6..c9650cad89e4 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java @@ -43,6 +43,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; @@ -124,6 +125,7 @@ import org.netbeans.modules.java.lsp.server.Utils; import org.netbeans.modules.java.lsp.server.debugging.attach.AttachConfigurations; import org.netbeans.modules.java.lsp.server.debugging.attach.AttachNativeConfigurations; +import org.netbeans.modules.java.lsp.server.progress.ModuleInfo; import org.netbeans.modules.java.lsp.server.project.LspProjectInfo; import org.netbeans.modules.java.lsp.server.singlesourcefile.SingleFileOptionsQueryImpl; import org.netbeans.modules.java.source.ElementHandleAccessor; @@ -401,11 +403,15 @@ public CompletableFuture executeCommand(ExecuteCommandParams params) { LOG.log(Level.INFO, "Project {2}: {0} test roots opened in {1}ms", new Object[] { testRoots.size(), (System.currentTimeMillis() - t), file}); BiFunction, Collection> f = (fo, methods) -> { String url = Utils.toUri(fo); + Project owner = FileOwnerQuery.getOwner(fo); + String moduleName = owner != null ? ProjectUtils.getInformation(owner).getDisplayName(): null; + List paths = getModuleTestPaths(owner); + String modulePath = firstModulePath(paths, moduleName); Map suite2infos = new LinkedHashMap<>(); for (TestMethodController.TestMethod testMethod : methods) { TestSuiteInfo suite = suite2infos.computeIfAbsent(testMethod.getTestClassName(), name -> { Position pos = testMethod.getTestClassPosition() != null ? Utils.createPosition(fo, testMethod.getTestClassPosition().getOffset()) : null; - return new TestSuiteInfo(name, url, pos != null ? new Range(pos, pos) : null, TestSuiteInfo.State.Loaded, new ArrayList<>()); + return new TestSuiteInfo(name, moduleName, modulePath, url, pos != null ? new Range(pos, pos) : null, TestSuiteInfo.State.Loaded, new ArrayList<>()); }); String id = testMethod.getTestClassName() + ':' + testMethod.method().getMethodName(); Position startPos = testMethod.start() != null ? Utils.createPosition(fo, testMethod.start().getOffset()) : null; @@ -805,6 +811,33 @@ public CompletableFuture executeCommand(ExecuteCommandParams params) { throw new UnsupportedOperationException("Command not supported: " + params.getCommand()); } + private String firstModulePath(List paths, String moduleName) { + if (paths == null || paths.isEmpty()) { + return null; + } else if (paths.size() > 1) { + LOG.log(Level.WARNING, "Mutliple test roots are not yet supported for module {0}", moduleName); + } + return paths.iterator().next(); + } + + private static List getModuleTestPaths(Project project) { + if (project == null) { + return null; + } + SourceGroup[] sourceGroups = ProjectUtils.getSources(project).getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA); + Set paths = new LinkedHashSet<>(); + for (SourceGroup sourceGroup : sourceGroups) { + URL[] urls = UnitTestForSourceQuery.findUnitTests(sourceGroup.getRootFolder()); + for (URL u : urls) { + FileObject f = URLMapper.findFileObject(u); + if (f != null) { + paths.add(f.getPath()); + } + } + } + return paths.isEmpty() ? null : new ArrayList<>(paths); + } + private class ProjectInfoWorker { final URL[] locations; final boolean projectStructure; diff --git a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchDelegateTest.java b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchDelegateTest.java index 2035554950f8..64dd84b44272 100644 --- a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchDelegateTest.java +++ b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/debugging/launch/NbLaunchDelegateTest.java @@ -25,6 +25,7 @@ import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectManager; import org.netbeans.spi.project.ActionProvider; +import org.netbeans.spi.project.NestedClass; import org.netbeans.spi.project.ProjectFactory; import org.netbeans.spi.project.ProjectState; import org.netbeans.spi.project.SingleMethod; @@ -43,7 +44,7 @@ public NbLaunchDelegateTest() { @Test public void testFileObjectsLookup() throws Exception { FileObject fo = FileUtil.createMemoryFileSystem().getRoot().createData("test.txt"); - Lookup lkp = NbLaunchDelegate.createTargetLookup(null, null, fo); + Lookup lkp = NbLaunchDelegate.createTargetLookup(null, null, null, fo, null); assertEquals(fo, lkp.lookup(FileObject.class)); DataObject obj = lkp.lookup(DataObject.class); @@ -56,8 +57,10 @@ public void testFileObjectsLookup() throws Exception { @Test public void testFileObjectsLookupWithSingleMethod() throws Exception { FileObject fo = FileUtil.createMemoryFileSystem().getRoot().createData("test-with-method.txt"); - SingleMethod m = new SingleMethod(fo, "main"); - Lookup lkp = NbLaunchDelegate.createTargetLookup(null, m, fo); + NestedClass nc = new NestedClass("ChildClass", "ParentClass", fo); + SingleMethod m = new SingleMethod("main", nc); + + Lookup lkp = NbLaunchDelegate.createTargetLookup(null, m, nc, fo, null); assertEquals(fo, lkp.lookup(FileObject.class)); DataObject obj = lkp.lookup(DataObject.class); @@ -76,7 +79,7 @@ public void testFindsMavenProject() throws Exception { assertNotNull("Project dir recognized", prj); SingleMethod m = new SingleMethod(xml, "main"); - Lookup lkp = NbLaunchDelegate.createTargetLookup(prj, null, null); + Lookup lkp = NbLaunchDelegate.createTargetLookup(prj, null, null, null, null); assertNull("No file object", lkp.lookup(FileObject.class)); DataObject obj = lkp.lookup(DataObject.class); assertNull("No DataObject ", obj); @@ -91,8 +94,9 @@ public void testFindsWithFileObjectAndDataObject() throws Exception { Project prj = ProjectManager.getDefault().findProject(dir); assertNotNull("Project dir recognized", prj); - SingleMethod m = new SingleMethod(xml, "main"); - Lookup lkp = NbLaunchDelegate.createTargetLookup(prj, m, xml); + NestedClass nc = new NestedClass("ChildClass", "ParentClass", xml); + SingleMethod m = new SingleMethod("main", nc); + Lookup lkp = NbLaunchDelegate.createTargetLookup(prj, m, nc, xml, null); assertEquals("File object is available", xml, lkp.lookup(FileObject.class)); DataObject obj = lkp.lookup(DataObject.class); assertNotNull("DataObject is available", obj); @@ -108,7 +112,7 @@ public void testFindsWithFileObjectAndDataObjectNoMethod() throws Exception { Project prj = ProjectManager.getDefault().findProject(dir); assertNotNull("Project dir recognized", prj); - Lookup lkp = NbLaunchDelegate.createTargetLookup(prj, null, xml); + Lookup lkp = NbLaunchDelegate.createTargetLookup(prj, null, null, xml, null); assertEquals("File object is available", xml, lkp.lookup(FileObject.class)); DataObject obj = lkp.lookup(DataObject.class); assertNotNull("DataObject is available", obj); diff --git a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/progress/TestProgressHandlerTest.java b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/progress/TestProgressHandlerTest.java index 660bb64fae40..765934b950b3 100644 --- a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/progress/TestProgressHandlerTest.java +++ b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/progress/TestProgressHandlerTest.java @@ -32,6 +32,7 @@ import org.junit.Test; import org.netbeans.api.extexecution.print.LineConvertors; import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectUtils; import org.netbeans.junit.NbTestCase; import org.netbeans.modules.gsf.testrunner.api.Report; import org.netbeans.modules.gsf.testrunner.api.Status; @@ -79,8 +80,6 @@ public void testProgress() { assertNotNull(fo); List msgs = new ArrayList<>(); MockLanguageClient mlc = new MockLanguageClient(msgs); - TestProgressHandler progressHandler = new TestProgressHandler(mlc, new IDebugProtocolClient() {}, fo.toURI().toString()); - progressHandler.displaySuiteRunning(progressHandler, "TestSuiteName"); FileObject projectDir = fo; Project project = new Project() { @Override @@ -98,6 +97,10 @@ public FileObject find(String filename) { }); } }; + TestProgressHandler progressHandler = new TestProgressHandler(mlc, new IDebugProtocolClient() {}, fo.toURI().toString()); + String moduleName = ProjectUtils.getInformation(project).getDisplayName(); + final ModuleInfo moduleInfo = new ModuleInfo(moduleName, List.of(project.getProjectDirectory().getPath())); + progressHandler.displaySuiteRunning(moduleInfo, "TestSuiteName"); Report report = new Report("TestSuiteName", project); TestSession session = new TestSession("TestSession", project, TestSession.SessionType.TEST); Testcase[] tests = new Testcase[] { @@ -117,7 +120,7 @@ public FileObject find(String filename) { report.setTotalTests(2); report.setPassed(1); report.setFailures(1); - progressHandler.displayReport(progressHandler, report); + progressHandler.displayReport(moduleInfo, report); assertEquals("Two messages", 2, msgs.size()); assertEquals(fo.toURI().toString(), msgs.get(0).getUri()); TestSuiteInfo suite = msgs.get(0).getSuite(); diff --git a/java/java.lsp.server/vscode/src/extension.ts b/java/java.lsp.server/vscode/src/extension.ts index 779e3e0cdd6a..dbd6de6b40e1 100644 --- a/java/java.lsp.server/vscode/src/extension.ts +++ b/java/java.lsp.server/vscode/src/extension.ts @@ -70,11 +70,11 @@ import { shouldHideGuideFor } from './panels/guidesUtil'; const API_VERSION : string = "1.0"; export const COMMAND_PREFIX : string = "nbls"; const DATABASE: string = 'Database'; -const listeners = new Map(); +export const listeners = new Map(); export let client: Promise; export let clientRuntimeJDK : string | null = null; export const MINIMAL_JDK_VERSION = 17; - +export const TEST_PROGRESS_EVENT: string = "testProgress"; let testAdapter: NbTestAdapter | undefined; let nbProcess : ChildProcess | null = null; let debugPort: number = -1; @@ -458,7 +458,10 @@ class LineBufferingPseudoterminal implements vscode.Pseudoterminal { pty: this, }); } - this.terminal.show(true); + // Prevent 'stealing' of the focus when running tests in parallel + if (!testAdapter?.testInParallelProfileExist()) { + this.terminal.show(true); + } } /** @@ -882,7 +885,7 @@ export function activate(context: ExtensionContext): VSNetBeansAPI { }); } - const runDebug = async (noDebug: boolean, testRun: boolean, uri: any, methodName?: string, launchConfiguration?: string, project : boolean = false, ) => { + const runDebug = async (noDebug: boolean, testRun: boolean, uri: any, methodName?: string, nestedClass?: string, launchConfiguration?: string, project : boolean = false, testInParallel : boolean = false, projects: string[] | undefined = undefined) => { const docUri = contextUri(uri); if (docUri) { // attempt to find the active configuration in the vsode launch settings; undefined if no config is there. @@ -894,6 +897,9 @@ export function activate(context: ExtensionContext): VSNetBeansAPI { if (methodName) { debugConfig['methodName'] = methodName; } + if (nestedClass) { + debugConfig['nestedClass'] = nestedClass; + } if (launchConfiguration == '') { if (debugConfig['launchConfiguration']) { delete debugConfig['launchConfiguration']; @@ -914,8 +920,13 @@ export function activate(context: ExtensionContext): VSNetBeansAPI { const debugOptions : vscode.DebugSessionOptions = { noDebug: noDebug, } - - + if (testInParallel) { + debugConfig['testInParallel'] = testInParallel; + } + if (projects?.length) { + debugConfig['projects'] = projects; + } + const ret = await vscode.debug.startDebugging(workspaceFolder, debugConfig, debugOptions); return ret ? new Promise((resolve) => { const listener = vscode.debug.onDidTerminateDebugSession(() => { @@ -925,30 +936,38 @@ export function activate(context: ExtensionContext): VSNetBeansAPI { }) : ret; } }; - - context.subscriptions.push(commands.registerCommand(COMMAND_PREFIX + '.run.test', async (uri, methodName?, launchConfiguration?) => { - await runDebug(true, true, uri, methodName, launchConfiguration); + + context.subscriptions.push(commands.registerCommand(COMMAND_PREFIX + '.run.test.parallel', async (projects?) => { + testAdapter?.runTestsWithParallelParallel(projects); + })); + + context.subscriptions.push(commands.registerCommand(COMMAND_PREFIX + '.run.test.parallel.createProfile', async (projects?) => { + testAdapter?.registerRunInParallelProfile(projects); + })); + + context.subscriptions.push(commands.registerCommand(COMMAND_PREFIX + '.run.test', async (uri, methodName?, nestedClass?, launchConfiguration?, testInParallel?, projects?) => { + await runDebug(true, true, uri, methodName, nestedClass, launchConfiguration, false, testInParallel, projects); })); - context.subscriptions.push(commands.registerCommand(COMMAND_PREFIX + '.debug.test', async (uri, methodName?, launchConfiguration?) => { - await runDebug(false, true, uri, methodName, launchConfiguration); + context.subscriptions.push(commands.registerCommand(COMMAND_PREFIX + '.debug.test', async (uri, methodName?, nestedClass?, launchConfiguration?) => { + await runDebug(false, true, uri, methodName, nestedClass, launchConfiguration); })); - context.subscriptions.push(commands.registerCommand(COMMAND_PREFIX + '.run.single', async (uri, methodName?, launchConfiguration?) => { - await runDebug(true, false, uri, methodName, launchConfiguration); + context.subscriptions.push(commands.registerCommand(COMMAND_PREFIX + '.run.single', async (uri, methodName?, nestedClass?, launchConfiguration?) => { + await runDebug(true, false, uri, methodName, nestedClass, launchConfiguration); })); - context.subscriptions.push(commands.registerCommand(COMMAND_PREFIX + '.debug.single', async (uri, methodName?, launchConfiguration?) => { - await runDebug(false, false, uri, methodName, launchConfiguration); + context.subscriptions.push(commands.registerCommand(COMMAND_PREFIX + '.debug.single', async (uri, methodName?, nestedClass?, launchConfiguration?) => { + await runDebug(false, false, uri, methodName, nestedClass, launchConfiguration); })); context.subscriptions.push(commands.registerCommand(COMMAND_PREFIX + '.project.run', async (node, launchConfiguration?) => { - return runDebug(true, false, contextUri(node)?.toString() || '', undefined, launchConfiguration, true); + return runDebug(true, false, contextUri(node)?.toString() || '', undefined, undefined, launchConfiguration, true); })); context.subscriptions.push(commands.registerCommand(COMMAND_PREFIX + '.project.debug', async (node, launchConfiguration?) => { - return runDebug(false, false, contextUri(node)?.toString() || '', undefined, launchConfiguration, true); + return runDebug(false, false, contextUri(node)?.toString() || '', undefined, undefined, launchConfiguration, true); })); context.subscriptions.push(commands.registerCommand(COMMAND_PREFIX + '.project.test', async (node, launchConfiguration?) => { - return runDebug(true, true, contextUri(node)?.toString() || '', undefined, launchConfiguration, true); + return runDebug(true, true, contextUri(node)?.toString() || '', undefined, undefined, launchConfiguration, true); })); context.subscriptions.push(commands.registerCommand(COMMAND_PREFIX + '.package.test', async (uri, launchConfiguration?) => { - await runDebug(true, true, uri, undefined, launchConfiguration); + await runDebug(true, true, uri, undefined, undefined, launchConfiguration); })); context.subscriptions.push(commands.registerCommand(COMMAND_PREFIX + '.open.stacktrace', async (uri, methodName, fileName, line) => { const location: string | undefined = uri ? await commands.executeCommand(COMMAND_PREFIX + '.resolve.stacktrace.location', uri, methodName, fileName) : undefined; @@ -1598,6 +1617,10 @@ function doActivateWithJDK(specifiedJDK: string | null, context: ExtensionContex return data; }); c.onNotification(TestProgressNotification.type, param => { + const testProgressListeners = listeners.get(TEST_PROGRESS_EVENT); + testProgressListeners?.forEach(listener => { + commands.executeCommand(listener, param.suite); + }) if (testAdapter) { testAdapter.testProgress(param.suite); } diff --git a/java/java.lsp.server/vscode/src/protocol.ts b/java/java.lsp.server/vscode/src/protocol.ts index 0a1856f0ed40..39682b671474 100644 --- a/java/java.lsp.server/vscode/src/protocol.ts +++ b/java/java.lsp.server/vscode/src/protocol.ts @@ -158,6 +158,8 @@ export interface TestProgressParams { export interface TestSuite { name: string; + moduleName?: string; + modulePath?: string; file?: string; range?: Range; state: 'loaded' | 'started' | 'passed' | 'failed' | 'skipped' | 'errored'; diff --git a/java/java.lsp.server/vscode/src/testAdapter.ts b/java/java.lsp.server/vscode/src/testAdapter.ts index fb5563e94021..976de6647226 100644 --- a/java/java.lsp.server/vscode/src/testAdapter.ts +++ b/java/java.lsp.server/vscode/src/testAdapter.ts @@ -18,10 +18,12 @@ */ 'use strict'; -import { commands, debug, tests, workspace, CancellationToken, TestController, TestItem, TestRunProfileKind, TestRunRequest, Uri, TestRun, TestMessage, Location, Position, MarkdownString } from "vscode"; +import { commands, debug, tests, workspace, CancellationToken, TestController, TestItem, TestRunProfileKind, TestRunRequest, Uri, TestRun, TestMessage, Location, Position, MarkdownString, TestRunProfile, CancellationTokenSource } from "vscode"; import * as path from 'path'; import { asRange, TestCase, TestSuite } from "./protocol"; -import { COMMAND_PREFIX } from "./extension"; +import { COMMAND_PREFIX, listeners, TEST_PROGRESS_EVENT } from "./extension"; + +type SuiteState = 'enqueued' | 'started' | 'passed' | 'failed' | 'skipped' | 'errored'; export class NbTestAdapter { @@ -30,6 +32,8 @@ export class NbTestAdapter { private currentRun: TestRun | undefined; private itemsToRun: Set | undefined; private started: boolean = false; + private suiteStates: Map; + private parallelRunProfile: TestRunProfile | undefined; constructor() { this.testController = tests.createTestController('apacheNetBeansController', 'Apache NetBeans'); @@ -38,6 +42,24 @@ export class NbTestAdapter { this.testController.createRunProfile('Debug Tests', TestRunProfileKind.Debug, runHandler); this.disposables.push(this.testController); this.load(); + this.suiteStates = new Map(); + } + + public registerRunInParallelProfile(projects: string[]) { + const runHandler = (request: TestRunRequest, cancellation: CancellationToken) => this.run(request, cancellation, true, projects); + this.parallelRunProfile = this.testController.createRunProfile("Run Tests In Parallel", TestRunProfileKind.Run, runHandler, true); + this.testController.items.replace([]); + this.load(); + } + + public testInParallelProfileExist(): boolean { + return this.parallelRunProfile ? true : false; + } + + public runTestsWithParallelParallel(projects?: string[]) { + if (this.parallelRunProfile) { + this.run(new TestRunRequest(undefined, undefined, this.parallelRunProfile), new CancellationTokenSource().token, true, projects); + } } async load(): Promise { @@ -51,9 +73,11 @@ export class NbTestAdapter { } } - async run(request: TestRunRequest, cancellation: CancellationToken): Promise { + async run(request: TestRunRequest, cancellation: CancellationToken, testInParallel: boolean = false, projects?: string[]): Promise { if (!this.currentRun) { - commands.executeCommand('workbench.debug.action.focusRepl'); + if (!testInParallel) { + commands.executeCommand('workbench.debug.action.focusRepl'); + } cancellation.onCancellationRequested(() => this.cancel()); this.currentRun = this.testController.createTestRun(request); this.itemsToRun = new Set(); @@ -63,9 +87,23 @@ export class NbTestAdapter { for (let item of include) { if (item.uri) { this.set(item, 'enqueued'); + item.parent?.children.forEach(child => { + if (child.id?.includes(item.id)) { + this.set(child, 'enqueued'); + } + }) const idx = item.id.indexOf(':'); + const isNestedClass = item.id.includes('$'); + const topLevelClassName = item.id.lastIndexOf('.'); + let nestedClass: string | undefined; + if (isNestedClass && topLevelClassName > 0) { + nestedClass = idx < 0 + ? item.id.slice(topLevelClassName + 1) + : item.id.substring(topLevelClassName + 1, idx); + nestedClass = nestedClass.replace('$', '.'); + } if (!cancellation.isCancellationRequested) { - await commands.executeCommand(request.profile?.kind === TestRunProfileKind.Debug ? COMMAND_PREFIX + '.debug.single' : COMMAND_PREFIX + '.run.single', item.uri.toString(), idx < 0 ? undefined : item.id.slice(idx + 1)); + await commands.executeCommand(request.profile?.kind === TestRunProfileKind.Debug ? COMMAND_PREFIX + '.debug.single' : COMMAND_PREFIX + '.run.single', item.uri.toString(), idx < 0 ? undefined : item.id.slice(idx + 1), nestedClass); } } } @@ -73,12 +111,20 @@ export class NbTestAdapter { this.testController.items.forEach(item => this.set(item, 'enqueued')); for (let workspaceFolder of workspace.workspaceFolders || []) { if (!cancellation.isCancellationRequested) { - await commands.executeCommand(request.profile?.kind === TestRunProfileKind.Debug ? COMMAND_PREFIX + '.debug.test': COMMAND_PREFIX + '.run.test', workspaceFolder.uri.toString()); + if (testInParallel) { + await commands.executeCommand(COMMAND_PREFIX + '.run.test', workspaceFolder.uri.toString(), undefined, undefined, undefined, true, projects); + } else { + await commands.executeCommand(request.profile?.kind === TestRunProfileKind.Debug ? COMMAND_PREFIX + '.debug.test': COMMAND_PREFIX + '.run.test', workspaceFolder.uri.toString()); + } } } } if (this.started) { this.itemsToRun.forEach(item => this.set(item, 'skipped')); + } + // TBD - message + else { + this.itemsToRun.forEach(item => this.set(item, 'failed', new TestMessage('Build failure'), false, true)); } this.itemsToRun = undefined; this.currentRun.end(); @@ -86,30 +132,79 @@ export class NbTestAdapter { } } - set(item: TestItem, state: 'enqueued' | 'started' | 'passed' | 'failed' | 'skipped' | 'errored', message?: TestMessage | readonly TestMessage[], noPassDown? : boolean): void { + set(item: TestItem, state: SuiteState, message?: TestMessage | readonly TestMessage[], noPassDown? : boolean, dispatchBuildFailEvent?: boolean): void { if (this.currentRun) { switch (state) { case 'enqueued': + this.dispatchTestEvent(state, item); this.itemsToRun?.add(item); this.currentRun.enqueued(item); break; + case 'skipped': + this.dispatchTestEvent(state, item); case 'started': case 'passed': - case 'skipped': this.itemsToRun?.delete(item); this.currentRun[state](item); break; case 'failed': case 'errored': + if (dispatchBuildFailEvent) { + this.dispatchTestEvent(state, item); + } this.itemsToRun?.delete(item); this.currentRun[state](item, message || new TestMessage("")); break; } + this.suiteStates.set(item, state); if (!noPassDown) { - item.children.forEach(child => this.set(child, state, message, noPassDown)); + item.children.forEach(child => this.set(child, state, message, noPassDown, dispatchBuildFailEvent)); } } } + + dispatchTestEvent(state: SuiteState, testItem: TestItem): void { + if (testItem.parent && testItem.children.size > 0) { + this.dispatchEvent({ + name: testItem.id, + moduleName: testItem.parent.id, + modulePath: testItem.parent.uri?.path, + state, + }); + } else if (testItem.children.size === 0) { + const testSuite = testItem.parent; + if (testSuite) { + const testSuiteEvent: any = { + name: testSuite.id, + moduleName: testSuite.parent?.id, + modulePath: testSuite.parent?.uri?.path, + state, + tests: [] + } + testSuite?.children.forEach(suite => { + if (suite.id === testItem.id) { + const idx = suite.id.indexOf(':'); + if (idx >= 0) { + const name = suite.id.slice(idx + 1); + testSuiteEvent.tests?.push({ + id: suite.id, + name, + state + }) + } + } + }) + this.dispatchEvent(testSuiteEvent); + } + } + } + + dispatchEvent(event: any): void { + const testProgressListeners = listeners.get(TEST_PROGRESS_EVENT); + testProgressListeners?.forEach(listener => { + commands.executeCommand(listener, event); + }) + } cancel(): void { debug.stopDebugging(); @@ -130,7 +225,9 @@ export class NbTestAdapter { } testProgress(suite: TestSuite): void { - const currentSuite = this.testController.items.get(suite.name); + const currentModule = this.testController.items.get(this.getModuleItemId(suite.moduleName)); + const currentSuite = currentModule?.children.get(suite.name); + switch (suite.state) { case 'loaded': this.updateTests(suite); @@ -147,7 +244,7 @@ export class NbTestAdapter { case 'skipped': if (suite.tests) { this.updateTests(suite, true); - if (currentSuite) { + if (currentSuite && currentModule) { const suiteMessages: TestMessage[] = []; suite.tests?.forEach(test => { if (this.currentRun) { @@ -198,19 +295,42 @@ export class NbTestAdapter { } else { this.set(currentSuite, suite.state, undefined, true); } + this.set(currentModule, this.calculateStateFor(currentModule), undefined, true); } } break; } } + calculateStateFor(testItem: TestItem): 'passed' | 'failed' | 'skipped' | 'errored' { + let passed: number = 0; + testItem.children.forEach(item => { + const state = this.suiteStates.get(item); + if (state === 'enqueued' || state === 'failed') return state; + if (state === 'passed') passed++; + }) + if (passed > 0) return 'passed'; + return 'skipped'; + } + updateTests(suite: TestSuite, testExecution?: boolean): void { - let currentSuite = this.testController.items.get(suite.name); + const moduleName = this.getModuleItemId(suite.moduleName); + let currentModule = this.testController.items.get(moduleName); + if (!currentModule) { + const parsedName = this.parseModuleName(moduleName); + currentModule = this.testController.createTestItem(moduleName, this.getNameWithIcon(parsedName, 'module'), this.getModulePath(suite)); + this.testController.items.add(currentModule); + } + + const suiteChildren: TestItem[] = [] + let currentSuite = currentModule.children.get(suite.name); const suiteUri = suite.file ? Uri.parse(suite.file) : undefined; if (!currentSuite || suiteUri && currentSuite.uri?.toString() !== suiteUri.toString()) { - currentSuite = this.testController.createTestItem(suite.name, suite.name, suiteUri); - this.testController.items.add(currentSuite); + currentSuite = this.testController.createTestItem(suite.name, this.getNameWithIcon(suite.name, 'class'), suiteUri); + suiteChildren.push(currentSuite); } + currentModule.children.forEach(suite => suiteChildren.push(suite)); + const suiteRange = asRange(suite.range); if (!testExecution && suiteRange && suiteRange !== currentSuite.range) { currentSuite.range = suiteRange; @@ -222,7 +342,7 @@ export class NbTestAdapter { const testUri = test.file ? Uri.parse(test.file) : undefined; if (currentTest) { if (testUri && currentTest.uri?.toString() !== testUri?.toString()) { - currentTest = this.testController.createTestItem(test.id, test.name, testUri); + currentTest = this.testController.createTestItem(test.id, this.getNameWithIcon(test.name, 'method'), testUri); currentSuite?.children.add(currentTest); } const testRange = asRange(test.range); @@ -246,10 +366,10 @@ export class NbTestAdapter { parentTests.set(parent.test, arr = []); children.push(parent.test); } - arr.push(this.testController.createTestItem(test.id, parent.label)); + arr.push(this.testController.createTestItem(test.id, this.getNameWithIcon(parent.label, 'method'))); } } else { - currentTest = this.testController.createTestItem(test.id, test.name, testUri); + currentTest = this.testController.createTestItem(test.id, this.getNameWithIcon(test.name, 'method'), testUri); currentTest.range = asRange(test.range); children.push(currentTest); currentSuite?.children.add(currentTest); @@ -265,14 +385,53 @@ export class NbTestAdapter { }); } else { currentSuite.children.replace(children); + currentModule.children.replace(suiteChildren); } } + getModuleItemId(moduleName?: string): string { + return moduleName?.replace(":", "-") || ""; + } + + parseModuleName(moduleName: string): string { + if (!this.parallelRunProfile) { + return moduleName.replace(":", "-"); + } + const index = moduleName.indexOf(":"); + if (index !== -1) { + return moduleName.slice(index + 1); + } + const parts = moduleName.split("-"); + return parts[parts.length - 1]; + } + + getModulePath(suite: TestSuite): Uri { + return Uri.parse(suite.modulePath || ""); + } + + getNameWithIcon(itemName: string, itemType: 'module' | 'class' | 'method'): string { + switch (itemType) { + case 'module': + return `$(project) ${itemName}`; + case 'class': + return `$(symbol-class) ${itemName}`; + case 'method': + return `$(symbol-method) ${itemName}`; + default: + return itemName; + } + } + + getNameWithoutIcon(itemName: string): string { + return itemName.replace(/^\$\([^)]+\)\s*/, ""); + } + subTestName(item: TestItem, test: TestCase): string | undefined { if (test.id.startsWith(item.id)) { let label = test.name; - if (label.startsWith(item.label)) { - label = label.slice(item.label.length).trim(); + const nameWithoutIcon = this.getNameWithoutIcon(item.label); + if (label.startsWith(nameWithoutIcon)) { + label = label.slice(nameWithoutIcon.length).trim(); } return label; } else { diff --git a/java/java.source.base/apichanges.xml b/java/java.source.base/apichanges.xml index d473ad581c23..236e7eca85bc 100644 --- a/java/java.source.base/apichanges.xml +++ b/java/java.source.base/apichanges.xml @@ -25,6 +25,18 @@ Java Source API + + + SourceUtils.classNameFor nested classes support + + + + + + SourceUtils.classNameFor support for nested classes. + + + Adding SourceUtils.classNameFor diff --git a/java/java.source.base/nbproject/project.properties b/java/java.source.base/nbproject/project.properties index 9e2b1ca15845..31ef685fd842 100644 --- a/java/java.source.base/nbproject/project.properties +++ b/java/java.source.base/nbproject/project.properties @@ -23,7 +23,7 @@ javadoc.name=Java Source Base javadoc.title=Java Source Base javadoc.arch=${basedir}/arch.xml javadoc.apichanges=${basedir}/apichanges.xml -spec.version.base=2.73.0 +spec.version.base=2.74.0 test.qa-functional.cp.extra=${refactoring.java.dir}/modules/ext/nb-javac-api.jar test.unit.run.cp.extra=${o.n.core.dir}/core/core.jar:\ ${o.n.core.dir}/lib/boot.jar:\ diff --git a/java/java.source.base/nbproject/project.xml b/java/java.source.base/nbproject/project.xml index 7cc5ba6e5f51..9162fc869a8d 100644 --- a/java/java.source.base/nbproject/project.xml +++ b/java/java.source.base/nbproject/project.xml @@ -231,7 +231,7 @@ 1 - 1.13 + 1.99 diff --git a/java/java.source.base/src/org/netbeans/api/java/source/SourceUtils.java b/java/java.source.base/src/org/netbeans/api/java/source/SourceUtils.java index b61c1a075a78..b0a5a20889f0 100644 --- a/java/java.source.base/src/org/netbeans/api/java/source/SourceUtils.java +++ b/java/java.source.base/src/org/netbeans/api/java/source/SourceUtils.java @@ -114,6 +114,7 @@ import org.netbeans.modules.parsing.api.indexing.IndexingManager; import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport; import org.netbeans.spi.java.classpath.support.ClassPathSupport; +import org.netbeans.spi.project.NestedClass; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; @@ -1477,14 +1478,21 @@ public static void forceSource(CompilationController cc, FileObject file) { * * @param info the ClasspathInfo used to resolve * @param relativePath input source file path relative to the corresponding source root + * @param nestedClass nested class which name is searched * @return class name for the corresponding input source file - * @since 2.73 + * @since 2.74 */ - public static String classNameFor(ClasspathInfo info, String relativePath) { + public static String classNameFor(ClasspathInfo info, String relativePath, NestedClass nestedClass) { ClassPath cachedCP = ClasspathInfoAccessor.getINSTANCE().getCachedClassPath(info, PathKind.COMPILE); int idx = relativePath.indexOf('.'); String rel = idx < 0 ? relativePath : relativePath.substring(0, idx); String className = rel.replace('/', '.'); + int lastDotIndex = className.lastIndexOf('.'); + String fqnForNestedClass = null; + if (lastDotIndex > -1 && nestedClass != null) { + String packageName = className.substring(0, lastDotIndex); + fqnForNestedClass = nestedClass.getFQN(packageName, "$"); + } FileObject rsFile = cachedCP.findResource(rel + '.' + FileObjects.RS); if (rsFile != null) { List lines = new ArrayList<>(); @@ -1493,6 +1501,8 @@ public static String classNameFor(ClasspathInfo info, String relativePath) { while ((line = in.readLine())!=null) { if (className.equals(line)) { return className; + } else if (fqnForNestedClass != null && fqnForNestedClass.equals(line)) { + return line; } lines.add(line); } diff --git a/java/maven.junit/manifest.mf b/java/maven.junit/manifest.mf index 0617eb31f4a6..d154e282e116 100644 --- a/java/maven.junit/manifest.mf +++ b/java/maven.junit/manifest.mf @@ -2,4 +2,4 @@ Manifest-Version: 1.0 OpenIDE-Module: org.netbeans.modules.maven.junit/1 OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/maven/junit/Bundle.properties AutoUpdate-Show-In-Client: false -OpenIDE-Module-Specification-Version: 1.61 +OpenIDE-Module-Specification-Version: 1.62 diff --git a/java/maven.junit/nbproject/project.xml b/java/maven.junit/nbproject/project.xml index 9a35f1acff6d..9d92e0b37134 100644 --- a/java/maven.junit/nbproject/project.xml +++ b/java/maven.junit/nbproject/project.xml @@ -48,7 +48,7 @@ - 1.0 + 2.74 diff --git a/java/maven.junit/src/org/netbeans/modules/maven/junit/JUnitOutputListenerProvider.java b/java/maven.junit/src/org/netbeans/modules/maven/junit/JUnitOutputListenerProvider.java index 7d9487b5a39d..ab23a1ca6ae1 100644 --- a/java/maven.junit/src/org/netbeans/modules/maven/junit/JUnitOutputListenerProvider.java +++ b/java/maven.junit/src/org/netbeans/modules/maven/junit/JUnitOutputListenerProvider.java @@ -24,11 +24,13 @@ import java.text.ParseException; import java.util.ArrayList; import java.util.Collection; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.StringTokenizer; import java.util.logging.Level; @@ -83,13 +85,11 @@ public class JUnitOutputListenerProvider implements OutputProcessor { private static final String TESTTYPE_UNIT = "UNIT"; //NOI81N private static final String TESTTYPE_INTEGRATION = "INTEGRATION"; //NOI81N - private TestSession session; private String testType; private String reportNameSuffix; private final Pattern runningPattern; private final Pattern outDirPattern2; private final Pattern outDirPattern; - private File outputDir; private String runningTestClass; private final Set usedNames; private final long startTimeStamp; @@ -97,8 +97,10 @@ public class JUnitOutputListenerProvider implements OutputProcessor { private static final Logger LOG = Logger.getLogger(JUnitOutputListenerProvider.class.getName()); private final RunConfig config; private boolean surefireRunningInParallel = false; - private ArrayList runningTestClasses; - private ArrayList runningTestClassesInParallel; + private final Map> runningTestClass2outputDirs = new HashMap<>(); + private final ArrayList runningTestClassesInParallel = new ArrayList<>(); + private final Map project2outputDirs = new HashMap<>(); + private final Map outputDir2sessions = new HashMap<>(); private static final String GROUP_FILE_NAME = "dir"; @@ -109,23 +111,21 @@ public JUnitOutputListenerProvider(RunConfig config) { this.config = config; usedNames = new HashSet<>(); startTimeStamp = System.currentTimeMillis(); - runningTestClasses = new ArrayList<>(); - runningTestClassesInParallel = new ArrayList<>(); surefireRunningInParallel = isSurefireRunningInParallel(); } private boolean isSurefireRunningInParallel() { // http://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html // http://maven.apache.org/surefire/maven-surefire-plugin/examples/fork-options-and-parallel-execution.html - String parallel = PluginPropertyUtils.getPluginProperty(config.getMavenProject(), + String parallel = config.getProperties().containsKey("parallel") ? config.getProperties().get("parallel") : PluginPropertyUtils.getPluginProperty(config.getMavenProject(), Constants.GROUP_APACHE_PLUGINS, Constants.PLUGIN_SUREFIRE, "parallel", "test", "parallel"); //NOI18N if (parallel != null) { return true; } - String forkMode = PluginPropertyUtils.getPluginProperty(config.getMavenProject(), + String forkMode = config.getProperties().containsKey("forkMode") ? config.getProperties().get("forkMode") : PluginPropertyUtils.getPluginProperty(config.getMavenProject(), Constants.GROUP_APACHE_PLUGINS, Constants.PLUGIN_SUREFIRE, "forkMode", "test", "forkMode"); //NOI18N if ("perthread".equals(forkMode)) { - String threadCount = PluginPropertyUtils.getPluginProperty(config.getMavenProject(), + String threadCount = config.getProperties().containsKey("threadCount") ? config.getProperties().get("threadCount") : PluginPropertyUtils.getPluginProperty(config.getMavenProject(), Constants.GROUP_APACHE_PLUGINS, Constants.PLUGIN_SUREFIRE, "threadCount", "test", "threadCount"); if (threadCount != null) { if (Integer.parseInt(threadCount) > 1) { @@ -133,7 +133,7 @@ private boolean isSurefireRunningInParallel() { } } } - String forkCount = PluginPropertyUtils.getPluginProperty(config.getMavenProject(), + String forkCount = config.getProperties().containsKey("forkCount") ? config.getProperties().get("forkCount") : PluginPropertyUtils.getPluginProperty(config.getMavenProject(), Constants.GROUP_APACHE_PLUGINS, Constants.PLUGIN_SUREFIRE, "forkCount", "test", "forkCount"); if (forkCount != null) { int index = forkCount.indexOf("C"); @@ -167,22 +167,20 @@ private boolean isSurefireRunningInParallel() { public @Override void processLine(String line, OutputVisitor visitor) { Matcher match = outDirPattern.matcher(line); if (match.matches()) { - outputDir = new File(match.group(GROUP_FILE_NAME)); - if (session == null) { - createSession(outputDir); - } + File outputDir = new File(match.group(GROUP_FILE_NAME)); + LOG.log(Level.FINER, "Line matches reports directory: {0}", outputDir); + createSession(outputDir); return; } match = outDirPattern2.matcher(line); if (match.matches()) { - outputDir = new File(match.group(GROUP_FILE_NAME)); - if (session == null) { - createSession(outputDir); - } + File outputDir = new File(match.group(GROUP_FILE_NAME)); + LOG.log(Level.FINER, "Line matches reports directory: {0}", outputDir); + createSession(outputDir); return; } - - if (session == null) { + + if (outputDir2sessions.isEmpty()) { return; } match = runningPattern.matcher(line); @@ -190,12 +188,18 @@ private boolean isSurefireRunningInParallel() { if (surefireRunningInParallel) { // make sure results are displayed in case of a failure runningTestClassesInParallel.add(match.group(1)); + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "Got running test: line {0}, class: {1}. Parallel run: {2}", new Object[] { line, match.group(1), + runningTestClassesInParallel.toString() }); + } } else { - if (runningTestClass != null && outputDir != null) { + if (runningTestClass != null) { + LOG.log(Level.FINE, "Got running test SINGLE: line {0}, class: {1}", new Object[] { line, match.group(1) }); // match.group(1) should be the FQN of a running test class but let's check to be on the safe side // If the matcher matches it means that we have a new test class running, // if not it probably means that this is user's text, e.g. "Running my cool test", so we can safely ignore it if (!isFullJavaId(match.group(1))) { + LOG.log(Level.FINE, "Not full java id match!"); return; } // tests are running sequentially, so update Test Results Window @@ -207,10 +211,10 @@ private boolean isSurefireRunningInParallel() { match = testSuiteStatsPattern.matcher(line); if (match.matches() && surefireRunningInParallel) { runningTestClass = match.group(6); - if (runningTestClass != null && outputDir != null && !runningTestClasses.contains(runningTestClass)) { - // When using reuseForks=true and a forkCount value larger than one, - // the same output is produced many times, so show it only once in Test Results window - runningTestClasses.add(runningTestClass); + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "Got test statistics: {0}, running classes: {1}", new Object[] { match.group(6), runningTestClassesInParallel.toString() }); + } + if (runningTestClass != null) { // runningTestClass should be the FQN of a running test class but let's check to be on the safe side // If the matcher matches it means that we have a new test class running, // if not it probably means that this is user's text, e.g. "Running my cool test", so we can safely ignore it @@ -222,12 +226,15 @@ private boolean isSurefireRunningInParallel() { runningTestClassesInParallel.remove(runningTestClass); // runningTestClass might be the last one so make it null to avoid appearing twice when sequenceEnd() is called runningTestClass = null; + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "Statistics done for {0}. Cleaning runnintTestClass, stillRunning: {1}", new Object[] { match.group(6), runningTestClassesInParallel.toString() }); + } } } } private static final String SECONDS_REGEX = "s(?:ec(?:ond)?(?:s|\\(s\\))?)?"; //NOI18N - private static final String TESTSUITE_STATS_REGEX = "Tests run: +([0-9]+), +Failures: +([0-9]+), +Errors: +([0-9]+), +Skipped: +([0-9]+), +Time elapsed: +(.+)" + SECONDS_REGEX + " - in (.*)"; + private static final String TESTSUITE_STATS_REGEX = "Tests run: +([0-9]+), +Failures: +([0-9]+), +Errors: +([0-9]+), +Skipped: +([0-9]+), +Time elapsed: +(.+)" + SECONDS_REGEX + " *(?: * <+ FAILURE! *)?-+ in (.*)"; private static final Pattern testSuiteStatsPattern = Pattern.compile(TESTSUITE_STATS_REGEX); static boolean isTestSuiteStats(String line) { @@ -250,7 +257,6 @@ static boolean isFullJavaId(String possibleNewRunningTestClass) { } public @Override void sequenceStart(String sequenceId, OutputVisitor visitor) { - session = null; reportNameSuffix = null; testType = null; String reportsDirectory = null; @@ -280,6 +286,7 @@ static boolean isFullJavaId(String possibleNewRunningTestClass) { testType = TESTTYPE_INTEGRATION; //NOI81N } if (null != reportsDirectory) { + File outputDir = null; File absoluteFile = new File(reportsDirectory); // configuration might be "target/directory", which is relative // to the maven project or an absolute path @@ -309,6 +316,7 @@ static boolean isFullJavaId(String possibleNewRunningTestClass) { } if (null != outputDir) { createSession(outputDir); + project2outputDirs.put(visitor.getContext().getCurrentProject(), outputDir); } } } @@ -388,59 +396,63 @@ private CoreManager getManagerProvider() { } private void createSession(File nonNormalizedFile) { - if (session == null) { + if (!outputDir2sessions.containsKey(nonNormalizedFile)) { File fil = FileUtil.normalizeFile(nonNormalizedFile); - Project prj = FileOwnerQuery.getOwner(Utilities.toURI(fil)); - if (prj != null) { - NbMavenProject mvnprj = prj.getLookup().lookup(NbMavenProject.class); - if (mvnprj != null) { + Project prj = FileOwnerQuery.getOwner(Utilities.toURI(fil)); + LOG.log(Level.FINE, "Creating session for project {0}", prj); + if (prj != null) { + NbMavenProject mvnprj = prj.getLookup().lookup(NbMavenProject.class); + if (mvnprj != null) { + LOG.log(Level.FINE, "Maven project instance: {0}", mvnprj); File projectFile = FileUtil.toFile(prj.getProjectDirectory()); if (projectFile != null) { UnitTestsUsage.getInstance().logUnitTestUsage(Utilities.toURI(projectFile), getJUnitVersion(config.getMavenProject())); } - TestSession.SessionType type = TestSession.SessionType.TEST; - String action = config.getActionName(); - if (action != null) { //custom - if (action.contains("debug")) { //NOI81N - type = TestSession.SessionType.DEBUG; - } - } - final TestSession.SessionType fType = type; + TestSession.SessionType type = TestSession.SessionType.TEST; + String action = config.getActionName(); + if (action != null) { //custom + if (action.contains("debug")) { //NOI81N + type = TestSession.SessionType.DEBUG; + } + } + final TestSession.SessionType fType = type; CoreManager junitManager = getManagerProvider(); if (junitManager != null) { junitManager.registerNodeFactory(); } - session = new TestSession(createSessionName(mvnprj.getMavenProject().getId()), prj, TestSession.SessionType.TEST); - session.setRerunHandler(new RerunHandler() { - public @Override - void rerun() { - RunUtils.executeMaven(config); - } + TestSession session = new TestSession(createSessionName(mvnprj.getMavenProject().getId()), prj, TestSession.SessionType.TEST); + outputDir2sessions.put(nonNormalizedFile, session); + LOG.log(Level.FINE, "Created session: {0}", session); + session.setRerunHandler(new RerunHandler() { + public @Override + void rerun() { + RunUtils.executeMaven(config); + } - public @Override - void rerun(Set tests) { - RunConfig brc = RunUtils.cloneRunConfig(config); - StringBuilder tst = new StringBuilder(); - Map> methods = new HashMap<>(); + public @Override + void rerun(Set tests) { + RunConfig brc = RunUtils.cloneRunConfig(config); + StringBuilder tst = new StringBuilder(); + Map> methods = new HashMap<>(); //#222776 calculate the approximate space the failed tests will occupy on the cmd line. //important on windows which places a limit on the length. int windowslimitcount = 0; - for (Testcase tc : tests) { - //TODO just when is the classname null?? - if (tc.getClassName() != null) { - Collection lst = methods.get(tc.getClassName()); - if (lst == null) { - lst = new ArrayList<>(); - methods.put(tc.getClassName(), lst); + for (Testcase tc : tests) { + //TODO just when is the classname null?? + if (tc.getClassName() != null) { + Collection lst = methods.get(tc.getClassName()); + if (lst == null) { + lst = new ArrayList<>(); + methods.put(tc.getClassName(), lst); windowslimitcount = windowslimitcount + tc.getClassName().length() + 1; // + 1 for , - } - lst.add(tc.getName()); + } + lst.add(tc.getName()); windowslimitcount = windowslimitcount + tc.getName().length() + 1; // + 1 for # or + - } - } + } + } boolean exceedsWindowsLimit = Utilities.isWindows() && windowslimitcount > 6000; //just be conservative here, the limit is more (8000+) - for (Map.Entry> ent : methods.entrySet()) { - tst.append(","); + for (Map.Entry> ent : methods.entrySet()) { + tst.append(","); if (exceedsWindowsLimit) { String clazzName = ent.getKey(); int lastDot = ent.getKey().lastIndexOf("."); @@ -452,58 +464,60 @@ void rerun(Set tests) { tst.append(ent.getKey()); } - //#name only in surefire > 2.7.2 and junit > 4.0 or testng - // bug works with the setting also for junit 3.x - tst.append("#"); - boolean first = true; - for (String meth : ent.getValue()) { - if (!first) { - tst.append("+"); - } - first = false; - tst.append(meth); - } - } - if (tst.length() > 0) { - brc.setProperty("test", tst.substring(1)); - } - RunUtils.executeMaven(brc); - } + //#name only in surefire > 2.7.2 and junit > 4.0 or testng + // bug works with the setting also for junit 3.x + tst.append("#"); + boolean first = true; + for (String meth : ent.getValue()) { + if (!first) { + tst.append("+"); + } + first = false; + tst.append(meth); + } + } + if (tst.length() > 0) { + brc.setProperty("test", tst.substring(1)); + } + RunUtils.executeMaven(brc); + } - public @Override - boolean enabled(RerunType type) { - //debug should now properly update debug port in runconfig... - if (fType.equals(TestSession.SessionType.TEST) || fType.equals(TestSession.SessionType.DEBUG)) { - if (RerunType.ALL.equals(type)) { - return true; - } - if (RerunType.CUSTOM.equals(type)) { - if (usingTestNG(config.getMavenProject())) { //#214334 test for testng has to come first, as itself depends on junit - return usingSurefire28(config.getMavenProject()); - } else if (usingJUnit4(config.getMavenProject())) { //#214334 - return usingSurefire2121(config.getMavenProject()); - } else if (getJUnitVersion(config.getMavenProject()).equals("JUNIT5")){ + public @Override + boolean enabled(RerunType type) { + //debug should now properly update debug port in runconfig... + if (fType.equals(TestSession.SessionType.TEST) || fType.equals(TestSession.SessionType.DEBUG)) { + if (RerunType.ALL.equals(type)) { + return true; + } + if (RerunType.CUSTOM.equals(type)) { + if (usingTestNG(config.getMavenProject())) { //#214334 test for testng has to come first, as itself depends on junit + return usingSurefire28(config.getMavenProject()); + } else if (usingJUnit4(config.getMavenProject())) { //#214334 + return usingSurefire2121(config.getMavenProject()); + } else if (getJUnitVersion(config.getMavenProject()).equals("JUNIT5")){ return usingSurefire2220(config.getMavenProject()); } - } - } - return false; - } + } + } + return false; + } - public @Override - void addChangeListener(ChangeListener listener) { - } + public @Override + void addChangeListener(ChangeListener listener) { + } - public @Override - void removeChangeListener(ChangeListener listener) { - } - }); - if (junitManager != null) { + public @Override + void removeChangeListener(ChangeListener listener) { + } + }); + if (junitManager != null) { junitManager.testStarted(session); } - } - } - } + } + } + } else { + LOG.log(Level.FINE, "Session for directory {0} already opened", nonNormalizedFile); + } } private boolean usingSurefire219(MavenProject prj) { @@ -570,22 +584,21 @@ private boolean usingJUnit4(MavenProject prj) { // SUREFIRE-724 } public @Override void sequenceEnd(String sequenceId, OutputVisitor visitor) { - if (session == null) { + if (outputDir2sessions.isEmpty()) { return; } - if (runningTestClass != null && outputDir != null) { + if (runningTestClass != null) { generateTest(); } - CoreManager junitManager = getManagerProvider(); - if (junitManager != null) { - junitManager.sessionFinished(session); + File outputDir = project2outputDirs.remove(visitor.getContext().getCurrentProject()); + TestSession session = outputDir != null ? outputDir2sessions.remove(outputDir) : null; + if (session != null) { + CoreManager junitManager = getManagerProvider(); + if (junitManager != null) { + junitManager.sessionFinished(session); + } } runningTestClass = null; - outputDir = null; - session = null; - surefireRunningInParallel = false; - runningTestClasses = null; - runningTestClassesInParallel = null; } private static final Pattern COMPARISON_PATTERN = Pattern.compile(".*expected:<(.*)> but was:<(.*)>$"); //NOI18N @@ -620,37 +633,63 @@ static Trouble constructTrouble(@NonNull String type, @NullAllowed String messag } public @Override void sequenceFail(String sequenceId, OutputVisitor visitor) { + LOG.log(Level.FINE, "Got sequenceFail: {0}, line {1}", new Object[] { visitor.getContext().getCurrentProject(), visitor.getLine() }); // try to get the failed test class. How can this be solved if it is not the first one in the list? if(surefireRunningInParallel) { - if(runningTestClassesInParallel.isEmpty()) { - // no test case is currently running, so do nothing (is this a more serious failure?) - return; + String saveRunningTestClass = runningTestClass; + + Project currentProject = visitor.getContext().getCurrentProject(); + for (String s : runningTestClassesInParallel) { + File outputDir = locateOutputDirAndWait(s, false); + // match the output dir to the project + if (outputDir != null) { + Project outputOwner = FileOwnerQuery.getOwner(FileUtil.toFileObject(outputDir)); + if (outputOwner == currentProject) { + LOG.log(Level.FINE, "Found unfinished test {0} in {1}, trying to finish", new Object[] { s, currentProject }); + runningTestClass = s; + if (Objects.equals(saveRunningTestClass, s)) { + saveRunningTestClass = null; + } + generateTest(); + } + } } - runningTestClass = runningTestClassesInParallel.get(0); + runningTestClass = saveRunningTestClass; } sequenceEnd(sequenceId, visitor); } + + private File locateOutputDirAndWait(String candidateClass, boolean consume) { + String suffix = reportNameSuffix == null ? "" : "-" + reportNameSuffix; + File outputDir = locateOutputDir(candidateClass, suffix, consume); + if (outputDir == null && surefireRunningInParallel) { + // try waiting a bit to give time for the result file to be created + try { + Thread.sleep(500); + } catch (InterruptedException ex) { + Exceptions.printStackTrace(ex); + } + outputDir = locateOutputDir(candidateClass, suffix, consume); + } + return outputDir; + } private void generateTest() { - String suffix = reportNameSuffix; - if (suffix == null) { - suffix = ""; - } else { - //#204480 - suffix = "-" + suffix; + LOG.log(Level.FINE, "generateTest called for class {0}, ", new Object[] { runningTestClass }); + File outputDir = locateOutputDirAndWait(runningTestClass, true); + if (outputDir == null) { + LOG.log(Level.WARNING, "Output directory is not created"); } - File report = new File(outputDir, "TEST-" + runningTestClass + suffix + ".xml"); - if (!report.isFile() || report.lastModified() < startTimeStamp) { //#219097 ignore results from previous invokation. - if(surefireRunningInParallel) { // try waiting a bit to give time for the result file to be created - try { - Thread.sleep(500); - } catch (InterruptedException ex) { - Exceptions.printStackTrace(ex); - } - } - if (!report.isFile() || report.lastModified() < startTimeStamp) { // and now try again - return; - } + String suffix = reportNameSuffix == null ? "" : "-" + reportNameSuffix; + File report = outputDir != null ? new File(outputDir, "TEST-" + runningTestClass + suffix + ".xml") : null; + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "Reading report file {0}, class {1}, timestamp {2}", new Object[] { report, runningTestClass, report == null ? -1 : + new Date(report.lastModified()) }); + } + TestSession session = outputDir != null ? outputDir2sessions.get(outputDir) : null; + if (outputDir == null || report == null || session == null) { + LOG.log(Level.FINE, "No session for outdir {0}", outputDir); + return; } if (report.length() > 50 * 1024 * 1024) { LOG.log(Level.INFO, "Skipping report file as size is too big (> 50MB): {0}", report.getPath()); @@ -662,6 +701,7 @@ private void generateTest() { try { document = builder.build(report); } catch (Exception x) { + LOG.log(Level.WARNING, "Exception reading from file {0}", report); try { // maybe the report file was not created yet, try waiting a bit and then try again Thread.sleep(500); document = builder.build(report); @@ -672,6 +712,7 @@ private void generateTest() { } } if(document == null) { + LOG.log(Level.WARNING, "No document read from dir {0}", outputDir); return; } Element testSuite = document.getRootElement(); @@ -680,7 +721,7 @@ private void generateTest() { session.addSuite(suite); CoreManager junitManager = getManagerProvider(); if (junitManager != null) { - junitManager.displaySuiteRunning(session, suite.getName()); + junitManager.displaySuiteRunning(session, suite); } File output = new File(outputDir, runningTestClass + suffix + "-output.txt"); @@ -770,6 +811,30 @@ private void generateTest() { } } + private File locateOutputDir(String runningTestClass, String suffix, boolean consume) { + Set outputDirs = runningTestClass2outputDirs.computeIfAbsent(runningTestClass, t -> new HashSet<>()); + LOG.log(Level.FINE, "trying output dirs for class {0}: {1}, sessions: {2}", new Object[] { runningTestClass, outputDirs, outputDir2sessions.keySet() }); + for (File outputDir : outputDir2sessions.keySet()) { + if (!outputDirs.contains(outputDir)) { + // When using reuseForks=true and a forkCount value larger than one, + // the same output is produced many times, so show it only once in Test Results window + File report = new File(outputDir, "TEST-" + runningTestClass + suffix + ".xml"); + if (report.isFile() && report.lastModified() >= startTimeStamp) { //#219097 ignore results from previous invokation. + LOG.log(Level.FINE, "Adding output dir {0} for report {1}", new Object[] { outputDir, report }); + if (consume) { + outputDirs.add(outputDir); + } + return outputDir; + } else { + LOG.log(Level.FINE, "Report file {0} exists, but is old; ignoring", report); + } + } else { + LOG.log(Level.FINE, "Output dir already created, not reporting again: {0}", outputDir.getAbsolutePath()); + } + } + return null; + } + private void logText(String text, Testcase test, boolean failure) { StringTokenizer tokens = new StringTokenizer(text, "\n"); //NOI18N List lines = new ArrayList<>(); @@ -778,7 +843,7 @@ private void logText(String text, Testcase test, boolean failure) { } CoreManager junitManager = getManagerProvider(); if (junitManager != null) { - junitManager.displayOutput(session, text, failure); + junitManager.displayOutput(test.getSession(), text, failure); } test.addOutputLines(lines); } diff --git a/java/maven/apichanges.xml b/java/maven/apichanges.xml index 11adec3ab584..5f1baf0f3f68 100644 --- a/java/maven/apichanges.xml +++ b/java/maven/apichanges.xml @@ -83,6 +83,21 @@ is the proper place. + + + RunConfig provides access to Maven options + + + + + +

+ RunConfig now provides access to Maven options / flags. Client can decide which + options / flags will be passed to command line. +

+
+ +
Enable using PluginConfigPathParams with null valued pathItemName diff --git a/java/maven/mavensrc/org/netbeans/modules/maven/event/NbEventSpy.java b/java/maven/mavensrc/org/netbeans/modules/maven/event/NbEventSpy.java index a823be8e846f..8cb23750ed94 100644 --- a/java/maven/mavensrc/org/netbeans/modules/maven/event/NbEventSpy.java +++ b/java/maven/mavensrc/org/netbeans/modules/maven/event/NbEventSpy.java @@ -199,6 +199,12 @@ public void onEvent(Object event) throws Exception { } } } + MavenProject mp = ex.getProject(); + if (mp != null) { + if (mp.getFile() != null) { //file is null in superpom + mojo.put("prjFile", mp.getFile().getParentFile().getAbsolutePath()); + } + } root.put("mojo", mojo); } if (ExecutionEvent.Type.MojoFailed.equals(ex.getType()) && ex.getException() != null) { diff --git a/java/maven/nbproject/org-netbeans-modules-maven.sig b/java/maven/nbproject/org-netbeans-modules-maven.sig index f835922732fb..f5723dccd7a4 100644 --- a/java/maven/nbproject/org-netbeans-modules-maven.sig +++ b/java/maven/nbproject/org-netbeans-modules-maven.sig @@ -1,5 +1,5 @@ #Signature file v4.1 -#Version 2.165.0 +#Version 2.167.0 CLSS public abstract java.awt.Component cons protected init() @@ -1764,6 +1764,8 @@ meth public abstract java.lang.String getExecutionName() meth public abstract java.lang.String getTaskDisplayName() meth public abstract java.util.List getActivatedProfiles() meth public abstract java.util.List getGoals() +meth public abstract java.util.Map getOptions() + anno 0 org.netbeans.api.annotations.common.NonNull() meth public abstract java.util.Map getProperties() anno 0 org.netbeans.api.annotations.common.NonNull() meth public abstract java.util.Map getInternalProperties() @@ -1773,6 +1775,8 @@ meth public abstract org.netbeans.api.project.Project getProject() meth public abstract org.netbeans.modules.maven.api.execute.RunConfig getPreExecution() meth public abstract org.netbeans.modules.maven.api.execute.RunConfig$ReactorStyle getReactorStyle() meth public abstract org.openide.filesystems.FileObject getSelectedFileObject() +meth public abstract void addOptions(java.util.Map) + anno 1 org.netbeans.api.annotations.common.NonNull() meth public abstract void addProperties(java.util.Map) anno 1 org.netbeans.api.annotations.common.NonNull() meth public abstract void setActivatedProfiles(java.util.List) @@ -1781,6 +1785,9 @@ meth public abstract void setInternalProperty(java.lang.String,java.lang.Object) anno 1 org.netbeans.api.annotations.common.NonNull() anno 2 org.netbeans.api.annotations.common.NullAllowed() meth public abstract void setOffline(java.lang.Boolean) +meth public abstract void setOption(java.lang.String,java.lang.String) + anno 1 org.netbeans.api.annotations.common.NonNull() + anno 2 org.netbeans.api.annotations.common.NullAllowed() meth public abstract void setPreExecution(org.netbeans.modules.maven.api.execute.RunConfig) meth public abstract void setProperty(java.lang.String,java.lang.String) anno 1 org.netbeans.api.annotations.common.NonNull() @@ -1964,6 +1971,7 @@ cons protected init(org.netbeans.api.project.Project,org.netbeans.api.progress.P fld protected final static java.lang.String PRJ_EXECUTE = "project-execute" fld protected final static java.lang.String SESSION_EXECUTE = "session-execute" fld protected java.util.HashMap> processors +fld protected java.util.HashMap id2count fld protected java.util.Set toFinishProcessors fld protected java.util.Set currentProcessors fld protected org.netbeans.modules.maven.api.output.OutputVisitor visitor @@ -2066,16 +2074,19 @@ meth public final void setShowError(boolean) meth public final void setTaskDisplayName(java.lang.String) meth public final void setUpdateSnapshots(boolean) meth public java.lang.String getActionName() +meth public java.util.Map getOptions() meth public org.netbeans.modules.maven.api.execute.RunConfig getPreExecution() meth public org.openide.filesystems.FileObject getSelectedFileObject() meth public org.openide.util.Lookup getActionContext() +meth public void addOptions(java.util.Map) meth public void reassignMavenProjectFromParent() meth public void setActionContext(org.openide.util.Lookup) meth public void setActionName(java.lang.String) meth public void setFileObject(org.openide.filesystems.FileObject) +meth public void setOption(java.lang.String,java.lang.String) meth public void setPreExecution(org.netbeans.modules.maven.api.execute.RunConfig) supr java.lang.Object -hfds actionContext,actionName,activate,executionDirectory,executionName,goals,interactive,internalProperties,mp,offline,parent,preexecution,project,projectDirectory,properties,reactor,recursive,selectedFO,showDebug,showError,taskName,updateSnapshots +hfds actionContext,actionName,activate,executionDirectory,executionName,goals,interactive,internalProperties,mp,offline,options,parent,preexecution,project,projectDirectory,properties,reactor,recursive,selectedFO,showDebug,showError,taskName,updateSnapshots CLSS public org.netbeans.modules.maven.execute.CommandLineOutputHandler cons public init(org.openide.windows.InputOutput,org.netbeans.api.project.Project,org.netbeans.api.progress.ProgressHandle,org.netbeans.modules.maven.api.execute.RunConfig,boolean) @@ -2119,7 +2130,7 @@ meth public java.lang.String convert(java.lang.String,org.openide.util.Lookup) meth public java.util.Map createReplacements(java.lang.String,org.openide.util.Lookup) meth public static java.util.Map readVariables() supr java.lang.Object -hfds ABSOLUTE_PATH,ARTIFACTID,CLASSNAME,CLASSNAME_EXT,CLASSPATHSCOPE,GROUPID,PACK_CLASSNAME,VARIABLE_PREFIX,project +hfds ABSOLUTE_PATH,ARTIFACTID,CLASSNAME,CLASSNAME_EXT,CLASSPATHSCOPE,GROUPID,PACK_CLASSNAME,PROJECTS,VARIABLE_PREFIX,project CLSS public org.netbeans.modules.maven.execute.MavenCommandLineExecutor cons public init(org.netbeans.modules.maven.api.execute.RunConfig,org.openide.windows.InputOutput,org.netbeans.modules.maven.execute.AbstractMavenExecutor$TabContext) @@ -2137,6 +2148,12 @@ cons public init() meth public org.openide.execution.ExecutorTask execute(org.netbeans.modules.maven.api.execute.RunConfig,org.openide.windows.InputOutput,org.netbeans.modules.maven.execute.AbstractMavenExecutor$TabContext) supr java.lang.Object +CLSS public org.netbeans.modules.maven.execute.MavenCommandLineOptions +cons public init() +meth public static boolean optionRequiresValue(java.lang.String) +supr java.lang.Object +hfds OPTIONS_WITH_VALUES + CLSS public abstract interface org.netbeans.modules.maven.execute.MavenExecutor intf java.lang.Runnable meth public abstract org.openide.windows.InputOutput getInputOutput() @@ -2276,13 +2293,16 @@ meth public java.lang.String getReactor() meth public java.util.List getActivatedProfiles() meth public java.util.List getGoals() meth public java.util.List getPackagings() +meth public java.util.Map getOptions() meth public java.util.Map getProperties() meth public void addActivatedProfile(java.lang.String) meth public void addGoal(java.lang.String) +meth public void addOption(java.lang.String,java.lang.String) meth public void addPackaging(java.lang.String) meth public void addProperty(java.lang.String,java.lang.String) meth public void removeActivatedProfile(java.lang.String) meth public void removeGoal(java.lang.String) +meth public void removeOption(java.lang.String) meth public void removePackaging(java.lang.String) meth public void setActionName(java.lang.String) meth public void setActivatedProfiles(java.util.List) @@ -2290,13 +2310,14 @@ meth public void setBasedir(java.lang.String) meth public void setDisplayName(java.lang.String) meth public void setGoals(java.util.List) meth public void setModelEncoding(java.lang.String) +meth public void setOptions(java.util.Map) meth public void setPackagings(java.util.List) meth public void setPreAction(java.lang.String) meth public void setProperties(java.util.Map) meth public void setReactor(java.lang.String) meth public void setRecursive(boolean) supr java.lang.Object -hfds actionName,activatedProfiles,basedir,displayName,goals,modelEncoding,packagings,preAction,properties,reactor,recursive +hfds actionName,activatedProfiles,basedir,displayName,goals,modelEncoding,options,packagings,preAction,properties,reactor,recursive CLSS public org.netbeans.modules.maven.execute.model.NetbeansActionProfile cons public init() diff --git a/java/maven/nbproject/project.properties b/java/maven/nbproject/project.properties index 08d443c50876..eb74ba81e6bc 100644 --- a/java/maven/nbproject/project.properties +++ b/java/maven/nbproject/project.properties @@ -21,7 +21,7 @@ javadoc.apichanges=${basedir}/apichanges.xml javadoc.arch=${basedir}/arch.xml javahelp.hs=maven.hs extra.module.files=maven-nblib/ -spec.version.base=2.166.0 +spec.version.base=2.167.0 # The CPExtender test fails in library processing (not randomly) since NetBeans 8.2; disabling. test.excludes=**/CPExtenderTest.class diff --git a/java/maven/nbproject/project.xml b/java/maven/nbproject/project.xml index 6490c0d6a501..aaba984bda35 100644 --- a/java/maven/nbproject/project.xml +++ b/java/maven/nbproject/project.xml @@ -201,7 +201,7 @@ - 2.73 + 2.74
@@ -324,7 +324,7 @@ 1 - 1.89 + 1.99 diff --git a/java/maven/src/org/netbeans/modules/maven/ActionProviderImpl.java b/java/maven/src/org/netbeans/modules/maven/ActionProviderImpl.java index 617801f26ecb..1ebff0ee4680 100644 --- a/java/maven/src/org/netbeans/modules/maven/ActionProviderImpl.java +++ b/java/maven/src/org/netbeans/modules/maven/ActionProviderImpl.java @@ -132,6 +132,7 @@ public class ActionProviderImpl implements ActionProvider { "javadoc", //NOI18N COMMAND_TEST, COMMAND_TEST_SINGLE, + COMMAND_TEST_PARALLEL, SingleMethod.COMMAND_RUN_SINGLE_METHOD, SingleMethod.COMMAND_DEBUG_SINGLE_METHOD, @@ -394,7 +395,8 @@ private boolean checkCompilerPlugin(final String action) { action.equals(ActionProvider.COMMAND_PROFILE) || action.equals(ActionProvider.COMMAND_REBUILD) || action.equals(ActionProvider.COMMAND_RUN) || - action.equals(ActionProvider.COMMAND_TEST)) + action.equals(ActionProvider.COMMAND_TEST) || + action.equals(ActionProvider.COMMAND_TEST_PARALLEL)) { if (!ModuleInfoUtils.checkModuleInfoAndCompilerFit(proj)) { if (NbPreferences.forModule(ActionProviderImpl.class).getBoolean(SHOW_COMPILER_TOO_OLD_WARNING, true)) { @@ -432,6 +434,7 @@ public static Map replacements(Project proj, String action, Looku "# {0} - artifactId", "TXT_ApplyCodeChanges=Apply Code Changes ({0})", "# {0} - artifactId", "TXT_Profile=Profile ({0})", "# {0} - artifactId", "TXT_Test=Test ({0})", + "# {0} - artifactId", "TXT_Test_Parallel=Test In Parallel ({0})", "# {0} - artifactId", "TXT_Build=Build ({0})", "# {0} - action name", "# {1} - project name", "TXT_CustomNamed={0} ({1})" }) @@ -457,6 +460,8 @@ private static void setupTaskName(String action, RunConfig config, Lookup lkp) { title = TXT_Profile(prjLabel); } else if (ActionProvider.COMMAND_TEST.equals(action)) { title = TXT_Test(prjLabel); + } else if (ActionProvider.COMMAND_TEST_PARALLEL.equals(action)) { + title = TXT_Test_Parallel(prjLabel); } else if (action.startsWith(ActionProvider.COMMAND_RUN_SINGLE)) { title = TXT_Run(dobjName); } else if (action.startsWith(ActionProvider.COMMAND_DEBUG_SINGLE) || ActionProvider.COMMAND_DEBUG_TEST_SINGLE.equals(action)) { @@ -607,8 +612,10 @@ private ModelRunConfig createCustomRunConfig(M2ConfigProvider conf) { acts.addAll(conf.getActiveConfiguration().getActivatedProfiles()); rc.setActivatedProfiles(acts); Map props = new HashMap<>(rc.getProperties()); + Map options = new HashMap<>(rc.getOptions()); props.putAll(conf.getActiveConfiguration().getProperties()); rc.addProperties(props); + rc.addOptions(options); rc.setTaskDisplayName(TXT_Build(proj.getLookup().lookup(NbMavenProject.class).getMavenProject().getArtifactId())); return rc; } diff --git a/java/maven/src/org/netbeans/modules/maven/api/execute/RunConfig.java b/java/maven/src/org/netbeans/modules/maven/api/execute/RunConfig.java index 5c8cd8de01fa..d6622fb26655 100644 --- a/java/maven/src/org/netbeans/modules/maven/api/execute/RunConfig.java +++ b/java/maven/src/org/netbeans/modules/maven/api/execute/RunConfig.java @@ -79,6 +79,28 @@ public interface RunConfig { String getActionName(); + /** + * Options/switches passed to maven. + * @return a read-only copy of the current maven options + * @since 2.167 + */ + @NonNull Map getOptions(); + + /** + * Sets option that will be passed to maven. + * @param key a key that represents option/switch name + * @param value a value of the option/switch + * @since 2.167 + */ + void setOption(@NonNull String key, @NullAllowed String value); + + /** + * Adds options/switches that will be passed to maven. + * @param options options/switches that will be added + * @since 2.167 + */ + void addOptions(@NonNull Map options); + /** * Properties to be used in execution. * @return a read-only copy of the current properties (possibly inherited from the parent) diff --git a/java/maven/src/org/netbeans/modules/maven/execute/AbstractOutputHandler.java b/java/maven/src/org/netbeans/modules/maven/execute/AbstractOutputHandler.java index 439e869f6177..1be25db72795 100644 --- a/java/maven/src/org/netbeans/modules/maven/execute/AbstractOutputHandler.java +++ b/java/maven/src/org/netbeans/modules/maven/execute/AbstractOutputHandler.java @@ -27,6 +27,7 @@ import java.util.Iterator; import java.util.List; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import org.netbeans.api.progress.ProgressHandle; import org.netbeans.modules.maven.api.execute.RunConfig; import org.netbeans.modules.maven.api.output.ContextOutputProcessorFactory; @@ -60,6 +61,7 @@ public enum Level {DEBUG, INFO, WARNING, ERROR, FATAL} protected static final String SESSION_EXECUTE = "session-execute"; //NOI18N protected HashMap> processors; + protected HashMap id2count; protected Set currentProcessors; protected Set toFinishProcessors; protected OutputVisitor visitor; @@ -70,6 +72,7 @@ public enum Level {DEBUG, INFO, WARNING, ERROR, FATAL} protected AbstractOutputHandler(Project proj, final ProgressHandle hand, RunConfig config, OutputVisitor visitor) { processors = new HashMap>(); + id2count = new HashMap<>(); currentProcessors = new HashSet(); this.visitor = visitor; toFinishProcessors = new HashSet(); @@ -175,9 +178,12 @@ protected final void initProcessorList(Project proj, RunConfig config) { protected final void processStart(String id, OutputWriter writer) { checkSleepiness(); - Set set = processors.get(id); - if (set != null) { - currentProcessors.addAll(set); + AtomicInteger count = id2count.computeIfAbsent(id, s -> new AtomicInteger()); + if (count.getAndIncrement() == 0) { + Set set = processors.get(id); + if (set != null) { + currentProcessors.addAll(set); + } } visitor.resetVisitor(); for (OutputProcessor proc : currentProcessors) { @@ -226,12 +232,16 @@ protected final void processEnd(String id, OutputWriter writer) { writer.println(visitor.getLine()); } } - Set set = processors.get(id); - if (set != null) { - //TODO a bulletproof way would be to keep a list of currently started - // sections and compare to the list of getRegisteredOutputSequences fo each of the - // processors in set.. - currentProcessors.removeAll(set); + AtomicInteger count = id2count.getOrDefault(id, new AtomicInteger(1)); + if (count.decrementAndGet() == 0) { + Set set = processors.get(id); + if (set != null) { + //TODO a bulletproof way would be to keep a list of currently started + // sections and compare to the list of getRegisteredOutputSequences fo each of the + // processors in set.. + currentProcessors.removeAll(set); + } + id2count.remove(id); } } diff --git a/java/maven/src/org/netbeans/modules/maven/execute/BeanRunConfig.java b/java/maven/src/org/netbeans/modules/maven/execute/BeanRunConfig.java index 0b73d4422564..1262d057923a 100644 --- a/java/maven/src/org/netbeans/modules/maven/execute/BeanRunConfig.java +++ b/java/maven/src/org/netbeans/modules/maven/execute/BeanRunConfig.java @@ -28,7 +28,6 @@ import java.util.Map; import java.util.Properties; import org.apache.maven.project.MavenProject; -import org.codehaus.plexus.PlexusContainerException; import org.netbeans.api.annotations.common.NonNull; import org.netbeans.api.annotations.common.NullAllowed; import org.netbeans.api.project.Project; @@ -55,6 +54,7 @@ public class BeanRunConfig implements RunConfig { private List goals; private String executionName; private Map properties; + private Map options; private Map internalProperties; //for these delegate to default options for defaults. private boolean showDebug = MavenSettings.getDefault().isShowDebug(); @@ -398,5 +398,39 @@ public final ReactorStyle getReactorStyle() { public final void setReactorStyle(ReactorStyle style) { reactor = style; } + + @Override + public Map getOptions() { + if (options == null) { + return parent != null ? parent.getOptions(): Collections.emptyMap(); + } + return Collections.unmodifiableMap(new LinkedHashMap(options)); + } + + @Override + public void setOption(String key, String value) { + if (options == null) { + options = new LinkedHashMap(); + if (parent != null) { + options.putAll(parent.getOptions()); + } + } + if (value != null) { + options.put(key, value); + } else { + options.remove(key); + } + } + + @Override + public void addOptions(Map args) { + if (options == null) { + options = new LinkedHashMap(); + if (parent != null) { + options.putAll(parent.getOptions()); + } + } + options.putAll(args); + } } diff --git a/java/maven/src/org/netbeans/modules/maven/execute/CommandLineOutputHandler.java b/java/maven/src/org/netbeans/modules/maven/execute/CommandLineOutputHandler.java index 3bc178dd0809..79b030709637 100644 --- a/java/maven/src/org/netbeans/modules/maven/execute/CommandLineOutputHandler.java +++ b/java/maven/src/org/netbeans/modules/maven/execute/CommandLineOutputHandler.java @@ -468,7 +468,11 @@ private void processExecEvent(ExecutionEventObject obj) { assert prjNode != null; ExecProject p = (ExecProject) prjNode.getStartEvent(); handle.progress(p.gav.artifactId + " " + tag); - CommandLineOutputHandler.this.processStart(getEventId(SEC_MOJO_EXEC, tag), stdOut); + if (contextImpl != null) { + Project pr = exec.findProject(); + contextImpl.setCurrentProject(pr); + CommandLineOutputHandler.this.processStart(getEventId(SEC_MOJO_EXEC, tag), stdOut); + } } if (ExecutionEvent.Type.MojoSucceeded.equals(obj.type)) { if (MavenSettings.getDefault().isCollapseSuccessFolds()) { @@ -479,7 +483,11 @@ private void processExecEvent(ExecutionEventObject obj) { mergeClasspath(exec, mavencoreurls); trimTree(exec); String tag = goalPrefixFromArtifactId(exec.plugin.artifactId) + ":" + exec.goal; - CommandLineOutputHandler.this.processEnd(getEventId(SEC_MOJO_EXEC, tag), stdOut); + if (contextImpl != null) { + Project pr = exec.findProject(); + contextImpl.setCurrentProject(pr); + CommandLineOutputHandler.this.processEnd(getEventId(SEC_MOJO_EXEC, tag), stdOut); + } } else if (ExecutionEvent.Type.MojoFailed.equals(obj.type)) { currentTreeNode.finishFold(); @@ -487,7 +495,11 @@ else if (ExecutionEvent.Type.MojoFailed.equals(obj.type)) { mergeClasspath(exec, mavencoreurls); trimTree(exec); String tag = goalPrefixFromArtifactId(exec.plugin.artifactId) + ":" + exec.goal; - CommandLineOutputHandler.this.processFail(getEventId(SEC_MOJO_EXEC, tag), stdOut); + if (contextImpl != null) { + Project pr = exec.findProject(); + contextImpl.setCurrentProject(pr); + CommandLineOutputHandler.this.processFail(getEventId(SEC_MOJO_EXEC, tag), stdOut); + } } else if (ExecutionEvent.Type.ProjectStarted.equals(obj.type)) { growTree(obj); @@ -496,7 +508,7 @@ else if (ExecutionEvent.Type.ProjectStarted.equals(obj.type)) { ExecProject pr = (ExecProject)obj; Project project = pr.findProject(); contextImpl.setCurrentProject(project); - CommandLineOutputHandler.this.processStart(getEventId(PRJ_EXECUTE, null), stdOut); + CommandLineOutputHandler.this.processStart(getEventId(PRJ_EXECUTE, null), stdOut); } } else if (ExecutionEvent.Type.ProjectSkipped.equals(obj.type)) { @@ -510,12 +522,22 @@ else if (ExecutionEvent.Type.ProjectSucceeded.equals(obj.type)) { } currentTreeNode.finishFold(); trimTree(obj); - CommandLineOutputHandler.this.processEnd(getEventId(PRJ_EXECUTE, null), stdOut); + if (contextImpl != null) { + ExecProject pr = (ExecProject)obj; + Project project = pr.findProject(); + contextImpl.setCurrentProject(project); + CommandLineOutputHandler.this.processEnd(getEventId(PRJ_EXECUTE, null), stdOut); + } } else if (ExecutionEvent.Type.ProjectFailed.equals(obj.type)) { currentTreeNode.finishFold(); trimTree(obj); - CommandLineOutputHandler.this.processEnd(getEventId(PRJ_EXECUTE, null), stdOut); + if (contextImpl != null) { + ExecProject pr = (ExecProject)obj; + Project project = pr.findProject(); + contextImpl.setCurrentProject(project); + CommandLineOutputHandler.this.processEnd(getEventId(PRJ_EXECUTE, null), stdOut); + } } else if (ExecutionEvent.Type.ForkStarted.equals(obj.type)) { growTree(obj); } else if (ExecutionEvent.Type.ForkedProjectStarted.equals(obj.type)) { diff --git a/java/maven/src/org/netbeans/modules/maven/execute/DefaultReplaceTokenProvider.java b/java/maven/src/org/netbeans/modules/maven/execute/DefaultReplaceTokenProvider.java index c8ba46e7b3e0..e6c05cbf9ab8 100644 --- a/java/maven/src/org/netbeans/modules/maven/execute/DefaultReplaceTokenProvider.java +++ b/java/maven/src/org/netbeans/modules/maven/execute/DefaultReplaceTokenProvider.java @@ -48,6 +48,8 @@ import org.netbeans.modules.maven.spi.actions.ActionConvertor; import org.netbeans.modules.maven.spi.actions.ReplaceTokenProvider; import org.netbeans.spi.project.ActionProvider; +import org.netbeans.api.project.ContainedProjectFilter; +import org.netbeans.spi.project.NestedClass; import org.netbeans.spi.project.ProjectServiceProvider; import org.netbeans.spi.project.SingleMethod; import org.netbeans.spi.project.support.ant.EditableProperties; @@ -72,6 +74,7 @@ public class DefaultReplaceTokenProvider implements ReplaceTokenProvider, Action static final String CLASSNAME = "className";//NOI18N static final String CLASSNAME_EXT = "classNameWithExtension";//NOI18N static final String PACK_CLASSNAME = "packageClassName";//NOI18N + static final String PROJECTS = "projects";//NOI18N static final String ABSOLUTE_PATH = "absolutePathName"; public static final String METHOD_NAME = "nb.single.run.methodName"; //NOI18N private static final String VARIABLE_PREFIX = "var."; //NOI18N @@ -100,7 +103,10 @@ private static FileObject[] extractFileObjectsfromLookup(Lookup lookup) { } @Override public Map createReplacements(String actionName, Lookup lookup) { + NestedClass nestedClass = lookup.lookup(NestedClass.class); FileObject[] fos = extractFileObjectsfromLookup(lookup); + List projects = extractProjectsFromLookup(lookup); + SourceGroup group = findGroup(ProjectUtils.getSources(project).getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA), fos); HashMap replaceMap = new HashMap(); // read environment variables in the IDE and prefix them with "env." just in case someone uses it as variable in the action mappings @@ -138,7 +144,8 @@ private static FileObject[] extractFileObjectsfromLookup(Lookup lookup) { if (!isTest && !(ActionProvider.COMMAND_TEST_SINGLE.equals(actionName) || ActionProvider.COMMAND_DEBUG_TEST_SINGLE.equals(actionName) || ActionProvider.COMMAND_PROFILE_TEST_SINGLE.equals(actionName) || - ActionProvider.COMMAND_TEST.equals(actionName))) { + ActionProvider.COMMAND_TEST.equals(actionName) || + ActionProvider.COMMAND_TEST_PARALLEL.equals(actionName))) { // Execution can not have more files separated by commas. Only test can. break; } else { @@ -162,7 +169,7 @@ private static FileObject[] extractFileObjectsfromLookup(Lookup lookup) { classname.append(pkg); // ? classnameExt.append(pkg); // ?? } else { // XXX do we need to limit to text/x-java? What about files of other type? - String cn = SourceUtils.classNameFor(ClasspathInfo.create(file), rel); + String cn = SourceUtils.classNameFor(ClasspathInfo.create(file), rel, nestedClass); int idx = cn.lastIndexOf('.'); String n = idx < 0 ? cn : cn.substring(idx + 1); if (uniqueClassNames.add(cn)) { @@ -229,6 +236,10 @@ private static FileObject[] extractFileObjectsfromLookup(Lookup lookup) { if (classnameExt.length() > 0) { //#213671 replaceMap.put(CLASSNAME_EXT, classnameExt.toString()); } + if (projects != null && !projects.isEmpty()) { + List projectReplacements = createProjectsReplacement(projects); + replaceMap.put(PROJECTS, String.join(",", projectReplacements)); + } Collection methods = lookup.lookupAll(SingleMethod.class); if (methods.size() == 1) { @@ -246,7 +257,19 @@ private static FileObject[] extractFileObjectsfromLookup(Lookup lookup) { } return replaceMap; } - + + private List extractProjectsFromLookup(Lookup lookup) { + ContainedProjectFilter projectFilter = lookup.lookup(ContainedProjectFilter.class); + return projectFilter == null ? null : projectFilter.getProjectsToProcess(); + } + + private List createProjectsReplacement(List projects) { + return projects + .stream() + .map(prj -> prj.getProjectDirectory().getName()) + .toList(); + } + private void addSelectedFiles(boolean testRoots, FileObject[] candidates, HashSet test) { NbMavenProjectImpl prj = project.getLookup().lookup(NbMavenProjectImpl.class); if (prj != null) { diff --git a/java/maven/src/org/netbeans/modules/maven/execute/MavenCommandLineExecutor.java b/java/maven/src/org/netbeans/modules/maven/execute/MavenCommandLineExecutor.java index a2a439e13221..c45f4928a618 100644 --- a/java/maven/src/org/netbeans/modules/maven/execute/MavenCommandLineExecutor.java +++ b/java/maven/src/org/netbeans/modules/maven/execute/MavenCommandLineExecutor.java @@ -71,7 +71,9 @@ import org.netbeans.modules.maven.options.MavenSettings; import org.netbeans.modules.maven.runjar.MavenExecuteUtils; import org.netbeans.spi.project.ui.support.BuildExecutionSupport; +import org.openide.DialogDisplayer; import org.openide.LifecycleManager; +import org.openide.NotifyDescriptor; import org.openide.awt.HtmlBrowser; import org.openide.awt.NotificationDisplayer; import org.openide.execution.ExecutionEngine; @@ -428,6 +430,9 @@ public boolean cancel() { return true; } + @NbBundle.Messages({ + "MSG_MissingValue=Option: {0} requires value to be present" + }) private static List createMavenExecutionCommand(RunConfig config, Constructor base) { List toRet = new ArrayList<>(base.construct()); @@ -445,10 +450,29 @@ private static List createMavenExecutionCommand(RunConfig config, Constr LOGGER.log(Level.FINE, "Could not canonicalize " + basedir, x); } } - + //#164234 //if maven.bat file is in space containing path, we need to quote with simple quotes. String quote = "\""; + + for (Map.Entry entry : config.getOptions().entrySet()) { + String key = entry.getKey(); + String value = quote2apos(entry.getValue()); + if (MavenCommandLineOptions.optionRequiresValue(key)) { + if (value.isEmpty()) { + DialogDisplayer.getDefault().notifyLater(new NotifyDescriptor.Message(Bundle.MSG_MissingValue(key), NotifyDescriptor.WARNING_MESSAGE)); + continue; + } else if (value.equals("${" + key + "}")) { //NOI18N + continue; + } + } + toRet.add("--" + key); + if (value != null && !value.isBlank()) { + String s = (Utilities.isWindows() && value.contains(" ") ? quote + value + quote : value); + toRet.add(value); + } + } + // the command line parameters with space in them need to be quoted and escaped to arrive // correctly to the java runtime on windows for (Map.Entry entry : config.getProperties().entrySet()) { diff --git a/java/maven/src/org/netbeans/modules/maven/execute/MavenCommandLineOptions.java b/java/maven/src/org/netbeans/modules/maven/execute/MavenCommandLineOptions.java new file mode 100644 index 000000000000..96b86b65d73e --- /dev/null +++ b/java/maven/src/org/netbeans/modules/maven/execute/MavenCommandLineOptions.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.modules.maven.execute; + +import java.util.Set; + +/** + * + * @author Dusan Petrovic + */ +public class MavenCommandLineOptions { + + private static final Set OPTIONS_WITH_VALUES = Set.of( + "activate-profiles", + "builder", + "color", + "define", + "encrypt-master-password", + "encrypt-password", + "file", + "global-settings", + "global-toolchains", + "log-file", + "projects", + "resume-from", + "settings", + "threads", + "toolchains" + ); + + public static boolean optionRequiresValue(String option) { + return OPTIONS_WITH_VALUES.contains(option); + } +} diff --git a/java/maven/src/org/netbeans/modules/maven/execute/ModelRunConfig.java b/java/maven/src/org/netbeans/modules/maven/execute/ModelRunConfig.java index 0ed52f2690c8..94d1b831dbdd 100644 --- a/java/maven/src/org/netbeans/modules/maven/execute/ModelRunConfig.java +++ b/java/maven/src/org/netbeans/modules/maven/execute/ModelRunConfig.java @@ -79,6 +79,11 @@ public ModelRunConfig(Project proj, NetbeansActionMapping mod, String actionName } setProperty(key, value); } + for (Map.Entry entry : model.getOptions().entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + setOption(key, value); + } setGoals(model.getGoals()); setExecutionDirectory(ActionToGoalUtils.resolveProjectExecutionBasedir(mod, proj)); setRecursive(mod.isRecursive()); diff --git a/java/maven/src/org/netbeans/modules/maven/execute/cmd/ExecMojo.java b/java/maven/src/org/netbeans/modules/maven/execute/cmd/ExecMojo.java index 87676d6d5584..f78ea02db5c7 100644 --- a/java/maven/src/org/netbeans/modules/maven/execute/cmd/ExecMojo.java +++ b/java/maven/src/org/netbeans/modules/maven/execute/cmd/ExecMojo.java @@ -19,6 +19,8 @@ package org.netbeans.modules.maven.execute.cmd; +import java.io.File; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.StandardCharsets; @@ -31,6 +33,11 @@ import org.codehaus.plexus.util.Base64; import org.json.simple.JSONArray; import org.json.simple.JSONObject; +import org.netbeans.api.annotations.common.CheckForNull; +import org.netbeans.api.project.Project; +import org.netbeans.api.project.ProjectManager; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; import org.openide.util.Exceptions; /** @@ -46,6 +53,7 @@ public class ExecMojo extends ExecutionEventObject { private InputLocation location; private URL[] classpathURLs; private String implementationClass; + private File currentProjectLocation; public ExecMojo(String goal, GAV plugin, String phase, String executionId, ExecutionEvent.Type type) { @@ -101,6 +109,12 @@ public static ExecMojo create(JSONObject obj, ExecutionEvent.Type t) { toRet.setClasspathURLs(urlList.toArray(new URL[0])); } toRet.setImplementationClass((String) mojo.get("impl")); + File prjFile = null; + String file = (String) mojo.get("prjFile"); + if (file != null) { + prjFile = FileUtil.normalizeFile(new File(file)); + } + toRet.setCurrentProjectLocation(prjFile); return toRet; } @@ -155,4 +169,24 @@ public String getImplementationClass() { void setImplementationClass(String implementationClass) { this.implementationClass = implementationClass; } + + public @CheckForNull Project findProject() { + if (currentProjectLocation != null) { + FileObject fo = FileUtil.toFileObject(currentProjectLocation); + if (fo != null) { + try { + return ProjectManager.getDefault().findProject(fo); + } catch (IOException ex) { + Exceptions.printStackTrace(ex); + } catch (IllegalArgumentException ex) { + Exceptions.printStackTrace(ex); + } + } + } + return null; + } + + void setCurrentProjectLocation(File currentProjectLocation) { + this.currentProjectLocation = currentProjectLocation; + } } diff --git a/java/maven/src/org/netbeans/modules/maven/execute/defaultActionMappings.xml b/java/maven/src/org/netbeans/modules/maven/execute/defaultActionMappings.xml index ba06e9b1bbae..d493332788e4 100644 --- a/java/maven/src/org/netbeans/modules/maven/execute/defaultActionMappings.xml +++ b/java/maven/src/org/netbeans/modules/maven/execute/defaultActionMappings.xml @@ -78,6 +78,23 @@ test + + test.parallel + + * + + + 0.5C + ${projects} + + + + test + + + classes + + test.single diff --git a/java/maven/src/org/netbeans/modules/maven/execute/model/NetbeansActionMapping.java b/java/maven/src/org/netbeans/modules/maven/execute/model/NetbeansActionMapping.java index a1a0082a8b00..a152b4b511d9 100644 --- a/java/maven/src/org/netbeans/modules/maven/execute/model/NetbeansActionMapping.java +++ b/java/maven/src/org/netbeans/modules/maven/execute/model/NetbeansActionMapping.java @@ -68,6 +68,11 @@ public class NetbeansActionMapping implements java.io.Serializable { */ private Map properties; + /** + * Field options. + */ + private Map options; + /** * Field activatedProfiles. */ @@ -78,7 +83,7 @@ public class NetbeansActionMapping implements java.io.Serializable { private String preAction; private String reactor; - + //-----------/ //- Methods -/ //-----------/ @@ -149,6 +154,17 @@ public void addProperty(String key, String value) getProperties().put( key, value ); } //-- void addProperty(String, String) + /** + * Method addOptions. + * + * @param key + * @param value + */ + public void addOption(String key, String value) + { + getOptions().put( key, value ); + } + /** * Get the actionName field. * @@ -223,6 +239,16 @@ public Map getProperties() return this.properties; } + + public Map getOptions() + { + if ( this.options == null ) + { + this.options = new LinkedHashMap(); + } + + return this.options; + } /** * Get the recursive field. @@ -264,6 +290,16 @@ public void removePackaging(String string) getPackagings().remove( string ); } //-- void removePackaging(String) + /** + * Method removeOption. + * + * @param string + */ + public void removeOption(String string) + { + getOptions().remove( string ); + } + /** * Set the actionName field. * @@ -319,6 +355,11 @@ public void setProperties(Map properties) { this.properties = properties; } + + public void setOptions(Map options) + { + this.options = options; + } /** * Set the recursive field. diff --git a/java/maven/src/org/netbeans/modules/maven/execute/model/io/jdom/NetbeansBuildActionJDOMWriter.java b/java/maven/src/org/netbeans/modules/maven/execute/model/io/jdom/NetbeansBuildActionJDOMWriter.java index 6c8a93da0aa5..07429ad72e69 100644 --- a/java/maven/src/org/netbeans/modules/maven/execute/model/io/jdom/NetbeansBuildActionJDOMWriter.java +++ b/java/maven/src/org/netbeans/modules/maven/execute/model/io/jdom/NetbeansBuildActionJDOMWriter.java @@ -401,6 +401,7 @@ protected void updateNetbeansActionMapping(NetbeansActionMapping value, String x findAndReplaceSimpleLists(innerCount, root, value.getPackagings(), "packagings", "packaging"); findAndReplaceSimpleLists(innerCount, root, value.getGoals(), "goals", "goal"); findAndReplaceProperties(innerCount, root, "properties", value.getProperties()); + findAndReplaceProperties(innerCount, root, "options", value.getOptions()); findAndReplaceSimpleLists(innerCount, root, value.getActivatedProfiles(), "activatedProfiles", "activatedProfile"); } //-- void updateNetbeansActionMapping(NetbeansActionMapping, String, Counter, Element) diff --git a/java/maven/src/org/netbeans/modules/maven/execute/model/io/xpp3/NetbeansBuildActionXpp3Reader.java b/java/maven/src/org/netbeans/modules/maven/execute/model/io/xpp3/NetbeansBuildActionXpp3Reader.java index 84a1feb28249..704de28db811 100644 --- a/java/maven/src/org/netbeans/modules/maven/execute/model/io/xpp3/NetbeansBuildActionXpp3Reader.java +++ b/java/maven/src/org/netbeans/modules/maven/execute/model/io/xpp3/NetbeansBuildActionXpp3Reader.java @@ -402,6 +402,20 @@ else if ( parser.getName().equals( "properties" ) ) netbeansActionMapping.addProperty( key, value ); } } + else if ( parser.getName().equals( "options" ) ) + { + if ( parsed.contains( "options" ) ) + { + throw new XmlPullParserException( "Duplicated tag: '" + parser.getName() + "'", parser, null ); + } + parsed.add( "options" ); + while ( parser.nextTag() == XmlPullParser.START_TAG ) + { + String key = parser.getName(); + String value = parser.nextText().trim(); + netbeansActionMapping.addOption(key, value ); + } + } else if ( parser.getName().equals( "activatedProfiles" ) ) { if ( parsed.contains( "activatedProfiles" ) ) diff --git a/java/maven/src/org/netbeans/modules/maven/execute/model/io/xpp3/NetbeansBuildActionXpp3Writer.java b/java/maven/src/org/netbeans/modules/maven/execute/model/io/xpp3/NetbeansBuildActionXpp3Writer.java index 13dc7bb0a2de..a32cecc313ed 100644 --- a/java/maven/src/org/netbeans/modules/maven/execute/model/io/xpp3/NetbeansBuildActionXpp3Writer.java +++ b/java/maven/src/org/netbeans/modules/maven/execute/model/io/xpp3/NetbeansBuildActionXpp3Writer.java @@ -179,6 +179,17 @@ private void writeNetbeansActionMapping(NetbeansActionMapping netbeansActionMapp } serializer.endTag( NAMESPACE, "properties" ); } + if ( netbeansActionMapping.getOptions()!= null && netbeansActionMapping.getOptions().size() > 0 ) + { + serializer.startTag( NAMESPACE, "options" ); + for ( Iterator iter = netbeansActionMapping.getOptions().keySet().iterator(); iter.hasNext(); ) + { + String key = (String) iter.next(); + String value = (String) netbeansActionMapping.getOptions().get( key ); + serializer.startTag( NAMESPACE, "" + key + "" ).text( value ).endTag( NAMESPACE, "" + key + "" ); + } + serializer.endTag( NAMESPACE, "options" ); + } if ( netbeansActionMapping.getActivatedProfiles() != null && netbeansActionMapping.getActivatedProfiles().size() > 0 ) { serializer.startTag( NAMESPACE, "activatedProfiles" ); diff --git a/java/maven/src/org/netbeans/modules/maven/queries/MavenArtifactsImplementation.java b/java/maven/src/org/netbeans/modules/maven/queries/MavenArtifactsImplementation.java index b791b1b141d7..056176a01c0d 100644 --- a/java/maven/src/org/netbeans/modules/maven/queries/MavenArtifactsImplementation.java +++ b/java/maven/src/org/netbeans/modules/maven/queries/MavenArtifactsImplementation.java @@ -81,6 +81,7 @@ public Res evaluate(ProjectArtifactsQuery.Filter query) { case ActionProvider.COMMAND_RUN_SINGLE: case ActionProvider.COMMAND_DEBUG_SINGLE: case ActionProvider.COMMAND_TEST: + case ActionProvider.COMMAND_TEST_PARALLEL: case ActionProvider.COMMAND_TEST_SINGLE: case ActionProvider.COMMAND_DEBUG_STEP_INTO: break; diff --git a/java/maven/test/unit/src/org/netbeans/modules/maven/debug/DebuggerCheckerTest.java b/java/maven/test/unit/src/org/netbeans/modules/maven/debug/DebuggerCheckerTest.java index 09a0da2b21ee..1a36731a9a4d 100644 --- a/java/maven/test/unit/src/org/netbeans/modules/maven/debug/DebuggerCheckerTest.java +++ b/java/maven/test/unit/src/org/netbeans/modules/maven/debug/DebuggerCheckerTest.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.Properties; +import static junit.framework.TestCase.fail; import org.apache.maven.project.MavenProject; import org.netbeans.api.project.Project; import org.netbeans.junit.NbTestCase; @@ -57,7 +58,8 @@ public void testDebugAttachTrue() { private static final class MockConfig implements RunConfig, Project { private Map props = new HashMap<>(); - + private Map options = new HashMap<>(); + @Override public File getExecutionDirectory() { fail(); @@ -215,6 +217,21 @@ public Lookup getLookup() { fail(); return null; } + + @Override + public Map getOptions() { + return Collections.unmodifiableMap(options); + } + + @Override + public void setOption(String key, String value) { + options.put(key, value); + } + + @Override + public void addOptions(Map properties) { + fail(); + } } }