This is an automated email from the ASF dual-hosted git repository.

dbalek pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/netbeans.git


The following commit(s) were added to refs/heads/master by this push:
     new 9ae9262  LSP: Various Test Explorer fixes. (#3224)
9ae9262 is described below

commit 9ae92629835220c79e8395bdd8da26355a4fb5f1
Author: Dusan Balek <[email protected]>
AuthorDate: Mon Oct 11 16:28:16 2021 +0200

    LSP: Various Test Explorer fixes. (#3224)
    
    * LSP: TestExplorer support for @Nested tests and various minor fixes.
    * TestSession.finishSuite() added - required by the @Nested tests support.
---
 .../support/actions/GroovyComputeTestMethods.java  |  18 +-
 .../gsf/testrunner/ui/TestMethodFinderImpl.java    |  58 +++---
 .../gsf/testrunner/ui/api/TestMethodFinder.java    |   4 +-
 ide/gsf.testrunner/apichanges.xml                  |  12 ++
 ide/gsf.testrunner/manifest.mf                     |   2 +-
 .../modules/gsf/testrunner/api/TestSession.java    |  26 ++-
 java/gradle.test/nbproject/project.xml             |   2 +-
 .../gradle/test/GradleTestProgressListener.java    |   1 +
 .../lsp/server/progress/TestProgressHandler.java   |  67 +++---
 .../java/lsp/server/protocol/TestSuiteInfo.java    |   8 +-
 .../lsp/server/protocol/WorkspaceServiceImpl.java  |  32 +--
 .../server/progress/TestProgressHandlerTest.java   |  10 +-
 .../java/lsp/server/protocol/ServerTest.java       |   3 +-
 java/java.lsp.server/vscode/src/extension.ts       |  15 ++
 java/java.lsp.server/vscode/src/protocol.ts        |   2 +-
 .../vscode/src/test/suite/extension.test.ts        | 227 ++++++++++++---------
 java/java.lsp.server/vscode/src/testAdapter.ts     |  65 ++++--
 java/junit.ant/nbproject/project.xml               |  12 +-
 .../modules/junit/ant/JUnitOutputReader.java       |   4 +-
 .../junit/ui/actions/TestClassInfoTask.java        |  88 +++++---
 java/maven.junit/nbproject/project.xml             |   2 +-
 .../maven/junit/JUnitOutputListenerProvider.java   |   1 +
 22 files changed, 416 insertions(+), 243 deletions(-)

diff --git 
a/groovy/groovy.support/src/org/netbeans/modules/groovy/support/actions/GroovyComputeTestMethods.java
 
b/groovy/groovy.support/src/org/netbeans/modules/groovy/support/actions/GroovyComputeTestMethods.java
index 3425d19..6e63f6b 100644
--- 
a/groovy/groovy.support/src/org/netbeans/modules/groovy/support/actions/GroovyComputeTestMethods.java
+++ 
b/groovy/groovy.support/src/org/netbeans/modules/groovy/support/actions/GroovyComputeTestMethods.java
@@ -19,7 +19,9 @@
 package org.netbeans.modules.groovy.support.actions;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
 import javax.swing.text.Position;
 import org.codehaus.groovy.ast.AnnotationNode;
@@ -62,8 +64,7 @@ public class GroovyComputeTestMethods implements 
ComputeTestMethods {
             return result;
         }
         for (ClassNode classNode : moduleNode.getClasses()) {
-            ClassNode superClass = classNode.getSuperClass();
-            if ("spock.lang.Specification".equals(superClass.getName())) {
+            if (isSpecification(classNode.getSuperClass())) {
                 int classStartLine = classNode.getLineNumber();
                 int classStartColumn = classNode.getColumnNumber();
                 int classOffset = classStartLine > 0 && classStartColumn > 0 ? 
getOffset(text, classStartLine, classStartColumn) : 0;
@@ -95,6 +96,19 @@ public class GroovyComputeTestMethods implements 
ComputeTestMethods {
         return result;
     }
 
+    private static boolean isSpecification(ClassNode classNode) {
+        Set<String> visited = new HashSet<>();
+        String name;
+        while (classNode != null && !visited.contains(name = 
classNode.getName())) {
+            if ("spock.lang.Specification".equals(name)) {
+                return true;
+            }
+            visited.add(name);
+            classNode = classNode.getSuperClass();
+        }
+        return false;
+    }
+
     private static boolean isTestSource(FileObject fo) {
         ClassPath cp = ClassPath.getClassPath(fo, ClassPath.SOURCE);
         if (cp != null) {
diff --git 
a/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/TestMethodFinderImpl.java
 
b/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/TestMethodFinderImpl.java
index 1198c8d..53260a3 100644
--- 
a/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/TestMethodFinderImpl.java
+++ 
b/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/TestMethodFinderImpl.java
@@ -26,9 +26,13 @@ import java.io.PrintWriter;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.BiConsumer;
+import java.util.logging.Logger;
 import org.netbeans.api.editor.mimelookup.MimeLookup;
 import org.netbeans.api.editor.mimelookup.MimeRegistration;
 import org.netbeans.modules.gsf.testrunner.ui.api.TestMethodController;
@@ -52,7 +56,7 @@ public final class TestMethodFinderImpl extends 
EmbeddingIndexer {
 
     public static final String NAME = "tests"; // NOI18N
     public static final int VERSION = 1;
-    public static TestMethodFinderImpl INSTANCE = null;
+    public static final TestMethodFinderImpl INSTANCE = new 
TestMethodFinderImpl();
 
     private final WeakSet<BiConsumer<FileObject, 
Collection<TestMethodController.TestMethod>>> listeners = new WeakSet<>();
 
@@ -76,6 +80,7 @@ public final class TestMethodFinderImpl extends 
EmbeddingIndexer {
     public void addListener(BiConsumer<FileObject, 
Collection<TestMethodController.TestMethod>> listener) {
         synchronized(listeners) {
             listeners.putIfAbsent(listener);
+            
Logger.getLogger(TestMethodFinderImpl.class.getName()).info("Listener added: " 
+ listener);
         }
     }
 
@@ -87,29 +92,37 @@ public final class TestMethodFinderImpl extends 
EmbeddingIndexer {
                 output.delete();
             }
         } else {
+            Map<String, List<TestMethodController.TestMethod>> class2methods = 
new LinkedHashMap<>();
+            Map<String, Integer> class2offsets = new HashMap<>();
+            for (TestMethodController.TestMethod method : methods) {
+                String className = method.getTestClassName();
+                if (method.getTestClassPosition() != null) {
+                    class2offsets.putIfAbsent(className, 
method.getTestClassPosition().getOffset());
+                }
+                class2methods.computeIfAbsent(className, name -> new 
ArrayList<>()).add(method);
+            }
             output.getParentFile().mkdirs();
-            boolean printHeader = true;
             try (PrintWriter pw = new PrintWriter(new OutputStreamWriter(new 
FileOutputStream(output), "UTF-8"))) {
-                for (TestMethodController.TestMethod method : methods) {
-                    if (printHeader) {
-                        pw.print("url: "); //NOI18N
-                        pw.println(url.toString());
-                        pw.print("class: "); //NOI18N
-                        pw.print(method.getTestClassName());
-                        if (method.getTestClassPosition() != null) {
-                            pw.print(':'); //NOI18N
-                            
pw.println(method.getTestClassPosition().getOffset());
-                        } else {
-                            pw.println();
-                        }
-                        printHeader = false;
+                pw.print("url: "); //NOI18N
+                pw.println(url.toString());
+                for (Map.Entry<String, List<TestMethodController.TestMethod>> 
entry : class2methods.entrySet()) {
+                    pw.print("class: "); //NOI18N
+                    pw.print(entry.getKey());
+                    Integer offset = class2offsets.get(entry.getKey());
+                    if (offset != null) {
+                        pw.print(':'); //NOI18N
+                        pw.println(offset);
+                    } else {
+                        pw.println();
+                    }
+                    for (TestMethodController.TestMethod method : 
entry.getValue()) {
+                        pw.print("method: "); //NOI18N
+                        pw.print(method.method().getMethodName());
+                        pw.print(':'); //NOI18N
+                        pw.print(method.start().getOffset());
+                        pw.print('-'); //NOI18N
+                        pw.println(method.end().getOffset());
                     }
-                    pw.print("method: "); //NOI18N
-                    pw.print(method.method().getMethodName());
-                    pw.print(':'); //NOI18N
-                    pw.print(method.start().getOffset());
-                    pw.print('-'); //NOI18N
-                    pw.println(method.end().getOffset());
                 }
             } catch (IOException ex) {
                 Exceptions.printStackTrace(ex);
@@ -122,9 +135,6 @@ public final class TestMethodFinderImpl extends 
EmbeddingIndexer {
 
         @Override
         public EmbeddingIndexer createIndexer(Indexable indexable, Snapshot 
snapshot) {
-            if (INSTANCE == null) {
-                INSTANCE = new TestMethodFinderImpl();
-            }
             return INSTANCE;
         }
 
diff --git 
a/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/api/TestMethodFinder.java
 
b/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/api/TestMethodFinder.java
index 22ae987..0c334c9 100644
--- 
a/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/api/TestMethodFinder.java
+++ 
b/ide/gsf.testrunner.ui/src/org/netbeans/modules/gsf/testrunner/ui/api/TestMethodFinder.java
@@ -55,9 +55,7 @@ public final class TestMethodFinder {
      * @since 1.27
      */
     public static Map<FileObject, Collection<TestMethodController.TestMethod>> 
findTestMethods(Iterable<FileObject> testRoots, BiConsumer<FileObject, 
Collection<TestMethodController.TestMethod>> listener) {
-        if (TestMethodFinderImpl.INSTANCE != null) {
-            TestMethodFinderImpl.INSTANCE.addListener(listener);
-        }
+        TestMethodFinderImpl.INSTANCE.addListener(listener);
         Map<FileObject, Collection<TestMethodController.TestMethod>> 
file2TestMethods = new HashMap<>();
         for (FileObject testRoot : testRoots) {
             try {
diff --git a/ide/gsf.testrunner/apichanges.xml 
b/ide/gsf.testrunner/apichanges.xml
index 1a49bed..34f2252 100644
--- a/ide/gsf.testrunner/apichanges.xml
+++ b/ide/gsf.testrunner/apichanges.xml
@@ -52,6 +52,18 @@
 <!-- ACTUAL CHANGES BEGIN HERE: -->
 
 <changes>
+    <change id="TestSession_finishSuite">
+        <api name="CommonTestrunnerAPI"/>
+        <summary>TestSession.finishSuite method added</summary>
+        <version major="2" minor="26"/>
+        <date day="8" month="10" year="2021"/>
+        <author login="dbalek"/>
+        <compatibility addition="yes"/>
+        <description>
+            TestSession.finishSuite() method added.
+        </description>
+        <class package="org.netbeans.modules.gsf.testrunner.api" 
name="TestSession"/>
+    </change>
     <change id="CommonTestrunnerAPI_Public">
         <api name="CommonTestrunnerAPI"/>
         <summary>Common Test Runner API made public</summary>
diff --git a/ide/gsf.testrunner/manifest.mf b/ide/gsf.testrunner/manifest.mf
index 03f8517..585b10f 100644
--- a/ide/gsf.testrunner/manifest.mf
+++ b/ide/gsf.testrunner/manifest.mf
@@ -3,5 +3,5 @@ AutoUpdate-Show-In-Client: false
 OpenIDE-Module: org.netbeans.modules.gsf.testrunner/2
 OpenIDE-Module-Localizing-Bundle: 
org/netbeans/modules/gsf/testrunner/Bundle.properties
 OpenIDE-Module-Layer: org/netbeans/modules/gsf/testrunner/layer.xml
-OpenIDE-Module-Specification-Version: 2.25
+OpenIDE-Module-Specification-Version: 2.26
 
diff --git 
a/ide/gsf.testrunner/src/org/netbeans/modules/gsf/testrunner/api/TestSession.java
 
b/ide/gsf.testrunner/src/org/netbeans/modules/gsf/testrunner/api/TestSession.java
index b850136..a630e7b 100644
--- 
a/ide/gsf.testrunner/src/org/netbeans/modules/gsf/testrunner/api/TestSession.java
+++ 
b/ide/gsf.testrunner/src/org/netbeans/modules/gsf/testrunner/api/TestSession.java
@@ -22,6 +22,7 @@ import java.lang.ref.WeakReference;
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Stack;
 import org.netbeans.api.extexecution.print.LineConvertors.FileLocator;
 import org.netbeans.api.project.FileOwnerQuery;
 import org.netbeans.api.project.Project;
@@ -62,6 +63,10 @@ public class TestSession {
      */
     private final List<TestSuite> testSuites = new ArrayList<TestSuite>();
     /**
+     * Current suite indexes.
+     */
+    private final Stack<Integer> suiteIdxs = new Stack<Integer>();
+    /**
      * Holds output for testcases. Since a testcase is created only after 
      * a test finishes, the output of that testcase needs to be associated 
      * with it after it has been created.
@@ -176,7 +181,22 @@ public class TestSession {
                 output.clear();
             }
         }
-        testSuites.add(suite);
+        synchronized (this) {
+            suiteIdxs.push(testSuites.size());
+            testSuites.add(suite);
+        }
+    }
+
+    /**
+     * Marks the currently running test suite as finished.
+     *
+     * @param suite the suite to mark as finished
+     * @since 2.26
+     */
+    public synchronized void finishSuite(TestSuite suite) {
+        if (!suiteIdxs.isEmpty() && suite == getCurrentSuite()) {
+            suiteIdxs.pop();
+        }
     }
 
     /**
@@ -204,8 +224,8 @@ public class TestSession {
      * @return the suite that is currently running or <code>null</code> if 
      * no suite is running.
      */
-    public TestSuite getCurrentSuite() {
-        return testSuites.isEmpty() ? null : testSuites.get(testSuites.size() 
-1);
+    public synchronized TestSuite getCurrentSuite() {
+        return testSuites.isEmpty() ? null : 
testSuites.get(suiteIdxs.isEmpty() ? testSuites.size() -1 : suiteIdxs.peek());
     }
 
     /**
diff --git a/java/gradle.test/nbproject/project.xml 
b/java/gradle.test/nbproject/project.xml
index 5057501..2c90eb7 100644
--- a/java/gradle.test/nbproject/project.xml
+++ b/java/gradle.test/nbproject/project.xml
@@ -74,7 +74,7 @@
                     <compile-dependency/>
                     <run-dependency>
                         <release-version>2</release-version>
-                        <specification-version>1.42.1</specification-version>
+                        <specification-version>2.26</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>
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 11d1614..97a215c 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
@@ -177,6 +177,7 @@ public final class GradleTestProgressListener implements 
ProgressListener, Gradl
         String suiteName = GradleTestSuite.suiteName(op);
         if (suiteName.equals(currentSuite.getName())) {
             Report report = session.getReport(result.getEndTime() - 
result.getStartTime());
+            session.finishSuite(currentSuite);
             CoreManager manager = getManager();
             if (manager != null) {
                 manager.displayReport(session, report, true);
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 be7b83d..470293d 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
@@ -28,6 +28,7 @@ import org.eclipse.lsp4j.debug.OutputEventArguments;
 import org.eclipse.lsp4j.debug.services.IDebugProtocolClient;
 import org.netbeans.api.extexecution.print.LineConvertors;
 import org.netbeans.modules.gsf.testrunner.api.Report;
+import org.netbeans.modules.gsf.testrunner.api.Status;
 import org.netbeans.modules.gsf.testrunner.api.TestSession;
 import org.netbeans.modules.gsf.testrunner.api.TestSuite;
 import org.netbeans.modules.gsf.testrunner.api.Testcase;
@@ -63,7 +64,7 @@ public final class TestProgressHandler implements 
TestResultDisplayHandler.Spi<T
     public void displayOutput(TestProgressHandler token, String text, boolean 
error) {
         if (text != null) {
             OutputEventArguments output = new OutputEventArguments();
-            output.setOutput(text.endsWith("\n") ? text : text + "\n");
+            output.setOutput(text.trim() + "\n");
             debugClient.output(output);
         }
     }
@@ -90,23 +91,7 @@ public final class TestProgressHandler implements 
TestResultDisplayHandler.Spi<T
                 name = name.substring(0, idx);
             }
             String id = className + ':' + name;
-            String status;
-            switch (test.getStatus()) {
-                case PASSED:
-                    status = TestSuiteInfo.State.Passed;
-                    break;
-                case ERROR:
-                    status = TestSuiteInfo.State.Errored;
-                    break;
-                case FAILED:
-                    status = TestSuiteInfo.State.Failed;
-                    break;
-                case SKIPPED:
-                    status = TestSuiteInfo.State.Skipped;
-                    break;
-                default:
-                    throw new IllegalStateException("Unexpected testcase 
status: " + test.getStatus());
-            }
+            String state = statusToState(test.getStatus());
             List<String> stackTrace = test.getTrouble() != null ? 
Arrays.asList(test.getTrouble().getStackTrace()) : null;
             String location = test.getLocation();
             FileObject fo = location != null ? 
fileLocations.computeIfAbsent(location, loc -> {
@@ -119,27 +104,16 @@ public final class TestProgressHandler implements 
TestResultDisplayHandler.Spi<T
             }) : null;
             TestSuiteInfo.TestCaseInfo info = testCases.get(id);
             if (info != null) {
-                updateState(info, status);
+                updateState(info, state);
             } else {
-                info = new TestSuiteInfo.TestCaseInfo(id, name, fo != null ? 
Utils.toUri(fo) : null, null, status, stackTrace);
+                info = new TestSuiteInfo.TestCaseInfo(id, name, fo != null ? 
Utils.toUri(fo) : null, null, state, stackTrace);
                 testCases.put(id, info);
             }
         }
-        String status;
-        switch (report.getStatus()) {
-            case PASSED:
-            case FAILED:
-                status = TestSuiteInfo.State.Completed;
-                break;
-            case ERROR:
-                status = TestSuiteInfo.State.Errored;
-                break;
-            default:
-                throw new IllegalStateException("Unexpected testsuite status: 
" + report.getStatus());
-        }
+        String state = statusToState(report.getStatus());
         FileObject fo = fileLocations.size() == 1 ? 
fileLocations.values().iterator().next() : null;
         lspClient.notifyTestProgress(new TestProgressParams(uri, new 
TestSuiteInfo(report.getSuiteClassName(),
-                fo != null ? Utils.toUri(fo) : null, null, status, new 
ArrayList<>(testCases.values()))));
+                fo != null ? Utils.toUri(fo) : null, null, state, new 
ArrayList<>(testCases.values()))));
     }
 
     @Override
@@ -155,6 +129,26 @@ public final class TestProgressHandler implements 
TestResultDisplayHandler.Spi<T
         return 0;
     }
 
+    private String statusToState(Status status) {
+        switch (status) {
+            case PASSED:
+            case PASSEDWITHERRORS:
+                return TestSuiteInfo.State.Passed;
+            case ERROR:
+                return TestSuiteInfo.State.Errored;
+            case FAILED:
+                return TestSuiteInfo.State.Failed;
+            case SKIPPED:
+            case ABORTED:
+            case IGNORED:
+                return TestSuiteInfo.State.Skipped;
+            case PENDING:
+                return TestSuiteInfo.State.Started;
+            default:
+                throw new IllegalStateException("Unexpected testsuite status: 
" + status);
+        }
+    }
+
     private void updateState(TestSuiteInfo.TestCaseInfo info, String state) {
         switch (state) {
             case TestSuiteInfo.State.Errored:
@@ -166,7 +160,12 @@ public final class TestProgressHandler implements 
TestResultDisplayHandler.Spi<T
                 }
                 break;
             case TestSuiteInfo.State.Passed:
-                if (TestSuiteInfo.State.Skipped.equals(info.getState())) {
+                if (TestSuiteInfo.State.Skipped.equals(info.getState()) || 
TestSuiteInfo.State.Started.equals(info.getState())) {
+                    info.setState(state);
+                }
+                break;
+            case TestSuiteInfo.State.Skipped:
+                if (TestSuiteInfo.State.Started.equals(info.getState())) {
                     info.setState(state);
                 }
                 break;
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 36ea81b..6258caf 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
@@ -51,7 +51,7 @@ public final class TestSuiteInfo {
 
     /**
      * The state of the tests suite. Can be one of the following values:
-     * "loaded" | "started" | "completed" | "errored"
+     * "loaded" | "started" | "passed" | "failed" | "skipped" | "errored"
      */
     @NonNull
     private String state;
@@ -125,7 +125,7 @@ public final class TestSuiteInfo {
 
     /**
      * The state of the tests suite. Can be one of the following values:
-     * "loaded" | "started" | "completed" | "errored"
+     * "loaded" | "started" | "passed" | "failed" | "skipped" | "errored"
      */
     @Pure
     @NonNull
@@ -135,7 +135,7 @@ public final class TestSuiteInfo {
 
     /**
      * The state of the tests suite. Can be one of the following values:
-     * "loaded" | "started" | "completed" | "errored"
+     * "loaded" | "started" | "passed" | "failed" | "skipped" | "errored"
      */
     public void setState(@NonNull final String state) {
         this.state = Preconditions.checkNotNull(state, "state");
@@ -434,8 +434,6 @@ public final class TestSuiteInfo {
 
         public static final String Started = "started";
 
-        public static final String Completed  = "completed";
-
         public static final String Passed  = "passed";
 
         public static final String Failed  = "failed";
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 4ab6a64..2942003 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.concurrent.atomic.AtomicReference;
 import java.util.function.BiConsumer;
 import java.util.function.BiFunction;
 import java.util.function.Supplier;
+import java.util.logging.Logger;
 import java.util.stream.Collectors;
 import javax.lang.model.element.Element;
 import javax.lang.model.element.ElementKind;
@@ -201,38 +202,39 @@ public final class WorkspaceServiceImpl implements 
WorkspaceService, LanguageCli
                     JavaSource js = 
JavaSource.create(ClasspathInfo.create(ClassPath.EMPTY, ClassPath.EMPTY, 
ClassPath.EMPTY));
                     try {
                         js.runWhenScanFinished(controller -> {
-                            BiFunction<FileObject, 
Collection<TestMethodController.TestMethod>, TestSuiteInfo> f = (fo, methods) 
-> {
-                                List<TestSuiteInfo.TestCaseInfo> tests = new 
ArrayList<>(methods.size());
+                            BiFunction<FileObject, 
Collection<TestMethodController.TestMethod>, Collection<TestSuiteInfo>> f = 
(fo, methods) -> {
                                 String url = Utils.toUri(fo);
-                                String testClassName = null;
-                                Range testClassRange = null;
+                                Map<String, TestSuiteInfo> suite2infos = new 
LinkedHashMap<>();
                                 for (TestMethodController.TestMethod 
testMethod : methods) {
-                                    if (testClassName == null) {
-                                        testClassName = 
testMethod.getTestClassName();
-                                    }
-                                    if (testClassRange == null && 
testMethod.getTestClassPosition() != null) {
-                                        Position pos = 
Utils.createPosition(fo, testMethod.getTestClassPosition().getOffset());
-                                        testClassRange = new Range(pos, pos);
-                                    }
+                                    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<>());
+                                    });
                                     String id = testMethod.getTestClassName() 
+ ':' + testMethod.method().getMethodName();
                                     Position startPos = testMethod.start() != 
null ? Utils.createPosition(fo, testMethod.start().getOffset()) : null;
                                     Position endPos = testMethod.end() != null 
? Utils.createPosition(fo, testMethod.end().getOffset()) : startPos;
                                     Range range = startPos != null ? new 
Range(startPos, endPos) : null;
-                                    tests.add(new 
TestSuiteInfo.TestCaseInfo(id, testMethod.method().getMethodName(), url, range, 
TestSuiteInfo.State.Loaded, null));
+                                    suite.getTests().add(new 
TestSuiteInfo.TestCaseInfo(id, testMethod.method().getMethodName(), url, range, 
TestSuiteInfo.State.Loaded, null));
                                 }
-                                return new TestSuiteInfo(testClassName, url, 
testClassRange, TestSuiteInfo.State.Loaded, tests);
+                                return suite2infos.values();
                             };
                             testMethodsListener.compareAndSet(null, (fo, 
methods) -> {
+                                
Logger.getLogger(WorkspaceServiceImpl.class.getName()).info("FileObject 
reindexed: " + fo.getPath());
                                 try {
-                                    client.notifyTestProgress(new 
TestProgressParams(Utils.toUri(fo), f.apply(fo, methods)));
+                                    for (TestSuiteInfo testSuiteInfo : 
f.apply(fo, methods)) {
+                                        client.notifyTestProgress(new 
TestProgressParams(Utils.toUri(fo), testSuiteInfo));
+                                    }
                                 } catch (Exception e) {
+                                    
Logger.getLogger(WorkspaceServiceImpl.class.getName()).severe(e.getMessage());
+                                    Exceptions.printStackTrace(e);
                                     testMethodsListener.set(null);
                                 }
                             });
+                            
Logger.getLogger(WorkspaceServiceImpl.class.getName()).info("Attaching 
listener: " + testMethodsListener.get());
                             Map<FileObject, 
Collection<TestMethodController.TestMethod>> testMethods = 
TestMethodFinder.findTestMethods(testRoots, testMethodsListener.get());
                             Collection<TestSuiteInfo> suites = new 
ArrayList<>(testMethods.size());
                             for (Entry<FileObject, 
Collection<TestMethodController.TestMethod>> entry : testMethods.entrySet()) {
-                                suites.add(f.apply(entry.getKey(), 
entry.getValue()));
+                                suites.addAll(f.apply(entry.getKey(), 
entry.getValue()));
                             }
                             future.complete(suites);
                         }, true);
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 c325b59..a80d125 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
@@ -121,17 +121,17 @@ public class TestProgressHandlerTest extends NbTestCase {
         assertEquals(fo.toURI().toString(), msgs.get(1).getUri());
         suite = msgs.get(1).getSuite();
         assertEquals("TestSuiteName", suite.getName());
-        assertEquals(TestSuiteInfo.State.Completed, suite.getState());
+        assertEquals(TestSuiteInfo.State.Failed, suite.getState());
         assertEquals(2, suite.getTests().size());
         TestSuiteInfo.TestCaseInfo testCase = suite.getTests().get(0);
-        assertEquals("TestSuiteName:test1", testCase.getId());
-        assertEquals("test1", testCase.getName());
+        assertEquals("TestSuiteName:TestSuiteName.test1", testCase.getId());
+        assertEquals("TestSuiteName.test1", testCase.getName());
         assertEquals(fo.toURI().toString(), testCase.getFile());
         assertEquals(TestSuiteInfo.State.Passed, testCase.getState());
         assertNull(testCase.getStackTrace());
         testCase = suite.getTests().get(1);
-        assertEquals("TestSuiteName:test2", testCase.getId());
-        assertEquals("test2", testCase.getName());
+        assertEquals("TestSuiteName:TestSuiteName.test2", testCase.getId());
+        assertEquals("TestSuiteName.test2", testCase.getName());
         assertEquals(fo.toURI().toString(), testCase.getFile());
         assertEquals(TestSuiteInfo.State.Failed, testCase.getState());
         assertNotNull(testCase.getStackTrace());
diff --git 
a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/ServerTest.java
 
b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/ServerTest.java
index e891ddc..9e906d7 100644
--- 
a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/ServerTest.java
+++ 
b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/ServerTest.java
@@ -3489,7 +3489,8 @@ public class ServerTest extends NbTestCase {
                      "    @Override\n" +
                      "    public String toString() {\n" +
                      "        StringBuilder sb = new StringBuilder();\n" +
-                     "        sb.append(\"Test{f1=\").append(f1);\n" +
+                     "        sb.append(\"Test{\");\n" +
+                     "        sb.append(\"f1=\").append(f1);\n" +
                      "        sb.append('}');\n" +
                      "        return sb.toString();\n" +
                      "    }\n",
diff --git a/java/java.lsp.server/vscode/src/extension.ts 
b/java/java.lsp.server/vscode/src/extension.ts
index 871a722..4fb71bb 100644
--- a/java/java.lsp.server/vscode/src/extension.ts
+++ b/java/java.lsp.server/vscode/src/extension.ts
@@ -174,6 +174,8 @@ export function activate(context: ExtensionContext): 
VSNetBeansAPI {
     });
 
     //register debugger:
+    let debugTrackerFactory =new NetBeansDebugAdapterTrackerFactory();
+    
context.subscriptions.push(vscode.debug.registerDebugAdapterTrackerFactory('java8+',
 debugTrackerFactory));
     let configInitialProvider = new NetBeansConfigurationInitialProvider();
     
context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('java8+',
 configInitialProvider, vscode.DebugConfigurationProviderTriggerKind.Initial));
     let configDynamicProvider = new 
NetBeansConfigurationDynamicProvider(context);
@@ -712,6 +714,19 @@ export function deactivate(): Thenable<void> {
     return stopClient(client);
 }
 
+class NetBeansDebugAdapterTrackerFactory implements 
vscode.DebugAdapterTrackerFactory {
+
+    createDebugAdapterTracker(_session: vscode.DebugSession): 
vscode.ProviderResult<vscode.DebugAdapterTracker> {
+        return {
+            onDidSendMessage(message: any): void {
+                if (testAdapter && message.type === 'event' && message.event 
=== 'output') {
+                    testAdapter.testOutput(message.body.output);
+                }
+            }
+        }
+    }
+}
+
 class NetBeansDebugAdapterDescriptionFactory implements 
vscode.DebugAdapterDescriptorFactory {
 
     createDebugAdapterDescriptor(_session: vscode.DebugSession, _executable: 
vscode.DebugAdapterExecutable | undefined): 
vscode.ProviderResult<vscode.DebugAdapterDescriptor> {
diff --git a/java/java.lsp.server/vscode/src/protocol.ts 
b/java/java.lsp.server/vscode/src/protocol.ts
index c4bcaac..64b3da2 100644
--- a/java/java.lsp.server/vscode/src/protocol.ts
+++ b/java/java.lsp.server/vscode/src/protocol.ts
@@ -83,7 +83,7 @@ export interface TestSuite {
     name: string;
     file?: string;
     range?: Range;
-    state: 'loaded' | 'started' | 'completed' | 'errored';
+    state: 'loaded' | 'started' | 'passed' | 'failed' | 'skipped' | 'errored';
     tests?: TestCase[];
 }
 
diff --git a/java/java.lsp.server/vscode/src/test/suite/extension.test.ts 
b/java/java.lsp.server/vscode/src/test/suite/extension.test.ts
index e757b5b..562dacc 100644
--- a/java/java.lsp.server/vscode/src/test/suite/extension.test.ts
+++ b/java/java.lsp.server/vscode/src/test/suite/extension.test.ts
@@ -71,32 +71,7 @@ suite('Extension Test Suite', () => {
     async function demo(where: number) {
         let folder: string = assertWorkspace();
 
-        await fs.promises.writeFile(path.join(folder, 'pom.xml'), `
-<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
-    <modelVersion>4.0.0</modelVersion>
-    <groupId>org.netbeans.demo.vscode.t1</groupId>
-    <artifactId>basicapp</artifactId>
-    <version>1.0</version>
-    <properties>
-        <maven.compiler.source>1.8</maven.compiler.source>
-        <maven.compiler.target>1.8</maven.compiler.target>
-    </properties>
-</project>
-               `);
-
-        let pkg = path.join(folder, 'src', 'main', 'java', 'pkg');
-        let mainJava = path.join(pkg, 'Main.java');
-
-        await fs.promises.mkdir(pkg, { recursive: true });
-
-        await fs.promises.writeFile(mainJava, `
-package pkg;
-class Main {
-       public static void main(String... args) {
-               System.out.println("Hello World!");
-       }
-}
-               `);
+        await prepareProject(folder);
 
         vscode.workspace.saveAll();
 
@@ -131,42 +106,14 @@ class Main {
     async function mavenTerminateWithoutDebugger() {
         let folder: string = assertWorkspace();
 
-        await fs.promises.writeFile(path.join(folder, 'pom.xml'), `
-    <project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
-    <modelVersion>4.0.0</modelVersion>
-    <groupId>org.netbeans.demo.vscode.t1</groupId>
-    <artifactId>basicapp</artifactId>
-    <version>1.0</version>
-    <properties>
-        <maven.compiler.source>1.8</maven.compiler.source>
-        <maven.compiler.target>1.8</maven.compiler.target>
-    </properties>
-    </project>
-        `);
-
-        let pkg = path.join(folder, 'src', 'main', 'java', 'pkg');
-        let mainJava = path.join(pkg, 'Main.java');
-
-        await fs.promises.mkdir(pkg, { recursive: true });
-
-        await fs.promises.writeFile(mainJava, `
-    package pkg;
-    class Main {
-    public static void main(String... args) throws Exception {
-        System.out.println("Endless wait...");
-        while (true) {
-            Thread.sleep(1000);
-        }
-    }
-    }
-        `);
+        await prepareProject(folder);
+
         vscode.workspace.saveAll();
-        let u : Uri = vscode.Uri.file(mainJava);
+        let u : Uri = vscode.Uri.file(path.join(folder, 'src', 'main', 'java', 
'pkg', 'Main.java'));
         let doc : TextDocument = await vscode.workspace.openTextDocument(u);
         let e : TextEditor = await vscode.window.showTextDocument(doc);
 
         try {
-            let terminated = false;
             let r = new Promise((resolve, reject) => {
                 function waitUserApplication(cnt : number, running: boolean, 
cb : () => void) {
                     ps.lookup({
@@ -210,34 +157,7 @@ class Main {
     async function getProjectInfo() {
         let folder: string = assertWorkspace();
 
-        await fs.promises.writeFile(path.join(folder, 'pom.xml'), `
-<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
-    <modelVersion>4.0.0</modelVersion>
-    <groupId>org.netbeans.demo.vscode.t1</groupId>
-    <artifactId>basicapp</artifactId>
-    <version>1.0</version>
-    <properties>
-        <maven.compiler.source>1.8</maven.compiler.source>
-        <maven.compiler.target>1.8</maven.compiler.target>
-    </properties>
-</project>
-               `);
-
-        let pkg = path.join(folder, 'src', 'main', 'java', 'pkg');
-        let resources = path.join(folder, 'src', 'main', 'resources');
-        let mainJava = path.join(pkg, 'Main.java');
-
-        await fs.promises.mkdir(pkg, { recursive: true });
-        await fs.promises.mkdir(resources, { recursive: true });
-
-        await fs.promises.writeFile(mainJava, `
-package pkg;
-class Main {
-       public static void main(String... args) {
-               System.out.println("Hello World!");
-       }
-}
-               `);
+        await prepareProject(folder);
 
         vscode.workspace.saveAll();
 
@@ -245,30 +165,33 @@ class Main {
             console.log("Test: get project java source roots");
             let res: any = await 
vscode.commands.executeCommand("java.get.project.source.roots", 
Uri.file(folder).toString());
             console.log(`Test: get project java source roots finished with 
${res}`);
-            assert.ok(res, "No java source root rertuned");
-            assert.strictEqual(res.length, 1, `Invalid number of java roots 
returned`);
-            assert.strictEqual(res[0], path.join('file:', folder, 'src', 
'main', 'java') + path.sep, `Invalid java source root returned`);
+            assert.ok(res, "No java source root returned");
+            assert.strictEqual(res.length, 2, `Invalid number of java roots 
returned`);
+            assert.strictEqual(res[0], path.join('file:', folder, 'src', 
'main', 'java') + path.sep, `Invalid java main source root returned`);
+            assert.strictEqual(res[1], path.join('file:', folder, 'src', 
'test', 'java') + path.sep, `Invalid java test source root returned`);
 
             console.log("Test: get project resource roots");
             res = await 
vscode.commands.executeCommand("java.get.project.source.roots", 
Uri.file(folder).toString(), 'resources');
             console.log(`Test: get project resource roots finished with 
${res}`);
             assert.ok(res, "No resource root returned");
             assert.strictEqual(res.length, 1, `Invalid number of resource 
roots returned`);
-            assert.strictEqual(res[0], path.join('file:', resources) + 
path.sep, `Invalid resource root returned`);
+            assert.strictEqual(res[0], path.join('file:', folder, 'src', 
'main', 'resources') + path.sep, `Invalid resource root returned`);
 
             console.log("Test: get project compile classpath");
             res = await 
vscode.commands.executeCommand("java.get.project.classpath", 
Uri.file(folder).toString());
             console.log(`Test: get project compile classpath finished with 
${res}`);
             assert.ok(res, "No compile classpath returned");
-            assert.strictEqual(res.length, 1, `Invalid number of compile 
classpath roots returned`);
-            assert.strictEqual(res[0], path.join('file:', folder, 'target', 
'classes') + path.sep, `Invalid compile classpath root returned`);
+            assert.strictEqual(res.length, 9, `Invalid number of compile 
classpath roots returned`);
+            assert.ok(res.find((item: any) => item === path.join('file:', 
folder, 'target', 'classes') + path.sep, `Invalid compile classpath root 
returned`));
 
             console.log("Test: get project source classpath");
             res = await 
vscode.commands.executeCommand("java.get.project.classpath", 
Uri.file(folder).toString(), 'SOURCE');
             console.log(`Test: get project source classpath finished with 
${res}`);
             assert.ok(res, "No source classpath returned");
-            assert.strictEqual(res.length, 1, `Invalid number of source 
classpath roots returned`);
-            assert.strictEqual(res[0], path.join('file:', folder, 'src', 
'main', 'java') + path.sep, `Invalid source classpath root returned`);
+            assert.strictEqual(res.length, 3, `Invalid number of source 
classpath roots returned`);
+            assert.ok(res.find((item: any) => item === path.join('file:', 
folder, 'src', 'main', 'java') + path.sep, `Invalid source classpath root 
returned`));
+            assert.ok(res.find((item: any) => item === path.join('file:', 
folder, 'src', 'main', 'resources') + path.sep, `Invalid source classpath root 
returned`));
+            assert.ok(res.find((item: any) => item === path.join('file:', 
folder, 'src', 'test', 'java') + path.sep, `Invalid source classpath root 
returned`));
 
             console.log("Test: get project boot classpath");
             res = await 
vscode.commands.executeCommand("java.get.project.classpath", 
Uri.file(folder).toString(), 'BOOT');
@@ -302,6 +225,37 @@ class Main {
 
     test("Get project sources, classpath, and packages", async() => 
getProjectInfo());
 
+    async function testExplorerTests() {
+        let folder: string = assertWorkspace();
+
+        await prepareProject(folder);
+
+        vscode.workspace.saveAll();
+
+        try {
+            console.log("Test: load workspace tests");
+            let tests: any = await 
vscode.commands.executeCommand("java.load.workspace.tests", 
Uri.file(folder).toString());
+            console.log(`Test: load workspace tests finished with ${tests}`);
+            assert.ok(tests, "No tests returned for workspace");
+            assert.strictEqual(tests.length, 2, `Invalid number of test suites 
returned`);
+            assert.strictEqual(tests[0].name, 'pkg.MainTest', `Invalid test 
suite name returned`);
+            assert.strictEqual(tests[0].tests.length, 1, `Invalid number of 
tests in suite returned`);
+            assert.strictEqual(tests[0].tests[0].name, 'testGetName', `Invalid 
test name returned`);
+            assert.strictEqual(tests[1].name, 'pkg.MainTest$NestedTest', 
`Invalid test suite name returned`);
+            assert.strictEqual(tests[1].tests.length, 1, `Invalid number of 
tests in suite returned`);
+            assert.strictEqual(tests[1].tests[0].name, 'testTrue', `Invalid 
test name returned`);
+
+            console.log("Test: run all workspace tests");
+            const workspaceFolder = (vscode.workspace.workspaceFolders!)[0];
+            await vscode.commands.executeCommand('java.run.test', 
workspaceFolder.uri.toString());
+            console.log(`Test: run all workspace tests finished`);
+        } catch (error) {
+            dumpJava();
+            throw error;
+        }
+    }
+
+    test("Test Explorer tests", async() => testExplorerTests());
 });
 
 function assertWorkspace(): string {
@@ -313,6 +267,93 @@ function assertWorkspace(): string {
     return folder;
 }
 
+async function prepareProject(folder: string) {
+    await fs.promises.writeFile(path.join(folder, 'pom.xml'), `
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+<modelVersion>4.0.0</modelVersion>
+<groupId>org.netbeans.demo.vscode.t1</groupId>
+<artifactId>basicapp</artifactId>
+<version>1.0</version>
+<properties>
+    <maven.compiler.source>1.8</maven.compiler.source>
+    <maven.compiler.target>1.8</maven.compiler.target>
+</properties>
+<build>
+<plugins>
+    <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <version>2.22.0</version>
+    </plugin>
+</plugins>
+</build>
+<dependencies>
+<dependency>
+    <groupId>org.junit.jupiter</groupId>
+    <artifactId>junit-jupiter-api</artifactId>
+    <version>5.3.1</version>
+    <scope>test</scope>
+</dependency>
+<dependency>
+    <groupId>org.junit.jupiter</groupId>
+    <artifactId>junit-jupiter-params</artifactId>
+    <version>5.3.1</version>
+    <scope>test</scope>
+</dependency>
+<dependency>
+    <groupId>org.junit.jupiter</groupId>
+    <artifactId>junit-jupiter-engine</artifactId>
+    <version>5.3.1</version>
+    <scope>test</scope>
+</dependency>
+</dependencies>
+</project>
+            `);
+
+            let pkg = path.join(folder, 'src', 'main', 'java', 'pkg');
+            let testPkg = path.join(folder, 'src', 'test', 'java', 'pkg');
+            let resources = path.join(folder, 'src', 'main', 'resources');
+            let mainJava = path.join(pkg, 'Main.java');
+            let mainTestJava = path.join(testPkg, 'MainTest.java');
+
+            await fs.promises.mkdir(pkg, { recursive: true });
+            await fs.promises.mkdir(resources, { recursive: true });
+            await fs.promises.mkdir(testPkg, { recursive: true });
+
+            await fs.promises.writeFile(mainJava, `
+package pkg;
+public class Main {
+    public static void main(String... args) throws Exception {
+        System.out.println("Endless wait...");
+        while (true) {
+            Thread.sleep(1000);
+        }
+    }
+    public String getName() {
+        return "John";
+    }
+}
+            `);
+
+            await fs.promises.writeFile(mainTestJava, `
+package pkg;
+import static org.junit.jupiter.api.Assertions.*;
+class MainTest {
+    @org.junit.jupiter.api.Test
+    public void testGetName() {
+        assertEquals("John", new Main().getName());
+    }
+    @org.junit.jupiter.api.Nested
+    class NestedTest {
+        @org.junit.jupiter.api.Test
+        public void testTrue() {
+            assertTrue(true);
+        }
+    }
+}
+            `);
+}
+
 async function dumpJava() {
     const cmd = 'jps';
     const args = [ '-v' ];
diff --git a/java/java.lsp.server/vscode/src/testAdapter.ts 
b/java/java.lsp.server/vscode/src/testAdapter.ts
index 26daaac..3ba428a 100644
--- a/java/java.lsp.server/vscode/src/testAdapter.ts
+++ b/java/java.lsp.server/vscode/src/testAdapter.ts
@@ -20,7 +20,7 @@
 
 import { commands, debug, tests, workspace, CancellationToken, TestController, 
TestItem, TestRunProfileKind, TestRunRequest, Uri, TestRun, TestMessage, 
Location, Position } from "vscode";
 import * as path from 'path';
-import { asRange, TestSuite } from "./protocol";
+import { asRange, TestCase, TestSuite } from "./protocol";
 import { LanguageClient } from "vscode-languageclient";
 
 export class NbTestAdapter {
@@ -114,6 +114,12 @@ export class NbTestAdapter {
                this.disposables = [];
        }
 
+    testOutput(output: string): void {
+        if (this.currentRun && output) {
+            this.currentRun.appendOutput(output.replace(/\n/g, '\r\n'));
+        }
+    }
+
     testProgress(suite: TestSuite): void {
         const currentSuite = this.testController.items.get(suite.name);
         switch (suite.state) {
@@ -125,8 +131,10 @@ export class NbTestAdapter {
                     this.set(currentSuite, 'started');
                 }
                 break;
-            case 'completed':
+            case 'passed':
+            case "failed":
             case 'errored':
+            case 'skipped':
                 if (suite.tests) {
                     this.updateTests(suite, true);
                     if (currentSuite) {
@@ -136,8 +144,11 @@ export class NbTestAdapter {
                                 let currentTest = 
currentSuite.children.get(test.id);
                                 if (!currentTest) {
                                     currentSuite.children.forEach(item => {
-                                        if (!currentTest && 
test.id.startsWith(item.id)) {
-                                            currentTest = 
item.children.get(test.id);
+                                        if (!currentTest) {
+                                            const subName = 
this.subTestName(item, test);
+                                            if (subName) {
+                                                currentTest = 
item.children.get(test.id);
+                                            }
                                         }
                                     });
                                 }
@@ -174,6 +185,8 @@ export class NbTestAdapter {
                         if (suiteMessages.length > 0) {
                             this.set(currentSuite, 'errored', suiteMessages, 
true);
                             currentSuite.children.forEach(item => 
this.set(item, 'skipped'));
+                        } else {
+                            this.set(currentSuite, suite.state, undefined, 
true);
                         }
                     }
                 }
@@ -209,23 +222,22 @@ export class NbTestAdapter {
                 children.push(currentTest);
             } else {
                 if (testExecution) {
-                    const parents: TestItem[] = [];
+                    const parents: Map<TestItem, string> = new Map();
                     currentSuite?.children.forEach(item => {
-                        if (test.id.startsWith(item.id)) {
-                            parents.push(item);
+                        const subName = this.subTestName(item, test);
+                        if (subName) {
+                            parents.set(item, subName);
                         }
                     });
-                    if (parents.length === 1) {
-                        let arr = parentTests.get(parents[0]);
-                        if (!arr) {
-                            parentTests.set(parents[0], arr = []);
-                            children.push(parents[0]);
-                        }
-                        let label = test.name;
-                        if (label.startsWith(parents[0].label)) {
-                            label = 
label.slice(parents[0].label.length).trim();
-                        }
-                        arr.push(this.testController.createTestItem(test.id, 
label));
+                    if (parents.size === 1) {
+                        parents.forEach((label, parentTest) => {
+                            let arr = parentTests.get(parentTest);
+                            if (!arr) {
+                                parentTests.set(parentTest, arr = []);
+                                children.push(parentTest);
+                            }
+                            
arr.push(this.testController.createTestItem(test.id, label));
+                        });
                     }
                 } else {
                     currentTest = this.testController.createTestItem(test.id, 
test.name, testUri);
@@ -246,4 +258,21 @@ export class NbTestAdapter {
             currentSuite.children.replace(children);
         }
     }
+
+    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();
+            }
+            return label;
+        } else {
+            const regexp = new RegExp(item.id.replace(/#\S*/g, '\\S*'));
+            if (regexp.test(test.id)) {
+                let idx = test.id.indexOf(':');
+                return idx < 0 ? test.id : test.id.slice(idx + 1);
+            }
+        }
+        return undefined;
+    }
 }
diff --git a/java/junit.ant/nbproject/project.xml 
b/java/junit.ant/nbproject/project.xml
index eeb69a3..11ab478 100644
--- a/java/junit.ant/nbproject/project.xml
+++ b/java/junit.ant/nbproject/project.xml
@@ -58,7 +58,7 @@
                     <compile-dependency/>
                     <run-dependency>
                         <release-version>2</release-version>
-                        <specification-version>2.0</specification-version>
+                        <specification-version>2.26</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>
@@ -148,7 +148,7 @@
                     </run-dependency>
                 </dependency>
                 <dependency>
-                    <code-name-base>org.openide.util.ui</code-name-base>
+                    <code-name-base>org.openide.util</code-name-base>
                     <build-prerequisite/>
                     <compile-dependency/>
                     <run-dependency>
@@ -156,19 +156,19 @@
                     </run-dependency>
                 </dependency>
                 <dependency>
-                    <code-name-base>org.openide.util</code-name-base>
+                    <code-name-base>org.openide.util.lookup</code-name-base>
                     <build-prerequisite/>
                     <compile-dependency/>
                     <run-dependency>
-                        <specification-version>9.3</specification-version>
+                        <specification-version>8.25</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>
-                    <code-name-base>org.openide.util.lookup</code-name-base>
+                    <code-name-base>org.openide.util.ui</code-name-base>
                     <build-prerequisite/>
                     <compile-dependency/>
                     <run-dependency>
-                        <specification-version>8.25</specification-version>
+                        <specification-version>9.3</specification-version>
                     </run-dependency>
                 </dependency>
             </module-dependencies>
diff --git 
a/java/junit.ant/src/org/netbeans/modules/junit/ant/JUnitOutputReader.java 
b/java/junit.ant/src/org/netbeans/modules/junit/ant/JUnitOutputReader.java
index 21d1b21..0e866e0 100644
--- a/java/junit.ant/src/org/netbeans/modules/junit/ant/JUnitOutputReader.java
+++ b/java/junit.ant/src/org/netbeans/modules/junit/ant/JUnitOutputReader.java
@@ -692,7 +692,8 @@ final class JUnitOutputReader {
         int addFail = failures;
         int addError = errors;
         int addPass = total - failures - errors;
-        for(Testcase tc: testSession.getCurrentSuite().getTestcases()){
+        TestSuite suite = testSession.getCurrentSuite();
+        for(Testcase tc: suite.getTestcases()){
             switch(tc.getStatus()){
                 case ERROR: addError--;break;
                 case FAILED: addFail--;break;
@@ -717,6 +718,7 @@ final class JUnitOutputReader {
 
         lastSuiteTime = time;
         state = State.SUITE_FINISHED;
+        testSession.finishSuite(suite);
     }
 
     private void testCaseStarted(String name){
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 85578a5..36534eb 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
@@ -24,6 +24,7 @@ import com.sun.source.tree.Tree;
 import com.sun.source.tree.Tree.Kind;
 import com.sun.source.util.SourcePositions;
 import com.sun.source.util.TreePath;
+import com.sun.source.util.Trees;
 import java.io.IOException;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
@@ -50,6 +51,7 @@ import org.netbeans.api.java.source.CompilationController;
 import org.netbeans.api.java.source.CompilationInfo;
 import org.netbeans.api.java.source.JavaSource.Phase;
 import org.netbeans.api.java.source.Task;
+import org.netbeans.api.java.source.TreeUtilities;
 import 
org.netbeans.modules.gsf.testrunner.ui.api.TestMethodController.TestMethod;
 import org.netbeans.modules.java.testrunner.ui.spi.ComputeTestMethods;
 import org.netbeans.modules.java.testrunner.ui.spi.ComputeTestMethods.Factory;
@@ -64,6 +66,7 @@ public final class TestClassInfoTask implements 
Task<CompilationController> {
     private SingleMethod singleMethod;
 
     private static final String JUNIT4_ANNOTATION = "org.junit.Test"; //NOI18N
+    private static final String JUNIT5_NESTED_ANNOTATION = 
"org.junit.jupiter.api.Nested"; //NOI18N
     private static final String JUNIT5_ANNOTATION = 
"org.junit.platform.commons.annotation.Testable"; //NOI18N
     private static final String TESTCASE = "junit.framework.TestCase"; //NOI18N
 
@@ -83,39 +86,46 @@ public final class TestClassInfoTask implements 
Task<CompilationController> {
         if (!isTestSource(fileObject)) {
             return Collections.emptyList();
         }
-        ClassTree clazz;
-        List<TreePath> methods;
+        List<TestMethod> result = new ArrayList<>();
         if (caretPosIfAny == (-1)) {
             Optional<? extends Tree> anyClass = 
info.getCompilationUnit().getTypeDecls().stream().filter(t -> t.getKind() == 
Kind.CLASS).findAny();
             if (!anyClass.isPresent()) {
                 return Collections.emptyList();
             }
-            clazz = (ClassTree) anyClass.get();
+            ClassTree clazz = (ClassTree) anyClass.get();
             TreePath pathToClass = new TreePath(new 
TreePath(info.getCompilationUnit()), clazz);
-            methods = clazz.getMembers().stream().filter(m -> m.getKind() == 
Kind.METHOD).map(m -> new TreePath(pathToClass, 
m)).collect(Collectors.toList());
-        } else {
-            TreePath tp = info.getTreeUtilities().pathFor(caretPosIfAny);
-            while (tp != null && tp.getLeaf().getKind() != Kind.METHOD) {
-                tp = tp.getParentPath();
-            }
-            if (tp != null) {
-                clazz = (ClassTree) tp.getParentPath().getLeaf();
-                methods = Collections.singletonList(tp);
-            } else {
-                return Collections.emptyList();
-            }
+            List<TreePath> 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;
         }
-        int clazzPreferred = info.getTreeUtilities().findNameSpan(clazz)[0];
-        TypeElement typeElement = (TypeElement) info.getTrees().getElement(new 
TreePath(new TreePath(info.getCompilationUnit()), clazz));
+        TreePath tp = info.getTreeUtilities().pathFor(caretPosIfAny);
+        while (tp != null && tp.getLeaf().getKind() != Kind.METHOD) {
+            tp = tp.getParentPath();
+        }
+        if (tp != null) {
+            collect(info, tp.getParentPath(), Collections.singletonList(tp), 
false, cancel, result);
+            return result;
+        }
+        return Collections.emptyList();
+    }
+
+    SingleMethod getSingleMethod() {
+        return singleMethod;
+    }
+
+    private static void collect(CompilationInfo info, TreePath clazz, 
List<TreePath> methods, boolean searchNested, AtomicBoolean cancel, 
List<TestMethod> result) {
+        Trees trees = info.getTrees();
         Elements elements = info.getElements();
+        TreeUtilities treeUtilities = info.getTreeUtilities();
+        int clazzPreferred = treeUtilities.findNameSpan((ClassTree) 
clazz.getLeaf())[0];
+        TypeElement typeElement = (TypeElement) trees.getElement(clazz);
         TypeElement testcase = elements.getTypeElement(TESTCASE);
         boolean junit3 = (testcase != null && typeElement != null) ? 
info.getTypes().isSubtype(typeElement.asType(), testcase.asType()) : false;
-        List<TestMethod> result = new ArrayList<>();
         for (TreePath tp : methods) {
             if (cancel.get()) {
-                return null;
+                return;
             }
-            Element element = info.getTrees().getElement(tp);
+            Element element = trees.getElement(tp);
             if (element != null) {
                 String mn = element.getSimpleName().toString();
                 boolean testMethod = false;
@@ -128,15 +138,15 @@ public final class TestClassInfoTask implements 
Task<CompilationController> {
                     }
                 }
                 if (testMethod) {
-                    SourcePositions sp = info.getTrees().getSourcePositions();
+                    SourcePositions sp = trees.getSourcePositions();
                     int start = (int) 
sp.getStartPosition(tp.getCompilationUnit(), tp.getLeaf());
-                    int preferred = 
info.getTreeUtilities().findNameSpan((MethodTree) tp.getLeaf())[0];
+                    int preferred = treeUtilities.findNameSpan((MethodTree) 
tp.getLeaf())[0];
                     int end = (int) sp.getEndPosition(tp.getCompilationUnit(), 
tp.getLeaf());
                     Document doc = 
info.getSnapshot().getSource().getDocument(false);
                     try {
-                        result.add(new 
TestMethod(typeElement.getQualifiedName().toString(),
+                        result.add(new 
TestMethod(elements.getBinaryName(typeElement).toString(),
                                 doc != null ? 
doc.createPosition(clazzPreferred) : new SimplePosition(clazzPreferred),
-                                new SingleMethod(fileObject, mn),
+                                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)));
@@ -146,11 +156,20 @@ public final class TestClassInfoTask implements 
Task<CompilationController> {
                 }
             }
         }
-        return result;
-    }
-
-    SingleMethod getSingleMethod() {
-        return singleMethod;
+        if (searchNested && !cancel.get()) {
+            ((ClassTree)clazz.getLeaf()).getMembers().stream().filter(m -> 
m.getKind() == Kind.CLASS).map(n -> new TreePath(clazz, n)).forEach(nested -> {
+                if (!cancel.get()) {
+                    Element nestedElement = trees.getElement(nested);
+                    if (nestedElement != null && !junit3) {
+                        List<? extends AnnotationMirror> allAnnotationMirrors 
= elements.getAllAnnotationMirrors(nestedElement);
+                        if (isJunit5Nested(allAnnotationMirrors)) {
+                            List<TreePath> nestedMethods = ((ClassTree) 
nested.getLeaf()).getMembers().stream().filter(m -> m.getKind() == 
Kind.METHOD).map(m -> new TreePath(nested, m)).collect(Collectors.toList());
+                            collect(info, nested, nestedMethods, true, cancel, 
result);
+                        }
+                    }
+                }
+            });
+        }
     }
 
     private static boolean isTestSource(FileObject fo) {
@@ -175,6 +194,17 @@ public final class TestClassInfoTask implements 
Task<CompilationController> {
         return false;
     }
     
+    private static boolean isJunit5Nested(List<? extends AnnotationMirror> 
allAnnotationMirrors) {
+        for (Iterator<? extends AnnotationMirror> it = 
allAnnotationMirrors.iterator(); it.hasNext();) {
+            AnnotationMirror annotationMirror = it.next();
+            TypeElement typeElement = (TypeElement) 
annotationMirror.getAnnotationType().asElement();
+            if 
(typeElement.getQualifiedName().contentEquals(JUNIT5_NESTED_ANNOTATION)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private static boolean isJunit5Testable(List<? extends AnnotationMirror> 
allAnnotationMirrors) {
         Queue<AnnotationMirror> pendingMirrorsToCheck = new 
ArrayDeque<>(allAnnotationMirrors);
         Set<AnnotationMirror> alreadyAddedMirrorsToCheck = new 
HashSet<>(allAnnotationMirrors);
diff --git a/java/maven.junit/nbproject/project.xml 
b/java/maven.junit/nbproject/project.xml
index e9fb585..9a35f1a 100644
--- a/java/maven.junit/nbproject/project.xml
+++ b/java/maven.junit/nbproject/project.xml
@@ -40,7 +40,7 @@
                     <compile-dependency/>
                     <run-dependency>
                         <release-version>2</release-version>
-                        <specification-version>2.0</specification-version>
+                        <specification-version>2.26</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>
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 e923bd6..4cda2cc 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
@@ -734,6 +734,7 @@ public class JUnitOutputListenerProvider implements 
OutputProcessor {
             } else { // update report status as a minimum
                 session.getReport(timeinmilis).setCompleted(true);
             }
+            session.finishSuite(suite);
             if (output.isFile()) {
                 if (junitManager != null) {
                     junitManager.displayOutput(session, 
FileUtils.fileRead(output), false);

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists

Reply via email to