From b58c367f79537d0fa79275209e14db375d082c1e Mon Sep 17 00:00:00 2001 From: ratcash Date: Tue, 3 Jun 2025 08:38:57 +0200 Subject: [PATCH 1/2] Support JUnit5 Nested annotation --- .../api/java/classpath/ClassPath.java | 5 ++- .../junit/ui/actions/TestClassInfoTask.java | 26 ++++++++--- .../maven/junit/ui/MavenJUnitNodeOpener.java | 43 ++++++++++++++++++- .../junit/JUnitOutputListenerProvider.java | 8 +++- .../netbeans/modules/maven/TestChecker.java | 23 ++++++++-- .../execute/DefaultReplaceTokenProvider.java | 2 + .../actions/AbstractMavenActionsProvider.java | 1 + 7 files changed, 94 insertions(+), 14 deletions(-) diff --git a/ide/api.java.classpath/src/org/netbeans/api/java/classpath/ClassPath.java b/ide/api.java.classpath/src/org/netbeans/api/java/classpath/ClassPath.java index 65b78191e069..5196f52749fc 100644 --- a/ide/api.java.classpath/src/org/netbeans/api/java/classpath/ClassPath.java +++ b/ide/api.java.classpath/src/org/netbeans/api/java/classpath/ClassPath.java @@ -1204,8 +1204,9 @@ private static FileObject findPath( final StringBuilder relativePathBuilder = new StringBuilder(); FileObject child; String separator = ""; //NOI18N - for (int i = 0; i < nameParts.length && parent != null; i += 2, parent = child) { - child = parent.getFileObject(nameParts[i], nameParts[i + 1]); + for (int i = 0; i < nameParts.length && parent != null; i += 2, parent = child) { + String classDefinedInFile = nameParts[i].split("\\$")[0]; + child = parent.getFileObject(classDefinedInFile, nameParts[i + 1]); if (child != null) { relativePathBuilder.append(separator).append(child.getNameExt()); } 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..eb5f078ce4ec 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 @@ -58,6 +58,7 @@ import org.netbeans.modules.parsing.spi.Parser; import org.netbeans.spi.java.hints.unused.UsedDetector; import org.netbeans.spi.project.SingleMethod; +import org.netbeans.spi.project.NestedClass; import org.openide.filesystems.FileObject; import org.openide.util.Exceptions; import org.openide.util.lookup.ServiceProvider; @@ -150,12 +151,13 @@ private static void collect(CompilationInfo info, TreePath clazz, List int end = (int) sp.getEndPosition(tp.getCompilationUnit(), tp.getLeaf()); Document doc = info.getSnapshot().getSource().getDocument(false); try { + NestedClass nestedClass = getNestedClass(elements.getBinaryName(typeElement).toString(), info.getFileObject()); result.add(new TestMethod(elements.getBinaryName(typeElement).toString(), - doc != null ? doc.createPosition(clazzPreferred) : new SimplePosition(clazzPreferred), - new SingleMethod(info.getFileObject(), mn), - doc != null ? doc.createPosition(start) : new SimplePosition(start), - doc != null ? doc.createPosition(preferred) : new SimplePosition(preferred), - doc != null ? doc.createPosition(end) : new SimplePosition(end))); + doc != null ? doc.createPosition(clazzPreferred) : new SimplePosition(clazzPreferred), + nestedClass != null ? new SingleMethod(mn, nestedClass) : new SingleMethod(info.getFileObject(), mn), + doc != null ? doc.createPosition(start) : new SimplePosition(start), + doc != null ? doc.createPosition(preferred) : new SimplePosition(preferred), + doc != null ? doc.createPosition(end) : new SimplePosition(end))); } catch (BadLocationException ex) { //ignore } @@ -178,6 +180,20 @@ private static void collect(CompilationInfo info, TreePath clazz, List } } + private static NestedClass getNestedClass(String fqClassName, FileObject fileObject) { + // drop the package name + String fileName = fileObject.getName(); + String classOnly = fqClassName.substring(fqClassName.lastIndexOf('.') + 1); + if (classOnly.startsWith(fileName) && classOnly.length() > fileName.length()) { + int topLevelClassSeparatorIdx = classOnly.indexOf("$"); + String topLevelClass = classOnly.substring(0,topLevelClassSeparatorIdx); + String nestedName = classOnly.substring(topLevelClassSeparatorIdx + 1); + NestedClass nestedClass = new NestedClass(nestedName, topLevelClass, fileObject); + return nestedClass; + } + return null; + } + private static boolean isTestSource(FileObject fo) { ClassPath cp = ClassPath.getClassPath(fo, ClassPath.SOURCE); if (cp != null) { 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..c2c1c5465a40 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 @@ -114,11 +114,16 @@ public void run(CompilationController compilationController) throws Exception { compilationController.toPhase(Phase.ELEMENTS_RESOLVED); Trees trees = compilationController.getTrees(); CompilationUnitTree compilationUnitTree = compilationController.getCompilationUnit(); + String[] classHierarchy = classHierarchy(node.getTestcase().getClassName()); 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()); + Element classElement = getClassElement(element, classHierarchy, 0); + if (classElement == null) { + classElement = getClassElement(element, new String[] {fo2open[0].getName()}, 0); + } + if (classElement != null) { + List methodElements = ElementFilter.methodsIn(classElement.getEnclosedElements()); for (Element child : methodElements) { String name = node.getTestcase().getName(); // package.name.method.name if (child.getSimpleName().contentEquals(name.substring(name.lastIndexOf(".") + 1))) { @@ -145,6 +150,40 @@ public void run(CompilationController compilationController) throws Exception { } } + private String[] classHierarchy(String testNodeClassName) { + String classNamesOnly = testNodeClassName; + // strip package names + int lastDot = testNodeClassName.lastIndexOf("."); + if (lastDot >= 0) { + classNamesOnly = classNamesOnly.substring(lastDot + 1); + } + + // split embedded classes + return classNamesOnly.split("\\$"); + } + + Element getClassElement(Element element, String[] classHierarchy, int depth) { + if (element == null || element.getKind() != ElementKind.CLASS || depth >= classHierarchy.length) { + return null; + } + + if (!element.getSimpleName().contentEquals(classHierarchy[depth])) { + return null; + } + + if (depth + 1 == classHierarchy.length) { + return element; + } + + for (Element enclosedElement : element.getEnclosedElements()) { + Element enclosedClass = getClassElement(enclosedElement, classHierarchy, depth + 1); + if (enclosedClass != null) { + return enclosedClass; + } + } + return null; + } + 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)) { 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 494251718654..14d04af46194 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 @@ -778,6 +778,13 @@ private void generateTest() { } } + String classname = testcase.getAttributeValue("classname"); + // keep the embedded class in classnames and add to display name + int suiteNameLength = suite.getName().length(); + if (classname.length() > suiteNameLength && classname.startsWith(suite.getName())) { + displayName = classname.substring(suiteNameLength) + "." + methodName; + } + Testcase test = new JUnitTestcase(methodName, displayName, testType, session); Element stdout = testcase.getChild("system-out"); //NOI18N // If *-output.txt file exists do not log standard output here to avoid logging it twice. @@ -811,7 +818,6 @@ private void generateTest() { float fl = NumberFormat.getNumberInstance(Locale.ENGLISH).parse(time).floatValue(); test.setTimeMillis((long)(fl * 1000)); } - String classname = testcase.getAttributeValue("classname"); if (classname != null) { //#204480 if (classname.endsWith(nameSuffix)) { diff --git a/java/maven/src/org/netbeans/modules/maven/TestChecker.java b/java/maven/src/org/netbeans/modules/maven/TestChecker.java index af6282ab896c..7c33ee9d10bb 100644 --- a/java/maven/src/org/netbeans/modules/maven/TestChecker.java +++ b/java/maven/src/org/netbeans/modules/maven/TestChecker.java @@ -54,9 +54,17 @@ public class TestChecker implements PrerequisitesChecker { { //NOI18N - profile-tests is not really nice but well. String test = config.getProperties().get("test"); String method = config.getProperties().get(DefaultReplaceTokenProvider.METHOD_NAME); - if (test != null && method != null) { + String enclosingType = config.getProperties().get(DefaultReplaceTokenProvider.ENCLOSING_TYPE_NAME); + if (test != null) { config.setProperty(DefaultReplaceTokenProvider.METHOD_NAME, null); - config.setProperty("test", test + '#' + method); + config.setProperty(DefaultReplaceTokenProvider.ENCLOSING_TYPE_NAME, null); + // ensure that maven executes all the methods in embedded classes too. + String methodConstraint = method == null ? "," + test + "$*" : "#" + method; + if (enclosingType != null) { + config.setProperty("test", test + enclosingType + methodConstraint); + } else { + config.setProperty("test", test + methodConstraint); + } } } if (ActionProviderImpl.COMMAND_INTEGRATION_TEST_SINGLE.equals(action) || @@ -65,9 +73,16 @@ public class TestChecker implements PrerequisitesChecker { { String test = config.getProperties().get("it.test"); //NOI18N String method = config.getProperties().get(DefaultReplaceTokenProvider.METHOD_NAME); - if (test != null && method != null) { + String enclosingType = config.getProperties().get(DefaultReplaceTokenProvider.ENCLOSING_TYPE_NAME); + if (test != null) { config.setProperty(DefaultReplaceTokenProvider.METHOD_NAME, null); - config.setProperty("it.test", test + '#' + method); //NOI18N + config.setProperty(DefaultReplaceTokenProvider.ENCLOSING_TYPE_NAME, null); + String methodConstraint = method == null ? "," + test + "$*" : "#" + method; + if (enclosingType != null) { + config.setProperty("it.test", test + enclosingType + methodConstraint); + } else { + config.setProperty("it.test", test + methodConstraint); + } } } if (MavenSettings.getDefault().isSkipTests()) { 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..b076fae0aba3 100644 --- a/java/maven/src/org/netbeans/modules/maven/execute/DefaultReplaceTokenProvider.java +++ b/java/maven/src/org/netbeans/modules/maven/execute/DefaultReplaceTokenProvider.java @@ -77,6 +77,7 @@ public class DefaultReplaceTokenProvider implements ReplaceTokenProvider, Action static final String PROJECTS = "projects";//NOI18N static final String ABSOLUTE_PATH = "absolutePathName"; public static final String METHOD_NAME = "nb.single.run.methodName"; //NOI18N + public static final String ENCLOSING_TYPE_NAME = "nb.single.run.enclosingType"; //NOI18N private static final String VARIABLE_PREFIX = "var."; //NOI18N // as defined in org.netbeans.modules.project.ant.VariablesModel public static String[] fileBasedProperties = new String[] { @@ -246,6 +247,7 @@ private static FileObject[] extractFileObjectsfromLookup(Lookup lookup) { //sort of hack to push the method name through the current apis.. SingleMethod method = methods.iterator().next(); replaceMap.put(METHOD_NAME, method.getMethodName()); + replaceMap.put(ENCLOSING_TYPE_NAME, method.getNestedClass().getClassName()); } if (group != null && diff --git a/java/maven/src/org/netbeans/modules/maven/spi/actions/AbstractMavenActionsProvider.java b/java/maven/src/org/netbeans/modules/maven/spi/actions/AbstractMavenActionsProvider.java index a793b5d05b7e..00a58608c605 100644 --- a/java/maven/src/org/netbeans/modules/maven/spi/actions/AbstractMavenActionsProvider.java +++ b/java/maven/src/org/netbeans/modules/maven/spi/actions/AbstractMavenActionsProvider.java @@ -271,6 +271,7 @@ private RunConfig mapGoalsToAction(Project project, String actionName, Map Date: Thu, 5 Jun 2025 11:46:32 +0200 Subject: [PATCH 2/2] add missing class delimiter --- java/maven/src/org/netbeans/modules/maven/TestChecker.java | 4 ++-- .../modules/maven/execute/DefaultReplaceTokenProvider.java | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/java/maven/src/org/netbeans/modules/maven/TestChecker.java b/java/maven/src/org/netbeans/modules/maven/TestChecker.java index 7c33ee9d10bb..6b560d198b32 100644 --- a/java/maven/src/org/netbeans/modules/maven/TestChecker.java +++ b/java/maven/src/org/netbeans/modules/maven/TestChecker.java @@ -61,7 +61,7 @@ public class TestChecker implements PrerequisitesChecker { // ensure that maven executes all the methods in embedded classes too. String methodConstraint = method == null ? "," + test + "$*" : "#" + method; if (enclosingType != null) { - config.setProperty("test", test + enclosingType + methodConstraint); + config.setProperty("test", test + "$" + enclosingType + methodConstraint); } else { config.setProperty("test", test + methodConstraint); } @@ -79,7 +79,7 @@ public class TestChecker implements PrerequisitesChecker { config.setProperty(DefaultReplaceTokenProvider.ENCLOSING_TYPE_NAME, null); String methodConstraint = method == null ? "," + test + "$*" : "#" + method; if (enclosingType != null) { - config.setProperty("it.test", test + enclosingType + methodConstraint); + config.setProperty("it.test", test + "$" + enclosingType + methodConstraint); } else { config.setProperty("it.test", test + methodConstraint); } 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 b076fae0aba3..c53217d84d0e 100644 --- a/java/maven/src/org/netbeans/modules/maven/execute/DefaultReplaceTokenProvider.java +++ b/java/maven/src/org/netbeans/modules/maven/execute/DefaultReplaceTokenProvider.java @@ -247,7 +247,9 @@ private static FileObject[] extractFileObjectsfromLookup(Lookup lookup) { //sort of hack to push the method name through the current apis.. SingleMethod method = methods.iterator().next(); replaceMap.put(METHOD_NAME, method.getMethodName()); - replaceMap.put(ENCLOSING_TYPE_NAME, method.getNestedClass().getClassName()); + if(method.getNestedClass() != null) { + replaceMap.put(ENCLOSING_TYPE_NAME, method.getNestedClass().getClassName()); + } } if (group != null &&