From a66190377fa34d1f2864df6969b1d87cd68fb9c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Bl=C3=A4sing?= Date: Mon, 21 Jul 2025 20:32:43 +0200 Subject: [PATCH 1/7] SourceUtils#classNameFor does not handle nested classes correctly The assumption in the method is, that the names of nested classes are placed into an index, but that is not the case. In the index only toplevel classes are recorded. So this: ``` package test; public class Demo { static class Nested {} } class NonPublic {} ``` will yield: ``` test.NonPublic test.Demo ``` But the nested class `test.Demo.Nested` will not be found! So use the index only as a validator if a NestedClass is provided. --- .../netbeans/api/java/source/SourceUtils.java | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) 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 90f5131ff935..c725dbd1d699 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 @@ -1490,20 +1490,33 @@ public static String classNameFor(ClasspathInfo info, String relativePath, Neste String className = rel.replace('/', '.'); int lastDotIndex = className.lastIndexOf('.'); String fqnForNestedClass = null; - if (lastDotIndex > -1 && nestedClass != null) { - String packageName = className.substring(0, lastDotIndex); + String topLevelClass = null; + if (nestedClass != null) { + String packageName; + if(lastDotIndex >= 0) { + packageName = className.substring(0, lastDotIndex); + } else { + packageName = ""; + } fqnForNestedClass = nestedClass.getFQN(packageName, "$"); + topLevelClass = packageName + ( packageName.isBlank() ? "" : "." ) + nestedClass.getTopLevelClassName(); } + // This is really an ugly hack. It is pure luck, that the cache directory + // is placed on the CP, nothing guarantes that or at least that is non + // obvious. This also makes it hard/impossible to test. FileObject rsFile = cachedCP.findResource(rel + '.' + FileObjects.RS); if (rsFile != null) { List lines = new ArrayList<>(); try (BufferedReader in = new BufferedReader(new InputStreamReader(rsFile.getInputStream(), StandardCharsets.UTF_8))) { String line; - while ((line = in.readLine())!=null) { - if (className.equals(line)) { + while ((line = in.readLine()) != null) { + if (topLevelClass == null && className.equals(line)) { return className; - } else if (fqnForNestedClass != null && fqnForNestedClass.equals(line)) { - return line; + } else if (topLevelClass != null && topLevelClass.equals(line)) { + // The "RS" Index holds only toplevel classes, so we + // assume, that if the toplevel is found here, the FQN + // based on NestedClass is also present + return fqnForNestedClass; } lines.add(line); } @@ -1512,6 +1525,10 @@ public static String classNameFor(ClasspathInfo info, String relativePath, Neste return lines.get(0); } } - return className; + if(fqnForNestedClass != null) { + return fqnForNestedClass; + } else { + return className; + } } } From 700afc4336f5ac3888e042b4d0bce52159fc2ffd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Bl=C3=A4sing?= Date: Mon, 21 Jul 2025 20:33:35 +0200 Subject: [PATCH 2/7] Handle NestedClass with empty package name or empty nested class (only toplevel) --- ide/projectapi/nbproject/project.properties | 2 +- .../org/netbeans/spi/project/NestedClass.java | 42 +++++++++++++++---- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/ide/projectapi/nbproject/project.properties b/ide/projectapi/nbproject/project.properties index 735ab72a9111..ca962f553e5c 100644 --- a/ide/projectapi/nbproject/project.properties +++ b/ide/projectapi/nbproject/project.properties @@ -18,7 +18,7 @@ is.autoload=true javac.compilerargs=-Xlint -Xlint:-serial -javac.source=1.8 +javac.release=17 javadoc.arch=${basedir}/arch.xml javadoc.apichanges=${basedir}/apichanges.xml diff --git a/ide/projectapi/src/org/netbeans/spi/project/NestedClass.java b/ide/projectapi/src/org/netbeans/spi/project/NestedClass.java index 5df90f2a7104..14f97cf0b8c6 100644 --- a/ide/projectapi/src/org/netbeans/spi/project/NestedClass.java +++ b/ide/projectapi/src/org/netbeans/spi/project/NestedClass.java @@ -46,11 +46,16 @@ public final class NestedClass { * 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 className name of a class inside the file. Can be an empty string + * if this NestedClass represents a top level element in the source file. + * This is relevant for Java, which allows multiple top level class + * declarations as long as they are not public. The class assumes, that the + * {@code className} follows java convention (i.e. is separated by dots). + * @param topLevelClassName top level name of a class inside the file. This + * is the simple name without package qualification. * @param file file to be kept in the object * @exception java.lang.IllegalArgumentException - * if the file or class name is {@code null} + * if the file, topLevelClassName or class name is {@code null} * @since 1.99 */ public NestedClass(String className, String topLevelClassName, FileObject file) { @@ -90,7 +95,8 @@ public String getClassName() { } /** - * Returns name of a top level class within a file. + * Returns name of a top level class within a file. This is the simple name + * without package qualification. * * @return top level class name held by this object * @since 1.99 @@ -108,9 +114,19 @@ public String getTopLevelClassName() { * @since 1.99 */ public String getFQN(String packageName) { - return String.join(".", packageName, topLevelClassName, className); + String classNameSuffix; + if (className.isBlank()) { + classNameSuffix = topLevelClassName; + } else { + classNameSuffix = topLevelClassName + "." + className; + } + if (packageName.isBlank()) { + return classNameSuffix; + } else { + return String.join(".", packageName, classNameSuffix); + } } - + /** * Returns fully qualified name. * @@ -121,9 +137,19 @@ public String getFQN(String packageName) { * @since 1.99 */ public String getFQN(String packageName, String nestedClassSeparator) { - return String.join(".", packageName, String.join(nestedClassSeparator, topLevelClassName, className.replace(".", nestedClassSeparator))); + String classNameSuffix; + if (className.isBlank()) { + classNameSuffix = topLevelClassName; + } else { + classNameSuffix = topLevelClassName + nestedClassSeparator + className.replace(".", nestedClassSeparator); + } + if (packageName.isBlank()) { + return classNameSuffix; + } else { + return String.join(".", packageName, classNameSuffix); + } } - + @Override public int hashCode() { int hash = 3; From c557d8975cfa7e4a48a78d6d19c6b5f6075e4865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Bl=C3=A4sing?= Date: Sun, 20 Jul 2025 13:52:14 +0200 Subject: [PATCH 3/7] Maven JUnit: Use NestedClass info from SingleMethod if present DefaultReplaceTokenProvider looks up NestedClass from invocation context to determine the classname used in tokens. For unittests the NestedClass is passed as part of the SingleMethod so extract it from there. --- .../execute/DefaultReplaceTokenProvider.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) 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 e6c05cbf9ab8..e3ed61429aa8 100644 --- a/java/maven/src/org/netbeans/modules/maven/execute/DefaultReplaceTokenProvider.java +++ b/java/maven/src/org/netbeans/modules/maven/execute/DefaultReplaceTokenProvider.java @@ -102,8 +102,22 @@ private static FileObject[] extractFileObjectsfromLookup(Lookup lookup) { return files.toArray(new FileObject[0]); } - @Override public Map createReplacements(String actionName, Lookup lookup) { + private static NestedClass extractNestedClassFromLookup(Lookup lookup) { NestedClass nestedClass = lookup.lookup(NestedClass.class); + if (nestedClass != null) { + return nestedClass; + } + + SingleMethod sm = lookup.lookup(SingleMethod.class); + if (sm != null) { + return sm.getNestedClass(); + } + + return null; + } + + @Override public Map createReplacements(String actionName, Lookup lookup) { + NestedClass nestedClass = extractNestedClassFromLookup(lookup); FileObject[] fos = extractFileObjectsfromLookup(lookup); List projects = extractProjectsFromLookup(lookup); From d26e2764c254df410fd5b0a5edae6b315a7827d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Bl=C3=A4sing?= Date: Wed, 16 Jul 2025 20:39:12 +0200 Subject: [PATCH 4/7] Maven JUnit: Stabilize unit test result extraction It was observed, that also for singlethreaded runs the XML reports are sometimes created delayed. The existing retry logic for parallel runs needs to be enhanced to cover the single threaded case and also more than one retry. Observed with a trivial nested testcases at least one retry was observed. --- .../junit/JUnitOutputListenerProvider.java | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) 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 bfa3aed455b5..64d78676d5f8 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 @@ -700,15 +700,20 @@ static Trouble constructTrouble(@NonNull String type, @NullAllowed String messag 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 { + File outputDir = null; + // Test report might be in flight, so scan for it multiple times. + // Problems were observed with surefire running tests in parallel and + // also single threaded mode at leat on linus. + try { + for (int i = 1; i <= 40; i++) { + outputDir = locateOutputDir(candidateClass, suffix, consume); + if (outputDir != null) { + LOG.log(Level.FINE, "Found output dir for test {0} in {1}. iteration ", new Object[]{candidateClass, i}); + break; + } Thread.sleep(500); - } catch (InterruptedException ex) { - Exceptions.printStackTrace(ex); } - outputDir = locateOutputDir(candidateClass, suffix, consume); + } catch (InterruptedException ex) { } return outputDir; } From 37cb3a0bbe7290ddfc4bef44ebbbe572f762d2fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Bl=C3=A4sing?= Date: Sun, 20 Jul 2025 11:52:58 +0200 Subject: [PATCH 5/7] Maven JUnit: add org.junit.jupiter:junit-jupiter-api as another module to indicate JUnit5 usage This pom.xml runs fine with surefire, but triggers the update surefire/junit warning: ``` 4.0.0 test TestNested 1.0-SNAPSHOT jar UTF-8 17 test.testnested.TestNested org.junit.jupiter junit-jupiter-api 5.13.3 test jar org.junit.jupiter junit-jupiter-params 5.13.3 test jar org.apache.maven.plugins maven-surefire-plugin 3.5.3 ``` --- .../src/org/netbeans/modules/maven/ActionProviderImpl.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/java/maven/src/org/netbeans/modules/maven/ActionProviderImpl.java b/java/maven/src/org/netbeans/modules/maven/ActionProviderImpl.java index 1ebff0ee4680..183f2790f478 100644 --- a/java/maven/src/org/netbeans/modules/maven/ActionProviderImpl.java +++ b/java/maven/src/org/netbeans/modules/maven/ActionProviderImpl.java @@ -226,8 +226,11 @@ private boolean usingJUnit4() { // SUREFIRE-724 private boolean usingJUnit5() { return proj.getLookup().lookup(NbMavenProject.class).getMavenProject().getArtifacts() .stream() - .anyMatch((a) -> ("org.junit.jupiter".equals(a.getGroupId()) && "junit-jupiter-engine".equals(a.getArtifactId()) || - "org.junit.platform".equals(a.getGroupId()) && "junit-platform-engine".equals(a.getArtifactId()))); + .anyMatch((a) -> ( + "org.junit.jupiter".equals(a.getGroupId()) && "junit-jupiter-engine".equals(a.getArtifactId()) || + "org.junit.jupiter".equals(a.getGroupId()) && "junit-jupiter-api".equals(a.getArtifactId()) || + "org.junit.platform".equals(a.getGroupId()) && "junit-platform-engine".equals(a.getArtifactId())) + ); } private boolean usingTestNG() { From aa9c7a7a85af2af64b47e0c77ec5616c9d53cb64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Bl=C3=A4sing?= Date: Thu, 17 Jul 2025 18:34:30 +0200 Subject: [PATCH 6/7] Maven JUnit: Correctly handle nested, and non-public top level classes --- .../junit/ui/actions/TestClassInfoTask.java | 39 ++++-- .../nbproject/project.properties | 2 +- .../maven/junit/ui/MavenJUnitNodeOpener.java | 123 +++++++++++------- .../junit/ui/MavenJUnitTestMethodNode.java | 34 +++-- java/maven.junit/nbproject/project.xml | 8 ++ .../junit/JUnitOutputListenerProvider.java | 38 +++++- 6 files changed, 180 insertions(+), 64 deletions(-) diff --git a/java/junit.ui/src/org/netbeans/modules/junit/ui/actions/TestClassInfoTask.java b/java/junit.ui/src/org/netbeans/modules/junit/ui/actions/TestClassInfoTask.java index 00103e9b961d..d73a7f403ce8 100644 --- a/java/junit.ui/src/org/netbeans/modules/junit/ui/actions/TestClassInfoTask.java +++ b/java/junit.ui/src/org/netbeans/modules/junit/ui/actions/TestClassInfoTask.java @@ -57,6 +57,7 @@ import org.netbeans.modules.java.testrunner.ui.spi.ComputeTestMethods.Factory; import org.netbeans.modules.parsing.spi.Parser; import org.netbeans.spi.java.hints.unused.UsedDetector; +import org.netbeans.spi.project.NestedClass; import org.netbeans.spi.project.SingleMethod; import org.openide.filesystems.FileObject; import org.openide.util.Exceptions; @@ -94,14 +95,17 @@ public static List computeTestMethods(CompilationInfo info, AtomicBo private static List doComputeTestMethods(CompilationInfo info, AtomicBoolean cancel, int caretPosIfAny) { List result = new ArrayList<>(); if (caretPosIfAny == (-1)) { - Optional anyClass = info.getCompilationUnit().getTypeDecls().stream().filter(t -> t.getKind() == Kind.CLASS).findAny(); - if (!anyClass.isPresent()) { - return Collections.emptyList(); + List clazzes = info.getCompilationUnit() + .getTypeDecls() + .stream() + .filter(t -> t.getKind() == Kind.CLASS) + .map(t -> (ClassTree) t) + .collect(Collectors.toList()); + for (ClassTree clazz : clazzes) { + TreePath pathToClass = new TreePath(new TreePath(info.getCompilationUnit()), clazz); + List methods = clazz.getMembers().stream().filter(m -> m.getKind() == Kind.METHOD).map(m -> new TreePath(pathToClass, m)).collect(Collectors.toList()); + collect(info, pathToClass, methods, true, cancel, result); } - ClassTree clazz = (ClassTree) anyClass.get(); - TreePath pathToClass = new TreePath(new TreePath(info.getCompilationUnit()), clazz); - List methods = clazz.getMembers().stream().filter(m -> m.getKind() == Kind.METHOD).map(m -> new TreePath(pathToClass, m)).collect(Collectors.toList()); - collect(info, pathToClass, methods, true, cancel, result); return result; } TreePath tp = info.getTreeUtilities().pathFor(caretPosIfAny); @@ -126,6 +130,7 @@ private static void collect(CompilationInfo info, TreePath clazz, List int clazzPreferred = treeUtilities.findNameSpan((ClassTree) clazz.getLeaf())[0]; TypeElement typeElement = (TypeElement) trees.getElement(clazz); TypeElement testcase = elements.getTypeElement(TESTCASE); + NestedClass nc = getNestedClass(info, typeElement); boolean junit3 = (testcase != null && typeElement != null) ? info.getTypes().isSubtype(typeElement.asType(), testcase.asType()) : false; for (TreePath tp : methods) { if (cancel.get()) { @@ -152,7 +157,7 @@ private static void collect(CompilationInfo info, TreePath clazz, List try { result.add(new TestMethod(elements.getBinaryName(typeElement).toString(), doc != null ? doc.createPosition(clazzPreferred) : new SimplePosition(clazzPreferred), - new SingleMethod(info.getFileObject(), mn), + nc == null ? new SingleMethod(info.getFileObject(), mn) : new SingleMethod(mn, nc), doc != null ? doc.createPosition(start) : new SimplePosition(start), doc != null ? doc.createPosition(preferred) : new SimplePosition(preferred), doc != null ? doc.createPosition(end) : new SimplePosition(end))); @@ -178,6 +183,24 @@ private static void collect(CompilationInfo info, TreePath clazz, List } } + private static NestedClass getNestedClass(CompilationInfo ci, TypeElement te) { + List nesting = new ArrayList<>(); + Element currentElement = te; + while (currentElement != null && currentElement.getKind() == ElementKind.CLASS) { + nesting.add(0, currentElement.getSimpleName().toString()); + currentElement = currentElement.getEnclosingElement(); + } + if(nesting.size() < 1 || (nesting.size() == 1 && nesting.get(0).equals(ci.getFileObject().getName()))) { + return null; + } else { + return new NestedClass( + nesting.subList(1, nesting.size()).stream().collect(Collectors.joining(".")), + nesting.get(0), + ci.getFileObject() + ); + } + } + private static boolean isTestSource(FileObject fo) { ClassPath cp = ClassPath.getClassPath(fo, ClassPath.SOURCE); if (cp != null) { diff --git a/java/maven.junit.ui/nbproject/project.properties b/java/maven.junit.ui/nbproject/project.properties index 26aa41b5ff22..ea24dfd387a4 100644 --- a/java/maven.junit.ui/nbproject/project.properties +++ b/java/maven.junit.ui/nbproject/project.properties @@ -15,6 +15,6 @@ # specific language governing permissions and limitations # under the License. is.eager=true -javac.source=1.8 +javac.release=17 javac.compilerargs=-Xlint -Xlint:-serial requires.nb.javac=true diff --git a/java/maven.junit.ui/src/org/netbeans/modules/maven/junit/ui/MavenJUnitNodeOpener.java b/java/maven.junit.ui/src/org/netbeans/modules/maven/junit/ui/MavenJUnitNodeOpener.java index daf44d4eb19c..d4d36712c3f7 100644 --- a/java/maven.junit.ui/src/org/netbeans/modules/maven/junit/ui/MavenJUnitNodeOpener.java +++ b/java/maven.junit.ui/src/org/netbeans/modules/maven/junit/ui/MavenJUnitNodeOpener.java @@ -29,25 +29,25 @@ import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.util.ElementFilter; -import javax.swing.Action; import org.netbeans.api.annotations.common.NonNull; import org.netbeans.api.extexecution.print.LineConvertors.FileLocator; -import org.netbeans.api.java.source.CompilationController; import org.netbeans.api.java.source.JavaSource; import org.netbeans.api.java.source.JavaSource.Phase; -import org.netbeans.api.java.source.Task; import org.netbeans.modules.gsf.testrunner.api.CommonUtils; import org.netbeans.modules.gsf.testrunner.ui.api.TestMethodNode; import org.netbeans.modules.gsf.testrunner.ui.api.TestsuiteNode; import org.netbeans.modules.junit.ui.api.JUnitTestMethodNode; import org.netbeans.modules.java.testrunner.ui.api.NodeOpener; import org.netbeans.modules.java.testrunner.ui.api.UIJavaUtils; +import org.netbeans.modules.junit.api.JUnitTestcase; import org.netbeans.modules.junit.ui.api.JUnitCallstackFrameNode; import org.openide.ErrorManager; import org.openide.filesystems.FileObject; import org.openide.nodes.Children; import org.openide.nodes.Node; +import static java.util.Arrays.asList; + /** * * @author Marian Petras @@ -57,33 +57,31 @@ public final class MavenJUnitNodeOpener extends NodeOpener { private static final Logger LOG = Logger.getLogger(MavenJUnitNodeOpener.class.getName()); - static final Action[] NO_ACTIONS = new Action[0]; - + @Override public void openTestsuite(TestsuiteNode node) { Children childrens = node.getChildren(); if (childrens != null) { Node child = childrens.getNodeAt(0); - if (child instanceof MavenJUnitTestMethodNode) { - final FileObject fo = ((MavenJUnitTestMethodNode) child).getTestcaseFileObject(); + if (child instanceof MavenJUnitTestMethodNode junitMethodNode) { + final FileObject fo = junitMethodNode.getTestcaseFileObject(); + final MethodInfo mi = MethodInfo.fromTestCase(junitMethodNode.getTestcase()); if (fo != null) { final long[] line = new long[]{0}; JavaSource javaSource = JavaSource.forFileObject(fo); if (javaSource != null) { try { - javaSource.runUserActionTask(new Task() { - @Override - public void run(CompilationController compilationController) throws Exception { - compilationController.toPhase(Phase.ELEMENTS_RESOLVED); - Trees trees = compilationController.getTrees(); - CompilationUnitTree compilationUnitTree = compilationController.getCompilationUnit(); - List typeDecls = compilationUnitTree.getTypeDecls(); - for (Tree tree : typeDecls) { - Element element = trees.getElement(trees.getPath(compilationUnitTree, tree)); - if (element != null && element.getKind() == ElementKind.CLASS && element.getSimpleName().contentEquals(fo.getName())) { - long pos = trees.getSourcePositions().getStartPosition(compilationUnitTree, tree); - line[0] = compilationUnitTree.getLineMap().getLineNumber(pos); - break; - } + javaSource.runUserActionTask(compilationController -> { + compilationController.toPhase(Phase.ELEMENTS_RESOLVED); + Trees trees = compilationController.getTrees(); + CompilationUnitTree compilationUnitTree = compilationController.getCompilationUnit(); + List typeDecls = compilationUnitTree.getTypeDecls(); + for (Tree tree : typeDecls) { + Element element = trees.getElement(trees.getPath(compilationUnitTree, tree)); + if (element != null && element.getKind() == ElementKind.CLASS && element.getSimpleName().contentEquals(mi.topLevelClass())) { + element = resolveNestedClass(mi.nestedClasses(), element); + long pos = trees.getSourcePositions().getStartPosition(compilationUnitTree, trees.getTree(element)); + line[0] = compilationUnitTree.getLineMap().getLineNumber(pos); + break; } } }, true); @@ -97,42 +95,43 @@ public void run(CompilationController compilationController) throws Exception { } } + @Override public void openTestMethod(final TestMethodNode node) { if (!(node instanceof MavenJUnitTestMethodNode)) { return; } - final FileObject fo = ((MavenJUnitTestMethodNode) node).getTestcaseFileObject(); + MavenJUnitTestMethodNode mtn = (MavenJUnitTestMethodNode) node; + final FileObject fo = mtn.getTestcaseFileObject(); + final MethodInfo mi = MethodInfo.fromTestCase(mtn.getTestcase()); if (fo != null) { final FileObject[] fo2open = new FileObject[]{fo}; final long[] line = new long[]{0}; JavaSource javaSource = JavaSource.forFileObject(fo2open[0]); if (javaSource != null) { try { - javaSource.runUserActionTask(new Task() { - @Override - public void run(CompilationController compilationController) throws Exception { - compilationController.toPhase(Phase.ELEMENTS_RESOLVED); - Trees trees = compilationController.getTrees(); - CompilationUnitTree compilationUnitTree = compilationController.getCompilationUnit(); - List typeDecls = compilationUnitTree.getTypeDecls(); - for (Tree tree : typeDecls) { - Element element = trees.getElement(trees.getPath(compilationUnitTree, tree)); - if (element != null && element.getKind() == ElementKind.CLASS && element.getSimpleName().contentEquals(fo2open[0].getName())) { - List methodElements = ElementFilter.methodsIn(element.getEnclosedElements()); - for (Element child : methodElements) { - String name = node.getTestcase().getName(); // package.name.method.name - if (child.getSimpleName().contentEquals(name.substring(name.lastIndexOf(".") + 1))) { - long pos = trees.getSourcePositions().getStartPosition(compilationUnitTree, trees.getTree(child)); - line[0] = compilationUnitTree.getLineMap().getLineNumber(pos); - break; - } + javaSource.runUserActionTask(compilationController -> { + compilationController.toPhase(Phase.ELEMENTS_RESOLVED); + Trees trees = compilationController.getTrees(); + CompilationUnitTree compilationUnitTree = compilationController.getCompilationUnit(); + List typeDecls = compilationUnitTree.getTypeDecls(); + for (Tree tree : typeDecls) { + Element element = trees.getElement(trees.getPath(compilationUnitTree, tree)); + if (element != null && element.getKind() == ElementKind.CLASS && element.getSimpleName().contentEquals(mi.topLevelClass())) { + element = resolveNestedClass(mi.nestedClasses(), element); + List methodElements = ElementFilter.methodsIn(element.getEnclosedElements()); + for (Element child : methodElements) { + String name = node.getTestcase().getName(); // package.name.method.name + if (child.getSimpleName().contentEquals(name.substring(name.lastIndexOf(".") + 1))) { + long pos = trees.getSourcePositions().getStartPosition(compilationUnitTree, trees.getTree(child)); + line[0] = compilationUnitTree.getLineMap().getLineNumber(pos); + break; } - // method not found in this FO, so try to find where this method belongs - if (line[0] == 0) { - UIJavaUtils.searchAllMethods(node, fo2open, line, compilationController, element); - } - break; } + // method not found in this FO, so try to find where this method belongs + if (line[0] == 0) { + UIJavaUtils.searchAllMethods(node, fo2open, line, compilationController, element); + } + break; } } }, true); @@ -145,6 +144,7 @@ public void run(CompilationController compilationController) throws Exception { } } + @Override public void openCallstackFrame(Node node, @NonNull String frameInfo) { if(frameInfo.isEmpty()) { // user probably clicked on a failed test method node, find failing line within the testMethod using the stacktrace if (!(node instanceof JUnitTestMethodNode)) { @@ -195,7 +195,7 @@ public void openCallstackFrame(Node node, @NonNull String frameInfo) { // and ignore the infrastructure stack lines in the process while (!testfo.equals(file) && index != -1) { file = UIJavaUtils.getFile(st[index], lineNumStorage, locator); - index = index - 1; + index -= 1; } } } @@ -206,4 +206,35 @@ public void openCallstackFrame(Node node, @NonNull String frameInfo) { UIJavaUtils.openFile(file, lineNumStorage[0]); } + private Element resolveNestedClass(List nestedClasses, Element e) { + if(nestedClasses.isEmpty()) { + return e; + } else { + String simpleName = nestedClasses.get(0); + for(Element childElement: e.getEnclosedElements()) { + if(childElement.getSimpleName().contentEquals(simpleName)) { + return resolveNestedClass(nestedClasses.subList(1, nestedClasses.size()), childElement); + } + } + return e; + } + } + + private record MethodInfo (String packageName, String topLevelClass, List nestedClasses, String method) { + public static MethodInfo fromTestCase(JUnitTestcase testcase) { + String className = testcase.getClassName(); + String[] nestedClasses = className.split("\\$"); + String packageName = null; + int lastDotInTopLevelClass = nestedClasses[0].lastIndexOf("."); + if (lastDotInTopLevelClass >= 0) { + packageName = nestedClasses[0].substring(0, lastDotInTopLevelClass); + nestedClasses[0] = nestedClasses[0].substring(lastDotInTopLevelClass + 1); + } + String method = null; + if(testcase.getName().startsWith(className)) { + method = testcase.getName().substring(className.length() + 1); + } + return new MethodInfo(packageName, nestedClasses[0], asList(nestedClasses).subList(1, nestedClasses.length), method); + } + } } diff --git a/java/maven.junit.ui/src/org/netbeans/modules/maven/junit/ui/MavenJUnitTestMethodNode.java b/java/maven.junit.ui/src/org/netbeans/modules/maven/junit/ui/MavenJUnitTestMethodNode.java index bb0b7f1af117..67ce0eecf6e3 100644 --- a/java/maven.junit.ui/src/org/netbeans/modules/maven/junit/ui/MavenJUnitTestMethodNode.java +++ b/java/maven.junit.ui/src/org/netbeans/modules/maven/junit/ui/MavenJUnitTestMethodNode.java @@ -28,18 +28,20 @@ import org.netbeans.api.extexecution.print.LineConvertors; import org.netbeans.api.project.FileOwnerQuery; import org.netbeans.api.project.Project; -import org.netbeans.modules.gsf.testrunner.api.Testcase; import org.netbeans.modules.gsf.testrunner.api.TestMethodNodeAction; +import org.netbeans.modules.gsf.testrunner.api.Testcase; import org.netbeans.modules.junit.ui.api.JUnitTestMethodNode; import org.netbeans.spi.project.ActionProvider; +import org.netbeans.spi.project.NestedClass; import org.netbeans.spi.project.SingleMethod; -import static org.netbeans.spi.project.SingleMethod.COMMAND_DEBUG_SINGLE_METHOD; -import static org.netbeans.spi.project.SingleMethod.COMMAND_RUN_SINGLE_METHOD; import org.openide.filesystems.FileObject; import org.openide.util.Lookup; import org.openide.util.NbBundle.Messages; import org.openide.util.lookup.Lookups; +import static org.netbeans.spi.project.SingleMethod.COMMAND_DEBUG_SINGLE_METHOD; +import static org.netbeans.spi.project.SingleMethod.COMMAND_RUN_SINGLE_METHOD; + /** * mkleint: copied from junit module * @@ -72,13 +74,29 @@ public Action[] getActions(boolean context) { if (actionProvider != null) { String mName = testcase.getName(); String tcName= testcase.getClassName(); - if (tcName!=null + if (tcName != null && mName.startsWith(tcName) - && mName.charAt(tcName.length())=='.'){ - mName= mName.substring(tcName.length()+1); + && mName.charAt(tcName.length()) == '.') { + mName = mName.substring(tcName.length() + 1); + } + SingleMethod methodSpec; + if (tcName != null && tcName.contains("$")) { + String[] nestedSplit = tcName.split("\\$", 2); + String[] topLevelSplit = nestedSplit[0].split("\\."); + methodSpec = new SingleMethod(mName, new NestedClass(nestedSplit[1].replace("$", "."), topLevelSplit[topLevelSplit.length - 1], testFO)); + } else { + if(tcName != null) { + String[] topLevelSplit = tcName.split("\\."); + if(! testFO.getName().equals(topLevelSplit[topLevelSplit.length - 1])) { + methodSpec = new SingleMethod(mName, new NestedClass("", topLevelSplit[topLevelSplit.length - 1], testFO)); + } else { + methodSpec = new SingleMethod(testFO, mName); + } + } else { + methodSpec = new SingleMethod(testFO, mName); + } } - SingleMethod methodSpec = new SingleMethod(testFO, mName); - Lookup nodeContext = Lookups.singleton(methodSpec); + Lookup nodeContext = Lookups.fixed(methodSpec); for (String action : actionProvider.getSupportedActions()) { if (unitTest diff --git a/java/maven.junit/nbproject/project.xml b/java/maven.junit/nbproject/project.xml index 9d92e0b37134..27429dc43a6c 100644 --- a/java/maven.junit/nbproject/project.xml +++ b/java/maven.junit/nbproject/project.xml @@ -34,6 +34,14 @@ 1.24 + + org.netbeans.libs.javacapi + + + + 8.53 + + org.netbeans.modules.gsf.testrunner 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 64d78676d5f8..2802ade19611 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 @@ -20,6 +20,7 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Path; import java.text.NumberFormat; import java.text.ParseException; import java.util.ArrayList; @@ -30,6 +31,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.StringTokenizer; @@ -37,6 +39,8 @@ import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; +import javax.lang.model.element.ElementKind; import javax.swing.event.ChangeListener; import org.apache.maven.project.MavenProject; import org.apache.maven.artifact.Artifact; @@ -50,6 +54,9 @@ import org.jdom2.input.SAXBuilder; import org.netbeans.api.annotations.common.NonNull; import org.netbeans.api.annotations.common.NullAllowed; +import org.netbeans.api.java.source.ClasspathInfo; +import org.netbeans.api.java.source.ElementHandle; +import org.netbeans.api.java.source.SourceUtils; import org.netbeans.api.project.FileOwnerQuery; import org.netbeans.api.project.Project; import org.netbeans.modules.gsf.testrunner.api.RerunHandler; @@ -71,6 +78,7 @@ import org.netbeans.modules.maven.api.execute.RunUtils; import org.netbeans.modules.maven.api.output.OutputProcessor; import org.netbeans.modules.maven.api.output.OutputVisitor; +import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; import org.openide.util.Exceptions; import org.openide.util.Lookup; @@ -735,6 +743,22 @@ private void generateTest() { LOG.log(Level.FINE, "No session for outdir {0}", outputDir); return; } + Map classpathInfo = Map.of(); + NbMavenProject nbMavenProject = session.getProject() + .getLookup() + .lookup(NbMavenProject.class); + if(nbMavenProject != null) { + classpathInfo = nbMavenProject + .getMavenProject() + .getTestCompileSourceRoots() + .stream() + .map(p -> (p.endsWith("/") || p.endsWith("\\")) ? p : (p + "/")) + .map(p -> new File(p)) + .collect(Collectors.toMap( + f -> ClasspathInfo.create(f), + f -> f.toPath() + )); + } if (report.length() > 50 * 1024 * 1024) { LOG.log(Level.INFO, "Skipping report file as size is too big (> 50MB): {0}", report.getPath()); return; @@ -831,7 +855,19 @@ private void generateTest() { classname = classname.substring(0, classname.length() - nameSuffix.length()); } test.setClassName(classname); - test.setLocation(test.getClassName().replace('.', '/') + ".java"); + String relativePath = null; + for (Entry ci : classpathInfo.entrySet()) { + FileObject fo = SourceUtils.getFile(ElementHandle.createTypeElementHandle(ElementKind.CLASS, classname), ci.getKey()); + if (fo != null) { + relativePath = ci.getValue().relativize(FileUtil.toFile(fo).toPath()).toString(); + break; + } + } + if (relativePath != null) { + test.setLocation(relativePath); + } else { + test.setLocation(classname.replace('.', '/').split("\\$")[0] + ".java"); + } } session.addTestCase(test); } From d21c96133591332a5537d70d7c5d6bdbf8059e00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20Bl=C3=A4sing?= Date: Tue, 22 Jul 2025 21:15:54 +0200 Subject: [PATCH 7/7] Gradle JUnit: Support nested and top-level non-public tests --- java/gradle.test/nbproject/project.properties | 2 +- java/gradle.test/nbproject/project.xml | 36 +++++--- .../test/GradleTestProgressListener.java | 92 +++++++++++++++---- .../test/ui/nodes/GradleTestMethodNode.java | 48 +++++++--- 4 files changed, 132 insertions(+), 46 deletions(-) diff --git a/java/gradle.test/nbproject/project.properties b/java/gradle.test/nbproject/project.properties index f39ff4a02687..6ef89b565a6b 100644 --- a/java/gradle.test/nbproject/project.properties +++ b/java/gradle.test/nbproject/project.properties @@ -16,6 +16,6 @@ # under the License. is.eager=true -javac.source=1.8 +javac.release=17 javac.compilerargs=-Xlint -Xlint:-serial nbm.module.author=Laszlo Kishalmi diff --git a/java/gradle.test/nbproject/project.xml b/java/gradle.test/nbproject/project.xml index efe3ef690ebb..b796e54b21b0 100644 --- a/java/gradle.test/nbproject/project.xml +++ b/java/gradle.test/nbproject/project.xml @@ -26,46 +26,46 @@ org.netbeans.modules.gradle.test - org.netbeans.modules.libs.gradle + org.netbeans.api.java.classpath + - 8 - 8.0.1 + 1 + 1.41.1 - org.netbeans.modules.gradle + org.netbeans.libs.javacapi - 2 - 2.0 + 8.53 - org.netbeans.modules.gradle.java + org.netbeans.modules.extexecution - 1.17 + 2 + 1.45 - org.netbeans.api.java.classpath + org.netbeans.modules.gradle - 1 - 1.41.1 + 2 + 2.0 - org.netbeans.modules.extexecution + org.netbeans.modules.gradle.java - 2 - 1.45 + 1.17 @@ -118,6 +118,14 @@ 1.3.1 + + org.netbeans.modules.libs.gradle + + + 8 + 8.0.1 + + org.netbeans.modules.projectapi 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 a45f9d20176d..cacf2c8d43e1 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 @@ -19,16 +19,17 @@ package org.netbeans.modules.gradle.test; +import java.nio.file.Path; import java.util.Arrays; -import org.netbeans.modules.gradle.api.NbGradleProject; import java.util.Collection; -import org.netbeans.modules.gradle.spi.GradleProgressListenerProvider; import java.util.EnumSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; +import javax.lang.model.element.ElementKind; import org.gradle.tooling.Failure; import org.gradle.tooling.events.OperationDescriptor; import org.gradle.tooling.events.OperationType; @@ -46,8 +47,15 @@ import org.gradle.tooling.events.test.TestSkippedResult; import org.gradle.tooling.events.test.TestStartEvent; import org.gradle.tooling.events.test.TestSuccessResult; +import org.netbeans.api.java.source.ClasspathInfo; +import org.netbeans.api.java.source.ElementHandle; +import org.netbeans.api.java.source.SourceUtils; import org.netbeans.api.project.Project; import org.netbeans.api.project.ProjectUtils; +import org.netbeans.modules.gradle.api.NbGradleProject; +import org.netbeans.modules.gradle.java.api.GradleJavaProject; +import org.netbeans.modules.gradle.java.api.GradleJavaSourceSet.SourceType; +import org.netbeans.modules.gradle.spi.GradleProgressListenerProvider; import org.netbeans.modules.gsf.testrunner.api.CommonUtils; import org.netbeans.modules.gsf.testrunner.api.CoreManager; import org.netbeans.modules.gsf.testrunner.api.Report; @@ -57,6 +65,8 @@ import org.netbeans.modules.gsf.testrunner.api.Testcase; import org.netbeans.modules.gsf.testrunner.api.Trouble; import org.netbeans.spi.project.ProjectServiceProvider; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; import org.openide.util.Lookup; /** @@ -68,8 +78,8 @@ public final class GradleTestProgressListener implements ProgressListener, Gradl private final Project project; private final Map sessions = new ConcurrentHashMap<>(); - - private Map> runningTests = new ConcurrentHashMap<>(); + private final Map> runningSuites = new ConcurrentHashMap<>(); + private final Map> runningTests = new ConcurrentHashMap<>(); public GradleTestProgressListener(Project project) { this.project = project; @@ -190,13 +200,21 @@ 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); - if (suiteName.equals(currentSuite.getName())) { + // In the NetBeans wording a testsuite is the class grouping multiple + // methods (testcase). In the gradle wording a suite can be nested, for + // example the hieararchy can be: + // - Gradle Test Executor started + // - Test class started + // => We flatten the list (suites are registered base on executed + // cases (see caseStart) + TestSuite testSuite = runningSuites.get(session).remove(suiteName); + if (testSuite != null) { Report report = session.getReport(result.getEndTime() - result.getStartTime()); - session.finishSuite(currentSuite); + session.finishSuite(testSuite); CoreManager manager = getManager(); if (manager != null) { + manager.displaySuiteRunning(session, testSuite); manager.displayReport(session, report, true); } } @@ -206,19 +224,21 @@ 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(); - TestSuite newSuite = new GradleTestSuite(getSuiteOpDesc((JvmTestOperationDescriptor) op.getParent(), op.getClassName())); - if ((currentSuite == null) || !currentSuite.equals(newSuite)) { - session.addSuite(newSuite); - CoreManager manager = getManager(); - if (manager != null) { - manager.displaySuiteRunning(session, newSuite); - } + String suiteName = GradleTestSuite.suiteName(op.getParent()); + Map sessionSuites = runningSuites.computeIfAbsent(session, s -> new ConcurrentHashMap<>()); + TestSuite ts = sessionSuites.computeIfAbsent(suiteName, s -> { + TestSuite suite = new GradleTestSuite(getSuiteOpDesc((JvmTestOperationDescriptor) op.getParent(), op.getClassName())); + session.addSuite(suite); + return suite; + }); + CoreManager manager = getManager(); + if (manager != null && sessionSuites.size() == 1) { + manager.displaySuiteRunning(session, ts); } Testcase tc = new GradleTestcase(op, session); - synchronized (this) { + synchronized (this) { runningTests.get(session).put(getTestOpKey(op), tc); - session.addTestCase(tc); + session.addTestCase(tc); } } @@ -233,7 +253,7 @@ private void caseFinish(TestFinishEvent evt, JvmTestOperationDescriptor op) { TestOperationResult result = evt.getResult(); long time = result.getEndTime() - result.getStartTime(); tc.setTimeMillis(time); - tc.setLocation(searchLocation(op.getClassName(), op.getMethodName(), null)); + tc.setLocation(searchLocation(tc, op.getClassName(), op.getMethodName(), null)); if (result instanceof TestSuccessResult) { tc.setStatus(Status.PASSED); } @@ -261,7 +281,7 @@ private void caseFinish(TestFinishEvent evt, JvmTestOperationDescriptor op) { stackTrace = desc.split("\\n"); trouble.setStackTrace(stackTrace); } - tc.setLocation(searchLocation(op.getClassName(), op.getMethodName(), stackTrace)); + tc.setLocation(searchLocation(tc, op.getClassName(), op.getMethodName(), stackTrace)); tc.setTrouble(trouble); } @@ -322,7 +342,39 @@ private static CoreManager getManager() { } - private String searchLocation(String className, String methodName, String[] stackTrace) { + private String searchLocation(Testcase tc, String className, String methodName, String[] stackTrace) { + Map classpathInfo = Map.of(); + NbGradleProject nbGradleProject = tc.getSession() + .getProject() + .getLookup() + .lookup(NbGradleProject.class); + GradleJavaProject gradleJavaProject = nbGradleProject != null ? nbGradleProject.projectLookup(GradleJavaProject.class) : null; + if (gradleJavaProject != null) { + classpathInfo = gradleJavaProject + .getSourceSets() + .values() + .stream() + .flatMap(gradleJavaSourceSet -> gradleJavaSourceSet.getSourceDirs(SourceType.JAVA).stream()) + .collect( + Collectors.toMap( + f -> ClasspathInfo.create(f), + f -> f.toPath() + ) + ); + } + + String relativePath = null; + for (Map.Entry ci : classpathInfo.entrySet()) { + FileObject fo = SourceUtils.getFile(ElementHandle.createTypeElementHandle(ElementKind.CLASS, className), ci.getKey()); + if (fo != null) { + relativePath = ci.getValue().relativize(FileUtil.toFile(fo).toPath()).toString(); + break; + } + } + if (relativePath != null) { + return relativePath; + } + StringBuilder ret = new StringBuilder(className.length() + methodName.length() + 10); String fileName = null; String line = null; diff --git a/java/gradle.test/src/org/netbeans/modules/gradle/test/ui/nodes/GradleTestMethodNode.java b/java/gradle.test/src/org/netbeans/modules/gradle/test/ui/nodes/GradleTestMethodNode.java index 4e11099aff07..13733d9445c5 100644 --- a/java/gradle.test/src/org/netbeans/modules/gradle/test/ui/nodes/GradleTestMethodNode.java +++ b/java/gradle.test/src/org/netbeans/modules/gradle/test/ui/nodes/GradleTestMethodNode.java @@ -19,10 +19,6 @@ package org.netbeans.modules.gradle.test.ui.nodes; -import org.netbeans.modules.gradle.api.execute.RunUtils; -import org.netbeans.modules.gradle.java.api.output.Location; -import org.netbeans.modules.gradle.test.ui.nodes.Bundle; -import org.netbeans.modules.gradle.test.GradleTestcase; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -30,16 +26,21 @@ import org.gradle.tooling.events.test.JvmTestOperationDescriptor; import org.netbeans.api.extexecution.print.LineConvertors; import org.netbeans.api.project.Project; +import org.netbeans.modules.gradle.java.api.output.Location; +import org.netbeans.modules.gradle.test.GradleTestcase; import org.netbeans.modules.gsf.testrunner.api.Testcase; import org.netbeans.modules.junit.ui.api.JUnitTestMethodNode; import org.netbeans.spi.project.ActionProvider; -import static org.netbeans.spi.project.SingleMethod.COMMAND_DEBUG_SINGLE_METHOD; -import static org.netbeans.spi.project.SingleMethod.COMMAND_RUN_SINGLE_METHOD; +import org.netbeans.spi.project.NestedClass; +import org.netbeans.spi.project.SingleMethod; import org.openide.filesystems.FileObject; import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.openide.util.lookup.Lookups; +import static org.netbeans.spi.project.SingleMethod.COMMAND_DEBUG_SINGLE_METHOD; +import static org.netbeans.spi.project.SingleMethod.COMMAND_RUN_SINGLE_METHOD; + /** * * @author Laszlo Kishalmi @@ -66,14 +67,39 @@ public Action[] getActions(boolean context) { actions.add(getPreferredAction()); } ActionProvider actionProvider = getProject().getLookup().lookup(ActionProvider.class); - if ((actionProvider != null) && (testcase instanceof GradleTestcase)) { + if ((actionProvider != null) && testcase instanceof GradleTestcase gradleTestcase) { List supportedActions = Arrays.asList(actionProvider.getSupportedActions()); boolean runSupported = supportedActions.contains(COMMAND_RUN_SINGLE_METHOD); boolean debugSupported = supportedActions.contains(COMMAND_DEBUG_SINGLE_METHOD); - JvmTestOperationDescriptor op = ((GradleTestcase) testcase).getOperation(); - String tcName = op.getClassName() + '.' + op.getMethodName(); - Lookup nodeContext = Lookups.singleton(RunUtils.simpleReplaceTokenProvider("selectedMethod", tcName)); + FileObject testFO = findFileObject(getTestLocation()); + JvmTestOperationDescriptor op = gradleTestcase.getOperation(); + // reporting adds signature to method name, this needs to be stripped away + String mName = op.getMethodName(); + if(mName != null) { + mName = mName.replaceFirst("[^\\p{javaJavaIdentifierPart}].*", ""); + } + String tcName = op.getClassName(); + + SingleMethod methodSpec; + if (tcName != null && tcName.contains("$")) { + String[] nestedSplit = tcName.split("\\$", 2); + String[] topLevelSplit = nestedSplit[0].split("\\."); + methodSpec = new SingleMethod(mName, new NestedClass(nestedSplit[1].replace("$", "."), topLevelSplit[topLevelSplit.length - 1], testFO)); + } else { + if (tcName != null) { + String[] topLevelSplit = tcName.split("\\."); + if (!testFO.getName().equals(topLevelSplit[topLevelSplit.length - 1])) { + methodSpec = new SingleMethod(mName, new NestedClass("", topLevelSplit[topLevelSplit.length - 1], testFO)); + } else { + methodSpec = new SingleMethod(testFO, mName); + } + } else { + methodSpec = new SingleMethod(testFO, mName); + } + } + + Lookup nodeContext = Lookups.fixed(methodSpec); if (runSupported) { actions.add(new ReRunTestAction(actionProvider, nodeContext, COMMAND_RUN_SINGLE_METHOD, Bundle.LBL_RerunTest())); @@ -83,7 +109,7 @@ public Action[] getActions(boolean context) { actions.add(new ReRunTestAction(actionProvider, nodeContext, COMMAND_DEBUG_SINGLE_METHOD, Bundle.LBL_DebugTest())); } } - return actions.toArray(new Action[0]); + return actions.toArray(Action[]::new); } @Override